geometry_strategy/distance.rs
1//! The [`DistanceStrategy`] trait, default strategy selection, and
2//! the [`Reversed`] argument-swapping adapter.
3//!
4//! Mirrors three pieces of Boost.Geometry that collaborate to make
5//! `boost::geometry::distance(a, b)` work for any geometry pair in
6//! any coordinate system, with implicit or explicit strategy
7//! selection:
8//!
9//! * `boost/geometry/strategies/distance.hpp` — the
10//! `boost::geometry::strategy::distance` namespace and the per-CS
11//! strategy concept.
12//! * `boost/geometry/strategies/distance/services.hpp` — the
13//! `services::default_strategy<G1, G2>` metafunction that picks
14//! the right strategy for a CS pair (Pythagoras for cartesian,
15//! Haversine / Andoyer for spherical / geographic, …).
16//! * `boost/geometry/strategies/default_strategy.hpp` plus
17//! `boost/geometry/core/reverse_dispatch.hpp` and the partial
18//! specialisation at
19//! `boost/geometry/algorithms/detail/distance/interface.hpp:53-77`
20//! — the symmetry-collapsing layer that gives you
21//! `distance(segment, point)` for free once you have
22//! `distance(point, segment)`.
23//!
24//! T21 lands the *trait surface* only — no concrete strategies yet.
25//! T22 adds `Pythagoras` / `ComparablePythagoras` and the first
26//! [`DefaultDistance`] impls; T23 wires this into the free-function
27//! `geometry-algorithm::distance` entry point.
28
29use geometry_coords::CoordinateScalar;
30use geometry_cs::CoordinateSystem;
31use geometry_trait::{Geometry, Point};
32
33pub use crate::reversal::Reversed;
34
35/// A strategy for computing the distance between two geometries.
36///
37/// Mirrors the per-CS strategy concept declared in
38/// `boost/geometry/strategies/distance.hpp` and refined per
39/// coordinate system in `strategies/{cartesian,spherical,geographic}/
40/// distance_*.hpp`. The Boost concept exposes
41/// `apply(g1, g2)` plus a `return_type<G1, G2>` metafunction plus a
42/// `comparable_type<...>` sibling; we collapse those three onto one
43/// Rust trait via associated types.
44///
45/// # Associated items
46///
47/// * [`Self::Out`] — the scalar the distance comes back as.
48/// Equivalent to Boost's
49/// `boost::geometry::strategy::distance::services::return_type<...>`
50/// (`strategies/distance/services.hpp`). Typically the wider of
51/// `A`'s and `B`'s coordinate scalars after promotion.
52/// * [`Self::Comparable`] — the "skip the sqrt" form of
53/// this strategy. For Pythagoras this is `ComparablePythagoras`;
54/// for strategies where there is nothing to save by deferring a
55/// square root (Andoyer, Vincenty), set `type Comparable = Self;`
56/// and the optimiser collapses the indirection. Mirrors
57/// `comparable_type<...>` from the same Boost services header.
58///
59/// # Method shape
60///
61/// `distance(&self, &A, &B)` mirrors `apply(g1, g2)` on Boost's
62/// strategy structs (e.g.
63/// `strategies/cartesian/distance_pythagoras.hpp:75-95` for the
64/// comparable form, `:99-115` for the sqrt-paying form).
65/// `comparable(&self)` mirrors `get_comparable(strategy)` from
66/// `strategies/distance/services.hpp`.
67pub trait DistanceStrategy<A: Geometry, B: Geometry> {
68 /// The output scalar type. Typically the promoted scalar of
69 /// `A` and `B`. Mirrors
70 /// `services::return_type<Strategy, P1, P2>::type` from
71 /// `strategies/distance/services.hpp`.
72 type Out: CoordinateScalar;
73
74 /// The "skip the sqrt" form of this strategy.
75 ///
76 /// For Pythagoras → `ComparablePythagoras` (returns squared
77 /// distance). For strategies where there is no win from a
78 /// comparable form (Vincenty, Andoyer), `type Comparable = Self;`
79 /// — the optimiser collapses it. Mirrors
80 /// `comparable_type<Strategy>::type` from
81 /// `strategies/distance/services.hpp`.
82 ///
83 /// The bound `DistanceStrategy<A, B, Out = Self::Out>` enforces
84 /// the invariant that comparing two comparable distances gives
85 /// the same ordering as comparing two real distances — both
86 /// sides of the comparison must speak the same scalar type.
87 type Comparable: DistanceStrategy<A, B, Out = Self::Out>;
88
89 /// Compute the distance between `a` and `b`.
90 ///
91 /// Mirrors `apply(g1, g2)` on Boost strategy structs (e.g.
92 /// `strategies/cartesian/distance_pythagoras.hpp:99-115`).
93 fn distance(&self, a: &A, b: &B) -> Self::Out;
94
95 /// Return the comparable-form companion of this strategy.
96 ///
97 /// Mirrors `services::get_comparable(strategy)` from
98 /// `strategies/distance/services.hpp` — most concrete strategies
99 /// implement it as a no-op constructor of the sibling
100 /// `comparable::*` type.
101 fn comparable(&self) -> Self::Comparable;
102}
103
104/// "Which distance strategy do we pick by default for a pair of
105/// coordinate-system *families* `Self` (for the first geometry's
106/// CS family) and `Other` (for the second)?"
107///
108/// Mirrors `boost::geometry::strategy::distance::services::
109/// default_strategy<Tag1, Tag2, Point1, Point2, CsTag1, CsTag2>`
110/// from `boost/geometry/strategies/distance/services.hpp`, together
111/// with the per-CS specialisations in
112/// `strategies/{cartesian,spherical,geographic}/distance.hpp` and
113/// the umbrella `strategies/default_strategy.hpp`.
114///
115/// In Boost the dispatch keys on the *coordinate-system tag*
116/// (`cs_tag<Point>::type`). The Rust analogue is the
117/// [`CoordinateSystem::Family`] associated type defined in
118/// `geometry-cs` — every concrete CS (`Cartesian`,
119/// `Spherical<Degree>`, `Spherical<Radian>`, `Geographic<Degree>`,
120/// …) reports one of the family marker types
121/// (`CartesianFamily`, `SphericalFamily`, `GeographicFamily`,
122/// `PolarFamily`).
123///
124/// Implementations land in `geometry-strategy` alongside each
125/// concrete strategy:
126///
127/// ```ignore
128/// impl DefaultDistance<CartesianFamily> for CartesianFamily { type Strategy = Pythagoras; }
129/// impl DefaultDistance<SphericalFamily> for SphericalFamily { type Strategy = Haversine; }
130/// impl DefaultDistance<GeographicFamily> for GeographicFamily { type Strategy = Andoyer; }
131/// ```
132///
133/// The `Strategy: Default` bound matches Boost's expectation that
134/// `services::default_strategy<...>::type` is default-constructible
135/// (every `strategies/.../distance_*.hpp` concrete strategy has a
136/// no-arg constructor).
137pub trait DefaultDistance<Other> {
138 /// The strategy chosen for `(Self, Other)`. Must implement
139 /// `Default` because the free-function `distance(a, b)` builds
140 /// it without arguments — same contract as Boost's
141 /// default-constructed `services::default_strategy<...>::type`.
142 type Strategy: Default;
143}
144
145/// Type alias resolving the default distance strategy for the pair
146/// `(A, B)` by walking
147/// `A -> A::Point -> Cs -> Family -> DefaultDistance<…B's Family…>::Strategy`.
148///
149/// Replaces the C++ free function
150/// `boost::geometry::strategy::distance::services::
151/// default_strategy<G1, G2>::type` from
152/// `strategies/distance/services.hpp`, plumbed through the
153/// per-CS files.
154///
155/// At call sites in `geometry-algorithm::distance`, this is the
156/// type the strategy-less `distance(a, b)` overload monomorphises
157/// against:
158///
159/// ```ignore
160/// pub fn distance<A: Geometry, B: Geometry>(a: &A, b: &B)
161/// -> <DefaultDistanceStrategy<A, B> as DistanceStrategy<A, B>>::Out
162/// where
163/// DefaultDistanceStrategy<A, B>: DistanceStrategy<A, B>,
164/// {
165/// DefaultDistanceStrategy::<A, B>::default().distance(a, b)
166/// }
167/// ```
168pub type DefaultDistanceStrategy<A, B> =
169 <<<<A as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family as DefaultDistance<
170 <<<B as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family,
171 >>::Strategy;
172
173#[cfg(test)]
174mod tests {
175 //! Compile-time witnesses for the trait surface that the
176 //! algorithm crate (T23) will rely on. No runtime assertions —
177 //! a concrete strategy lands in T22.
178
179 use super::{DefaultDistance, DefaultDistanceStrategy, DistanceStrategy, Reversed};
180 use geometry_coords::CoordinateScalar;
181 use geometry_cs::CoordinateSystem;
182 use geometry_trait::{Geometry, Point};
183
184 /// Witness: any type implementing `DistanceStrategy<A, B>` must
185 /// expose `Out`, `Comparable`, `distance`, `comparable` with the
186 /// exact bounds T23's `distance(a, b)` will depend on.
187 fn _accepts_strategy<A, B, S>()
188 where
189 A: Geometry,
190 B: Geometry,
191 S: DistanceStrategy<A, B>,
192 S::Out: CoordinateScalar,
193 S::Comparable: DistanceStrategy<A, B, Out = S::Out>,
194 {
195 }
196
197 /// Witness: `Reversed<S>` implements `DistanceStrategy<B, A>`
198 /// for any `S: DistanceStrategy<A, B>`. Exercises the blanket
199 /// impl above so the algorithm crate can rely on it without a
200 /// concrete strategy in scope.
201 fn _reversed_witness<A, B, S>()
202 where
203 A: Geometry,
204 B: Geometry,
205 S: DistanceStrategy<A, B>,
206 Reversed<S>: DistanceStrategy<B, A, Out = S::Out>,
207 {
208 }
209
210 /// Witness: a `Reversed<Reversed<S>>` is *not* spelled the same
211 /// as `S` but does compute the same thing — the blanket impl
212 /// composes. Useful because reverse-dispatch lookups from
213 /// asymmetric-pair algorithms may layer two `Reversed`s in
214 /// degenerate code paths.
215 fn _double_reversed_witness<A, B, S>()
216 where
217 A: Geometry,
218 B: Geometry,
219 S: DistanceStrategy<A, B>,
220 Reversed<Reversed<S>>: DistanceStrategy<A, B, Out = S::Out>,
221 {
222 }
223
224 /// Witness: `DefaultDistanceStrategy<A, B>` resolves to the
225 /// `Strategy` projection of `DefaultDistance` keyed on the two
226 /// geometries' CS families, and that strategy implements
227 /// `DistanceStrategy<A, B>`. This is the exact bound T23's
228 /// strategy-less `distance(a, b)` will carry.
229 fn _default_strategy_witness<A, B>()
230 where
231 A: Geometry,
232 B: Geometry,
233 <<A as Geometry>::Point as Point>::Cs: CoordinateSystem,
234 <<B as Geometry>::Point as Point>::Cs: CoordinateSystem,
235 <<<A as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family:
236 DefaultDistance<<<<B as Geometry>::Point as Point>::Cs as CoordinateSystem>::Family>,
237 DefaultDistanceStrategy<A, B>: DistanceStrategy<A, B> + Default,
238 {
239 }
240
241 use crate::cartesian::distance_pythagoras::{ComparablePythagoras, Pythagoras};
242 use geometry_cs::Cartesian;
243 use geometry_model::Point2D;
244
245 type P = Point2D<f64, Cartesian>;
246
247 /// Instantiating each type-level witness with a concrete strategy
248 /// (`Pythagoras` over two cartesian points) proves the trait bounds
249 /// they encode actually resolve, and executes their bodies.
250 #[test]
251 #[allow(
252 clippy::used_underscore_items,
253 reason = "the test exists to run the compile-time witness's body"
254 )]
255 fn witnesses_resolve_for_pythagoras_over_cartesian_points() {
256 _accepts_strategy::<P, P, Pythagoras>();
257 _reversed_witness::<P, P, Pythagoras>();
258 _double_reversed_witness::<P, P, Pythagoras>();
259 _default_strategy_witness::<P, P>();
260 }
261
262 /// `Reversed<Pythagoras>` computes the same distance as `Pythagoras`
263 /// with the arguments swapped, and its `comparable()` returns the
264 /// wrapped comparable strategy (squared distance).
265 #[test]
266 #[allow(
267 clippy::float_cmp,
268 reason = "compared values are exact integer-valued literals"
269 )]
270 fn reversed_swaps_arguments_and_forwards_comparable() {
271 let a = P::new(0.0, 0.0);
272 let b = P::new(3.0, 4.0);
273
274 let forward = Pythagoras.distance(&a, &b);
275 let reversed = Reversed(Pythagoras).distance(&b, &a);
276 assert_eq!(forward, reversed);
277 assert_eq!(forward, 5.0);
278
279 // The comparable companion skips the sqrt: 3² + 4² = 25.
280 let rev: Reversed<Pythagoras> = Reversed(Pythagoras);
281 let cmp = DistanceStrategy::<P, P>::comparable(&rev);
282 let cmp_val = cmp.distance(&b, &a);
283 assert_eq!(cmp_val, ComparablePythagoras.distance(&a, &b));
284 assert_eq!(cmp_val, 25.0);
285 }
286}