基于Ganos百行代码实现亿级矢量空间数据在线可视化

本文介绍如何使用PolarDB PostgreSQL版的时空引擎Ganos提供的数据库快显技术,仅需百行代码即可实现亿级海量几何空间数据的在线快速显示及流畅地图交互,同时无需关注切片存储和效率问题。

背景

一直以来,如何对时空数据库中的亿级矢量空间数据进行在线可视化,始终是业界面临的重大挑战。由于数据体量庞大,传统方法需要将数据库中的数据进行基于缓存切片的服务发布,才能实现可视化。这一过程操作流程冗长,并且涉及众多需要考虑的问题:

  • 在对矢量数据进行预切片时,数据切片所需的时间为多久?切分多少级别为适宜?存储瓦片所需的硬盘空间是否充足?

  • 使用实时瓦片时,实时渲染瓦片的响应时间是否能够得到保证?

  • 使用矢量瓦片时,小比例尺的瓦片体积可能会达到多大?数据传输是否会成为瓶颈?前端渲染能够承受多大数据量?

若需快速浏览数据库中的大规模在线数据,传统用于“底图服务”的离线切片生产流程几乎无法适用,不仅耗时耗力,还无法实现在线联机处理。

PolarDB PostgreSQL版时空引擎Ganos提供了数据库快显技术,仅需百行代码即可实现亿级海量几何空间数据的在线快速显示和流畅地图交互,同时无需关注切片存储和效率问题,解决传统离线切片存在的问题。

技术原理

Ganos在线快显处理的核心在于将数据库与可视化进行了有效关联,提供一种新的可视化索引技术——稀疏矢量金字塔(Sparse Vector Pyramid,SVP)索引。SVP索引具备两个关键特性:快与省。

其中,快指以下两个阶段的高效表现:

  • 金字塔创建快Ganos利用空间索引对数据进行空间密集度划分,并基于密集度建立了一种稀疏矢量金字塔索引,相较于传统切图流程,数据计算量减少90%。同时,金字塔的创建采用了完全并行处理模式,即使是在处理1亿条地类图斑数据时,生成金字塔的时间也仅需约10分钟。

  • 数据展现快Ganos采用视觉可见性剔除算法,基于Z-order排序,过滤掉大量不影响显示效果的数据,从而显著提高实时显示的效率。Ganos支持直接输出PNG格式的栅格瓦片和MVT格式的矢量瓦片,对1亿地类图斑数据的实时渲染显示,其响应时间均达到秒级。

省指以下两个维度:

  • 节省磁盘空间:1亿条地类图斑数据生成的金字塔索引仅占原表大小的5%作为额外空间。

  • 节省开发时间:仅需使用简单的SQL语句,通过调整语句参数即可灵活地控制显示效果。

使用说明

使用Ganos的快显引擎需要安装插件。

CREATE EXTENSION ganos_geometry_pyramid CASCADE;

建立稀疏矢量金字塔

假设您已创建某个矢量大表并导入数据,然后可以使用Ganos的ST_BuildPyramid函数创建矢量金字塔,加速数据显示。详情请参考ST_BuildPyramid

示例

以下SQL语句为表points的geom字段创建矢量金字塔,金字塔名称指定为points_geom,同时将金字塔的逻辑瓦片大小设定为512。

说明

此处指定矢量金字塔的名称及所使用的逻辑瓦片大小(该瓦片大小并非实际存在的瓦片,仅代表一种空间逻辑划分)。

ST_BuildPyramid('points', 'geom', 'gid', '{"name":"points_geom","tileSize":512}')

获取栅格瓦片

栅格瓦片是以图片形式呈现的瓦片(Tile),是目前使用最广泛的地图瓦片形式之一。Ganos的ST_AsPng函数在数据库端提供按需动态渲染矢量数据为栅格瓦片的功能。该功能具备基本的栅格符号化能力,主要适用于一些不需要复杂符号化的轻量级场景,例如数据管理系统。详情请参考ST_AsPng

示例

以下SQL语句从矢量金字塔中获取索引行列号为x=2,y=1,z=1的矢量瓦片,并将该瓦片根据我们配置的样式渲染为栅格瓦片,最终返回PNG格式的图片。

ST_AsPng('points_geom', '1_2_1','{"point_size": 5,"line_width": 2,"line_color": "#003399FF","fill_color": "#6699CCCC","background": "#FFFFFF00"}')

获取矢量瓦片

矢量瓦片是一种新兴的地图瓦片技术,具备在前端配置样式的灵活性,采用WebGL进行渲染,效果更为美观。诸如Mapbox等地图框架可便捷地支持此格式。Ganos的ST_Tile函数能够以矢量瓦片的形式提供矢量金字塔中的数据。详情请参考ST_Tile

示例

调用ST_Tile函数只需提供标准的TMS行列号及金字塔表的名称。以下SQL语句从矢量金字塔中获取索引行列号为x=2,y=1,z=1的矢量瓦片,返回数据将采用标准的MVT格式。

ST_Tile('points_geom', '1_2_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)|

全栈架构

数据库-快显全栈架构由数据库服务器、Python服务端和用户端三部分构成,其全栈架构示意如下图所示。

image

服务端代码

为实现代码的简洁性并更注重逻辑描述,本案例选用了Python作为后端语言,Web框架采用了基于Python的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=<database_name> user=<user_name> password=<user_password> host=<host> port=<port>"


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的矢量金字塔功能实现在线可视化是一种更为轻量且实用的选择。

  • 针对栅格瓦片,能够通过修改代码实现样式控制,从而显著增强灵活性。

  • 无需引入第三方组件,也无需进行专门的优化,即可实现令人满意的响应性能。

  • 支持自由选择其熟悉的编程语言和框架,无需进行复杂的专业参数配置,对于非地理从业者更加友好。

用户端代码

本案例选择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>

效果展示

矢量瓦片动态效果

  • 2

  • 在前端调节不同效果。调整新的图层参数后,效果如下:

    {
      "circle-radius": 4,
      "circle-color": "#000000",
      "circle-stroke-width": 2,
      "circle-opacity": 0.3,
      "circle-stroke-color": "#003399",
      "circle-stroke-opacity": 0.9,
    }

    2

栅格瓦片动态效果

2

与pgAdmin集成

PostgreSQL数据库管理工具pgAdmin原生支持矢量数据的可视化,但由于缺乏快速渲染技术,仅能实现单个对象显示或有限结果集的展示,无法对大规模矢量数据进行流畅的全局浏览。将Ganos的矢量快显功能与pgAdmin进行集成,实现在数据入库后在线浏览全局,快速评估数据概况,从而显著提升数据管理体验。

2

总结

基于稀疏矢量金字塔的原理与优势,可以利用Ganos实现在线可视化服务的各种功能,最终通过百行代码实现可以应对亿级数据的地图可视化服务。在可视化基础上,还可以进一步利用云原生数据库PolarDBGanos的服务器端快速查询和分析能力进行对象属性查询、空间圈选、空间分析等更复杂功能。这就是Ganos所带来的大规模空间图形显示加速技术——稀疏矢量金字塔索引所引发的变革。如果您对此有兴趣,请参考使用简介获取更多信息。您可以前往PolarDB集群购买页面开通PolarDB服务。