两个多月前写了上一篇计算周围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 "";
}
}