博客 / 詳情

返回

基於Ganos百行代碼實現億級矢量空間數據在線可視化

簡介: 本文介紹如何使用RDS PG或PolarDB(兼容PG版或Oracle版)的Ganos時空引擎提供的數據庫快顯技術,僅用百行代碼實現億級海量幾何空間數據的在線快速顯示和流暢地圖交互,且無需關注切片存儲和效率問題。

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。

  1. 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格式的圖片。

  1. 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所帶來的大規模空間圖形顯示加速黑科技——稀疏矢量金字塔索引帶來的變革。

作者:李鶴
原文鏈接
本文為阿里雲原創內容,未經允許不得轉載

user avatar tigerandflower 頭像 nihaojob 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.