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, fold_dims, ordinate};
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. The
184    // crossing test is planar, so it only applies to 2-D points; higher
185    // dimensions fall through to the endpoint projections.
186    if P::DIM == 2 {
187        if let Some(pt) = segment_intersection(a0, a1, b0, b1) {
188            return (copy_point(&pt), pt);
189        }
190    }
191
192    // Otherwise the minimum is one of the four endpoint projections.
193    let c1 = (copy_point(a0), foot_on_segment(a0, b0, b1));
194    let c2 = (copy_point(a1), foot_on_segment(a1, b0, b1));
195    let c3 = (foot_on_segment(b0, a0, a1), copy_point(b0));
196    let c4 = (foot_on_segment(b1, a0, a1), copy_point(b1));
197
198    let mut best = c1;
199    let mut best_d = squared_distance(&best.0, &best.1);
200    for cand in [c2, c3, c4] {
201        let d = squared_distance(&cand.0, &cand.1);
202        if d < best_d {
203            best_d = d;
204            best = cand;
205        }
206    }
207    best
208}
209
210/// Proper-crossing intersection point of two 2D segments, or `None`
211/// when they do not cross (parallel, collinear, or disjoint).
212fn segment_intersection<P>(a0: &P, a1: &P, b0: &P, b1: &P) -> Option<P>
213where
214    P: Point<Scalar = f64> + PointMut + Default,
215{
216    let (x1, y1) = (a0.get::<0>(), a0.get::<1>());
217    let (x2, y2) = (a1.get::<0>(), a1.get::<1>());
218    let (x3, y3) = (b0.get::<0>(), b0.get::<1>());
219    let (x4, y4) = (b1.get::<0>(), b1.get::<1>());
220
221    let denom = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3);
222    if denom == 0.0 {
223        return None;
224    }
225    let t = ((x3 - x1) * (y4 - y3) - (y3 - y1) * (x4 - x3)) / denom;
226    let u = ((x3 - x1) * (y2 - y1) - (y3 - y1) * (x2 - x1)) / denom;
227    if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
228        let mut out = P::default();
229        out.set::<0>(x1 + t * (x2 - x1));
230        out.set::<1>(y1 + t * (y2 - y1));
231        Some(out)
232    } else {
233        None
234    }
235}
236
237/// Compute `(dot(p − a, b − a), dot(b − a, b − a))` over every dimension.
238#[inline]
239fn dots<P: Point<Scalar = f64>>(p: &P, a: &P, b: &P) -> (f64, f64) {
240    fold_dims((0.0, 0.0), p, |(ap_ab, ab_ab), p, d| {
241        let ap = ordinate(p, d) - ordinate(a, d);
242        let ab = ordinate(b, d) - ordinate(a, d);
243        (ap_ab + ap * ab, ab_ab + ab * ab)
244    })
245}
246
247/// Squared distance between two points over every dimension.
248#[inline]
249fn squared_distance<P: Point<Scalar = f64>>(a: &P, b: &P) -> f64 {
250    fold_dims(0.0, a, |sum, a, d| {
251        let delta = ordinate(a, d) - ordinate(b, d);
252        sum + delta * delta
253    })
254}
255
256/// Linear per-dimension blend `out[D] = a[D] + t·(b[D] − a[D])`.
257#[inline]
258fn blend<P>(a: &P, b: &P, t: f64) -> P
259where
260    P: Point<Scalar = f64> + PointMut + Default,
261{
262    let mut out = P::default();
263    geometry_trait::fold_dims((), a, |(), _p, d| {
264        let av = get_dim(a, d);
265        let bv = get_dim(b, d);
266        set_dim(&mut out, d, av + t * (bv - av));
267    });
268    out
269}
270
271/// Copy a point coordinate-by-coordinate (avoids a `Copy` bound where
272/// only `PointMut + Default` is available).
273#[inline]
274fn copy_point<P>(a: &P) -> P
275where
276    P: Point<Scalar = f64> + PointMut + Default,
277{
278    let mut out = P::default();
279    geometry_trait::fold_dims((), a, |(), _p, d| {
280        set_dim(&mut out, d, get_dim(a, d));
281    });
282    out
283}
284
285#[inline]
286fn get_dim<P: Point<Scalar = f64>>(p: &P, d: usize) -> f64 {
287    match d {
288        0 => p.get::<0>(),
289        1 => p.get::<1>(),
290        2 => p.get::<2>(),
291        3 => p.get::<3>(),
292        _ => unreachable!(),
293    }
294}
295
296#[inline]
297fn set_dim<P: PointMut<Scalar = f64>>(p: &mut P, d: usize, v: f64) {
298    match d {
299        0 => p.set::<0>(v),
300        1 => p.set::<1>(v),
301        2 => p.set::<2>(v),
302        3 => p.set::<3>(v),
303        _ => unreachable!(),
304    }
305}
306
307#[cfg(test)]
308#[allow(
309    clippy::float_cmp,
310    reason = "Closest-point coordinates are exact for these inputs."
311)]
312mod tests {
313    //! Reference values mirror the point↔segment cases in
314    //! `boost/geometry/test/algorithms/closest_points/pl_l.cpp` and the
315    //! v1 `PointToSegment` distances from
316    //! `test/strategies/projected_point.cpp`.
317
318    use super::{CartesianClosestPoints, ClosestPointsStrategy};
319    use crate::cartesian::Pythagoras;
320    use crate::distance::DistanceStrategy;
321    use geometry_cs::Cartesian;
322    use geometry_model::{Point2D, Segment};
323    use geometry_trait::Point as _;
324
325    type Pt = Point2D<f64, Cartesian>;
326
327    #[test]
328    fn point_above_segment_drops_perpendicular() {
329        let p = Pt::new(0., 5.);
330        let s = Segment::new(Pt::new(0., 0.), Pt::new(10., 0.));
331        let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
332        assert_eq!((a.get::<0>(), a.get::<1>()), (0., 5.));
333        assert_eq!((b.get::<0>(), b.get::<1>()), (0., 0.));
334        assert!((Pythagoras.distance(&a, &b) - 5.0).abs() < 1e-12);
335    }
336
337    #[test]
338    fn point_on_segment_returns_input() {
339        let p = Pt::new(1., 1.);
340        let s = Segment::new(Pt::new(0., 0.), Pt::new(3., 3.));
341        let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
342        assert!((a.get::<0>() - 1.0).abs() < 1e-12);
343        assert!((b.get::<0>() - 1.0).abs() < 1e-12);
344        assert!(Pythagoras.distance(&a, &b) < 1e-12);
345    }
346
347    #[test]
348    fn point_beyond_segment_clamps_to_endpoint() {
349        // POINT(6 1) to segment (1 4)-(4 1): projects past (4 1), so the
350        // closest point on the segment is that endpoint; distance 2.
351        let p = Pt::new(6., 1.);
352        let s = Segment::new(Pt::new(1., 4.), Pt::new(4., 1.));
353        let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
354        assert_eq!((b.get::<0>(), b.get::<1>()), (4., 1.));
355        assert!((Pythagoras.distance(&a, &b) - 2.0).abs() < 1e-9);
356    }
357
358    #[test]
359    fn crossing_segments_share_intersection_point() {
360        let a = Segment::new(Pt::new(0., 0.), Pt::new(2., 2.));
361        let b = Segment::new(Pt::new(0., 2.), Pt::new(2., 0.));
362        let (ca, cb) = CartesianClosestPoints.closest_points(&a, &b);
363        assert!((ca.get::<0>() - 1.0).abs() < 1e-12);
364        assert!((ca.get::<1>() - 1.0).abs() < 1e-12);
365        assert!(Pythagoras.distance(&ca, &cb) < 1e-12);
366    }
367
368    /// The point ↔ segment pair walks every dimension: a point on a
369    /// vertical 3-D segment is its own foot, and the pair's distance
370    /// agrees with `PointToSegment`, which already folds all dimensions.
371    #[test]
372    fn three_dimensional_point_on_vertical_segment_is_its_own_foot() {
373        use geometry_model::Point3D;
374        type P3 = Point3D<f64, Cartesian>;
375        let p = P3::new(0., 0., 5.);
376        let s = Segment::new(P3::new(0., 0., 0.), P3::new(0., 0., 10.));
377        let (a, b) = CartesianClosestPoints.closest_points(&p, &s);
378        assert_eq!((a.get::<0>(), a.get::<1>(), a.get::<2>()), (0., 0., 5.));
379        assert_eq!((b.get::<0>(), b.get::<1>(), b.get::<2>()), (0., 0., 5.));
380        let via_distance = crate::PointToSegment::<Pythagoras>::default().distance(&p, &s);
381        assert!((Pythagoras.distance(&a, &b) - via_distance).abs() < 1e-12);
382    }
383}