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

经纬度二进制表

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

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;

    }

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容