Skip to main content

geometry_algorithm/
closest_points.rs

1//! `closest_points(&a, &b) -> (Point, Point)` — nearest-point pair.
2//!
3//! Mirrors `boost::geometry::closest_points(g1, g2, segment_out)` from
4//! `boost/geometry/algorithms/closest_points.hpp`. Boost returns the
5//! closest pair as a `Segment`; the Rust port returns a `(Point, Point)`
6//! tuple — same information, no `Segment::new` boilerplate at the call
7//! site. The first returned point lies on `a`, the second on `b`.
8//!
9//! v1 ships the Cartesian pairs the Boost test fixtures cover:
10//! point↔point, point↔segment, segment↔segment, and
11//! linestring↔linestring. The areal (polygon) pairs depend on overlay
12//! machinery and land in `phase_03`.
13
14use geometry_strategy::{CartesianClosestPoints, ClosestPointsStrategy};
15
16/// Return the pair of nearest points on `(a, b)` — `(pa, pb)` where
17/// `pa` lies on `a`, `pb` on `b`, and `|pa − pb|` is minimal.
18///
19/// Mirrors `boost::geometry::closest_points` from
20/// `boost/geometry/algorithms/closest_points.hpp`. The distance between
21/// the returned points equals the geometry-pair distance
22/// `distance(a, b)`.
23///
24/// # Panics
25///
26/// Panics if a linestring operand has fewer than 2 points — Boost
27/// treats empty input as an error (`empty_input_exception`); the
28/// Rust port panics with a clear message. Point and segment
29/// operands cannot be empty and never panic.
30#[inline]
31#[must_use]
32pub fn closest_points<A, B>(
33    a: &A,
34    b: &B,
35) -> (
36    <CartesianClosestPoints as ClosestPointsStrategy<A, B>>::Out,
37    <CartesianClosestPoints as ClosestPointsStrategy<A, B>>::Out,
38)
39where
40    CartesianClosestPoints: ClosestPointsStrategy<A, B>,
41{
42    CartesianClosestPoints.closest_points(a, b)
43}
44
45#[cfg(test)]
46#[allow(
47    clippy::float_cmp,
48    reason = "Closest-point coordinates are exact for these inputs."
49)]
50mod tests {
51    //! Reference values mirror the point↔segment cases in
52    //! `boost/geometry/test/algorithms/closest_points/pl_l.cpp` and the
53    //! v1 `PointToSegment` distances (`test/strategies/projected_point.cpp`):
54    //! the distance between the returned closest points equals the
55    //! geometry-pair distance.
56
57    use super::closest_points;
58    use geometry_cs::Cartesian;
59    use geometry_model::{Linestring, Point2D, Segment};
60    use geometry_strategy::{DistanceStrategy, Pythagoras};
61    use geometry_trait::Point as _;
62
63    type Pt = Point2D<f64, Cartesian>;
64
65    #[test]
66    fn point_above_segment_drops_perpendicular() {
67        // (0,5) to segment (0,0)-(10,0) → closest pair ((0,5), (0,0)).
68        let p = Pt::new(0., 5.);
69        let s = Segment::new(Pt::new(0., 0.), Pt::new(10., 0.));
70        let (a, b) = closest_points(&p, &s);
71        assert_eq!((a.get::<0>(), a.get::<1>()), (0., 5.));
72        assert_eq!((b.get::<0>(), b.get::<1>()), (0., 0.));
73        assert!((Pythagoras.distance(&a, &b) - 5.0).abs() < 1e-12);
74    }
75
76    #[test]
77    fn point_on_segment_returns_input() {
78        let p = Pt::new(1., 1.);
79        let s = Segment::new(Pt::new(0., 0.), Pt::new(3., 3.));
80        let (a, b) = closest_points(&p, &s);
81        assert!(Pythagoras.distance(&a, &b) < 1e-12);
82    }
83
84    #[test]
85    fn crossing_segments_share_intersection_point() {
86        // Two crossing segments → the intersection point (1,1) on both.
87        let a = Segment::new(Pt::new(0., 0.), Pt::new(2., 2.));
88        let b = Segment::new(Pt::new(0., 2.), Pt::new(2., 0.));
89        let (ca, cb) = closest_points(&a, &b);
90        assert!((ca.get::<0>() - 1.0).abs() < 1e-12 && (ca.get::<1>() - 1.0).abs() < 1e-12);
91        assert!(Pythagoras.distance(&ca, &cb) < 1e-12);
92    }
93
94    #[test]
95    fn parallel_linestrings_closest_pair() {
96        // Two horizontal tracks 3 apart; nearest pair is vertically
97        // aligned, distance 3.
98        let a: Linestring<Pt> =
99            Linestring::from_vec(alloc::vec![Pt::new(0., 0.), Pt::new(10., 0.),]);
100        let b: Linestring<Pt> =
101            Linestring::from_vec(alloc::vec![Pt::new(2., 3.), Pt::new(8., 3.),]);
102        let (ca, cb) = closest_points(&a, &b);
103        assert!((Pythagoras.distance(&ca, &cb) - 3.0).abs() < 1e-9);
104    }
105
106    #[test]
107    #[should_panic(expected = "empty or degenerate linestring in closest_points")]
108    fn degenerate_linestring_panics() {
109        let a: Linestring<Pt> = Linestring::from_vec(alloc::vec![Pt::new(0., 0.)]);
110        let b: Linestring<Pt> =
111            Linestring::from_vec(alloc::vec![Pt::new(0., 0.), Pt::new(1., 0.),]);
112        let _ = closest_points(&a, &b);
113    }
114}