Zoom Levels and Tile Grid

Service version: 2
Last edit: 2023.08.01

Purpose

TomTom Maps use the Spherical Mercator projection coordinate system (EPSG:3857).

Zoom levels

The world is divided into square tiles.

  • Maps Raster
    • Map tiles has 23 zoom levels, numbered 0 through 22.
    • Satellite tiles has 20 zoom levels, numbered 0 through 19.
    • Hillshade tiles has 14 zoom levels, numbered 0 through 13.
  • Maps Vector has 23 zoom levels, numbered 0 through 22.

At zoom level 0, the entire world fits on a single tile:

World on a single tile

Zoom level 1 uses 4 tiles to render the world: a 2 x 2 square.

upper-left tileupper-right title
lower-left tilelower-right tile

Each subsequent zoom level quad divides the tiles of the previous one, creating a grid of 2zoom x 2zoom. For example, zoom level 22 is a grid 222 x 222, or 4,194,304 x 4,194,304 tiles (result: 17,592,186,044,416 in total).

To discover the real-world size of a single tile on a given zoom level, we can use the formula circumference of earth / 2zoom level that produces number of meters per tile side, where the circumference of the earth equals 40,075,017 meters. The full data table of values for zoom levels is here:

zoom level

meters/pixel

meters/tile side

0

15654340075017

1

78271.520037508

2

39135.810018754

3

19567.885009377.1

4

9783.942504688.5

5

4891.971252344.3

6

2445.98626172.1

7

1222.99313086.1

8

611.5156543

9

305.7578271.5

10

152.8739135.8

11

76.4419567.9

12

38.2199783.94

13

19.1094891.97

14

9.5552445.98

15

4.7771222.99

16

2.3887611.496

17

1.1943305.748

18

0.5972152.874

19

0.298676.437

20

0.1492938.2185

21

0.07464619.10926

22

0.0373239.55463

Tile grid

Tiles are called by zoom level and the x and y coordinates corresponding to the tile's position on the grid for that zoom level.

When determining which zoom level to use, remember that each location is in a fixed position on its tile.

  • This means that the number of tiles needed to display a given expanse of territory is dependent on the specific placement of zoom grid on the world.
  • For instance, if there are two points 900 meters apart, it may only take three tiles to display a route between them at zoom level 17.

However, if the western point is on the right side of its tile, and the eastern point on its left side, it may take four tiles as shown in the following diagram:

4 tiles and 3 tiles routes

Once the zoom level is determined, the x and y coordinate values can be calculated:

  • The top-left tile in each zoom grid is
    **x=0**
    ,
    **y=0**
    .
  • The bottom-right tile is at
    **x=2 zoom -1**
    ,
    **y=2 zoom -1**
    .

Here is the zoom grid for zoom level 1:

Zoom grid for zoom level 1

Coordinates conversion

Convert latitude/longitude coordinates to tile z/x/y coordinates.

CoordinatesZoom levelResult
Convert
Latitude/longitude to tile z/x/y
Select...
1function latLonToTileZXY(lat, lon, zoomLevel) {
2 const MIN_ZOOM_LEVEL = 0
3 const MAX_ZOOM_LEVEL = 22
4 const MIN_LAT = -85.051128779807
5 const MAX_LAT = 85.051128779806
6 const MIN_LON = -180.0
7 const MAX_LON = 180.0
8
9 if (
10 zoomLevel == undefined ||
11 isNaN(zoomLevel) ||
12 zoomLevel < MIN_ZOOM_LEVEL ||
13 zoomLevel > MAX_ZOOM_LEVEL
14 ) {
15 throw new Error(
16 "Zoom level value is out of range [" +
17 MIN_ZOOM_LEVEL.toString() +
18 ", " +
19 MAX_ZOOM_LEVEL.toString() +
20 "]"
21 )
22 }
23
24 if (lat == undefined || isNaN(lat) || lat < MIN_LAT || lat > MAX_LAT) {
25 throw new Error(
26 "Latitude value is out of range [" +
27 MIN_LAT.toString() +
28 ", " +
29 MAX_LAT.toString() +
30 "]"
31 )
32 }
33
34 if (lon == undefined || isNaN(lon) || lon < MIN_LON || lon > MAX_LON) {
35 throw new Error(
36 "Longitude value is out of range [" +
37 MIN_LON.toString() +
38 ", " +
39 MAX_LON.toString() +
40 "]"
41 )
42 }
43
44 let z = Math.trunc(zoomLevel)
45 let xyTilesCount = Math.pow(2, z)
46 let x = Math.trunc(Math.floor(((lon + 180.0) / 360.0) * xyTilesCount))
47 let y = Math.trunc(
48 Math.floor(
49 ((1.0 -
50 Math.log(
51 Math.tan((lat * Math.PI) / 180.0) +
52 1.0 / Math.cos((lat * Math.PI) / 180.0)
53 ) /
54 Math.PI) /
55 2.0) *
56 xyTilesCount
57 )
58 )
59
60 return z.toString() + "/" + x.toString() + "/" + y.toString()
61}
1std::string latLonToTileZXY(double lat, double lon, unsigned int z)
2{
3 constexpr unsigned int MAX_ZOOM_LEVEL = 22;
4 constexpr double MIN_LAT = -85.051128779807;
5 constexpr double MAX_LAT = 85.051128779806;
6 constexpr double MIN_LON = -180.0;
7 constexpr double MAX_LON = 180.0;
8
9 if (z > MAX_ZOOM_LEVEL) {
10 throw std::invalid_argument("Zoom level value is out of range [0, " +
11 std::to_string(MAX_ZOOM_LEVEL) + "]");
12 }
13
14 if (!std::isfinite(lat) || (lat < MIN_LAT) || (lat > MAX_LAT)) {
15 throw std::invalid_argument("Latitude value is out of range [" +
16 std::to_string(MIN_LAT) + ", " + std::to_string(MAX_LAT) + "]");
17 }
18
19 if (!std::isfinite(lon) || (lon < MIN_LON) || (lon > MAX_LON)) {
20 throw std::invalid_argument("Longitude value is out of range [" +
21 std::to_string(MIN_LON) + ", " + std::to_string(MAX_LON) + "]");
22 }
23
24 const int xyTilesCount = 1 << z;
25 int x = floor((lon + 180.0) / 360.0 * xyTilesCount);
26 int y = floor((1.0 - log(tan(lat * M_PI / 180.0) + 1.0 / cos(lat * M_PI / 180.0)) / M_PI) /
27 2.0 * xyTilesCount);
28
29 return std::to_string(z) + "/" + std::to_string(x) + "/" + std::to_string(y);
30}
1public static String latLonToTileZXY(double lat, double lon, int z)
2{
3 final int MIN_ZOOM_LEVEL = 0;
4 final int MAX_ZOOM_LEVEL = 22;
5 final double MIN_LAT = -85.051128779807;
6 final double MAX_LAT = 85.051128779806;
7 final double MIN_LON = -180.0;
8 final double MAX_LON = 180.0;
9
10 if ((z < MIN_ZOOM_LEVEL) || (z > MAX_ZOOM_LEVEL))
11 {
12 throw new IllegalArgumentException("Zoom level value is out of range [" +
13 Integer.toString(MIN_ZOOM_LEVEL) + ", " +
14 Integer.toString(MAX_ZOOM_LEVEL) + "]");
15 }
16
17 if (!Double.isFinite(lat) || (lat < MIN_LAT) || (lat > MAX_LAT))
18 {
19 throw new IllegalArgumentException("Latitude value is out of range [" +
20 Double.toString(MIN_LAT) + ", " +
21 Double.toString(MAX_LAT) + "]");
22 }
23
24 if (!Double.isFinite(lon) || (lon < MIN_LON) || (lon > MAX_LON))
25 {
26 throw new IllegalArgumentException("Longitude value is out of range [" +
27 Double.toString(MIN_LON) + ", " +
28 Double.toString(MAX_LON) + "]");
29 }
30
31 int xyTilesCount = (int)Math.pow(2, z);
32 int x = (int) Math.floor((lon + 180.0) / 360.0 * xyTilesCount);
33 int y = (int) Math.floor((1.0 - Math.log(Math.tan(lat * Math.PI / 180.0) + 1.0 / Math.cos(lat * Math.PI / 180.0)) / Math.PI) / 2.0 * xyTilesCount);
34
35 return Integer.toString(z) + "/" + Integer.toString(x) + "/" + Integer.toString(y);
36}

Convert tile z/x/y coordinates to latitude/longitude coordinates.

Tile coordinatesResult
Convert
Tile z/x/y to latitude/longitude
Select...
1function tileZXYToLatLon(zoomLevel, x, y) {
2 const MIN_ZOOM_LEVEL = 0
3 const MAX_ZOOM_LEVEL = 22
4
5 if (
6 zoomLevel == undefined ||
7 isNaN(zoomLevel) ||
8 zoomLevel < MIN_ZOOM_LEVEL ||
9 zoomLevel > MAX_ZOOM_LEVEL
10 ) {
11 throw new Error(
12 "Zoom level value is out of range [" +
13 MIN_ZOOM_LEVEL.toString() +
14 "," +
15 MAX_ZOOM_LEVEL.toString() +
16 "]"
17 )
18 }
19
20 let z = Math.trunc(zoomLevel)
21 let minXY = 0
22 let maxXY = Math.pow(2, z) - 1
23
24 if (x == undefined || isNaN(x) || x < minXY || x > maxXY) {
25 throw new Error(
26 "Tile x value is out of range [" +
27 minXY.toString() +
28 "," +
29 maxXY.toString() +
30 "]"
31 )
32 }
33
34 if (y == undefined || isNaN(y) || y < minXY || y > maxXY) {
35 throw new Error(
36 "Tile y value is out of range [" +
37 minXY.toString() +
38 "," +
39 maxXY.toString() +
40 "]"
41 )
42 }
43
44 let lon = (x / Math.pow(2, z)) * 360.0 - 180.0
45
46 let n = Math.PI - (2.0 * Math.PI * y) / Math.pow(2, z)
47 let lat = (180.0 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))
48
49 return lat.toString() + "/" + lon.toString()
50}
1std::string tileZXYToLatLon(unsigned int z, unsigned int x, unsigned int y)
2{
3 constexpr unsigned int MAX_ZOOM_LEVEL = 22;
4 if (z > MAX_ZOOM_LEVEL)
5 {
6 throw std::invalid_argument("Zoom level value is out of range [0, " +
7 std::to_string(MAX_ZOOM_LEVEL) + "]");
8 }
9
10 const long maxXY = (1 << z) - 1;
11 if (x > maxXY)
12 {
13 throw std::invalid_argument("Tile x value is out of range [0, " +
14 std::to_string(maxXY) + "]");
15 }
16
17 if (y > maxXY)
18 {
19 throw std::invalid_argument("Tile y value is out of range [0, " +
20 std::to_string(maxXY) + "]");
21 }
22
23 const int xyTilesCount = 1 << z;
24 const double lon = static_cast<double>(x) / xyTilesCount * 360.0 - 180.0;
25
26 const double n = M_PI - 2.0 * M_PI * static_cast<double>(y) / xyTilesCount;
27 const double lat = 180.0 / M_PI * atan(0.5 * (exp(n) - exp(-n)));
28
29 return std::to_string(lat) + "/" + std::to_string(lon);
30}
1public static String tileZXYToLatLon(int z, int x, int y)
2{
3 final int MIN_ZOOM_LEVEL = 0;
4 final int MAX_ZOOM_LEVEL = 22;
5
6 if ((z < MIN_ZOOM_LEVEL) || (z > MAX_ZOOM_LEVEL))
7 {
8 throw new IllegalArgumentException("Zoom level value is out of range [" +
9 Integer.toString(MIN_ZOOM_LEVEL) + ", " +
10 Integer.toString(MAX_ZOOM_LEVEL) + "]");
11 }
12
13 int minXY = 0;
14 int maxXY = (int)(Math.pow(2, z) - 1);
15 if ((x < minXY) || (x > maxXY))
16 {
17 throw new IllegalArgumentException("Tile x value is out of range [" +
18 Integer.toString(minXY) + ", " +
19 Integer.toString(maxXY) + "]");
20 }
21
22 if ((y < 0) || (y > maxXY))
23 {
24 throw new IllegalArgumentException("Tile y value is out of range [" +
25 Integer.toString(minXY) + ", " +
26 Integer.toString(maxXY) + "]");
27 }
28
29 double lon = (double)x / Math.pow(2, z) * 360.0 - 180.0;
30
31 double n = Math.PI - 2.0 * Math.PI * (double)y / Math.pow(2, z);
32 double lat = 180.0 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
33
34 return Double.toString(lat) + "/" + Double.toString(lon);
35}

Convert tile z/x/y coordinates to latitude/longitude bounding box.

Tile coordinatesResult
Convert
Tile z/x/y to latitude/longitude bounding box
Select...
1function tileZXYToLatLonBBox(zoomLevel, x, y) {
2 const MIN_ZOOM_LEVEL = 0
3 const MAX_ZOOM_LEVEL = 22
4
5 if (
6 zoomLevel == undefined ||
7 isNaN(zoomLevel) ||
8 zoomLevel < MIN_ZOOM_LEVEL ||
9 zoomLevel > MAX_ZOOM_LEVEL
10 ) {
11 throw new Error(
12 "Zoom level value is out of range [" +
13 MIN_ZOOM_LEVEL.toString() +
14 "," +
15 MAX_ZOOM_LEVEL.toString() +
16 "]"
17 )
18 }
19
20 let z = Math.trunc(zoomLevel)
21 let minXY = 0
22 let maxXY = Math.pow(2, z) - 1
23
24 if (x == undefined || isNaN(x) || x < minXY || x > maxXY) {
25 throw new Error(
26 "Tile x value is out of range [" +
27 minXY.toString() +
28 "," +
29 maxXY.toString() +
30 "]"
31 )
32 }
33
34 if (y == undefined || isNaN(y) || y < minXY || y > maxXY) {
35 throw new Error(
36 "Tile y value is out of range [" +
37 minXY.toString() +
38 "," +
39 maxXY.toString() +
40 "]"
41 )
42 }
43
44 let lon1 = (x / Math.pow(2, z)) * 360.0 - 180.0
45
46 let n1 = Math.PI - (2.0 * Math.PI * y) / Math.pow(2, z)
47 let lat1 = (180.0 / Math.PI) * Math.atan(0.5 * (Math.exp(n1) - Math.exp(-n1)))
48
49 let lon2 = ((x + 1) / Math.pow(2, z)) * 360.0 - 180.0
50
51 let n2 = Math.PI - (2.0 * Math.PI * (y + 1)) / Math.pow(2, z)
52 let lat2 = (180.0 / Math.PI) * Math.atan(0.5 * (Math.exp(n2) - Math.exp(-n2)))
53
54 return (
55 lat1.toString() +
56 "/" +
57 lon1.toString() +
58 "/" +
59 lat2.toString() +
60 "/" +
61 lon2.toString()
62 )
63}
1std::string tileZXYToLatLonBBox(unsigned int z, unsigned int x, unsigned int y)
2{
3 constexpr unsigned int MAX_ZOOM_LEVEL = 22;
4 if (z > MAX_ZOOM_LEVEL)
5 {
6 throw std::invalid_argument("Zoom level value is out of range [0, " +
7 std::to_string(MAX_ZOOM_LEVEL) + "]");
8 }
9
10 const long maxXY = (1 << z) - 1;
11 if (x > maxXY)
12 {
13 throw std::invalid_argument("Tile x value is out of range [0, " +
14 std::to_string(maxXY) + "]");
15 }
16
17 if (y > maxXY)
18 {
19 throw std::invalid_argument("Tile y value is out of range [0, " +
20 std::to_string(maxXY) + "]");
21 }
22
23 const int xyTilesCount = 1 << z;
24 const double lon1 = static_cast<double>(x) / xyTilesCount * 360.0 - 180.0;
25
26 const double n1 = M_PI - 2.0 * M_PI * static_cast<double>(y) / xyTilesCount;
27 const double lat1 = 180.0 / M_PI * atan(0.5 * (exp(n1) - exp(-n1)));
28
29 const double lon2 = static_cast<double>(x + 1) / xyTilesCount * 360.0 - 180.0;
30
31 const double n2 = M_PI - 2.0 * M_PI * static_cast<double>(y + 1) / xyTilesCount;
32 const double lat2 = 180.0 / M_PI * atan(0.5 * (exp(n2) - exp(-n2)));
33
34 return std::to_string(lat1) + "/" + std::to_string(lon1) + "/" + std::to_string(lat2) + "/" + std::to_string(lon2);
35}
1public static String tileZXYToLatLonBBox(int z, int x, int y)
2{
3 final int MIN_ZOOM_LEVEL = 0;
4 final int MAX_ZOOM_LEVEL = 22;
5
6 if ((z < MIN_ZOOM_LEVEL) || (z > MAX_ZOOM_LEVEL))
7 {
8 throw new IllegalArgumentException("Zoom level value is out of range [" +
9 Integer.toString(MIN_ZOOM_LEVEL) + ", " +
10 Integer.toString(MAX_ZOOM_LEVEL) + "]");
11 }
12
13 int minXY = 0;
14 int maxXY = (int)(Math.pow(2, z) - 1);
15 if ((x < minXY) || (x > maxXY))
16 {
17 throw new IllegalArgumentException("Tile x value is out of range [" +
18 Integer.toString(minXY) + ", " +
19 Integer.toString(maxXY) + "]");
20 }
21
22 if ((y < 0) || (y > maxXY))
23 {
24 throw new IllegalArgumentException("Tile y value is out of range [" +
25 Integer.toString(minXY) + ", " +
26 Integer.toString(maxXY) + "]");
27 }
28
29 double lon1 = (double)x / Math.pow(2, z) * 360.0 - 180.0;
30
31 double n1 = Math.PI - 2.0 * Math.PI * (double)y / Math.pow(2, z);
32 double lat1 = 180.0 / Math.PI * Math.atan(0.5 * (Math.exp(n1) - Math.exp(-n1)));
33
34 double lon2 = (double)(x + 1) / Math.pow(2, z) * 360.0 - 180.0;
35
36 double n2 = Math.PI - 2.0 * Math.PI * (double)(y + 1) / Math.pow(2, z);
37 double lat2 = 180.0 / Math.PI * Math.atan(0.5 * (Math.exp(n2) - Math.exp(-n2)));
38
39 return Double.toString(lat1) + "/" + Double.toString(lon1) + "/" + Double.toString(lat2) + "/" + Double.toString(lon2);
40}