作者:李鹤
01 引言
如何对时空数据库中的亿级矢量空间数据进行在线可视化一直是业界难题。因数据体量大,传统方法需要将数据库中数据进行基于缓存切片的服务发布才能可视化,操作流程冗长,且有一大堆需要考虑的问题:
- 如果对矢量数据进行预切片,数据要切多久?切多少级合适?存储瓦片的硬盘空间够用吗?
- 如果使用实时瓦片,实时渲染瓦片的响应时间能保证吗?
- 如果使用矢量瓦片,小比例尺的瓦片可能会有多大体积?传输会不会成为瓶颈?前端渲染能承受多大的数据量?
如果是要快速浏览数据库中的大规模在线数据,传统用于“底图服务”的离线切片生产流程几乎无解,不但费时费力,又无法在线联机处理。黑科技来了,本文介绍如何使用RDS PG或PolarDB(兼容PG版或Oracle版)的Ganos时空引擎提供的数据库快显技术,仅用百行代码实现亿级海量几何空间数据的在线快速显示和流畅地图交互,且无需关注切片存储和效率问题。
02 技术特性解读
Ganos的在线快显处理的核心是将数据库和可视化进行了关联,提供了一种新的可视化索引技术——稀疏矢量金字塔(Sparse Vector Pyramid,SVP)索引。SVP具备两个关键特性:快与省。
其中,快指两个阶段的快:
-
金字塔创建快:Ganos利用空间索引对数据在空间上进行密集度划分,根据密集度建立一种稀疏矢量金字塔索引,相比传统切图流程减少了90%的数据计算量。同时,创建金字塔采用了完全并行处理模式,即使1亿条地类图斑数据生成金字塔也仅需耗费约10分钟时间。
-
数据展现快:Ganos采用了视觉可见性剔除算法,根据Z-order排序,过滤掉大量不影响显示效果的数据,从而加快实时显示的效率。Ganos支持直接输出PNG格式的栅格瓦片和MVT格式的矢量瓦片,1亿地类图斑数据实时渲染显示的响应时间都达到秒级。
省也具有两个维度:
-
节省磁盘空间:1亿条地类图斑数据生成金字塔索引仅仅占据原表5%大小的额外空间。
-
节省开发时间:仅使用简单的SQL语句,通过调整语句参数即可灵活控制显示效果。
03 使用步骤
Ganos的快显引擎使用上非常简洁,已高度封装了SQL函数。需要注意的是,第一次使用快显引擎之前,需要显式创建对应的扩展模块,执行的语句如下:
CREATE EXTENSION ganos_geometry_pyramid CASCADE;
通过执行以上语句,快显引擎的计算组件将会被加载起来。
3.1 建立稀疏矢量金字塔
假设您已创建了某个矢量大表并导入了数据,接着就可以使用Ganos的st_buildpyramid方法创建矢量金字塔。
方法原型如下,更详细的参数描述可以参考官方文档。
boolean ST_BuildPyramid(cstring table, cstring geom, cstring fid, cstring config)
注:*左右滑动阅览
其中
- table:矢量数据所在的表名。
- geom:矢量字段名。
- fid:矢量要素记录的唯一标识,支持Int4/Int8类型。
- config:json格式的配置参数字符串。
- 在本例中,我们指定矢量金字塔的名称和使用的逻辑瓦片大小(这个瓦片大小并非真实存在的瓦片,仅表示一种空间上的逻辑划分)
- 在本例中,我们指定矢量金字塔的名称和使用的逻辑瓦片大小(这个瓦片大小并非真实存在的瓦片,仅表示一种空间上的逻辑划分)
实际调用如下:
ST_BuildPyramid('points', 'geom', 'gid', '{"name":"points_geom","tileSize":512}')
注:*左右滑动阅览
我们为表points的geom字段创建了一个矢量金字塔,金字塔名我们指定为points_geom,同时设定金字塔的逻辑瓦片大小为512。
3. 2 获取栅格瓦片
栅格瓦片是图片形式的瓦片(Tile),是使用最广泛的一种地图瓦片形式。Ganos的ST_AsPng方法提供了在数据库端将矢量数据按需动态渲染为栅格瓦片的功能。该功能提供了最基础的栅格符号化能力,更多面向一些不需要复杂符号化的轻量级场景,如数管系统中。
方法原型如下,更详细的参数描述可以参考官方文档:
bytes ST_AsPng( cstring name, cstring tile, cstring style)
其中
- name:金字塔表名。
- tile:瓦片索引行列号,Z_X_Y的形式。
- style:渲染样式。我们可以通过如下参数调节渲染效果:
- point_size:点大小,单位为像素。
- line_width:线宽,对线要素和面要素的外边框起作用,单位为像素。
- line_color:线渲染颜色,对线要素和面要素的外边框起作用。前6位为16进制颜色,后2位为16进制透明度。
- fill_color:填充颜色,对面要素起作用。
- background:背景色。一般设置为
FFFFFF00
,即纯透明。
- point_size:点大小,单位为像素。
实际调用如下:
ST_AsPng('points_geom', '1_2_1','{"point_size": 5,"line_width": 2,"line_color": "#003399FF","fill_color": "#6699CCCC","background": "#FFFFFF00"}')
注:*左右滑动阅览
我们从矢量金字塔中获取到索引行列号为x=2,y=1,z=1的矢量瓦片,并将该矢量瓦片按照我们配置的样式渲染为栅格瓦片,返回PNG格式的图片。
3. 3 获取矢量瓦片
矢量瓦片是新兴的地图瓦片技术,具有在前端配置样式的灵活特性,使用WebGL渲染,效果也更加美观,Mapbox等地图框架可以方便的支持这一格式。使用Ganos的ST_Tile方法可以将矢量金字塔中的数据以矢量瓦片的形式提供。
方法原型如下,更详细的参数描述可以参考官方文档。
bytea ST_Tile(cstring name, cstring key);
其中
- name:金字塔名。在本例中为表名_矢量字段名。
- key:瓦片索引行列号,Z_X_Y的形式。
我们只需要提供标准的TMS行列号和金字塔表的名称即可调用。
实际调用如下:
ST_Tile('points_geom', '1_2_1');
我们从矢量金字塔中获取到索引行列号为x=2,y=1,z=1的矢量瓦片,返回数据为标准的MVT格式。
04 实战案例
4.1 测试数据
我们准备两份矢量数据作为测试样例。
buildings表为面数据,数据总计1.25亿条,展现使用栅格瓦片的在线可视化效果。
gid|geom | ---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 1|MULTIPOLYGON(((-88.066953 34.916114 0,-88.066704 34.916114 0,-88.066704 34.91602 0,-88.066953 34.91602 0,-88.066953 34.916114 0))) 2|MULTIPOLYGON(((-87.924658 34.994797 0,-87.924791 34.99476 0,-87.924817 34.994824 0,-87.924685 34.994861 0,-87.924658 34.994797 0)))
注:*左右滑动阅览
points表为点数据,数据总计10.7万条,展现使用矢量瓦片的在线可视化效果。
id|geom | --|------------------------------| 1|POINT (113.5350205 22.1851929)| 2|POINT (113.5334245 22.1829781)|
4.2 全栈架构
数据库-快显全栈架构包含数据库服务器、python服务端和用户端三个部分,全栈架构如下图所示。
4.3 服务端代码
为了代码简洁,更侧重于逻辑的描述,我们选择了Python(兼容Python3.6及以上版本)作为后端语言,Web框架使用了基于Python的Flask(使用pip install flask
进行安装)框架,数据库连接框架使用了基于Python的Psycopg2(使用pip install psycopg2
进行安装)。
值得一提的是,我们实现了最基础的功能,当Web服务自身的性能出现瓶颈时,可按不同的平台与框架进行优化,获得更好的响应性能。
我们在后端首先建立了矢量金字塔,其后分别实现了两个接口,矢量瓦片接口使用points表中的数据,栅格瓦片接口使用buildings表中的数据,并定义好样式,供前端直接调用。为了方便说明,后端代码同时提供了矢量栅格两个接口,实际使用时可以按需选择。
# -*- coding: utf-8 -*- # @File : Vector.py import json from psycopg2 import pool from threading import Semaphore from flask import Flask, jsonify, Response, send_from_directory import binascii # 连接参数 CONNECTION = "dbname=postgres user=postgres password=postgres host=YOUR_HOST port=5432" class ReallyThreadedConnectionPool(pool.ThreadedConnectionPool): """ 面向多线程的连接池,提高地图瓦片类高并发场景的响应。 """ def __init__(self, minconn, maxconn, *args, **kwargs): self._semaphore = Semaphore(maxconn) super().__init__(minconn, maxconn, *args, **kwargs) def getconn(self, *args, **kwargs): self._semaphore.acquire() return super().getconn(*args, **kwargs) def putconn(self, *args, **kwargs): super().putconn(*args, **kwargs) self._semaphore.release() class VectorViewer: def __init__(self, connect, table_name, column_name, fid): self.table_name = table_name self.column_name = column_name # 创建一个连接池 self.connect = ReallyThreadedConnectionPool(5, 10, connect) # 约定金字塔表名 self.pyramid_table = f"{self.table_name}_{self.column_name}" self.fid = fid self.tileSize = 512 # self._build_pyramid() def _build_pyramid(self): """创建金字塔""" config = { "name": self.pyramid_table, "tileSize": self.tileSize } sql = f"select st_BuildPyramid('{self.table_name}','{self.column_name}','{self.fid}','{json.dumps(config)}')" self.poll_query(sql) def poll_query(self, query: str): pg_connection = self.connect.getconn() pg_cursor = pg_connection.cursor() pg_cursor.execute(query) record = pg_cursor.fetchone() pg_connection.commit() pg_cursor.close() self.connect.putconn(pg_connection) if record is not None: return record[0] class PngViewer(VectorViewer): def get_png(self, x, y, z): # 默认参数 config = { "point_size": 5, "line_width": 2, "line_color": "#003399FF", "fill_color": "#6699CCCC", "background": "#FFFFFF00" } # 在使用psycpg2时,将二进制数据以16进制字符串的形式传回效率更高 sql = f"select encode(st_aspng('{self.pyramid_table}','{z}_{x}_{y}','{json.dumps(config)}'),'hex')" result = self.poll_query(sql) # 只有在使用16进制字符串的形式传回时才需要将其转换回来 result = binascii.a2b_hex(result) return result class MvtViewer(VectorViewer): def get_mvt(self, x, y, z): # 在使用psycpg2时,将二进制数据以16进制字符串的形式传回效率更高 sql = f"select encode(st_tile('{self.pyramid_table}','{z}_{x}_{y}'),'hex')" result = self.poll_query(sql) # 只有在使用16进制字符串的形式传回时才需要将其转换回来 result = binascii.a2b_hex(result) return result app = Flask(__name__) @app.route('/vector') def vector_demo(): return send_from_directory("./", "Vector.html") # 定义表名,字段名称等 pngViewer = PngViewer(CONNECTION, 'usbf', 'geom', 'gid') @app.route('/vector/png/<int:z>/<int:x>/<int:y>') def vector_png(z, x, y): png = pngViewer.get_png(x, y, z) return Response( response=png, mimetype="image/png" ) mvtViewer = MvtViewer(CONNECTION, 'points', 'geom', 'gid') @app.route('/vector/mvt/<int:z>/<int:x>/<int:y>') def vector_mvt(z, x, y): mvt=mvtViewer.get_mvt(x, y, z) return Response( response=mvt, mimetype="application/vnd.mapbox-vector-tile" ) if __name__ == "__main__": app.run(port=5000, threaded=True)
注:*左右滑动阅览
将以上代码保存为Vector.py文件,执行python Vector.py
命令即可启动服务。
从代码不难推断,无论我们使用何种语言、何种框架,我们只需将矢量或栅格瓦片的SQL语句封装为接口即可实现完全相同的功能。相比发布传统的地图服务,借助Ganos的矢量金字塔功能实现在线可视化是更加轻量好用的选择:
- 针对栅格瓦片,可以在通过改变代码进行样式控制,灵活性大大增强。
- 无需引入第三方的其他组件,也不需要进行针对性优化,就有令人满意的响应性能。
- 可以任意选择使用者熟悉的编程语言与框架,也无需复杂专业的参数配置,对非地理从业者更加的友好。
4.4 用户端代码
我们选用Mapbox作为前端地图框架,展示后端提供的矢量瓦片层和栅格瓦片层,并为矢量瓦片层配置了渲染参数。
为了方便说明,前端代码同时添加了矢量、栅格两个图层,实际使用时可以按需选择。
我们在后端代码的同一文件目录下新建名为Vector.html的文件,写入下列代码,在后端服务启动后,就可以通过http://localhost:5000/vector
访问了。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title></title> <link href="https://cdn.bootcdn.net/ajax/libs/mapbox-gl/1.13.0/mapbox-gl.min.css" rel="stylesheet" /> </head> <script src="https://cdn.bootcdn.net/ajax/libs/mapbox-gl/1.13.0/mapbox-gl.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.0/axios.min.js"></script> <body> <div id="map" style="height: 100vh" /> <script> const sources = { osm: { type: "raster", tiles: ["https://b.tile.openstreetmap.org/{z}/{x}/{y}.png"], tileSize: 256, }, }; const layers = [ { id: "base_map", type: "raster", source: "osm", layout: { visibility: "visible" }, }, ]; const map = new mapboxgl.Map({ container: "map", style: { version: 8, layers, sources }, }); map.on("load", async () => { map.resize(); // 添加栅格瓦片数据源 map.addSource("png_source", { type: "raster", minzoom: 1, tiles: [`${window.location.href}/png/{z}/{x}/{y}`], tileSize: 512, }); // 添加栅格瓦片图层 map.addLayer({ id: "png_layer", type: "raster", layout: { visibility: "visible" }, source: "png_source", }); // 添加矢量瓦片数据源 map.addSource("mvt_source", { type: "vector", minzoom: 1, tiles: [`${window.location.href}/mvt/{z}/{x}/{y}`], tileSize: 512, }); // 添加矢量瓦片图层,并为矢量瓦片添加样式 map.addLayer({ id: "mvt_layer", paint: { "circle-radius": 4, "circle-color": "#6699CC", "circle-stroke-width": 2, "circle-opacity": 0.8, "circle-stroke-color": "#ffffff", "circle-stroke-opacity": 0.9, }, type: "circle", source: "mvt_source", "source-layer": "points_geom", }); }); </script> </body> </html>
注:*左右滑动阅览
4.5 矢量瓦片的动态效果
可以在前端调节不同效果。调整为新的图层参数后效果如下:
{ "circle-radius": 4, "circle-color": "#000000", "circle-stroke-width": 2, "circle-opacity": 0.3, "circle-stroke-color": "#003399", "circle-stroke-opacity": 0.9, }
4.6 栅格瓦片的动态效果
05 与PGADmin集成
PG数据库管理工具PGAdmin原生支持矢量数据的可视化,但因缺乏快显技术,仅能单对象显示或有限结果集显示,无法对大规模矢量数据进行畅快淋漓的全局浏览。我们将Ganos的矢量快显功能与PGAdmin集成,数据入库即可在线浏览全局,快速评估数据概况,大大增强了数据管理的使用体验。
06 总结
本文从稀疏矢量金字塔的原理与优势入手,介绍了如何利用Ganos实现在线可视化服务的各种功能,并最终通过百行代码实现了一个可以应对亿级数据的地图可视化服务。读者可以进一步在可视化基础上,利用PG/PolarDB Ganos的服务器端快速查询和分析能力进行对象属性查询、空间圈选、空间分析等更复杂功能。这就是Ganos所带来的大规模空间图形显示加速黑科技——稀疏矢量金字塔索引带来的变革。如果您对此有兴趣,更多信息可以参考官方文档。