0%

OpenLayers6瓦片地图加载

在之前的实例中设置图层的 source 属性时,可以发现经常用ol.source.OSM()ol.source.BingMaps()来创建图层的源。之所以加载源这么简单,是因为 OpenLayers 库中对 OSM(OpenStreetMap 地图)和 BingMap(必应地图)这类的国际比较常用的地图加载做了封装。但是 OpenLayers 不可能对所有的地图厂商都进行封装,这时候就需要自己手动拼接字符串来加载瓦片地图。绝大多数的瓦片地图都是用的大致相同的规律来加载的,这就使手动拼接字符串简单了许多。

LOD

介绍瓦片地图之前,首先应该了解 LODLevels of Detail),意思是多细节层次。这是一种常用的游戏优化技术,按照模型的位置和重要程度来决定渲染时的资源分配,把非重要物体的面数和细节度降低,在渲染时就能更加高效。

在地图中,LOD 的概念也很重要,比如观察国家层次的地图时,街道信息就是我们不想要了解的信息。而且如果街道信息全部加载,地图一定非常的辣眼睛,设备也不一定能带的动。所以在不同的高度观察地图时,所需要展示信息的细节类型和程度都应该是不同的。所以对于同一个地点来说,在鼠标滚轮放大缩小处在不同层级时,其实是在眼皮底下完成了图片替换。

为了便于理解 GIS 系统中“层级的不同就是图片不同”这个概念,使用 google 的在线瓦片地图来说明。在为最小层级 0 的情况下,只使用了 1256×256 像素的图片来表示整个地球平面:

放大一个层级,为 1 的时候,用了 4 张 256×256 像素的图片来表示整个地球:

4map

瓦片计算

原理

虽然瓦片地图其实是利用了 LOD 技术实现图片替换,但是由 google 地图的例子可以发现,示例的层级是 0 级和 1 级,所需要的图片数目是 14,也就是 2^02^2。如果层级越大,显示整个地球时所需要的256×256 像素的图片就越多,n 层的图片数为 2^2n,如下所示:

按照这个规律,如果层级为 10,那么显示全球时所需要 256×256 像素的图片数是 1048576,这个数目是非常大的,全部展示在浏览器是不现实的。但是不管你的屏幕分辨率是 1920×1080 还是 1024×768,一个浏览器的能同时显示的内容最大范围是固定的,所以只显示我们正在观察的这一张地图图片的附近若干张地图,就可以把屏幕全撑满,当移动或者放大缩小时,再加载当前中心位置附近的若干张,就实现了一个 WEB 地图。

日常中我们使用的网页地图,在大幅度移动或者缩放地图时,经常能够发现地图其实就是一张张加载出来,这种情况在网速不好的时候尤其明显,本质上就是新的地图图片正在加载。

瓦片坐标

当瓦片处于 n 层的时候,瓦片数目是 2^2nxy 方向上都被切割成了 2^n 个。所以对于任何一个层级,都可以对它的瓦片像一个矩阵一样进行编号。
在 OpenLayers 的 source 中,有一个可以用于调试瓦片的源 ol.source.TileDebug,结合 OSM 的地图源可以清晰的观察到瓦片的坐标,在地图网格中,z 是层级,x 是经度方向的瓦片位置,y 是纬度的方向的瓦片位置。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<div id="map" style="width: 100%;height: 100%;"></div>
<script type="text/javascript">
let osmSource = new ol.source.OSM();
let map = new ol.Map({
layers: [
// 添加Open Street Map地图图层
new ol.layer.Tile({
source: osmSource
}),
// 添加一个显示Open Street Map地图瓦片网格的图层
new ol.layer.Tile({
source: new ol.source.TileDebug({
projection: "EPSG:3857",
tileGrid: osmSource.getTileGrid()
})
})
],
target: "map",
view: new ol.View({
// 把经纬度转化为3857坐标系的坐标
center: ol.proj.transform(
[114.3177223, 30.528461],
"EPSG:4326",
"EPSG:3857"
),
zoom: 10
})
});
</script>

瓦片地图标准

在地图服务中,比较常用的两个标准如下:

WMTS

WMTS(Web Map Tile Service),是由OGC(Open Geospatial Consortium)制定的网页地图瓦片服务标准,标准中,原点在左上角(西北角),x 向右(东)为正方向,y 向下(南)为正方向。这也是 OpenLayers 中 ol.source.XYZ 使用的标准。

TMS

TMS(Tile Map Service),是由OSGeo(Open Source Geospatial Foundation)制定的地图瓦片服务标准,标准中,原点在左下角(西南角),x 向右(东)为正方向,y向上(北)为正方向。

自定义瓦片地图加载

ol.source.XYZ

国内常用的一些 WEB 地图并没有被 OpenLayer 官方封装,但是 OpenLayers 中有一个ol.source.XYZ对象,ol.source.XYZ 对象的 url属性 API 如下

URL template. Must include {x}, {y} or {-y}, and {z} placeholders. A {?-?} template pattern, for example subdomain{a-f}.domain.com, may be used instead of defining each one separately in the urls option.

翻译一下:

URL 模板。必须包含{x}, {y}{-y}, 和{z} 占位符。一个{?-?}模板模式,比如{a-f}.domain.com。可以直接返回 url 代替定义 urls 属性。

有了这个 url 模板字符串,就可以很轻松的自定义一个 url 模板来读取各厂家地图。

高德地图示例

比如常用的高德地图,关于高德地图的服务地址规则可以参考下面这篇简书博客《高德 WMTS 瓦片地图服务地图图源规律》,分析一个典型的高德地图瓦片请求:

https://wprd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x=54658&y=26799&z=16&scl=1&ltype=1

分析这个请求,protocolhttpshostnamewprd02.is.autonavi.compath 中的/appmaptile 代表瓦片,query 中参数效果如下:

变量 说明
hostname 目前还没有找出规律(webst、webrd、wpst、wprd)
lang 可以通过 zh_cn 设置中文,en 设置英文
style 地图类型控制,6(卫星图),7(简图),8(透明带道路详图)
scl 尺寸控制,1=256,2=512
ltype 线性控制,增加后,只对地图要素进行控制,没有文字注记,要素多少,是否透明

知道了上述参数后,就可以很方便的来构造高德地图的 URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<div id="map" style="width: 100%;height: 100%;"></div>
<script>
// 高德地图层
let aMapLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url:
"http://webrd0{1-4}.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&size=1&scale=1&style=7"
})
});

// 创建地图
let map = new ol.Map({
layers: [
aMapLayer,
// 添加一个显示高德地图瓦片网格的图层
new ol.layer.Tile({
source: new ol.source.TileDebug({
projection: "EPSG:3857",
tileGrid: aMapSource.getTileGrid()
})
})
],
view: new ol.View({
// 设置武汉为地图中心
center: [114.3177223, 30.528461],
projection: "EPSG:4326",
zoom: 10
}),
target: "map"
});
</script>

形成的地图如下所示:

ol.source.TileImage

并不是所有的厂家的地图像 OpenLayers 或者高德地图那样按照 WMTS 标准,比如百度地图的坐标系,原点在地图中心位置,向右为 x 正方向,向上为 y 正方向,而且分辨率也和 OpenLayers 的分辨率不同,此时就需要更基础的ol.source.TileImage来加载地图。主要需要的两个属性是tileGrid(瓦片网格)和tileUrlFunction(瓦片 URL 函数)。

百度地图示例

百度地图坐标系和 Openlayers 默认坐标系的主要区别是:

  • 原点:百度坐标系原点在地图中心,OpenLayers 默认坐标系在地图左上角(西北角);
  • y 方向:百度地图向上(北)为正方向,OpenLayers 默认坐标系向(下)南为正方向;
  • 网格分辨率:百度地图每一层级网格的分辨率与 OpenLayers 默认不同;
  • 投影格式:百度地图投影格式为 BD-MC,OpenLayers 默认为 EPSG:3857

首先看百度地图瓦片链接,这个链接是百度地图开发者平台的Javascript API,里面有一个地图展示的 demo。如下图所示,打开控制台Network,筛选请求为img,然后随便在地图上拖动或者放大缩小,就会发现左下方列表里请求了新的瓦片图片。

百度地图瓦片

http://maponline1.bdimg.com/tile/?qt=vtile&x=787&y=294&z=12&styles=pl&scaler=1&udt=20200211

分析这个请求,protocolhttphostnamemaponline1.bdimg.compath 中的/tile 代表瓦片,query 是最关键的,x=787&y=294&z=12 说明 x>y>z 就是百度瓦片地图的模板字符串顺序。

加载百度地图代码如下:

  • 原点:在 ol.tilegrid.TileGrid 设置 origin 坐标为[0, 0],也就是地图中央。
  • 分辨率:根据百度地图网格分辨率规律递归计算出了一个分辨率数组,赋给了 ol.tilegrid.TileGridresolutions
  • 投影方式:百度地图使用的投影格式为BD-MC,使用 proj4 定义了 BD-MC 的投影,并 ol.proj.proj4.register(proj4)注册给了 OpenLayers。
  • xyz:在瓦片 URL 返回函数 tileUrlFunction 中,把y 坐标+1并加一个负号;再把所有为负数的 xy负号换为 M
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<div id="map" style="height: 100%;width: 100%;"></div>
<script>
proj4.defs("EPSG:4008", "+proj=longlat +ellps=clrk66 +no_defs");
proj4.defs("BD-MC", "+proj=merc +lon_0=0 +units=m +ellps=clrk66 +no_defs");
ol.proj.proj4.register(proj4);
// 自定义分辨率和瓦片坐标系
let resolutions = [];
let maxZoom = 18;
// 计算百度使用的分辨率
for (let i = 0; i <= maxZoom; i++) {
resolutions[i] = Math.pow(2, maxZoom - i);
}
let tilegrid = new ol.tilegrid.TileGrid({
origin: [0, 0], // 设置原点坐标
resolutions: resolutions // 设置分辨率
});
// 创建百度地图的数据源
let baiduSource = new ol.source.TileImage({
projection: "BD-MC",
tileGrid: tilegrid,
tileUrlFunction: function(tileCoord, pixelRatio, proj) {
let z = tileCoord[0];
let x = tileCoord[1];
let y = tileCoord[2] + 1;
// 百度瓦片服务url将负数使用M前缀来标识
if (x < 0) {
x = "M" + -x;
}
y = -y;
if (y < 0) {
y = "M" + -y;
}
let num = Math.ceil(Math.random() * 3);
return (
"http://maponline" +
num +
".bdimg.com//onlinelabel/?qt=vtile&x=" +
x +
"&y=" +
y +
"&z=" +
z +
"&styles=pl&udt=20200211&scaler=1&p=0"
);
}
});
// 百度地图层
let baiduMapLayer = new ol.layer.Tile({
source: baiduSource
});
// 创建地图
new ol.Map({
layers: [baiduMapLayer],
view: new ol.View({
// 设置武汉为地图中心
center: ol.proj.transform([114.3177223, 30.528461], "EPSG:4326", "BD-MC"),
zoom: 10
}),
target: "map"
});
</script>

需要解密的瓦片地图

谷歌地图示例

首先说明因为地图服务商也是要恰饭的,所以建议大家最遵守版权和数据申明,通过申请开发者 API 合理合法的方式使用。

下面分析 google 加密后的地图 url 链接,加载 google 瓦片地图,通过调试工具拿到 url 如下:

https://www.google.com/maps/vt/pb=!1m4!1m3!1i8!2i211!3i105!2m3!1e0!2sm!3i345013117!3m8!2szh-CN!3scn!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0

https://www.google.com/maps/vt/pb=!1m4!1m3!1i8!2i212!3i106!2m3!1e0!2sm!3i345013117!3m8!2szh-CN!3scn!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0

很明显,两个瓦片 url 区别是 !2i 后的 211212 以及 !3i 后的 105106,这就是 xy 的位置。再缩放一下地图,就能发现 !1i后的 8 就是层级 z,所以就可以构造 url,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="map" style="height: 100%;width: 100%;"></div>
<script>
// google地图层
let googleMapLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url:
"http://www.google.com/maps/vt/pb=!1m4!1m3!1i{z}!2i{x}!3i{y}!2m3!1e0!2sm!3i345013117!3m8!2szh-CN!3scn!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0"
})
});
// 创建地图
let map = new ol.Map({
layers: [googleMapLayer],
view: new ol.View({
// 设置武汉为地图中心
center: [114.3177223, 30.528461],
projection: "EPSG:4326",
zoom: 10
}),
target: "map"
});
</script>
👆 全文结束,棒槌时间到 👇