geohash: 一个实用的geocoding方法
来自:http://blog.csdn.net/historyasamirror/archive/2010/06/01/5638248.aspx
这年头和 location 相关的应用越来越火。从 foursquare 的热闹程度就可见一般(什么,没听过 foursquare…. 哥们,你 out 了)。和 location 有关的应用一般都包括一些共同的操作,最常见的一个,就是找附近的东东(餐馆,商店 …. )。
所以,这里就抛出了一个问题,怎样才能知道两个物体离得近呢?
我之前转过一篇 blog ,是关于 用 cellid进行定位的 ,当然,这种方法是在不得已的情况下才使用,比如得不到 gps 。这里,我们假设可以拿到两个物体的 gps 数据,所以一个最直观的办法,计算两个 gps 点的直线距离。当然,这个计算不精确,不要忘了,地球是圆的,所以两个 gps 点之间的距离应该是一个弧线。上网搜一下,应该能找到一个复杂的公式,专门用来计算这个弧长。
对于两个点来说,上述的方法就够用了。当如果有很多个点呢,难道要我计算两两之间的距离么?
这个问题属于 spatial data indexing&management 的范畴,有很多关于 database 或者 GIS 的书都会讲到一些解决的算法和特殊的数据结构。我在这只介绍一个简单的方法,叫做 geohash 。
geohash 其实是对 gps 数据进行了编码,使得上述问题更容易得到解决。(关于 geohash 的详细论述可以看 wiki ,介绍得很全面)
假设我们有一个 gps 数据,为 <42.6, -5.6> ,首先 (1) 我们会将经纬度分别编码成一个 binarycode ,比如纬度 42.6 被编码成“ 101111001001 ”,经度 -5.6 被翻译成“ 0111110000000 ”,然后 (2) 将两个 binarycode 连起来,经度的 binarycode 作为奇数位,纬度的 binarycode 作为偶数位,就变成了“ 01101 11111 11000 00100 00010 ”,最后,将这个 binarycode 转化为一个 32 进制的字符串,变成“ ezs42 ”。
需要说一下经纬度转化成 binarycode 的算法。举例来说,比如纬度的范围是 +90 ~ -90 ,我们将这个分为两个区间,分别是 (-90, 0) 和 (0,90) ,如果 gps 的 x( 纬度 ) 落在了第一个区间,那么它的第一位 binarycode 就是 0 ,如果落在第二个区间,那么它的第一位 binarycode 就是 1 ,显然 42.6 是在第二个区间,所以它的第一位 binarycode 是 1 ,然后再对 (0,90) 这个区间做二分,再计算下一步的 binarycode….
编码方式说完了,说说 geohash 的好处。当两个 gps 数据对应的 geohash 数据有一定长度的前缀是相同的,表示这两个数据在一定程度上距离接近,相同的前缀越长,那么两个点越离得近。( nearby places will often (but not always) present similar prefixes. )
注意,需要提及的是,两个 geohash 有相同前缀,表示这两个点离得近,但是!两个点离得近,不一定 geohash 有相同的前缀。 geohash 在这里存在一个缺陷,就是所谓的 edge case 。详情见 wiki 。
再往深入琢磨一下, geohash 的本质是什么。其实它就是对一个二维平面进行了一个索引,首先对这个平面竖着切一刀,刀的左边标记为 0 ,刀的右边标记为 1 ,然后再横着切一刀,并且继续标记,然后再竖着切 …. 有很多 spatial data indexing 的方法都是这样的思路,它的作用就是把平面的这种二维数据改造成一维的数据。而一维数据有个好处,就是可以做 sorting 。
到此,我们还没有回答之前提的问题,如果有很多点,该怎样从其中找出附近的点呢?答案貌似已经呼之欲出,俺就不多说了。
Reference :
http://en.wikipedia.org/wiki/Geohash
Geohash算法 收藏
1.算法背景 Geohash的初衷是如何用尽量短的URL来标志地图上的某个位置,而地图上的位置一般是用经纬度来表示,问题就转化为如何把经纬度转化为一个尽量短的URL。 Geohash的算法描述请参考:http://en.wikipedia.org/wiki/Geohash ,本文的主要目的是更加细致地解释该算法的原理及实用场景。
2.算法 算法的主要思想是对某一数字通过二分法进行无限逼近,比如纬度的区间是[-90,90],假如给定一个纬度值:46.5,可以通过下面算法对46.5进行无限逼近:
(1)把区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定46.5属于右区间[0,90]
(2)递归上述过程46.5总是属于某个区间,无论第几次迭代46.5总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,根据极限可知[a,b]会收敛到46.5,用δ来描述就是,任意给定一个 ε,总存在一个N使得: δ=|x-a/2N |< ε,x为任意给定的纬度
(3)上述分析过程保证了算法收敛性的同时,再记录一下收敛的过程:如果给定的纬度x(46.5)属于左区间,则记录0,如果属于右区间则记录1,这样随着算法的进行会产生一个序列1011100,序列的长度跟给定的收敛次数N相关。 反过来,如果我们知道了序列1011100,我们就可以分别能确定纬度x(46.5)属于哪个更小的迭代区间,也就是说该算法是可逆的
(4)算法的精度:显然的是,不可能让计算机执行无穷计算,加入执行N此计算,则x属于的区间长度为(b-a)/2N+1 ,以纬度计算为例,则为180/2N ,误差近似计算为:err= 180/2N+1 =90/2N ,如果N=20,则误差最大为:0.00009。但无论如何这样表明Geohash是一种近似算法。
3.编码 在对纬度产生了序列1011100后,在对经度做相同的算法也会产生一个序列,比如0011101。根据偶数位放经度,奇数位放纬度(0被视为偶数),把2个串合二为一,产生一个新串:01001111110010,对该串进行Base32编码,则可获得一个ASIIC码的字符串,关于Base32编码,请参考:http://en.wikipedia.org/wiki/Base32
4.解码 解码的过程相对比较简单
(1)对拿到的字符串进行Base32解码
(2)根据奇偶位取出纬度、经度
(3)根据序列反向得到每个区间,并取中间值(0为左区间,1为右区间)
5.应用 该算法目前主要用在地图的地址搜索,有了该算法可以为数据库中的地址建立索引,极大提高地图数据检索的速度。 仔细观察,该算法还有另为一个特点,对相近的x,y,会得到相同前缀的序列,原因是相近的x,y,在递归的绝大多数时间会处在同一个区间,故而,逼近的轨迹是一致的,这又可以解决地图中“离我最近的搜索“的问题,同时,对进行hash的模糊检索也有一定的启发作用。
6.代码 下面的实现代码很精美,引用自:http://bloggermap.org/rss/readblog/70907,格式不太好,大家自己整理一下即可
C版
#define BASE32 "0123456789bcdefghjkmnpqrstuvwxyz"
static void encode_geohash(double latitude, double longitude, int precision, char *geohash) { int is_even=1, i=0; double lat[2], lon[2], mid; char bits[] = {16,8,4,2,1}; int bit=0, ch=0; lat[0] = -90.0; lat[1] = 90.0; lon[0] = -180.0; lon[1] = 180.0; while (i < precision) { if (is_even) { mid = (lon[0] + lon[1]) / 2; if (longitude > mid) { ch |= bits[bit]; lon[0] = mid; } else lon[1] = mid; } else { mid = (lat[0] + lat[1]) / 2; if (latitude > mid) { ch |= bits[bit]; lat[0] = mid; } else lat[1] = mid; } is_even = !is_even; if (bit < 4) bit++; else { geohash[i++] = BASE32[ch]; bit = 0; ch = 0; } } geohash[i] = 0; }
C#版 public class GeoHashClass {
public const string BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"; //对经纬度进行GeoHash编码
public string encode_geohash(double latitude, double longitude, int precision) { char[] geohash = new char[precision]; bool is_even = true; //1 int i=0; double[] lat=new double[2]; double[] lon=new double[2]; double mid; //char[] bits =new char[]{16,8,4,2,1}; int[] bits = new int[] { 16, 8, 4, 2, 1 }; int bit=0, ch=0; lat[0] = -90.0; lat[1] = 90.0; //纬度(-90,90) lon[0] = -180.0; lon[1] = 180.0; //经度(-180,180) while (i < precision) { if (is_even==true) { mid = (lon[0] + lon[1]) / 2; //求中间经度值 if (longitude > mid) { ch |= bits[bit]; lon[0] = mid; } else lon[1] = mid; } else { mid = (lat[0] + lat[1]) / 2; //求中间纬度值 if (latitude > mid) { ch |= bits[bit]; lat[0] = mid; } else lat[1] = mid; } is_even = !is_even; if (bit < 4) bit++; else { geohash[i++] = GeoHashClass.BASE32[ch]; bit = 0; ch = 0; } } geohash[i] = '0'; string rbc=""; for (int k = 0; k < geohash.Length; k++) { rbc += geohash[k].ToString(); } return rbc; } }