H3 是 uber 设计的六边形空间索引,go 语言操作包是 h3-go,可以通过经纬度获取所在的 h3 六边形边界,每个经纬度对应的六边形都是确定的,每个六边形唯一对应了一个 h3index。在业务开发中,我们可以通过 h3index 来对地理空间中的对象做聚合,本质还是将经纬度查询转换成了 h3index 的查询。

维度

不同维度的 h3index 表示不同大小的地理空间,我们可以查看官方介绍来了解对应关系。h3 有 16 个维度,维度越高,表示的面积越小,也就越是精确。下图的对应关系,表示不同维度的h3index 表示地球的组成。

拿维度 15 来说,已经是 h3 能表示的最小单元了。用这个维度来表示地球的话,需要存储 569,707,381,193,162 个该维度的 h3index 索引。后面又列举了其中六边形(hexagon)和五边形(pentagon)的组成个数。为什么组成中还会有五变形呢? 我们应该从设计的根源上去探索解释。


使用 h3-go 包,我们通过指定经纬度、以及维度就可以获取到对应的 h3index 索引。还可以通过 h3index 获取所在六边形的组成点。下面代码中 FromGeo 就返回了对应的 h3index。

geo := GeoCoord{Latitude:  37.775938728915946,Longitude: -122.41795063018799,
}
resolution := 9
fmt.Printf("%#x\n", FromGeo(geo, resolution))

想比较 h3index 能表示的最小单元格个数而言,我们最关心的还是单元格所能表示的面积大小,不同维度的 h3index 究竟能表示多大的范围。

8级网格究竟有多大

现在在地图上可视化查看 8 级网格的大小,主要就是确定8级网格的各个顶点坐标。对应的 sdk 都提供了对应的获取方法,我们选取一个经纬度,绘制这个经纬度所在的 8 级网格。我先在地图上拾取天台公园的坐标:116.410732,39.881836,根据这个坐标来查看对应的8级 h3 网格。

FromGeo 参数1指定位置信息,参数2设置维度为 8 级网格,计算得出对应的 h3index,然后调用 ToGeoBoundary 计算的出网格的各个顶点坐标。然后在网上任意找一个地图坐标的连线工具就可以看查看具体的边界。

geo := h3.GeoCoord{Longitude: 116.410732,Latitude:  39.881836,}
h3Index := h3.FromGeo(geo, 8)
boundary := h3.ToGeoBoundary(h3Index)

这样看 8 级网格还是少点意思,没有距离信息,啥都不是。但要在正六边形上标注出距离,我估计了一下,要花费不少时间,还是乖乖地当一个“调包侠”吧。用现成的API集成工具,查出两点间的距离,上下图对比的来看一下,也很容易看出对应标注的距离。

正六边形边的长度是 491米,两条平行边间的距离是814米,正对着两个点的距离是873米。现在我们去看官方对于 8 级网格平均边长的描述:0.531414010 km,看来这个正六边的边长小于平均值。

扩圈

函数 KRing 方法用来计算 h3 扩圈,扩1圈就是当前当前h3网格按照同维度向外扩一圈,它会返回扩圈范围内的所有h3index。在上述代码的基础上,我们展示针对 h3Index 扩1圈和扩2圈的结果。

 indexs := h3.KRing(h3Index,1)fmt.Println(indexs)indexs := h3.KRing(h3Index, 2)fmt.Println(indexs)

为了让代码展示的更加直观,我专门处理一下返回的 indexs,能够在地图上勾勒出一个个网格图形。上面的代码 Println 也能展示出来,但展示的效果很不直观。我们还是配合地图多边形绘制工具,将扩圈后的网格展示出来。

func drawLine(indexes []h3.H3Index) string {draw := make([]string, 0)for i := range indexes {boundary := h3.ToGeoBoundary(indexes[i])points := make([]string, 0)for j := range boundary {points = append(points, fmt.Sprintf("[%f,%f]", boundary[j].Longitude, boundary[j].Latitude))}jsStr := `var path_%d = [%s]var polygon_%d = new AMap.Polygon({path: path_%d,strokeColor: "#FF33FF",strokeWeight: 6,strokeOpacity: 0.2,fillOpacity: 0.4,fillColor: '#1791fc',zIndex: 50,})map.add(polygon_%d)`draw = append(draw, fmt.Sprintf(jsStr, i, strings.Join(points, ","), i, i, i))}return  strings.Join(draw, "\n")
}

左图是基于中心点 h3index 扩1圈后的效果,右图是扩 2 圈后的效果。扩1圈返回 7 个六边形,扩 2 圈返回 19 个六边形。如果要解决边界边界问题,只需要向外扩一圈就可以了。

H3 设计

将真实世界的 3 维地理空间映射到 2 维的网格体系,是每个地图索引都需要面对的问题。H3 通过球心投影的方式将地球抽象成一个空间正20 面体,但地球本身也不是极致的圆形,抽象肯定会牺牲一部分准确性。

正20面体的每个面都是一个三角形平面,抽象到地图上的话,每个平面应该都是二维的平面。然后就变成了一个圆形的正20面体,就正好可以接受地图的投影。而且,如果把正 20 面体的每个面进行编码,我们可以通过选择正 20 面体,将合适的面对应到合适的地理区域上。


正 20 面体展开到二维平面,就是地球的投影,下面就是其中的一种展开方式。因为有陆地和海洋的区别,而我们要分析的地理数据基本都是在陆地上的。所以,正 20 面体通过合理的转动,可以将它的顶点都落在海洋的区域。为什么要特别处理这些顶点数据呢?


H3网格索引没有直接利用正 20 面体,而是在每个面的基础上继续做了网格拆分。为什么最终选择使用正六边形作为拆分的单元呢,还有没有别的多边形可以选择呢?其实还可以有三角形和正方形,GeoHash 采用的就是正方面的网格,那么正方形网格和正六边形有什么区别吗?

假设一个 n 面体,多面体的内角和计算公式为 (n-2)*180,假设 m 个正多边形在任意顶点出拼接形成一个 360 度才能形成网格,最终计算的表达式:(n-2)*180/n*m=360。如果 n 表示三角形,就需要有 6 个面拼接;如果 n 是正方形,就需要 4 个面拼接,如果 n 是六边形,就需要 3 个面拼接。


如果按照三角形或者四边形的拆分方式,某一个网格单元和临边的网格单元的中心距离是不相同的,如上图。但采用正六边形,网格单元和临边的其它网格单元是相同的。那么,在网格体系中,我们如何获取某个网格单元周边的网格呢?

H3 网格独立于现实中的区域,真实世界的区域边界往往会发生一些变化,区域的边界形态各异,经常还会发生变动调整,如果保持现实地理的区域跟踪,保证区域边界数据动态更新难度可想而知。试想一下,如果我们根据北京的每个区、每个街道进行数据统计,复杂性是非常大的。

而 H3 是在地球基础上的网格系统,它不用关心现实的街道、区域边界变化,无论现实中的地域如何变化,H3 都不会发生变化,也就是将地图上的某一个区域固定化了。如果要统计某个区域的维度信息,只需要先获取该区域的所有 H3 网格,汇总所有网格的信息就可以了。


在 0 级索引下,每个面包含了 10 个单元,如上图,其中边界线上的一些单元会被多个平面共同包含,这个面总共包含了 5.5 个六边形和 3 个三角形。上文提到的,正20面体所包含的 110 个六边形和 12 个五边形就是这么计算的出来的。正20面体包含有 20 个面、30条边、12 个顶点,其中的 12 个顶点就正好对应了这 12 个五边形。

网格关系


上图形象的描述了,2个等级 H3 网格的大小关系,高级别的 H3 网格大概需要 7 个才接近一个低级别的网格,两个级别之间不是完全的包含关系。

Mongo

mongo 自身集成了空间索引的能力,说到底,地理空间的数据都需要被存储和被索引,如何存储位置数据以及如何查询位置数据,是每个业务都要面临的难题。我们专门去看看 mongo 提供了哪些空间能力。

GeoJSON 对象

地理空间中经度和维度可以唯一确定一个位置,这属于位置点信息。市面上很多基于用户位置提供的服务,都是经纬度的一些聚合运算。GeoJSON 是 Mongo 中存储空间数据的对象,这种对象类型的存储模式,我一直都觉得应该是一种趋势的。

最基本的对象,也是地理空间里最基本的概念:点、线、面。参考一下它面的结构,一般来说,面是线段收尾拼接组成的,关键是看看 Mongo 中如何表示面这个结构。下面就是多边形的表示,比较有特点的是首尾点是重复的。

{type: "Polygon",coordinates: [ [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0  ] ] ]
}

想一想,给我们一堆点,我们如何把这堆点两两连接起来,最后形成一个面呢?关键就是看最后一个点是否要明确的声明出来。从这个面的对象也可以看出,Mongo 它支持不规则的面空间,这个多边形完全由用户自己定义。

Geospatial

Mongo 提供了哪些便捷的空间查询呢,既然有了点、线、面的空间对象定义,对应的操作肯定也少不了。

geoIntersects

这个方法会应用在哪些业务场景下,或者说,我们在哪些场景中才需要用到这个方法,是我们关注的重点。函数名称是空间相交,但其实就是圈定一个地理范围,在这个范围内进行其他位置数据查找。

参考文章:

  1. Uber’s Hexagonal Hierarchical Spatial Index
  2. h3geo文档
  3. mongo geojson

Uber H3 index 地图索引思考相关推荐

  1. Uber H3简单介绍

    一.什么是H3? 将地球空间划分成可是识别的单元.将经纬度H3编码成六边形的网格索引. 二.为什么用H3? 2.1 GEOHASH存在一些不足 不同精度下网格的形状不一且精度的变化幅度时小时大 在不同 ...

  2. 如何建立知识管理系统并制作自己的人生地图索引?

    如何建立知识管理系统并制作自己的人生地图索引?简而言之,就是建立自己的"人生一图知识管理系统",顾名思义就是将知识管理.人生一表组合在一起,目标就是用云笔记工具将一辈子的积累通过一 ...

  3. 【地理空间】Uber H3使用

    h3简介 H3把全球经纬度划分成了六边形格子,每个六边形格子都有一个ID,可以调用H3Core中的一系列方法获取到当前经纬度在哪一个格子. 正六边形优点 首先正六边形相邻单元距离相等,且近似圆,不仅自 ...

  4. MySQL force Index 强制索引概述

    以下的文章主要介绍的是MySQL force Index  强制索引,以及其他的强制操作,其优先操作的具体操作步骤如下:我们以MySQL中常用的hint来进行详细的解析,如果你是经常使用Oracle的 ...

  5. Lucene——Field.Store(存储域选项)及Field.Index(索引选项)

    Field.Store.YES或者NO(存储域选项) 设置为YES表示或把这个域中的内容完全存储到文件中,方便进行文本的还原 设置为NO表示把这个域的内容不存储到文件中,但是可以被索引,此时内容无法完 ...

  6. 【实施工程师之家】——mysql四种索引PRIMARY(主键索引)、INDEX(一般索引)、UNIQUE(非空索引)、FULLTEXT(全文索引)应用

    mysql四种索引PRIMARY(主键索引).INDEX(一般索引).UNIQUE(非空索引).FULLTEXT(全文索引)应用 目录 1)PRIMARY: 2)NORMAL: 3)UNIQUE: 4 ...

  7. mysql force_MySQL force Index 强制索引概述

    以下的文章主要介绍的是MySQL force Index  强制索引,以及其他的强制操作,其优先操作的具体操作步骤如下:我们以MySQL中常用的hint来进行详细的解析,如果你是经常使用Oracle的 ...

  8. Mysql数据库(十一)unique index 唯一索引

    Mysql数据库(十一)unique index 唯一索引 唯一索引和普通索引: 1.都是能够加快搜索速度 2.唯一索引中的值不允许重复,普通索引的值允许重复 表contacts 建表语句如下,其中, ...

  9. postgresql 索引状态_PostgreSQL索引思考

    当在看Monetdb列存行只支持IMPRINTS和ORDERED这两种索引,且只支持定长数值类型时,就在思考,对于列存,还有必要建索引吗?在PostgreSQL的索引就要灵活很多,我对常用列建合理的索 ...

最新文章

  1. 【bzoj5100】[POI2018]Plan metra 构造
  2. python升级版本命令-pythonpip命令版本过低问题版本升级问题
  3. python 在python的class中的,self到底是什么?
  4. centos5.3搭建安全高效的LNMP服务器
  5. oracle中常用的时间格式转换
  6. jdbc 批量insert_037 深入JDBC中PreparedStatement对象使用
  7. sql二进制转十进制_了解SQL十进制数据类型
  8. docker容器的标准使用过程_docker容器的使用
  9. javascript的offset、client、scroll使用方法
  10. 【leetcode 简单】第三十八题 两数之和 II - 输入有序数组
  11. EditPlus for python
  12. _beginthread
  13. 有道词典使用离线翻译
  14. python项目打包成whl文件
  15. iconfont 在项目中使用阿里icon
  16. linux版本qq,QQLinux版下载-QQ for Linux下载v2.0.0 最新版-西西软件下载
  17. Day06(上)C++继承和派生
  18. 硬件设计9---什么是示波器?
  19. 3.10Hello,C语言君
  20. 【UI】常见基础知识整理

热门文章

  1. 现在社会趋势下,很多人想做网上商城,那你知道软件部署方式吗?
  2. 组的迷惑行为之 Linux用户的初始用户组(主组,基本组)与有效用户组(附加组)
  3. 什么是软件功能测试,如何做好功能测试?
  4. Claus Hansen加入Entrust Datacard,担任亚太地区和日本销售副总裁
  5. archlinux什么桌面好_ArchLinux搭建高效便捷的平铺式桌面
  6. UCASE() 函数
  7. 凭借近2亿的年销量,小米三年超越苹果的可能性有多大?
  8. 微信数据库最新的解密方式,使用C++代码解密微信加密数据库信息!
  9. mysql随机生成中文姓名_mysql 随机生成姓名函数,及模拟大量测试数据
  10. unity 监听文件夹更新,获取本地图片并显示