<?php
/**
* 計算兩點經緯度間的球面距離
*
* @param float $lat1 起點緯度
* @param float $lon1 起點經度
* @param float $lat2 終點緯度
* @param float $lon2 終點經度
* @param string $unit 回傳單位:km(預設)、m、mi、ft、yd、nm
* @return float|array 單位指定時回傳浮點數;null 回傳全部單位陣列
*/
function geoDistance(float $lat1, float $lon1, float $lat2, float $lon2, $unit = 'km') {
// 角度轉弧度
$lat1 = deg2rad($lat1);
$lon1 = deg2rad($lon1);
$lat2 = deg2rad($lat2);
$lon2 = deg2rad($lon2);
// Haversine 公式
$dLat = $lat2 - $lat1;
$dLon = $lon2 - $lon1;
$a = sin($dLat / 2) ** 2 + cos($lat1) * cos($lat2) * sin($dLon / 2) ** 2;
$c = 2 * asin(min(1, sqrt($a))); // min(1,..) 防浮點誤差超出定義域
// 地球平均半徑 (km)
$R = 6371.0;
$km = $R * $c;
$units = [
'km' => $km,
'm' => $km * 1000,
'mi' => $km * 0.621371,
'ft' => $km * 3280.84,
'yd' => $km * 1093.61,
'nm' => $km * 0.539957, // 海裡
];
return $unit === null ? $units : ($units[$unit] ?? $km);
}
// 使用範例:台北 101 → 高雄 85 大樓
$dist = geoDistance(25.033964, 121.564468, 22.613939, 120.301477, 'km');
echo "直線距離: {$dist} km\n"; // 約 285 km
假設地球為標準球體(半徑 R),兩點經緯度 (φ₁, λ₁), (φ₂, λ₂):
Δφ = φ₂ - φ₁ Δλ = λ₂ - λ₁ a = sin²(Δφ/2) + cos(φ₁) cos(φ₂) sin²(Δλ/2) c = 2 · atan2(√a, √(1-a)) 或 2 · asin(√a) d = R · c
本實作使用 asin(min(1, √a)) 避免浮點誤差導致 a > 1 拋出錯誤。
| 方法 | 精度 | 速度 | 適用場景 |
|---|---|---|---|
| Haversine (球面) | ~0.5% 誤差 | 極快 | 一般 LBS、距離排序、半徑搜尋 |
| Vincenty (橢球體) | 毫米級 | 慢 10-20x | 測繪、精密導航、軍事 |
| MySQL ST_Distance_Sphere | 同 Haversine | DB 層加速 | 資料庫層面就近查詢 |
| PostGIS geography | 高精度 | 中等 | GIS 專業應用 |
結論:Web/LBS 服務 99% 場景 Haversine 足夠,且 PHP 純運算每秒可處理百萬次以上。
大量座標對計算時,改用 C 擴展或預計算常數:
<?php
// 預計算常數
const EARTH_RADIUS_KM = 6371.0;
const DEG2RAD = M_PI / 180.0;
function geoDistanceFast(float $lat1, float $lon1, float $lat2, float $lon2): float {
$lat1 *= DEG2RAD; $lon1 *= DEG2RAD;
$lat2 *= DEG2RAD; $lon2 *= DEG2RAD;
$dLat = $lat2 - $lat1;
$dLon = $lon2 - $lon1;
$a = sin($dLat * 0.5) ** 2 + cos($lat1) * cos($lat2) * sin($dLon * 0.5) ** 2;
return EARTH_RADIUS_KM * 2 * asin(min(1, sqrt($a)));
}
-- MySQL 5.7+ / MariaDB 10.2+ 空間索引
ALTER TABLE shops ADD COLUMN location POINT NOT NULL;
UPDATE shops SET location = POINT(lng, lat);
ALTER TABLE shops ADD SPATIAL INDEX idx_location (location);
-- 半徑 5km 內的店家(MySQL 8.0+ ST_Distance_Sphere)
SELECT *, ST_Distance_Sphere(location, POINT(121.56, 25.03)) / 1000 AS dist_km
FROM shops
WHERE ST_Distance_Sphere(location, POINT(121.56, 25.03)) <= 5000
ORDER BY dist_km ASC
LIMIT 20;
資料庫空間索引可將「附近搜尋」從全表掃描降為毫秒級。
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 寫入店家座標
$redis->geoAdd('shops:taiwan', 121.564468, 25.033964, 'shop_101');
$redis->geoAdd('shops:taiwan', 120.301477, 22.613939, 'shop_85');
// 半徑 10km 內搜尋(含距離、座標)
$nearby = $redis->geoRadius('shops:taiwan', 121.5, 25.0, 10, 'km', ['WITHDIST', 'WITHCOORD', 'ASC']);
// [['shop_101', '2.3', '121.564468', '25.033964'], ...]
Redis GEO 使用 Geohash + 有序集合,百萬級 POI 亞毫秒回應,適合高併發「附近的人/店」場景。
geoDistance($lat, $lng, ...) 緯度在前,經度在後(x=lng, y=lat)