【搬磚筆記】 利用GeoHash爲地理位置編碼——實現篇

兄弟篇:【搬磚筆記】 利用GeoHash爲地理位置編碼——理論篇java

1、前言

本篇介紹採用Java實現GeoHash算法,如有任何錯誤或建議,望不吝賜教,不勝感激。git

2、 實現

完整代碼見:GitHubgithub

先羅列下實現的功能:算法

  1. 將經緯度轉換爲二進制編碼,見 getBinary函數。
  2. 將二進制編碼轉換爲base32編碼,見getBase32函數。
  3. (通用法)根據二進制編碼、方向獲取對應方向的鄰塊二進制編碼,見getNeighborWithDirection函數。
  4. (通用法)根據二進制編碼,獲取鄰近九宮格的二進制編碼,見getNeighbors函數。
  5. (查表法)根據base32編碼、方向獲取對應方向的鄰塊base32編碼,見getNeighborsByTable函數。
  6. (查表法)根據base32編碼,獲取鄰近九宮格的base32編碼,見getNeighborsByTable函數。

實現代碼以下:數組

import java.util.HashMap;
import java.util.Map;

public class GeoHash {

    /** * 經度最小值 */
    private static final double MIN_LONGTITUDE = -180.0;

    /** * 經度最大值 */
    private static final double MAX_LONGTITUDE = 180.0;

    /** * 緯度最小值 */
    private static final double MIN_LATITUDE = -90.0;

    /** * 緯度最大值 */
    private static final double MAX_LATITUDE = 90.0;

    /** * 二進制表達最長爲64位 */
    private static final Integer MAX_BINARY_BITS = 64;

    /** * base32編碼最長爲13位,由於 5*13 = 65 > 64 */
    private static final Integer MAX_BASE32_LENGTH = 13;

    /** * 每五個bit轉換爲base32編碼 */
    private static final int ENCODE_EVERY_BITS = 5;

    /** * base32 編碼表 */
    private static final char[] BASE32_LOOKUP = {'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'};

    /** * base32 反編碼對照表 */
    private final static Map<Character, Integer> BASE32_DECODE_LOOKUP = new HashMap<>();

    /** * 編碼 0 填充對照表 */
    private static final String[] PADDING_LOOKUP = {"", "0", "00", "000", "0000"};

    /** * 方向枚舉 */
    private enum Direction {
        Top,
        Right,
        Bottom,
        Left;
    }

    /** * 奇數查表 */
    private static Map<Direction, String> ODD_LOOKUP = new HashMap<>(4);

    /** * 偶數查表 */
    private static Map<Direction, String> EVEN_LOOKUP = new HashMap<>(4);

    /** * 奇數查表邊界 */
    private static Map<Direction, String> ODD_BORDERS = new HashMap<>(4);

    /** * 偶數查表邊界 */
    private static Map<Direction, String> EVEN_BORDERS = new HashMap<>(4);

    /** * 64bit中第一位的標記 */
    private static final long FIRST_BIT_FLAGGED = 0x8000000000000000L;

    /** * 靜態塊 */
    static {
        for (int i = 0; i < BASE32_LOOKUP.length; i++) {
            BASE32_DECODE_LOOKUP.put(BASE32_LOOKUP[i], i);
        }
        ODD_LOOKUP.put(Direction.Top, "238967debc01fg45kmstqrwxuvhjyznp");
        ODD_LOOKUP.put(Direction.Right, "14365h7k9dcfesgujnmqp0r2twvyx8zb");
        ODD_LOOKUP.put(Direction.Bottom, "bc01fg45238967deuvhjyznpkmstqrwx");
        ODD_LOOKUP.put(Direction.Left, "p0r21436x8zb9dcf5h7kjnmqesgutwvy");
        EVEN_LOOKUP.put(Direction.Top, "14365h7k9dcfesgujnmqp0r2twvyx8zb");
        EVEN_LOOKUP.put(Direction.Right, "238967debc01fg45kmstqrwxuvhjyznp");
        EVEN_LOOKUP.put(Direction.Bottom, "p0r21436x8zb9dcf5h7kjnmqesgutwvy");
        EVEN_LOOKUP.put(Direction.Left, "bc01fg45238967deuvhjyznpkmstqrwx");
        ODD_BORDERS.put(Direction.Top, "bcfguvyz");
        ODD_BORDERS.put(Direction.Right, "prxz");
        ODD_BORDERS.put(Direction.Bottom, "0145hjnp");
        ODD_BORDERS.put(Direction.Left, "028b");
        EVEN_BORDERS.put(Direction.Top, "prxz");
        EVEN_BORDERS.put(Direction.Right, "bcfguvyz");
        EVEN_BORDERS.put(Direction.Bottom, "028b");
        EVEN_BORDERS.put(Direction.Left, "0145hjnp");
    }

    /** * 將經緯度轉換爲二進制編碼 * * @param lon 經度 * @param lat 緯度 * @param precision 精度,例如13表明每一個維度上用13位二進制表示,經緯結合,共26位二進制。最長精度爲32。 * @return 二進制編碼 */
    public static String getBinary(double lon, double lat, int precision) {
        if (lon <= MIN_LONGTITUDE || lon >= MAX_LONGTITUDE) {
            throw new IllegalArgumentException(String.format("經度取值範圍爲(%f, %f)", MIN_LONGTITUDE, MAX_LONGTITUDE));
        }
        if (lat <= MIN_LATITUDE || lat >= MAX_LATITUDE) {
            throw new IllegalArgumentException(String.format("緯度取值範圍爲(%f, %f)", MIN_LATITUDE, MAX_LATITUDE));
        }
        if ((precision << 1) > MAX_BINARY_BITS) {
            throw new IllegalArgumentException("精度最長爲32位");
        }

        // 經緯標識符,0表明經度,1表明緯度
        int xyFlag = 0;
        int bits = 1;
        int binaryBits = precision << 1;
        double[] lon_range = {MIN_LONGTITUDE, MAX_LONGTITUDE};
        double[] lat_range = {MIN_LATITUDE, MAX_LATITUDE};
        StringBuilder result = new StringBuilder();
        while (bits <= binaryBits) {
            char divideResult;
            if (xyFlag == 0) {
                divideResult = divideConquer(lon_range, lon);
            } else {
                divideResult = divideConquer(lat_range, lat);
            }
            result.append(divideResult);
            bits++;
            xyFlag = xyFlag ^ 0x1;
        }
        return result.toString();
    }

    /** * 將二進制編碼轉換爲base32編碼 * [!] 注意,這裏理應對二進制位數作出5的倍數的限制,可是爲了達到不一樣精度均可以經過base32編碼,縮短編碼長度,這裏不作限制, * 但計算出來的編碼不可用於查表法,只能經過通用法,解析經緯的方式計算鄰近。 * * @param binary 二進制編碼 * @return base32編碼 */
    public static String getBase32(final String binary) {
        valideBinary(binary);
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < binary.length(); i += ENCODE_EVERY_BITS) {
            int delta = Math.min(ENCODE_EVERY_BITS, binary.length() - i);
            String tmp = binary.substring(i, i + delta);
            int decimal = Integer.parseInt(tmp, 2);
            result.append(BASE32_LOOKUP[decimal]);
        }
        return result.toString();
    }

    /** * 將base32編碼轉換爲二進制編碼 * * @param base32 base32編碼 * @param bits 二進制長度 * @return 二進制編碼 */
    public static String getBinary(final String base32, int bits) {
        valideBase32(base32);
        int[] region = {(base32.length() - 1) * ENCODE_EVERY_BITS, base32.length() * ENCODE_EVERY_BITS};
        if (bits <= region[0] || bits > region[1]) {
            throw new IllegalArgumentException(String.format("精度與base32編碼不匹配,長度爲%d的base32編碼,其二進制長度應爲(%d, %d]", base32.length(), region[0], region[1]));
        }
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < base32.length(); ++i) {
            int decimal = BASE32_DECODE_LOOKUP.get(base32.charAt(i));
            String binary = Integer.toBinaryString(decimal);
            // 須要補0的位數
            int padding = Math.min(ENCODE_EVERY_BITS - binary.length(), bits - result.length());
            binary = PADDING_LOOKUP[padding] + binary;
            result.append(binary);
        }
        return result.toString();
    }

    /** * 查表法,獲取以當前base32編碼塊造成的九宮格,順序按照編碼順序,以下: * 0 1 2 * 3 4 5 * 6 7 8 * [!] 注意,查表法僅適用於二進制長度爲5的倍數。非5倍數的請用通用法。 * * @param base32 base32編碼 * @return base32編碼的數組 */
    public static String[] getNeighborsByTable(final String base32) {
        valideBase32(base32);
        String[] result = new String[9];
        result[4] = base32;
        result[1] = getNeighborWithDirectionByTable(base32, Direction.Top);
        result[0] = getNeighborWithDirectionByTable(result[1], Direction.Left);
        result[2] = getNeighborWithDirectionByTable(result[1], Direction.Right);
        result[5] = getNeighborWithDirectionByTable(base32, Direction.Right);
        result[7] = getNeighborWithDirectionByTable(base32, Direction.Bottom);
        result[3] = getNeighborWithDirectionByTable(base32, Direction.Left);
        result[6] = getNeighborWithDirectionByTable(result[7], Direction.Left);
        result[8] = getNeighborWithDirectionByTable(result[7], Direction.Right);
        return result;
    }

    /** * 查表法,根據輸入的base32編碼和方向,獲取相應方向相鄰的base32編碼 * * @param base32 base32編碼 * @param dire 方向 * @return 相應方向相鄰的base32編碼 */
    public static String getNeighborWithDirectionByTable(final String base32, Direction dire) {
        valideBase32(base32);
        boolean isOdd = (base32.length() & 0x1) == 0x1;
        String prefix = base32.substring(0, base32.length() - 1);
        char lastChar = base32.charAt(base32.length() - 1);
        // 是否處於邊界
        boolean inBorder;
        char postfix;
        if (isOdd) {
            inBorder = ODD_BORDERS.get(dire).indexOf(lastChar) != -1;
            postfix = ODD_LOOKUP.get(dire).charAt(BASE32_DECODE_LOOKUP.get(lastChar));
        } else {
            inBorder = EVEN_BORDERS.get(dire).indexOf(lastChar) != -1;
            postfix = EVEN_LOOKUP.get(dire).charAt(BASE32_DECODE_LOOKUP.get(lastChar));
        }
        // 若處於邊界,繼續往下遞歸
        if (inBorder) {
            prefix = getNeighborWithDirectionByTable(prefix, dire);
        }
        String result = prefix + postfix;
        return result;
    }

    /** * 通用法,根據輸入的base32編碼和方向,獲取相應方向相鄰的base32編碼 * * @param binary 二進制編碼 * @param dire 方向 * @return 相應方向相鄰的base32編碼 */
    public static String getNeighborWithDirection(final String binary, final Direction dire) {
        if (binary.length() <= 1) {
            throw new IllegalArgumentException("二進制編碼長度至少爲2");
        }
        String result = binary;
        String lat, lon;
        int decimal;
        switch (dire) {
            case Top:
                lat = extractEveryTwoBit(binary, 1);
                decimal = Integer.parseInt(lat, 2);
                decimal += 1;
                lat = maskLastNBit(decimal, lat.length());
                result = integrateEveryTwoBit(result, 1, lat);
                break;
            case Right:
                lon = extractEveryTwoBit(binary, 0);
                decimal = Integer.parseInt(lon, 2);
                decimal += 1;
                lon = maskLastNBit(decimal, lon.length());
                result = integrateEveryTwoBit(result, 0, lon);
                break;
            case Bottom:
                lat = extractEveryTwoBit(binary, 1);
                decimal = Integer.parseInt(lat, 2);
                decimal -= 1;
                lat = maskLastNBit(decimal, lat.length());
                result = integrateEveryTwoBit(result, 1, lat);
                break;
            case Left:
                lon = extractEveryTwoBit(binary, 0);
                decimal = Integer.parseInt(lon, 2);
                decimal -= 1;
                lon = maskLastNBit(decimal, lon.length());
                result = integrateEveryTwoBit(result, 0, lon);
                break;
        }
        return result;
    }

    /** * 通用法,根據二進制編碼,計算鄰近九宮格 * 0 1 2 * 3 4 5 * 6 7 8 * @param binary 二進制編碼 * @return 相應方向相鄰的二進制編碼 */
    public static String[] getNeighbors(final String binary) {
        valideBinary(binary);
        String[] result = new String[9];
        result[4] = binary;
        result[1] = getNeighborWithDirection(binary, Direction.Top);
        result[0] = getNeighborWithDirection(result[1], Direction.Left);
        result[2] = getNeighborWithDirection(result[1], Direction.Right);
        result[5] = getNeighborWithDirection(binary, Direction.Right);
        result[7] = getNeighborWithDirection(binary, Direction.Bottom);
        result[3] = getNeighborWithDirection(binary, Direction.Left);
        result[6] = getNeighborWithDirection(result[7], Direction.Left);
        result[8] = getNeighborWithDirection(result[7], Direction.Right);
        return result;
    }

    /** * 取decimal中最後n位 * * @param decimal 十進制 * @param n 最後位數 * @return 最後n位的字符串 */
    private static String maskLastNBit(long decimal, int n) {
        long mask = 0xffffffffffffffffL;
        decimal <<= (MAX_BINARY_BITS - n);
        long nBit = decimal & mask;
        StringBuilder res = new StringBuilder();
        for (int i = 0; i < n; ++i, nBit <<= 1) {
            if ((nBit & FIRST_BIT_FLAGGED) == FIRST_BIT_FLAGGED) {
                res.append('1');
            } else {
                res.append('0');
            }
        }
        return res.toString();
    }

    /** * 以索引start爲起始,提取二進制編碼binary中每隔一位的值 * * @param binary 二進制編碼 * @param start 起始下標 * @return 二進制編碼binary中每隔一位的值 */
    private static String extractEveryTwoBit(String binary, int start) {
        if (start >= binary.length()) {
            throw new IllegalArgumentException("起始下表超出長度界限");
        }
        StringBuilder res = new StringBuilder();
        for (int i = start; i < binary.length(); i += 2) {
            res.append(binary.charAt(i));
        }
        return res.toString();
    }

    /** * 以索引start爲起始,將integrate值設置進二進制編碼binary中每隔一位的值 * * @param binary 二進制編碼 * @param start 起始下標 * @param integrate 待整合的編碼 * @return 整合後的二進制編碼 */
    private static String integrateEveryTwoBit(String binary, int start, String integrate) {
        if (start >= binary.length()) {
            throw new IllegalArgumentException("起始下表超出長度界限");
        }
        StringBuilder res = new StringBuilder(binary);
        for (int i = start, j = 0; i < binary.length() && j < integrate.length(); i += 2, ++j) {
            res.replace(i, i + 1, integrate.substring(j, j + 1));
        }
        return res.toString();
    }

    /** * 二分,根據區間,計算中間值,大於給定值則給0,小於給定值則給1 * * @param range 區間 * @param target 給定值 * @return 大於給定值則給0,小於給定值則給1 */
    private static char divideConquer(double[] range, double target) {
        // 防止越界
        double mid = (range[1] - range[0]) / 2 + range[0];
        if (target < mid) {
            range[1] = mid;
            return '0';
        } else {
            range[0] = mid;
            return '1';
        }
    }

    /** * 驗證base32編碼,若驗證失敗則拋出異常 * * @param base32 base32編碼 */
    private static void valideBase32(String base32) throws IllegalArgumentException {
        if (base32 == null || base32.length() == 0) {
            throw new IllegalArgumentException("base32編碼不能爲空");
        }
        if (base32.length() > MAX_BASE32_LENGTH) {
            throw new IllegalArgumentException("base32編碼最長爲13位");
        }
    }

    /** * 驗證base32編碼,若驗證失敗則拋出異常 * * @param binary base32編碼 */
    private static void valideBinary(String binary) throws IllegalArgumentException {
        if (binary == null || binary.length() == 0) {
            throw new IllegalArgumentException("二進制編碼不能爲空");
        }
        if (binary.length() > MAX_BINARY_BITS) {
            throw new IllegalArgumentException("二進制編碼最長爲64位");
        }
    }

}
複製代碼
相關文章
相關標籤/搜索