Slippy map tilenames

From OpenStreetMap Wiki

Jump to: navigation, search

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.

A other python implementation

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

References

(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!)
Personal tools
Recent changes