GeoHash算法原理
GeoHash是目前比較主流實現位置服務的技術,Geohash算法將經緯度二維數據編碼爲一個字符串,本質是一個降維的過程
樣例數據(基於15次區域分割)
位置 | 經緯度 | Geohash |
---|---|---|
北京站 | 116.433589,39.910508 | wx4g19 |
天安門 | 116.403874,39.913884 | wx4g0f |
首都機場 | 116.606819,40.086109 | wx4uj3 |
GeoHash算法思想
我們知道,經度範圍是東經180到西經180,緯度範圍是南緯90到北緯90,我們設定西經爲負,南緯爲負,所以地球上的經度範圍就是[-180, 180],緯度範圍就是[-90,90]。如果以本初子午線、赤道爲界,地球可以分成4個部分。
GeoHash的思想就是將地球劃分的四部分映射到二維座標上。
[-90˚,0˚)代表0,(0˚,90˚]代表1,[-180˚,0)代表0,(0˚,180˚]代表1
映射到二維空間劃分爲四部分則如下圖
但是這麼粗略的劃分沒有什麼意義,想要更精確的使用GeoHash就需要再進一步二分切割
通過上圖可看出,進一步二分切割
將原本大略的劃分變爲細緻的區域劃分,這樣就會更加精確。GeoHash算法就是基於這種思想,遞歸劃分的次數越多,所計算出的數據越精確。
GeoHash算法原理
GeoHash算法大體上分爲三步:1. 計算經緯度的二進制、2. 合併經緯度的二進制、3. 通過Base32對合並後的二進制進行編碼。
- 計算經緯度的二進制
//根據經緯度和範圍,獲取對應的二進制
private BitSet getBits(double l, double floor, double ceiling) {
BitSet buffer = new BitSet(numbits);
for (int i = 0; i < numbits; i++) {
double mid = (floor + ceiling) / 2;
if (l >= mid) {
buffer.set(i);
floor = mid;
} else {
ceiling = mid;
}
}
return buffer;
}
上述代碼numbits爲:private static int numbits = 3 * 5; //經緯度單獨編碼長度
也就是說將地球進行15次二分切割
注: 這裏需要對BitSet類進行一下剖析,沒了解過該類的話指定懵。
瞭解BitSet只需了去瞭解它的set()、get()方法就足夠了
- BitSet的set方法
/**
* Sets the bit at the specified index to {@code true}.
*
* @param bitIndex a bit index
* @throws IndexOutOfBoundsException if the specified index is negative
* @since JDK1.0
*/
public void set(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
int wordIndex = wordIndex(bitIndex);
expandTo(wordIndex);
words[wordIndex] |= (1L << bitIndex); // Restores invariants
checkInvariants();
}
set方法內wordIndex(bitIndex)
底層將bitIndex右移6位然後返回,ADDRESS_BITS_PER_WORD
爲常量6
/**
* Given a bit index, return word index containing it.
*/
private static int wordIndex(int bitIndex) {
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
set方法內的expandTo(wordIndex)
只是一個判斷數組是否需要擴容的方法
/**
* Ensures that the BitSet can accommodate a given wordIndex,
* temporarily violating the invariants. The caller must
* restore the invariants before returning to the user,
* possibly using recalculateWordsInUse().
* @param wordIndex the index to be accommodated.
*/
private void expandTo(int wordIndex) {
int wordsRequired = wordIndex+1;
if (wordsInUse < wordsRequired) {
ensureCapacity(wordsRequired);
wordsInUse = wordsRequired;
}
}
set內重要的一行代碼words[wordIndex] |= (1L << bitIndex)
,這裏只解釋一下|=
a|=b
就是a=a|b,就是說將a、b轉爲二進制按位與,同0爲0,否則爲1
- BitSet的get方法
/**
* Returns the value of the bit with the specified index. The value
* is {@code true} if the bit with the index {@code bitIndex}
* is currently set in this {@code BitSet}; otherwise, the result
* is {@code false}.
*
* @param bitIndex the bit index
* @return the value of the bit with the specified index
* @throws IndexOutOfBoundsException if the specified index is negative
*/
public boolean get(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
checkInvariants();
int wordIndex = wordIndex(bitIndex);
return (wordIndex < wordsInUse)
&& ((words[wordIndex] & (1L << bitIndex)) != 0);
}
get方法用一句話概括就是:如果傳入的下標有值,返回true;反之爲false
以天安門座標爲例:39.913884, 116.403874
BitSet latbits = getBits(lat, -90, 90);
BitSet lonbits = getBits(lon, -180, 180);
// 緯度
for (int i = 0; i < numbits; i++) {
System.out.print(latbits.get(i) + " ");
}
// 經度
for (int i = 0; i < numbits; i++) {
System.out.print(lonbits.get(i) + " ");
}
緯度經過轉換爲:
true false true true true false false false true true false false false true false
轉爲二進制:
1 0 1 1 1 0 0 0 1 1 0 0 0 1 0
經度經過轉換爲:
true true false true false false true false true true false false false true true
轉爲二進制:
1 1 0 1 0 0 1 0 1 1 0 0 0 1 1
- 合併經緯度二進制
合併原則:經度佔偶數位,緯度佔奇數位。也就是說經緯度交替合併,首位0位置爲經度的0位置
合併後二進制編碼爲:
11100 11101 00100 01111 00000 01110
- 使用Base32對合並後的經緯度二進制進行編碼
- 代碼實現
// Base32進行編碼
public String encode(double lat, double lon) {
BitSet latbits = getBits(lat, -90, 90);
BitSet lonbits = getBits(lon, -180, 180);
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < numbits; i++) {
buffer.append((lonbits.get(i)) ? '1' : '0');
buffer.append((latbits.get(i)) ? '1' : '0');
}
String code = base32(Long.parseLong(buffer.toString(), 2));
return code;
}
本文案例經緯度編碼後
wx4g0f
後續問題
如果要使用此功能實現附近的人。假如紅點爲使用者,經過Geohash算法分割後只會推薦同區域0011
中的綠點,但是如下圖所示,藍色點相對於綠色點更接近用戶,所以區域劃分的弊端就展現在這裏。
針對上述問題,我們可以人爲獲取紅色用戶所在的0011
區域周邊八個區域中的用戶,即獲取0011
的同時還要獲取0100
,0110
,1100
,0001
,1001
,0000
,0010
,1000
- 代碼實現
public ArrayList<String> getArroundGeoHash(double lat, double lon) {
ArrayList<String> list = new ArrayList<>();
double uplat = lat + minLat;
double downLat = lat - minLat;
double leftlng = lon - minLng;
double rightLng = lon + minLng;
String leftUp = encode(uplat, leftlng);
list.add(leftUp);
String leftMid = encode(lat, leftlng);
list.add(leftMid);
String leftDown = encode(downLat, leftlng);
list.add(leftDown);
String midUp = encode(uplat, lon);
list.add(midUp);
String midMid = encode(lat, lon);
list.add(midMid);
String midDown = encode(downLat, lon);
list.add(midDown);
String rightUp = encode(uplat, rightLng);
list.add(rightUp);
String rightMid = encode(lat, rightLng);
list.add(rightMid);
String rightDown = encode(downLat, rightLng);
list.add(rightDown);
return list;
}
然後根據球體兩點間的距離計算紅色用戶與周邊區域用戶距離,從而進行附近的人功能實現
- 通過兩經緯度計算距離java代碼實現
static double getDistance(double lat1, double lon1, double lat2, double lon2) {
// 經緯度(角度)轉弧度。弧度用作參數,以調用Math.cos和Math.sin
double radiansAX = Math.toRadians(lon1); // A經弧度
double radiansAY = Math.toRadians(lat1); // A緯弧度
double radiansBX = Math.toRadians(lon2); // B經弧度
double radiansBY = Math.toRadians(lat2); // B緯弧度
// 公式中“cosβ1cosβ2cos(α1-α2)+sinβ1sinβ2”的部分,得到∠AOB的cos值
double cos = Math.cos(radiansAY) * Math.cos(radiansBY) * Math.cos(radiansAX - radiansBX)
+ Math.sin(radiansAY) * Math.sin(radiansBY);
double acos = Math.acos(cos); // 反餘弦值
return EARTH_RADIUS * acos; // 最終結果
}
GeoHash算法代碼實現
public class GeoHash {
public static final double MINLAT = -90;
public static final double MAXLAT = 90;
public static final double MINLNG = -180;
public static final double MAXLNG = 180;
private static int numbits = 3 * 5; //經緯度單獨編碼長度
private static double minLat;
private static double minLng;
private final static char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
//定義編碼映射關係
final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();
//初始化編碼映射內容
static {
int i = 0;
for (char c : digits)
lookup.put(c, i++);
}
public GeoHash() {
setMinLatLng();
}
// Base32進行編碼
public String encode(double lat, double lon) {
BitSet latbits = getBits(lat, -90, 90);
BitSet lonbits = getBits(lon, -180, 180);
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < numbits; i++) {
buffer.append((lonbits.get(i)) ? '1' : '0');
buffer.append((latbits.get(i)) ? '1' : '0');
}
String code = base32(Long.parseLong(buffer.toString(), 2));
return code;
}
public ArrayList<String> getArroundGeoHash(double lat, double lon) {
ArrayList<String> list = new ArrayList<>();
double uplat = lat + minLat;
double downLat = lat - minLat;
double leftlng = lon - minLng;
double rightLng = lon + minLng;
String leftUp = encode(uplat, leftlng);
list.add(leftUp);
String leftMid = encode(lat, leftlng);
list.add(leftMid);
String leftDown = encode(downLat, leftlng);
list.add(leftDown);
String midUp = encode(uplat, lon);
list.add(midUp);
String midMid = encode(lat, lon);
list.add(midMid);
String midDown = encode(downLat, lon);
list.add(midDown);
String rightUp = encode(uplat, rightLng);
list.add(rightUp);
String rightMid = encode(lat, rightLng);
list.add(rightMid);
String rightDown = encode(downLat, rightLng);
list.add(rightDown);
return list;
}
//根據經緯度和範圍,獲取對應的二進制
private BitSet getBits(double l, double floor, double ceiling) {
BitSet buffer = new BitSet(numbits);
for (int i = 0; i < numbits; i++) {
double mid = (floor + ceiling) / 2;
if (l >= mid) {
buffer.set(i);
floor = mid;
} else {
ceiling = mid;
}
}
return buffer;
}
//將經緯度合併後的二進制進行指定的32位編碼
private String base32(long i) {
char[] buf = new char[65];
int charPos = 64;
boolean negative = (i < 0);
if (!negative) {
i = -i;
}
while (i <= -32) {
buf[charPos--] = digits[(int) (-(i % 32))];
i /= 32;
}
buf[charPos] = digits[(int) (-i)];
if (negative) {
buf[--charPos] = '-';
}
return new String(buf, charPos, (65 - charPos));
}
private void setMinLatLng() {
minLat = MAXLAT - MINLAT;
for (int i = 0; i < numbits; i++) {
minLat /= 2.0;
}
minLng = MAXLNG - MINLNG;
for (int i = 0; i < numbits; i++) {
minLng /= 2.0;
}
}
//根據二進制和範圍解碼
private double decode(BitSet bs, double floor, double ceiling) {
double mid = 0;
for (int i = 0; i < bs.length(); i++) {
mid = (floor + ceiling) / 2;
if (bs.get(i))
floor = mid;
else
ceiling = mid;
}
return mid;
}
//對編碼後的字符串解碼
public double[] decode(String geohash) {
StringBuilder buffer = new StringBuilder();
for (char c : geohash.toCharArray()) {
int i = lookup.get(c) + 32;
buffer.append(Integer.toString(i, 2).substring(1));
}
BitSet lonset = new BitSet();
BitSet latset = new BitSet();
//偶數位,經度
int j = 0;
for (int i = 0; i < numbits * 2; i += 2) {
boolean isSet = false;
if (i < buffer.length())
isSet = buffer.charAt(i) == '1';
lonset.set(j++, isSet);
}
//奇數位,緯度
j = 0;
for (int i = 1; i < numbits * 2; i += 2) {
boolean isSet = false;
if (i < buffer.length())
isSet = buffer.charAt(i) == '1';
latset.set(j++, isSet);
}
double lon = decode(lonset, -180, 180);
double lat = decode(latset, -90, 90);
return new double[]{lat, lon};
}
public static void main(String[] args) {
GeoHash geoHash = new GeoHash();
// 北京站
String encode = geoHash.encode(39.910508, 116.433589);
System.out.println(encode);
// 天安門
System.out.println(geoHash.encode(39.913884, 116.403874));
// 首都機場
System.out.println(geoHash.encode(40.086109, 116.606819));
BitSet latbits = geoHash.getBits(39.913884, -90, 90);
BitSet lonbits = geoHash.getBits(116.403874, -180, 180);
// for (int i=0; i< latbits.length(); i++) {
// System.out.println(latbits.get(i));
// }
for (int i = 0; i < numbits; i++) {
// System.out.print(latbits.get(i));
System.out.print(latbits.get(i) ? '1' : '0');
System.out.print(" ");
}
System.out.println();
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < numbits; i++) {
buffer.append((lonbits.get(i)) ? '1' : '0');
buffer.append((latbits.get(i)) ? '1' : '0');
}
System.out.println(buffer.toString());
System.out.println(geoHash.encode(39.913884, 116.403874));
}
}
寫在最後
如果嫌GeoHash算法麻煩,但是還想用它,沒關係。
Redis知道你懶Redis官網GeoHash用法