geometry_strategy/geographic/
inverse_karney.rs1use geometry_cs::{CoordinateSystem, GeographicFamily, Spheroid};
12use geometry_tag::SameAs;
13use geometry_trait::Point;
14
15use crate::distance::DistanceStrategy;
16
17use super::inverse::InverseResult;
18
19#[cfg(feature = "std")]
20use super::direct::normalize_longitude;
21#[cfg(feature = "std")]
22use super::direct_karney::KarneyDirect;
23#[cfg(feature = "std")]
24use crate::normalise::{HasAngularUnits, lonlat_radians};
25
26#[derive(Debug, Clone, Copy)]
33pub struct KarneyInverse {
34 pub spheroid: Spheroid,
36 pub max_iterations: u32,
38 pub tolerance: f64,
40}
41
42pub type Karney = KarneyInverse;
45
46impl KarneyInverse {
47 pub const WGS84: Self = Self {
49 spheroid: Spheroid::WGS84,
50 max_iterations: 50,
51 tolerance: 2e-14,
52 };
53
54 #[cfg(feature = "std")]
64 #[inline]
65 #[must_use]
66 #[allow(
67 clippy::many_single_char_names,
68 clippy::similar_names,
69 clippy::float_cmp,
70 reason = "symbols and exact coincident checks follow the inverse-geodesic equations"
71 )]
72 pub fn apply(&self, lon1: f64, lat1: f64, lon2: f64, lat2: f64) -> InverseResult {
73 let delta_lon = normalize_longitude(lon2 - lon1);
74 if delta_lon == 0.0 && lat1 == lat2 {
75 return InverseResult {
76 distance: 0.0,
77 azimuth: 0.0,
78 reverse_azimuth: 0.0,
79 converged: true,
80 reduced_length: 0.0,
81 geodesic_scale: 1.0,
82 };
83 }
84
85 let sin_delta_lon = delta_lon.sin();
86 let cos_delta_lon = delta_lon.cos();
87 let sin_lat1 = lat1.sin();
88 let cos_lat1 = lat1.cos();
89 let sin_lat2 = lat2.sin();
90 let cos_lat2 = lat2.cos();
91 let central_angle = (sin_lat1 * sin_lat2 + cos_lat1 * cos_lat2 * cos_delta_lon)
92 .clamp(-1.0, 1.0)
93 .acos();
94 let spherical_azimuth = (sin_delta_lon * cos_lat2)
95 .atan2(cos_lat1 * sin_lat2 - sin_lat1 * cos_lat2 * cos_delta_lon);
96 let mean_radius = self.spheroid.equatorial_radius * (1.0 - self.spheroid.flattening / 3.0);
97 let spherical_distance = central_angle * mean_radius;
98 let half_meridian = core::f64::consts::PI * self.spheroid.polar_radius();
99 let direct = KarneyDirect {
100 spheroid: self.spheroid,
101 };
102
103 let azimuth_seeds = [
104 spherical_azimuth,
105 0.0,
106 core::f64::consts::FRAC_PI_4,
107 -core::f64::consts::FRAC_PI_4,
108 core::f64::consts::FRAC_PI_2,
109 -core::f64::consts::FRAC_PI_2,
110 3.0 * core::f64::consts::FRAC_PI_4,
111 -3.0 * core::f64::consts::FRAC_PI_4,
112 core::f64::consts::PI,
113 ];
114 let distance_seeds = [spherical_distance, half_meridian];
115 let first_candidate = self.solve_seed(
116 &direct,
117 lon1,
118 lat1,
119 lon2,
120 lat2,
121 distance_seeds[0],
122 azimuth_seeds[0],
123 );
124 let first_endpoint = direct.apply(
125 lon1,
126 lat1,
127 first_candidate.distance,
128 first_candidate.azimuth,
129 );
130 let first_error = endpoint_error(first_endpoint.lon2, first_endpoint.lat2, lon2, lat2);
131 let mut best = (first_error, first_candidate);
132
133 for (azimuth_index, &azimuth_seed) in azimuth_seeds.iter().enumerate() {
134 let remaining_distances = if azimuth_index == 0 {
135 &distance_seeds[1..]
136 } else {
137 &distance_seeds[..]
138 };
139 for &distance_seed in remaining_distances {
140 let candidate =
141 self.solve_seed(&direct, lon1, lat1, lon2, lat2, distance_seed, azimuth_seed);
142 let endpoint = direct.apply(lon1, lat1, candidate.distance, candidate.azimuth);
143 let error = endpoint_error(endpoint.lon2, endpoint.lat2, lon2, lat2);
144 let (best_error, best_result) = best;
145 let replace = error < best_error
146 || (error <= self.tolerance
147 && best_error <= self.tolerance
148 && candidate.distance < best_result.distance);
149 if replace {
150 best = (error, candidate);
151 }
152 }
153 }
154
155 best.1
156 }
157
158 #[cfg(feature = "std")]
159 #[allow(
160 clippy::too_many_arguments,
161 clippy::similar_names,
162 reason = "the Newton state mirrors the two endpoints plus distance/azimuth seed"
163 )]
164 fn solve_seed(
165 &self,
166 direct: &KarneyDirect,
167 lon1: f64,
168 lat1: f64,
169 lon2: f64,
170 lat2: f64,
171 mut distance: f64,
172 mut azimuth: f64,
173 ) -> InverseResult {
174 let max_distance = 1.1 * core::f64::consts::PI * self.spheroid.equatorial_radius;
175 let mut converged = false;
176 for _ in 0..self.max_iterations {
177 let current = direct.apply(lon1, lat1, distance, azimuth);
178 let [residual_lon, residual_lat] = residual(current.lon2, current.lat2, lon2, lat2);
179 let error = residual_lon.hypot(residual_lat);
180 if error <= self.tolerance {
181 converged = true;
182 break;
183 }
184
185 let distance_step = 10.0;
186 let azimuth_step = 1e-6;
187 let plus_distance = direct.apply(lon1, lat1, distance + distance_step, azimuth);
188 let minus_distance = direct.apply(lon1, lat1, distance - distance_step, azimuth);
189 let plus_azimuth = direct.apply(lon1, lat1, distance, azimuth + azimuth_step);
190 let minus_azimuth = direct.apply(lon1, lat1, distance, azimuth - azimuth_step);
191 let [pd_lon, pd_lat] = residual(plus_distance.lon2, plus_distance.lat2, lon2, lat2);
192 let [md_lon, md_lat] = residual(minus_distance.lon2, minus_distance.lat2, lon2, lat2);
193 let [pa_lon, pa_lat] = residual(plus_azimuth.lon2, plus_azimuth.lat2, lon2, lat2);
194 let [ma_lon, ma_lat] = residual(minus_azimuth.lon2, minus_azimuth.lat2, lon2, lat2);
195 let j00 = (pd_lon - md_lon) / (2.0 * distance_step);
196 let j10 = (pd_lat - md_lat) / (2.0 * distance_step);
197 let j01 = (pa_lon - ma_lon) / (2.0 * azimuth_step);
198 let j11 = (pa_lat - ma_lat) / (2.0 * azimuth_step);
199 let determinant = j00 * j11 - j01 * j10;
200 if determinant.abs() < 1e-24 || !determinant.is_finite() {
201 break;
202 }
203 let mut distance_update = (-residual_lon * j11 + j01 * residual_lat) / determinant;
204 let mut azimuth_update = (residual_lon * j10 - j00 * residual_lat) / determinant;
205 distance_update = distance_update.clamp(-2_000_000.0, 2_000_000.0);
206 azimuth_update = azimuth_update.clamp(-0.5, 0.5);
207 distance = (distance + distance_update).clamp(0.0, max_distance);
208 azimuth = normalize_longitude(azimuth + azimuth_update);
209 }
210
211 let endpoint = direct.apply(lon1, lat1, distance, azimuth);
212 if endpoint_error(endpoint.lon2, endpoint.lat2, lon2, lat2) <= self.tolerance {
213 converged = true;
214 }
215 InverseResult {
216 distance,
217 azimuth,
218 reverse_azimuth: endpoint.reverse_azimuth,
219 converged,
220 reduced_length: endpoint.reduced_length,
221 geodesic_scale: endpoint.geodesic_scale,
222 }
223 }
224}
225
226#[cfg(feature = "std")]
227fn residual(lon: f64, lat: f64, target_lon: f64, target_lat: f64) -> [f64; 2] {
228 [
229 normalize_longitude(lon - target_lon) * target_lat.cos(),
230 lat - target_lat,
231 ]
232}
233
234#[cfg(feature = "std")]
235fn endpoint_error(lon: f64, lat: f64, target_lon: f64, target_lat: f64) -> f64 {
236 let [lon_error, lat_error] = residual(lon, lat, target_lon, target_lat);
237 lon_error.hypot(lat_error)
238}
239
240impl Default for KarneyInverse {
241 #[inline]
242 fn default() -> Self {
243 Self::WGS84
244 }
245}
246
247#[cfg(feature = "std")]
248impl<P1, P2> DistanceStrategy<P1, P2> for KarneyInverse
249where
250 P1: Point<Scalar = f64>,
251 P2: Point<Scalar = f64>,
252 P1::Cs: HasAngularUnits,
253 P2::Cs: HasAngularUnits,
254 <P1::Cs as CoordinateSystem>::Family: SameAs<GeographicFamily>,
255 <P2::Cs as CoordinateSystem>::Family: SameAs<GeographicFamily>,
256{
257 type Out = f64;
258 type Comparable = Self;
259
260 #[inline]
261 fn distance(&self, first: &P1, second: &P2) -> Self::Out {
262 let (lon1, lat1) = lonlat_radians(first);
263 let (lon2, lat2) = lonlat_radians(second);
264 self.apply(lon1, lat1, lon2, lat2).distance
265 }
266
267 #[inline]
268 fn comparable(&self) -> Self::Comparable {
269 *self
270 }
271}