2018-11-29 GeoHash算法

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,依次计算下去,即可得到纬度的二进制表示,如下表:

2018-11-29 GeoHash算法_第1张图片
经纬度二进制表

最后得到纬度的二进制表示为:

10111000110001111001

同理可以得到经度116.390705的二进制表示为:

11010010110001000100

2、合并纬度、经度的二进制: 合并方法是将经度、纬度二进制按照奇偶位合并: 

11100 11101 00100 01111 00000 01101 01011 00001


3

、按照Base32进行编码: 

Base32

编码表(其中一种):


将上述合并后二进制编码后结果为: 

wx4g0ec1

 三、GeoHash的精度

2018-11-29 GeoHash算法_第2张图片

编码越长,表示的范围越小,位置也越精确。因此我们就可以通过比较GeoHash匹配的位数来判断两个点之间的大概距离。 5位的编码能表示10平方千米范围的矩形区域,而6位编码能表示更精细的区域(约0.34平方千米)

四、不足之处及解决方法 1、边缘附近的点,黄色的点要比黑色的点更加靠近红点,但是由于黑点跟红点的GeoHash前缀匹配数目更多,因此得到黑点更加靠近红点的结果

2018-11-29 GeoHash算法_第3张图片

解决方法:

可以通过筛选周围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;

    }

}

你可能感兴趣的:(2018-11-29 GeoHash算法)