gistools/geometry/tools/polys/clean.rs
1use crate::geometry::{Area, clean_linestring, dekink_polygon};
2use alloc::{vec, vec::Vec};
3use s2json::{BBox, FullXY};
4
5/// Ensures the collection of polygon ring order is correct, removes duplicate points,
6/// and runs a dekink to be thorough.
7///
8/// NOTE: This will not remove/reduce points that follow a path angle like [[0, 0], [0, 1], [0, 2], ...].
9/// The decision to leave this to the user is due to the fact that not all projections are guaranteed
10/// to support a linear relationship. Also sometimes the user want's to have these extra points for
11/// future/cleaner projection changes. For example, having higher precision works well when
12/// translating to spherical projections for instance. If you do want to remove these points, pass
13/// in true to `remove_collinear_points`
14///
15/// ## Parameters
16/// - `polygons`: the collection of polygon as either a VectorFeature, VectorMultiPolygonGeometry, or raw VectorMultiPolygon
17/// - `remove_collinear_points`: - if true, remove superfluous points
18/// - `clean_wgs84`: if true, clean WGS84 points to be valid WGS84 points
19///
20/// ## Returns
21/// The cleaned polygons as a new collection of polygons
22pub fn clean_polygons<P: FullXY>(
23 polygons: &[Vec<Vec<P>>],
24 remove_collinear_points: bool,
25 clean_wgs84: bool,
26) -> Option<(Vec<Vec<Vec<P>>>, BBox)> {
27 let mut res: Vec<Vec<Vec<P>>> = vec![];
28 let mut final_bbox: BBox = BBox::default();
29
30 for p in polygons {
31 if let Some((mut cleaned, bbox)) = clean_polygon(p, remove_collinear_points, clean_wgs84) {
32 res.append(&mut cleaned);
33 final_bbox.merge_in_place(&bbox);
34 }
35 }
36 if res.is_empty() { None } else { Some((res, final_bbox)) }
37}
38
39/// Ensures the polygon ring order is correct, removes duplicate points, and runs a dekink to be
40/// thorough.
41///
42/// NOTE: This will not remove/reduce points that follow a path angle like [[0, 0], [0, 1], [0, 2], ...].
43/// The decision to leave this to the user is due to the fact that not all projections are guaranteed
44/// to support a linear relationship. Also sometimes the user want's to have these extra points for
45/// future/cleaner projection changes. For example, having higher precision works well when
46/// translating to spherical projections for instance. If you do want to remove these points, pass
47/// in true to `remove_collinear_points`
48///
49/// ## Parameters
50/// - `polygon`: the polygon as either a VectorFeature, VectorPolygonGeometry, or raw VectorPolygon
51/// - `remove_collinear_points`: if true, remove superfluous points
52/// - `clean_wgs84`: if true, clean WGS84 points to be valid WGS84 points
53///
54/// ## Returns
55/// The cleaned polygon, split into a multi-polygon as necessary
56pub fn clean_polygon<P: FullXY>(
57 polygon: &[Vec<P>],
58 remove_collinear_points: bool,
59 clean_wgs84: bool,
60) -> Option<(Vec<Vec<Vec<P>>>, BBox)> {
61 // remove duplicates from the rings
62 let mut res: Vec<Vec<P>> = vec![];
63 for (index, ring) in polygon.iter().enumerate() {
64 let mut last_point: Option<&P> = None;
65
66 if remove_collinear_points {
67 match clean_linestring(ring, true, None, clean_wgs84) {
68 Some(cleaned) => res.push(cleaned),
69 None => {
70 if index == 0 {
71 return None;
72 }
73 }
74 }
75 } else {
76 let mut new_ring: Vec<P> = vec![];
77 for point in ring {
78 if last_point.is_none() || *point != *last_point.unwrap() {
79 new_ring.push(point.clone());
80 last_point = Some(point);
81 }
82 }
83 if new_ring.len() >= 4 {
84 res.push(new_ring);
85 } else if index == 0 {
86 return None;
87 }
88 }
89 }
90 // run polygon_ring_area for each ring and invert if it's direction is wrong for the ring type
91 for (i, ring) in res.iter_mut().enumerate() {
92 let area = ring.area(Some(1.));
93 // flip the ring if outer-ring and area is negative OR inner-ring and area is positive
94 if if i == 0 { area < 0. } else { area > 0. } {
95 ring.reverse();
96 }
97 }
98
99 dekink_polygon(&res)
100}