最近需要實現一個功能,查找車輛附近的加油站,如果車和加油站距離在200米以內,則查找成功。
加油站數量肯定不小,能否縮小查找范圍,否則以遍歷形式,效率肯定高不了。
Geohash算法就是將經緯度編碼,將二維變一維,給地址位置分區的一種算法。
基本原理
GeoHash是一種地址編碼方法。他能夠把二維的空間經緯度數據編碼成一個字符串
我們知道,經度范圍是東經180到西經180,緯度范圍是南緯90到北緯90,我們設定西經為負,南緯為負,所以地球上的經度范圍就是[-180, 180],緯度范圍就是[-90,90]。如果以本初子午線、赤道為界,地球可以分成4個部分。
如果緯度范圍[-90°, 0°)用二進制0代表,(0°, 90°]用二進制1代表,經度范圍[-180°, 0°)用二進制0代表,(0°, 180°]用二進制1代表,那么地球可以分成如下4個部分
如果在小塊范圍內遞歸對半劃分呢?
可以看到,劃分的區域更多了,也更精確了。geohash算法就是基于這種思想,劃分的次數更多,區域更多,區域面積更小了。通過將經緯度編碼,給地理位置分區
Geohash算法
Geohash算法一共有三步。
首先將經緯度變成二進制。
比如這樣一個點(39.923201, 116.390705)
緯度的范圍是(-90,90),其中間值為0。對于緯度39.923201,在區間(0,90)中,因此得到一個1;(0,90)區間的中間值為45度,緯度39.923201小于45,因此得到一個0,依次計算下去,即可得到緯度的二進制表示,如下表:
最后得到緯度的二進制表示為:
10111000110001111001
同理可以得到經度116.390705的二進制表示為:
11010010110001000100
第2步,就是將經緯度合并。
經度占偶數位,緯度占奇數位,注意,0也是偶數位。
11100 11101 00100 01111 00000 01101 01011 00001
第3步,按照Base32進行編碼
Base32編碼表的其中一種如下,是用0-9、b-z(去掉a, i, l, o)這32個字母進行編碼。具體操作是先將上一步得到的合并后二進制轉換為10進制數據,然后對應生成Base32碼。需要注意的是,將5個二進制位轉換成一個base32碼。上例最終得到的值為
wx4g0ec1
Geohash比直接用經緯度的高效很多,而且使用者可以發布地址編碼,既能表明自己位于北海公園附近,又不至于暴露自己的精確坐標,有助于隱私保護。
- GeoHash用一個字符串表示經度和緯度兩個坐標。在數據庫中可以實現在一列上應用索引(某些情況下無法在兩列上同時應用索引)
- GeoHash表示的并不是一個點,而是一個矩形區域
- GeoHash編碼的前綴可以表示更大的區域。例如wx4g0ec1,它的前綴wx4g0e表示包含編碼wx4g0ec1在內的更大范圍。 這個特性可以用于附近地點搜索
編碼越長,表示的范圍越小,位置也越精確。因此我們就可以通過比較GeoHash匹配的位數來判斷兩個點之間的大概距離。
問題
geohash算法有兩個問題。首先是邊緣問題。
如圖,如果車在紅點位置,區域內還有一個黃點。相鄰區域內的綠點明顯離紅點更近。但因為黃點的編碼和紅點一樣,最終找到的將是黃點。這就有問題了。
要解決這個問題,很簡單,只要再查找周邊8個區域內的點,看哪個離自己更近即可。
另外就是曲線突變問題。
本文第2張圖片比較好地解釋了這個問題。其中0111和1000兩個編碼非常相近,但它們的實際距離確很遠。所以編碼相近的兩個單位,并不一定真實距離很近,這需要實際計算兩個點的距離才行。
代碼實現
geohash原理清楚后,代碼實現就比較簡單了。不過仍然有一個問題需要解決,就是如何計算周邊的8個區域key值呢
假設我們計算的key值是6位,那么二進制位數就是 6*5 = 30位,所以經緯度分別是15位。我們以緯度為例,緯度會均分15次。這樣我們很容易能夠算出15次后,劃分的最小單位是多少
private void setMinLatLng() {
minLat = MAXLAT - MINLAT;
for (int i = 0; i < numbits; i++) {
minLat /= 2.0;
}
minLng = MAXLNG - MINLNG;
for (int i = 0; i < numbits; i++) {
minLng /= 2.0;
}
}
得到了最小單位,那么周邊區域的經緯度也可以計算得到了。比如說左邊區域的經度肯定是自身經度減去最小經度單位。緯度也可以通過加減,得到上下的緯度值,最終周圍8個單位也可以計算得到。
可以到 http://geohash.co/ 進行geohash編碼,以確定自己代碼是否寫錯
整體代碼如下所示:
public class GeoHash {
public static final double MINLAT = -90;
public static final double MAXLAT = 90;
public static final double MINLNG = -180;
public static final double MAXLNG = 180;
private static int numbits = 3 * 5; //經緯度單獨編碼長度
private static double minLat;
private static double minLng;
private final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
//定義編碼映射關系
final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();
//初始化編碼映射內容
static {
int i = 0;
for (char c : digits)
lookup.put(c, i++);
}
public GeoHash(){
setMinLatLng();
}
public String encode(double lat, double lon) {
BitSet latbits = getBits(lat, -90, 90);
BitSet lonbits = getBits(lon, -180, 180);
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < numbits; i++) {
buffer.append( (lonbits.get(i))?'1':'0');
buffer.append( (latbits.get(i))?'1':'0');
}
String code = base32(Long.parseLong(buffer.toString(), 2));
//Log.i("okunu", "encode lat = " + lat + " lng = " + lon + " code = " + code);
return code;
}
public ArrayList<String> getArroundGeoHash(double lat, double lon){
//Log.i("okunu", "getArroundGeoHash lat = " + lat + " lng = " + lon);
ArrayList<String> list = new ArrayList<>();
double uplat = lat + minLat;
double downLat = lat - minLat;
double leftlng = lon - minLng;
double rightLng = lon + minLng;
String leftUp = encode(uplat, leftlng);
list.add(leftUp);
String leftMid = encode(lat, leftlng);
list.add(leftMid);
String leftDown = encode(downLat, leftlng);
list.add(leftDown);
String midUp = encode(uplat, lon);
list.add(midUp);
String midMid = encode(lat, lon);
list.add(midMid);
String midDown = encode(downLat, lon);
list.add(midDown);
String rightUp = encode(uplat, rightLng);
list.add(rightUp);
String rightMid = encode(lat, rightLng);
list.add(rightMid);
String rightDown = encode(downLat, rightLng);
list.add(rightDown);
//Log.i("okunu", "getArroundGeoHash list = " + list.toString());
return list;
}
//根據經緯度和范圍,獲取對應的二進制
private BitSet getBits(double lat, double floor, double ceiling) {
BitSet buffer = new BitSet(numbits);
for (int i = 0; i < numbits; i++) {
double mid = (floor + ceiling) / 2;
if (lat >= mid) {
buffer.set(i);
floor = mid;
} else {
ceiling = mid;
}
}
return buffer;
}
//將經緯度合并后的二進制進行指定的32位編碼
private String base32(long i) {
char[] buf = new char[65];
int charPos = 64;
boolean negative = (i < 0);
if (!negative){
i = -i;
}
while (i <= -32) {
buf[charPos--] = digits[(int) (-(i % 32))];
i /= 32;
}
buf[charPos] = digits[(int) (-i)];
if (negative){
buf[--charPos] = '-';
}
return new String(buf, charPos, (65 - charPos));
}
private void setMinLatLng() {
minLat = MAXLAT - MINLAT;
for (int i = 0; i < numbits; i++) {
minLat /= 2.0;
}
minLng = MAXLNG - MINLNG;
for (int i = 0; i < numbits; i++) {
minLng /= 2.0;
}
}
//根據二進制和范圍解碼
private double decode(BitSet bs, double floor, double ceiling) {
double mid = 0;
for (int i=0; i<bs.length(); i++) {
mid = (floor + ceiling) / 2;
if (bs.get(i))
floor = mid;
else
ceiling = mid;
}
return mid;
}
//對編碼后的字符串解碼
public double[] decode(String geohash) {
StringBuilder buffer = new StringBuilder();
for (char c : geohash.toCharArray()) {
int i = lookup.get(c) + 32;
buffer.append( Integer.toString(i, 2).substring(1) );
}
BitSet lonset = new BitSet();
BitSet latset = new BitSet();
//偶數位,經度
int j =0;
for (int i=0; i< numbits*2;i+=2) {
boolean isSet = false;
if ( i < buffer.length() )
isSet = buffer.charAt(i) == '1';
lonset.set(j++, isSet);
}
//奇數位,緯度
j=0;
for (int i=1; i< numbits*2;i+=2) {
boolean isSet = false;
if ( i < buffer.length() )
isSet = buffer.charAt(i) == '1';
latset.set(j++, isSet);
}
double lon = decode(lonset, -180, 180);
double lat = decode(latset, -90, 90);
return new double[] {lat, lon};
}
public static void main(String[] args) throws Exception{
GeoHash geohash = new GeoHash();
// String s = geohash.encode(40.222012, 116.248283);
// System.out.println(s);
geohash.getArroundGeoHash(40.222012, 116.248283);
// double[] geo = geohash.decode(s);
// System.out.println(geo[0]+" "+geo[1]);
}
}