Slippy map tilenames
This article describes the file naming conventions for the Slippy Map application.
- 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.
Tile servers
| It has been proposed that this page or section be merged with TMS. (Discuss) |
The first part of the URL specifies the tile server, and perhaps other parameters which might influence the style.
- Generally several subdomains (server names) are provided to get around browser limitations on the number of simultaneous HTTP connections to each host. Browser-based applications can thus request multiple tiles from multiple subdomains faster than from one subdomain. For example, OSM, OpenCycleMap and CloudMade servers have three subdomains (a.tile, b.tile, c.tile), MapQuest has four (otile1, otile2, otile3, otile4), all pointing to the same CDN.
That all comes before the /zoom/x/y.png tail.
Here are some examples:
| Name | URL template | zoomlevels |
|---|---|---|
| OSM 'standard' style | http://[abc].tile.openstreetmap.org/zoom/x/y.png | 0-18 |
| OpenCycleMap | http://[abc].tile.opencyclemap.org/cycle/zoom/x/y.png | 0-18 |
| OpenCycleMap Transport (experimental) | http://[abc].tile2.opencyclemap.org/transport/zoom/x/y.png | 0-18 |
| CloudMade (Web style) | http://[abc].tile.cloudmade.com/your_CloudMade_API_key/1/256/zoom/x/y.png | 0-18 |
| CloudMade (Fine line style) | http://[abc].tile.cloudmade.com/your_CloudMade_API_key/2/256/zoom/x/y.png | 0-18 |
| CloudMade (NoNames style) | http://[abc].tile.cloudmade.com/your_CloudMade_API_key/3/256/zoom/x/y.png | 0-18 |
| MapQuest | http://otile[1234].mqcdn.com/tiles/1.0.0/osm/zoom/x/y.jpg | 0-19 |
| MapQuest Open Aerial | http://otile[1234].mqcdn.com/tiles/1.0.0/sat/zoom/x/y.jpg | 0-11 globally, 12+ in the U.S. |
| Migurski's Terrain | http://tile.stamen.com/terrain-background/zoom/x/y.jpg | 4-18, US-only (for now) |
Further tilesets are available from various '3rd party' sources.
Zoom levels
The zoom parameter is an integer between 0 (zoomed out) and 18 (zoomed in). 18 is normally the maximum, but some tile servers might go beyond that.
| 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 |
| 16 | 232 = 4 294 967 296 tiles | |
| 17 | 17 179 869 184 tiles | |
| 18 | Maximum zoom for Mapnik layer | 68 719 476 736 tiles |
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 (from EPSG:4326 to EPSG:3857):
- x = lon
- y = arsinh(tan(lat)) = 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 arsinh(x) = log(x + (x^2 + 1)^0.5) sec^2(x) = tan^2(x) + 1 → arsinh(tan(x)) = log(tan(x) + sec(x))
Please note that log represents logarithmus naturalis (also known as ln or loge), not decimal logarithm (log10), as used on some calculators.
Lon./lat. to tile numbers
n = 2 ^ zoom xtile = n * ((lon_deg + 180) / 360) ytile = n * (1 - (log(tan(lat_rad) + sec(lat_rad)) / π)) / 2
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 / π
Mathematics
Idem with mathematic signs:
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); } }
Tile bounding box
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 scala.math._ case class Tile(x: Int,y: Int, z: Short){ def toLatLon = new LatLonPoint( toDegrees(atan(sinh(Pi * (1.0 - 2.0 * y.toDouble / (1<<z))))), x.toDouble / (1<<z) * 360.0 - 180.0, z) def toURI = new java.net.URI("http://tile.openstreetmap.org/"+z+"/"+x+"/"+y+".png") } case class LatLonPoint(lat: Double, lon: Double, z: Short){ def toTile = new Tile( ((lon + 180.0) / 360.0 * (1<<z)).toInt, ((1 - log(tan(toRadians(lat)) + 1 / cos(toRadians(lat))) / Pi) / 2.0 * (1<<z)).toInt, z) } //Usage: val point = LatLonPoint(51.51202,0.02435,17) val tile = point.toTile // ==> Tile(65544,43582,17) val uri = tile.toURI // ==> http://tile.openstreetmap.org/17/65544/43582.png
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 [list $zoom $lat $lon]
Tile number to lat/lon
map::slippy tile 2geo [list $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 lat_rad, n: Real; begin n := Power(2, zoom); lat_rad := Arctan (Sinh (Pi * (1 - 2 * ytile / n))); lat_deg := RadtoDeg (lat_rad); lon_deg := xtile / n * 360.0 - 180.0; end;
R
Coordinates to tile numbers
deg2num<-function(lat_deg, lon_deg, zoom){
lat_rad <- lat_deg * pi /180
n <- 2.0 ^ zoom
xtile <- floor((lon_deg + 180.0) / 360.0 * n)
ytile = floor((1.0 - log(tan(lat_rad) + (1 / cos(lat_rad))) / pi) / 2.0 * n)
return( c(xtile, ytile))
# return(paste(paste("http://a.tile.openstreetmap.org", zoom, xtile, ytile, sep="/"),".png",sep=""))
}Bourne shell with Awk
Tile numbers to lat./lon. / Coordinates to tile numbers / Sample of usage, with optional tms-format support
xtile2long() { xtile=$1 zoom=$2 echo "${xtile} ${zoom}" | awk '{printf("%.9f", $1 / 2.0^$2 * 360.0 - 180)}' } long2xtile() { long=$1 zoom=$2 echo "${long} ${zoom}" | awk '{ xtile = ($1 + 180.0) / 360 * 2.0^$2; xtile+=xtile<0?-0.5:0.5; printf("%d", xtile ) }' } ytile2lat() { ytile=$1; zoom=$2; tms=$3; if [ ! -z "${tms}" ] then # from tms_numbering into osm_numbering ytile=`echo "${ytile}" ${zoom} | awk '{printf("%d\n",((2.0^$2)-1)-$1)}'`; fi lat=`echo "${ytile} ${zoom}" | awk -v PI=3.14159265358979323846 '{ num_tiles = PI - 2.0 * PI * $1 / 2.0^$2; printf("%.9f", 180.0 / PI * atan2(0.5 * (exp(num_tiles) - exp(-num_tiles)),1)); }'`; echo "${lat}"; } lat2ytile() { lat=$1; zoom=$2; tms=$3; ytile=`echo "${lat} ${zoom}" | awk -v PI=3.14159265358979323846 '{ tan_x=sin($1 * PI / 180.0)/cos($1 * PI / 180.0); ytile = (1 - log(tan_x + 1/cos($1 * PI/ 180))/PI)/2 * 2.0^$2; ytile+=ytile<0?-0.5:0.5; printf("%d", ytile ) }'`; if [ ! -z "${tms}" ] then # from oms_numbering into tms_numbering ytile=`echo "${ytile}" ${zoom} | awk '{printf("%d\n",((2.0^$2)-1)-$1)}'`; fi echo "${ytile}"; } # ------------------------------------ # Sample of use: # Position Brandenburg Gate, Berlin # ------------------------------------ LONG=13.37771496361961; LAT=52.51628011262304; ZOOM=17; TILE_X=70406; TILE_Y=42987; TILE_Y_TMS=88084; TMS=""; # when NOT empty: tms format assumed # ------------------------------------ # assume input/output of y is in oms-format: LONG=$( xtile2long ${TILE_X} ${ZOOM} ); LAT=$( ytile2lat ${TILE_Y} ${ZOOM} ${TMS} ); # Result should be longitude[13.375854492] latitude[52.517892228] TILE_X=$( long2xtile ${LONG} ${ZOOM} ); TILE_Y=$( lat2ytile ${LAT} ${ZOOM} ${TMS} ); # Result should be x[70406] y_oms[42987] # ------------------------------------ # assume input/output of y is in tms-format: TMS="tms"; TILE_Y_TMS=$( lat2ytile ${LAT} ${ZOOM} ${TMS} ); LAT_TMS=$( ytile2lat ${TILE_Y_TMS} ${ZOOM} ${TMS} ); echo "Result should be y_oms[${TILE_Y}] latitude[${LAT}] ; y_tms[${TILE_Y_TMS}] latitude_tms[${LAT_TMS}] " # latitude and latitude_tms should have the same value ; y_oms and y_tms should have the given start values: # Result should be y_oms[42987] latitude[52.517892228] ; y_tms[88084] latitude_tms[52.517892228] # ------------------------------------
Tile bounding box and center
n=$(ytile2lat `expr ${TILE_Y}` ${ZOOM}) s=$(ytile2lat `expr ${TILE_Y} + 1` ${ZOOM}) e=$(xtile2long `expr ${TILE_X} + 1` ${ZOOM}) w=$(xtile2long `expr ${TILE_X}` ${ZOOM}) echo "bbox=$w,$s,$e,$n" echo "-I-> Result should be [bbox=13.375854492,52.516220864,13.378601074,52.517892228]"; center_lat=`echo "$s $n" | awk '{printf("%.8f", ($1 + $2) / 2.0)}'` center_lon=`echo "$w $e" | awk '{printf("%.8f", ($1 + $2) / 2.0)}'` echo "center=$center_lat,$center_lon" echo "-I-> Result should be [center=52.51705655,13.37722778]";
tileinfo.sh
Based on the above functions, this script can show some basic information about a tile position:
Calling tileinfo.sh without parameters shows a Help-Text:
Usage: tileinfo.sh zoom-level x-position y-position [tms]
if 'x/y-position' are decimal numbers : Convert to tiles using the zoom-level if 'x/y-position' are integers numbers : Convert to Latitude/Longitude using the zoom-level if [tms] is set the tile y-position will be assumed as in the tms-format, otherwise as the oms-format Sample: Lat/Long to Tile: tileinfo.sh 17 13.37771496361961 52.51628011262304 Sample: Oms-Tile to Lat/Long: tileinfo.sh 17 70406 42987 Sample: Tms-Tile to Lat/Long: tileinfo.sh 17 70406 88084 tms Sample: Test-Position: tileinfo.sh -test
#!/bin/bash ################################################# # Copyright (c) 2013, Mark Johnson # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. #****************************************************************************** # http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames # http://oms.wff.ch/calc.htm # http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification # ----------------------------------------------------------------------- # Returns basic information about a tile-position # ----------------------------------------------------------------------- BENE="'Tutto bene!'"; BENENO="'No bene!'"; HABE_FERTIG="Ich habe fertig."; BASE_NAME=`basename $0`; BASE_NAME_CUT=`basename $0 | cut -d '.' -f1`; MESSAGE_TYPE="-I->"; exit_rc=0; tile_tmsosm() { # from tms/oms_numbering into osm/tms_numbering echo "${1}" ${2} | awk '{printf("%d\n",((2.0^$2)-1)-$1)}'; } # ------------------------------------ TMS=""; # ------------------------------------ LONG=13.37771496361961; LAT=52.51628011262304; ZOOM=17; TILE_X=70406; TILE_Y=42987; # 88084 TILE_Y_TMS=88084; # ------------------------------------ MODUS=""; INPUT_PARMS=""; # ------------------------------------ if [ $# -gt "0" ] then if [ $# -eq "1" ] && [ $1 == "-test" ] then MODUS=1; INPUT_PARMS="Test-Position Brandenburg Gate z/x,y[${ZOOM}/${LONG},${LAT}]"; fi if [ $# -eq "3" ] || [ $# -eq "4" ] then OIFS=$IFS; IFS='.'; ARRAY_02=( $2 ); ARRAY_03=( $3 ); IFS=$OIFS; if [ "${#ARRAY_02[@]}" -gt "1" ] && [ "${#ARRAY_03[@]}" -gt "1" ] then # This are decimal numbers MODUS=1; # long/lat to z/x/y ZOOM=$1; LONG=$2; LAT=$3; TILE_X=""; TILE_Y=""; TILE_Y_TMS=""; INPUT_PARMS="Input: Position z/x,y[${ZOOM}/${LONG},${LAT}]"; else MODUS=2; # z/x/y to long/lat ZOOM=$1; LONG=""; TILE_X=$2; if [ ! -z "$4" ] then # TMS="tms"; TILE_Y_TMS=$3; TILE_Y=$( tile_tmsosm ${TILE_Y_TMS} ${ZOOM} ); INPUT_PARMS="Input: Tms-Tile z/x/y[${ZOOM}/${TILE_X}/${TILE_Y_TMS}] oms[${TILE_Y}]"; else TILE_Y=$3; TILE_Y_TMS=$( tile_tmsosm ${TILE_Y} ${ZOOM} ); INPUT_PARMS="Input: Oms-Tile z/x/y[${ZOOM}/${TILE_X}/${TILE_Y}] tms[${TILE_Y_TMS}]"; fi fi fi fi # ------------------------------------ xtile2long() { xtile=$1 zoom=$2 echo "${xtile} ${zoom}" | awk '{printf("%.9f", $1 / 2.0^$2 * 360.0 - 180)}' } # ------------------------------------ long2xtile() { long=$1 zoom=$2 echo "${long} ${zoom}" | awk '{ xtile = ($1 + 180.0) / 360 * 2.0^$2; xtile+=xtile<0?-0.5:0.5; printf("%d", xtile ) }' } # ------------------------------------ ytile2lat() { ytile=$1; zoom=$2; tms=$3; if [ ! -z "${tms}" ] then # from tms_numbering into osm_numbering ytile=$( tile_tmsosm ${ytile} ${zoom} ); fi lat=`echo "${ytile} ${zoom}" | awk -v PI=3.14159265358979323846 '{ num_tiles = PI - 2.0 * PI * $1 / 2.0^$2; printf("%.9f", 180.0 / PI * atan2(0.5 * (exp(num_tiles) - exp(-num_tiles)),1)); }'`; echo "${lat}"; } # ------------------------------------ lat2ytile() { lat=$1; zoom=$2; tms=$3; ytile=`echo "${lat} ${zoom}" | awk -v PI=3.14159265358979323846 '{ tan_x=sin($1 * PI / 180.0)/cos($1 * PI / 180.0); ytile = (1 - log(tan_x + 1/cos($1 * PI/ 180))/PI)/2 * 2.0^$2; ytile+=ytile<0?-0.5:0.5; printf("%d", ytile ) }'`; if [ ! -z "${tms}" ] then # from oms_numbering into tms_numbering ytile=`echo "${ytile}" ${zoom} | awk '{printf("%d\n",((2.0^$2)-1)-$1)}'`; fi echo "${ytile}"; return; } # ------------------------------------ # Crude map scale # Beware: True scale value will depend on your specific monitor size and current resolution. # This is just a guide, map scale is only really meaningful for the printed page. # This dpi to meters-on-the-ground "pixel factor" is based on someone's ancient CRT monitor, perhaps a 15" model at 800x600 resolution calc_webtile_scale() { lat=$1 scale=$2 echo "$lat $scale" | awk -v PI=3.14159265358979323846 \ -v PIXELFACT=2817.947378 '{ a = 6378137.0; printf("%.0f", (a * 2*PI * cos($1 * PI/180) * PIXELFACT) / (256 * 2^$2)) }' } # ------------------------------------ as_bounding_box() { zoom=$1; xtile=$2; ytile=$3; tms=$4; n=$(ytile2lat `expr ${ytile}` ${zoom} ${tms} ); s=$(ytile2lat `expr ${ytile} + 1` ${zoom} ${tms} ); e=$(xtile2long `expr ${xtile} + 1` ${zoom} ${tms} ); w=$(xtile2long `expr ${xtile}` ${zoom} ${tms} ); echo "bbox=$w,$s,$e,$n" } # ------------------------------------ as_center() { zoom=$1; xtile=$2; ytile=$3; tms=$4; n=$(ytile2lat `expr ${ytile}` ${zoom} ${tms} ); s=$(ytile2lat `expr ${ytile} + 1` ${zoom} ${tms} ); e=$(xtile2long `expr ${xtile} + 1` ${zoom} ${tms} ); w=$(xtile2long `expr ${xtile}` ${zoom} ${tms} ); center_lat=`echo "$s $n" | awk '{printf("%.8f", ($1 + $2) / 2.0)}'` center_lon=`echo "$w $e" | awk '{printf("%.8f", ($1 + $2) / 2.0)}'` echo "center=$center_lon,$center_lat" } # ------------------------------------ as_ewkt() { zoom=$1; xtile=$2; ytile=$3; tms=$4; n=$(ytile2lat `expr ${ytile}` ${zoom} ${tms} ); s=$(ytile2lat `expr ${ytile} + 1` ${zoom} ${tms} ); e=$(xtile2long `expr ${xtile} + 1` ${zoom} ${tms} ); w=$(xtile2long `expr ${xtile}` ${zoom} ${tms} ); center_lat=`echo "$s $n" | awk '{printf("%.8f", ($1 + $2) / 2.0)}'` center_lon=`echo "$w $e" | awk '{printf("%.8f", ($1 + $2) / 2.0)}'` # SRID=4326;POINT(-83.3203125000000 30.9587685707799) # SRID=4326;POLYGON((-91.3842773437500 35.2231850497018, -75.2563476562500 35.2231850497018, -75.2563476562500 26.4951567924440, -91.3842773437500 26.4951567924440, -91.3842773437500 35.2231850497018)) echo "SRID=4326;POINT($center_lon $center_lat)\nSRID=4326;POLYGON($w $n,$e $n,$e $s,$w $s,$w $n)" } # ------------------------------------ as_position() { zoom=$1; xtile=$2; ytile=$3; tms=$4; CENTER=$(as_center ${zoom} ${xtile} ${ytile} ${tms} ); BBOX=$(as_bounding_box ${zoom} ${xtile} ${ytile} ${tms} ); echo "${CENTER}\n${BBOX}" } # ------------------------------------ as_tile() { zoom=$1; xtile=$2; ytile=$3; tms=$4; TILE_TMS=""; TILE_OMS=""; if [ -z "${tms}" ] then TILE_OMS="tile_oms : ${zoom}/${xtile}/${ytile}"; ytile=$( tile_tmsosm ${ytile} ${zoom} ); TILE_TMS="tile_tms : ${zoom}/${xtile}/${ytile}"; else TILE_TMS="tile_tms : ${zoom}/${xtile}/${ytile}"; ytile=$( tile_tmsosm ${ytile} ${zoom} ); TILE_OMS="tile_oms : ${zoom}/${xtile}/${ytile}"; fi echo -e "${TILE_OMS}\n${TILE_TMS}"; } # ------------------------------------ On_Help() { T=" "; POS_TEXT="${T}${T}"; #------------------------------------------------------------------------------ RC_TEXT="$RC_TEXT\n-H-> Usage: ${BASE_NAME} zoom-level x-position y-position [tms]"; RC_TEXT="$RC_TEXT\n${POS_TEXT}: if 'x/y-position' are decimal numbers : Convert to tiles using the zoom-level"; RC_TEXT="$RC_TEXT\n${POS_TEXT}: if 'x/y-position' are integers numbers : Convert to Latitude/Longitude using the zoom-level"; RC_TEXT="$RC_TEXT\n${POS_TEXT}: if [tms] is set the tile y-position will be assumed as in the tms-format, otherwise as the oms-format"; RC_TEXT="$RC_TEXT\n-H->${POS_TEXT}Sample:${T}Lat/Long to Tile:${T}${T}${BASE_NAME} ${ZOOM} ${LONG} ${LAT}"; RC_TEXT="$RC_TEXT\n-H->${POS_TEXT}Sample:${T}Oms-Tile to Lat/Long:${T}${BASE_NAME} ${ZOOM} ${TILE_X} ${TILE_Y}"; RC_TEXT="$RC_TEXT\n-H->${POS_TEXT}Sample:${T}Tms-Tile to Lat/Long:${T}${BASE_NAME} ${ZOOM} ${TILE_X} ${TILE_Y_TMS} tms"; RC_TEXT="$RC_TEXT\n-H->${POS_TEXT}Sample:${T}Test-Position:${T}${BASE_NAME} -test"; #------------------------------------------------------------------------------ echo ${RC_TEXT}; } # ------------------------------------ echo "${MESSAGE_TYPE} ${INPUT_PARMS}"; if [ ! -z "${MODUS}" ] then if [ "${MODUS}" -eq "1" ] then TILE_X=$( long2xtile ${LONG} ${ZOOM} ); TILE_Y=$( lat2ytile ${LAT} ${ZOOM} ${TMS} ) fi if [ "${MODUS}" -eq "2" ] then LONG=$( xtile2long ${TILE_X} ${ZOOM} ); LAT=$( ytile2lat ${TILE_Y} ${ZOOM} ${TMS} ); fi TILE=$(as_tile ${ZOOM} ${TILE_X} ${TILE_Y} ${TMS}); echo -e "${TILE}"; POSITION=$(as_position ${ZOOM} ${TILE_X} ${TILE_Y} ${TMS}); echo -e "${POSITION}"; EWKT=$(as_ewkt ${ZOOM} ${TILE_X} ${TILE_Y} ${TMS}); echo -e "${EWKT}"; else # ------------------------------------ RC_TEXT=$( On_Help ); echo -e "${RC_TEXT}"; MESSAGE_TYPE="-H->"; fi #--------------------------------------------------- if [ "$exit_rc" -eq "0" ] then RC_TEXT=$BENE; else RC_TEXT="$BENENO"; MESSAGE_TYPE="-E->"; fi echo "${MESSAGE_TYPE} ${BASE_NAME_CUT} rc=$exit_rc [${RC_TEXT}] - ${HABE_FERTIG}"; exit $exit_rc; #---------------------------------------------------
Crude map scale
Beware: True scale value will depend on your specific monitor size and current resolution. This is just a guide, map scale is only really meaningful for the printed page. This dpi to meters-on-the-ground "pixel factor" is based on someone's ancient CRT monitor, perhaps a 15" model at 800x600 resolution.
calc_webtile_scale() { lat=$1 scale=$2 echo "$lat $scale" | awk -v PI=3.14159265358979323846 \ -v PIXELFACT=2817.947378 '{ a = 6378137.0; printf("%.0f", (a * 2*PI * cos($1 * PI/180) * PIXELFACT) / (256 * 2^$2)) }' } mapscale=$(calc_webtile_scale $center_lat $zoom)
Octave
Lon./lat. to tile numbers
% convert the degrees to radians rho = pi/180; lon_rad = lon_deg * rho; lat_rad = lat_deg * rho; n = 2 ^ zoom xtile = n * ((lon_deg + 180) / 360) ytile = n * (1 - (log(tan(lat_rad) + sec(lat_rad)) / pi)) / 2
Emacs-lisp
(defun longitude2tile (lon zoom) (* (expt 2 zoom) (/ (+ lon 180) 360))) (defun tile2longitude (x zoom) (- (/ (* x 360) (expt 2 zoom)) 180)) (defun latitude2tile (lat zoom) (* (expt 2 zoom) (/ (- 1 (/ (log (+ (tan (/ (* lat pi) 180)) (/ 1 (cos (/ (* lat pi) 180))))) pi)) 2))) (defun sinh (value) (/ (- (exp value) (exp (- value))) 2)) (defun tile2latitude (y zoom) (/ (* 180 (atan (sinh (* pi (- 1 (* 2 (/ y (expt 2 zoom)))))))) pi))
Erlang
-module(slippymap). -export([deg2num/3]). -export([num2deg/3]). deg2num(Lat,Lon,Zoom)-> X=math:pow(2, Zoom) * ((Lon + 180) / 360), Sec=1/math:cos(deg2rad(Lat)), R = math:log(math:tan(deg2rad(Lat)) + Sec)/math:pi(), Y=math:pow(2, Zoom) * (1 - R) / 2, {round(X),round(Y)}. num2deg(X,Y,Zoom)-> N=math:pow(2, Zoom), Lon=X/N*360-180, Lat_rad=math:atan(math:sinh(math:pi()*(1-2*Y/N))), Lat=Lat_rad*180/math:pi(), {Lon,Lat}. deg2rad(C)-> C*math:pi()/180.
Lua
function deg2num(lon, lat, zoom) local n = 2 ^ zoom local lon_deg = tonumber(lon) local lat_rad = math.rad(lat) local xtile = math.floor(n * ((lon_deg + 180) / 360)) local ytile = math.floor(n * (1 - (math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi)) / 2) return xtile, ytile end function num2deg(x, y, z) local n = 2 ^ z local lon_deg = x / n * 360.0 - 180.0 local lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * y / n))) local lat_deg = lat_rad * 180.0 / math.pi return lon_deg, lat_deg end
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)
Resolution and Scale
Exact length of the equator (according to wikipedia) is 40075.016686 km in WGS-84. A horizontal tile size at zoom 0 would be 156543.034 meters. Which gives us a formula to calculate resolution at any given zoom:
resolution = 156543.034 meters/pixel * cos(latitude) / (2 ^ zoomlevel)
Some applications need to know a map scale, that is, how 1 cm on a screen translates to 1 cm of a map.
scale = 1 : (screen_dpi * 39.37 in/m * resolution)
And here is the table to rid you of those calculations. All values are shown for equator, and you have to multiply them by cos(latitude) to adjust to a given latitude. For example, divide those by 2 for latitude 60 (Oslo, Helsinki, Saint-Petersburg).
| zoom | resolution, m/px | scale 96 dpi | 1 screen cm is | scale 120 dpi |
|---|---|---|---|---|
| 0 | 156543.03 | 1 : 554 678 932 | 5547 km | 1 : 739 571 909 |
| 1 | 78271.52 | 1 : 277 339 466 | 2773 km | 1 : 369 785 954 |
| 2 | 39135.76 | 1 : 138 669 733 | 1337 km | 1 : 184 892 977 |
| 3 | 19567.88 | 1 : 69 334 866 | 693 km | 1 : 92 446 488 |
| 4 | 9783.94 | 1 : 34 667 433 | 347 km | 1 : 46 223 244 |
| 5 | 4891.97 | 1 : 17 333 716 | 173 km | 1 : 23 111 622 |
| 6 | 2445.98 | 1 : 8 666 858 | 86.7 km | 1 : 11 555 811 |
| 7 | 1222.99 | 1 : 4 333 429 | 43.3 km | 1 : 5 777 905 |
| 8 | 611.50 | 1 : 2 166 714 | 21.7 km | 1 : 2 888 952 |
| 9 | 305.75 | 1 : 1 083 357 | 10.8 km | 1 : 1 444 476 |
| 10 | 152.87 | 1 : 541 678 | 5.4 km | 1 : 722 238 |
| 11 | 76.437 | 1 : 270 839 | 2.7 km | 1 : 361 119 |
| 12 | 38.219 | 1 : 135 419 | 1.4 km | 1 : 180 559 |
| 13 | 19.109 | 1 : 67 709 | 677 m | 1 : 90 279 |
| 14 | 9.5546 | 1 : 33 854 | 339 m | 1 : 45 139 |
| 15 | 4.7773 | 1 : 16 927 | 169 m | 1 : 22 569 |
| 16 | 2.3887 | 1 : 8 463 | 84.6 m | 1 : 11 284 |
| 17 | 1.1943 | 1 : 4 231 | 42.3 m | 1 : 5 642 |
| 18 | 0.5972 | 1 : 2 115 | 21.2 m | 1 : 2 821 |
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
- Kachelbrowser
- File:Lat lon.odt feuille de calcul openoffice (sheet)
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!)