Skip to content

Commit d28970f

Browse files
authored
Merge pull request #1246 from urschrei/cascaded_union
Cascaded / Unary union
2 parents a7c40da + 3bb1881 commit d28970f

File tree

10 files changed

+231
-48
lines changed

10 files changed

+231
-48
lines changed

geo-test-fixtures/fixtures/nl_plots_epsg_28992.wkt

+1
Large diffs are not rendered by default.

geo-test-fixtures/src/lib.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,21 @@ where
126126
}
127127

128128
// From https://afnemers.ruimtelijkeplannen.nl/afnemers/services?request=GetFeature&service=WFS&srsName=EPSG:4326&typeName=Enkelbestemming&version=2.0.0&bbox=165618,480983,166149,481542";
129-
pub fn nl_plots<T>() -> MultiPolygon<T>
129+
pub fn nl_plots_wgs84<T>() -> MultiPolygon<T>
130130
where
131131
T: WktFloat + Default + FromStr,
132132
{
133133
multi_polygon("nl_plots.wkt")
134134
}
135135

136+
pub fn nl_plots_epsg_28992<T>() -> MultiPolygon<T>
137+
where
138+
T: WktFloat + Default + FromStr,
139+
{
140+
// https://epsg.io/28992
141+
multi_polygon("nl_plots_epsg_28992.wkt")
142+
}
143+
136144
fn line_string<T>(name: &str) -> LineString<T>
137145
where
138146
T: WktFloat + Default + FromStr,

geo/CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Add Unary Union algorithm for fast union ops on adjacent / overlapping geometries
6+
- <https://github.com/georust/geo/pull/1246>
57
- Loosen bounds on `RemoveRepeatedPoints` trait (`num_traits::FromPrimitive` isn't required)
68
- <https://github.com/georust/geo/pull/1278>
79

geo/benches/coordinate_position.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use criterion::Criterion;
1111

1212
fn criterion_benchmark(c: &mut Criterion) {
1313
c.bench_function("Point position to rect", |bencher| {
14-
let plot_centroids: Vec<Point> = geo_test_fixtures::nl_plots()
14+
let plot_centroids: Vec<Point> = geo_test_fixtures::nl_plots_wgs84()
1515
.iter()
1616
.map(|plot| plot.centroid().unwrap())
1717
.collect();

geo/benches/intersection.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use geo::intersects::Intersects;
33
use geo::MultiPolygon;
44

55
fn multi_polygon_intersection(c: &mut Criterion) {
6-
let plot_polygons: MultiPolygon = geo_test_fixtures::nl_plots();
6+
let plot_polygons: MultiPolygon = geo_test_fixtures::nl_plots_wgs84();
77
let zone_polygons: MultiPolygon = geo_test_fixtures::nl_zones();
88

99
c.bench_function("MultiPolygon intersects", |bencher| {
@@ -30,7 +30,7 @@ fn multi_polygon_intersection(c: &mut Criterion) {
3030
fn rect_intersection(c: &mut Criterion) {
3131
use geo::algorithm::BoundingRect;
3232
use geo::geometry::Rect;
33-
let plot_bbox: Vec<Rect> = geo_test_fixtures::nl_plots()
33+
let plot_bbox: Vec<Rect> = geo_test_fixtures::nl_plots_wgs84()
3434
.iter()
3535
.map(|plot| plot.bounding_rect().unwrap())
3636
.collect();
@@ -63,7 +63,7 @@ fn rect_intersection(c: &mut Criterion) {
6363
fn point_rect_intersection(c: &mut Criterion) {
6464
use geo::algorithm::{BoundingRect, Centroid};
6565
use geo::geometry::{Point, Rect};
66-
let plot_centroids: Vec<Point> = geo_test_fixtures::nl_plots()
66+
let plot_centroids: Vec<Point> = geo_test_fixtures::nl_plots_wgs84()
6767
.iter()
6868
.map(|plot| plot.centroid().unwrap())
6969
.collect();
@@ -96,7 +96,7 @@ fn point_rect_intersection(c: &mut Criterion) {
9696
fn point_triangle_intersection(c: &mut Criterion) {
9797
use geo::{Centroid, TriangulateEarcut};
9898
use geo_types::{Point, Triangle};
99-
let plot_centroids: Vec<Point> = geo_test_fixtures::nl_plots()
99+
let plot_centroids: Vec<Point> = geo_test_fixtures::nl_plots_wgs84()
100100
.iter()
101101
.map(|plot| plot.centroid().unwrap())
102102
.collect();

geo/benches/prepared_geometry.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use geo_types::MultiPolygon;
55

66
fn criterion_benchmark(c: &mut Criterion) {
77
c.bench_function("relate prepared polygons", |bencher| {
8-
let plot_polygons: MultiPolygon = geo_test_fixtures::nl_plots();
8+
let plot_polygons: MultiPolygon = geo_test_fixtures::nl_plots_wgs84();
99
let zone_polygons = geo_test_fixtures::nl_zones();
1010

1111
bencher.iter(|| {
@@ -38,7 +38,7 @@ fn criterion_benchmark(c: &mut Criterion) {
3838
});
3939

4040
c.bench_function("relate unprepared polygons", |bencher| {
41-
let plot_polygons: MultiPolygon = geo_test_fixtures::nl_plots();
41+
let plot_polygons: MultiPolygon = geo_test_fixtures::nl_plots_wgs84();
4242
let zone_polygons = geo_test_fixtures::nl_zones();
4343

4444
bencher.iter(|| {

geo/src/algorithm/bool_ops/mod.rs

+93-9
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,25 @@ mod i_overlay_integration;
22
#[cfg(test)]
33
mod tests;
44

5-
use crate::bool_ops::i_overlay_integration::convert::{
6-
multi_polygon_from_shapes, ring_to_shape_path,
7-
};
8-
use crate::bool_ops::i_overlay_integration::BoolOpsCoord;
5+
use i_overlay_integration::convert::{multi_polygon_from_shapes, ring_to_shape_path};
6+
use i_overlay_integration::BoolOpsCoord;
7+
pub use i_overlay_integration::BoolOpsNum;
8+
9+
use crate::geometry::{LineString, MultiLineString, MultiPolygon, Polygon};
10+
use crate::winding_order::{Winding, WindingOrder};
11+
912
use i_overlay::core::fill_rule::FillRule;
13+
use i_overlay::core::overlay_rule::OverlayRule;
1014
use i_overlay::float::clip::FloatClip;
15+
use i_overlay::float::overlay::FloatOverlay;
1116
use i_overlay::float::single::SingleFloatOverlay;
1217
use i_overlay::string::clip::ClipRule;
13-
pub use i_overlay_integration::BoolOpsNum;
14-
15-
use crate::geometry::{LineString, MultiLineString, MultiPolygon, Polygon};
1618

1719
/// Boolean Operations on geometry.
1820
///
1921
/// Boolean operations are set operations on geometries considered as a subset
20-
/// of the 2-D plane. The operations supported are: intersection, union, xor or
21-
/// symmetric difference, and set-difference on pairs of 2-D geometries and
22+
/// of the 2-D plane. The operations supported are: intersection, union,
23+
/// symmetric difference (xor), and set-difference on pairs of 2-D geometries and
2224
/// clipping a 1-D geometry with self.
2325
///
2426
/// These operations are implemented on [`Polygon`] and the [`MultiPolygon`]
@@ -34,6 +36,11 @@ use crate::geometry::{LineString, MultiLineString, MultiPolygon, Polygon};
3436
/// In particular, taking `union` with an empty geom should remove degeneracies
3537
/// and fix invalid polygons as long the interior-exterior requirement above is
3638
/// satisfied.
39+
///
40+
/// # Performance
41+
///
42+
/// For union operations on a large number of [`Polygon`]s or [`MultiPolygons`],
43+
/// using [`unary_union`] will yield far better performance.
3744
pub trait BooleanOps {
3845
type Scalar: BoolOpsNum;
3946

@@ -57,18 +64,26 @@ pub trait BooleanOps {
5764
multi_polygon_from_shapes(shapes)
5865
}
5966

67+
/// Returns the overlapping regions shared by both `self` and `other`.
6068
fn intersection(
6169
&self,
6270
other: &impl BooleanOps<Scalar = Self::Scalar>,
6371
) -> MultiPolygon<Self::Scalar> {
6472
self.boolean_op(other, OpType::Intersection)
6573
}
74+
75+
/// Combines the regions of both `self` and `other` into a single geometry, removing
76+
/// overlaps and merging boundaries.
6677
fn union(&self, other: &impl BooleanOps<Scalar = Self::Scalar>) -> MultiPolygon<Self::Scalar> {
6778
self.boolean_op(other, OpType::Union)
6879
}
80+
81+
/// The regions that are in either `self` or `other`, but not in both.
6982
fn xor(&self, other: &impl BooleanOps<Scalar = Self::Scalar>) -> MultiPolygon<Self::Scalar> {
7083
self.boolean_op(other, OpType::Xor)
7184
}
85+
86+
/// The regions of `self` which are not in `other`.
7287
fn difference(
7388
&self,
7489
other: &impl BooleanOps<Scalar = Self::Scalar>,
@@ -109,6 +124,75 @@ pub enum OpType {
109124
Xor,
110125
}
111126

127+
/// Efficient [union](BooleanOps::union) of many adjacent / overlapping geometries
128+
///
129+
/// This is typically much faster than `union`ing a bunch of geometries together one at a time.
130+
///
131+
/// Note: Geometries can be wound in either direction, but the winding order must be consistent,
132+
/// and the polygon's interiors must be wound opposite to its exterior.
133+
///
134+
/// See [Orient] for more information.
135+
///
136+
/// [Orient]: crate::algorithm::orient::Orient
137+
///
138+
/// # Arguments
139+
///
140+
/// `boppables`: A collection of `Polygon` or `MultiPolygons` to union together.
141+
///
142+
/// returns the union of all the inputs.
143+
///
144+
/// # Examples
145+
///
146+
/// ```
147+
/// use geo::algorithm::unary_union;
148+
/// use geo::wkt;
149+
///
150+
/// let right_piece = wkt!(POLYGON((4. 0.,4. 4.,8. 4.,8. 0.,4. 0.)));
151+
/// let left_piece = wkt!(POLYGON((0. 0.,0. 4.,4. 4.,4. 0.,0. 0.)));
152+
///
153+
/// // touches neither right nor left piece
154+
/// let separate_piece = wkt!(POLYGON((14. 10.,14. 14.,18. 14.,18. 10.,14. 10.)));
155+
///
156+
/// let polygons = vec![left_piece, separate_piece, right_piece];
157+
/// let actual_output = unary_union(&polygons);
158+
///
159+
/// let expected_output = wkt!(MULTIPOLYGON(
160+
/// // left and right piece have been combined
161+
/// ((0. 0., 0. 4., 8. 4., 8. 0., 0. 0.)),
162+
/// // separate piece remains separate
163+
/// ((14. 10., 14. 14., 18. 14.,18. 10., 14. 10.))
164+
/// ));
165+
/// assert_eq!(actual_output, expected_output);
166+
/// ```
167+
pub fn unary_union<'a, B: BooleanOps + 'a>(
168+
boppables: impl IntoIterator<Item = &'a B>,
169+
) -> MultiPolygon<B::Scalar> {
170+
let mut winding_order: Option<WindingOrder> = None;
171+
let subject = boppables
172+
.into_iter()
173+
.flat_map(|boppable| {
174+
let rings = boppable.rings();
175+
rings
176+
.map(|ring| {
177+
if winding_order.is_none() {
178+
winding_order = ring.winding_order();
179+
}
180+
ring_to_shape_path(ring)
181+
})
182+
.collect::<Vec<_>>()
183+
})
184+
.collect::<Vec<_>>();
185+
186+
let fill_rule = if winding_order == Some(WindingOrder::Clockwise) {
187+
FillRule::Positive
188+
} else {
189+
FillRule::Negative
190+
};
191+
192+
let shapes = FloatOverlay::with_subj(&subject).overlay(OverlayRule::Subject, fill_rule);
193+
multi_polygon_from_shapes(shapes)
194+
}
195+
112196
impl<T: BoolOpsNum> BooleanOps for Polygon<T> {
113197
type Scalar = T;
114198

geo/src/algorithm/bool_ops/tests.rs

+89-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,94 @@
1-
use super::BooleanOps;
2-
use crate::{wkt, Convert, MultiPolygon, Relate};
1+
use super::{unary_union, BooleanOps};
2+
use crate::{wkt, Convert, MultiPolygon, Polygon, Relate};
3+
use std::time::Instant;
34
use wkt::ToWkt;
45

6+
#[test]
7+
fn test_unary_union() {
8+
let poly1: Polygon = wkt!(POLYGON((204.0 287.0,203.69670020700084 288.2213844497616,200.38308697914755 288.338793163584,204.0 287.0)));
9+
let poly2: Polygon = wkt!(POLYGON((210.0 290.0,204.07584923592933 288.2701221108328,212.24082541367974 285.47846008552216,210.0 290.0)));
10+
let poly3: Polygon = wkt!(POLYGON((211.0 292.0,202.07584923592933 288.2701221108328,212.24082541367974 285.47846008552216,210.0 290.0)));
11+
12+
let polys = vec![poly1.clone(), poly2.clone(), poly3.clone()];
13+
let poly_union = unary_union(&polys);
14+
assert_eq!(poly_union.0.len(), 1);
15+
16+
let multi_poly_12 = MultiPolygon::new(vec![poly1.clone(), poly2.clone()]);
17+
let multi_poly_3 = MultiPolygon::new(vec![poly3]);
18+
let multi_polys = vec![multi_poly_12.clone(), multi_poly_3.clone()];
19+
let multi_poly_union = unary_union(&multi_polys);
20+
assert_eq!(multi_poly_union.0.len(), 1);
21+
}
22+
23+
#[test]
24+
fn test_unary_union_errors() {
25+
let input: MultiPolygon = geo_test_fixtures::nl_plots_epsg_28992();
26+
27+
assert_eq!(input.0.len(), 316);
28+
29+
let input_area = input.signed_area();
30+
assert_relative_eq!(input_area, 763889.4732974821);
31+
32+
let naive_union = {
33+
let start = Instant::now();
34+
let mut output = MultiPolygon::new(Vec::new());
35+
for poly in input.iter() {
36+
output = output.union(poly);
37+
}
38+
let union = output;
39+
let duration = start.elapsed();
40+
println!("Time elapsed (naive): {:.2?}", duration);
41+
union
42+
};
43+
44+
let simplified_union = {
45+
let start = Instant::now();
46+
let union = unary_union(input.iter());
47+
let duration = start.elapsed();
48+
println!("Time elapsed (simplification): {:.2?}", duration);
49+
union
50+
};
51+
52+
use crate::algorithm::Area;
53+
let naive_area = naive_union.unsigned_area();
54+
let simplified_area = simplified_union.unsigned_area();
55+
assert_relative_eq!(naive_area, simplified_area, max_relative = 1e-5);
56+
57+
// Serial vs. parallel are expected to have slightly different results.
58+
//
59+
// Each boolean operation scales the floating point to a discrete
60+
// integer grid, which introduces some error, and this error factor depends on the magnitude
61+
// of the input.
62+
//
63+
// Because the serial vs. parallel approaches group inputs differently, error is accumulated
64+
// differently - hence the slightly different outputs.
65+
//
66+
// xor'ing the two shapes represents the magnitude of the difference between the two outputs.
67+
//
68+
// We want to verify that this error is small - it should be near 0, but the
69+
// magnitude of the error is relative to the magnitude of the input geometries, so we offset
70+
// both the error and 0 by `input_area` to make a scale relative comparison.
71+
let naive_vs_simplified_discrepancy = simplified_union.xor(&naive_union);
72+
assert_relative_eq!(
73+
input_area + naive_vs_simplified_discrepancy.unsigned_area(),
74+
0.0 + input_area,
75+
max_relative = 1e-5
76+
);
77+
78+
assert_eq!(simplified_union.0.len(), 1);
79+
assert_relative_eq!(simplified_area, input_area, max_relative = 1e-5);
80+
}
81+
82+
#[test]
83+
fn test_unary_union_winding() {
84+
let input: MultiPolygon = geo_test_fixtures::nl_plots_epsg_28992();
85+
86+
use crate::orient::{Direction, Orient};
87+
let default_winding_union = unary_union(input.orient(Direction::Default).iter());
88+
let reversed_winding_union = unary_union(input.orient(Direction::Reversed).iter());
89+
assert_eq!(default_winding_union, reversed_winding_union);
90+
}
91+
592
#[test]
693
fn jts_test_overlay_la_1() {
794
// From TestOverlayLA.xml test case with description "mLmA - A and B complex, overlapping and touching #1"

geo/src/algorithm/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ pub use kernels::{Kernel, Orientation};
66
pub mod area;
77
pub use area::Area;
88

9-
/// Boolean Ops such as union, xor, difference.
9+
/// Boolean Operations such as the union, xor, or difference of two geometries.
1010
pub mod bool_ops;
11-
pub use bool_ops::{BooleanOps, OpType};
11+
pub use bool_ops::{unary_union, BooleanOps, OpType};
1212

1313
/// Calculate the bounding rectangle of a `Geometry`.
1414
pub mod bounding_rect;

0 commit comments

Comments
 (0)