Slippy map tilenames
From OpenStreetMap Wiki
This article describes the file naming conventions for the Slippy Map application.
Contents
|
Zoom levels
| 0 | 1 tile covers whole world | 1 tile |
| 1 | 2 × 2 tiles | 4 tiles |
| 2 | 4 × 4 tiles | 16 tiles |
| n | 2n × 2n tiles | 22n tiles |
| 12 | 4096 x 4096 tiles | 16.777.216 |
| 17 | Maximum zoom for Osmarender layer | 17.179.869.184 |
| 18 | Maximum zoom for Mapnik layer | 68.719.476.736 |
X and Y
- X goes from 0 (left edge is 180 °W) to 2zoom -1 (right edge is 180 °E)
- Y goes from 0 (top edge is 85.0511 °N) to 2zoom -1 (bottom edge is 85.0511 °S) in a Mercator projection
For the curious, the num 85.0511 is the result of arctan(sinh(π)). By using this bound, the entire map becomes a (very large) square. See also the Osmarender bug.
Derivation of tile names
- Reproject the coordinates to the Mercator projection:
- x = lon
- y = log(tan(lat) + sec(lat))
- (lat and lon are in radians)
- Transform range of x and y to 0 - 1 and shift origin to top left corner:
- x = (1 + (x / π)) / 2
- y = (1 - (y / π)) / 2
- Calculate the number of tiles across the map, n, using 2zoom
- Multiply x and y by n. Round results down to give tilex and tiley.
Implementations
Pseudo-Code
For those who like pseudo-code, here's some hints: (sec == 1/cos)
Please note that log stands for logarithmus naturalis (also known as ln(x) ), not decimalis as used on some calculators.
lon/lat to tile numbers
n = 2 ^ zoom xtile = ((lon_deg + 180) / 360) * n ytile = (1 - (log(tan(lat_rad) + sec(lat_rad)) / π)) / 2 * n
tile numbers to lon/lat
n = 2 ^ zoom lon_deg = xtile / n * 360.0 - 180.0 lat_rad = arctan(sinh(π * (1 - 2 * ytile / n))) lat_deg = lat_rad * 180.0 / π
Python
lon/lat to tile numbers
import math def deg2num(lat_deg, lon_deg, zoom): lat_rad = math.radians(lat_deg) n = 2.0 ** zoom xtile = int((lon_deg + 180.0) / 360.0 * n) ytile = int((1.0 - math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n) return(xtile, ytile)
tile numbers to lon/lat
import math def num2deg(xtile, ytile, zoom): n = 2.0 ** zoom lon_deg = xtile / n * 360.0 - 180.0 lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) lat_deg = math.degrees(lat_rad) return(lat_deg, lon_deg)
This returns the NW-corner of the square. Use the function with xtile+1 and/or ytile+1 to get the other corners. With xtile+0.5 & ytile+0.5 it will return the center of the tile.
Perl
lon/lat to tile numbers
use Math::Trig; sub getTileNumber { my ($lat,$lon,$zoom) = @_; my $xtile = int( ($lon+180)/360 *2**$zoom ) ; my $ytile = int( (1 - log(tan(deg2rad($lat)) + sec(deg2rad($lat)))/pi)/2 *2**$zoom ) ; return ($xtile, $ytile); }
tile numbers to lon/lat
use Math::Trig; sub Project { my ($X,$Y, $Zoom) = @_; my $Unit = 1 / (2 ** $Zoom); my $relY1 = $Y * $Unit; my $relY2 = $relY1 + $Unit; # note: $LimitY = ProjectF(degrees(atan(sinh(pi)))) = log(sinh(pi)+cosh(pi)) = pi # note: degrees(atan(sinh(pi))) = 85.051128.. #my $LimitY = ProjectF(85.0511); # so stay simple and more accurate my $LimitY = pi; my $RangeY = 2 * $LimitY; $relY1 = $LimitY - $RangeY * $relY1; $relY2 = $LimitY - $RangeY * $relY2; my $Lat1 = ProjectMercToLat($relY1); my $Lat2 = ProjectMercToLat($relY2); $Unit = 360 / (2 ** $Zoom); my $Long1 = -180 + $X * $Unit; return ($Lat2, $Long1, $Lat1, $Long1 + $Unit); # S,W,N,E } sub ProjectMercToLat($){ my $MercY = shift; return rad2deg(atan(sinh($MercY))); } sub ProjectF { my $Lat = shift; $Lat = deg2rad($Lat); my $Y = log(tan($Lat) + sec($Lat)); return $Y; }
PHP
lon/lat to tile numbers
$xtile = floor((($lon + 180) / 360) * pow(2, $zoom)); $ytile = floor((1 - log(tan(deg2rad($lat)) + 1 / cos(deg2rad($lat))) / pi()) /2 * pow(2, $zoom));
tile numbers to lon/lat
$n = pow(2, $zoom); $lon_deg = $xtile / $n * 360.0 - 180.0; $lat_deg = rad2deg(atan(sinh(pi() * (1 - 2 * $ytile / $n))));
ECMAScript (JavaScript/ActionScript etc.)
function long2tile(lon,zoom) { return (Math.floor((lon+180)/360*Math.pow(2,zoom))); } function lat2tile(lat,zoom) { return (Math.floor((1-Math.log(Math.tan(lat*Math.PI/180) + 1/Math.cos(lat*Math.PI/180))/Math.PI)/2 *Math.pow(2,zoom))); }
Inverse process:
function tile2long(x,z) { return (x/Math.pow(2,z)*360-180); } function tile2lat(y,z) { var n=Math.PI-2*Math.PI*y/Math.pow(2,z); return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n)))); }
Example: Tilesname WebCalc V1.0
C/C++
int long2tilex(double lon, int z) { return (int)(floor((lon + 180.0) / 360.0 * pow(2.0, z))); } int lat2tiley(double lat, int z) { return (int)(floor((1.0 - log( tan(lat * M_PI/180.0) + 1.0 / cos(lat * M_PI/180.0)) / M_PI) / 2.0 * pow(2.0, z))); } double tilex2long(int x, int z) { return x / pow(2.0, z) * 360.0 - 180; } double tiley2lat(int y, int z) { double n = M_PI - 2.0 * M_PI * y / pow(2.0, z); return 180.0 / M_PI * atan(0.5 * (exp(n) - exp(-n))); }
Java
public class slippytest { public static void main(String[] args) { int zoom = 10; double lat = 47.968056d; double lon = 7.909167d; System.out.println("http://tile.openstreetmap.org/" + getTileNumber(lat, lon, zoom) + ".png"); } public static String getTileNumber(final double lat, final double lon, final int zoom) { int xtile = (int)Math.floor( (lon + 180) / 360 * (1<<zoom) ) ; int ytile = (int)Math.floor( (1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2 * (1<<zoom) ) ; return("" + zoom + "/" + xtile + "/" + ytile); } }
compute bounding box for tile number
class BoundingBox { double north; double south; double east; double west; } BoundingBox tile2boundingBox(final int x, final int y, final int zoom) { BoundingBox bb = new BoundingBox(); bb.north = tile2lat(y, zoom); bb.south = tile2lat(y + 1, zoom); bb.west = tile2lon(x, zoom); bb.east = tile2lon(x + 1, zoom); return bb; } static double tile2lon(int x, int z) { return x / Math.pow(2.0, z) * 360.0 - 180; } static double tile2lat(int y, int z) { double n = Math.PI - (2.0 * Math.PI * y) / Math.pow(2.0, z); return Math.toDegrees(Math.atan(Math.sinh(n))); }
VB.Net
Private Function CalcTileXY(ByVal lat As Single, ByVal lon As Single, ByVal zoom As Long) As Point CalcTileXY.X = CLng(Math.Floor((lon + 180) / 360 * 2 ^ zoom)) CalcTileXY.Y = CLng(Math.Floor((1 - Math.Log(Math.Tan(lat * Math.PI / 180) + 1 / Math.Cos(lat * Math.PI / 180)) / Math.PI) / 2 * 2 ^ zoom)) End Function
C#
public PointF WorldToTilePos(double lon, double lat, int zoom) { PointF p = new Point(); p.X = (float)((lon + 180.0) / 360.0 * (1 << zoom)); p.Y = (float)((1.0 - Math.Log(Math.Tan(lat * Math.PI / 180.0) + 1.0 / Math.Cos(lat * Math.PI / 180.0)) / Math.PI) / 2.0 * (1 << zoom)); return p; } public PointF TileToWorldPos(double tile_x, double tile_y, int zoom) { PointF p = new Point(); double n = Math.PI - ((2.0 * Math.PI * tile_y) / Math.Pow(2.0, zoom)); p.X = (float)((tile_x / Math.Pow(2.0, zoom) * 360.0) - 180.0); p.Y = (float)(180.0 / Math.PI * Math.Atan(Math.Sinh(n))); return p; }
XSLT
Requires math extensions from exslt.org.
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:m="http://exslt.org/math" extension-element-prefixes="m" version="1.0"> <xsl:output method="text"/> <xsl:variable name="pi" select="3.14159265358979323846"/> <xsl:template name="tiley"> <xsl:param name="lat"/> <xsl:param name="zoomfact"/> <xsl:variable name="a" select="($lat * $pi) div 180.0"/> <xsl:variable name="b" select="m:log(m:tan($a) + (1.0 div m:cos($a)))"/> <xsl:variable name="c" select="(1.0 - ($b div $pi)) div 2.0"/> <xsl:value-of select="floor($c * $zoomfact)"/> </xsl:template> <xsl:template name="tilename"> <xsl:param name="lat"/> <xsl:param name="lon"/> <xsl:param name="zoom" select="10"/> <xsl:variable name="zoomfact" select="m:power(2,$zoom)"/> <xsl:variable name="x" select="floor((360.0 + ($lon * 2)) * $zoomfact div 720.0)"/> <xsl:variable name="y"> <xsl:call-template name="tiley"> <xsl:with-param name="lat" select="$lat"/> <xsl:with-param name="zoomfact" select="$zoomfact"/> </xsl:call-template> </xsl:variable> <xsl:value-of select="concat($zoom,'/',$x,'/',$y)"/> </xsl:template> <xsl:template match="/"> <xsl:call-template name="tilename"> <xsl:with-param name="lat" select="49.867731999999997"/> <xsl:with-param name="lon" select="8.6295369999999991"/> <xsl:with-param name="zoom" select="14"/> </xsl:call-template> </xsl:template> </xsl:transform>
Scala
import java.lang.Math._ def xy2latlon(pos: Tuple2[Double, Double], z: Int) = (toDegrees(atan(sinh(PI * (1.0 - 2.0 * pos._2 / (1<<z))))), pos._1 / (1<<z) * 360.0 - 180.0) def latlon2XY(latLon: Tuple2[Double, Double], z: Int) : Tuple2[Int, Int] = (((latLon._2 + 180.0) / 360.0 * (1<<z)).toInt, ((1 - log(tan(toRadians(latLon._1)) + 1 / cos(toRadians(latLon._1))) / PI) / 2.0 * (1<<z)).toInt) println(latlon2XY((51.0, 2.0), 16)) println(xy2latlon((33132, 21940), 16))
Revolution/Transcript
function osmTileRef iLat, iLong, iZoom --> part path local n, xTile, yTile put (2 ^ iZoom) into n put (iLong + 180) / 360 * n into xTile multiply iLat by (pi / 180) -- convert to radians put ((1 - ln(tan(iLat) + 1 / cos(iLat)) / pi) / 2) * n into yTile return "/" & iZoom & "/" & trunc(xTile) & "/" & trunc(yTile) end osmTileRef function osmTileCoords xTile, yTile, iZoom --> coordinates local twoPzoom, iLong, iLat, n put (2 ^ iZoom) into twoPzoom put xTile / twoPzoom * 360 - 180 into iLong put pi - 2 * pi * yTile / twoPzoom into n put "n1=" && n put 180 / pi * atan(0.5 * (exp(n) - exp(-n))) into iLat return iLat & comma & iLong end osmTileCoords
Mathematica
Deg2Num[lat_, lon_, zoom_] :=
{IntegerPart[(2^(-3 + zoom)*(180 + lon))/45], IntegerPart[2^(-1 + zoom)*(1 - Log[Sec[Degree*lat] + Tan[Degree*lat]]/Pi)]}
Num2Deg[xtile_,ytile_,zoom_] :=
{ArcTan[Sinh[Pi*(1 - 2*(ytile/2^zoom))]]/Degree, (xtile/2^zoom)*360 - 180} // N
Tcl
First of all, you need to use the package map::slippy from Tcllib:
package require map::slippy
lat/lon to tile number
map::slippy geo 2tile {$zoom $lat $lon}
tile number to lat/lon
map::slippy tile 2geo {$zoom $row $col}
Pascal
(translated from the Pythoncode above to Pascal)
coordinates to tile numbers
uses {...}, Math; {...} var zoom: Integer; lat_rad, lat_deg, lon_deg, n: Real; begin lat_rad := DegToRad(lat_deg); n := Power(2, zoom); xtile := Trunc(((lon_deg + 180) / 360) * n); ytile := Trunc((1 - (ln(Tan(lat_rad) + (1 /Cos(lat_rad))) / Pi)) / 2 * n); end;
tile numbers to coordinates
uses {...}, Math; {...} var zoom: Integer; lat_rad, lat_deg, lon_deg, n: Real; begin lat_rad := DegToRad(lat_deg); n := Power(2, zoom); xtile := Trunc(((lon_deg + 180) / 360) * n); ytile := Trunc((1 - (ln(Tan(lat_rad) + (1 /Cos(lat_rad))) / Pi)) / 2 * n); end;
Tiles
- Tiles are 256 × 256 pixel PNG files
- Each zoom level is a directory, each column is a subdirectory, and each tile in that column is a file
- Filename(url) format is /zoom/x/y.png
The slippy map expects tiles to be served up at URLs following this scheme, so all tile server URLs look pretty similar. For example:
| Name | URL | zoomlevel |
|---|---|---|
| OSM Mapnik | http://tile.openstreetmap.org/12/2047/1362.png | 0-18 |
| OSM Osmarender/Tiles@Home | http://tah.openstreetmap.org/Tiles/tile/12/2047/1362.png | 0-17 |
| OSM Cycle Map | http://andy.sandbox.cloudmade.com/tiles/cycle/12/2047/1362.png | 0-18 |
| OSM CloudMade Web style | http://tile.cloudmade.com/<YOUR CLOUDMADE API KEY>/1/256/12/2047/1362.png | 0-18 |
| OSM CloudMade Fine line style | http://tile.cloudmade.com/<YOUR CLOUDMADE API KEY>/2/256/12/2047/1362.png | 0-18 |
| OSM CloudMade NoNames style | http://tile.cloudmade.com/<YOUR CLOUDMADE API KEY>/3/256/12/2047/1362.png | 0-18 |
| Further tilesets are available from various '3rd party' sources. | ||
Subtiles
If you're looking at tile x,y and want to zoom in, the subtiles are (in the next zoom-level's coordinate system):
| 2x, 2y | 2x + 1, 2y |
| 2x, 2y + 1 | 2x + 1, 2y + 1 |
Similarly, zoom out by halving x and y (in the previous zoom level)
Tools
- Online X,Y <-> lat/long conversion (PHP-source)
- Same as above plus Tiles preview and direct link to Bigmap
- Javascript Example: Tilesname WebCalc V1.0
- Geo-OSM-Tiles: a Perl module that calculates tile numbers along with a script that downloads map tiles
References
- http://code.google.com/apis/maps/documentation/overlays.html#Google_Maps_Coordinates
- http://cfis.savagexi.com/articles/2006/05/03/google-maps-deconstructed
- "Google Map" projection, see Spatialreference.org [1]
- OSM mailing list refering to this page.
- Setting up TMS
- TMS specification from the OSGeo Foundation
- (note: Slippy tiles and Google map tiles count tile 0,0 down from the top-left of the tile grid; the TMS spec specifies tiles count up from 0,0 in the lower-left!)

