原文地址
前言
首先,自学习地址库这个名字是我随便取的,含义就是通过各种渠道获取经纬度点和所在区域的联系,记录在数据库中,然后通过各种算法去除噪点、校正地址,范围切分来实现由对于某个地球位置基于不同特征进行解释。在区域数据足够多的情况下,就可以做到给定任意经纬度做出回应:在小区特征下,这个地点是甲小区,在河流特征下,该地点不沿河流,在地铁特征下,该地点距离最近的地铁站100米等等
看起来是不是很像监督学习,通过已有动物的照片来分析一张新的照片,但是我们这里的实现逻辑要简单很多了
技术方案
之前给公司搭建的自学习地址服务是带有公司业务逻辑在的,所以如果需要开源需要将业务逻辑抽离,并且写入大量参数和抽象,工作量还是比较大的。目前抽象工作还在完善当中,这里先介绍简化的技术方案,不会涉及太多的实现细节,主要介绍工作流程
项目架构方面
抽象后的结果现阶段是打算做成一个可以直接部署的服务,部署后其他服务可以通过http
调用到该服务,和其他项目一样,我这里的使用的还是Java17
,考虑到一般服务器不会带Java17
而且目前基本都是容器部署,我会在项目中添加Dockerfile
以及docker-compose.yml
文件,当然项目镜像也会上传到docker
仓库,嫌麻烦的小伙伴也可以直接从仓库下载,参考docker-compose.yml
配置即可。至于如果想要注册到自己的注册中心里面的话,后期会考虑支持consul
和nacos
项目实现方面
插入
首先,需要用到一个我之前写的工具包
<dependency>
<groupId>com.astercasc</groupId>
<artifactId>geo-data-sebastian</artifactId>
<version>${version}</version>
</dependency>
这包已经放在了公共仓库,主要是为了综合三种地图包以及对外提供工具接口,三种地图包包括postgis
,googleS2
以及jts
,他们三个的主要作用分别是对接数据库,生成CellId
提高查询效率,复杂图形计算。对外提供了包括求两地球点距离,判断多边形,构建多边形,判断点在区域内,根据点求凹面多边形等等工具算法
然后是数据库选择,我们这里选择是postgresql
,因为他这里有专门做地图的插件库postgis
,mysql
也可以做地图但是在效率和实现上个人认为没有postgis
做的好,可以通过postgresql
增加插件的方式也可以直接下载postgis
的镜像构建,这里推荐第二种
之后是就是核心技术方案了,首先我们需要5张核心表,分别为特征配置表,点表,点贡献表,区域表,区域点特征关联表
- 特征配置表:特征是指河流、道路、街区、山脉等等可以形成区域的标记,该表用于配置各种区域参数,比如是否允许区域重合,区域的去噪点参数,区域的形成参数,当插入一个点的时候,必须要获取该点属区的特征才能进行计算
- 点表:用于存储点信息,包含行政区划、经纬度、道路、权重等信息,权重作用于去除噪点算法以及在查询时候过滤低权重数据保证业务真实性
- 点贡献表:用于权重积累和记录,可以在特征配置中选择同用户是否可以重复贡献相同点来提高点权重
- 区域表:用于存储区域信息,行政规划,道路,名称、特征,区域范围,中心点、权重等,区域权重由有效点权重累加而得
- 区域点特征关联表:记录某个点在某个特征下属于某个区域,当服务收到一条记录时,必然包含一个区域信息一个特征和至少一个点信息,比如
(46.591463834, 125.150457784)
以及(46.591463834, 125.150457888)
在街区特征下属于东北石油大学
所以我们在增加点的时候一般的数据传入为
{
"pointData": [
{
"detail": "东北石油大学5号门",
"street": "发展路199号",
"divisionCode": "230603",
"province": "黑龙江省",
"city": "大庆市",
"district": "龙凤区",
"lat": 46.59127759855117,
"lng": 125.15564864108842
}
],
"baseData": {
"rangeName": "东北石油大学",
"rangeStreet": "高新技术开发区发展路199号",
"rangeType": "neighbourhood",
"rangeConfig": "{}",
"rangeTag": 1
},
"weight": 50,
"contributorId": "YU1"
}
逻辑处理流程,先根据特征neighbourhood
在配置表将配置读出,根据其写入点表和点贡献表,区域表和区域点特征关联表,在写入后我们需要校正区域表,去除噪点,重算中心点,和同特征其他区域联合推演校正等等
简单演示下校正效果,首先插入一些数据
这里中国内地图由于保密原因可能会有一定偏移,如果小伙伴们是从google
地图等国外地图上取点,由于地图数据位置偏移导致很可能取到的经纬度也是错的,所以取点的话最好从卫星地图取点测试,先保证取到的经纬度肯定是最对的。如果需要展示在页面上那么前端要根据使用的地图包做出相应偏移计算,这个计算网络上代码很多这里就不过多赘述了,关键词WGS84
,GCJ02
在抛出噪点之后(我这里没有写入噪点,噪点的处理是根据权重和参数来去除的,参数包括特征最长半径距离,最短相邻距离等),根据配置中的凹面比率生成范围,我们就可以得到在neighbourhood
特征下,东北石油大学的大致范围了
我们会发现,范围覆盖在右边缺了一角,这个时候,只要我们再增加一个点
即可获取更正确的范围定位
查询方案1
对于googleS2
的应用,我们这里主要是为了提升查询效率。对于s2
的理解,小伙伴可以参考他们的官网,同样不再赘述。地图中比较常规的查询一般就是
- 查看点属于哪个区域
- 传入一个不定范围(可以是圆形可以是方形可以是不定多边形)查范围内所有的点
对于点属于哪个区域,对于我们来说就是,点在某个特征下,在哪个区域。我们在插入时候求得区域唯一最小cellId
的值,然后对值做索引,通过前缀比较确定满足条件的所有区域,然后在业务或者后续查询中再做一次过滤即可
对于区域内所有点,我们需要通过s2
算法将不定范围切分成若干cellId
,然后通过S2CellId::rangeMin
以及S2CellId::rangeMax
拿到所有cellId
最大值和最小值,对点的cellId
做索引,拿到数据库中所有在这个多组cellIdMin-cellIdMax
范围中的点,同样需要在业务或者后续中再做一次校正过滤
这两个方案都是通过cellId
简化查询,粗略地查询所有满足条件的数据,然后再做精准过滤,大幅减少查询时间,即使是没有行政区划的帮助,也可以在海量数据中完成快速查询
查询方案2
细心的小伙伴可能发现,我们上面种种方案起始压根没有用到PostGis
中的功能和函数,甚至如果全部使用googleS2
处理,我们都不需要空间数据库,直接普通数据库也能做,就是不好看数据罢了。这里简单介绍第二种方案,基于R-Tree
空间索引的空间函数数据库操作,具体R树原理和PostGis
空间函数详解这个网上一大把这就不深入了,就给出上面说的两种常见检索的SQL
select * from block b where ST_Contains(b.site_scope, 'SRID=4326;POINT(120.10278239438777 30.278106708516432)')
select * from village v where ST_Within(v.site_center, 'SRID=4326;POLYGON((120.09658926283012 30.316541421521414,120.1414512412157 30.30014706527925,120.1213085011113 30.269528420798494, 120.09658926283012 30.316541421521414))')
postgis-intro-index,其实严格来说PostGis
并没有使用R-Tree
而是基于GiST
的R-Tree
实现,即R-Tree-Over-GiST
,我们可以从postgis-faq中了解到
9.12.Why aren’t PostgreSQL R-Tree indexes supported?
Early versions of PostGIS used the PostgreSQL R-Tree indexes. However, PostgreSQL R-Trees have been completely discarded since version 0.6, and spatial indexing is provided with an R-Tree-over-GiST scheme.
Our tests have shown search speed for native R-Tree and GiST to be comparable. Native PostgreSQL R-Trees have two limitations which make them undesirable for use with GIS features (note that these limitations are due to the current PostgreSQL native R-Tree implementation, not the R-Tree concept in general):
- R-Tree indexes in PostgreSQL cannot handle features which are larger than 8K in size. GiST indexes can, using the “lossy” trick of substituting the bounding box for the feature itself.
- R-Tree indexes in PostgreSQL are not “null safe”, so building an index on a geometry column which contains null geometries will fail.
- GiST (Generalized Search Trees) indexes break up data into “things to one side”, “things which overlap”, “things which are inside” and can be used on a wide range of data-types, including GIS data. PostGIS uses an R-Tree index implemented on top of GiST to index GIS data.
这两种执行方案本项目会基于不同场景混合使用,小伙伴们也可以根据自己需求选用
最后
整个工程抽象没有完成,完成后会贴出公共仓库位置,并会附带一个比较详细的说明文档
同时附上国外测试用例东京大学的实现样例,国外地图取点就无所谓了,怎么取怎么对
计算后可获得