gis系统中使用mvt切片实现百万级数据动态渲染全流程实践

1.背景介绍

在gis系统中地图上渲染大数据量一直是比较难搞的问题,特别是涉及数据需要动态实时更新的情况,一般会采用一下几种方式:

  1. geojson、wkt等方式添加图层,这类格式一般都是从后端接口读取数据,但是要一次性展示十万以上数据的话接口会直接报超时,行不通。
  2. 将数据发布成OGC服务供前端调用。可以使用geoserverArcGis等工具将数据发布成失量切片服务,这样可以实现大数据量渲染,但是一般都是直接读数据库中的静态数据,无法处理实时生成的动态数据。
  3. 使用mvt方式加载。mvt全称Mapbox Vector Tile,是Mapbox定义的一种矢量瓦片标准。矢量切片被编码为Google Protobufs (PBF)格式,允许序列化结构化数据。为了清楚起见,Mapbox 矢量切片使用.mvt文件后缀。数据解析原理官方文档中介绍的比较详细,这里不多介绍。这种方式可以实现数据切片渲染。那么如何才可以将数据转化为mvt格式呢?这里要说到PostgreSQL中的 PostGISPostGIS 在对象关系型数据库PostgreSQL上增加了存储管理空间数据的能力。利用Postgis中的ST_AsMVTST_AsMVTGeom这两个函数可以将空间数据转化为mvt格式输出,并且可以通过ST_TileEnvelop对应到地图中的xyz坐标实现切片渲染。这种方式无疑是渲染大数据量动态数据的最优解。

mvt在我们平时的业务中经常使用,但是一直没有涉及到百万级的数据量,于是今天想测试一下mvt加载百万级的数据量地图展示效果如何,本文给大家介绍从制作数据,设计后端服务、前端调用地图展示的全流程实践。

2.效果展示

先上效果,可以看到渲染百万级数据还是没有什么压力的(看起来一帧一帧的动是因为我放大地图的时候操作不太丝滑/狗头) 源码也给大家贴这里了:

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服务方式解决了大数据量在地图上动态渲染的问题,总的来说体验效果还是很丝滑的,并且也可以把相关属性参数等一起输出,前端可以实现分级渲染以及点击要素获取相关信息等功能。如果大家还有其他大数据量的渲染方案可以在评论区分享出来供大家参考学习~

相关推荐
疯狂学习GIS15 小时前
ArcGIS填补面图层的细小空白并删除主体部分外的零散部分
arcgis·gis·学术工作效率·gis数据
GIS思维1 天前
ArcGIS定义投影与投影的区别(数据和底图不套合的原因和解决办法)
arcgis·gis·地理信息·arcgis坐标系·动态投影
duansamve2 天前
WebGIS地图框架有哪些?
javascript·gis·openlayers·cesium·mapbox·leaflet·webgis
moonless02223 天前
【GISer精英计划_01】GIS的生态位与基石
gis
WineMonk3 天前
ArcGIS Pro ADGeoProcessing DAML
arcgis·gis·arcgis pro sdk·daml
兔子小姐_4 天前
实验十三 生态安全评价
arcgis·gis
WineMonk8 天前
ArcGIS Pro ADGeoDatabase DAML
arcgis·gis·arcgis pro sdk·arcgis pro·daml
WineMonk8 天前
ArcGIS Pro ADCore DAML
arcgis·gis·arcgis pro sdk·arcgis pro·daml
山海鲸可视化10 天前
GIS融合之路(八)-如何用Cesium直接加载OSGB文件(不用转换成3dtiles)
3d·gis·数字孪生·cesium·倾斜摄影·osgb
枝上棉蛮15 天前
GISBox VS ArcGIS:分别适用于大型和小型项目的两款GIS软件
arcgis·gis·数据可视化·数据处理·地理信息系统·gis工具箱·gisbox