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