Haversine 公式

Haversine 公式是球面几何中用于计算**球面上两点间最短路径(大圆距离)**的数值稳定公式。源自球面余弦定理,但通过使用半正矢函数避免了小角度下余弦差损失带来的精度问题。


1. 公式目的与应用

目的
计算球体表面上两点之间的最短弧长距离(大圆距离)。

典型应用

  • 地球表面两点间的真实距离(GPS、地图、LBS)
  • 航空航海路线估算
  • GIS 地理信息系统

输入:两点经纬度(弧度)、地球半径
输出:大圆距离 d


2. 数学符号定义

符号描述单位
\varphi_1,\ \varphi_2点 A、B 的纬度弧度
\lambda_1,\ \lambda_2点 A、B 的经度弧度
\Delta\varphi纬度差:\ \varphi_2 - \varphi_1弧度
\Delta\lambda经度差:\ \lambda_2 - \lambda_1弧度
R球体半径(地球约 6371\ \text{km}长度
d两点间距离长度

3. 数学推导

3.1 半正矢函数

半正矢(Haversine)定义:
\ \operatorname{haversin}(\theta) = \dfrac{1 - \cos\theta}{2} = \sin^2\left(\dfrac{\theta}{2}\right)\ ;

弧长公式:
\ d = R \cdot \theta\ ;


3.2 推导过程

① 球面余弦定理

\ \cos\theta = \sin(\varphi_1)\sin(\varphi_2) + \cos(\varphi_1)\cos(\varphi_2)\cos(\Delta\lambda)\ ;

② 引入半正矢变换

\ \operatorname{haversin}(\theta) = \dfrac{1 - \cos(\theta)}{2} ;

代入余弦定理得到:
\ \operatorname{haversin}(\theta) = \dfrac{1 - [\sin\varphi_1\sin\varphi_2 + \cos\varphi_1\cos\varphi_2\cos(\Delta\lambda)]}{2}\ ;

③ 使用半正矢恒等式

\ \operatorname{haversin}(\theta) = \operatorname{haversin}(\Delta\varphi)\cos(\varphi_1)\cos(\varphi_2)\operatorname{haversin}(\Delta\lambda)\ ;

④ 写成显式形式(常用)

\ \sin^2\left(\frac{\theta}{2}\right) = \sin^2\left(\frac{\Delta\varphi}{2}\right)\cos(\varphi_1)\cos(\varphi_2)\sin^2\left(\frac{\Delta\lambda}{2}\right)\ ;

⑤ 定义中间变量

\ a = \sin^2\left(\frac{\Delta\varphi}{2}\right)\cos(\varphi_1)\cos(\varphi_2)\sin^2\left(\frac{\Delta\lambda}{2}\right)\ ;

⑥ 求圆心角 \theta

主公式:
\ \theta = 2\arcsin(\sqrt{a})\ ;

数值更稳定的版本:
\ \theta = 2\arctan\left(\dfrac{\sqrt{a}}{\sqrt{1-a}}\right)\ ;


4. 最终公式(Haversine 距离公式)

\ \boxed{ d = 2R \cdot \arctan\left(\dfrac{\sqrt{a}}{\sqrt{1-a}}\right) }

其中:

\ a = \sin^2\left(\frac{\Delta\varphi}{2}\right)\cos(\varphi_1)\cos(\varphi_2)\sin^2\left(\frac{\Delta\lambda}{2}\right)


5. 实际计算步骤(工程实现)

  1. 经纬度从度数转为弧度
  2. 计算差值
    • \ \Delta\varphi = \varphi_2 - \varphi_1
    • \ \Delta\lambda = \lambda_2 - \lambda_1
  3. 计算中间量 a
  4. 计算圆心角 \ \theta = 2\arctan\left(\frac{\sqrt{a}}{\sqrt{1-a}}\right)
  5. 计算距离\ d = R\theta

6. 公式的优点与局限

优点

  • 小角度(近距离)计算非常稳定
  • 不会发生余弦差导致的精度丢失
  • 适合大多数地理、工程场景
  • 实现简单,性能优秀

局限

  • 假设地球为完美球体
  • 存在约 0.3% 的系统误差
  • 不适合厘米级精度场景(航空、大地测量)
  • 可使用 Vincenty 公式替代

7. 历史背景

Haversine 来自 “half-versed sine”,是航海与天文学用于查表的函数。
其主要目的就是避免余弦计算带来的负值处理与精度问题。


8. 关键参数与误差

地球半径常用值:

  • 平均值:6371\ \text{km}(推荐)
  • 赤道半径:6378.137\ \text{km}
  • 极半径:6356.752\ \text{km}

误差来源:

  • 使用球体模型替代椭球体
  • 近极地区效果略差

9. Haversine 与球面余弦定理的比较

特性Haversine 公式球面余弦定理
数值稳定性(近距离无精度损失)低(小角度误差明显)
计算复杂度稍高较低
适用场景通用,特别适合短距离计算适合大角度场景

10. 总结以及代码实现

Haversine 是计算球面大圆距离最稳定、最实用的数学公式之一。由于地球并非完美球形故使用此算法计算距离较远时误差较大,

为解决一般采用 Vincenty 算法(基于椭球模型计算其表面两点距离的迭代算法)

java实现的Haversine以及 Vincenty 算法

DistanceCalculateUtil.java

package com.bingbaihanji.app.springboot.utils.geo;

import lombok.extern.slf4j.Slf4j;

/**
 * 距离计算工具类
 * <p>
 * 实现 Haversine 与 Vincenty 公式,用于计算地球表面两点之间的最短距离(大地线距离)。
 * Vincenty 基于椭球模型,更精确;Haversine 基于球体模型,速度更快。
 * <p>
 * 作者:bingbaihanji
 * 日期:2025-11-25
 */
@Slf4j
public final class DistanceCalculateUtil {

    private DistanceCalculateUtil() {
        throw new IllegalStateException("禁止实例化");
    }

    /**
     * 地球平均半径(米)
     */
    private static final double EARTH_RADIUS = 6371000.0;

    /**
     * WGS-84 椭球参数
     */
    private static final double A = 6378137.0;                 // 长半轴
    private static final double B = 6356752.314245;            // 短半轴
    private static final double F = 1 / 298.257223563;         // 扁率

    /**
     * Vincenty 迭代最大次数
     */
    private static final int VINCENTY_MAX_ITER = 40;


    /**
     * 使用Haversine公式计算两个经纬度点之间的地球表面距离
     *
     * @param lat1 第一个点的纬度(十进制度数)
     * @param lon1 第一个点的经度(十进制度数)
     * @param lat2 第二个点的纬度(十进制度数)
     * @param lon2 第二个点的经度(十进制度数)
     * @return 两点之间的地球表面距离(单位:米)
     */
    public static double calculateHaversineDistance(double lat1, double lon1,
                                                    double lat2, double lon2) {

        validateLatLon(lat1, lon1);
        validateLatLon(lat2, lon2);

        // 将角度差转换为弧度
        double dLat = Math.toRadians(lat2 - lat1);
        double dLon = Math.toRadians(lon2 - lon1);

        // 将纬度转换为弧度
        double radLat1 = Math.toRadians(lat1);
        double radLat2 = Math.toRadians(lat2);

        // 计算半正矢公式中的正弦值
        double sinDLat = Math.sin(dLat / 2.0);
        double sinDLon = Math.sin(dLon / 2.0);

        // 应用Haversine公式计算球面距离
        double a = sinDLat * sinDLat +
                Math.cos(radLat1) * Math.cos(radLat2) *
                        sinDLon * sinDLon;

        // 计算中心角
        double c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

        // 计算实际距离(弧长乘以地球半径)
        double distance = EARTH_RADIUS * c;

        log.trace("Haversine距离计算完成:{} m", distance);
        return distance;
    }


    /**
     * 使用 Vincenty 公式计算两个经纬度点之间的椭球大地线距离(单位:米)。
     * <p>
     * 该方法基于 WGS-84 椭球模型,通过迭代求解椭球面上两点间的最短路径(大地线)长度,
     * 精度高但可能在某些极端情况下不收敛。
     * </p>
     *
     * @param lat1 第一个点的纬度(十进制度数)
     * @param lon1 第一个点的经度(十进制度数)
     * @param lat2 第二个点的纬度(十进制度数)
     * @param lon2 第二个点的经度(十进制度数)
     * @return 返回两点间沿地球表面的椭球大地线距离,单位为米;若算法未收敛则返回 NaN
     */
    public static double calculateVincentyDistance(double lat1, double lon1,
                                                   double lat2, double lon2) {
        validateLatLon(lat1, lon1);
        validateLatLon(lat2, lon2);

        // 将经度差转换为弧度
        double L = Math.toRadians(lon2 - lon1);

        // 计算降纬度(辅助球面坐标)
        double U1 = Math.atan((1 - F) * Math.tan(Math.toRadians(lat1)));
        double U2 = Math.atan((1 - F) * Math.tan(Math.toRadians(lat2)));

        double sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);
        double sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);

        double lambda = L;
        double lambdaPrev;
        double sinSigma, cosSigma, sigma;
        double cosSqAlpha = 0;
        double cos2SigmaM = 0;

        int iter = VINCENTY_MAX_ITER;

        // 迭代计算 lambda 直到收敛或达到最大迭代次数
        do {
            double sinLambda = Math.sin(lambda);
            double cosLambda = Math.cos(lambda);

            // 计算 sigma 的正弦值(球面三角中的边长)
            sinSigma = Math.sqrt(
                    (cosU2 * sinLambda) * (cosU2 * sinLambda) +
                            (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) *
                                    (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda)
            );

            // 若两点重合,则直接返回 0 距离
            if (sinSigma == 0) return 0;

            cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
            sigma = Math.atan2(sinSigma, cosSigma);

            // 计算方位角的正弦,并用于后续修正项计算
            double sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
            cosSqAlpha = 1 - sinAlpha * sinAlpha;

            // 处理赤道线情况下的特殊值
            if (cosSqAlpha == 0) {
                cos2SigmaM = 0;
            } else {
                cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;
            }

            // 计算修正系数 C
            double C = F / 16 * cosSqAlpha * (4 + F * (4 - 3 * cosSqAlpha));

            lambdaPrev = lambda;
            // 更新 lambda 值进行下一次迭代
            lambda = L + (1 - C) * F * sinAlpha *
                    (sigma + C * sinSigma * (cos2SigmaM +
                            C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM)));

        } while (Math.abs(lambda - lambdaPrev) > 1e-12 && --iter > 0);

        // 判断是否收敛,如未收敛记录警告并返回 NaN
        if (iter == 0) {
            log.warn("Vincenty公式未收敛");
            return Double.NaN;
        }

        // 计算椭球修正因子 u^2
        double uSq = cosSqAlpha * (A * A - B * B) / (B * B);
        // 计算大地线长度修正系数 A 和 B
        double bigA = 1 + uSq / 16384.0 *
                (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));

        double bigB = uSq / 1024.0 *
                (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));

        // 最终修正项 deltaSigma
        double deltaSigma = bigB * sinSigma *
                (cos2SigmaM + bigB / 4.0 *
                        (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) -
                                bigB / 6.0 * cos2SigmaM *
                                        (-3 + 4 * sinSigma * sinSigma) *
                                        (-3 + 4 * cos2SigmaM * cos2SigmaM)));

        // 返回最终的距离结果(米)
        return B * bigA * (sigma - deltaSigma);
    }


    /**
     * 验证给定的纬度和经度值是否有效。
     *
     * @param lat 纬度值
     * @param lon 经度值
     */
    private static void validateLatLon(double lat, double lon) {
        if (lat < -90 || lat > 90) {
            throw new IllegalArgumentException("纬度必须在 -90 到 90 之间");
        }
        if (lon < -180 || lon > 180) {
            throw new IllegalArgumentException("经度必须在 -180 到 180 之间");
        }
    }
}

人生不作安期生,醉入东海骑长鲸