GeoHash算法
涉及到地图的内容,基本都会遇到搜索附近的功能,比如附近的人、附近的店铺等。要实现这样的功能,我们可以用最笨的方法:根据经纬度计算距离,然后划定一个阈值,只要小于该阈值就算是附近的。这种方法在数据量小时基本没问题,但是,如果数据量特别大,那服务器就需要进行大量的计算,负担很重!为了解决这一类问题,一个比较常用的方法就是利用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匹配的位数来判断两个点之间的大概距离。 5位的编码能表示10平方千米范围的矩形区域,而6位编码能表示更精细的区域(约0.34平方千米)
四、不足之处及解决方法 1、边缘附近的点,黄色的点要比黑色的点更加靠近红点,但是由于黑点跟红点的GeoHash前缀匹配数目更多,因此得到黑点更加靠近红点的结果
解决方法:
可以通过筛选周围8个区域内的所有点,然后计算距离得到满足条件结果。
2、因为现有的GeoHash算法使用的是Peano空间填充曲线(可感兴趣的可自己查看),这种曲线会产生突变,造成了编码虽然相似但距离可能相差很大的问题,因此在查询附近的时候,首先筛选GeoHash编码相似的点,然后进行实际距离计算。
参考文献:https://blog.csdn.net/youhongaa/article/details/78816700
########################################################################################
java实现:
GeoHash.java
package com.fhc.modules.api.utils.geohash;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 数据处理 Data conversion
*/
public class GeoHash {
private LocationBeanlocation;
/**
* 1 2500km;2 630km;3 78km;4 30km
* 5 2.4km; 6 610m; 7 76m; 8 19m
*/
private int hashLength =8; //经纬度转化为geohash长度
private int latLength =20; //纬度转化为二进制长度
private int lngLength =20; //经度转化为二进制长度
private double minLat;//每格纬度的单位大小
private double minLng;//每个经度的倒下
private static final char[]CHARS = {'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'};
public GeoHash(double lat, double lng) {
location =new LocationBean(lat, lng);
setMinLatLng();
}
public int gethashLength() {
return hashLength;
}
/**
* @Author:lulei
* @Description: 设置经纬度的最小单位
*/
private void setMinLatLng() {
minLat = LocationBean.MAXLAT - LocationBean.MINLAT;
for (int i =0; i
minLat /=2.0;
}
minLng = LocationBean.MAXLNG - LocationBean.MINLNG;
for (int i =0; i
minLng /=2.0;
}
}
/**
* @return
* @Author:lulei
* @Description: 求所在坐标点及周围点组成的九个
*/
public ListgetGeoHashBase32For9() {
double leftLat =location.getLat() -minLat;
double rightLat =location.getLat() +minLat;
double upLng =location.getLng() -minLng;
double downLng =location.getLng() +minLng;
List base32For9 =new ArrayList();
//左侧从上到下 3个
String leftUp = getGeoHashBase32(leftLat, upLng);
if (!(leftUp ==null ||"".equals(leftUp))) {
base32For9.add(leftUp);
}
String leftMid = getGeoHashBase32(leftLat, location.getLng());
if (!(leftMid ==null ||"".equals(leftMid))) {
base32For9.add(leftMid);
}
String leftDown = getGeoHashBase32(leftLat, downLng);
if (!(leftDown ==null ||"".equals(leftDown))) {
base32For9.add(leftDown);
}
//中间从上到下 3个
String midUp = getGeoHashBase32(location.getLat(), upLng);
if (!(midUp ==null ||"".equals(midUp))) {
base32For9.add(midUp);
}
String midMid = getGeoHashBase32(location.getLat(), location.getLng());
if (!(midMid ==null ||"".equals(midMid))) {
base32For9.add(midMid);
}
String midDown = getGeoHashBase32(location.getLat(), downLng);
if (!(midDown ==null ||"".equals(midDown))) {
base32For9.add(midDown);
}
//右侧从上到下 3个
String rightUp = getGeoHashBase32(rightLat, upLng);
if (!(rightUp ==null ||"".equals(rightUp))) {
base32For9.add(rightUp);
}
String rightMid = getGeoHashBase32(rightLat, location.getLng());
if (!(rightMid ==null ||"".equals(rightMid))) {
base32For9.add(rightMid);
}
String rightDown = getGeoHashBase32(rightLat, downLng);
if (!(rightDown ==null ||"".equals(rightDown))) {
base32For9.add(rightDown);
}
return base32For9;
}
/**
* @param length
* @return
* @Author:lulei
* @Description: 设置经纬度转化为geohash长度
*/
public boolean sethashLength(int length) {
if (length <1) {
return false;
}
hashLength = length;
latLength = (length *5) /2;
if (length %2 ==0) {
lngLength =latLength;
}else {
lngLength =latLength +1;
}
setMinLatLng();
return true;
}
/**
* @return
* @Author:lulei
* @Description: 获取经纬度的base32字符串
*/
public StringgetGeoHashBase32() {
return getGeoHashBase32(location.getLat(), location.getLng());
}
/**
* @param lat
* @param lng
* @return
* @Author:lulei
* @Description: 获取经纬度的base32字符串
*/
private StringgetGeoHashBase32(double lat, double lng) {
boolean[] bools = getGeoBinary(lat, lng);
if (bools ==null) {
return null;
}
StringBuffer sb =new StringBuffer();
for (int i =0; i < bools.length; i = i +5) {
boolean[] base32 =new boolean[5];
for (int j =0; j <5; j++) {
base32[j] = bools[i + j];
}
char cha = getBase32Char(base32);
if (' ' == cha) {
return null;
}
sb.append(cha);
}
return sb.toString();
}
/**
* @param base32
* @return
* @Author:lulei
* @Description: 将五位二进制转化为base32
*/
private char getBase32Char(boolean[] base32) {
if (base32 ==null || base32.length !=5) {
return ' ';
}
int num =0;
for (boolean bool : base32) {
num <<=1;
if (bool) {
num +=1;
}
}
return CHARS[num %CHARS.length];
}
/**
* @param lat
* @param lng
* @return
* @Author:lulei
* @Description: 获取坐标的geo二进制字符串
*/
private boolean[]getGeoBinary(double lat, double lng) {
boolean[] latArray = getHashArray(lat, LocationBean.MINLAT, LocationBean.MAXLAT, latLength);
boolean[] lngArray = getHashArray(lng, LocationBean.MINLNG, LocationBean.MAXLNG, lngLength);
return merge(latArray, lngArray);
}
/**
* @param latArray
* @param lngArray
* @return
* @Author:lulei
* @Description: 合并经纬度二进制
*/
private boolean[]merge(boolean[] latArray, boolean[] lngArray) {
if (latArray ==null || lngArray ==null) {
return null;
}
boolean[] result =new boolean[lngArray.length + latArray.length];
Arrays.fill(result, false);
for (int i =0; i < lngArray.length; i++) {
result[2 * i] = lngArray[i];
}
for (int i =0; i < latArray.length; i++) {
result[2 * i +1] = latArray[i];
}
return result;
}
/**
* @param value
* @param min
* @param max
* @return
* @Author:lulei
* @Description: 将数字转化为geohash二进制字符串
*/
private boolean[]getHashArray(double value, double min, double max, int length) {
if (value < min || value > max) {
return null;
}
if (length <1) {
return null;
}
boolean[] result =new boolean[length];
for (int i =0; i < length; i++) {
double mid = (min + max) /2.0;
if (value > mid) {
result[i] =true;
min = mid;
}else {
result[i] =false;
max = mid;
}
}
return result;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
GeoHash g =new GeoHash(38.045362, 114.415686);
System.out.println("西王" + g.getGeoHashBase32());
GeoHash g1 =new GeoHash(38.049113, 114.518596);
System.out.println("北国" + g1.getGeoHashBase32());
GeoHash g2 =new GeoHash(38.05583333333333, 114.35777777777778);
System.out.println("白佛" + g2.getGeoHashBase32());
// System.out.println(JsonUtil.parseJson(g.getGeoHashBase32For9()));
}
}
LocationBean.java
/**
*@Description: 存储经纬度信息
*/
package com.fhc.modules.api.utils.geohash;
public class LocationBean {
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 double lat;//纬度[-90,90]
private double lng;//经度[-180,180]
public LocationBean(double lat, double lng) {
this.lat = lat;
this.lng = lng;
}
public double getLat() {
return lat;
}
public void setLat(double lat) {
this.lat = lat;
}
public double getLng() {
return lng;
}
public void setLng(double lng) {
this.lng = lng;
}
}