先来一张动图感受一下。
2D矢量快显是由Ganos的快显引擎提供的对亿级二维矢量数据(点、线、面)的可视化功能。现有系统支持矢量数据可视化,主要是采用离线切片的方式,当可视化请求到来时,将预先建好的切片经过简单处理后返回给用户系统。这种处理方案有两大痛点:一个是“慢”,大规模数据集上运行离线切片往往需要十几个小时甚至几天的时间才能完成;另一个是“大”,在最高支持16层级缩放的地图服务中,需要事先存储几十亿的切片,存储开销很大。另外由于“慢”这个问题,还衍生出了对数据更新不友好这个新的问题。这个问题在数据管理能力较弱的GIS系统中尤为明显,由于缺乏对数据更新内容、及其对切片影响范围的感知,一旦发生数据更新,哪怕只是小范围的局部更新,也只能采用耗时费力的全盘重建切片的方式来解决。
Ganos的2D矢量快显功能能够很好地解决以上问题,Ganos创新性地提出了稀疏金字塔索引,跳过对数据稀疏区域的切片构建,结合数据库查询优化并通过视觉可见性剔除算法,过滤掉大量不影响显示效果的数据,一举解决了切片时间漫长和存储开销巨大的两大痛点。不仅如此,Ganos还支持稀疏金字塔的动态更新。当矢量数据发生了小范围内的局部更新时,Ganos可以自动识别出显示效果发生变化的切片,将切片的更新限制在尽量小的范围内,避免了需要推翻稀疏金字塔重建,具有更好的更新效率。实测在一台配置普通的8核PolarDB for PG公共云实例上,给包含七千万条房屋的数据集构建稀疏金字塔,仅需6分钟,当发生一百万条以上的数据更新时,稀疏金字塔的更新也仅需不到1分钟。响应可视化请求时,平均返回一个切片的时间不到1毫秒。拥有如此高效的效率,但是所需要的磁盘存储空间却只需3GB左右。本文将结合实例帮助读者快速上手体验Ganos的矢量快显功能。
2D矢量快显使用步骤
准备数据表
首先需要准备一个包含Geometry属性的数据表。Ganos提供了丰富的函数可用于将矢量数据写入到数据表中,包括常见的ST_GeomFromText、ST_GeomFromWKT、ST_GeomFromWKB等。要使用Ganos的矢量快显功能,只需要数据表包含主键id和Geometry属性即可,十分简单。在开始使用Ganos的快显功能之前,先需要使用如下命令创建Ganos的快显功能的扩展:
CREATE EXTENSIION ganos_geometry_pyramid CASCADE;
执行如下命令创建一个只包含主键id和geom列的数据表:
CREATE TABLE try_ganos_viz(id SERIAL NOT NULL, geom Geometry);
用户可以使用脚本或其它工具将一个矢量数据集导入该表,只需要该脚本或工具能够生成类似如下的SQL命令即可(假设数据集文件存储的是4326坐标系的WKT格式数据):
INSERT INTO try_ganos_viz(geom) VALUES (ST_GeomFromText('WKT FORMAT TEXT', 4326));
上述命令中,是将数据以4326坐标系格式存储到数据库中,用户也可以指定存储3857坐标系格式。导入所有数据后,还需要对Geometry属性列构建一个空间索引,可执行如下命令:
CREATE INDEX ON try_ganos_viz USING gist(geom);
到此,就完成了数据表的准备,可以开始体验矢量快显功能了。
构建稀疏金字塔
构建稀疏金字塔功能已经被封装成自定义函数,方法原型如下:
boolean ST_BuildPyramid(cstring table_name, cstring geom_col_name, cstring id_col_name, cstring config);
上述第一个参数表示矢量数据所在的表格的名字,第二个参数表示Geometry的列名,第三个参数表示id的列名,第四个参数指定构建金字塔时的配置,配置信息是以JSON格式指定,可以为空,接下来会以示例的形式做更多介绍。
执行以下命令,创建一个使用默认参数配置的稀疏金字塔:
SELECT ST_BuildPyramid('try_ganos_viz', 'geom', 'id', '');
用户可以在config参数中指定构建稀疏金字塔时的并行度,执行如下命令以32并行度构建矢量金字塔:
SELECT ST_BuildPyramid('try_ganos_viz', 'geom', 'id', '{"parallel":32}');
和PostGIS一样,Ganos也允许用户定义切片的像素大小和裁切范围,这是通过在config参数中提供"tileSize"和“tileExtend”的值来实现的,示例如下:
SELECT ST_BuildPyramid('try_ganos_viz', 'geom', 'id', '{"parallel":32, "tileSize":4096, "tileExtend":128}');
Ganos允许用户根据其对性能和存储开销的综合考量,用参数控制稀疏金字塔的结构。若用户相对于存储开销更加看重查询性能,可以通过设置较小的"splitSize"值(如果一个切片中的要素小于splitSize,则该切片是在查询时动态构建的),使得尽量多的切片都是事先已经构建好的;若用户更加看重存储开销,除了可以设置较大的“splitSize”值以外,还可以设置较小的"maxLevel"控制金字塔的高度,从而减少需要维护的切片的数量。下面的示例,构建了一个高度为10,"splitSize"值为1000的稀疏金字塔:
SELECT ST_BuildPyramid('try_ganos_viz', 'geom', '{"maxLevel":10, "splitSize":1000}');
除了上述常用的config参数以外,Ganos还提供了“buildRules”支持用户非常灵活地控制稀疏金字塔的构造规则。比如用户希望在层级小于等于5的切片中只可视化面积大于100的要素,则可以使用如下命令:
SELECT ST_BuildPyramid('try_ganos_viz', 'geom', '{"maxLevel":10, "buildRules":[
{"level":[0,1,2,3,4,5], "value":{"filter": "ST_Area(geom)>100"}}
]}');
在上个例子中,“filter”对应的过滤条件可以是任意SQL语句中位于WHERE后面的条件语句。
更多关于ST_BuildPyramid函数的参数信息,请参考官方文档。
除了ST_BuildPyramid函数,Ganos新增ST_BuildPyramidUseGeomSideLen函数构建矢量金字塔。ST_BuildPyramidUseGeomSideLen相对于ST_BuildPyramid进行了性能优化,在数据表中含有很多面积很小的要素时,能够有效提升稀疏金字塔的构建效率。预要使用ST_BuildPyramidUseGeomSideLen函数,数据表中需要新增一列实数类型的属性,用于记录geom列的ST_XMax(geom)-ST_XMin(geom)和ST_YMax(geom)-ST_YMin(geom)的较大值,同时还需要针对该属性列构建一个索引。以上述的try_ganos_viz表为例,使用以下语句新增一个max_side_len属性列,并对该列构建一个B树索引:
ALTER TABLE try_ganos_viz
ADD COLUMN max_side_len DOUBLE PRECISION;
CREATE OR REPLACE FUNCTION add_max_len_values() RETURNS VOID AS $$ DECLARE t_curs CURSOR FOR SELECT * FROM try_ganos_viz; t_row usbf%ROWTYPE; gm GEOMETRY; x_min DOUBLE PRECISION; x_max DOUBLE PRECISION; y_min DOUBLE PRECISION; y_max DOUBLE PRECISION; BEGIN FOR t_row IN t_curs LOOP SELECT t_row.geom INTO gm; SELECT ST_XMin(gm) INTO x_min; SELECT ST_XMax(gm) INTO x_max; SELECT ST_YMin(gm) INTO y_min; SELECT ST_YMax(gm) INTO y_max; UPDATE try_ganos_viz SET max_side_len = GREATEST(x_max - x_min, y_max - y_min) WHERE CURRENT OF t_curs; END LOOP; END; $$ LANGUAGE plpgsql;
SELECT add_max_len_values();
CREATE INDEX ON try_ganos_viz USING btree(max_side_len);
在调用ST_BuildPyramidUseGeomSideLen时,需要提供新增列的列名,其余参数和ST_BuildPyramid相同,其函数原型如下所示:
boolean ST_BuildPyramidUseGeomSideLen(cstring table, cstring geom_field, cstring geom_side_len_field, cstring fid, cstring config);
执行下列语句,对新增了max_side_len列的try_ganos_viz表构建稀疏金字塔:
SELECT ST_BuildPyramidUseGeomSideLen('try_ganos_viz', 'geom', 'max_side_len', 'id', '{"parallel":32}');
ST_BuildPyramidUseGeomSideLen同样支持各类config参数,允许用户灵活地调整稀疏金字塔的构建规则。
更新金字塔
在数据表的数据发生数据更新时,Ganos提供了ST_UpdatePyramid函数对金字塔进行更新,用户只需提供数据更新的外包框范围即可,其函数原型如下:
boolean ST_UpdatePyramid(cstring table, cstring geom_field, cstring id_field, BOX2D update_extent, cstring rules) ;
其中update_extent和rules分别表示数据更新所在的外包框范围和JSON格式的更新参数值。rules参数包括updateBoxScale和sourceSRS,其含义如下
• 参数updateBoxScale会影响到更新自底向上最高传递到哪一层,如updateBoxScale=2,用户指定的更新区域的最大宽高值为100时,只会更新到层次为1的切片(假设全局范围是全球经纬度),因为层次为1的切片的最大宽高值除以100小于2,而层次为0的切片的最大宽高值除以100大于2。updateBoxScale默认值为10。
• sourceSRS指定参数update_extent的EPSG格式,默认值为4326。
假设用户在[(lon1, lat1), (lon2, lat2)]的经纬度范围内插入了多条数据,如果用户想要所有被影响到的切片都被更新(假设maxLevel=16),可以执行下列语句:
SELECT ST_UpdatePyramid('try_ganos_viz', 'geom', 'id', ST_MakeEnvelope(0,-10,20,30, 4326), '{"updateBoxScale":100000}');
在上述例子中假设lon1=0, lat1=-10, lon2=20, lat2=30。
如果用户并不想要全局大规模的更新,可以指定一个较小的updateBoxScale参数值,避免较小层级的切片也被更新,执行下列语句更新只比更新范围稍大的切片及其层级以下的切片:
SELECT ST_UpdatePyramid('try_ganos_viz', 'geom', 'id', ST_MakeEnvelope(0,-10,20,30, 4326), '{"updateBoxScale":2}');
调用ST_UpdatePyramid时,不需要指定并行度。函数会自动采用调用ST_BuildPyramid或ST_BuildPyramidUseGeomSideLen时提供的并行值。由于更新金字塔需要涉及稀疏金字塔的更新、旧切片的删除,以及新切片的生成等步骤,在发生大范围的数据更新时,建议直接调用ST_BuildPyramid或ST_BuildPyramidUseGeomSideLen对金字塔进行重建。
获取矢量切片
矢量切片具有矢量保留要素信息的优点,在地图服务中,多级缩放相比于传统的栅格切片更加平滑,视觉效果更优。Ganos提供了ST_Tile函数,允许用户实时调用获取矢量切片,其函数原型如下:
bytea ST_Tile(cstring name, cstring key);
bytea ST_Tile(cstring name, int x, int y, int z);
第一个函数原型中的key为'z_x_y'的格式,表示切片的编号,z表示切片所在的层级,x表示x轴方向的编号(从左到右递增),y表示表示y轴方向的编号(从上到下递增)。执行以下语句之一,返回中国所在的切片z=1,x=1,y=0:
SELECT ST_Tile('try_ganos_viz', '1_1_0');
SELECT ST_Tile('try_ganos_viz', 1, 0, 1);
获取栅格切片
Ganos同样支持仍然广泛使用的栅格切片。栅格切片是图片形式的切片,相对于矢量切片,不支持客户端动态渲染,对客户端系统的性能要求较低。Ganos提供了ST_AsPng函数,允许在数据库端将矢量数据按需动态渲染为栅格切片,再返回给用户。该函数提供了最基础的栅格符号化能力,更多面向一些不需要复杂符号化的轻量级场景。ST_AsPng的函数原型如下:
bytea ST_AsPng(cstring name, cstring key, cstring style);
bytea ST_AsPng(cstring name, int x, int y, int z, cstring style);
ST_AsPng函数相对于ST_Tile函数新增了style参数。该参数是JSON格式,指定了渲染样式,包含的渲染样式如下:
• point_size: 点大小,单位为像素
• line_width: 线宽,指定组成线和面要素的外包框的线的像素宽度
• line_color: 线渲染颜色,对线要素和面要素的外包框起作用。前六位为16进制颜色,后两位为16进制透明度
• fill_color: 填充颜色,对面要素起作用
• background: 背景色,通常设置为FFFFF00,即纯透明
调用以下语句返回切片编号为'1_1_0'的栅格切片,该切片按照提供的渲染参数进行渲染并以PNG图片格式返回:
SELECT ST_AsPng('try_ganos_viz', '1_1_0', '{"point_size":5, "line_width":2, "line_color":"#003399FF",
"fill_color":"#6699CCCC", "background":"#FFFFFF00"}');
基于2D矢量快显的Web地图服务
上述内容介绍了如何使用Ganos的2D矢量快显功能,接下来我们讲解如何基于Ganos的矢量快显功能,快速搭建一个Web地图服务。
全栈架构
该Web服务由数据库、Python服务端和用户端三部分组成,全栈架构图如下所示:
数据库
需要导入数据到数据库中,并构建好稀疏金字塔所需要的索引,具体内容参考前文。
服务器端代码
为了代码简洁,更侧重于逻辑的描述,我们选择了Python(兼容Python3.6及以上版本)作为后端语言,Web框架使用了基于Python的Flask(使用pip install flask进行安装)框架,数据库连接框架使用了基于Python的Psycopg2(使用pip install psycopg2进行安装)。
我们在后端首先建立了矢量金字塔,其后分别实现了两个接口,矢量切片接口使用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=DB_NAME user=USER_NAME password=PASSWORD host=YOUR_HOST port=PORT_NO"
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")
定义表名,geom字段名、id字段名,用户可按照自己的情况进行修改
pngViewer = PngViewer(CONNECTION, 'buildings', 'geom', 'id')
@app.route('/vector/png///')
def vector_png(z, x, y):
png = pngViewer.get_png(x, y, z)
return Response(
response=png,
mimetype="image/png"
)
定义表名,geom字段名、id字段名,用户可按照自己的情况进行修改
mvtViewer = MvtViewer(CONNECTION, 'points', 'geom', 'id')
@app.route('/vector/mvt///')
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命令即可启动服务。
从以上代码可以看出,无论我们使用何种语言、何种框架,我们只需将访问2D矢量或栅格切片的SQL语句封装为接口即可实现完全相同的功能。相比发布传统的地图服务,借助Ganos的2D矢量快显功能实现在线可视化是更加轻量好用的选择:
• 针对栅格瓦片,可以在通过改变代码进行样式控制,灵活性大大增强。
• 无需引入第三方的其他组件,也不需要进行针对性优化,就有令人满意的响应性能。
• 可以任意选择使用者熟悉的编程语言与框架,也无需复杂专业的参数配置,对非地理从业者更加的友好。
用户端代码
我们选用Mapbox作为前端地图框架,展示服务器端提供的矢量切片层和栅格切片层,并为矢量切片层配置了渲染参数。为了方便说明,前端代码同时添加了矢量、栅格两个图层,实际使用时可以按需选择。
在后端代码的同一文件目录下新建名为Vector.html的文件,写入以下代码,在服务器端服务启动后,就可以通过http://localhost:5000/vector访问了。
<!DOCTYPE html>