I am using Leaflet with Proj4Leaflet to work with tiles in 25832. The application is fairly simple: I am trying to overlay tiles in EPSG:25832 onto a omniscale basemap. I have copied the individual resolutions and origin from the tilemap meta information. The problem I am facing is that the map is not aligned and once I zoom in the tiles are not placed in the correct order. I'd appreciate any kind of support here (by the way, this is a working example which is using openlayers).
I guess I am doing something wrong here:
// Set resolutions
var resolutions = [156367.7919628329,78183.89598141646,39091.94799070823,19545.973995354114,9772.986997677057,4886.4934988385285,2443.2467494192642,1221.6233747096321,610.8116873548161,305.40584367740803,152.70292183870401,76.35146091935201,38.175730459676004,19.087865229838002,9.543932614919001,4.7719663074595005,2.3859831537297502,1.1929915768648751];
// Define CRS
var rs25832 = new L.Proj.CRS(
'EPSG:25832',
proj4rs25832def,
{
origin: [ 273211.2532533697, 6111822.37943825 ],
resolutions: resolutions
}
);
...using the tiles information from https://mapproxy.bba.atenekom.eu/tms/1.0.0/privat_alle_50_mbit/germany .
Afterwards I add a tile layer
var url = 'https://mapproxy.bba.atenekom.eu/tms/1.0.0/privat_alle_50_mbit/germany/{z}/{x}/{y}.png';
var tileLayer = L.tileLayer(
url,
{
tms: true,
crs: rs25832,
continuousWorld: true,
maxZoom: resolutions.length
}
);
And add them to the map..
// Setup map
var map = L.map('map', {
crs: rs25832,
center: [ 50.8805, 7.3389 ],
zoom:5,
maxZoom: resolutions.length,
layers: [ baseWms, tileLayer ]
});
The bare minimum of code can be found here: https://jsfiddle.net/6gcam7w5/8/
This boils down to how the Y coordinate of TMS tiles is inverted (it becomes higher when going north, as opposed to default TileLayers, in which the Y coordinate becomes larger when going south).
Having a look on the Leaflet code that takes care of this specific feature will shed some light on the issue:
if (this._map && !this._map.options.crs.infinite) {
var invertedY = this._globalTileRange.max.y - coords.y;
if (this.options.tms) {
data['y'] = invertedY;
}
data['-y'] = invertedY;
}
There are two things critical to calculating the right Y coordinate for your tiles here:
The CRS must be finite (it must have bounds)
There must be a finite global tile range (which in Leaflet is ultimately defined by the CRS bounds and not the TileLayer bounds)
Long story short, your CRS should be defined with known bounds. For this particular case, taking information from the TMS capabilities document...
<BoundingBox minx="273211.2532533697" miny="5200000.0" maxx="961083.6232988155" maxy="6111822.37943825"/>
...and turned into a L.Bounds definition when defining the Leaflet CRS, like...
// Define CRS
var rs25832 = new L.Proj.CRS(
'EPSG:25832',
proj4rs25832def,
{
origin: [ 273211.2532533697, 6111822.37943825 ],
resolutions: resolutions,
bounds: [[273211.2532533697, 5200000],[961083.6232988155, 6111822.37943825]]
}
);
Stuff should just work (with no need to pass the CRS to the tilelayers, since they will all use the map's), as in this working example.
Related
For a project I need to convert latitude and longitude coordinates to the map layer (map html canvas) point coordinates (in x and y). I have gone through almost the whole of Mapbox's documentation, but I can't seem to find it. Does anybody know how to do it?
(Javascript)
This:
let point;
coordinates = [20,50]
point = convert(coordinates); // => point = (x, y);
You can use mapboxgl.Map method called "project". It returns mapboxgl.Point by LngLatLike coordinates.
TypeScript:
const coordinates: LngLatLike = [37, 55];
const point = map.project(coordinates); // return Point object
// output: {
// x: 713.802690844605
// y: 390.2335262644118
// }
Here is an example code from the official website.
You have to register from the official website. And then you should get an access token for using Mapbox in your project. So it is quite simple.
You can watch this video to understand visually how to do it.
<script>
mapboxgl.accessToken = '<your access token here>';
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v11',
center: [12.550343, 55.665957],
zoom: 8
});
var marker = new mapboxgl.Marker()
.setLngLat([12.550343, 55.665957])
.addTo(map);
</script>
Here you see above, in the "center" attribute you can define your own lat(firstly) and lng(secondly)
I have a simple fictional map that I want to control using Leaflet. It is a flat 2D plane and its "latitude/longitude"/coordinate system spans from [0,0] to [999,999].
I have customized the map as follows:
window.map = L.map('leaflet-map', {
crs: L.CRS.Simple,
center: [500, 500],
zoom: 13,
maxBounds: [
[0, 0],
[999, 999],
],
layers: [new MyLayer()],
});
To draw this map, I've created a new layer, MyLayer, which extends gridLayer:
export var MyLayer = GridLayer.extend({
createTile: function(coords, done) {
var error;
var xmlhttprequest = new XMLHttpRequest();
xmlhttprequest.addEventListener('readystatechange', function() {
done(error, dothething());
});
xmlhttprequest.open('GET', /* url */);
xmlhttprequest.send();
},
});
The problem I have is the URL accepts the [0,0] to [999,999] coordinate system as parameters but I can't find how to actually get those. I understand there may be some decimal element but I can floor that as appropriate.
When centered on [500, 500, 13] the coords object contains { x: 15516, y: -21558, z: 13 }. When passed to L.CRS.Simple.pointToLatLng(coords, coords.z) I get { lat: 2.631591796875, lng: 1.89404296875 }.
I've downloaded the source code in an attempt to understand how this transformation happens from Map._move(center, zoom, data) but all that appears to do is call this.options.crs.latLngToPoint(), which is exactly what I reverse in L.CRS.Simple.pointToLatLng. I'm frankly at a loss.
First of all, I encourage you to read the Leaflet tutorial on L.CRS.Simple once again. Let me quote a relevant bit from there :
In a CRS.Simple, one horizontal map unit is mapped to one horizontal pixel, and idem with vertical. [...] we can set minZoom to values lower than zero:
So you have no reason to go down to zoom level 13 on your L.CRS.Simple map by default, really. For a [0,0]-[999,999] map, use zoom level zero for an overview, or use map.fitBounds([[0,0],[999,999]]).
The values that the createTile() method receives are tile coordinates, not CRS coordinates. A level-0 tile is split into four level-1 tiles, sixteen level-2 tiles, 64 level-3 tiles, and so on, up to 2^13 tiles at level 13. This is easier to visualize by playing with a L.GridLayer that displays the tile coordinates, like:
var grid = L.gridLayer({
attribution: 'Grid Layer',
// tileSize: L.point(100, 100),
});
grid.createTile = function (coords) {
var tile = L.DomUtil.create('div', 'tile-coords');
tile.innerHTML = [coords.x, coords.y, coords.z].join(', ');
return tile;
};
map.addLayer(grid);
Second: you want to use the internal _tileCoordsToBounds method, defined at L.GridLayer. Give it a set of tile coordinates, and you'll get back a L.LatLngBounds with the area covered by such a tile.
The following example (try it live here) should put you on track. Remember to read the documentation for L.LatLngBounds as well.
var grid = L.gridLayer({
attribution: 'Grid Layer',
// tileSize: L.point(100, 100),
});
grid.createTile = function (coords) {
var tile = L.DomUtil.create('div', 'tile-coords');
var tileBounds = this._tileCoordsToBounds(coords);
tile.innerHTML = [coords.x, coords.y, coords.z].join(', ') +
"<br>" + tileBounds.toBBoxString();
return tile;
};
map.addLayer(grid);
Having spent several hours looking through the docs, searching on here and looking at the "L" Object returned, I'm at a loss how to reference my leafletjs map for modifications, adding layers, markers, etc, throughout my code.
I get how the initial reference is destroyed to conserve memory, but how is it possible for me to reference my leaflet map and add/remove markers, layers, set styles, fit to bounds, etc after it's been created.
var layers=[];
var leafletMap = new L.map('mmp', { zoomControl: false, preferCanvas: true }).setView([51.505, -0.09], 13);
L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png', {
maxZoom: 18, attribution: '© OpenStreetMap, ©CARTO'
}).addTo(leafletMap);
var myGeojSonLayerGroup = L.geoJson(worldCountries.features, {
onEachFeature: myOnEachFeature,
style: myStyle
}).addTo(leafletMap)
function myOnEachFeature(feature, layer) {
var props = feature.properties;
var ni = {}
ni.name = props.name;
ni.layer = layer
leafletLayers.push(ni);
}
$('#mmp').data('layers',layers)
Using the above I can indeed colour various countries later on in my code, but I can't call leafletMap.fitBounds because leafletmap is no longer available, even though this was created as a global variable.
Is there any possible solution to this?
I am working on a leaflet project where I need the coordinates 0 to 4096 and which the leaflet coordinates that I need are the following:
0,0 bottom left (southWest)
0,4096' top left (northWest)
4096',4096' top right (northEast)
4096',0 bottom right (southEast)
I have tried many things to get the coordinates to map as needed but no luck. Here is my jsfiddle with my example, which has a console.log when you click on the map which shows the coordinates. If you click in the top left you will see its 0,0 and if you click in the bottom right you will see 4096,4096 which is backwards from whats needed. Here is my code that I have
url = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png';
weight = 4096;
height = 4096;
mapMinZoom = 1;
mapMaxZoom = 7;
map = L.map('map', {
maxZoom: mapMaxZoom,
minZoom: mapMinZoom,
noWrap: true,
detectRetina: true
});
unproject = function(coord) {
return map.unproject(coord, 4);
};
southWest = unproject([0, 0]);
northEast = unproject([height, weight]);
map.setMaxBounds(new L.LatLngBounds(southWest, northEast));
tileLayer = L.tileLayer(url, {
minZoom: mapMinZoom,
maxZoom: mapMaxZoom,
noWrap: true,
tileSize: 256,
detectRetina: false
}).addTo(map);
map.setView(unproject([0, 0]), 2);
map.on('click', function(e) {
var coords, latLong;
latLong = new L.LatLng(e.latlng.lat, e.latlng.lng);
coords = map.project(latLong, 4);
console.log("x="+coords.x+", y="+coords.y);
});
If I understand correctly, you would like a map coordinates like this:
[0, 4096] .. [4096, 4096]
.........................
.........................
[0, 0] ........ [4096, 0]
It is good as vertical coordinate increases while going up (like latitude on Leaflet) and horizontal coordinate increases while going right (like longitude on Leaflet). So you do not seem to need to invert the vertical axis.
As for the order of the coordinates, unfortunately Leaflet uses latitude first, longitude second, and it is almost impossible to switch them natively. However, you can easily build a wrapper method that just swaps the coordinates. (similar to Leaflet LatLngBounds with simpler CRS & projection, but no need to use negative vertical coordinate -y since in your case you want it in the same direction as latitude)
Then another question would be how you want the coordinates in-between those 4 corners. The default CRS (Web Mercator) is not linear along the vertical axis. If you need your coordinates to be linear, you should use L.CRS.Simple. See Leaflet tutorial for non-Earth-based maps.
I am using Leaflet.js for desktop and mobile browser-based maps, and need to support a variety of map tile services. Some of these tile services are defined with coarse zoom levels (like 1, 5, 10, 15), and if I make a request for an unsupported zoom level, the service does not return a tile. (For example, if I request service/tiles/6/x/y when zoom level 6 is not supported).
Leaflet tile layers support minZoom and maxZoom but I'm trying to figure out if there is a best practice for doing coarse zoom levels, and if other folks have encountered this.
I found this post on GitHub that addresses tile scaling for unsupported zoom levels: https://github.com/Leaflet/Leaflet/pull/1802
But I am not sure if this applies. (I'm not sure I want to scale or 'tween' the zoom levels... but if this makes sense and is not too difficult I am willing to try.)
I've started experimenting with this approach which gets messy because zooming can cause more zooming and I have to differentiate user-driven zooms from system-driven zooms:
// layer metadata (assumption: Levels is in ascending order of zoom)
var layerDef = { Title: 'Service lines', Levels: [10, 15, 19] };
// create Leaflet Tile Layer and show on map
var layer = L.tileLayer('://host/service/{z}/{x}/{y}');
layer.minZoom = layerDef.Levels[0];
layer.maxZoom = layerDef.Levels[layerDef.Levels-1];
layer.addTo(map);
// initialize lastZoom for coarse zoom management
var lastZoom = map.getZoom();
var userZoom = true;
// handle supported zoom levels when zoom changes
map.on('zoomend', function (e)
{
// get new zoom level
var z = e.target._zoom || map.getZoom();
if (userZoom) // assume user initiated this zoom
{
// is this zoom level supported?
var zIdx = $.inArray(z, layerDef.Levels);
if (zIdx === -1)
{
// zoom level is not supported; zoom out or in to supported level
// delta: < 0 for zoom out, > 0 for zoom in
var zDelta = z - lastZoom;
var zLastIdx = $.inArray(lastZoom, layerDef.Levels);
var newZoom = -1;
if (zDelta > 0)
{
// user zoomed in to unsupported level.
// zoom in to next supported level (rely on layer.maxZoom)
newZoom = layerDef.Levels[zLastIdx + 1];
}
else
{
// user zoomed out to unsupported level.
// zoom out to next supported level (rely on layer.minZoom)
newZoom = layerDef.Levels[zLastIdx - 1];
}
if (newZoom !== -1)
{
userZoom = false; // set flag
setTimeout(function ()
{
map.setZoom(newZoom); // set zoom -- seems to not work if called from within zoomend handler
}, 100); // delay call to setZoom() to fix issue
}
}
}
else
{
userZoom = true; // clear flag
}
lastZoom = z;
});
(Side note: I hope the reason for coarse zoom levels is obvious: it can get expensive to create and store raster tiles at each zoom level, especially for large geographic areas, and especially when used with offline mobile devices with their own [local] tile servers, limited wireless data plans, limited storage capacity, etc. This is perhaps not something you might encounter with toy apps and Google maps, for example, but rather with domain-specific and mobile applications in which space and bandwidth are at a premium.)
Thanks!
UPDATE: I found that the problem I was having with this code is that map.setZoom(z) does not work right when called from within the zoomEnd handler (it does set the zoom, but causes display issue with gray/nonexistent tiles, perhaps because Leaflet is still in process of scaling / zooming). Fix was to use setTimeout to delay the call to setZoom() a bit. However, I'm still really curious if anybody else has dealt with this, and if there is a 'better way'... (I updated above code to work with setZoom fix)
There is currently a commit under review in Leaflet's repository on GitHub. It adds zoomFactor to the map's options. Maybe that's what you're looking for. At least, i think it will work just as long as your available tileset has zoomlevels which are (don't know if this is the correct technical term) multiples of the lowest available zoomlevel.
See: https://github.com/Leaflet/Leaflet/pull/3285
The following (no guarantees, based on this) should work with Leaflet v1.7.3 but probably not with current master.
It uses a serverZooms option to specify available zoom levels on the tile server as an ordered array.
Overrides L.TileLayer._getZoomForUrl to return a matching or the next lower available server zoom. Also overrides L.TileLayer._getTileSize to increase tile size in order to scale the tiles in between server zooms.
L.TileLayer.Overzoom = L.TileLayer.extend({
options: {
// List of available server zoom levels in ascending order. Empty means all
// client zooms are available (default). Allows to only request tiles at certain
// zooms and resizes tiles on the other zooms.
serverZooms: []
},
// add serverZooms (when maxNativeZoom is not defined)
// #override
_getTileSize: function() {
var map = this._map,
options = this.options,
zoom = map.getZoom() + options.zoomOffset,
zoomN = options.maxNativeZoom || this._getServerZoom(zoom);
// increase tile size when overscaling
return zoomN && zoom !== zoomN ?
Math.round(map.getZoomScale(zoom) / map.getZoomScale(zoomN) * options.tileSize) :
options.tileSize;
},
// #override
_getZoomForUrl: function () {
var zoom = L.TileLayer.prototype._getZoomForUrl.call(this);
return this._getServerZoom(zoom);
},
// Returns the appropriate server zoom to request tiles for the current zoom level.
// Next lower or equal server zoom to current zoom, or minimum server zoom if no lower
// (should be restricted by setting minZoom to avoid loading too many tiles).
_getServerZoom: function(zoom) {
var serverZooms = this.options.serverZooms || [],
result = zoom;
// expects serverZooms to be sorted ascending
for (var i = 0, len = serverZooms.length; i < len; i++) {
if (serverZooms[i] <= zoom) {
result = serverZooms[i];
} else {
if (i === 0) {
// zoom < smallest serverZoom
result = serverZooms[0];
}
break;
}
}
return result;
}
});
(function () {
var map = new L.Map('map');
map.setView([50, 10], 5);
new L.TileLayer.Overzoom('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
serverZooms: [0, 1, 2, 3, 6, 9, 12, 15, 17],
attribution : '© <a target="_parent" href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
})();
body {
margin: 0;
}
html, body, #map {
width: 100%;
height: 100%;
}
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
<div id="map"></div>