Skip to main content

geometry_algorithm/
discrete_frechet.rs

1//! `discrete_frechet_distance(&l1, &l2)` — DP table over the two
2//! point sequences.
3//!
4//! Mirrors `boost::geometry::discrete_frechet_distance` from
5//! `boost/geometry/algorithms/discrete_frechet_distance.hpp`. The
6//! discrete variant operates on vertex pairs only (no continuous
7//! re-parameterisation); the coupling-measure DP table is
8//!
9//! ```text
10//! C(i, j) = max( min( C(i-1, j), C(i-1, j-1), C(i, j-1) ),
11//!                dist(L1[i], L2[j]) )
12//! result  = C(m-1, n-1)
13//! ```
14//!
15//! with `C(0, 0) = dist(L1[0], L2[0])`.
16
17use alloc::vec::Vec;
18
19use geometry_cs::CoordinateSystem;
20use geometry_strategy::distance::DefaultDistance;
21use geometry_strategy::{DefaultDistanceStrategy, DistanceStrategy};
22use geometry_trait::{Geometry, Linestring, Point};
23
24/// Shorthand for the coordinate-system family of a point type.
25type Family<P> = <<P as Point>::Cs as CoordinateSystem>::Family;
26
27/// The scalar output type of the default distance strategy between the
28/// point types of two linestrings.
29type DefaultDistOut<L1, L2> = <DefaultDistanceStrategy<
30    <L1 as Geometry>::Point,
31    <L2 as Geometry>::Point,
32> as DistanceStrategy<<L1 as Geometry>::Point, <L2 as Geometry>::Point>>::Out;
33
34/// Discrete Fréchet distance between two linestrings, using the
35/// default distance strategy for their coordinate systems (Pythagoras
36/// for Cartesian, Andoyer for Geographic, …).
37///
38/// Mirrors `boost::geometry::discrete_frechet_distance(l1, l2)` from
39/// `boost/geometry/algorithms/discrete_frechet_distance.hpp`.
40///
41/// # Panics
42///
43/// Panics on an empty linestring — Boost throws; the Rust port panics
44/// with a clear message.
45#[inline]
46#[must_use]
47pub fn discrete_frechet_distance<L1, L2>(l1: &L1, l2: &L2) -> DefaultDistOut<L1, L2>
48where
49    L1: Linestring,
50    L2: Linestring,
51    Family<L1::Point>: DefaultDistance<Family<L2::Point>>,
52    DefaultDistanceStrategy<L1::Point, L2::Point>: DistanceStrategy<L1::Point, L2::Point> + Default,
53{
54    let s = <DefaultDistanceStrategy<L1::Point, L2::Point>>::default();
55    discrete_frechet_distance_with(l1, l2, s)
56}
57
58/// Discrete Fréchet distance between two linestrings using an explicit
59/// distance strategy `dist`.
60///
61/// Mirrors the strategy-taking `boost::geometry::discrete_frechet_distance`
62/// overload from `boost/geometry/algorithms/discrete_frechet_distance.hpp`.
63///
64/// # Panics
65///
66/// Panics if either linestring is empty.
67#[must_use]
68#[allow(
69    clippy::needless_pass_by_value,
70    reason = "Distance strategies are zero-sized/Copy; taking by value matches `distance_with`."
71)]
72pub fn discrete_frechet_distance_with<L1, L2, S>(l1: &L1, l2: &L2, dist: S) -> S::Out
73where
74    L1: Linestring,
75    L2: Linestring,
76    S: DistanceStrategy<L1::Point, L2::Point>,
77    S::Out: PartialOrd + Copy,
78{
79    let seq1: Vec<&L1::Point> = l1.points().collect();
80    let seq2: Vec<&L2::Point> = l2.points().collect();
81    assert!(
82        !seq1.is_empty() && !seq2.is_empty(),
83        "empty linestring in discrete_frechet"
84    );
85
86    let len1 = seq1.len();
87    let len2 = seq2.len();
88    let mut table: Vec<Vec<S::Out>> = (0..len1).map(|_| Vec::with_capacity(len2)).collect();
89
90    // Base case + first row + first column.
91    table[0].push(dist.distance(seq1[0], seq2[0]));
92    for j in 1..len2 {
93        let here = dist.distance(seq1[0], seq2[j]);
94        let prev = table[0][j - 1];
95        table[0].push(if here > prev { here } else { prev });
96    }
97    for i in 1..len1 {
98        let here = dist.distance(seq1[i], seq2[0]);
99        let prev = table[i - 1][0];
100        table[i].push(if here > prev { here } else { prev });
101    }
102
103    for i in 1..len1 {
104        for j in 1..len2 {
105            let here = dist.distance(seq1[i], seq2[j]);
106            let mut best = table[i - 1][j];
107            if table[i][j - 1] < best {
108                best = table[i][j - 1];
109            }
110            if table[i - 1][j - 1] < best {
111                best = table[i - 1][j - 1];
112            }
113            table[i].push(if here > best { here } else { best });
114        }
115    }
116
117    table[len1 - 1][len2 - 1]
118}
119
120#[cfg(test)]
121#[allow(
122    clippy::float_cmp,
123    reason = "Fréchet reference values compared with an epsilon."
124)]
125mod tests {
126    //! Reference values from
127    //! `boost/geometry/test/algorithms/similarity/discrete_frechet_distance.cpp:28-35`.
128
129    use super::discrete_frechet_distance;
130    use geometry_cs::Cartesian;
131    use geometry_model::{Linestring, Point2D, linestring};
132
133    type Pt = Point2D<f64, Cartesian>;
134
135    /// `discrete_frechet_distance.cpp:28` — coupling distance 3.
136    #[test]
137    fn diagonal_distance_3() {
138        let a: Linestring<Pt> = linestring![(3., 0.), (2., 1.), (3., 2.)];
139        let b: Linestring<Pt> = linestring![(0., 0.), (3., 4.), (4., 3.)];
140        assert!((discrete_frechet_distance(&a, &b) - 3.0).abs() < 1e-9);
141    }
142
143    /// `discrete_frechet_distance.cpp:30` — identical linestrings → 0.
144    #[test]
145    fn identical_linestrings_distance_zero() {
146        let ls: Linestring<Pt> = linestring![(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)];
147        assert!(discrete_frechet_distance(&ls, &ls) < 1e-12);
148    }
149
150    /// `discrete_frechet_distance.cpp:31` — reversed unit square = √2.
151    #[test]
152    fn reversed_unit_square() {
153        let a: Linestring<Pt> = linestring![(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)];
154        let b: Linestring<Pt> = linestring![(1., 1.), (0., 1.), (0., 0.), (1., 0.), (1., 1.)];
155        assert!((discrete_frechet_distance(&a, &b) - 2.0_f64.sqrt()).abs() < 1e-9);
156    }
157
158    /// `discrete_frechet_distance.cpp:35` — opposite traversal, 3-4-5 → 5.
159    #[test]
160    fn opposite_traversal_3_4_5() {
161        let a: Linestring<Pt> = linestring![(0., 0.), (3., 4.), (4., 3.)];
162        let b: Linestring<Pt> = linestring![(4., 3.), (3., 4.), (0., 0.)];
163        assert!((discrete_frechet_distance(&a, &b) - 5.0).abs() < 1e-9);
164    }
165}