国内主要地图瓦片坐标系定义及计算原理

本文将介绍瓦片坐标相关知识,并提供高德地图、百度地图、谷歌地图的经纬度坐标与瓦片坐标的相互转换方法和类库。

背景

互联网地图服务商的在线地图都通过瓦片的方式提供,称为瓦片地图服务。最常见的地图瓦片是图片格式的,现在有的地图服务商也提供了矢量的瓦片数据,然后在用户端使用Canvas渲染成图片,如node-canvas实现百度地图个性化底图绘制
在进行地图开发时,为获取特定经纬度所在区域的瓦片和获取瓦片上像素点对应的经纬度,经常需要进行经纬度坐标与瓦片坐标、像素坐标的相互转换。本文将介绍瓦片坐标相关知识,并提供高德地图、百度地图、谷歌地图的经纬度坐标与瓦片坐标的相互转换方法和转换类库–tile-lnglat-transform

主要经纬度坐标系

国际标准的经纬度坐标是WGS84,Open Street Map、外国版的Google Map都是采用WGS84;高德地图使用的坐标系是GCJ-02;百度地图使用的坐标系是BD-09。高德地图和百度地图都提供了在线的单向坐标转换接口,将其他坐标系换化到自己的坐标系,但这种转换受限于http url请求字段长度和网络请求延迟,批量处理并不实用。离线相互转换可以通过开源JavaScript库coordtransform实现,误差在10米左右。
虽然各地图服务商经纬度坐标系不同,但某一互联网地图的经纬度坐标与瓦片坐标相互转换只与该地图商的墨卡托投影和瓦片编号的定义有关,跟地图商采用的大地坐标系标准无关。

墨卡托投影

使用经纬度表示位置的大地坐标系虽然可以描述地球上点的位置,但是对于地图地理数据在二维平面内展示的场景,需要通过投影的方式将三维空间中的点映射到二维空间中。地图投影需要建立地球表面点与投影平面点的一一对应关系,在互联网地图中常使用墨卡托投影。墨卡托投影是荷兰地理学家墨卡托于1569年提出的一种地球投影方法,该方法是圆柱投影的一种。投影的更多内容,可以查看地图投影的N种姿势

墨卡托投影示意图

据我了解,各大地图服务商都采用了Web Mercator进行投影,瓦片坐标系的不同主要是投影截取的地球范围不同、瓦片坐标起点不同。

值得注意的是:

  • 墨卡托投影并不是一种坐标系,而是为了在二维平面上展示三维地球而进行的一种空间映射。所以在GIS地图和互联网地图中,虽然用户看到的地图经过了墨卡托投影,但依然使用经纬度坐标来表示地球上点的位置。
  • 在地图绘制和地图可视化时,就需要将地图数据使用投影的方式来呈现。

瓦片切割和瓦片坐标

对于经过墨卡托投影为平面的世界地图,在不同的地图分辨率(整个世界地图的像素大小)下,通过切割的方式将世界地图划分为像素为$256\times256$的地图单元,划分成的每一块地图单元称为地图瓦片。
地图瓦片具有以下特点:

  • 具有唯一的瓦片等级(Level)和瓦片坐标编号(tileX, tileY)。
  • 瓦片分辨率为256$\times$256。
  • 最小的地图等级是0,此时世界地图只由一张瓦片组成。
  • 瓦片等级越高,组成世界地图的瓦片数越多,可以展示的地图越详细。
  • 某一瓦片等级地图的瓦片是由低一级的各瓦片切割成的4个瓦片组成,形成了瓦片金字塔。

瓦片切割(瓦片金字塔)

高德地图瓦片坐标

坐标系定义

高德地图瓦片坐标与Google Map、Open Street Map相同。高德地图的墨卡托投影截取了纬度(约85.05ºS, 约85.05ºN)之间部分的地球,使得投影后的平面地图水平方向和垂直方向长度相等。将墨卡托投影地图的左上角作为瓦片坐标系起点,往左方向为X轴,X轴与北纬85.05º重合且方向向左;往下方向为Y轴,Y轴与东经180º(亦为西经180º)重合且方向向下。瓦片坐标最小等级为0级,此时平面地图是一个像素为256*256的瓦片。在某一瓦片层级Level下,瓦片坐标的X轴和Y轴各有$2^{Level}$个瓦片编号,瓦片地图上的瓦片总数为$2^{Level}\times2^{Level}$。

高德地图Level=2的瓦片坐标编号情况

如上图所示,此时X方向和Y方向各有4个瓦片编号,总瓦片数为16。中国大概位于高德瓦片坐标的(3,1)中。

坐标转换图解

高德地图坐标转换图解

从高德地图坐标转换图解中可以看出,高德地图的坐标转换具有以下特点:

  • 所有坐标转换都在某一瓦片等级下进行,不同瓦片等级下的转换结果不同。
  • 经纬度坐标可以直接转换为瓦片坐标和瓦片像素坐标。
  • 瓦片像素坐标需要结合其瓦片坐标才能得到该像素坐标的经纬度坐标。

坐标转换公式

方法参考:Slippy map tilenames

  • 经纬度坐标(lng, lat)转瓦片坐标(tileX, tileY):

$$tileX=\lfloor\frac{lng + 180}{360}\times{2^{Level}}\rfloor$$
$$tileY=\lfloor{(\frac{1}{2}-\frac{\ln(\tan(lat\times\pi/180)+\sec(lat\times\pi/180))}{2\timesπ}})\times{2^{Level}}\rfloor$$

  • 经纬度坐标(lng, lat)转像素坐标(pixelX, pixelY)

$$pixelX=\lfloor\frac{lng + 180}{360}\times{2^{Level}}\times256\%256\rfloor$$
$$pixelY=\lfloor{(1-\frac{\ln(\tan(lat\times\pi/180)+\sec(lat\times\pi/180))}{2\timesπ}})\times{2^{Level}}\times256\%256\rfloor$$

  • 瓦片(tileX, tileY)的像素坐标(pixelX, pixelY)转经纬度坐标(lng, lat)

$$lng=\frac{tileX+\frac{pixelX}{256}}{2^{Level}}\times360-180$$
$$lat=\arctan({\sinh({\pi-2\times\pi\times\frac{tileY+\frac{pixelY}{256}}{2^{Level}}})})\times\frac{180}{\pi}$$

百度地图瓦片坐标

坐标系定义

百度地图的瓦片坐标系定义与高德地图并不相同,其墨卡托投影的参数也不同。百度地图瓦片坐标以墨卡托投影地图中赤道与0º经线相交位置为原点,沿着赤道往左方向为X轴,沿着0º经线向上方向为Y轴。
百度瓦片坐标定义了另一种二维坐标系,称为百度平面坐标系。百度平面坐标系的坐标原点与百度瓦片坐标原点相同,以瓦片等级18级为基准,规定18级时百度平面坐标的一个单位等于屏幕上的一个像素。平面坐标与地图所展示的级别没有关系,也就是说在1级和18级下,同一个经纬度坐标的百度平面坐标都是一致的。

百度地图Level=2的瓦片坐标编号情况

此时X方向和Y方向各有4个瓦片编号,但是外围的某些瓦片只有部分区域有地图或完全没有地图。没有地图的区域也可以认为其瓦片是无效的,即百度地图中X方向或Y方向的有效瓦片不一定达到$2^{Level}$个。
中国大概位于百度瓦片坐标的(0,0)中。

坐标转换图解

百度地图坐标转换图解

从百度地图坐标转换图解中可以看出,百度地图的坐标转换具有以下特点:

  • 百度经纬度坐标与百度平面坐标可以直接相互转换,并且与瓦片地图等级无关。
  • 经纬度坐标需要先转换为平面坐标,然后才能在某一瓦片等级下转换为瓦片坐标和瓦片像素坐标。
  • 瓦片像素坐标需要结合其瓦片坐标才能得到该像素坐标的平面坐标,然后再转换为经纬度坐标。

坐标转换公式

方法参考:百度地图API详解之地图坐标系统
发现百度JavaScript API的一个bug:百度JavaScript API中经纬度坐标转瓦片坐标bug

  • 经纬度坐标(lng, lat)转平面坐标(pointX, pointY)
    百度经纬度坐标与百度平面坐标的相互转换,并没有公开的公式,需要通过百度地图的API实现。
    主要代码为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Bmap为百度JavaScript API V2.0的地图对象
    lnglatToPoint(longitude, latitude) {
    let projection = new BMap.MercatorProjection();
    let lnglat = new BMap.Point(longitude, latitude);
    let point = projection.lngLatToPoint(lnglat);
    return {
    pointX: point.x,
    pointY: point.y
    };
    }
  • 平面坐标(pointX, pointY)转经纬度坐标(lng, lat)
    也需要通过百度地图的API实现。
    主要代码为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    pointToLnglat(pointX, pointY) {
    let projection = new BMap.MercatorProjection();
    let point = new BMap.Pixel(pointX, pointY);
    let lnglat = projection.pointToLngLat(point);
    return {
    lng: lnglat.lng,
    lat: lnglat.lat
    };
    }
  • 平面坐标(pointX, pointY)转瓦片坐标(tileX, tileY)

$$tileX=\lfloor\frac{pointX\times2^{Level-18}}{256}\rfloor$$
$$tileY=\lfloor\frac{pointY\times2^{Level-18}}{256}\rfloor$$

  • 平面坐标(pointX, pointY)转像素坐标(pixelX, pixelY)

$$pixelX=\lfloor{pointX\times2^{Level-18}-\lfloor\frac{pointX\times2^{Level-18}}{256}\rfloor\times256}\rfloor$$
$$pixelY=\lfloor{pointY\times2^{Level-18}-{\lfloor\frac{pointY\times2^{Level-18}}{256}\rfloor\times256}}\rfloor$$

  • 瓦片(tileX, tileY)的像素坐标(pixelX, pixelY)转平面坐标(pointX, pointY)

$$pointX=\frac{tileX\times256+pixelX}{2^{Level-18}}$$
$$pointY=\frac{tileY\times256+pixelY}{2^{Level-18}}$$

  • 经纬度坐标与瓦片坐标、像素坐标的相互转换,以平面坐标为中间量进行转换。

吐槽

百度地图JavaScript的代码非常奇葩,非常迷惑:
经纬度类是Point,平面坐标类是Pixel
经纬度转平面坐标是lngLatToPoint,接收一个Point对象,返回一个Pixel对象。
平面坐标转经纬度坐标是在pointToLngLat,接收Pixel对象,返回一个Point对象。
WTF!

转换类库

本文笔者根据前文介绍的经纬度坐标与瓦片坐标、像素坐标相互转换规则,编写了一个JavaScript类库–tile-lnglat-transform,提供了高德地图、百度地图、谷歌地图的经纬度坐标与瓦片坐标的相互转换。该模块是使用了UMD模块打包方式,可以在node和broswer中使用。
类库地址:https://github.com/CntChen/tile-lnglat-transform
该类库的详细信息及使用方法请在项目主页中查看。

瓦片地图等级范围

  • 瓦片地图等级范围反映了地图可缩放的程度。
  • 虽然最小的瓦片等级是0,但是部分地图并不提供0级或其他较小瓦片等级的地图,因为此时的世界地图将会很小,不能铺满用户设备窗口。

经过实际测试,各地图服务商的瓦片等级和测试链接如下:

需注意的问题

  • 瓦片像素坐标的起始点
    • 高德地图、谷歌地图的瓦片坐标起点在左上角,像素坐标(pixelX, pixelY)在瓦片中的起点为左上角。
    • 百度地图中,像素坐标(pixelX, pixelY)的起点为左下角。

参考资料

瓦片地图服务

https://en.wikipedia.org/wiki/Tile_Map_Service

node-canvas实现百度地图个性化底图绘制

http://www.cnblogs.com/well1010/articles/baidu-map-node-canvas.html

tile-lnglat-transform

https://github.com/CntChen/tile-lnglat-transform

coordtransform

https://github.com/wandergis/coordtransform

地图投影的N种姿势

http://blog.sina.com.cn/s/blog_517eed9f0102w4rm.html

Web Mercator

https://en.wikipedia.org/wiki/Web_Mercator

Slippy map tilenames

http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames

百度地图API详解之地图坐标系统

http://www.cnblogs.com/jz1108/archive/2011/07/02/2095376.html

百度JavaScript API中经纬度坐标转瓦片坐标bug

http://cntchen.github.io/2016/05/09/百度JavaScirpt%20%20API中经纬度坐标转瓦片坐标bug/

高德地图层级

http://lbs.amap.com/api/javascript-api/reference/map/