geo/algorithm/
k_nearest_concave_hull.rs

1use crate::{
2    Contains, ConvexHull, Coord, CoordNum, GeoFloat, Intersects, LineString, MultiPoint, Point,
3    Polygon,
4};
5use num_traits::Float;
6use rstar::RTreeNum;
7use std::cmp::max;
8
9const K_MULTIPLIER: f32 = 1.5;
10
11/// Another approach for [concave hull](trait.algorithm.ConcaveHull.html). This algorithm is based
12/// on a [k nearest neighbours approach](https://pdfs.semanticscholar.org/2397/17005c3ebd5d6a42fc833daf97a0edee1ce4.pdf)
13/// by Adriano Moreira and Maribel Santos.
14///
15/// The idea of the algorithm is simple:
16/// 1. Find a point on a future hull (e. g. a point with the smallest Y coordinate).
17/// 2. Find K nearest neighbours to the chosen point.
18/// 3. As the next point on the hull chose one of the nearest points, that would make the largest
19///    left hand turn from the previous segment.
20/// 4. Repeat 2-4.
21///
22/// In cases when the hull cannot be calculated for the given K, a larger value is chosen and
23/// calculation starts from the beginning.
24///
25/// In the worst case scenario, when no K can be found to build a correct hull, the convex hull is
26/// returned.
27///
28/// This algorithm is generally several times slower then the one used in the
29/// [ConcaveHull](trait.algorithm.ConcaveHull.html) trait, but gives better results and
30/// does not require manual coefficient adjustment.
31///
32/// The larger K is given to the algorithm, the more "smooth" the hull will generally be, but the
33/// longer calculation may take. If performance is not critical, K=3 is a safe value to set
34/// (lower values do not make sense for this algorithm). If K is equal or larger than the number of
35/// input points, the convex hull will be produced.
36pub trait KNearestConcaveHull {
37    type Scalar: CoordNum;
38    fn k_nearest_concave_hull(&self, k: u32) -> Polygon<Self::Scalar>;
39}
40
41impl<T> KNearestConcaveHull for Vec<Point<T>>
42where
43    T: GeoFloat + RTreeNum,
44{
45    type Scalar = T;
46    fn k_nearest_concave_hull(&self, k: u32) -> Polygon<Self::Scalar> {
47        concave_hull(self.iter().map(|point| &point.0), k)
48    }
49}
50
51impl<T> KNearestConcaveHull for [Point<T>]
52where
53    T: GeoFloat + RTreeNum,
54{
55    type Scalar = T;
56    fn k_nearest_concave_hull(&self, k: u32) -> Polygon<Self::Scalar> {
57        concave_hull(self.iter().map(|point| &point.0), k)
58    }
59}
60
61impl<T> KNearestConcaveHull for Vec<Coord<T>>
62where
63    T: GeoFloat + RTreeNum,
64{
65    type Scalar = T;
66    fn k_nearest_concave_hull(&self, k: u32) -> Polygon<Self::Scalar> {
67        concave_hull(self.iter(), k)
68    }
69}
70
71impl<T> KNearestConcaveHull for [Coord<T>]
72where
73    T: GeoFloat + RTreeNum,
74{
75    type Scalar = T;
76    fn k_nearest_concave_hull(&self, k: u32) -> Polygon<Self::Scalar> {
77        concave_hull(self.iter(), k)
78    }
79}
80
81impl<T> KNearestConcaveHull for MultiPoint<T>
82where
83    T: GeoFloat + RTreeNum,
84{
85    type Scalar = T;
86    fn k_nearest_concave_hull(&self, k: u32) -> Polygon<Self::Scalar> {
87        concave_hull(self.iter().map(|point| &point.0), k)
88    }
89}
90
91fn concave_hull<'a, T: 'a>(coords: impl Iterator<Item = &'a Coord<T>>, k: u32) -> Polygon<T>
92where
93    T: GeoFloat + RTreeNum,
94{
95    let dataset = prepare_dataset(coords);
96    concave_hull_inner(dataset, k)
97}
98
99const DELTA: f32 = 0.000000001;
100
101/// Removes duplicate coords from the dataset.
102fn prepare_dataset<'a, T: 'a>(coords: impl Iterator<Item = &'a Coord<T>>) -> rstar::RTree<Coord<T>>
103where
104    T: GeoFloat + RTreeNum,
105{
106    let mut dataset: rstar::RTree<Coord<T>> = rstar::RTree::new();
107    for coord in coords {
108        let closest = dataset.nearest_neighbor(coord);
109        if let Some(closest) = closest {
110            if coords_are_equal(coord, closest) {
111                continue;
112            }
113        }
114
115        dataset.insert(*coord)
116    }
117
118    dataset
119}
120
121/// The points are considered equal, if both coordinate values are same with 0.0000001% range
122/// (see the value of DELTA constant).
123fn coords_are_equal<T>(c1: &Coord<T>, c2: &Coord<T>) -> bool
124where
125    T: GeoFloat + RTreeNum,
126{
127    float_equal(c1.x, c2.x) && float_equal(c1.y, c2.y)
128}
129
130fn float_equal<T>(a: T, b: T) -> bool
131where
132    T: GeoFloat,
133{
134    let da = a * T::from(DELTA)
135        .expect("Conversion from constant is always valid.")
136        .abs();
137    b > (a - da) && b < (a + da)
138}
139
140fn polygon_from_tree<T>(dataset: &rstar::RTree<Coord<T>>) -> Polygon<T>
141where
142    T: GeoFloat + RTreeNum,
143{
144    assert!(dataset.size() <= 3);
145
146    let mut coords: Vec<Coord<T>> = dataset.iter().cloned().collect();
147    if !coords.is_empty() {
148        // close the linestring provided it's not empty
149        coords.push(coords[0]);
150    }
151
152    Polygon::new(LineString::from(coords), vec![])
153}
154
155fn concave_hull_inner<T>(original_dataset: rstar::RTree<Coord<T>>, k: u32) -> Polygon<T>
156where
157    T: GeoFloat + RTreeNum,
158{
159    let set_length = original_dataset.size();
160    if set_length <= 3 {
161        return polygon_from_tree(&original_dataset);
162    }
163    if k >= set_length as u32 {
164        return fall_back_hull(&original_dataset);
165    }
166
167    let k_adjusted = adjust_k(k);
168    let mut dataset = original_dataset.clone();
169
170    let first_coord = get_first_coord(&dataset);
171    let mut hull = vec![first_coord];
172
173    let mut current_coord = first_coord;
174    dataset.remove(&first_coord);
175
176    let mut prev_coord = current_coord;
177    let mut curr_step = 2;
178    while (current_coord != first_coord || curr_step == 2) && dataset.size() > 0 {
179        if curr_step == 5 {
180            dataset.insert(first_coord);
181        }
182
183        let mut nearest_coords: Vec<_> =
184            get_nearest_coords(&dataset, &current_coord, k_adjusted).collect();
185        sort_by_angle(&mut nearest_coords, &current_coord, &prev_coord);
186
187        let selected = nearest_coords
188            .iter()
189            .find(|x| !intersects(&hull, &[&current_coord, x]));
190
191        if let Some(sel) = selected {
192            prev_coord = current_coord;
193            current_coord = **sel;
194            hull.push(current_coord);
195            dataset.remove(&current_coord);
196
197            curr_step += 1;
198        } else {
199            return concave_hull_inner(original_dataset, get_next_k(k_adjusted));
200        }
201    }
202
203    let poly = Polygon::new(LineString::from(hull), vec![]);
204
205    if original_dataset
206        .iter()
207        .any(|&coord| !coord_inside(&coord, &poly))
208    {
209        return concave_hull_inner(original_dataset, get_next_k(k_adjusted));
210    }
211
212    poly
213}
214
215fn fall_back_hull<T>(dataset: &rstar::RTree<Coord<T>>) -> Polygon<T>
216where
217    T: GeoFloat + RTreeNum,
218{
219    let multipoint = MultiPoint::from(dataset.iter().cloned().collect::<Vec<Coord<T>>>());
220    multipoint.convex_hull()
221}
222
223fn get_next_k(curr_k: u32) -> u32 {
224    max(curr_k + 1, ((curr_k as f32) * K_MULTIPLIER) as u32)
225}
226
227fn adjust_k(k: u32) -> u32 {
228    max(k, 3)
229}
230
231fn get_first_coord<T>(coord_set: &rstar::RTree<Coord<T>>) -> Coord<T>
232where
233    T: GeoFloat + RTreeNum,
234{
235    let mut min_y = Float::max_value();
236    let mut result = coord_set
237        .iter()
238        .next()
239        .expect("We checked that there are more then 3 coords in the set before.");
240
241    for coord in coord_set.iter() {
242        if coord.y < min_y {
243            min_y = coord.y;
244            result = coord;
245        }
246    }
247
248    *result
249}
250
251fn get_nearest_coords<'a, T>(
252    dataset: &'a rstar::RTree<Coord<T>>,
253    base_coord: &Coord<T>,
254    candidate_no: u32,
255) -> impl Iterator<Item = &'a Coord<T>>
256where
257    T: GeoFloat + RTreeNum,
258{
259    dataset
260        .nearest_neighbor_iter(base_coord)
261        .take(candidate_no as usize)
262}
263
264fn sort_by_angle<T>(coords: &mut [&Coord<T>], curr_coord: &Coord<T>, prev_coord: &Coord<T>)
265where
266    T: GeoFloat,
267{
268    let base_angle = pseudo_angle(prev_coord.x - curr_coord.x, prev_coord.y - curr_coord.y);
269    coords.sort_by(|a, b| {
270        let mut angle_a = pseudo_angle(a.x - curr_coord.x, a.y - curr_coord.y) - base_angle;
271        if angle_a < T::zero() {
272            angle_a = angle_a + T::from(4.0).unwrap();
273        }
274
275        let mut angle_b = pseudo_angle(b.x - curr_coord.x, b.y - curr_coord.y) - base_angle;
276        if angle_b < T::zero() {
277            angle_b = angle_b + T::from(4.0).unwrap();
278        }
279
280        angle_a.partial_cmp(&angle_b).unwrap().reverse()
281    });
282}
283
284fn pseudo_angle<T>(dx: T, dy: T) -> T
285where
286    T: GeoFloat,
287{
288    if dx == T::zero() && dy == T::zero() {
289        return T::zero();
290    }
291
292    let p = dx / (dx.abs() + dy.abs());
293    if dy < T::zero() {
294        T::from(3.).unwrap() + p
295    } else {
296        T::from(1.).unwrap() - p
297    }
298}
299
300fn intersects<T>(hull: &[Coord<T>], line: &[&Coord<T>; 2]) -> bool
301where
302    T: GeoFloat,
303{
304    // This is the case of finishing the contour.
305    if *line[1] == hull[0] {
306        return false;
307    }
308
309    let coords = hull.iter().take(hull.len() - 1).cloned().collect();
310    let linestring = LineString::new(coords);
311    let line = crate::Line::new(*line[0], *line[1]);
312    linestring.intersects(&line)
313}
314
315fn coord_inside<T>(coord: &Coord<T>, poly: &Polygon<T>) -> bool
316where
317    T: GeoFloat,
318{
319    poly.contains(coord) || poly.exterior().contains(coord)
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325    use crate::coords_iter::CoordsIter;
326    use crate::geo_types::coord;
327
328    #[test]
329    fn coord_ordering() {
330        let coords = vec![
331            coord!(x: 1.0, y: 1.0),
332            coord!(x: -1.0, y: 0.0),
333            coord!(x: 0.0, y: 1.0),
334            coord!(x: 1.0, y: 0.0),
335        ];
336
337        let mut coords_mapped: Vec<&Coord<f32>> = coords.iter().collect();
338
339        let center = coord!(x: 0.0, y: 0.0);
340        let prev_coord = coord!(x: 1.0, y: 1.0);
341
342        let expected = vec![&coords[3], &coords[1], &coords[2], &coords[0]];
343
344        sort_by_angle(&mut coords_mapped, &center, &prev_coord);
345        assert_eq!(coords_mapped, expected);
346
347        let expected = vec![&coords[1], &coords[2], &coords[0], &coords[3]];
348
349        let prev_coord = coord!(x: 1.0, y: -1.0);
350        sort_by_angle(&mut coords_mapped, &center, &prev_coord);
351        assert_eq!(coords_mapped, expected);
352    }
353
354    #[test]
355    fn get_first_coord_test() {
356        let coords = vec![
357            coord!(x: 1.0, y: 1.0),
358            coord!(x: -1.0, y: 0.0),
359            coord!(x: 0.0, y: 1.0),
360            coord!(x: 0.0, y: 0.5),
361        ];
362        let tree = rstar::RTree::bulk_load(coords);
363        let first = coord!(x: -1.0, y: 0.0);
364
365        assert_eq!(get_first_coord(&tree), first);
366    }
367
368    #[test]
369    fn concave_hull_test() {
370        let coords = vec![
371            coord!(x: 0.0, y: 0.0),
372            coord!(x: 1.0, y: 0.0),
373            coord!(x: 2.0, y: 0.0),
374            coord!(x: 3.0, y: 0.0),
375            coord!(x: 0.0, y: 1.0),
376            coord!(x: 1.0, y: 1.0),
377            coord!(x: 2.0, y: 1.0),
378            coord!(x: 3.0, y: 1.0),
379            coord!(x: 0.0, y: 2.0),
380            coord!(x: 1.0, y: 2.5),
381            coord!(x: 2.0, y: 2.5),
382            coord!(x: 3.0, y: 2.0),
383            coord!(x: 0.0, y: 3.0),
384            coord!(x: 3.0, y: 3.0),
385        ];
386
387        let poly = concave_hull(coords.iter(), 3);
388        assert_eq!(poly.exterior().coords_count(), 12);
389
390        let must_not_be_in = vec![&coords[6]];
391        for coord in poly.exterior().coords_iter() {
392            for not_coord in must_not_be_in.iter() {
393                assert_ne!(&coord, *not_coord);
394            }
395        }
396    }
397
398    #[test]
399    fn empty_hull() {
400        let actual: Polygon<f64> = concave_hull(vec![].iter(), 3);
401        let expected = Polygon::new(LineString::new(vec![]), vec![]);
402        assert_eq!(actual, expected);
403    }
404}