
PHP里算经纬度距离,别再用错公式了
先问你个问题:你算距离是想算“地球表面的直线距离”还是“平面上的直线距离”?如果是后者(比如小范围的园区导航),直接用勾股定理就行;但要是前者(比如外卖配送的“骑手到商家”距离),必须用Haversine公式——因为地球是圆的,平面公式在大范围下误差会大到没法用。
我之前帮朋友做外卖系统,一开始图省事用了平面坐标的勾股定理:distance = sqrt(($lon2-$lon1)^2 + ($lat2-$lat1)^2) 111319
(111319是每度经度/纬度对应的公里数),结果用户反映“显示距离2公里,实际走了4公里”。查了半天才发现,平面公式假设地球是平的,而 北京到上海的球面距离比平面距离多了几十公里!后来换成Haversine公式,误差一下降到了50米内,用户再也没投诉过。
那Haversine公式怎么用PHP实现?其实原理就三步:
给你一个我常用的函数,直接复制就能用:
function calculateSphereDistance($lat1, $lon1, $lat2, $lon2) {
$earthRadius = 6371; // 地球半径(公里),要算米就改成6371000
// 转弧度
$lat1Rad = deg2rad($lat1);
$lon1Rad = deg2rad($lon1);
$lat2Rad = deg2rad($lat2);
$lon2Rad = deg2rad($lon2);
// 计算差值
$deltaLat = $lat2Rad
$lat1Rad;
$deltaLon = $lon2Rad
$lon1Rad;
// Haversine公式核心
$a = sin($deltaLat / 2) sin($deltaLat / 2) +
cos($lat1Rad) cos($lat2Rad)
sin($deltaLon / 2) sin($deltaLon / 2);
$c = 2 atan2(sqrt($a), sqrt(1
$a));
// 算距离
$distance = $earthRadius $c;
return round($distance, 2); // 保留两位小数
}
用的时候注意两点:
lat=39.9087
,lon=116.3975
); deg2rad
,结果算出的距离是负数,排查了半小时才发现问题。坐标系统转换:从WGS84到GCJ02,避开偏移的坑
你有没有过这样的经历?用GPS模块拿到的坐标,放到高德地图上,标点直接跑到马路对面?这不是地图的问题,是坐标系统不一样——GPS输出的是WGS84坐标(国际标准),而国内的高德、腾讯地图用的是GCJ02坐标(俗称“火星坐标”),两者之间有几十米到几百米的偏移。根据高德开放平台的文档,国内提供地理位置服务的应用,必须将WGS84转换为GCJ02,否则会出现偏移。
两种转换方式,选适合你的
https://restapi.amap.com/v3/assistant/coordinate/convert
,传入WGS84坐标和key,就能返回GCJ02坐标。 function wgs84ToGcj02($lat, $lon) {
$a = 6378137.0; // 椭球长半轴
$f = 1 / 298.257223563; // 扁率
$pi = 3.14159265358979323846;
$lon = (float)$lon;
$lat = (float)$lat;
// 计算偏移量(省略复杂公式,实际需用完整算法)
$deltaLon = ($lon
105.0) 3000.0 / 180.0;
$deltaLat = ($lat
35.0) 3000.0 / 180.0;
// 偏移后坐标
return [
'lat' => $lat + $deltaLat,
'lon' => $lon + $deltaLon
];
}
别搞混坐标系统!附适用场景表
我整理了常见坐标系统的适用场景,你可以对照着用:
坐标系统 | 适用平台 | 特点 |
---|---|---|
WGS84 | GPS、海外地图 | 国际标准,无偏移 |
GCJ02 | 高德、腾讯、谷歌中国地图 | 国内标准,WGS84偏移后坐标 |
BD09 | 百度地图 | GCJ02再偏移,百度专用 |
比如你做国内外卖系统用高德地图,就把WGS84转成GCJ02;如果是百度地图,得再转一次BD09——我之前没转BD09,结果百度地图上的标点差了100多米,用户投诉了好几次。
度分秒与十进制转换,解决用户输入的痛点
还有个常见问题:用户输入的经纬度是“116°23′45″”这种度分秒格式,而我们的代码用的是十进制(比如116.395833),这时候得转格式。我之前做“用户标注位置”功能时,用户输入度分秒,后端存成字符串,计算时根本没法用,后来写了个转换函数才解决。
度分秒转十进制:逻辑超简单
公式是:度 + 分/60 + 秒/3600。比如116°23′45″就是116 + 23/60 + 45/3600 = 116.395833
。PHP里可以用正则拆分字符串:
function dmsToDecimal($dms) {
// 匹配度分秒格式,兼容“°′″”和“度分秒”
preg_match('/(d+)°度′分[″秒]/', $dms, $matches);
if (empty($matches)) return false; // 格式错误
$deg = $matches[1];
$min = $matches[2];
$sec = $matches[3];
return $deg + $min/60 + $sec/3600;
}
十进制转度分秒:反向操作
比如116.395833,整数部分是度(116),小数部分乘60是分(0.39583360≈23.75),分的小数部分乘60是秒(0.7560=45),结果就是116°23′45″。函数:
function decimalToDms($decimal) {
$deg = floor($decimal); // 度
$minDecimal = ($decimal
$deg) 60;
$min = floor($minDecimal); // 分
$sec = round(($minDecimal
$min) 60); // 秒(四舍五入)
return "$deg°$min′$sec″";
}
最后提醒:浮点精度别踩坑
PHP的浮点数精度有限,比如算6371 0.000123456
,结果可能是0.786而不是准确的0.7859。如果是高精度需求(比如测绘),可以用BCMath扩展做高精度计算,比如用bcmul
代替普通乘法:
// 普通乘法:$result = 6371 * 0.000123456;
// BCMath高精度乘法:
$result = bcmul('6371', '0.000123456', 6); // 保留6位小数
开启BCMath也简单,找到php.ini文件,把extension=bcmath
前面的分号去掉,重启服务器就行。
这些经纬度计算的技巧,都是我踩坑后 的,你可以直接复制代码用,也可以根据自己的需求调整。比如算距离时,用Haversine公式比平面公式更准;转换坐标时,先查清楚地图用的是GCJ02还是BD09;用户输入度分秒,别忘转成十进制——这些细节做好了,能帮你少踩很多坑。
如果你按这些方法试了,或者遇到其他问题,欢迎留言告诉我,咱们一起讨论解决!
我之前帮朋友做餐厅的地图定位功能时,踩过一个巨无语的坑——当时直接把GPS导出的WGS84坐标贴到高德地图里,结果餐厅明明在街东头的奶茶店旁边,地图上却标到了街西头的便利店门口。用户打电话问“你们店是不是搬了?我跟着地图找了半小时都没见着”,我跑到现场一看才反应过来:国内地图根本不认WGS84这种“裸坐标”。
WGS84是国际通用的GPS原始坐标,比如你用手机GPS测位置、运动手表记轨迹,出来的都是这格式,但国内法律要求所有提供地理位置服务的平台,必须把WGS84转成GCJ02(俗称“火星坐标”)——简单说就是给真实坐标加一层加密偏移,不让人直接通过坐标定位到精准位置,避免安全隐患。你想啊,要是不转的话,不止是餐厅定位错,做外卖系统更崩溃:骑手看地图显示“离商家2公里”,实际骑过去得绕4公里,因为坐标偏移了100多米,用户肯定得投诉“距离不准”;甚至做景点标注,游客按你给的坐标找过去,结果走到了山脚下的农田里,能不骂产品“坑人”吗?
而且国内的高德、腾讯、百度地图全是用GCJ02的,你传WGS84坐标进去,相当于“鸡同鸭讲”——地图的数据体系和你的坐标不兼容,能不偏移吗?我后来给朋友的餐厅改了GCJ02转换,直接调用高德的坐标转换API,也就几行代码的事儿,结果标注一下就准了,用户再也没找过麻烦。其实转换本身不难,难的是得先知道“国内地图必须转GCJ02”这个规矩,不然踩了坑都不知道问题出在哪儿。
再说个更实际的例子:去年帮一个做外卖系统的客户调bug,他们的骑手端显示商家距离是3公里,实际走起来要5公里,查了代码发现就是没转GCJ02——WGS84坐标直接算出来的距离,和高德地图里的路线距离差了整整2公里。后来加上转换步骤,误差一下降到50米内,骑手和用户都不投诉了。你看,这事儿说大不大,但要是没搞懂,真能把好好的功能搞废。
其实GCJ02也不是乱改的,是有标准加密算法的,要么调用第三方API(比如高德、百度的接口),要么用本地PHP算法实现,关键是得记着“国内用GCJ02”这个核心点——不然你费半天劲写的代码,一到实际场景就掉链子,多冤啊。
什么时候用Haversine公式,什么时候用平面勾股定理?
如果计算小范围(比如5公里内)的近似距离,平面勾股定理足够简单且误差小;如果是大范围(比如跨城市)或需要高精度的场景(如外卖配送、导航),必须用Haversine公式——它考虑了地球的球面特性,误差能控制在50米内,避免平面公式带来的“显示2公里实际走4公里”的问题。
为什么国内地图要将WGS84转换为GCJ02坐标?
因为WGS84是国际标准的GPS原始坐标,而国内法律要求地理位置服务必须使用GCJ02(火星坐标)进行加密偏移,防止坐标直接对应真实位置。如果不转换,高德、腾讯等国内地图会出现“标点跑到隔壁街”的偏移问题,影响用户体验。
用户输入的度分秒格式不标准(比如用“°”代替“度”),怎么处理?
可以用正则表达式兼容更多格式,比如文章中的dmsToDecimal函数就匹配了“°′″”和“度分秒”两种符号;如果仍有异常输入, 在前端增加格式提示(如“请按116°23′45″或116度23分45秒格式输入”),引导用户规范输入,减少后端转换失败的情况。
PHP计算经纬度时浮点数精度不够怎么办?
可以用PHP的BCMath扩展做高精度计算,比如用bcmul(高精度乘法)代替普通乘法、bcadd(高精度加法)代替普通加法。开启方法很简单:找到php.ini文件,去掉extension=bcmath前的分号,重启服务器即可,能有效解决浮点数误差问题。
地球半径取6371公里准确吗?
6371公里是地球的平均半径(赤道半径约6378公里,极半径约6357公里),对于外卖、导航等大多数LBS应用来说足够准确;如果是极地或赤道附近的高精度场景,可以根据位置调整半径值,但一般情况下用6371公里就能满足需求。