前言

介绍地图之前我们首先了解它的基本组成。相信大家都了解拼图游戏的原理,把一张大图切割成若干小块,然后按照一定的规则拼接起来。地图的原理也是一样。我搜罗的目前移动端比较常见的绘制地图的方法,与大家分享。

背景

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

主要经纬度坐标系

地球本身是个椭球体, 要把它以平面的方式绘制在一个矩形上也真的不好办, 现在有不少绘制的方式但是都有各自的优缺点, 感兴趣的朋友可以查查看具体的细节, 我这里就简单介绍下比较常见的方式;

  1. 经纬度EPSG:4326
    也就是地图的默认坐标;现在球体上定义好经纬度, 然后在正方形纸上画出刻度, 对应的绘制;

  2. 墨卡托投影(EPSG:3785 )
    把地球放在一个圆筒里面, 假设地球内部有个光源, 那么地球在圆柱上的投影就是地图

  3. WGS84坐标
    WGS-84 坐标是一个国际的标准,一般卫星导航,原始的 GPS 设备中的数据都是采用这一坐标系。国外的 GoogleMap、OpenStreetMap、MapBox、OpenLayer 等采用的都是这一坐标。

  4. 火星坐标系
    火星坐标是国家测绘局为了国家安全在原始坐标的基础上(国际标准的经纬度坐标是WGS84)进行偏移得到的坐标,基本国内的电子地图、导航设备都是采用的这一坐标系或在这一坐标的基础上进行二次加密得到的。
    火星坐标的真实名称应该是 GCJ-02 坐标,基本上所有的国内的电子地图采用的都是火星坐标系甚至 Google 地图中国部分都特意为中国政府做了偏移。

  5. 百度坐标
    由于国内的电子地图都要至少使用火星坐标进行一次加密,百度直接就任性一些,直接自己又研究了一套加密算法,来了个 二次加密,这就是我们所熟知的百度坐标 BD-09,当然只有百度地图使用的是百度坐标。

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

WEB墨卡托投影介绍

墨卡托投影是正轴等角圆柱投影,由荷兰地图学家墨卡托于1596年创立。其投影方式为:假想一个与地轴方向一致的圆柱切或割于地球,按等角条件,将经纬网投影到圆柱上,按圆柱面展开为平面后,即得本投影。WEB墨卡托辅助球投影(EPSG: 3857)是将球面墨卡托投影公式运用于椭球面坐标的投影计算,为计算简单和实现方便,WEB墨卡托投影假设地球为球体,其半径取为WGS84椭球的长半轴半径6,378,137.0米,这样理论上精度可以控制在0.33%之内。该投影坐标原点位于赤道和本初子午线的交点,经线和经线之间相互平行且间隔相等,对应的经度±180度其范围是±20,037,508.34米。其投影纬线和纬线之间也相互平行,间隔从赤道开始向两级逐渐增大,为便于计算机处理,提高显示效率,取地球纬度范围也是±20,037,508.34米,使地图呈正方形,经过反算可得其对应的纬度范围是±85.05度 。

WEB墨卡托投影在2005年发布的谷歌地图中首次使用,考虑到相互之间的平台兼容,随后跟进的微软BING MAPS、在线地图服务公司的MAPQUEST MAPS、雅虎公司的YAHOO MAPS均采用了谷歌地图的WEB墨卡托地图投影方式。

瓦片切割和瓦片坐标

用户通过浏览器使用谷歌地图时,看到的是一张铺面整个窗口的地图图片。但是实际上,这张大的地图图片是由多个尺寸相同(通常是256 × 256像素)的小图片按照既定规则无缝拼接而成的。这些小图片就是栅格瓦片(简称瓦片),瓦片按照如图2所示的金字塔结构组织,每张瓦片都可以通过细节等级LOD级别、列号、行号三个要素唯一标记。瓦片地图金子塔模型是一种多分辨率层次模型,从瓦片金字塔的顶层开始,分辨率越来越高,瓦片数量越来越多,但每个层次表示的地理范围不变,例如当LOD级别为0时,整个地图就只有一个256 × 256像素的瓦片组成,当LOD级别为1时,地图分裂为4个瓦片,这4个瓦片排列成两列两行,依此类推,每放大一倍,每一块小瓦片都分裂为四块。因此当缩放等级为n时,地图分裂为2n × 2n个瓦片,这些瓦片排列成2n行2n列,按照从左到右,从上到下的顺序给瓦片编号,即可以通过瓦片LOD级别、列号、行号定位瓦片。目前基本上所有的在线栅格地图均采用了瓦片地图的存储方式 。

*** 地图瓦片具有以下特点:**

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

瓦片坐标和经纬度坐标之间存在关系,而这层关系是通过后端接口加密处理并且一一映射后的,有自己的计算公式

  • 坐标转换图解
    示例

瓦片地图等级范围

瓦片地图等级范围反映了地图可缩放的程度。
虽然最小的瓦片等级是0,但是部分地图并不提供0级或其他较小瓦片等级的地图,因为此时的世界地图将会很小,不能铺满用户设备窗口。
经过实际测试,各地图服务商的瓦片等级和测试链接如下:

下图是百度地图的一个瓦片图片展示:

示例

  • 瓦片等级由2的次方来划分的,即1到4,4到16,16到
    如图示意:
    示例

需要注意的问题

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

地图控件设计和实现

离线的、实时的二维电子地图需要首先获取地图的栅格瓦片数据,然后按照地图的投影方式和瓦片的组织规则,在OpenGL图形渲染环境中根据用户交互信息读取和渲染瓦片,以多个瓦片进行无缝拼接成为用户可见的电子地图。二维电子地图从结构组成上需要包括用户交互处理模块、栅格瓦片读取模块、地图渲染绘制模块以及为进行瓦片数据处理的瓦片处理工具。

  1. 交互处理模块负责响应用户的地图平移、缩放、测距等交互操作,这些操作将被封装成地图显示信息(地图显示范围、LOD等级等)用于驱动地图渲染模块进行地图的更新显示;

  2. 地图渲染绘制模块首先根据当前地图显示范围、LOD等级等信息计算需要加载显示的瓦片单元,并且将瓦片单元加入到一个瓦片队列用于驱动栅格瓦片读取模块加载栅格瓦片,然后从带索引标识的加载完毕的瓦片集合中获取瓦片信息并以OpenGL图形渲染技术绘制和显示地图瓦片。由于实时性要求,地图渲染绘制模块工作在一个单独的渲染线程中;

  3. 栅格瓦片读取模块依次读取需要加载的瓦片单元信息,从地图数据包中读取对应的瓦片数据,然后将结果加入到已加载瓦片集合供地图渲染绘制模块使用。为避免读取仿真数据拖慢地图渲染绘制的速度,瓦片读取模块也工作在一个独立的线程中;

  4. 瓦片处理工具功能是将根据指定的地图覆盖范围、细节等级LOD级别范围等参数,对分散在不同目录下的地图栅格瓦片进行处理,最终生成一个单一文件的地图数据包。

地图数据包的文件格式

通过地图批量下载工具下载的电子地图瓦片是JPG或PNG格式的图片,其中第一级目录表示瓦片的LOD等级,第二级目录名称表示瓦片所在的列号,第三级文件名称是瓦片所在的行号,由于瓦片文件分散在不同的目录下,在渲染和绘制时如果用户进行了缩放、平移等操作,则程序将需要频繁的进行瓦片数据加载和卸载,如果每次加载瓦片都需要进行一次比较耗时的文件打开或关闭操作,则会影响整个地图控件渲染绘制和交互响应的实时性。

为此可以将所有的地图瓦片打包到一个文件中,形成一个单一文件的地图数据包,然后在地图控件启动时打开地图数据包文件,之后每次需要加载瓦片时都从地图数据包中读取,就可以大大加快地图瓦片加载的速度,提高地图交互响应的实时性。根据需求,地图数据包的文件格式由三个部分组成,分别是文件头、瓦片数据和索引数据。

  1. 文件头:包括地图数据包的文件版本、瓦片数据缩放范围(即对应地图瓦片的LOD级别的最小值和最大值)、瓦片索引数据起始位置在地图数据包文件中的偏移量、瓦片索引数据的长度以及地图边界(分别用经度和纬度的最小值、最大值表示)。

  2. 瓦片数据:即将一定覆盖范围和LOD级别内的所有栅格瓦片文件依次集中存放的地图瓦片数据,其存储方法为:依次读取代表地图瓦片的JPG或PNG格式图片文件的内容,然后依次写入到地图数据包的瓦片数据块中。

  3. 索引数据:为加载瓦片时快速定位瓦片地址而设计,其内容包括两部分:瓦片ID和瓦片信息,瓦片ID的内容包括瓦片的LOD级别、列号、行号、类型(普通地图、卫星地图或者地形图),瓦片信息的内容包括瓦片数据在地图数据包中的偏移量和长度。

通过上述定义,控件加载在地图数据包时,可以首先根据文件头中的索引数据偏移量确定在索引数据在地图数据包文件中位置,然后依次读取索引数据并将其存储起来作为加载地图瓦片的索引。

栅格瓦片读取

栅格瓦片数据经过瓦片处理工具的处理,分散的瓦片地图数据集被打包成单一的地图数据包文件,当二维电子地图控件启动时,栅格瓦片读取线程随之启动。之后在该线程的每一次循环中程序都按照以下流程进行处理,直至线程随二维电子地图控件关闭而结束:

  1. 在线程循环开始时,首先检查需要加载的瓦片队列是否为空,如果为空,则延迟1毫秒后重新进入线程循环,否则进行步骤2。

  2. 从队列中取出一个瓦片,然后依据瓦片标识(包括LOD级别、列号、行号)等信息在索引数据中二分法查找瓦片信息,然后根据瓦片数据在地图数据包中的偏移量和长度,从地图数据包文件中加载指定的瓦片数据。

  3. 瓦片数据加载完毕后,将瓦片数据(即栅格图像数据)添加到瓦片集合供地图渲染绘制模块使用,然后再次跳转到步骤1,直至需要加载的瓦片队列处理完毕。

地图渲染绘制

为保证实时性,地图的渲染绘制也在一个独立的渲染线程中完成,渲染线程的主体是一个渲染循环,在每一次循环中程序均要完成绘制准备、瓦片绘制、瓦片清理等工作:

1. 绘制准备

在绘制准备阶段,首先需要获取控件客户区的大小(即地图显示区域的大小)、当前的LOD级别、窗口中心对应的地图位置等信息,然后计算得到需要参与绘制的瓦片数组。

  1. 首先,已知当前LOD级别n,可以计算瓦片在WEB墨卡托投影坐标下的瓦片的宽度和高度均为T = 2 * 20,037,508.34/2n,由于瓦片像素尺寸为256 × 256,所以可以计算每个像素对应的WEB墨卡托投影坐标下的宽高为L = T/256。

  2. 然后,根据窗口中心的WEB墨卡托投影坐标(lon,lat)和控件客户区域的宽度W和高度H,可以计算客户区左上角和右下角对应的地图WEB墨卡托投影下的坐标分别为(wmX_LT, wmY_LT)和(wmX_BR, wmY_BR),计算代码如下:

double wmX_LT=lon-W*L/2;

double wmY_LT=lat+H*L/2;

double wmX_BR=lon+W*L/2;

double wmY_BR=lat-H*L/2。

最后,分别计算客户区左上角和右下角对应的瓦片列号、行号,部分计算代码如下:

int C1=(int)floor((20037508.34+wmX_LT)/T);

int L1=(int)floor((20037508.34-wmY_LT)/T);

int C2=(int)ceil((20037508.34+wmX_BR)/T);

int L2=(int)ceil((20037508.34-wmY_BR)/T)。

计算完毕后,所有处于C1到C2列、L1到L2行中间的瓦片都需要参与绘制。

2. 瓦片绘制
在瓦片绘制阶段,需要依次遍历步骤1计算所得的需要参与显示绘制的瓦片数组,然后对每个瓦片按如下步骤进行绘制显:

  1. 计算瓦片在OpenGL中绘制显示必需的位置信息。

  2. 根据瓦片标识从瓦片集合中查找瓦片,如果查找成功,则继续步骤3,否则跳转到步骤4。

  3. 根据步骤2查找所得的瓦片像素数据,生成并绑定对应的纹理,然后使用OpenGL绘制在指定位置绘制瓦片,然后跳转到步骤1继续遍历。

  4. 将瓦片标识添加到需要加载的瓦片队列,驱动瓦片加载线程进行瓦片数据加载。然后跳转到步骤1继续遍历。

3. 瓦片清理

在瓦片清理阶段,程序需要再次遍历瓦片集合,检查瓦片集合中的瓦片是否需要参与当前的显示绘制(即判断瓦片是否在需要参与显示绘制的瓦片数组中),如果瓦片不需要参与显示绘制,则释放瓦片纹理和图像数据,并从集合中删除瓦片。

百度地图技术介绍

简单介绍一个地图的底图的相关知识吧,我们眼中看到的丰富的地图信息,其中组成地图的主要元素,莫过于地图的一张张底图瓦片了。如下图所示,

一般我们打开了一个地图,其实际上可能会像上图一样,由一堆瓦片组成。1,2,3,4表示不同的瓦片。 而某一张瓦片如下图所示
在线地址:http://developer.baidu.com/map/custom/

百度地图底图绘制技术现状

要想绘制上面所示的底图,目前现在主要有两类技术

  • 栅格: 也就是传意义的图片技术,在server端把图片画好。浏览器使用node-canvas实现百度地图个性化底图绘制标签拼出来

  • 矢量:在浏览器使用canvas技术,将矢量的数据,在浏览器完成渲染。它最大的问题在于:只支持高端浏览器

百度地图,目前两种技术都已经实现,如果大家使用的是mapapi,在高端浏览器下打开,你会发现,他是使用canvas绘制的。
其中的地图底图在IE678等浏览器下,就是使用node-canvas在后端绘制出来的,获取当前绘制经纬度,之后通过接口调用瓦片,
经纬度坐标与瓦片坐标、像素坐标的相互转换,以平面坐标为中间量进行转换。主要代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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
};
}

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
};
}

http://developer.baidu.com/map/jsdemo.htm#a1_2

有兴趣可以使用不同的浏览器打开看看就可以看出来。

如图所示

示例

canvas绘制图片的优点

  • 1.它允许我们使用canvas的语法和接口写成的js代码,放在server跑。
  • 2.减少大量的DOM操作可以大大提升性能。

如介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Canvas = require('canvas')
, canvas = new Canvas(200,200)
, ctx = canvas.getContext('2d');

ctx.font = '30px Impact';
ctx.rotate(.1);
ctx.fillText("Awesome!", 50, 100);

var te = ctx.measureText('Awesome!');
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
ctx.beginPath();
ctx.lineTo(50, 102);
ctx.lineTo(50 + te.width, 102);
ctx.stroke();

console.log('node-canvas实现百度地图个性化底图绘制');

目前使用canvas的原因有以下几点:

  1. 百度地图已经很好的实现使用canvas技术在浏览器完成渲染。

  并有不错的展现效果和性能,在移动端体验更好。因为矢量的数据比请求图片的体积要小的多

  1. 在canvas的方案下,已经实现个性化底图的绘制效果

  底图绘制由样式+矢量数据组成。只要修改替换样式文件,就可以实现个性化地图的渲染。

  1. 低端浏览器如IE6-8等浏览器,是不支持canvas功能的。

  展现地图底图,必须使用栅格图实现。需要有后端技术来生成底图

  1. 由于不样的样式要求得到不同的底图。就需要图片是实时绘制的,而且要求性能必须好

在线地址:http://api.map.baidu.com/customimage/tile?x=788&y=293&z=12&customid=midnight
可以看的出来,速度是很快的。
示例
(9个瓦片生成的大图)

参考资料

  1. 瓦片地图服务
  2. node-canvas实现百度地图个性化底图绘制
  3. 地图投影的N种姿势
  4. 百度地图API详解之地图坐标系统
  5. 高德地图层级