Lambert conic conformal projection in d3 - javascript

I'm trying to project a set of points ([long, lat] tuples) on top of an SVG map of my home country Austria:
https://commons.wikimedia.org/wiki/File:Austria-geographic_map-blank.svg
The description of the SVG file on Wikimedia gives a projection name and the bounds of the map:
Lambert Conformal Conic, WGS84 datum
Geographic limits of the map:
West: 17.2° W
East: 9.3° W
North: 49.2° N
South: 46.0° N
Naive as I am, I thought this information would be enough to create the right kind of projection with D3.
This is what I tried first:
let gcc = d3.geoConicConformal()
.fitSize([width, height], bbox)
Where bbox is a GeoJSON polygon representing the boundaries of the map as given above.
Unfortunately the result is not the correct projection:
Now, from reading the D3 docs I can guess that I have to specify more parameters for the projection, e.g. two standard parallels. Unfortunately I have no idea what they are, and trying various values around the western and eastern boundaries of the map didn't work. I assume they can be derived from what I know about the map though, or maybe not?
Secondly, what confuses me is that the projection is not just wrongly rotated but incorrectly scaled as well -- I thought using .fitSize would take care of that.
Can anybody give me any pointers on correctly setting up a Lambert conic conformal projection?

Fitsize will translate and scale the map properly, however, with a conic projection you need to rotate, and as you noted, set the parallels.
Parallels:
There is a documented Austria Lambert Conic Conformal map projection, its specifications can be found here or here. The parallels that likely are correct are in this case are [46,49], though the map you are using could be a custom projection.
Rotation
Now you need to rotate the map, along the x axis by longitude. Conic maps are generally rotated along the x axis and centered on the y axis (see my answer here here for a graphical explanation of why (and how parallels change your projection)).
Rotation moves the world so that your area of interest is aligned properly, such that the central meridian of the map is vertical in your map. Based on the projection specifications noted above, the central meridian should be at 13 degrees, 20 minutes (13.333 degrees), though there is a small disagreement between the two references. Rotation along the x axis is set at the negative of this value.
Using these parameters:
d3.geoConicConformal()
.parallels([46,49])
.rotate([-13.333,0])
.fitSize([width,height],bbox)
I managed a pretty good fit with my very downsampled goto world topojson:
It is possible that the svg image uses parameters that differ slightly from the posted parameters (rounding, typos) or that the parallels are custom selected; however, this should be a fairly tight fit.

Related

Maximum latitude map panning with leaflet

I am new to leaflet and I want the restrict the panning of a world map horizontally and not vertically (longitude but not latitude) because this map will display pictures when I click on them and I cant see well the image when i restrict the panning horizontally AND vertically. The map by itself it not a picture, it's a real world map. But when I click on certain location, a small picture will appear on the map.
I try to play with maxBounds and setMaxbounds. The normal maxBounds (to view the world map) is :
maxBounds: [[-85, -180.0],[85, 180.0]],
When i try to put the latitude to
[[-150, -180.0],[150, 180.0]]
, the vertical panning is still restricted. Can somebody help please? Thank you.
This sounds similar to a (quite obscure) issue in the Leaflet issue tracker a
while back: see https://github.com/Leaflet/Leaflet/issues/3081
However, that issue was dealing with infinite horizontal bounds, not vertical bounds in a CRS that already has some preset limits.
If you set the map's maxBounds to a value larger than 85 (the value for MAX_LATITUDE of L.Projection.Spherical) and run a debugger, the call stack goes through the map's _panInsideMapBounds(), then panInsideBounds(), then _limitCenter(), then _getBoundsOffset, then project(), then through the map CRS's latLngToPoint, then untimately L.Projection.Spherical's project(). L.Projection.Spherical.project() projects the bounds' limits into pixel coordinates, and clamps the projected point to be inside the projection's limits.
There are a lot of reasons behind this, one of them being to prevent users from putting markers outside the area covered with tiles:
(This is particularly important when a user confuses lat-lng with lng-lat and tries to use a value outside the [-90,90] range for latitude, and the projection code starts returning Infinity values everywhere)
How to get around this? Well, we can always specify the map's CRS, and we can create a CRS with a hacked projection which enforces a different limit. Please be aware that this changes how the pixelOrigin works internally (as explained in the Leaflet tutorial about extending layers), so stuff (particularly plugins) might break.
So something like:
var hackedSphericalMercator = L.Util.extend(L.Projection.SphericalMercator, {
MAX_LATITUDE: 89.999
});
var hackedEPSG3857 = L.Util.extend(L.CRS.EPSG3857, {
projection: hackedSphericalMercator
});
var map = new L.Map('mapcontainer', {
crs: hackedEPSG3857,
});
Of course, then you can set up your own maxBounds:
var map = new L.Map('mapcontainer', {
crs: hackedEPSG3857,
maxBounds: [[-Infinity, -10], [Infinity, 10]]
});
In this case, the bounds' limits would still be clamped to hackedSphericalMercator.MAX_LATITUDE, but you should have enough wiggle room for your application.
As a side note: A radically different approach to this problem would be to use a different map projection. We're used to a spherical cylindrical projection, but that's not the only way to flatten the earth.
In particular, a Transverse Mercator projection (or pretty much any other transverse cylindrical projection, for that matter) works pretty much in the same way, but wraps vertically instead of horizontally, and it's the projected longitudes, not latitudes, the ones which approach infinity asymptotically when approaching the [-180, 180] range. Let me borrow an image from its wikipedia article:
This implies a different set of challenges (namely finding some raster tiles appropriate for your application, including which prime meridian to use, and making proj4leaflet play nice), but it's definitely doable.

Can d3 voronoi map work well with any other projection besides geoAlbers?

I'm learning voronoi map from the example link
https://bl.ocks.org/mbostock/7608400
But it doesn't work when I wanna change the projection to mercator.Why?
This is the only code I change:
var projection = d3.geoMercator()
.translate([width / 2, height / 2])
.scale(1280);
Yes, a voronoi should work with any projection - it uses the projected points in svg coordinate space. If you see points, you can make a voronoi.
The issue is you need to modify the parameters of the projection. An Albers projection requires two secant lines or parallels where the ellipsoid of the earth intersects the cone of the projection (or alternatively, one tangent line or parallel). Rarely are these set to the equator, so D3 has default settings of an Albers projection to be suited to and centered on the US as it doesn't make much sense to leave all the defaults to zero.
Most other projections in D3 have their default settings resulting in a map centered at [0,0], which is the prime meridian and the equator, just off the coast of Africa. Consequently, that's where you are looking when you set the projection as you have - no visible points, no visible voronoi (other than perhaps the periphery.
If you were to set your scale to be much smaller, say 170, you would see the your map, but all the features would be in the top left corner:
Instead of just zooming out, we can use the default centering coordinates of the Albers to center the Mercator:
projection.center([-96,39])
If you are too zoomed in, lower the scale value, and increase it if you are zoomed out too far. Because Mercator's distortion of size gets pretty bad near the poles, you will likely need to scale out.
Here's what I get with the example's code and this projection, zooming out a bit, but depending on if you want Alaska or not, you might want to zoom in or out:
var projection = d3.geoMercator()
.translate([width / 2, height / 2])
.scale(600)
.center([-96,39])

D3 Topojson Circle with Radius Scaled in Miles

(assuming existing projection/topojson)
What I'm trying to do is create a circle at a point ([long,lat]) of radius (r) in miles. I know there is a d3.geo function for this, but after some consideration I don't think it will be very compatible with my particular application.
So now I'm looking for using a native svg circle solution, where cx and cy are the lat and long, and r is the radius in miles. I know the cx and cy, but I don't know how to make sure the r is say 15 miles. So the main thing is how to make sure the radius is scaled in miles when drawn in pixel space. There must be someway to use the projection function to set the appropriate scale for the radius. But I haven't seen this in practice.
Also I should point out that my projection is dynamic, depending on user events the projection (including scale) can change. So I'm not sure if that will have bearing on how circles are scaled within the context of an existing projection, but I thought I would disclose that to be on the safe side.
Why not use the built-in circle generator d3.geoCircle()?
Returns a new GeoJSON geometry object of type “Polygon” approximating a circle on the surface of a sphere, with the current center, radius and precision. Any arguments are passed to the accessors.
The only task left to you is to calculate the radius of the circle in degrees. Because earth is not a perfect sphere this can become quite challenge of its own. But for many applications an approximation will suffice. Taking just the mean radius of 3,958 mi into account, the calculations can be written as:
var EARTH_RADIUS = 3959; // mean radius in miles
var radiusMi = 5; // radius to be drawn in miles
var radiusDeg = radiusMi / EARTH_RADIUS * 90; // radius in degrees for circle generator
This can then be passed to the circle generator:
var circle = d3.geoCircle().radius(radiusDeg);
Finally, the circle generator is used to pass its output via data binding to an appropriate path generator taking into account the projection:
svg.append("path")
.datum(circle)
.attr("d", path);
Have a look at this Block which features circles of 50 miles radius each at various positions around the globe. The circle generator in combination with the projection will take control of the correct sizing and the correct appearance of the circle.
D3 v3
If you are still stuck to D3 v3 the example works as well. However, you need to adjust the names accordingly:
d3.geo.circle ↦ d3.geoCircle
In addition to that, some of the circle generator's methods have been renamed:
circle.origin() ↦ circle.center()
circle.angle() ↦ circle.radius()
Applying those adjustments to my above linked Block, this works for v3 just as well: v3 Block.
This approach gets to play to its strengths when it comes to unusual projections having severe distortions. Just by changing the projection in the Block to d3.geoGnomonic() this becomes easily visible. The following screenshot from the updated Block still shows the same circles as above having a radius of 50 miles each:

Leaflet doesn't load negative coordinate tiles

I'm learning Leaflet and I can't seem to figure out what I did (or didn't do) to make negative coordinate tiles not load.
My code currently looks like this:
var map = L.map('map', {
crs: L.CRS.Simple
}).setView([0,0]);
L.tileLayer('img/tile_{x}_{y}.png', {
tileSize: 100,
noWrap: true,
format: 'image/png',
}).addTo(map);
Any help is appreciated.
It's not the most intuitive description, but it's probably the tileLayer continuousWorld option you're looking for.
If set to true, the tile coordinates won't be wrapped by world width (-180 to 180 longitude) or clamped to lie within world height (-90 to 90). Use this if you use Leaflet for maps that don't reflect the real world (e.g. game, indoor or photo maps).
At least for me this worked with tiles that using the scheme "{z}_{y}_{x}.png" e.g. 1_0_-1.png, 1_-2_0.png 1_1_-1.png for a ¬ shaped map that's left of the 0,0 coordinates.
Beware that by default negative y is up and positive is down. To reverse this behavior use the tms option.

OpenLayers LonLat transformations are not within correct projection bounds

My map uses EPSG:900013 projection. As a result I get values in meters in the range of -20037508.342789244 to 20037508.342789244 when getting my mouse position.
I used the .transform() method of the LonLat class, using EPSG:900913 as the source projection, and (without thinking) used EPSG:4329 as the destination projection.
My question is, why is the EPSG:4329 giving me ranges from -180, 180, -80.05, 85.05 (which i wanted) instead of -180, 180, -90, 90 (which it should have given me, since those are the correct bounds http://spatialreference.org/ref/epsg/wgs-84/)?
I'm relatively sure your source projection (900913) is setting those constraints, so that when you move your mouse, you're limited to travelling so many meters away from 0,0, which corresponds to the 85.05 and -80.05 in your transformations.
Said differently, EPSG 900913 doesn't cover the complete globe. So when you move your mouse to the furthest north/south, respectively, it would transform not to +/- 90, but to 85.05 and -80.05, as you've discovered.
If you go and check this page in the OpenLayers docs, they explain it as follows:
Specifically, most spherical mercator maps use an extent of the world
from -180 to 180 longitude, and from -85.0511 to 85.0511 latitude.
Because the mercator projection stretches to infinity as you approach
the poles, a cutoff in the north-south direction is required, and this
particular cutoff results in a perfect square of projected meters.

Categories