use angle_sc::{Angle, Radians, trig};
pub const MIN_VALUE: f64 = 2.0 * f64::EPSILON;
#[must_use]
pub fn calculate_haversine_distance(
a_lat: Angle,
b_lat: Angle,
delta_long: Angle,
delta_lat: Angle,
) -> Radians {
let haversine_lat = trig::sq_sine_half(delta_lat.cos());
let haversine_lon = trig::sq_sine_half(delta_long.cos());
let a = (haversine_lat + a_lat.cos().0 * b_lat.cos().0 * haversine_lon).clamp(0.0, 1.0);
if a < MIN_VALUE {
Radians(0.0)
} else {
Radians(2.0 * a.sqrt().asin())
}
}
#[must_use]
pub fn e2gc_distance(e: f64) -> Radians {
Radians(2.0 * (0.5 * e.clamp(0.0, 2.0)).asin())
}
#[must_use]
pub fn gc2e_distance(gc: Radians) -> f64 {
2.0 * (0.5 * gc.0).sin()
}
#[must_use]
pub fn sq_euclidean_distance(a_lat: Angle, b_lat: Angle, delta_long: Angle) -> f64 {
let delta_x = b_lat.cos().0 * delta_long.cos().0 - a_lat.cos().0;
let delta_y = b_lat.cos().0 * delta_long.sin().0;
let delta_z = b_lat.sin().0 - a_lat.sin().0;
let result = delta_x * delta_x + delta_y * delta_y + delta_z * delta_z;
result.clamp(0.0, 4.0)
}
#[must_use]
pub fn calculate_gc_distance(a_lat: Angle, b_lat: Angle, delta_long: Angle) -> Radians {
e2gc_distance(sq_euclidean_distance(a_lat, b_lat, delta_long).sqrt())
}
#[must_use]
pub fn calculate_gc_azimuth(a_lat: Angle, b_lat: Angle, delta_long: Angle) -> Angle {
if a_lat.cos().0 < MIN_VALUE {
if a_lat.sin().0.is_sign_negative() {
Angle::default()
} else {
Angle::new(trig::UnitNegRange(0.0), trig::UnitNegRange(-1.0))
}
} else {
let sin_azimuth = b_lat.cos().0 * delta_long.sin().0;
let temp = (a_lat.sin().0 * b_lat.cos().0 * delta_long.sin().0 * delta_long.sin().0)
/ (1.0 + delta_long.cos().abs().0);
let cos_azimuth = if delta_long.cos().0 < 0.0 {
b_lat.sin().0 * a_lat.cos().0 + a_lat.sin().0 * b_lat.cos().0 - temp
} else {
b_lat.sin().0 * a_lat.cos().0 - a_lat.sin().0 * b_lat.cos().0 + temp
};
Angle::from_y_x(sin_azimuth, cos_azimuth)
}
}
#[must_use]
pub fn calculate_sigma(a_lat: Angle, b_lat: Angle, delta_long: Angle) -> Angle {
let a = b_lat.cos().0 * delta_long.sin().0;
let b = a_lat.cos().0 * b_lat.sin().0 - a_lat.sin().0 * b_lat.cos().0 * delta_long.cos().0;
let sin_sigma = trig::UnitNegRange(a.hypot(b));
let cos_sigma = trig::UnitNegRange(
a_lat.sin().0 * b_lat.sin().0 + a_lat.cos().0 * b_lat.cos().0 * delta_long.cos().0,
);
Angle::new(sin_sigma, cos_sigma)
}
#[must_use]
pub fn calculate_latitude(lat: Angle, azi: Angle, sigma: Angle) -> Angle {
let sin_lat =
trig::UnitNegRange(lat.sin().0 * sigma.cos().0 + lat.cos().0 * sigma.sin().0 * azi.cos().0);
let cos_lat = trig::UnitNegRange(
(lat.cos().0 * azi.sin().0)
.hypot(lat.sin().0 * sigma.sin().0 - lat.cos().0 * sigma.cos().0 * azi.cos().0),
);
Angle::new(sin_lat, cos_lat)
}
#[must_use]
pub fn calculate_delta_longitude(lat: Angle, azi: Angle, sigma: Angle) -> Angle {
let sin_lon = sigma.sin().0 * azi.sin().0;
let cos_lon = lat.cos().0 * sigma.cos().0 - lat.sin().0 * sigma.sin().0 * azi.cos().0;
Angle::from_y_x(sin_lon, cos_lon)
}
#[cfg(test)]
mod tests {
use super::*;
use angle_sc::{Degrees, is_within_tolerance};
#[test]
fn test_distance_conversion_functions() {
assert_eq!(core::f64::consts::PI, e2gc_distance(2.0).0);
assert!(is_within_tolerance(
core::f64::consts::FRAC_PI_2,
e2gc_distance(core::f64::consts::SQRT_2).0,
f64::EPSILON
));
assert!(is_within_tolerance(
core::f64::consts::SQRT_2,
gc2e_distance(Radians(core::f64::consts::FRAC_PI_2)),
f64::EPSILON
));
assert_eq!(2.0, gc2e_distance(Radians(core::f64::consts::PI)));
}
#[test]
fn test_distance_and_azimuth_functions() {
let angle_0 = Angle::default();
let angle_45 = Angle::from_y_x(1.0, 1.0);
let gc_distance = calculate_gc_distance(angle_0, angle_45, -angle_45);
let haversine_distance =
calculate_haversine_distance(angle_0, angle_45, -angle_45, -angle_45);
assert!(is_within_tolerance(
gc_distance.0,
haversine_distance.0,
f64::EPSILON
));
let sigma = calculate_sigma(angle_0, angle_45, -angle_45);
assert!(is_within_tolerance(
gc_distance.0,
Radians::from(sigma).0,
f64::EPSILON
));
let gc_azimuth = calculate_gc_azimuth(angle_0, angle_45, -angle_45);
assert!(is_within_tolerance(
-35.264389682754654,
Degrees::from(gc_azimuth).0,
32.0 * f64::EPSILON
));
let latitude = calculate_latitude(angle_0, gc_azimuth, sigma);
assert_eq!(angle_45, latitude);
let delta_lon = calculate_delta_longitude(angle_0, gc_azimuth, sigma);
assert!(is_within_tolerance(
-45.0,
Degrees::from(delta_lon).0,
f64::EPSILON
));
let gc_distance = calculate_gc_distance(angle_45, angle_45, angle_0);
assert_eq!(0.0, gc_distance.0);
let haversine_distance = calculate_haversine_distance(angle_45, angle_45, angle_0, angle_0);
assert_eq!(0.0, haversine_distance.0);
let sigma = calculate_sigma(angle_45, angle_45, angle_0);
assert_eq!(0.0, Radians::from(sigma).0);
let gc_azimuth = calculate_gc_azimuth(angle_45, angle_45, angle_0);
assert_eq!(0.0, Degrees::from(gc_azimuth).0);
let latitude = calculate_latitude(angle_45, gc_azimuth, sigma);
assert!(is_within_tolerance(
45.0,
Degrees::from(latitude).0,
f64::EPSILON
));
let delta_lon = calculate_delta_longitude(angle_45, gc_azimuth, sigma);
assert_eq!(0.0, Degrees::from(delta_lon).0);
}
#[test]
fn test_north_and_south_pole_azimuths() {
let angle_90 = Angle::new(trig::UnitNegRange(1.0), trig::UnitNegRange(0.0));
let angle_m90 = -angle_90;
let angle_0 = Angle::default();
let angle_45 = Angle::from_y_x(1.0, 1.0);
let angle_180 = Angle::new(trig::UnitNegRange(0.0), trig::UnitNegRange(-1.0));
let gc_azimuth = calculate_gc_azimuth(angle_m90, angle_45, angle_0);
assert_eq!(angle_0, gc_azimuth);
let gc_azimuth = calculate_gc_azimuth(angle_90, angle_45, angle_0);
assert_eq!(angle_180, gc_azimuth);
}
}