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}