夜以继日,从7月28,到今天此时此刻,用laravel做的app接口,1.0版本终于做完了。后面等整个项目上线了,可以慢慢的来回顾一下这次开发的过程。今天主要来说下附近的X这个功能。
现在附近的人、附近的店、非常的有用。之前一直在思考这个东西应该怎么做,怎么来实现它。要实现这个功能的逻辑,非常简单,这里我们以查找附近的店距离。
首先要做的是,查找出平台所有店铺。然后根据经纬度,算出app使用者与这些店铺的距离。然后对计算出来的距离进行排序。这个排序结构就是由近及远的一个结果。
看起来这个问题得到了解决。那么问题来了。如果你做的应用,里边店铺有100万家,那么你首先要从数据库(mysql为例)把所有店铺的经纬度读出来。然后利用php代码,根据app使用者的经纬度,计算出100万家的距离,然后再排序。我都不忍心往下说了,大家想想这个效率会是一个什么情况。
这种方法,无法利用数据库的分页、条件查找等,大大增加了数据的负担。解决这个问题的方法很多。今天我只说 GeoHash 的解决方案。
简单说,Geohash算法;geohash是一种地址编码,它能把二维的经纬度编码成一维的字符串。
如果想要了解的详细点:可以查看该文章:GeoHash核心原理解析
如果你不太想了解它实现的细枝末节,一点都不会影响到你的使用。因为它的实现,已经实现好了。你通过它的实现类,可以轻易的完成使用。PS:所有代码,会放在github上,链接在文章底部
来说一下它的优缺点。
优点:
缺点: 距离和排序需二次运算(筛选结果中运行,其实挺快)
但是这里还有一个问题需要注意,那就是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后端开发系列文章目录