geo/algorithm/
concave_hull.rs

1use crate::convex_hull::qhull;
2use crate::utils::partial_min;
3use crate::{
4    coord, Centroid, Coord, CoordNum, EuclideanDistance, EuclideanLength, GeoFloat, Line,
5    LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon,
6};
7use rstar::{RTree, RTreeNum};
8use std::collections::VecDeque;
9
10/// Returns a polygon which covers a geometry. Unlike convex hulls, which also cover
11/// their geometry, a concave hull does so while trying to further minimize its area by
12/// constructing edges such that the exterior of the polygon incorporates points that would
13/// be interior points in a convex hull.
14///
15/// This implementation is inspired by <https://github.com/mapbox/concaveman>
16/// and also uses ideas from the following paper:
17/// www.iis.sinica.edu.tw/page/jise/2012/201205_10.pdf
18///
19/// # Examples
20/// ```
21/// use geo::{line_string, polygon};
22/// use geo::ConcaveHull;
23///
24/// // a square shape
25/// let poly = polygon![
26///     (x: 0.0, y: 0.0),
27///     (x: 4.0, y: 0.0),
28///     (x: 4.0, y: 4.0),
29///     (x: 0.0, y: 4.0),
30/// ];
31///
32/// // The correct concave hull coordinates
33/// let correct_hull = line_string![
34///     (x: 4.0, y: 0.0),
35///     (x: 4.0, y: 4.0),
36///     (x: 0.0, y: 4.0),
37///     (x: 0.0, y: 0.0),
38///     (x: 4.0, y: 0.0),
39/// ];
40///
41/// let res = poly.concave_hull(2.0);
42/// assert_eq!(res.exterior(), &correct_hull);
43/// ```
44pub trait ConcaveHull {
45    type Scalar: CoordNum;
46    fn concave_hull(&self, concavity: Self::Scalar) -> Polygon<Self::Scalar>;
47}
48
49impl<T> ConcaveHull for Polygon<T>
50where
51    T: GeoFloat + RTreeNum,
52{
53    type Scalar = T;
54    fn concave_hull(&self, concavity: Self::Scalar) -> Polygon<Self::Scalar> {
55        let mut points: Vec<_> = self.exterior().0.clone();
56        Polygon::new(concave_hull(&mut points, concavity), vec![])
57    }
58}
59
60impl<T> ConcaveHull for MultiPolygon<T>
61where
62    T: GeoFloat + RTreeNum,
63{
64    type Scalar = T;
65    fn concave_hull(&self, concavity: Self::Scalar) -> Polygon<Self::Scalar> {
66        let mut aggregated: Vec<Coord<Self::Scalar>> = self
67            .0
68            .iter()
69            .flat_map(|elem| elem.exterior().0.clone())
70            .collect();
71        Polygon::new(concave_hull(&mut aggregated, concavity), vec![])
72    }
73}
74
75impl<T> ConcaveHull for LineString<T>
76where
77    T: GeoFloat + RTreeNum,
78{
79    type Scalar = T;
80    fn concave_hull(&self, concavity: Self::Scalar) -> Polygon<Self::Scalar> {
81        Polygon::new(concave_hull(&mut self.0.clone(), concavity), vec![])
82    }
83}
84
85impl<T> ConcaveHull for MultiLineString<T>
86where
87    T: GeoFloat + RTreeNum,
88{
89    type Scalar = T;
90    fn concave_hull(&self, concavity: T) -> Polygon<T> {
91        let mut aggregated: Vec<Coord<T>> = self.iter().flat_map(|elem| elem.0.clone()).collect();
92        Polygon::new(concave_hull(&mut aggregated, concavity), vec![])
93    }
94}
95
96impl<T> ConcaveHull for MultiPoint<T>
97where
98    T: GeoFloat + RTreeNum,
99{
100    type Scalar = T;
101    fn concave_hull(&self, concavity: T) -> Polygon<T> {
102        let mut coordinates: Vec<Coord<T>> = self.iter().map(|point| point.0).collect();
103        Polygon::new(concave_hull(&mut coordinates, concavity), vec![])
104    }
105}
106
107fn find_point_closest_to_line<T>(
108    interior_coords_tree: &RTree<Coord<T>>,
109    line: Line<T>,
110    max_dist: T,
111    edge_length: T,
112    concavity: T,
113    line_tree: &RTree<Line<T>>,
114) -> Option<Coord<T>>
115where
116    T: GeoFloat + RTreeNum,
117{
118    let h = max_dist + max_dist;
119    let w = line.euclidean_length() + h;
120    let two = T::add(T::one(), T::one());
121    let search_dist = T::div(T::sqrt(T::powi(w, 2) + T::powi(h, 2)), two);
122    let centroid = line.centroid();
123    let centroid_coord = coord! {
124        x: centroid.x(),
125        y: centroid.y(),
126    };
127    let mut candidates = interior_coords_tree
128        .locate_within_distance(centroid_coord, search_dist)
129        .peekable();
130    let peek = candidates.peek();
131    match peek {
132        None => None,
133        Some(&point) => {
134            let closest_point =
135                candidates.fold(Point::new(point.x, point.y), |acc_point, candidate| {
136                    let candidate_point = Point::new(candidate.x, candidate.y);
137                    if line.euclidean_distance(&acc_point)
138                        > line.euclidean_distance(&candidate_point)
139                    {
140                        candidate_point
141                    } else {
142                        acc_point
143                    }
144                });
145            let mut edges_nearby_point = line_tree
146                .locate_within_distance(closest_point, search_dist)
147                .peekable();
148            let peeked_edge = edges_nearby_point.peek();
149
150            // Clippy is having an issue here. It might be a valid suggestion,
151            // but the automatic clippy fix breaks the code, so may need to be done by hand.
152            // See https://github.com/rust-lang/rust/issues/94241
153            #[allow(clippy::manual_map)]
154            let closest_edge_option = match peeked_edge {
155                None => None,
156                Some(&edge) => Some(edges_nearby_point.fold(*edge, |acc, candidate| {
157                    if closest_point.euclidean_distance(&acc)
158                        > closest_point.euclidean_distance(candidate)
159                    {
160                        *candidate
161                    } else {
162                        acc
163                    }
164                })),
165            };
166            let decision_distance = partial_min(
167                closest_point.euclidean_distance(&line.start_point()),
168                closest_point.euclidean_distance(&line.end_point()),
169            );
170            if let Some(closest_edge) = closest_edge_option {
171                let far_enough = edge_length / decision_distance > concavity;
172                let are_edges_equal = closest_edge == line;
173                if far_enough && are_edges_equal {
174                    Some(coord! {
175                        x: closest_point.x(),
176                        y: closest_point.y(),
177                    })
178                } else {
179                    None
180                }
181            } else {
182                None
183            }
184        }
185    }
186}
187
188// This takes significant inspiration from:
189// https://github.com/mapbox/concaveman/blob/54838e1/index.js#L11
190fn concave_hull<T>(coords: &mut [Coord<T>], concavity: T) -> LineString<T>
191where
192    T: GeoFloat + RTreeNum,
193{
194    let hull = qhull::quick_hull(coords);
195
196    if coords.len() < 4 {
197        return hull;
198    }
199
200    //Get points in overall dataset that aren't on the exterior linestring of the hull
201    let hull_tree: RTree<Coord<T>> = RTree::bulk_load(hull.clone().0);
202
203    let interior_coords: Vec<Coord<T>> = coords
204        .iter()
205        .filter(|coord| !hull_tree.contains(coord))
206        .copied()
207        .collect();
208    let mut interior_points_tree: RTree<Coord<T>> = RTree::bulk_load(interior_coords);
209    let mut line_tree: RTree<Line<T>> = RTree::new();
210
211    let mut concave_list: Vec<Point<T>> = vec![];
212    let lines = hull.lines();
213    let mut line_queue: VecDeque<Line<T>> = VecDeque::new();
214
215    for line in lines {
216        line_queue.push_back(line);
217        line_tree.insert(line);
218    }
219    while let Some(line) = line_queue.pop_front() {
220        let edge_length = line.euclidean_length();
221        let dist = edge_length / concavity;
222        let possible_closest_point = find_point_closest_to_line(
223            &interior_points_tree,
224            line,
225            dist,
226            edge_length,
227            concavity,
228            &line_tree,
229        );
230
231        if let Some(closest_point) = possible_closest_point {
232            interior_points_tree.remove(&closest_point);
233            line_tree.remove(&line);
234            let point = Point::new(closest_point.x, closest_point.y);
235            let start_line = Line::new(line.start_point(), point);
236            let end_line = Line::new(point, line.end_point());
237            line_tree.insert(start_line);
238            line_tree.insert(end_line);
239            line_queue.push_front(end_line);
240            line_queue.push_front(start_line);
241        } else {
242            // Make sure we don't add duplicates
243            if concave_list.is_empty() || !concave_list.ends_with(&[line.start_point()]) {
244                concave_list.push(line.start_point());
245            }
246            concave_list.push(line.end_point());
247        }
248    }
249
250    concave_list.into()
251}
252
253#[cfg(test)]
254mod test {
255    use super::*;
256    use crate::{line_string, polygon};
257    use geo_types::Coord;
258
259    #[test]
260    fn triangle_test() {
261        let mut triangle = vec![
262            coord! { x: 0.0, y: 0.0 },
263            coord! { x: 4.0, y: 0.0 },
264            coord! { x: 2.0, y: 2.0 },
265        ];
266
267        let correct = line_string![
268            (x: 0.0, y: 0.0),
269            (x: 4.0, y: 0.0),
270            (x: 2.0, y: 2.0),
271            (x: 0.0, y: 0.0),
272        ];
273
274        let concavity = 2.0;
275        let res = concave_hull(&mut triangle, concavity);
276        assert_eq!(res, correct);
277    }
278
279    #[test]
280    fn square_test() {
281        let mut square = vec![
282            coord! { x: 0.0, y: 0.0 },
283            coord! { x: 4.0, y: 0.0 },
284            coord! { x: 4.0, y: 4.0 },
285            coord! { x: 0.0, y: 4.0 },
286        ];
287
288        let correct = line_string![
289            (x: 4.0, y: 0.0),
290            (x: 4.0, y: 4.0),
291            (x: 0.0, y: 4.0),
292            (x: 0.0, y: 0.0),
293            (x: 4.0, y: 0.0),
294        ];
295
296        let concavity = 2.0;
297        let res = concave_hull(&mut square, concavity);
298        assert_eq!(res, correct);
299    }
300
301    #[test]
302    fn one_flex_test() {
303        let mut v = vec![
304            coord! { x: 0.0, y: 0.0 },
305            coord! { x: 2.0, y: 1.0 },
306            coord! { x: 4.0, y: 0.0 },
307            coord! { x: 4.0, y: 4.0 },
308            coord! { x: 0.0, y: 4.0 },
309        ];
310        let correct = line_string![
311            (x: 4.0, y: 0.0),
312            (x: 4.0, y: 4.0),
313            (x: 0.0, y: 4.0),
314            (x: 0.0, y: 0.0),
315            (x: 2.0, y: 1.0),
316            (x: 4.0, y: 0.0),
317        ];
318        let concavity = 1.0;
319        let res = concave_hull(&mut v, concavity);
320        assert_eq!(res, correct);
321    }
322
323    #[test]
324    fn four_flex_test() {
325        let mut v = vec![
326            coord! { x: 0.0, y: 0.0 },
327            coord! { x: 2.0, y: 1.0 },
328            coord! { x: 4.0, y: 0.0 },
329            coord! { x: 3.0, y: 2.0 },
330            coord! { x: 4.0, y: 4.0 },
331            coord! { x: 2.0, y: 3.0 },
332            coord! { x: 0.0, y: 4.0 },
333            coord! { x: 1.0, y: 2.0 },
334        ];
335        let correct = line_string![
336            (x: 4.0, y: 0.0),
337            (x: 3.0, y: 2.0),
338            (x: 4.0, y: 4.0),
339            (x: 2.0, y: 3.0),
340            (x: 0.0, y: 4.0),
341            (x: 1.0, y: 2.0),
342            (x: 0.0, y: 0.0),
343            (x: 2.0, y: 1.0),
344            (x: 4.0, y: 0.0),
345        ];
346        let concavity = 1.7;
347        let res = concave_hull(&mut v, concavity);
348        assert_eq!(res, correct);
349    }
350
351    #[test]
352    fn consecutive_flex_test() {
353        let mut v = vec![
354            coord! { x: 0.0, y: 0.0 },
355            coord! { x: 4.0, y: 0.0 },
356            coord! { x: 4.0, y: 4.0 },
357            coord! { x: 3.0, y: 1.0 },
358            coord! { x: 3.0, y: 2.0 },
359        ];
360        let correct = line_string![
361            (x: 4.0, y: 0.0),
362            (x: 4.0, y: 4.0),
363            (x: 3.0, y: 2.0),
364            (x: 3.0, y: 1.0),
365            (x: 0.0, y: 0.0),
366            (x: 4.0, y: 0.0),
367        ];
368        let concavity = 2.0;
369        let res = concave_hull(&mut v, concavity);
370        assert_eq!(res, correct);
371    }
372
373    #[test]
374    fn concave_hull_norway_test() {
375        let norway = geo_test_fixtures::norway_main::<f64>();
376        let norway_concave_hull: LineString = geo_test_fixtures::norway_concave_hull::<f64>();
377        let res = norway.concave_hull(2.0);
378        assert_eq!(res.exterior(), &norway_concave_hull);
379    }
380
381    #[test]
382    fn concave_hull_linestring_test() {
383        let linestring = line_string![
384            (x: 0.0, y: 0.0),
385            (x: 4.0, y: 0.0),
386            (x: 4.0, y: 4.0),
387            (x: 3.0, y: 1.0),
388            (x: 3.0, y: 2.0)
389        ];
390        let concave = linestring.concave_hull(2.0);
391        let correct = vec![
392            Coord::from((4.0, 0.0)),
393            Coord::from((4.0, 4.0)),
394            Coord::from((3.0, 2.0)),
395            Coord::from((3.0, 1.0)),
396            Coord::from((0.0, 0.0)),
397            Coord::from((4.0, 0.0)),
398        ];
399        assert_eq!(concave.exterior().0, correct);
400    }
401
402    #[test]
403    fn concave_hull_multilinestring_test() {
404        let v1 = line_string![
405             (x: 0.0, y: 0.0),
406             (x: 4.0, y: 0.0)
407        ];
408        let v2 = line_string![
409             (x: 4.0, y: 4.0),
410             (x: 3.0, y: 1.0),
411             (x: 3.0, y: 2.0)
412        ];
413        let mls = MultiLineString::new(vec![v1, v2]);
414        let correct = vec![
415            Coord::from((4.0, 0.0)),
416            Coord::from((4.0, 4.0)),
417            Coord::from((3.0, 2.0)),
418            Coord::from((3.0, 1.0)),
419            Coord::from((0.0, 0.0)),
420            Coord::from((4.0, 0.0)),
421        ];
422        let res = mls.concave_hull(2.0);
423        assert_eq!(res.exterior().0, correct);
424    }
425
426    #[test]
427    fn concave_hull_multipolygon_test() {
428        let v1 = polygon![
429             (x: 0.0, y: 0.0),
430             (x: 4.0, y: 0.0)
431        ];
432        let v2 = polygon![
433             (x: 4.0, y: 4.0),
434             (x: 3.0, y: 1.0),
435             (x: 3.0, y: 2.0)
436        ];
437        let multipolygon = MultiPolygon::new(vec![v1, v2]);
438        let res = multipolygon.concave_hull(2.0);
439        let correct = vec![
440            Coord::from((4.0, 0.0)),
441            Coord::from((4.0, 4.0)),
442            Coord::from((3.0, 2.0)),
443            Coord::from((3.0, 1.0)),
444            Coord::from((0.0, 0.0)),
445            Coord::from((4.0, 0.0)),
446        ];
447        assert_eq!(res.exterior().0, correct);
448    }
449}