
本文详解如何解析 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 '{$closest['id']}' 请求潮位详情:
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,输出精准、结构清晰、易于扩展,是地理距离检索类任务的可靠基础模板。











