作者:李鶴
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
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所帶來的大規模空間圖形顯示加速黑科技——稀疏矢量金字塔索引帶來的變革。如果您對此有興趣,更多資訊可以參考