給定兩點經緯度 (度),計算球面距離 (米)。
<?php
/**
* 計算兩點球面距離
* @param float $lat1 起點緯度
* @param float $lng1 起點經度
* @param float $lat2 終點緯度
* @param float $lng2 終點經度
* @return float 距離 (公尺)
*/
function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float {
$earthRadius = 6371000; // 地球平均半徑 (公尺)
// 角度轉弧度
$lat1 = deg2rad($lat1);
$lng1 = deg2rad($lng1);
$lat2 = deg2rad($lat2);
$lng2 = deg2rad($lng2);
$dLat = $lat2 - $lat1;
$dLng = $lng2 - $lng1;
$a = sin($dLat / 2) ** 2 + cos($lat1) * cos($lat2) * sin($dLng / 2) ** 2;
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $earthRadius * $c;
}
// 使用
$dist = haversineDistance(39.9042, 116.4074, 31.2304, 121.4737); // 北京 → 上海
echo round($dist / 1000, 1) . ' km'; // 約 1068.5 km
?>
百度地圖使用 **BD-09** 加密座標,直接用 Haversine 會有偏移 (約幾百公尺)。需先轉換。
<?php
function bd09ToWgs84Api(float $lng, float $lat, string $ak): ?array {
$url = "https://api.map.baidu.com/geoconv/v1/?coords={$lng},{$lat}&from=5&to=1&ak={$ak}";
$json = @file_get_contents($url);
if (!$json) return null;
$data = json_decode($json, true);
if (($data['status'] ?? -1) !== 0) return null;
$result = $data['result'][0] ?? null;
return $result ? [$result['x'], $result['y']] : null; // [lng, lat]
}
?>
參考開源實作 (如 `wgs84togcj02`、`gcj02tobd09` 反向)。
<?php
const PI = 3.14159265358979324;
const A = 6378245.0; // 長半軸
const EE = 0.00669342162296594323; // 扁率平方
function transformLat(float $lng, float $lat): float {
$ret = -100.0 + 2.0 * $lng + 3.0 * $lat + 0.2 * $lat * $lat + 0.1 * $lng * $lat + 0.2 * sqrt(abs($lng));
$ret += (20.0 * sin(6.0 * $lng * PI) + 20.0 * sin(2.0 * $lng * PI)) * 2.0 / 3.0;
$ret += (20.0 * sin($lat * PI) + 40.0 * sin($lat / 3.0 * PI)) * 2.0 / 3.0;
$ret += (160.0 * sin($lat / 12.0 * PI) + 320.0 * sin($lat / 30.0 * PI)) * 2.0 / 3.0;
return $ret;
}
function transformLng(float $lng, float $lat): float {
$ret = 300.0 + $lng + 2.0 * $lat + 0.1 * $lng * $lng + 0.1 * $lng * $lat + 0.1 * sqrt(abs($lng));
$ret += (20.0 * sin(6.0 * $lng * PI) + 20.0 * sin(2.0 * $lng * PI)) * 2.0 / 3.0;
$ret += (20.0 * sin($lng * PI) + 40.0 * sin($lng / 3.0 * PI)) * 2.0 / 3.0;
$ret += (150.0 * sin($lng / 12.0 * PI) + 300.0 * sin($lng / 30.0 * PI)) * 2.0 / 3.0;
return $ret;
}
function wgs84ToGcj02(float $lng, float $lat): array {
if (outOfChina($lng, $lat)) return [$lng, $lat];
$dLat = transformLat($lng - 105.0, $lat - 35.0);
$dLng = transformLng($lng - 105.0, $lat - 35.0);
$radLat = $lat / 180.0 * PI;
$magic = sin($radLat);
$magic = 1 - $EE * $magic * $magic;
$sqrtMagic = sqrt($magic);
$dLat = ($dLat * 180.0) / ((A * (1 - $EE)) / ($magic * $sqrtMagic) * PI);
$dLng = ($dLng * 180.0) / (A / $sqrtMagic * cos($radLat) * PI);
return [$lng + $dLng, $lat + $dLat];
}
function gcj02ToBd09(float $lng, float $lat): array {
$z = sqrt($lng * $lng + $lat * $lat) + 0.00002 * sin($lat * PI);
$theta = atan2($lat, $lng) + 0.000003 * cos($lng * PI);
return [$z * cos($theta) + 0.0065, $z * sin($theta) + 0.006];
}
function bd09ToGcj02(float $lng, float $lat): array {
$x = $lng - 0.0065;
$y = $lat - 0.006;
$z = sqrt($x * $x + $y * $y) - 0.00002 * sin($y * PI);
$theta = atan2($y, $x) - 0.000003 * cos($x * PI);
return [$z * cos($theta), $z * sin($theta)];
}
function gcj02ToWgs84(float $lng, float $lat): array {
if (outOfChina($lng, $lat)) return [$lng, $lat];
[$gcjLng, $gcjLat] = wgs84ToGcj02($lng, $lat);
return [$lng * 2 - $gcjLng, $lat * 2 - $gcjLat];
}
function bd09ToWgs84(float $lng, float $lat): array {
[$gcjLng, $gcjLat] = bd09ToGcj02($lng, $lat);
return gcj02ToWgs84($gcjLng, $gcjLat);
}
function outOfChina(float $lng, float $lat): bool {
return $lng < 72.004 || $lng > 137.8347 || $lat < 0.8293 || $lat > 55.8271;
}
// 使用
[$wgsLng, $wgsLat] = bd09ToWgs84(116.4074, 39.9042); // 百度座標 → GPS
echo "WGS84: $wgsLng, $wgsLat";
?>
<?php
function baiduPoiDistance(array $poi1, array $poi2, ?string $ak = null): float {
// poi = ['lng' => ..., 'lat' => ...] (BD-09)
if ($ak) {
// 使用 API 轉換 (較準)
$wgs1 = bd09ToWgs84Api($poi1['lng'], $poi1['lat'], $ak);
$wgs2 = bd09ToWgs84Api($poi2['lng'], $poi2['lat'], $ak);
if (!$wgs1 || !$wgs2) throw new RuntimeException('百度 API 轉換失敗');
} else {
// 離線轉換 (免費、批次快)
$wgs1 = bd09ToWgs84($poi1['lng'], $poi1['lat']);
$wgs2 = bd09ToWgs84($poi2['lng'], $poi2['lat']);
}
return haversineDistance($wgs1[1], $wgs1[0], $wgs2[1], $wgs2[0]);
}
// 範例:天安門 → 東方明珠
$poi1 = ['lng' => 116.3975, 'lat' => 39.9087]; // 百度座標
$poi2 = ['lng' => 121.4726, 'lat' => 31.2378];
echo round(baiduPoiDistance($poi1, $poi2) / 1000, 1) . ' km';
?>
| 座標系 | 用途 | 中國偏移 | 來源 |
|---|---|---|---|
| WGS-84 | GPS 原始、Google Earth、國際標準 | 無 | 衛星定位 |
| GCJ-02 | 高德、騰訊、Google 中國、OSM 中國 | 有 (加密) | 國測局標準 |
| BD-09 | 百度地圖 | 有 (GCJ-02 再加密) | 百度自有 |
ALTER TABLE pois ADD COLUMN location POINT NOT NULL;
UPDATE pois SET location = ST_PointFromText(CONCAT('POINT(', wgs_lng, ' ', wgs_lat, ')'), 4326);
ALTER TABLE pois ADD SPATIAL INDEX idx_location (location);
-- 查詢 5km 內的 POI
SELECT *, ST_Distance_Sphere(location, ST_PointFromText('POINT(116.3975 39.9087)', 4326)) AS dist_m
FROM pois
WHERE ST_Distance_Sphere(location, ST_PointFromText('POINT(116.3975 39.9087)', 4326)) <= 5000
ORDER BY dist_m ASC;
掌握座標系轉換與 Haversine 公式,配合 MySQL 空間索引,打造高效、精準的 LBS 服務。