一、前言
最近有個需求,要計算出客戶座標附近5公里的所有門店,並按照步行距離排序。
最直接的方法就是遍歷該城市下的所有門店,但是該方法明顯不可取,因為在門店數量巨大,且還需要計算步行距離並排序的情況下,時間複雜度過高。
突然想到當年做影像遇見一個問題:給定一個視訊中連續的三千幀,但是已經亂序,告訴你第一幀,將這三千幀進行排序。遍歷影像的所有畫素點同樣不可取,當時的解決方案是利用感知雜湊計算出所有影像的指紋,接著利用明氏距離計算距離第一張最近的影像作為第二張,然後計算距離第二張最近的作為第三張,以此類推。
同樣,肯定也有雜湊方法可將地理位置轉換成某種編碼,並且編碼可用於地理計算。它就是 GeoHash。
二、相關知識
進入正文之前,先一起回顧一下初中地理:
本初子午線以西為西經,分十二區,每區15度共180度;以東為東經,同樣分十二區,共180度。
赤道以北為北緯,共90度;以南為南緯,同樣90度。
那麼在計算機中,座標表示為:
西經為負數,東經為正數,因此經度取值
[-180, 180]
。北緯為正數,南緯為負數,因此緯度取值
[-90, 90]
。
我們知道赤道長約四萬公里,因此經度上每度約等於111公里。地球實際上是個不規則球體,但為了簡便計算,我們假設把緯度上每度約等於222公里。
三、初識 GeoHash
1. 計算二進位制編碼
首先計算二進位制編碼,經度上以[-180, 180]
開始,緯度以[-90, 90]
開始,每次將區間進行二分,若輸入座標小於中間值則編碼為0
,下次區間取左半區間;若大於則編碼為1
,下次區間取右半區間。以此類推,編碼越長,越接近座標值,進而越精確。
以 118°04′04, 24°26′46
為例,首先計算經度的編碼:
編碼長度 | 區間 | 中間值 | 編碼 | 說明 | 精度(千米) |
---|---|---|---|---|---|
1 | [-180, 180] | 0 | 1 | 118°04′04 大於 0°,因此編碼1,取右區間 | 19980 |
2 | [0, 180] | 90 | 1 | 118°04′04 大於 90° | 9990 |
3 | [90, 180] | 135 | 0 | 118°04′04 小於 135°,因此編碼0,取左區間 | 4995 |
4 | [90, 135] | 112.5 | 1 | 2497.5 | |
5 | [112.5, 135] | 123.75 | 0 | 1248.75 | |
6 | [112.5, 123.75] | 118.125 | 0 | 624.375 | |
7 | [112.5, 118.125] | 115.3125 | 1 | 312.188 | |
8 | [115.3125, 118.125] | 116.71875 | 1 | 156.094 | |
9 | [116.71875, 118.125] | 117.421875 | 1 | 78.047 | |
10 | [117.421875, 118.125] | 117.7734375 | 1 | 39.024 | |
N | ... | ... | . | ... |
同樣道理,計算緯度得編碼:
編碼長度 | 區間 | 中間值 | 編碼 | 說明 | 精度(千米) |
---|---|---|---|---|---|
1 | [-90, 90] | 0 | 1 | 24°26′46 大於 0°,因此編碼1,取右區間 | 19980 |
2 | [0, 90] | 45 | 0 | 24°26′46 小於 45°,因此編碼0,取左區間 | 9990 |
3 | [0, 45] | 22.5 | 1 | 4995 | |
4 | [22.5, 45] | 33.75 | 0 | 2497.5 | |
5 | [22.5, 33.75] | 28.125 | 0 | 1248.75 | |
6 | [22.5, 28.125] | 25.3125 | 0 | 624.375 | |
7 | [22.5, 25.3125] | 23.906 | 1 | 312.188 | |
8 | [23.906, 25.3125] | 24.609 | 0 | 156.094 | |
9 | [23.906, 24.609] | 24.2575 | 1 | 78.047 | |
10 | [24.2575, 24.609] | 24.433 | 0 | 39.024 | |
N | ... | ... | . | ... |
綜上,假設我們只取十位編碼,經度上得到得編碼為 1101001111
,緯度上編碼為 1010001010
。
將兩個編碼就像經緯交織網一樣進行交織:
最後得到經緯二進位制編碼為11100110000011101110
,結合以上兩表的精度資料,我們知道該編碼其實代表的是一塊長寬為39.024千米的矩形塊。
2. 轉換base32編碼
二進位制編碼其實就可以用來作為地理位置編碼,但是:
- 不便於查詢周邊鄰塊。計算鄰塊,需要解析成經、緯兩個編碼再做計算。而採用base32編碼後,可用查表法,加速計算。
- 二進位制編碼長度過長,不利於檢索。
因此,GeoHash 採用了 base32 和 base36編碼,因為大多數採用 base32 編碼,因此本文僅介紹 base32 編碼。
Decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Base 32 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | B | C | D | E | F | G |
Decimal | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Base 32 | H | J | K | M | N | P | Q | R | S | T | U | V | W | X | Y | Z |
base32 編碼共 32 個編碼,因此需要 5 個bit,將11100110000011101110
轉換為 base32 編碼:
WS7F
。
四、計算鄰近塊
得到了編碼後,如何計算鄰塊?網上大部分文章僅講訴如何利用查表法計算鄰塊,但是這個查表如何得到呢?
根據前面,我們知道編碼由5個bit二進位制,並經緯交織組成。因此,若該編碼處於奇數位上,即 經 緯 經 緯 經
交織方式;若處於偶數位上,則 緯 經 緯 經 緯
交織方式。
那麼,以 W
為例,二進位制為 11100
,若處於奇數位則在表中為 右 上 右 下 左
,若處於偶數位則在表中為 上 右 下 左 下
,因此 W
在查表中的位置如下圖:
WS7F
為例,現在要求該位置的鄰近塊,取末位F
,從偶數位表中可以看到,F
的鄰近為E, G, D, 9, C
,以及往右出了界的 5, 4, 1
。因此5, 4, 1
三個鄰塊還需要看倒數第二位 7
。從奇數表中可以看到,7
的右邊沒有超界(若超界了,看倒數第三位,以此類推),為K
。
因此,得到周圍鄰塊分別位 WS7E, WS7G, WS7D, WS79, WS7C, WSK5, WSK4, WSK1
,位置關係如下:
五、總結
主要從理論上介紹:
- GeoHash對地理位置點進行編碼的方法:根據“遞迴二分”獲取二進位制,將二進位制轉換為 Base32 編碼。
- GeoHash鄰塊的快速計算方法:取末位編碼,根據偶數位表查詢對應鄰塊編碼,若某方向出界,則查詢前一位編碼,並根據奇/偶表查詢相應方向的編碼。
具體實現後繼釋出。
六、延申
-
GeoHash 不僅可以用來對位置點進行編碼,也可以用來對面進行編碼,有助於處理點、面的多種組合關係計算。比如,判斷點位置是否在門店的配送圍欄之內。
-
GeoHash 其實就是 Peano 曲線 的一種應用,如下:
還有許多空間填充曲線,比如公認比較好的,沒有較大突變的 Hilbert 曲線。