php LBS(附近地理位置)功能实现的一些思路

在开发中经常会遇到把数据库已有经纬度的地方进行距离排序然后返回给用户

例如一些外卖app打开会返回附近的商店,这个是怎么做到的呢?

思路一:

根据用户当前的位置,用计算经纬度距离的算法逐一计算比对距离,然后进行排序。这里可以参考下面这个算法:

<?php
/**
 * 查找两个经纬度之间的距离
 *
 * @param  $latitude1   float  起始纬度
 * @param  $longitude1  float  起始经度
 * @param  $latitude2   float  目标纬度
 * @param  $longitude2  float  目标经度
 * @return array(miles=>英里,feet=>英尺,yards=>码,kilometers=>公里,meters=>米)
 * @example
 *
 *         $point1 = array(‘lat‘ => 40.770623, ‘long‘ => -73.964367);
 *         $point2 = array(‘lat‘ => 40.758224, ‘long‘ => -73.917404);
 *         $distance = getDistanceBetweenPointsNew($point1[‘lat‘], $point1[‘long‘], $point2[‘lat‘], $point2[‘long‘]);
 *         foreach ($distance as $unit => $value) {
 *             echo $unit.‘: ‘.number_format($value,4);
 *         }
 *
 *         The example returns the following:
 *
 *         miles: 2.6025       //英里
 *         feet: 13,741.4350   //英尺
 *         yards: 4,580.4783   //码
 *         kilometers: 4.1884  //公里
 *         meters: 4,188.3894  //米
 *
 */
function getDistanceBetweenPointsNew($latitude1, $longitude1, $latitude2, $longitude2) {
    $theta = $longitude1 - $longitude2;
    $miles = (sin(deg2rad($latitude1)) * sin(deg2rad($latitude2))) + (cos(deg2rad($latitude1)) * cos(deg2rad($latitude2)) * cos(deg2rad($theta)));
    $miles = acos($miles);
    $miles = rad2deg($miles);
    $miles = $miles * 60 * 1.1515;
    $feet = $miles * 5280;
    $yards = $feet / 3;
    $kilometers = $miles * 1.609344;
    $meters = $kilometers * 1000;
    return compact(‘miles‘, ‘feet‘, ‘yards‘, ‘kilometers‘, ‘meters‘);
}
?>

这个思路是要每次都获取全部数据,然后进行不断的循环计算,对于大数据量来说简直是噩梦。

思路二:

利用二维的经纬度转换成一维的数据,然后直接sql查询,无须一一比对。

例如经纬度 110.993736,21.495705 => w7yfm9pjt9b4

这就是geohash算法,这里简单说一下,geohash是一种地理位置编码,通过数学的方法进行一定的转换,使其与经纬度对应,变成一串可比对的字符串。

这里不做深入的了解,大概知道一下就好,转换出来的编码有一定的规律,例如同一个省份的前几位字符是一样的,字符数相似越多,证明距离越近。类似于公民身份证一样。

有兴趣的可以自行搜索了解一下。

下面直接给出转换的php代码

<?php
/**
 * Encode and decode geohashes
 *
 */
class Geohash {
    private $coding = "0123456789bcdefghjkmnpqrstuvwxyz";
    private $codingMap = array();

    public function Geohash() {
        //build map from encoding char to 0 padded bitfield
        for ($i = 0; $i < 32; $i++) {
            $this->codingMap[substr($this->coding, $i, 1)] = str_pad(decbin($i), 5, "0", STR_PAD_LEFT);
        }

    }

    /**
     * Decode a geohash and return an array with decimal lat,long in it
     */
    public function decode($hash) {
        //decode hash into binary string
        $binary = "";
        $hl = strlen($hash);
        for ($i = 0; $i < $hl; $i++) {
            $binary .= $this->codingMap[substr($hash, $i, 1)];
        }

        //split the binary into lat and log binary strings
        $bl = strlen($binary);
        $blat = "";
        $blong = "";
        for ($i = 0; $i < $bl; $i++) {
            if ($i % 2) {
                $blat = $blat . substr($binary, $i, 1);
            } else {
                $blong = $blong . substr($binary, $i, 1);
            }

        }

        //now concert to decimal
        $lat = $this->binDecode($blat, -90, 90);
        $long = $this->binDecode($blong, -180, 180);

        //figure out how precise the bit count makes this calculation
        $latErr = $this->calcError(strlen($blat), -90, 90);
        $longErr = $this->calcError(strlen($blong), -180, 180);

        //how many decimal places should we use? There‘s a little art to
        //this to ensure I get the same roundings as geohash.org
        $latPlaces = max(1, -round(log10($latErr))) - 1;
        $longPlaces = max(1, -round(log10($longErr))) - 1;

        //round it
        $lat = round($lat, $latPlaces);
        $long = round($long, $longPlaces);

        return array($lat, $long);
    }

    /**
     * Encode a hash from given lat and long
     */
    public function encode($lat, $long) {
        //how many bits does latitude need?
        $plat = $this->precision($lat);
        $latbits = 1;
        $err = 45;
        while ($err > $plat) {
            $latbits++;
            $err /= 2;
        }

        //how many bits does longitude need?
        $plong = $this->precision($long);
        $longbits = 1;
        $err = 90;
        while ($err > $plong) {
            $longbits++;
            $err /= 2;
        }

        //bit counts need to be equal
        $bits = max($latbits, $longbits);

        //as the hash create bits in groups of 5, lets not
        //waste any bits - lets bulk it up to a multiple of 5
        //and favour the longitude for any odd bits
        $longbits = $bits;
        $latbits = $bits;
        $addlong = 1;
        while (($longbits + $latbits) % 5 != 0) {
            $longbits += $addlong;
            $latbits += !$addlong;
            $addlong = !$addlong;
        }

        //encode each as binary string
        $blat = $this->binEncode($lat, -90, 90, $latbits);
        $blong = $this->binEncode($long, -180, 180, $longbits);

        //merge lat and long together
        $binary = "";
        $uselong = 1;
        while (strlen($blat) + strlen($blong)) {
            if ($uselong) {
                $binary = $binary . substr($blong, 0, 1);
                $blong = substr($blong, 1);
            } else {
                $binary = $binary . substr($blat, 0, 1);
                $blat = substr($blat, 1);
            }
            $uselong = !$uselong;
        }

        //convert binary string to hash
        $hash = "";
        for ($i = 0; $i < strlen($binary); $i += 5) {
            $n = bindec(substr($binary, $i, 5));
            $hash = $hash . $this->coding[$n];
        }

        return $hash;
    }

    /**
     * What‘s the maximum error for $bits bits covering a range $min to $max
     */
    private function calcError($bits, $min, $max) {
        $err = ($max - $min) / 2;
        while ($bits--) {
            $err /= 2;
        }

        return $err;
    }

    /*
     * returns precision of number
     * precision of 42 is 0.5
     * precision of 42.4 is 0.05
     * precision of 42.41 is 0.005 etc
     */
    private function precision($number) {
        $precision = 0;
        $pt = strpos($number, ‘.‘);
        if ($pt !== false) {
            $precision = -(strlen($number) - $pt - 1);
        }

        return pow(10, $precision) / 2;
    }

    /**
     * create binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example
     * removing the tail recursion is left an exercise for the reader
     */
    private function binEncode($number, $min, $max, $bitcount) {
        if ($bitcount == 0) {
            return "";
        }

        #echo "$bitcount: $min $max<br>";

        //this is our mid point - we will produce a bit to say
        //whether $number is above or below this mid point
        $mid = ($min + $max) / 2;
        if ($number > $mid) {
            return "1" . $this->binEncode($number, $mid, $max, $bitcount - 1);
        } else {
            return "0" . $this->binEncode($number, $min, $mid, $bitcount - 1);
        }

    }

    /**
     * decodes binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example
     * removing the tail recursion is left an exercise for the reader
     */
    private function binDecode($binary, $min, $max) {
        $mid = ($min + $max) / 2;

        if (strlen($binary) == 0) {
            return $mid;
        }

        $bit = substr($binary, 0, 1);
        $binary = substr($binary, 1);

        if ($bit == 1) {
            return $this->binDecode($binary, $mid, $max);
        } else {
            return $this->binDecode($binary, $min, $mid);
        }

    }
}
?>

把每一个经纬度都转换成geohash编码并储存起来,比对的时候直接sql

$sql = ‘select * from xxx where geohash like "‘.$like_geohash.‘%"‘;

这里like_geohash位数越多说明越精确。

下面是geohash经度距离换算关系,比如geohash如果有7位数,说明范围在76米左右,八位数则是19米,可以根据这个进行查询。

geohash长度 Lat位数 Lng位数 Lat误差 Lng误差 km误差
1 2 3 ±23 ±23 ±2500
2 5 5 ± 2.8 ±5.6 ±630
3 7 8 ± 0.70 ± 0.7 ±78
4 10 10 ± 0.087 ± 0.18 ±20
5 12 13 ± 0.022 ± 0.022 ±2.4
6 15 15 ± 0.0027 ± 0.0055 ±0.61
7 17 18 ±0.00068 ±0.00068 ±0.076
8 20 20 ±0.000086 ±0.000172 ±0.01911
9 22 23 ±0.000021 ±0.000021 ±0.00478
10 25 25 ±0.00000268 ±0.00000536 ±0.0005971
11 27 28 ±0.00000067 ±0.00000067 ±0.0001492
12 30 30 ±0.00000008 ±0.00000017 ±0.0000186

这个思路明显优于第一个思路,且查询起来速度非常快,也不用管有多大的数据,直接在数据库里面进行like查询就好,不过要做好索引才行,缺点也是比较明显

无法控制想要的精确访问,对于返回的数据无法进行距离的先后排序,不过已经能满足一定的需求,后期再结合思路一也可以做到距离的先后。

思路三:

前面两种方法都是通过很生硬的数学方法进行比对,所计算的也都是直线距离,但是现实并不是数学那样理想。

现实中两个很靠近的经纬度中间也有可能隔着一条跨不过去的河导致要绕很远的路,这时就要考虑实际情况。

很庆幸有些地图厂商已经帮我们考虑到了,所以还可以借助第三方api。

这里简单说一下高德地图的[云图服务API](http://lbs.amap.com/api/yuntu)

1、注册高德地图账户,并申请云图key。

2、创建云地图,也就是把你现在的数据放到高德地图上 [云图存储API](http://lbs.amap.com/api/yuntu/guide/data/storage),这里可以手动创建也能调用相关的api创建。

可以把数据导出excel然后批量上传,当然后期如果要新增尽量还是用它提供的接口进行增量添加。创建完成大概会生成这样一张表,有tableid,这个后面查询接口需要使用,字段可以自定义,方便业务逻辑。

3、使用高德api进行查询你的云地图 [数据检索](http://lbs.amap.com/api/yuntu/guide/data/search) ,这里使用周边检索,可以根据你当前的位置进行检索。

过程其实也不复杂,就是把数据放到高德,高德帮你完成了距离的排序,当然它提供的是比较实际的距离。具体实现需要研究一下高德提供的接口。

这个思路可以解决精准度问题,但开发成本大,还要跑一遍第三方去获取数据,可能会牺牲一定效率,具体取舍,仁者见仁吧。

原文地址:https://www.cnblogs.com/chriiess/p/9078620.html

时间: 2024-10-16 21:07:00

php LBS(附近地理位置)功能实现的一些思路的相关文章

C#开发微信门户及应用(15)-微信菜单增加扫一扫、发图片、发地理位置功能

前面介绍了很多篇关于使用C#开发微信门户及应用的文章,基本上把当时微信能做的接口都封装差不多了,微信框架也积累了不少模块和用户,最近发现微信公众平台增加了不少内容,特别是在自定义菜单里面增加了扫一扫.发图片.发地理位置功能,这几个功能模块很重要,想想以前想在微信公众号里面增加一个扫描二维码的功能,都做不了,现在可以了,还可以拍照上传等功能,本文主要介绍基于我前面的框架系列文章,进一步介绍如何集成和使用这些新增功能. 1.微信几个功能的官方介绍 1). 扫码推送事件 用户点击按钮后,微信客户端将调

undo/redo功能的原理和思路

一些具有操作记录的系统,如店铺装修.富文本编辑等,都具有undo/redo功能,可实现界面操作过程的撤销和恢复,简述开发undo/redo功能的原理和思路. undo是将用户上一步做的操作对程序造成的改动恢复到改动之前,而redo操作是指重新实现这种改动. undo/redo操作的实现方式分为两类:记录数据和记录操作. 记录数据是指将信息编辑窗口打开时,保存原始数据,然后记录用户每次操作后的结果数据,这里的数据是指信息编辑窗口中所有可能发生变动的数据.做undo操作时程序将用户上一步操作前的数据

百度搜索附近加盟店等基于LBS云搜索功能的实现

一.注册百度账号,进入开发者平台 创建应用并获取ak 地址如下 http://lbsyun.baidu.com/apiconsole/key/update?app-id=7546025 ok获取到了. 二.下载SDK ,SDK需要用到哪些功能就用那些功能吧.多的就不需要了 三.创建LBS云存储数据管理平台 http://lbsyun.baidu.com/datamanager/datamanage 就在这里 这里有个细节一定要注意,是否发布到检索:一定要选择是.不然,搜不到. 其他的,就随意了.

如何实现LBS轨迹回放功能?含多平台实现代码

本篇文章告诉您,如何实现轨迹回放.并且提供了web端,iOS端,Android端3个平台的轨迹回放代码.拷贝后可以直接使用.另外,文末有小彩蛋,算是开发者的福利. Web端/JavaScript 实现轨迹回放有2个主要功能需要实现,1个是定位取点,1个是按照轨迹慢慢移动Marker. 如何实现定位取点,可以看之前的文章:http://www.cnblogs.com/milkmap/p/4962085.html 本篇文章里的定位点,我就直接假设一堆点,可以push到数组里. var marker,

度量快速开发平台中“导入”功能的一般实现思路

度量快速开发平台中,"导入"功能在很多业务中都会用到.一般是在其他系统或者人为整理好的数据,比如要导入基础的数据,比如要在另外的系统中导入工资信息等.导入功能,一般是与管理界面相匹配可进行的.我们常用的实现思路是,首先做好一个管理界面,比如按照条件查询出数据来.下图是一个在职人员工资情况的管理界面,该功能是在办公管理系统中制作人员的工资数据,然后每个人可以查阅自己的工资. 人员工资本身是在财务系统中编制好的,并且从财务系统中导出为xls的文件. 在"导入"按钮上,打

怎么样才是设计功能函数的好思路(javascript)?

在js里面,对于函数的调用,实际上也是也是面向对象的思路,于是写好js函数,也是考核面向对象设计的能力,同时也必须考虑到如何实现高内聚和低耦合,拿一个例子来说,现在的需求是这样的,实现个投资进度框,就是如图所示:总共分四步来走,第一步“创建订单中”,成功改变提示信息“创建订单成功!”,显示,不成功改变提示信息“创建订单失败!”,显示,依次下去第二步,第三步,第四步! 我的dom结构是这样的: <!--投资操作进度tip--> <div class="invest_progres

小项目-购物网站个别功能的具体实现思路(新手)

第一步: /* * 用户注册的界面. * 用户点击注册按钮,会跳转到注册界面,用户填写完注册信息则会跳转到此Servlet. * 在此Servlet中我们首先需要设置编码格式. * 第二步是获取请求参数,就是用户在注册界面填写的信息. * 第三步则是需要验证用户名是否已经被注册,如果已经被注册则提示用户名被占用. * 如果没有被注册则将用户的信息添加到数据库表中. * 最后请求转发到注册界面. * * 该Servlet需要配合注册的JSP页面进行使用. * */ 第二步: /* * 登录界面的实

iOS @功能的部分实现思路

需求描述 1. 发布信息时,通过键盘键入@符号,或者点选相关功能键,唤醒@列表,进行选择 2.选择结束后,输入栏改色显示相关内容 3.删除时,整体删除@区块,且不能让光标落在@区块之间 实现步骤 1. 键入@符号,触发相关功能 1 - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{ 2 //判断键入的字符 3 if ([te

C#开发微信门户及应用(25)-微信企业号的客户端管理功能

我们知道,微信公众号和企业号都提供了一个官方的Web后台,方便我们对微信账号的配置,以及相关数据的管理功能,对于微信企业号来说,有通讯录中的组织架构管理.标签管理.人员管理.以及消息的发送等功能,其中微信企业号的组织架构和标签可以添加相应的人员,消息发送可以包含文本.图片.语音.视频.图文.文件等内容.对于企业号来说,官方的接口几乎可以无限的发送消息,因此构建一个管理后台,管理企业号的人员,以及用来给企业成员发送消息就是一个很好的功能亮点,有时候可以提高我们企业内部的消息通讯效率和日常工作管理效