Geohash算法原理及實現

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

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

Geohash算法就是將經緯度編碼,將二維變一維,給地址位置分區的一種算法。

基本原理

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

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

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

如果在小塊范圍內遞歸對半劃分呢?

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

Geohash算法

Geohash算法一共有三步。

首先將經緯度變成二進制。

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

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

  10111000110001111001

同理可以得到經度116.390705的二進制表示為:

  11010010110001000100

第2步,就是將經緯度合并。

經度占偶數位,緯度占奇數位,注意,0也是偶數位。

  11100 11101 00100 01111 00000 01101 01011 00001

第3步,按照Base32進行編碼

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

  wx4g0ec1

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

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

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

問題

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

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

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

另外就是曲線突變問題。

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

代碼實現

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

假設我們計算的key值是6位,那么二進制位數就是 6*5 = 30位,所以經緯度分別是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;
    }
}

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

可以到 http://geohash.co/ 進行geohash編碼,以確定自己代碼是否寫錯

整體代碼如下所示:

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; //經緯度單獨編碼長度

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' };

//定義編碼映射關系
final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();
//初始化編碼映射內容
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;
}

//根據經緯度和范圍,獲取對應的二進制
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;
}

//將經緯度合并后的二進制進行指定的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;
    }
}

//根據二進制和范圍解碼
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();

    //偶數位,經度
    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);
    }

    //奇數位,緯度
    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]);
}
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容

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