首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 計算兩地經緯度距離:Haversine 公式完整實作與效能優化

     編輯:PHP 綜合知識

核心函式:回傳多種單位距離

<?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

Haversine 公式數學原理

假設地球為標準球體(半徑 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同 HaversineDB 層加速資料庫層面就近查詢
PostGIS geography高精度中等GIS 專業應用

結論:Web/LBS 服務 99% 場景 Haversine 足夠,且 PHP 純運算每秒可處理百萬次以上。

效能優化技巧

1. 批次計算向量化(PHP 8.1+ FFI / 拓展)

大量座標對計算時,改用 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)));
}

2. 資料庫層面過濾(避免全表掃描)

-- 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;

資料庫空間索引可將「附近搜尋」從全表掃描降為毫秒級。

3. Redis GEO 即時高併發

<?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)
  • 單位統一:前端顯示 km/m,後端儲存/計算統一用米或公里,避免反覆轉換
  • 極地/跨經線:Haversine 在兩極、跨 ±180° 經線仍正確,但平面投影法會失效
  • 浮點精度:極近距離(<1m)浮點誤差明顯,室內定位請用 UWB/Beacon 而非 GPS
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved