ThinkPHP5+MySQL实现根据接收到的经纬度搜索数据库中与之距离最近的地点API

一、需求

首先还是讲一下需求:
现在有一张浙江省的地点表 address(包含470W+条项目所属场所地点数据)表结构与部分数据展示如下图


其中主要用到的字段基本都写备注了,没写备注的就是这个功能里用不到的。
然后根据前端在地图上标记后传来的经纬度坐标,去 address 表中查找 location 字段与这个接收到的经纬度坐标距离最近的一条数据,返回给前端就行了。

二、思路

由于数据量较大,肯定不能每次都把470W条经纬度全都读出来一一去对比。
我的思路是将地图网格化成 50×50的等差网格阵列,即2500个边长相等小网格;
先将这些小网格的左上角和右下角的经纬度坐标计算出来,根据这两个经纬度坐标就可以得到一个二维平面区域围栏,将这2500个小区域对应的左上角、右下角经纬度坐标存入网格数据表 zhejiang_grid 中;
zhejiang_grid结构:

简单画一张4×4的网格图 帮助理解一下思路

再一次性预处理好这些小网格(二维平面区域围栏)内包含了 address 表中的若干经纬度坐标,存入记录每个小网格包含若干经纬坐标的数据表 zhejiang_grid_point 中, 根据共有字段将三张表关联起来。
zhejiang_grid_point表结构:

至此,我们的预处理工作就全都做完了。

最后的业务逻辑为:
接收前端发来的经纬度坐标->网格数据表zhejiang_grid查询出该经纬度坐标属于哪个小网格->根据所属小网格id去zhejiang_grid_point表查询到该小网格内包含的所有经纬度坐标->遍历计算每个经纬度坐标和接收到的经纬度坐标之间的距离->根据距离排序,取出距离最近的一条数据id->最后去address表中根据这个id查询到这条数据的需求字段信息

三、代码部分

首选将 address 表的470W+条原始数据按照上面讲的分成 2500块 网格化预处理到 zhejiang_grid
再根据这2500个小网格去 address 表进行匹配,得到每个小网格内的包含的所有经纬度坐标点,存入zhejiang_grid_point表。

在此之前先根据下面这篇文章的步骤,创建一个可以在cmd内执行的php脚本文件
[记录]在cmd内运行ThinkPHP5目录内的php文件

在app > index 目录下创建名为 command 的文件夹, 再创建一个名为 Put.php 的文件
写入以下代码:

<?php

namespace app\index\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;

class Put extends Command
{
    protected function configure()
    {
        // 指令配置
        $this->setName('put')
            ->setDescription('经纬度网格数据处理');
    }

    protected function execute(Input $input, Output $output)
    {
        $size = 50; // 网格大小 50*50

        // 指令输出
        $output->writeln("数据处理开始启动……\n");

        // 读出处理原始数据所需的字段
        $data = Db::name('address')
            ->field('order_number,location')
            ->select();

        // 浙江省矩形框选后左上角 右下角经纬度分别为:
        // 左上: 118.040471, 31.182539
        // 右下: 122.964048, 27.047229
        // 匹配到的数据总条数: 成功:4747813
        $lt_lon = '118.040471';
        $lt_lat = '31.182539';
        $rd_lon = '122.964048';
        $rd_lat = '27.047229';

        // 获取处理好的经纬度网格坐标列表
        $list = $this->drowPrid($lt_lon, $lt_lat, $rd_lon, $rd_lat, $size, $table_name);

        $sueecss = 0;
        $error = 0;

        foreach ($data as $key => $value){
            // 处理得到经纬度
            $xy = explode(',', $value['location']);
            $addr_lon = $xy[0];
            $addr_lat = $xy[1];

            // 遍历网格对比经纬度
            foreach ($list as $k => $v){
                // 如果当前坐标在当前网格内的判断逻辑:
                // 当前经度 > 左上经度 &&
                // 当前纬度 < 左上纬度 &&
                // 当前经度 < 右下经度 &&
                // 当前纬度 < 右下纬度
                // 满足上述条件, 则当前经纬度坐标就在这个小网格内
                if ((float)$addr_lon > (float)$v['grid_lt_lon'] &&
                    (float)$addr_lat < (float)$v['grid_lt_lat'] &&
                    (float)$addr_lon < (float)$v['grid_rd_lon'] &&
                    (float)$addr_lat > (float)$v['grid_rd_lat']){

                    $output->writeln($value['location'] . "\n");

                    // 整理数据插入数据库
                    $param = [];
                    $param['addr_lon'] = $addr_lon;
                    $param['addr_lat'] = $addr_lat;
                    $param['grid_number'] = $v['id'];
                    $param['order_number'] = $value['order_number'];

                    // 网格内的数据存入网格包含坐标点表
                    if (Db::name('zhejiang_grid_point')->insert($param)){
                        $sueecss++;
                    }else{
                        $error++;
                    }
                }
            }
        }

        $output->writeln('成功:' . $sueecss . "\n");
        $output->writeln('失败:' . $error . "\n");

    }

    /**
     * 绘制网格,将地址表内的坐标按照指定区域划分至网格内
     * @param $lt_lon         string  左上角经度
     * @param $lt_lat         string  左上角经度
     * @param $rd_lon         string  左上角经度
     * @param $rd_lat         string  左上角经度
     * @param $size           integer 网格大小:例如5*5的网格就输入5
     * @param $table_name     string  数据表城市名
     * @return array
     */
    public function drowPrid($lt_lon, $lt_lat, $rd_lon, $rd_lat, $size, $table_name){
        // 求坐标经纬度差值绘制5*5网格
        $diff_lon = bcsub($rd_lon, $lt_lon, 6);
        $diff_lat = bcsub($rd_lat, $lt_lat, 6);

        // 求等差阵列的等差平均经纬度
        $avg_lon = bcdiv($diff_lon, $size, 6);
        $avg_lat = bcdiv($diff_lat, $size, 6);

        // 计算网格各点坐标的等差平均值
        // 循环生成等差阵列
        $list = [];
        $k = 0;
        for ($i = 0; $i < $size; $i++){
            // 这里的计算方式, 可以按照自己的思路写, 我也不确定我这么写的精确度高不高
            $lon = bcadd($lt_lon, bcmul($i, $avg_lon, 6), 6);
            for ($j = 0; $j < $size; $j++){
                $lat = bcadd($lt_lat, bcmul($j, $avg_lat, 6), 6);
                // 每个小网格的左上点坐标和右下点坐标
                $list[$k]['id'] = $k + 1;  // 子网格序号
                $list[$k]['grid_lt_lon'] = bcmul($lon, 1, 5);
                $list[$k]['grid_lt_lat'] = bcmul($lat, 1, 5);
                $list[$k]['grid_rd_lon'] = bcmul(bcadd($lon, $avg_lon, 6), 1, 5);
                $list[$k]['grid_rd_lat'] = bcmul(bcadd($lat, $avg_lat, 6), 1, 5);
                $k++;
            }
        }

        foreach ($list as $k => $v){
            // 子网格存入网格表
            @Db::name('zhejiang_grid')->insert($v);
        }
        return $list;
    }
}

将数据全都预处理好之后, 开始写获取离传入经纬度最近的地点API

<?php

namespace app\index\controller;

use app\common\controller\Frontend;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
use think\Db;

class Index extends Frontend
{
    public function index()
    {
        $this->view->engine->layout(false);
        return $this->fetch('index/index');
    }

    public function map()
    {
        $this->view->engine->layout(false);
        return $this->fetch('index/map');
    }

    public function tu1()
    {
        $this->view->engine->layout(false);
        return $this->fetch('index/tu1');
    }

    public function tu2()
    {
        $this->view->engine->layout(false);
        return $this->fetch('index/tu2');
    }

    // 接收前端发来的坐标数据
    public function dataReception(){
        if ($this->request->isPost()) {
            $param = $this->request->post();
            $msg = time() . mt_rand(1, 50) . ',' . $param['msg'];
            $res = $this->sendMQ($msg);
//            var_dump($res);

            if(empty($res->body)){
                return json(['code' => 500, 'msg' => '消息发送失败', 'data' => $msg]);
            }else{
                return json(['code' => 200, 'msg' => '消息发送成功', 'data' => $res->body]);
            }
        }else{
            return json(['code' => 200, 'msg' => '请求方式错误']);
        }
    }


    // 发送rabbitMQ消息
    public function sendMQ($data)
    {
        try {
            $connection = new AMQPStreamConnection('192.168.124.73', '5672', 'admin', 'admin', '/');

            $channel = $connection->channel();

            $ex_name = 'gfLonLat';
            // 创建交换机
            $channel->exchange_declare($ex_name,'fanout',false,true,false);

            $msg = new AMQPMessage($data, ['delivery_mode' => AMQPMessage:: DELIVERY_MODE_PERSISTENT ]);

            $channel->basic_publish($msg, $ex_name,'');

            $channel->close();

            $connection->close();
        }catch (\Exception $e){
            return $e;
        }
        return $msg;
    }


    // 获取离传入经纬度最近的地点
    // http://domain/index/index/getNearestAddress?lon=120.1474672&lat=30.3029259
    public function getNearestAddress(){
        $lon = input('lon/s');//经度
        $lat = input('lat/s');//纬度

        $data = Db::name('zhejiang_grid')->select();

        // 在25个子网格中查询接受到的经纬度属于哪个子网格
        $son_grid = [];

        foreach ($data as $key => $value){

            // 如果当前坐标在当前网格内的判断逻辑:
            // 当前经度 > 左上经度 &&
            // 当前纬度 < 左上纬度 &&
            // 当前经度 < 右下经度 &&
            // 当前纬度 < 右下纬度
            // 满足上述条件, 则当前经纬度坐标就在这个小网格内
            if ((float)$lon > (float)$value['grid_lt_lon'] &&
                (float)$lat < (float)$value['grid_lt_lat'] &&
                (float)$lon < (float)$value['grid_rd_lon'] &&
                (float)$lat > (float)$value['grid_rd_lat']){

                $son_grid = $value;
            }
        }
        
        // 如果查询到
        if (!empty($son_grid)){
            // 获取当前子网格的id
            $grid_number = $son_grid['id'];
            // 查询当前网格内的所有坐标点
            $son_grid_point_list = Db::name('zhejiang_grid_point')
                ->where('grid_number', $grid_number)
                ->select();

            // 遍历测距
            foreach ($son_grid_point_list as $k => $val){
                $son_grid_point_list[$k]['distance'] =  $this->getDistance($lon, $lat, $val['addr_lon'], $val['addr_lat']);
            }

            // 按照距离对数组进行排序
            $distanceArr = array_column($son_grid_point_list, 'distance');
            array_multisort($distanceArr, SORT_ASC, $son_grid_point_list);

            // 取距输入经纬度最近的一个坐标点
            $pointArr = $son_grid_point_list[0];
//            var_dump($pointArr);
            // 去地址表查询该坐标点id对应的地址名称
            $addressArr = Db::name('address')
                ->where('order_number', $pointArr['order_number'])
                ->field('pname,cityname,adname,address,name,location,type')
                ->find();

            $addressArr['distance'] = $pointArr['distance'];
//            var_dump($addressArr);
            return json(['code' => 200, 'msg' => 'success', 'data' => $addressArr]);

        }else{
            return json(['code' => 200, 'msg' => '无结果', 'data' => []]);
        }

    }


    /**
     * 获取两个坐标之间的距离
     * @param $from_lon
     * @param $from_lat
     * @param $to_lon
     * @param $to_lat
     * @param $km    bool    是否以公里为单位 false:米 true:公里(千米)
     * @param $decimal  integer    保留小数位数
     * @return float
     */
    public function getDistance($from_lon, $from_lat, $to_lon, $to_lat, $km = false, $decimal=2){
        $EARTH_RADIUS = 6378.137;   // 地球半径系数
//        var_dump('经纬度:', $from_lon, $from_lat, $to_lon, $to_lat);
        //将角度转为弧度
        $from_lon = deg2rad($from_lon);//deg2rad()函数将角度转换为弧度
        $from_lat = deg2rad($from_lat);
        $to_lon = deg2rad($to_lon);
        $to_lat = deg2rad($to_lat);
//        var_dump('经纬度转为弧度:', $from_lon, $from_lat, $to_lon, $to_lat);

        // 求弧度差
        $lon_radian_diff = $from_lon - $to_lon;
        $lat_radian_diff = $from_lat - $to_lat;
//        var_dump('经纬度弧度差:', $lon_radian_diff, $lat_radian_diff);

        //asin() 函数返回不同数值的反正弦,返回的结果是介于 -PI/2 与 PI/2 之间的弧度值。
        //pow() 函数返回 x 的 y 次方。
        //sin() 函数返回一个数的正弦。
        //cos() 函数返回一个数的余弦。
        $s = 2 * asin(sqrt(pow(sin($lat_radian_diff / 2), 2) + cos($from_lat) * cos($to_lat) * pow(sin($lon_radian_diff / 2), 2))) * $EARTH_RADIUS;

        // 是否转换单位为米, 处理保留小数位数
        if ($km){
            $distance = round($s, $decimal);
        }else{
            $distance = round($s * 1000, $decimal);
        }

        return $distance;
    }
}

– End –

风影OvO

风影OvO, 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA 4.0协议进行授权 | 转载请注明原文链接

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐