Skip to main content

geometry_algorithm/
dyn_distance.rs

1//! `distance_dyn` — runtime-dispatched distance between two
2//! [`DynGeometry`] inputs.
3//!
4//! Mirrors the variant-aware overload of
5//! `boost::geometry::distance(g1, g2)` reached when at least one
6//! argument has tag `dynamic_geometry_tag`
7//! (`algorithms/distance.hpp` + `geometries/adapted/boost_variant.hpp`).
8//! On the Rust side we make the dispatch explicit:
9//!
10//! 1. `match (a, b)` on the kind pairs.
11//! 2. Each arm calls the static distance kernels from `crate::distance`
12//!    on the unwrapped concrete types — dispatch inside the arm is
13//!    monomorphic and produces the same code as a direct call would.
14//! 3. Pairs with no static impl (e.g. `Polygon × Polygon` in v1) return
15//!    `Err(DynKindMismatch)` rather than panicking.
16
17use geometry_coords::CoordinateScalar;
18use geometry_cs::{CartesianFamily, CoordinateSystem};
19use geometry_model::{DynGeometry, DynKind, Linestring, Point, Segment};
20use geometry_strategy::{
21    DefaultDistance, DefaultDistanceStrategy, DistanceStrategy, PointToSegment, Pythagoras,
22};
23use geometry_tag::SameAs;
24use geometry_trait::Linestring as LinestringTrait;
25
26use crate::distance::{distance, distance_with};
27use crate::dyn_error::DynKindMismatch;
28
29/// The supported `(left, right)` kind combinations for `distance_dyn`.
30/// Mirrors the per-tag-pair static specialisations in
31/// `algorithms/distance.hpp` + the leaf strategies under
32/// `strategies/cartesian/distance_*.hpp`.
33const SUPPORTED: &[&[DynKind]] = &[
34    &[DynKind::Point, DynKind::Point],
35    &[DynKind::Point, DynKind::LineString], // via PointToSegment per segment
36    &[DynKind::LineString, DynKind::Point], // reversed
37];
38
39/// Runtime-dispatched distance.
40///
41/// Returns `Ok(d)` for any pair v1's static `distance` supports
42/// (point-point, and point-linestring via `PointToSegment`).
43///
44/// The Cartesian family bound reflects that `PointToSegment` (T24) is
45/// Cartesian-only in v1; point-point works for any family, but the
46/// shared bound keeps the wrapper's `where` clause a single line.
47///
48/// # Errors
49///
50/// Returns `Err(DynKindMismatch)` for any kind pair with no static
51/// distance impl (e.g. polygon-polygon).
52///
53/// # Panics
54///
55/// Panics on an empty linestring: there is no point to measure to, and
56/// `0` would read as "touching". Boost throws `empty_input_exception`
57/// here; the port panics with a clear message, as
58/// [`discrete_hausdorff_distance`](crate::discrete_hausdorff_distance)
59/// does.
60#[allow(
61    clippy::match_same_arms,
62    reason = "The two point↔linestring arms differ by argument order; keeping them separate documents both directions."
63)]
64pub fn distance_dyn<S, Cs>(
65    a: &DynGeometry<S, Cs>,
66    b: &DynGeometry<S, Cs>,
67) -> Result<S, DynKindMismatch>
68where
69    S: CoordinateScalar,
70    Cs: CoordinateSystem + Copy,
71    Cs::Family: SameAs<CartesianFamily> + DefaultDistance<Cs::Family>,
72    DefaultDistanceStrategy<Point<S, 2, Cs>, Point<S, 2, Cs>>:
73        DistanceStrategy<Point<S, 2, Cs>, Point<S, 2, Cs>, Out = S> + Default,
74    PointToSegment<Pythagoras>:
75        DistanceStrategy<Point<S, 2, Cs>, Segment<Point<S, 2, Cs>>, Out = S>,
76{
77    use DynGeometry::{LineString, Point as PointArm};
78    match (a, b) {
79        (PointArm(p), PointArm(q)) => Ok(distance(p, q)),
80        (PointArm(p), LineString(ls)) => Ok(point_to_linestring(p, ls)),
81        (LineString(ls), PointArm(p)) => Ok(point_to_linestring(p, ls)),
82        _ => Err(DynKindMismatch {
83            got: alloc::vec![a.kind(), b.kind()],
84            expected: SUPPORTED,
85        }),
86    }
87}
88
89/// Minimum distance from a point to any segment of a linestring.
90///
91/// Mirrors `algorithms/detail/distance/point_to_geometry.hpp`: the
92/// point-to-linestring distance is the minimum point-to-segment
93/// distance over the linestring's segments.
94fn point_to_linestring<S, Cs>(p: &Point<S, 2, Cs>, ls: &Linestring<Point<S, 2, Cs>>) -> S
95where
96    S: CoordinateScalar,
97    Cs: CoordinateSystem + Copy,
98    Cs::Family: SameAs<CartesianFamily>,
99    PointToSegment<Pythagoras>:
100        DistanceStrategy<Point<S, 2, Cs>, Segment<Point<S, 2, Cs>>, Out = S>,
101{
102    let strategy = PointToSegment::<Pythagoras>::default();
103    let mut best: Option<S> = None;
104    let mut prev: Option<Point<S, 2, Cs>> = None;
105    for pt in ls.points() {
106        if let Some(start) = prev {
107            let seg = Segment::new(start, *pt);
108            let d = distance_with(p, &seg, strategy);
109            best = Some(match best {
110                Some(m) if m <= d => m,
111                _ => d,
112            });
113        }
114        prev = Some(*pt);
115    }
116    // Degenerate: fewer than two points. A single vertex is handled as
117    // a zero-length segment (its point-to-segment distance is the
118    // point-to-vertex distance); an empty linestring has no distance.
119    best.unwrap_or_else(|| match ls.points().next() {
120        Some(v) => distance_with(p, &Segment::new(*v, *v), strategy),
121        None => panic!("distance_dyn: empty linestring has no distance to a point"),
122    })
123}