作者 | 李鹤
01 引言
如果对矢量数据进行预切片,数据要切多久?切多少级合适?存储瓦片的硬盘空间够用吗?
如果使用实时瓦片,实时渲染瓦片的响应时间能保证吗?
如果使用矢量瓦片,小比例尺的瓦片可能会有多大体积?传输会不会成为瓶颈?前端渲染能承受多大的数据量?
02 技术特性解读
金字塔创建快:Ganos利用空间索引对数据在空间上进行密集度划分,根据密集度建立一种稀疏矢量金字塔索引,相比传统切图流程减少了90%的数据计算量。同时,创建金字塔采用了完全并行处理模式,即使1亿条地类图斑数据生成金字塔也仅需耗费约10分钟时间。
数据展现快:Ganos采用了视觉可见性剔除算法,根据Z-order排序,过滤掉大量不影响显示效果的数据,从而加快实时显示的效率。Ganos支持直接输出PNG格式的栅格瓦片和MVT格式的矢量瓦片,1亿地类图斑数据实时渲染显示的响应时间都达到秒级。
节省磁盘空间:1亿条地类图斑数据生成金字塔索引仅仅占据原表5%大小的额外空间。
节省开发时间:仅使用简单的SQL语句,通过调整语句参数即可灵活控制显示效果。
03 使用步骤
CREATE EXTENSION ganos_geometry_pyramid CASCADE;
3.1 建立稀疏矢量金字塔
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}')
3.2 获取栅格瓦片
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,即纯透明。
ST_AsPng('points_geom', '1_2_1','{"point_size": 5,"line_width": 2,"line_color": "#003399FF","fill_color": "#6699CCCC","background": "#FFFFFF00"}')
3.3 获取矢量瓦片
bytea ST_Tile(cstring name, cstring key);
name:金字塔名。在本例中为表名_矢量字段名。
key:瓦片索引行列号,Z_X_Y的形式。
ST_Tile('points_geom', '1_2_1');
04 实战案例
4.1 测试数据
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)))
id|geom |
--|------------------------------|
1|POINT (113.5350205 22.1851929)|
2|POINT (113.5334245 22.1829781)|
4.3 服务端代码
# -*- 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)
针对栅格瓦片,可以在通过改变代码进行样式控制,灵活性大大增强。
无需引入第三方的其他组件,也不需要进行针对性优化,就有令人满意的响应性能。
可以任意选择使用者熟悉的编程语言与框架,也无需复杂专业的参数配置,对非地理从业者更加的友好。
4.4 用户端代码
<!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 栅格瓦片的动态效果
函数计算镜像加速:从分钟到秒的跨越
多中心容灾实践:如何实现真正的异地多活?

