
本文详解如何解析 admiralty tide api 的 geojson 数据,结合 haversine 公式精确计算球面距离,并快速定位最近站点的 `id` 字段,适用于潮位查询、地理服务集成等实际场景。
在处理地理坐标数据(如潮位站、气象站或地图标记)时,一个常见需求是:给定用户当前经纬度,从远程 API 返回的大量站点中找出物理距离最近的一个,并提取其唯一标识(如 properties->Id)。原始代码存在多重嵌套循环、错误的数组遍历路径(误将整个 $locations 当作多维数组逐层 foreach),且使用欧氏距离(平面直角坐标系)计算经纬度——这在地球曲面上会产生显著误差(尤其跨纬度时)。下面提供一套健壮、可复用、符合地理精度要求的解决方案。
✅ 正确解析结构 & 遍历逻辑
Admiralty 的 /Home/GetStations 接口返回标准 GeojsON FeatureCollection,其核心结构为:
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [lng, lat] }, "properties": { "Id": "0065", "Name": "PORTSMOUTH", ... } } ] }
关键点:
- coordinates 是 [longitude, latitude](注意顺序!X 轴为经度,Y 轴为纬度);
- 应直接遍历 $stations->features(对象模式)或 $stations[‘features’](关联数组模式),无需深层嵌套 foreach;
- 使用 json_decode($json, false)(默认 false)返回对象,更符合 GeoJSON 规范访问习惯。
✅ 使用 Haversine 公式计算真实球面距离
地球是球体,两点间最短路径是大圆弧。以下函数封装了高精度 Haversine 计算,支持公里(km)、英里(mi)和海里(nmi)三种单位:
立即学习“PHP免费学习笔记(深入)”;
function getDistance(float $from_lat, float $from_lng, float $to_lat, float $to_lng, string $unit = 'nmi', int $decimals = 2): float { $lat1 = deg2rad($from_lat); $lng1 = deg2rad($from_lng); $lat2 = deg2rad($to_lat); $lng2 = deg2rad($to_lng); $dlat = $lat2 - $lat1; $dlng = $lng2 - $lng1; $a = sin($dlat / 2) * sin($dlat / 2) + cos($lat1) * cos($lat2) * sin($dlng / 2) * sin($dlng / 2); $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); // 地球平均半径(km) $earth_radius_km = 6371.0; $distance_km = $earth_radius_km * $c; switch (strtolower($unit)) { case 'km': $distance = $distance_km; break; case 'mi': $distance = $distance_km * 0.621371; break; case 'nmi': $distance = $distance_km * 0.539957; break; default: $distance = $distance_km; } return round($distance, $decimals); }
⚠️ 注意:原始答案中使用的 rad2deg(acos(…)) 方法在极近距离或数值精度边界下可能因浮点误差导致 acos() 参数略超 [-1,1] 范围而报错;Haversine 的 atan2 形式更鲁棒。
✅ 完整可运行示例(含错误处理)
50.77842324616663, 'lng' => -1.087804949548603 ]; // 3. 遍历 features,计算距离,追踪最小值 $closest = null; $min_distance = null; foreach ($stations->features as $index => $feature) { // 安全检查:确保 geometry 和 coordinates 存在 if (!isset($feature->geometry->coordinates[0], $feature->geometry->coordinates[1])) { continue; } $station_lng = (float)$feature->geometry->coordinates[0]; $station_lat = (float)$feature->geometry->coordinates[1]; $distance = getDistance( $user_location['lat'], $user_location['lng'], $station_lat, $station_lng, 'nmi', // 单位:海里(航海常用) 4 ); if ($min_distance === null || $distance < $min_distance) { $min_distance = $distance; $closest = [ 'distance' => $distance, 'index' => $index, 'id' => $feature->properties->Id ?? 'N/A', 'name' => $feature->properties->Name ?? 'Unknown', 'country' => $feature->properties->Country ?? 'N/A' ]; } } // 4. 输出结果(供后续调用,如 file_get_contents 构造新请求) if ($closest) { echo "✅ 最近站点信息
"; echo ""; echo "- ID: {$closest['id']}
"; echo "- 名称: {$closest['name']}
"; echo "- 距离: {$closest['distance']} 海里
"; echo "- 国家: {$closest['country']}
"; echo "
"; // ✅ 关键:这就是你后续请求所需的 ID echo "下一步可使用 ID '{$closest['id']}' 请求潮位详情:
"; echo "file_get_contents('https://easytide.admiralty.co.uk/Home/GetTideData?stationId={$closest['id']}')
"; } else { echo "⚠️ 未找到有效站点"; }
? 总结与最佳实践
- 避免平面距离陷阱:永远不要对经纬度直接使用 (lat1-lat2)² + (lng1-lng2)²,它仅在赤道附近小范围近似有效;
- 结构化遍历:GeoJSON 层级清晰,直击 $data->features 即可,无需多层 foreach 嵌套;
- 防御性编程:始终检查 isset() 和 json_last_error(),API 数据格式可能变更;
- 性能提示:若站点数达万级,建议预存为数据库并建立空间索引(如 mysql POINT + ST_Distance_Sphere);
- 单位一致性:getDistance() 函数明确区分输入(十进制度)与输出(指定单位),避免混淆。
此方案已验证可稳定对接 Admiralty Tide API,输出精准、结构清晰、易于扩展,是地理距离检索类任务的可靠基础模板。