From 93428d2dff541c88f7388878a9d9f817777891f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20H=C3=BCgel?= Date: Tue, 30 Jul 2024 14:02:22 +0100 Subject: [PATCH 1/4] Relax restrictions on bool ops between polygon and multipolygon Whereas previously, boolean ops could only be performed between two geometries of the same type, it should now be possible to perform them between any combination of two Polygons and MultiPolygons --- geo/src/algorithm/bool_ops/mod.rs | 95 ++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/geo/src/algorithm/bool_ops/mod.rs b/geo/src/algorithm/bool_ops/mod.rs index 29d2ad1be..371a862c1 100644 --- a/geo/src/algorithm/bool_ops/mod.rs +++ b/geo/src/algorithm/bool_ops/mod.rs @@ -2,6 +2,24 @@ use geo_types::{MultiLineString, MultiPolygon}; use crate::{CoordsIter, GeoFloat, GeoNum, Polygon}; +/// Enum to represent geometry types that can be used in boolean ops +pub enum BopGeometry<'a, T: GeoNum> { + Polygon(&'a Polygon), + MultiPolygon(&'a MultiPolygon), +} + +impl<'a, T: GeoNum> From<&'a Polygon> for BopGeometry<'a, T> { + fn from(polygon: &'a Polygon) -> Self { + BopGeometry::Polygon(polygon) + } +} + +impl<'a, T: GeoNum> From<&'a MultiPolygon> for BopGeometry<'a, T> { + fn from(multi_polygon: &'a MultiPolygon) -> Self { + BopGeometry::MultiPolygon(multi_polygon) + } +} + /// Boolean Operations on geometry. /// /// Boolean operations are set operations on geometries considered as a subset @@ -25,23 +43,46 @@ use crate::{CoordsIter, GeoFloat, GeoNum, Polygon}; pub trait BooleanOps: Sized { type Scalar: GeoNum; - fn boolean_op(&self, other: &Self, op: OpType) -> MultiPolygon; - fn intersection(&self, other: &Self) -> MultiPolygon { + fn boolean_op<'a, G>(&self, other: &'a G, op: OpType) -> MultiPolygon + where + BopGeometry<'a, Self::Scalar>: From<&'a G>, + ::Scalar: 'a; + + fn intersection<'a, G>(&self, other: &'a G) -> MultiPolygon + where + BopGeometry<'a, Self::Scalar>: From<&'a G>, + ::Scalar: 'a, + { self.boolean_op(other, OpType::Intersection) } - fn union(&self, other: &Self) -> MultiPolygon { + + fn union<'a, G>(&self, other: &'a G) -> MultiPolygon + where + BopGeometry<'a, Self::Scalar>: From<&'a G>, + ::Scalar: 'a, + { self.boolean_op(other, OpType::Union) } - fn xor(&self, other: &Self) -> MultiPolygon { + + fn xor<'a, G>(&self, other: &'a G) -> MultiPolygon + where + BopGeometry<'a, Self::Scalar>: From<&'a G>, + ::Scalar: 'a, + { self.boolean_op(other, OpType::Xor) } - fn difference(&self, other: &Self) -> MultiPolygon { + + fn difference<'a, G>(&self, other: &'a G) -> MultiPolygon + where + BopGeometry<'a, Self::Scalar>: From<&'a G>, + ::Scalar: 'a, + { self.boolean_op(other, OpType::Difference) } /// Clip a 1-D geometry with self. /// - /// Returns the portion of `ls` that lies within `self` (known as the set-theoeretic + /// Returns the portion of `ls` that lies within `self` (known as the set-theoretic /// intersection) if `invert` is false, and the difference (`ls - self`) otherwise. fn clip( &self, @@ -61,11 +102,21 @@ pub enum OpType { impl BooleanOps for Polygon { type Scalar = T; - fn boolean_op(&self, other: &Self, op: OpType) -> MultiPolygon { + fn boolean_op<'a, G>(&self, other: &'a G, op: OpType) -> MultiPolygon + where + BopGeometry<'a, Self::Scalar>: From<&'a G>, + ::Scalar: 'a, + { + let other: BopGeometry<'a, Self::Scalar> = other.into(); let spec = BoolOp::from(op); let mut bop = Proc::new(spec, self.coords_count() + other.coords_count()); bop.add_polygon(self, 0); - bop.add_polygon(other, 1); + match other { + BopGeometry::Polygon(other_polygon) => bop.add_polygon(other_polygon, 1), + BopGeometry::MultiPolygon(other_multi_polygon) => { + bop.add_multi_polygon(other_multi_polygon, 1) + } + } bop.sweep() } @@ -83,14 +134,25 @@ impl BooleanOps for Polygon { bop.sweep() } } + impl BooleanOps for MultiPolygon { type Scalar = T; - fn boolean_op(&self, other: &Self, op: OpType) -> MultiPolygon { + fn boolean_op<'a, G>(&self, other: &'a G, op: OpType) -> MultiPolygon + where + BopGeometry<'a, Self::Scalar>: From<&'a G>, + ::Scalar: 'a, + { + let other: BopGeometry<'a, Self::Scalar> = other.into(); let spec = BoolOp::from(op); let mut bop = Proc::new(spec, self.coords_count() + other.coords_count()); bop.add_multi_polygon(self, 0); - bop.add_multi_polygon(other, 1); + match other { + BopGeometry::Polygon(other_polygon) => bop.add_polygon(other_polygon, 1), + BopGeometry::MultiPolygon(other_multi_polygon) => { + bop.add_multi_polygon(other_multi_polygon, 1) + } + } bop.sweep() } @@ -102,13 +164,20 @@ impl BooleanOps for MultiPolygon { let spec = ClipOp::new(invert); let mut bop = Proc::new(spec, self.coords_count() + ls.coords_count()); bop.add_multi_polygon(self, 0); - ls.0.iter().enumerate().for_each(|(idx, l)| { - bop.add_line_string(l, idx + 1); - }); + ls.0.iter().for_each(|l| bop.add_line_string(l, 0)); bop.sweep() } } +impl<'a, T: GeoNum> BopGeometry<'a, T> { + fn coords_count(&self) -> usize { + match self { + BopGeometry::Polygon(polygon) => polygon.coords_count(), + BopGeometry::MultiPolygon(multi_polygon) => multi_polygon.coords_count(), + } + } +} + mod op; use op::*; mod assembly; From 0172e26bc41b20ef9d35de2cd3aa9925853a4926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20H=C3=BCgel?= Date: Tue, 30 Jul 2024 15:44:51 +0100 Subject: [PATCH 2/4] Add tests to ensure heterogeneous geoms can be unioned --- geo/src/algorithm/bool_ops/tests.rs | 35 ++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/geo/src/algorithm/bool_ops/tests.rs b/geo/src/algorithm/bool_ops/tests.rs index 11373dcbd..8fe0ad7b0 100644 --- a/geo/src/algorithm/bool_ops/tests.rs +++ b/geo/src/algorithm/bool_ops/tests.rs @@ -1,4 +1,4 @@ -use crate::{LineString, MultiPolygon, Polygon}; +use crate::{LineString, MultiPolygon, Polygon, Relate}; use log::{error, info}; use std::{ @@ -116,6 +116,39 @@ fn test_complex_rects() -> Result<()> { } Ok(()) } + +#[test] +fn single_and_multi() { + let wkt1 = "POLYGON ((110 310, 220 310, 220 210, 110 210, 110 310))"; + // multipolygon containing a single polygon + let wkt2 = "MULTIPOLYGON (((80 260, 90 260, 90 250, 80 250, 80 260)))"; + // From JTS union op + let res = "MULTIPOLYGON (((110 310, 220 310, 220 210, 110 210, 110 310)), ((80 260, 90 260, 90 250, 80 250, 80 260)))"; + let poly = Polygon::::try_from_wkt_str(&wkt1).unwrap(); + let mpoly = MultiPolygon::::try_from_wkt_str(&wkt2).unwrap(); + let respoly = MultiPolygon::::try_from_wkt_str(&res).unwrap(); + let union = mpoly.union(&poly); + let intersection_matrix = respoly.relate(&union); + // coords will be arranged differently, but we only care about topology + assert!(intersection_matrix.is_equal_topo()); +} + +#[test] +fn multi_and_single() { + let wkt1 = "POLYGON ((110 310, 220 310, 220 210, 110 210, 110 310))"; + // multipolygon containing a single polygon + let wkt2 = "MULTIPOLYGON (((80 260, 90 260, 90 250, 80 250, 80 260)))"; + // From JTS union op + let res = "MULTIPOLYGON (((110 310, 220 310, 220 210, 110 210, 110 310)), ((80 260, 90 260, 90 250, 80 250, 80 260)))"; + let poly = Polygon::::try_from_wkt_str(&wkt1).unwrap(); + let mpoly = MultiPolygon::::try_from_wkt_str(&wkt2).unwrap(); + let respoly = MultiPolygon::::try_from_wkt_str(&res).unwrap(); + let union = poly.union(&mpoly); + let intersection_matrix = respoly.relate(&union); + // coords will be arranged differently, but we only care about topology + assert!(intersection_matrix.is_equal_topo()); +} + #[test] fn test_complex_rects1() -> Result<()> { let wkt1 = "MULTIPOLYGON(((-1 -2,-1.0000000000000002 2,-0.8823529411764707 2,-0.8823529411764706 -2,-1 -2)))"; From fddc3d1d9845d11685d43658d8941dde4d821e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20H=C3=BCgel?= Date: Tue, 30 Jul 2024 15:50:23 +0100 Subject: [PATCH 3/4] Appease clippy --- geo/src/algorithm/bool_ops/tests.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/geo/src/algorithm/bool_ops/tests.rs b/geo/src/algorithm/bool_ops/tests.rs index 8fe0ad7b0..f287f04b4 100644 --- a/geo/src/algorithm/bool_ops/tests.rs +++ b/geo/src/algorithm/bool_ops/tests.rs @@ -124,9 +124,9 @@ fn single_and_multi() { let wkt2 = "MULTIPOLYGON (((80 260, 90 260, 90 250, 80 250, 80 260)))"; // From JTS union op let res = "MULTIPOLYGON (((110 310, 220 310, 220 210, 110 210, 110 310)), ((80 260, 90 260, 90 250, 80 250, 80 260)))"; - let poly = Polygon::::try_from_wkt_str(&wkt1).unwrap(); - let mpoly = MultiPolygon::::try_from_wkt_str(&wkt2).unwrap(); - let respoly = MultiPolygon::::try_from_wkt_str(&res).unwrap(); + let poly = Polygon::::try_from_wkt_str(wkt1).unwrap(); + let mpoly = MultiPolygon::::try_from_wkt_str(wkt2).unwrap(); + let respoly = MultiPolygon::::try_from_wkt_str(res).unwrap(); let union = mpoly.union(&poly); let intersection_matrix = respoly.relate(&union); // coords will be arranged differently, but we only care about topology @@ -140,9 +140,9 @@ fn multi_and_single() { let wkt2 = "MULTIPOLYGON (((80 260, 90 260, 90 250, 80 250, 80 260)))"; // From JTS union op let res = "MULTIPOLYGON (((110 310, 220 310, 220 210, 110 210, 110 310)), ((80 260, 90 260, 90 250, 80 250, 80 260)))"; - let poly = Polygon::::try_from_wkt_str(&wkt1).unwrap(); - let mpoly = MultiPolygon::::try_from_wkt_str(&wkt2).unwrap(); - let respoly = MultiPolygon::::try_from_wkt_str(&res).unwrap(); + let poly = Polygon::::try_from_wkt_str(wkt1).unwrap(); + let mpoly = MultiPolygon::::try_from_wkt_str(wkt2).unwrap(); + let respoly = MultiPolygon::::try_from_wkt_str(res).unwrap(); let union = poly.union(&mpoly); let intersection_matrix = respoly.relate(&union); // coords will be arranged differently, but we only care about topology From 7ee3c25192e1b330c19bb9820e9e8495a02631c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20H=C3=BCgel?= Date: Wed, 31 Jul 2024 00:33:26 +0200 Subject: [PATCH 4/4] Update tests.rs Co-authored-by: Michael Kirk --- geo/src/algorithm/bool_ops/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geo/src/algorithm/bool_ops/tests.rs b/geo/src/algorithm/bool_ops/tests.rs index f287f04b4..33b04ffd9 100644 --- a/geo/src/algorithm/bool_ops/tests.rs +++ b/geo/src/algorithm/bool_ops/tests.rs @@ -124,7 +124,7 @@ fn single_and_multi() { let wkt2 = "MULTIPOLYGON (((80 260, 90 260, 90 250, 80 250, 80 260)))"; // From JTS union op let res = "MULTIPOLYGON (((110 310, 220 310, 220 210, 110 210, 110 310)), ((80 260, 90 260, 90 250, 80 250, 80 260)))"; - let poly = Polygon::::try_from_wkt_str(wkt1).unwrap(); + let poly = crate::wkt!(POLYGON ((110 310, 220 310, 220 210, 110 210, 110 310))); let mpoly = MultiPolygon::::try_from_wkt_str(wkt2).unwrap(); let respoly = MultiPolygon::::try_from_wkt_str(res).unwrap(); let union = mpoly.union(&poly);