24、距离计算:Haversine公式、SphericalUtil工具类、多点距离与面积计算
说到地理位置的距离计算,很多人第一反应就是「直接算直线距离不就行了?」。嗯,如果你是在一张平面地图上,那确实可以。但问题是——地球是圆的。
我在刚做LBS项目那会儿,就踩过这个坑。直接用欧几里得距离算两个经纬度点,结果在短距离上误差还能忍,一旦跨城市、跨国家,那偏差简直离谱。后来我才意识到,必须用球面几何来处理。
这一章,我们就来聊聊Android里最常用的距离计算方案:Haversine公式、Google Maps的SphericalUtil工具类,以及如何计算多点路径长度和多边形面积。
24.1 为什么不能直接用平面距离?
先看一个简单例子:
- 北京:39.9042°N, 116.4074°E
- 纽约:40.7128°N, 74.0060°W
如果你用平面直角坐标去算,结果大概在几千公里。但实际飞行距离大约是1.1万公里。为什么会差这么多?因为地球表面是曲面,经线在极点汇聚,纬线长度随纬度变化。平面公式完全没考虑这些。
说白了,经纬度是球面坐标,不是平面坐标。你要在球面上算距离,就得用球面三角学。
24.2 Haversine公式
Haversine公式是计算球面上两点间大圆距离的经典方法。它基于球面余弦定理的变体,数值稳定性更好,尤其适合短距离计算。
公式长这样:
a = sin²(Δlat/2) + cos(lat1)·cos(lat2)·sin²(Δlon/2)
c = 2 · atan2(√a, √(1−a))
d = R · c
其中R是地球半径,通常取6371公里。
我直接给你一个可用的Java实现:
public class DistanceCalculator {
private static final double EARTH_RADIUS_KM = 6371.0;
public static double haversine(double lat1, double lon1,
double lat2, double lon2) {
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
+ Math.cos(Math.toRadians(lat1))
* Math.cos(Math.toRadians(lat2))
* Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS_KM * c;
}
}
调用方式:
double distance = DistanceCalculator.haversine(
39.9042, 116.4074, // 北京
40.7128, -74.0060 // 纽约
);
// 结果约 10989 km
24.3 SphericalUtil工具类
如果你用的是Google Maps SDK,那其实不用自己手写Haversine。Google在com.google.maps.android:android-maps-utils库中提供了SphericalUtil类,封装了各种球面计算。
添加依赖:
implementation 'com.google.maps.android:android-maps-utils:2.4.0'
计算两点距离:
import com.google.maps.android.SphericalUtil;
import com.google.android.gms.maps.model.LatLng;
LatLng beijing = new LatLng(39.9042, 116.4074);
LatLng newYork = new LatLng(40.7128, -74.0060);
double distanceMeters = SphericalUtil.computeDistanceBetween(beijing, newYork);
// 结果单位是米,约 10989000 m
我个人习惯用SphericalUtil,原因有三:
- 它内部用的是Haversine的优化版本,精度和性能都很好
- 返回单位是米,省去单位换算
- 它还提供了方位角、终点推算等实用方法
比如计算从北京出发,沿正东方向走1000公里后的位置:
LatLng destination = SphericalUtil.computeOffset(
beijing, 1000000, 90 // 1000公里,正东90度
);
24.4 多点距离计算
实际场景中,我们经常需要计算一条路径的总长度。比如骑行轨迹、外卖配送路线。这时候就需要把路径拆成若干线段,逐段累加。
用SphericalUtil实现很简单:
public double computePathLength(List<LatLng> points) {
double total = 0;
for (int i = 0; i < points.size() - 1; i++) {
total += SphericalUtil.computeDistanceBetween(
points.get(i), points.get(i + 1));
}
return total;
}
如果你自己实现Haversine版本,逻辑完全一样,只是把computeDistanceBetween换成你的函数。
这里有个坑——我曾经遇到过:用户上传的GPS轨迹点非常密集,每秒一个点。如果直接逐段累加,计算量很大,而且相邻点之间的抖动会导致路径长度被高估。我的做法是先做轨迹简化(比如Douglas-Peucker算法),再计算长度。
24.5 多边形面积计算
除了距离,面积计算也很常见。比如圈地、电子围栏、农田测量等。
球面上多边形面积的计算比平面复杂得多。好在SphericalUtil也提供了现成方法:
List<LatLng> polygon = Arrays.asList(
new LatLng(39.90, 116.40),
new LatLng(39.91, 116.41),
new LatLng(39.89, 116.42),
new LatLng(39.90, 116.40) // 首尾闭合
);
double areaMeters = SphericalUtil.computeArea(polygon);
返回单位是平方米。如果你需要平方公里,除以1e6即可。
如果你不能使用Google服务,自己实现球面面积计算可以用以下公式(基于球面三角剖分):
public static double computeAreaOnSphere(List<LatLng> points, double radius) {
int n = points.size();
if (n < 3) return 0;
double total = 0;
for (int i = 0; i < n; i++) {
LatLng p1 = points.get(i);
LatLng p2 = points.get((i + 1) % n);
LatLng p3 = points.get((i + 2) % n);
// 计算球面三角形的面积,累加
// 这里省略具体实现,核心是球面角盈公式
}
return Math.abs(total) * radius * radius;
}
嗯,这个实现细节比较多。如果你只是做普通App,我建议直接用SphericalUtil。只有当你需要离线计算或自定义地球半径时,才考虑手写。
24.6 知识体系总览
下面这张图帮你理清本章的核心逻辑:
24.7 避坑指南
最后,我总结几个实际项目中容易踩的坑:
- 经纬度顺序搞反:很多API是(lat, lng),但有些是(lng, lat)。我曾在集成第三方地图SDK时,因为顺序问题排查了一整天。建议统一用LatLng对象封装。
- 地球半径取值不一致:Haversine公式中R取6371km还是6378km?差别不大,但如果你做高精度测量,建议用WGS84椭球体的平均半径6371.009km。
- 面积计算要求闭合多边形:首尾点必须相同,否则SphericalUtil会默认帮你闭合,但结果可能不是你想要的。
- 性能问题:如果你需要实时计算大量点(比如每秒更新轨迹),建议把经纬度转成弧度缓存起来,避免重复调用Math.toRadians。
核心要点:
- 球面距离用Haversine公式,短距离精度足够
- 能用SphericalUtil就用,省心省力
- 多点距离就是逐段累加,注意轨迹简化
- 面积计算依赖球面角盈,推荐直接用工具类
好了,这一章的内容就到这里。距离计算是LBS开发的基础功,但也是容易出细节问题的地方。你想想看,如果外卖App的配送距离算错了,用户投诉可不是闹着玩的。所以,多测试、多验证,准没错。
公众号:蓝海资料掘金营,微信deep3321