瓦片地图

前言

前一段时间在写项目的时候,使用了一个轻量级的可视化库,专用于打造一个交互性良好的地图。下面放一个使用小栗子。

【小栗子】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--引入leaflet.js和css文件 -->
<link rel="stylesheet" type="text/css" href="assets/css/leaflet.css" />
<script type="text/javascript" src="assets/js/leaflet.js"></script>
<!--设置一个div对象用于承载地图 -->
<div id="map" class="leaflet-map" style="width:450px;height:300px"></div>
<script>
var map = L.map('map').setView([51.505, -0.09], 3);
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
// attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
L.marker([51.505, -0.09]).addTo(map)
.bindPopup('A pretty CSS3 popup.<br> Easily customizable.')
.openPopup();
</script>

但是今天并不是分享这个可视化库,而是里面所采用到的一个技术之一————瓦片地图


瓦片地图概念

地图瓦片地址栗子,以google为例:
enter description here

现在就是要将一张张这类的地图瓦片,在客户端拼接成一幅完整的地图。
其中每一片的瓦片大小为:256像素x256像素

瓦片地图金字塔模型

Q:为什么出现瓦片地图金字塔模型这个概念

现在,我假设我们的服务器上有一个1G的影像,需要将其在前端进行显示。我们传统的做法就是首先将服务器中的1G影像下载到前端,然后浏览器加载渲染出图。但是大家想想,首先客户端下载1G的影像需要的时间一定是个漫长的过程,其次浏览器加载这么大的文件也多半会导致其崩溃。而最重要的一个问题是,我们的需求仅仅是浏览全图中的某一个区域下的某几个级别,现在却将全图下载完毕了,而这同样还导致了数据的不安全性(下载到本地),同时我们的每一次放大和缩小以及拖拽都将会使浏览器花上足够长的时间去渲染。

可见,传统的方式是不符合实际需求的。到后来,又有了新的解决方法,比如arcgis的IMS版本中提出了动态出图的概念。也就是当前端发出的请求里包含了需要显示的范围、显示窗口的大小等参数后,后台动态的在原始数据中切出一个符合需求的瓦片,然后将这个数据返回给前台,并且在服务器中对这个瓦片做缓存。

但是,这个方法前端出图依旧很慢,并且使地图服务器的压力过大。终于,我们的瓦片金字塔模型解决方案出现了。

瓦片地图金字塔模型是一种多分辨率层次模型,从瓦片金字塔的底层到顶层,分辨率越来越低,但表示的地理范围不变。

enter description here

地图切图方式:
一幅地图由4^n个256256的正方形组成,n为级别
例如:第0级为4^0个,即世界地图由一个256
256图片表示。
第1级世界地图应由4^1(4)个256图片组成,也就是将世界地图(上一级的单个图片)等分成4块256*256图片。
往下每一级依此类推……

url中关键参数

参数 描述
mt2.google.cn Google瓦片服务服务器,可以尝试mt1.google.cn依然有效。Google提供多台瓦片服务器,减轻服务器负载,提高网络访问效率。
x 瓦片的横向索引,起始位置为最左边,数值为0,向右+1递增。
y 瓦片的纵向索引,起始位置为最上面,数值为0,向下+1递增。
z 地图的级别,以Google为例,最上一级为0,向下依次递增。

比如说,1级下面,有两行和两列。这四张图片的地址如下:
http://mt2.google.cn/vt/x=0&y=0&z=1
http://mt2.google.cn/vt/x=0&y=0&z=1
http://mt2.google.cn/vt/x=0&y=1&z=1
enter description here
http://mt2.google.cn/vt/x=1&y=0&z=1
enter description here
http://mt2.google.cn/vt/x=1&y=1&z=1
enter description here
如果把这四张图片合在一起的话,就变成如下的图,这就是一个世界的地图了。
enter description here

涉及的一些概念与变量

1)地图投影方式: Web墨卡托——互联网地图通用的地图投影方式。

Q1:什么是墨卡托投影?

墨卡托(Mercator)投影,又名“等角正轴圆柱投影”,荷兰地图学家墨卡托(Mercator)在1569年拟定,假设地球被围在一个中空的圆柱里,其赤道与圆柱相接触,然后再假想地球中心有一盏灯,把球面上的图形投影到圆柱体上,再把圆柱体展开,这就是一幅标准纬线为零度(即赤道)的“墨卡托投影”绘制出的世界地图。
enter description here

Q2:Google们为什么选择墨卡托投影?

墨卡托投影的“等角”特性,保证了对象的形状的不变行,正方形的物体投影后不会变为长方形。“等角”也保证了方向和相互位置的正确性,因此在航海和航空中常常应用,而Google们在计算人们查询地物的方向时不会出错。

墨卡托投影的“圆柱”特性,保证了南北(纬线)和东西(经线)都是平行直线,并且相互垂直。而且经线间隔是相同的,纬线间隔从标准纬线(此处是赤道,也可能是其他纬线)向两级逐渐增大。
但是,“等角”不可避免的带来的面积的巨大变形,特别是两极地区,明显的如格陵兰岛比实际面积扩大了N倍。

2)Bounds(地图范围): [ -20037508.3427892, -20037508.3427892, 20037508.3427892,20037508.3427892],单位为米,20037508.3427892表示地图周长的一半,以地图中心点做为(0,0)坐标。
3)Levels: 地图的级别,例如:0……22。
4)Resolutions: 分辨率数组,与级别相对应,即一个级别对应一个分辨率,分辨率表示当前级别下单个像素代表的地理长度,即一像素代表多少米。Resolutions[n] = 20037508.3427892 2 / 256 / (2^n)
1.png
所以,低分辨率下面,显示洲名称和海洋名称。中分辨率下面,显示省名。高分辨率下面,就显示POI信息(每个POI包含四方面信息,名称、类别、经度、纬度,一个POI可以是一栋房子、一个商铺、一个邮筒、一个公交站等)
5)Center: 地图显示中心点。
*6)viewSize:
地图控件窗口的大小。

enter description here

瓦片地图相关算法

根据已知地图中心点、显示级别可以将地图显示范围计算出来:
viewBounds = [Center.x - Resolutions[l]viewSize.width/2, Center.y - Resolutions[l]viewSize.height/2, Center.x + Resolutions[l].viewSize.width/w, Center.y + Resolutions[l].viewSize.height/h]   

拼图算法剖析:

计算瓦片url

要想出图就发须知道地图控件可视范围起始点瓦片索引、末尾瓦片索引,中间区域的瓦片索引循环遍历即可得出。
下面看看如果计算出起始点、末尾瓦片url索引:

已知:

  • l(缩放级别)
  • bounds(地图范围——[ -20037508.3427892, -20037508.3427892, 20037508.3427892, 20037508.3427892])
  • viewBounds(地图控件可视范围)
  • 分辨率(Resolutions[l])
  • 瓦片像素宽高(256)。

未知:

  • startX(视图起始瓦片X方向索引)
  • startY(视图起始瓦片Y方向索引)
  • endX(视图未尾瓦片x方向索引)
  • endY(视图未尾瓦片y方向索引)。

求解:

startX = floor(((viewBounds.leftTop.x - bounds.leftTop.x) / Resolutions[l]) / 256);
startY = floor(((viewBounds.leftTop.y - bounds.leftTop.y) / Resolutions[l]) / 256);
endX = floor(((viewBounds.rightBottom.x - bounds.rightBottom.x) / Resolutions[l]) / 256);
endY = floor(((viewBounds.rightBottom.y - bounds.rightBottom.y) / Resolutions[l]) / 256);
firstTileUrl(起始瓦片Url) = http://**********?x=startX&y=startY&z=l;
endTileUrl(末尾瓦片Url) = http://**********?x=endX&y=startY&z=l;

中间部分的url循环遍历即可得出。
获得了所有瓦片的url,之后就是拼接瓦片的问题。

计算瓦片放在地图控件上的位置

分析:
其实只要将起始位置的瓦片像素位置算出来就可以了,由于瓦片像素大小为256,后面的各瓦片位置也就明了了。所以这里只探讨一下起始瓦片的像素位置。

已知:

  • startX(视图起始瓦片X方向索引)
  • startY(视图起始瓦片Y方向索引)
  • 分辨率(Resolutions[l])
  • 瓦片像素宽高(256)
  • bounds(地图范围)
  • viewBounds(地图控件可视范围)

未知:

  • startTileX(起始瓦片左上角X方向地理坐标)
  • startTileY(起始瓦片左上角Y方向地理坐标)
  • distanceX(瓦片左边与地图控件左边相距的像素距离)
  • distanceY(瓦片上边与地图控件上边相距的像素距离)。

求解:

startTileX = bounds.leftTop.x + (startX * 256 * Resolutions[l]);
startTileY = bounds.rightBottom.y - (startY * 256 * Resolutions[l]);
distanceX = (viewBounds.leftTop.x - startTileX) / Resolutions[l];
distanceY = (startTileY - viewBounds.rightBottom.y) / Resolutions[l]

公式不是最简,以方便理解,相信看官此时已经知道起始瓦片在地图控件中的摆放位置了——设地图控件起始像素位置为(0,0),那么此瓦片的像素的位置就是(-distanceX、-distanceY)。其它瓦片依据256像素宽高的关系依次而出。

到此已经算出了各瓦片的url以及它们应该摆放的位置,准备工作已完成,直接帖图即可完成出图工作。


参考资料