1. 程式人生 > >用打表的方式解決求Geohash當前區域周圍8個區域編碼

用打表的方式解決求Geohash當前區域周圍8個區域編碼

兩個多月前寫了上一篇計算周圍8個格子編碼的文章,實際用時發現效率不高,因為每次計算都需要先解碼成二進位制串算完再進行編碼。後來在github上發現一個js專案,用打表的方法來求周圍8個格子,效率提高了不少。這裡寫一點自己的理解。

拿”wx4g”這個geohash來看,解碼成二進位制串就是 11100 11101 00100 01111 。單獨看’w’,也就是 11100,按照編碼的順序,就是 “右 上 右 下 左”,這樣就確定了一個座標的大概範圍,然後第二位’x’在第一次確定的範圍內按照”上 右 上 左 上”的順序確定一個更小的範圍。如此進行下去直到編碼結束就能把一個座標確定在一個很小的範圍內。因此,geohash實際上就是將大方格劃分成32個小方格,再將每一個個小方格劃分成更小的32個小方格的過程。

我們發現,對於geohash中奇數位的字元(’w’,’4’),它們的編碼是按照”經 緯 經 緯 經”的順序,而偶數為字元(’x’,’g’)則是按照”緯 經 緯 經 緯”的順序。

所以,我們很容易確定base32編碼時每一個字元對應的方格的位置。

對於奇數位字元:

 b  c  f  g  u  v  y  z
 8  9  d  e  s  t  w  x
 2  3  6  7  k  m  q  r
 0  1  4  5  h  j  n  p

對於偶數位字元:

 p  r  x  z
 n  q  w  y
 j  m  t  v  
 h  k  s  u
 5  7
e g 4 6 d f 1 3 9 c 0 2 8 b

從表格很容易得出每一個字元的周圍8個字元是什麼。處於邊界上的字元稍有不同(例如偶數位,b的右邊是0,0的左邊是b,b的下邊是z,z的上邊是b)。

所以,要求geohash周圍的8個hash值,就相當於求出最後一個字元周圍的8個字元,如果最後一個字元處於邊界,還要求倒數第二個字元周圍同方向的相鄰字元,如果倒數第二個字元也處於邊界就要求倒數第三個,以此類推。

還是拿”wx4g”來看,”g”為偶數位,正好處於右邊界,從表中可以看出與g相鄰的8個字元分別是 ‘e’,’s’,’u’,’d’,’f’,’h’,’5’,’4’,其中後三個使超出右邊界的結果。由於’g’往右超出了邊界,因此看倒數第二位’4’,為奇數為並且沒有超出邊界,’4’右邊相鄰的字元是’5’,因此”wx4g”周圍8個區域編碼分別為”wx4e”,”wx4s”,”wx4u”,”wx4d”,”wx4f”,”wx5h”,”wx55”,”wx54”。

下面貼上程式碼:

private static final int TOP = 0;
private static final int RIGHT = 1;
private static final int BOTTOM = 2;
private static final int LEFT = 3;

private static final int EVEN = 0;
private static final int ODD = 1;

private static String[][] NEIGHBORS;
private static String[][] BORDERS;

static {
    NEIGHBORS = new String[2][4];
    BORDERS = new String[2][4];

    BORDERS[ODD][TOP] = "bcfguvyz";
    BORDERS[ODD][RIGHT] = "prxz";
    BORDERS[ODD][BOTTOM] = "0145hjnp";
    BORDERS[ODD][LEFT] = "028b";

    BORDERS[EVEN][TOP] = BORDERS[ODD][RIGHT];
    BORDERS[EVEN][RIGHT] = BORDERS[ODD][TOP];
    BORDERS[EVEN][BOTTOM] = BORDERS[ODD][LEFT];
    BORDERS[EVEN][LEFT] = BORDERS[ODD][BOTTOM];

    NEIGHBORS[ODD][TOP] = "238967debc01fg45kmstqrwxuvhjyznp";
    NEIGHBORS[ODD][RIGHT] = "14365h7k9dcfesgujnmqp0r2twvyx8zb";
    NEIGHBORS[ODD][BOTTOM] = "bc01fg45238967deuvhjyznpkmstqrwx";
    NEIGHBORS[ODD][LEFT] = "p0r21436x8zb9dcf5h7kjnmqesgutwvy";

    NEIGHBORS[EVEN][TOP] = NEIGHBORS[ODD][RIGHT];
    NEIGHBORS[EVEN][RIGHT] = NEIGHBORS[ODD][TOP];
    NEIGHBORS[EVEN][BOTTOM] = NEIGHBORS[ODD][LEFT];
    NEIGHBORS[EVEN][LEFT] = NEIGHBORS[ODD][BOTTOM];
}
/**
* 求與當前geohash相鄰的8個格子的geohash值。
 * 
 * @param geohash
 * @param suffix
 *            資料庫查詢中字首匹配使用的萬用字元
 * @return string陣列,周圍格子的geohash值
 */
public static String[] expand(String geohash,String suffix) {

    String left = calculate(geohash, LEFT);
    String right = calculate(geohash, RIGHT);
    String top = calculate(geohash, TOP);
    String bottom = calculate(geohash, BOTTOM);

    String topLeft = calculate(top, LEFT);
    String topRight = calculate(top, RIGHT);
    String bottomLeft = calculate(bottom, LEFT);
    String bottomRight = calculate(bottom, RIGHT);

    return new String[] {topLeft+suffix, top+suffix, topRight+suffix, left+suffix, geohash+suffix, right+suffix, bottomLeft+suffix, bottom+suffix, bottomRight+suffix };
}

/**
 * 遞迴計算當前區域特定方向的geohash值
 * 
 * @param geohash
 * @param direction
 *            偏移方向
 * @return 周圍區域的geohash值,超出邊界則返回空字串""
 */
private static String calculate(String geohash, int direction) {
    if ("".equals(geohash))      //如果遞迴到第一個字元仍然處於邊界,則不存在這一方向的相鄰格子
        return "";
    int length = geohash.length();
    char lastChar = geohash.charAt(length - 1);
    int charType = (geohash.length() % 2) == 1 ? ODD : EVEN;  //最後一位是奇數還是偶數
    String base = geohash.substring(0, length - 1);
    if (BORDERS[charType][direction].indexOf(lastChar) != -1) { //判斷對後一位是否處在邊界
        base = calculate(base, direction);
    }
    if (!"".equals(base)) {
        return base + NEIGHBORS[charType][direction].charAt(BASE32.indexOf(lastChar));
    } else {
        return "";
    }
}