首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 實作 GeoHash 編碼與經緯度反解完整指南

     編輯:Java 相關知識

什麼是 GeoHash?

將二維經緯度編碼為短字串(Base32),透過字串前綴長度控制精度。特性:

  • 字串越長,精度越高(1 字元 ≈ 2500km,8 字元 ≈ 19m)
  • 前綴相同 → 地理位置鄰近(適合範圍查詢、鄰近搜尋)
  • 可反解回經緯度(取中點)

核心演算法流程

  1. 經緯度 → 二進制:緯度 [-90,90]、經度 [-180,180] 各自二分編碼(1=上/右,0=下/左)
  2. 交織合併:經度位、緯度位交替組成單一位串流
  3. 分組編碼:每 5 位轉為 1 個 Base32 字元(0-9, b-z 除去 a,i,l,o)

完整 Java 實作

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房間級

實務應用模式

  • 鄰近搜尋:取得目標 GeoHash 的 9 宮格鄰居,查詢資料庫 WHERE geohash IN (...) 再精確計算距離
  • 空間索引:MySQL/PostgreSQL/Redis/RocksDB 直接索引 GeoHash 字串,支援前綴範圍查詢
  • 資料分片:按 GeoHash 前綴分表/分庫,實現地理分片
  • 去重/聚合:相同精度 GeoHash 即為同一網格,可直接 GROUP BY

結語

GeoHash 以簡單的位運算實現空間到字串的雙向映射,是 LBS 系統、地理資訊索引的基石。掌握其編碼/解碼/鄰居計算核心邏輯,並理解精度與字串長度的權衡,能靈活應用於「附近的人」、「地理圍欄」、「空間 JOIN」等經典場景。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved