將二維經緯度編碼為短字串(Base32),透過字串前綴長度控制精度。特性:
package com.lulei.geo;
import java.util.*;
public class GeoHash {
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 static final Map<Character, Integer> DECODE_MAP = new HashMap<>();
static {
for (int i = 0; i < BASE32.length; i++) DECODE_MAP.put(BASE32[i], i);
}
private final double lat, lng;
private final int precision; // 字元數
private final int latBits, lngBits;
public GeoHash(double lat, double lng, int precision) {
this.lat = lat; this.lng = lng; this.precision = precision;
this.latBits = (precision * 5) / 2;
this.lngBits = latBits + (precision % 2);
}
/** 經緯度 → GeoHash */
public String encode() {
boolean[] latBitsArr = encodeCoord(lat, -90, 90, latBits);
boolean[] lngBitsArr = encodeCoord(lng, -180, 180, lngBits);
// 交織
boolean[] merged = new boolean[latBits + lngBits];
for (int i = 0; i < lngBits; i++) merged[2*i] = lngBitsArr[i];
for (int i = 0; i < latBits; i++) merged[2*i+1] = latBitsArr[i];
// 每 5 位轉 Base32
StringBuilder sb = new StringBuilder();
for (int i = 0; i < merged.length; i += 5) {
int val = 0;
for (int j = 0; j < 5; j++) {
val = (val << 1) | (merged[i+j] ? 1 : 0);
}
sb.append(BASE32[val]);
}
return sb.toString();
}
/** 單一座標二分編碼 */
private boolean[] encodeCoord(double value, double min, double max, int bits) {
boolean[] result = new boolean[bits];
for (int i = 0; i < bits; i++) {
double mid = (min + max) / 2;
if (value > mid) {
result[i] = true;
min = mid;
} else {
result[i] = false;
max = mid;
}
}
return result;
}
/** GeoHash → 經緯度(取中點) */
public static double[] decode(String geoHash) {
// 1. Base32 → 位串流
StringBuilder bits = new StringBuilder();
for (char c : geoHash.toCharArray()) {
Integer val = DECODE_MAP.get(c);
if (val == null) continue;
bits.append(String.format("%5s", Integer.toBinaryString(val)).replace(' ', '0'));
}
// 2. 解交織
StringBuilder latBits = new StringBuilder(), lngBits = new StringBuilder();
for (int i = 0; i < bits.length(); i++) {
if (i % 2 == 0) lngBits.append(bits.charAt(i));
else latBits.append(bits.charAt(i));
}
// 3. 二進制 → 座標
double lat = decodeCoord(latBits.toString(), -90, 90);
double lng = decodeCoord(lngBits.toString(), -180, 180);
return new double[]{lat, lng};
}
private static double decodeCoord(String bits, double min, double max) {
if (bits.isEmpty()) return (min + max) / 2;
char first = bits.charAt(0);
double mid = (min + max) / 2;
if (first == '1') return decodeCoord(bits.substring(1), mid, max);
else return decodeCoord(bits.substring(1), min, mid);
}
/** 取得鄰近 9 宮格 GeoHash(含自身) */
public List getNeighbors() {
double minLatDelta = 180.0 / (1 << latBits);
double minLngDelta = 360.0 / (1 << lngBits);
List result = new ArrayList<>();
for (int dLat = -1; dLat <= 1; dLat++) {
for (int dLng = -1; dLng <= 1; dLng++) {
double nLat = lat + dLat * minLatDelta;
double nLng = lng + dLng * minLngDelta;
if (nLat >= -90 && nLat <= 90 && nLng >= -180 && nLng <= 180) {
result.add(new GeoHash(nLat, nLng, precision).encode());
}
}
}
return result;
}
// 測試
public static void main(String[] args) {
GeoHash gh = new GeoHash(40.221227, 116.24875, 8);
String hash = gh.encode(); // wx4g0ec1
System.out.println("GeoHash: " + hash);
double[] decoded = decode(hash);
System.out.printf("Decoded: %.6f, %.6f%n", decoded[0], decoded[1]);
System.out.println("Neighbors: " + gh.getNeighbors());
}
}
| 字元數 | 緯度誤差 | 經度誤差 | 適用場景 |
|---|---|---|---|
| 1 | ±111 km | ±111 km | 國家級 |
| 3 | ±1.4 km | ±1.4 km | 城市級 |
| 5 | ±37 m | ±37 m | 街區級 |
| 7 | ±2.3 m | ±2.3 m | 建築物級 |
| 8 | ±0.6 m | ±0.6 m | 房間級 |
WHERE geohash IN (...) 再精確計算距離GeoHash 以簡單的位運算實現空間到字串的雙向映射,是 LBS 系統、地理資訊索引的基石。掌握其編碼/解碼/鄰居計算核心邏輯,並理解精度與字串長度的權衡,能靈活應用於「附近的人」、「地理圍欄」、「空間 JOIN」等經典場景。