Expecting: correct tile URLs
Observed behaviour: invalid tile URLs
Reason: please consider following piece of code from Leaflet with comment:
getTileUrl: function (coords) {
...
if (this._map && !this._map.options.crs.infinite) {
// Following line of code is invalid - the _globalTileRange is updated only when
// _setView() is called, which does not happen when calling getTileUrl()
// for different tile layers
var invertedY = this._globalTileRange.max.y - coords.y;
if (this.options.tms) {
data['y'] = invertedY;
}
data['-y'] = invertedY;
}
return template(this._url, extend(data, this.options));
}
The bug happens only for TMS layers, as only TMS layers go into the if (this.options.tms) { } condition. The problem is that URLs of new tiles with zoom level, that is different from the current one (like in example, we are going from zoomMin=10 to zoomMax=12), are not calculated correctly because of the irrelevant _globalTileRange.
The _globalTileRange needs to be somehow updated if the coords.z parameter of the getTileUrl() is different from the internal value of zoom, I think.
https://jsbin.com/payowah/edit?html,console,output
I don't really understand the bug report. What is a "valid tile URL" for you, and what is an "invalid" one? How do they look like? I don't understand what you're trying to show in your example.
Also, you mention:
the
_globalTileRangeis updated only when_setView()is called, which does not happen when callinggetTileUrl()for different tile layers
This is imprecise. _globalTileRange is updated in L.GridLayer._resetGrid(), which is called from L.GridLayer._setView but only if this._tileZoom has changed.
There is no reason to update _globalTileRange more often than that. Also, keep in mind that _globalTileRange is a property for each L.GridLayer - the _globalTileRange of a L.GridLayer (or a L.TileLayer) doesn't affect the _globalTileRange of a different instance of L.GridLayer.
Am I missing something here?
@IvanSanchez Thank you very much for the comment! I've updated the issue, hope that helps!
@IvanSanchez Could you please verify if there is enough information?
Hi @sashuk,
Sorry you still have not answered to those requests for more information from your report:
Hello, @ghybs! Sorry, I will answer them below:
The example was changed a bit, so now (there is an updated JSBin link https://jsbin.com/buhatuquca/edit?html,console,output) we are zooming map from 15 to 16 zoom. Tiles in the example are requested using two approaches: by zooming the actual map and programmatically, using the getTileUrl() method.
1. Valid tile URLs
If we zoom in map, following URLs are fecthed for the 16 zoom level:
https://gc2.io/mapcache/baselayers/tms/1.0.0/geodk.bright/16/34574/45477.png
https://gc2.io/mapcache/baselayers/tms/1.0.0/geodk.bright/16/34574/45478.png
etc.
If we follow any of this links, then we will see that those are valid images and all works great.
2. Invalid tile URLs
However, if we try to generate these links programmatically, using the code in the example, the following URLs will be generated for the 16 zoom level:
https://gc2.io/mapcache/baselayers/tms/1.0.0/geodk.bright/15/34573/12709.png
https://gc2.io/mapcache/baselayers/tms/1.0.0/geodk.bright/15/34573/12708.png
etc.
If we follow any of this links, the TMS provider will return error.
3. What is example about
First, I am intereseted if it is okay that if I provide the z coordinate to getTileUrl() it is not taken into account on 11501 line of the https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/leaflet-src.js?
Second (it probably is caused by the first issue), if I use the hack and put on 11501 line following code (please note that now the z coordinate is taken into the account):
getTileUrl: function (coords) {
var data = {
r: retina ? '@2x' : '',
s: this._getSubdomain(coords),
x: coords.x,
y: coords.y,
z: (coords.z ? coords.z : this._getZoomForUrl())
};
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;
}
return template(this._url, extend(data, this.options));
},
generated URLs use the 16 zoom level, but the TMS provider still gives an error. This is probably because of Leaflet internally thinks that this is still the previous zoom level which is different from the one provided in the coords.z parameter of the getTileUrl().
Overall, the problem is simple - I want to generate URLs for layer for different zoom levels programmatically, so I can pre-fetch them in background and make them available offline. The getTileUrl() works great for WMS layers, but fails to generate correct URLs for TMS layers.
Hope that provided infromation helps.
Hi,
Unfortunately, the getTileUrl method, while its name might be a bit misleading, is not meant to be used externally to generate URL from arbitrary coords input. As stated in the docs (emphasis mine):
Called only internally, returns the URL for a tile given its coordinates. Classes extending TileLayer can override this function to provide custom tile URL naming schemes.
As you noticed, the z (zoom) property is discarded and only the current Tile Layer zoom (_tileZoom) is used instead.
I guess it is so because of the _globalTileRange that is also dependent on zoom.
While this scheme may be discussed, I do not have the history behind it to be able to determine whether this is for a reason or if it is unnecessarily complicated.
But in your case, you really do not need to use getTileUrl at all: given that you already know the coords of the tiles you want to fetch, you only need the y reversal to fit your TMS scheme, then apply those coordinates to your URL template:
function myGetTileUrl(coords) {
// Determine the max Y for the given zoom. You might want to cache this value.
var max_y_for_z = Math.pow(2, coords.z) - 1;
// Now reverse the y coordinate.
coords.y = max_y_for_z - coords.y;
return L.Util.template(urlTemplate, coords);
}
Updated JSBin: https://jsbin.com/faditemita/1/edit?html,console,output
@ghybs Thank you very much for the reply!
Indeed, using the L.Util.template() (assuming that coordinates are already known) really solved the issue! Final code:
/**
* Generates all URL for fetching the underlying tile set for specified boundary
*
* @param {*} map Leaflet map instance
* @param {*} bounds bounds object
* @param {*} tileLayer tile layer
* @param {*} minZoom minimum map zoom
* @param {*} maxZoom maximum map zoom
*/
getTileUrls (map, bounds, tileLayer, minZoom, maxZoom) {
if (!tileLayer) throw new Error('Tile layer is undefined');
let urls = [];
console.log(`Getting all tiles for specified boundary from ${minZoom} to ${maxZoom} zoom`);
for (let localZoom = minZoom; localZoom <= maxZoom; localZoom++) {
let min = map.project(bounds.getNorthWest(), localZoom).divideBy(256).floor();
let max = map.project(bounds.getSouthEast(), localZoom).divideBy(256).floor();
const max_y = (Math.pow(2, localZoom) - 1);
for (let i = min.x; i <= max.x; i++) {
for (let j = min.y; j <= max.y; j++) {
let coords = new L.Point(i, j);
coords.z = localZoom;
// Check if layer is WMS or TMS, tileLayer.options.tms is used as an example
if (tileLayer.options.tms) {
coords.y = max_y - coords.y;
}
let url = L.Util.template(tileLayer._url, coords);
urls.push(url);
}
}
}
console.log(`Total tile URLs: ${urls.length}`);
return urls;
};