前言

近期,突发奇想,因为学校的地图并不太直观,要么细节数据不全,要么操作很难,需要点击多级才可以进入查看
因此,决定自己动手创建一份地图,方便日常使用

最后在询问了 AI 之后,决定使用 Tileserver-GL(后端底图)+ MapLiberGL(前端显示+POI叠加层)+ OSM(数据源) 来创建自己的地图

部署 Tileserver-GL

安装 Tileserver-GL

根据官方的教程, 使用 docker 来安装 Tileserver-GL 是最为便捷的

但是,官方并没有说明后期需要导入数据的时候应该如何操作,我在这里也踩了很多坑

最后,我根据自己的情况,写了一个 docker-compose.yml文件,内容如下,可以参考一下:

1
2
3
4
5
6
7
8
9
10
11
services:
tileserver:
image: maptiler/tileserver-gl
ports:
- "8080:8080"
volumes:
- "/home/u0_a244/TileServer-GL/data:/data"
- "/home/u0_a244/TileServer-GL/styles:/styles"
- "/home/u0_a244/TileServer-GL/config.json:/config.json"
- "/home/u0_a244/TileServer-GL/fonts:/fonts"
command: ["--config", "/config.json"]

其中,ports 部分是将容器的 8080 端口映射到宿主机的 8080 端口
volumes 部分是将宿主机的相关目录挂载到容器中

在我的环境中,/home/u0_a244/TileServer-GL/ 是我用来存放 Tileserver-GL 相关文件的目录,数据如下:

1
2
3
4
5
6
7
8
9
10
~/TileServer-GL$ tree -L 2
.
├── config.json
├── data
│ └── guangdong.mbtiles
├── docker-compose.yml
├── fonts
│ └── MiSans Semibold
└── styles
└── osm_liberty.json

请根据自己的实际情况,修改 docker-compose.yml 中的路径

至于这些文件是怎么来的,后面会讲到

修改配置文件

在前面的 docker-compose.yml 文件中,我挂载了一个 config.json 文件,这个文件就是用来配置 Tileserver-GL 的配置

相关的文档可以在官网找到

我这里使用的配置文件为:

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
{
"options": {
"paths": {
"root": "",
"styles": "styles",
"fonts": "fonts",
"mbtiles": "data"
},
"domains": [
"my-domain:8080"
],
"formatOptions": {
"jpeg": {
"quality": 80
},
"webp": {
"quality": 90
}
},
"maxScaleFactor": 3,
"maxSize": 2048,
"pbfAlias": "pbf",
"serveAllFonts": false,
"serveAllStyles": false,
"serveStaticMaps": true,
"allowRemoteMarkerIcons": true,
"allowInlineMarkerImages": true,
"staticAttributionText": "© OpenMapTiles © OpenStreetMaps",
"tileMargin": 0
},
"styles": {
"osm-liberty": {
"style": "osm_liberty.json",
"serve_rendered": true
},
"remote": {
"style": "https://demotiles.maplibre.org/style.json"
}
},
"data": {
"openmaptiles": {
"mbtiles": "guangdong.mbtiles"
}
}
}

这里重点需要修改的是 domains部分,将 my-domain:8080 修改为你自己的域名或者 IP 地址加端口号,否则无法正常请求,另外根据文档所述,还能在访问的时候达到负载均衡的效果(挂载到服务器用的时候绝对不能写成 localhost)

此外,mbtiles 部分需要修改为你自己的 mbtiles 文件名(因为我在文件上方已经说明了 mbtiles 是在 data 目录下的,所以就不用再写 data/ 了)

styles 部分是用来配置样式文件的,也需要改成自己的样式文件名,后面会讲到如何获取和修改样式文件

获取 mbtiles 数据

Tileserver-GL 需要 mbtiles 格式的数据才能运行,获取 mbtiles 数据有很多种方式,这里介绍两种比较常用的方式

方式一:BBBike 下载

BBBike 是一个提供地图数据下载的网站,支持多种格式的地图数据下载,包括 mbtiles 格式

  1. 访问 BBBike 官网
  2. 在地图上选择你需要的区域,可以通过搜索城市名称来快速定位
  3. 选择输出格式为 mbtiles
  4. 提交下载请求,填入邮箱,等待 10 分钟左右处理完成后下载 mbtiles 文件

这个方式的优点是简单快捷,适合下载小范围的地图数据

但是因为地图的生成方式不是自己控制的,后续在数据处理和渲染的时候会遇到不少问题

这部分我会在渲染样式的时候讲到

方式二:使用 MapTiler 下载

MapTiler 官方提供了数据下载,可以直接访问 MapTiler 数据下载页面,选择区域下载即可

但是它的免费数据下载有一些限制:

  • OpenStreetMap Vectors (2020)
  • Satellite Low-Res (2016)
  • Non-commercial

反正我是觉得 2020 年的数据有点旧了,而且所以就没有选这个

方式三:使用 OSM 数据 + tilemaker 自己生成 mbtiles

  1. 下载 OSM 数据,可以从 Geofabrik 下载所需区域的 PBF 文件
  2. 安装 tilemaker,可以参考 tilemaker 的 GitHub 页面

其实,如果并不在意程序的最新版本的话,可以直接在GitHub的 Releases 页面下载编译好的二进制文件但是需要注意的是,Windows 下的 v3.0.0 版本是有问题的,Windows用户 建议下载 v2.4.0 版本
另外,还需要下载配置文件 config-openmaptiles.json 和处理脚本 process-openmaptiles.lua,可以从该仓库的 resources 目录中获取

  1. 使用 tilemaker 将 PBF 文件转换为 mbtiles 文件,实例命令如下:
1
tilemaker --input "guangdong-260113.osm.pbf"  --output "guangdong.mbtiles" --config .\config-openmaptiles.json --process .\process-openmaptiles.lua

其中,--input 指定输入的 PBF 文件,--output 指定输出的 mbtiles 文件,--config--process 分别指定配置文件和处理脚本,需要自己替换成实际的文件路径

如果想裁切数据,可以使用 --bbox 参数,格式为 minLon,minLat,maxLon,maxLat,例如:

1
tilemaker.exe --input "guangdong-260113.osm.pbf" --bbox 113,22.34,114,22.50 --output "guangdong.mbtiles" --config .\config-openmaptiles.json --process .\process-openmaptiles.lua

还有值得注意的是,process-openmaptiles.lua 文件开头的几个带 language 的变量需要根据自己后面的 style.json 文件来决定,否则可能会导致某些标签无法显示,我后面会再次提到

然后打完这个命令之后,等待一段时间(我生成一个学校的大小用时大概为 30s, 使用 i7-12700H CPU,占用 5.5GB 内存),就可以得到一个 mbtiles 文件了

最后记得将它移动到 Tileserver-GL 的 data 目录下

但是这个方法也有一定的缺点(相对于 Maptiler 提供的文件来说),因为它经过一次转换,例如 building:part 之类的细节就会被去掉,没法准确的渲染出来 3D 效果

tilemaker 生成的数据结果预览

另外,这个图也解释了为什么无法直接从平台 Geojson 变成 Tileserver-GL 样式渲染需要的格式:它是分为右侧那几大类来分类渲染,普通 Geojson 是没分类的,会导致无法识别类型而对应不上样式

至于有没有方法可以从普通的在 Overpass 上获得的 Geojson 转化为这个分类的格式,再用 osmium-tool 之类的转为 mbtiles呢?或许是有机会的,但是我似乎没有找到,如果有找到的话可以在评论区提一下

方法四:使用 OpenMapTiles 构建完整数据集

这个是官方推荐的方法,使用 OpenMapTiles 工具链来生成完整的地图数据集,但是部署的文件相当复杂,会变得非常慢

可以参考官方 GitHub 仓库的 README 文档来进行操作

如果需要裁切之前的整个 PBF 为特定区域的数据,可以使用 OSMconvert 来操作,链接里面直接下载二进制文件就行了,然后使用以下命令来裁切数据,经纬度可以使用 Bounding Box Tool 来获取

(这是我在 Windows 下的命令,其他系统请自行参照文档更改)

1
.\osmconvert64-0.8.8p.exe .\guangdong-260113.osm.pbf -b="113.4,22.3,114.5,22.6" -o="guangdong.osm.pbf"

然后将下载好的 pbf 放进 OpenMapTiles 的 data 目录下,根据文档走一次,就可以生成一个完整的 mbtiles 文件了

以下内容截取自 GitHub 仓库的 README 文档,有些我标了 optional 的没有需要就可以跳过:

1
2
3
4
5
6
7
8
9
10
make clean                  # clean / remove existing build files
make # generate build files
make start-db # start up the database container.
make import-data # Import external data from OpenStreetMapData, Natural Earth and OpenStreetMap Lake Labels.
make download area=albania # download albania .osm.pbf file -- can be skipped if a .osm.pbf file already existing (optional)
make import-osm # import data into postgres
make import-wikidata # import Wikidata (optional)
make import-sql # create / import sql functions
make generate-bbox-file # compute data bbox -- not needed for the whole planet or for downloaded area by `make download`
make generate-tiles-pg # generate tiles

实测以我老笔记本 i7-2677M + SSD 的配置,生成一个学校的范围就部署了将近 5 个小时(因为主力机没装 Linux,没法跑 make)
主要卡在 SSD 的 I/O 写入瓶颈上,因为总共会生成多个 Docker Container,产生大量的写入
建议使用高速的 M.2 NVMe SSD 来进行操作

第二次跑似乎就快点了,0.05 的经度和 0.03 的纬度范围大概跑了 20 分钟,跑出来的文件大小在 500KB 左右

然后我又试了下我 Azure 的 1 年免费 VPS(2C1G + 64G Premium SSD,开了 2G 的 Swap),跑了就大概 5 分钟,读写平均上到了 70MB/s,可以根据自己的硬件情况来预估时间

另外服务器上部署要注意的是,HDD 的服务器几乎完全不能部署,性能都不太行,跑几分钟还不够我笔记本上跑几秒

OpenMapTiles 的数据结果预览

获取 Tileserver-GL 渲染样式文件

虽然默认直接启动,它是会自动加载自带的样式文件的,但是因为调整配置不方便,而且并不是很好看,我就选择了使用其他的 style 文件

我是在 Maputnik 上的左上角找到了一个叫做 OSM Liberty 的样式文件,预览后觉得还可以,就点击左上角的 Save 按钮下载

但是,由于各个文件的渲染逻辑不同,为了不渲染出现问题,需要对样式文件进行一些修改

修改名称读取

打开下载下来的 osm-liberty.json 文件,ctrl+f找到 name 部分,将某些的 name:latin 进行修改

具体是怎么改,还是要看回去上一节中 process-openmaptiles.lua 文件中 language 变量的设置

比如我在上面的 lua 设置的是:

1
2
3
4
5
6
7
8
-- Preferred language can be (for example) "en" for English, "de" for German, or nil to use OSM's name tag:
preferred_language = nil
-- This is written into the following vector tile attribute (usually "name:latin")
preferred_language_attribute = "name"
-- If OSM's name tag differs, then write it into this attribute (usually "name_int"):
default_language_attribute = "name_int"
-- Also write these languages if they differ - for example, { "de", "fr" }
additional_languages = {"zh","en"}

这个设置就决定在前面生成 mbtiles 的时候,将 OpenStreetMap 中的 name 标签写入 name 属性中,name:zh 标签写入 name:zh 属性中,name:en 标签写入 name:en 属性中 (理论上是,但是不知道为什么我生成的文件中并没有 name:zh 标签)

我这样改是为了避免歧义,反正默认操作会在生成 mbtiles 的时候把中文写入 name:latin标签,听起来逻辑怪怪的

所以,我就应该把全部 name:latin 改成 name,因为生成的文件根本就没有 name:latin 标签

另外,如果不是用 maputnik 转换生成文件的话(即不是用上面的方法3),具体应该写什么可以先跳过这部分,先启动一次 Tileserver-GL,在网页下方的 Data 选项卡的 Inspect 按钮,点进去随机看几个 POI,看文件里面究竟写了什么字段

示例:

使用方法 4 OpenMapTiles 构建的数据

如上图,我写的就是下面这样,来同时支持中文和英文的显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"text-field": [
"case",
[
"all",
["has", "name:latin"],
["!=", ["get", "name"], ["get", "name:latin"]]
],
[
"format",
["get", "name"], {},
"\n", {},
["get", "name:latin"], { "font-scale": 0.8 }
],
["get", "name"]
],

修改地图路径

在样式文件中,通常都会直接指定地图 tiles 的路径,但是这个也可以通过引用 config.json 文件中的数据源来读取

比如我就写成了

1
2
3
4
5
6
7
"sources": {
"openmaptiles": {
"type": "vector",
"url": "mbtiles://{openmaptiles}",
"attribution": "© OpenStreetMap contributors"
},
}

这样就可以直接引用 config.json 文件中 data 部分的 openmaptiles 数据源了

如果你的 config.json 文件中 data 部分的名称不是 openmaptiles 的话,需要相应地修改这里的名称

修改字体

默认字体是无法显示中文的,所以需要修改字体

首先是下载字体,我这里使用的是 MiSans ,先获取到字体的 ttf 或者 otf 文件

然后,需要用特殊的工具将字体转换为 PBF 格式

可以使用 font-maker 这个网站来转换

也可以使用 fonts 这个工具来转换

效果应该都差不多的

转换完成后,将生成的存有 pbf 文件夹整个放到 Tileserver-GL 的 fonts 目录下

最后,修改样式文件中的字体引用,找到以下位置修改:

1
"glyphs": "{fontstack}/{range}.pbf",

此外,还需要搜索整个文件,找到带有 text-font 的地方,将字体名称修改为你刚才转换的字体文件夹的名称

例如我写的是 "text-font": ["MiSans Semibold"],

启动 Tileserver-GL

完成以上步骤后,就可以启动 Tileserver-GL 了
在 Tileserver-GL 目录下,运行以下命令:

1
docker-compose up -d

然后,打开浏览器,访问 http://your-domain:8080 (将 your-domain 替换为你的域名或者 IP 地址),就可以看到自己的地图了

Tileserver-GL + Openmaptiles + OSM Liberty 最终渲染效果

创建前端 html 页面

最后,如果想要创建一个前端的 html 页面来展示地图,可以参考以下代码:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Map</title>
<link href="https://unpkg.com/maplibre-gl@2.1.9/dist/maplibre-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/maplibre-gl@2.1.9/dist/maplibre-gl.js"></script>
<script>
const map = new maplibregl.Map({
container: 'map',
style: 'http://your-domain:8080/styles/osm-liberty/style.json',
center: [113.5, 22.4],
zoom: 12
});
</script>
</body>
</html>

这里的 style 地址需要修改为你自己的 Tileserver-GL 的样式地址,建议使用 https 来避免跨域问题

后面再在上方加 POI 标记、自定义画线什么的,参照 MapLibre GL JS 官方文档 就可以了,或者问 AI 也行,实测错误率不高

反正我后面使用的 Overpass API 来获取 POI 数据,然后使用 MapLibre GL JS 的 Marker 功能来渲染这些数据,效果还不错

结语

以上就是使用 Tileserver-GL + MapLiberGL + OSM 创建自己的地图的详细教程
希望对大家有所帮助,如果有任何问题,欢迎在评论区留言讨论!

参考资料