use crate::model::GeoPoint;
pub const EARTH_RADIUS_NM: f64 = 3_440.065;
pub fn distance_nm(a: GeoPoint, b: GeoPoint) -> f64 {
let (lat_a, lat_b) = (a.lat.to_radians(), b.lat.to_radians());
let half_dlat = (lat_b - lat_a) / 2.0;
let half_dlon = (b.lon - a.lon).to_radians() / 2.0;
let h = half_dlat.sin().powi(2) + lat_a.cos() * lat_b.cos() * half_dlon.sin().powi(2);
2.0 * EARTH_RADIUS_NM * h.sqrt().asin()
}
pub fn initial_bearing_deg(a: GeoPoint, b: GeoPoint) -> f64 {
let (lat_a, lat_b) = (a.lat.to_radians(), b.lat.to_radians());
let dlon = (b.lon - a.lon).to_radians();
let y = dlon.sin() * lat_b.cos();
let x = lat_a.cos() * lat_b.sin() - lat_a.sin() * lat_b.cos() * dlon.cos();
(y.atan2(x).to_degrees() + 360.0) % 360.0
}
pub fn destination(from: GeoPoint, bearing_deg: f64, distance_nm: f64) -> GeoPoint {
let angular = distance_nm / EARTH_RADIUS_NM;
let bearing = bearing_deg.to_radians();
let lat_from = from.lat.to_radians();
let lat_to =
(lat_from.sin() * angular.cos() + lat_from.cos() * angular.sin() * bearing.cos()).asin();
let lon_to = from.lon.to_radians()
+ (bearing.sin() * angular.sin() * lat_from.cos())
.atan2(angular.cos() - lat_from.sin() * lat_to.sin());
GeoPoint {
lat: lat_to.to_degrees(),
lon: normalize_lon(lon_to.to_degrees()),
}
}
pub fn cross_track_nm(point: GeoPoint, leg_start: GeoPoint, leg_end: GeoPoint) -> f64 {
let dist_sp = distance_nm(leg_start, point) / EARTH_RADIUS_NM;
let bearing_sp = initial_bearing_deg(leg_start, point).to_radians();
let bearing_se = initial_bearing_deg(leg_start, leg_end).to_radians();
(dist_sp.sin() * (bearing_sp - bearing_se).sin()).asin() * EARTH_RADIUS_NM
}
pub fn along_track_nm(point: GeoPoint, leg_start: GeoPoint, leg_end: GeoPoint) -> f64 {
let dist_sp = distance_nm(leg_start, point) / EARTH_RADIUS_NM;
let cross = cross_track_nm(point, leg_start, leg_end) / EARTH_RADIUS_NM;
let magnitude = (dist_sp.cos() / cross.cos()).clamp(-1.0, 1.0).acos() * EARTH_RADIUS_NM;
let relative = (initial_bearing_deg(leg_start, point)
- initial_bearing_deg(leg_start, leg_end))
.to_radians();
if relative.cos() < 0.0 {
-magnitude
} else {
magnitude
}
}
pub fn normalize_lon(lon: f64) -> f64 {
let wrapped = (lon + 180.0).rem_euclid(360.0) - 180.0;
if wrapped == -180.0 { 180.0 } else { wrapped }
}
#[cfg(test)]
mod tests;