geo/algorithm/
chaikin_smoothing.rs

1use std::ops::Mul;
2
3use num_traits::FromPrimitive;
4
5use crate::{
6    coord, Coord, CoordFloat, Geometry, LineString, MultiLineString, MultiPolygon, Polygon,
7};
8
9/// Smoothen `LineString`, `Polygon`, `MultiLineString` and `MultiPolygon` using Chaikins algorithm.
10///
11/// [Chaikins smoothing algorithm](http://www.idav.ucdavis.edu/education/CAGDNotes/Chaikins-Algorithm/Chaikins-Algorithm.html)
12///
13/// Each iteration of the smoothing doubles the number of vertices of the geometry, so in some
14/// cases it may make sense to apply a simplification afterwards to remove insignificant
15/// coordinates.
16///
17/// This implementation preserves the start and end vertices of an open linestring and
18/// smoothes the corner between start and end of a closed linestring.
19pub trait ChaikinSmoothing<T>
20where
21    T: CoordFloat + FromPrimitive,
22{
23    /// create a new geometry with the Chaikin smoothing being
24    /// applied `n_iterations` times.
25    fn chaikin_smoothing(&self, n_iterations: usize) -> Self;
26}
27
28impl<T> ChaikinSmoothing<T> for LineString<T>
29where
30    T: CoordFloat + FromPrimitive,
31{
32    fn chaikin_smoothing(&self, n_iterations: usize) -> Self {
33        if n_iterations == 0 {
34            self.clone()
35        } else {
36            let mut smooth = smoothen_linestring(self);
37            for _ in 0..(n_iterations - 1) {
38                smooth = smoothen_linestring(&smooth);
39            }
40            smooth
41        }
42    }
43}
44
45impl<T> ChaikinSmoothing<T> for MultiLineString<T>
46where
47    T: CoordFloat + FromPrimitive,
48{
49    fn chaikin_smoothing(&self, n_iterations: usize) -> Self {
50        MultiLineString::new(
51            self.0
52                .iter()
53                .map(|ls| ls.chaikin_smoothing(n_iterations))
54                .collect(),
55        )
56    }
57}
58
59impl<T> ChaikinSmoothing<T> for Polygon<T>
60where
61    T: CoordFloat + FromPrimitive,
62{
63    fn chaikin_smoothing(&self, n_iterations: usize) -> Self {
64        Polygon::new(
65            self.exterior().chaikin_smoothing(n_iterations),
66            self.interiors()
67                .iter()
68                .map(|ls| ls.chaikin_smoothing(n_iterations))
69                .collect(),
70        )
71    }
72}
73
74impl<T> ChaikinSmoothing<T> for MultiPolygon<T>
75where
76    T: CoordFloat + FromPrimitive,
77{
78    fn chaikin_smoothing(&self, n_iterations: usize) -> Self {
79        MultiPolygon::new(
80            self.0
81                .iter()
82                .map(|poly| poly.chaikin_smoothing(n_iterations))
83                .collect(),
84        )
85    }
86}
87
88macro_rules! blanket_run_chaikin_smoothing {
89    ($geo:expr, $n_iter:expr) => {{
90        let smooth = $geo.chaikin_smoothing($n_iter);
91        let geo: Geometry<T> = smooth.into();
92        geo
93    }};
94}
95
96impl<T> ChaikinSmoothing<T> for Geometry<T>
97where
98    T: CoordFloat + FromPrimitive,
99{
100    fn chaikin_smoothing(&self, n_iterations: usize) -> Geometry<T> {
101        match self {
102            Geometry::LineString(child) => blanket_run_chaikin_smoothing!(child, n_iterations),
103            Geometry::MultiLineString(child) => blanket_run_chaikin_smoothing!(child, n_iterations),
104            Geometry::Polygon(child) => blanket_run_chaikin_smoothing!(child, n_iterations),
105            Geometry::MultiPolygon(child) => blanket_run_chaikin_smoothing!(child, n_iterations),
106            _ => self.clone(),
107        }
108    }
109}
110
111fn smoothen_linestring<T>(linestring: &LineString<T>) -> LineString<T>
112where
113    T: CoordFloat + Mul<T> + FromPrimitive,
114{
115    let mut out_coords: Vec<_> = Vec::with_capacity(linestring.0.len() * 2);
116
117    if let (Some(first), Some(last)) = (linestring.0.first(), linestring.0.last()) {
118        if first != last {
119            // preserve start coordinate when the linestring is open
120            out_coords.push(*first);
121        }
122    }
123    for window_coordinates in linestring.0.windows(2) {
124        let (q, r) = smoothen_coordinates(window_coordinates[0], window_coordinates[1]);
125        out_coords.push(q);
126        out_coords.push(r);
127    }
128
129    if let (Some(first), Some(last)) = (linestring.0.first(), linestring.0.last()) {
130        if first != last {
131            // preserve the last coordinate of an open linestring
132            out_coords.push(*last);
133        } else {
134            // smoothen the edge between the beginning and the end in closed
135            // linestrings while keeping the linestring closed.
136            if let Some(out_first) = out_coords.first().copied() {
137                out_coords.push(out_first);
138            }
139        }
140    }
141    out_coords.into()
142}
143
144fn smoothen_coordinates<T>(c0: Coord<T>, c1: Coord<T>) -> (Coord<T>, Coord<T>)
145where
146    T: CoordFloat + Mul<T> + FromPrimitive,
147{
148    let q = coord! {
149        x: (T::from(0.75).unwrap() * c0.x) + (T::from(0.25).unwrap() * c1.x),
150        y: (T::from(0.75).unwrap() * c0.y) + (T::from(0.25).unwrap() * c1.y),
151    };
152    let r = coord! {
153        x: (T::from(0.25).unwrap() * c0.x) + (T::from(0.75).unwrap() * c1.x),
154        y: (T::from(0.25).unwrap() * c0.y) + (T::from(0.75).unwrap() * c1.y),
155    };
156    (q, r)
157}
158
159#[cfg(test)]
160mod test {
161    use crate::ChaikinSmoothing;
162    use crate::{Geometry, LineString, Point, Polygon};
163
164    #[test]
165    fn geometry() {
166        // Test implemented geometry
167        let ls = LineString::from(vec![(3.0, 0.0), (6.0, 3.0), (3.0, 6.0), (0.0, 3.0)]);
168        let ls_geo: Geometry = ls.into();
169        let ls_geo_out = ls_geo.chaikin_smoothing(1);
170        let ls_out: LineString = ls_geo_out.try_into().unwrap();
171        assert_eq!(
172            ls_out,
173            LineString::from(vec![
174                (3.0, 0.0),
175                (3.75, 0.75),
176                (5.25, 2.25),
177                (5.25, 3.75),
178                (3.75, 5.25),
179                (2.25, 5.25),
180                (0.75, 3.75),
181                (0.0, 3.0),
182            ])
183        );
184
185        // Test unimplemented geometry
186        let pt = Point::from((3.0, 0.0));
187        let pt_geo: Geometry = pt.into();
188        let pt_geo_out = pt_geo.chaikin_smoothing(1);
189        let pt_out: Point = pt_geo_out.try_into().unwrap();
190        assert_eq!(pt_out, Point::from((3.0, 0.0)));
191    }
192
193    #[test]
194    fn linestring_open() {
195        let ls = LineString::from(vec![(3.0, 0.0), (6.0, 3.0), (3.0, 6.0), (0.0, 3.0)]);
196        let ls_out = ls.chaikin_smoothing(1);
197        assert_eq!(
198            ls_out,
199            LineString::from(vec![
200                (3.0, 0.0),
201                (3.75, 0.75),
202                (5.25, 2.25),
203                (5.25, 3.75),
204                (3.75, 5.25),
205                (2.25, 5.25),
206                (0.75, 3.75),
207                (0.0, 3.0),
208            ])
209        );
210    }
211
212    #[test]
213    fn linestring_closed() {
214        let ls = LineString::from(vec![
215            (3.0, 0.0),
216            (6.0, 3.0),
217            (3.0, 6.0),
218            (0.0, 3.0),
219            (3.0, 0.0),
220        ]);
221        let ls_out = ls.chaikin_smoothing(1);
222        assert_eq!(
223            ls_out,
224            LineString::from(vec![
225                (3.75, 0.75),
226                (5.25, 2.25),
227                (5.25, 3.75),
228                (3.75, 5.25),
229                (2.25, 5.25),
230                (0.75, 3.75),
231                (0.75, 2.25),
232                (2.25, 0.75),
233                (3.75, 0.75)
234            ])
235        );
236    }
237
238    #[test]
239    fn polygon() {
240        let poly = Polygon::new(
241            LineString::from(vec![
242                (3.0, 0.0),
243                (6.0, 3.0),
244                (3.0, 6.0),
245                (0.0, 3.0),
246                (3.0, 0.0),
247            ]),
248            vec![],
249        );
250        let poly_out = poly.chaikin_smoothing(1);
251        assert_eq!(
252            poly_out.exterior(),
253            &LineString::from(vec![
254                (3.75, 0.75),
255                (5.25, 2.25),
256                (5.25, 3.75),
257                (3.75, 5.25),
258                (2.25, 5.25),
259                (0.75, 3.75),
260                (0.75, 2.25),
261                (2.25, 0.75),
262                (3.75, 0.75)
263            ])
264        );
265    }
266}