1use crate::{EARTH_RADIUS_NM, Position, angle};
12
13const MERIDIONAL_EPS: f64 = 1e-12;
17
18fn projected_lat(lat_rad: f64) -> f64 {
21 (std::f64::consts::FRAC_PI_4 + lat_rad / 2.0).tan().ln()
22}
23
24#[must_use]
26pub fn distance_nm(from: Position, to: Position) -> f64 {
27 let (lat1, lat2) = (from.lat_rad(), to.lat_rad());
28 let d_lat = lat2 - lat1;
29 let d_lon = shortest_d_lon(from.lon_rad(), to.lon_rad());
30 let d_proj = projected_lat(lat2) - projected_lat(lat1);
31
32 let q = if d_proj.abs() > MERIDIONAL_EPS {
35 d_lat / d_proj
36 } else {
37 lat1.cos()
38 };
39
40 (d_lat * d_lat + q * q * d_lon * d_lon).sqrt() * EARTH_RADIUS_NM
41}
42
43#[must_use]
48pub fn bearing_deg(from: Position, to: Position) -> f64 {
49 let d_lon = shortest_d_lon(from.lon_rad(), to.lon_rad());
50 let d_proj = projected_lat(to.lat_rad()) - projected_lat(from.lat_rad());
51 angle::norm_360(d_lon.atan2(d_proj).to_degrees())
52}
53
54#[must_use]
56pub fn destination(from: Position, bearing_deg: f64, distance_nm: f64) -> Position {
57 let angular = distance_nm / EARTH_RADIUS_NM;
58 let brg = bearing_deg.to_radians();
59 let (lat1, lon1) = (from.lat_rad(), from.lon_rad());
60
61 let d_lat = angular * brg.cos();
62 let mut lat2 = lat1 + d_lat;
63
64 if lat2.abs() > std::f64::consts::FRAC_PI_2 {
68 lat2 = if lat2 > 0.0 {
69 std::f64::consts::PI - lat2
70 } else {
71 -std::f64::consts::PI - lat2
72 };
73 }
74
75 let d_proj = projected_lat(lat2) - projected_lat(lat1);
76 let q = if d_proj.abs() > MERIDIONAL_EPS {
77 d_lat / d_proj
78 } else {
79 lat1.cos()
80 };
81 let lon2 = lon1 + angular * brg.sin() / q;
82
83 Position::new(lat2.to_degrees(), angle::norm_180(lon2.to_degrees()))
84}
85
86fn shortest_d_lon(lon1_rad: f64, lon2_rad: f64) -> f64 {
88 let d = lon2_rad - lon1_rad;
89 if d.abs() > std::f64::consts::PI {
90 if d > 0.0 {
91 d - std::f64::consts::TAU
92 } else {
93 d + std::f64::consts::TAU
94 }
95 } else {
96 d
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::great_circle;
104
105 fn close(a: f64, b: f64, tol: f64) -> bool {
106 (a - b).abs() < tol
107 }
108
109 #[test]
110 fn along_a_meridian_it_agrees_with_the_great_circle() {
111 let a = Position::new(45.0, 13.0);
114 let b = Position::new(46.0, 13.0);
115 assert!(close(distance_nm(a, b), great_circle::distance_nm(a, b), 1e-6));
116 assert!(close(bearing_deg(a, b), 0.0, 1e-9));
117 }
118
119 #[test]
120 fn along_the_equator_it_agrees_too() {
121 let a = Position::new(0.0, 10.0);
122 let b = Position::new(0.0, 11.0);
123 assert!(close(distance_nm(a, b), great_circle::distance_nm(a, b), 1e-6));
124 assert!(close(bearing_deg(a, b), 90.0, 1e-9));
125 }
126
127 #[test]
128 fn along_a_parallel_it_is_longer_than_the_great_circle() {
129 let a = Position::new(60.0, -10.0);
132 let b = Position::new(60.0, 10.0);
133 assert!(distance_nm(a, b) > great_circle::distance_nm(a, b));
134 assert!(close(bearing_deg(a, b), 90.0, 1e-9));
135 }
136
137 #[test]
138 fn destination_inverts_bearing_and_distance() {
139 let start = Position::new(45.55, 13.73);
140 for bearing in [0.0, 37.5, 90.0, 180.0, 271.3, 359.0] {
141 for dist in [0.5, 12.0, 300.0] {
142 let end = destination(start, bearing, dist);
143 assert!(
144 close(distance_nm(start, end), dist, 1e-6),
145 "distance differs for {bearing}/{dist}"
146 );
147 assert!(
148 close(bearing_deg(start, end), bearing, 1e-6),
149 "bearing differs for {bearing}/{dist}"
150 );
151 }
152 }
153 }
154
155 #[test]
156 fn bearing_holds_all_the_way_along_the_leg() {
157 let start = Position::new(43.0, 9.0);
160 let end = destination(start, 62.0, 400.0);
161 for frac in [0.1, 0.25, 0.5, 0.9] {
162 let mid = destination(start, 62.0, 400.0 * frac);
163 assert!(
164 close(bearing_deg(mid, end), 62.0, 1e-6),
165 "bearing drifted at {frac}"
166 );
167 }
168 }
169
170 #[test]
171 fn crossing_the_antimeridian_takes_the_short_way() {
172 let a = Position::new(10.0, 179.0);
173 let b = Position::new(10.0, -179.0);
174 let d = distance_nm(a, b);
175 assert!(d < 130.0, "took the long way round: {d}");
176 assert!(close(bearing_deg(a, b), 90.0, 1e-6));
177 }
178}