目标:
为了应对会场展馆内可能出现的无网络情况,在不更换使用技术栈的情况下,实现地图的离线加载。
现状:
接手当前的前端项目,使用的前端框架为 Vue,地图展示使用的组件为 Vue-Leaflet
,以下是相关代码:
1
2
3
4
<l-tile-layer
:url="tileProvider.url"
layer-type="base"
/></>
1
2
3
4
5
6
tileProviders: [
{
name: "test",
url: 'https://api.mapbox.com/styles/v1/zhangy123/ckkessgis1ktq17oad1yky6sf/tiles/512/{z}/{x}/{y}?access_token=pk.eyJ1Ijoiemhhbmd5MTIzIiwiYSI6ImNra2RyMGprczA1amsybnF0NmZmMjZ0MXcifQ.cFSvfTNV6AQKP4g2FmTL7w'
},
],
这里使用了在线加载地图,使用的是通过 Mapbox studio
自定义后的地图,这里我们的 url
设置为对应的接口地址,每次向 https://api.mapbox.com
发送请求获取对应地图。
尝试一(Mapbox、高德地图、百度地图)
首先,研究当前使用的 mapbox
可否支持离线加载功能。
Mapbox
的官方文档中写:使用Android或iOS的Mapbox Maps SDK构建的可以下载选定区域的地图,以在设备缺乏网络连接时使用。在 web
端无法做到离线地图加载。
高德地图、百度地图离线地图支持
再看看国内的百度地图与高德地图是否支持离线地图加载。
高德地图 | 百度地图 | |
---|---|---|
web 离线 | 不支持 | 不支持 |
Android 离线 | 支持 | 支持 |
iOS 离线 | 支持 | 支持 |
参考:
结论:目前调查的几个地图服务提供商所支持的离线地图加载都是建立在 Android SDK
和 iOS SDK
的基础上,对于 web 应用的离线支持目前没有,原因我认为是离线地图涉及庞大的静态文件,如果直接放在前端项目中会造成加载缓慢。
尝试二(Bigemap)
偶然发现国内有家地图厂商可以提供离线地图加载功能,可以在 Bigemap 官网看到相对的使用教程:
该软件确实可以完美实现地图的获取+运行本地地图服务,且可以自定义地图样式,但有一个问题是该软件不是免费软件,在实现基本的离线地图展示下,两款软件(地图下载器、离线地图服务器)的总价大约在 7000+ 人民币,在预算不充足的情况下这种解决方案不予考虑。
尝试三(瓦片地图)
首先科普一下什么是瓦片地图?
地球是一个椭球,使用 Web墨卡托投影(又称球体墨卡托投影,是墨卡托投影的变种)地球就变为平面的一张地图。
对这张地图进行等级切分。在最高级(zoom=0),需要的信息最少,只需保留最重要的宏观信息,因此用一张256x256像素的图片表示即可;在下一级(zoom=1),信息量变多,用一张512x512像素的图片表示;以此类推,级别越低的像素越高,下一级的像素是当前级的4倍。这样从最高层级往下到最低层级就形成了一个金字塔坐标体系。
我们使用的 Mapbox
在瓦片编号规则上与 Google Map
一致,Z 表示缩放层级,Z = zoom
;XY 的原点在左上角,X 从左向右,Y 从上向下,可以参考下图:
其实 Mapbox 实现的在线加载是同样的道理,Vue-Leaflet
负责将需要的层级、横纵坐标提取出来,并向我们给定的地址发送请求。在 Chrome 的调试工具中可以看出即使是在线地图,资源也是按照瓦片一块一块加载的:
最重要的一点是 Mapbox
返回地图没有做防爬虫处理直接是 png
格式,我们可以将所需层级所有的瓦片地图爬取下来。
实现思路
在 Vue-Leaflet
的控制下,我们将地图以 /z/x/y.png
的格式保存下来即可,然后在本地启动一个 node
服务,让 Vue-Leaflet
的 url
绑定到我们启动的本地服务地址即可。
实现步骤
① 创建地图
登录 Mapbox Studio,这里需要注册一个账号,然后创建新地图:
Mapbox Studio
的功能十分强大,可定制性十分高,包括不限于国际化、图例定制、自定义显示等。
定制好地图后可以点击右上角 Publish
发布,这时会拿到一个链接,这也就是我们可以绑定到 Vue-Leaflet
的 url
中实现在线地图展示的链接。
可以仔细看下这个链接,分为地址 + token:
1
https://api.mapbox.com/styles/v1/zhangy123/ckkessgis1ktq17oad1yky6sf.html?fresh=true&title=view&access_token=pk.eyJ1Ijoiemhhbmd5MTIzIiwiYSI6ImNra2RyMGprczA1amsybnF0NmZmMjZ0MXcifQ.cFSvfTNV6AQKP4g2FmTL7w
② 爬取地图
这里我们用 node.js
写了一个简单爬虫:
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
// getMap.js
const fs = require('fs'); //引入文件读取模块
const request = require('request');
// ================== Config =========================================
// 图片保存到的地址
const dir = `assets/map`
// 爬取层级, 每级的横纵块数
const level = 6
// 地图配置相关
let imgUrlRoot = `https://api.mapbox.com/styles/v1/meroychen/ckiod6ao60b0p17sg086wtnb4/tiles/512`
const token = `pk.eyJ1IjoibWVyb3ljaGVuIiwiYSI6ImNraTc2NTR2ejRlNGQyeHJtc29jcTJ4ZXUifQ.xrCXPnyPfIuxHpXXdmcPMQ`
// ================== Config =========================================
// 错误处理
const catchErr = err => {
if (err) throw err;
}
// 三层循环(x、y、z轴)爬取地图
for (let levelTemp = 1; levelTemp <= level; levelTemp ++) {
// 创建对应深度文件夹
fs.mkdirSync(`${dir}/${levelTemp}`, catchErr)
// 在瓦片地图中每个深度的横轴、纵轴数量相等,数量为 2^(深度数)
const horizontalNum = Math.pow(2, levelTemp) -1
for (let horizontal = 0; horizontal <= horizontalNum; horizontal ++ ) {
// 创建对应横轴文件夹
fs.mkdirSync(`${dir}/${levelTemp}/${horizontal}`, catchErr)
const verticalNum = Math.pow(2, levelTemp) -1
for ( let vertical = 0; vertical <= verticalNum; vertical ++) {
// 创建对应纵轴文件夹
const imgUrl = `${imgUrlRoot}/${levelTemp}/${horizontal}/${vertical}?access_token=${token}`
// 利用 request 获取图片,在管道中创建文件写入流
request(imgUrl).pipe(fs.createWriteStream(`${dir}/${levelTemp}/${horizontal}/${vertical}.jpg`));
}
}
}
注意:这里的 imgUrlRoot
、token
使用上文中 Mapbox
提供的链接。
如果一切正常我们可以看到一个如下的目录结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.
├── 1
│ ├── 0
│ └── 1
├── 2
│ ├── 0
│ ├── 1
│ ├── 2
│ └── 3
├── 3
│ ├── 0
│ ├── 1
│ ├── 2
│ ├── 3
│ ├── 4
│ ├── 5
│ ├── 6
│ └── 7
……
进入目录,里面保存的图片都应该是直接可以点开查看的,如果无法打开则说明保存失败。
③ 本地图片服务
地图爬取完成后,我们需要再写一个可以按照前端发送的链接返回对应图片的服务。
前端发送的请求格式为:http://localhost:<port>/z/x/y.png
,我们只需将后缀的 z/x/y.png
截取下来,然后利用 node.js
启动一个 http server
,然后利用 fs
模块,对对用的文件进行读取,然后返回到前端。
代码参考如下:
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
// map-server.js
const http = require('http');
const fs = require('fs'); //引入文件读取模块
// 设置侦听端口号
const port = 8081
// 需要访问的文件的存放目录
// 这里使用相对路径,匹配当前地图文件存放在前端项目中的情况。
let documentRoot = 'assets/map';
let server = http.createServer((req, res) => {
// 这里对 url 做了去除首字母的处理
let url = req.url
let file = documentRoot + url; // flie 格式为:assets/map/5/21/12.jpg
fs.readFile(file, (err, data) => {
if (err) {
res.writeHeader(404, {
'content-type': 'text/html;charset="utf-8"'
});
res.write('<h1>404错误</h1><p>你要找的页面不存在</p>');
res.end();
} else {
res.writeHeader(200, {
});
res.write(data); //将index.html显示在客户端
res.end();
}
});
}).listen(port);
console.log(`地图服务器开启成功,端口:${port}`);
④ 修改 Vue-Leaflet
配置项
如果是按照上文中 map-server.js
的代码,那我们当前的链接应该为 http://localhost:8081/{z}/{x}/{y}.jpg
。
我们之前提到,Vue-Leaflet
展示地图依靠我们给他绑定的 url
,现在可以回到前端代码中,将此前绑定的 Mapbox
的在线链接替换为上文启动的本地服务的链接即可。