一、需求
首先还是讲一下需求:
现在有一张浙江省的地点表 address
(包含470W+条项目所属场所地点数据)表结构与部分数据展示如下图
其中主要用到的字段基本都写备注了,没写备注的就是这个功能里用不到的。
然后根据前端在地图上标记后传来的经纬度坐标,去 address
表中查找 location
字段与这个接收到的经纬度坐标距离最近的一条数据,返回给前端就行了。
二、思路
由于数据量较大,肯定不能每次都把470W条经纬度全都读出来一一去对比。
我的思路是将地图网格化成 50×50的等差网格阵列,即2500个边长相等小网格;
先将这些小网格的左上角和右下角的经纬度坐标计算出来,根据这两个经纬度坐标就可以得到一个二维平面区域围栏,将这2500个小区域对应的左上角、右下角经纬度坐标存入网格数据表 zhejiang_grid
中;
zhejiang_grid
结构:
再一次性预处理好这些小网格(二维平面区域围栏)内包含了 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 –