Ganos H3地理网格能力解析与最佳实践

Ganos地理网格模型是一种基于六边形结构的高效地理空间数据处理技术,广泛适用于物流、社交网络、数据分析及应急响应等多种场景。该模型利用独特的六边形网格体系实现更均匀的数据分布及固定的邻居关系,从而优化空间数据分析、路径规划等功能。Ganos地理网格模型支持GeoSOT和H3两种网格类型,具备丰富的编码方式、高性能的查询及聚合分析能力,并能够与几何数据和栅格数据进行有效融合,从而显著提升数据处理效率和存储成本效益。

背景

地理网格是一种用于再现地球表面的多边形网格单元集合,能够有效表示地物在地理空间中的位置信息,并融合其他各类时空数据。地理网格计算通常采用由粗到细的逐级分割方式,对地球表面进行处理。通过将地球的曲面用一定大小的多边形网格进行近似模拟,实现地理空间定位与地理特征描述的有机结合,同时将误差范围控制在网格单元的可接受范围内。每个网格单元都会进行编码,网格与编码是一一对应的。三维地理网格不只考虑经纬度,还把高度维纳入剖分和编码范围。

Ganos地理网格引擎目前涵盖GeoSOT和H3两种地理网格。

  • GeoSOT是中国提出的一套地球空间剖分理论,并在此基础上发展出的一种离散化、多尺度区域位置标识体系。关于GeoSOT网格的最佳实践可参考基于GeoSOT地理网格模型:无人机路径规划能力实践

  • H3是Uber研发的一种覆盖全球表面的二维地理网格,采用的基本网格是正六边形。

H3地理网格设计独特之处在于其采用的六边形结构。相较于传统的四边形或三角形网格,六边形网格具有分布更均匀、邻居关系固定且无方向性等优点,这使得在进行空间数据分析、路径规划、地理编码以及地理围栏等领域时,能够更加精确和高效地组织和查询地理空间数据。利用Ganos地理网格的相关函数,可以将不同的空间范围转换为网格编码,并能够确定网格编码所对应的空间范围、层级及其父子网格关系。Ganos支持退化网格计算(如下图),充分利用网格的层级关系,通过更精简的网格组合对空间范围进行有效表达。此外,Ganos自研的地理网格索引,可用于高效查询网格编码并加速聚合计算。

7

应用场景

H3地理网格技术在诸多业务场景中得到广泛应用,主要包括:

  • 物流与出行服务:基于地理网格开展路线规划、区域覆盖分析、配送范围界定及热点区域发现等功能的建设。

  • 数据分析:基于地理网格进行人口密度分析、移动用户行为分析以及地理市场细分等大数据分析领域的研究。

  • 物联网(IoT):面向智能城市、环境监测和资产追踪等需要实时监控的数据,基于地理网格进行监测数据空间分布分析的场景。

  • 社交网络:基于地理网格构建的面向位置服务(LBS)、好友位置共享、事件通知等社交场景的应用。

  • 应急响应与公共服务:基于地理网格开展灾害分布分析、灾害预警热力图、应急资源分布以及紧急救援区域的划分等工作。

综上,H3地理网格技术为企业和开发者提供了一种强大工具,能够更有效地管理和利用地理空间数据,从而提升与位置相关的决策效率和准确性。

能力解析

Ganos H3地理网格具备多种功能,包括网格输入/输出、网格父子关系判断、网格路径分析和网格查询等。此外,还支持转换为Ganos Geometry类型,以便与其他矢量数据进行空间分析。值得强调的是,Ganos H3地理网格同样支持退化功能,通过更精简的网格组合对空间范围进行表达,从而降低用户因数据编码而带来的数据库存储成本。关于Ganos H3地理网格详细功能,请参考GeomGrid SQL参考

技术优势

相比其他H3开源产品,Ganos H3具有如下技术优势:

  • 支持更为多样化的打码方式,例如可以将Ganos的点、线、面类型直接转换为H3编码。

  • Ganos H3在打码效率与格网查询效率方面进行了大量的性能优化。

  • Ganos H3支持与其他Ganos模型进行联合查询分析,能够将几何类型直接转换为H3编码,或利用H3与栅格模型进行基于格网的像素统计等。

  • Ganos H3基于PolarDB底层的多态分层存储技术,能够实现基于OSS的大规模数据点的打码与存储,从而显著降低存储成本。

最佳实践

本案例采用真实场景数据,为您介绍如何使用Ganos H3地理网格实现空间点数据的入库、打码、查询及最终显示等功能。本案例使用Uber发布的2023年纽约出租车位置数据集FOIL进行测试。

数据导入

  1. 使用Ganos H3地理网格能力前,需要安装ganos_geomgrid插件。

    CREATE EXTENSION ganos_geomgrid CASCADE;
  2. 创建带有h3grid类型的数据表FOIL2013,用于保存FOIL点数据。地理空间模型提供了h3grid字段类型,用于表示H3编码。以下SQL语句中的h3_lev13代表使用的是第13层级的H3编码。H3不同层级网格具有不同分辨率,您可以根据具体业务需求灵活定义。H3各个层级对应的空间分辨率请参考社区文档

    -- 创建表,用于保存foil点数据,h3_lev13代表13级编码
    CREATE TABLE FOIL2013 (
      id text, 
      lon float, 
      lat float, 
      h3_lev13 h3grid);
  3. 数据入库。FOIL文件以CSV格式保存,您可以通过编程方式从CSV中提取相关信息通过SQL入库,也可以通过FDW方式入库。此处使用Ganos FDW模块,使用FDW方式实现数据快速入库。

    1. 将测试数据文件上传至OSS指定路径。详细操作请参考上传文件到OSS

    2. 在测试数据库中安装ganos_fdw插件。

      CREATE EXTENSION ganos_fdw CASCADE;
    3. 创建SERVER,负责管理CSV文件。以下SQL语句中的format'CSV',代表管理的数据格式为CSV。datasource参数信息请参考OSS文件路径

      CREATE SERVER csvserver
        FOREIGN DATA WRAPPER ganos_fdw
        OPTIONS (
          datasource 'OSS://<access_id>:<secrect_key>@[<Endpoint>]/<bucket>/path_to/file.csv',
          format 'CSV' );
      
      CREATE USER MAPPING FOR CURRENT_USER SERVER csvserver OPTIONS (user '<access_id>', password '<secrect_key>');
    4. 通过外部表形式,将OSS上的CSV文件映射到数据库中,以便作为普通表进行查询。详细的SQL语句如下。

      此处选择medallion、pickup_longitude和pickup_latitude三列数据,其映射的外部表名称为trip_data_1:

      CREATE FOREIGN TABLE trip_data_1 (
        medallion varchar,
        pickup_longitude varchar,
        pickup_latitude varchar)
        SERVER csvserver
        OPTIONS ( layer 'trip_data_1' );

      查询外部表:

      SELECT * FROM trip_data_1;
    5. 将外部表数据导入到FOIL2013中。

      INSERT INTO FOIL2013
      SELECT medallion as id ,cast (pickup_longitude as double precision) as lon, cast(pickup_latitude as double precision) as lat FROM trip_data_1;

      查询FOIL2013表确认CSV数据已经成功导入:

      SELECT * FROM FOIL2013;

对象打码

数据入库后,可以对点数据进行编码。Ganos H3提供了多种编码方式,例如通过指定经纬度、标准H3字符串、Integer类型H3编码、二进制类型H3编码,以及直接将Point类型转换为H3等方式。详细内容请参考:

本案例采用ST_H3FromLatLng函数,通过指定经纬度及目标层级,可以直接获得对应的H3编码。以下SQL语句利用FOIL表中的lat和lon字段,生成第13层级的H3编码,并将其保存于h3_lev13列中。随后,通过ST_AsText函数进行具体H3编码的查询。

-- Level 13
UPDATE FOIL2013 SET h3_lev13 = ST_H3FromLatLng(lat,lon,13);

-- 查询
SELECT id,lon,lat,ST_AsText(h3_lev13) AS h3 FROM FOIL2013 LIMIT 100;

网格聚合

地理网格引擎的一个典型应用场景是对空间数据进行基于格网编码的空间聚合统计分析,以获取热力图等专题地图信息。以下SQL语句依据h3_lev13列中的H3编码,从FOIL23表中统计每个网格内的点数量(count(*)):

--按照h3_lev13进行统计
CREATE TABLE h3_count_lev13 AS
SELECT ST_AsText(h3_lev13) AS h3code,count(*) FROM FOIL2013 GROUP BY h3_lev13;

-- 查询统计结果
SELECT ST_AsText(h3_lev13), ST_AsText(geometry),count FROM h3_count_lev13 ORDER BY count DESC;

网格查询

Ganos H3地理网格模型提供了多种基于H3编码的操作。以下SQL语句通过ST_GridDistance函数获取FOIL23中所有与空间位置(40.71481749,-73.99100368)对应的格网距离小于10的格网点:

SELECT * FROM foil2013
WHERE
ST_GridDistance(ST_H3FromLatLng(40.71481749,-73.99100368,13),h3_lev13)<10;

网格可视化

Ganos支持像几何数据一样可视化H3网格,即可以把H3网格转化为矢量瓦片,再由前端渲染查看。Ganos提供原生H3网格MVT函数及相应索引,能够方便地查询和可视化H3网格及其所包含的统计信息。值得一提的是,Ganos支持可视化动态生成的H3网格。例如,当希望可视化层级为10的H3网格时,如果表格中并未保存层级为10的H3网格,可以利用ST_AsMVT和ST_AsMVTGeom(ST_H3FromLatLng(lat, lon, 10), ...)命令动态生成层级为10的H3网格的可视化结果。然而,使用此方法在效率上不及预先保存的H3网格。因此,以下内容将主要介绍如何可视化保存在表格中的H3网格。

  1. 创建H3网格索引(可选,创建索引对可有效提升可视化效率)。

    CREATE INDEX ON h3_count_lev13 USING GIST(h3_lev13);
  2. 根据H3网格获取编号为(14, 4826, 6157)的矢量瓦片。

    SELECT ST_AsMVT(tile) FROM 
      (SELECT ST_AsMVTGeom(h3_lev13, ST_Transform(ST_TileEnvelope(14, 4826, 6157), 4326)) AS grid, count 
      FROM h3_count_lev13 
      WHERE h3_lev13 && ST_Transform(ST_TileEnvelope(14, 4826, 6157), 4326)) AS tile;
  3. 可视化效果展示。前端实时渲染,以下展示在数据库端动态查询H3网格的矢量瓦片的结果。在此过程中,网格的颜色是根据其对应的统计值动态确定的。

    说明

    前端部分仅需一个Python脚本和一个HTML文件。启动时,只需运行该Python脚本,并在浏览器中输入localhost:5100即可查看结果。Python脚本将根据用户在地图上的鼠标位置和缩放级别自动生成相应的SQL查询,并将其发送至数据库,随后将数据库返回的查询结果展示在网页上。具体代码请参考附录

    7

总结

地理网格是移动对象相关应用场景的重要支撑。在与轨迹、矢量、栅格等数据类型的融合过程中,它能够带来极大的业务价值和广阔的想象空间。本文利用Ganos H3地理网格的相关功能,进行矢量数据的聚合、空间关系的判断与可视化。Ganos作为全球首个支持移动对象(MOD)的数据库,相关能力已经在交通、物流、出行、汽车等多个客户侧得到有效验证。相较于传统中间件或业务代码实现电子围栏的方式,Ganos从数据库系统的底层为大规模移动对象提供了时空处理框架,显著提高了计算效率,并在综合成本方面实现了大幅改善。未来,Ganos将继续提供更高效的面向移动对象场景的库内原生分析能力,推动相关领域的空间信息应用全面迈向“在线化”。

附录

  • 可视化前端Python脚本:

    from quart import Quart, send_file, render_template
    import asyncpg
    import io
    import re
    
    ## 数据库连接参数
    CONNECTION = {"host": "YOUR-HOST-NAME-OR-IP", "port": PORT_NO, "database": "DATABASE_NAME",
                  "user": "USER_NAME", "password": "PASSWORD"}
    
    ## 目标表名/字段/ID
    TABLE = "h3_count_lev13"
    H3_COL = "h3_lev13"
    H3_GEOM_COL = "geometry"
    AGG_VAL_COL = "count"
    COL_SRID = 4326
    
    app = Quart(__name__, template_folder='./')
    
    
    @app.before_serving
    async def create_db_pool():
        app.db_pool = await asyncpg.create_pool(**CONNECTION)
    
    
    @app.after_serving
    async def close_db_pool():
        await app.db_pool.close()
    
    
    @app.route("/")
    async def home():
        sql = f'''
        SELECT ST_Extent(ST_Transform(ST_Envelope({H3_GEOM_COL}), 4326))
        FROM {TABLE};
        '''
        async with app.db_pool.acquire() as connection:
            box = await connection.fetchval(sql)
            box = re.findall('BOX\((.*?) (.*?),(.*?) (.*?)\)', box)[0]
            min_x, min_y, max_x, max_y = list(map(float, box))
            bounds = [[min_x, min_y], [max_x, max_y]]
            center = [(min_x + max_x) / 2, (min_y + max_y) / 2]
            return await render_template('./index.html', center=str(center), bounds=str(bounds))
    
    
    @app.route("/h3_mvt/<int:z>/<int:x>/<int:y>")
    async def h3_mvt(z, x, y):
        sql = f'''
        SELECT ST_AsMVT(tile.*)
        FROM
          (SELECT ST_AsMVTGeom({H3_COL}, ST_Transform(ST_TileEnvelope($1,$2,$3),{COL_SRID}), 4096, 512, true) geometry,
           {AGG_VAL_COL} count
          FROM {TABLE}
          WHERE ({H3_COL} && ST_Transform(ST_TileEnvelope($1,$2,$3),{COL_SRID}))) tile'''
        async with app.db_pool.acquire() as connection:
            tile = await connection.fetchval(sql, z, x, y)
            return await send_file(io.BytesIO(tile), mimetype='application/vnd.mapbox-vector-tile')
    
    if __name__ == "__main__":
        app.run(port=5100)

  • 前端页面index.html文件:

    <!DOCTYPE html>
    <html>
    
    <head>
      <meta charset="utf-8">
      <title>map viewer</title>
      <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
      <link href="https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.css" rel="stylesheet">
      <script src="https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.4.2/chroma.min.js"></script>
    </head>
    
    <body>
      <div id="map" style="position: absolute;left:0; top: 0; bottom: 0; width: 100%;cursor:pointer;"></div>
      <div class="counter"
        style="position: absolute;left:2%;font-size: 20px;padding: .1em .1em;text-shadow: 3px 3px 3px black;">
        <span>当前网格计数:</span>
        <span id="count">0</span>
      </div>
      <script>
        let YOUR_TOKEN = "pk.eyJ1Ijoia3pmaWxlIiwiYSI6ImNqbHZueXdlZjB2cG4zdnFucGl1OHJsMjkifQ.kW_Utrh8ETQltRk6fnpa_A"
    
        mapboxgl.accessToken = YOUR_TOKEN;
        const map = new mapboxgl.Map({
          container: "map",
          style: "mapbox://styles/mapbox/navigation-night-v1",
          center: {{ center }},
          zoom: 1
        })
    
        map.on("load", () => {
          map.fitBounds({{ bounds }})
    
          map.on('mousemove', 'h3', (e) => {
            map.getCanvas().style.cursor = "default";
            if (e.features.length > 0)
              document.getElementById('count').innerText = e.features[0].properties.count
          })
          map.on('mouseleave', 'h3', () => {
            map.getCanvas().style.cursor = "grab";
            document.getElementById('count').innerText = 0
          })
          map.addSource("h3_source", {
            type: "vector",
            tiles: [`${window.location.href}h3_mvt/{z}/{x}/{y}`],
            tileSize: 512
          });
    
          // make color map
          const MIN = 1
          const MAX = 600
          const STEP = 10
          color_map = chroma.scale(["#536edb", "#5d96a5", "#68be70", "#91d54d", "#cddf37", "#fede28", "#fda938", "#fb7447", "#f75a40", "#f24734", "#e9352a", "#da2723", "#cb181d"])
            .domain([MIN, MAX]);
          let colors = []
          for (let i = MIN; i < MAX; i += STEP)
            colors.push(color_map(i).hex(), i)
          colors.push(color_map(MAX).hex())
    
          map.addLayer({
            id: "h3",
            type: "fill",
            source: "h3_source",
            "source-layer": "default",
            paint: {
              "fill-color": [
                "step", ["get", "count"],
                ...colors
              ],
              "fill-opacity": 0.8
            }
          });
    
        });
      </script>
    </body>
    
    </html>