Skip to main content

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