目錄
最近項目中需要搜索周邊的 POI 信息,查找的過程中了解到了 Geohash ,這這里記錄下以便自己牢記也和大家分享下。
一、簡介
GeoHash是一種地址編碼方法。他能夠把二維的空間經緯度數據編碼成一個字符串。GeoHash具有以下特點:
1、GeoHash用一個字符串表示經度和緯度兩個坐標。在數據庫中可以實現在一列上應用索引
2、GeoHash表示的並不是一個點,而是一個區域;
3、GeoHash編碼的前綴可以表示更大的區域。例如wx4g0ec1,它的前綴wx4g0e表示包含編碼wx4g0ec1在內的更大范圍。 這個特性可以用於附近地點搜索
二、計算方法
GeoHash的計算過程分為三步:
1、將經緯度轉換成二進制:
比如這樣一個點(39.923201, 116.390705)
緯度的范圍是(-90,90),其中間值為0。對於緯度39.923201,在區間(0,90)中,因此得到一個1;(0,90)區間的中間值為45度,緯度39.923201小於45,因此得到一個0,依次計算下去,即可得到緯度的二進制表示,如下表:
最后得到緯度的二進制表示為:
10111000110001111001
同理可以得到經度116.390705的二進制表示為:
11010010110001000100
2、合並緯度、經度的二進制:
合並方法是將經度、緯度二進制按照奇偶位合並:
11100 11101 00100 01111 00000 01101 01011 00001
3、按照Base32進行編碼:
Base32編碼表(其中一種):
將上述合並后二進制編碼后結果為:
wx4g0ec1
三、GeoHash的精度
編碼越長,表示的范圍越小,位置也越精確。因此我們就可以通過比較GeoHash匹配的位數來判斷兩個點之間的大概距離。
四、查找相鄰8個區域的Geohash編碼(.NET)
為什么會有這樣的算法,原因是Geohash是有缺點的,如下:
邊緣附近的點,黃色的點要比黑色的點更加靠近紅點,但是由於黑點跟紅點的GeoHash前綴匹配數目更多,因此得到黑點更加靠近紅點的結果(如下圖)
這個問題的解決辦法就是:篩選周圍8個區域內的所有點,然后計算距離得到滿足條件結果
下面是用C#寫的在 .NET 平台下的尋找給定區域相鄰的8個區域的代碼

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 /// <summary> 43 /// 計算相鄰 44 /// </summary> 45 /// <param name="hash"></param> 46 /// <param name="direction"></param> 47 /// <returns></returns> 48 public static String CalculateAdjacent(String hash, Direction direction) 49 { 50 hash = hash.ToLower(); 51 52 char lastChr = hash[hash.Length - 1]; 53 int type = hash.Length%2; 54 var dir = (int) direction; 55 string nHash = hash.Substring(0, hash.Length - 1); 56 57 if (Borders[type][dir].IndexOf(lastChr) != -1) 58 { 59 nHash = CalculateAdjacent(nHash, (Direction) dir); 60 } 61 return nHash + Base32[Neighbors[type][dir].IndexOf(lastChr)]; 62 } 63 /// <summary> 64 /// 細化間隔 65 /// </summary> 66 /// <param name="interval"></param> 67 /// <param name="cd"></param> 68 /// <param name="mask"></param> 69 public static void RefineInterval(ref double[] interval, int cd, int mask) 70 { 71 if ((cd & mask) != 0) 72 { 73 interval[0] = (interval[0] + interval[1])/2; 74 } 75 else 76 { 77 interval[1] = (interval[0] + interval[1])/2; 78 } 79 } 80 81 /// <summary> 82 /// 解碼 83 /// </summary> 84 /// <param name="geohash"></param> 85 /// <returns></returns> 86 public static double[] Decode(String geohash) 87 { 88 bool even = true; 89 double[] lat = {-90.0, 90.0}; 90 double[] lon = {-180.0, 180.0}; 91 92 foreach (char c in geohash) 93 { 94 int cd = Base32.IndexOf(c); 95 for (int j = 0; j < 5; j++) 96 { 97 int mask = Bits[j]; 98 if (even) 99 { 100 RefineInterval(ref lon, cd, mask); 101 } 102 else 103 { 104 RefineInterval(ref lat, cd, mask); 105 } 106 even = !even; 107 } 108 } 109 110 return new[] {(lat[0] + lat[1])/2, (lon[0] + lon[1])/2}; 111 } 112 /// <summary> 113 /// 編碼 114 /// </summary> 115 /// <param name="latitude">緯度</param> 116 /// <param name="longitude">經度</param> 117 /// <param name="precision">精度</param> 118 /// <returns></returns> 119 public static String Encode(double latitude, double longitude, int precision = 12) 120 { 121 bool even = true; 122 int bit = 0; 123 int ch = 0; 124 string geohash = ""; 125 126 double[] lat = {-90.0, 90.0}; 127 double[] lon = {-180.0, 180.0}; 128 129 if (precision < 1 || precision > 20) precision = 12; 130 131 while (geohash.Length < precision) 132 { 133 double mid; 134 135 if (even) 136 { 137 mid = (lon[0] + lon[1])/2; 138 if (longitude > mid) 139 { 140 ch |= Bits[bit]; 141 lon[0] = mid; 142 } 143 else 144 lon[1] = mid; 145 } 146 else 147 { 148 mid = (lat[0] + lat[1])/2; 149 if (latitude > mid) 150 { 151 ch |= Bits[bit]; 152 lat[0] = mid; 153 } 154 else 155 lat[1] = mid; 156 } 157 158 even = !even; 159 if (bit < 4) 160 bit++; 161 else 162 { 163 geohash += Base32[ch]; 164 bit = 0; 165 ch = 0; 166 } 167 } 168 return geohash; 169 } 170 171 /// <summary> 172 /// 獲取九個格子 順序 本身 上、下、左、右、 左上、 右上、 左下、右下 173 /// </summary> 174 /// <param name="geohash"></param> 175 /// <returns></returns> 176 public static String[] getGeoHashExpand(String geohash) 177 { 178 179 try { 180 String geohashTop = CalculateAdjacent(geohash, Direction.Top);//上 181 182 String geohashBottom = CalculateAdjacent(geohash, Direction.Bottom);//下 183 184 String geohashLeft = CalculateAdjacent(geohash, Direction.Left);//左 185 186 String geohashRight = CalculateAdjacent(geohash, Direction.Right);//右 187 188 189 String geohashTopLeft = CalculateAdjacent(geohashLeft, Direction.Top);//左上 190 191 String geohashTopRight = CalculateAdjacent(geohashRight, Direction.Top);//右上 192 193 String geohashBottomLeft = CalculateAdjacent(geohashLeft, Direction.Bottom);//左下 194 195 String geohashBottomRight = CalculateAdjacent(geohashRight, Direction.Bottom);//右下 196 197 String[] expand = { geohash, geohashTop, geohashBottom, geohashLeft, geohashRight, geohashTopLeft, geohashTopRight, geohashBottomLeft, geohashBottomRight}; 198 return expand; 199 } catch (Exception e) { 200 return null; 201 } 202 } 203 204 205 ///// <summary> 206 ///// test 207 ///// </summary> 208 ///// <param name="args"></param> 209 //public void main() 210 //{ 211 // double lat = 39.90403; 212 // double lon = 116.407526; //需要查詢經緯度,目前指向的是BeiJing 213 // string hash = Geohash.Encode(lat, lon); 214 // int geohashLen = 6; 215 // /*獲取中心點的geohash*/ 216 // String geohash = hash.Substring(0, geohashLen); 217 // /*獲取所有的矩形geohash, 一共是九個 ,包含中心點,打印順序請參考參數*/ 218 // String[] result = Geohash.getGeoHashExpand(geohash); 219 //} 220 } 221 }
五、MySQL 中使用 GeoHash
在MySQL 5.7 以后,對GIS空間數據的更好的支持,加上虛擬列可以很方便的使用GeoHash。
選擇存儲 GeoHash 的列,設置為“虛擬列”,在“表達式”里填入“st_geohash(`point`,6)”,其中 point 是要編碼的點字段
參考:
https://blog.csdn.net/youhongaa/article/details/78816700
https://www.cnblogs.com/lucoo/p/5085986.html