Skip to main content

geometry_algorithm/
distance.rs

1//! User-facing `distance` / `distance_with` / `comparable_distance`
2//! entry points.
3//!
4//! Mirrors the three free-function overloads Boost.Geometry users
5//! reach for:
6//!
7//! * `boost::geometry::distance(g1, g2)` — strategy-less, picks the
8//!   default strategy for the coordinate-system pair. See
9//!   `boost/geometry/algorithms/distance.hpp` and the dispatch
10//!   plumbing in
11//!   `boost/geometry/algorithms/detail/distance/interface.hpp`.
12//! * `boost::geometry::distance(g1, g2, strategy)` — explicit-strategy
13//!   companion, same file.
14//! * `boost::geometry::comparable_distance(g1, g2)` — the
15//!   "skip the sqrt" form, from
16//!   `boost/geometry/algorithms/comparable_distance.hpp`.
17//!
18//! The strategy trait, default-strategy projection, and
19//! reverse-dispatch wrapper all live in `geometry-strategy::distance`
20//! (T21/T22). T23 is the thin layer that turns them into ordinary
21//! Rust functions.
22
23use geometry_cs::CoordinateSystem;
24use geometry_strategy::distance::{DefaultDistance, DefaultDistanceStrategy, DistanceStrategy};
25use geometry_trait::{Geometry, Point};
26
27/// Shorthand for the CS family of `G`'s point type. Used purely to
28/// keep the `where` clauses on the three free functions readable;
29/// matches the family projection spelled out long-form in the
30/// definition of
31/// [`geometry_strategy::distance::DefaultDistanceStrategy`].
32type Family<G> = <<<G as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family;
33
34/// Distance between two geometries using the default strategy for
35/// their coordinate-system pair.
36///
37/// Mirrors the no-strategy overload of `boost::geometry::distance(g1, g2)`
38/// from `boost/geometry/algorithms/distance.hpp` and the dispatch
39/// in `boost/geometry/algorithms/detail/distance/interface.hpp`. The
40/// default strategy is resolved via
41/// [`DefaultDistanceStrategy`], which is the Rust analogue of Boost's
42/// `boost::geometry::strategy::distance::services::default_strategy<…>::type`.
43#[inline]
44pub fn distance<A, B>(
45    a: &A,
46    b: &B,
47) -> <DefaultDistanceStrategy<A, B> as DistanceStrategy<A, B>>::Out
48where
49    A: Geometry,
50    B: Geometry,
51    Family<A>: DefaultDistance<Family<B>>,
52    DefaultDistanceStrategy<A, B>: DistanceStrategy<A, B> + Default,
53{
54    DefaultDistanceStrategy::<A, B>::default().distance(a, b)
55}
56
57/// Distance between two geometries using an explicit strategy.
58///
59/// Mirrors the with-strategy overload
60/// `boost::geometry::distance(g1, g2, strategy)` from
61/// `boost/geometry/algorithms/distance.hpp` and the dispatch
62/// in `boost/geometry/algorithms/detail/distance/interface.hpp`.
63///
64/// The C++ overload is spelled the same as the strategy-less one and
65/// disambiguated by SFINAE on the strategy concept; on the Rust side
66/// the two overloads become two distinct names ([`distance`] and
67/// `distance_with`) so the strategy argument never clashes with the
68/// strategy-less form during type inference.
69///
70/// Taking the strategy by value (rather than by reference, as Boost
71/// does with `Strategy const&` at
72/// `boost/geometry/algorithms/detail/distance/interface.hpp:68`)
73/// keeps the call site free of an `&` everyone would forget. Concrete
74/// strategies are zero-sized or pointer-sized configuration objects
75/// (`Pythagoras`, `Haversine`, `Andoyer { spheroid }`), so passing
76/// them by value monomorphises into nothing.
77#[inline]
78#[allow(
79    clippy::needless_pass_by_value,
80    reason = "Strategies are ZST/small Copy configuration objects; by-value matches the user-facing call shape."
81)]
82pub fn distance_with<A, B, S>(a: &A, b: &B, strategy: S) -> S::Out
83where
84    A: Geometry,
85    B: Geometry,
86    S: DistanceStrategy<A, B>,
87{
88    strategy.distance(a, b)
89}
90
91/// Comparable distance — equivalent ordering, cheaper math.
92///
93/// Mirrors `boost::geometry::comparable_distance(g1, g2)` from
94/// `boost/geometry/algorithms/comparable_distance.hpp`. The result
95/// preserves the ordering of [`distance`] but skips work the ordering
96/// does not need (e.g. the final `sqrt` for Pythagoras). The
97/// comparable companion of the default strategy is selected via
98/// [`DistanceStrategy::Comparable`], the Rust analogue of Boost's
99/// `boost::geometry::strategy::distance::services::comparable_type<…>::type`.
100#[inline]
101#[must_use]
102pub fn comparable_distance<A, B>(
103    a: &A,
104    b: &B,
105) -> <<DefaultDistanceStrategy<A, B> as DistanceStrategy<A, B>>::Comparable as DistanceStrategy<
106    A,
107    B,
108>>::Out
109where
110    A: Geometry,
111    B: Geometry,
112    Family<A>: DefaultDistance<Family<B>>,
113    DefaultDistanceStrategy<A, B>: DistanceStrategy<A, B> + Default,
114{
115    DefaultDistanceStrategy::<A, B>::default()
116        .comparable()
117        .distance(a, b)
118}
119
120/// Comparable distance using an explicitly supplied strategy.
121///
122/// Mirrors the strategy overload of `boost::geometry::comparable_distance`
123/// from `boost/geometry/algorithms/detail/comparable_distance/interface.hpp:226-254`.
124/// The supplied
125/// strategy is converted through [`DistanceStrategy::comparable`] before the
126/// distance is evaluated.
127#[inline]
128#[must_use]
129#[allow(
130    clippy::needless_pass_by_value,
131    reason = "distance strategies are zero-sized or small Copy values and match distance_with's public call shape"
132)]
133pub fn comparable_distance_with<A, B, S>(a: &A, b: &B, strategy: S) -> S::Out
134where
135    A: Geometry,
136    B: Geometry,
137    S: DistanceStrategy<A, B>,
138{
139    strategy.comparable().distance(a, b)
140}
141
142#[cfg(test)]
143mod tests {
144    //! Canonical 3-4-5 triangle, exercised through every entry point.
145    //! Reference value matches `geometry/test/strategies/pythagoras.cpp`
146    //! (lines 50-66) and `doc/quickstart.qbk` (Cartesian distance
147    //! example).
148
149    use super::{comparable_distance, distance, distance_with};
150    use geometry_cs::Cartesian;
151    use geometry_model::Point2D;
152    use geometry_strategy::cartesian::Pythagoras;
153
154    // 3² + 4² = 25 exactly and √25 = 5 exactly in IEEE-754; `assert_eq!`
155    // is the spec's chosen shape (T23 spec, `Tests` block). The
156    // pedantic `float_cmp` warning does not apply to integer-valued
157    // floats from finite arithmetic on small integers.
158    #[allow(clippy::float_cmp, reason = "3-4-5 is exact in IEEE-754 f64.")]
159    #[test]
160    fn cartesian_quickstart_3_4_5() {
161        let a = Point2D::<f64, Cartesian>::new(0.0, 0.0);
162        let b = Point2D::<f64, Cartesian>::new(3.0, 4.0);
163        assert_eq!(distance(&a, &b), 5.0);
164        assert_eq!(distance_with(&a, &b, Pythagoras), 5.0);
165        assert_eq!(comparable_distance(&a, &b), 25.0);
166    }
167
168    /// The 1D squared-difference walk: |7 − 2| = 5, squared = 25.
169    /// Exercises the `DIM == 1` dispatch arm of the Pythagoras kernel.
170    #[test]
171    fn one_dimensional_distance() {
172        type P1 = geometry_model::Point<f64, 1, Cartesian>;
173        let a = P1::new(2.0);
174        let b = P1::new(7.0);
175        assert!((distance(&a, &b) - 5.0).abs() < 1e-12);
176        assert!((comparable_distance(&a, &b) - 25.0).abs() < 1e-12);
177    }
178
179    /// The 4D walk (`MAX_DIM`): unit steps on each of four axes give
180    /// squared distance 4 and real distance 2. Exercises the `DIM == 4`
181    /// dispatch arm.
182    #[test]
183    fn four_dimensional_distance() {
184        use geometry_trait::PointMut as _;
185        type P4 = geometry_model::Point<f64, 4, Cartesian>;
186        let o = P4::default();
187        let mut p = P4::default();
188        p.set::<0>(1.0);
189        p.set::<1>(1.0);
190        p.set::<2>(1.0);
191        p.set::<3>(1.0);
192        assert!((comparable_distance(&o, &p) - 4.0).abs() < 1e-12);
193        assert!((distance(&o, &p) - 2.0).abs() < 1e-12);
194    }
195
196    /// A 3D point-to-segment distance exercises the `DIM == 3` dispatch
197    /// arms of the projected-point kernel. The point `(3, 4, 0)`
198    /// projects onto the z-axis segment `(0,0,0)-(0,0,10)` at the start
199    /// endpoint, so the distance is `sqrt(3² + 4²) = 5`.
200    #[test]
201    fn three_dimensional_point_to_segment() {
202        use geometry_model::{Point3D, Segment};
203        use geometry_strategy::PointToSegment;
204        type P3 = Point3D<f64, Cartesian>;
205        let p = P3::new(3.0, 4.0, 0.0);
206        let s = Segment::new(P3::new(0.0, 0.0, 0.0), P3::new(0.0, 0.0, 10.0));
207        let d = distance_with(&p, &s, PointToSegment::<Pythagoras>::default());
208        assert!((d - 5.0).abs() < 1e-12, "got {d}");
209    }
210
211    /// The north pole reached from two distinct longitudes is one
212    /// physical point (distance 0) for every geodesic strategy. The
213    /// differing longitudes bypass the coordinate-equality
214    /// short-circuit, so the in-formula degenerate guards report it.
215    #[cfg(feature = "std")]
216    #[test]
217    fn geodesic_coincident_poles_at_different_longitudes_are_zero() {
218        use geometry_adapt::{Adapt, WithCs};
219        use geometry_cs::{Degree, Geographic};
220        use geometry_strategy::geographic::{Andoyer, Thomas, Vincenty};
221
222        type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
223        let deg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
224
225        let a = deg(0.0, 90.0);
226        let b = deg(180.0, 90.0);
227        assert!(distance_with(&a, &b, Andoyer::WGS84).abs() < 1e-3);
228        assert!(distance_with(&a, &b, Thomas::WGS84).abs() < 1e-6);
229        assert!(distance_with(&a, &b, Vincenty::WGS84).abs() < 1e-3);
230    }
231
232    /// Thomas: a line whose *second* endpoint sits exactly on a pole
233    /// exercises the `lat2 == ±π/2` reduced-latitude short-circuit.
234    /// `(1, 80) → (0, 90)` ≈ 1116.825795 km.
235    #[cfg(feature = "std")]
236    #[test]
237    fn thomas_second_endpoint_at_pole() {
238        use geometry_adapt::{Adapt, WithCs};
239        use geometry_cs::{Degree, Geographic};
240        use geometry_strategy::geographic::Thomas;
241
242        type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
243        let deg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
244
245        let d = distance_with(&deg(1.0, 80.0), &deg(0.0, 90.0), Thomas::WGS84);
246        assert!((d / 1000.0 - 1_116.825_795).abs() < 0.012);
247    }
248
249    /// Vincenty: a pair straddling the antimeridian normalises Δλ into
250    /// `(-π, π]` in both directions; both describe the same 20°
251    /// equatorial arc (≈ 2 226 km on WGS84).
252    #[cfg(feature = "std")]
253    #[test]
254    fn vincenty_antimeridian_longitude_is_normalised_both_ways() {
255        use geometry_adapt::{Adapt, WithCs};
256        use geometry_cs::{Degree, Geographic};
257        use geometry_strategy::geographic::Vincenty;
258
259        type Gg = WithCs<Adapt<[f64; 2]>, Geographic<Degree>>;
260        let deg = |lon: f64, lat: f64| -> Gg { WithCs::new(Adapt([lon, lat])) };
261
262        let east = distance_with(&deg(170.0, 0.0), &deg(-170.0, 0.0), Vincenty::WGS84);
263        let west = distance_with(&deg(-170.0, 0.0), &deg(170.0, 0.0), Vincenty::WGS84);
264        assert!((east - west).abs() < 1e-6, "{east} vs {west}");
265        assert!((east / 1000.0 - 2_226.0).abs() < 5.0);
266    }
267}