gistools/geometry/tools/polys/union.rs
1use crate::geometry::{
2 Area, InterPointLookup, NextRingChunk, PolyPath, PolyPathRef, RingChunkRef,
3 RingIntersectionLookup, Segment, build_paths_and_chunks, merge_intersection_pairs,
4 polygons_intersections_lookup,
5};
6use alloc::{
7 collections::{BTreeMap, BTreeSet},
8 vec,
9 vec::Vec,
10};
11use core::mem::take;
12use s2json::{BBox, FullXY};
13
14/// Given a collection of polygons, if any of the polygons interact/overlap eachother, merge them.
15///
16/// ## Parameters
17/// - `polygons`: the collection of polygons to apply a union to. The points in the polygons are
18/// expected to implement the trait [`FullXY`] which extends [`GetXY`] and [`NewXY`]
19///
20/// ## Returns
21/// A union of polygons should a union exist.
22pub fn polygons_union<P: FullXY>(
23 vector_polygons: &[Vec<Vec<P>>],
24) -> Option<(Vec<Vec<Vec<P>>>, BBox)> {
25 // not enough data, just clone
26 if vector_polygons.is_empty() {
27 return None;
28 }
29
30 // 1) build intersections `[poly_index][ring_index] -> Intersections`. Store where on the ring other rings intersect
31 let mut ring_intersect_lookup: RingIntersectionLookup =
32 polygons_intersections_lookup(vector_polygons, None::<fn(&Segment, &Segment) -> bool>);
33
34 // 2) Build Poly Pieces
35 // Setup result paths with chunks that are the final structure of joined polygons.
36 // Lookup is a helper for quickly finding the right path in the future as paths can consume multiple polygons
37 // If no intersections for the poly_index+Ring_index -> it's immediately consumed into paths. Otherwise it's a chunk
38 let (mut paths, mut path_lookup, chnks, ints, bboxes) =
39 build_paths_and_chunks(vector_polygons, &mut ring_intersect_lookup);
40
41 // 3) Consume chunks into PolyPaths
42 // If no intersections for the poly_index+Ring_index -> push as completed ring
43 // build_paths_from_chunks(&mut chunks, &mut path_lookup, &mut paths);
44 build_paths_from_chunks(&mut paths, &mut path_lookup, &ints, &chnks, &bboxes);
45
46 // 4) Convert PolyPaths into the resultant MultiPolygon
47 let coordinates: Vec<Vec<Vec<P>>> =
48 paths.iter_mut().filter_map(|p| p.borrow_mut().get_path()).collect();
49 let bbox = paths.iter().map(|p| p.borrow().bbox).fold(BBox::default(), |mut acc, b| {
50 acc.merge_in_place(&b);
51 acc
52 });
53 if coordinates.is_empty() { None } else { Some((coordinates, bbox)) }
54}
55
56/// Given a set of chunks, build a set of paths
57///
58/// ## Parameters
59/// - `chunks`: a set of chunks
60/// - `path_lookup`: a lookup of existing paths
61/// - `paths`: a set of paths to add to
62fn build_paths_from_chunks<P: FullXY>(
63 paths: &mut Vec<PolyPathRef<P>>,
64 path_lookup: &mut BTreeMap<usize, PolyPathRef<P>>,
65 intersections: &InterPointLookup<P>,
66 chunks: &[RingChunkRef<P>],
67 bboxes: &[BBox],
68) {
69 // merge in all potential "dead" outer-rings (do we need this?)
70 for path in paths.iter() {
71 store_inner_old_outers(path, bboxes);
72 }
73 // for each intersections, connect all the from and to, smallest angle between from->to first slowly work your way through
74 for int in intersections.lookup.values() {
75 merge_intersection_pairs(int);
76 }
77 for chunk in chunks {
78 if chunk.borrow().visted {
79 continue;
80 }
81 // follow along a chunk until we find our start point again
82 let start = chunk.borrow().from;
83 let mut curr_chunk: RingChunkRef<P> = chunk.clone();
84 let mut found_polygons: BTreeSet<usize> = BTreeSet::new();
85 let mut linestring: Vec<P> = vec![P::new_xy(start.0, start.1)];
86 let mut bbox = curr_chunk.borrow().bbox;
87 loop {
88 let next = {
89 let curr_chunk = &mut curr_chunk.borrow_mut();
90 if curr_chunk.visted {
91 break;
92 }
93 // add the chunk and mark it as visited
94 curr_chunk.visted = true;
95 linestring.append(&mut curr_chunk.mid);
96 found_polygons.insert(curr_chunk.poly_index);
97 bbox.merge_in_place(&curr_chunk.bbox);
98 if let Some(NextRingChunk { chunk, int_point }) = curr_chunk.next.as_ref() {
99 linestring.push(P::new_xy(int_point.0, int_point.1));
100 chunk.clone()
101 } else {
102 break;
103 }
104 };
105 curr_chunk = next;
106 if linestring.last() == linestring.first() {
107 break;
108 }
109 }
110
111 let area = (&linestring).area(Some(1.));
112 if area == 0. || linestring.len() < 4 || linestring.first() != linestring.last() {
113 continue;
114 }
115 // now build the path or add to an existing path
116 let is_ccw = area > 0.;
117 // Find the correct PolyPath to insert into, otherwise create a new one, update the lookup to
118 // include all new polygon indexes used in the path
119 let mut found_paths = vec![];
120 // Pull in all the old paths to merge with this one (may expand upon multiple paths, consume the holes)
121 let mut curr_id = 0;
122 for poly_index in &found_polygons {
123 if let Some(path) = path_lookup.get(poly_index) {
124 path.borrow_mut().id = curr_id;
125 curr_id += 1;
126 found_paths.push(path.clone());
127 }
128 }
129 // filter foundPaths if they have the same id as the previous
130 found_paths.sort_by(|a, b| a.borrow().id.cmp(&b.borrow().id));
131 found_paths = found_paths
132 .iter()
133 .enumerate()
134 .filter_map(|(i, p)| {
135 if i == 0 || p.borrow().id != found_paths[i - 1].borrow().id {
136 Some(p.clone())
137 } else {
138 None
139 }
140 })
141 .collect();
142 // merge
143 let path: PolyPathRef<P>;
144 if found_paths.is_empty() {
145 path = PolyPath::new_ref(linestring, found_polygons.clone(), is_ccw, Some(bbox));
146 paths.push(path.clone());
147 } else {
148 // TODO: `chunk.ringIndex !== 0` may not be enough as may contain a hole chunk but started as an outer chunk
149 // if only one found, update that one, otherwise create a new merged path and store the new result
150 path = if found_paths.len() == 1 {
151 found_paths[0].clone()
152 } else {
153 merge_paths(&found_paths)
154 };
155 add_chunk_to_path(
156 &mut path.borrow_mut(),
157 linestring,
158 &found_polygons,
159 &bbox,
160 is_ccw,
161 chunk.borrow().ring_index != 0,
162 );
163 }
164 // Store all inner outer rings that have not yet been consumed by the new outer but are inside the new outer
165 store_inner_old_outers(&path, bboxes);
166 // All found poly_index references now point to the new path
167 for poly_index in found_polygons {
168 path_lookup.insert(poly_index, path.clone());
169 }
170 // TODO: Poly's may still be able to consume eachother
171 }
172}
173
174/**
175 * Add a chunks built into a line+bbox to a path
176 * @param path - the path to add to
177 * @param ring - the linestring to add
178 * @param poly_indexes - all polygon indexes touched
179 * @param bbox - the bounding box of the collection of chunks (ring)
180 * @param is_ccw - whether the ring is CCW
181 * @param was_hole - whether the ring is a hole
182 */
183fn add_chunk_to_path<P: FullXY>(
184 path: &mut PolyPath<P>,
185 ring: Vec<P>,
186 poly_indexes: &BTreeSet<usize>,
187 bbox: &BBox,
188 is_ccw: bool,
189 was_hole: bool,
190) {
191 path.polys_consumed.extend(poly_indexes.iter());
192
193 // If one poly outer ring is entirely in another poly AND its CCW, it gets "consumed" (deleted. path is
194 // because of the ordering, the first chunk to be an outer will be the one creating the path,
195 // so we know all future CCW chunks that share a path will be "outers" that are inside the existing
196 // path outer)
197 // If one poly outer ring is entirely in another poly AND its CW, it converts to a hole
198 // If one poly inner ring is CW, it gets consumed by an associated outer
199 // If one poly inner ring is CCW, remove it
200 // If a hole is found, it didn't come from a hole, and it's inside one of the old outer's bboxes, delete the pair.
201 if is_ccw {
202 if was_hole {
203 return;
204 }
205 if path.outer.is_none() {
206 path.outer = Some(ring);
207 path.bbox.merge_in_place(bbox);
208 } else {
209 // If the ring's bbox is smaller than the existing outer, store. Otherwise replace
210 if bbox.inside(&path.bbox) {
211 path.old_outers.push(*bbox);
212 } else {
213 path.old_outers.push(path.bbox);
214 path.outer = Some(ring);
215 path.bbox.merge_in_place(bbox);
216 }
217 }
218 } else {
219 if !was_hole {
220 // Store discarded smaller outer rings, if hole is inside inner outer-ring, it cancels out the hole
221 for old_outer in &path.old_outers {
222 if bbox.inside(old_outer) {
223 return;
224 }
225 }
226 }
227 path.holes.push(ring);
228 }
229}
230
231/// Store all inner old outers that have not yet been consumed by the new outer
232/// @param path - the path
233/// @param bboxes - the bboxes of all outer rings we are merging
234fn store_inner_old_outers<P: FullXY>(path: &PolyPathRef<P>, bboxes: &[BBox]) {
235 let path = &mut path.borrow_mut();
236 if path.outer.is_none() {
237 return;
238 }
239 for (i, bbox) in bboxes.iter().enumerate().skip(1) {
240 if path.polys_consumed.contains(&i) {
241 continue;
242 }
243 if bbox.inside(&path.bbox) {
244 path.old_outers.push(*bbox);
245 }
246 }
247}
248
249/// Merge in a collection of paths
250///
251/// ## Parameters
252/// - `paths_to_merge`: the collection of paths
253/// - `path_store`: the collection of paths
254///
255/// ## Returns
256/// The result of all paths merged into one
257fn merge_paths<P: FullXY>(paths_to_merge: &[PolyPathRef<P>]) -> PolyPathRef<P> {
258 let result = paths_to_merge[0].clone();
259 {
260 let res = &mut result.borrow_mut();
261 for other in paths_to_merge.iter().skip(1) {
262 let other = &mut other.borrow_mut();
263 // If this bbox is smaller than the existing outer, replace
264 let other_bbox = other.bbox;
265 if res.bbox.inside(&other_bbox)
266 && let Some(other_outer) = &mut other.outer
267 {
268 if res.outer.is_some() {
269 res.old_outers.push(other_bbox);
270 }
271 res.outer = Some(take(other_outer));
272 }
273 res.holes.append(&mut other.holes);
274 res.polys_consumed.extend(&other.polys_consumed);
275 res.bbox.merge_in_place(&other.bbox);
276 // clear the path now that we comsumed it
277 other.outer = None;
278 other.old_outers = vec![];
279 }
280 }
281
282 result
283}