app后端开发四:GeoHash实现查找附近的X

夜以继日,从7月28,到今天此时此刻,用laravel做的app接口,1.0版本终于做完了。后面等整个项目上线了,可以慢慢的来回顾一下这次开发的过程。今天主要来说下附近的X这个功能。


情景描述

现在附近的人、附近的店、非常的有用。之前一直在思考这个东西应该怎么做,怎么来实现它。要实现这个功能的逻辑,非常简单,这里我们以查找附近的店距离。
首先要做的是,查找出平台所有店铺。然后根据经纬度,算出app使用者与这些店铺的距离。然后对计算出来的距离进行排序。这个排序结构就是由近及远的一个结果。

上面的问题

看起来这个问题得到了解决。那么问题来了。如果你做的应用,里边店铺有100万家,那么你首先要从数据库(mysql为例)把所有店铺的经纬度读出来。然后利用php代码,根据app使用者的经纬度,计算出100万家的距离,然后再排序。我都不忍心往下说了,大家想想这个效率会是一个什么情况。

这种方法,无法利用数据库的分页、条件查找等,大大增加了数据的负担。解决这个问题的方法很多。今天我只说 GeoHash 的解决方案。

什么是GeoHash

简单说,Geohash算法;geohash是一种地址编码,它能把二维的经纬度编码成一维的字符串。

如果想要了解的详细点:可以查看该文章:GeoHash核心原理解析

如果你不太想了解它实现的细枝末节,一点都不会影响到你的使用。因为它的实现,已经实现好了。你通过它的实现类,可以轻易的完成使用。PS:所有代码,会放在github上,链接在文章底部

来说一下它的优缺点。

优点:

  • 利用一个字段,便可存储经纬度;搜索时,只需一条索引,效率较高
  • 编码的前缀可以表示更大的区域,查找附近的,非常方便。 SQL中,LIKE ‘wm3yr3%’,即可查询附近的所有地点,可以进行分页,对数据库压力大大降低
  • 通过编码精度可模糊坐标、隐私保护等。

缺点: 距离和排序需二次运算(筛选结果中运行,其实挺快)

但是这里还有一个问题需要注意,那就是GeoHash它的编码规则是,将一个区域分成九宫格,那如果当某一个用户在使用的时候,刚好处在九宫格的边界上,那么问题就来了。
这里写图片描述
如上图,假设用户在绿色点的位置,明显的B距离他更近。但是在查询的时候会发现距离较远店铺的GeoHash编码与用户一样(因为在同一个GeoHash区域块上),而较近餐馆的GeoHash编码与我们不一致。这个问题往往产生在边界处。
这种问题的解决方案是,当我们需要获取绿色位置附近的店铺时,不要单单只查找它所在的区域,而要同时查找它附近的八个区域。也就是要查找途中最大的矩形所有区域。具体实现,我也做好了,你使用的时候,只需调用一个函数即可。

使用策略

该说的理论基本上都说完了,现在来说一说实际的操作过程。

首先,在添加店铺的数据时,当对店铺数据入库时,需要同时将店铺的经纬度进行geohash编码,存入数据库中(因为店铺位置基本不会改变)。
当用户使用附近的店查找时,从客户端上传上来用户的经纬度,然后进行geohash编码,通过sql:

where left(`geohash`,5) in('wm3yx','wm3yp','wm6n2','wm3yq','wm3yw','wm6n8','wm6n0','wm3yn','wm3yr')

查找附近相应的店铺。
查找完成后,如果需要继续出距离,则,可以使用下面的方法,求出当前位置与目标位置的距离

/**
 * 
 * 
 * @desc 地理位置信息
 */
class Location
{
    // 地球的求半径,单位还是m
    const EARTH_RADIUS = 6378137;

    /**
     * 经纬度转化为幅度
     * @param string $d
     * @return number
     */
    private static function fnRad($d)
    {
        return $d * pi() / 180.0;
    }

    /**
     * 计算两点之间的距离,单位m
     * latitude(-90,90)
     * longitude(-180,180)
     * @param string $lnglat1
     * @param string $lnglat2
     */
    public static function getP2PDistance($srcLongLat, $destLongLat)
    {
        $srcLongLat = explode(',', $srcLongLat);
        $destLongLat = explode(',', $destLongLat);
        list($lat1, $lng1) = $srcLongLat;
        list($lat2, $lng2) = $destLongLat;

        //return self::googleDistance($lat1, $lng1, $lat2, $lng2);
        return self::selfDistance($lat1, $lng1, $lat2, $lng2);
    }

    /**
     * 自定义算法
     * 效率更高
     */
    private static function selfDistance($lat1, $lng1, $lat2, $lng2)
    {
        //将角度转为狐度
        $radLat1 = deg2rad($lat1);
        $radLat2 = deg2rad($lat2);
        $radLng1 = deg2rad($lng1);
        $radLng2 = deg2rad($lng2);
        //结果
        $s = acos(cos($radLat1)*cos($radLat2)*cos($radLng1-$radLng2)+sin($radLat1)*sin($radLat2))*self::EARTH_RADIUS;
        //精度
        $s = round($s* 10000)/10000;

        return  round($s);
    }

    /**
     * google的算法
     * 效率稍微差一点
     */
    private static function googleDistance($lat1, $lng1, $lat2, $lng2)
    {
        // 通过纬度取得对应的幅度
        $srcRadLat = self::fnRad($lat1);
        $destRadLat = self::fnRad($lat2);

        // 获取两点纬度弧度差
        $a = $srcRadLat - $destRadLat;
        // 获取两点经度的弧度差
        $b = self::fnRad($lng1) - self::fnRad($lng2);

        // 计算球体上该弧度对应的距离
        $s = 2 * asin(sqrt(pow(sin($a/2),2) + cos($srcRadLat)*cos($destRadLat)*pow(sin($b/2),2))) * self::EARTH_RADIUS;
        // 取得距离的km数
        $s = round($s * 10000) / 10000;

        return round($s);
    }
}

在计算距离的方法中,使用了两种方法,一种是推导出的数学公式,一种是google给出的算法,最后测试发现推导的数学公式效率更高。不信的可以动手试试。至于计算完,距离,如何排序就不多了。

geohash类的方法介绍,请参考github上的文档。
项目地址:https://github.com/helei112g/laravel_geohash

app后端开发系列文章目录

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值