diff --git a/c/sedona-tg/src/tg.rs b/c/sedona-tg/src/tg.rs index e6c8bdb8f..0759f4508 100644 --- a/c/sedona-tg/src/tg.rs +++ b/c/sedona-tg/src/tg.rs @@ -23,11 +23,12 @@ use crate::{error::TgError, tg_bindgen::*}; /// /// These index types are used to inform the internal index used in geometries /// to accelerate repeated operations. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub enum IndexType { /// Do not build an index Unindexed, /// Use the statically set default + #[default] Default, /// Use natural indexing Natural, @@ -35,12 +36,6 @@ pub enum IndexType { YStripes, } -impl Default for IndexType { - fn default() -> Self { - Self::Default - } -} - impl IndexType { fn to_tg(self) -> tg_index { match self { diff --git a/rust/sedona-geo-generic-alg/benches/intersection.rs b/rust/sedona-geo-generic-alg/benches/intersection.rs index a5891e0bb..d981ac38e 100644 --- a/rust/sedona-geo-generic-alg/benches/intersection.rs +++ b/rust/sedona-geo-generic-alg/benches/intersection.rs @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. use criterion::{criterion_group, criterion_main, Criterion}; +use geo::Triangle; use geo_traits::to_geo::ToGeoGeometry; use geo_types::Geometry; use sedona_geo_generic_alg::MultiPolygon; @@ -49,6 +50,26 @@ fn multi_polygon_intersection(c: &mut Criterion) { }); }); + c.bench_function("MultiPolygon intersects 2", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_geoms { + for b in &zone_geoms { + if criterion::black_box(a.intersects(b)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 974); + assert_eq!(non_intersects, 27782); + }); + }); + c.bench_function("MultiPolygon intersects geo", |bencher| { bencher.iter(|| { let mut intersects = 0; @@ -68,6 +89,26 @@ fn multi_polygon_intersection(c: &mut Criterion) { assert_eq!(non_intersects, 27782); }); }); + + c.bench_function("MultiPolygon intersects geo 2", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_geoms { + for b in &zone_geoms { + if criterion::black_box(geo::Intersects::intersects(a, b)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 974); + assert_eq!(non_intersects, 27782); + }); + }); } fn multi_polygon_intersection_wkb(c: &mut Criterion) { @@ -395,6 +436,97 @@ fn point_triangle_intersection(c: &mut Criterion) { }); } +fn linestring_polygon_intersection(c: &mut Criterion) { + use geo::{coord, line_string, LineString, Polygon, Rect}; + c.bench_function("LineString above Polygon", |bencher| { + let ls = line_string![ + coord! {x:0., y:1.}, + coord! {x:5., y:6.}, + coord! {x:10., y:1.} + ]; + let poly = Polygon::new( + line_string![ + coord! {x:0., y:0.}, + coord! {x:5., y:4.}, + coord! {x:10., y:0.} + ], + vec![], + ); + + bencher.iter(|| { + assert!(!criterion::black_box(&ls).intersects(criterion::black_box(&poly))); + }); + }); + c.bench_function("LineString above Triangle", |bencher| { + let ls = line_string![ + coord! {x:0., y:1.}, + coord! {x:5., y:6.}, + coord! {x:10., y:1.} + ]; + let poly = Triangle::new( + coord! {x:0., y:0.}, + coord! {x:5., y:4.}, + coord! {x:10., y:0.}, + ); + + bencher.iter(|| { + assert!(!criterion::black_box(&ls).intersects(criterion::black_box(&poly))); + }); + }); + c.bench_function("LineString around Rectangle", |bencher| { + let ls = line_string![ + coord! {x:-1., y:-1.}, + coord! {x:-1., y:11.}, + coord! {x:11., y:11.} + ]; + let poly = Rect::new(coord! {x:0., y:0.}, coord! {x:10., y:10.}); + + bencher.iter(|| { + assert!(!criterion::black_box(&ls).intersects(criterion::black_box(&poly))); + }); + }); + + c.bench_function("long disjoint ", |bencher| { + let ls = LineString::from_iter((0..1000).map(|x| coord! {x:x as f64, y:x as f64})); + let ln = (0..1000).map(|x| coord! {x:x as f64, y:(x-1) as f64}); + let k = vec![coord! {x:-5. ,y:-5. }].into_iter(); + let ext = ln.chain(k); + + let poly = Polygon::new(LineString::from_iter(ext), vec![]); + + bencher.iter(|| { + assert!(!criterion::black_box(&ls).intersects(criterion::black_box(&poly))); + }); + }); + + c.bench_function("ls within poly ", |bencher| { + let ls = line_string![ + coord! {x:1., y:1.}, + coord! {x:5., y:6.}, + coord! {x:9., y:1.} + ]; + + let poly: Polygon = Rect::new(coord! {x:0., y:0.}, coord! {x:10., y:10.}).into(); + + bencher.iter(|| { + assert!(criterion::black_box(&ls).intersects(criterion::black_box(&poly))); + }); + }); + c.bench_function("ls within rect ", |bencher| { + let ls = line_string![ + coord! {x:1., y:1.}, + coord! {x:5., y:6.}, + coord! {x:9., y:1.} + ]; + + let poly = Rect::new(coord! {x:0., y:0.}, coord! {x:10., y:10.}); + + bencher.iter(|| { + assert!(criterion::black_box(&ls).intersects(criterion::black_box(&poly))); + }); + }); +} + criterion_group! { name = bench_multi_polygons; config = Criterion::default().sample_size(10); @@ -444,12 +576,15 @@ criterion_group! { targets = point_polygon_intersection_wkb_conv } +criterion_group! { bench_linestring_poly,linestring_polygon_intersection} + criterion_main!( bench_multi_polygons, bench_multi_polygons_wkb, bench_multi_polygons_wkb_aligned, bench_multi_polygons_wkb_conv, bench_rects, + bench_linestring_poly, bench_point_rect, bench_point_triangle, bench_point_polygon, diff --git a/rust/sedona-geo-generic-alg/src/algorithm/centroid.rs b/rust/sedona-geo-generic-alg/src/algorithm/centroid.rs index 704cf0319..18d197c1f 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/centroid.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/centroid.rs @@ -17,7 +17,7 @@ //! Generic Centroid algorithm //! //! Ported (and contains copied code) from `geo::algorithm::centroid`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. use core::borrow::Borrow; use std::cmp::Ordering; @@ -870,7 +870,7 @@ mod test { // Tests: Centroid of MultiLineString #[test] fn empty_multilinestring_test() { - let mls: MultiLineString = MultiLineString::new(vec![]); + let mls: MultiLineString = MultiLineString::empty(); let centroid = mls.centroid(); assert!(centroid.is_none()); } @@ -1049,7 +1049,7 @@ mod test { fn empty_interior_polygon_test() { let poly = Polygon::new( LineString::from(vec![p(0., 0.), p(0., 1.), p(1., 1.), p(1., 0.), p(0., 0.)]), - vec![LineString::new(vec![])], + vec![LineString::empty()], ); assert_eq!(poly.centroid(), Some(p(0.5, 0.5))); } @@ -1072,7 +1072,7 @@ mod test { // Tests: Centroid of MultiPolygon #[test] fn empty_multipolygon_polygon_test() { - assert!(MultiPolygon::::new(Vec::new()).centroid().is_none()); + assert!(MultiPolygon::::empty().centroid().is_none()); } #[test] diff --git a/rust/sedona-geo-generic-alg/src/algorithm/coordinate_position.rs b/rust/sedona-geo-generic-alg/src/algorithm/coordinate_position.rs index 1adb2f5ab..aaef5510e 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/coordinate_position.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/coordinate_position.rs @@ -17,7 +17,7 @@ //! Generic Coordinate Position algorithm //! //! Ported (and contains copied code) from `geo::algorithm::coordinate_position`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. use core::borrow::Borrow; use std::cmp::Ordering; @@ -589,7 +589,7 @@ mod test { #[test] fn test_empty_poly() { - let square_poly: Polygon = Polygon::new(LineString::new(vec![]), vec![]); + let square_poly: Polygon = Polygon::empty(); assert_eq!( square_poly.coordinate_position(&Coord::zero()), CoordPos::Outside diff --git a/rust/sedona-geo-generic-alg/src/algorithm/dimensions.rs b/rust/sedona-geo-generic-alg/src/algorithm/dimensions.rs index a2f7dc5c5..192de04c6 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/dimensions.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/dimensions.rs @@ -17,7 +17,7 @@ //! Generic Dimensions (HasDimensions) algorithm //! //! Ported (and contains copied code) from `geo::algorithm::dimensions`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. use core::borrow::Borrow; use sedona_geo_traits_ext::*; @@ -73,7 +73,7 @@ pub trait HasDimensions { /// ]); /// assert!(!line_string.is_empty()); /// - /// let empty_line_string: LineString = LineString::new(vec![]); + /// let empty_line_string: LineString = LineString::empty(); /// assert!(empty_line_string.is_empty()); /// /// let point = Point::new(0.0, 0.0); @@ -112,7 +112,7 @@ pub trait HasDimensions { /// assert_eq!(Dimensions::ZeroDimensional, point.dimensions()); /// /// // An `Empty` dimensionality is distinct from, and less than, being 0-dimensional - /// let empty_collection = GeometryCollection::::new_from(vec![]); + /// let empty_collection = GeometryCollection::::empty(); /// assert_eq!(Dimensions::Empty, empty_collection.dimensions()); /// assert!(empty_collection.dimensions() < point.dimensions()); /// ``` @@ -147,7 +147,7 @@ pub trait HasDimensions { /// let geometry_collection = GeometryCollection::new_from(vec![degenerate_line_rect.into(), degenerate_point_rect.into()]); /// assert_eq!(Dimensions::ZeroDimensional, geometry_collection.boundary_dimensions()); /// - /// let geometry_collection = GeometryCollection::::new_from(vec![]); + /// let geometry_collection = GeometryCollection::::empty(); /// assert_eq!(Dimensions::Empty, geometry_collection.boundary_dimensions()); /// ``` fn boundary_dimensions(&self) -> Dimensions; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/collections.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/collections.rs index f0e5956cc..1bf2385d5 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/intersects/collections.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/collections.rs @@ -17,7 +17,7 @@ //! Intersects implementations for Geometry and GeometryCollection (generic) //! //! Ported (and contains copied code) from `geo::algorithm::intersects::collections`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. use core::borrow::Borrow; use sedona_geo_traits_ext::*; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/coordinate.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/coordinate.rs index 31b3585e3..1d1c03f06 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/intersects/coordinate.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/coordinate.rs @@ -17,7 +17,7 @@ //! Intersects implementations for Coord and Point (generic) //! //! Ported (and contains copied code) from `geo::algorithm::intersects::coordinate`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. use sedona_geo_traits_ext::{CoordTag, CoordTraitExt, PointTag, PointTraitExt}; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/line.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/line.rs index 025855b5f..3939d9cab 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/intersects/line.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/line.rs @@ -17,7 +17,7 @@ //! Intersects implementations for Line (generic) //! //! Ported (and contains copied code) from `geo::algorithm::intersects::line`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. use sedona_geo_traits_ext::*; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/line_string.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/line_string.rs index 74fddcbe1..7a39fadf0 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/intersects/line_string.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/line_string.rs @@ -17,7 +17,7 @@ //! Intersects implementations for LineString and MultiLineString (generic) //! //! Ported (and contains copied code) from `geo::algorithm::intersects::line_string`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. use sedona_geo_traits_ext::*; @@ -46,15 +46,98 @@ macro_rules! impl_intersects_line_string_from_line { impl_intersects_line_string_from_line!(CoordTraitExt, CoordTag); impl_intersects_line_string_from_line!(PointTraitExt, PointTag); impl_intersects_line_string_from_line!(LineStringTraitExt, LineStringTag); -impl_intersects_line_string_from_line!(PolygonTraitExt, PolygonTag); impl_intersects_line_string_from_line!(MultiPointTraitExt, MultiPointTag); impl_intersects_line_string_from_line!(MultiLineStringTraitExt, MultiLineStringTag); -impl_intersects_line_string_from_line!(MultiPolygonTraitExt, MultiPolygonTag); impl_intersects_line_string_from_line!(GeometryTraitExt, GeometryTag); impl_intersects_line_string_from_line!(GeometryCollectionTraitExt, GeometryCollectionTag); impl_intersects_line_string_from_line!(LineTraitExt, LineTag); -impl_intersects_line_string_from_line!(RectTraitExt, RectTag); -impl_intersects_line_string_from_line!(TriangleTraitExt, TriangleTag); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: LineStringTraitExt, + RHS: PolygonTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + if self.num_coords() == 0 { + return false; + } + if let Some(exterior) = rhs.exterior_ext() { + if has_disjoint_bboxes(self, rhs) { + return false; + } + + // if no lines intersections, then linestring is either disjoint or within the polygon + // therefore sufficient to check any one point + let first_coord = unsafe { self.geo_coord_unchecked(0) }; + first_coord.intersects(rhs) + || self.lines().any(|l| { + exterior.lines().any(|other| l.intersects(&other)) + || rhs + .interiors_ext() + .any(|interior| interior.lines().any(|other| l.intersects(&other))) + }) + } else { + false + } + } +} + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: LineStringTraitExt, + RHS: MultiPolygonTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + if self.num_coords() == 0 { + return false; + } + if has_disjoint_bboxes(self, rhs) { + return false; + } + // splitting into `LineString intersects Polygon` + rhs.polygons_ext().any(|poly| self.intersects(&poly)) + } +} + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: LineStringTraitExt, + RHS: RectTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + if self.num_coords() == 0 { + return false; + } + + let first_coord = unsafe { self.geo_coord_unchecked(0) }; + first_coord.intersects(rhs) + || self + .lines() + .any(|l| rhs.to_lines().iter().any(|other| l.intersects(&other))) + } +} + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: LineStringTraitExt, + RHS: TriangleTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + if self.num_coords() == 0 { + return false; + } + + let first_coord = unsafe { self.geo_coord_unchecked(0) }; + first_coord.intersects(rhs) + || self + .lines() + .any(|l| rhs.to_lines().iter().any(|other| l.intersects(&other))) + } +} symmetric_intersects_trait_impl!( GeoNum, @@ -148,3 +231,83 @@ symmetric_intersects_trait_impl!( MultiLineStringTraitExt, MultiLineStringTag ); + +#[cfg(test)] +mod test { + use geo::{Convert, CoordsIter}; + + use super::*; + use crate::wkt; + + #[test] + fn test_linestring_inside_polygon() { + let ls: LineString = wkt! {LINESTRING(1 1, 2 2)}.convert(); + let poly: Polygon = Rect::new((0, 0), (10, 10)).to_polygon().convert(); + assert!(ls.intersects(&poly)); + } + + #[test] + fn test_linestring_partial_polygon() { + let ls: LineString = wkt! {LINESTRING(-1 -1, 2 2)}.convert(); + let poly: Polygon = Rect::new((0, 0), (10, 10)).to_polygon().convert(); + assert!(ls.intersects(&poly)); + } + #[test] + fn test_linestring_disjoint_polygon() { + let ls: LineString = wkt! {LINESTRING(-1 -1, -2 -2)}.convert(); + let poly: Polygon = Rect::new((0, 0), (10, 10)).to_polygon().convert(); + assert!(!ls.intersects(&poly)); + } + #[test] + fn test_linestring_in_polygon_hole() { + let ls: LineString = wkt! {LINESTRING(4 4, 6 6)}.convert(); + let bound = Rect::new((0, 0), (10, 10)).convert(); + let hole = Rect::new((1, 1), (9, 9)).convert(); + let poly = Polygon::new( + bound.exterior_coords_iter().collect(), + vec![hole.exterior_coords_iter().collect()], + ); + + assert!(!ls.intersects(&poly)); + } + + // ls_rect + #[test] + fn test_linestring_inside_rect() { + let ls: LineString = wkt! {LINESTRING(1 1, 2 2)}.convert(); + let poly: Rect = Rect::new((0, 0), (10, 10)).convert(); + assert!(ls.intersects(&poly)); + } + #[test] + fn test_linestring_partial_rect() { + let ls: LineString = wkt! {LINESTRING(-1 -1, 2 2)}.convert(); + let poly: Rect = Rect::new((0, 0), (10, 10)).convert(); + assert!(ls.intersects(&poly)); + } + #[test] + fn test_linestring_disjoint_rect() { + let ls: LineString = wkt! {LINESTRING(-1 -1, -2 -2)}.convert(); + let poly: Rect = Rect::new((0, 0), (10, 10)).convert(); + assert!(!ls.intersects(&poly)); + } + + // ls_triangle + #[test] + fn test_linestring_inside_triangle() { + let ls: LineString = wkt! {LINESTRING(5 5, 5 4)}.convert(); + let poly: Triangle = wkt! {TRIANGLE(0 0, 10 0, 5 10)}.convert(); + assert!(ls.intersects(&poly)); + } + #[test] + fn test_linestring_partial_triangle() { + let ls: LineString = wkt! {LINESTRING(5 5, 5 -4)}.convert(); + let poly: Triangle = wkt! {TRIANGLE(0 0, 10 0, 5 10)}.convert(); + assert!(ls.intersects(&poly)); + } + #[test] + fn test_linestring_disjoint_triangle() { + let ls: LineString = wkt! {LINESTRING(5 -5, 5 -4)}.convert(); + let poly: Triangle = wkt! {TRIANGLE(0 0, 10 0, 5 10)}.convert(); + assert!(!ls.intersects(&poly)); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/mod.rs index a0b5df337..cfab5c747 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/intersects/mod.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/mod.rs @@ -17,7 +17,7 @@ //! Generic Intersects algorithm //! //! Ported (and contains copied code) from `geo::algorithm::intersects` (and its submodules): -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. use sedona_geo_traits_ext::GeoTraitExtWithTypeTag; @@ -174,7 +174,7 @@ mod test { #[test] fn empty_linestring2_test() { let linestring = line_string![(x: 3., y: 2.), (x: 7., y: 6.)]; - assert!(!linestring.intersects(&LineString::new(Vec::new()))); + assert!(!linestring.intersects(&LineString::empty())); } #[test] fn empty_all_linestring_test() { @@ -630,6 +630,7 @@ mod test { let _ = c.intersects(&multi_ls); let _ = c.intersects(&multi_poly); + let _ = pt.intersects(&c); let _ = pt.intersects(&pt); let _ = pt.intersects(&ln); let _ = pt.intersects(&ls); @@ -641,6 +642,8 @@ mod test { let _ = pt.intersects(&multi_point); let _ = pt.intersects(&multi_ls); let _ = pt.intersects(&multi_poly); + + let _ = ln.intersects(&c); let _ = ln.intersects(&pt); let _ = ln.intersects(&ln); let _ = ln.intersects(&ls); @@ -652,6 +655,8 @@ mod test { let _ = ln.intersects(&multi_point); let _ = ln.intersects(&multi_ls); let _ = ln.intersects(&multi_poly); + + let _ = ls.intersects(&c); let _ = ls.intersects(&pt); let _ = ls.intersects(&ln); let _ = ls.intersects(&ls); @@ -663,6 +668,8 @@ mod test { let _ = ls.intersects(&multi_point); let _ = ls.intersects(&multi_ls); let _ = ls.intersects(&multi_poly); + + let _ = poly.intersects(&c); let _ = poly.intersects(&pt); let _ = poly.intersects(&ln); let _ = poly.intersects(&ls); @@ -674,6 +681,8 @@ mod test { let _ = poly.intersects(&multi_point); let _ = poly.intersects(&multi_ls); let _ = poly.intersects(&multi_poly); + + let _ = rect.intersects(&c); let _ = rect.intersects(&pt); let _ = rect.intersects(&ln); let _ = rect.intersects(&ls); @@ -685,6 +694,8 @@ mod test { let _ = rect.intersects(&multi_point); let _ = rect.intersects(&multi_ls); let _ = rect.intersects(&multi_poly); + + let _ = tri.intersects(&c); let _ = tri.intersects(&pt); let _ = tri.intersects(&ln); let _ = tri.intersects(&ls); @@ -696,6 +707,8 @@ mod test { let _ = tri.intersects(&multi_point); let _ = tri.intersects(&multi_ls); let _ = tri.intersects(&multi_poly); + + let _ = geom.intersects(&c); let _ = geom.intersects(&pt); let _ = geom.intersects(&ln); let _ = geom.intersects(&ls); @@ -707,6 +720,8 @@ mod test { let _ = geom.intersects(&multi_point); let _ = geom.intersects(&multi_ls); let _ = geom.intersects(&multi_poly); + + let _ = gc.intersects(&c); let _ = gc.intersects(&pt); let _ = gc.intersects(&ln); let _ = gc.intersects(&ls); @@ -718,6 +733,8 @@ mod test { let _ = gc.intersects(&multi_point); let _ = gc.intersects(&multi_ls); let _ = gc.intersects(&multi_poly); + + let _ = multi_point.intersects(&c); let _ = multi_point.intersects(&pt); let _ = multi_point.intersects(&ln); let _ = multi_point.intersects(&ls); @@ -729,6 +746,8 @@ mod test { let _ = multi_point.intersects(&multi_point); let _ = multi_point.intersects(&multi_ls); let _ = multi_point.intersects(&multi_poly); + + let _ = multi_ls.intersects(&c); let _ = multi_ls.intersects(&pt); let _ = multi_ls.intersects(&ln); let _ = multi_ls.intersects(&ls); @@ -740,6 +759,8 @@ mod test { let _ = multi_ls.intersects(&multi_point); let _ = multi_ls.intersects(&multi_ls); let _ = multi_ls.intersects(&multi_poly); + + let _ = multi_poly.intersects(&c); let _ = multi_poly.intersects(&pt); let _ = multi_poly.intersects(&ln); let _ = multi_poly.intersects(&ls); diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/point.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/point.rs index 1426050c1..e0b28521c 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/intersects/point.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/point.rs @@ -17,7 +17,7 @@ //! Intersects implementations for Point and MultiPoint (generic) //! //! Ported (and contains copied code) from `geo::algorithm::intersects::point`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. use sedona_geo_traits_ext::*; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/polygon.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/polygon.rs index 76cef479f..25fbe5042 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/intersects/polygon.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/polygon.rs @@ -17,7 +17,7 @@ //! Intersects implementations for Polygon and MultiPolygon (generic) //! //! Ported (and contains copied code) from `geo::algorithm::intersects::polygon`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. use super::{has_disjoint_bboxes, IntersectsTrait}; use crate::coordinate_position::CoordPos; @@ -76,38 +76,6 @@ symmetric_intersects_trait_impl!( MultiLineStringTag ); -impl IntersectsTrait for LHS -where - T: GeoNum, - LHS: PolygonTraitExt, - RHS: RectTraitExt, -{ - fn intersects_trait(&self, rhs: &RHS) -> bool { - self.intersects_trait(&rhs.to_polygon()) - } -} - -symmetric_intersects_trait_impl!(GeoNum, RectTraitExt, RectTag, PolygonTraitExt, PolygonTag); - -impl IntersectsTrait for LHS -where - T: GeoNum, - LHS: PolygonTraitExt, - RHS: TriangleTraitExt, -{ - fn intersects_trait(&self, rhs: &RHS) -> bool { - self.intersects_trait(&rhs.to_polygon()) - } -} - -symmetric_intersects_trait_impl!( - GeoNum, - TriangleTraitExt, - TriangleTag, - PolygonTraitExt, - PolygonTag -); - impl IntersectsTrait for LHS where T: GeoNum, @@ -122,11 +90,49 @@ where if let (Some(self_exterior), Some(polygon_exterior)) = (self.exterior_ext(), polygon.exterior_ext()) { - // self intersects (or contains) any line in polygon - self.intersects_trait(&polygon_exterior) || - polygon.interiors_ext().any(|inner_line_string| self.intersects_trait(&inner_line_string)) || - // self is contained inside polygon - polygon.intersects_trait(&self_exterior) + // if there are no line intersections among exteriors and interiors, + // then either one fully contains the other + // or they are disjoint + + // check 1 point of each polygon being within the other + self_exterior.coord_iter().take(1).any(|p| polygon.intersects_trait(&p)) + || polygon_exterior.coord_iter().take(1).any(|p| self.intersects_trait(&p)) + // exterior exterior + || self_exterior + .lines() + .any(|self_line| polygon_exterior.lines().any(|poly_line| self_line.intersects_trait(&poly_line))) + // exterior interior + || self + .interiors_ext() + .any(|inner_line_string| polygon_exterior.intersects_trait(&inner_line_string)) + || polygon + .interiors_ext() + .any(|inner_line_string| self_exterior.intersects_trait(&inner_line_string)) + + // interior interior (not needed) + /* + suppose interior-interior is a required check + this requires that there are no ext-ext intersections + and that there are no ext-int intersections + and that self-ext[0] not intersects other + and other-ext[0] not intersects self + and there is some intersection between self and other + + if ext-ext disjoint, then one ext ring must be within the other ext ring + + suppose self-ext is within other-ext and self-ext[0] is not intersects other + then self-ext[0] must be within an interior hole of other-ext + if self-ext does not intersect the interior ring which contains self-ext[0], + then self is contained within other interior hole + and hence self and other cannot intersect + therefore for self to intersect other, some part of the self-ext must intersect the other-int ring + However, this is a contradiction because one of the premises for requiring this check is that self-ext ring does not intersect any other-int ring + + By symmetry, the mirror case of other-ext ring within self-ext ring is also true + + therefore, if there cannot exist and int-int intersection when all the prior checks are false + and so we can skip the interior-interior check + */ } else { false } diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/rect.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/rect.rs index 23483b10b..818a6ba37 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/intersects/rect.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/rect.rs @@ -17,13 +17,13 @@ //! Intersects implementations for Rect (generic) //! //! Ported (and contains copied code) from `geo::algorithm::intersects::rect`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. -use geo_traits::CoordTrait; +use geo_traits::{CoordTrait, LineStringTrait}; use sedona_geo_traits_ext::*; use super::IntersectsTrait; -use crate::*; +use crate::{intersects::has_disjoint_bboxes, *}; impl IntersectsTrait for LHS where @@ -52,6 +52,60 @@ symmetric_intersects_trait_impl!( MultiPointTag ); +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: RectTraitExt, + RHS: PolygonTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + // simplified logic based on Polygon intersects Polygon + + if has_disjoint_bboxes(self, rhs) { + return false; + } + + // empty polygon cannot intersect with rectangle + let Some(exterior) = rhs.exterior_ext() else { + return false; + }; + if exterior.num_coords() == 0 { + return false; + } + + // if any of the polygon's corners intersect the rectangle + let first_coord = unsafe { exterior.geo_coord_unchecked(0) }; + if self.intersects(&first_coord) { + return true; + } + + // or any point of the rectangle intersects the polygon + if self.min_coord().intersects(rhs) { + return true; + } + + let rect_lines = self.to_lines(); + + // or any of the polygon's lines intersect the rectangle's lines + if exterior.lines().any(|rhs_line| { + rect_lines + .iter() + .any(|self_line| self_line.intersects(&rhs_line)) + }) { + return true; + } + rhs.interiors_ext().any(|interior| { + interior.lines().any(|rhs_line| { + rect_lines + .iter() + .any(|self_line| self_line.intersects(&rhs_line)) + }) + }) + } +} + +symmetric_intersects_trait_impl!(GeoNum, PolygonTraitExt, PolygonTag, RectTraitExt, RectTag); + impl IntersectsTrait for LHS where T: CoordNum, @@ -87,10 +141,10 @@ where RHS: LineTraitExt, { fn intersects_trait(&self, rhs: &RHS) -> bool { - let lt = self.min_coord(); - let rb = self.max_coord(); - let lb = Coord::from((lt.x, rb.y)); - let rt = Coord::from((rb.x, lt.y)); + let lb = self.min_coord(); + let rt = self.max_coord(); + let lt = Coord::from((lb.x, rt.y)); + let rb = Coord::from((rt.x, lb.y)); // If either rhs.{start,end} lies inside Rect, then true self.intersects_trait(&rhs.start_ext()) @@ -104,15 +158,99 @@ where symmetric_intersects_trait_impl!(GeoNum, LineTraitExt, LineTag, RectTraitExt, RectTag); -impl IntersectsTrait for LHS -where - T: GeoNum, - LHS: RectTraitExt, - RHS: TriangleTraitExt, -{ - fn intersects_trait(&self, other: &RHS) -> bool { - self.intersects_trait(&other.to_polygon()) +#[cfg(test)] +mod test_triangle { + use geo::Convert; + + use super::*; + + #[test] + fn test_disjoint() { + let rect: Rect = Rect::new((0, 0), (10, 10)).convert(); + let triangle = Triangle::from([(0., 11.), (1., 11.), (1., 12.)]); + assert!(!rect.intersects(&triangle)); + } + + #[test] + fn test_partial() { + let rect: Rect = Rect::new((0, 0), (10, 10)).convert(); + let triangle = Triangle::from([(1., 1.), (1., 2.), (2., 1.)]); + assert!(rect.intersects(&triangle)); + } + + #[test] + fn test_triangle_inside_rect() { + let rect: Rect = Rect::new((0, 0), (10, 10)).convert(); + let triangle = Triangle::from([(1., 1.), (1., 2.), (2., 1.)]); + assert!(rect.intersects(&triangle)); + } + + #[test] + fn test_rect_inside_triangle() { + let rect: Rect = Rect::new((1, 1), (2, 2)).convert(); + let triangle = Triangle::from([(0., 10.), (10., 0.), (0., 0.)]); + assert!(rect.intersects(&triangle)); } } -symmetric_intersects_trait_impl!(GeoNum, TriangleTraitExt, TriangleTag, RectTraitExt, RectTag); +#[cfg(test)] +mod test_polygon { + use geo::{Convert, CoordsIter}; + + use super::*; + + #[test] + fn test_disjoint() { + let rect: Rect = Rect::new((0, 0), (10, 10)).convert(); + let polygon: Polygon = Rect::new((11, 11), (12, 12)).to_polygon().convert(); + assert!(!rect.intersects(&polygon)); + } + + #[test] + fn test_partial() { + let rect: Rect = Rect::new((0, 0), (10, 10)).convert(); + let polygon: Polygon = Rect::new((9, 9), (12, 12)).to_polygon().convert(); + assert!(rect.intersects(&polygon)); + } + + #[test] + fn test_rect_inside_polygon() { + let rect: Rect = Rect::new((1, 1), (2, 2)).convert(); + let polygon: Polygon = Rect::new((0, 0), (10, 10)).to_polygon().convert(); + assert!(rect.intersects(&polygon)); + } + + #[test] + fn test_polygon_inside_rect() { + let rect: Rect = Rect::new((0, 0), (10, 10)).convert(); + let polygon: Polygon = Rect::new((1, 1), (2, 2)).to_polygon().convert(); + assert!(rect.intersects(&polygon)); + } + + // Hole related tests + + #[test] + fn test_rect_inside_polygon_hole() { + let bound: Rect = Rect::new((0, 0), (10, 10)).convert(); + let hole = Rect::new((1, 1), (9, 9)).convert(); + let rect = Rect::new((4, 4), (6, 6)).convert(); + let polygon = Polygon::new( + bound.exterior_coords_iter().collect(), + vec![hole.exterior_coords_iter().collect()], + ); + + assert!(!rect.intersects(&polygon)); + } + + #[test] + fn test_rect_equals_polygon_hole() { + let bound: Rect = Rect::new((0, 0), (10, 10)).convert(); + let rect: Rect = Rect::new((4, 4), (6, 6)).convert(); + let polygon = Polygon::new( + bound.exterior_coords_iter().collect(), + vec![rect.exterior_coords_iter().collect()], + ); + + assert!(rect.intersects(&polygon)); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/triangle.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/triangle.rs index 7f15f5630..ae04b477a 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/intersects/triangle.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/triangle.rs @@ -17,10 +17,11 @@ //! Intersects implementations for Triangle (generic) //! //! Ported (and contains copied code) from `geo::algorithm::intersects::triangle`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. use super::IntersectsTrait; -use crate::*; +use crate::{intersects::has_disjoint_bboxes, *}; +use geo_traits::LineStringTrait; use sedona_geo_traits_ext::*; impl IntersectsTrait for LHS @@ -90,3 +91,153 @@ where self.to_polygon().intersects_trait(&rhs.to_polygon()) } } + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: TriangleTraitExt, + RHS: PolygonTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + // simplified logic based on Polygon intersects Polygon + + if has_disjoint_bboxes(self, rhs) { + return false; + } + + // empty polygon cannot intersect with triangle + let Some(exterior) = rhs.exterior_ext() else { + return false; + }; + if exterior.num_coords() == 0 { + return false; + } + + // if any of the polygon's corners intersect the triangle + let first_coord = unsafe { exterior.geo_coord_unchecked(0) }; + if self.intersects(&first_coord) { + return true; + } + + // or any point of the triangle intersects the polygon + if self.first_coord().intersects(rhs) { + return true; + } + + let rect_lines = self.to_lines(); + + // or any of the polygon's lines intersect the triangle's lines + if exterior.lines().any(|rhs_line| { + rect_lines + .iter() + .any(|self_line| self_line.intersects(&rhs_line)) + }) { + return true; + } + rhs.interiors_ext().any(|interior| { + interior.lines().any(|rhs_line| { + rect_lines + .iter() + .any(|self_line| self_line.intersects(&rhs_line)) + }) + }) + } +} + +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + TriangleTraitExt, + TriangleTag +); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: TriangleTraitExt, + RHS: RectTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + // simplified logic based on Polygon intersects Polygon + + if has_disjoint_bboxes(self, rhs) { + return false; + } + + // if any of the rectangle's corners intersect the triangle + self.intersects(&rhs.min_coord()) + + // or some corner of the triangle intersects the rectangle + || self.first_coord().intersects(rhs) + + // or any of the triangle's lines intersect the rectangle's lines + || rhs.to_lines().iter().any(|rhs_line| { + self.to_lines().iter().any(|self_line| self_line.intersects(&rhs_line)) + }) + } +} + +symmetric_intersects_trait_impl!(GeoNum, RectTraitExt, RectTag, TriangleTraitExt, TriangleTag); + +#[cfg(test)] +mod test_polygon { + use geo::{Convert, CoordsIter}; + + use super::*; + + #[test] + fn test_disjoint() { + let triangle = Triangle::from([(0., 0.), (10., 0.), (10., 10.)]); + let polygon: Polygon = Rect::new((11, 11), (12, 12)).to_polygon().convert(); + assert!(!triangle.intersects(&polygon)); + } + + #[test] + fn test_partial() { + let triangle = Triangle::from([(0., 0.), (10., 0.), (10., 10.)]); + let polygon: Polygon = Rect::new((9, 9), (12, 12)).to_polygon().convert(); + assert!(triangle.intersects(&polygon)); + } + + #[test] + fn test_triangle_inside_polygon() { + let triangle = Triangle::from([(1., 1.), (2., 1.), (2., 2.)]); + let polygon: Polygon = Rect::new((0, 0), (10, 10)).to_polygon().convert(); + assert!(triangle.intersects(&polygon)); + } + + #[test] + fn test_polygon_inside_triangle() { + let triangle = Triangle::from([(0., 0.), (10., 0.), (10., 10.)]); + let polygon: Polygon = Rect::new((1, 1), (2, 2)).to_polygon().convert(); + assert!(triangle.intersects(&polygon)); + } + + // Hole related tests + + #[test] + fn test_rect_inside_polygon_hole() { + let bound: Rect = Rect::new((0, 0), (10, 10)).convert(); + let hole = Rect::new((1, 1), (9, 9)).convert(); + let triangle = Triangle::from([(4., 4.), (4., 6.), (6., 6.)]); + let polygon = Polygon::new( + bound.exterior_coords_iter().collect(), + vec![hole.exterior_coords_iter().collect()], + ); + + assert!(!triangle.intersects(&polygon)); + } + + #[test] + fn test_triangle_equals_polygon_hole() { + let bound: Rect = Rect::new((0, 0), (10, 10)).convert(); + let triangle = Triangle::from([(4., 4.), (4., 6.), (6., 6.)]); + let polygon = Polygon::new( + bound.exterior_coords_iter().collect(), + vec![triangle.exterior_coords_iter().collect()], + ); + + assert!(triangle.intersects(&polygon)); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/map_coords.rs b/rust/sedona-geo-generic-alg/src/algorithm/map_coords.rs index 8705bf579..815bc0eb9 100644 --- a/rust/sedona-geo-generic-alg/src/algorithm/map_coords.rs +++ b/rust/sedona-geo-generic-alg/src/algorithm/map_coords.rs @@ -17,7 +17,7 @@ //! Generic Map Coords algorithm //! //! Ported (and contains copied code) from `geo::algorithm::map_coords`: -//! . +//! . //! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. pub(crate) use crate::geometry::*; pub(crate) use crate::CoordNum; @@ -336,7 +336,7 @@ where fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { let exterior = match self.exterior_ext() { Some(ext) => ext.map_coords(func), - None => LineString::new(vec![]), + None => LineString::empty(), }; let interiors = self @@ -353,7 +353,7 @@ where ) -> Result { let exterior = match self.exterior_ext() { Some(ext) => ext.try_map_coords(func)?, - None => LineString::new(vec![]), + None => LineString::empty(), }; let interiors = self diff --git a/rust/sedona-geo-traits-ext/src/multi_polygon.rs b/rust/sedona-geo-traits-ext/src/multi_polygon.rs index 9b90afad2..0a9028ef8 100644 --- a/rust/sedona-geo-traits-ext/src/multi_polygon.rs +++ b/rust/sedona-geo-traits-ext/src/multi_polygon.rs @@ -83,7 +83,25 @@ impl MultiPolygonTraitExt for MultiPolygon where T: CoordNum, { - forward_multi_polygon_trait_ext_funcs!(); + type PolygonTypeExt<'a> + = ::InnerPolygonType<'a> + where + Self: 'a; + + #[inline] + fn polygon_ext(&self, i: usize) -> Option> { + self.0.get(i) + } + + #[inline] + unsafe fn polygon_unchecked_ext(&self, i: usize) -> Self::PolygonTypeExt<'_> { + self.0.get_unchecked(i) + } + + #[inline] + fn polygons_ext(&self) -> impl Iterator> { + self.0.iter() + } } impl GeoTraitExtWithTypeTag for MultiPolygon { @@ -94,7 +112,25 @@ impl MultiPolygonTraitExt for &MultiPolygon where T: CoordNum, { - forward_multi_polygon_trait_ext_funcs!(); + type PolygonTypeExt<'a> + = ::InnerPolygonType<'a> + where + Self: 'a; + + #[inline] + fn polygon_ext(&self, i: usize) -> Option> { + self.0.get(i) + } + + #[inline] + unsafe fn polygon_unchecked_ext(&self, i: usize) -> Self::PolygonTypeExt<'_> { + self.0.get_unchecked(i) + } + + #[inline] + fn polygons_ext(&self) -> impl Iterator> { + self.0.iter() + } } impl GeoTraitExtWithTypeTag for &MultiPolygon { diff --git a/rust/sedona-geoparquet/src/metadata.rs b/rust/sedona-geoparquet/src/metadata.rs index 98ca0ff40..09caac76a 100644 --- a/rust/sedona-geoparquet/src/metadata.rs +++ b/rust/sedona-geoparquet/src/metadata.rs @@ -39,10 +39,11 @@ use serde_json::Value; /// /// In contrast to the _user-specified API_, which is just "WKB" or "Native", here we need to know /// the actual written encoding type so that we can save that in the metadata. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)] #[allow(clippy::upper_case_acronyms)] pub enum GeoParquetColumnEncoding { /// Serialized Well-known Binary encoding + #[default] WKB, /// Native Point encoding #[serde(rename = "point")] @@ -64,12 +65,6 @@ pub enum GeoParquetColumnEncoding { MultiPolygon, } -impl Default for GeoParquetColumnEncoding { - fn default() -> Self { - Self::WKB - } -} - impl Display for GeoParquetColumnEncoding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use GeoParquetColumnEncoding::*; diff --git a/rust/sedona-geoparquet/src/options.rs b/rust/sedona-geoparquet/src/options.rs index 43eafad30..0301716f2 100644 --- a/rust/sedona-geoparquet/src/options.rs +++ b/rust/sedona-geoparquet/src/options.rs @@ -48,12 +48,13 @@ impl From for TableGeoParquetOptions { } /// The GeoParquet Version to write for output with spatial columns -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub enum GeoParquetVersion { /// Write GeoParquet 1.0 metadata /// /// GeoParquet 1.0 has the widest support among readers and writers; however /// it does not include row-group level statistics. + #[default] V1_0, /// Write GeoParquet 1.1 metadata and optional bounding box column @@ -80,12 +81,6 @@ pub enum GeoParquetVersion { Omitted, } -impl Default for GeoParquetVersion { - fn default() -> Self { - Self::V1_0 - } -} - impl FromStr for GeoParquetVersion { type Err = DataFusionError;