如何使用PHP筛选地图指定半径范围内的指定数量坐标点
经纬度半径范围点位筛选实现方案
你当前全表查询返回所有点位的逻辑无法满足半径筛选需求,直接在SQL层通过Haversine球面距离公式做筛选即可,3000条数据量下性能完全够用,还支持直接限制返回点位数量,最终效果就是指定圆形范围内的点位被保留、范围外点位被排除,和你给出的参考示例逻辑一致。
实现逻辑
- 先做矩形粗筛:根据给定半径先算出经纬度的大致上下界,过滤掉明显不在范围内的点,减少后续计算量
- 1纬度对应的地表距离约为111km,因此纬度筛选范围为中心点纬度 ± (半径公里数/111)
- 经度每度对应的距离随纬度升高而缩短,计算公式为
111 * cos(中心点纬度弧度值),因此经度筛选范围为中心点经度 ± (半径公里数/上述计算值)
- 再做球面距离精算:用Haversine公式计算粗筛后每个点位和中心点的实际地表距离,只保留距离小于等于指定半径的点
- 最后按距离从近到远排序,限制返回指定数量的点位即可
改造后的代码
替换你原来的全表查询逻辑即可,代码中用了预处理语句避免SQL注入问题:
<?php require_once("db.php"); function parseToXML($htmlStr) { $xmlStr=str_replace('<','<',$htmlStr); $xmlStr=str_replace('>','>',$xmlStr); $xmlStr=str_replace('"','"',$xmlStr); $xmlStr=str_replace("'",''',$xmlStr); $xmlStr=str_replace("&",'&',$xmlStr); return $xmlStr; } // 筛选参数,可根据业务场景从GET/POST参数中获取 $centerLat = $_GET['lat'] ?? 0; // 中心点纬度 $centerLng = $_GET['lng'] ?? 0; // 中心点经度 $radiusKm = $_GET['radius'] ?? 5; // 筛选半径,单位公里,默认5公里 $limit = $_GET['limit'] ?? 100; // 返回点位数量,默认100个 // 参数合法性校验 if (!is_numeric($centerLat) || !is_numeric($centerLng) || !is_numeric($radiusKm) || !is_numeric($limit)) { die('Invalid parameter'); } $centerLat = floatval($centerLat); $centerLng = floatval($centerLng); $radiusKm = floatval($radiusKm); $limit = intval($limit); // 计算粗筛经纬度边界 $latRange = $radiusKm / 111; $minLat = $centerLat - $latRange; $maxLat = $centerLat + $latRange; $lngRange = $radiusKm / (111 * cos(deg2rad($centerLat))); $minLng = $centerLng - $lngRange; $maxLng = $centerLng + $lngRange; // Haversine公式计算距离的SQL片段 $distanceSql = " 6371 * ACOS( LEAST(1, COS(RADIANS(?)) * COS(RADIANS(lat)) * COS(RADIANS(lng) - RADIANS(?)) + SIN(RADIANS(?)) * SIN(RADIANS(lat))) ) AS distance "; // 预处理查询语句,先粗筛再精算,排序后限制数量 $query = " SELECT id, address, lat, lng, $distanceSql FROM addresses WHERE lat BETWEEN ? AND ? AND lng BETWEEN ? AND ? HAVING distance <= ? ORDER BY distance ASC LIMIT ? "; $stmt = mysqli_prepare($conn, $query); mysqli_stmt_bind_param($stmt, 'ddddddddi', $centerLat, $centerLng, $centerLat, $minLat, $maxLat, $minLng, $maxLng, $radiusKm, $limit); mysqli_stmt_execute($stmt); $result = mysqli_stmt_get_result($stmt); if (!$result) { die('Invalid query: ' . mysqli_error($conn)); } header("Content-type: text/xml"); // 输出XML echo "<?xml version='1.0' ?>"; echo '<markers>'; $ind=0; while ($row = mysqli_fetch_assoc($result)){ echo '<marker '; echo 'id="' . $row['id'] . '" '; echo 'name="' . $row['id'] . '" '; echo 'address="' . parseToXML($row['address']) . '" '; echo 'lat="' . $row['lat'] . '" '; echo 'lng="' . $row['lng'] . '" '; echo 'distance="' . round($row['distance'], 2) . '" '; // 可选输出点位到中心点的距离,单位公里 echo 'type="House" '; echo '/>'; $ind++; } echo '</markers>'; mysqli_stmt_close($stmt); mysqli_close($conn); ?>
注意点
- 代码里的6371是地球半径,单位为公里,如果需要用英里做单位,把这个值改成3959即可
LEAST(1, ...)是为了避免浮点计算误差导致ACOS参数超过1抛出数据库错误- 你当前3000条的数据量,即使去掉粗筛逻辑直接全表计算距离也不会有明显性能问题,加矩形粗筛是为了后续数据量增长到几万、几十万级别的时候依然能保持查询效率
- 如果后续数据量超过10万条,建议给lat、lng字段加普通索引,或者升级使用MySQL空间索引做地理位置查询,性能会提升几个量级
- 如果你使用的是MySQL 5.7.6及以上版本,也可以用内置的空间函数
ST_Distance_Sphere替代手写Haversine公式,写法更简洁,性能也更好,对应距离计算SQL片段如下,注意POINT参数顺序为经度在前、纬度在后,计算结果默认单位为米,除以1000即可转为公里:
ST_Distance_Sphere(POINT(lng, lat), POINT(?, ?)) / 1000 AS distance
内容的提问来源于stack exchange,提问作者m0nsterr
相关产品推荐
相关产品推荐

