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,格式不太好,大家自己整理一下即可
- #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;
- }
2010-05-10 22:48
2207人阅读
收藏
举报
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,格式不太好,大家自己整理一下即可
- #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;
- }
LBS排名算法技术之一:基站轨迹定位算法
前言
我在哪?是LBS领域首先要解决的问题。因为技术限制,传统的GPS卫星定位只有室外的空旷地区才能够准确定位,对于室内环境来说,GPS定位往往会因“搜星”失败而无法定位。正因为GPS定位的天然缺陷,基于手机基站的定位技术正在蓬勃发展。然而因为基站的覆盖范围大,很难以取得高精度的效果,本文利用基站轨迹,提出了一个提高基站定位精度的方法。
关键字:基站定位,轨迹定位,Viterbi算法
绪论
对于单基站定位,如果仅根据用户当前的基站ID进行定位,精度必定有限。用户可能出现在基站覆盖范围内的任意一个地方,基站的覆盖范围越大,推测出来的用户位置就越不准。
如果我们还知道用户之前一段时间内经过的基站ID序列(称为基站轨迹),此时即可大致判定用户行动轨迹,可借此提高精度。
例举一个简单例子:
如上图所示,假设用户在一瞬间,基站ID从A切换到了B,此时用户属于B基站,单单从B这一个基站考虑的话,很难从其巨大的覆盖圆内取出精准位置。
但是从假设条件我们知道,用户之前一直是在A基站范围内,切换到B基站只是刚刚发生的事情,通过这个条件,人很容易就想到用户很大可能在两圆相交的位置(图中手机的位置)。当然,对于这种特殊情况来说,不光是人,程序也很容易去模拟,但是如果用户继续行走呢?比如一直走到B基站的右侧,还有算法可以继续推算吗?
本算法便是提出一种模型,用以解决此问题。
隐马尔可夫链模型(HMM)与Viterbi算法介绍
隐马尔可夫链作为一个常见的序列预测模型,其具体定义在这里就不细细展开了,有兴趣的用户可以到wiki上搜索相关资料。此模型已经在多个计算机场景中得到了成功的应用,比如拼音输入法中,我们输入”wo zai bai du da sha”,实际想输入的是“我在百度大厦”,也就是说其中有两个序列、一个是拼音字母序列(明序列),一个是汉字序列(隐序列)。两个序列之间有一定的相互关系,如何通过一个已知的“明序列”来推测另一个未知的“隐序列”便是隐马尔可夫链模型要解决的问题。本文的重点也是如何用隐马尔可夫链模型解决轨迹基站定位。
解决隐马尔可夫链问题中的一个著名算法是Viterbi算法。建议读者先去阅读wiki中的Viterbi算法介绍,里面有一个简单的天气的例子详细说明了该算法的大体过程。:
http://en.wikipedia.org/wiki/Viterbi_algorithm
算法细节隐马尔可夫链模型
我们知道隐马尔可夫链中有两个序列,一个是明序列(A1、A2、A3……),一个是隐序列(B1、B2、B3……)。在本模型之中,明序列(A1、A2、A3……)代表了用户经过的基站序列,隐序列是用户的实际位置序列。我们要做的是通过基站序列,来推测用户的实际位置序列。
首先,我们做如下假设:
● 用户行使在道路上。
●用户在匀速的行驶。
有了这两个假设,再把道路分解成一个一个的路段,我们便可以用路段序列来代替用户的位置序列。也就是说,我们需要通过观察到的用户基站序列,来推测用户的路段序列。如下图所示,我们观察到的是用户所经过的基站覆盖圆情况,需要求得的是用户的路段行驶轨迹。假设观察到基站轨迹是图中的绿色基站,那很明显用户最大可能是沿着红色箭头在行走。
Viterbi算法
如果要用Viterbi算法来解决本应用,只要知道如下几个问题即可。
路段的定义:将路网数据每隔10m抽象成一个有向线段,并且假设用户在这些路段之间离散的跳跃,每一个有向线段,即为一个路段。并且在这里假设用户将要跳跃到的路段只和当前路段相关,与过去的路段无关。
基站序列的定义:每秒钟检测其所处的基站,并且做记录。比如记录信息为(基站A、基站B、基站B、基站C),则表示用户在四秒钟的时间内分别属于基站ABBC三个基站,并且在B基站待了2秒。
路段序列定义:如果用户能够每秒钟记录其所属的路段,这个序列便是路段序列,这是用户的真实位置序列。
路段从属于基站的概率:Viterbi算法中需要知道隐状态从属于明状态的概率,拿到本应用中来看,便是要知道路段从属于基站的概率。解决此问题有如下两个方法:
1.根据路段在基站覆盖圆的具体位置来调整概率,比如,距离基站覆盖圆中心越近,则从属于此基站的概率越大。
2.根据真实的用户数据来推算概率。此方法最准确,其原理也简单,根据用户的真实位置(GPS点)变可以知道用户处于的哪个路段,也就知道了这个路段曾经属于过哪些基站,以及其概率。
如果有用户的真实数据,那么采用方案2无疑是最好的,但是没有数据的情况下,用方案1也可以最大可能的模拟。
路段之间的转移概率:路段之间的转移概率是本算法的重点,在这里做如下定义:假设路段A指向了路段B和C,那么从路段A转移到B和C的概率分别是33%,也就是概率等分,请注意路段A能转移到自身,其到自身的概率也是33%。
Viterbi算法大体过程
Viterbi算法过程是在各个路段之间进行概率转移。如下图所示,用户的基站序列为ABBC,则需要进行三(n-1)次概率转移过程。
对于基站序列ABBC来说,需要分成以下几步:
1.求得每一个路段的初始概率,即各个路段属于基站A的概率。
2.进行第一次转移,从基站A到基站B。比如路段A转移到路段B的概率为:路段A的概率×两个路段的转移概率×路段B属于基站的概率。
3.重复过程2,再转移两次,分别为 基站Bà基站B 基站Bà基站C。
4.最后取出概率最大的路段即为用户位置。
有心的读者可能已经看到,路段之间转移概率的定义是一个路段只能转移与其相连接的路段,这样有一个问题便是,按照上述算法可能所有路段的最终概率均为0。这是因为我们假设在时间间隔内,用户只能从一个路段行驶到相邻的路段,然而实际情况下,用户的速度可能较快,在我们扫描基站的间隔内,用户能跨越多个路段。对于这个问题,我们需要根据用户的速度来对用户扫描到的基站序列进行修复,比如速度是我们假定速度两倍的用户可以将基站序列ABBC变换成AABBBBCC。用户的速度可以通过速度传感器或者基站轨迹进行大概的推算,比如用户经过了10个基站,将这10个基站的覆盖圆中心连接起来,用总长除以时间,即为大概的速度。
算法效果
作者在实现本算法之后,对于匀速运动的定位用户的定位精度提升了一倍之多。对于一些特定用户,更是能明显的起到优化作用,比如高速公路上的用户,基站覆盖半径大,没有用本算法的话,只能返回巨大基站覆盖圆的中心给用户,定位精度极差。采用本算法之后定位精度会得到极大的提高,甚至用户的定位点丝毫不差的跟随用户的真实走动而走动,这也是因为在高速公路上路网比较简单,此算法效果会更突出。
方法一:基于球面距离搜索附近地点
点评:需要使用2个字段进行查询,查询效率差,适用于数据量较少的小型应用。
维基百科推荐使用 Haversine 公式计算球面距离
方法二:基于Geohash算法搜索附近地点
点评:精度可控,使用单字段字符串前缀查询,查询效率较高,适用于大数据的应用。
其它方法:
PHP源代码
1 //
2 // 使用 Haversine 公式计算两个地理坐标点之间的球面距离。
3 //
4 // 参数说明:
5 // $lat1 点1的纬度值,单位:degree
6 // $lng1 点1的经度值,单位:degree
7 // $lat2 点2的纬度值,单位:degree
8 // $lng2 点2的经度值,单位:degree
9 //
10 // 返回值:两点之间的球面距离,单位: km
11 //
12 function haversine_distance($lat1, $lng1, $lat2, $lng2) {
13 $EARTH_RADIUS = 6371.00; // 地球平均半径,6371km
14
15 // 角度转换成弧度
16 $rlat1 = deg2rad($lat1);
17 $rlng1 = deg2rad($lng1);
18 $rlat2 = deg2rad($lat2);
19 $rlng2 = deg2rad($lng2);
20
21 $rlat_diff = abs($rlat1 - $rlat2);
22 $rlng_diff = abs($rlng1 - $rlng2);
23
24 $h = pow(sin($rlat_diff/2), 2) + cos($rlat1) * cos($rlat2) * pow(sin($rlng_diff/2), 2);
25 return 2 * $EARTH_RADIUS * asin(sqrt($h));
26 }
27
28 //
29 // 根据到中心点的距离计算东西两侧的经度边界范围,采用 Haversine 公式计算。
30 //
31 // 参数说明:
32 // $lat 中心点的纬度,单位:degree
33 // $lng 中心点的经度,单位:degree
34 // $distance 到中心点的距离,单位:km
35 //
36 // 返回值: array($lat_diff, $lng_diff)
37 // $lat_diff 到中心点的纬度边界范围,单位: degree
38 // $lng_diff 到中心点的纬度边界范围,单位: degree
39 //
40 function lbs_haversine_diff($lat, $lng, $distance) {
41 $EARTH_RADIUS = 6371.00; // 地球平均半径,6371km
42
43 // 纬度边界
44 $rlat_diff = $distance / $EARTH_RADIUS;
45 $dlat_diff = rad2deg($rlat_diff);
46
47 // 经度边界
48 $rlat = deg2rad($lat);
49 $rlng_diff = 2 * asin( sin($distance / (2 * $EARTH_RADIUS)) / cos($rlat) );
50 $dlng_diff = rad2deg($rlng_diff);
51
52 return array($dlat_diff, $dlng_diff);
53 }
基于LBS的应用在眼下已是如火如荼,地理位置功能都相应的被添加在各大应用中,基本上算是作为了行业的标杆。最近开发的一个应用也是涉及到对用户发表帖子的当前位置下的附近帖子的搜索,类似的搜索功能其实也不是什么新鲜事,但是貌似都没有公布其实现,所以当时也是非常的茫然。
想法一:
最容易想到的肯定就是给定范围然后直接全库搜索,但是一旦数据量过大,性能肯定下降得非常快,所以行不通否定了。
想法二:
幸好之前有看到过一些介绍R树的文章,就是基于空间搜索,将常用的数据库的一维搜索扩展到了二维甚至是多维。虽然有了点思路,但是有看过R树的就应该知道,R树的分裂与合并是比较麻烦的,如果自己写,肯定是没有戏的。所以很快就否定了,真是有理论没得实践。
想法三:
后来在网上搜到还有一种常用的算法是geohash算法,它是一种地址编码,它能把二维的经纬度编码成一维的字符串,但是其算法自身也是有缺陷的,位于格子边界两侧的两点,虽然十分接近,但编码会完全不同。实际应用中,可以同时搜索当前格子周围的8个格子,即可解决这个问题,虽然不是十全十美,但是还是能解决问题。。
想法四:
因为想法三的局限性,所以就进一步查了下,突然发现一个好东西,那就是空间数据库,而其底层的实现方式果然就是用到了想法二中的R树算法,所以虽然想法二自己不能实现,但是还是证明了其强大性,O(∩_∩)O哈哈~。。
那下面就具体的介绍一下想法三和想法四的具体实现:
一、geohash算法
geohash算法有以下几个特点:
首先,geohash用一个字符串表示经度和纬度两个坐标。某些情况下无法在两列上同时应用索引(例如MySQL 4之前的版本,Google App Engine的数据层等),利用geohash,只需在一列上应用索引即可。
其次,geohash表示的并不是一个点,而是一个矩形区域。比如编码wx4g0ec19,它表示的是一个矩形区域。使用者可以发布地址编码,既能表明自己位于北海公园附近,又不至于暴露自己的精确坐标,有助于隐私保护。
第三,编码的前缀可以表示更大的区域。例如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。这个特性可以用于附近地点搜索。首先根据用户当前坐标计算geohash(例如wx4g0ec1)然后取其前缀进行查询(SELECT * FROM place WHERE geohash LIKE 'wx4g0e%'
),即可查询附近的所有地点。
下面以(39.92324, 116.3906)为例,介绍一下geohash的编码算法。首先将纬度范围(-90, 90)平分成两个区间(-90, 0)、(0, 90),如果目标纬度位于前一个区间,则编码为0,否则编码为1。由于39.92324属于(0, 90),所以取编码为1。然后再将(0, 90)分成 (0, 45), (45, 90)两个区间,而39.92324位于(0, 45),所以编码为0。以此类推,直到精度符合要求为止,得到纬度编码为1011 1000 1100 0111 1001。
纬度范围 | 划分区间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的编码为1101 0010 1100 0100 0100。
经度范围 | 划分区间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 |
接下来将经度和纬度的编码合并,奇数位是纬度,偶数位是经度,得到编码 11100 11101 00100 01111 00000 01101 01011 00001。
最后,用0-9、b-z(去掉a, i, l, o)这32个字母进行base32编码,得到(39.92324, 116.3906)的编码为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 |
解码算法与编码算法相反,先进行base32解码,然后分离出经纬度,最后根据二进制编码对经纬度范围进行细分即可,这里不再赘述。不过由于geohash表示的是区间,编码越长越精确,但不可能解码出完全一致的地址。
geohash的应用:附近地址搜索
geohash的最大用途就是附近地址搜索了。不过,从geohash的编码算法中可以看出它的一个缺点:位于格子边界两侧的两点,虽然十分接近,但编码会完全不同。实际应用中,可以同时搜索当前格子周围的8个格子,即可解决这个问题。
二、空间数据库
根据OpenGIS规范的的空间数据库实现有几种方式,自己采用的是Mysql的实现,比较简单,但是Mysql存在一些尚未实现的GIS特性,所以如果你的应用相对复杂的话,可以采用其它实现方式。
对于Mysql的具体实现请参考Mysql中的空间扩展文档
http://dev.mysql.com/doc/refman/5.1/zh/spatial-extensions-in-mysql.html#gis-class-polygon
先关参考:
从B树、B+树、B*谈到R树
http://blog.csdn.net/v_JULY_v/article/details/6530142
geohash:用字符串实现附近地点搜索
http://tech.idv2.com/2011/07/05/geohash-intro/
MySQL中的空间扩展
http://dev.mysql.com/doc/refman/5.1/zh/spatial-extensions-in-mysql.html#gis-class-polygon
基于LBS的应用在眼下已是如火如荼,地理位置功能都相应的被添加在各大应用中,基本上算是作为了行业的标杆。最近开发的一个应用也是涉及到对用户发表帖子的当前位置下的附近帖子的搜索,类似的搜索功能其实也不是什么新鲜事,但是貌似都没有公布其实现,所以当时也是非常的茫然。
想法一:
最容易想到的肯定就是给定范围然后直接全库搜索,但是一旦数据量过大,性能肯定下降得非常快,所以行不通否定了。
想法二:
幸好之前有看到过一些介绍R树的文章,就是基于空间搜索,将常用的数据库的一维搜索扩展到了二维甚至是多维。虽然有了点思路,但是有看过R树的就应该知道,R树的分裂与合并是比较麻烦的,如果自己写,肯定是没有戏的。所以很快就否定了,真是有理论没得实践。
想法三:
后来在网上搜到还有一种常用的算法是geohash算法,它是一种地址编码,它能把二维的经纬度编码成一维的字符串,但是其算法自身也是有缺陷的,位于格子边界两侧的两点,虽然十分接近,但编码会完全不同。实际应用中,可以同时搜索当前格子周围的8个格子,即可解决这个问题,虽然不是十全十美,但是还是能解决问题。。
想法四:
因为想法三的局限性,所以就进一步查了下,突然发现一个好东西,那就是空间数据库,而其底层的实现方式果然就是用到了想法二中的R树算法,所以虽然想法二自己不能实现,但是还是证明了其强大性,O(∩_∩)O哈哈~。。
那下面就具体的介绍一下想法三和想法四的具体实现:
一、geohash算法
geohash算法有以下几个特点:
首先,geohash用一个字符串表示经度和纬度两个坐标。某些情况下无法在两列上同时应用索引(例如MySQL 4之前的版本,Google App Engine的数据层等),利用geohash,只需在一列上应用索引即可。
其次,geohash表示的并不是一个点,而是一个矩形区域。比如编码wx4g0ec19,它表示的是一个矩形区域。使用者可以发布地址编码,既能表明自己位于北海公园附近,又不至于暴露自己的精确坐标,有助于隐私保护。
第三,编码的前缀可以表示更大的区域。例如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。这个特性可以用于附近地点搜索。首先根据用户当前坐标计算geohash(例如wx4g0ec1)然后取其前缀进行查询(SELECT * FROM place WHERE geohash LIKE 'wx4g0e%'
),即可查询附近的所有地点。
下面以(39.92324, 116.3906)为例,介绍一下geohash的编码算法。首先将纬度范围(-90, 90)平分成两个区间(-90, 0)、(0, 90),如果目标纬度位于前一个区间,则编码为0,否则编码为1。由于39.92324属于(0, 90),所以取编码为1。然后再将(0, 90)分成 (0, 45), (45, 90)两个区间,而39.92324位于(0, 45),所以编码为0。以此类推,直到精度符合要求为止,得到纬度编码为1011 1000 1100 0111 1001。
纬度范围 | 划分区间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的编码为1101 0010 1100 0100 0100。
经度范围 | 划分区间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 |
接下来将经度和纬度的编码合并,奇数位是纬度,偶数位是经度,得到编码 11100 11101 00100 01111 00000 01101 01011 00001。
最后,用0-9、b-z(去掉a, i, l, o)这32个字母进行base32编码,得到(39.92324, 116.3906)的编码为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 |
解码算法与编码算法相反,先进行base32解码,然后分离出经纬度,最后根据二进制编码对经纬度范围进行细分即可,这里不再赘述。不过由于geohash表示的是区间,编码越长越精确,但不可能解码出完全一致的地址。
geohash的应用:附近地址搜索
geohash的最大用途就是附近地址搜索了。不过,从geohash的编码算法中可以看出它的一个缺点:位于格子边界两侧的两点,虽然十分接近,但编码会完全不同。实际应用中,可以同时搜索当前格子周围的8个格子,即可解决这个问题。
二、空间数据库
根据OpenGIS规范的的空间数据库实现有几种方式,自己采用的是Mysql的实现,比较简单,但是Mysql存在一些尚未实现的GIS特性,所以如果你的应用相对复杂的话,可以采用其它实现方式。
对于Mysql的具体实现请参考Mysql中的空间扩展文档
http://dev.mysql.com/doc/refman/5.1/zh/spatial-extensions-in-mysql.html#gis-class-polygon
先关参考:
从B树、B+树、B*谈到R树
http://blog.csdn.net/v_JULY_v/article/details/6530142
geohash:用字符串实现附近地点搜索
http://tech.idv2.com/2011/07/05/geohash-intro/
MySQL中的空间扩展
http://dev.mysql.com/doc/refman/5.1/zh/spatial-extensions-in-mysql.html#gis-class-polygon