Skip to main content

geometry_strategy/geographic/
inverse_karney.rs

1//! Globally seeded Karney inverse geodesic on a spheroid.
2//!
3//! Provides the capability of `boost::geometry::formula::karney_inverse`
4//! from `formulas/karney_inverse.hpp:76-981` by numerically inverting this
5//! crate's order-8 [`KarneyDirect`](super::KarneyDirect) mapping. The C++
6//! implementation expands the inverse equations directly; Rust uses the same
7//! Karney series in the forward map and a bounded two-variable Newton solve.
8//! Multiple azimuth seeds preserve convergence near antipodal points, the
9//! motivating property of Karney's formula.
10
11use 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/// Karney order-8 inverse geodesic solver.
27///
28/// Mirrors the public role and result convention of
29/// `formula::karney_inverse<CT, ..., 8>` from
30/// `formulas/karney_inverse.hpp:76-981`. See the module-level divergence note
31/// for the bounded inverse-of-direct implementation used in Rust.
32#[derive(Debug, Clone, Copy)]
33pub struct KarneyInverse {
34    /// Reference ellipsoid.
35    pub spheroid: Spheroid,
36    /// Maximum Newton updates per starting azimuth.
37    pub max_iterations: u32,
38    /// Target endpoint residual in radians.
39    pub tolerance: f64,
40}
41
42/// Conventional short name for [`KarneyInverse`] when selecting a distance
43/// strategy.
44pub type Karney = KarneyInverse;
45
46impl KarneyInverse {
47    /// Karney inverse on WGS84 with bounded globally seeded iteration.
48    pub const WGS84: Self = Self {
49        spheroid: Spheroid::WGS84,
50        max_iterations: 50,
51        tolerance: 2e-14,
52    };
53
54    /// Solve the inverse geodesic problem between two longitude/latitude
55    /// pairs in radians.
56    ///
57    /// Reproduces the outputs of `karney_inverse::apply` from
58    /// `formulas/karney_inverse.hpp:105-981`: shortest distance, forward
59    /// azimuth, and final azimuth. The search evaluates several globally
60    /// distributed azimuth seeds and selects the shortest converged geodesic,
61    /// which handles the near-antipodal cases in
62    /// `test/formulas/inverse_karney.cpp:52-72`.
63    #[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}