mapbox-gl教程(8):矢量瓦片图层的发布与使用

本文对矢量瓦片规范进行简单解读,并讲述如何基于java springboot发布一个矢量瓦片服务,以及前端如何展现矢量瓦片。

矢量瓦片简介

矢量瓦片(mapbox-vector-tile,下简称mvt),是mapbox的一项伟大设计,它基于protobuf,设计了一套字节压缩规范,将矢量数据压缩成了瓦片格式。

之所以叫矢量瓦片,是因为它有别于图片格式的栅格瓦片,瓦片中存放了序列化为bytes的features,并且可以在前端反序列化回features。

矢量瓦片压缩规范

规范中有的概念比较晦涩,所以官方有给出了一篇可视化的描述文档,来描述数据的压缩过程

矢量瓦片包含了瓦片基本信息及若干图层,图层又包含了若干要素,而要素则由图形(geometry)和属性(attributes)组成:
矢量瓦片包含数据

geometry和attributes的压缩设计得非常精妙,下面结合官方可视化文档进行简单说明:

图形(geometry)的压缩

mvt为每个瓦片定义了一个瓦片坐标系,左上角坐标为(0, 0),右下角坐标为(4096, 4096)(严格来说,4096这个值是叫做extent,可以根据需要调整,为了方便叙述,本文中extent一律采用默认值4096)
瓦片上的两个坐标系

wgs84坐标系下的geometry,落在瓦片上时需要映射未瓦片坐标系。 例如,一条线段LINESTRING(103.31 23.35, 103.41 23.41)落在瓦片(z = 12, x = 3223, y = 1774)上时,
变成了LINESTRING(1795 2679, 6456 -367)
,具体的转换方式参考这段代码
值得注意的是,线段的第二个坐标并不在范围[0, 4096]内,因为mvt允许存在在瓦片范围外的坐标,以便处理一个跨瓦片的图形在两个瓦片交界处的连接问题。
与之对应,示例代码中的MvtCoordinateConvertor也允许在构造方法中传入指定的缓冲半径,以合理地切割图形在范围外的部分。

随后,mvt定义了一系列数值对应MoveTo、LineTo等操作,完成了geometry在瓦片上的绘制,如下图所示:
mvt geometry
可见,mvt对geometry的压缩是一种有损压缩,extent越小压缩率越高、坐标精度越低,而extent=4096是一个在视觉上看不出误差且压缩率较好的一个取值。

属性(attributes)的压缩

我们知道,属性是以key-value的形式存储的,在一组features中,key、value的值有很大比例是重复的,于是mvt定义了一个整数型字段tags,为key、value进行唯一编号,最大限度地压缩了空间占用,如下图所示:
mvt attributes

矢量瓦片的优点

1、在瓦片包含的features数量不多的前提下,矢量瓦片在体积和渲染速度上都要优于栅格瓦片

2、由于数据是矢量的,给前端提供了很大的灵活性,例如在前端动态配置要素渲染样式,点击事件可以直接选中要素等等。

矢量瓦片的缺点

1、如果瓦片包含了太多的features,瓦片体积和渲染开销都会大幅提高。所以,我们需要合理规划好图层的展现层级,只在必要的层级展现对应的features。

2、矢量瓦片借助webgl渲染展现,对浏览器兼容性有一定要求,ie这种坑货肯定不行,就算是chrome内核,如果版本过低也会出现渲染颜色不对等问题。 另外,如果客户端没有GPU,那webgl会由CPU来模拟渲染,性能会下降许多。

发布和展现一个矢量瓦片图层

本节中,我们从阿里云数据可视化平台下载一份全国省份数据,构建一个包含三个图层的map

编写并发布一个矢量瓦片服务(java)

我们基于springboot,编写一个矢量瓦片服务 引入springboot的maven依赖以及giscat-mvt:

1
2
3
4
5
<dependency>
<artifactId>giscat-vector-mvt</artifactId>
<groupId>org.wowtools</groupId>
<version>g1.6.1</version>
</dependency>

编写Controller,为了节约篇幅,这里把SpringBootApplication也写在同一个类中了

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

import org.locationtech.jts.geom.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wowtools.giscat.vector.pojo.Feature;
import org.wowtools.giscat.vector.pojo.FeatureCollection;
import org.wowtools.giscat.vector.pojo.converter.GeoJsonFeatureConverter;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Map;

/**
* 用springboot起一个web服务演示矢量瓦片的使用
*
* @author liuyu
* @date 2022/4/26
*/
@SpringBootApplication
@RestController()
@RequestMapping("/tile")
@CrossOrigin
public class WebDemo {
public static void main(String[] args) {
SpringApplication.run(WebDemo.class, args);
}

/**
* 测试数据,中国省份
* 数据来源
* https://datav.aliyun.com/portal/school/atlas/area_selector
*/
private static final FeatureCollection areaFeatureCollection;//面数据
private static final FeatureCollection lineFeatureCollection;//线数据
private static final FeatureCollection pointFeatureCollection;//点数据

private static final GeometryFactory geometryFactory = new GeometryFactory();


private static final String vtContentType = "application/octet-stream";

@RequestMapping("/{z}/{x}/{y}")
public void getTile(@PathVariable int z, @PathVariable int x, @PathVariable int y, HttpServletResponse response) {
//构造一个MvtBuilder对象
MvtBuilder mvtBuilder = new MvtBuilder(z, x, y, geometryFactory);

//向mvt中添加layer
MvtLayer layer = mvtBuilder.getOrCreateLayer("省区域");
//向layer中添加feature
for (Feature feature : areaFeatureCollection.getFeatures()) {
//这里简单地从内存中取数据并判断其是否与瓦片有交集,实际运用中可从数据库查询,例如postgis的ST_intersects函数
if (mvtBuilder.getBbox().envIntersects(feature.getGeometry())) {
layer.addFeature(feature);
}
}

//如法炮制添加layer
layer = mvtBuilder.getOrCreateLayer("省边界");
for (Feature feature : lineFeatureCollection.getFeatures()) {
if (mvtBuilder.getBbox().envIntersects(feature.getGeometry())) {
layer.addFeature(feature);
}
}

//如法炮制添加layer
layer = mvtBuilder.getOrCreateLayer("省会位置");
for (Feature feature : pointFeatureCollection.getFeatures()) {
if (mvtBuilder.getBbox().envIntersects(feature.getGeometry())) {
layer.addFeature(feature);
}
}

//数据添加完毕,转为
byte[] bytes = mvtBuilder.toBytes();
exportByte(bytes, vtContentType, response);
}

//将bytes写进HttpServletResponse
private void exportByte(byte[] bytes, String contentType, HttpServletResponse response) {
response.setContentType(contentType);
try (OutputStream os = response.getOutputStream()) {
os.write(bytes);
os.flush();
} catch (org.apache.catalina.connector.ClientAbortException e) {
//地图移动时客户端主动取消, 产生异常"你的主机中的软件中止了一个已建立的连接",无需处理
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

完整的服务端代码在这里
启动后,我们便成功发布了一个矢量瓦片服务http://localhost:8080/tile/{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
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
62
    // 新建一个空白地图
const map = new mapboxgl.Map({
container: 'map',
style: {
"version": 8,
"sources": {},
"layers": []
},
center: [102.712251, 25.040609],
zoom: 4
});
map.on('load', function () {
//添加刚才发布的mvt数据源
map.addSource('tile', {
"type": "vector",
"tiles": [
'http://localhost:8080/tile/{z}/{x}/{y}'
],
"minZoom": 1,
"maxZoom": 22
})
//添加各图层
map.addLayer({
"id": "area-layer",
"type": "fill",
"source": "tile",// 上一步添加的数据源id
"source-layer": "省区域",// source-layer和mvt服务中的图层名对应
"layout": {"visibility": "visible"},
"paint": {"fill-color": '#51bbd6', "fill-opacity": 0.3, "fill-outline-color": '#0000ff'}
})

map.addLayer({
"id": "point-layer",
"source": 'tile',
"source-layer": "省会位置",
"type": "circle",
"paint": {
"circle-color": "#FF00FF",
"circle-radius": 5
}
});

map.addLayer({
"id": "route",
"type": "line",
"source": 'tile',
"source-layer": "省边界",
"layout": {
"line-join": "round",
"line-cap": "round"
},
"paint": {
"line-color": "#FF0000",
"line-width": 3
}
});

//矢量瓦片图层可以被点击
map.on('click', 'area-layer', (e) => {
console.log(e.features[0])
})
})

完整的前端代码在这里

展现效果如下:
最终效果


本文采用 CC BY-SA 4.0 协议 ,转载请注明原始链接: https://blog.wowtools.org/2022/04/28/2022-04-28-mapbox-gl-tutorial-8/

×

请作者喝杯咖啡