前言须知

本章讲解如何使用Geohash查找附近的位置,以及如何解决Geohash区域边缘问题。

一个Geohash代表的是一个区域,一个Geohash字符串代表一个区域(如一个正方形),同一区域内的Geohash相同,不同区域的Geohash不同。

因相邻区域的Geohash不同,所以会有边缘问题:如范围2.4km的Geohash直接匹配则查不到相邻区域相距100米的目标。

本篇使用九宫格方法来解决边缘问题。

特别提醒:Geohash是根据经纬度计算直线距离,而大多地图导航软件会根据目标之间的路线计算路径距离,所以会有差距。

干货

1.加载依赖–使用开源项目Geohash的发行jar包

项目Github地址:https://github.com/kungfoo/geohash-java

发行jar包的Maven仓库地址:https://mvnrepository.com/artifact/ch.hsr/geohash

pom依赖

1
2
3
4
5
<dependency>
<groupId>ch.hsr</groupId>
<artifactId>geohash</artifactId>
<version>1.4.0</version>
</dependency>

2.计算geoHash字符串

本篇从地图上选了四个地点,以一个示例来讲计算方法及说明相关问题:

以下代码使用示例地图上的四个点的经纬度生成对应的geoHash字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import ch.hsr.geohash.GeoHash;
import org.junit.Test;

public class JTest {

@Test
public void test() {
//geoHash在5位时范围±2.4km
int numberOfCharacters = 5;

//和平饭店(用户所在地址)121.495787,31.244966
GeoHash geoHash = GeoHash.withCharacterPrecision(31.244966, 121.495787, numberOfCharacters);
String geoHashBaseCode = geoHash.toBase32();
System.out.println("和平饭店(用户所在地址)geoHash:" + geoHashBaseCode);

//东方明珠塔(相距和平饭店直线距离1km)
GeoHash geoHash2 = GeoHash.withCharacterPrecision(31.245414, 121.506379, numberOfCharacters);
String geoHashBaseCode2 = geoHash2.toBase32();
System.out.println("东方明珠塔(相距和平饭店直线距离1km)geoHash:" + geoHashBaseCode2);

//东新大厦(相距和平饭店直线距离1.3km)
GeoHash geoHash3 = GeoHash.withCharacterPrecision(31.234512, 121.488695, numberOfCharacters);
String geoHashBaseCode3 = geoHash3.toBase32();
System.out.println("东新大厦(相距和平饭店直线距离1.3km)geoHash:" + geoHashBaseCode3);

//房地大厦(相距和平饭店直线距离1.7km)
GeoHash geoHash4 = GeoHash.withCharacterPrecision(31.242358, 121.477928, numberOfCharacters);
String geoHashBaseCode4 = geoHash4.toBase32();
System.out.println("房地大厦(相距和平饭店直线距离1.7km)geoHash:" + geoHashBaseCode4);

//和平饭店(用户所在地址)周围八宫格
GeoHash[] adjacent = geoHash.getAdjacent();
for (GeoHash g : adjacent) {
System.out.println("和平饭店(用户所在地址)周围八宫格geoHash:" + g.toBase32());
}
}

}
输出为:
1
2
3
4
5
6
7
8
9
10
11
12
和平饭店(用户所在地址)geoHash:wtw3s
东方明珠塔(相距和平饭店直线距离1km)geoHash:wtw3u
东新大厦(相距和平饭店直线距离1.3km)geoHash:wtw3s
房地大厦(相距和平饭店直线距离1.7km)geoHash:wtw3s
和平饭店(用户所在地址)周围八宫格geoHash:wtw3u
和平饭店(用户所在地址)周围八宫格geoHash:wtw3v
和平饭店(用户所在地址)周围八宫格geoHash:wtw3t
和平饭店(用户所在地址)周围八宫格geoHash:wtw3m
和平饭店(用户所在地址)周围八宫格geoHash:wtw3k
和平饭店(用户所在地址)周围八宫格geoHash:wtw37
和平饭店(用户所在地址)周围八宫格geoHash:wtw3e
和平饭店(用户所在地址)周围八宫格geoHash:wtw3g
  • 说明:
    相信有些读者可能注意到:
    丛示例的输出可以看出“和平饭店”与“东方明珠塔”相距1km,geoHash却不同;
    “和平饭店”与“东新大厦”、“房地大厦”相距更远,其geoHash却相同;
    ↑ 这就是下一段落要说的边缘问题。

GeoHash可利用字符串前几位匹配的规则查找相距不超过一定距离的目标;

  • 字符位数与距离对应表:
    geoHash位数 距离(km)
    1 ±2500
    2 ±630
    3 ±78
    4 ±20
    5 ±2.4
    6 ±0.61
    7 ±0.076
    8 ±0.019

上例中使用5位数的geoHash及锁定了±2.4km附近的位置。

即用2.4km范围内的所有地点的经纬度计算所得的5位geoHash都是一致的
因此可以根据这个特点做附近地点的高效检索:

从数据库高效检索附近地点

1.将所有地点的geoHash计算出;
2.将各地点的geoHash在数据库的表中保存为一列geoHash并建立索引
Tips:数据库里geoHash应建立索引,且应为BTree索引。因为Hash索引不支持模糊匹配;
可直接使用如下语句获取同一个geoHash区域中的附近地点

1
where geoHash like 'wtw3s%'

3.geoHash区域边缘问题

可能已经发现,既然geoHash代表的是一个区域,那必然会产生边缘性问题:
如上例中的用户所在地“和平饭店”,距离“东方明珠塔”只有1km,二者geoHash却不同。
这是因为两者处于两个geoHash区域的边缘。所以直接匹配一个字符串会出现问题。

  • 笔者使用九宫格的想法:获取目标位置周围八个宫格,从数据库检索获取九个宫格中所有的目标,再在Java应用中逐个进行目标距离计算比较,根据距离约束排除在距离外的目标即可。
    上述距离计算使用经纬度可直接计算两个目标的直线距离:
    详细计算方法可参照我的另一篇分享:根据经纬度计算两者距离 (笔者尽快更新)

4.结语

如有疑问欢迎联系博主,谢谢!