Tuesday 19 July 2016

leaflet - Tile names & coordinates relationship


I'm aware that this site is intended for cartographers, geographers and GIS professionals. I must admit that I'm absolute beginner in the area of maps, but not in the area of programming (40+). I hope somebody will be kind enough to point me in the right direction.


I have map consisting of a set of 256x256 jpg orthophoto tiles. For each tile, I have WGS84 lat/long coordinates and info that 1 pixel is 0.5 m. First I would like to create local Leaflet map on my PC (concept from http://gis.stackexchange.com/questions/82936/how-i-can-load-tilelayer-in-leaflet-framework-using-local-tiles) and then also on Android (concept from https://android.stackexchange.com/questions/8312/how-can-i-can-open-local-files-in-the-default-android-browser).


How far did I manage to get? I found an online map which uses Leaflet: https://mr.si/topo/. One of the layers on this map (Topo) is using tiles the way I want to: L.tileLayer("https://s3-eu-west-1.amazonaws.com/topo-slovenia/z{z}/{y}/{x}.png", { minZoom: 10, maxNativeZoom: 15, detectRetina: !0, attribution: '© GURS', unloadInvisibleTiles: !1 })


Question for me is, of course, how do I name my folders and tiles to correspond to {y} and {x}?


I obtained one of the tiles from above map at address https://s3-eu-west-1.amazonaws.com/topo-slovenia/z15/11652/17705.png. From another map I got WGS84 (approximate) coordinates of upper left corner of this tile: 46° 2' 34,28" N, 14° 30' 47,06" E. With the help of proj4.js I converted this from EPSG:4326 to EPSG:3857 (or at least I hope I did) and got 5125467.235607444, 1633146.5805398274. I don't have the slightest idea how this relates to 11652, 17705.




Answer



After three weeks of learning about web maps I finally found answers to all of my questions. They will be more in the form of tutorial since this may help some other beginner.


First I learned how standard tiling for EPSG:3857 works. It's nicely explained on http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/. Key thing is zoom factor (z). At zoom level 0 whole world is one 256x256 tile. Coordinate origin is at the center of the tile (Greenwich at equator), vertical coordinates (y) going up to 20037508.342789244 and down to -20037508.342789244, horizontal coordinates (x) going left to -20037508.342789244 and right to 20037508.342789244. At zoom level 1 there are 4 equal tiles (2x2), at zoom level 2 there are 16 equal tiles (4x4) and so on. Tiles are numbered from upper left corner right and down. Upper left is (0,0), first right is (1,0), first down is (0,1) and so on. Generally at zoom level z there are 2z rows and columns of tiles, tile (2z/2, 2z/2 - 1) has upper left corner at coordinates (0,0). Another nice tutorial on map tiling systems: http://www.liedman.net/tiled-maps/.

Now I had to figure out tiling system of my tiles. One tile covers 128 x 128 m area. Since number of tile rows and columns at zoom level z is 2z, I had to find z for which tiles cover whole Slovenia, which covers roughly 260 x 163 km. With z=11 I got 2048 rows/columns covering area of 262.144 x 262.144 km.

Now the question was how to handle my tiles, which are based on different CRS, namely ellipsoidal transverse Mercator EPSG:3912 (old Slovenian D48, which is local Slovenian Gauß-Krüger projection). So my tiles have origin, zoom and projection that differ from EPSG:3857. I found Leaflet plugin  Proj4Leaflet which "allows you to use all kinds of weird projections in Leaflet".


To use the Proj4Leaflet plugin I needed the folowing data:



  • EPSG:3912 decription in proj4js format: "+proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=500000 +y_0=-5000000 +ellps=bessel +towgs84=682,-203,480,0,0,0,0 +units=m +no_defs"  (which I found here: http://epsg.io/3912)

  •  Map area origin. To calculate it I took a reference tile for which I had original row and column numbers and projected coordinates of upper left corner: refRow, refCol, refX and refY. Coordinates of origin (upper left corner), which has row and column number 0, are then origX = refX - refCol*128 and origY = refY - refRow*128, assuming that original tiling division was the same as the new one (which luckily proved to be so). Calculated origin was (368000, 243144).

  • Pixel/m resolutions for all zoom levels. Since at higest zoom level 11 on pixel was 0.5m, the rest were calculate by multiplying by 2 and the result was: [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5]


Creation of the map with my custom CRS and for standard tile naming then looked like this:



var crs3912 = new L.Proj.CRS('EPSG:3912',
'+proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=500000 +y_0=-5000000
+ellps=bessel +towgs84=682,-203,480,0,0,0,0 +units=m +no_defs',
{
origin: [368000, 243144],
resolutions: [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5],
});

var map = L.map('map',
crs: crs3912,

});

var myLayer = new L.TileLayer("tiles/{z}/{y}/{x}.jpg",
tileSize: 256,
maxZoom: 11,
minZoom: 11,
attribution: '© GURS'
});
map.addLayer(myLayer);


map.setView([46.047711, 14.507136], 11);

Tile naming of my tiles is completely different, so I just renamed a few of them to standard naming to test the above solution and it worked correctly.


The next question was would it be possible to use my tiles without renaming them. Their naming scheme is "tiles/ortofoto5000/{y}/SI{x}{y}.jpg", where x and y are tile coordinates in three digit hexadecimal format and y is in reverse order (from bottom up). I found the solution to this problem here https://stackoverflow.com/questions/43826338/leaflet-custom-url-custom-tiles and here http://leafletjs.com/examples/extending/extending-2-layers.html.


Creation of the map now looked like this:


L.TileLayer.MyCustomLayerClass = L.TileLayer.extend({
getTileUrl: function(coords) {
coords.x = ('00' + (coords.x).toString(16)).slice(-3);
coords.y = ('00' + (2047 - coords.y).toString(16)).slice(-3);
return L.TileLayer.prototype.getTileUrl.call(this, coords);

}
});

L.tileLayer.myCustomLayer = function(templateUrl, options) {
return new L.TileLayer.MyCustomLayerClass(templateUrl, options);
}

var crs3912 = new L.Proj.CRS('EPSG:3912',
"+proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=500000 +y_0=-5000000
+ellps=bessel +towgs84=682,-203,480,0,0,0,0 +units=m +no_defs",

{
origin: [368000, 243144],
resolutions: [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5],
});

var map = L.map('map', {
crs: crs3912,
});

var myLayer = L.tileLayer.myCustomLayer("tiles/ortofoto5000/{y}/SI{x}{y}.jpg", {

tileSize: 256,
maxZoom: 11,
minZoom: 11,
attribution: '© GURS'
});
map.addLayer(myLayer);

map.setView([46.180527, 14.507304], 11);

And now the final challenge: would this be possible also with original tiles available on the web and for all the zoom levels? Tiling scheme of the original tiles on the web is quite complex. I managed to decipher it with the help of Telerik Fiddler web debugging tool (http://www.telerik.com/fiddler). I just went through all the zoom levels and watched web requests.


So here is then the final solution:



L.TileLayer.MyCustomLayerClass = L.TileLayer.extend({
getTileUrl: function(coords) {
var tileS;
var tileX;
var tileY;
var tileZ;
var tileName;
var hexX;
var hexY;
var hexXhexY;


tileS = this._getSubdomain(coords);
tileX = coords.x;
tileY = (2048 / Math.pow(2, 11 - coords.z)) - coords.y - 1;
tileZ = 'S' + '789ABCDEFGHI'.charAt(coords.z);
switch (Math.ceil(coords.z / 4)) {
case 3:
var hexX = ('00' + (tileX.toString(16)).toUpperCase()).slice(-3);
var hexY = ('00' + (tileY.toString(16)).toUpperCase()).slice(-3);
var hexXhexY = hexX + hexY;

var tileName = hexXhexY.substr(0, 2) + '/' + hexXhexY.substr(2, 2) + '/' + tileZ
+ hexXhexY + '.jpg';
break;
case 2:
var hexX = ('0' + (tileX.toString(16)).toUpperCase()).slice(-2);
var hexY = ('0' + (tileY.toString(16)).toUpperCase()).slice(-2);
var hexXhexY = hexX + hexY;
var tileName = hexXhexY.substr(0, 2) + '/' + tileZ + hexXhexY + '.jpg';
break;
default:

var hexX = (tileX.toString(16)).toUpperCase();
var hexY = (tileY.toString(16)).toUpperCase();
var tileName = tileZ + hexX + hexY + '.jpg';
}
return 'http://gpcl' + tileS +
'.geopedia.si/v1/AUTH_d7e1266c-6b4e-4629-91e6-17d4b370846d/gurs.dof.50cm.2011.epsg:3912/'
+ tileZ + '/' + tileName;
}
});


L.tileLayer.myCustomLayer = function(templateUrl, options)
return new L.TileLayer.MyCustomLayerClass(templateUrl, options);
}

var crs3912 = new L.Proj.CRS('EPSG:3912',
'+proj=tmerc +lat_0=0 +lon_0=15 +k=0.9999 +x_0=500000 +y_0=-5000000
+ellps=bessel +towgs84=682,-203,480,0,0,0,0 +units=m +no_defs',
{
origin: [368000, 243144],
resolutions: [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5],

});

var map = L.map('map', {
crs: crs3912,
});

var myLayer = L.tileLayer.myCustomLayer("", {
subdomains: ['01', '02', '03', '04', '05', '06', '07', '08', '09'],
tileSize: 256,
maxZoom: 11,

minZoom: 0,
attribution: '© GURS'
});

map.addLayer(myLayer);

map.setView([46.047711, 14.507136], 11);

Working example is available at jsfiddle https://jsfiddle.net/TomazicM/vfefa69x/. Example includes also display of tile grid and zoom level and projected coordinates, which are useful for debugging purposes.


Tile grid is displyed with the help of the following CSS modification:


.leaflet-tile {

border: solid red 1px;
}

Zoom level and projected coordinates are displayed with the help of Leaflet.Coordinates plugin.


I also had to learn how to include those github resources into jsfiddle example that do not give the right mime type when referenced. I found the solution here https://blog.radix.cc/using-rawgit-to-serve-files-a2e4acad7f2d.


Finnaly I tried this solution on Android 7.0. I simply copied the examples with the resources to Android folder /sdcard/maps/LeafletExamples and called examples in browser by file:///sdcard/maps/LeafletExamples/ExampleN.html. It worked in Chrome and Firefox.


No comments:

Post a Comment

arcpy - Changing output name when exporting data driven pages to JPG?

Is there a way to save the output JPG, changing the output file name to the page name, instead of page number? I mean changing the script fo...