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}