package com.xiaoleilu.hutool.geo;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import com.xiaoleilu.hutool.util.StrUtil;
/**
* GeoHash实现<br>
* 参考:https://github.com/kungfoo/geohash-java
*
* @author Looly
*
*/
public final class GeoHash implements Comparable<GeoHash>, Serializable {
private static final long serialVersionUID = -8553214249630252175L;
/** GeoHash长度 */
private static final int MAX_BIT_PRECISION = 64;
private static final int MAX_CHARACTER_PRECISION = 12;
private static final int[] BITS = { 16, 8, 4, 2, 1 };
private static final int BASE32_BITS = 5;
public static final long FIRST_BIT_FLAGGED = 0x8000000000000000l;
private static final char[] base32 = { '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' };
private final static Map<Character, Integer> decodeMap = new HashMap<>();
static {
int sz = base32.length;
for (int i = 0; i < sz; i++) {
decodeMap.put(base32[i], i);
}
}
protected long bits = 0;
/** 中点 */
private Location point;
private BoundingBox boundingBox;
/** 有效bit位 */
protected byte significantBits = 0;
private GeoHash() {}
/**
* 使用字符数限制精度
* @param latitude 纬度
* @param longitude 经度
* @param numberOfCharacters 字符数
* @return {@link GeoHash}
*/
public static GeoHash withCharacterPrecision(double latitude, double longitude, int numberOfCharacters) {
if (numberOfCharacters > MAX_CHARACTER_PRECISION) {
throw new IllegalArgumentException("A geohash can only be " + MAX_CHARACTER_PRECISION + " character long.");
}
// int desiredPrecision = (numberOfCharacters * 5 <= 60) ? numberOfCharacters * 5 : 60;
return new GeoHash(latitude, longitude, numberOfCharacters * 5);
}
/**
* 创建 {@link GeoHash},限制bit位精度
* @param latitude 纬度
* @param longitude 经度
* @param numberOfBits 限制bit位数
* @return {@link GeoHash}
*/
public static GeoHash withBitPrecision(double latitude, double longitude, int numberOfBits) {
if (numberOfBits > MAX_BIT_PRECISION) {
throw new IllegalArgumentException("A Geohash can only be " + MAX_BIT_PRECISION + " bits long!");
}
if (Math.abs(latitude) > 90.0 || Math.abs(longitude) > 180.0) {
throw new IllegalArgumentException("Can't have lat/lon values out of (-90,90)/(-180/180)");
}
return new GeoHash(latitude, longitude, numberOfBits);
}
/**
* 二进制字符串转为 {@link GeoHash}
* @param binaryString 二进制字符串
* @return {@link GeoHash}
*/
public static GeoHash fromBinaryString(String binaryString) {
GeoHash geohash = new GeoHash();
for (int i = 0; i < binaryString.length(); i++) {
if (binaryString.charAt(i) == '1') {
geohash.addOnBitToEnd();
} else if (binaryString.charAt(i) == '0') {
geohash.addOffBitToEnd();
} else {
throw new IllegalArgumentException(binaryString + " is not a valid geohash as a binary string");
}
}
geohash.bits <<= (MAX_BIT_PRECISION - geohash.significantBits);
long[] latitudeBits = geohash.getRightAlignedLatitudeBits();
long[] longitudeBits = geohash.getRightAlignedLongitudeBits();
return geohash.recombineLatLonBitsToHash(latitudeBits, longitudeBits);
}
/**
* GeoHash字符串(base32编码后)转为 {@link GeoHash}对象
* @param geohash GeoHash字符串
* @return {@link GeoHash}
*/
public static GeoHash fromGeohashString(String geohash) {
double[] latitudeRange = { -90.0, 90.0 };
double[] longitudeRange = { -180.0, 180.0 };
boolean isEvenBit = true;
GeoHash hash = new GeoHash();
for (int i = 0; i < geohash.length(); i++) {
int cd = decodeMap.get(geohash.charAt(i));
for (int j = 0; j < BASE32_BITS; j++) {
int mask = BITS[j];
if (isEvenBit) {//偶数表示经度
divideRangeDecode(hash, longitudeRange, (cd & mask) != 0);
} else {//奇数表示纬度
divideRangeDecode(hash, latitudeRange, (cd & mask) != 0);
}
isEvenBit = !isEvenBit;
}
}
hash.boundingBox = new BoundingBox(latitudeRange[0], latitudeRange[1], longitudeRange[0], longitudeRange[1]);
hash.point = hash.boundingBox.getCenterPoint();
hash.bits <<= (MAX_BIT_PRECISION - hash.significantBits);
return hash;
}
public static GeoHash fromLongValue(long hashVal, int significantBits) {
String binaryString = Long.toBinaryString(hashVal);
if(binaryString.length() > significantBits){
binaryString = StrUtil.subPre(binaryString, significantBits);
}
return fromBinaryString(binaryString);
}
/**
* 坐标转为base32GeoHash编码
* @param latitude 纬度
* @param longitude 经度
* @param numberOfCharacters 字符长度
* @return GeoHash字符串
*/
public static String geoHashStringWithCharacterPrecision(double latitude, double longitude, int numberOfCharacters) {
GeoHash hash = withCharacterPrecision(latitude, longitude, numberOfCharacters);
return hash.toBase32();
}
/**
* 构造
* @param latitude 纬度
* @param longitude 经度
* @param desiredPrecision 限制长度
*/
private GeoHash(double latitude, double longitude, int desiredPrecision) {
point = new Location(latitude, longitude);
desiredPrecision = Math.min(desiredPrecision, MAX_BIT_PRECISION);
boolean isEvenBit = true;
double[] latitudeRange = { -90, 90 };
double[] longitudeRange = { -180, 180 };
while (significantBits < desiredPrecision) {
if (isEvenBit) {
divideRangeEncode(longitude, longitudeRange);
} else {
divideRangeEncode(latitude, latitudeRange);
}
isEvenBit = !isEvenBit;
}
this.boundingBox = new BoundingBox(latitudeRange[0], latitudeRange[1], longitudeRange[0], longitudeRange[1]);
bits <<= (MAX_BIT_PRECISION - desiredPrecision);
}
public GeoHash next(int step) {
return fromOrd(ord() + step, significantBits);
}
public GeoHash next() {
return next(1);
}
public GeoHash prev() {
return next(-1);
}
public long ord() {
int insignificantBits = MAX_BIT_PRECISION - significantBits;
return bits >>> insignificantBits;
}
/**
* Returns the number of characters that represent this hash.
*
* @throws IllegalStateException
* when the hash cannot be encoded in base32, i.e. when the
* precision is not a multiple of 5.
*/
public int getCharacterPrecision() {
if (significantBits % 5 != 0) {
throw new IllegalStateException(
"precision of GeoHash is not divisble by 5: " + this);
}
return significantBits / 5;
}
public static GeoHash fromOrd(long ord, int significantBits) {
int insignificantBits = MAX_BIT_PRECISION - significantBits;
return fromLongValue(ord << insignificantBits, significantBits);
}
/**
* Counts the number of geohashes contained between the two (ie how many
* times next() is called to increment from one to two) This value depends
* on the number of significant bits.
*
* @param one
* @param two
* @return number of steps
*/
public static long stepsBetween(GeoHash one, GeoHash two) {
if (one.significantBits() != two.significantBits()) {
throw new IllegalArgumentException(
"It is only valid to compare the number of steps between two hashes if they have the same number of significant bits");
}
return two.ord() - one.ord();
}
private void divideRangeEncode(double value, double[] range) {
double mid = (range[0] + range[1]) / 2;
if (value >= mid) {
addOnBitToEnd();
range[0] = mid;
} else {
addOffBitToEnd();
range[1] = mid;
}
}
/**
* 当值为1在range的右半区,为0在左半区
* @param hash {@link GeoHash}
* @param range 区域
* @param b bit值
*/
private static void divideRangeDecode(GeoHash hash, double[] range, boolean b) {
double mid = (range[0] + range[1]) / 2;
if (b) {
hash.addOnBitToEnd();
range[0] = mid;
} else {
hash.addOffBitToEnd();
range[1] = mid;
}
}
/**
* 获得相邻8个GeoHash<br>
* 顺序:北, 东北, 东, 东南, 南, 西南, 西, 西北
* @return 相邻8个GeoHash
*/
public GeoHash[] getAdjacent() {
GeoHash northern = getNorthernNeighbour();
GeoHash eastern = getEasternNeighbour();
GeoHash southern = getSouthernNeighbour();
GeoHash western = getWesternNeighbour();
return new GeoHash[] {
northern, //北
northern.getEasternNeighbour(), //东北
eastern, //东
southern.getEasternNeighbour(),//东南
southern,//南
southern.getWesternNeighbour(), //西南
western, //西
northern.getWesternNeighbour() //西北
};
}
/**
* 有效bit位
* @return 有效bit位
*/
public int significantBits() {
return significantBits;
}
/**
* long值
* @return long值
*/
public long longValue() {
return bits;
}
/**
* {@link GeoHash}的base32值
* @return {@link GeoHash}的base32值
*/
public String toBase32() {
if (significantBits % 5 != 0) {
throw new IllegalStateException("Cannot convert a geohash to base32 if the precision is not a multiple of 5.");
}
StringBuilder buf = new StringBuilder();
long firstFiveBitsMask = 0xf800000000000000l;
long bitsCopy = bits;
int partialChunks = (int) Math.ceil(((double) significantBits / 5));
for (int i = 0; i < partialChunks; i++) {
int pointer = (int) ((bitsCopy & firstFiveBitsMask) >>> 59);
buf.append(base32[pointer]);
bitsCopy <<= 5;
}
return buf.toString();
}
/**
* returns true iff this is within the given geohash bounding box.
*/
public boolean within(GeoHash boundingBox) {
return (bits & boundingBox.mask()) == boundingBox.bits;
}
/**
* find out if the given point lies within this hashes bounding box.<br>
* <i>Note: this operation checks the bounding boxes coordinates, i.e. does
* not use the {@link GeoHash}s special abilities.s</i>
*/
public boolean contains(Location point) {
return boundingBox.contains(point);
}
/**
* returns the {@link Location} that was originally used to set up this.<br>
* If it was built from a base32-{@link String}, this is the center point of
* the bounding box.
*/
public Location getPoint() {
return point;
}
/**
* return the center of this {@link GeoHash}s bounding box. this is rarely
* the same point that was used to build the hash.
*/
public Location getBoundingBoxCenterPoint() {
return boundingBox.getCenterPoint();
}
public BoundingBox getBoundingBox() {
return boundingBox;
}
public boolean enclosesCircleAroundPoint(Location point, double radius) {
return false;
}
/**
* 组合经纬度bit
* @param latBits 纬度bit
* @param lonBits 经度bit
* @return {@link GeoHash}
*/
protected GeoHash recombineLatLonBitsToHash(long[] latBits, long[] lonBits) {
GeoHash hash = new GeoHash();
boolean isEvenBit = false;
latBits[0] <<= (MAX_BIT_PRECISION - latBits[1]);
lonBits[0] <<= (MAX_BIT_PRECISION - lonBits[1]);
double[] latitudeRange = { -90.0, 90.0 };
double[] longitudeRange = { -180.0, 180.0 };
for (int i = 0; i < latBits[1] + lonBits[1]; i++) {
if (isEvenBit) {
divideRangeDecode(hash, latitudeRange, (latBits[0] & FIRST_BIT_FLAGGED) == FIRST_BIT_FLAGGED);
latBits[0] <<= 1;
} else {
divideRangeDecode(hash, longitudeRange, (lonBits[0] & FIRST_BIT_FLAGGED) == FIRST_BIT_FLAGGED);
lonBits[0] <<= 1;
}
isEvenBit = !isEvenBit;
}
hash.bits <<= (MAX_BIT_PRECISION - hash.significantBits);
this.boundingBox = new BoundingBox(latitudeRange[0], latitudeRange[1], longitudeRange[0], longitudeRange[1]);
hash.point = hash.boundingBox.getCenterPoint();
return hash;
}
public GeoHash getNorthernNeighbour() {
long[] latitudeBits = getRightAlignedLatitudeBits();
long[] longitudeBits = getRightAlignedLongitudeBits();
latitudeBits[0] += 1;
latitudeBits[0] = maskLastNBits(latitudeBits[0], latitudeBits[1]);
return recombineLatLonBitsToHash(latitudeBits, longitudeBits);
}
public GeoHash getSouthernNeighbour() {
long[] latitudeBits = getRightAlignedLatitudeBits();
long[] longitudeBits = getRightAlignedLongitudeBits();
latitudeBits[0] -= 1;
latitudeBits[0] = maskLastNBits(latitudeBits[0], latitudeBits[1]);
return recombineLatLonBitsToHash(latitudeBits, longitudeBits);
}
public GeoHash getEasternNeighbour() {
long[] latitudeBits = getRightAlignedLatitudeBits();
long[] longitudeBits = getRightAlignedLongitudeBits();
longitudeBits[0] += 1;
longitudeBits[0] = maskLastNBits(longitudeBits[0], longitudeBits[1]);
return recombineLatLonBitsToHash(latitudeBits, longitudeBits);
}
public GeoHash getWesternNeighbour() {
long[] latitudeBits = getRightAlignedLatitudeBits();
long[] longitudeBits = getRightAlignedLongitudeBits();
longitudeBits[0] -= 1;
longitudeBits[0] = maskLastNBits(longitudeBits[0], longitudeBits[1]);
return recombineLatLonBitsToHash(latitudeBits, longitudeBits);
}
protected long[] getRightAlignedLatitudeBits() {
long copyOfBits = bits << 1;
long value = extractEverySecondBit(copyOfBits, getNumberOfLatLonBits()[0]);
return new long[] { value, getNumberOfLatLonBits()[0] };
}
protected long[] getRightAlignedLongitudeBits() {
long copyOfBits = bits;
long value = extractEverySecondBit(copyOfBits, getNumberOfLatLonBits()[1]);
return new long[] { value, getNumberOfLatLonBits()[1] };
}
private long extractEverySecondBit(long copyOfBits, int numberOfBits) {
long value = 0;
for (int i = 0; i < numberOfBits; i++) {
if ((copyOfBits & FIRST_BIT_FLAGGED) == FIRST_BIT_FLAGGED) {
value |= 0x1;
}
value <<= 1;
copyOfBits <<= 2;
}
value >>>= 1;
return value;
}
protected int[] getNumberOfLatLonBits() {
if (significantBits % 2 == 0) {
return new int[] { significantBits / 2, significantBits / 2 };
} else {
return new int[] { significantBits / 2, significantBits / 2 + 1 };
}
}
protected final void addOnBitToEnd() {
significantBits++;
bits <<= 1;
bits = bits | 0x1;
}
protected final void addOffBitToEnd() {
significantBits++;
bits <<= 1;
}
@Override
public String toString() {
if (significantBits % 5 == 0) {
return String.format("%s -> %s -> %s", Long.toBinaryString(bits), boundingBox, toBase32());
} else {
return String.format("%s -> %s, bits: %d", Long.toBinaryString(bits), boundingBox, significantBits);
}
}
public String toBinaryString() {
StringBuilder bui = new StringBuilder();
long bitsCopy = bits;
for (int i = 0; i < significantBits; i++) {
if ((bitsCopy & FIRST_BIT_FLAGGED) == FIRST_BIT_FLAGGED) {
bui.append('1');
} else {
bui.append('0');
}
bitsCopy <<= 1;
}
return bui.toString();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof GeoHash) {
GeoHash other = (GeoHash) obj;
if (other.significantBits == significantBits && other.bits == bits) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
int f = 17;
f = 31 * f + (int) (bits ^ (bits >>> 32));
f = 31 * f + significantBits;
return f;
}
/**
* return a long mask for this hashes significant bits.
*/
private long mask() {
if (significantBits == 0) {
return 0;
} else {
long value = FIRST_BIT_FLAGGED;
value >>= (significantBits - 1);
return value;
}
}
private long maskLastNBits(long value, long n) {
long mask = 0xffffffffffffffffl;
mask >>>= (MAX_BIT_PRECISION - n);
return value & mask;
}
@Override
public int compareTo(GeoHash o) {
int bitsCmp = Long.compare(bits ^ FIRST_BIT_FLAGGED, o.bits ^ FIRST_BIT_FLAGGED);
if (bitsCmp != 0) {
return bitsCmp;
} else {
return Integer.compare(significantBits, o.significantBits);
}
}
}