GeoHash原理与Java实现
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