Leaflet doesn't load negative coordinate tiles - javascript

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.

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.

How to use Leaflet flyTo() with unproject() and GeoJSON data on a large raster image?

I'm building a story map with Leaflet using a large image sliced into tiles rather than 'real world' map data. I'm using this plugin: https://commenthol.github.io/leaflet-rastercoords/ and this repo: https://github.com/jackdougherty/leaflet-storymap
Loading my GeoJSON data and unprojecting the coordinates correctly plots them on my image map:
$.getJSON('map.geojson', function(data) {
var geojson = L.geoJson(data, {
// correctly map the geojson coordinates on the image
coordsToLatLng: function (coords) {
return rc.unproject(coords)
},
But when I get to onEachFeature, I hit the wall with map.flyTo(), which is calling geometry.coordinates from my JSON file, but not unprojecting them so flyTo() is interpreting them as geospatial coordinates way off the map:
map.flyTo([feature.geometry.coordinates[1], feature.geometry.coordinates[0] ], feature.properties['zoom']);
I tried passing the unprojected coordinates to variables and then to map.flyTo() and variations on nesting functions, such as map.flyTo.unproject(..., but no luck.
How do I pass my raster coordinates to flyTo()?
I'm not only new to Leaflet, but new to JavaScript. I hacked my way this far, but I'm stumped. I'm sure the solution is obvious. Any help is greatly appreciated.
In your case you would probably just need to use rc.unproject to convert your coordinates into LatLng that you can pass to flyTo:
map.flyTo(
rc.unproject(feature.geometry.coordinates),
feature.properties['zoom']
);
That being said, I must admit I do not exactly see the point of using leaflet-rastercoords plugin, since you can easily do the same by following the Leaflet tutorial "Non-geographical maps".
var yx = L.latLng;
var xy = function(x, y) {
if (L.Util.isArray(x)) { // When doing xy([x, y]);
return yx(x[1], x[0]);
}
return yx(y, x); // When doing xy(x, y);
};
With this, whenever you want to convert your "raster" coordinates into something usable by Leaflet, you would just use xy(x, y) with x being your horizontal value, and y your vertical one.
The added benefit is that many other things will become easily compatible.
Since you use tiles instead of a single image (that is stretched with ImageOverlay in the tutorial in order to fit the coordinates), you should modify the CRS transformation, so that at zoom 0, your tile 0/0/0 fits your entire coordinates. See also Leaflet custom coordinates on image
I.e. in the case of leaflet-rastercoords example:
Original raster image size: 3831 px width x 3101 px height
Tiles size: 256 x 256 px
Vertical "raster" coordinates are increasing while going down (whereas in the Leaflet tutorial, they increase going up, like latitude).
Tile 0/0/0 actually covers more than the original image, as if the latter were 4096 x 4096 px (the rest is filled with white)
Determination of the new CRS:
Tile 0/0/0 should cover coordinates from top-left [0, 0] to bottom-right [4096, 4096] (i.e. 256 * 2^4 = 256 * 16 = 4096) => transformation coefficients a and c should be 1/16
No offset needed => offsets b and d are 0
No reversion of y vertical coordinate => c is positive
Therefore the new CRS to be used would be:
L.CRS.MySimple = L.extend({}, L.CRS.Simple, {
// coefficients: a b c d
transformation: new L.Transformation(1 / 16, 0, 1 / 16, 0)
});
Now your flyTo is very similar, but many other things are also compatible:
map.flyTo(
xy(feature.geometry.coordinates),
feature.properties['zoom']
);
Demo adapted from leaflet-rastercoords example, and using an extra plugin to demonstrate compatibility: https://plnkr.co/edit/Gvei5I0S9yEo6fCYXPuy?p=preview

Lambert conic conformal projection in d3

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.

Match center of a Tile Layer with center of the Map on Leaflet

I am trying to make a Map of a Distribution Center using a flat image of the place. I need to be able to zoom in and out keeping a good quality of the image. So far I've been using Leaflet to draw the Map and MapTiler to create a tile schema that makes possible zooming without any quality loss and good performance.
I have everything set and working, but I need my Tile Layer center to match with the Map center. No matter what I do the Tile Layer top-left corner always starts at the (0,0) coordinate of the Map.
At zoom level 3, for example, my image is 1536x1280px, so the top-left corner should be at coordinate (-768,640) of the Map when talking about absolute pixel coordinates.
Here is my main script:
var map = L.map('map',{
crs: L.CRS.Simple,
center:[0,0]
}).setView([0,0],3);
//mapHeight and mapWidth are the pixel dimensions of the flat image at max zoom level.
//In this case it's 6144x4608px. By default the tiles size is 256x256px.
var southWest = map.unproject([ 0, mapHeight], getMaxZoom());
var northEast = map.unproject([ mapWidth, 0 ], getMaxZoom());
var mapBounds = new L.LatLngBounds(southWest, northEast);
L.tileLayer('tile/{z}/{x}/{y}.png',{
minZoom: 2,
maxZoom: 5,
noWrap:true,
bounds:mapBounds
}).addTo(map);
I have messed with the center, setView and bounds, but no success in making the Tile Layer move.
The documentation of Leaflet can be found here http://leafletjs.com/reference.html
Please help me if you can. Thank you.
In Leaflet, tile coordinates are tightly bound to an internal mechanism - pixel coordinates.
For every zoom level, coordinates in LatLng get projected to the CRS coordinates (that's EPSG:4326→EPSG:3857 for earth maps and a vertical flip for L.CRS.Simple maps), then the CRS coordinate gets scaled by a factor dependant on the zoom level to give a pixel coordinate. Layers (tiles, markers, etc) are drawn using these pixel coordinates.
For "normal" tiles (of both GridLayer and TileLayer), the {x} and {y} fields on the tile template URL are simply the pixel coordinate divided by the tile size. A 256px tile at pixel [0, 0] will have the tile coords [0, 0], a tile at pixel [512, 256] will have [2, 1] and so on.
If you read the code for L.TileLayer.WMS, however, you'll notice that the tile URLs don't necessarily depend on the tile coordinates.
Back to your problem. You can overcome it by using the same strategy of L.TileLayer.WMS: overriding the getTileUrl method, with something like:
var t = L.tileLayer(…);
t.getTileUrl = function(coords) {
var myZ = coords.z;
var myX = coords.x + (8 * Math.pow(2, myZ - 3));
var myY = coords.y + (6 * Math.pow(2, myZ - 3));
return '/tile/' + myZ + '/' + myX + '/' + myY + '.png';
}
The code is simplistic (and I haven't bothered to make the math so that things fit where they should), but that should put you on the right track.
A different way to achieve the same would be to create a custom L.CRS based on L.CRS.Simple which applies a translation transformation to convert between LatLngs and CRS coordinates. If the code in src/geo/crs/CRS.Simple.js and src/geometry/Transformation.js in the Leaflet repo makes sense to you, I suggest you try this approach.

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