Geohash算法原理及實現(xiàn)

最近需要實現(xiàn)一個功能,查找車輛附近的加油站,如果車和加油站距離在200米以內(nèi),則查找成功。

加油站數(shù)量肯定不小,能否縮小查找范圍,否則以遍歷形式,效率肯定高不了。

Geohash算法就是將經(jīng)緯度編碼,將二維變一維,給地址位置分區(qū)的一種算法。

基本原理

GeoHash是一種地址編碼方法。他能夠把二維的空間經(jīng)緯度數(shù)據(jù)編碼成一個字符串

我們知道,經(jīng)度范圍是東經(jīng)180到西經(jīng)180,緯度范圍是南緯90到北緯90,我們設(shè)定西經(jīng)為負,南緯為負,所以地球上的經(jīng)度范圍就是[-180, 180],緯度范圍就是[-90,90]。如果以本初子午線、赤道為界,地球可以分成4個部分。

如果緯度范圍[-90°, 0°)用二進制0代表,(0°, 90°]用二進制1代表,經(jīng)度范圍[-180°, 0°)用二進制0代表,(0°, 180°]用二進制1代表,那么地球可以分成如下4個部分

如果在小塊范圍內(nèi)遞歸對半劃分呢?

可以看到,劃分的區(qū)域更多了,也更精確了。geohash算法就是基于這種思想,劃分的次數(shù)更多,區(qū)域更多,區(qū)域面積更小了。通過將經(jīng)緯度編碼,給地理位置分區(qū)

Geohash算法

Geohash算法一共有三步。

首先將經(jīng)緯度變成二進制。

比如這樣一個點(39.923201, 116.390705)
緯度的范圍是(-90,90),其中間值為0。對于緯度39.923201,在區(qū)間(0,90)中,因此得到一個1;(0,90)區(qū)間的中間值為45度,緯度39.923201小于45,因此得到一個0,依次計算下去,即可得到緯度的二進制表示,如下表:

最后得到緯度的二進制表示為:

  10111000110001111001

同理可以得到經(jīng)度116.390705的二進制表示為:

  11010010110001000100

第2步,就是將經(jīng)緯度合并。

經(jīng)度占偶數(shù)位,緯度占奇數(shù)位,注意,0也是偶數(shù)位。

  11100 11101 00100 01111 00000 01101 01011 00001

第3步,按照Base32進行編碼

Base32編碼表的其中一種如下,是用0-9、b-z(去掉a, i, l, o)這32個字母進行編碼。具體操作是先將上一步得到的合并后二進制轉(zhuǎn)換為10進制數(shù)據(jù),然后對應(yīng)生成Base32碼。需要注意的是,將5個二進制位轉(zhuǎn)換成一個base32碼。上例最終得到的值為

  wx4g0ec1

Geohash比直接用經(jīng)緯度的高效很多,而且使用者可以發(fā)布地址編碼,既能表明自己位于北海公園附近,又不至于暴露自己的精確坐標(biāo),有助于隱私保護。

  • GeoHash用一個字符串表示經(jīng)度和緯度兩個坐標(biāo)。在數(shù)據(jù)庫中可以實現(xiàn)在一列上應(yīng)用索引(某些情況下無法在兩列上同時應(yīng)用索引)
  • GeoHash表示的并不是一個點,而是一個矩形區(qū)域
  • GeoHash編碼的前綴可以表示更大的區(qū)域。例如wx4g0ec1,它的前綴wx4g0e表示包含編碼wx4g0ec1在內(nèi)的更大范圍。 這個特性可以用于附近地點搜索

編碼越長,表示的范圍越小,位置也越精確。因此我們就可以通過比較GeoHash匹配的位數(shù)來判斷兩個點之間的大概距離。

問題

geohash算法有兩個問題。首先是邊緣問題。

如圖,如果車在紅點位置,區(qū)域內(nèi)還有一個黃點。相鄰區(qū)域內(nèi)的綠點明顯離紅點更近。但因為黃點的編碼和紅點一樣,最終找到的將是黃點。這就有問題了。

要解決這個問題,很簡單,只要再查找周邊8個區(qū)域內(nèi)的點,看哪個離自己更近即可。

另外就是曲線突變問題。

本文第2張圖片比較好地解釋了這個問題。其中0111和1000兩個編碼非常相近,但它們的實際距離確很遠。所以編碼相近的兩個單位,并不一定真實距離很近,這需要實際計算兩個點的距離才行。

代碼實現(xiàn)

geohash原理清楚后,代碼實現(xiàn)就比較簡單了。不過仍然有一個問題需要解決,就是如何計算周邊的8個區(qū)域key值呢

假設(shè)我們計算的key值是6位,那么二進制位數(shù)就是 6*5 = 30位,所以經(jīng)緯度分別是15位。我們以緯度為例,緯度會均分15次。這樣我們很容易能夠算出15次后,劃分的最小單位是多少

  private void setMinLatLng() {
    minLat = MAXLAT - MINLAT;
    for (int i = 0; i < numbits; i++) {
        minLat /= 2.0;
    }
    minLng = MAXLNG - MINLNG;
    for (int i = 0; i < numbits; i++) {
        minLng /= 2.0;
    }
}

得到了最小單位,那么周邊區(qū)域的經(jīng)緯度也可以計算得到了。比如說左邊區(qū)域的經(jīng)度肯定是自身經(jīng)度減去最小經(jīng)度單位。緯度也可以通過加減,得到上下的緯度值,最終周圍8個單位也可以計算得到。

可以到 http://geohash.co/ 進行g(shù)eohash編碼,以確定自己代碼是否寫錯

整體代碼如下所示:

public class GeoHash {
public static final double MINLAT = -90;
public static final double MAXLAT = 90;
public static final double MINLNG = -180;
public static final double MAXLNG = 180;

private static int numbits = 3 * 5; //經(jīng)緯度單獨編碼長度

private static double minLat;
private static double minLng;

private final static char[] digits = { '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' };

//定義編碼映射關(guān)系
final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();
//初始化編碼映射內(nèi)容
static {
    int i = 0;
    for (char c : digits)
        lookup.put(c, i++);
}

public GeoHash(){
    setMinLatLng();
}

public String encode(double lat, double lon) {
    BitSet latbits = getBits(lat, -90, 90);
    BitSet lonbits = getBits(lon, -180, 180);
    StringBuilder buffer = new StringBuilder();
    for (int i = 0; i < numbits; i++) {
        buffer.append( (lonbits.get(i))?'1':'0');
        buffer.append( (latbits.get(i))?'1':'0');
    }
    String code = base32(Long.parseLong(buffer.toString(), 2));
    //Log.i("okunu", "encode  lat = " + lat + "  lng = " + lon + "  code = " + code);
    return code;
}

public ArrayList<String> getArroundGeoHash(double lat, double lon){
    //Log.i("okunu", "getArroundGeoHash  lat = " + lat + "  lng = " + lon);
    ArrayList<String> list = new ArrayList<>();
    double uplat = lat + minLat;
    double downLat = lat - minLat;

    double leftlng = lon - minLng;
    double rightLng = lon + minLng;

    String leftUp = encode(uplat, leftlng);
    list.add(leftUp);

    String leftMid = encode(lat, leftlng);
    list.add(leftMid);

    String leftDown = encode(downLat, leftlng);
    list.add(leftDown);

    String midUp = encode(uplat, lon);
    list.add(midUp);

    String midMid = encode(lat, lon);
    list.add(midMid);

    String midDown = encode(downLat, lon);
    list.add(midDown);

    String rightUp = encode(uplat, rightLng);
    list.add(rightUp);

    String rightMid = encode(lat, rightLng);
    list.add(rightMid);

    String rightDown = encode(downLat, rightLng);
    list.add(rightDown);

    //Log.i("okunu", "getArroundGeoHash list = " + list.toString());
    return list;
}

//根據(jù)經(jīng)緯度和范圍,獲取對應(yīng)的二進制
private BitSet getBits(double lat, double floor, double ceiling) {
    BitSet buffer = new BitSet(numbits);
    for (int i = 0; i < numbits; i++) {
        double mid = (floor + ceiling) / 2;
        if (lat >= mid) {
            buffer.set(i);
            floor = mid;
        } else {
            ceiling = mid;
        }
    }
    return buffer;
}

//將經(jīng)緯度合并后的二進制進行指定的32位編碼
private String base32(long i) {
    char[] buf = new char[65];
    int charPos = 64;
    boolean negative = (i < 0);
    if (!negative){
        i = -i;
    }
    while (i <= -32) {
        buf[charPos--] = digits[(int) (-(i % 32))];
        i /= 32;
    }
    buf[charPos] = digits[(int) (-i)];
    if (negative){
        buf[--charPos] = '-';
    }
    return new String(buf, charPos, (65 - charPos));
}

private void setMinLatLng() {
    minLat = MAXLAT - MINLAT;
    for (int i = 0; i < numbits; i++) {
        minLat /= 2.0;
    }
    minLng = MAXLNG - MINLNG;
    for (int i = 0; i < numbits; i++) {
        minLng /= 2.0;
    }
}

//根據(jù)二進制和范圍解碼
private double decode(BitSet bs, double floor, double ceiling) {
    double mid = 0;
    for (int i=0; i<bs.length(); i++) {
        mid = (floor + ceiling) / 2;
        if (bs.get(i))
            floor = mid;
        else
            ceiling = mid;
    }
    return mid;
}

//對編碼后的字符串解碼
public double[] decode(String geohash) {
    StringBuilder buffer = new StringBuilder();
    for (char c : geohash.toCharArray()) {
        int i = lookup.get(c) + 32;
        buffer.append( Integer.toString(i, 2).substring(1) );
    }

    BitSet lonset = new BitSet();
    BitSet latset = new BitSet();

    //偶數(shù)位,經(jīng)度
    int j =0;
    for (int i=0; i< numbits*2;i+=2) {
        boolean isSet = false;
        if ( i < buffer.length() )
            isSet = buffer.charAt(i) == '1';
        lonset.set(j++, isSet);
    }

    //奇數(shù)位,緯度
    j=0;
    for (int i=1; i< numbits*2;i+=2) {
        boolean isSet = false;
        if ( i < buffer.length() )
            isSet = buffer.charAt(i) == '1';
        latset.set(j++, isSet);
    }

    double lon = decode(lonset, -180, 180);
    double lat = decode(latset, -90, 90);

    return new double[] {lat, lon};
}

public static void main(String[] args)  throws Exception{
    GeoHash geohash = new GeoHash();
//        String s = geohash.encode(40.222012, 116.248283);
//        System.out.println(s);
    geohash.getArroundGeoHash(40.222012, 116.248283);
//        double[] geo = geohash.decode(s);
//        System.out.println(geo[0]+" "+geo[1]);
}
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1. 引言 GeoHash本質(zhì)上是空間索引的一種方式,其基本原理是將地球理解為一個二維平面,將平面遞歸分解成更小的...
    renzehello閱讀 38,763評論 1 17
  • GeoHash算法 涉及到地圖的內(nèi)容,基本都會遇到搜索附近的功能,比如附近的人、附近的店鋪等。要實現(xiàn)這樣的功能,我...
    小蘇c閱讀 2,036評論 0 2
  • 1.場景 隨著智能手機和傳感器技術(shù)的發(fā)展,LBS(Location based service)類的應(yīng)用也逐漸多了...
    Daniel_adu閱讀 11,495評論 3 13
  • 膠州秧歌又稱地秧歌、跑秧歌,當(dāng)?shù)孛耖g稱扭斷腰、三道彎,是山東省的漢族民俗舞蹈之一,屬于三大秧歌之一。膠州秧歌有23...
    中經(jīng)全媒體閱讀 753評論 0 0
  • 【垂釣前言】 氣象臺本周每天都有多次雷暴雨預(yù)警,但雨基本一閃而過;盡管預(yù)報周六有短時強降水和大風(fēng),我們還是不太相信...
    huanbi4410閱讀 478評論 0 1