基於經緯度計算球面距離是 LBS 應用核心功能。本文提供標準 Haversine 公式 PHP 實現,支援公里/英里雙單位,並封裝為高性能工具函數。
<?php
/**
* 計算兩點經緯度間的球面距離
*
* @param float $lat1 起點緯度
* @param float $lng1 起點經度
* @param float $lat2 終點緯度
* @param float $lng2 終點經度
* @param string $unit 單位:'km' 公里(默認) | 'mi' 英里 | 'm' 米
* @return float 距離
*/
function haversineDistance(
float $lat1, float $lng1,
float $lat2, float $lng2,
string $unit = 'km'
): float {
// 地球平均半徑(單位:公里)
$earthRadius = [
'km' => 6371.0,
'mi' => 3958.8,
'm' => 6371000.0,
];
$R = $earthRadius[$unit] ?? $earthRadius['km'];
// 角度轉弧度
$lat1 = deg2rad($lat1);
$lng1 = deg2rad($lng1);
$lat2 = deg2rad($lat2);
$lng2 = deg2rad($lng2);
// 差值
$dLat = $lat2 - $lat1;
$dLng = $lng2 - $lng1;
// Haversine 公式
$a = sin($dLat / 2) ** 2
+ cos($lat1) * cos($lat2) * sin($dLng / 2) ** 2;
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $R * $c;
}
// 使用示例
// 北京天安門 -> 上海外灘
$distance = haversineDistance(39.9042, 116.4074, 31.2304, 121.4737);
echo "距離: " . round($distance, 2) . " km\n"; // 約 1067 km
$distanceM = haversineDistance(39.9042, 116.4074, 31.2304, 121.4737, 'm');
echo "距離: " . number_format($distanceM, 0) . " m\n"; // 約 1,067,000 m
?>
Haversine 公式計算球面上兩點間的大圓距離:
hav(θ) = sin²(θ/2) = (1 - cos θ) / 2 a = hav(Δφ) + cos φ1 ⋅ cos φ2 ⋅ hav(Δλ) c = 2 ⋅ atan2(√a, √(1−a)) d = R ⋅ c φ: 緯度, λ: 經度, R: 地球半徑
/**
* 批量計算單點到多點距離(向量化優化)
* 返回索引 => 距離 的數組
*/
function batchHaversine(
float $lat1, float $lng1,
array $targets, // [['lat' => , 'lng' =>], ...]
string $unit = 'km'
): array {
$R = match ($unit) {
'km' => 6371.0,
'mi' => 3958.8,
'm' => 6371000.0,
default => 6371.0,
};
$lat1 = deg2rad($lat1);
$lng1 = deg2rad($lng1);
$cosLat1 = cos($lat1);
$results = [];
foreach ($targets as $i => $target) {
$lat2 = deg2rad($target['lat']);
$lng2 = deg2rad($target['lng']);
$dLat = $lat2 - $lat1;
$dLng = $lng2 - $lng1;
$a = sin($dLat / 2) ** 2
+ $cosLat1 * cos($lat2) * sin($dLng / 2) ** 2;
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
$results[$i] = $R * $c;
}
return $results;
}
// 使用:查找附近 5km 內的門店
$stores = [
['id' => 1, 'lat' => 39.91, 'lng' => 116.41],
['id' => 2, 'lat' => 39.95, 'lng' => 116.38],
// ...
];
$distances = batchHaversine(39.9042, 116.4074, $stores, 'km');
$nearby = array_filter($distances, fn($d) => $d <= 5);
?>
| 方法 | 精度 | 性能 | 適用場景 |
|---|---|---|---|
| Haversine | ~0.5% 誤差 | 極快 | 通用 LBS、距離排序、篩選 |
| Vincenty 公式 | 毫米級 | 較慢 | 測繪、導航、高精度測量 |
| MySQL ST_Distance_Sphere | 同 Haversine | DB 端計算 | 數據庫層篩選、空間索引 |
| PostGIS ST_Distance | 高精度 | DB 端 | 複雜空間查詢、地理圍欄 |
-- 查找 5km 內門店
SELECT id, name,
ST_Distance_Sphere(point(lng, lat), point(116.4074, 39.9042)) / 1000 AS distance_km
FROM stores
WHERE ST_Distance_Sphere(point(lng, lat), point(116.4074, 39.9042)) <= 5000
ORDER BY distance_km
LIMIT 20;
-- 空間索引加速 (MySQL 8.0)
ALTER TABLE stores ADD SPATIAL INDEX idx_location (location);
-- location 為 POINT 類型字段
Haversine 公式以其簡潔高效成為 LBS 應用首選,配合數據庫空間索引可支撐千萬級 POI 查詢。