GeoHash算法原理
1.基本原理
GeoHash算法采用将经纬度网转化成一个个小区域,为落在相同区域中的点生成同样的GeoHash字符串,通过将经纬度二维数据转化成一维的字符串,简化了对地理位置操作的复杂性。
如下图所示,一片区域被分割成9块,落在相同区域内的点有着相同的GeoHash字符串。通过这种划分,我们可以根据点所对应的GeoHash字符串来判断两点是否在同一区域或者相邻区域内。
2.区域的划分方式
在地理上,纬度的范围为-90°~90°,经度的范围为-180°~180°。我们可以从纬度和经度两个维度上对空间进行二分,若点落在二分后的左区间则记为0,若点落在二分后的右区间则记为1。
我们以地点(纬度39.928167,经度116.389550)为例,Geohash算法遵循以下步骤:
- 对纬度区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,39.928167属于右区间[0,90],所以标记为1;
- 将区间[0,90]进行二分为[0,45),[45,90],可以确定39.928167属于左区间 [0,45),给标记为0;
- 递归上述的划分过程,39.928167总是属于某个区间[a,b]。随着每次迭代区间[a,b]逐渐缩小,其区域将越来越接近纬度39.928167这条线;
- 左区间则记为0,右区间则记为1,在我们进行10次划分后可以得到纬度序列1011100011。
我们用同样的方法对经度范围[-180,180]进行递归划分后得到经度序列1101001011。序列的的长度由我们定义的精度所确定,序列越长则区域越小,所表示的位置也会更精确。
3.组合和转换
通过上一步的区域划分我们得到了纬度序列1011100011,经度序列1101001011。现在我们对两个序列进行组合,我们在偶数位(包括0)放经度,奇数位放纬度进行交叉合并,最后得到经纬度所对应的二进制串11100 11101 00100 01111。
在完成组合后我们将得到的二进制串中每五个字符一组转换成base32编码的字符。base32编码规则如下所示:
完成转换后,11100 11101 00100 01111被转换为wx4g,即得到了最后的4位精度GeoHash字符串。
4.精度与实际距离
精度越高,则GeoHash字符串所表示的地理区域范围越小,用GeoHash字符串表示的地点位置越精确。具体精度表示范围如下表:
精度 位数 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
宽度 | 50009.4 km |
1,252.3 km |
156.5 km |
39.1 km |
4.9 km |
1.2 km |
152.9 m |
38.2 m |
4.8 m |
1.2 m |
长度 | 624.1 km |
156 km |
19.5 km |
4.9 km |
609.4 m |
152.4 m |
19 m |
4.8 m |
59.5 cm |
附近的人功能实现方式
根据上述的GeoHash算法步骤,我们已经可以给每个用户匹配一个其所在位置的GeoHash字符串,通过设置精度后查询拥有相同GeoHash字符串的用户即可简单实现附近的人功能。
但此方法有一个不足之处:若两用户所在区域相邻,而且两用户正好位于区域相邻边的附近,就会出现虽然距离接近但是通过GeoHash串无法找到彼此的情况。如下图所示:
为了解决此问题,我们必须找到全部相邻的八个区域及中心区域共九个区域后再寻找附近的人。
在将用户范围缩小到周围包括中心的九个区域后,我们就可以去逐个计算用户间的距离筛选出在指定距离内的用户,最终实现附近的人功能。
代码实现
1.计算经纬度二进制串、合并二进制串、转码得到GeoHash字符串
class GeoHashCreator: LAT_INTERVAL = 180 # 纬度 LNG_INTERVAL = 360 # 经度 LAT_OFFSET = 90 # 纬度偏移量 LNG_OFFSET = 180 # 经度偏移量 PRECISION = 5 # 精度(5->4.9km) # 通过对经纬度偏移让其大于0,方便递归 # 根据经纬度返回GeoHash串 @classmethod def getGeoHashCode(cls, lat, lng): section_num = cls.PRECISION * 5 // 2 # 根据精度反推二进制串长度(递归次数) lng = lng + cls.LNG_OFFSET lat = lat + cls.LAT_OFFSET latCode = cls.getCode(lat, 0, cls.LAT_INTERVAL, section_num) lngCode = cls.getCode(lng, 0, cls.LNG_INTERVAL, section_num) mergeCode = cls.mergeCode(latCode, lngCode, section_num) geoCode = cls.toBase32(mergeCode) return geoCode # 递归划分区间得到单一二进制串 @classmethod def getCode(cls, lng, start, final, count=15): if count==0: return '' middle = (start+final)/2 if lng<middle: return '0' + cls.getCode(lng,start,middle,count-1) else: return '1' + cls.getCode(lng,middle,final,count-1) # 对经纬度二进制串进行合并 @classmethod def mergeCode(cls, latCode, lngCode, count=15): mergeStr = '' index = 0 while index<count: mergeStr += lngCode[index] + latCode[index] index += 1 return mergeStr # 对合并串五个字符一组转换成base32编码 @classmethod def toBase32(cls, mergeCode): BASEDICT = {0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: 'b', 11: 'c', 12: 'd', 13: 'e', 14: 'f', 15: 'g', 16: 'h', 17: 'j', 18: 'k', 19: 'm', 20: 'n', 21: 'p', 22: 'q', 23: 'r', 24: 's', 25: 't', 26: 'u', 27: 'v', 28: 'w', 29: 'x', 30: 'y', 31: 'z'} geoCode = '' codeList = [] index = 0 while index+5 <= len(mergeCode): # 五个字符一组 codeList.append(mergeCode[index: index+5]) index = index + 5 if index < len(mergeCode): codeList.append(mergeCode[index:]) for item in codeList: # 逐个转换 geoCode += BASEDICT[int(item, 2)] return geoCode
2.找到相邻区域
观察划分后邻近区域所对应的经度二进制串和纬度二进制串的特点,我们可以发现只要对中心区域的二进制串进行 -1 和 +1 操作后就可以得到相邻区域的二进制串。如图所示以单个方向为例:
class GeoHashCreator: LAT_INTERVAL = 180 # 纬度 LNG_INTERVAL = 360 # 经度 LAT_OFFSET = 90 # 纬度偏移量 LNG_OFFSET = 180 # 经度偏移量 PRECISION = 5 # 精度(5->4.9km) # 根据经纬度返回邻近区域GeoHash串 @classmethod def getAdjacentList(cls, lat, lng): lng = lng + cls.LNG_OFFSET lat = lat + cls.LAT_OFFSET section_num = cls.PRECISION * 5 // 2 # 根据精度反推二进制串长度(递归次数) # 对纬度和经度二进制串分别+1和-1后得到相邻区域的二进制串 latNum = int(cls.getCode(lat, 0, cls.LAT_INTERVAL, section_num), 2) lngNum = int(cls.getCode(lng, 0, cls.LNG_INTERVAL, section_num), 2) latList = [cls.intToBinStr(latNum - 1, section_num), cls.intToBinStr(latNum, section_num), cls.intToBinStr(latNum + 1, section_num)] lngList = [cls.intToBinStr(lngNum - 1, section_num), cls.intToBinStr(lngNum, section_num), cls.intToBinStr(lngNum + 1, section_num)] # 对纬度和经度二进制串组合后得到9个区域的二进制串,将其转化为geohash码 adList = [] for latCode in latList: for lngCode in lngList: adList.append(cls.toBase32(cls.mergeCode(latCode, lngCode, section_num))) return adList # 需要先将二进制字符串转化为数字加减后再转为字符串,此函数为调整字符串格式 @classmethod def intToBinStr(cls, num, count): binstr = bin(num)[2:] return (count - len(binstr))*'0' + binstr
3.根据经纬度计算用户间距离
class NearbyPeople: EARTH_REDIUS = 6378.137 @classmethod def rad(cls, d): return d * math.pi / 180.0 @classmethod def getDistance(cls, lat1, lng1, lat2, lng2): radLat1 = cls.rad(lat1) radLat2 = cls.rad(lat2) a = radLat1 - radLat2 b = cls.rad(lng1) - cls.rad(lng2) s = 2 * math.asin(math.sqrt(math.pow(math.sin(a / 2), 2) + math.cos(radLat1) * math.cos(radLat2) * math.pow(math.sin(b / 2), 2))) s = s * cls.EARTH_REDIUS return s*1000 # 根据两点经纬度判断两点间距离是否符合范围 @classmethod def judgeDistance(cls, lat1, lng1, lat2, lng2, range=500): distance = cls.getDistance(lat1, lng1, lat2, lng2) return distance < range
4.得到附近的人列表
# 得到附近用户列表 @classmethod def getNearbyPeople(cls, lat, lng, PeopleList): # PeopleList为用户对象集合 nearbyPeopleList = [] adjacentGeoHashList = GeoHashCreator.getAdjacentList(lat, lng) # 得到周围区域geohash码 for People in PeopleList: if People.geohash in adjacentGeoHashList: # 利用用户的geohash码匹配周围区域geohash码 if cls.judgeDistance(People.lat, People.lng, lat, lng): # 判断距离是否符合 distance = cls.getDistance(People.lat, People.lng, lat, lng) nearbyPeopleList.append({'name':People.name, 'distance': distance}) return nearbyPeopleList