1use 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
24type Family<P> = <<P as Point>::Cs as CoordinateSystem>::Family;
26
27type 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#[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#[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 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 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 #[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 #[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 #[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 #[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}