矢量金字塔

本文介绍了矢量金字塔模型的用途、基本构成和快速入门等内容。

模型用途

简介

矢量金字塔模型是为了大规模空间几何数据(千万级以上)快速显示而设计的一种结构。矢量金字塔可以对空间几何数据创建稀疏索引、按规则对密集区域预处理、输出标准的mvt-pbf格式。通过Ganos提供的矢量金字塔功能,可实现亿条空间几何记录,分钟级预处理,终端秒级显示的效果。

Ganos Geometry Pyramid是对象关系型数据库PostgreSQL兼容版本(PolarDB PostgreSQL版(兼容Oracle))的一个时空引擎扩展,Geometry Pyramid(矢量金字塔)通过对一个包含Geometry属性列的表格构建矢量金字塔,用户可以快速查询得到地图不同层级的矢量瓦片并用于前端绘制查看。和传统的地图切片方案相比,Ganos 的矢量金字塔具有“快”和“省”的优势:

  • “快”是指构建效率高,实测在一台配置普通的8核PolarDB PostgreSQL版(兼容Oracle)集群上,仅需6分钟即可完成构建七千万房屋面数据的矢量金字塔。

  • “省”是指存储开销小,矢量金字塔忽略数据量稀疏的区域,仅存储包含数据量较多的矢量瓦片,有效地节省了存储成本。

功能概述

矢量金字塔的使用分为创建、查询、更新,以及删除四个部分。

  • 创建:使用ST_BuildPyramid函数或ST_BuildPyramidUseGeomSideLen函数对包含Geometry对象的表格创建矢量金字塔,要求已经有Geometry列的空间索引。

  • 查询:使用ST_Tile函数或ST_AsPNG函数查询由参数指定的地图瓦片,其中ST_Tile返回的是MVT格式的矢量瓦片,ST_AsPNG返回的是渲染成PNG格式的图片。

  • 删除:使用ST_DeletePyramid函数删除已有的矢量金字塔。

  • 更新:使用ST_UpdatePyramid函数对数据表发生更新的矢量金字塔进行更新。

功能详情请参见Geometry Pyramid SQL参考

主要业务场景

Geometry对象可以表示现实世界的空间数据实体,如道路、建筑物、POI 等。可视化Geometry对象能够展示不同空间数据实体在地图上的位置分布,更加直观地向用户展示空间数据包含的信息。矢量金字塔可用于任意将数据保存为Geometry对象且需要高效的数据可视化的应用场景。下面分别从可视化点、线、面三种不同类型的Geometry数据来举例说明矢量金字塔的实际应用。

  • 可视化POI、轨迹点

    在地图或移动对象应用场景中,需要处理的数据(POI或者轨迹点)主要是以Geometry Point的形式保存在数据库中。通过在数据集上预建矢量金字塔,用户可快速查看POI或者轨迹点在不同区域的密集程度,以此确定不同区域的繁忙程度,效果图如下所示:

    gp_point.gif

  • 可视化道路、航道线

    在出行应用场景中,道路和航运公司的航道线主要是以Geometry Linestring的形式保存在数据库中。通过矢量金字塔,用户可以快速查看不同道路和航道线路的分布情况,从而更好地规划出行路线,效果图如下所示:

    gp_line.gif

  • 可视化房屋、河流、林地

    在城市规划场景中,房屋、河流和林地这些数据常以Geometry Polygon的形式保存在数据库中。矢量金字塔可以让用户快速查看城市或更大范围内的建筑、河流等分布情况,助力用户做出决策,效果图如下所示:

    gp_poly.gif

基本构成

概念

矢量切片

矢量切片是指把待显示的数据的矢量信息写入名为矢量瓦片的载体上,写入的信息包括矢量的类型(点、线或者面)、组成矢量的各个点在矢量瓦片上的相对坐标等。用户的前端软件(浏览器或者GIS软件)能够把矢量瓦片里的矢量信息提取出来,把每个矢量根据用户自定义的显示样式(点或者线的颜色、面的填充色等)绘制出来。通俗的理解是,矢量瓦片告诉前端软件应该给用户看哪些东西,然后前端软件根据用户指定的绘画风格一笔一画地画出来。由于现在的硬件发展,用户端软件能够高效率地完成矢量瓦片的绘制,因此矢量切片因其显示效果好的优点受到越来越多用户的青睐。

MVT

MVT(Mapbox Vector Tile)是一套广泛采用的用于存储和传输矢量瓦片的格式,其定义了对一组矢量要素及其属性信息的编码方法。MVT的内部结构包含一组命名的图层。每个图层包含几何要素和元数据信息,其中几何要素部分包含几何类型的编码、坐标的编码、命令的编码(MoveTo、LineTo、ClosePath等)等,元数据信息部分以键值对的形式分别记录属性名和属性值。主流的前端软件都支持MVT,因此本文后面会使用MVT来代指矢量瓦片。

动态矢量瓦片

动态矢量瓦片是指数据库在执行用户的可视化请求时,在线完成读取待可视化的数据和将数据打包成MVT并返回的过程。以PostGIS为例,整个过程大致分为三步:

  1. 根据矢量瓦片的空间范围进行空间查询,从数据表中拿到待可视化的Geometry对象。

  2. 将获得的Geometry对象按照矢量瓦片的空间范围进行坐标转换,其中还涉及到对Geometry对象的简化和过滤,这个过程由ST_AsMVTGeom函数完成。

  3. 将上一步的众多Geometry对象按照MVT规范编码、打包放到一个二进制结构里,这个过程由ST_AsMVT函数完成。

Ganos的快显引擎针对上述三个步骤都提供了相关函数来改进PostGIS的可视化效率:

  • 使用ST_IsRandomSampled函数仅读取随机采样后的Geometry对象。

  • 使用ST_AsMVTGeomEx函数过滤转换到MVT坐标系后像素数很少的Geometry对象。

  • 使用ST_AsMVTEx函数过滤可视化后视觉上不重要的Geometry对象。

用户可根据需要使用其中的一个或多个函数来提升动态矢量瓦片的效率。

预切片

预切片是指离线生成MVT并将其保存起来,在用户的可视化请求到来时返回相应的MVT给用户。

稀疏金字塔

Ganos的矢量金字塔技术采用了稀疏金字塔的结构,将预切片和动态矢量瓦片结合起来,仅在数据密集的区域离线生成和保存MVT,而数据稀疏的区域采用动态矢量瓦片。下图是稀疏金字塔的示意图。只有包含两个或以上Geometry对象的MVT会被保存起来,在查询这些MVT时,Ganos可以直接从表中读出对应的MVT返回给用户。灰色的区域表示数据稀疏区域,执行可视化查询时,Ganos会动态生成这些区域的MVT。

image

渲染

前端软件接收到MVT后,需要将MVT包含的信息绘制成用户可看的图像,这个过程被称为渲染。MVT的渲染通常是由前端软件来完成的,而Ganos的矢量金字塔技术既支持发送MVT交由前端软件渲染,也支持在数据库端将MVT渲染成图片后再交给前端软件直接给用户查看。

流程

矢量金字塔的使用流程为创建矢量金字塔和查询矢量金字塔。如果创建金字塔后,数据表发生了更新,在查询前需要先更新矢量金字塔。

对比

和矢量金字塔相比,动态瓦片无需事先构建任何其它结构即可使用(出于性能考虑,建议对Geometry列构建空间索引),但是在执行可视化请求时会多出读取瓦片范围内的Geometry数据以及在线生成MVT的开销。另外,动态瓦片无需考虑数据更新问题,而矢量金字塔在数据更新后,需要调用ST_UpdatePyramid函数进行更新。建议用户先使用动态瓦片确认性能是否符合需求,若性能不符合预期,可考虑使用矢量金字塔。

快速入门

简介

快速入门文档帮助用户快速理解 Ganos GeomGrid 引擎的基本用法,包括扩展创建、创建金字塔、读取瓦片数据、更新金字塔、高级功能等部分。

更多专业用法可参考Geometry Pyramid最佳实践文章:Ganos 矢量快显功能上手

同时Ganos还针对动态MVT瓦片提供了增强能力,具体用法可参考最佳实践文章:Ganos 矢量快显功能上手系列 2:增强的 MVT 能力

语法说明

  • 创建扩展。

    CREATE EXTENSION ganos_geometry_pyramid CASCADE;
    说明

    建议将扩展安装在public模式下,避免权限问题。

    CREATE extension ganos_geometry_pyramid WITH schema public;
  • 为空间表创建金字塔。

    -- 为数据表 test 创建金字塔
    -- 指定表 test 的要素id字段名, 必须为int4/int8类型
    -- 指定表 test 的空间字段名称, 需要先为该字段创建空间索引
    -- 以JSON形式说明Geometry的坐标系为EPSG:4326
    SELECT ST_BuildPyramid('test', 'geom', 'id', '{"sourceSRS":4326}');
  • 从金字塔中读取MVT数据。

    -- 从金字塔中读取瓦片编号为 '0_0_0' 的数据(任意编号,不管金字塔中是否存在,都可返回数据)
    -- 瓦片编号方式为: z_x_y,默认投影坐标系为EPSG:3857
    SELECT ST_Tile('test', '0_0_0');
  • 更新金字塔。

    当数据表的数据发生更新时,需要对金字塔进行更新才能看到更新后的地图可视化结果。用户需要以参数形式传入发生数据更新的空间范围,调用ST_UpdatePyramid函数更新金字塔。

    -- 在局部空间范围内插入3条新的Geometry对象
    INSERT INTO test(id, geom) VALUES (1, ST_GeomFromEWKT('SRID=4326;POINT(10.1 10.1)'));
    INSERT INTO test(id, geom) VALUES (2, ST_GeomFromEWKT('SRID=4326;LINESTRING(10.1 10.1,11 11)'));
    INSERT INTO test(id, geom) VALUES (3, ST_GeomFromEWKT('SRID=4326;POLYGON((10 10,11 11,11 12,10 10))'));
    
    -- 更新矢量金字塔,数据更新范围由一个Box2D参数指定
    SELECT ST_UpdatePyramid('test', 'geom', 'id', ST_SetSRID(ST_MakeBox2D(ST_Point(9, 9), ST_Point(12, 12)), 4326));

    ST_UpdatePyramid函数适用于数据表的更新发生在一个局部空间区域内的应用场景。例如,更新区域的面积不足全局面积的百分之一,此时ST_UpdatePyramid函数能够高效完成金字塔的更新。若数据表发生大空间范围的数据更新,建议使用ST_BuildPyramid函数重新生成新的金字塔。

    -- 插入3条分布在不同区域的Geometry对象
    INSERT INTO test(id, geom) VALUES (4, ST_GeomFromEWKT('SRID=4326;POINT(-59 -45)'));
    INSERT INTO test(id, geom) VALUES (5, ST_GeomFromEWKT('SRID=4326;LINESTRING(110 60,115 70)'));
    INSERT INTO test(id, geom) VALUES (6, ST_GeomFromEWKT('SRID=4326;POLYGON((-120 59,-110 65,-110 70,-120 59))'));
    
    -- 重建金字塔,ST_BuildPyramid会自动删除旧的金字塔
    SELECT ST_BuildPyramid('test', 'geom', 'id', '{"sourceSRS":4326}');
  • 删除金字塔(可选)。

    SELECT ST_DeletePyramid('test');
  • 删除扩展(可选)。

    DROP EXTENSION ganos_geometry_pyramid CASCADE;

使用进阶

为金字塔命名

金字塔默认和数据表同名,也可指定金字塔名称,实现一份数据,多个金字塔的目的。

-- 为 test 表创建一个名为 hello 的金字塔
SELECT ST_BuildPyramid('test', 'geom', 'id', '{"name": "hello"}');

并行构建

  • 指定构建矢量金字塔的并行任务数,默认为0(表示并行最大化)。

  • 并行任务数最大不应该超过CPU数量的4倍。

  • 并行构建使用了两阶段事务机制,需要设置max_prepared_transactions参数。

  • 设置数据库参数max_prepared_transactions = 100 (或者更高,重启生效)。

-- 使用4个并行任务来构建矢量金字塔
SELECT ST_BuildPyramid('test', 'geom', 'id','{"parallel": 4}');

瓦片参数

指定瓦片的尺寸、外扩大小。 尺寸最大值为4096,且必须是256的整数倍。 外扩大小最大为256,最小为0。

海量数据全幅显示,应该设置较小的tileSize,提升分块渲染并行度和出图体验。

-- 指定瓦片的大小为 512,外扩大小为 8
SELECT ST_BuildPyramid('test', 'geom', 'id','{
                        "tileSize": 512,
                        "tileExtend": 8
                      }');

金字塔的最大层级

当地图的zoom层级大于某级时,就不再使用这个图层、或者不需要生成金字塔,可指定金字塔的最大层级。 如不设置,矢量金字塔会根据数据的密度自动计算出合理的最大层级。最大级别的默认值为16。

-- 指定金字塔的最大层级为 12 级,超过12级则会实时读取数据生成 mvt
SELECT ST_BuildPyramid('test', 'geom', 'id', '{"maxLevel": 12}');

分层处理

  • 可为金字塔的每个层级设置不同的处理条件,例如显示字段、过滤条件等。

  • 通过添加buildRules规则,为每个层级设置生成条件。

  • 顶层金字塔生成耗时较长,可以通过分层规则跳过顶层的处理。

-- 分层处理生成金字塔
-- 第0层到第5层,不显示任何数据,设置过滤条件为 "1!=1" 生成空的mvt
-- 第6层到第9层,显示code=1的数据,并且包含"name"字段
-- 第10到第15级,无过滤条件,包含"name"、"width"两个字段
SELECT ST_BuildPyramid('test', 'geom', 'id', '{
                        "buildRules":[
                            {
                                "level":[0,1,2,3,4,5],
                                "value": {
                                    "filter": "1!=1"
                                }
                            },
                            {
                                "level":[6,7,8,9],
                                "value": {
                                    "filter": "code=1",
                                    "attrFields": ["name"]
                                }
                            },
                            {
                                "level":[10,11,12,13,14,15],
                                "value": {
                                    "attrFields": ["name", "width"]
                                }
                            }
                        ]
                       }');

提升金字塔的构建和更新效率

除了ST_BuildPyramid函数,Ganos还提供了ST_BuildPyramidUseGeomSideLen函数来构建金字塔。和ST_BuildPyramid函数相比ST_BuildPyramidUseGeomSideLen函数能够有效提升金字塔的创建和更新效率。ST_BuildPyramidUseGeomSideLen函数的使用条件是数据表存在一个表示Geometry属性的x轴或y轴跨度的较大值的列,且为该列创建了一个索引。

-- 为表格'test'增加一列'geom_side_len',表示'geom'属性的x轴或y轴跨度的较大值
ALTER TABLE test
ADD COLUMN geom_side_len DOUBLE PRECISION;

CREATE OR REPLACE FUNCTION add_max_len_values() RETURNS VOID AS $$
DECLARE
  t_curs CURSOR FOR
    SELECT * FROM test;
  t_row test%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 test
      SET geom_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();

-- 为'geom_side_len'属性构建B树索引
CREATE INDEX ON test USING btree(geom_side_len);

-- 指定表示'geom'属性的x轴或y轴跨度的较大值的列名为'geom_side_len'
SELECT ST_BuildPyramidUseGeomSideLen('roads', 'geom', 'geom_side_len', 'id',
  '{"sourceSRS":4326}');

高级功能

数据按规则融合

可按属性规则对瓦片范围内的数据进行融合,减少数据量。

设置 "buildRules" -> "value" -> "merge" 字段,添加["code=1","code=2"]两个条件表达式。

对属性code的值为1的进行融合,连接空间上有touch关系的点,并合并成一个大的要素。

同样过程对code值为2的进行融合。

对code值既不等于1、也不等于2的要素不采用融合操作。

-- 将 code=1、code=2 的数据,分别进行融合处理
SELECT ST_BuildPyramid('test', 'geom', 'id', '{
                        "buildRules":[
                          {
                            "level":[0,1,2,3,4,5],
                            "value": {
                              "merge": ["code=1","code=2"]
                            }
                          }
                        ]
                      }');

SQL参考

详细SQL手册请参见Geometry Pyramid SQL参考