GeoHash算法
首先,你要Baidu下,找到该算法核心原理,这里摘自网络文档,简单介绍下。
GeoHash算法是通过二分法,经过一定次数的无限逼近,将经纬度的二维坐标浮点值变成一个可排序、可比较的的字符串编码。
在编码中的每个字符代表一个区域,并且前面的字符是后面字符的父区域,即父子字符串有相同的前缀。其算法的过程如下:
地球纬度区间是[-90,90], 如某纬度是39.92324,可以通过下面算法对39.92324进行逼近编码:
1)区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定39.92324属于右区间[0,90],给标记为1;
2)接着将区间[0,90]进行二分为 [0,45),[45,90],可以确定39.92324属于左区间 [0,45),给标记为0;
3)递归上述过程39.92324总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,并越来越逼近39.928167;
4)如果给定的纬度(39.92324)属于左区间,则记录0,如果属于右区间则记录1,这样随着算法的进行会产生一个序列1011 1000 1100 01111001,序列的长度跟给定的区间划分次数有关。
纬度范围 | 划分区间0 | 划分区间1 | 39.92324所属区间 |
(-90, 90) | (-90, 0.0) | (0.0, 90) | 1 |
(0.0, 90) | (0.0, 45.0) | (45.0, 90) | 0 |
(0.0, 45.0) | (0.0, 22.5) | (22.5, 45.0) | 1 |
(22.5, 45.0) | (22.5, 33.75) | (33.75, 45.0) | 1 |
(33.75, 45.0) | (33.75, 39.375) | (39.375, 45.0) | 1 |
(39.375, 45.0) | (39.375, 42.1875) | (42.1875, 45.0) | 0 |
(39.375, 42.1875) | (39.375, 40.7812) | (40.7812, 42.1875) | 0 |
(39.375, 40.7812) | (39.375, 40.0781) | (40.0781, 40.7812) | 0 |
(39.375, 40.0781) | (39.375, 39.7265) | (39.7265, 40.0781) | 1 |
(39.7265, 40.0781) | (39.7265, 39.9023) | (39.9023, 40.0781) | 1 |
(39.9023, 40.0781) | (39.9023, 39.9902) | (39.9902, 40.0781) | 0 |
(39.9023, 39.9902) | (39.9023, 39.9462) | (39.9462, 39.9902) | 0 |
(39.9023, 39.9462) | (39.9023, 39.9243) | (39.9243, 39.9462) | 0 |
(39.9023, 39.9243) | (39.9023, 39.9133) | (39.9133, 39.9243) | 1 |
(39.9133, 39.9243) | (39.9133, 39.9188) | (39.9188, 39.9243) | 1 |
(39.9188, 39.9243) | (39.9188, 39.9215) | (39.9215, 39.9243) | 1 |
同理,地球经度区间是[-180,180],对经度116.3906进行编码的过程也类似:
经度范围 | 划分区间0 | 划分区间1 | 116.3906所属区间 |
(-180, 180) | (-180, 0.0) | (0.0, 180) | 1 |
(0.0, 180) | (0.0, 90.0) | (90.0, 180) | 1 |
(90.0, 180) | (90.0, 135.0) | (135.0, 180) | 0 |
(90.0, 135.0) | (90.0, 112.5) | (112.5, 135.0) | 1 |
(112.5, 135.0) | (112.5, 123.75) | (123.75, 135.0) | 0 |
(112.5, 123.75) | (112.5, 118.125) | (118.125, 123.75) | 0 |
(112.5, 118.125) | (112.5, 115.312) | (115.312, 118.125) | 1 |
(115.312, 118.125) | (115.312, 116.718) | (116.718, 118.125) | 0 |
(115.312, 116.718) | (115.312, 116.015) | (116.015, 116.718) | 1 |
(116.015, 116.718) | (116.015, 116.367) | (116.367, 116.718) | 1 |
(116.367, 116.718) | (116.367, 116.542) | (116.542, 116.718) | 0 |
(116.367, 116.542) | (116.367, 116.455) | (116.455, 116.542) | 0 |
(116.367, 116.455) | (116.367, 116.411) | (116.411, 116.455) | 0 |
(116.367, 116.411) | (116.367, 116.389) | (116.389, 116.411) | 1 |
(116.389, 116.411) | (116.389, 116.400) | (116.400, 116.411) | 0 |
(116.389, 116.400) | (116.389, 116.394) | (116.394, 116.400) | 0 |
组码
通过上述计算,纬度产生的编码为1011 1000 1100 0111 1001,经度产生的编码为1101 00101100 0100 0100。奇数位放纬度,偶数位放经度,相互穿插,把2条串编码组合生成新串:1110011101001000111100000011010101100001。
最后使用用0-9、b-z(去掉a, i, l, o)这32个字母进行base32编码,不会base32的网上搜搜。首先将1110011101 00100 01111 00000 01101 01011 00001转成十进制 28,29,4,15,0,13,11,1,十进制对应的编码就是wx4g0ec1。同理,将编码转换成经纬度的解码算法与之相反,具体不再赘述。
十进制 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
base32 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | b | c | d | e | f | g |
十进制 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
base32 | h | j | k | m | n | p | q | r | s | t | u | v | w | x | y | z |
由上可知,字符串越长,表示的范围越精确。当GeoHash base32编码长度为8时,精度在19米左右,而当编码长度为9时,精度在2米左右,编码长度需要根据数据情况进行选择。不过从GeoHash的编码算法中可以看出它的一个缺点,位于边界两侧的两点,虽然十分接近,但编码会完全不同。实际应用中,可以同时搜索该点所在区域的其他八个区域的点,即可解决这个问题。
以上原理部分是我从网路上摘录的,以下是我的c实现方法:
/**
* C语言实现版本
*/
#include <stdio.h>
#define BASE32 "0123456789bcdefghjkmnpqrstuvwxyz"
// 把ASCII码表搬下来,去掉我们不用的字符,将解码要用的下标转换如下
static int UNBASE32[]={0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,11,12,13,14,15,16,0,17,18,0,19,20,0,21,22,23,24,25,26,27,28,29,30,31};
/**
* @param latitude 纬度
* @param longitude 经度
* @param precision 字符编码结果的长度(精度)
* @param geohash 编码结果
*/
void geohash_encode(double latitude,double longitude,int precision,char *geohash){
char is_odd =0;// 是否奇数标志,1奇数 0偶数
int i =0;// 编码计数
double lats[2];// 纬度,lats[0]当前查找区间的左边界,最小值;lats[1]当前查找范围的右边界,最大值;
double lons[2];// 经度,lons[0]当前查找区间的左边界,最小值;lons[1]当前查找范围的右边界,最大值;
double mid;// 对当前查找区间向左或向右折半后,新的边界值。
char bits[]= {0x10,0x08,0x04,0x02,0x01};// 用二进制表示,依次为 {00010000,00001000,00000100,00000010,00000001}
int bit =0;//bits的下标
char tmp =0;//每计算满5位二进制位后,进行一次Base32编码,然后清零重复计算。这个值就是临时存储5位二进制中间值的。
// 初始化,将查找区间设为最大。
lats[0]= -90.0;
lats[1]= 90.0;
lons[0]= -180.0;
lons[1]= 180.0;
while (i< precision){
if (is_odd){ // 奇数,处理纬度
mid = (lats[0]+ lats[1])/ 2;
if (latitude > mid){
tmp |= bits[bit];
lats[0]= mid;
}
else
lats[1]= mid;
} else{ // 偶数,处理经度
mid = (lons[0]+ lons[1])/ 2;
if (longitude > mid){ // 落在右侧
tmp |= bits[bit];// 求“或”运算,实际是给第i位赋值“1”。
lons[0]= mid;
}
else // 落在左侧,给第i位赋值“0”,由于初始化就是0,所以这里不用操作了。
lons[1]= mid;
}
is_odd = !is_odd; //取反
if (bit< 4)
bit++;
else { //满4bit,进行base32编码,然后对中间值清零
geohash[i++]= BASE32[tmp];// 进行Base32编码
bit = 0;
tmp = 0;
}
}
geohash[i]= 0;//最后一位置0,数组结束标志
}
// 二分查找
int find(char c){
int start =0;
int end =31;
int mid =15;
for(;;){
// 二分查找
mid = (start + end)/ 2;
if(BASE32[mid]== c || start>= end){
return mid;
} elseif (c < BASE32[mid]){
end = mid;// 左侧
} else{
start = mid;// 右侧
}
}
}
void geohash_decode(double*latitude,double *longitude,int *precision,const char *geohash){
char is_even =1;
char index =0;
char tmp1 =0;
int bit =0;
double lats[2];
double lons[2];
double mid;
char *p= geohash;
*precision =0;
lats[0]= -90.0;
lats[1]= 90.0;
lons[0]= -180.0;
lons[1]= 180.0;
while(0!= *p){
bit = 0;
//index = find(*p);
index = UNBASE32[(*p- '0')];
while(bit<5){
tmp1 = (index >> (4-bit))& 0x01;// 将结果右移到最低位,和1求“与”后,取出值
if(is_even){
mid = (lons[0]+ lons[1])/ 2;
if(tmp1)
lons[0]= mid;// 右区间
else
lons[1]= mid;
} else {
mid = (lats[0]+ lats[1])/ 2;
if(tmp1)
lats[0]= mid;
else
lats[1]= mid;
}
is_even = !is_even;
bit++;
}
//printf("%d %c [%f,%f %f,%f]\n", index, *p, lats[0], lats[1],lons[0], lons[1]);
(*precision)++;
p++;
}
*latitude =(lats[0]+ lats[1])/ 2;
*longitude =(lons[0]+ lons[1])/ 2;
}
int main(){
double latitude =39.909605;
double longitude =116.397228;
int precision =5;
char geohash[20];
geohash_encode(latitude, longitude, precision, geohash);
printf("[%f,%f]编码结果:%s\n", latitude, longitude, geohash);
latitude = 0;
longitude = 0;
precision = 0;
geohash_decode(&latitude,&longitude,&precision, geohash);
printf("%s解码结果:[%f,%f]%d\n", geohash, latitude, longitude, precision);
return 0;
}
执行结果:
经度为5的时候
[39.909605,116.397228]编码结果:wx4g0
wx4g0 解码结果:[39.924316,116.389160] 5
经度为10的时候
[39.909605,116.397228]编码结果:wx4g09mf7c
wx4g09mf7c解码结果:[39.909604,116.397223] 10
经过对比,明显能看出精度为10的时候,解码的结果更接近原始经纬度值。