Skip to main content

geometry_strategy/
closest_points.rs

1//! `ClosestPointsStrategy<A, B>` — pair of nearest points on
2//! `(A, B)`.
3//!
4//! Mirrors `boost::geometry::strategy::closest_points::*` from
5//! `boost/geometry/strategies/closest_points/`. The Cartesian
6//! implementations reuse the clamped-projection kernel that
7//! [`crate::PointToSegment`] is built on for the point↔segment case.
8//!
9//! ## Coherence note
10//!
11//! Same workaround as [`crate::intersects`] / [`crate::within`]: the
12//! impls key off the concrete `geometry-model` structs (`Point`,
13//! `Segment`, `Linestring`) rather than the open geometry traits, so a
14//! downstream type implementing several geometry traits at once cannot
15//! trigger overlapping-impl (E0119) errors.
16//!
17//! ## Asymmetry
18//!
19//! `closest_points` is *not* symmetric in the output tuple order — the
20//! first returned point lives on `a`, the second on `b`. Each pair is
21//! written in its canonical `(A, B)` direction here; there is no
22//! `Reversed` blanket.
23
24use alloc::vec::Vec;
25
26use geometry_coords::CoordinateScalar;
27use geometry_cs::{CartesianFamily, CoordinateSystem};
28use geometry_model::{Linestring, Point as ModelPoint, Segment};
29use geometry_tag::SameAs;
30use geometry_trait::{Linestring as LinestringTrait, Point, PointMut};
31
32/// A strategy for the pair of nearest points on `(A, B)`.
33///
34/// Mirrors `boost::geometry::strategy::closest_points::*` from
35/// `boost/geometry/strategies/closest_points/`. Boost returns the pair
36/// as a `Segment`; the Rust port returns a `(Out, Out)` tuple — same
37/// information, no `Segment::new` boilerplate at the call site.
38pub trait ClosestPointsStrategy<A, B> {
39    /// The point type the closest-pair is returned as.
40    type Out: PointMut + Default;
41
42    /// Return `(pa, pb)` where `pa` lies on `a`, `pb` lies on `b`, and
43    /// the distance `|pa − pb|` is minimal over the two geometries.
44    ///
45    /// Mirrors `apply(g1, g2, closest_pair)` on Boost's closest-points
46    /// strategy structs, returning the pair by value.
47    fn closest_points(&self, a: &A, b: &B) -> (Self::Out, Self::Out);
48}
49
50/// The Cartesian closest-points kernel.
51///
52/// Mirrors the registration in
53/// `boost/geometry/strategies/cartesian/closest_points_*.hpp`. Carries
54/// no state — every per-pair computation is parameter-less.
55#[derive(Debug, Default, Clone, Copy)]
56pub struct CartesianClosestPoints;
57
58// ---- Point × Point ---------------------------------------------------
59//
60// The two closest points are trivially the two inputs. Mirrors the
61// pointlike/pointlike arm at `strategies/cartesian/closest_points_pt_pt.hpp`.
62
63impl<T, const D: usize, Cs> ClosestPointsStrategy<ModelPoint<T, D, Cs>, ModelPoint<T, D, Cs>>
64    for CartesianClosestPoints
65where
66    T: CoordinateScalar,
67    Cs: CoordinateSystem,
68    Cs::Family: SameAs<CartesianFamily>,
69    ModelPoint<T, D, Cs>: PointMut + Default + Copy,
70{
71    type Out = ModelPoint<T, D, Cs>;
72
73    #[inline]
74    fn closest_points(
75        &self,
76        a: &ModelPoint<T, D, Cs>,
77        b: &ModelPoint<T, D, Cs>,
78    ) -> (Self::Out, Self::Out) {
79        (*a, *b)
80    }
81}
82
83// ---- Point × Segment -------------------------------------------------
84//
85// The closest point on the segment is the clamped foot of the
86// perpendicular from the point. Mirrors
87// `strategies/cartesian/closest_points_pt_seg.hpp`.
88
89impl<P> ClosestPointsStrategy<P, Segment<P>> for CartesianClosestPoints
90where
91    P: Point<Scalar = f64> + PointMut + Default + Copy,
92    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
93{
94    type Out = P;
95
96    #[inline]
97    fn closest_points(&self, p: &P, s: &Segment<P>) -> (Self::Out, Self::Out) {
98        (*p, foot_on_segment(p, s.start(), s.end()))
99    }
100}
101
102// ---- Segment × Segment -----------------------------------------------
103//
104// If the two segments cross, the closest pair is the shared point
105// (distance 0). Otherwise the minimum is attained by one of the four
106// endpoint-to-opposite-segment projections. Mirrors
107// `strategies/cartesian/closest_points_seg_seg.hpp` reduced to the
108// candidate-projection form.
109
110impl<P> ClosestPointsStrategy<Segment<P>, Segment<P>> for CartesianClosestPoints
111where
112    P: Point<Scalar = f64> + PointMut + Default + Copy,
113    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
114{
115    type Out = P;
116
117    fn closest_points(&self, a: &Segment<P>, b: &Segment<P>) -> (Self::Out, Self::Out) {
118        segment_segment_closest(a.start(), a.end(), b.start(), b.end())
119    }
120}
121
122// ---- Linestring × Linestring -----------------------------------------
123//
124// Walk every sub-segment pair and keep the closest. Mirrors the
125// linear/linear arm at `strategies/cartesian/closest_points_l_l.hpp`.
126//
127// Panics on an empty or single-point linestring (mirrors Boost's
128// empty_input_exception; see the algorithm-layer rustdoc).
129
130impl<P> ClosestPointsStrategy<Linestring<P>, Linestring<P>> for CartesianClosestPoints
131where
132    P: Point<Scalar = f64> + PointMut + Default + Copy,
133    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
134{
135    type Out = P;
136
137    fn closest_points(&self, a: &Linestring<P>, b: &Linestring<P>) -> (Self::Out, Self::Out) {
138        let pa: Vec<&P> = a.points().collect();
139        let pb: Vec<&P> = b.points().collect();
140        assert!(
141            pa.len() >= 2 && pb.len() >= 2,
142            "empty or degenerate linestring in closest_points"
143        );
144
145        let mut best: Option<((P, P), f64)> = None;
146        for wa in pa.windows(2) {
147            for wb in pb.windows(2) {
148                let (ca, cb) = segment_segment_closest(wa[0], wa[1], wb[0], wb[1]);
149                let d = squared_distance(&ca, &cb);
150                if best.is_none_or(|(_, bd)| d < bd) {
151                    best = Some(((ca, cb), d));
152                }
153            }
154        }
155        best.unwrap().0
156    }
157}
158
159// ---- Kernels ---------------------------------------------------------
160
161/// Closest point on segment `a`-`b` to `p`: the clamped foot of the
162/// perpendicular. Mirrors
163/// `closest_points::detail::compute_closest_point_to_segment` in
164/// `strategies/cartesian/closest_points_pt_seg.hpp`.
165fn foot_on_segment<P>(p: &P, a: &P, b: &P) -> P
166where
167    P: Point<Scalar = f64> + PointMut + Default,
168{
169    let (numerator, denominator) = dots(p, a, b);
170    if denominator <= 0.0 {
171        return copy_point(a);
172    }
173    let t = (numerator / denominator).clamp(0.0, 1.0);
174    blend(a, b, t)
175}
176
177/// Closest pair between two segments `(a0,a1)` and `(b0,b1)`.
178fn segment_segment_closest<P>(a0: &P, a1: &P, b0: &P, b1: &P) -> (P, P)
179where
180    P: Point<Scalar = f64> + PointMut + Default,
181{
182    // Crossing segments share a point — the closest pair is that point
183    // on both. Compute it directly from the line-line intersection.
184    if let Some(pt) = segment_intersection(a0, a1, b0, b1) {
185        return (copy_point(&pt), pt);
186    }
187
188    // Otherwise the minimum is one of the four endpoint projections.
189    let c1 = (copy_point(a0), foot_on_segment(a0, b0, b1));
190    let c2 = (copy_point(a1), foot_on_segment(a1, b0, b1));
191    let c3 = (foot_on_segment(b0, a0, a1), copy_point(b0));
192    let c4 = (foot_on_segment(b1, a0, a1), copy_point(b1));
193
194    let mut best = c1;
195    let mut best_d = squared_distance(&best.0, &best.1);
196    for cand in [c2, c3, c4] {
197        let d = squared_distance(&cand.0, &cand.1);
198        if d < best_d {
199            best_d = d;
200            best = cand;
201        }
202    }
203    best
204}
205
206/// Proper-crossing intersection point of two 2D segments, or `None`
207/// when they do not cross (parallel, collinear, or disjoint).
208fn segment_intersection<P>(a0: &P, a1: &P, b0: &P, b1: &P) -> Option<P>
209where
210    P: Point<Scalar = f64> + PointMut + Default,
211{
212    let (x1, y1) = (a0.get::<0>(), a0.get::<1>());
213    let (x2, y2) = (a1.get::<0>(), a1.get::<1>());
214    let (x3, y3) = (b0.get::<0>(), b0.get::<1>());
215    let (x4, y4) = (b1.get::<0>(), b1.get::<1>());
216
217    let denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
218    if denom == 0.0 {
219        return None;
220    }
221    let t = ((x3 - x1) * (y4 - y3) - (y3 - y1) * (x4 - x3)) / denom;
222    let u = ((x3 - x1) * (y2 - y1) - (y3 - y1) * (x2 - x1)) / denom;
223    if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
224        let mut out = P::default();
225        out.set::<0>(x1 + t * (x2 - x1));
226        out.set::<1>(y1 + t * (y2 - y1));
227        Some(out)
228    } else {
229        None
230    }
231}
232
233/// Compute `(dot(p − a, b − a), dot(b − a, b − a))` over 2D.
234#[inline]
235fn dots<P: Point<Scalar = f64>>(p: &P, a: &P, b: &P) -> (f64, f64) {
236    let apx = p.get::<0>() - a.get::<0>();
237    let apy = p.get::<1>() - a.get::<1>();
238    let abx = b.get::<0>() - a.get::<0>();
239    let aby = b.get::<1>() - a.get::<1>();
240    (apx * abx + apy * aby, abx * abx + aby * aby)
241}
242
243/// Squared 2D distance between two points.
244#[inline]
245fn squared_distance<P: Point<Scalar = f64>>(a: &P, b: &P) -> f64 {
246    let dx = a.get::<0>() - b.get::<0>();
247    let dy = a.get::<1>() - b.get::<1>();
248    dx * dx + dy * dy
249}
250
251/// Linear per-dimension blend `out[D] = a[D] + t·(b[D] − a[D])`.
252#[inline]
253fn blend<P>(a: &P, b: &P, t: f64) -> P
254where
255    P: Point<Scalar = f64> + PointMut + Default,
256{
257    let mut out = P::default();
258    geometry_trait::fold_dims((), a, |(), _p, d| {
259        let av = get_dim(a, d);
260        let bv = get_dim(b, d);
261        set_dim(&mut out, d, av + t * (bv - av));
262    });
263    out
264}
265
266/// Copy a point coordinate-by-coordinate (avoids a `Copy` bound where
267/// only `PointMut + Default` is available).
268#[inline]
269fn copy_point<P>(a: &P) -> P
270where
271    P: Point<Scalar = f64> + PointMut + Default,
272{
273    let mut out = P::default();
274    geometry_trait::fold_dims((), a, |(), _p, d| {
275        set_dim(&mut out, d, get_dim(a, d));
276    });
277    out
278}
279
280#[inline]
281fn get_dim<P: Point<Scalar = f64>>(p: &P, d: usize) -> f64 {
282    match d {
283        0 => p.get::<0>(),
284        1 => p.get::<1>(),
285        2 => p.get::<2>(),
286        3 => p.get::<3>(),
287        _ => unreachable!(),
288    }
289}
290
291#[inline]
292fn set_dim<P: PointMut<Scalar = f64>>(p: &mut P, d: usize, v: f64) {
293    match d {
294        0 => p.set::<0>(v),
295        1 => p.set::<1>(v),
296        2 => p.set::<2>(v),
297        3 => p.set::<3>(v),
298        _ => unreachable!(),
299    }
300}
301
302#[cfg(test)]
303#[allow(
304    clippy::float_cmp,
305    reason = "Closest-point coordinates are exact for these inputs."
306)]
307mod tests {
308    //! Reference values mirror the point↔segment cases in
309    //! `boost/geometry/test/algorithms/closest_points/pl_l.cpp` and the
310    //! v1 `PointToSegment` distances from
311    //! `test/strategies/projected_point.cpp`.
312
313    use super::{CartesianClosestPoints, ClosestPointsStrategy};
314    use crate::cartesian::Pythagoras;
315    use crate::distance::DistanceStrategy;
316    use geometry_cs::Cartesian;
317    use geometry_model::{Point2D, Segment};
318    use geometry_trait::Point as _;
319
320    type Pt = Point2D<f64, Cartesian>;
321
322    #[test]
323    fn point_above_segment_drops_perpendicular() {
324        let p = Pt::new(0., 5.);
325        let s = Segment::new(Pt::new(0., 0.), Pt::new(10., 0.));
326        let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
327        assert_eq!((a.get::<0>(), a.get::<1>()), (0., 5.));
328        assert_eq!((b.get::<0>(), b.get::<1>()), (0., 0.));
329        assert!((Pythagoras.distance(&a, &b) - 5.0).abs() < 1e-12);
330    }
331
332    #[test]
333    fn point_on_segment_returns_input() {
334        let p = Pt::new(1., 1.);
335        let s = Segment::new(Pt::new(0., 0.), Pt::new(3., 3.));
336        let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
337        assert!((a.get::<0>() - 1.0).abs() < 1e-12);
338        assert!((b.get::<0>() - 1.0).abs() < 1e-12);
339        assert!(Pythagoras.distance(&a, &b) < 1e-12);
340    }
341
342    #[test]
343    fn point_beyond_segment_clamps_to_endpoint() {
344        // POINT(6 1) to segment (1 4)-(4 1): projects past (4 1), so the
345        // closest point on the segment is that endpoint; distance 2.
346        let p = Pt::new(6., 1.);
347        let s = Segment::new(Pt::new(1., 4.), Pt::new(4., 1.));
348        let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
349        assert_eq!((b.get::<0>(), b.get::<1>()), (4., 1.));
350        assert!((Pythagoras.distance(&a, &b) - 2.0).abs() < 1e-9);
351    }
352
353    #[test]
354    fn crossing_segments_share_intersection_point() {
355        let a = Segment::new(Pt::new(0., 0.), Pt::new(2., 2.));
356        let b = Segment::new(Pt::new(0., 2.), Pt::new(2., 0.));
357        let (ca, cb) = CartesianClosestPoints.closest_points(&a, &b);
358        assert!((ca.get::<0>() - 1.0).abs() < 1e-12);
359        assert!((ca.get::<1>() - 1.0).abs() < 1e-12);
360        assert!(Pythagoras.distance(&ca, &cb) < 1e-12);
361    }
362}