1.背景介绍
在gis系统中地图上渲染大数据量一直是比较难搞的问题,特别是涉及数据需要动态实时更新的情况,一般会采用一下几种方式:
- geojson、wkt等方式添加图层,这类格式一般都是从后端接口读取数据,但是要一次性展示十万以上数据的话接口会直接报超时,行不通。
- 将数据发布成OGC服务供前端调用。可以使用geoserver、ArcGis等工具将数据发布成失量切片服务,这样可以实现大数据量渲染,但是一般都是直接读数据库中的静态数据,无法处理实时生成的动态数据。
- 使用mvt方式加载。mvt全称Mapbox Vector Tile,是Mapbox定义的一种矢量瓦片标准。矢量切片被编码为Google Protobufs (PBF)格式,允许序列化结构化数据。为了清楚起见,Mapbox 矢量切片使用
.mvt
文件后缀。数据解析原理官方文档中介绍的比较详细,这里不多介绍。这种方式可以实现数据切片渲染。那么如何才可以将数据转化为mvt格式呢?这里要说到PostgreSQL中的 PostGIS,PostGIS 在对象关系型数据库PostgreSQL上增加了存储管理空间数据的能力。利用Postgis中的ST_AsMVT、ST_AsMVTGeom这两个函数可以将空间数据转化为mvt格式输出,并且可以通过ST_TileEnvelop对应到地图中的xyz坐标实现切片渲染。这种方式无疑是渲染大数据量动态数据的最优解。
mvt在我们平时的业务中经常使用,但是一直没有涉及到百万级的数据量,于是今天想测试一下mvt加载百万级的数据量地图展示效果如何,本文给大家介绍从制作数据,设计后端服务、前端调用地图展示的全流程实践。
2.效果展示
先上效果,可以看到渲染百万级数据还是没有什么压力的(看起来一帧一帧的动是因为我放大地图的时候操作不太丝滑/狗头) 源码也给大家贴这里了:
- 后端代码: github.com/lukeSuperCo...
- 前端地图框架:github.com/lukeSuperCo...
- mapbox加载mvt样例代码以及python脚本代码已在下文中完整贴出
3.制作数据
正片开始,首先由于没有百万级的空间数据,于是就想着写个python脚本在制定范围内随机生成一百万个点位入库,首先确保安装了PostgreSQL并且添加了PostGIS扩展,然后就是建表、写脚本。
sql
CREATE EXTENSION postgis; //在pgsql中添加postgis扩展
CREATE TABLE points_data (name varchar, geom geometry); //创建数据表
创建完成后,开始编写py脚本。
ini
#python连接pgsql数据库
import psycopg2
import random
import math
#连接数据库
conn = psycopg2.connect(dbname="gistable", user="luke", password="luke123", host="127.0.0.1", port="5432")
# 创建游标对象
cursor = conn.cursor()
# 定义地球平均半径(单位:公里)
EARTH_RADIUS = 6371.0
#在制定半径和中心点内随机生成一个点位
def generate_random_point_within_radius(id, center_lat, center_lng, radius):
# 将角度转为弧度
center_lat_radians = math.radians(center_lat)
center_lng_radians = math.radians(center_lng)
# 随机生成一个新的经度和纬度偏移量(单位:弧度)
random_angle = random.uniform(0, 2 * math.pi)
offset_distance = random.uniform(0, radius / EARTH_RADIUS)
# 根据圆周率计算新的纬度和经度
new_lat_radians = math.asin(math.sin(center_lat_radians) * math.cos(offset_distance) +
math.cos(center_lat_radians) * math.sin(offset_distance) * math.cos(random_angle))
new_lng_radians = center_lng_radians + math.atan2(math.sin(random_angle) * math.sin(offset_distance) * math.cos(center_lat_radians),
math.cos(offset_distance) - math.sin(center_lat_radians) * math.sin(new_lat_radians))
# 将新生成的经纬度从弧度转回角度
new_lat = math.degrees(new_lat_radians)
new_lng = math.degrees(new_lng_radians)
point = 'POINT(' + str(new_lng) + ' ' + str(new_lat) + ')'
point_list.append([str(id),point])
def commit_to_db():
#向数据库插入多条数据,多条数据合并为一次插入
cursor.executemany("insert into points_data (name, geom) values (%s, %s)", point_list)
conn.commit()
#生成100w条数据
center_lat = 35.582046 # 假设中心的纬度
center_lng = 108.043544 # 假设中心的经度
radius_km = 1000 # 范围半径为1000公里
point_list = []
for i in range(1000000):
generate_random_point_within_radius(i, center_lat, center_lng, radius_km)
commit_to_db()
编写完成后执行脚本,可以看到100万条数据已生成
4.搭建springboot服务以及编写mvt服务
首先搭建一个springboot框架,并且在application.yml中添加相关pgsql数据库连接配置。然后在pom.xml中添加java-vector-tile依赖
xml
<!-- https://mvnrepository.com/artifact/no.ecc.vectortile/java-vector-tile -->
<dependency>
<groupId>no.ecc.vectortile</groupId>
<artifactId>java-vector-tile</artifactId>
<version>1.2.5</version>
</dependency>
mapper.xml中添加要执行的sql语句,其中要注意的是:
- ST_AsMVT(mvtgeom.*, 'layer', 4096, 'geom',NULL)中的'layer'是定义图层名称,在前端调用的时候需要在source-layer数据中对应上。
- 我这里是生成的4326坐标系数据,但是由于使用的gis地图是3857坐标系,所以使用st_transform将数据转化为3857坐标系,st_setSRID是指定面数据的坐标系。这里要切记输出的坐标系格式要与前端地图坐标系保持一致。
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace mapper接口 必填 随便填值 防止SQL语句ID重名-->
<mapper namespace="com.test.mapper.MvtMapper">
<select id="getMvtByArea" resultType="java.util.Map" parameterType="com.test.pojo.MVTReq">
WITH mvtgeom as (
SELECT
ST_AsMVTGeom (
st_transform(st_setSRID(geom, 4326), 3857) ,
ST_TileEnvelope(${z}, ${x}, ${y}), 4096, 512, false) as geom FROM points_data
)
SELECT ST_AsMVT(mvtgeom.*, 'layer', 4096, 'geom',NULL) AS mvt from mvtgeom;
</select>
</mapper>
在controller中添加对应的方法
typescript
@GetMapping(value = "/getMvtByArea", produces = "application/x-protobuf")
public Object getMvtByArea(MVTReq req) {
return userService.getMvtByArea(req);
}
定义MVTReq类
java
package com.test.pojo;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
import java.util.List;
@Data
public class MVTReq implements Serializable {
private static final long serialVersionUID = 1656087792952781046L;
private Integer x;
private Integer y;
private Integer z;
@NotEmpty
private String typeId;
private List<String> typeIds;
private List<String> areaCodes;
private Integer level;
private String geom;
private String serviceTypeId;
private String tile;
private String bbox;
private String pcode;
private String tb;
}
在service以及impl中对应的添加方法
typescript
//impl
@Override
public Object getMvtByArea(MVTReq req) {
Map<String, Object> resultMap = userMapper.getMvtByArea(req);
mvt = (byte[]) resultMap.get("mvt");
return mvt;
}
//service
public Object getMvtByArea(MVTReq req);
大功告成,然后测试一下,发现可以调用,这里推荐一个openlayer的mvt文件在线预览,可以查看展示效果,不过坐标系不一致的话可能会有偏差。
5.前端地图调用加载
gis地图我使用的是mapbox,自带mvt,加载方式比较简单。如果使用openlayer或者leaflet引入对应的工具包也可以调用。
php
addMvtLayer() {
let mvt_url = 'http://127.0.0.1:8093/getMvtByArea/?x={x}&y={y}&z={z}'
this.map.addLayer({
'id': 'mvt-fill',
source: {
type: "vector",
tiles: [mvt_url],
minzoom: 0,
maxzoom: 24
},
"source-layer": 'layer',
"type" : "circle",
"paint": {
'circle-color': 'red',
}
});
}
完整样例代码
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>mapbox</title>
<link href='https://api.mapbox.com/mapbox-gl-js/v2.0.0/mapbox-gl.css' rel='stylesheet' />
<style>
body { margin:0; padding:0; }
html,body {
padding: 0;
margin: 0;
height: 100%;
}
#map {
width: 100%;
height: 100%;
}
#map { position:absolute; top:0; bottom:0; width:100%; }
</style>
</head>
<body>
<div id='map'>
<div class="map-legend">
</div>
</div>
<script src='https://api.mapbox.com/mapbox-gl-js/v2.0.0/mapbox-gl.js'> </script>
<script>
mapboxgl.accessToken = 'your access token';
const map = new mapboxgl.Map({
//地图容器div的id
container: 'map', // 地图容器
// Choose from Mapbox's core styles, or make your own style with Mapbox Studio
style: 'mapbox://styles/mapbox/streets-v12', // style URL
center: [108, 35], // starting position [lng, lat]
zoom: 9, // starting zoom
});
let mvt_url = 'http://127.0.0.1:8093/getMvtByArea/?x={x}&y={y}&z={z}'
setTimeout(()=> {
map.addLayer({
'id': 'mvt-fill',
source: {
type: "vector",
tiles: [mvt_url],
minzoom: 0,
maxzoom: 24
},
"source-layer": 'layer',
"type" : "circle",
"paint": {
'circle-color': 'red',
}
});
},500)
</script>
</body>
6.总结
mvt服务方式解决了大数据量在地图上动态渲染的问题,总的来说体验效果还是很丝滑的,并且也可以把相关属性参数等一起输出,前端可以实现分级渲染以及点击要素获取相关信息等功能。如果大家还有其他大数据量的渲染方案可以在评论区分享出来供大家参考学习~