地理距离计算方法详解:正交投影、Haversine与Vincenty公式

适用于无人机、ROV水下机器人、自动驾驶等导航系统开发

目录


一、概述与应用场景

在无人机、ROV(水下机器人)、自动驾驶等导航系统开发中,计算两个地理坐标点之间的距离是最基础也是最重要的功能之一。根据不同的应用场景和精度要求,我们需要选择不同的计算方法。

1.1 三种方法速览

方法 适用距离 计算速度 精度 典型应用
正交投影法 < 10km ★★★★★ 最快 0.1% 实时位置控制、悬停保持
Haversine公式 10km-1000km ★★★★ 快 0.3% 航线规划、地理围栏
Vincenty公式 任意距离 ★★ 较慢 0.5mm 精密测绘、大地测量

1.2 选择策略

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      距离计算方法选择                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   距离 < 10km?  ──Yes──►  正交投影法 (最快)                 │
│        │                                                    │
│        No                                                   │
│        ▼                                                    │
│   距离 < 100km? ──Yes──►  Haversine (平衡)                  │
│        │                                                    │
│        No                                                   │
│        ▼                                                    │
│   需要毫米级精度? ──Yes──►  Vincenty (最精确)               │
│        │                                                    │
│        No                                                   │
│        ▼                                                    │
│   Haversine (够用)                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、地球模型基础

2.1 三种地球模型

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        地球模型层次                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Level 3: WGS84椭球体 (Vincenty)                               │
│   ┌─────────────────────────────────┐                           │
│   │  a = 6378137.0 m (长半轴)       │                           │
│   │  b = 6356752.314245 m (短半轴)  │  ← 最精确                 │
│   │  f = 1/298.257223563 (扁率)     │                           │
│   └─────────────────────────────────┘                           │
│                                                                 │
│   Level 2: 正球体 (Haversine)                                   │
│   ┌─────────────────────────────────┐                           │
│   │  R = 6371000 m (平均半径)       │  ← 中等精度               │
│   │  球面三角学精确解                │                           │
│   └─────────────────────────────────┘                           │
│                                                                 │
│   Level 1: 局部平面 (Equirectangular)                           │
│   ┌─────────────────────────────────┐                           │
│   │  平面近似 + 纬度余弦修正         │  ← 计算最快               │
│   │  适用于小范围 (<10km)           │                           │
│   └─────────────────────────────────┘                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2.2 WGS84椭球体参数

WGS84(World Geodetic System 1984)是GPS系统使用的地球参考椭球体:

复制代码
                    北极
                      │
                      │ b = 6356.752 km
              ┌───────┼───────┐
             ╱        │        ╲
            ╱         │         ╲
           │          │          │
    ──────┼──────────┼──────────┼────── 赤道
           │          │          │
            ╲         │         ╱
             ╲        │        ╱
              └───────┼───────┘
                      │
                    南极
           ◄─── a = 6378.137 km ───►

核心参数:

  • 长半轴(赤道半径):a = 6,378,137.0 m
  • 短半轴(极半径):b = 6,356,752.314245 m
  • 扁率:f = (a-b)/a ≈ 1/298.257223563
  • 第一偏心率平方:e² = (a²-b²)/a² ≈ 0.00669438

2.3 经纬度与距离的关系

复制代码
在赤道:1°经度 ≈ 111.32 km
在60°纬度:1°经度 ≈ 111.32 km × cos(60°) ≈ 55.66 km
纬度方向始终:1°纬度 ≈ 110.57 km(略有变化)

这就是为什么经度方向的距离需要用余弦修正的原因。


三、方法1:正交投影法(近距离导航)

3.1 原理详解

正交投影法(Equirectangular Projection)将球面的一个小区域近似为平面,是最简单快速的距离计算方法。

核心思想:

  1. 纬度方向(南北) :地球子午线近似等长
    Δ y = R × Δ ϕ \Delta y = R \times \Delta\phi Δy=R×Δϕ

  2. 经度方向(东西) :纬线圈半径随纬度变化
    Δ x = R × Δ λ × cos ⁡ ( ϕ a v g ) \Delta x = R \times \Delta\lambda \times \cos(\phi_{avg}) Δx=R×Δλ×cos(ϕavg)

  3. 总距离 :勾股定理
    d = Δ x 2 + Δ y 2 d = \sqrt{\Delta x^2 + \Delta y^2} d=Δx2+Δy2

几何示意:

复制代码
          北
          ↑
    ┌─────┼─────┐
    │     │Δy   │
西 ─┼─────○─────┼─ 东
    │  Δx │     │  
    └─────┼─────┘
          ↓
          南

距离 d = √(Δx² + Δy²)

其中:
Δy = R × (lat2 - lat1)           ← 纬度差直接转距离
Δx = R × (lon2 - lon1) × cos(φ)  ← 经度差需要余弦修正

3.2 为什么需要余弦修正?

复制代码
       北极 (纬度90°)
         ╱╲
        ╱  ╲  ← 纬线圈半径 = 0
       ╱    ╲
      ╱      ╲
     ╱   60°  ╲ ← 纬线圈半径 = R×cos(60°) = 0.5R
    ╱          ╲
   ╱            ╲
  ╱      0°      ╲ ← 纬线圈半径 = R×cos(0°) = R (赤道)
  ────────────────

在不同纬度,相同经度差对应的实际距离不同:

  • 赤道:1°经度 ≈ 111 km
  • 纬度60°:1°经度 ≈ 111 × cos(60°) ≈ 55.5 km
  • 纬度80°:1°经度 ≈ 111 × cos(80°) ≈ 19.3 km

3.3 精度分析

距离范围 误差
< 1 km < 0.01%
1-10 km < 0.1%
10-100 km 0.1-1%
> 100 km > 1%(不推荐使用)

3.4 优势与适用场景

优势:

  • 计算速度最快(仅需基本三角函数)
  • 可直接得到NED/ENU坐标偏移量
  • 无需迭代,实时性好

适用场景:

  • 无人机/ROV位置保持控制(100Hz控制循环)
  • 近距离航点导航
  • 本地坐标系转换
  • 嵌入式系统实时计算

四、方法2:Haversine公式(中远距离)

4.1 问题背景

当距离较远时,平面近似误差变大,需要使用球面三角学。Haversine公式计算球面上两点间的大圆距离(Great-circle distance)。

4.2 Haversine函数

Haversine是"half-versed-sine"的缩写:

hav ( θ ) = sin ⁡ 2 ( θ 2 ) = 1 − cos ⁡ ( θ ) 2 \text{hav}(\theta) = \sin^2\left(\frac{\theta}{2}\right) = \frac{1 - \cos(\theta)}{2} hav(θ)=sin2(2θ)=21−cos(θ)

4.3 公式推导

球面余弦定理:
cos ⁡ ( c ) = sin ⁡ ( ϕ 1 ) sin ⁡ ( ϕ 2 ) + cos ⁡ ( ϕ 1 ) cos ⁡ ( ϕ 2 ) cos ⁡ ( Δ λ ) \cos(c) = \sin(\phi_1)\sin(\phi_2) + \cos(\phi_1)\cos(\phi_2)\cos(\Delta\lambda) cos(c)=sin(ϕ1)sin(ϕ2)+cos(ϕ1)cos(ϕ2)cos(Δλ)

问题 :当两点很近时, cos ⁡ ( c ) ≈ 1 \cos(c) \approx 1 cos(c)≈1, arccos ⁡ ( 1 − ϵ ) \arccos(1-\epsilon) arccos(1−ϵ) 的数值精度很差。

Haversine的改进

利用恒等式 cos ⁡ ( θ ) = 1 − 2 sin ⁡ 2 ( θ / 2 ) \cos(\theta) = 1 - 2\sin^2(\theta/2) cos(θ)=1−2sin2(θ/2),将球面余弦定理转换为:

sin ⁡ 2 ( c 2 ) = sin ⁡ 2 ( Δ ϕ 2 ) + cos ⁡ ( ϕ 1 ) cos ⁡ ( ϕ 2 ) sin ⁡ 2 ( Δ λ 2 ) \sin^2\left(\frac{c}{2}\right) = \sin^2\left(\frac{\Delta\phi}{2}\right) + \cos(\phi_1)\cos(\phi_2)\sin^2\left(\frac{\Delta\lambda}{2}\right) sin2(2c)=sin2(2Δϕ)+cos(ϕ1)cos(ϕ2)sin2(2Δλ)

求解步骤:

  1. 计算中间量a:
    a = sin ⁡ 2 ( Δ ϕ 2 ) + cos ⁡ ( ϕ 1 ) cos ⁡ ( ϕ 2 ) sin ⁡ 2 ( Δ λ 2 ) a = \sin^2\left(\frac{\Delta\phi}{2}\right) + \cos(\phi_1)\cos(\phi_2)\sin^2\left(\frac{\Delta\lambda}{2}\right) a=sin2(2Δϕ)+cos(ϕ1)cos(ϕ2)sin2(2Δλ)

  2. 计算角距离c:
    c = 2 ⋅ atan2 ( a , 1 − a ) c = 2 \cdot \text{atan2}\left(\sqrt{a}, \sqrt{1-a}\right) c=2⋅atan2(a ,1−a )

  3. 计算距离d:
    d = R ⋅ c d = R \cdot c d=R⋅c

4.4 为什么Haversine比球面余弦更好?

特性 球面余弦定理 Haversine公式
计算简洁度 更简洁 稍复杂
小距离精度 (浮点误差)
大距离精度
数值稳定性 一般 优秀

关键原因 :当 θ \theta θ 很小时:

  • cos ⁡ ( θ ) ≈ 1 \cos(\theta) \approx 1 cos(θ)≈1,精度损失严重
  • sin ⁡ 2 ( θ / 2 ) ≈ θ 2 / 4 \sin^2(\theta/2) \approx \theta^2/4 sin2(θ/2)≈θ2/4,仍保持良好精度

4.5 方位角计算

从点1到点2的起始方位角:

θ = atan2 ( sin ⁡ ( Δ λ ) cos ⁡ ( ϕ 2 ) , cos ⁡ ( ϕ 1 ) sin ⁡ ( ϕ 2 ) − sin ⁡ ( ϕ 1 ) cos ⁡ ( ϕ 2 ) cos ⁡ ( Δ λ ) ) \theta = \text{atan2}\left(\sin(\Delta\lambda)\cos(\phi_2), \cos(\phi_1)\sin(\phi_2) - \sin(\phi_1)\cos(\phi_2)\cos(\Delta\lambda)\right) θ=atan2(sin(Δλ)cos(ϕ2),cos(ϕ1)sin(ϕ2)−sin(ϕ1)cos(ϕ2)cos(Δλ))

4.6 适用场景

  • 航线规划(10km-1000km)
  • 地理围栏判断
  • 航点间距离计算
  • 任务规划系统

五、方法3:Vincenty公式(高精度测绘)

5.1 椭球体模型

Vincenty公式基于WGS84椭球体,是目前民用领域最精确的距离计算方法。

为什么需要椭球体模型?

地球不是正球体,而是一个扁球体:

  • 赤道半径比极半径大约21公里
  • 扁率约为1/298
  • 这导致在高精度应用中,球体模型会产生约0.3%的误差

5.2 Vincenty逆问题(已知两点求距离)

算法步骤:

  1. 计算归化纬度(Reduced Latitude)
    tan ⁡ ( U ) = ( 1 − f ) tan ⁡ ( ϕ ) \tan(U) = (1-f)\tan(\phi) tan(U)=(1−f)tan(ϕ)

  2. 迭代求解辅助角λ(通常4-5次迭代收敛)

  3. 计算测地线长度
    s = b ⋅ A ⋅ ( σ − Δ σ ) s = b \cdot A \cdot (\sigma - \Delta\sigma) s=b⋅A⋅(σ−Δσ)

迭代公式(简化版):

复制代码
初始化: λ = L (经度差)

迭代:
    sin(σ) = √[(cos(U₂)sin(λ))² + (cos(U₁)sin(U₂) - sin(U₁)cos(U₂)cos(λ))²]
    cos(σ) = sin(U₁)sin(U₂) + cos(U₁)cos(U₂)cos(λ)
    σ = atan2(sin(σ), cos(σ))
    
    sin(α) = cos(U₁)cos(U₂)sin(λ) / sin(σ)
    cos²(α) = 1 - sin²(α)
    
    cos(2σₘ) = cos(σ) - 2sin(U₁)sin(U₂)/cos²(α)
    
    C = f/16 × cos²(α) × [4 + f(4 - 3cos²(α))]
    
    λ' = L + (1-C) × f × sin(α) × [σ + C×sin(σ)×(cos(2σₘ) + C×cos(σ)×(-1 + 2cos²(2σₘ)))]
    
    如果 |λ' - λ| < 10⁻¹² 则收敛
    λ = λ'

5.3 收敛问题

对跖点问题:当两点接近地球直径两端(antipodal points)时,迭代可能不收敛。

解决方案

  • 检测迭代次数超过阈值
  • 回退到Haversine公式
  • 使用特殊处理算法

5.4 精度对比

距离 Haversine误差 Vincenty误差
100 km ~300 m ~0.5 mm
1000 km ~3 km ~0.5 mm
10000 km ~30 km ~0.5 mm

5.5 适用场景

  • 大地测量
  • 精密测绘
  • 科学研究
  • 长距离航线精确计算
  • 需要毫米级精度的应用

六、三种方法对比与选择

6.1 性能测试结果

在Intel i7处理器上的测试结果(100,000次计算):

方法 单次耗时 相对速度
正交投影法 0.010 μs 45倍
Haversine 0.055 μs 8倍
Vincenty 0.448 μs 1倍(基准)

6.2 精度测试结果

测试场景 距离 正交投影误差 Haversine误差 Vincenty误差
北京天安门到故宫 845m 0.10% 0.10% 0.04%
北京到上海 1068km 0.01% 0.04% 0.18%
纽约到伦敦 5570km 4.49% 0.00% 0.27%
洛杉矶到东京 8815km 166% 0.05% 0.27%

6.3 选择建议

复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                        三种方法选择指南                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  1. 正交投影法 (Equirectangular)                                        │
│     适用场景: 无人机/ROV近距离导航 (<10km)                              │
│     优点: 计算速度最快,实现简单,可直接得到NED/ENU偏移                 │
│     精度: 10km内误差 < 0.1%                                             │
│     推荐: 实时控制循环、嵌入式系统、位置保持                            │
│                                                                         │
│  2. Haversine公式                                                       │
│     适用场景: 中远距离航线规划 (10km-1000km)                            │
│     优点: 数值稳定,无需迭代,适用于任意距离                            │
│     精度: 约0.3%误差(忽略椭球扁率)                                      │
│     推荐: 航线规划、距离估算、地理围栏                                  │
│                                                                         │
│  3. Vincenty公式                                                        │
│     适用场景: 高精度测绘、大地测量 (任意距离)                           │
│     优点: 最高精度,考虑WGS84椭球体                                     │
│     精度: 0.5mm级别                                                     │
│     推荐: 精密定位、测量测绘、科研计算                                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

6.4 ROV/无人机开发建议

功能模块 推荐方法 原因
位置控制循环(100Hz) 正交投影 计算延迟最小
航点导航 Haversine 精度与速度平衡
任务规划 Vincenty 对实时性要求低
地理围栏 正交投影/Haversine 取决于围栏大小
DVL里程计融合 正交投影 短距离增量计算

七、完整工程代码

7.1 头文件 (geo_distance.h)

c 复制代码
/**
 * @file geo_distance.h
 * @brief 地理距离计算库 - 支持三种精度等级的距离计算
 * 
 * 适用场景:
 * - 正交投影法 (Equirectangular): 无人机/ROV近距离导航 (<10km)
 * - Haversine公式: 中远距离航线规划 (10km - 1000km)
 * - Vincenty公式: 高精度测绘和大地测量 (任意距离)
 */

#ifndef GEO_DISTANCE_H
#define GEO_DISTANCE_H

#include <stdbool.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

/*===========================================================================*/
/*                              常量定义                                      */
/*===========================================================================*/

/** WGS84 椭球体参数 */
#define WGS84_A             6378137.0           /**< 长半轴 (赤道半径) 单位:米 */
#define WGS84_B             6356752.314245      /**< 短半轴 (极半径) 单位:米 */
#define WGS84_F             (1.0 / 298.257223563) /**< 扁率 */
#define WGS84_E2            0.00669437999014    /**< 第一偏心率的平方 */
#define WGS84_EP2           0.00673949674228    /**< 第二偏心率的平方 */

/** 简化球体模型参数 */
#define EARTH_RADIUS_MEAN   6371000.0           /**< 地球平均半径 单位:米 */
#define EARTH_RADIUS_EQUATOR 6378137.0          /**< 赤道半径 单位:米 */

/** 数学常量 */
#define GEO_PI              3.14159265358979323846
#define DEG_TO_RAD          (GEO_PI / 180.0)
#define RAD_TO_DEG          (180.0 / GEO_PI)

/** Vincenty迭代参数 */
#define VINCENTY_MAX_ITERATIONS  100            /**< 最大迭代次数 */
#define VINCENTY_CONVERGENCE     1e-12          /**< 收敛阈值 */

/*===========================================================================*/
/*                              数据结构                                      */
/*===========================================================================*/

/**
 * @brief 地理坐标点 (WGS84)
 */
typedef struct {
    double latitude;    /**< 纬度,单位:度,范围 [-90, 90] */
    double longitude;   /**< 经度,单位:度,范围 [-180, 180] */
    double altitude;    /**< 高度,单位:米,相对于WGS84椭球面 */
} GeoPoint;

/**
 * @brief NED坐标系偏移量 (North-East-Down)
 */
typedef struct {
    double north;       /**< 北向偏移,单位:米 */
    double east;        /**< 东向偏移,单位:米 */
    double down;        /**< 下向偏移,单位:米 (高度差的负值) */
} NEDOffset;

/**
 * @brief ENU坐标系偏移量 (East-North-Up)
 */
typedef struct {
    double east;        /**< 东向偏移,单位:米 */
    double north;       /**< 北向偏移,单位:米 */
    double up;          /**< 上向偏移,单位:米 */
} ENUOffset;

/**
 * @brief 距离和方位角结果
 */
typedef struct {
    double distance;        /**< 距离,单位:米 */
    double initial_bearing; /**< 起始方位角,单位:度,北为0,顺时针 [0, 360) */
    double final_bearing;   /**< 终点方位角,单位:度 */
} GeoResult;

/**
 * @brief Vincenty计算的详细结果
 */
typedef struct {
    double distance;        /**< 椭球面距离,单位:米 */
    double initial_bearing; /**< 起始方位角,单位:度 */
    double final_bearing;   /**< 终点方位角,单位:度 */
    int iterations;         /**< 迭代次数 */
    bool converged;         /**< 是否收敛 */
} VincentyResult;

/**
 * @brief 计算方法枚举
 */
typedef enum {
    GEO_METHOD_EQUIRECTANGULAR = 0, /**< 正交投影法 (最快,适用<10km) */
    GEO_METHOD_HAVERSINE,           /**< Haversine公式 (中等精度) */
    GEO_METHOD_VINCENTY,            /**< Vincenty公式 (最精确) */
    GEO_METHOD_AUTO                 /**< 自动选择 (根据距离) */
} GeoMethod;

/*===========================================================================*/
/*                          方法1: 正交投影法                                  */
/*===========================================================================*/

/**
 * @brief 使用正交投影法计算两点间距离
 * 
 * @param lat1 起点纬度 (度)
 * @param lon1 起点经度 (度)
 * @param lat2 终点纬度 (度)
 * @param lon2 终点经度 (度)
 * @return 距离 (米)
 */
double geo_distance_equirectangular(double lat1, double lon1, 
                                     double lat2, double lon2);

/**
 * @brief 正交投影法计算NED偏移量
 */
NEDOffset geo_to_ned_equirectangular(const GeoPoint *from, const GeoPoint *to);

/**
 * @brief 正交投影法计算ENU偏移量
 */
ENUOffset geo_to_enu_equirectangular(const GeoPoint *from, const GeoPoint *to);

/**
 * @brief 从NED偏移量反算经纬度
 */
void ned_to_geo_equirectangular(const GeoPoint *ref, const NEDOffset *offset, 
                                 GeoPoint *result);

/**
 * @brief 从ENU偏移量反算经纬度
 */
void enu_to_geo_equirectangular(const GeoPoint *ref, const ENUOffset *offset,
                                 GeoPoint *result);

/*===========================================================================*/
/*                          方法2: Haversine公式                               */
/*===========================================================================*/

/**
 * @brief 使用Haversine公式计算两点间大圆距离
 */
double geo_distance_haversine(double lat1, double lon1, 
                               double lat2, double lon2);

/**
 * @brief Haversine公式计算距离和方位角
 */
GeoResult geo_distance_bearing_haversine(double lat1, double lon1,
                                          double lat2, double lon2);

/**
 * @brief 根据起点、方位角和距离计算终点
 */
void geo_destination_haversine(double lat1, double lon1,
                                double bearing, double distance,
                                double *lat2, double *lon2);

/**
 * @brief 计算大圆航线的中点
 */
void geo_midpoint_haversine(double lat1, double lon1,
                             double lat2, double lon2,
                             double *lat_mid, double *lon_mid);

/*===========================================================================*/
/*                          方法3: Vincenty公式                                */
/*===========================================================================*/

/**
 * @brief 使用Vincenty公式计算椭球面上两点间距离
 */
double geo_distance_vincenty(double lat1, double lon1,
                              double lat2, double lon2);

/**
 * @brief Vincenty公式完整计算 (逆问题)
 */
VincentyResult geo_vincenty_inverse(double lat1, double lon1,
                                     double lat2, double lon2);

/**
 * @brief Vincenty直接问题:根据起点、方位角和距离计算终点
 */
void geo_vincenty_direct(double lat1, double lon1,
                          double bearing, double distance,
                          double *lat2, double *lon2, double *final_bearing);

/*===========================================================================*/
/*                            通用接口函数                                     */
/*===========================================================================*/

/**
 * @brief 自动选择最优方法计算距离
 */
double geo_distance_auto(double lat1, double lon1, double lat2, double lon2);

/**
 * @brief 使用指定方法计算距离
 */
double geo_distance(double lat1, double lon1, double lat2, double lon2,
                    GeoMethod method);

/**
 * @brief 计算方位角
 */
double geo_bearing(double lat1, double lon1, double lat2, double lon2);

/**
 * @brief 检查坐标是否有效
 */
bool geo_is_valid_coordinate(double lat, double lon);

/**
 * @brief 归一化经度到 [-180, 180]
 */
double geo_normalize_longitude(double lon);

/**
 * @brief 归一化方位角到 [0, 360)
 */
double geo_normalize_bearing(double bearing);

/*===========================================================================*/
/*                            应用层函数                                       */
/*===========================================================================*/

/**
 * @brief 判断点是否在圆形围栏内
 */
bool geo_is_inside_circular_fence(double lat, double lon,
                                   double fence_lat, double fence_lon,
                                   double fence_radius);

/**
 * @brief 判断点是否在多边形围栏内
 */
bool geo_is_inside_polygon(double lat, double lon,
                            const double *polygon_lats,
                            const double *polygon_lons,
                            int num_points);

/**
 * @brief 计算点到线段的最短距离
 */
double geo_distance_to_segment(double lat, double lon,
                                double lat1, double lon1,
                                double lat2, double lon2);

/**
 * @brief 计算航线上的插值点
 */
void geo_interpolate(double lat1, double lon1,
                      double lat2, double lon2,
                      double fraction,
                      double *lat_out, double *lon_out);

/**
 * @brief 计算Cross-Track Distance (偏离航线距离)
 */
double geo_cross_track_distance(double lat, double lon,
                                 double lat1, double lon1,
                                 double lat2, double lon2);

/**
 * @brief 计算Along-Track Distance (沿航线距离)
 */
double geo_along_track_distance(double lat, double lon,
                                 double lat1, double lon1,
                                 double lat2, double lon2);

#ifdef __cplusplus
}
#endif

#endif /* GEO_DISTANCE_H */

7.2 实现文件 (geo_distance.c)

c 复制代码
/**
 * @file geo_distance.c
 * @brief 地理距离计算库实现
 */

#include "geo_distance.h"
#include <math.h>
#include <float.h>
#include <stddef.h>

/*===========================================================================*/
/*                              内部辅助函数                                   */
/*===========================================================================*/

static inline double to_radians(double degrees)
{
    return degrees * DEG_TO_RAD;
}

static inline double to_degrees(double radians)
{
    return radians * RAD_TO_DEG;
}

static inline double sq(double x)
{
    return x * x;
}

static inline double safe_asin(double x)
{
    if (x > 1.0) x = 1.0;
    if (x < -1.0) x = -1.0;
    return asin(x);
}

static inline double safe_acos(double x)
{
    if (x > 1.0) x = 1.0;
    if (x < -1.0) x = -1.0;
    return acos(x);
}

/*===========================================================================*/
/*                      方法1: 正交投影法 (Equirectangular)                     */
/*===========================================================================*/

double geo_distance_equirectangular(double lat1, double lon1,
                                     double lat2, double lon2)
{
    double phi1 = to_radians(lat1);
    double phi2 = to_radians(lat2);
    double delta_phi = to_radians(lat2 - lat1);
    double delta_lambda = to_radians(lon2 - lon1);
    
    /* 使用平均纬度的余弦作为修正系数 */
    double cos_phi_avg = cos((phi1 + phi2) / 2.0);
    
    /* 计算平面坐标差 */
    double dx = EARTH_RADIUS_MEAN * delta_lambda * cos_phi_avg;
    double dy = EARTH_RADIUS_MEAN * delta_phi;
    
    return sqrt(dx * dx + dy * dy);
}

NEDOffset geo_to_ned_equirectangular(const GeoPoint *from, const GeoPoint *to)
{
    NEDOffset offset;
    
    double phi1 = to_radians(from->latitude);
    double phi2 = to_radians(to->latitude);
    double delta_phi = to_radians(to->latitude - from->latitude);
    double delta_lambda = to_radians(to->longitude - from->longitude);
    
    double cos_phi_avg = cos((phi1 + phi2) / 2.0);
    
    offset.north = EARTH_RADIUS_MEAN * delta_phi;
    offset.east = EARTH_RADIUS_MEAN * delta_lambda * cos_phi_avg;
    offset.down = from->altitude - to->altitude;
    
    return offset;
}

ENUOffset geo_to_enu_equirectangular(const GeoPoint *from, const GeoPoint *to)
{
    NEDOffset ned = geo_to_ned_equirectangular(from, to);
    ENUOffset enu;
    
    enu.east = ned.east;
    enu.north = ned.north;
    enu.up = -ned.down;
    
    return enu;
}

void ned_to_geo_equirectangular(const GeoPoint *ref, const NEDOffset *offset,
                                 GeoPoint *result)
{
    double phi_ref = to_radians(ref->latitude);
    
    double delta_phi = offset->north / EARTH_RADIUS_MEAN;
    result->latitude = ref->latitude + to_degrees(delta_phi);
    
    double phi_avg = phi_ref + delta_phi / 2.0;
    double cos_phi_avg = cos(phi_avg);
    
    if (cos_phi_avg > 1e-10) {
        double delta_lambda = offset->east / (EARTH_RADIUS_MEAN * cos_phi_avg);
        result->longitude = ref->longitude + to_degrees(delta_lambda);
    } else {
        result->longitude = ref->longitude;
    }
    
    result->altitude = ref->altitude - offset->down;
}

void enu_to_geo_equirectangular(const GeoPoint *ref, const ENUOffset *offset,
                                 GeoPoint *result)
{
    NEDOffset ned;
    ned.north = offset->north;
    ned.east = offset->east;
    ned.down = -offset->up;
    
    ned_to_geo_equirectangular(ref, &ned, result);
}

/*===========================================================================*/
/*                         方法2: Haversine公式                                */
/*===========================================================================*/

double geo_distance_haversine(double lat1, double lon1,
                               double lat2, double lon2)
{
    double phi1 = to_radians(lat1);
    double phi2 = to_radians(lat2);
    double delta_phi = to_radians(lat2 - lat1);
    double delta_lambda = to_radians(lon2 - lon1);
    
    double sin_dphi_2 = sin(delta_phi / 2.0);
    double sin_dlam_2 = sin(delta_lambda / 2.0);
    
    double a = sin_dphi_2 * sin_dphi_2 +
               cos(phi1) * cos(phi2) * sin_dlam_2 * sin_dlam_2;
    
    double c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a));
    
    return EARTH_RADIUS_MEAN * c;
}

GeoResult geo_distance_bearing_haversine(double lat1, double lon1,
                                          double lat2, double lon2)
{
    GeoResult result;
    
    double phi1 = to_radians(lat1);
    double phi2 = to_radians(lat2);
    double delta_phi = to_radians(lat2 - lat1);
    double delta_lambda = to_radians(lon2 - lon1);
    
    /* 距离计算 */
    double sin_dphi_2 = sin(delta_phi / 2.0);
    double sin_dlam_2 = sin(delta_lambda / 2.0);
    
    double a = sin_dphi_2 * sin_dphi_2 +
               cos(phi1) * cos(phi2) * sin_dlam_2 * sin_dlam_2;
    double c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a));
    
    result.distance = EARTH_RADIUS_MEAN * c;
    
    /* 方位角计算 */
    double y = sin(delta_lambda) * cos(phi2);
    double x = cos(phi1) * sin(phi2) - sin(phi1) * cos(phi2) * cos(delta_lambda);
    
    result.initial_bearing = to_degrees(atan2(y, x));
    result.initial_bearing = geo_normalize_bearing(result.initial_bearing);
    
    /* 终点方位角 */
    y = sin(-delta_lambda) * cos(phi1);
    x = cos(phi2) * sin(phi1) - sin(phi2) * cos(phi1) * cos(-delta_lambda);
    
    result.final_bearing = to_degrees(atan2(y, x));
    result.final_bearing = geo_normalize_bearing(result.final_bearing + 180.0);
    
    return result;
}

void geo_destination_haversine(double lat1, double lon1,
                                double bearing, double distance,
                                double *lat2, double *lon2)
{
    double phi1 = to_radians(lat1);
    double lambda1 = to_radians(lon1);
    double theta = to_radians(bearing);
    double delta = distance / EARTH_RADIUS_MEAN;
    
    double sin_phi2 = sin(phi1) * cos(delta) + cos(phi1) * sin(delta) * cos(theta);
    double phi2 = safe_asin(sin_phi2);
    
    double y = sin(theta) * sin(delta) * cos(phi1);
    double x = cos(delta) - sin(phi1) * sin(phi2);
    double lambda2 = lambda1 + atan2(y, x);
    
    *lat2 = to_degrees(phi2);
    *lon2 = geo_normalize_longitude(to_degrees(lambda2));
}

void geo_midpoint_haversine(double lat1, double lon1,
                             double lat2, double lon2,
                             double *lat_mid, double *lon_mid)
{
    double phi1 = to_radians(lat1);
    double phi2 = to_radians(lat2);
    double lambda1 = to_radians(lon1);
    double delta_lambda = to_radians(lon2 - lon1);
    
    double Bx = cos(phi2) * cos(delta_lambda);
    double By = cos(phi2) * sin(delta_lambda);
    
    double phi_mid = atan2(sin(phi1) + sin(phi2),
                           sqrt((cos(phi1) + Bx) * (cos(phi1) + Bx) + By * By));
    double lambda_mid = lambda1 + atan2(By, cos(phi1) + Bx);
    
    *lat_mid = to_degrees(phi_mid);
    *lon_mid = geo_normalize_longitude(to_degrees(lambda_mid));
}

/*===========================================================================*/
/*                          方法3: Vincenty公式                                */
/*===========================================================================*/

double geo_distance_vincenty(double lat1, double lon1,
                              double lat2, double lon2)
{
    VincentyResult result = geo_vincenty_inverse(lat1, lon1, lat2, lon2);
    return result.distance;
}

VincentyResult geo_vincenty_inverse(double lat1, double lon1,
                                     double lat2, double lon2)
{
    VincentyResult result;
    result.converged = false;
    result.iterations = 0;
    result.distance = NAN;
    result.initial_bearing = NAN;
    result.final_bearing = NAN;
    
    const double a = WGS84_A;
    const double b = WGS84_B;
    const double f = WGS84_F;
    
    double phi1 = to_radians(lat1);
    double phi2 = to_radians(lat2);
    double L = to_radians(lon2 - lon1);
    
    /* 特殊情况:同一点 */
    if (fabs(lat1 - lat2) < 1e-12 && fabs(lon1 - lon2) < 1e-12) {
        result.distance = 0.0;
        result.initial_bearing = 0.0;
        result.final_bearing = 0.0;
        result.converged = true;
        return result;
    }
    
    /* 归化纬度 */
    double tan_U1 = (1.0 - f) * tan(phi1);
    double tan_U2 = (1.0 - f) * tan(phi2);
    
    double cos_U1 = 1.0 / sqrt(1.0 + tan_U1 * tan_U1);
    double sin_U1 = tan_U1 * cos_U1;
    double cos_U2 = 1.0 / sqrt(1.0 + tan_U2 * tan_U2);
    double sin_U2 = tan_U2 * cos_U2;
    
    double lambda = L;
    double lambda_prev;
    double sin_lambda, cos_lambda;
    double sin_sigma, cos_sigma, sigma;
    double sin_alpha, cos_sq_alpha;
    double cos_2sigma_m;
    double C;
    
    int iter;
    for (iter = 0; iter < VINCENTY_MAX_ITERATIONS; iter++) {
        sin_lambda = sin(lambda);
        cos_lambda = cos(lambda);
        
        double term1 = cos_U2 * sin_lambda;
        double term2 = cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_lambda;
        
        sin_sigma = sqrt(term1 * term1 + term2 * term2);
        
        if (sin_sigma < 1e-12) {
            result.distance = 0.0;
            result.initial_bearing = 0.0;
            result.final_bearing = 0.0;
            result.converged = true;
            return result;
        }
        
        cos_sigma = sin_U1 * sin_U2 + cos_U1 * cos_U2 * cos_lambda;
        sigma = atan2(sin_sigma, cos_sigma);
        
        sin_alpha = cos_U1 * cos_U2 * sin_lambda / sin_sigma;
        cos_sq_alpha = 1.0 - sin_alpha * sin_alpha;
        
        if (cos_sq_alpha < 1e-12) {
            cos_2sigma_m = 0.0;
        } else {
            cos_2sigma_m = cos_sigma - 2.0 * sin_U1 * sin_U2 / cos_sq_alpha;
        }
        
        C = f / 16.0 * cos_sq_alpha * (4.0 + f * (4.0 - 3.0 * cos_sq_alpha));
        
        lambda_prev = lambda;
        lambda = L + (1.0 - C) * f * sin_alpha * 
                 (sigma + C * sin_sigma * 
                  (cos_2sigma_m + C * cos_sigma * 
                   (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)));
        
        if (fabs(lambda - lambda_prev) < VINCENTY_CONVERGENCE) {
            result.converged = true;
            break;
        }
    }
    
    result.iterations = iter + 1;
    
    if (!result.converged) {
        /* 不收敛,回退到Haversine */
        result.distance = geo_distance_haversine(lat1, lon1, lat2, lon2);
        GeoResult hav = geo_distance_bearing_haversine(lat1, lon1, lat2, lon2);
        result.initial_bearing = hav.initial_bearing;
        result.final_bearing = hav.final_bearing;
        return result;
    }
    
    /* 计算距离 */
    double u_sq = cos_sq_alpha * (a * a - b * b) / (b * b);
    
    double A = 1.0 + u_sq / 16384.0 * 
               (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
    
    double B = u_sq / 1024.0 * 
               (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
    
    double cos_2sigma_m_sq = cos_2sigma_m * cos_2sigma_m;
    
    double delta_sigma = B * sin_sigma * 
        (cos_2sigma_m + B / 4.0 * 
         (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m_sq) -
          B / 6.0 * cos_2sigma_m * (-3.0 + 4.0 * sin_sigma * sin_sigma) *
          (-3.0 + 4.0 * cos_2sigma_m_sq)));
    
    result.distance = b * A * (sigma - delta_sigma);
    
    /* 方位角 */
    double alpha1 = atan2(cos_U2 * sin_lambda,
                          cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_lambda);
    result.initial_bearing = geo_normalize_bearing(to_degrees(alpha1));
    
    double alpha2 = atan2(cos_U1 * sin_lambda,
                          -sin_U1 * cos_U2 + cos_U1 * sin_U2 * cos_lambda);
    result.final_bearing = geo_normalize_bearing(to_degrees(alpha2 + GEO_PI));
    
    return result;
}

void geo_vincenty_direct(double lat1, double lon1,
                          double bearing, double distance,
                          double *lat2, double *lon2, double *final_bearing)
{
    const double a = WGS84_A;
    const double b = WGS84_B;
    const double f = WGS84_F;
    
    double phi1 = to_radians(lat1);
    double alpha1 = to_radians(bearing);
    double s = distance;
    
    double tan_U1 = (1.0 - f) * tan(phi1);
    double cos_U1 = 1.0 / sqrt(1.0 + tan_U1 * tan_U1);
    double sin_U1 = tan_U1 * cos_U1;
    
    double sigma1 = atan2(tan_U1, cos(alpha1));
    double sin_alpha = cos_U1 * sin(alpha1);
    double cos_sq_alpha = 1.0 - sin_alpha * sin_alpha;
    
    double u_sq = cos_sq_alpha * (a * a - b * b) / (b * b);
    
    double A = 1.0 + u_sq / 16384.0 * 
               (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
    
    double B = u_sq / 1024.0 * 
               (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
    
    double sigma = s / (b * A);
    double sigma_prev;
    double sin_sigma, cos_sigma;
    double cos_2sigma_m;
    double delta_sigma;
    
    for (int iter = 0; iter < VINCENTY_MAX_ITERATIONS; iter++) {
        cos_2sigma_m = cos(2.0 * sigma1 + sigma);
        sin_sigma = sin(sigma);
        cos_sigma = cos(sigma);
        
        double cos_2sigma_m_sq = cos_2sigma_m * cos_2sigma_m;
        
        delta_sigma = B * sin_sigma * 
            (cos_2sigma_m + B / 4.0 * 
             (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m_sq) -
              B / 6.0 * cos_2sigma_m * (-3.0 + 4.0 * sin_sigma * sin_sigma) *
              (-3.0 + 4.0 * cos_2sigma_m_sq)));
        
        sigma_prev = sigma;
        sigma = s / (b * A) + delta_sigma;
        
        if (fabs(sigma - sigma_prev) < VINCENTY_CONVERGENCE) {
            break;
        }
    }
    
    sin_sigma = sin(sigma);
    cos_sigma = cos(sigma);
    cos_2sigma_m = cos(2.0 * sigma1 + sigma);
    
    double term1 = sin_U1 * cos_sigma + cos_U1 * sin_sigma * cos(alpha1);
    double term2 = (1.0 - f) * sqrt(sin_alpha * sin_alpha + 
                   sq(sin_U1 * sin_sigma - cos_U1 * cos_sigma * cos(alpha1)));
    
    double phi2 = atan2(term1, term2);
    *lat2 = to_degrees(phi2);
    
    double lambda = atan2(sin_sigma * sin(alpha1),
                          cos_U1 * cos_sigma - sin_U1 * sin_sigma * cos(alpha1));
    
    double C = f / 16.0 * cos_sq_alpha * (4.0 + f * (4.0 - 3.0 * cos_sq_alpha));
    
    double L = lambda - (1.0 - C) * f * sin_alpha *
               (sigma + C * sin_sigma *
                (cos_2sigma_m + C * cos_sigma * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)));
    
    *lon2 = geo_normalize_longitude(lon1 + to_degrees(L));
    
    if (final_bearing != NULL) {
        double alpha2 = atan2(sin_alpha,
                              -sin_U1 * sin_sigma + cos_U1 * cos_sigma * cos(alpha1));
        *final_bearing = geo_normalize_bearing(to_degrees(alpha2));
    }
}

/*===========================================================================*/
/*                            通用接口函数                                     */
/*===========================================================================*/

double geo_distance_auto(double lat1, double lon1, double lat2, double lon2)
{
    double approx_dist = geo_distance_equirectangular(lat1, lon1, lat2, lon2);
    
    if (approx_dist < 10000.0) {
        return approx_dist;
    } else if (approx_dist < 100000.0) {
        return geo_distance_haversine(lat1, lon1, lat2, lon2);
    } else {
        return geo_distance_vincenty(lat1, lon1, lat2, lon2);
    }
}

double geo_distance(double lat1, double lon1, double lat2, double lon2,
                    GeoMethod method)
{
    switch (method) {
        case GEO_METHOD_EQUIRECTANGULAR:
            return geo_distance_equirectangular(lat1, lon1, lat2, lon2);
        case GEO_METHOD_HAVERSINE:
            return geo_distance_haversine(lat1, lon1, lat2, lon2);
        case GEO_METHOD_VINCENTY:
            return geo_distance_vincenty(lat1, lon1, lat2, lon2);
        case GEO_METHOD_AUTO:
        default:
            return geo_distance_auto(lat1, lon1, lat2, lon2);
    }
}

double geo_bearing(double lat1, double lon1, double lat2, double lon2)
{
    double phi1 = to_radians(lat1);
    double phi2 = to_radians(lat2);
    double delta_lambda = to_radians(lon2 - lon1);
    
    double y = sin(delta_lambda) * cos(phi2);
    double x = cos(phi1) * sin(phi2) - sin(phi1) * cos(phi2) * cos(delta_lambda);
    
    double bearing = to_degrees(atan2(y, x));
    return geo_normalize_bearing(bearing);
}

bool geo_is_valid_coordinate(double lat, double lon)
{
    return (lat >= -90.0 && lat <= 90.0 && 
            lon >= -180.0 && lon <= 180.0 &&
            isfinite(lat) && isfinite(lon));
}

double geo_normalize_longitude(double lon)
{
    while (lon > 180.0) lon -= 360.0;
    while (lon < -180.0) lon += 360.0;
    return lon;
}

double geo_normalize_bearing(double bearing)
{
    bearing = fmod(bearing, 360.0);
    if (bearing < 0.0) bearing += 360.0;
    return bearing;
}

/*===========================================================================*/
/*                            应用层函数                                       */
/*===========================================================================*/

bool geo_is_inside_circular_fence(double lat, double lon,
                                   double fence_lat, double fence_lon,
                                   double fence_radius)
{
    double distance = geo_distance_equirectangular(lat, lon, fence_lat, fence_lon);
    return distance <= fence_radius;
}

bool geo_is_inside_polygon(double lat, double lon,
                            const double *polygon_lats,
                            const double *polygon_lons,
                            int num_points)
{
    bool inside = false;
    
    int j = num_points - 1;
    for (int i = 0; i < num_points; i++) {
        double xi = polygon_lons[i], yi = polygon_lats[i];
        double xj = polygon_lons[j], yj = polygon_lats[j];
        
        if (((yi > lat) != (yj > lat)) &&
            (lon < (xj - xi) * (lat - yi) / (yj - yi) + xi)) {
            inside = !inside;
        }
        j = i;
    }
    
    return inside;
}

double geo_distance_to_segment(double lat, double lon,
                                double lat1, double lon1,
                                double lat2, double lon2)
{
    GeoPoint ref = {lat1, lon1, 0};
    GeoPoint point = {lat, lon, 0};
    GeoPoint end = {lat2, lon2, 0};
    
    NEDOffset p = geo_to_ned_equirectangular(&ref, &point);
    NEDOffset e = geo_to_ned_equirectangular(&ref, &end);
    
    double seg_len_sq = e.north * e.north + e.east * e.east;
    
    if (seg_len_sq < 1e-10) {
        return sqrt(p.north * p.north + p.east * p.east);
    }
    
    double t = (p.north * e.north + p.east * e.east) / seg_len_sq;
    if (t < 0.0) t = 0.0;
    if (t > 1.0) t = 1.0;
    
    double nearest_n = t * e.north;
    double nearest_e = t * e.east;
    
    double dn = p.north - nearest_n;
    double de = p.east - nearest_e;
    
    return sqrt(dn * dn + de * de);
}

void geo_interpolate(double lat1, double lon1,
                      double lat2, double lon2,
                      double fraction,
                      double *lat_out, double *lon_out)
{
    double phi1 = to_radians(lat1);
    double phi2 = to_radians(lat2);
    double lambda1 = to_radians(lon1);
    double delta_lambda = to_radians(lon2 - lon1);
    
    double cos_d = sin(phi1) * sin(phi2) + cos(phi1) * cos(phi2) * cos(delta_lambda);
    double d = safe_acos(cos_d);
    
    if (d < 1e-10) {
        *lat_out = lat1;
        *lon_out = lon1;
        return;
    }
    
    double sin_d = sin(d);
    double a = sin((1.0 - fraction) * d) / sin_d;
    double b = sin(fraction * d) / sin_d;
    
    double x = a * cos(phi1) * cos(lambda1) + b * cos(phi2) * cos(lambda1 + delta_lambda);
    double y = a * cos(phi1) * sin(lambda1) + b * cos(phi2) * sin(lambda1 + delta_lambda);
    double z = a * sin(phi1) + b * sin(phi2);
    
    *lat_out = to_degrees(atan2(z, sqrt(x * x + y * y)));
    *lon_out = to_degrees(atan2(y, x));
}

double geo_cross_track_distance(double lat, double lon,
                                 double lat1, double lon1,
                                 double lat2, double lon2)
{
    double d13 = geo_distance_haversine(lat1, lon1, lat, lon) / EARTH_RADIUS_MEAN;
    double theta13 = to_radians(geo_bearing(lat1, lon1, lat, lon));
    double theta12 = to_radians(geo_bearing(lat1, lon1, lat2, lon2));
    
    double dxt = asin(sin(d13) * sin(theta13 - theta12)) * EARTH_RADIUS_MEAN;
    
    return dxt;
}

double geo_along_track_distance(double lat, double lon,
                                 double lat1, double lon1,
                                 double lat2, double lon2)
{
    double d13 = geo_distance_haversine(lat1, lon1, lat, lon) / EARTH_RADIUS_MEAN;
    double dxt = geo_cross_track_distance(lat, lon, lat1, lon1, lat2, lon2) / EARTH_RADIUS_MEAN;
    
    double dat = acos(cos(d13) / cos(dxt)) * EARTH_RADIUS_MEAN;
    
    return dat;
}

7.3 Makefile

makefile 复制代码
# Makefile for Geo Distance Library

CC = gcc
CFLAGS = -Wall -Wextra -O2 -std=c99
LDFLAGS = -lm

TARGET = test_geo_distance
OBJS = geo_distance.o test_geo_distance.o

LIB_STATIC = libgeo_distance.a

.PHONY: all clean test lib

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

geo_distance.o: geo_distance.c geo_distance.h
	$(CC) $(CFLAGS) -c $< -o $@

test_geo_distance.o: test_geo_distance.c geo_distance.h
	$(CC) $(CFLAGS) -c $< -o $@

lib: $(LIB_STATIC)

$(LIB_STATIC): geo_distance.o
	ar rcs $@ $^

test: $(TARGET)
	./$(TARGET)

clean:
	rm -f $(TARGET) $(OBJS) $(LIB_STATIC)

八、实际应用示例

8.1 ROV位置控制

c 复制代码
#include "geo_distance.h"
#include <stdio.h>

/* ROV位置控制 - 使用正交投影法获取NED误差 */
void rov_position_control(void)
{
    /* 当前GPS/USBL位置 */
    GeoPoint current = {
        .latitude = 22.5431,
        .longitude = 113.9360,
        .altitude = -50.0  /* 水下50米 */
    };
    
    /* 目标航点 */
    GeoPoint target = {
        .latitude = 22.5440,
        .longitude = 113.9370,
        .altitude = -45.0
    };
    
    /* 计算NED偏移量 */
    NEDOffset offset = geo_to_ned_equirectangular(&current, &target);
    
    printf("位置误差:\n");
    printf("  北向: %.2f m\n", offset.north);
    printf("  东向: %.2f m\n", offset.east);
    printf("  深度: %.2f m\n", offset.down);
    
    /* 计算到目标的距离和方位 */
    double distance = sqrt(offset.north * offset.north + 
                          offset.east * offset.east);
    double bearing = atan2(offset.east, offset.north) * RAD_TO_DEG;
    if (bearing < 0) bearing += 360.0;
    
    printf("  距离: %.2f m\n", distance);
    printf("  方位: %.2f°\n", bearing);
    
    /* 这些误差值可以直接用于PID控制器 */
}

8.2 航线规划

c 复制代码
/* 计算多航点路径总长度 */
double calculate_route_distance(const GeoPoint *waypoints, int count)
{
    double total_distance = 0.0;
    
    for (int i = 0; i < count - 1; i++) {
        /* 使用Haversine计算航点间距离 */
        double dist = geo_distance_haversine(
            waypoints[i].latitude, waypoints[i].longitude,
            waypoints[i+1].latitude, waypoints[i+1].longitude
        );
        
        total_distance += dist;
        
        /* 计算航段方位角 */
        double bearing = geo_bearing(
            waypoints[i].latitude, waypoints[i].longitude,
            waypoints[i+1].latitude, waypoints[i+1].longitude
        );
        
        printf("航段 %d→%d: 距离=%.1fm, 方位=%.1f°\n",
               i, i+1, dist, bearing);
    }
    
    return total_distance;
}

8.3 地理围栏检测

c 复制代码
/* 综合围栏检测 */
typedef struct {
    bool in_home_zone;      /* 是否在返航区域 */
    bool in_no_fly_zone;    /* 是否在禁飞区 */
    double distance_to_home; /* 距离返航点距离 */
} FenceStatus;

FenceStatus check_geofence(double lat, double lon)
{
    FenceStatus status;
    
    /* 返航点和最大飞行半径 */
    double home_lat = 22.5431, home_lon = 113.9360;
    double max_radius = 5000.0;  /* 5公里 */
    
    /* 检查是否在飞行半径内 */
    status.distance_to_home = geo_distance_equirectangular(
        lat, lon, home_lat, home_lon);
    status.in_home_zone = (status.distance_to_home <= max_radius);
    
    /* 禁飞区多边形 */
    double nfz_lats[] = {22.55, 22.56, 22.56, 22.55};
    double nfz_lons[] = {113.94, 113.94, 113.95, 113.95};
    
    status.in_no_fly_zone = geo_is_inside_polygon(
        lat, lon, nfz_lats, nfz_lons, 4);
    
    return status;
}

8.4 航线偏离检测

c 复制代码
/* 检测是否偏离航线 */
void check_track_deviation(double current_lat, double current_lon,
                           double wp1_lat, double wp1_lon,
                           double wp2_lat, double wp2_lon)
{
    /* Cross-Track Distance: 垂直偏离距离 */
    double xtd = geo_cross_track_distance(
        current_lat, current_lon,
        wp1_lat, wp1_lon, wp2_lat, wp2_lon);
    
    /* Along-Track Distance: 沿航线已飞距离 */
    double atd = geo_along_track_distance(
        current_lat, current_lon,
        wp1_lat, wp1_lon, wp2_lat, wp2_lon);
    
    /* 航段总长度 */
    double leg_distance = geo_distance_haversine(
        wp1_lat, wp1_lon, wp2_lat, wp2_lon);
    
    printf("航线状态:\n");
    printf("  偏离航线: %.1f m (%s)\n", 
           fabs(xtd), xtd > 0 ? "右偏" : "左偏");
    printf("  已飞距离: %.1f m (%.1f%%)\n", 
           atd, atd / leg_distance * 100);
    
    /* 告警阈值 */
    if (fabs(xtd) > 100.0) {
        printf("  ⚠️ 警告: 偏离航线超过100米!\n");
    }
}

总结

本文详细介绍了三种地理距离计算方法:

  1. 正交投影法:适用于10km以内的近距离计算,速度最快,是无人机/ROV实时控制的首选。

  2. Haversine公式:适用于中远距离计算,数值稳定性好,是航线规划的常用方法。

  3. Vincenty公式:基于WGS84椭球体,精度可达毫米级,适用于高精度测绘需求。

在实际开发中,可以根据具体场景灵活选择,或使用geo_distance_auto()函数自动选择最优方法。


参考资料:

  • WGS84椭球体参数标准
  • Vincenty, T. (1975). "Direct and Inverse Solutions of Geodesics on the Ellipsoid"
  • Movable Type Scripts - Calculate distance and bearing between two Latitude/Longitude points
相关推荐
乾元2 小时前
AI 驱动的网络攻防演练与安全态势推演——从“规则检测”到“行为级对抗”的工程体系
网络·人工智能·安全·web安全·架构·自动化·运维开发
秃了也弱了。2 小时前
OpenResty+redis实现基于ip的代理层灰度发布
redis·tcp/ip·openresty
源代码•宸2 小时前
goframe框架签到系统项目开发(用户认证、基于 JWT 实现认证、携带access token获取用户信息)
服务器·开发语言·网络·分布式·后端·golang·jwt
一执念2 小时前
【路由器-AP、DHCP、ARP、广播帧、交换机、信道】-初级知识串联(二)
网络·智能路由器
刺客xs2 小时前
TCP服务器并发编程
服务器·网络协议·tcp/ip
G_H_S_3_2 小时前
【网络运维】SQL 语言:MySQL数据库基础与管理
运维·网络·数据库·mysql
bing_feilong2 小时前
windows和ubuntu: ssh失败
网络·ubuntu·ssh
浔川python社3 小时前
快手遭黑灰产猛烈攻击事件暴露出哪些安全漏洞?
网络·安全
雨大王5123 小时前
工业互联网赋能装备制造智能化:企业如何抓住机遇规避风险
人工智能·机器学习·汽车·制造