geohash算法原理及实现方式

1、geohash特点

2、geohash原理

3、geohash的php 、python、java、C#实现代码

4、观点讨论

 w微博:http://weibo.com/dxl0321

geohash有以下几个特点:

首先,geohash用一个字符串表示经度和纬度两个坐标。某些情况下无法在两列上同时应用索引 (例如MySQL 4之前的版本,Google App Engine的数据层等),利用geohash,只需在一列上应用索引即可。

其次,geohash表示的并不是一个点,而是一个矩形区域。比如编码wx4g0ec19,它表示的是一个矩形区域。 使用者可以发布地址编码,既能表明自己位于北海公园附近,又不至于暴露自己的精确坐标,有助于隐私保护。

第三,编码的前缀可以表示更大的区域。例如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。 这个特性可以用于附近地点搜索。首先根据用户当前坐标计算geohash(例如wx4g0ec1)然后取其前缀进行查询 (SELECT * FROM place WHERE geohash LIKE 'wx4g0e%'),即可查询附近的所有地点。

Geohash比直接用经纬度的高效很多。

 

Geohash的原理

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解码,然后分离出经纬度,最后根据二进制编码对经纬度范围进行细分即可,这里不再赘述。

实现代码:

php版本的实现方式:http://blog.dixo.net/downloads/geohash-php-class/  我下载了一个上传的

 php:

geohash.class.php

View Code
  1 <?php

  2 /**

  3  * Geohash generation class

  4  * http://blog.dixo.net/downloads/

  5  *

  6  * This file copyright (C) 2008 Paul Dixon ([email protected])

  7  *

  8  * This program is free software; you can redistribute it and/or

  9  * modify it under the terms of the GNU General Public License

 10  * as published by the Free Software Foundation; either version 3

 11  * of the License, or (at your option) any later version.

 12  *

 13  * This program is distributed in the hope that it will be useful,

 14  * but WITHOUT ANY WARRANTY; without even the implied warranty of

 15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the

 16  * GNU General Public License for more details.

 17  *

 18  * You should have received a copy of the GNU General Public License

 19  * along with this program; if not, write to the Free Software

 20  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

 21  */

 22 

 23 

 24 

 25 /**

 26 * Encode and decode geohashes

 27 *

 28 */

 29 class Geohash

 30 {

 31     private $coding="0123456789bcdefghjkmnpqrstuvwxyz";

 32     private $codingMap=array();

 33     

 34     public function Geohash()

 35     {

 36         //build map from encoding char to 0 padded bitfield

 37         for($i=0; $i<32; $i++)

 38         {

 39             $this->codingMap[substr($this->coding,$i,1)]=str_pad(decbin($i), 5, "0", STR_PAD_LEFT);

 40         }

 41         

 42     }

 43     

 44     /**

 45     * Decode a geohash and return an array with decimal lat,long in it

 46     */

 47     public function decode($hash)

 48     {

 49         //decode hash into binary string

 50         $binary="";

 51         $hl=strlen($hash);

 52         for($i=0; $i<$hl; $i++)

 53         {

 54             $binary.=$this->codingMap[substr($hash,$i,1)];

 55         }

 56         

 57         //split the binary into lat and log binary strings

 58         $bl=strlen($binary);

 59         $blat="";

 60         $blong="";

 61         for ($i=0; $i<$bl; $i++)

 62         {

 63             if ($i%2)

 64                 $blat=$blat.substr($binary,$i,1);

 65             else

 66                 $blong=$blong.substr($binary,$i,1);

 67             

 68         }

 69         

 70         //now concert to decimal

 71         $lat=$this->binDecode($blat,-90,90);

 72         $long=$this->binDecode($blong,-180,180);

 73         

 74         //figure out how precise the bit count makes this calculation

 75         $latErr=$this->calcError(strlen($blat),-90,90);

 76         $longErr=$this->calcError(strlen($blong),-180,180);

 77                 

 78         //how many decimal places should we use? There's a little art to

 79         //this to ensure I get the same roundings as geohash.org

 80         $latPlaces=max(1, -round(log10($latErr))) - 1;

 81         $longPlaces=max(1, -round(log10($longErr))) - 1;

 82         

 83         //round it

 84         $lat=round($lat, $latPlaces);

 85         $long=round($long, $longPlaces);

 86         

 87         return array($lat,$long);

 88     }

 89 

 90     

 91     /**

 92     * Encode a hash from given lat and long

 93     */

 94     public function encode($lat,$long)

 95     {

 96         //how many bits does latitude need?    

 97         $plat=$this->precision($lat);

 98         $latbits=1;

 99         $err=45;

100         while($err>$plat)

101         {

102             $latbits++;

103             $err/=2;

104         }

105         

106         //how many bits does longitude need?

107         $plong=$this->precision($long);

108         $longbits=1;

109         $err=90;

110         while($err>$plong)

111         {

112             $longbits++;

113             $err/=2;

114         }

115         

116         //bit counts need to be equal

117         $bits=max($latbits,$longbits);

118         

119         //as the hash create bits in groups of 5, lets not

120         //waste any bits - lets bulk it up to a multiple of 5

121         //and favour the longitude for any odd bits

122         $longbits=$bits;

123         $latbits=$bits;

124         $addlong=1;

125         while (($longbits+$latbits)%5 != 0)

126         {

127             $longbits+=$addlong;

128             $latbits+=!$addlong;

129             $addlong=!$addlong;

130         }

131         

132         

133         //encode each as binary string

134         $blat=$this->binEncode($lat,-90,90, $latbits);

135         $blong=$this->binEncode($long,-180,180,$longbits);

136         

137         //merge lat and long together

138         $binary="";

139         $uselong=1;

140         while (strlen($blat)+strlen($blong))

141         {

142             if ($uselong)

143             {

144                 $binary=$binary.substr($blong,0,1);

145                 $blong=substr($blong,1);

146             }

147             else

148             {

149                 $binary=$binary.substr($blat,0,1);

150                 $blat=substr($blat,1);

151             }

152             $uselong=!$uselong;

153         }

154         

155         //convert binary string to hash

156         $hash="";

157         for ($i=0; $i<strlen($binary); $i+=5)

158         {

159             $n=bindec(substr($binary,$i,5));

160             $hash=$hash.$this->coding[$n];

161         }

162         

163         

164         return $hash;

165     }

166     

167     /**

168     * What's the maximum error for $bits bits covering a range $min to $max

169     */

170     private function calcError($bits,$min,$max)

171     {

172         $err=($max-$min)/2;

173         while ($bits--)

174             $err/=2;

175         return $err;

176     }

177     

178     /*

179     * returns precision of number

180     * precision of 42 is 0.5

181     * precision of 42.4 is 0.05

182     * precision of 42.41 is 0.005 etc

183     */

184     private function precision($number)

185     {

186         $precision=0;

187         $pt=strpos($number,'.');

188         if ($pt!==false)

189         {

190             $precision=-(strlen($number)-$pt-1);

191         }

192         

193         return pow(10,$precision)/2;

194     }

195     

196     

197     /**

198     * create binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example

199     * removing the tail recursion is left an exercise for the reader

200     */

201     private function binEncode($number, $min, $max, $bitcount)

202     {

203         if ($bitcount==0)

204             return "";

205         

206         #echo "$bitcount: $min $max<br>";

207             

208         //this is our mid point - we will produce a bit to say

209         //whether $number is above or below this mid point

210         $mid=($min+$max)/2;

211         if ($number>$mid)

212             return "1".$this->binEncode($number, $mid, $max,$bitcount-1);

213         else

214             return "0".$this->binEncode($number, $min, $mid,$bitcount-1);

215     }

216     

217 

218     /**

219     * decodes binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example

220     * removing the tail recursion is left an exercise for the reader

221     */

222     private function binDecode($binary, $min, $max)

223     {

224         $mid=($min+$max)/2;

225         

226         if (strlen($binary)==0)

227             return $mid;

228             

229         $bit=substr($binary,0,1);

230         $binary=substr($binary,1);

231         

232         if ($bit==1)

233             return $this->binDecode($binary, $mid, $max);

234         else

235             return $this->binDecode($binary, $min, $mid);

236     }

237 }

238 

239 

240 

241 

242 

243 

244 ?>

 

python:

python版本的geohash:python-geohash

java:

java版本的geohash,实现:http://code.google.com/p/geospatialweb/source/browse/#svn/trunk/geohash/src

View Code
  1 import java.io.File;  

  2 import java.io.FileInputStream;  

  3 import java.util.BitSet;  

  4 import java.util.HashMap;  

  5   

  6   

  7 public class Geohash {  

  8   

  9     private static int numbits = 6 * 5;  

 10     final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8',  

 11             '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',  

 12             'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };  

 13       

 14     final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();  

 15     static {  

 16         int i = 0;  

 17         for (char c : digits)  

 18             lookup.put(c, i++);  

 19     }  

 20   

 21     public static void main(String[] args)  throws Exception{  

 22   

 23         System.out.println(new Geohash().encode(45, 125));  

 24               

 25     }  

 26 

 27     public double[] decode(String geohash) {  

 28         StringBuilder buffer = new StringBuilder();  

 29         for (char c : geohash.toCharArray()) {  

 30   

 31             int i = lookup.get(c) + 32;  

 32             buffer.append( Integer.toString(i, 2).substring(1) );  

 33         }  

 34           

 35         BitSet lonset = new BitSet();  

 36         BitSet latset = new BitSet();  

 37           

 38         //even bits  

 39         int j =0;  

 40         for (int i=0; i< numbits*2;i+=2) {  

 41             boolean isSet = false;  

 42             if ( i < buffer.length() )  

 43               isSet = buffer.charAt(i) == '1';  

 44             lonset.set(j++, isSet);  

 45         }  

 46           

 47         //odd bits  

 48         j=0;  

 49         for (int i=1; i< numbits*2;i+=2) {  

 50             boolean isSet = false;  

 51             if ( i < buffer.length() )  

 52               isSet = buffer.charAt(i) == '1';  

 53             latset.set(j++, isSet);  

 54         }  

 55           

 56         double lon = decode(lonset, -180, 180);  

 57         double lat = decode(latset, -90, 90);  

 58           

 59         return new double[] {lat, lon};       

 60     }  

 61       

 62     private double decode(BitSet bs, double floor, double ceiling) {  

 63         double mid = 0;  

 64         for (int i=0; i<bs.length(); i++) {  

 65             mid = (floor + ceiling) / 2;  

 66             if (bs.get(i))  

 67                 floor = mid;  

 68             else  

 69                 ceiling = mid;  

 70         }  

 71         return mid;  

 72     }  

 73       

 74       

 75     public String encode(double lat, double lon) {  

 76         BitSet latbits = getBits(lat, -90, 90);  

 77         BitSet lonbits = getBits(lon, -180, 180);  

 78         StringBuilder buffer = new StringBuilder();  

 79         for (int i = 0; i < numbits; i++) {  

 80             buffer.append( (lonbits.get(i))?'1':'0');  

 81             buffer.append( (latbits.get(i))?'1':'0');  

 82         }  

 83         return base32(Long.parseLong(buffer.toString(), 2));  

 84     }  

 85   

 86     private BitSet getBits(double lat, double floor, double ceiling) {  

 87         BitSet buffer = new BitSet(numbits);  

 88         for (int i = 0; i < numbits; i++) {  

 89             double mid = (floor + ceiling) / 2;  

 90             if (lat >= mid) {  

 91                 buffer.set(i);  

 92                 floor = mid;  

 93             } else {  

 94                 ceiling = mid;  

 95             }  

 96         }  

 97         return buffer;  

 98     }  

 99   

100     public static String base32(long i) {  

101         char[] buf = new char[65];  

102         int charPos = 64;  

103         boolean negative = (i < 0);  

104         if (!negative)  

105             i = -i;  

106         while (i <= -32) {  

107             buf[charPos--] = digits[(int) (-(i % 32))];  

108             i /= 32;  

109         }  

110         buf[charPos] = digits[(int) (-i)];  

111   

112         if (negative)  

113             buf[--charPos] = '-';  

114         return new String(buf, charPos, (65 - charPos));  

115     }  

116   

117 }  

C#:

 C#版本的geohash代
  1 using System;

  2 

  3 namespace sharonjl.utils

  4 {

  5     public static class Geohash

  6     {

  7         #region Direction enum

  8 

  9         public enum Direction

 10         {

 11             Top = 0,

 12             Right = 1,

 13             Bottom = 2,

 14             Left = 3 

 15         }

 16 

 17         #endregion

 18 

 19         private const string Base32 = "0123456789bcdefghjkmnpqrstuvwxyz";

 20         private static readonly int[] Bits = new[] {16, 8, 4, 2, 1};

 21 

 22         private static readonly string[][] Neighbors = {

 23                                                            new[]

 24                                                                {

 25                                                                    "p0r21436x8zb9dcf5h7kjnmqesgutwvy", // Top

 26                                                                    "bc01fg45238967deuvhjyznpkmstqrwx", // Right

 27                                                                    "14365h7k9dcfesgujnmqp0r2twvyx8zb", // Bottom

 28                                                                    "238967debc01fg45kmstqrwxuvhjyznp", // Left

 29                                                                }, new[]

 30                                                                       {

 31                                                                           "bc01fg45238967deuvhjyznpkmstqrwx", // Top

 32                                                                           "p0r21436x8zb9dcf5h7kjnmqesgutwvy", // Right

 33                                                                           "238967debc01fg45kmstqrwxuvhjyznp", // Bottom

 34                                                                           "14365h7k9dcfesgujnmqp0r2twvyx8zb", // Left

 35                                                                       }

 36                                                        };

 37 

 38         private static readonly string[][] Borders = {

 39                                                          new[] {"prxz", "bcfguvyz", "028b", "0145hjnp"},

 40                                                          new[] {"bcfguvyz", "prxz", "0145hjnp", "028b"}

 41                                                      };

 42 

 43         public static String CalculateAdjacent(String hash, Direction direction)

 44         {

 45             hash = hash.ToLower();

 46 

 47             char lastChr = hash[hash.Length - 1];

 48             int type = hash.Length%2;

 49             var dir = (int) direction;

 50             string nHash = hash.Substring(0, hash.Length - 1);

 51 

 52             if (Borders[type][dir].IndexOf(lastChr) != -1)

 53             {

 54                 nHash = CalculateAdjacent(nHash, (Direction) dir);

 55             }

 56             return nHash + Base32[Neighbors[type][dir].IndexOf(lastChr)];

 57         }

 58 

 59         public static void RefineInterval(ref double[] interval, int cd, int mask)

 60         {

 61             if ((cd & mask) != 0)

 62             {

 63                 interval[0] = (interval[0] + interval[1])/2;

 64             }

 65             else

 66             {

 67                 interval[1] = (interval[0] + interval[1])/2;

 68             }

 69         }

 70 

 71         public static double[] Decode(String geohash)

 72         {

 73             bool even = true;

 74             double[] lat = {-90.0, 90.0};

 75             double[] lon = {-180.0, 180.0};

 76 

 77             foreach (char c in geohash)

 78             {

 79                 int cd = Base32.IndexOf(c);

 80                 for (int j = 0; j < 5; j++)

 81                 {

 82                     int mask = Bits[j];

 83                     if (even)

 84                     {

 85                         RefineInterval(ref lon, cd, mask);

 86                     }

 87                     else

 88                     {

 89                         RefineInterval(ref lat, cd, mask);

 90                     }

 91                     even = !even;

 92                 }

 93             }

 94 

 95             return new[] {(lat[0] + lat[1])/2, (lon[0] + lon[1])/2};

 96         }

 97 

 98         public static String Encode(double latitude, double longitude, int precision = 12)

 99         {

100             bool even = true;

101             int bit = 0;

102             int ch = 0;

103             string geohash = "";

104 

105             double[] lat = {-90.0, 90.0};

106             double[] lon = {-180.0, 180.0};

107 

108             if (precision < 1 || precision > 20) precision = 12;

109 

110             while (geohash.Length < precision)

111             {

112                 double mid;

113 

114                 if (even)

115                 {

116                     mid = (lon[0] + lon[1])/2;

117                     if (longitude > mid)

118                     {

119                         ch |= Bits[bit];

120                         lon[0] = mid;

121                     }

122                     else

123                         lon[1] = mid;

124                 }

125                 else

126                 {

127                     mid = (lat[0] + lat[1])/2;

128                     if (latitude > mid)

129                     {

130                         ch |= Bits[bit];

131                         lat[0] = mid;

132                     }

133                     else

134                         lat[1] = mid;

135                 }

136 

137                 even = !even;

138                 if (bit < 4)

139                     bit++;

140                 else

141                 {

142                     geohash += Base32[ch];

143                     bit = 0;

144                     ch = 0;

145                 }

146             }

147             return geohash;

148         }

149     }

150 }

C#代码来自:https://github.com/sharonjl/geohash-net

geohash演示:http://openlocation.org/geohash/geohash-js/

 

各种版本下载:打包下载

观点讨论

引用阿里云以为技术专家的博客上的讨论:

1.两个离的越近,geohash的结果相同的位数越多,对么?
这一点是有些用户对geohash的误解,虽然geo确实尽可能的将位置相近的点hash到了一起,可是这并不是严格意义上的(实际上也并不可能,因为毕竟多一维坐标),
例如在方格4的左下部分的点和大方格1的右下部分的点离的很近,可是它们的geohash值一定是相差的相当远,因为头一次的分块就相差太大了,很多时候我们对geohash的值进行简单的排序比较,结果貌似真的能够找出相近的点,并且似乎还是按照距离的远近排列的,可是实际上会有一些点被漏掉了。
上述这个问题,可以通过搜索一个格子,周围八个格子的数据,统一获取后再进行过滤。这样就在编码层次解决了这个问题。
2.既然不能做到将相近的点hash值也相近,那么geohash的意义何在呢?
我觉得geohash还是相当有用的一个算法,毕竟这个算法通过无穷的细分,能确保将每一个小块的geohash值确保在一定的范围之内,这样就为灵活的周边查找和范围查找提供了可能。

 

常见的一些应用场景

A、如果想查询附近的点?如何操作

查出改点的gehash值,然后到数据库里面进行前缀匹配就可以了。

 

B、如果想查询附近点,特定范围内,例如一个点周围500米的点,如何搞?

可以查询结果,在结果中进行赛选,将geohash进行解码为经纬度,然后进行比较

 

 *在纬度相等的情况下:

 *经度每隔0.00001度,距离相差约1米;

 *每隔0.0001度,距离相差约10米;

 *每隔0.001度,距离相差约100米;

 *每隔0.01度,距离相差约1000米;

 *每隔0.1度,距离相差约10000米。

 *在经度相等的情况下:

 *纬度每隔0.00001度,距离相差约1.1米;

 *每隔0.0001度,距离相差约11米;

 *每隔0.001度,距离相差约111米;

 *每隔0.01度,距离相差约1113米;

 *每隔0.1度,距离相差约11132米。

Geohash,如果geohash的位数是6位数的时候,大概为附近1千米…

参考资料:

http://iamzhongyong.iteye.com/blog/1399333

http://tech.idv2.com/2011/06/17/location-search/

http://blog.sina.com.cn/s/blog_62ba0fdd0100tul4.html

 

你可能感兴趣的:(hash)