diff --git a/LICENSE b/LICENSE index 8535a11c..b36f29e1 100644 --- a/LICENSE +++ b/LICENSE @@ -212,6 +212,7 @@ c/sedona-geoarrow-c/src/geoarrow (vendored from https://github.com/geoarrow/geoa c/sedona-geoarrow-c/src/nanoarrow (vendored from https://github.com/apache/arrow-nanoarrow) c/sedona-s2geography/s2geography (submodule from https://github.com/paleolimbot/s2geography) c/sedona-s2geography/s2geometry (submodule from https://github.com/google/s2geometry) +rust/sedona-geo-generic-alg (ported and contains copied code from https://github.com/georust/geo) MIT License -------------------------------------- diff --git a/rust/sedona-geo-generic-alg/Cargo.toml b/rust/sedona-geo-generic-alg/Cargo.toml new file mode 100644 index 00000000..666d42b0 --- /dev/null +++ b/rust/sedona-geo-generic-alg/Cargo.toml @@ -0,0 +1,76 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +[package] +name = "sedona-geo-generic-alg" +version = "0.2.0" +authors = ["Apache Sedona "] +license = "Apache-2.0" +homepage = "https://github.com/apache/sedona-db" +repository = "https://github.com/apache/sedona-db" +description = "geo algorithms refactored to work with sedona-geo-traits-ext" +readme = "README.md" +edition = "2021" + +[workspace] + +[dependencies] +float_next_after = "1" +geo-traits = { version = "0.3.0" } +geo-types = { version = "0.7.17", features = ["approx", "use-rstar_0_12"] } +sedona-geo-traits-ext = { path = "../sedona-geo-traits-ext" } +log = "0.4.11" +num-traits = { version = "0.2", default-features = false, features = ["libm"] } +robust = "1.1.0" +rstar = "0.12.0" +i_overlay = { version = "4.0.0, < 4.1.0", default-features = false } + +[dev-dependencies] +approx = "0.5" +criterion = { version = "0.5", features = ["html_reports"] } +pretty_env_logger = "0.4" +rand = "0.8" +rand_distr = "0.4.3" +geo = "0.31.0" +wkb = "0.9.1" +wkt = "0.14.0" + +[patch.crates-io] +wkb = { git = "https://github.com/georust/wkb.git", rev = "130eb0c2b343bc9299aeafba6d34c2a6e53f3b6a" } + +[[bench]] +name = "area" +harness = false + +[[bench]] +name = "intersection" +harness = false + +[[bench]] +name = "centroid" +harness = false + +[[bench]] +name = "length" +harness = false + +[[bench]] +name = "distance" +harness = false + +[[bench]] +name = "perimeter" +harness = false diff --git a/rust/sedona-geo-generic-alg/README.md b/rust/sedona-geo-generic-alg/README.md new file mode 100644 index 00000000..1ba86778 --- /dev/null +++ b/rust/sedona-geo-generic-alg/README.md @@ -0,0 +1,22 @@ + + +# Generic Algorithms for Geo-Traits + +This crate contains algorithms ported from the [`geo` crate](https://github.com/georust/geo), +but works with traits defined in `geo-traits-ext` instead of concrete geometry types defined in +`geo-types`. diff --git a/rust/sedona-geo-generic-alg/benches/area.rs b/rust/sedona-geo-generic-alg/benches/area.rs new file mode 100644 index 00000000..ab9e5da8 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/area.rs @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo_traits::to_geo::ToGeoGeometry; +use sedona_geo_generic_alg::Area; +use sedona_geo_generic_alg::Polygon; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("area_generic_f32", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).signed_area()); + }); + }); + + c.bench_function("area_generic", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).signed_area()); + }); + }); + + c.bench_function("area_geo_f32", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(geo::Area::signed_area(criterion::black_box(&polygon))); + }); + }); + + c.bench_function("area_geo", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(geo::Area::signed_area(criterion::black_box(&polygon))); + }); + }); + + c.bench_function("area_wkb", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + criterion::black_box(wkb_geom.signed_area()); + }); + }); + + c.bench_function("area_wkb_convert", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + let geom = wkb_geom.to_geometry(); + criterion::black_box(geom.signed_area()); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rust/sedona-geo-generic-alg/benches/centroid.rs b/rust/sedona-geo-generic-alg/benches/centroid.rs new file mode 100644 index 00000000..62d7c033 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/centroid.rs @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo_traits::to_geo::ToGeoGeometry; +use sedona_geo_generic_alg::Centroid; +use sedona_geo_generic_alg::Polygon; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("centroid_generic_f32", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).centroid()); + }); + }); + + c.bench_function("centroid_generic", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).centroid()); + }); + }); + + c.bench_function("centroid_geo_f32", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(geo::Centroid::centroid(criterion::black_box(&polygon))); + }); + }); + + c.bench_function("centroid_geo", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(geo::Centroid::centroid(criterion::black_box(&polygon))); + }); + }); + + c.bench_function("centroid_wkb", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + criterion::black_box(wkb_geom.centroid()); + }); + }); + + c.bench_function("centroid_wkb_convert", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + let geom = wkb_geom.to_geometry(); + criterion::black_box(geom.centroid()); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rust/sedona-geo-generic-alg/benches/distance.rs b/rust/sedona-geo-generic-alg/benches/distance.rs new file mode 100644 index 00000000..313e4cf7 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/distance.rs @@ -0,0 +1,511 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo::{Distance as GeoDistance, Euclidean}; +use sedona_geo_generic_alg::algorithm::line_measures::DistanceExt; +use sedona_geo_generic_alg::{coord, LineString, MultiPolygon, Point, Polygon}; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +// Helper function to create complex polygons with many vertices for stress testing +fn create_complex_polygon( + center_x: f64, + center_y: f64, + radius: f64, + num_vertices: usize, +) -> Polygon { + let mut vertices = Vec::with_capacity(num_vertices + 1); + + for i in 0..num_vertices { + let angle = 2.0 * std::f64::consts::PI * i as f64 / num_vertices as f64; + // Add some variation to make it non-regular + let r = radius * (1.0 + 0.1 * (i as f64 * 0.3).sin()); + let x = center_x + r * angle.cos(); + let y = center_y + r * angle.sin(); + vertices.push(coord!(x: x, y: y)); + } + + // Close the polygon + vertices.push(vertices[0]); + + Polygon::new(LineString::from(vertices), vec![]) +} + +// Helper function to create multipolygons for testing iteration overhead +fn create_multipolygon(num_polygons: usize) -> MultiPolygon { + let mut polygons = Vec::with_capacity(num_polygons); + + for i in 0..num_polygons { + let offset = i as f64 * 50.0; + let poly = Polygon::new( + LineString::from(vec![ + coord!(x: offset, y: offset), + coord!(x: offset + 30.0, y: offset), + coord!(x: offset + 30.0, y: offset + 30.0), + coord!(x: offset, y: offset + 30.0), + coord!(x: offset, y: offset), + ]), + vec![], + ); + polygons.push(poly); + } + + MultiPolygon::new(polygons) +} + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("distance_point_to_point", |bencher| { + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(100.0, 100.0); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&p1).distance_ext(criterion::black_box(&p2))); + }); + }); + + c.bench_function("distance_linestring_to_linestring", |bencher| { + let ls1 = sedona_testing::fixtures::norway_main::(); + let ls2 = LineString::from(vec![ + coord!(x: 100.0, y: 100.0), + coord!(x: 200.0, y: 200.0), + coord!(x: 300.0, y: 300.0), + ]); + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&ls1).distance_ext(criterion::black_box(&ls2)), + ); + }); + }); + + c.bench_function("distance_polygon_to_polygon", |bencher| { + let poly1 = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 0.0, y: 100.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let poly2 = Polygon::new( + LineString::from(vec![ + coord!(x: 200.0, y: 200.0), + coord!(x: 300.0, y: 200.0), + coord!(x: 300.0, y: 300.0), + coord!(x: 200.0, y: 300.0), + coord!(x: 200.0, y: 200.0), + ]), + vec![], + ); + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&poly1).distance_ext(criterion::black_box(&poly2)), + ); + }); + }); + + c.bench_function("distance_wkb_point_to_point", |bencher| { + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(100.0, 100.0); + let wkb_bytes1 = wkb_util::geo_to_wkb(p1); + let wkb_bytes2 = wkb_util::geo_to_wkb(p2); + + bencher.iter(|| { + let wkb_geom1 = wkb::reader::read_wkb(&wkb_bytes1).unwrap(); + let wkb_geom2 = wkb::reader::read_wkb(&wkb_bytes2).unwrap(); + criterion::black_box(wkb_geom1.distance_ext(&wkb_geom2)); + }); + }); + + c.bench_function("distance_wkb_linestring_to_linestring", |bencher| { + let ls1 = sedona_testing::fixtures::norway_main::(); + let ls2 = LineString::from(vec![ + coord!(x: 100.0, y: 100.0), + coord!(x: 200.0, y: 200.0), + coord!(x: 300.0, y: 300.0), + ]); + let wkb_bytes1 = wkb_util::geo_to_wkb(ls1); + let wkb_bytes2 = wkb_util::geo_to_wkb(ls2); + + bencher.iter(|| { + let wkb_geom1 = wkb::reader::read_wkb(&wkb_bytes1).unwrap(); + let wkb_geom2 = wkb::reader::read_wkb(&wkb_bytes2).unwrap(); + criterion::black_box(wkb_geom1.distance_ext(&wkb_geom2)); + }); + }); + + c.bench_function("distance_multipolygon_to_multipolygon", |bencher| { + let poly1 = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 50.0, y: 0.0), + coord!(x: 50.0, y: 50.0), + coord!(x: 0.0, y: 50.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let poly2 = Polygon::new( + LineString::from(vec![ + coord!(x: 60.0, y: 60.0), + coord!(x: 110.0, y: 60.0), + coord!(x: 110.0, y: 110.0), + coord!(x: 60.0, y: 110.0), + coord!(x: 60.0, y: 60.0), + ]), + vec![], + ); + let mp1 = MultiPolygon::new(vec![poly1.clone(), poly1]); + let mp2 = MultiPolygon::new(vec![poly2.clone(), poly2]); + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&mp1).distance_ext(criterion::black_box(&mp2)), + ); + }); + }); + + c.bench_function("distance_concrete_point_to_point", |bencher| { + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(100.0, 100.0); + + bencher.iter(|| { + criterion::black_box( + Euclidean.distance(criterion::black_box(p1), criterion::black_box(p2)), + ); + }); + }); + + c.bench_function("distance_concrete_linestring_to_linestring", |bencher| { + let ls1 = sedona_testing::fixtures::norway_main::(); + let ls2 = LineString::from(vec![ + coord!(x: 100.0, y: 100.0), + coord!(x: 200.0, y: 200.0), + coord!(x: 300.0, y: 300.0), + ]); + + bencher.iter(|| { + criterion::black_box( + geo::Euclidean.distance(criterion::black_box(&ls1), criterion::black_box(&ls2)), + ); + }); + }); + + c.bench_function("distance_cross_type_point_to_linestring", |bencher| { + let point = Point::new(50.0, 50.0); + let linestring = LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 200.0, y: 0.0), + ]); + + bencher.iter(|| { + criterion::black_box(Euclidean.distance( + criterion::black_box(&point), + criterion::black_box(&linestring), + )); + }); + }); + + c.bench_function("distance_cross_type_linestring_to_polygon", |bencher| { + let linestring = + LineString::from(vec![coord!(x: -50.0, y: 50.0), coord!(x: 150.0, y: 50.0)]); + let polygon = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 0.0, y: 100.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + + bencher.iter(|| { + criterion::black_box(Euclidean.distance( + criterion::black_box(&linestring), + criterion::black_box(&polygon), + )); + }); + }); + + c.bench_function("distance_cross_type_point_to_polygon", |bencher| { + let point = Point::new(150.0, 50.0); + let polygon = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 0.0, y: 100.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + + bencher.iter(|| { + criterion::black_box( + Euclidean.distance(criterion::black_box(&point), criterion::black_box(&polygon)), + ); + }); + }); + + // ┌────────────────────────────────────────────────────────────┐ + // │ Targeted Performance Benchmarks: Generic vs Concrete │ + // └────────────────────────────────────────────────────────────┘ + + c.bench_function( + "generic_vs_concrete_polygon_containment_simple", + |bencher| { + // Simple polygon-to-polygon distance (no holes, no containment) + let poly1 = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 50.0, y: 0.0), + coord!(x: 50.0, y: 50.0), + coord!(x: 0.0, y: 50.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let poly2 = Polygon::new( + LineString::from(vec![ + coord!(x: 100.0, y: 100.0), + coord!(x: 150.0, y: 100.0), + coord!(x: 150.0, y: 150.0), + coord!(x: 100.0, y: 150.0), + coord!(x: 100.0, y: 100.0), + ]), + vec![], + ); + + bencher.iter(|| { + // Generic implementation + criterion::black_box( + criterion::black_box(&poly1).distance_ext(criterion::black_box(&poly2)), + ); + }); + }, + ); + + c.bench_function( + "concrete_vs_generic_polygon_containment_simple", + |bencher| { + let poly1 = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 50.0, y: 0.0), + coord!(x: 50.0, y: 50.0), + coord!(x: 0.0, y: 50.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let poly2 = Polygon::new( + LineString::from(vec![ + coord!(x: 100.0, y: 100.0), + coord!(x: 150.0, y: 100.0), + coord!(x: 150.0, y: 150.0), + coord!(x: 100.0, y: 150.0), + coord!(x: 100.0, y: 100.0), + ]), + vec![], + ); + + bencher.iter(|| { + // Concrete implementation + criterion::black_box( + Euclidean.distance(criterion::black_box(&poly1), criterion::black_box(&poly2)), + ); + }); + }, + ); + + c.bench_function("generic_polygon_with_holes_distance", |bencher| { + // Polygon with holes - this triggers the containment check and temporary object creation + let outer = LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 200.0, y: 0.0), + coord!(x: 200.0, y: 200.0), + coord!(x: 0.0, y: 200.0), + coord!(x: 0.0, y: 0.0), + ]); + let hole = LineString::from(vec![ + coord!(x: 50.0, y: 50.0), + coord!(x: 150.0, y: 50.0), + coord!(x: 150.0, y: 150.0), + coord!(x: 50.0, y: 150.0), + coord!(x: 50.0, y: 50.0), + ]); + let poly_with_hole = Polygon::new(outer, vec![hole]); + + // Small polygon that might be inside the hole (triggers containment logic) + let small_poly = Polygon::new( + LineString::from(vec![ + coord!(x: 75.0, y: 75.0), + coord!(x: 125.0, y: 75.0), + coord!(x: 125.0, y: 125.0), + coord!(x: 75.0, y: 125.0), + coord!(x: 75.0, y: 75.0), + ]), + vec![], + ); + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&poly_with_hole) + .distance_ext(criterion::black_box(&small_poly)), + ); + }); + }); + + c.bench_function("concrete_polygon_with_holes_distance", |bencher| { + let outer = LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 200.0, y: 0.0), + coord!(x: 200.0, y: 200.0), + coord!(x: 0.0, y: 200.0), + coord!(x: 0.0, y: 0.0), + ]); + let hole = LineString::from(vec![ + coord!(x: 50.0, y: 50.0), + coord!(x: 150.0, y: 50.0), + coord!(x: 150.0, y: 150.0), + coord!(x: 50.0, y: 150.0), + coord!(x: 50.0, y: 50.0), + ]); + let poly_with_hole = Polygon::new(outer, vec![hole]); + let small_poly = Polygon::new( + LineString::from(vec![ + coord!(x: 75.0, y: 75.0), + coord!(x: 125.0, y: 75.0), + coord!(x: 125.0, y: 125.0), + coord!(x: 75.0, y: 125.0), + coord!(x: 75.0, y: 75.0), + ]), + vec![], + ); + + bencher.iter(|| { + criterion::black_box(Euclidean.distance( + criterion::black_box(&poly_with_hole), + criterion::black_box(&small_poly), + )); + }); + }); + + c.bench_function("generic_complex_polygon_distance", |bencher| { + // Complex polygons with many vertices - stress test for temporary object creation + let complex_poly1 = create_complex_polygon(0.0, 0.0, 50.0, 20); // 20 vertices + let complex_poly2 = create_complex_polygon(100.0, 100.0, 30.0, 15); // 15 vertices + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&complex_poly1) + .distance_ext(criterion::black_box(&complex_poly2)), + ); + }); + }); + + c.bench_function("concrete_complex_polygon_distance", |bencher| { + let complex_poly1 = create_complex_polygon(0.0, 0.0, 50.0, 20); + let complex_poly2 = create_complex_polygon(100.0, 100.0, 30.0, 15); + + bencher.iter(|| { + criterion::black_box(Euclidean.distance( + criterion::black_box(&complex_poly1), + criterion::black_box(&complex_poly2), + )); + }); + }); + + c.bench_function("generic_linestring_to_polygon_intersecting", |bencher| { + // LineString that intersects polygon - tests early exit performance + let polygon = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 0.0, y: 100.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let intersecting_linestring = LineString::from(vec![ + coord!(x: -50.0, y: 50.0), + coord!(x: 150.0, y: 50.0), // Crosses through the polygon + ]); + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&intersecting_linestring) + .distance_ext(criterion::black_box(&polygon)), + ); + }); + }); + + c.bench_function("concrete_linestring_to_polygon_intersecting", |bencher| { + let polygon = Polygon::new( + LineString::from(vec![ + coord!(x: 0.0, y: 0.0), + coord!(x: 100.0, y: 0.0), + coord!(x: 100.0, y: 100.0), + coord!(x: 0.0, y: 100.0), + coord!(x: 0.0, y: 0.0), + ]), + vec![], + ); + let intersecting_linestring = + LineString::from(vec![coord!(x: -50.0, y: 50.0), coord!(x: 150.0, y: 50.0)]); + + bencher.iter(|| { + criterion::black_box(Euclidean.distance( + criterion::black_box(&intersecting_linestring), + criterion::black_box(&polygon), + )); + }); + }); + + c.bench_function("generic_multipolygon_distance_overhead", |bencher| { + // Test multipolygon distance to measure iterator and temporary object overhead + let mp1 = create_multipolygon(5); // 5 polygons + let mp2 = create_multipolygon(3); // 3 polygons + + bencher.iter(|| { + criterion::black_box( + criterion::black_box(&mp1).distance_ext(criterion::black_box(&mp2)), + ); + }); + }); + + c.bench_function("concrete_multipolygon_distance_overhead", |bencher| { + let mp1 = create_multipolygon(5); + let mp2 = create_multipolygon(3); + + bencher.iter(|| { + criterion::black_box( + Euclidean.distance(criterion::black_box(&mp1), criterion::black_box(&mp2)), + ); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rust/sedona-geo-generic-alg/benches/intersection.rs b/rust/sedona-geo-generic-alg/benches/intersection.rs new file mode 100644 index 00000000..a5891e0b --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/intersection.rs @@ -0,0 +1,458 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo_traits::to_geo::ToGeoGeometry; +use geo_types::Geometry; +use sedona_geo_generic_alg::MultiPolygon; +use sedona_geo_generic_alg::{intersects::Intersects, Centroid}; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +fn multi_polygon_intersection(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + let plot_geoms: Vec = plot_polygons.into_iter().map(|p| p.into()).collect(); + let zone_geoms: Vec = zone_polygons.into_iter().map(|p| p.into()).collect(); + + c.bench_function("MultiPolygon intersects", |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(b.intersects(a)) { + 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; + let mut non_intersects = 0; + + for a in &plot_geoms { + for b in &zone_geoms { + if criterion::black_box(geo::Intersects::intersects(b, a)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 974); + assert_eq!(non_intersects, 27782); + }); + }); +} + +fn multi_polygon_intersection_wkb(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + + // Convert intersected polygons to WKB + let mut plot_polygon_wkbs = Vec::new(); + let mut zone_polygon_wkbs = Vec::new(); + for plot_polygon in plot_polygons { + plot_polygon_wkbs.push(wkb_util::geo_to_wkb(plot_polygon)); + } + for zone_polygon in zone_polygons { + zone_polygon_wkbs.push(wkb_util::geo_to_wkb(zone_polygon)); + } + + c.bench_function("MultiPolygon intersects wkb", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_polygon_wkbs { + for b in &zone_polygon_wkbs { + let a_geom = wkb::reader::read_wkb(a).unwrap(); // Skip padding + let b_geom = wkb::reader::read_wkb(b).unwrap(); // Skip padding + if criterion::black_box(b_geom.intersects(&a_geom)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 974); + assert_eq!(non_intersects, 27782); + }); + }); +} + +fn multi_polygon_intersection_wkb_aligned(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + + // Convert intersected polygons to WKB + let mut plot_polygon_wkbs = Vec::new(); + let mut zone_polygon_wkbs = Vec::new(); + for plot_polygon in plot_polygons { + let mut wkb = vec![0, 0, 0]; // Add 3-byte padding + wkb.extend_from_slice(&wkb_util::geo_to_wkb(plot_polygon)); + plot_polygon_wkbs.push(wkb); + } + for zone_polygon in zone_polygons { + let mut wkb = vec![0, 0, 0]; // Add 3-byte padding + wkb.extend_from_slice(&wkb_util::geo_to_wkb(zone_polygon)); + zone_polygon_wkbs.push(wkb); + } + + c.bench_function("MultiPolygon intersects wkb aligned", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_polygon_wkbs { + for b in &zone_polygon_wkbs { + let a_geom = wkb::reader::read_wkb(&a[3..]).unwrap(); // Skip padding + let b_geom = wkb::reader::read_wkb(&b[3..]).unwrap(); // Skip padding + if criterion::black_box(b_geom.intersects(&a_geom)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 974); + assert_eq!(non_intersects, 27782); + }); + }); +} + +fn multi_polygon_intersection_wkb_conv(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + + // Convert intersected polygons to WKB + let mut plot_polygon_wkbs = Vec::new(); + let mut zone_polygon_wkbs = Vec::new(); + for plot_polygon in plot_polygons { + plot_polygon_wkbs.push(wkb_util::geo_to_wkb(plot_polygon)); + } + for zone_polygon in zone_polygons { + zone_polygon_wkbs.push(wkb_util::geo_to_wkb(zone_polygon)); + } + + c.bench_function("MultiPolygon intersects wkb conv", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_polygon_wkbs { + for b in &zone_polygon_wkbs { + let a_geom = wkb::reader::read_wkb(a).unwrap(); + let b_geom = wkb::reader::read_wkb(b).unwrap(); + let a_geom = a_geom.to_geometry(); + let b_geom = b_geom.to_geometry(); + if criterion::black_box(b_geom.intersects(&a_geom)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 974); + assert_eq!(non_intersects, 27782); + }); + }); +} + +fn point_polygon_intersection(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + let plot_geoms: Vec = plot_polygons + .into_iter() + .map(|p| { + let centroid = p.centroid().unwrap(); + centroid.into() + }) + .collect(); + let zone_geoms: Vec = zone_polygons.into_iter().map(|p| p.into()).collect(); + + c.bench_function("Point polygon intersects", |bencher| { + bencher.iter(|| { + for a in &plot_geoms { + for b in &zone_geoms { + criterion::black_box(b.intersects(a)); + } + } + }); + }); + + c.bench_function("Point polygon intersects geo", |bencher| { + bencher.iter(|| { + for a in &plot_geoms { + for b in &zone_geoms { + criterion::black_box(geo::Intersects::intersects(b, a)); + } + } + }); + }); +} + +fn point_polygon_intersection_wkb(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + + // Convert intersected polygons to WKB + let mut plot_centroid_wkbs = Vec::new(); + let mut zone_polygon_wkbs = Vec::new(); + for plot_polygon in plot_polygons { + let centroid = plot_polygon.centroid().unwrap(); + plot_centroid_wkbs.push(wkb_util::geo_to_wkb(centroid)); + } + for zone_polygon in zone_polygons { + zone_polygon_wkbs.push(wkb_util::geo_to_wkb(zone_polygon)); + } + + c.bench_function("Point polygon intersects wkb", |bencher| { + bencher.iter(|| { + for a in &plot_centroid_wkbs { + for b in &zone_polygon_wkbs { + let a_geom = wkb::reader::read_wkb(a).unwrap(); + let b_geom = wkb::reader::read_wkb(b).unwrap(); + criterion::black_box(b_geom.intersects(&a_geom)); + } + } + }); + }); +} + +fn point_polygon_intersection_wkb_conv(c: &mut Criterion) { + let plot_polygons: MultiPolygon = sedona_testing::fixtures::nl_plots_wgs84(); + let zone_polygons: MultiPolygon = sedona_testing::fixtures::nl_zones(); + + // Convert intersected polygons to WKB + let mut plot_centroid_wkbs = Vec::new(); + let mut zone_polygon_wkbs = Vec::new(); + for plot_polygon in plot_polygons { + let centroid = plot_polygon.centroid().unwrap(); + plot_centroid_wkbs.push(wkb_util::geo_to_wkb(centroid)); + } + for zone_polygon in zone_polygons { + zone_polygon_wkbs.push(wkb_util::geo_to_wkb(zone_polygon)); + } + + c.bench_function("Point polygon intersects wkb conv", |bencher| { + bencher.iter(|| { + for a in &plot_centroid_wkbs { + for b in &zone_polygon_wkbs { + let a_geom = wkb::reader::read_wkb(a).unwrap(); + let b_geom = wkb::reader::read_wkb(b).unwrap(); + let a_geom = a_geom.to_geometry(); + let b_geom = b_geom.to_geometry(); + criterion::black_box(b_geom.intersects(&a_geom)); + } + } + }); + }); +} + +fn rect_intersection(c: &mut Criterion) { + use sedona_geo_generic_alg::algorithm::BoundingRect; + use sedona_geo_generic_alg::Rect; + let plot_bbox: Vec = sedona_testing::fixtures::nl_plots_wgs84() + .iter() + .map(|plot| plot.bounding_rect().unwrap()) + .collect(); + let zone_bbox: Vec = sedona_testing::fixtures::nl_zones() + .iter() + .map(|plot| plot.bounding_rect().unwrap()) + .collect(); + + c.bench_function("Rect intersects", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_bbox { + for b in &zone_bbox { + if criterion::black_box(a.intersects(b)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 3054); + assert_eq!(non_intersects, 25702); + }); + }); +} + +fn point_rect_intersection(c: &mut Criterion) { + use sedona_geo_generic_alg::algorithm::{BoundingRect, Centroid}; + use sedona_geo_generic_alg::geometry::{Point, Rect}; + let plot_centroids: Vec = sedona_testing::fixtures::nl_plots_wgs84() + .iter() + .map(|plot| plot.centroid().unwrap()) + .collect(); + let zone_bbox: Vec = sedona_testing::fixtures::nl_zones() + .iter() + .map(|plot| plot.bounding_rect().unwrap()) + .collect(); + + c.bench_function("Point intersects rect", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_centroids { + for b in &zone_bbox { + if criterion::black_box(a.intersects(b)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 2246); + assert_eq!(non_intersects, 26510); + }); + }); +} + +fn point_triangle_intersection(c: &mut Criterion) { + use geo::algorithm::TriangulateEarcut; + use sedona_geo_generic_alg::{Point, Triangle}; + let plot_centroids: Vec = sedona_testing::fixtures::nl_plots_wgs84() + .iter() + .map(|plot| plot.centroid().unwrap()) + .collect(); + let zone_triangles: Vec = sedona_testing::fixtures::nl_zones() + .iter() + .flat_map(|plot| plot.earcut_triangles_iter()) + .collect(); + + c.bench_function("Point intersects triangle", |bencher| { + bencher.iter(|| { + let mut intersects = 0; + let mut non_intersects = 0; + + for a in &plot_centroids { + for b in &zone_triangles { + if criterion::black_box(a.intersects(b)) { + intersects += 1; + } else { + non_intersects += 1; + } + } + } + + assert_eq!(intersects, 533); + assert_eq!(non_intersects, 5450151); + }); + }); + + c.bench_function("Triangle intersects point", |bencher| { + let triangle = Triangle::from([(0., 0.), (10., 0.), (5., 10.)]); + let point = Point::new(5., 5.); + + bencher.iter(|| { + assert!(criterion::black_box(&triangle).intersects(criterion::black_box(&point))); + }); + }); + + c.bench_function("Triangle intersects point on edge", |bencher| { + let triangle = Triangle::from([(0., 0.), (10., 0.), (6., 10.)]); + let point = Point::new(3., 5.); + + bencher.iter(|| { + assert!(criterion::black_box(&triangle).intersects(criterion::black_box(&point))); + }); + }); +} + +criterion_group! { + name = bench_multi_polygons; + config = Criterion::default().sample_size(10); + targets = multi_polygon_intersection +} +criterion_group! { + name = bench_multi_polygons_wkb; + config = Criterion::default().sample_size(10); + targets = multi_polygon_intersection_wkb +} +criterion_group! { + name = bench_multi_polygons_wkb_aligned; + config = Criterion::default().sample_size(10); + targets = multi_polygon_intersection_wkb_aligned +} +criterion_group! { + name = bench_multi_polygons_wkb_conv; + config = Criterion::default().sample_size(10); + targets = multi_polygon_intersection_wkb_conv +} + +criterion_group!(bench_rects, rect_intersection); +criterion_group! { + name = bench_point_rect; + config = Criterion::default().sample_size(50); + targets = point_rect_intersection +} +criterion_group! { + name = bench_point_triangle; + config = Criterion::default().sample_size(50); + targets = point_triangle_intersection +} + +criterion_group! { + name = bench_point_polygon; + config = Criterion::default().sample_size(50); + targets = point_polygon_intersection +} +criterion_group! { + name = bench_point_polygon_wkb; + config = Criterion::default().sample_size(50); + targets = point_polygon_intersection_wkb +} +criterion_group! { + name = bench_point_polygon_wkb_conv; + config = Criterion::default().sample_size(50); + targets = point_polygon_intersection_wkb_conv +} + +criterion_main!( + bench_multi_polygons, + bench_multi_polygons_wkb, + bench_multi_polygons_wkb_aligned, + bench_multi_polygons_wkb_conv, + bench_rects, + bench_point_rect, + bench_point_triangle, + bench_point_polygon, + bench_point_polygon_wkb, + bench_point_polygon_wkb_conv +); diff --git a/rust/sedona-geo-generic-alg/benches/length.rs b/rust/sedona-geo-generic-alg/benches/length.rs new file mode 100644 index 00000000..24970d06 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/length.rs @@ -0,0 +1,64 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo_traits::to_geo::ToGeoGeometry; +use sedona_geo_generic_alg::algorithm::line_measures::{Euclidean, LengthMeasurableExt}; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("length_f32", |bencher| { + let linestring = sedona_testing::fixtures::norway_main::(); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&linestring).length_ext(&Euclidean)); + }); + }); + + c.bench_function("length", |bencher| { + let linestring = sedona_testing::fixtures::norway_main::(); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&linestring).length_ext(&Euclidean)); + }); + }); + + c.bench_function("length_wkb", |bencher| { + let linestring = sedona_testing::fixtures::norway_main::(); + let wkb_bytes = wkb_util::geo_to_wkb(linestring); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + criterion::black_box(wkb_geom.length_ext(&Euclidean)); + }); + }); + + c.bench_function("length_wkb_convert", |bencher| { + let linestring = sedona_testing::fixtures::norway_main::(); + let wkb_bytes = wkb_util::geo_to_wkb(linestring); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + let geom = wkb_geom.to_geometry(); + criterion::black_box(geom.length_ext(&Euclidean)); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rust/sedona-geo-generic-alg/benches/perimeter.rs b/rust/sedona-geo-generic-alg/benches/perimeter.rs new file mode 100644 index 00000000..95d9e0a3 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/perimeter.rs @@ -0,0 +1,69 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +use criterion::{criterion_group, criterion_main, Criterion}; +use geo_traits::to_geo::ToGeoGeometry; +use sedona_geo_generic_alg::algorithm::line_measures::{Euclidean, LengthMeasurableExt}; +use sedona_geo_generic_alg::Polygon; + +#[path = "utils/wkb_util.rs"] +mod wkb_util; + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("perimeter_f32", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).perimeter_ext(&Euclidean)); + }); + }); + + c.bench_function("perimeter", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + + bencher.iter(|| { + criterion::black_box(criterion::black_box(&polygon).perimeter_ext(&Euclidean)); + }); + }); + + c.bench_function("perimeter_wkb", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + criterion::black_box(wkb_geom.perimeter_ext(&Euclidean)); + }); + }); + + c.bench_function("perimeter_wkb_convert", |bencher| { + let norway = sedona_testing::fixtures::norway_main::(); + let polygon = Polygon::new(norway, vec![]); + let wkb_bytes = wkb_util::geo_to_wkb(polygon); + + bencher.iter(|| { + let wkb_geom = wkb::reader::read_wkb(&wkb_bytes).unwrap(); + let geom = wkb_geom.to_geometry(); + criterion::black_box(geom.perimeter_ext(&Euclidean)); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/rust/sedona-geo-generic-alg/benches/utils/wkb_util.rs b/rust/sedona-geo-generic-alg/benches/utils/wkb_util.rs new file mode 100644 index 00000000..b76a9403 --- /dev/null +++ b/rust/sedona-geo-generic-alg/benches/utils/wkb_util.rs @@ -0,0 +1,25 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +pub fn geo_to_wkb(geo: G) -> Vec +where + G: Into, +{ + let geom = geo.into(); + let mut out: Vec = vec![]; + wkb::writer::write_geometry(&mut out, &geom, &wkb::writer::WriteOptions::default()).unwrap(); + out +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/area.rs b/rust/sedona-geo-generic-alg/src/algorithm/area.rs new file mode 100644 index 00000000..f9f1de1f --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/area.rs @@ -0,0 +1,626 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Generic Area algorithm +//! +//! Ported (and contains copied code) from `geo::algorithm::area`: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +use sedona_geo_traits_ext::*; + +use crate::{CoordFloat, CoordNum}; +use core::borrow::Borrow; + +pub(crate) fn twice_signed_ring_area>( + linestring: &LS, +) -> T { + // LineString with less than 3 points is empty, or a + // single point, or is not closed. + let num_coords = linestring.num_coords(); + if num_coords < 3 { + return T::zero(); + } + + unsafe { + // Above test ensures the vector has at least 2 elements. + // We check if linestring is closed, and return 0 otherwise. + if linestring.geo_coord_unchecked(0) != linestring.geo_coord_unchecked(num_coords - 1) { + return T::zero(); + } + + // Use a reasonable shift for the line-string coords + // to avoid numerical-errors when summing the + // determinants. + // + // Note: we can't use the `Centroid` trait as it + // requires `T: Float` and in fact computes area in the + // implementation. Another option is to use the average + // of the coordinates, but it is not fool-proof to + // divide by the length of the linestring (eg. a long + // line-string with T = u8) + let shift = linestring.geo_coord_unchecked(0); + + let mut tmp = T::zero(); + for line in linestring.lines() { + use crate::MapCoords; + let line = line.map_coords(|c| c - shift); + tmp = tmp + line.determinant(); + } + + tmp + } +} + +/// Signed and unsigned planar area of a geometry. +/// +/// # Examples +/// +/// ``` +/// use sedona_geo_generic_alg::polygon; +/// use sedona_geo_generic_alg::Area; +/// +/// let mut polygon = polygon![ +/// (x: 0., y: 0.), +/// (x: 5., y: 0.), +/// (x: 5., y: 6.), +/// (x: 0., y: 6.), +/// (x: 0., y: 0.), +/// ]; +/// +/// assert_eq!(polygon.signed_area(), 30.); +/// assert_eq!(polygon.unsigned_area(), 30.); +/// +/// polygon.exterior_mut(|line_string| { +/// line_string.0.reverse(); +/// }); +/// +/// assert_eq!(polygon.signed_area(), -30.); +/// assert_eq!(polygon.unsigned_area(), 30.); +/// ``` +pub trait Area +where + T: CoordNum, +{ + fn signed_area(&self) -> T; + + fn unsigned_area(&self) -> T; +} + +impl Area for G +where + T: CoordNum, + G: GeoTraitExtWithTypeTag + AreaTrait, +{ + fn signed_area(&self) -> T { + self.signed_area_trait() + } + + fn unsigned_area(&self) -> T { + self.unsigned_area_trait() + } +} + +trait AreaTrait +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T; + + fn unsigned_area_trait(&self) -> T; +} + +// Calculation of simple (no interior holes) Polygon area +pub(crate) fn get_linestring_area>(linestring: &LS) -> T +where + T: CoordFloat, +{ + twice_signed_ring_area(linestring) / (T::one() + T::one()) +} + +impl> AreaTrait for P +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + T::zero() + } + + fn unsigned_area_trait(&self) -> T { + T::zero() + } +} + +impl> AreaTrait for LS +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + T::zero() + } + + fn unsigned_area_trait(&self) -> T { + T::zero() + } +} + +impl> AreaTrait for L +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + T::zero() + } + + fn unsigned_area_trait(&self) -> T { + T::zero() + } +} + +/// **Note.** The implementation handles polygons whose +/// holes do not all have the same orientation. The sign of +/// the output is the same as that of the exterior shell. +impl> AreaTrait for P +where + T: CoordFloat, +{ + fn signed_area_trait(&self) -> T { + match self.exterior_ext() { + Some(exterior) => { + let area = get_linestring_area(&exterior); + + // We could use winding order here, but that would + // result in computing the shoelace formula twice. + let is_negative = area < T::zero(); + + let area = self.interiors_ext().fold(area.abs(), |total, next| { + total - get_linestring_area(&next).abs() + }); + + if is_negative { + -area + } else { + area + } + } + None => T::zero(), + } + } + + fn unsigned_area_trait(&self) -> T { + self.signed_area_trait().abs() + } +} + +impl> AreaTrait for MP +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + T::zero() + } + + fn unsigned_area_trait(&self) -> T { + T::zero() + } +} + +impl> AreaTrait for MLS +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + T::zero() + } + + fn unsigned_area_trait(&self) -> T { + T::zero() + } +} + +/// **Note.** The implementation is a straight-forward +/// summation of the signed areas of the individual +/// polygons. In particular, `unsigned_area` is not +/// necessarily the sum of the `unsigned_area` of the +/// constituent polygons unless they are all oriented the +/// same. +impl> AreaTrait for MP +where + T: CoordFloat, +{ + fn signed_area_trait(&self) -> T { + self.polygons_ext() + .fold(T::zero(), |total, next| total + next.signed_area_trait()) + } + + fn unsigned_area_trait(&self) -> T { + self.polygons_ext().fold(T::zero(), |total, next| { + total + next.signed_area_trait().abs() + }) + } +} + +/// Because a `Rect` has no winding order, the area will always be positive. +impl> AreaTrait for R +where + T: CoordNum, +{ + fn signed_area_trait(&self) -> T { + self.width() * self.height() + } + + fn unsigned_area_trait(&self) -> T { + self.width() * self.height() + } +} + +impl> AreaTrait for TT +where + T: CoordFloat, +{ + fn signed_area_trait(&self) -> T { + self.to_lines() + .iter() + .fold(T::zero(), |total, line| total + line.determinant()) + / (T::one() + T::one()) + } + + fn unsigned_area_trait(&self) -> T { + self.signed_area_trait().abs() + } +} + +impl> AreaTrait for GC +where + T: CoordFloat, +{ + fn signed_area_trait(&self) -> T { + self.geometries_ext() + .map(|g| g.signed_area_trait()) + .fold(T::zero(), |acc, next| acc + next) + } + + fn unsigned_area_trait(&self) -> T { + self.geometries_ext() + .map(|g| g.unsigned_area_trait()) + .fold(T::zero(), |acc, next| acc + next) + } +} + +impl> AreaTrait for G +where + T: CoordFloat, +{ + fn signed_area_trait(&self) -> T { + if self.is_collection() { + self.geometries_ext() + .map(|g_inner| g_inner.borrow().signed_area_trait()) + .fold(T::zero(), |acc, next| acc + next) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.signed_area_trait(), + GeometryTypeExt::Line(g) => g.signed_area_trait(), + GeometryTypeExt::LineString(g) => g.signed_area_trait(), + GeometryTypeExt::Polygon(g) => g.signed_area_trait(), + GeometryTypeExt::MultiPoint(g) => g.signed_area_trait(), + GeometryTypeExt::MultiLineString(g) => g.signed_area_trait(), + GeometryTypeExt::MultiPolygon(g) => g.signed_area_trait(), + GeometryTypeExt::Rect(g) => g.signed_area_trait(), + GeometryTypeExt::Triangle(g) => g.signed_area_trait(), + } + } + } + + fn unsigned_area_trait(&self) -> T { + if self.is_collection() { + self.geometries_ext() + .map(|g_inner| g_inner.borrow().unsigned_area_trait()) + .fold(T::zero(), |acc, next| acc + next) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.unsigned_area_trait(), + GeometryTypeExt::Line(g) => g.unsigned_area_trait(), + GeometryTypeExt::LineString(g) => g.unsigned_area_trait(), + GeometryTypeExt::Polygon(g) => g.unsigned_area_trait(), + GeometryTypeExt::MultiPoint(g) => g.unsigned_area_trait(), + GeometryTypeExt::MultiLineString(g) => g.unsigned_area_trait(), + GeometryTypeExt::MultiPolygon(g) => g.unsigned_area_trait(), + GeometryTypeExt::Rect(g) => g.unsigned_area_trait(), + GeometryTypeExt::Triangle(g) => g.unsigned_area_trait(), + } + } + } +} + +#[cfg(test)] +mod test { + use crate::Area; + use crate::{coord, polygon, wkt, Line, MultiPolygon, Polygon, Rect, Triangle}; + + // Area of the polygon + #[test] + fn area_empty_polygon_test() { + let poly: Polygon = polygon![]; + assert_relative_eq!(poly.signed_area(), 0.); + } + + #[test] + fn area_one_point_polygon_test() { + let poly = wkt! { POLYGON((1. 0.)) }; + assert_relative_eq!(poly.signed_area(), 0.); + } + #[test] + fn area_polygon_test() { + let polygon = wkt! { POLYGON((0. 0.,5. 0.,5. 6.,0. 6.,0. 0.)) }; + assert_relative_eq!(polygon.signed_area(), 30.); + } + #[test] + fn area_polygon_numerical_stability() { + let polygon = { + use std::f64::consts::PI; + const NUM_VERTICES: usize = 10; + const ANGLE_INC: f64 = 2. * PI / NUM_VERTICES as f64; + + Polygon::new( + (0..NUM_VERTICES) + .map(|i| { + let angle = i as f64 * ANGLE_INC; + coord! { + x: angle.cos(), + y: angle.sin(), + } + }) + .collect::>() + .into(), + vec![], + ) + }; + + let area = polygon.signed_area(); + + let shift = coord! { x: 1.5e8, y: 1.5e8 }; + + use crate::map_coords::MapCoords; + let polygon = polygon.map_coords(|c| c + shift); + + let new_area = polygon.signed_area(); + let err = (area - new_area).abs() / area; + + assert!(err < 1e-2); + } + #[test] + fn rectangle_test() { + let rect1: Rect = Rect::new(coord! { x: 10., y: 30. }, coord! { x: 20., y: 40. }); + assert_relative_eq!(rect1.signed_area(), 100.); + + let rect2: Rect = Rect::new(coord! { x: 10, y: 30 }, coord! { x: 20, y: 40 }); + assert_eq!(rect2.signed_area(), 100); + } + #[test] + fn area_polygon_inner_test() { + let poly = polygon![ + exterior: [ + (x: 0., y: 0.), + (x: 10., y: 0.), + (x: 10., y: 10.), + (x: 0., y: 10.), + (x: 0., y: 0.) + ], + interiors: [ + [ + (x: 1., y: 1.), + (x: 2., y: 1.), + (x: 2., y: 2.), + (x: 1., y: 2.), + (x: 1., y: 1.), + ], + [ + (x: 5., y: 5.), + (x: 6., y: 5.), + (x: 6., y: 6.), + (x: 5., y: 6.), + (x: 5., y: 5.) + ], + ], + ]; + assert_relative_eq!(poly.signed_area(), 98.); + } + #[test] + fn area_multipolygon_test() { + let poly0 = polygon![ + (x: 0., y: 0.), + (x: 10., y: 0.), + (x: 10., y: 10.), + (x: 0., y: 10.), + (x: 0., y: 0.) + ]; + let poly1 = polygon![ + (x: 1., y: 1.), + (x: 2., y: 1.), + (x: 2., y: 2.), + (x: 1., y: 2.), + (x: 1., y: 1.) + ]; + let poly2 = polygon![ + (x: 5., y: 5.), + (x: 6., y: 5.), + (x: 6., y: 6.), + (x: 5., y: 6.), + (x: 5., y: 5.) + ]; + let mpoly = MultiPolygon::new(vec![poly0, poly1, poly2]); + assert_relative_eq!(mpoly.signed_area(), 102.); + assert_relative_eq!(mpoly.signed_area(), 102.); + } + #[test] + fn area_line_test() { + let line1 = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 1.0, y: 1.0 }); + assert_relative_eq!(line1.signed_area(), 0.); + } + + #[test] + fn area_triangle_test() { + let triangle = Triangle::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 0.0, y: 1.0 }, + ); + assert_relative_eq!(triangle.signed_area(), 0.5); + + let triangle = Triangle::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 0.0, y: 1.0 }, + coord! { x: 1.0, y: 0.0 }, + ); + // triangles are always ccw, thus positive + assert_relative_eq!(triangle.signed_area(), 0.5); + } + + #[test] + fn area_geometry_test() { + let geom = wkt! { + MULTIPOLYGON( + ((0. 0.,5. 0.,5. 6.,0. 6.,0. 0.)), + ((1. 1.,2. 1.,2. 2.,1. 2.,1. 1.)) + ) + }; + assert_relative_eq!(geom.signed_area(), 31.0); + } + + #[test] + fn area_multi_polygon_area_reversed() { + let polygon_cw: Polygon = polygon![ + coord! { x: 0.0, y: 0.0 }, + coord! { x: 0.0, y: 1.0 }, + coord! { x: 1.0, y: 1.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 0.0, y: 0.0 }, + ]; + let polygon_ccw: Polygon = polygon![ + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 1.0, y: 1.0 }, + coord! { x: 0.0, y: 1.0 }, + coord! { x: 0.0, y: 0.0 }, + ]; + let polygon_area = polygon_cw.unsigned_area(); + + let multi_polygon = MultiPolygon::new(vec![polygon_cw, polygon_ccw]); + + assert_eq!(polygon_area * 2., multi_polygon.unsigned_area()); + } + + #[test] + fn area_north_america_cutout() { + let poly = polygon![ + exterior: [ + (x: -102.902861858977, y: 31.6943450891131), + (x: -102.917375513247, y: 31.6990175356827), + (x: -102.917887344527, y: 31.7044889522597), + (x: -102.938892711173, y: 31.7032871894594), + (x: -102.939919687305, y: 31.7142296141915), + (x: -102.946922353444, y: 31.713828170995), + (x: -102.954642979004, y: 31.7210594956594), + (x: -102.960927457803, y: 31.7130240707676), + (x: -102.967929895872, y: 31.7126214137469), + (x: -102.966383373178, y: 31.6962079209847), + (x: -102.973384192133, y: 31.6958049292994), + (x: -102.97390013779, y: 31.701276160078), + (x: -102.980901394769, y: 31.7008727405409), + (x: -102.987902575456, y: 31.7004689164622), + (x: -102.986878877087, y: 31.7127206248263), + (x: -102.976474089689, y: 31.7054378797983), + (x: -102.975448432121, y: 31.7176893134691), + (x: -102.96619351228, y: 31.7237224912303), + (x: -102.976481009643, y: 31.7286309669534), + (x: -102.976997412845, y: 31.7341016591658), + (x: -102.978030448215, y: 31.7450427747035), + (x: -102.985035821671, y: 31.7446391683265), + (x: -102.985552968771, y: 31.7501095683386), + (x: -102.992558780682, y: 31.7497055338313), + (x: -102.993594334215, y: 31.7606460184322), + (x: -102.973746840657, y: 31.7546100958509), + (x: -102.966082339116, y: 31.767730116605), + (x: -102.959074676589, y: 31.768132602064), + (x: -102.95206693787, y: 31.7685346826851), + (x: -102.953096767614, y: 31.7794749110023), + (x: -102.953611796704, y: 31.7849448911322), + (x: -102.952629078076, y: 31.7996518517642), + (x: -102.948661251495, y: 31.8072257578725), + (x: -102.934638176282, y: 31.8080282207231), + (x: -102.927626524626, y: 31.8084288446215), + (x: -102.927113253813, y: 31.8029591283411), + (x: -102.920102042027, y: 31.8033593239799), + (x: -102.919076759513, y: 31.792419577395), + (x: -102.912066503301, y: 31.7928193216213), + (x: -102.911554491357, y: 31.7873492912889), + (x: -102.904544675025, y: 31.7877486073783), + (x: -102.904033254331, y: 31.7822784646103), + (x: -102.903521909259, y: 31.7768082325431), + (x: -102.895800463718, y: 31.7695748336589), + (x: -102.889504111843, y: 31.7776055573633), + (x: -102.882495099915, y: 31.7780036124077), + (x: -102.868476849997, y: 31.7787985077398), + (x: -102.866950998738, y: 31.7623869292283), + (x: -102.873958615171, y: 31.7619897531194), + (x: -102.87888647278, y: 31.7688910039026), + (x: -102.879947237315, y: 31.750650764952), + (x: -102.886953672823, y: 31.750252825268), + (x: -102.89396003296, y: 31.7498544807869), + (x: -102.892939355062, y: 31.7389128078806), + (x: -102.913954892669, y: 31.7377154844276), + (x: -102.913443122277, y: 31.7322445829725), + (x: -102.912931427507, y: 31.7267735918962), + (x: -102.911908264767, y: 31.7158313407426), + (x: -102.904905220014, y: 31.7162307607961), + (x: -102.904394266551, y: 31.7107594775392), + (x: -102.903372586049, y: 31.6998166417321), + (x: -102.902861858977, y: 31.6943450891131), + ], + interiors: [ + [ + (x: -102.916514879554, y: 31.7650686485918), + (x: -102.921022256876, y: 31.7770831833398), + (x: -102.93367363719, y: 31.771184865332), + (x: -102.916514879554, y: 31.7650686485918), + ], + [ + (x: -102.935483140202, y: 31.7419852607081), + (x: -102.932452314332, y: 31.7328567234689), + (x: -102.918345099146, y: 31.7326099897391), + (x: -102.925566322952, y: 31.7552505533503), + (x: -102.928990700436, y: 31.747856686604), + (x: -102.935996606762, y: 31.7474559134477), + (x: -102.939021176592, y: 31.7539885279379), + (x: -102.944714388971, y: 31.7488395547293), + (x: -102.935996606762, y: 31.7474559134477), + (x: -102.935483140202, y: 31.7419852607081), + ], + [ + (x: -102.956498858767, y: 31.7407805824758), + (x: -102.960959476367, y: 31.7475080456347), + (x: -102.972817445204, y: 31.742072061889), + (x: -102.956498858767, y: 31.7407805824758), + ] + ], + ]; + // Value from shapely + assert_relative_eq!( + poly.unsigned_area(), + 0.006547948219252177, + max_relative = 0.0001 + ); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/bounding_rect.rs b/rust/sedona-geo-generic-alg/src/algorithm/bounding_rect.rs new file mode 100644 index 00000000..a86edbe8 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/bounding_rect.rs @@ -0,0 +1,424 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Generic Bounding Rectangle algorithm +//! +//! Ported (and contains copied code) from `geo::algorithm::bounding_rect`: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +use crate::utils::{partial_max, partial_min}; +use crate::{coord, geometry::*, CoordNum}; +use core::borrow::Borrow; +use geo_types::private_utils::get_bounding_rect; +use sedona_geo_traits_ext::*; + +/// Calculation of the bounding rectangle of a geometry. +pub trait BoundingRect { + type Output: Into>>; + + /// Return the bounding rectangle of a geometry + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::BoundingRect; + /// use sedona_geo_generic_alg::line_string; + /// + /// let line_string = line_string![ + /// (x: 40.02f64, y: 116.34), + /// (x: 42.02f64, y: 116.34), + /// (x: 42.02f64, y: 118.34), + /// ]; + /// + /// let bounding_rect = line_string.bounding_rect().unwrap(); + /// + /// assert_eq!(40.02f64, bounding_rect.min().x); + /// assert_eq!(42.02f64, bounding_rect.max().x); + /// assert_eq!(116.34, bounding_rect.min().y); + /// assert_eq!(118.34, bounding_rect.max().y); + /// ``` + fn bounding_rect(&self) -> Self::Output; +} + +impl BoundingRect for G +where + T: CoordNum, + G: GeoTraitExtWithTypeTag + BoundingRectTrait, +{ + type Output = G::Output; + + fn bounding_rect(&self) -> Self::Output { + self.bounding_rect_trait() + } +} + +pub trait BoundingRectTrait +where + T: CoordNum, +{ + type Output: Into>>; + + fn bounding_rect_trait(&self) -> Self::Output; +} + +impl> BoundingRectTrait for C +where + T: CoordNum, +{ + type Output = Rect; + + /// Return the bounding rectangle for a `Coord`. It will have zero width + /// and zero height. + fn bounding_rect_trait(&self) -> Self::Output { + Rect::new(self.geo_coord(), self.geo_coord()) + } +} + +impl> BoundingRectTrait for P +where + T: CoordNum, +{ + type Output = Rect; + + /// Return the bounding rectangle for a `Point`. It will have zero width + /// and zero height. + fn bounding_rect_trait(&self) -> Self::Output { + match self.geo_coord() { + Some(coord) => Rect::new(coord, coord), + None => { + let zero = Coord::::zero(); + Rect::new(zero, zero) + } + } + } +} + +impl> BoundingRectTrait for MP +where + T: CoordNum, +{ + type Output = Option>; + + /// + /// Return the BoundingRect for a MultiPoint + fn bounding_rect_trait(&self) -> Self::Output { + get_bounding_rect(self.coord_iter()) + } +} + +impl> BoundingRectTrait for L +where + T: CoordNum, +{ + type Output = Rect; + + fn bounding_rect_trait(&self) -> Self::Output { + Rect::new(self.start_coord(), self.end_coord()) + } +} + +impl> BoundingRectTrait for LS +where + T: CoordNum, +{ + type Output = Option>; + + /// + /// Return the BoundingRect for a LineString + fn bounding_rect_trait(&self) -> Self::Output { + get_bounding_rect(self.coord_iter()) + } +} + +impl> BoundingRectTrait for MLS +where + T: CoordNum, +{ + type Output = Option>; + + /// + /// Return the BoundingRect for a MultiLineString + fn bounding_rect_trait(&self) -> Self::Output { + self.line_strings_ext().fold(None, |acc, p| { + let rect = p.bounding_rect_trait(); + match (acc, rect) { + (None, None) => None, + (Some(r), None) | (None, Some(r)) => Some(r), + (Some(r1), Some(r2)) => Some(bounding_rect_merge(r1, r2)), + } + }) + } +} + +impl> BoundingRectTrait for P +where + T: CoordNum, +{ + type Output = Option>; + + /// + /// Return the BoundingRect for a Polygon + fn bounding_rect_trait(&self) -> Self::Output { + let exterior = self.exterior_ext(); + exterior.and_then(|e| get_bounding_rect(e.coord_iter())) + } +} + +impl> BoundingRectTrait for MP +where + T: CoordNum, +{ + type Output = Option>; + + /// + /// Return the BoundingRect for a MultiPolygon + fn bounding_rect_trait(&self) -> Self::Output { + self.polygons_ext().fold(None, |acc, p| { + let rect = p + .exterior_ext() + .and_then(|e| get_bounding_rect(e.coord_iter())); + match (acc, rect) { + (None, None) => None, + (Some(r), None) | (None, Some(r)) => Some(r), + (Some(r1), Some(r2)) => Some(bounding_rect_merge(r1, r2)), + } + }) + } +} + +impl> BoundingRectTrait for TT +where + T: CoordNum, +{ + type Output = Rect; + + fn bounding_rect_trait(&self) -> Self::Output { + get_bounding_rect(self.coord_iter()).unwrap() + } +} + +impl> BoundingRectTrait for R +where + T: CoordNum, +{ + type Output = Rect; + + fn bounding_rect_trait(&self) -> Self::Output { + self.geo_rect() + } +} + +impl> BoundingRectTrait for GC +where + T: CoordNum, +{ + type Output = Option>; + + fn bounding_rect_trait(&self) -> Self::Output { + self.geometries_ext().fold(None, |acc, next| { + let next_bounding_rect = next.bounding_rect_trait(); + + match (acc, next_bounding_rect) { + (None, None) => None, + (Some(r), None) | (None, Some(r)) => Some(r), + (Some(r1), Some(r2)) => Some(bounding_rect_merge(r1, r2)), + } + }) + } +} + +impl> BoundingRectTrait for G +where + T: CoordNum, +{ + type Output = Option>; + + fn bounding_rect_trait(&self) -> Self::Output { + if self.is_collection() { + self.geometries_ext().fold(None, |acc, next| { + let next_bounding_rect = next.borrow().bounding_rect_trait(); + + match (acc, next_bounding_rect) { + (None, None) => None, + (Some(r), None) | (None, Some(r)) => Some(r), + (Some(r1), Some(r2)) => Some(bounding_rect_merge(r1, r2)), + } + }) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.bounding_rect_trait().into(), + GeometryTypeExt::Line(g) => g.bounding_rect_trait().into(), + GeometryTypeExt::LineString(g) => g.bounding_rect_trait(), + GeometryTypeExt::Polygon(g) => g.bounding_rect_trait(), + GeometryTypeExt::MultiPoint(g) => g.bounding_rect_trait(), + GeometryTypeExt::MultiLineString(g) => g.bounding_rect_trait(), + GeometryTypeExt::MultiPolygon(g) => g.bounding_rect_trait(), + GeometryTypeExt::Rect(g) => g.bounding_rect_trait().into(), + GeometryTypeExt::Triangle(g) => g.bounding_rect_trait().into(), + } + } + } +} + +// Return a new rectangle that encompasses the provided rectangles +fn bounding_rect_merge(a: Rect, b: Rect) -> Rect { + Rect::new( + coord! { + x: partial_min(a.min().x, b.min().x), + y: partial_min(a.min().y, b.min().y), + }, + coord! { + x: partial_max(a.max().x, b.max().x), + y: partial_max(a.max().y, b.max().y), + }, + ) +} + +#[cfg(test)] +mod test { + use super::bounding_rect_merge; + use crate::line_string; + use crate::BoundingRect; + use crate::{ + coord, point, polygon, Geometry, GeometryCollection, Line, LineString, MultiLineString, + MultiPoint, MultiPolygon, Polygon, Rect, + }; + + #[test] + fn empty_linestring_test() { + let linestring: LineString = line_string![]; + let bounding_rect = linestring.bounding_rect(); + assert!(bounding_rect.is_none()); + } + #[test] + fn linestring_one_point_test() { + let linestring = line_string![(x: 40.02f64, y: 116.34)]; + let bounding_rect = Rect::new( + coord! { + x: 40.02f64, + y: 116.34, + }, + coord! { + x: 40.02, + y: 116.34, + }, + ); + assert_eq!(bounding_rect, linestring.bounding_rect().unwrap()); + } + #[test] + fn linestring_test() { + let linestring = line_string![ + (x: 1., y: 1.), + (x: 2., y: -2.), + (x: -3., y: -3.), + (x: -4., y: 4.) + ]; + let bounding_rect = Rect::new(coord! { x: -4., y: -3. }, coord! { x: 2., y: 4. }); + assert_eq!(bounding_rect, linestring.bounding_rect().unwrap()); + } + #[test] + fn multilinestring_test() { + let multiline = MultiLineString::new(vec![ + line_string![(x: 1., y: 1.), (x: -40., y: 1.)], + line_string![(x: 1., y: 1.), (x: 50., y: 1.)], + line_string![(x: 1., y: 1.), (x: 1., y: -60.)], + line_string![(x: 1., y: 1.), (x: 1., y: 70.)], + ]); + let bounding_rect = Rect::new(coord! { x: -40., y: -60. }, coord! { x: 50., y: 70. }); + assert_eq!(bounding_rect, multiline.bounding_rect().unwrap()); + } + #[test] + fn multipoint_test() { + let multipoint = MultiPoint::from(vec![(1., 1.), (2., -2.), (-3., -3.), (-4., 4.)]); + let bounding_rect = Rect::new(coord! { x: -4., y: -3. }, coord! { x: 2., y: 4. }); + assert_eq!(bounding_rect, multipoint.bounding_rect().unwrap()); + } + #[test] + fn polygon_test() { + let linestring = line_string![ + (x: 0., y: 0.), + (x: 5., y: 0.), + (x: 5., y: 6.), + (x: 0., y: 6.), + (x: 0., y: 0.), + ]; + let line_bounding_rect = linestring.bounding_rect().unwrap(); + let poly = Polygon::new(linestring, Vec::new()); + assert_eq!(line_bounding_rect, poly.bounding_rect().unwrap()); + } + #[test] + fn multipolygon_test() { + let mpoly = MultiPolygon::new(vec![ + polygon![(x: 0., y: 0.), (x: 50., y: 0.), (x: 0., y: -70.), (x: 0., y: 0.)], + polygon![(x: 0., y: 0.), (x: 5., y: 0.), (x: 0., y: 80.), (x: 0., y: 0.)], + polygon![(x: 0., y: 0.), (x: -60., y: 0.), (x: 0., y: 6.), (x: 0., y: 0.)], + ]); + let bounding_rect = Rect::new(coord! { x: -60., y: -70. }, coord! { x: 50., y: 80. }); + assert_eq!(bounding_rect, mpoly.bounding_rect().unwrap()); + } + #[test] + fn line_test() { + let line1 = Line::new(coord! { x: 0., y: 1. }, coord! { x: 2., y: 3. }); + let line2 = Line::new(coord! { x: 2., y: 3. }, coord! { x: 0., y: 1. }); + assert_eq!( + line1.bounding_rect(), + Rect::new(coord! { x: 0., y: 1. }, coord! { x: 2., y: 3. },) + ); + assert_eq!( + line2.bounding_rect(), + Rect::new(coord! { x: 0., y: 1. }, coord! { x: 2., y: 3. },) + ); + } + + #[test] + fn bounding_rect_merge_test() { + assert_eq!( + bounding_rect_merge( + Rect::new(coord! { x: 0., y: 0. }, coord! { x: 1., y: 1. }), + Rect::new(coord! { x: 1., y: 1. }, coord! { x: 2., y: 2. }), + ), + Rect::new(coord! { x: 0., y: 0. }, coord! { x: 2., y: 2. }), + ); + } + + #[test] + fn point_bounding_rect_test() { + assert_eq!( + Rect::new(coord! { x: 1., y: 2. }, coord! { x: 1., y: 2. }), + point! { x: 1., y: 2. }.bounding_rect(), + ); + } + + #[test] + fn geometry_collection_bounding_rect_test() { + assert_eq!( + Some(Rect::new(coord! { x: 0., y: 0. }, coord! { x: 1., y: 2. })), + GeometryCollection::new_from(vec![ + Geometry::Point(point! { x: 0., y: 0. }), + Geometry::Point(point! { x: 1., y: 2. }), + ]) + .bounding_rect(), + ); + assert_eq!( + Some(Rect::new(coord! { x: 0., y: 0. }, coord! { x: 1., y: 2. })), + Geometry::GeometryCollection(GeometryCollection::new_from(vec![ + Geometry::Point(point! { x: 0., y: 0. }), + Geometry::Point(point! { x: 1., y: 2. }), + ])) + .bounding_rect(), + ); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/centroid.rs b/rust/sedona-geo-generic-alg/src/algorithm/centroid.rs new file mode 100644 index 00000000..704cf031 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/centroid.rs @@ -0,0 +1,1238 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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; + +use sedona_geo_traits_ext::*; + +use crate::area::{get_linestring_area, Area}; +use crate::dimensions::{Dimensions, Dimensions::*, HasDimensions}; +use crate::geometry::*; +use crate::line_measures::metric_spaces::euclidean::Euclidean; +use crate::line_measures::LengthMeasurableExt; +use crate::GeoFloat; + +/// Calculation of the centroid. +/// The centroid is the arithmetic mean position of all points in the shape. +/// Informally, it is the point at which a cutout of the shape could be perfectly +/// balanced on the tip of a pin. +/// The geometric centroid of a convex object always lies in the object. +/// A non-convex object might have a centroid that _is outside the object itself_. +/// +/// # Examples +/// +/// ``` +/// use sedona_geo_generic_alg::Centroid; +/// use sedona_geo_generic_alg::{point, polygon}; +/// +/// // rhombus shaped polygon +/// let polygon = polygon![ +/// (x: -2., y: 1.), +/// (x: 1., y: 3.), +/// (x: 4., y: 1.), +/// (x: 1., y: -1.), +/// (x: -2., y: 1.), +/// ]; +/// +/// assert_eq!( +/// Some(point!(x: 1., y: 1.)), +/// polygon.centroid(), +/// ); +/// ``` +pub trait Centroid { + type Output; + + /// See: + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{line_string, point}; + /// + /// let line_string = line_string![ + /// (x: 40.02f64, y: 116.34), + /// (x: 40.02f64, y: 118.23), + /// ]; + /// + /// assert_eq!( + /// Some(point!(x: 40.02, y: 117.285)), + /// line_string.centroid(), + /// ); + /// ``` + fn centroid(&self) -> Self::Output; +} + +impl Centroid for G +where + G: GeoTraitExtWithTypeTag + CentroidTrait, +{ + type Output = G::Output; + + fn centroid(&self) -> Self::Output { + self.centroid_trait() + } +} + +pub trait CentroidTrait { + type Output; + + fn centroid_trait(&self) -> Self::Output; +} + +impl CentroidTrait for L +where + L: LineTraitExt, + T: GeoFloat, +{ + type Output = Point; + + /// The Centroid of a [`Line`] is its middle point + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{Line, point}; + /// + /// let line = Line::new( + /// point!(x: 1.0f64, y: 3.0), + /// point!(x: 2.0f64, y: 4.0), + /// ); + /// + /// assert_eq!( + /// point!(x: 1.5, y: 3.5), + /// line.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let two = T::one() + T::one(); + let start = self.start_coord(); + let end = self.end_coord(); + let center = (start + end) / two; + center.into() + } +} + +impl CentroidTrait for LS +where + LS: LineStringTraitExt, + T: GeoFloat, +{ + type Output = Option>; + + // The Centroid of a [`LineString`] is the mean of the middle of the segment + // weighted by the length of the segments. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{line_string, point}; + /// + /// let line_string = line_string![ + /// (x: 1.0f32, y: 1.0), + /// (x: 2.0, y: 2.0), + /// (x: 4.0, y: 4.0) + /// ]; + /// + /// assert_eq!( + /// // (1.0 * (1.5, 1.5) + 2.0 * (3.0, 3.0)) / 3.0 + /// Some(point!(x: 2.5, y: 2.5)), + /// line_string.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_line_string(self); + operation.centroid() + } +} + +impl CentroidTrait for MLS +where + MLS: MultiLineStringTraitExt, + T: GeoFloat, +{ + type Output = Option>; + + /// The Centroid of a [`MultiLineString`] is the mean of the centroids of all the constituent linestrings, + /// weighted by the length of each linestring + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{MultiLineString, line_string, point}; + /// + /// let multi_line_string = MultiLineString::new(vec![ + /// // centroid: (2.5, 2.5) + /// line_string![(x: 1.0f32, y: 1.0), (x: 2.0, y: 2.0), (x: 4.0, y: 4.0)], + /// // centroid: (4.0, 4.0) + /// line_string![(x: 1.0, y: 1.0), (x: 3.0, y: 3.0), (x: 7.0, y: 7.0)], + /// ]); + /// + /// assert_eq!( + /// // ( 3.0 * (2.5, 2.5) + 6.0 * (4.0, 4.0) ) / 9.0 + /// Some(point!(x: 3.5, y: 3.5)), + /// multi_line_string.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_multi_line_string(self); + operation.centroid() + } +} + +impl CentroidTrait for P +where + P: PolygonTraitExt, + T: GeoFloat, +{ + type Output = Option>; + + /// The Centroid of a [`Polygon`] is the mean of its points + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{polygon, point}; + /// + /// let polygon = polygon![ + /// (x: 0.0f32, y: 0.0), + /// (x: 2.0, y: 0.0), + /// (x: 2.0, y: 1.0), + /// (x: 0.0, y: 1.0), + /// ]; + /// + /// assert_eq!( + /// Some(point!(x: 1.0, y: 0.5)), + /// polygon.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_polygon(self); + operation.centroid() + } +} + +impl CentroidTrait for MP +where + MP: MultiPolygonTraitExt, + T: GeoFloat, +{ + type Output = Option>; + + /// The Centroid of a [`MultiPolygon`] is the mean of the centroids of its polygons, weighted + /// by the area of the polygons + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{MultiPolygon, polygon, point}; + /// + /// let multi_polygon = MultiPolygon::new(vec![ + /// // centroid (1.0, 0.5) + /// polygon![ + /// (x: 0.0f32, y: 0.0), + /// (x: 2.0, y: 0.0), + /// (x: 2.0, y: 1.0), + /// (x: 0.0, y: 1.0), + /// ], + /// // centroid (-0.5, 0.0) + /// polygon![ + /// (x: 1.0, y: 1.0), + /// (x: -2.0, y: 1.0), + /// (x: -2.0, y: -1.0), + /// (x: 1.0, y: -1.0), + /// ] + /// ]); + /// + /// assert_eq!( + /// // ( 2.0 * (1.0, 0.5) + 6.0 * (-0.5, 0.0) ) / 8.0 + /// Some(point!(x: -0.125, y: 0.125)), + /// multi_polygon.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_multi_polygon(self); + operation.centroid() + } +} + +impl CentroidTrait for R +where + R: RectTraitExt, + T: GeoFloat, +{ + type Output = Point; + + /// The Centroid of a [`Rect`] is the mean of its [`Point`]s + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{Rect, point}; + /// + /// let rect = Rect::new( + /// point!(x: 0.0f32, y: 0.0), + /// point!(x: 1.0, y: 1.0), + /// ); + /// + /// assert_eq!( + /// point!(x: 0.5, y: 0.5), + /// rect.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + self.center().into() + } +} + +impl CentroidTrait for TT +where + T: GeoFloat, + TT: TriangleTraitExt, +{ + type Output = Point; + + /// The Centroid of a [`Triangle`] is the mean of its [`Point`]s + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{Triangle, coord, point}; + /// + /// let triangle = Triangle::new( + /// coord!(x: 0.0f32, y: -1.0), + /// coord!(x: 3.0, y: 0.0), + /// coord!(x: 0.0, y: 1.0), + /// ); + /// + /// assert_eq!( + /// point!(x: 1.0, y: 0.0), + /// triangle.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_triangle(self); + operation + .centroid() + .expect("triangle cannot have an empty centroid") + } +} + +impl CentroidTrait for P +where + T: GeoFloat, + P: PointTraitExt, +{ + type Output = Point; + + /// The Centroid of a [`Point`] is the point itself + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::point; + /// + /// let point = point!(x: 1.0f32, y: 2.0); + /// + /// assert_eq!( + /// point!(x: 1.0f32, y: 2.0), + /// point.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + self.geo_point() + .unwrap_or_else(|| Point::new(T::zero(), T::zero())) + } +} + +impl CentroidTrait for MP +where + T: GeoFloat, + MP: MultiPointTraitExt, +{ + type Output = Option>; + + /// The Centroid of a [`MultiPoint`] is the mean of all [`Point`]s + /// + /// # Example + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{MultiPoint, Point}; + /// + /// let empty: Vec = Vec::new(); + /// let empty_multi_points: MultiPoint<_> = empty.into(); + /// assert_eq!(empty_multi_points.centroid(), None); + /// + /// let points: MultiPoint<_> = vec![(5., 1.), (1., 3.), (3., 2.)].into(); + /// assert_eq!(points.centroid(), Some(Point::new(3., 2.))); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_multi_point(self); + operation.centroid() + } +} + +impl CentroidTrait for G +where + T: GeoFloat, + G: GeometryTraitExt, +{ + type Output = Option>; + + /// The Centroid of a [`Geometry`] is the centroid of its enum variant + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{Geometry, Rect, point}; + /// + /// let rect = Rect::new( + /// point!(x: 0.0f32, y: 0.0), + /// point!(x: 1.0, y: 1.0), + /// ); + /// let geometry = Geometry::from(rect.clone()); + /// + /// assert_eq!( + /// Some(rect.centroid()), + /// geometry.centroid(), + /// ); + /// + /// assert_eq!( + /// Some(point!(x: 0.5, y: 0.5)), + /// geometry.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + if self.is_collection() { + // Handle geometry collection by computing weighted centroid + let mut operation = CentroidOperation::new(); + for g_inner in self.geometries_ext() { + operation.add_geometry(g_inner.borrow()); + } + operation.centroid() + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => Some(g.centroid_trait()), + GeometryTypeExt::Line(g) => Some(g.centroid_trait()), + GeometryTypeExt::LineString(g) => g.centroid_trait(), + GeometryTypeExt::Polygon(g) => g.centroid_trait(), + GeometryTypeExt::MultiPoint(g) => g.centroid_trait(), + GeometryTypeExt::MultiLineString(g) => g.centroid_trait(), + GeometryTypeExt::MultiPolygon(g) => g.centroid_trait(), + GeometryTypeExt::Rect(g) => Some(g.centroid_trait()), + GeometryTypeExt::Triangle(g) => Some(g.centroid_trait()), + } + } + } +} + +impl CentroidTrait for GC +where + T: GeoFloat, + GC: GeometryCollectionTraitExt, +{ + type Output = Option>; + + /// The Centroid of a [`GeometryCollection`] is the mean of the centroids of elements, weighted + /// by the area of its elements. + /// + /// Note that this means, that elements which have no area are not considered when calculating + /// the centroid. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::Centroid; + /// use sedona_geo_generic_alg::{Geometry, GeometryCollection, Rect, Triangle, point, coord}; + /// + /// let rect_geometry = Geometry::from(Rect::new( + /// point!(x: 0.0f32, y: 0.0), + /// point!(x: 1.0, y: 1.0), + /// )); + /// + /// let triangle_geometry = Geometry::from(Triangle::new( + /// coord!(x: 0.0f32, y: -1.0), + /// coord!(x: 3.0, y: 0.0), + /// coord!(x: 0.0, y: 1.0), + /// )); + /// + /// let point_geometry = Geometry::from( + /// point!(x: 12351.0, y: 129815.0) + /// ); + /// + /// let geometry_collection = GeometryCollection::new_from( + /// vec![ + /// rect_geometry, + /// triangle_geometry, + /// point_geometry + /// ] + /// ); + /// + /// assert_eq!( + /// Some(point!(x: 0.875, y: 0.125)), + /// geometry_collection.centroid(), + /// ); + /// ``` + fn centroid_trait(&self) -> Self::Output { + let mut operation = CentroidOperation::new(); + operation.add_geometry_collection(self); + operation.centroid() + } +} + +struct CentroidOperation(Option>); +impl CentroidOperation { + fn new() -> Self { + CentroidOperation(None) + } + + fn centroid(&self) -> Option> { + self.0.as_ref().map(|weighted_centroid| { + Point::from(weighted_centroid.accumulated / weighted_centroid.weight) + }) + } + + fn centroid_dimensions(&self) -> Dimensions { + self.0 + .as_ref() + .map(|weighted_centroid| weighted_centroid.dimensions) + .unwrap_or(Empty) + } + + fn add_coord(&mut self, coord: Coord) { + self.add_centroid(ZeroDimensional, coord, T::one()); + } + + fn add_line(&mut self, line: &L) + where + L: LineTraitExt, + { + match line.dimensions() { + ZeroDimensional => self.add_coord(line.start_coord()), + OneDimensional => { + let weight = line.length_ext(&Euclidean); + self.add_centroid(OneDimensional, line.centroid().0, weight) + } + _ => unreachable!("Line must be zero or one dimensional"), + } + } + + fn add_line_string(&mut self, line_string: &LS) + where + LS: LineStringTraitExt, + { + if self.centroid_dimensions() > OneDimensional { + return; + } + + if line_string.num_coords() == 1 { + unsafe { self.add_coord(line_string.geo_coord_unchecked(0)) }; + return; + } + + for line in line_string.lines() { + self.add_line(&line); + } + } + + fn add_multi_line_string(&mut self, multi_line_string: &MLS) + where + MLS: MultiLineStringTraitExt, + { + if self.centroid_dimensions() > OneDimensional { + return; + } + + for element in multi_line_string.line_strings_ext() { + self.add_line_string(&element); + } + } + + fn add_polygon

(&mut self, polygon: &P) + where + P: PolygonTraitExt, + { + // Polygons which are completely covered by their interior rings have zero area, and + // represent a unique degeneracy into a line_string which cannot be handled by accumulating + // directly into `self`. Instead, we perform a sub-operation, inspect the result, and only + // then incorporate the result into `self. + + let mut exterior_operation = CentroidOperation::new(); + if let Some(exterior) = polygon.exterior_ext() { + exterior_operation.add_ring(&exterior); + } + + let mut interior_operation = CentroidOperation::new(); + for interior in polygon.interiors_ext() { + interior_operation.add_ring(&interior); + } + + if let Some(exterior_weighted_centroid) = exterior_operation.0 { + let mut poly_weighted_centroid = exterior_weighted_centroid; + if let Some(interior_weighted_centroid) = interior_operation.0 { + poly_weighted_centroid.sub_assign(interior_weighted_centroid); + if poly_weighted_centroid.weight.is_zero() { + // A polygon with no area `interiors` completely covers `exterior`, degenerating to a linestring + polygon.exterior_ext().iter().for_each(|exterior| { + self.add_line_string(exterior); + }); + return; + } + } + self.add_weighted_centroid(poly_weighted_centroid); + } + } + + fn add_multi_point(&mut self, multi_point: &MP) + where + MP: MultiPointTraitExt, + { + if self.centroid_dimensions() > ZeroDimensional { + return; + } + + for element in multi_point.coord_iter() { + self.add_coord(element); + } + } + + fn add_multi_polygon(&mut self, multi_polygon: &MP) + where + MP: MultiPolygonTraitExt, + { + for element in multi_polygon.polygons_ext() { + self.add_polygon(&element); + } + } + + fn add_geometry_collection(&mut self, geometry_collection: &GC) + where + GC: GeometryCollectionTraitExt, + { + for element in geometry_collection.geometries_ext() { + self.add_geometry(&element); + } + } + + fn add_rect(&mut self, rect: &R) + where + R: RectTraitExt, + { + match rect.dimensions() { + ZeroDimensional => self.add_coord(rect.min_coord()), + OneDimensional => { + // Degenerate rect is a line, treat it the same way we treat flat polygons + self.add_line(&Line::new(rect.min_coord(), rect.min_coord())); + self.add_line(&Line::new(rect.min_coord(), rect.max_coord())); + self.add_line(&Line::new(rect.max_coord(), rect.max_coord())); + self.add_line(&Line::new(rect.max_coord(), rect.min_coord())); + } + TwoDimensional => { + self.add_centroid(TwoDimensional, rect.centroid().0, rect.unsigned_area()) + } + Empty => unreachable!("Rect dimensions cannot be empty"), + } + } + + fn add_triangle(&mut self, triangle: &TT) + where + TT: TriangleTraitExt, + { + match triangle.dimensions() { + ZeroDimensional => self.add_coord(triangle.first_coord()), + OneDimensional => { + // Degenerate triangle is a line, treat it the same way we treat flat + // polygons + let l0_1 = Line::new(triangle.first_coord(), triangle.second_coord()); + let l1_2 = Line::new(triangle.second_coord(), triangle.third_coord()); + let l2_0 = Line::new(triangle.third_coord(), triangle.first_coord()); + self.add_line(&l0_1); + self.add_line(&l1_2); + self.add_line(&l2_0); + } + TwoDimensional => { + let centroid = + (triangle.first_coord() + triangle.second_coord() + triangle.third_coord()) + / T::from(3).unwrap(); + self.add_centroid(TwoDimensional, centroid, triangle.unsigned_area()); + } + Empty => unreachable!("Rect dimensions cannot be empty"), + } + } + + fn add_geometry(&mut self, geometry: &G) + where + G: GeometryTraitExt, + { + if geometry.is_collection() { + for g_inner in geometry.geometries_ext() { + self.add_geometry(g_inner.borrow()); + } + } else { + match geometry.as_type_ext() { + GeometryTypeExt::Point(g) => { + if let Some(coord) = g.geo_coord() { + self.add_coord(coord) + } + } + GeometryTypeExt::Line(g) => self.add_line(g), + GeometryTypeExt::LineString(g) => self.add_line_string(g), + GeometryTypeExt::Polygon(g) => self.add_polygon(g), + GeometryTypeExt::MultiPoint(g) => self.add_multi_point(g), + GeometryTypeExt::MultiLineString(g) => self.add_multi_line_string(g), + GeometryTypeExt::MultiPolygon(g) => self.add_multi_polygon(g), + GeometryTypeExt::Rect(g) => self.add_rect(g), + GeometryTypeExt::Triangle(g) => self.add_triangle(g), + } + } + } + + fn add_ring(&mut self, ring: &LS) + where + LS: LineStringTraitExt, + { + debug_assert!(ring.is_closed()); + + let area = get_linestring_area(ring); + if area == T::zero() { + match ring.dimensions() { + // empty ring doesn't contribute to centroid + Empty => {} + // degenerate ring is a point + ZeroDimensional => unsafe { self.add_coord(ring.geo_coord_unchecked(0)) }, + // zero-area ring is a line string + _ => self.add_line_string(ring), + } + return; + } + + // Since area is non-zero, we know the ring has at least one point + let shift = unsafe { ring.geo_coord_unchecked(0) }; + + let accumulated_coord = ring.lines().fold(Coord::zero(), |accum, line| { + use crate::MapCoords; + let line = line.map_coords(|c| c - shift); + let tmp = line.determinant(); + accum + (line.end + line.start) * tmp + }); + let six = T::from(6).unwrap(); + let centroid = accumulated_coord / (six * area) + shift; + let weight = area.abs(); + self.add_centroid(TwoDimensional, centroid, weight); + } + + fn add_centroid(&mut self, dimensions: Dimensions, centroid: Coord, weight: T) { + let weighted_centroid = WeightedCentroid { + dimensions, + weight, + accumulated: centroid * weight, + }; + self.add_weighted_centroid(weighted_centroid); + } + + fn add_weighted_centroid(&mut self, other: WeightedCentroid) { + match self.0.as_mut() { + Some(centroid) => centroid.add_assign(other), + None => self.0 = Some(other), + } + } +} + +// Aggregated state for accumulating the centroid of a geometry or collection of geometries. +struct WeightedCentroid { + weight: T, + accumulated: Coord, + /// Collections of Geometries can have different dimensionality. Centroids must be considered + /// separately by dimensionality. + /// + /// e.g. If I have several Points, adding a new `Point` will affect their centroid. + /// + /// However, because a Point is zero dimensional, it is infinitely small when compared to + /// any 2-D Polygon. Thus a Point will not affect the centroid of any GeometryCollection + /// containing a 2-D Polygon. + /// + /// So, when accumulating a centroid, we must track the dimensionality of the centroid + dimensions: Dimensions, +} + +impl WeightedCentroid { + fn add_assign(&mut self, b: WeightedCentroid) { + match self.dimensions.cmp(&b.dimensions) { + Ordering::Less => *self = b, + Ordering::Greater => {} + Ordering::Equal => { + self.accumulated = self.accumulated + b.accumulated; + self.weight = self.weight + b.weight; + } + } + } + + fn sub_assign(&mut self, b: WeightedCentroid) { + match self.dimensions.cmp(&b.dimensions) { + Ordering::Less => *self = b, + Ordering::Greater => {} + Ordering::Equal => { + self.accumulated = self.accumulated - b.accumulated; + self.weight = self.weight - b.weight; + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{coord, line_string, point, polygon, wkt}; + + /// small helper to create a coordinate + fn c(x: T, y: T) -> Coord { + coord! { x: x, y: y } + } + + /// small helper to create a point + fn p(x: T, y: T) -> Point { + point! { x: x, y: y } + } + + // Tests: Centroid of LineString + #[test] + fn empty_linestring_test() { + let linestring: LineString = line_string![]; + let centroid = linestring.centroid(); + assert!(centroid.is_none()); + } + #[test] + fn linestring_one_point_test() { + let coord = coord! { + x: 40.02f64, + y: 116.34, + }; + let linestring = line_string![coord]; + let centroid = linestring.centroid(); + assert_eq!(centroid, Some(Point::from(coord))); + } + #[test] + fn linestring_test() { + let linestring = line_string![ + (x: 1., y: 1.), + (x: 7., y: 1.), + (x: 8., y: 1.), + (x: 9., y: 1.), + (x: 10., y: 1.), + (x: 11., y: 1.) + ]; + assert_eq!(linestring.centroid(), Some(point!(x: 6., y: 1. ))); + } + #[test] + fn linestring_with_repeated_point_test() { + let l1 = LineString::from(vec![p(1., 1.), p(1., 1.), p(1., 1.)]); + assert_eq!(l1.centroid(), Some(p(1., 1.))); + + let l2 = LineString::from(vec![p(2., 2.), p(2., 2.), p(2., 2.)]); + let mls = MultiLineString::new(vec![l1, l2]); + assert_eq!(mls.centroid(), Some(p(1.5, 1.5))); + } + // Tests: Centroid of MultiLineString + #[test] + fn empty_multilinestring_test() { + let mls: MultiLineString = MultiLineString::new(vec![]); + let centroid = mls.centroid(); + assert!(centroid.is_none()); + } + #[test] + fn multilinestring_with_empty_line_test() { + let mls: MultiLineString = MultiLineString::new(vec![line_string![]]); + let centroid = mls.centroid(); + assert!(centroid.is_none()); + } + #[test] + fn multilinestring_length_0_test() { + let coord = coord! { + x: 40.02f64, + y: 116.34, + }; + let mls: MultiLineString = MultiLineString::new(vec![ + line_string![coord], + line_string![coord], + line_string![coord], + ]); + assert_relative_eq!(mls.centroid().unwrap(), Point::from(coord)); + } + #[test] + fn multilinestring_one_line_test() { + let linestring = line_string![ + (x: 1., y: 1.), + (x: 7., y: 1.), + (x: 8., y: 1.), + (x: 9., y: 1.), + (x: 10., y: 1.), + (x: 11., y: 1.) + ]; + let mls: MultiLineString = MultiLineString::new(vec![linestring]); + assert_relative_eq!(mls.centroid().unwrap(), point! { x: 6., y: 1. }); + } + #[test] + fn multilinestring_test() { + let mls = wkt! { + MULTILINESTRING( + (0.0 0.0,1.0 10.0), + (1.0 10.0,2.0 0.0,3.0 1.0), + (-12.0 -100.0,7.0 8.0) + ) + }; + assert_relative_eq!( + mls.centroid().unwrap(), + point![x: -1.9097834383655845, y: -37.683866439745714] + ); + } + // Tests: Centroid of Polygon + #[test] + fn empty_polygon_test() { + let poly: Polygon = polygon![]; + assert!(poly.centroid().is_none()); + } + #[test] + fn polygon_one_point_test() { + let p = point![ x: 2., y: 1. ]; + let poly = polygon![p.0]; + assert_relative_eq!(poly.centroid().unwrap(), p); + } + + #[test] + fn centroid_polygon_numerical_stability() { + let polygon = { + use std::f64::consts::PI; + const NUM_VERTICES: usize = 10; + const ANGLE_INC: f64 = 2. * PI / NUM_VERTICES as f64; + + Polygon::new( + (0..NUM_VERTICES) + .map(|i| { + let angle = i as f64 * ANGLE_INC; + coord! { + x: angle.cos(), + y: angle.sin(), + } + }) + .collect::>() + .into(), + vec![], + ) + }; + + let centroid = polygon.centroid().unwrap(); + + let shift = coord! { x: 1.5e8, y: 1.5e8 }; + + use crate::map_coords::MapCoords; + let polygon = polygon.map_coords(|c| c + shift); + + let new_centroid = polygon.centroid().unwrap().map_coords(|c| c - shift); + debug!("centroid {:?}", centroid.0); + debug!("new_centroid {:?}", new_centroid.0); + assert_relative_eq!(centroid.0.x, new_centroid.0.x, max_relative = 0.0001); + assert_relative_eq!(centroid.0.y, new_centroid.0.y, max_relative = 0.0001); + } + + #[test] + fn polygon_test() { + let poly = polygon![ + (x: 0., y: 0.), + (x: 2., y: 0.), + (x: 2., y: 2.), + (x: 0., y: 2.), + (x: 0., y: 0.) + ]; + assert_relative_eq!(poly.centroid().unwrap(), point![x:1., y:1.]); + } + #[test] + fn polygon_hole_test() { + // hexagon + let p1 = wkt! { POLYGON( + (5.0 1.0,4.0 2.0,4.0 3.0,5.0 4.0,6.0 4.0,7.0 3.0,7.0 2.0,6.0 1.0,5.0 1.0), + (5.0 1.3,5.5 2.0,6.0 1.3,5.0 1.3), + (5.0 2.3,5.5 3.0,6.0 2.3,5.0 2.3) + ) }; + let centroid = p1.centroid().unwrap(); + assert_relative_eq!(centroid, point!(x: 5.5, y: 2.5518518518518523)); + } + #[test] + fn flat_polygon_test() { + let poly = wkt! { POLYGON((0. 1.,1. 1.,0. 1.)) }; + assert_eq!(poly.centroid(), Some(p(0.5, 1.))); + } + #[test] + fn multi_poly_with_flat_polygon_test() { + let multipoly = wkt! { MULTIPOLYGON(((0. 0.,1. 0.,0. 0.))) }; + assert_eq!(multipoly.centroid(), Some(p(0.5, 0.))); + } + #[test] + fn multi_poly_with_multiple_flat_polygon_test() { + let multipoly = wkt! { MULTIPOLYGON( + ((1. 1.,1. 3.,1. 1.)), + ((2. 2.,6. 2.,2. 2.)) + )}; + + assert_eq!(multipoly.centroid(), Some(p(3., 2.))); + } + #[test] + fn multi_poly_with_only_points_test() { + let p1 = wkt! { POLYGON((1. 1.,1. 1.,1. 1.)) }; + assert_eq!(p1.centroid(), Some(p(1., 1.))); + + let multipoly = wkt! { MULTIPOLYGON( + ((1. 1.,1. 1.,1. 1.)), + ((2. 2., 2. 2.,2. 2.)) + ) }; + assert_eq!(multipoly.centroid(), Some(p(1.5, 1.5))); + } + #[test] + fn multi_poly_with_one_ring_and_one_real_poly() { + // if the multipolygon is composed of a 'normal' polygon (with an area not null) + // and a ring (a polygon with a null area) + // the centroid of the multipolygon is the centroid of the 'normal' polygon + let normal = Polygon::new( + LineString::from(vec![p(1., 1.), p(1., 3.), p(3., 1.), p(1., 1.)]), + vec![], + ); + let flat = Polygon::new( + LineString::from(vec![p(2., 2.), p(6., 2.), p(2., 2.)]), + vec![], + ); + let multipoly = MultiPolygon::new(vec![normal.clone(), flat]); + assert_eq!(multipoly.centroid(), normal.centroid()); + } + #[test] + fn polygon_flat_interior_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::from(vec![p(0., 0.), p(0., 1.), p(0., 0.)])], + ); + assert_eq!(poly.centroid(), Some(p(0.5, 0.5))); + } + #[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![])], + ); + assert_eq!(poly.centroid(), Some(p(0.5, 0.5))); + } + #[test] + fn polygon_ring_test() { + let square = LineString::from(vec![p(0., 0.), p(0., 1.), p(1., 1.), p(1., 0.), p(0., 0.)]); + let poly = Polygon::new(square.clone(), vec![square]); + assert_eq!(poly.centroid(), Some(p(0.5, 0.5))); + } + #[test] + fn polygon_cell_test() { + // test the centroid of polygon with a null area + // this one a polygon with 2 interior polygon that makes a partition of the exterior + let square = LineString::from(vec![p(0., 0.), p(0., 2.), p(2., 2.), p(2., 0.), p(0., 0.)]); + let bottom = LineString::from(vec![p(0., 0.), p(2., 0.), p(2., 1.), p(0., 1.), p(0., 0.)]); + let top = LineString::from(vec![p(0., 1.), p(2., 1.), p(2., 2.), p(0., 2.), p(0., 1.)]); + let poly = Polygon::new(square, vec![top, bottom]); + assert_eq!(poly.centroid(), Some(p(1., 1.))); + } + // Tests: Centroid of MultiPolygon + #[test] + fn empty_multipolygon_polygon_test() { + assert!(MultiPolygon::::new(Vec::new()).centroid().is_none()); + } + + #[test] + fn multipolygon_one_polygon_test() { + let linestring = + LineString::from(vec![p(0., 0.), p(2., 0.), p(2., 2.), p(0., 2.), p(0., 0.)]); + let poly = Polygon::new(linestring, Vec::new()); + assert_eq!(MultiPolygon::new(vec![poly]).centroid(), Some(p(1., 1.))); + } + #[test] + fn multipolygon_two_polygons_test() { + let linestring = + LineString::from(vec![p(2., 1.), p(5., 1.), p(5., 3.), p(2., 3.), p(2., 1.)]); + let poly1 = Polygon::new(linestring, Vec::new()); + let linestring = + LineString::from(vec![p(7., 1.), p(8., 1.), p(8., 2.), p(7., 2.), p(7., 1.)]); + let poly2 = Polygon::new(linestring, Vec::new()); + let centroid = MultiPolygon::new(vec![poly1, poly2]).centroid().unwrap(); + assert_relative_eq!( + centroid, + point![x: 4.071428571428571, y: 1.9285714285714286] + ); + } + #[test] + fn multipolygon_two_polygons_of_opposite_clockwise_test() { + let linestring = LineString::from(vec![(0., 0.), (2., 0.), (2., 2.), (0., 2.), (0., 0.)]); + let poly1 = Polygon::new(linestring, Vec::new()); + let linestring = LineString::from(vec![(0., 0.), (-2., 0.), (-2., 2.), (0., 2.), (0., 0.)]); + let poly2 = Polygon::new(linestring, Vec::new()); + assert_relative_eq!( + MultiPolygon::new(vec![poly1, poly2]).centroid().unwrap(), + point![x: 0., y: 1.] + ); + } + #[test] + fn bounding_rect_test() { + let bounding_rect = Rect::new(coord! { x: 0., y: 50. }, coord! { x: 4., y: 100. }); + let point = point![x: 2., y: 75.]; + assert_eq!(point, bounding_rect.centroid()); + } + #[test] + fn line_test() { + let line1 = Line::new(c(0., 1.), c(1., 3.)); + assert_eq!(line1.centroid(), point![x: 0.5, y: 2.]); + } + #[test] + fn collection_weighting() { + let p0 = point!(x: 0.0, y: 0.0); + let p1 = point!(x: 2.0, y: 0.0); + let p2 = point!(x: 2.0, y: 2.0); + let p3 = point!(x: 0.0, y: 2.0); + + let multi_point = MultiPoint::new(vec![p0, p1, p2, p3]); + assert_eq!(multi_point.centroid().unwrap(), point!(x: 1.0, y: 1.0)); + + let collection = + GeometryCollection::new_from(vec![MultiPoint::new(vec![p1, p2, p3]).into(), p0.into()]); + + assert_eq!(collection.centroid().unwrap(), point!(x: 1.0, y: 1.0)); + } + #[test] + fn triangles() { + // boring triangle + assert_eq!( + Triangle::new(c(0., 0.), c(3., 0.), c(1.5, 3.)).centroid(), + point!(x: 1.5, y: 1.0) + ); + + // flat triangle + assert_eq!( + Triangle::new(c(0., 0.), c(3., 0.), c(1., 0.)).centroid(), + point!(x: 1.5, y: 0.0) + ); + + // flat triangle that's not axis-aligned + assert_eq!( + Triangle::new(c(0., 0.), c(3., 3.), c(1., 1.)).centroid(), + point!(x: 1.5, y: 1.5) + ); + + // triangle with some repeated points + assert_eq!( + Triangle::new(c(0., 0.), c(0., 0.), c(1., 0.)).centroid(), + point!(x: 0.5, y: 0.0) + ); + + // triangle with all repeated points + assert_eq!( + Triangle::new(c(0., 0.5), c(0., 0.5), c(0., 0.5)).centroid(), + point!(x: 0., y: 0.5) + ) + } + + #[test] + fn degenerate_triangle_like_ring() { + let triangle = Triangle::new(c(0., 0.), c(1., 1.), c(2., 2.)); + let poly: Polygon<_> = triangle.into(); + + let line = Line::new(c(0., 1.), c(1., 3.)); + + let g1 = GeometryCollection::new_from(vec![triangle.into(), line.into()]); + let g2 = GeometryCollection::new_from(vec![poly.into(), line.into()]); + assert_eq!(g1.centroid(), g2.centroid()); + } + + #[test] + fn degenerate_rect_like_ring() { + let rect = Rect::new(c(0., 0.), c(0., 4.)); + let poly: Polygon<_> = rect.into(); + + let line = Line::new(c(0., 1.), c(1., 3.)); + + let g1 = GeometryCollection::new_from(vec![rect.into(), line.into()]); + let g2 = GeometryCollection::new_from(vec![poly.into(), line.into()]); + assert_eq!(g1.centroid(), g2.centroid()); + } + + #[test] + fn rectangles() { + // boring rect + assert_eq!( + Rect::new(c(0., 0.), c(4., 4.)).centroid(), + point!(x: 2.0, y: 2.0) + ); + + // flat rect + assert_eq!( + Rect::new(c(0., 0.), c(4., 0.)).centroid(), + point!(x: 2.0, y: 0.0) + ); + + // rect with all repeated points + assert_eq!( + Rect::new(c(4., 4.), c(4., 4.)).centroid(), + point!(x: 4., y: 4.) + ); + + // collection with rect + let mut collection = GeometryCollection::new_from(vec![ + p(0., 0.).into(), + p(6., 0.).into(), + p(6., 6.).into(), + ]); + // sanity check + assert_eq!(collection.centroid().unwrap(), point!(x: 4., y: 2.)); + + // 0-d rect treated like point + collection.0.push(Rect::new(c(0., 6.), c(0., 6.)).into()); + assert_eq!(collection.centroid().unwrap(), point!(x: 3., y: 3.)); + + // 1-d rect treated like line. Since a line has higher dimensions than the rest of the + // collection, its centroid clobbers everything else in the collection. + collection.0.push(Rect::new(c(0., 0.), c(0., 2.)).into()); + assert_eq!(collection.centroid().unwrap(), point!(x: 0., y: 1.)); + + // 2-d has higher dimensions than the rest of the collection, so its centroid clobbers + // everything else in the collection. + collection + .0 + .push(Rect::new(c(10., 10.), c(11., 11.)).into()); + assert_eq!(collection.centroid().unwrap(), point!(x: 10.5, y: 10.5)); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/coordinate_position.rs b/rust/sedona-geo-generic-alg/src/algorithm/coordinate_position.rs new file mode 100644 index 00000000..1adb2f5a --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/coordinate_position.rs @@ -0,0 +1,901 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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; + +use crate::geometry::*; +use crate::intersects::{point_in_rect, value_in_between}; +use crate::kernels::*; +use crate::GeoNum; +use crate::{BoundingRect, HasDimensions, Intersects}; +use sedona_geo_traits_ext::*; + +/// The position of a `Coord` relative to a `Geometry` +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum CoordPos { + OnBoundary, + Inside, + Outside, +} + +/// Determine whether a `Coord` lies inside, outside, or on the boundary of a geometry. +/// +/// # Examples +/// +/// ```rust +/// use sedona_geo_generic_alg::{polygon, coord}; +/// use sedona_geo_generic_alg::coordinate_position::{CoordinatePosition, CoordPos}; +/// +/// let square_poly = polygon![(x: 0.0, y: 0.0), (x: 2.0, y: 0.0), (x: 2.0, y: 2.0), (x: 0.0, y: 2.0), (x: 0.0, y: 0.0)]; +/// +/// let inside_coord = coord! { x: 1.0, y: 1.0 }; +/// assert_eq!(square_poly.coordinate_position(&inside_coord), CoordPos::Inside); +/// +/// let boundary_coord = coord! { x: 0.0, y: 1.0 }; +/// assert_eq!(square_poly.coordinate_position(&boundary_coord), CoordPos::OnBoundary); +/// +/// let outside_coord = coord! { x: 5.0, y: 5.0 }; +/// assert_eq!(square_poly.coordinate_position(&outside_coord), CoordPos::Outside); +/// ``` +pub trait CoordinatePosition { + type Scalar: GeoNum; + fn coordinate_position(&self, coord: &Coord) -> CoordPos { + let mut is_inside = false; + let mut boundary_count = 0; + + self.calculate_coordinate_position(coord, &mut is_inside, &mut boundary_count); + + // “The boundary of an arbitrary collection of geometries whose interiors are disjoint + // consists of geometries drawn from the boundaries of the element geometries by + // application of the ‘mod 2’ union rule” + // + // ― OpenGIS Simple Feature Access § 6.1.15.1 + if boundary_count % 2 == 1 { + CoordPos::OnBoundary + } else if is_inside { + CoordPos::Inside + } else { + CoordPos::Outside + } + } + + // impls of this trait must: + // 1. set `is_inside = true` if `coord` is contained within the Interior of any component. + // 2. increment `boundary_count` for each component whose Boundary contains `coord`. + fn calculate_coordinate_position( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ); +} + +impl CoordinatePosition for G +where + G: GeoTraitExtWithTypeTag, + G: CoordinatePositionTrait, +{ + type Scalar = G::T; + + fn calculate_coordinate_position( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + self.calculate_coordinate_position_trait(coord, is_inside, boundary_count); + } +} + +pub trait CoordinatePositionTrait { + type T: GeoNum; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ); +} + +impl CoordinatePositionTrait for C +where + T: GeoNum, + C: CoordTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + _boundary_count: &mut usize, + ) { + if &self.geo_coord() == coord { + *is_inside = true; + } + } +} + +impl CoordinatePositionTrait for P +where + T: GeoNum, + P: PointTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + _boundary_count: &mut usize, + ) { + if let Some(point_coord) = self.geo_coord() { + if &point_coord == coord { + *is_inside = true; + } + } + } +} + +impl CoordinatePositionTrait for L +where + T: GeoNum, + L: LineTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + let start = self.start_coord(); + let end = self.end_coord(); + + // degenerate line is a point + if start == end { + self.start_ext() + .calculate_coordinate_position(coord, is_inside, boundary_count); + return; + } + + if coord == &start || coord == &end { + *boundary_count += 1; + } else if self.intersects(coord) { + *is_inside = true; + } + } +} + +impl CoordinatePositionTrait for LS +where + T: GeoNum, + LS: LineStringTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + let num_coords = self.num_coords(); + if num_coords < 2 { + debug_assert!(false, "invalid line string with less than 2 coords"); + return; + } + + if num_coords == 2 { + // line string with two coords is just a line + unsafe { + let start = self.geo_coord_unchecked(0); + let end = self.geo_coord_unchecked(1); + Line::new(start, end).calculate_coordinate_position( + coord, + is_inside, + boundary_count, + ); + } + return; + } + + // optimization: return early if there's no chance of an intersection + // since bounding rect is not empty, we can safely `unwrap`. + if !self.bounding_rect().unwrap().intersects(coord) { + return; + } + + // A closed linestring has no boundary, per SFS + if !self.is_closed() { + // since we have at least two coords, first and last will exist + unsafe { + let first = self.geo_coord_unchecked(0); + let last = self.geo_coord_unchecked(num_coords - 1); + if coord == &first || coord == &last { + *boundary_count += 1; + return; + } + } + } + + if self.intersects(coord) { + // We've already checked for "Boundary" condition, so if there's an intersection at + // this point, coord must be on the interior + *is_inside = true + } + } +} + +impl CoordinatePositionTrait for TT +where + T: GeoNum, + TT: TriangleTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + *is_inside = self + .to_lines() + .map(|l| { + let orientation = T::Ker::orient2d(l.start, l.end, *coord); + if orientation == Orientation::Collinear + && point_in_rect(*coord, l.start, l.end) + && coord.x != l.end.x + { + *boundary_count += 1; + } + orientation + }) + .windows(2) + .all(|win| win[0] == win[1] && win[0] != Orientation::Collinear); + } +} + +impl CoordinatePositionTrait for R +where + T: GeoNum, + R: RectTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + let mut boundary = false; + let min = self.min_coord(); + + match coord.x.partial_cmp(&min.x).unwrap() { + Ordering::Less => return, + Ordering::Equal => boundary = true, + Ordering::Greater => {} + } + match coord.y.partial_cmp(&min.y).unwrap() { + Ordering::Less => return, + Ordering::Equal => boundary = true, + Ordering::Greater => {} + } + + let max = self.max_coord(); + + match max.x.partial_cmp(&coord.x).unwrap() { + Ordering::Less => return, + Ordering::Equal => boundary = true, + Ordering::Greater => {} + } + match max.y.partial_cmp(&coord.y).unwrap() { + Ordering::Less => return, + Ordering::Equal => boundary = true, + Ordering::Greater => {} + } + + if boundary { + *boundary_count += 1; + } else { + *is_inside = true; + } + } +} + +impl CoordinatePositionTrait for MP +where + T: GeoNum, + MP: MultiPointTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + _boundary_count: &mut usize, + ) { + if self + .points_ext() + .any(|p| p.geo_coord().is_some_and(|c| &c == coord)) + { + *is_inside = true; + } + } +} + +impl CoordinatePositionTrait for P +where + T: GeoNum, + P: PolygonTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + let Some(exterior) = self.exterior_ext() else { + return; + }; + + if self.is_empty() { + return; + } + + match coord_pos_relative_to_ring(*coord, &exterior) { + CoordPos::Outside => {} + CoordPos::OnBoundary => { + *boundary_count += 1; + } + CoordPos::Inside => { + for hole in self.interiors_ext() { + match coord_pos_relative_to_ring(*coord, &hole) { + CoordPos::Outside => {} + CoordPos::OnBoundary => { + *boundary_count += 1; + return; + } + CoordPos::Inside => { + return; + } + } + } + // the coord is *outside* the interior holes, so it's *inside* the polygon + *is_inside = true; + } + } + } +} + +impl CoordinatePositionTrait for MLS +where + T: GeoNum, + MLS: MultiLineStringTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + for line_string in self.line_strings_ext() { + line_string.calculate_coordinate_position_trait(coord, is_inside, boundary_count); + } + } +} + +impl CoordinatePositionTrait for MP +where + T: GeoNum, + MP: MultiPolygonTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + for polygon in self.polygons_ext() { + polygon.calculate_coordinate_position_trait(coord, is_inside, boundary_count); + } + } +} + +impl CoordinatePositionTrait for GC +where + T: GeoNum, + GC: GeometryCollectionTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + for geometry in self.geometries_ext() { + geometry.calculate_coordinate_position_trait(coord, is_inside, boundary_count); + } + } +} + +fn geometry_calculate_coordinate_position( + g: &G, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, +) where + T: GeoNum, + G: GeometryTraitExt, +{ + if g.is_collection() { + for g_inner in g.geometries_ext() { + geometry_calculate_coordinate_position( + g_inner.borrow(), + coord, + is_inside, + boundary_count, + ); + } + } else { + match g.as_type_ext() { + GeometryTypeExt::Point(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::Line(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::LineString(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::Polygon(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::MultiPoint(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::MultiLineString(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::MultiPolygon(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::Rect(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + GeometryTypeExt::Triangle(g) => { + g.calculate_coordinate_position_trait(coord, is_inside, boundary_count) + } + } + } +} + +impl CoordinatePositionTrait for G +where + T: GeoNum, + G: GeometryTraitExt, +{ + type T = T; + + fn calculate_coordinate_position_trait( + &self, + coord: &Coord, + is_inside: &mut bool, + boundary_count: &mut usize, + ) { + geometry_calculate_coordinate_position(self, coord, is_inside, boundary_count); + } +} + +/// Calculate the position of a `Coord` relative to a +/// closed `LineString`. +pub fn coord_pos_relative_to_ring(coord: Coord, linestring: &LS) -> CoordPos +where + T: GeoNum, + LS: LineStringTraitExt, +{ + debug_assert!(linestring.is_closed()); + + // LineString without points + if linestring.num_coords() == 0 { + return CoordPos::Outside; + } + if linestring.num_coords() == 1 { + // If LineString has one point, it will not generate + // any lines. So, we handle this edge case separately. + return if coord == unsafe { linestring.geo_coord_unchecked(0) } { + CoordPos::OnBoundary + } else { + CoordPos::Outside + }; + } + + // Use winding number algorithm with on boundary short-cicuit + // See: https://en.wikipedia.org/wiki/Point_in_polygon#Winding_number_algorithm + let mut winding_number = 0; + for line in linestring.lines() { + // Edge Crossing Rules: + // 1. an upward edge includes its starting endpoint, and excludes its final endpoint; + // 2. a downward edge excludes its starting endpoint, and includes its final endpoint; + // 3. horizontal edges are excluded + // 4. the edge-ray intersection point must be strictly right of the coord. + if line.start.y <= coord.y { + if line.end.y >= coord.y { + let o = T::Ker::orient2d(line.start, line.end, coord); + if o == Orientation::CounterClockwise && line.end.y != coord.y { + winding_number += 1 + } else if o == Orientation::Collinear + && value_in_between(coord.x, line.start.x, line.end.x) + { + return CoordPos::OnBoundary; + } + }; + } else if line.end.y <= coord.y { + let o = T::Ker::orient2d(line.start, line.end, coord); + if o == Orientation::Clockwise { + winding_number -= 1 + } else if o == Orientation::Collinear + && value_in_between(coord.x, line.start.x, line.end.x) + { + return CoordPos::OnBoundary; + } + } + } + if winding_number == 0 { + CoordPos::Outside + } else { + CoordPos::Inside + } +} + +#[cfg(test)] +mod test { + use geo_types::coord; + + use super::*; + use crate::{line_string, point, polygon}; + + #[test] + fn test_empty_poly() { + let square_poly: Polygon = Polygon::new(LineString::new(vec![]), vec![]); + assert_eq!( + square_poly.coordinate_position(&Coord::zero()), + CoordPos::Outside + ); + } + + #[test] + fn test_simple_poly() { + let square_poly = polygon![(x: 0.0, y: 0.0), (x: 2.0, y: 0.0), (x: 2.0, y: 2.0), (x: 0.0, y: 2.0), (x: 0.0, y: 0.0)]; + + let inside_coord = coord! { x: 1.0, y: 1.0 }; + assert_eq!( + square_poly.coordinate_position(&inside_coord), + CoordPos::Inside + ); + + let vertex_coord = coord! { x: 0.0, y: 0.0 }; + assert_eq!( + square_poly.coordinate_position(&vertex_coord), + CoordPos::OnBoundary + ); + + let boundary_coord = coord! { x: 0.0, y: 1.0 }; + assert_eq!( + square_poly.coordinate_position(&boundary_coord), + CoordPos::OnBoundary + ); + + let outside_coord = coord! { x: 5.0, y: 5.0 }; + assert_eq!( + square_poly.coordinate_position(&outside_coord), + CoordPos::Outside + ); + } + + #[test] + fn test_poly_interior() { + let poly = polygon![ + exterior: [ + (x: 11., y: 11.), + (x: 20., y: 11.), + (x: 20., y: 20.), + (x: 11., y: 20.), + (x: 11., y: 11.), + ], + interiors: [ + [ + (x: 13., y: 13.), + (x: 13., y: 17.), + (x: 17., y: 17.), + (x: 17., y: 13.), + (x: 13., y: 13.), + ] + ], + ]; + + let inside_hole = coord! { x: 14.0, y: 14.0 }; + assert_eq!(poly.coordinate_position(&inside_hole), CoordPos::Outside); + + let outside_poly = coord! { x: 30.0, y: 30.0 }; + assert_eq!(poly.coordinate_position(&outside_poly), CoordPos::Outside); + + let on_outside_border = coord! { x: 20.0, y: 15.0 }; + assert_eq!( + poly.coordinate_position(&on_outside_border), + CoordPos::OnBoundary + ); + + let on_inside_border = coord! { x: 13.0, y: 15.0 }; + assert_eq!( + poly.coordinate_position(&on_inside_border), + CoordPos::OnBoundary + ); + + let inside_coord = coord! { x: 12.0, y: 12.0 }; + assert_eq!(poly.coordinate_position(&inside_coord), CoordPos::Inside); + } + + #[test] + fn test_simple_line() { + use crate::point; + let line = Line::new(point![x: 0.0, y: 0.0], point![x: 10.0, y: 10.0]); + + let start = coord! { x: 0.0, y: 0.0 }; + assert_eq!(line.coordinate_position(&start), CoordPos::OnBoundary); + + let end = coord! { x: 10.0, y: 10.0 }; + assert_eq!(line.coordinate_position(&end), CoordPos::OnBoundary); + + let interior = coord! { x: 5.0, y: 5.0 }; + assert_eq!(line.coordinate_position(&interior), CoordPos::Inside); + + let outside = coord! { x: 6.0, y: 5.0 }; + assert_eq!(line.coordinate_position(&outside), CoordPos::Outside); + } + + #[test] + fn test_degenerate_line() { + let line = Line::new(point![x: 0.0, y: 0.0], point![x: 0.0, y: 0.0]); + + let start = coord! { x: 0.0, y: 0.0 }; + assert_eq!(line.coordinate_position(&start), CoordPos::Inside); + + let outside = coord! { x: 10.0, y: 10.0 }; + assert_eq!(line.coordinate_position(&outside), CoordPos::Outside); + } + + #[test] + fn test_point() { + let p1 = point![x: 2.0, y: 0.0]; + + let c1 = coord! { x: 2.0, y: 0.0 }; + let c2 = coord! { x: 3.0, y: 3.0 }; + + assert_eq!(p1.coordinate_position(&c1), CoordPos::Inside); + assert_eq!(p1.coordinate_position(&c2), CoordPos::Outside); + + assert_eq!(c1.coordinate_position(&c1), CoordPos::Inside); + assert_eq!(c1.coordinate_position(&c2), CoordPos::Outside); + } + + #[test] + fn test_simple_line_string() { + let line_string = + line_string![(x: 0.0, y: 0.0), (x: 1.0, y: 1.0), (x: 2.0, y: 0.0), (x: 3.0, y: 0.0)]; + + let start = Coord::zero(); + assert_eq!( + line_string.coordinate_position(&start), + CoordPos::OnBoundary + ); + + let midpoint = coord! { x: 0.5, y: 0.5 }; + assert_eq!(line_string.coordinate_position(&midpoint), CoordPos::Inside); + + let vertex = coord! { x: 2.0, y: 0.0 }; + assert_eq!(line_string.coordinate_position(&vertex), CoordPos::Inside); + + let end = coord! { x: 3.0, y: 0.0 }; + assert_eq!(line_string.coordinate_position(&end), CoordPos::OnBoundary); + + let outside = coord! { x: 3.0, y: 1.0 }; + assert_eq!(line_string.coordinate_position(&outside), CoordPos::Outside); + } + + #[test] + fn test_degenerate_line_strings() { + let line_string = line_string![(x: 0.0, y: 0.0), (x: 0.0, y: 0.0)]; + + let start = Coord::zero(); + assert_eq!(line_string.coordinate_position(&start), CoordPos::Inside); + + let line_string = line_string![(x: 0.0, y: 0.0), (x: 2.0, y: 0.0)]; + + let start = Coord::zero(); + assert_eq!( + line_string.coordinate_position(&start), + CoordPos::OnBoundary + ); + } + + #[test] + fn test_closed_line_string() { + let line_string = line_string![(x: 0.0, y: 0.0), (x: 1.0, y: 1.0), (x: 2.0, y: 0.0), (x: 3.0, y: 2.0), (x: 0.0, y: 2.0), (x: 0.0, y: 0.0)]; + + // sanity check + assert!(line_string.is_closed()); + + // closed line strings have no boundary + let start = Coord::zero(); + assert_eq!(line_string.coordinate_position(&start), CoordPos::Inside); + + let midpoint = coord! { x: 0.5, y: 0.5 }; + assert_eq!(line_string.coordinate_position(&midpoint), CoordPos::Inside); + + let outside = coord! { x: 3.0, y: 1.0 }; + assert_eq!(line_string.coordinate_position(&outside), CoordPos::Outside); + } + + #[test] + fn test_boundary_rule() { + let multi_line_string = MultiLineString::new(vec![ + // first two lines have same start point but different end point + line_string![(x: 0.0, y: 0.0), (x: 1.0, y: 1.0)], + line_string![(x: 0.0, y: 0.0), (x: -1.0, y: -1.0)], + // third line has its own start point, but it's end touches the middle of first line + line_string![(x: 0.0, y: 1.0), (x: 0.5, y: 0.5)], + // fourth and fifth have independent start points, but both end at the middle of the + // second line + line_string![(x: 0.0, y: -1.0), (x: -0.5, y: -0.5)], + line_string![(x: 0.0, y: -2.0), (x: -0.5, y: -0.5)], + ]); + + let outside_of_all = coord! { x: 123.0, y: 123.0 }; + assert_eq!( + multi_line_string.coordinate_position(&outside_of_all), + CoordPos::Outside + ); + + let end_of_one_line = coord! { x: -1.0, y: -1.0 }; + assert_eq!( + multi_line_string.coordinate_position(&end_of_one_line), + CoordPos::OnBoundary + ); + + // in boundary of first and second, so considered *not* in the boundary by mod 2 rule + let shared_start = Coord::zero(); + assert_eq!( + multi_line_string.coordinate_position(&shared_start), + CoordPos::Outside + ); + + // *in* the first line, on the boundary of the third line + let one_end_plus_midpoint = coord! { x: 0.5, y: 0.5 }; + assert_eq!( + multi_line_string.coordinate_position(&one_end_plus_midpoint), + CoordPos::OnBoundary + ); + + // *in* the first line, on the *boundary* of the fourth and fifth line + let two_ends_plus_midpoint = coord! { x: -0.5, y: -0.5 }; + assert_eq!( + multi_line_string.coordinate_position(&two_ends_plus_midpoint), + CoordPos::Inside + ); + } + + #[test] + fn test_rect() { + let rect = Rect::new((0.0, 0.0), (10.0, 10.0)); + assert_eq!( + rect.coordinate_position(&coord! { x: 5.0, y: 5.0 }), + CoordPos::Inside + ); + assert_eq!( + rect.coordinate_position(&coord! { x: 0.0, y: 5.0 }), + CoordPos::OnBoundary + ); + assert_eq!( + rect.coordinate_position(&coord! { x: 15.0, y: 15.0 }), + CoordPos::Outside + ); + } + + #[test] + fn test_triangle() { + let triangle = Triangle::new((0.0, 0.0).into(), (5.0, 10.0).into(), (10.0, 0.0).into()); + assert_eq!( + triangle.coordinate_position(&coord! { x: 5.0, y: 5.0 }), + CoordPos::Inside + ); + assert_eq!( + triangle.coordinate_position(&coord! { x: 2.5, y: 5.0 }), + CoordPos::OnBoundary + ); + assert_eq!( + triangle.coordinate_position(&coord! { x: 2.49, y: 5.0 }), + CoordPos::Outside + ); + } + + #[test] + fn test_collection() { + let triangle = Triangle::new((0.0, 0.0).into(), (5.0, 10.0).into(), (10.0, 0.0).into()); + let rect = Rect::new((0.0, 0.0), (10.0, 10.0)); + let collection = GeometryCollection::new_from(vec![triangle.into(), rect.into()]); + let geom = Geometry::GeometryCollection(collection.clone()); + + // outside of both + assert_eq!( + collection.coordinate_position(&coord! { x: 15.0, y: 15.0 }), + CoordPos::Outside + ); + assert_eq!( + geom.coordinate_position(&coord! { x: 15.0, y: 15.0 }), + CoordPos::Outside + ); + + // inside both + assert_eq!( + collection.coordinate_position(&coord! { x: 5.0, y: 5.0 }), + CoordPos::Inside + ); + assert_eq!( + geom.coordinate_position(&coord! { x: 5.0, y: 5.0 }), + CoordPos::Inside + ); + + // inside one, boundary of other + assert_eq!( + collection.coordinate_position(&coord! { x: 2.5, y: 5.0 }), + CoordPos::OnBoundary + ); + assert_eq!( + geom.coordinate_position(&coord! { x: 2.5, y: 5.0 }), + CoordPos::OnBoundary + ); + + // boundary of both + assert_eq!( + collection.coordinate_position(&coord! { x: 5.0, y: 10.0 }), + CoordPos::Outside + ); + assert_eq!( + geom.coordinate_position(&coord! { x: 5.0, y: 10.0 }), + CoordPos::Outside + ); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/dimensions.rs b/rust/sedona-geo-generic-alg/src/algorithm/dimensions.rs new file mode 100644 index 00000000..a2f7dc5c --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/dimensions.rs @@ -0,0 +1,786 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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::*; + +use crate::Orientation::Collinear; +use crate::{CoordNum, GeoNum}; + +/// Geometries can have 0, 1, or two dimensions. Or, in the case of an [`empty`](#is_empty) +/// geometry, a special `Empty` dimensionality. +/// +/// # Examples +/// +/// ``` +/// use geo_types::{Point, Rect, line_string}; +/// use sedona_geo_generic_alg::dimensions::{HasDimensions, Dimensions}; +/// +/// let point = Point::new(0.0, 5.0); +/// let line_string = line_string![(x: 0.0, y: 0.0), (x: 5.0, y: 5.0), (x: 0.0, y: 5.0)]; +/// let rect = Rect::new((0.0, 0.0), (10.0, 10.0)); +/// assert_eq!(Dimensions::ZeroDimensional, point.dimensions()); +/// assert_eq!(Dimensions::OneDimensional, line_string.dimensions()); +/// assert_eq!(Dimensions::TwoDimensional, rect.dimensions()); +/// +/// assert!(point.dimensions() < line_string.dimensions()); +/// assert!(rect.dimensions() > line_string.dimensions()); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum Dimensions { + /// Some geometries, like a `MultiPoint` or `GeometryCollection` may have no elements - thus no + /// dimensions. Note that this is distinct from being `ZeroDimensional`, like a `Point`. + Empty, + /// Dimension of a point + ZeroDimensional, + /// Dimension of a line or curve + OneDimensional, + /// Dimension of a surface + TwoDimensional, +} + +/// Operate on the dimensionality of geometries. +pub trait HasDimensions { + /// Some geometries, like a `MultiPoint`, can have zero coordinates - we call these `empty`. + /// + /// Types like `Point` and `Rect`, which have at least one coordinate by construction, can + /// never be considered empty. + /// ``` + /// use geo_types::{Point, coord, LineString}; + /// use sedona_geo_generic_alg::HasDimensions; + /// + /// let line_string = LineString::new(vec![ + /// coord! { x: 0., y: 0. }, + /// coord! { x: 10., y: 0. }, + /// ]); + /// assert!(!line_string.is_empty()); + /// + /// let empty_line_string: LineString = LineString::new(vec![]); + /// assert!(empty_line_string.is_empty()); + /// + /// let point = Point::new(0.0, 0.0); + /// assert!(!point.is_empty()); + /// ``` + fn is_empty(&self) -> bool; + + /// The dimensions of some geometries are fixed, e.g. a Point always has 0 dimensions. However + /// for others, the dimensionality depends on the specific geometry instance - for example + /// typical `Rect`s are 2-dimensional, but it's possible to create degenerate `Rect`s which + /// have either 1 or 0 dimensions. + /// + /// ## Examples + /// + /// ``` + /// use geo_types::{GeometryCollection, Rect, Point}; + /// use sedona_geo_generic_alg::dimensions::{Dimensions, HasDimensions}; + /// + /// // normal rectangle + /// let rect = Rect::new((0.0, 0.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::TwoDimensional, rect.dimensions()); + /// + /// // "rectangle" with zero height degenerates to a line + /// let degenerate_line_rect = Rect::new((0.0, 10.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::OneDimensional, degenerate_line_rect.dimensions()); + /// + /// // "rectangle" with zero height and zero width degenerates to a point + /// let degenerate_point_rect = Rect::new((10.0, 10.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::ZeroDimensional, degenerate_point_rect.dimensions()); + /// + /// // collections inherit the greatest dimensionality of their elements + /// let geometry_collection = GeometryCollection::new_from(vec![degenerate_line_rect.into(), degenerate_point_rect.into()]); + /// assert_eq!(Dimensions::OneDimensional, geometry_collection.dimensions()); + /// + /// let point = Point::new(10.0, 10.0); + /// 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![]); + /// assert_eq!(Dimensions::Empty, empty_collection.dimensions()); + /// assert!(empty_collection.dimensions() < point.dimensions()); + /// ``` + fn dimensions(&self) -> Dimensions; + + /// The dimensions of the `Geometry`'s boundary, as used by OGC-SFA. + /// + /// ## Examples + /// + /// ``` + /// use geo_types::{GeometryCollection, Rect, Point}; + /// use sedona_geo_generic_alg::dimensions::{Dimensions, HasDimensions}; + /// + /// // a point has no boundary + /// let point = Point::new(10.0, 10.0); + /// assert_eq!(Dimensions::Empty, point.boundary_dimensions()); + /// + /// // a typical rectangle has a *line* (one dimensional) boundary + /// let rect = Rect::new((0.0, 0.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::OneDimensional, rect.boundary_dimensions()); + /// + /// // a "rectangle" with zero height degenerates to a line, whose boundary is two points + /// let degenerate_line_rect = Rect::new((0.0, 10.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::ZeroDimensional, degenerate_line_rect.boundary_dimensions()); + /// + /// // a "rectangle" with zero height and zero width degenerates to a point, + /// // and points have no boundary + /// let degenerate_point_rect = Rect::new((10.0, 10.0), (10.0, 10.0)); + /// assert_eq!(Dimensions::Empty, degenerate_point_rect.boundary_dimensions()); + /// + /// // collections inherit the greatest dimensionality of their elements + /// 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![]); + /// assert_eq!(Dimensions::Empty, geometry_collection.boundary_dimensions()); + /// ``` + fn boundary_dimensions(&self) -> Dimensions; +} + +impl HasDimensions for G +where + G: GeoTraitExtWithTypeTag + HasDimensionsTrait, +{ + fn is_empty(&self) -> bool { + self.is_empty_trait() + } + + fn dimensions(&self) -> Dimensions { + self.dimensions_trait() + } + + fn boundary_dimensions(&self) -> Dimensions { + self.boundary_dimensions_trait() + } +} + +trait HasDimensionsTrait { + fn is_empty_trait(&self) -> bool; + fn dimensions_trait(&self) -> Dimensions; + fn boundary_dimensions_trait(&self) -> Dimensions; +} + +impl HasDimensionsTrait for G +where + G: GeometryTraitExt, +{ + fn is_empty_trait(&self) -> bool { + if self.is_collection() { + if self.num_geometries_ext() == 0 { + true + } else { + self.geometries_ext() + .all(|g_inner| g_inner.borrow().is_empty_trait()) + } + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.is_empty_trait(), + GeometryTypeExt::Line(g) => g.is_empty_trait(), + GeometryTypeExt::LineString(g) => g.is_empty_trait(), + GeometryTypeExt::Polygon(g) => g.is_empty_trait(), + GeometryTypeExt::MultiPoint(g) => g.is_empty_trait(), + GeometryTypeExt::MultiLineString(g) => g.is_empty_trait(), + GeometryTypeExt::MultiPolygon(g) => g.is_empty_trait(), + GeometryTypeExt::Rect(g) => g.is_empty_trait(), + GeometryTypeExt::Triangle(g) => g.is_empty_trait(), + } + } + } + + fn dimensions_trait(&self) -> Dimensions { + if self.is_collection() { + let mut max = Dimensions::Empty; + for geom in self.geometries_ext() { + let dimensions = geom.borrow().dimensions_trait(); + if dimensions == Dimensions::TwoDimensional { + // short-circuit since we know none can be larger + return Dimensions::TwoDimensional; + } + max = max.max(dimensions); + } + max + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.dimensions_trait(), + GeometryTypeExt::Line(g) => g.dimensions_trait(), + GeometryTypeExt::LineString(g) => g.dimensions_trait(), + GeometryTypeExt::Polygon(g) => g.dimensions_trait(), + GeometryTypeExt::MultiPoint(g) => g.dimensions_trait(), + GeometryTypeExt::MultiLineString(g) => g.dimensions_trait(), + GeometryTypeExt::MultiPolygon(g) => g.dimensions_trait(), + GeometryTypeExt::Rect(g) => g.dimensions_trait(), + GeometryTypeExt::Triangle(g) => g.dimensions_trait(), + } + } + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + if self.is_collection() { + let mut max = Dimensions::Empty; + for geom in self.geometries_ext() { + let d = geom.borrow().boundary_dimensions_trait(); + + if d == Dimensions::OneDimensional { + return Dimensions::OneDimensional; + } + + max = max.max(d); + } + max + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::Line(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::LineString(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::Polygon(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::MultiPoint(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::MultiLineString(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::MultiPolygon(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::Rect(g) => g.boundary_dimensions_trait(), + GeometryTypeExt::Triangle(g) => g.boundary_dimensions_trait(), + } + } + } +} + +impl HasDimensionsTrait for P +where + P: PointTraitExt, +{ + fn is_empty_trait(&self) -> bool { + false + } + + fn dimensions_trait(&self) -> Dimensions { + Dimensions::ZeroDimensional + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + Dimensions::Empty + } +} + +impl HasDimensionsTrait for L +where + L: LineTraitExt, +{ + fn is_empty_trait(&self) -> bool { + false + } + + fn dimensions_trait(&self) -> Dimensions { + if self.start_coord() == self.end_coord() { + // degenerate line is a point + Dimensions::ZeroDimensional + } else { + Dimensions::OneDimensional + } + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + if self.start_coord() == self.end_coord() { + // degenerate line is a point, which has no boundary + Dimensions::Empty + } else { + Dimensions::ZeroDimensional + } + } +} + +impl HasDimensionsTrait for LS +where + LS: LineStringTraitExt, +{ + fn is_empty_trait(&self) -> bool { + self.num_coords() == 0 + } + + fn dimensions_trait(&self) -> Dimensions { + if self.num_coords() == 0 { + return Dimensions::Empty; + } + + // There should be at least 1 coordinate since num_coords is not 0. + let first = unsafe { self.geo_coord_unchecked(0) }; + if self.coord_iter().any(|coord| first != coord) { + Dimensions::OneDimensional + } else { + // all coords are the same - i.e. a point + Dimensions::ZeroDimensional + } + } + + /// ``` + /// use geo_types::line_string; + /// use sedona_geo_generic_alg::dimensions::{HasDimensions, Dimensions}; + /// + /// let ls = line_string![(x: 0., y: 0.), (x: 0., y: 1.), (x: 1., y: 1.)]; + /// assert_eq!(Dimensions::ZeroDimensional, ls.boundary_dimensions()); + /// + /// let ls = line_string![(x: 0., y: 0.), (x: 0., y: 1.), (x: 1., y: 1.), (x: 0., y: 0.)]; + /// assert_eq!(Dimensions::Empty, ls.boundary_dimensions()); + ///``` + fn boundary_dimensions_trait(&self) -> Dimensions { + if self.is_closed() { + return Dimensions::Empty; + } + + match self.dimensions_trait() { + Dimensions::Empty | Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => unreachable!("line_string cannot be 2 dimensional"), + } + } +} + +impl HasDimensionsTrait for P +where + P: PolygonTraitExt, +{ + fn is_empty_trait(&self) -> bool { + self.exterior_ext() + .is_none_or(|exterior| exterior.is_empty_trait()) + } + + fn dimensions_trait(&self) -> Dimensions { + if let Some(exterior) = self.exterior_ext() { + let mut coords = exterior.coord_iter(); + + let Some(first) = coords.next() else { + // No coordinates - the polygon is empty + return Dimensions::Empty; + }; + + let Some(second) = coords.find(|next| *next != first) else { + // All coordinates in the polygon are the same point + return Dimensions::ZeroDimensional; + }; + + let Some(_third) = coords.find(|next| *next != first && *next != second) else { + // There are only two distinct coordinates in the Polygon - it's collapsed to a line + return Dimensions::OneDimensional; + }; + + Dimensions::TwoDimensional + } else { + Dimensions::Empty + } + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + match self.dimensions_trait() { + Dimensions::Empty | Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => Dimensions::OneDimensional, + } + } +} + +impl HasDimensionsTrait for MP +where + MP: MultiPointTraitExt, +{ + fn is_empty_trait(&self) -> bool { + self.num_points() == 0 + } + + fn dimensions_trait(&self) -> Dimensions { + if self.num_points() == 0 { + return Dimensions::Empty; + } + + Dimensions::ZeroDimensional + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + Dimensions::Empty + } +} + +impl HasDimensionsTrait for MLS +where + MLS: MultiLineStringTraitExt, +{ + fn is_empty_trait(&self) -> bool { + self.line_strings_ext().all(|ls| ls.is_empty_trait()) + } + + fn dimensions_trait(&self) -> Dimensions { + let mut max = Dimensions::Empty; + for line in self.line_strings_ext() { + match line.dimensions_trait() { + Dimensions::Empty => {} + Dimensions::ZeroDimensional => max = Dimensions::ZeroDimensional, + Dimensions::OneDimensional => { + // return early since we know multi line string dimensionality cannot exceed + // 1-d + return Dimensions::OneDimensional; + } + Dimensions::TwoDimensional => unreachable!("MultiLineString cannot be 2d"), + } + } + max + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + if self.is_closed() { + return Dimensions::Empty; + } + + match self.dimensions_trait() { + Dimensions::Empty | Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => unreachable!("line_string cannot be 2 dimensional"), + } + } +} + +impl HasDimensionsTrait for MP +where + MP: MultiPolygonTraitExt, +{ + fn is_empty_trait(&self) -> bool { + self.polygons_ext().all(|p| p.is_empty_trait()) + } + + fn dimensions_trait(&self) -> Dimensions { + let mut max = Dimensions::Empty; + for geom in self.polygons_ext() { + let dimensions = geom.dimensions_trait(); + if dimensions == Dimensions::TwoDimensional { + // short-circuit since we know none can be larger + return Dimensions::TwoDimensional; + } + max = max.max(dimensions) + } + max + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + match self.dimensions_trait() { + Dimensions::Empty | Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => Dimensions::OneDimensional, + } + } +} + +impl HasDimensionsTrait for GC +where + GC: GeometryCollectionTraitExt, +{ + fn is_empty_trait(&self) -> bool { + if self.num_geometries() == 0 { + true + } else { + self.geometries_ext().all(|g| g.is_empty_trait()) + } + } + + fn dimensions_trait(&self) -> Dimensions { + let mut max = Dimensions::Empty; + for geom in self.geometries_ext() { + let dimensions = geom.dimensions_trait(); + if dimensions == Dimensions::TwoDimensional { + // short-circuit since we know none can be larger + return Dimensions::TwoDimensional; + } + max = max.max(dimensions); + } + max + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + let mut max = Dimensions::Empty; + for geom in self.geometries_ext() { + let d = geom.boundary_dimensions_trait(); + + if d == Dimensions::OneDimensional { + return Dimensions::OneDimensional; + } + + max = max.max(d); + } + max + } +} + +impl HasDimensionsTrait for R +where + R: RectTraitExt, +{ + fn is_empty_trait(&self) -> bool { + false + } + + fn dimensions_trait(&self) -> Dimensions { + if self.min_coord() == self.max_coord() { + // degenerate rectangle is a point + Dimensions::ZeroDimensional + } else if self.min_coord().x == self.max_coord().x + || self.min_coord().y == self.max_coord().y + { + // degenerate rectangle is a line + Dimensions::OneDimensional + } else { + Dimensions::TwoDimensional + } + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + match self.dimensions_trait() { + Dimensions::Empty => { + unreachable!("even a degenerate rect should be at least 0-Dimensional") + } + Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => Dimensions::OneDimensional, + } + } +} + +impl HasDimensionsTrait for T +where + T: TriangleTraitExt, +{ + fn is_empty_trait(&self) -> bool { + false + } + + fn dimensions_trait(&self) -> Dimensions { + use crate::Kernel; + let (c0, c1, c2) = (self.first_coord(), self.second_coord(), self.third_coord()); + if Collinear == C::Ker::orient2d(c0, c1, c2) { + if c0 == c1 && c1 == c2 { + // degenerate triangle is a point + Dimensions::ZeroDimensional + } else { + // degenerate triangle is a line + Dimensions::OneDimensional + } + } else { + Dimensions::TwoDimensional + } + } + + fn boundary_dimensions_trait(&self) -> Dimensions { + match self.dimensions_trait() { + Dimensions::Empty => { + unreachable!("even a degenerate triangle should be at least 0-dimensional") + } + Dimensions::ZeroDimensional => Dimensions::Empty, + Dimensions::OneDimensional => Dimensions::ZeroDimensional, + Dimensions::TwoDimensional => Dimensions::OneDimensional, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use geo_types::*; + + const ONE: Coord = crate::coord!(x: 1.0, y: 1.0); + use crate::wkt; + + #[test] + fn point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(POINT(1.0 1.0)).dimensions_trait() + ); + } + + #[test] + fn line_string() { + assert_eq!( + Dimensions::OneDimensional, + wkt!(LINESTRING(1.0 1.0,2.0 2.0,3.0 3.0)).dimensions_trait() + ); + } + + #[test] + fn polygon() { + assert_eq!( + Dimensions::TwoDimensional, + wkt!(POLYGON((1.0 1.0,2.0 2.0,3.0 3.0,1.0 1.0))).dimensions_trait() + ); + } + + #[test] + fn multi_point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTIPOINT(1.0 1.0)).dimensions_trait() + ); + } + + #[test] + fn multi_line_string() { + assert_eq!( + Dimensions::OneDimensional, + wkt!(MULTILINESTRING((1.0 1.0,2.0 2.0,3.0 3.0))).dimensions_trait() + ); + } + + #[test] + fn multi_polygon() { + assert_eq!( + Dimensions::TwoDimensional, + wkt!(MULTIPOLYGON(((1.0 1.0,2.0 2.0,3.0 3.0,1.0 1.0)))).dimensions_trait() + ); + } + + mod empty { + use super::*; + #[test] + fn empty_line_string() { + assert_eq!( + Dimensions::Empty, + (wkt!(LINESTRING EMPTY) as LineString).dimensions_trait() + ); + } + + #[test] + fn empty_polygon() { + assert_eq!( + Dimensions::Empty, + (wkt!(POLYGON EMPTY) as Polygon).dimensions_trait() + ); + } + + #[test] + fn empty_multi_point() { + assert_eq!( + Dimensions::Empty, + (wkt!(MULTIPOINT EMPTY) as MultiPoint).dimensions_trait() + ); + } + + #[test] + fn empty_multi_line_string() { + assert_eq!( + Dimensions::Empty, + (wkt!(MULTILINESTRING EMPTY) as MultiLineString).dimensions_trait() + ); + } + + #[test] + fn multi_line_string_with_empty_line_string() { + let empty_line_string = wkt!(LINESTRING EMPTY) as LineString; + let multi_line_string = MultiLineString::new(vec![empty_line_string]); + assert_eq!(Dimensions::Empty, multi_line_string.dimensions_trait()); + } + + #[test] + fn empty_multi_polygon() { + assert_eq!( + Dimensions::Empty, + (wkt!(MULTIPOLYGON EMPTY) as MultiPolygon).dimensions_trait() + ); + } + + #[test] + fn multi_polygon_with_empty_polygon() { + let empty_polygon = (wkt!(POLYGON EMPTY) as Polygon); + let multi_polygon = MultiPolygon::new(vec![empty_polygon]); + assert_eq!(Dimensions::Empty, multi_polygon.dimensions_trait()); + } + } + + mod dimensional_collapse { + use super::*; + + #[test] + fn line_collapsed_to_point() { + assert_eq!( + Dimensions::ZeroDimensional, + Line::new(ONE, ONE).dimensions_trait() + ); + } + + #[test] + fn line_string_collapsed_to_point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(LINESTRING(1.0 1.0)).dimensions_trait() + ); + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(LINESTRING(1.0 1.0,1.0 1.0)).dimensions_trait() + ); + } + + #[test] + fn polygon_collapsed_to_point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(POLYGON((1.0 1.0))).dimensions_trait() + ); + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(POLYGON((1.0 1.0,1.0 1.0))).dimensions_trait() + ); + } + + #[test] + fn polygon_collapsed_to_line() { + assert_eq!( + Dimensions::OneDimensional, + wkt!(POLYGON((1.0 1.0,2.0 2.0))).dimensions_trait() + ); + } + + #[test] + fn multi_line_string_with_line_string_collapsed_to_point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTILINESTRING((1.0 1.0))).dimensions_trait() + ); + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTILINESTRING((1.0 1.0,1.0 1.0))).dimensions_trait() + ); + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTILINESTRING((1.0 1.0),(1.0 1.0))).dimensions_trait() + ); + } + + #[test] + fn multi_polygon_with_polygon_collapsed_to_point() { + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTIPOLYGON(((1.0 1.0)))).dimensions_trait() + ); + assert_eq!( + Dimensions::ZeroDimensional, + wkt!(MULTIPOLYGON(((1.0 1.0,1.0 1.0)))).dimensions_trait() + ); + } + + #[test] + fn multi_polygon_with_polygon_collapsed_to_line() { + assert_eq!( + Dimensions::OneDimensional, + wkt!(MULTIPOLYGON(((1.0 1.0,2.0 2.0)))).dimensions_trait() + ); + } + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/euclidean_length.rs b/rust/sedona-geo-generic-alg/src/algorithm/euclidean_length.rs new file mode 100644 index 00000000..3acb3b9d --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/euclidean_length.rs @@ -0,0 +1,571 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Generic Euclidean Length algorithm +//! +//! Ported (and contains copied code) from `geo::algorithm::euclidean_length`: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +use core::borrow::Borrow; +use std::iter::Sum; + +use crate::CoordFloat; +use sedona_geo_traits_ext::*; + +/// Calculation of the length +#[deprecated( + since = "0.29.0", + note = "Please use the `Euclidean.length(&line)` via the `Length` trait instead." +)] +pub trait EuclideanLength { + /// Calculation of the length of a Line + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::EuclideanLength; + /// use sedona_geo_generic_alg::line_string; + /// + /// let line_string = line_string![ + /// (x: 40.02f64, y: 116.34), + /// (x: 42.02f64, y: 116.34), + /// ]; + /// + /// assert_eq!( + /// 2., + /// line_string.euclidean_length(), + /// ) + /// ``` + fn euclidean_length(&self) -> T; +} + +#[allow(deprecated)] +impl EuclideanLength for G +where + T: CoordFloat + Sum, + G: GeoTraitExtWithTypeTag + EuclideanLengthTrait, +{ + fn euclidean_length(&self) -> T { + self.euclidean_length_trait() + } +} + +trait EuclideanLengthTrait +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T; +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for L +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + let start_coord = self.start_coord(); + let end_coord = self.end_coord(); + let delta = start_coord - end_coord; + delta.x.hypot(delta.y) + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for LS +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + let mut length = T::zero(); + for line in self.lines() { + let start_coord = line.start_coord(); + let end_coord = line.end_coord(); + let delta = start_coord - end_coord; + length = length + delta.x.hypot(delta.y); + } + length + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for MLS +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + let mut length = T::zero(); + for line_string in self.line_strings_ext() { + length = length + line_string.euclidean_length_trait(); + } + length + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for P +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Length is a 1D concept, doesn't apply to 2D polygons + // Return zero, similar to how Area returns zero for linear geometries + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for P +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // A point has no length dimension + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for MP +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Points have no length dimension + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for MPG +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Length is a 1D concept, doesn't apply to 2D polygons + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for R +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Length is a 1D concept, doesn't apply to 2D rectangles + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for TR +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Length is a 1D concept, doesn't apply to 2D triangles + T::zero() + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for GC +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + // Sum the lengths of all geometries in the collection + // Linear geometries (lines, linestrings) will contribute their actual length + // Non-linear geometries (points, polygons) will contribute zero + self.geometries_ext() + .map(|g| g.euclidean_length_trait()) + .fold(T::zero(), |acc, next| acc + next) + } +} + +#[allow(deprecated)] +impl> EuclideanLengthTrait for G +where + T: CoordFloat + Sum, +{ + fn euclidean_length_trait(&self) -> T { + if self.is_collection() { + self.geometries_ext() + .map(|g_inner| g_inner.borrow().euclidean_length_trait()) + .fold(T::zero(), |acc, next| acc + next) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(_) => T::zero(), + GeometryTypeExt::Line(line) => line.euclidean_length_trait(), + GeometryTypeExt::LineString(ls) => ls.euclidean_length_trait(), + GeometryTypeExt::Polygon(_) => T::zero(), + GeometryTypeExt::MultiPoint(_) => T::zero(), + GeometryTypeExt::MultiLineString(mls) => mls.euclidean_length_trait(), + GeometryTypeExt::MultiPolygon(_) => T::zero(), + GeometryTypeExt::Rect(_) => T::zero(), + GeometryTypeExt::Triangle(_) => T::zero(), + } + } + } +} + +#[cfg(test)] +mod test { + use crate::line_string; + #[allow(deprecated)] + use crate::EuclideanLength; + use crate::{coord, Line, MultiLineString}; + + #[allow(deprecated)] + #[test] + fn empty_linestring_test() { + let linestring = line_string![]; + assert_relative_eq!(0.0_f64, linestring.euclidean_length()); + } + #[allow(deprecated)] + #[test] + fn linestring_one_point_test() { + let linestring = line_string![(x: 0., y: 0.)]; + assert_relative_eq!(0.0_f64, linestring.euclidean_length()); + } + #[allow(deprecated)] + #[test] + fn linestring_test() { + let linestring = line_string![ + (x: 1., y: 1.), + (x: 7., y: 1.), + (x: 8., y: 1.), + (x: 9., y: 1.), + (x: 10., y: 1.), + (x: 11., y: 1.) + ]; + assert_relative_eq!(10.0_f64, linestring.euclidean_length()); + } + #[allow(deprecated)] + #[test] + fn multilinestring_test() { + let mline = MultiLineString::new(vec![ + line_string![ + (x: 1., y: 0.), + (x: 7., y: 0.), + (x: 8., y: 0.), + (x: 9., y: 0.), + (x: 10., y: 0.), + (x: 11., y: 0.) + ], + line_string![ + (x: 0., y: 0.), + (x: 0., y: 5.) + ], + ]); + assert_relative_eq!(15.0_f64, mline.euclidean_length()); + } + #[allow(deprecated)] + #[test] + fn line_test() { + let line0 = Line::new(coord! { x: 0., y: 0. }, coord! { x: 0., y: 1. }); + let line1 = Line::new(coord! { x: 0., y: 0. }, coord! { x: 3., y: 4. }); + assert_relative_eq!(line0.euclidean_length(), 1.); + assert_relative_eq!(line1.euclidean_length(), 5.); + } + + #[allow(deprecated)] + #[test] + fn polygon_returns_zero_test() { + use crate::{polygon, Polygon}; + let polygon: Polygon = polygon![ + (x: 0., y: 0.), + (x: 4., y: 0.), + (x: 4., y: 4.), + (x: 0., y: 4.), + (x: 0., y: 0.), + ]; + // Length doesn't apply to 2D polygons, should return zero + assert_relative_eq!(polygon.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn point_returns_zero_test() { + use crate::Point; + let point = Point::new(3.0, 4.0); + // Points have no length dimension + assert_relative_eq!(point.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn comprehensive_test_scenarios() { + use crate::{line_string, polygon}; + use crate::{ + Geometry, GeometryCollection, MultiLineString, MultiPoint, MultiPolygon, Point, + }; + + // Test cases matching the Python pytest scenarios + + // POINT EMPTY - represented as Point with NaN coordinates + // Note: In Rust we can't easily create "empty" points, so we test regular point + + // LINESTRING EMPTY + let empty_linestring: crate::LineString = line_string![]; + assert_relative_eq!(empty_linestring.euclidean_length(), 0.0); + + // POINT (0 0) + let point = Point::new(0.0, 0.0); + assert_relative_eq!(point.euclidean_length(), 0.0); + + // LINESTRING (0 0, 0 1) - length should be 1 + let linestring = line_string![(x: 0., y: 0.), (x: 0., y: 1.)]; + assert_relative_eq!(linestring.euclidean_length(), 1.0); + + // MULTIPOINT ((0 0), (1 1)) - should be 0 + let multipoint = MultiPoint::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]); + assert_relative_eq!(multipoint.euclidean_length(), 0.0); + + // MULTILINESTRING ((0 0, 1 1), (1 1, 2 2)) - should be ~2.828427 + // Distance from (0,0) to (1,1) = sqrt(2) ≈ 1.4142135623730951 + // Distance from (1,1) to (2,2) = sqrt(2) ≈ 1.4142135623730951 + // Total ≈ 2.8284271247461903 + let multilinestring = MultiLineString::new(vec![ + line_string![(x: 0., y: 0.), (x: 1., y: 1.)], + line_string![(x: 1., y: 1.), (x: 2., y: 2.)], + ]); + assert_relative_eq!( + multilinestring.euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) - should be 0 (perimeter not included) + let polygon = polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]; + assert_relative_eq!(polygon.euclidean_length(), 0.0); + + // MULTIPOLYGON - should be 0 + let multipolygon = MultiPolygon::new(vec![ + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + ]); + assert_relative_eq!(multipolygon.euclidean_length(), 0.0); + + // GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), POLYGON (...), LINESTRING (0 0, 1 1)) + // Should sum only the linestrings: 2 * sqrt(2) ≈ 2.8284271247461903 + let collection = GeometryCollection::new_from(vec![ + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // contributes 0 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) + ]); + // Now correctly sums only the linear geometries: 2 * sqrt(2) ≈ 2.8284271247461903 + // The polygon contributes 0 to the total + assert_relative_eq!( + collection.euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + assert_relative_eq!( + Geometry::GeometryCollection(collection).euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + } + + // Individual test functions matching pytest parametrized scenarios + + #[allow(deprecated)] + #[test] + fn test_point_empty() { + use crate::Point; + // POINT EMPTY -> 0 (represented as empty coordinates or NaN in Rust context) + let point = Point::new(f64::NAN, f64::NAN); + // NaN coordinates still result in zero length for points + assert_relative_eq!(point.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_linestring_empty() { + // LINESTRING EMPTY -> 0 + let empty_linestring: crate::LineString = line_string![]; + assert_relative_eq!(empty_linestring.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_point_0_0() { + use crate::Point; + // POINT (0 0) -> 0 + let point = Point::new(0.0, 0.0); + assert_relative_eq!(point.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_linestring_0_0_to_0_1() { + // LINESTRING (0 0, 0 1) -> 1 + let linestring = line_string![(x: 0., y: 0.), (x: 0., y: 1.)]; + assert_relative_eq!(linestring.euclidean_length(), 1.0); + } + + #[allow(deprecated)] + #[test] + fn test_multipoint() { + // MULTIPOINT ((0 0), (1 1)) -> 0 + use crate::{MultiPoint, Point}; + let multipoint = MultiPoint::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]); + assert_relative_eq!(multipoint.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_multilinestring_diagonal() { + // MULTILINESTRING ((0 0, 1 1), (1 1, 2 2)) -> 2.8284271247461903 + use crate::MultiLineString; + let multilinestring = MultiLineString::new(vec![ + line_string![(x: 0., y: 0.), (x: 1., y: 1.)], // sqrt(2) + line_string![(x: 1., y: 1.), (x: 2., y: 2.)], // sqrt(2) + ]); + assert_relative_eq!( + multilinestring.euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + } + + #[allow(deprecated)] + #[test] + fn test_polygon_unit_square() { + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) -> 0 (perimeters aren't included) + use crate::polygon; + let polygon = polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]; + assert_relative_eq!(polygon.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_multipolygon_double_unit_squares() { + // MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0))) -> 0 + use crate::{polygon, MultiPolygon}; + let multipolygon = MultiPolygon::new(vec![ + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + ]); + assert_relative_eq!(multipolygon.euclidean_length(), 0.0); + } + + #[allow(deprecated)] + #[test] + fn test_geometrycollection_mixed() { + // GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1)) + // Expected: 2.8284271247461903 (only linestrings contribute) + use crate::{polygon, Geometry, GeometryCollection}; + let collection = GeometryCollection::new_from(vec![ + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) ≈ 1.4142135623730951 + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // contributes 0 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) ≈ 1.4142135623730951 + ]); + // Now correctly returns the expected sum of only the linear geometries + // Expected: 2.8284271247461903 (sum of the two linestring lengths, polygon contributes 0) + assert_relative_eq!( + collection.euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + + // For now, let's test that individual geometries work correctly + let linestring1 = line_string![(x: 0., y: 0.), (x: 1., y: 1.)]; + let linestring2 = line_string![(x: 0., y: 0.), (x: 1., y: 1.)]; + let expected_total = linestring1.euclidean_length() + linestring2.euclidean_length(); + assert_relative_eq!(expected_total, 2.8284271247461903, epsilon = 1e-10); + } + + #[allow(deprecated)] + #[test] + fn test_geometrycollection_pytest_exact_scenario() { + // Exact match for the Python pytest scenario: + // GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1)) + // Expected: 2.8284271247461903 + use crate::{polygon, Geometry, GeometryCollection}; + + let collection = GeometryCollection::new_from(vec![ + // LINESTRING (0 0, 1 1) - length = sqrt(2) ≈ 1.4142135623730951 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) - contributes 0 (perimeter not included) + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), + // LINESTRING (0 0, 1 1) - length = sqrt(2) ≈ 1.4142135623730951 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), + ]); + + // Total length = sqrt(2) + 0 + sqrt(2) = 2 * sqrt(2) ≈ 2.8284271247461903 + assert_relative_eq!( + collection.euclidean_length(), + 2.8284271247461903, + epsilon = 1e-10 + ); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/collections.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/collections.rs new file mode 100644 index 00000000..f0e5956c --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/collections.rs @@ -0,0 +1,165 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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::*; + +use super::has_disjoint_bboxes; +use super::IntersectsTrait; +use crate::GeoNum; + +macro_rules! impl_intersects_geometry { + ($rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: GeoNum, + LHS: GeometryTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + if self.is_collection() { + self.geometries_ext() + .any(|lhs_inner| lhs_inner.borrow().intersects_trait(rhs)) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(g) => g.intersects_trait(rhs), + GeometryTypeExt::Line(g) => g.intersects_trait(rhs), + GeometryTypeExt::LineString(g) => g.intersects_trait(rhs), + GeometryTypeExt::Polygon(g) => g.intersects_trait(rhs), + GeometryTypeExt::MultiPoint(g) => g.intersects_trait(rhs), + GeometryTypeExt::MultiLineString(g) => g.intersects_trait(rhs), + GeometryTypeExt::MultiPolygon(g) => g.intersects_trait(rhs), + GeometryTypeExt::Rect(g) => g.intersects_trait(rhs), + GeometryTypeExt::Triangle(g) => g.intersects_trait(rhs), + } + } + } + } + }; +} + +impl_intersects_geometry!(CoordTraitExt, CoordTag); +impl_intersects_geometry!(PointTraitExt, PointTag); +impl_intersects_geometry!(LineStringTraitExt, LineStringTag); +impl_intersects_geometry!(PolygonTraitExt, PolygonTag); +impl_intersects_geometry!(MultiPointTraitExt, MultiPointTag); +impl_intersects_geometry!(MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_geometry!(MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_geometry!(GeometryTraitExt, GeometryTag); +impl_intersects_geometry!(GeometryCollectionTraitExt, GeometryCollectionTag); +impl_intersects_geometry!(LineTraitExt, LineTag); +impl_intersects_geometry!(RectTraitExt, RectTag); +impl_intersects_geometry!(TriangleTraitExt, TriangleTag); + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + GeometryTraitExt, + GeometryTag +); +symmetric_intersects_trait_impl!(GeoNum, LineTraitExt, LineTag, GeometryTraitExt, GeometryTag); +symmetric_intersects_trait_impl!(GeoNum, RectTraitExt, RectTag, GeometryTraitExt, GeometryTag); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + GeometryTraitExt, + GeometryTag +); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + GeometryTraitExt, + GeometryTag +); + +// Generate implementations for GeometryCollection by delegating to the Geometry implementation +macro_rules! impl_intersects_geometry_collection_from_geometry { + ($rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: GeoNum, + LHS: GeometryCollectionTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + if has_disjoint_bboxes(self, rhs) { + return false; + } + self.geometries_ext().any(|geom| geom.intersects_trait(rhs)) + } + } + }; +} + +impl_intersects_geometry_collection_from_geometry!(CoordTraitExt, CoordTag); +impl_intersects_geometry_collection_from_geometry!(PointTraitExt, PointTag); +impl_intersects_geometry_collection_from_geometry!(LineStringTraitExt, LineStringTag); +impl_intersects_geometry_collection_from_geometry!(PolygonTraitExt, PolygonTag); +impl_intersects_geometry_collection_from_geometry!(MultiPointTraitExt, MultiPointTag); +impl_intersects_geometry_collection_from_geometry!(MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_geometry_collection_from_geometry!(MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_geometry_collection_from_geometry!(GeometryTraitExt, GeometryTag); +impl_intersects_geometry_collection_from_geometry!( + GeometryCollectionTraitExt, + GeometryCollectionTag +); +impl_intersects_geometry_collection_from_geometry!(LineTraitExt, LineTag); +impl_intersects_geometry_collection_from_geometry!(RectTraitExt, RectTag); +impl_intersects_geometry_collection_from_geometry!(TriangleTraitExt, TriangleTag); + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_intersects_trait_impl!( + GeoNum, + LineTraitExt, + LineTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_intersects_trait_impl!( + GeoNum, + RectTraitExt, + RectTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/coordinate.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/coordinate.rs new file mode 100644 index 00000000..31b3585e --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/coordinate.rs @@ -0,0 +1,48 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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}; + +use super::IntersectsTrait; +use crate::*; + +impl IntersectsTrait for LHS +where + T: CoordNum, + LHS: CoordTraitExt, + RHS: CoordTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.geo_coord() == rhs.geo_coord() + } +} + +// The other side of this is handled via a blanket impl. +impl IntersectsTrait for LHS +where + T: CoordNum, + LHS: CoordTraitExt, + RHS: PointTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + rhs.geo_coord().is_some_and(|c| self.geo_coord() == c) + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/line.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/line.rs new file mode 100644 index 00000000..025855b5 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/line.rs @@ -0,0 +1,120 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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::*; + +use super::{point_in_rect, IntersectsTrait}; +use crate::*; + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: LineTraitExt, + RHS: CoordTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + let start = self.start_coord(); + let end = self.end_coord(); + let rhs = rhs.geo_coord(); + + // First we check if the point is collinear with the line. + T::Ker::orient2d(start, end, rhs) == Orientation::Collinear + // In addition, the point must have _both_ coordinates + // within the start and end bounds. + && point_in_rect(rhs, start, end) + } +} + +symmetric_intersects_trait_impl!(GeoNum, CoordTraitExt, CoordTag, LineTraitExt, LineTag); +symmetric_intersects_trait_impl!(GeoNum, LineTraitExt, LineTag, PointTraitExt, PointTag); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: LineTraitExt, + RHS: LineTraitExt, +{ + fn intersects_trait(&self, line: &RHS) -> bool { + let start_ext = self.start_ext(); + let self_start = self.start_coord(); + let self_end = self.end_coord(); + let line_start = line.start_coord(); + let line_end = line.end_coord(); + + // Special case: self is equiv. to a point. + if self_start == self_end { + return line.intersects_trait(&start_ext); + } + + // Precondition: start and end are distinct. + + // Check if orientation of rhs.{start,end} are different + // with respect to self.{start,end}. + let check_1_1 = T::Ker::orient2d(self_start, self_end, line_start); + let check_1_2 = T::Ker::orient2d(self_start, self_end, line_end); + + if check_1_1 != check_1_2 { + // Since the checks are different, + // rhs.{start,end} are distinct, and rhs is not + // collinear with self. Thus, there is exactly + // one point on the infinite extensions of rhs, + // that is collinear with self. + + // By continuity, this point is not on the + // exterior of rhs. Now, check the same with + // self, rhs swapped. + + let check_2_1 = T::Ker::orient2d(line_start, line_end, self_start); + let check_2_2 = T::Ker::orient2d(line_start, line_end, self_end); + + // By similar argument, there is (exactly) one + // point on self, collinear with rhs. Thus, + // those two have to be same, and lies (interior + // or boundary, but not exterior) on both lines. + check_2_1 != check_2_2 + } else if check_1_1 == Orientation::Collinear { + // Special case: collinear line segments. + + // Equivalent to 4 point-line intersection + // checks, but removes the calls to the kernel + // predicates. + point_in_rect(line_start, self_start, self_end) + || point_in_rect(line_end, self_start, self_end) + || point_in_rect(self_end, line_start, line_end) + || point_in_rect(self_end, line_start, line_end) + } else { + false + } + } +} + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: LineTraitExt, + RHS: TriangleTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.intersects_trait(&rhs.to_polygon()) + } +} + +symmetric_intersects_trait_impl!(GeoNum, TriangleTraitExt, TriangleTag, LineTraitExt, LineTag); 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 new file mode 100644 index 00000000..74fddcbe --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/line_string.rs @@ -0,0 +1,150 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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::*; + +use super::{has_disjoint_bboxes, IntersectsTrait}; +use crate::*; + +// Generate implementations for LineString by delegating to Line +macro_rules! impl_intersects_line_string_from_line { + ($rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: GeoNum, + LHS: LineStringTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + if has_disjoint_bboxes(self, rhs) { + return false; + } + self.lines().any(|l| l.intersects_trait(rhs)) + } + } + }; +} + +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); + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + LineStringTraitExt, + LineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + LineTraitExt, + LineTag, + LineStringTraitExt, + LineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + RectTraitExt, + RectTag, + LineStringTraitExt, + LineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + LineStringTraitExt, + LineStringTag +); + +// Generate implementations for MultiLineString by delegating to LineString +macro_rules! impl_intersects_multi_line_string_from_line_string { + ($rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: GeoNum, + LHS: MultiLineStringTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + if has_disjoint_bboxes(self, rhs) { + return false; + } + self.line_strings_ext().any(|ls| ls.intersects_trait(rhs)) + } + } + }; +} + +impl_intersects_multi_line_string_from_line_string!(CoordTraitExt, CoordTag); +impl_intersects_multi_line_string_from_line_string!(PointTraitExt, PointTag); +impl_intersects_multi_line_string_from_line_string!(LineStringTraitExt, LineStringTag); +impl_intersects_multi_line_string_from_line_string!(PolygonTraitExt, PolygonTag); +impl_intersects_multi_line_string_from_line_string!(MultiPointTraitExt, MultiPointTag); +impl_intersects_multi_line_string_from_line_string!(MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_multi_line_string_from_line_string!(MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_multi_line_string_from_line_string!(GeometryTraitExt, GeometryTag); +impl_intersects_multi_line_string_from_line_string!( + GeometryCollectionTraitExt, + GeometryCollectionTag +); +impl_intersects_multi_line_string_from_line_string!(LineTraitExt, LineTag); +impl_intersects_multi_line_string_from_line_string!(RectTraitExt, RectTag); +impl_intersects_multi_line_string_from_line_string!(TriangleTraitExt, TriangleTag); + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + MultiLineStringTraitExt, + MultiLineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + LineTraitExt, + LineTag, + MultiLineStringTraitExt, + MultiLineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + RectTraitExt, + RectTag, + MultiLineStringTraitExt, + MultiLineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + MultiLineStringTraitExt, + MultiLineStringTag +); diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/mod.rs new file mode 100644 index 00000000..a0b5df33 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/mod.rs @@ -0,0 +1,755 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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; + +use crate::BoundingRect; +use crate::*; + +/// Checks if the geometry Self intersects the geometry Rhs. +/// More formally, either boundary or interior of Self has +/// non-empty (set-theoretic) intersection with the boundary +/// or interior of Rhs. In other words, the [DE-9IM] +/// intersection matrix for (Self, Rhs) is _not_ `FF*FF****`. +/// +/// This predicate is symmetric: `a.intersects(b)` iff +/// `b.intersects(a)`. +/// +/// [DE-9IM]: https://en.wikipedia.org/wiki/DE-9IM +/// +/// # Examples +/// +/// ``` +/// use sedona_geo_generic_alg::Intersects; +/// use sedona_geo_generic_alg::line_string; +/// +/// let line_string_a = line_string![ +/// (x: 3., y: 2.), +/// (x: 7., y: 6.), +/// ]; +/// +/// let line_string_b = line_string![ +/// (x: 3., y: 4.), +/// (x: 8., y: 4.), +/// ]; +/// +/// let line_string_c = line_string![ +/// (x: 9., y: 2.), +/// (x: 11., y: 5.), +/// ]; +/// +/// assert!(line_string_a.intersects(&line_string_b)); +/// assert!(!line_string_a.intersects(&line_string_c)); +/// ``` +pub trait Intersects { + fn intersects(&self, rhs: &Rhs) -> bool; +} + +pub trait IntersectsTrait { + fn intersects_trait(&self, rhs: &Rhs) -> bool; +} + +impl Intersects for LHS +where + LHS: GeoTraitExtWithTypeTag, + RHS: GeoTraitExtWithTypeTag, + LHS: IntersectsTrait, +{ + fn intersects(&self, rhs: &RHS) -> bool { + self.intersects_trait(rhs) + } +} + +macro_rules! symmetric_intersects_trait_impl { + ($num_type:ident, $lhs_type:ident, $lhs_tag:ident, $rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait<$lhs_tag, $rhs_tag, RHS> for LHS + where + T: $num_type, + LHS: $lhs_type, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + rhs.intersects_trait(self) + } + } + }; +} + +mod collections; +mod coordinate; +mod line; +mod line_string; +mod point; +mod polygon; +mod rect; +mod triangle; + +// Helper function to check value lies between min and max. +// Only makes sense if min <= max (or always false) +#[inline] +fn value_in_range(value: T, min: T, max: T) -> bool +where + T: std::cmp::PartialOrd, +{ + value >= min && value <= max +} + +// Helper function to check value lies between two bounds, +// where the ordering of the bounds is not known +#[inline] +pub(crate) fn value_in_between(value: T, bound_1: T, bound_2: T) -> bool +where + T: std::cmp::PartialOrd, +{ + if bound_1 < bound_2 { + value_in_range(value, bound_1, bound_2) + } else { + value_in_range(value, bound_2, bound_1) + } +} + +// Helper function to check point lies inside rect given by +// bounds. The first bound need not be min. +#[inline] +pub(crate) fn point_in_rect(value: Coord, bound_1: Coord, bound_2: Coord) -> bool +where + T: CoordNum, +{ + value_in_between(value.x, bound_1.x, bound_2.x) + && value_in_between(value.y, bound_1.y, bound_2.y) +} + +// A cheap bbox check to see if we can skip the more expensive intersection computation +fn has_disjoint_bboxes(a: &A, b: &B) -> bool +where + T: CoordNum, + A: BoundingRect, + B: BoundingRect, +{ + let mut disjoint_bbox = false; + if let Some(a_bbox) = a.bounding_rect().into() { + if let Some(b_bbox) = b.bounding_rect().into() { + if !a_bbox.intersects(&b_bbox) { + disjoint_bbox = true; + } + } + } + disjoint_bbox +} + +#[cfg(test)] +mod test { + use geo_types::{Coord, GeometryCollection}; + + use crate::Intersects; + use crate::{ + coord, line_string, polygon, Geometry, Line, LineString, MultiLineString, MultiPoint, + MultiPolygon, Point, Polygon, Rect, + }; + + /// Tests: intersection LineString and LineString + #[test] + fn empty_linestring1_test() { + let linestring = line_string![(x: 3., y: 2.), (x: 7., y: 6.)]; + assert!(!line_string![].intersects(&linestring)); + } + #[test] + fn empty_linestring2_test() { + let linestring = line_string![(x: 3., y: 2.), (x: 7., y: 6.)]; + assert!(!linestring.intersects(&LineString::new(Vec::new()))); + } + #[test] + fn empty_all_linestring_test() { + let empty: LineString = line_string![]; + assert!(!empty.intersects(&empty)); + } + #[test] + fn intersect_linestring_test() { + let ls1 = line_string![(x: 3., y: 2.), (x: 7., y: 6.)]; + let ls2 = line_string![(x: 3., y: 4.), (x: 8., y: 4.)]; + assert!(ls1.intersects(&ls2)); + } + #[test] + fn parallel_linestrings_test() { + let ls1 = line_string![(x: 3., y: 2.), (x: 7., y: 6.)]; + let ls2 = line_string![(x: 3., y: 1.), (x: 7., y: 5.)]; + assert!(!ls1.intersects(&ls2)); + } + /// Tests: intersection LineString and Polygon + #[test] + fn linestring_in_polygon_test() { + let poly = polygon![ + (x: 0., y: 0.), + (x: 5., y: 0.), + (x: 5., y: 6.), + (x: 0., y: 6.), + (x: 0., y: 0.), + ]; + let ls = line_string![(x: 2., y: 2.), (x: 3., y: 3.)]; + assert!(poly.intersects(&ls)); + } + #[test] + fn linestring_on_boundary_polygon_test() { + let poly = Polygon::new( + LineString::from(vec![(0., 0.), (5., 0.), (5., 6.), (0., 6.), (0., 0.)]), + Vec::new(), + ); + assert!(poly.intersects(&LineString::from(vec![(0., 0.), (5., 0.)]))); + assert!(poly.intersects(&LineString::from(vec![(5., 0.), (5., 6.)]))); + assert!(poly.intersects(&LineString::from(vec![(5., 6.), (0., 6.)]))); + assert!(poly.intersects(&LineString::from(vec![(0., 6.), (0., 0.)]))); + } + #[test] + fn intersect_linestring_polygon_test() { + let poly = Polygon::new( + LineString::from(vec![(0., 0.), (5., 0.), (5., 6.), (0., 6.), (0., 0.)]), + Vec::new(), + ); + assert!(poly.intersects(&LineString::from(vec![(2., 2.), (6., 6.)]))); + } + #[test] + fn linestring_outside_polygon_test() { + let poly = Polygon::new( + LineString::from(vec![(0., 0.), (5., 0.), (5., 6.), (0., 6.), (0., 0.)]), + Vec::new(), + ); + assert!(!poly.intersects(&LineString::from(vec![(7., 2.), (9., 4.)]))); + } + #[test] + fn linestring_in_inner_polygon_test() { + let e = LineString::from(vec![(0., 0.), (5., 0.), (5., 6.), (0., 6.), (0., 0.)]); + let v = vec![LineString::from(vec![ + (1., 1.), + (4., 1.), + (4., 4.), + (1., 4.), + (1., 1.), + ])]; + let poly = Polygon::new(e, v); + assert!(!poly.intersects(&LineString::from(vec![(2., 2.), (3., 3.)]))); + assert!(poly.intersects(&LineString::from(vec![(2., 2.), (4., 4.)]))); + } + #[test] + fn linestring_traverse_polygon_test() { + let e = LineString::from(vec![(0., 0.), (5., 0.), (5., 6.), (0., 6.), (0., 0.)]); + let v = vec![LineString::from(vec![ + (1., 1.), + (4., 1.), + (4., 4.), + (1., 4.), + (1., 1.), + ])]; + let poly = Polygon::new(e, v); + assert!(poly.intersects(&LineString::from(vec![(2., 0.5), (2., 5.)]))); + } + #[test] + fn linestring_in_inner_with_2_inner_polygon_test() { + // (8,9) + // (2,8) | (14,8) + // ------------------------------------|------------------------------------------ + // | | | + // | (4,7) (6,7) | | + // | ------------------ | (11,7) | + // | | | | + // | (4,6) (7,6) | (9,6) | (12,6) | + // | ---------------------- | ----------------|--------- | + // | | | | | | | | + // | | (6,5) | | | | | | + // | | / | | | | | | + // | | / | | | | | | + // | | (5,4) | | | | | | + // | | | | | | | | + // | ---------------------- | ----------------|--------- | + // | (4,3) (7,3) | (9,3) | (12,3) | + // | | (11,2.5) | + // | | | + // ------------------------------------|------------------------------------------ + // (2,2) | (14,2) + // (8,1) + // + let e = LineString::from(vec![(2., 2.), (14., 2.), (14., 8.), (2., 8.), (2., 2.)]); + let v = vec![ + LineString::from(vec![(4., 3.), (7., 3.), (7., 6.), (4., 6.), (4., 3.)]), + LineString::from(vec![(9., 3.), (12., 3.), (12., 6.), (9., 6.), (9., 3.)]), + ]; + let poly = Polygon::new(e, v); + assert!(!poly.intersects(&LineString::from(vec![(5., 4.), (6., 5.)]))); + assert!(poly.intersects(&LineString::from(vec![(11., 2.5), (11., 7.)]))); + assert!(poly.intersects(&LineString::from(vec![(4., 7.), (6., 7.)]))); + assert!(poly.intersects(&LineString::from(vec![(8., 1.), (8., 9.)]))); + } + #[test] + fn polygons_do_not_intersect() { + let p1 = Polygon::new( + LineString::from(vec![(1., 3.), (3., 3.), (3., 5.), (1., 5.), (1., 3.)]), + Vec::new(), + ); + let p2 = Polygon::new( + LineString::from(vec![ + (10., 30.), + (30., 30.), + (30., 50.), + (10., 50.), + (10., 30.), + ]), + Vec::new(), + ); + + assert!(!p1.intersects(&p2)); + assert!(!p2.intersects(&p1)); + } + #[test] + fn polygons_overlap() { + let p1 = Polygon::new( + LineString::from(vec![(1., 3.), (3., 3.), (3., 5.), (1., 5.), (1., 3.)]), + Vec::new(), + ); + let p2 = Polygon::new( + LineString::from(vec![(2., 3.), (4., 3.), (4., 7.), (2., 7.), (2., 3.)]), + Vec::new(), + ); + + assert!(p1.intersects(&p2)); + assert!(p2.intersects(&p1)); + } + #[test] + fn polygon_contained() { + let p1 = Polygon::new( + LineString::from(vec![(1., 3.), (4., 3.), (4., 6.), (1., 6.), (1., 3.)]), + Vec::new(), + ); + let p2 = Polygon::new( + LineString::from(vec![(2., 4.), (3., 4.), (3., 5.), (2., 5.), (2., 4.)]), + Vec::new(), + ); + + assert!(p1.intersects(&p2)); + assert!(p2.intersects(&p1)); + } + #[test] + fn polygons_conincident() { + let p1 = Polygon::new( + LineString::from(vec![(1., 3.), (4., 3.), (4., 6.), (1., 6.), (1., 3.)]), + Vec::new(), + ); + let p2 = Polygon::new( + LineString::from(vec![(1., 3.), (4., 3.), (4., 6.), (1., 6.), (1., 3.)]), + Vec::new(), + ); + + assert!(p1.intersects(&p2)); + assert!(p2.intersects(&p1)); + } + #[test] + fn polygon_intersects_bounding_rect_test() { + // Polygon poly = + // + // (0,8) (12,8) + // ┌──────────────────────┐ + // │ (7,7) (11,7) │ + // │ ┌──────┐ │ + // │ │ │ │ + // │ │(hole)│ │ + // │ │ │ │ + // │ │ │ │ + // │ └──────┘ │ + // │ (7,4) (11,4) │ + // │ │ + // │ │ + // │ │ + // │ │ + // │ │ + // └──────────────────────┘ + // (0,0) (12,0) + let poly = Polygon::new( + LineString::from(vec![(0., 0.), (12., 0.), (12., 8.), (0., 8.), (0., 0.)]), + vec![LineString::from(vec![ + (7., 4.), + (11., 4.), + (11., 7.), + (7., 7.), + (7., 4.), + ])], + ); + let b1 = Rect::new(coord! { x: 11., y: 1. }, coord! { x: 13., y: 2. }); + let b2 = Rect::new(coord! { x: 2., y: 2. }, coord! { x: 8., y: 5. }); + let b3 = Rect::new(coord! { x: 8., y: 5. }, coord! { x: 10., y: 6. }); + let b4 = Rect::new(coord! { x: 1., y: 1. }, coord! { x: 3., y: 3. }); + // overlaps + assert!(poly.intersects(&b1)); + // contained in exterior, overlaps with hole + assert!(poly.intersects(&b2)); + // completely contained in the hole + assert!(!poly.intersects(&b3)); + // completely contained in the polygon + assert!(poly.intersects(&b4)); + // conversely, + assert!(b1.intersects(&poly)); + assert!(b2.intersects(&poly)); + assert!(!b3.intersects(&poly)); + assert!(b4.intersects(&poly)); + } + #[test] + fn bounding_rect_test() { + let bounding_rect_xl = + Rect::new(coord! { x: -100., y: -200. }, coord! { x: 100., y: 200. }); + let bounding_rect_sm = Rect::new(coord! { x: -10., y: -20. }, coord! { x: 10., y: 20. }); + let bounding_rect_s2 = Rect::new(coord! { x: 0., y: 0. }, coord! { x: 20., y: 30. }); + // confirmed using GEOS + assert!(bounding_rect_xl.intersects(&bounding_rect_sm)); + assert!(bounding_rect_sm.intersects(&bounding_rect_xl)); + assert!(bounding_rect_sm.intersects(&bounding_rect_s2)); + assert!(bounding_rect_s2.intersects(&bounding_rect_sm)); + } + #[test] + fn rect_intersection_consistent_with_poly_intersection_test() { + let bounding_rect_xl = + Rect::new(coord! { x: -100., y: -200. }, coord! { x: 100., y: 200. }); + let bounding_rect_sm = Rect::new(coord! { x: -10., y: -20. }, coord! { x: 10., y: 20. }); + let bounding_rect_s2 = Rect::new(coord! { x: 0., y: 0. }, coord! { x: 20., y: 30. }); + + assert!(bounding_rect_xl.to_polygon().intersects(&bounding_rect_sm)); + assert!(bounding_rect_xl.intersects(&bounding_rect_sm.to_polygon())); + assert!(bounding_rect_xl + .to_polygon() + .intersects(&bounding_rect_sm.to_polygon())); + + assert!(bounding_rect_sm.to_polygon().intersects(&bounding_rect_xl)); + assert!(bounding_rect_sm.intersects(&bounding_rect_xl.to_polygon())); + assert!(bounding_rect_sm + .to_polygon() + .intersects(&bounding_rect_xl.to_polygon())); + + assert!(bounding_rect_sm.to_polygon().intersects(&bounding_rect_s2)); + assert!(bounding_rect_sm.intersects(&bounding_rect_s2.to_polygon())); + assert!(bounding_rect_sm + .to_polygon() + .intersects(&bounding_rect_s2.to_polygon())); + + assert!(bounding_rect_s2.to_polygon().intersects(&bounding_rect_sm)); + assert!(bounding_rect_s2.intersects(&bounding_rect_sm.to_polygon())); + assert!(bounding_rect_s2 + .to_polygon() + .intersects(&bounding_rect_sm.to_polygon())); + } + #[test] + fn point_intersects_line_test() { + let p0 = Point::new(2., 4.); + // vertical line + let line1 = Line::from([(2., 0.), (2., 5.)]); + // point on line, but outside line segment + let line2 = Line::from([(0., 6.), (1.5, 4.5)]); + // point on line + let line3 = Line::from([(0., 6.), (3., 3.)]); + // point above line with positive slope + let line4 = Line::from([(1., 2.), (5., 3.)]); + // point below line with positive slope + let line5 = Line::from([(1., 5.), (5., 6.)]); + // point above line with negative slope + let line6 = Line::from([(1., 2.), (5., -3.)]); + // point below line with negative slope + let line7 = Line::from([(1., 6.), (5., 5.)]); + assert!(line1.intersects(&p0)); + assert!(p0.intersects(&line1)); + assert!(!line2.intersects(&p0)); + assert!(!p0.intersects(&line2)); + assert!(line3.intersects(&p0)); + assert!(p0.intersects(&line3)); + assert!(!line4.intersects(&p0)); + assert!(!p0.intersects(&line4)); + assert!(!line5.intersects(&p0)); + assert!(!p0.intersects(&line5)); + assert!(!line6.intersects(&p0)); + assert!(!p0.intersects(&line6)); + assert!(!line7.intersects(&p0)); + assert!(!p0.intersects(&line7)); + } + #[test] + fn line_intersects_line_test() { + let line0 = Line::from([(0., 0.), (3., 4.)]); + let line1 = Line::from([(2., 0.), (2., 5.)]); + let line2 = Line::from([(0., 7.), (5., 4.)]); + let line3 = Line::from([(0., 0.), (-3., -4.)]); + assert!(line0.intersects(&line0)); + assert!(line0.intersects(&line1)); + assert!(!line0.intersects(&line2)); + assert!(line0.intersects(&line3)); + + assert!(line1.intersects(&line0)); + assert!(line1.intersects(&line1)); + assert!(!line1.intersects(&line2)); + assert!(!line1.intersects(&line3)); + + assert!(!line2.intersects(&line0)); + assert!(!line2.intersects(&line1)); + assert!(line2.intersects(&line2)); + assert!(!line1.intersects(&line3)); + } + #[test] + fn line_intersects_linestring_test() { + let line0 = Line::from([(0., 0.), (3., 4.)]); + let linestring0 = LineString::from(vec![(0., 1.), (1., 0.), (2., 0.)]); + let linestring1 = LineString::from(vec![(0.5, 0.2), (1., 0.), (2., 0.)]); + assert!(line0.intersects(&linestring0)); + assert!(!line0.intersects(&linestring1)); + assert!(linestring0.intersects(&line0)); + assert!(!linestring1.intersects(&line0)); + } + #[test] + fn line_intersects_polygon_test() { + let line0 = Line::from([(0.5, 0.5), (2., 1.)]); + let poly0 = Polygon::new( + LineString::from(vec![(0., 0.), (1., 2.), (1., 0.), (0., 0.)]), + vec![], + ); + let poly1 = Polygon::new( + LineString::from(vec![(1., -1.), (2., -1.), (2., -2.), (1., -1.)]), + vec![], + ); + // line contained in the hole + let poly2 = Polygon::new( + LineString::from(vec![(-1., -1.), (-1., 10.), (10., -1.), (-1., -1.)]), + vec![LineString::from(vec![ + (0., 0.), + (3., 4.), + (3., 0.), + (0., 0.), + ])], + ); + assert!(line0.intersects(&poly0)); + assert!(poly0.intersects(&line0)); + + assert!(!line0.intersects(&poly1)); + assert!(!poly1.intersects(&line0)); + + assert!(!line0.intersects(&poly2)); + assert!(!poly2.intersects(&line0)); + } + #[test] + // See https://github.com/georust/geo/issues/419 + fn rect_test_419() { + let a = Rect::new( + coord! { + x: 9.228515625, + y: 46.83013364044739, + }, + coord! { + x: 9.2724609375, + y: 46.86019101567026, + }, + ); + let b = Rect::new( + coord! { + x: 9.17953, + y: 46.82018, + }, + coord! { + x: 9.26309, + y: 46.88099, + }, + ); + assert!(a.intersects(&b)); + assert!(b.intersects(&a)); + } + + #[test] + fn test_geom_collection_collection() { + let collection0 = Geometry::GeometryCollection(GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(0., 0.)), + Geometry::Point(Point::new(1., 1.)), + ])); + let collection1 = Geometry::GeometryCollection(GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(0., 0.)), + Geometry::Point(Point::new(2., 2.)), + ])); + let collection2 = Geometry::GeometryCollection(GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(3., 3.)), + Geometry::Point(Point::new(4., 4.)), + ])); + assert!(collection0.intersects(&collection1)); + assert!(collection1.intersects(&collection0)); + assert!(!collection0.intersects(&collection2)); + assert!(!collection2.intersects(&collection0)); + } + + #[test] + fn compile_test_geom_geom() { + // This test should check existence of all + // combinations of geometry types. + let geom: Geometry<_> = Line::from([(0.5, 0.5), (2., 1.)]).into(); + assert!(geom.intersects(&geom)); + } + + #[test] + fn exhaustive_compile_test() { + use geo_types::{GeometryCollection, Triangle}; + let c: Coord = coord! { x: 0., y: 0. }; + let pt: Point = Point::new(0., 0.); + let ln: Line = Line::new((0., 0.), (1., 1.)); + let ls = line_string![(0., 0.).into(), (1., 1.).into()]; + let poly = Polygon::new(LineString::from(vec![(0., 0.), (1., 1.), (1., 0.)]), vec![]); + let rect = Rect::new(coord! { x: 10., y: 20. }, coord! { x: 30., y: 10. }); + let tri = Triangle::new( + coord! { x: 0., y: 0. }, + coord! { x: 10., y: 20. }, + coord! { x: 20., y: -10. }, + ); + let geom = Geometry::Point(pt); + let gc = GeometryCollection::new_from(vec![geom.clone()]); + let multi_point = MultiPoint::new(vec![pt]); + let multi_ls = MultiLineString::new(vec![ls.clone()]); + let multi_poly = MultiPolygon::new(vec![poly.clone()]); + + let _ = c.intersects(&pt); + let _ = c.intersects(&ln); + let _ = c.intersects(&ls); + let _ = c.intersects(&poly); + let _ = c.intersects(&rect); + let _ = c.intersects(&tri); + let _ = c.intersects(&geom); + let _ = c.intersects(&gc); + let _ = c.intersects(&multi_point); + let _ = c.intersects(&multi_ls); + let _ = c.intersects(&multi_poly); + + let _ = pt.intersects(&pt); + let _ = pt.intersects(&ln); + let _ = pt.intersects(&ls); + let _ = pt.intersects(&poly); + let _ = pt.intersects(&rect); + let _ = pt.intersects(&tri); + let _ = pt.intersects(&geom); + let _ = pt.intersects(&gc); + let _ = pt.intersects(&multi_point); + let _ = pt.intersects(&multi_ls); + let _ = pt.intersects(&multi_poly); + let _ = ln.intersects(&pt); + let _ = ln.intersects(&ln); + let _ = ln.intersects(&ls); + let _ = ln.intersects(&poly); + let _ = ln.intersects(&rect); + let _ = ln.intersects(&tri); + let _ = ln.intersects(&geom); + let _ = ln.intersects(&gc); + let _ = ln.intersects(&multi_point); + let _ = ln.intersects(&multi_ls); + let _ = ln.intersects(&multi_poly); + let _ = ls.intersects(&pt); + let _ = ls.intersects(&ln); + let _ = ls.intersects(&ls); + let _ = ls.intersects(&poly); + let _ = ls.intersects(&rect); + let _ = ls.intersects(&tri); + let _ = ls.intersects(&geom); + let _ = ls.intersects(&gc); + let _ = ls.intersects(&multi_point); + let _ = ls.intersects(&multi_ls); + let _ = ls.intersects(&multi_poly); + let _ = poly.intersects(&pt); + let _ = poly.intersects(&ln); + let _ = poly.intersects(&ls); + let _ = poly.intersects(&poly); + let _ = poly.intersects(&rect); + let _ = poly.intersects(&tri); + let _ = poly.intersects(&geom); + let _ = poly.intersects(&gc); + let _ = poly.intersects(&multi_point); + let _ = poly.intersects(&multi_ls); + let _ = poly.intersects(&multi_poly); + let _ = rect.intersects(&pt); + let _ = rect.intersects(&ln); + let _ = rect.intersects(&ls); + let _ = rect.intersects(&poly); + let _ = rect.intersects(&rect); + let _ = rect.intersects(&tri); + let _ = rect.intersects(&geom); + let _ = rect.intersects(&gc); + let _ = rect.intersects(&multi_point); + let _ = rect.intersects(&multi_ls); + let _ = rect.intersects(&multi_poly); + let _ = tri.intersects(&pt); + let _ = tri.intersects(&ln); + let _ = tri.intersects(&ls); + let _ = tri.intersects(&poly); + let _ = tri.intersects(&rect); + let _ = tri.intersects(&tri); + let _ = tri.intersects(&geom); + let _ = tri.intersects(&gc); + let _ = tri.intersects(&multi_point); + let _ = tri.intersects(&multi_ls); + let _ = tri.intersects(&multi_poly); + let _ = geom.intersects(&pt); + let _ = geom.intersects(&ln); + let _ = geom.intersects(&ls); + let _ = geom.intersects(&poly); + let _ = geom.intersects(&rect); + let _ = geom.intersects(&tri); + let _ = geom.intersects(&geom); + let _ = geom.intersects(&gc); + let _ = geom.intersects(&multi_point); + let _ = geom.intersects(&multi_ls); + let _ = geom.intersects(&multi_poly); + let _ = gc.intersects(&pt); + let _ = gc.intersects(&ln); + let _ = gc.intersects(&ls); + let _ = gc.intersects(&poly); + let _ = gc.intersects(&rect); + let _ = gc.intersects(&tri); + let _ = gc.intersects(&geom); + let _ = gc.intersects(&gc); + let _ = gc.intersects(&multi_point); + let _ = gc.intersects(&multi_ls); + let _ = gc.intersects(&multi_poly); + let _ = multi_point.intersects(&pt); + let _ = multi_point.intersects(&ln); + let _ = multi_point.intersects(&ls); + let _ = multi_point.intersects(&poly); + let _ = multi_point.intersects(&rect); + let _ = multi_point.intersects(&tri); + let _ = multi_point.intersects(&geom); + let _ = multi_point.intersects(&gc); + let _ = multi_point.intersects(&multi_point); + let _ = multi_point.intersects(&multi_ls); + let _ = multi_point.intersects(&multi_poly); + let _ = multi_ls.intersects(&pt); + let _ = multi_ls.intersects(&ln); + let _ = multi_ls.intersects(&ls); + let _ = multi_ls.intersects(&poly); + let _ = multi_ls.intersects(&rect); + let _ = multi_ls.intersects(&tri); + let _ = multi_ls.intersects(&geom); + let _ = multi_ls.intersects(&gc); + let _ = multi_ls.intersects(&multi_point); + let _ = multi_ls.intersects(&multi_ls); + let _ = multi_ls.intersects(&multi_poly); + let _ = multi_poly.intersects(&pt); + let _ = multi_poly.intersects(&ln); + let _ = multi_poly.intersects(&ls); + let _ = multi_poly.intersects(&poly); + let _ = multi_poly.intersects(&rect); + let _ = multi_poly.intersects(&tri); + let _ = multi_poly.intersects(&geom); + let _ = multi_poly.intersects(&gc); + let _ = multi_poly.intersects(&multi_point); + let _ = multi_poly.intersects(&multi_ls); + let _ = multi_poly.intersects(&multi_poly); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/point.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/point.rs new file mode 100644 index 00000000..1426050c --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/point.rs @@ -0,0 +1,112 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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::*; + +use super::IntersectsTrait; +use crate::*; + +// Generate implementations for Point by delegating to Coord +macro_rules! impl_intersects_point_from_coord { + ($num_type:ident, $rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: $num_type, + LHS: PointTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.coord_ext().is_some_and(|c| c.intersects_trait(rhs)) + } + } + }; +} + +impl_intersects_point_from_coord!(CoordNum, CoordTraitExt, CoordTag); +impl_intersects_point_from_coord!(CoordNum, PointTraitExt, PointTag); +impl_intersects_point_from_coord!(GeoNum, LineStringTraitExt, LineStringTag); +impl_intersects_point_from_coord!(GeoNum, PolygonTraitExt, PolygonTag); +impl_intersects_point_from_coord!(CoordNum, MultiPointTraitExt, MultiPointTag); +impl_intersects_point_from_coord!(GeoNum, MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_point_from_coord!(GeoNum, MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_point_from_coord!(GeoNum, GeometryTraitExt, GeometryTag); +impl_intersects_point_from_coord!(GeoNum, GeometryCollectionTraitExt, GeometryCollectionTag); +impl_intersects_point_from_coord!(GeoNum, LineTraitExt, LineTag); +impl_intersects_point_from_coord!(CoordNum, RectTraitExt, RectTag); +impl_intersects_point_from_coord!(GeoNum, TriangleTraitExt, TriangleTag); + +// Generate implementations for MultiPoint by delegating to Point +macro_rules! impl_intersects_multipoint_from_point { + ($num_type:ident, $rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: $num_type, + LHS: MultiPointTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.points_ext().any(|p| p.intersects_trait(rhs)) + } + } + }; +} + +impl_intersects_multipoint_from_point!(CoordNum, CoordTraitExt, CoordTag); +impl_intersects_multipoint_from_point!(CoordNum, PointTraitExt, PointTag); +impl_intersects_multipoint_from_point!(GeoNum, LineStringTraitExt, LineStringTag); +impl_intersects_multipoint_from_point!(GeoNum, PolygonTraitExt, PolygonTag); +impl_intersects_multipoint_from_point!(CoordNum, MultiPointTraitExt, MultiPointTag); +impl_intersects_multipoint_from_point!(GeoNum, MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_multipoint_from_point!(GeoNum, MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_multipoint_from_point!(GeoNum, GeometryTraitExt, GeometryTag); +impl_intersects_multipoint_from_point!(GeoNum, GeometryCollectionTraitExt, GeometryCollectionTag); +impl_intersects_multipoint_from_point!(GeoNum, LineTraitExt, LineTag); +impl_intersects_multipoint_from_point!(CoordNum, RectTraitExt, RectTag); +impl_intersects_multipoint_from_point!(GeoNum, TriangleTraitExt, TriangleTag); + +symmetric_intersects_trait_impl!( + CoordNum, + CoordTraitExt, + CoordTag, + MultiPointTraitExt, + MultiPointTag +); +symmetric_intersects_trait_impl!( + GeoNum, + LineTraitExt, + LineTag, + MultiPointTraitExt, + MultiPointTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + MultiPointTraitExt, + MultiPointTag +); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + MultiPointTraitExt, + MultiPointTag +); diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/polygon.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/polygon.rs new file mode 100644 index 00000000..76cef479 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/polygon.rs @@ -0,0 +1,214 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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; +use crate::CoordinatePosition; +use crate::GeoNum; +use sedona_geo_traits_ext::*; + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: PolygonTraitExt, + RHS: CoordTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.coordinate_position(&rhs.geo_coord()) != CoordPos::Outside + } +} + +symmetric_intersects_trait_impl!(GeoNum, CoordTraitExt, CoordTag, PolygonTraitExt, PolygonTag); +symmetric_intersects_trait_impl!(GeoNum, PolygonTraitExt, PolygonTag, PointTraitExt, PointTag); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: PolygonTraitExt, + RHS: LineTraitExt, +{ + fn intersects_trait(&self, line: &RHS) -> bool { + // Check if line intersects any part of the polygon + if let Some(exterior) = self.exterior_ext() { + exterior.intersects_trait(line) + || self + .interiors_ext() + .any(|inner| inner.intersects_trait(line)) + || self.intersects_trait(&line.start_ext()) + || self.intersects_trait(&line.end_ext()) + } else { + false + } + } +} + +symmetric_intersects_trait_impl!(GeoNum, LineTraitExt, LineTag, PolygonTraitExt, PolygonTag); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + LineStringTraitExt, + LineStringTag +); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + MultiLineStringTraitExt, + 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, + LHS: PolygonTraitExt, + RHS: PolygonTraitExt, +{ + fn intersects_trait(&self, polygon: &RHS) -> bool { + if has_disjoint_bboxes(self, polygon) { + return false; + } + + 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) + } else { + false + } + } +} + +// Generate implementations for MultiPolygon by delegating to the Polygon implementation + +macro_rules! impl_intersects_multi_polygon_from_polygon { + ($rhs_type:ident, $rhs_tag:ident) => { + impl IntersectsTrait for LHS + where + T: GeoNum, + LHS: MultiPolygonTraitExt, + RHS: $rhs_type, + { + fn intersects_trait(&self, rhs: &RHS) -> bool { + if has_disjoint_bboxes(self, rhs) { + return false; + } + self.polygons_ext().any(|p| p.intersects_trait(rhs)) + } + } + }; +} + +impl_intersects_multi_polygon_from_polygon!(CoordTraitExt, CoordTag); +impl_intersects_multi_polygon_from_polygon!(PointTraitExt, PointTag); +impl_intersects_multi_polygon_from_polygon!(LineStringTraitExt, LineStringTag); +impl_intersects_multi_polygon_from_polygon!(PolygonTraitExt, PolygonTag); +impl_intersects_multi_polygon_from_polygon!(MultiPointTraitExt, MultiPointTag); +impl_intersects_multi_polygon_from_polygon!(MultiLineStringTraitExt, MultiLineStringTag); +impl_intersects_multi_polygon_from_polygon!(MultiPolygonTraitExt, MultiPolygonTag); +impl_intersects_multi_polygon_from_polygon!(GeometryTraitExt, GeometryTag); +impl_intersects_multi_polygon_from_polygon!(GeometryCollectionTraitExt, GeometryCollectionTag); +impl_intersects_multi_polygon_from_polygon!(LineTraitExt, LineTag); +impl_intersects_multi_polygon_from_polygon!(RectTraitExt, RectTag); +impl_intersects_multi_polygon_from_polygon!(TriangleTraitExt, TriangleTag); + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + MultiPolygonTraitExt, + MultiPolygonTag +); +symmetric_intersects_trait_impl!( + GeoNum, + LineTraitExt, + LineTag, + MultiPolygonTraitExt, + MultiPolygonTag +); +symmetric_intersects_trait_impl!( + GeoNum, + RectTraitExt, + RectTag, + MultiPolygonTraitExt, + MultiPolygonTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + MultiPolygonTraitExt, + MultiPolygonTag +); +symmetric_intersects_trait_impl!( + GeoNum, + PolygonTraitExt, + PolygonTag, + MultiPolygonTraitExt, + MultiPolygonTag +); + +#[cfg(test)] +mod tests { + use crate::*; + #[test] + fn geom_intersects_geom() { + let a = Geometry::::from(polygon![]); + let b = Geometry::from(polygon![]); + assert!(!a.intersects(&b)); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/rect.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/rect.rs new file mode 100644 index 00000000..23483b10 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/rect.rs @@ -0,0 +1,118 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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 sedona_geo_traits_ext::*; + +use super::IntersectsTrait; +use crate::*; + +impl IntersectsTrait for LHS +where + T: CoordNum, + LHS: RectTraitExt, + RHS: CoordTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + let lhs_x = rhs.x(); + let lhs_y = rhs.y(); + + lhs_x >= self.min().x() + && lhs_y >= self.min().y() + && lhs_x <= self.max().x() + && lhs_y <= self.max().y() + } +} + +symmetric_intersects_trait_impl!(CoordNum, CoordTraitExt, CoordTag, RectTraitExt, RectTag); +symmetric_intersects_trait_impl!(CoordNum, RectTraitExt, RectTag, PointTraitExt, PointTag); +symmetric_intersects_trait_impl!( + CoordNum, + RectTraitExt, + RectTag, + MultiPointTraitExt, + MultiPointTag +); + +impl IntersectsTrait for LHS +where + T: CoordNum, + LHS: RectTraitExt, + RHS: RectTraitExt, +{ + fn intersects_trait(&self, other: &RHS) -> bool { + if self.max().x() < other.min().x() { + return false; + } + + if self.max().y() < other.min().y() { + return false; + } + + if self.min().x() > other.max().x() { + return false; + } + + if self.min().y() > other.max().y() { + return false; + } + + true + } +} + +// Same logic as polygon x line, but avoid an allocation. +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: RectTraitExt, + 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)); + + // If either rhs.{start,end} lies inside Rect, then true + self.intersects_trait(&rhs.start_ext()) + || self.intersects_trait(&rhs.end_ext()) + || Line::new(lt, rt).intersects_trait(rhs) + || Line::new(rt, rb).intersects_trait(rhs) + || Line::new(lb, rb).intersects_trait(rhs) + || Line::new(lt, lb).intersects_trait(rhs) + } +} + +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()) + } +} + +symmetric_intersects_trait_impl!(GeoNum, TriangleTraitExt, TriangleTag, RectTraitExt, RectTag); diff --git a/rust/sedona-geo-generic-alg/src/algorithm/intersects/triangle.rs b/rust/sedona-geo-generic-alg/src/algorithm/intersects/triangle.rs new file mode 100644 index 00000000..7f15f563 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/intersects/triangle.rs @@ -0,0 +1,92 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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 sedona_geo_traits_ext::*; + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: TriangleTraitExt, + RHS: CoordTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + let rhs = rhs.geo_coord(); + + let mut orientations = self + .to_lines() + .map(|l| T::Ker::orient2d(l.start, l.end, rhs)); + + orientations.sort(); + + !orientations + .windows(2) + .any(|win| win[0] != win[1] && win[1] != Orientation::Collinear) + + // // neglecting robust predicates, hence faster + // let p0x = self.0.x.to_f64().unwrap(); + // let p0y = self.0.y.to_f64().unwrap(); + // let p1x = self.1.x.to_f64().unwrap(); + // let p1y = self.1.y.to_f64().unwrap(); + // let p2x = self.2.x.to_f64().unwrap(); + // let p2y = self.2.y.to_f64().unwrap(); + + // let px = rhs.x.to_f64().unwrap(); + // let py = rhs.y.to_f64().unwrap(); + + // let s = (p0x - p2x) * (py - p2y) - (p0y - p2y) * (px - p2x); + // let t = (p1x - p0x) * (py - p0y) - (p1y - p0y) * (px - p0x); + + // if (s < 0.) != (t < 0.) && s != 0. && t != 0. { + // return false; + // } + + // let d = (p2x - p1x) * (py - p1y) - (p2y - p1y) * (px - p1x); + // d == 0. || (d < 0.) == (s + t <= 0.) + } +} + +symmetric_intersects_trait_impl!( + GeoNum, + CoordTraitExt, + CoordTag, + TriangleTraitExt, + TriangleTag +); +symmetric_intersects_trait_impl!( + GeoNum, + TriangleTraitExt, + TriangleTag, + PointTraitExt, + PointTag +); + +impl IntersectsTrait for LHS +where + T: GeoNum, + LHS: TriangleTraitExt, + RHS: TriangleTraitExt, +{ + fn intersects_trait(&self, rhs: &RHS) -> bool { + self.to_polygon().intersects_trait(&rhs.to_polygon()) + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/kernels/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/kernels/mod.rs new file mode 100644 index 00000000..86d11a60 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/kernels/mod.rs @@ -0,0 +1,71 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Generic Kernel predicates (orientation, distance, dot product) +//! +//! Ported (and contains copied code) from `geo::algorithm::kernels`: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +use num_traits::Zero; + +use crate::{coord, Coord, CoordNum}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +pub enum Orientation { + CounterClockwise, + Clockwise, + Collinear, +} + +/// Kernel trait to provide predicates to operate on +/// different scalar types. +pub trait Kernel { + /// Gives the orientation of 3 2-dimensional points: + /// ccw, cw or collinear (None) + fn orient2d(p: Coord, q: Coord, r: Coord) -> Orientation { + let res = (q.x - p.x) * (r.y - q.y) - (q.y - p.y) * (r.x - q.x); + if res > Zero::zero() { + Orientation::CounterClockwise + } else if res < Zero::zero() { + Orientation::Clockwise + } else { + Orientation::Collinear + } + } + + fn square_euclidean_distance(p: Coord, q: Coord) -> T { + (p.x - q.x) * (p.x - q.x) + (p.y - q.y) * (p.y - q.y) + } + + /// Compute the sign of the dot product of `u` and `v` using + /// robust predicates. The output is `CounterClockwise` if + /// the sign is positive, `Clockwise` if negative, and + /// `Collinear` if zero. + fn dot_product_sign(u: Coord, v: Coord) -> Orientation { + let zero = Coord::zero(); + let vdash = coord! { + x: T::zero() - v.y, + y: v.x, + }; + Self::orient2d(zero, u, vdash) + } +} + +pub mod robust; +pub use self::robust::RobustKernel; + +pub mod simple; +pub use self::simple::SimpleKernel; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/kernels/robust.rs b/rust/sedona-geo-generic-alg/src/algorithm/kernels/robust.rs new file mode 100644 index 00000000..2a63e898 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/kernels/robust.rs @@ -0,0 +1,65 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Robust kernel predicates (orientation using robust predicates) +//! +//! Ported (and contains copied code) from `geo::algorithm::kernels::robust`: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +use super::{CoordNum, Kernel, Orientation}; +use crate::Coord; + +use num_traits::{Float, NumCast}; + +/// Robust kernel that uses [fast robust +/// predicates](//www.cs.cmu.edu/~quake/robust.html) to +/// provide robust floating point predicates. Should only be +/// used with types that can _always_ be casted to `f64` +/// _without loss in precision_. +#[derive(Default, Debug)] +pub struct RobustKernel; + +impl Kernel for RobustKernel +where + T: CoordNum + Float, +{ + fn orient2d(p: Coord, q: Coord, r: Coord) -> Orientation { + use robust::{orient2d, Coord}; + + let orientation = orient2d( + Coord { + x: ::from(p.x).unwrap(), + y: ::from(p.y).unwrap(), + }, + Coord { + x: ::from(q.x).unwrap(), + y: ::from(q.y).unwrap(), + }, + Coord { + x: ::from(r.x).unwrap(), + y: ::from(r.y).unwrap(), + }, + ); + + if orientation < 0. { + Orientation::Clockwise + } else if orientation > 0. { + Orientation::CounterClockwise + } else { + Orientation::Collinear + } + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/kernels/simple.rs b/rust/sedona-geo-generic-alg/src/algorithm/kernels/simple.rs new file mode 100644 index 00000000..6259df60 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/kernels/simple.rs @@ -0,0 +1,31 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Simple kernel predicates +//! +//! Ported (and contains copied code) from `geo::algorithm::kernels::simple`: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +use super::Kernel; +use crate::CoordNum; + +/// Simple kernel provides the direct implementation of the +/// predicates. These are meant to be used with exact +/// arithmetic signed types (eg. i32, i64). +#[derive(Default, Debug)] +pub struct SimpleKernel; + +impl Kernel for SimpleKernel {} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/distance.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/distance.rs new file mode 100644 index 00000000..6031e492 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/distance.rs @@ -0,0 +1,36 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Generic Distance traits (line measures) +//! +//! Ported (and contains copied code) from `geo::algorithm::line_measures::distance`: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +/// Calculate the minimum distance between two geometries. +pub trait Distance { + /// Note that not all implementations support all geometry combinations, but at least `Point` to `Point` + /// is supported. + /// See [specific implementations](#implementers) for details. + /// + /// # Units + /// + /// - `origin`, `destination`: geometry where the units of x/y depend on the trait implementation. + /// - returns: depends on the trait implementation. + fn distance(&self, origin: Origin, destination: Destination) -> F; +} + +// Re-export the DistanceExt trait from the refactored euclidean metric space +pub use super::metric_spaces::euclidean::DistanceExt; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/length.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/length.rs new file mode 100644 index 00000000..4c458936 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/length.rs @@ -0,0 +1,832 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Generic Length and Perimeter extension traits +//! +//! Ported (and contains copied code) from `geo::algorithm::line_measures::length` (and related perimeter logic): +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +use super::Distance; +use crate::{CoordFloat, Point}; +use geo_traits::{CoordTrait, PolygonTrait}; +use sedona_geo_traits_ext::*; +use std::borrow::Borrow; + +/// Extension trait that enables the modern Length and Perimeter API for WKB and other generic geometry types. +/// +/// This provides the same API as the concrete `LengthMeasurable` implementations but works with +/// any geometry type that implements the geo-traits-ext pattern. +/// +/// # Examples +/// ``` +/// use sedona_geo_generic_alg::algorithm::line_measures::{LengthMeasurableExt, Euclidean}; +/// use geo_types::{LineString, coord}; +/// let ls = LineString::new(vec![ +/// coord! { x: 0., y: 0. }, +/// coord! { x: 3., y: 4. }, +/// coord! { x: 3., y: 5. }, +/// ]); +/// let length = ls.length_ext(&Euclidean); +/// assert_eq!(length, 6.0); +/// ``` +pub trait LengthMeasurableExt { + /// Calculate the length using the given metric space. + /// + /// For 1D geometries (Line, LineString, MultiLineString), returns the actual length. + /// For 0D and 2D geometries, returns zero. + fn length_ext(&self, metric_space: &impl Distance, Point>) -> F; + + /// Calculate the perimeter using the given metric space. + /// + /// For 2D geometries (Polygon, MultiPolygon, Rect, Triangle), returns the perimeter. + /// For 1D geometries (Line, LineString, MultiLineString), returns zero. + /// For 0D geometries (Point, MultiPoint), returns zero. + fn perimeter_ext(&self, metric_space: &impl Distance, Point>) -> F; +} + +// Implementation for WKB and other generic geometries using the type-tag pattern +impl LengthMeasurableExt for G +where + F: CoordFloat, + G: GeoTraitExtWithTypeTag + LengthMeasurableTrait, +{ + fn length_ext(&self, metric_space: &impl Distance, Point>) -> F { + self.length_trait(metric_space) + } + + fn perimeter_ext(&self, metric_space: &impl Distance, Point>) -> F { + self.perimeter_trait(metric_space) + } +} + +// Internal trait that handles the actual length and perimeter computation for different geometry types +trait LengthMeasurableTrait +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F; + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F; +} + +// Implementation for Line geometries +impl> LengthMeasurableTrait for L +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F { + let start = Point::new(self.start_coord().x, self.start_coord().y); + let end = Point::new(self.end_coord().x, self.end_coord().y); + metric_space.distance(start, end) + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // For 1D geometries like lines, perimeter should be 0 according to PostGIS/OGC standards + F::zero() + } +} + +// Implementation for LineString geometries +impl> LengthMeasurableTrait for LS +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F { + let mut length = F::zero(); + for line in self.lines() { + let start = Point::new(line.start_coord().x, line.start_coord().y); + let end = Point::new(line.end_coord().x, line.end_coord().y); + length = length + metric_space.distance(start, end); + } + length + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // For 1D geometries like linestrings, perimeter should be 0 according to PostGIS/OGC standards + F::zero() + } +} + +// Implementation for MultiLineString geometries +impl> LengthMeasurableTrait for MLS +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F { + let mut length = F::zero(); + for line_string in self.line_strings_ext() { + length = length + line_string.length_trait(metric_space); + } + length + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // For 1D geometries like multilinestrings, perimeter should be 0 according to PostGIS/OGC standards + F::zero() + } +} + +// For geometry types that don't have a meaningful length (return zero) +impl> LengthMeasurableTrait for P +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + F::zero() + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + F::zero() + } +} + +impl> LengthMeasurableTrait for MP +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + F::zero() + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + F::zero() + } +} + +// Helper function to calculate the perimeter of a linestring using a metric space +fn linestring_perimeter_with_metric>( + linestring: &LS, + metric_space: &impl Distance, Point>, +) -> F +where + F: CoordFloat, +{ + let mut perimeter = F::zero(); + for line in linestring.lines() { + let start_coord = line.start_coord(); + let end_coord = line.end_coord(); + let start_point = Point::new(start_coord.x(), start_coord.y()); + let end_point = Point::new(end_coord.x(), end_coord.y()); + perimeter = perimeter + metric_space.distance(start_point, end_point); + } + perimeter +} + +// Helper function to calculate the perimeter of a ring using the basic LineStringTrait +fn ring_perimeter_with_metric( + ring: &LS, + metric_space: &impl Distance, Point>, +) -> F +where + F: CoordFloat, + LS: geo_traits::LineStringTrait, +{ + let mut perimeter = F::zero(); + let num_coords = ring.num_coords(); + if num_coords > 1 { + for i in 0..(num_coords - 1) { + let start_coord = ring.coord(i).unwrap(); + let end_coord = ring.coord(i + 1).unwrap(); + let start_point = Point::new(start_coord.x(), start_coord.y()); + let end_point = Point::new(end_coord.x(), end_coord.y()); + perimeter = perimeter + metric_space.distance(start_point, end_point); + } + } + perimeter +} + +impl> LengthMeasurableTrait for P +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // Length is a 1D concept, doesn't apply to 2D polygons + F::zero() + } + + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F { + // For polygons, return the perimeter (length of the boundary) + let mut total_perimeter = match self.exterior_ext() { + Some(exterior) => linestring_perimeter_with_metric(&exterior, metric_space), + None => F::zero(), + }; + + // Add interior rings perimeter + for interior in self.interiors_ext() { + total_perimeter = + total_perimeter + linestring_perimeter_with_metric(&interior, metric_space); + } + + total_perimeter + } +} + +impl> LengthMeasurableTrait for MP +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // Length is a 1D concept, doesn't apply to 2D multipolygons + F::zero() + } + + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F { + // For multipolygons, return the sum of all polygon perimeters + let mut total_perimeter = F::zero(); + for polygon in self.polygons() { + // Calculate perimeter for each polygon + let mut polygon_perimeter = match polygon.exterior() { + Some(exterior) => ring_perimeter_with_metric(&exterior, metric_space), + None => F::zero(), + }; + + // Add interior rings perimeter + for interior in polygon.interiors() { + polygon_perimeter = + polygon_perimeter + ring_perimeter_with_metric(&interior, metric_space); + } + + total_perimeter = total_perimeter + polygon_perimeter; + } + total_perimeter + } +} + +impl> LengthMeasurableTrait for R +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // Length is a 1D concept, doesn't apply to 2D rectangles + F::zero() + } + + fn perimeter_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // For rectangles, return the perimeter + let width = self.width(); + let height = self.height(); + let two = F::one() + F::one(); + two * (width + height) + } +} + +impl> LengthMeasurableTrait for T +where + F: CoordFloat, +{ + fn length_trait(&self, _metric_space: &impl Distance, Point>) -> F { + // Length is a 1D concept, doesn't apply to 2D triangles + F::zero() + } + + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F { + // For triangles, return the perimeter (sum of all three sides) + let coord0 = self.first_coord(); + let coord1 = self.second_coord(); + let coord2 = self.third_coord(); + + let p0 = Point::new(coord0.x, coord0.y); + let p1 = Point::new(coord1.x, coord1.y); + let p2 = Point::new(coord2.x, coord2.y); + + let side1 = metric_space.distance(p0, p1); + let side2 = metric_space.distance(p1, p2); + let side3 = metric_space.distance(p2, p0); + + side1 + side2 + side3 + } +} + +// Implementation for GeometryCollection with runtime type dispatch +impl> LengthMeasurableTrait + for GC +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F { + self.geometries_ext() + .map(|g| g.borrow().length_trait(metric_space)) + .fold(F::zero(), |acc, next| acc + next) + } + + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F { + self.geometries_ext() + .map(|g| g.borrow().perimeter_trait(metric_space)) + .fold(F::zero(), |acc, next| acc + next) + } +} + +impl> LengthMeasurableTrait for G +where + F: CoordFloat, +{ + fn length_trait(&self, metric_space: &impl Distance, Point>) -> F { + if self.is_collection() { + self.geometries_ext() + .map(|g_inner| g_inner.borrow().length_trait(metric_space)) + .fold(F::zero(), |acc, next| acc + next) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(_) => F::zero(), + GeometryTypeExt::Line(line) => line.length_trait(metric_space), + GeometryTypeExt::LineString(ls) => ls.length_trait(metric_space), + GeometryTypeExt::Polygon(_) => F::zero(), + GeometryTypeExt::MultiPoint(_) => F::zero(), + GeometryTypeExt::MultiLineString(mls) => mls.length_trait(metric_space), + GeometryTypeExt::MultiPolygon(_) => F::zero(), + GeometryTypeExt::Rect(_) => F::zero(), + GeometryTypeExt::Triangle(_) => F::zero(), + } + } + } + + fn perimeter_trait(&self, metric_space: &impl Distance, Point>) -> F { + if self.is_collection() { + self.geometries_ext() + .map(|g_inner| g_inner.borrow().perimeter_trait(metric_space)) + .fold(F::zero(), |acc, next| acc + next) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(_) => F::zero(), + GeometryTypeExt::Line(_) => F::zero(), // 1D geometry - no perimeter + GeometryTypeExt::LineString(_) => F::zero(), // 1D geometry - no perimeter + GeometryTypeExt::Polygon(polygon) => polygon.perimeter_trait(metric_space), + GeometryTypeExt::MultiPoint(_) => F::zero(), + GeometryTypeExt::MultiLineString(_) => F::zero(), // 1D geometry - no perimeter + GeometryTypeExt::MultiPolygon(mp) => mp.perimeter_trait(metric_space), + GeometryTypeExt::Rect(rect) => rect.perimeter_trait(metric_space), + GeometryTypeExt::Triangle(triangle) => triangle.perimeter_trait(metric_space), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Euclidean; + + // Tests for LengthMeasurableExt - adapted from euclidean_length.rs + mod length_measurable_ext_tests { + use geo::LineString; + + use super::*; + use crate::{ + coord, line_string, polygon, Geometry, GeometryCollection, Line, MultiLineString, + MultiPoint, MultiPolygon, Point, Polygon, + }; + + #[test] + fn empty_linestring_test() { + let linestring = line_string![]; + assert_relative_eq!(0.0_f64, linestring.length_ext(&Euclidean)); + } + + #[test] + fn linestring_one_point_test() { + let linestring = line_string![(x: 0., y: 0.)]; + assert_relative_eq!(0.0_f64, linestring.length_ext(&Euclidean)); + } + + #[test] + fn linestring_test() { + let linestring = line_string![ + (x: 1., y: 1.), + (x: 7., y: 1.), + (x: 8., y: 1.), + (x: 9., y: 1.), + (x: 10., y: 1.), + (x: 11., y: 1.) + ]; + assert_relative_eq!(10.0_f64, linestring.length_ext(&Euclidean)); + } + + #[test] + fn multilinestring_test() { + let mline = MultiLineString::new(vec![ + line_string![ + (x: 1., y: 0.), + (x: 7., y: 0.), + (x: 8., y: 0.), + (x: 9., y: 0.), + (x: 10., y: 0.), + (x: 11., y: 0.) + ], + line_string![ + (x: 0., y: 0.), + (x: 0., y: 5.) + ], + ]); + assert_relative_eq!(15.0_f64, mline.length_ext(&Euclidean)); + } + + #[test] + fn line_test() { + let line0 = Line::new(coord! { x: 0., y: 0. }, coord! { x: 0., y: 1. }); + let line1 = Line::new(coord! { x: 0., y: 0. }, coord! { x: 3., y: 4. }); + assert_relative_eq!(line0.length_ext(&Euclidean), 1.); + assert_relative_eq!(line1.length_ext(&Euclidean), 5.); + } + + #[test] + fn polygon_length_and_perimeter_test() { + let polygon: Polygon = polygon![ + (x: 0., y: 0.), + (x: 4., y: 0.), + (x: 4., y: 4.), + (x: 0., y: 4.), + (x: 0., y: 0.), + ]; + // For polygons, length_ext returns zero (length is a 1D concept) + assert_relative_eq!(polygon.length_ext(&Euclidean), 0.0); + // For polygons, perimeter_ext returns the perimeter: 4 + 4 + 4 + 4 = 16 + assert_relative_eq!(polygon.perimeter_ext(&Euclidean), 16.0); + } + + #[test] + fn point_returns_zero_test() { + let point = Point::new(3.0, 4.0); + // Points have no length dimension + assert_relative_eq!(point.length_ext(&Euclidean), 0.0); + } + + #[test] + fn comprehensive_length_test_scenarios() { + // Test cases for length calculations - should return actual length only for 1D geometries + + // LINESTRING EMPTY + let empty_linestring: crate::LineString = line_string![]; + assert_relative_eq!(empty_linestring.length_ext(&Euclidean), 0.0); + + // POINT (0 0) - 0D geometry + let point = Point::new(0.0, 0.0); + assert_relative_eq!(point.length_ext(&Euclidean), 0.0); + + // LINESTRING (0 0, 0 1) - 1D geometry, length should be 1 + let linestring = line_string![(x: 0., y: 0.), (x: 0., y: 1.)]; + assert_relative_eq!(linestring.length_ext(&Euclidean), 1.0); + + // MULTIPOINT ((0 0), (1 1)) - 0D geometry, should be 0 + let multipoint = MultiPoint::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]); + assert_relative_eq!(multipoint.length_ext(&Euclidean), 0.0); + + // MULTILINESTRING ((0 0, 1 1), (1 1, 2 2)) - 1D geometry, should be ~2.828427 + let multilinestring = MultiLineString::new(vec![ + line_string![(x: 0., y: 0.), (x: 1., y: 1.)], + line_string![(x: 1., y: 1.), (x: 2., y: 2.)], + ]); + assert_relative_eq!( + multilinestring.length_ext(&Euclidean), + 2.8284271247461903, + epsilon = 1e-10 + ); + + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) - 2D geometry, length should be 0 + let polygon = polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]; + assert_relative_eq!(polygon.length_ext(&Euclidean), 0.0); + + // MULTIPOLYGON - 2D geometry, length should be 0 + let multipolygon = MultiPolygon::new(vec![ + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + polygon![ + (x: 2., y: 2.), + (x: 3., y: 2.), + (x: 3., y: 3.), + (x: 2., y: 3.), + (x: 2., y: 2.), + ], + ]); + assert_relative_eq!(multipolygon.length_ext(&Euclidean), 0.0); + + // RECT - 2D geometry, length should be 0 + let rect = crate::Rect::new(coord! { x: 0., y: 0. }, coord! { x: 3., y: 4. }); + assert_relative_eq!(rect.length_ext(&Euclidean), 0.0); + + // TRIANGLE - 2D geometry, length should be 0 + let triangle = crate::Triangle::new( + coord! { x: 0., y: 0. }, + coord! { x: 3., y: 0. }, + coord! { x: 0., y: 4. }, + ); + assert_relative_eq!(triangle.length_ext(&Euclidean), 0.0); + + // GEOMETRYCOLLECTION - should sum only the 1D geometries (linestrings) + let collection = GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(0.0, 0.0)), // contributes 0 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) ≈ 1.414 + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // contributes 0 to length + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // sqrt(2) ≈ 1.414 + ]); + assert_relative_eq!( + collection.length_ext(&Euclidean), + 2.8284271247461903, // 2*sqrt(2) only from linestrings + epsilon = 1e-10 + ); + + // GEOMETRY representation of GEOMETRYCOLLECTION + assert_relative_eq!( + Geometry::GeometryCollection(collection.clone()).length_ext(&Euclidean), + 2.8284271247461903, // 2*sqrt(2) only from linestrings + epsilon = 1e-10 + ); + } + + #[test] + fn comprehensive_perimeter_test_scenarios() { + // Test cases for perimeter calculations + + // LINESTRING EMPTY - no perimeter + let empty_linestring: crate::LineString = line_string![]; + assert_relative_eq!(empty_linestring.perimeter_ext(&Euclidean), 0.0); + + // POINT (0 0) - 0D geometry, no perimeter + let point = Point::new(0.0, 0.0); + assert_relative_eq!(point.perimeter_ext(&Euclidean), 0.0); + + // LINESTRING (0 0, 0 1) - 1D geometry, perimeter should be 0 + let linestring = line_string![(x: 0., y: 0.), (x: 0., y: 1.)]; + assert_relative_eq!(linestring.perimeter_ext(&Euclidean), 0.0); + + // MULTIPOINT ((0 0), (1 1)) - 0D geometry, no perimeter + let multipoint = MultiPoint::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]); + assert_relative_eq!(multipoint.perimeter_ext(&Euclidean), 0.0); + + // MULTILINESTRING ((0 0, 1 1), (1 1, 2 2)) - 1D geometry, perimeter should be 0 + let multilinestring = MultiLineString::new(vec![ + line_string![(x: 0., y: 0.), (x: 1., y: 1.)], + line_string![(x: 1., y: 1.), (x: 2., y: 2.)], + ]); + assert_relative_eq!(multilinestring.perimeter_ext(&Euclidean), 0.0); + + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) - 2D geometry, actual perimeter + let polygon = polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]; + assert_relative_eq!(polygon.perimeter_ext(&Euclidean), 4.0); + + // MULTIPOLYGON - 2D geometry, sum of all polygon perimeters + let multipolygon = MultiPolygon::new(vec![ + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + polygon![ + (x: 2., y: 2.), + (x: 3., y: 2.), + (x: 3., y: 3.), + (x: 2., y: 3.), + (x: 2., y: 2.), + ], + ]); + assert_relative_eq!(multipolygon.perimeter_ext(&Euclidean), 8.0); + + // RECT - 2D geometry, perimeter = 2*(width + height) + let rect = crate::Rect::new(coord! { x: 0., y: 0. }, coord! { x: 3., y: 4. }); + assert_relative_eq!(rect.perimeter_ext(&Euclidean), 14.0); // 2*(3+4) = 14 + + // TRIANGLE - 2D geometry, sum of all three sides + let triangle = crate::Triangle::new( + coord! { x: 0., y: 0. }, + coord! { x: 3., y: 0. }, + coord! { x: 0., y: 4. }, + ); + assert_relative_eq!(triangle.perimeter_ext(&Euclidean), 12.0); // 3 + 4 + 5 = 12 + + // GEOMETRYCOLLECTION - should sum perimeters from all geometries + let collection = GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(0.0, 0.0)), // contributes 0 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // contributes 0 (1D geometry) + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // perimeter = 4.0 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // contributes 0 (1D geometry) + ]); + assert_relative_eq!( + collection.perimeter_ext(&Euclidean), + 4.0, // only polygon perimeter counts + epsilon = 1e-10 + ); + + // GEOMETRY representation of GEOMETRYCOLLECTION + assert_relative_eq!( + Geometry::GeometryCollection(collection).perimeter_ext(&Euclidean), + 4.0, // only polygon perimeter counts + epsilon = 1e-10 + ); + } + + #[test] + fn test_polygon_with_holes() { + // Test polygon with interior rings (holes) + let polygon = Polygon::new( + LineString::new(vec![ + coord! { x: 0., y: 0. }, + coord! { x: 10., y: 0. }, + coord! { x: 10., y: 10. }, + coord! { x: 0., y: 10. }, + coord! { x: 0., y: 0. }, + ]), + vec![LineString::new(vec![ + coord! { x: 2., y: 2. }, + coord! { x: 8., y: 2. }, + coord! { x: 8., y: 8. }, + coord! { x: 2., y: 8. }, + coord! { x: 2., y: 2. }, + ])], + ); + // Length should be 0 (2D geometry) + assert_relative_eq!(polygon.length_ext(&Euclidean), 0.0); + // Exterior perimeter: 40 (10+10+10+10), Interior perimeter: 24 (6+6+6+6) + assert_relative_eq!(polygon.perimeter_ext(&Euclidean), 64.0); + } + + #[test] + fn test_triangle_perimeter() { + use crate::Triangle; + // Right triangle with sides 3, 4, 5 + let triangle = Triangle::new( + coord! { x: 0., y: 0. }, + coord! { x: 3., y: 0. }, + coord! { x: 0., y: 4. }, + ); + // Length should be 0 (2D geometry) + assert_relative_eq!(triangle.length_ext(&Euclidean), 0.0); + // Perimeter should be 3 + 4 + 5 = 12 + assert_relative_eq!(triangle.perimeter_ext(&Euclidean), 12.0); + } + + #[test] + fn test_rect_perimeter() { + use crate::Rect; + // Rectangle 3x4 + let rect = Rect::new(coord! { x: 0., y: 0. }, coord! { x: 3., y: 4. }); + // Length should be 0 (2D geometry) + assert_relative_eq!(rect.length_ext(&Euclidean), 0.0); + // Perimeter should be 2*(3+4) = 14 + assert_relative_eq!(rect.perimeter_ext(&Euclidean), 14.0); + } + + #[test] + fn test_postgis_compliance_perimeter_scenarios() { + // Test cases based on PostGIS ST_Perimeter behavior to ensure compliance + // These test cases mirror the pytest.mark.parametrize scenarios + + // POINT EMPTY - should return 0 + // Note: We can't easily test empty point, so we test a regular point + let point = Point::new(0.0, 0.0); + assert_relative_eq!(point.perimeter_ext(&Euclidean), 0.0); + + // LINESTRING EMPTY - should return 0 + let empty_linestring: crate::LineString = line_string![]; + assert_relative_eq!(empty_linestring.perimeter_ext(&Euclidean), 0.0); + + // POINT (0 0) - should return 0 + let point_origin = Point::new(0.0, 0.0); + assert_relative_eq!(point_origin.perimeter_ext(&Euclidean), 0.0); + + // LINESTRING (0 0, 0 1) - should return 0 (1D geometry has no perimeter) + let linestring_simple = line_string![(x: 0., y: 0.), (x: 0., y: 1.)]; + assert_relative_eq!(linestring_simple.perimeter_ext(&Euclidean), 0.0); + + // MULTIPOINT ((0 0), (1 1)) - should return 0 + let multipoint = MultiPoint::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]); + assert_relative_eq!(multipoint.perimeter_ext(&Euclidean), 0.0); + + // MULTILINESTRING ((0 0, 1 1), (1 1, 2 2)) - should return 0 (1D geometry has no perimeter) + let multilinestring = MultiLineString::new(vec![ + line_string![(x: 0., y: 0.), (x: 1., y: 1.)], + line_string![(x: 1., y: 1.), (x: 2., y: 2.)], + ]); + assert_relative_eq!(multilinestring.perimeter_ext(&Euclidean), 0.0); + + // POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) - should return 4 (perimeter of unit square) + let polygon_unit_square = polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]; + assert_relative_eq!(polygon_unit_square.perimeter_ext(&Euclidean), 4.0); + + // MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((0 0, 1 0, 1 1, 0 1, 0 0))) - should return 8 (two unit squares) + let multipolygon_two_unit_squares = MultiPolygon::new(vec![ + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ], + ]); + assert_relative_eq!(multipolygon_two_unit_squares.perimeter_ext(&Euclidean), 8.0); + + // GEOMETRYCOLLECTION (POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)), LINESTRING (0 0, 1 1), POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))) + // Should return 8 (only polygons contribute to perimeter: 4 + 0 + 4 = 8) + let geometry_collection_mixed = GeometryCollection::new_from(vec![ + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // contributes 4 + Geometry::LineString(line_string![(x: 0., y: 0.), (x: 1., y: 1.)]), // contributes 0 (1D geometry) + Geometry::Polygon(polygon![ + (x: 0., y: 0.), + (x: 1., y: 0.), + (x: 1., y: 1.), + (x: 0., y: 1.), + (x: 0., y: 0.), + ]), // contributes 4 + ]); + assert_relative_eq!(geometry_collection_mixed.perimeter_ext(&Euclidean), 8.0); + } + + #[test] + fn test_perimeter_vs_length_distinction() { + // This test ensures we correctly distinguish between length and perimeter + // according to PostGIS/OGC standards + + let linestring = line_string![(x: 0., y: 0.), (x: 3., y: 4.)]; // length = 5.0 + let polygon = polygon![(x: 0., y: 0.), (x: 3., y: 0.), (x: 3., y: 4.), (x: 0., y: 4.), (x: 0., y: 0.)]; // perimeter = 14.0 + + // For 1D geometries: length > 0, perimeter = 0 + assert_relative_eq!(linestring.length_ext(&Euclidean), 5.0); + assert_relative_eq!(linestring.perimeter_ext(&Euclidean), 0.0); + + // For 2D geometries: length = 0, perimeter > 0 + assert_relative_eq!(polygon.length_ext(&Euclidean), 0.0); + assert_relative_eq!(polygon.perimeter_ext(&Euclidean), 14.0); + } + + #[test] + fn test_empty_geometry_perimeter() { + // Test empty geometries return 0 perimeter + + // Empty LineString + let empty_ls: crate::LineString = line_string![]; + assert_relative_eq!(empty_ls.perimeter_ext(&Euclidean), 0.0); + + // Empty MultiLineString + let empty_mls = MultiLineString::::new(vec![]); + assert_relative_eq!(empty_mls.perimeter_ext(&Euclidean), 0.0); + + // Empty MultiPoint + let empty_mp = MultiPoint::::new(vec![]); + assert_relative_eq!(empty_mp.perimeter_ext(&Euclidean), 0.0); + + // Empty GeometryCollection + let empty_gc = GeometryCollection::::new_from(vec![]); + assert_relative_eq!(empty_gc.perimeter_ext(&Euclidean), 0.0); + } + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/distance.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/distance.rs new file mode 100644 index 00000000..095485e3 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/distance.rs @@ -0,0 +1,2276 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Euclidean DistanceExt generic implementation +//! +//! Ported (and contains copied code) from `geo::algorithm::line_measures::metric_spaces::euclidean::distance`: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +use crate::{CoordFloat, GeoFloat, Point}; +use num_traits::{Bounded, Float}; +use std::borrow::Borrow; + +// Import all the utility functions from utils module +use super::utils::{ + distance_coord_to_line_generic, + // Symmetric distance functions generated by macro + distance_line_to_line_generic, + distance_line_to_linestring_generic, + distance_line_to_polygon_generic, + distance_linestring_to_polygon_generic, + distance_point_to_linestring_generic, + distance_point_to_point_generic, + distance_point_to_polygon_generic, + distance_polygon_to_polygon_generic, +}; + +// ┌──────────────────────────────────────────────────────────┐ +// │ Generic Trait Distance Extension │ +// └──────────────────────────────────────────────────────────┘ + +use sedona_geo_traits_ext::*; + +/// Extension trait for generic geometry types to calculate distances directly +/// using Euclidean metric space without conversion overhead +/// Supports both same-type and cross-type distance calculations +pub trait DistanceExt { + /// Calculate Euclidean distance using generic traits without conversion overhead + fn distance_ext(&self, other: &Rhs) -> F; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Generic trait macro implementations │ +// └──────────────────────────────────────────────────────────┘ + +/// Generic trait version of polygon-like geometry distance implementation +/// Follows the same pattern as impl_euclidean_distance_for_polygonlike_geometry! +macro_rules! impl_distance_ext_for_polygonlike_geometry_trait { + ($polygonlike_trait:ident, $polygonlike_tag:ident, [$(($geometry_trait:ident, $geometry_tag:ident)),*]) => { + // Self-to-self distance implementation + impl GenericDistanceTrait for LHS + where + F: GeoFloat, + LHS: $polygonlike_trait, + RHS: $polygonlike_trait, + { + fn generic_distance_trait(&self, rhs: &RHS) -> F { + let poly1 = self.to_polygon(); + let poly2 = rhs.to_polygon(); + distance_polygon_to_polygon_generic(&poly1, &poly2) + } + } + }; +} + +// Separate macro to generate individual implementations for each geometry type +macro_rules! impl_polygonlike_to_geometry_distance { + ($polygonlike_trait:ident, $polygonlike_tag:ident, $geometry_trait:ident, $geometry_tag:ident) => { + impl GenericDistanceTrait for PL + where + F: GeoFloat, + PL: $polygonlike_trait, + G: $geometry_trait, + { + fn generic_distance_trait(&self, rhs: &G) -> F { + let poly = self.to_polygon(); + impl_polygonlike_to_geometry_distance!(@call_distance poly, rhs, $geometry_tag) + } + } + }; + + (@call_distance $poly:expr, $rhs:expr, PointTag) => { + distance_point_to_polygon_generic($rhs, &$poly) + }; + (@call_distance $poly:expr, $rhs:expr, LineTag) => { + distance_line_to_polygon_generic($rhs, &$poly) + }; + (@call_distance $poly:expr, $rhs:expr, LineStringTag) => { + distance_linestring_to_polygon_generic($rhs, &$poly) + }; + (@call_distance $poly:expr, $rhs:expr, PolygonTag) => { + distance_polygon_to_polygon_generic(&$poly, $rhs) + }; + (@call_distance $poly:expr, $rhs:expr, RectTag) => { + { + let poly2 = $rhs.to_polygon(); + distance_polygon_to_polygon_generic(&$poly, &poly2) + } + }; + (@call_distance $poly:expr, $rhs:expr, TriangleTag) => { + { + let poly2 = $rhs.to_polygon(); + distance_polygon_to_polygon_generic(&$poly, &poly2) + } + }; + (@call_distance $poly:expr, $rhs:expr, MultiPointTag) => { + { + let mut min_dist: F = Bounded::max_value(); + for coord in $rhs.coord_iter() { + let point = Point::from(coord); + let dist = distance_point_to_polygon_generic(&point, &$poly); + min_dist = min_dist.min(dist); + } + if min_dist == Bounded::max_value() { + F::zero() + } else { + min_dist + } + } + }; + (@call_distance $poly:expr, $rhs:expr, MultiLineStringTag) => { + { + let mut min_dist: F = Bounded::max_value(); + for line_string in $rhs.line_strings_ext() { + let dist = distance_linestring_to_polygon_generic(&line_string, &$poly); + min_dist = min_dist.min(dist); + } + if min_dist == Bounded::max_value() { + F::zero() + } else { + min_dist + } + } + }; + (@call_distance $poly:expr, $rhs:expr, MultiPolygonTag) => { + { + let mut min_dist: F = Bounded::max_value(); + for polygon in $rhs.polygons_ext() { + let dist = distance_polygon_to_polygon_generic(&$poly, &polygon); + min_dist = min_dist.min(dist); + } + if min_dist == Bounded::max_value() { + F::zero() + } else { + min_dist + } + } + }; +} + +/// Generic trait version of multi-geometry distance implementation +/// Follows the same pattern as impl_euclidean_distance_for_iter_geometry! +macro_rules! impl_distance_ext_for_iter_geometry_trait { + ($iter_trait:ident, $iter_tag:ident, $member_method:ident) => { + impl GenericDistanceTrait for LHS + where + F: GeoFloat, + LHS: $iter_trait, + RHS: $iter_trait, + { + fn generic_distance_trait(&self, rhs: &RHS) -> F { + let mut min_dist: F = Float::max_value(); + for member1 in self.$member_method() { + for member2 in rhs.$member_method() { + let dist = member1.distance_ext(&member2); + min_dist = min_dist.min(dist); + } + } + if min_dist == Float::max_value() { + F::zero() + } else { + min_dist + } + } + } + }; +} + +// Array-based macro for systematic implementation generation +macro_rules! impl_cross_type_array { + // Generate multi-geometry self-implementations + (self_multi_geometries: [$(($trait:ident, $tag:ident, $method:ident)),+]) => { + $( + impl_distance_ext_for_iter_geometry_trait!($trait, $tag, $method); + )+ + }; + + // Generate single-to-multi implementations with Point to MultiPoint skip + (single_to_multi: $single_trait:ident, $single_tag:ident => [$(($multi_trait:ident, $multi_tag:ident, $method:ident)),+]) => { + $( + impl_cross_type_array!(@single_to_multi_check $single_trait, $single_tag, $multi_trait, $multi_tag, $method); + )+ + }; + + // Skip Point to MultiPoint (special implementation exists) + (@single_to_multi_check PointTraitExt, PointTag, MultiPointTraitExt, MultiPointTag, $method:ident) => {}; + + // Generate for all other combinations + (@single_to_multi_check $single_trait:ident, $single_tag:ident, $multi_trait:ident, $multi_tag:ident, $method:ident) => { + impl_single_to_multi_geometry_distance!($single_trait, $single_tag, $multi_trait, $multi_tag, $method); + }; + + // Generate symmetric implementations for single-to-multi + (symmetric_single_to_multi: $single_trait:ident, $single_tag:ident => [$(($multi_trait:ident, $multi_tag:ident)),+]) => { + $( + symmetric_distance_ext_trait_impl!(GeoFloat, $multi_trait, $multi_tag, $single_trait, $single_tag); + )+ + }; +} + +/// Macro for implementing cross-type distance calculations from single geometry to multi-geometry types +macro_rules! impl_single_to_multi_geometry_distance { + ($single_trait:ident, $single_tag:ident, $multi_trait:ident, $multi_tag:ident, $member_method:ident) => { + impl GenericDistanceTrait for S + where + F: GeoFloat, + S: $single_trait, + M: $multi_trait, + { + fn generic_distance_trait(&self, rhs: &M) -> F { + let mut min_dist: F = Bounded::max_value(); + for member in rhs.$member_method() { + let dist = self.distance_ext(&member); + min_dist = min_dist.min(dist); + } + if min_dist == Bounded::max_value() { + F::zero() + } else { + min_dist + } + } + } + }; +} + +// Implementation of DistanceExt for cross-type generic trait geometries using the two type-tag pattern +impl DistanceExt for LHS +where + F: GeoFloat, + LHS: GeoTraitExtWithTypeTag, + RHS: GeoTraitExtWithTypeTag, + LHS: GenericDistanceTrait, +{ + fn distance_ext(&self, other: &RHS) -> F { + self.generic_distance_trait(other) + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Internal trait for cross-type distance calculations │ +// └────────────────────────────────────────────────────────────┘ + +// Internal trait for cross-type distance calculations without conversion +trait GenericDistanceTrait +where + F: GeoFloat, +{ + fn generic_distance_trait(&self, rhs: &Rhs) -> F; +} +macro_rules! symmetric_distance_ext_trait_impl { + ($num_type:ident, $lhs_type:ident, $lhs_tag:ident, $rhs_type:ident, $rhs_tag:ident) => { + impl GenericDistanceTrait for LHS + where + F: $num_type, + LHS: $lhs_type, + RHS: $rhs_type, + { + fn generic_distance_trait(&self, rhs: &RHS) -> F { + rhs.generic_distance_trait(self) + } + } + }; +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for Coord (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Coord-to-Coord direct distance implementation +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: CoordTraitExt, + RHS: CoordTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + let delta = self.geo_coord() - rhs.geo_coord(); + delta.x.hypot(delta.y) + } +} + +// Coord-to-Point distance implementation +// The other side (Point-to-Coord) is handled via a symmetric impl or blanket impl +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: CoordTraitExt, + RHS: PointTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + if let Some(rhs_coord) = rhs.coord_ext() { + let delta = self.geo_coord() - rhs_coord.geo_coord(); + delta.x.hypot(delta.y) + } else { + F::zero() + } + } +} + +// Coord-to-Line distance implementation +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: CoordTraitExt, + RHS: LineTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + distance_coord_to_line_generic(self, rhs) + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for Point (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Point-to-Point direct distance implementation +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: PointTraitExt, + RHS: PointTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + distance_point_to_point_generic(self, rhs) + } +} + +// Point-to-Line distance implementation +impl GenericDistanceTrait for P +where + F: GeoFloat, + P: PointTraitExt, + L: LineTraitExt, +{ + fn generic_distance_trait(&self, rhs: &L) -> F { + if let Some(coord) = self.coord_ext() { + distance_coord_to_line_generic(&coord, rhs) + } else { + F::zero() + } + } +} + +// Point-to-LineString distance implementation +impl GenericDistanceTrait for P +where + F: GeoFloat, + P: PointTraitExt, + LS: LineStringTraitExt, +{ + fn generic_distance_trait(&self, rhs: &LS) -> F { + distance_point_to_linestring_generic(self, rhs) + } +} + +// Point-to-Polygon distance implementation +impl GenericDistanceTrait for P +where + F: GeoFloat, + P: PointTraitExt, + Poly: PolygonTraitExt, +{ + fn generic_distance_trait(&self, rhs: &Poly) -> F { + distance_point_to_polygon_generic(self, rhs) + } +} + +// Point to MultiPoint distance implementation +impl GenericDistanceTrait for P +where + F: GeoFloat, + P: PointTraitExt, + MP: MultiPointTraitExt, +{ + fn generic_distance_trait(&self, rhs: &MP) -> F { + if let Some(point_coord) = self.geo_coord() { + let mut min_dist: F = Bounded::max_value(); + for coord in rhs.coord_iter() { + let dist = point_coord.distance_ext(&coord); + min_dist = min_dist.min(dist); + } + if min_dist == Bounded::max_value() { + F::zero() + } else { + min_dist + } + } else { + F::zero() + } + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for Line (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Symmetric Line distance implementations +symmetric_distance_ext_trait_impl!(GeoFloat, LineTraitExt, LineTag, CoordTraitExt, CoordTag); +symmetric_distance_ext_trait_impl!(GeoFloat, LineTraitExt, LineTag, PointTraitExt, PointTag); + +// Line-to-Line direct distance implementation +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: LineTraitExt, + RHS: LineTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + distance_line_to_line_generic(self, rhs) + } +} + +// Line-to-LineString distance implementation +impl GenericDistanceTrait for L +where + F: GeoFloat, + L: LineTraitExt, + LS: LineStringTraitExt, +{ + fn generic_distance_trait(&self, rhs: &LS) -> F { + distance_line_to_linestring_generic(self, rhs) + } +} + +// Line-to-Polygon distance implementation +impl GenericDistanceTrait for L +where + F: GeoFloat, + L: LineTraitExt, + Poly: PolygonTraitExt, +{ + fn generic_distance_trait(&self, rhs: &Poly) -> F { + distance_line_to_polygon_generic(self, rhs) + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for LineString (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Symmetric LineString distance implementations +// LineString-to-Point (symmetric to Point-to-LineString) +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + PointTraitExt, + PointTag +); + +// LineString-to-Line (symmetric to Line-to-LineString) +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + LineTraitExt, + LineTag +); + +// LineString-to-LineString distance implementation +// This general implementation supports both same-type (LS to LS) and different-type (LS1 to LS2) +impl GenericDistanceTrait for LS1 +where + F: GeoFloat, + LS1: LineStringTraitExt, + LS2: LineStringTraitExt, +{ + fn generic_distance_trait(&self, rhs: &LS2) -> F { + let mut min_dist: F = Float::max_value(); + for line1 in self.lines() { + for line2 in rhs.lines() { + // Line-to-line distance using endpoints + let d1 = distance_coord_to_line_generic(&line1.start_coord(), &line2); + let d2 = distance_coord_to_line_generic(&line1.end_coord(), &line2); + let d3 = distance_coord_to_line_generic(&line2.start_coord(), &line1); + let d4 = distance_coord_to_line_generic(&line2.end_coord(), &line1); + let line_dist = d1.min(d2).min(d3).min(d4); + min_dist = min_dist.min(line_dist); + } + } + if min_dist == Float::max_value() { + F::zero() + } else { + min_dist + } + } +} + +// LineString-to-Polygon distance implementation +impl GenericDistanceTrait for LS +where + F: GeoFloat, + LS: LineStringTraitExt, + Poly: PolygonTraitExt, +{ + fn generic_distance_trait(&self, rhs: &Poly) -> F { + distance_linestring_to_polygon_generic(self, rhs) + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for Polygon (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Symmetric Polygon distance implementations +// Polygon-to-Point (symmetric to Point-to-Polygon) +symmetric_distance_ext_trait_impl!( + GeoFloat, + PolygonTraitExt, + PolygonTag, + PointTraitExt, + PointTag +); + +// Polygon-to-Line (symmetric to Line-to-Polygon) +symmetric_distance_ext_trait_impl!(GeoFloat, PolygonTraitExt, PolygonTag, LineTraitExt, LineTag); + +// Polygon-to-LineString (symmetric to LineString-to-Polygon) +symmetric_distance_ext_trait_impl!( + GeoFloat, + PolygonTraitExt, + PolygonTag, + LineStringTraitExt, + LineStringTag +); + +// Polygon-to-Polygon distance implementation +// This general implementation supports both same-type (P to P) and different-type (P1 to P2) +impl GenericDistanceTrait for P1 +where + F: GeoFloat, + P1: PolygonTraitExt, + P2: PolygonTraitExt, +{ + fn generic_distance_trait(&self, rhs: &P2) -> F { + distance_polygon_to_polygon_generic(self, rhs) + } +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for Rect and Triangle (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Triangle implementations +impl_distance_ext_for_polygonlike_geometry_trait!(TriangleTraitExt, TriangleTag, []); +impl_polygonlike_to_geometry_distance!(TriangleTraitExt, TriangleTag, PointTraitExt, PointTag); +impl_polygonlike_to_geometry_distance!(TriangleTraitExt, TriangleTag, LineTraitExt, LineTag); +impl_polygonlike_to_geometry_distance!( + TriangleTraitExt, + TriangleTag, + LineStringTraitExt, + LineStringTag +); +impl_polygonlike_to_geometry_distance!(TriangleTraitExt, TriangleTag, PolygonTraitExt, PolygonTag); +impl_polygonlike_to_geometry_distance!(TriangleTraitExt, TriangleTag, RectTraitExt, RectTag); +impl_polygonlike_to_geometry_distance!( + TriangleTraitExt, + TriangleTag, + MultiPointTraitExt, + MultiPointTag +); +impl_polygonlike_to_geometry_distance!( + TriangleTraitExt, + TriangleTag, + MultiLineStringTraitExt, + MultiLineStringTag +); +impl_polygonlike_to_geometry_distance!( + TriangleTraitExt, + TriangleTag, + MultiPolygonTraitExt, + MultiPolygonTag +); + +// Symmetric implementations for Triangle +symmetric_distance_ext_trait_impl!( + GeoFloat, + PointTraitExt, + PointTag, + TriangleTraitExt, + TriangleTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineTraitExt, + LineTag, + TriangleTraitExt, + TriangleTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + TriangleTraitExt, + TriangleTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + PolygonTraitExt, + PolygonTag, + TriangleTraitExt, + TriangleTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + RectTraitExt, + RectTag, + TriangleTraitExt, + TriangleTag +); + +// Rect implementations +impl_distance_ext_for_polygonlike_geometry_trait!(RectTraitExt, RectTag, []); +impl_polygonlike_to_geometry_distance!(RectTraitExt, RectTag, PointTraitExt, PointTag); +impl_polygonlike_to_geometry_distance!(RectTraitExt, RectTag, LineTraitExt, LineTag); +impl_polygonlike_to_geometry_distance!(RectTraitExt, RectTag, LineStringTraitExt, LineStringTag); +impl_polygonlike_to_geometry_distance!(RectTraitExt, RectTag, PolygonTraitExt, PolygonTag); +impl_polygonlike_to_geometry_distance!(RectTraitExt, RectTag, MultiPointTraitExt, MultiPointTag); +impl_polygonlike_to_geometry_distance!( + RectTraitExt, + RectTag, + MultiLineStringTraitExt, + MultiLineStringTag +); +impl_polygonlike_to_geometry_distance!( + RectTraitExt, + RectTag, + MultiPolygonTraitExt, + MultiPolygonTag +); + +// Symmetric implementations for Rect (excluding Triangle which is already handled above) +symmetric_distance_ext_trait_impl!(GeoFloat, PointTraitExt, PointTag, RectTraitExt, RectTag); +symmetric_distance_ext_trait_impl!(GeoFloat, LineTraitExt, LineTag, RectTraitExt, RectTag); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + RectTraitExt, + RectTag +); +symmetric_distance_ext_trait_impl!(GeoFloat, PolygonTraitExt, PolygonTag, RectTraitExt, RectTag); + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementations for multi-geometry types (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Multi-geometry self-implementations +impl_cross_type_array!(self_multi_geometries: [ + (MultiPointTraitExt, MultiPointTag, points_ext), + (MultiLineStringTraitExt, MultiLineStringTag, line_strings_ext), + (MultiPolygonTraitExt, MultiPolygonTag, polygons_ext) +]); + +// Single-geometry to multi-geometry implementations +impl_cross_type_array!(single_to_multi: PointTraitExt, PointTag => [ + (MultiPointTraitExt, MultiPointTag, points_ext), + (MultiLineStringTraitExt, MultiLineStringTag, line_strings_ext), + (MultiPolygonTraitExt, MultiPolygonTag, polygons_ext) +]); + +impl_cross_type_array!(single_to_multi: LineTraitExt, LineTag => [ + (MultiPointTraitExt, MultiPointTag, points_ext), + (MultiLineStringTraitExt, MultiLineStringTag, line_strings_ext), + (MultiPolygonTraitExt, MultiPolygonTag, polygons_ext) +]); + +impl_cross_type_array!(single_to_multi: LineStringTraitExt, LineStringTag => [ + (MultiPointTraitExt, MultiPointTag, points_ext), + (MultiLineStringTraitExt, MultiLineStringTag, line_strings_ext), + (MultiPolygonTraitExt, MultiPolygonTag, polygons_ext) +]); + +impl_cross_type_array!(single_to_multi: PolygonTraitExt, PolygonTag => [ + (MultiPointTraitExt, MultiPointTag, points_ext), + (MultiLineStringTraitExt, MultiLineStringTag, line_strings_ext), + (MultiPolygonTraitExt, MultiPolygonTag, polygons_ext) +]); + +// Multi-geometry to multi-geometry implementations +impl_single_to_multi_geometry_distance!( + MultiPointTraitExt, + MultiPointTag, + MultiLineStringTraitExt, + MultiLineStringTag, + line_strings_ext +); +impl_single_to_multi_geometry_distance!( + MultiPointTraitExt, + MultiPointTag, + MultiPolygonTraitExt, + MultiPolygonTag, + polygons_ext +); +impl_single_to_multi_geometry_distance!( + MultiLineStringTraitExt, + MultiLineStringTag, + MultiPointTraitExt, + MultiPointTag, + points_ext +); +impl_single_to_multi_geometry_distance!( + MultiLineStringTraitExt, + MultiLineStringTag, + MultiPolygonTraitExt, + MultiPolygonTag, + polygons_ext +); +impl_single_to_multi_geometry_distance!( + MultiPolygonTraitExt, + MultiPolygonTag, + MultiPointTraitExt, + MultiPointTag, + points_ext +); +impl_single_to_multi_geometry_distance!( + MultiPolygonTraitExt, + MultiPolygonTag, + MultiLineStringTraitExt, + MultiLineStringTag, + line_strings_ext +); + +// Symmetric implementations +impl_cross_type_array!(symmetric_single_to_multi: PointTraitExt, PointTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +impl_cross_type_array!(symmetric_single_to_multi: LineTraitExt, LineTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +impl_cross_type_array!(symmetric_single_to_multi: LineStringTraitExt, LineStringTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +impl_cross_type_array!(symmetric_single_to_multi: PolygonTraitExt, PolygonTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +impl_cross_type_array!(symmetric_single_to_multi: RectTraitExt, RectTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +impl_cross_type_array!(symmetric_single_to_multi: TriangleTraitExt, TriangleTag => [ + (MultiPointTraitExt, MultiPointTag), + (MultiLineStringTraitExt, MultiLineStringTag), + (MultiPolygonTraitExt, MultiPolygonTag) +]); + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementation for GeometryCollection (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Generate implementations for GeometryCollection by delegating to the Geometry implementation +macro_rules! impl_distance_geometry_collection_from_geometry { + ($rhs_type:ident, $rhs_tag:ident) => { + impl GenericDistanceTrait for LHS + where + F: GeoFloat, + LHS: GeometryCollectionTraitExt, + RHS: $rhs_type, + { + fn generic_distance_trait(&self, rhs: &RHS) -> F { + use num_traits::Bounded; + + // Use distance_ext which will route through the appropriate implementations + // The key insight is that this works for all geometry types except GeometryCollection, + // where we need special handling to avoid infinite recursion + self.geometries_ext() + .map(|geom| geom.distance_ext(rhs)) + .fold(Bounded::max_value(), |acc, dist| acc.min(dist)) + } + } + }; +} + +impl_distance_geometry_collection_from_geometry!(PointTraitExt, PointTag); +impl_distance_geometry_collection_from_geometry!(LineTraitExt, LineTag); +impl_distance_geometry_collection_from_geometry!(LineStringTraitExt, LineStringTag); +impl_distance_geometry_collection_from_geometry!(PolygonTraitExt, PolygonTag); +impl_distance_geometry_collection_from_geometry!(MultiPointTraitExt, MultiPointTag); +impl_distance_geometry_collection_from_geometry!(MultiLineStringTraitExt, MultiLineStringTag); +impl_distance_geometry_collection_from_geometry!(MultiPolygonTraitExt, MultiPolygonTag); +impl_distance_geometry_collection_from_geometry!(RectTraitExt, RectTag); +impl_distance_geometry_collection_from_geometry!(TriangleTraitExt, TriangleTag); + +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: GeometryCollectionTraitExt, + RHS: GeometryCollectionTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + let mut min_distance = ::max_value(); + for lhs_geom in self.geometries_ext() { + for rhs_geom in rhs.geometries_ext() { + let distance = lhs_geom.distance_ext(&rhs_geom); + min_distance = min_distance.min(distance); + + // Early exit optimization + if distance == F::zero() { + return F::zero(); + } + } + } + + min_distance + } +} + +symmetric_distance_ext_trait_impl!( + GeoFloat, + PointTraitExt, + PointTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineTraitExt, + LineTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + PolygonTraitExt, + PolygonTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiPointTraitExt, + MultiPointTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiLineStringTraitExt, + MultiLineStringTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiPolygonTraitExt, + MultiPolygonTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + RectTraitExt, + RectTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + TriangleTraitExt, + TriangleTag, + GeometryCollectionTraitExt, + GeometryCollectionTag +); + +// ┌────────────────────────────────────────────────────────────┐ +// │ Implementation for Geometry (generic traits) │ +// └────────────────────────────────────────────────────────────┘ + +// Generate implementations for Geometry with other types using conversion +macro_rules! impl_distance_geometry_to_type { + ($rhs_type:ident, $rhs_tag:ident) => { + impl GenericDistanceTrait for LHS + where + F: GeoFloat, + LHS: GeometryTraitExt, + RHS: $rhs_type, + { + fn generic_distance_trait(&self, rhs: &RHS) -> F { + if self.is_collection() { + let mut min_distance = ::max_value(); + for lhs_geom in self.geometries_ext() { + let lhs_geom = lhs_geom.borrow(); + let distance = lhs_geom.generic_distance_trait(rhs); + min_distance = min_distance.min(distance); + + // Early exit optimization + if distance == F::zero() { + return F::zero(); + } + } + min_distance + } else { + match self.as_type_ext() { + sedona_geo_traits_ext::GeometryTypeExt::Point(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Line(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::LineString(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Polygon(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::MultiPoint(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::MultiLineString(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::MultiPolygon(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Rect(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Triangle(g) => { + g.generic_distance_trait(rhs) + } + } + } + } + } + }; +} + +impl_distance_geometry_to_type!(PointTraitExt, PointTag); +impl_distance_geometry_to_type!(LineTraitExt, LineTag); +impl_distance_geometry_to_type!(LineStringTraitExt, LineStringTag); +impl_distance_geometry_to_type!(PolygonTraitExt, PolygonTag); +impl_distance_geometry_to_type!(MultiPointTraitExt, MultiPointTag); +impl_distance_geometry_to_type!(MultiLineStringTraitExt, MultiLineStringTag); +impl_distance_geometry_to_type!(MultiPolygonTraitExt, MultiPolygonTag); +impl_distance_geometry_to_type!(RectTraitExt, RectTag); +impl_distance_geometry_to_type!(TriangleTraitExt, TriangleTag); +impl_distance_geometry_to_type!(GeometryCollectionTraitExt, GeometryCollectionTag); + +symmetric_distance_ext_trait_impl!( + GeoFloat, + PointTraitExt, + PointTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineTraitExt, + LineTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + LineStringTraitExt, + LineStringTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + PolygonTraitExt, + PolygonTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiPointTraitExt, + MultiPointTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiLineStringTraitExt, + MultiLineStringTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + MultiPolygonTraitExt, + MultiPolygonTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + RectTraitExt, + RectTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + TriangleTraitExt, + TriangleTag, + GeometryTraitExt, + GeometryTag +); +symmetric_distance_ext_trait_impl!( + GeoFloat, + GeometryCollectionTraitExt, + GeometryCollectionTag, + GeometryTraitExt, + GeometryTag +); + +impl GenericDistanceTrait for LHS +where + F: GeoFloat, + LHS: GeometryTraitExt, + RHS: GeometryTraitExt, +{ + fn generic_distance_trait(&self, rhs: &RHS) -> F { + if self.is_collection() { + let mut min_distance = ::max_value(); + for lhs_geom in self.geometries_ext() { + let lhs_geom = lhs_geom.borrow(); + let distance = lhs_geom.generic_distance_trait(rhs); + min_distance = min_distance.min(distance); + + // Early exit optimization + if distance == F::zero() { + return F::zero(); + } + } + min_distance + } else { + match self.as_type_ext() { + sedona_geo_traits_ext::GeometryTypeExt::Point(g) => g.generic_distance_trait(rhs), + sedona_geo_traits_ext::GeometryTypeExt::Line(g) => g.generic_distance_trait(rhs), + sedona_geo_traits_ext::GeometryTypeExt::LineString(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Polygon(g) => g.generic_distance_trait(rhs), + sedona_geo_traits_ext::GeometryTypeExt::MultiPoint(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::MultiLineString(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::MultiPolygon(g) => { + g.generic_distance_trait(rhs) + } + sedona_geo_traits_ext::GeometryTypeExt::Rect(g) => g.generic_distance_trait(rhs), + sedona_geo_traits_ext::GeometryTypeExt::Triangle(g) => { + g.generic_distance_trait(rhs) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Line, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon}; + use geo::orient::{Direction, Orient}; + use geo_types::{coord, polygon, private_utils::line_segment_distance}; + + mod distance_cross_validation_tests { + use geo::{Coord, Distance, Euclidean, Geometry, GeometryCollection, Rect, Triangle}; + + use super::*; + + #[test] + fn line_segment_distance_test() { + let o1 = Point::new(8.0, 0.0); + let o2 = Point::new(5.5, 0.0); + let o3 = Point::new(5.0, 0.0); + let o4 = Point::new(4.5, 1.5); + + let p1 = Point::new(7.2, 2.0); + let p2 = Point::new(6.0, 1.0); + + // Test original implementation + let dist = line_segment_distance(o1, p1, p2); + let dist2 = line_segment_distance(o2, p1, p2); + let dist3 = line_segment_distance(o3, p1, p2); + let dist4 = line_segment_distance(o4, p1, p2); + // Results agree with Shapely + assert_relative_eq!(dist, 2.0485900789263356); + assert_relative_eq!(dist2, 1.118033988749895); + assert_relative_eq!(dist3, std::f64::consts::SQRT_2); // workaround clippy::correctness error approx_constant (1.4142135623730951) + assert_relative_eq!(dist4, 1.5811388300841898); + // Point is on the line + let zero_dist = line_segment_distance(p1, p1, p2); + assert_relative_eq!(zero_dist, 0.0); + + // Test generic implementation + if let (Some(p1_coord), Some(p2_coord)) = (p1.coord_ext(), p2.coord_ext()) { + let line_seg = Line::new(p1_coord, p2_coord); + + if let Some(o1_coord) = o1.coord_ext() { + let generic_dist = distance_coord_to_line_generic(&o1_coord, &line_seg); + assert_relative_eq!(generic_dist, 2.0485900789263356); + assert_relative_eq!(dist, generic_dist); + } + if let Some(o2_coord) = o2.coord_ext() { + let generic_dist2 = distance_coord_to_line_generic(&o2_coord, &line_seg); + assert_relative_eq!(generic_dist2, 1.118033988749895); + assert_relative_eq!(dist2, generic_dist2); + } + if let Some(o3_coord) = o3.coord_ext() { + let generic_dist3 = distance_coord_to_line_generic(&o3_coord, &line_seg); + assert_relative_eq!(generic_dist3, std::f64::consts::SQRT_2); + assert_relative_eq!(dist3, generic_dist3); + } + if let Some(o4_coord) = o4.coord_ext() { + let generic_dist4 = distance_coord_to_line_generic(&o4_coord, &line_seg); + assert_relative_eq!(generic_dist4, 1.5811388300841898); + assert_relative_eq!(dist4, generic_dist4); + } + if let Some(p1_coord_zero) = p1.coord_ext() { + let generic_zero_dist = + distance_coord_to_line_generic(&p1_coord_zero, &line_seg); + assert_relative_eq!(generic_zero_dist, 0.0); + assert_relative_eq!(zero_dist, generic_zero_dist); + } + } + } + #[test] + // Point to Polygon, outside point + fn point_polygon_distance_outside_test() { + // an octagon + let points = vec![ + (5., 1.), + (4., 2.), + (4., 3.), + (5., 4.), + (6., 4.), + (7., 3.), + (7., 2.), + (6., 1.), + (5., 1.), + ]; + let ls = LineString::from(points); + let poly = Polygon::new(ls, vec![]); + // A Random point outside the octagon + let p = Point::new(2.5, 0.5); + + // Test original implementation + let dist = Euclidean.distance(&p, &poly); + assert_relative_eq!(dist, 2.1213203435596424); + + // Test generic implementation + let generic_dist = p.distance_ext(&poly); + assert_relative_eq!(generic_dist, 2.1213203435596424); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to Polygon, inside point + fn point_polygon_distance_inside_test() { + // an octagon + let points = vec![ + (5., 1.), + (4., 2.), + (4., 3.), + (5., 4.), + (6., 4.), + (7., 3.), + (7., 2.), + (6., 1.), + (5., 1.), + ]; + let ls = LineString::from(points); + let poly = Polygon::new(ls, vec![]); + // A Random point inside the octagon + let p = Point::new(5.5, 2.1); + + // Test original implementation + let dist = Euclidean.distance(&p, &poly); + assert_relative_eq!(dist, 0.0); + + // Test generic implementation + let generic_dist = p.distance_ext(&poly); + assert_relative_eq!(generic_dist, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to Polygon, on boundary + fn point_polygon_distance_boundary_test() { + // an octagon + let points = vec![ + (5., 1.), + (4., 2.), + (4., 3.), + (5., 4.), + (6., 4.), + (7., 3.), + (7., 2.), + (6., 1.), + (5., 1.), + ]; + let ls = LineString::from(points); + let poly = Polygon::new(ls, vec![]); + // A point on the octagon + let p = Point::new(5.0, 1.0); + + // Test original implementation + let dist = Euclidean.distance(&p, &poly); + assert_relative_eq!(dist, 0.0); + + // Test generic implementation + let generic_dist = p.distance_ext(&poly); + assert_relative_eq!(generic_dist, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to Polygon, on boundary + fn point_polygon_boundary_test2() { + let exterior = LineString::from(vec![ + (0., 0.), + (0., 0.0004), + (0.0004, 0.0004), + (0.0004, 0.), + (0., 0.), + ]); + + let poly = Polygon::new(exterior, vec![]); + let bugged_point = Point::new(0.0001, 0.); + + // Test original implementation + let distance = Euclidean.distance(&poly, &bugged_point); + assert_relative_eq!(distance, 0.); + + // Test generic implementation + let generic_distance = poly.distance_ext(&bugged_point); + assert_relative_eq!(generic_distance, 0.); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // Point to Polygon, empty Polygon + fn point_polygon_empty_test() { + // an empty Polygon + let points = vec![]; + let ls = LineString::new(points); + let poly = Polygon::new(ls, vec![]); + // A point on the octagon + let p = Point::new(2.5, 0.5); + + // Test original implementation + let dist = Euclidean.distance(&p, &poly); + assert_relative_eq!(dist, 0.0); + + // Test generic implementation + let generic_dist = p.distance_ext(&poly); + assert_relative_eq!(generic_dist, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to Polygon with an interior ring + fn point_polygon_interior_cutout_test() { + // an octagon + let ext_points = vec![ + (4., 1.), + (5., 2.), + (5., 3.), + (4., 4.), + (3., 4.), + (2., 3.), + (2., 2.), + (3., 1.), + (4., 1.), + ]; + // cut out a triangle inside octagon + let int_points = vec![(3.5, 3.5), (4.4, 1.5), (2.6, 1.5), (3.5, 3.5)]; + let ls_ext = LineString::from(ext_points); + let ls_int = LineString::from(int_points); + let poly = Polygon::new(ls_ext, vec![ls_int]); + // A point inside the cutout triangle + let p = Point::new(3.5, 2.5); + + // Test original implementation + let dist = Euclidean.distance(&p, &poly); + // 0.41036467732879783 <-- Shapely + assert_relative_eq!(dist, 0.41036467732879767); + + // Test generic implementation + let generic_dist = p.distance_ext(&poly); + assert_relative_eq!(generic_dist, 0.41036467732879767); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + + #[test] + fn line_distance_multipolygon_do_not_intersect_test() { + // checks that the distance from the multipolygon + // is equal to the distance from the closest polygon + // taken in isolation, whatever that distance is + let ls1 = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (5.0, 15.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let ls2 = LineString::from(vec![ + (0.0, 30.0), + (0.0, 25.0), + (10.0, 25.0), + (10.0, 30.0), + (0.0, 30.0), + ]); + let ls3 = LineString::from(vec![ + (15.0, 30.0), + (15.0, 25.0), + (20.0, 25.0), + (20.0, 30.0), + (15.0, 30.0), + ]); + let pol1 = Polygon::new(ls1, vec![]); + let pol2 = Polygon::new(ls2, vec![]); + let pol3 = Polygon::new(ls3, vec![]); + let mp = MultiPolygon::new(vec![pol1.clone(), pol2, pol3]); + let pnt1 = Point::new(0.0, 15.0); + let pnt2 = Point::new(10.0, 20.0); + let ln = Line::new(pnt1.0, pnt2.0); + + // Test original implementation + let dist_mp_ln = Euclidean.distance(&ln, &mp); + let dist_pol1_ln = Euclidean.distance(&ln, &pol1); + assert_relative_eq!(dist_mp_ln, dist_pol1_ln); + + // Test generic implementation - compare line to polygon + let generic_dist_pol1_ln = ln.distance_ext(&pol1); + assert_relative_eq!(generic_dist_pol1_ln, dist_pol1_ln); + + // Ensure both implementations agree for the single polygon case + assert_relative_eq!(dist_pol1_ln, generic_dist_pol1_ln); + } + + #[test] + fn point_distance_multipolygon_test() { + let ls1 = LineString::from(vec![(0.0, 0.0), (1.0, 10.0), (2.0, 0.0), (0.0, 0.0)]); + let ls2 = LineString::from(vec![(3.0, 0.0), (4.0, 10.0), (5.0, 0.0), (3.0, 0.0)]); + let p1 = Polygon::new(ls1, vec![]); + let p2 = Polygon::new(ls2, vec![]); + let mp = MultiPolygon::new(vec![p1.clone(), p2.clone()]); + let p = Point::new(50.0, 50.0); + + // Test original implementation + let distance = Euclidean.distance(&p, &mp); + assert_relative_eq!(distance, 60.959002616512684); + + // Test generic implementation + let generic_dist = mp.distance_ext(&p); + assert_relative_eq!(generic_dist, 60.959002616512684); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_dist); + } + #[test] + // Point to LineString + fn point_linestring_distance_test() { + // like an octagon, but missing the lowest horizontal segment + let points = vec![ + (5., 1.), + (4., 2.), + (4., 3.), + (5., 4.), + (6., 4.), + (7., 3.), + (7., 2.), + (6., 1.), + ]; + let ls = LineString::from(points); + // A Random point "inside" the LineString + let p = Point::new(5.5, 2.1); + + // Test original implementation + let dist = Euclidean.distance(&p, &ls); + assert_relative_eq!(dist, 1.1313708498984762); + + // Test generic implementation + let generic_dist = p.distance_ext(&ls); + assert_relative_eq!(generic_dist, 1.1313708498984762); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to LineString, point lies on the LineString + fn point_linestring_contains_test() { + // like an octagon, but missing the lowest horizontal segment + let points = vec![ + (5., 1.), + (4., 2.), + (4., 3.), + (5., 4.), + (6., 4.), + (7., 3.), + (7., 2.), + (6., 1.), + ]; + let ls = LineString::from(points); + // A point which lies on the LineString + let p = Point::new(5.0, 4.0); + + // Test original implementation + let dist = Euclidean.distance(&p, &ls); + assert_relative_eq!(dist, 0.0); + + // Test generic implementation + let generic_dist = p.distance_ext(&ls); + assert_relative_eq!(generic_dist, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to LineString, closed triangle + fn point_linestring_triangle_test() { + let points = vec![(3.5, 3.5), (4.4, 2.0), (2.6, 2.0), (3.5, 3.5)]; + let ls = LineString::from(points); + let p = Point::new(3.5, 2.5); + + // Test original implementation + let dist = Euclidean.distance(&p, &ls); + assert_relative_eq!(dist, 0.5); + + // Test generic implementation + let generic_dist = p.distance_ext(&ls); + assert_relative_eq!(generic_dist, 0.5); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + // Point to LineString, empty LineString + fn point_linestring_empty_test() { + let points = vec![]; + let ls = LineString::new(points); + let p = Point::new(5.0, 4.0); + + // Test original implementation + let dist = Euclidean.distance(&p, &ls); + assert_relative_eq!(dist, 0.0); + + // Test generic implementation + let generic_dist = p.distance_ext(&ls); + assert_relative_eq!(generic_dist, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + fn distance_multilinestring_test() { + let v1 = LineString::from(vec![(0.0, 0.0), (1.0, 10.0)]); + let v2 = LineString::from(vec![(1.0, 10.0), (2.0, 0.0), (3.0, 1.0)]); + let mls = MultiLineString::new(vec![v1.clone(), v2.clone()]); + let p = Point::new(50.0, 50.0); + + // Test original implementation + let distance = Euclidean.distance(&p, &mls); + assert_relative_eq!(distance, 63.25345840347388); + + // Test generic implementation + let generic_dist = p.distance_ext(&mls); + assert_relative_eq!(generic_dist, 63.25345840347388); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_dist); + } + #[test] + fn distance1_test() { + let p1 = Point::new(0., 0.); + let p2 = Point::new(1., 0.); + + // Test original implementation + let distance = Euclidean.distance(&p1, &p2); + assert_relative_eq!(distance, 1.); + + // Test generic implementation + let generic_distance = distance_point_to_point_generic(&p1, &p2); + assert_relative_eq!(generic_distance, 1.); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + fn distance2_test() { + let p1 = Point::new(-72.1235, 42.3521); + let p2 = Point::new(72.1260, 70.612); + + // Test original implementation + let dist = Euclidean.distance(&p1, &p2); + assert_relative_eq!(dist, 146.99163308930207); + + // Test generic implementation + let generic_dist = distance_point_to_point_generic(&p1, &p2); + assert_relative_eq!(generic_dist, 146.99163308930207); + + // Ensure both implementations agree + assert_relative_eq!(dist, generic_dist); + } + #[test] + fn distance_multipoint_test() { + let v = vec![ + Point::new(0.0, 10.0), + Point::new(1.0, 1.0), + Point::new(10.0, 0.0), + Point::new(1.0, -1.0), + Point::new(0.0, -10.0), + Point::new(-1.0, -1.0), + Point::new(-10.0, 0.0), + Point::new(-1.0, 1.0), + Point::new(0.0, 10.0), + ]; + let mp = MultiPoint::new(v.clone()); + let p = Point::new(50.0, 50.0); + + // Test original implementation + let distance = Euclidean.distance(&p, &mp); + assert_relative_eq!(distance, 64.03124237432849); + + let generic_dist = mp.distance_ext(&p); + // Ensure both implementations agree + assert_relative_eq!(distance, generic_dist); + } + #[test] + fn distance_line_test() { + let line0 = Line::from([(0., 0.), (5., 0.)]); + let p0 = Point::new(2., 3.); + let p1 = Point::new(3., 0.); + let p2 = Point::new(6., 0.); + + // Test original implementation + let dist_line_p0 = Euclidean.distance(&line0, &p0); + let dist_p0_line = Euclidean.distance(&p0, &line0); + assert_relative_eq!(dist_line_p0, 3.); + assert_relative_eq!(dist_p0_line, 3.); + + let dist_line_p1 = Euclidean.distance(&line0, &p1); + let dist_p1_line = Euclidean.distance(&p1, &line0); + assert_relative_eq!(dist_line_p1, 0.); + assert_relative_eq!(dist_p1_line, 0.); + + let dist_line_p2 = Euclidean.distance(&line0, &p2); + let dist_p2_line = Euclidean.distance(&p2, &line0); + assert_relative_eq!(dist_line_p2, 1.); + assert_relative_eq!(dist_p2_line, 1.); + + // Test generic implementation + let generic_dist_p0 = if let Some(coord) = p0.coord_ext() { + distance_coord_to_line_generic(&coord, &line0) + } else { + 0.0 + }; + let generic_dist_p1 = if let Some(coord) = p1.coord_ext() { + distance_coord_to_line_generic(&coord, &line0) + } else { + 0.0 + }; + let generic_dist_p2 = if let Some(coord) = p2.coord_ext() { + distance_coord_to_line_generic(&coord, &line0) + } else { + 0.0 + }; + + assert_relative_eq!(generic_dist_p0, 3.); + assert_relative_eq!(generic_dist_p1, 0.); + assert_relative_eq!(generic_dist_p2, 1.); + + // Ensure both implementations agree + assert_relative_eq!(dist_line_p0, generic_dist_p0); + assert_relative_eq!(dist_p0_line, generic_dist_p0); + assert_relative_eq!(dist_line_p1, generic_dist_p1); + assert_relative_eq!(dist_p1_line, generic_dist_p1); + assert_relative_eq!(dist_line_p2, generic_dist_p2); + assert_relative_eq!(dist_p2_line, generic_dist_p2); + } + #[test] + fn distance_line_line_test() { + let line0 = Line::from([(0., 0.), (5., 0.)]); + let line1 = Line::from([(2., 1.), (7., 2.)]); + + // Test original implementation + let distance01 = Euclidean.distance(&line0, &line1); + let distance10 = Euclidean.distance(&line1, &line0); + assert_relative_eq!(distance01, 1.); + assert_relative_eq!(distance10, 1.); + + // Test generic implementation + let generic_distance01 = line0.distance_ext(&line1); + let generic_distance10 = line1.distance_ext(&line0); + assert_relative_eq!(generic_distance01, 1.); + assert_relative_eq!(generic_distance10, 1.); + + // Ensure both implementations agree + assert_relative_eq!(distance01, generic_distance01); + assert_relative_eq!(distance10, generic_distance10); + } + #[test] + // See https://github.com/georust/geo/issues/476 + fn distance_line_polygon_test() { + let line = Line::new( + coord! { + x: -0.17084137691985102, + y: 0.8748085493016657, + }, + coord! { + x: -0.17084137691985102, + y: 0.09858870312437906, + }, + ); + let poly: Polygon = polygon![ + coord! { + x: -0.10781391405721802, + y: -0.15433610862574643, + }, + coord! { + x: -0.7855276236615211, + y: 0.23694208404779793, + }, + coord! { + x: -0.7855276236615214, + y: -0.5456143012992907, + }, + coord! { + x: -0.10781391405721802, + y: -0.15433610862574643, + }, + ]; + + // Test original implementation + let distance = Euclidean.distance(&line, &poly); + assert_eq!(distance, 0.18752558079168907); + + // Test generic implementation + let generic_distance = line.distance_ext(&poly); + assert_relative_eq!(generic_distance, 0.18752558079168907); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // test edge-vertex minimum distance + fn test_minimum_polygon_distance() { + let points_raw = [ + (126., 232.), + (126., 212.), + (112., 202.), + (97., 204.), + (87., 215.), + (87., 232.), + (100., 246.), + (118., 247.), + ]; + let points = points_raw + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly1 = Polygon::new(LineString::from(points), vec![]); + + let points_raw_2 = [ + (188., 231.), + (189., 207.), + (174., 196.), + (164., 196.), + (147., 220.), + (158., 242.), + (177., 242.), + ]; + let points2 = points_raw_2 + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly2 = Polygon::new(LineString::from(points2), vec![]); + + // Test generic implementation + let generic_dist = poly1.exterior().distance_ext(poly2.exterior()); + assert_relative_eq!(generic_dist, 21.0); + } + #[test] + // test vertex-vertex minimum distance + fn test_minimum_polygon_distance_2() { + let points_raw = [ + (118., 200.), + (153., 179.), + (106., 155.), + (88., 190.), + (118., 200.), + ]; + let points = points_raw + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly1 = Polygon::new(LineString::from(points), vec![]); + + let points_raw_2 = [ + (242., 186.), + (260., 146.), + (182., 175.), + (216., 193.), + (242., 186.), + ]; + let points2 = points_raw_2 + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly2 = Polygon::new(LineString::from(points2), vec![]); + + // Test generic implementation + let generic_dist = poly1.exterior().distance_ext(poly2.exterior()); + assert_relative_eq!(generic_dist, 29.274562336608895); + } + #[test] + // test edge-edge minimum distance + fn test_minimum_polygon_distance_3() { + let points_raw = [ + (182., 182.), + (182., 168.), + (138., 160.), + (136., 193.), + (182., 182.), + ]; + let points = points_raw + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly1 = Polygon::new(LineString::from(points), vec![]); + + let points_raw_2 = [ + (232., 196.), + (234., 150.), + (194., 165.), + (194., 191.), + (232., 196.), + ]; + let points2 = points_raw_2 + .iter() + .map(|e| Point::new(e.0, e.1)) + .collect::>(); + let poly2 = Polygon::new(LineString::from(points2), vec![]); + + // Test generic implementation + let generic_dist = poly1.exterior().distance_ext(poly2.exterior()); + assert_relative_eq!(generic_dist, 12.0); + } + #[test] + fn test_large_polygon_distance() { + let ls = sedona_testing::fixtures::norway_main::(); + let poly1 = Polygon::new(ls, vec![]); + let vec2 = vec![ + (4.921875, 66.33750501996518), + (3.69140625, 65.21989393613207), + (6.15234375, 65.07213008560697), + (4.921875, 66.33750501996518), + ]; + let poly2 = Polygon::new(vec2.into(), vec![]); + + // Test original implementation + let distance = Euclidean.distance(&poly1, &poly2); + // GEOS says 2.2864896295566055 + assert_relative_eq!(distance, 2.2864896295566055); + + // Test generic implementation + let generic_distance = poly1.distance_ext(&poly2); + assert_relative_eq!(generic_distance, 2.2864896295566055); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // A polygon inside another polygon's ring; they're disjoint in the DE-9IM sense: + // FF2FF1212 + fn test_poly_in_ring() { + let shell = sedona_testing::fixtures::shell::(); + let ring = sedona_testing::fixtures::ring::(); + let poly_in_ring = sedona_testing::fixtures::poly_in_ring::(); + // inside is "inside" outside's ring, but they are disjoint + let outside = Polygon::new(shell, vec![ring]); + let inside = Polygon::new(poly_in_ring, vec![]); + + // Test original implementation + let distance = Euclidean.distance(&outside, &inside); + assert_relative_eq!(distance, 5.992772737231033); + + // Test generic implementation + let generic_distance = outside.distance_ext(&inside); + assert_relative_eq!(generic_distance, 5.992772737231033); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // two ring LineStrings; one encloses the other but they neither touch nor intersect + fn test_linestring_distance() { + let ring = sedona_testing::fixtures::ring::(); + let poly_in_ring = sedona_testing::fixtures::poly_in_ring::(); + + // Test original implementation + let distance = Euclidean.distance(&ring, &poly_in_ring); + assert_relative_eq!(distance, 5.992772737231033); + + // Test generic implementation + let generic_distance = ring.distance_ext(&poly_in_ring); + assert_relative_eq!(generic_distance, 5.992772737231033); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // Line-Polygon test: closest point on Polygon is NOT nearest to a Line end-point + fn test_line_polygon_simple() { + let line = Line::from([(0.0, 0.0), (0.0, 3.0)]); + let v = vec![(5.0, 1.0), (5.0, 2.0), (0.25, 1.5), (5.0, 1.0)]; + let poly = Polygon::new(v.into(), vec![]); + + // Test original implementation + let distance = Euclidean.distance(&line, &poly); + assert_relative_eq!(distance, 0.25); + + // Test generic implementation + let generic_distance = line.distance_ext(&poly); + assert_relative_eq!(generic_distance, 0.25); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // Line-Polygon test: Line intersects Polygon + fn test_line_polygon_intersects() { + let line = Line::from([(0.5, 0.0), (0.0, 3.0)]); + let v = vec![(5.0, 1.0), (5.0, 2.0), (0.25, 1.5), (5.0, 1.0)]; + let poly = Polygon::new(v.into(), vec![]); + + // Test original implementation + let distance = Euclidean.distance(&line, &poly); + assert_relative_eq!(distance, 0.0); + + // Test generic implementation + let generic_distance = line.distance_ext(&poly); + assert_relative_eq!(generic_distance, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // Line-Polygon test: Line contained by interior ring + fn test_line_polygon_inside_ring() { + let line = Line::from([(4.4, 1.5), (4.45, 1.5)]); + let v = vec![(5.0, 1.0), (5.0, 2.0), (0.25, 1.0), (5.0, 1.0)]; + let v2 = vec![(4.5, 1.2), (4.5, 1.8), (3.5, 1.2), (4.5, 1.2)]; + let poly = Polygon::new(v.into(), vec![v2.into()]); + + // Test original implementation + let distance = Euclidean.distance(&line, &poly); + assert_relative_eq!(distance, 0.04999999999999982); + + // Test generic implementation + let generic_distance = line.distance_ext(&poly); + assert_relative_eq!(generic_distance, 0.04999999999999982); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + // LineString-Line test + fn test_linestring_line_distance() { + let line = Line::from([(0.0, 0.0), (0.0, 2.0)]); + let ls: LineString<_> = vec![(3.0, 0.0), (1.0, 1.0), (3.0, 2.0)].into(); + + // Test original implementation + let distance = Euclidean.distance(&ls, &line); + assert_relative_eq!(distance, 1.0); + + // Test generic implementation + let generic_distance = ls.distance_ext(&line); + assert_relative_eq!(generic_distance, 1.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + + #[test] + // Triangle-Point test: point on vertex + fn test_triangle_point_on_vertex_distance() { + let triangle = Triangle::from([(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + let point = Point::new(0.0, 0.0); + + // Test original implementation + let distance = Euclidean.distance(&triangle, &point); + assert_relative_eq!(distance, 0.0); + + // Test generic implementation + let generic_distance = triangle.distance_ext(&point); + assert_relative_eq!(generic_distance, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + + #[test] + // Triangle-Point test: point on edge + fn test_triangle_point_on_edge_distance() { + let triangle = Triangle::from([(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + let point = Point::new(1.5, 0.0); + + // Test original implementation + let distance = Euclidean.distance(&triangle, &point); + assert_relative_eq!(distance, 0.0); + + // Test generic implementation + let generic_distance = triangle.distance_ext(&point); + assert_relative_eq!(generic_distance, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + + #[test] + // Triangle-Point test + fn test_triangle_point_distance() { + let triangle = Triangle::from([(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + let point = Point::new(2.0, 3.0); + + // Test original implementation + let distance = Euclidean.distance(&triangle, &point); + assert_relative_eq!(distance, 1.0); + + // Test generic implementation + let generic_distance = triangle.distance_ext(&point); + assert_relative_eq!(generic_distance, 1.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + + #[test] + // Triangle-Point test: point within triangle + fn test_triangle_point_inside_distance() { + let triangle = Triangle::from([(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + let point = Point::new(1.0, 0.5); + + // Test original implementation + let distance = Euclidean.distance(&triangle, &point); + assert_relative_eq!(distance, 0.0); + + // Test generic implementation + let generic_distance = triangle.distance_ext(&point); + assert_relative_eq!(generic_distance, 0.0); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + + #[test] + fn convex_and_nearest_neighbour_comparison() { + let ls1: LineString = vec![ + Coord::from((57.39453770777941, 307.60533608924663)), + Coord::from((67.1800355576469, 309.6654408997451)), + Coord::from((84.89693692793338, 225.5101593908847)), + Coord::from((75.1114390780659, 223.45005458038628)), + Coord::from((57.39453770777941, 307.60533608924663)), + ] + .into(); + let first_polygon: Polygon = Polygon::new(ls1, vec![]); + let ls2: LineString = vec![ + Coord::from((138.11769866645008, -45.75134112915392)), + Coord::from((130.50230476949187, -39.270154833870336)), + Coord::from((184.94426964987397, 24.699153900578573)), + Coord::from((192.55966354683218, 18.217967605294987)), + Coord::from((138.11769866645008, -45.75134112915392)), + ] + .into(); + let second_polygon = Polygon::new(ls2, vec![]); + + // Test original implementation + let distance = Euclidean.distance(&first_polygon, &second_polygon); + assert_relative_eq!(distance, 224.35357967013238); + + // Test generic implementation + let generic_distance = first_polygon.distance_ext(&second_polygon); + assert_relative_eq!(generic_distance, 224.35357967013238); + + // Ensure both implementations agree + assert_relative_eq!(distance, generic_distance); + } + #[test] + fn fast_path_regression() { + // this test will fail if the fast path algorithm is reintroduced without being fixed + let p1 = polygon!( + (x: 0_f64, y: 0_f64), + (x: 300_f64, y: 0_f64), + (x: 300_f64, y: 100_f64), + (x: 0_f64, y: 100_f64), + ) + .orient(Direction::Default); + let p2 = polygon!( + (x: 100_f64, y: 150_f64), + (x: 150_f64, y: 200_f64), + (x: 50_f64, y: 200_f64), + ) + .orient(Direction::Default); + let p3 = polygon!( + (x: 0_f64, y: 0_f64), + (x: 300_f64, y: 0_f64), + (x: 300_f64, y: 100_f64), + (x: 0_f64, y: 100_f64), + ) + .orient(Direction::Reversed); + let p4 = polygon!( + (x: 100_f64, y: 150_f64), + (x: 150_f64, y: 200_f64), + (x: 50_f64, y: 200_f64), + ) + .orient(Direction::Reversed); + + // Test original implementation + let distance_p1_p2 = Euclidean.distance(&p1, &p2); + let distance_p3_p4 = Euclidean.distance(&p3, &p4); + let distance_p1_p4 = Euclidean.distance(&p1, &p4); + let distance_p2_p3 = Euclidean.distance(&p2, &p3); + assert_eq!(distance_p1_p2, 50.0f64); + assert_eq!(distance_p3_p4, 50.0f64); + assert_eq!(distance_p1_p4, 50.0f64); + assert_eq!(distance_p2_p3, 50.0f64); + + // Test generic implementation + let generic_distance_p1_p2 = p1.distance_ext(&p2); + let generic_distance_p3_p4 = p3.distance_ext(&p4); + let generic_distance_p1_p4 = p1.distance_ext(&p4); + let generic_distance_p2_p3 = p2.distance_ext(&p3); + assert_relative_eq!(generic_distance_p1_p2, 50.0f64); + assert_relative_eq!(generic_distance_p3_p4, 50.0f64); + assert_relative_eq!(generic_distance_p1_p4, 50.0f64); + assert_relative_eq!(generic_distance_p2_p3, 50.0f64); + + // Ensure both implementations agree + assert_relative_eq!(distance_p1_p2, generic_distance_p1_p2); + assert_relative_eq!(distance_p3_p4, generic_distance_p3_p4); + assert_relative_eq!(distance_p1_p4, generic_distance_p1_p4); + assert_relative_eq!(distance_p2_p3, generic_distance_p2_p3); + } + #[test] + fn rect_to_polygon_distance_test() { + // Test that Rect to Polygon distance works + let rect = Rect::new((0.0, 0.0), (2.0, 2.0)); + let poly_points = vec![(3., 0.), (5., 0.), (5., 2.), (3., 2.), (3., 0.)]; + let poly = Polygon::new(LineString::from(poly_points), vec![]); + + // Test original implementation (both directions) + let dist1 = Euclidean.distance(&rect, &poly); + let dist2 = Euclidean.distance(&poly, &rect); + assert_relative_eq!(dist1, 1.0); + assert_relative_eq!(dist2, 1.0); + assert_relative_eq!(dist1, dist2); // Verify symmetry + + // Test generic implementation + let rect_as_poly = rect.to_polygon(); + let generic_dist1 = rect_as_poly.distance_ext(&poly); + let generic_dist2 = poly.distance_ext(&rect_as_poly); + assert_relative_eq!(generic_dist1, 1.0); + assert_relative_eq!(generic_dist2, 1.0); + + // Ensure both implementations agree + assert_relative_eq!(dist1, generic_dist1); + assert_relative_eq!(dist2, generic_dist2); + } + + #[test] + fn all_types_geometry_collection_test() { + let p = Point::new(0.0, 0.0); + let line = Line::from([(-1.0, -1.0), (-2.0, -2.0)]); + let ls = LineString::from(vec![(0.0, 0.0), (1.0, 10.0), (2.0, 0.0)]); + let poly = Polygon::new( + LineString::from(vec![(0.0, 0.0), (1.0, 10.0), (2.0, 0.0), (0.0, 0.0)]), + vec![], + ); + let tri = Triangle::from([(0.0, 0.0), (1.0, 10.0), (2.0, 0.0)]); + let rect = Rect::new((0.0, 0.0), (-1.0, -1.0)); + + let ls1 = LineString::from(vec![(0.0, 0.0), (1.0, 10.0), (2.0, 0.0), (0.0, 0.0)]); + let ls2 = LineString::from(vec![(3.0, 0.0), (4.0, 10.0), (5.0, 0.0), (3.0, 0.0)]); + let p1 = Polygon::new(ls1, vec![]); + let p2 = Polygon::new(ls2, vec![]); + let mpoly = MultiPolygon::new(vec![p1, p2]); + + let v = vec![ + Point::new(0.0, 10.0), + Point::new(1.0, 1.0), + Point::new(10.0, 0.0), + Point::new(1.0, -1.0), + Point::new(0.0, -10.0), + Point::new(-1.0, -1.0), + Point::new(-10.0, 0.0), + Point::new(-1.0, 1.0), + Point::new(0.0, 10.0), + ]; + let mpoint = MultiPoint::new(v); + + let v1 = LineString::from(vec![(0.0, 0.0), (1.0, 10.0)]); + let v2 = LineString::from(vec![(1.0, 10.0), (2.0, 0.0), (3.0, 1.0)]); + let mls = MultiLineString::new(vec![v1, v2]); + + let gc = GeometryCollection(vec![ + Geometry::Point(p), + Geometry::Line(line), + Geometry::LineString(ls), + Geometry::Polygon(poly), + Geometry::MultiPoint(mpoint), + Geometry::MultiLineString(mls), + Geometry::MultiPolygon(mpoly), + Geometry::Triangle(tri), + Geometry::Rect(rect), + ]); + + // Test original implementations + let test_p = Point::new(50., 50.); + let distance_p_gc = Euclidean.distance(&test_p, &gc); + assert_relative_eq!(distance_p_gc, 60.959002616512684); + + let test_multipoint = MultiPoint::new(vec![test_p]); + let distance_mp_gc = Euclidean.distance(&test_multipoint, &gc); + assert_relative_eq!(distance_mp_gc, 60.959002616512684); + + let test_line = Line::from([(50., 50.), (60., 60.)]); + let distance_line_gc = Euclidean.distance(&test_line, &gc); + assert_relative_eq!(distance_line_gc, 60.959002616512684); + + let test_ls = LineString::from(vec![(50., 50.), (60., 60.), (70., 70.)]); + let distance_ls_gc = Euclidean.distance(&test_ls, &gc); + assert_relative_eq!(distance_ls_gc, 60.959002616512684); + + let test_mls = MultiLineString::new(vec![test_ls]); + let distance_mls_gc = Euclidean.distance(&test_mls, &gc); + assert_relative_eq!(distance_mls_gc, 60.959002616512684); + + let test_poly = Polygon::new( + LineString::from(vec![ + (50., 50.), + (60., 50.), + (60., 60.), + (55., 55.), + (50., 50.), + ]), + vec![], + ); + let distance_poly_gc = Euclidean.distance(&test_poly, &gc); + assert_relative_eq!(distance_poly_gc, 60.959002616512684); + + let test_multipoly = MultiPolygon::new(vec![test_poly]); + let distance_multipoly_gc = Euclidean.distance(&test_multipoly, &gc); + assert_relative_eq!(distance_multipoly_gc, 60.959002616512684); + + let test_tri = Triangle::from([(50., 50.), (60., 50.), (55., 55.)]); + let distance_tri_gc = Euclidean.distance(&test_tri, &gc); + assert_relative_eq!(distance_tri_gc, 60.959002616512684); + + let test_rect = Rect::new(coord! { x: 50., y: 50. }, coord! { x: 60., y: 60. }); + let distance_rect_gc = Euclidean.distance(&test_rect, &gc); + assert_relative_eq!(distance_rect_gc, 60.959002616512684); + + let test_gc = GeometryCollection(vec![Geometry::Rect(test_rect)]); + let distance_gc_gc = Euclidean.distance(&test_gc, &gc); + assert_relative_eq!(distance_gc_gc, 60.959002616512684); + } + + #[test] + fn test_original_issue_verification() { + let point = Point::new(0.0, 0.0); + let linestring = LineString::from(vec![(0.0, 0.0), (1.0, 1.0)]); + + let gc1 = GeometryCollection(vec![ + Geometry::Point(point), + Geometry::LineString(linestring.clone()), + ]); + + let gc2 = GeometryCollection(vec![ + Geometry::Point(point), + Geometry::LineString(linestring), + ]); + + // Test the concrete Distance API + let distance = Euclidean.distance(&gc1, &gc2); + assert_eq!( + distance, 0.0, + "Distance between identical GeometryCollections should be 0" + ); + + // Test the generic distance_ext API directly + use crate::line_measures::DistanceExt; + let distance_ext = gc1.distance_ext(&gc2); + assert_eq!(distance_ext, 0.0, "Generic distance should also be 0"); + } + + #[test] + fn test_force_generic_trait_recursion() { + let point = Point::new(0.0, 0.0); + let linestring = LineString::from(vec![(0.0, 0.0), (1.0, 1.0)]); + + let gc1 = GeometryCollection(vec![ + Geometry::Point(point), + Geometry::LineString(linestring.clone()), + ]); + + let gc2 = GeometryCollection(vec![ + Geometry::Point(point), + Geometry::LineString(linestring), + ]); + + let distance_result = gc1.distance_ext(&gc2); + assert_eq!(distance_result, 0.0); + + let geom_gc1 = Geometry::GeometryCollection(gc1.clone()); + let geom_gc2 = Geometry::GeometryCollection(gc2.clone()); + let distance_result = geom_gc1.distance_ext(&geom_gc2); + assert_eq!(distance_result, 0.0); + + let distance_result = geom_gc1.distance_ext(&gc2); + assert_eq!(distance_result, 0.0); + + let distance_result = gc1.distance_ext(&geom_gc2); + assert_eq!(distance_result, 0.0); + } + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/mod.rs new file mode 100644 index 00000000..d1205a89 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/mod.rs @@ -0,0 +1,99 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Euclidean metric space implementations (generic) +//! +//! Ported (and contains copied code) from `geo::algorithm::line_measures::metric_spaces::euclidean`: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +mod distance; +mod utils; +pub use distance::DistanceExt; +use geo_types::{Coord, CoordFloat, Point}; + +use crate::line_measures::distance::Distance; + +/// Operations on the [Euclidean plane] measure distance with the pythagorean formula - +/// what you'd measure with a ruler. +/// +/// If you have lon/lat points, use the [`Haversine`], [`Geodesic`], or other [metric spaces] - +/// Euclidean methods will give nonsense results. +/// +/// If you wish to use Euclidean operations with lon/lat, the coordinates must first be transformed +/// using the [`Transform::transform`](crate::Transform::transform) / [`Transform::transform_crs_to_crs`](crate::Transform::transform_crs_to_crs) methods or their +/// immutable variants. Use of these requires the proj feature +/// +/// [Euclidean plane]: https://en.wikipedia.org/wiki/Euclidean_plane +/// [`Transform`]: crate::Transform +/// [`Haversine`]: super::Haversine +/// [`Geodesic`]: super::Geodesic +/// [metric spaces]: super +pub struct Euclidean; + +// ┌───────────────────────────┐ +// │ Implementations for Coord │ +// └───────────────────────────┘ + +impl Distance, Coord> for Euclidean { + fn distance(&self, origin: Coord, destination: Coord) -> F { + let delta = origin - destination; + delta.x.hypot(delta.y) + } +} + +// ┌───────────────────────────┐ +// │ Implementations for Point │ +// └───────────────────────────┘ + +/// Calculate the Euclidean distance (a.k.a. pythagorean distance) between two Points +impl Distance, Point> for Euclidean { + /// Calculate the Euclidean distance (a.k.a. pythagorean distance) between two Points + /// + /// # Units + /// - `origin`, `destination`: Point where the units of x/y represent non-angular units + /// — e.g. meters or miles, not lon/lat. For lon/lat points, use the + /// [`Haversine`] or [`Geodesic`] [metric spaces]. + /// - returns: distance in the same units as the `origin` and `destination` points + /// + /// # Example + /// ``` + /// use sedona_geo_generic_alg::{Euclidean, Distance}; + /// use sedona_geo_generic_alg::Point; + /// // web mercator + /// let new_york_city = Point::new(-8238310.24, 4942194.78); + /// // web mercator + /// let london = Point::new(-14226.63, 6678077.70); + /// let distance: f64 = Euclidean.distance(new_york_city, london); + /// + /// assert_eq!( + /// 8_405_286., // meters in web mercator + /// distance.round() + /// ); + /// ``` + /// + /// [`Haversine`]: crate::line_measures::metric_spaces::Haversine + /// [`Geodesic`]: crate::line_measures::metric_spaces::Geodesic + /// [metric spaces]: crate::line_measures::metric_spaces + fn distance(&self, origin: Point, destination: Point) -> F { + self.distance(origin.0, destination.0) + } +} + +impl Distance, &Point> for Euclidean { + fn distance(&self, origin: &Point, destination: &Point) -> F { + self.distance(*origin, *destination) + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/utils.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/utils.rs new file mode 100644 index 00000000..0b835f52 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/euclidean/utils.rs @@ -0,0 +1,2385 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Euclidean distance helper utilities (generic) +//! +//! Ported (and contains copied code) from `geo::algorithm::line_measures::metric_spaces::euclidean::utils`: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +use crate::algorithm::Intersects; +use crate::coordinate_position::{coord_pos_relative_to_ring, CoordPos}; +use crate::geometry::*; +use crate::{CoordFloat, GeoFloat, GeoNum}; +use geo_traits::{CoordTrait, LineStringTrait}; +use num_traits::{Bounded, Float}; +use sedona_geo_traits_ext::{ + LineStringTraitExt, LineTraitExt, PointTraitExt, PolygonTraitExt, TriangleTraitExt, +}; + +// ┌────────────────────────────────────────────────────────────┐ +// │ Helper functions for generic distance calculations │ +// └────────────────────────────────────────────────────────────┘ + +pub fn nearest_neighbour_distance(geom1: &LineString, geom2: &LineString) -> F { + let mut min_distance: F = Bounded::max_value(); + + // Primary computation: line-to-line distances + for line1 in geom1.lines() { + for line2 in geom2.lines() { + let line_distance = distance_line_to_line_generic(&line1, &line2); + min_distance = min_distance.min(line_distance); + + // Early exit if we found an intersection + if line_distance == F::zero() { + return F::zero(); + } + } + } + + // Check points of geom2 against lines of geom1 + for point in geom2.points() { + if let Some(coord) = point.coord_ext() { + for line1 in geom1.lines() { + let dist = distance_coord_to_line_generic(&coord, &line1); + min_distance = min_distance.min(dist); + } + } + } + + // Check points of geom1 against lines of geom2 + for point in geom1.points() { + if let Some(coord) = point.coord_ext() { + for line2 in geom2.lines() { + let dist = distance_coord_to_line_generic(&coord, &line2); + min_distance = min_distance.min(dist); + } + } + } + + min_distance +} + +pub fn ring_contains_coord(ring: &LineString, c: Coord) -> bool { + match coord_pos_relative_to_ring(c, ring) { + CoordPos::Inside => true, + CoordPos::OnBoundary | CoordPos::Outside => false, + } +} + +/// Generic point-to-point Euclidean distance calculation. +/// +/// # Algorithm Equivalence to Concrete Implementation +/// +/// This function is algorithmically identical to the concrete `Distance, Coord>` implementation. +/// +/// **Equivalence Details:** +/// - **Same mathematical formula**: Both use Euclidean distance `sqrt(Δx² + Δy²)` via `hypot()` +/// - **Same calculation steps**: Extract coordinates, compute deltas, apply `hypot()` +/// - **Same edge case handling**: Both return 0 for invalid/empty points +/// - **Same numerical precision**: Both use identical `hypot()` implementation +/// +/// The only difference is the abstraction layer - this generic version works with any +/// type implementing `PointTraitExt`, while concrete works with `Coord` directly. +pub fn distance_point_to_point_generic(p1: &P1, p2: &P2) -> F +where + F: CoordFloat, + P1: PointTraitExt, + P2: PointTraitExt, +{ + if let (Some(c1), Some(c2)) = (p1.coord(), p2.coord()) { + let delta_x = c1.x() - c2.x(); + let delta_y = c1.y() - c2.y(); + delta_x.hypot(delta_y) + } else { + F::zero() + } +} + +/// Generic coordinate-to-line-segment distance calculation. +/// +/// # Algorithm Equivalence to Concrete Implementation +/// +/// This function is algorithmically identical to the concrete `line_segment_distance` function +/// in `geo-types/src/private_utils.rs`. +/// +/// **Equivalence Details:** +/// - **Same parametric approach**: Both compute parameter `r` to find the closest point on the line +/// - **Same boundary handling**: Both check if `r <= 0` (closest to start) or `r >= 1` (closest to end) +/// - **Same degenerate case**: Both handle zero-length lines by computing direct point distance +/// - **Same perpendicular distance formula**: Both use cross product formula `s.abs() * dx.hypot(dy)` for interior points +/// - **Same numerical precision**: Both use identical calculations and `hypot()` calls +/// +/// The concrete implementation uses `line_euclidean_length()` helper for endpoint distances, +/// while this uses inline `delta.hypot()` - both compute the same Euclidean distance. +pub fn distance_coord_to_line_generic(coord: &C, line: &L) -> F +where + F: CoordFloat, + C: CoordTrait, + L: LineTraitExt, +{ + let point_x = coord.x(); + let point_y = coord.y(); + let start = line.start_coord(); + let end = line.end_coord(); + + // Handle degenerate case: line segment is a point + if start.x == end.x && start.y == end.y { + let delta_x = point_x - start.x; + let delta_y = point_y - start.y; + return delta_x.hypot(delta_y); + } + + let dx = end.x - start.x; + let dy = end.y - start.y; + let d_squared = dx * dx + dy * dy; + let r = ((point_x - start.x) * dx + (point_y - start.y) * dy) / d_squared; + + if r <= F::zero() { + // Closest point is the start point + let delta_x = point_x - start.x; + let delta_y = point_y - start.y; + return delta_x.hypot(delta_y); + } + if r >= F::one() { + // Closest point is the end point + let delta_x = point_x - end.x; + let delta_y = point_y - end.y; + return delta_x.hypot(delta_y); + } + + // Closest point is on the line segment - use perpendicular distance + let s = ((start.y - point_y) * dx - (start.x - point_x) * dy) / d_squared; + s.abs() * dx.hypot(dy) +} + +/// Generic point-to-linestring distance calculation. +/// +/// # Algorithm Equivalence to Concrete Implementation +/// +/// This function is algorithmically identical to the concrete `point_line_string_euclidean_distance` function +/// in `geo-types/src/private_utils.rs`. +/// +/// **Equivalence Details:** +/// - **Same containment check optimization**: Both check if point intersects/is contained in the linestring first +/// - **Same early exit**: Both return 0 immediately if point is on the linestring +/// - **Same iteration approach**: Both iterate through all line segments to find minimum distance +/// - **Same distance calculation**: Both use point-to-line-segment distance for each segment +/// - **Same empty handling**: Both return 0 for empty linestrings +/// +/// The concrete implementation uses `line_string_contains_point()` while this uses `intersects()` trait method, +/// but both perform the same containment check. The iteration pattern and minimum distance logic are identical. +pub fn distance_point_to_linestring_generic(point: &P, linestring: &LS) -> F +where + F: GeoFloat, + P: PointTraitExt, + LS: LineStringTraitExt, +{ + if let Some(coord) = point.coord() { + // Early exit optimization: if point is on the linestring, distance is 0 + // Check if the point is contained in the linestring using intersects + if linestring.intersects(point) { + return F::zero(); + } + + let mut lines = linestring.lines(); + if let Some(first_line) = lines.next() { + let mut min_distance = distance_coord_to_line_generic(&coord, &first_line); + for line in lines { + min_distance = min_distance.min(distance_coord_to_line_generic(&coord, &line)); + } + min_distance + } else { + F::zero() + } + } else { + F::zero() + } +} + +/// Point to Polygon distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &Polygon>` implementation: +/// +/// 1. **Intersection Check**: First checks if the point intersects the polygon +/// using the same `Intersects` trait, returning zero for any intersection +/// (boundary or interior). +/// +/// 2. **Ring Distance Calculation**: If no intersection, computes minimum distance +/// by iterating through all polygon rings (exterior and all interior holes). +/// +/// 3. **Minimum Selection**: Uses the same fold pattern to find the minimum +/// distance across all rings, starting with F::max_value(). +/// +/// The only difference is the generic trait-based interface for accessing +/// polygon components, while the core distance logic remains identical. +pub fn distance_point_to_polygon_generic(point: &P, polygon: &Poly) -> F +where + F: GeoFloat, + P: PointTraitExt, + Poly: PolygonTraitExt, +{ + // Check if the polygon is empty + if polygon.exterior_ext().is_none() { + return F::zero(); + } + + // If the point intersects the polygon (is inside or on boundary), distance is 0 + if polygon.intersects(point) { + return F::zero(); + } + + // Point is outside the polygon, calculate minimum distance to edges + if let (Some(coord), Some(exterior)) = (point.coord_ext(), polygon.exterior_ext()) { + let mut min_dist: F = Float::max_value(); + + // Calculate minimum distance to exterior ring - single loop + for line in exterior.lines() { + let dist = distance_coord_to_line_generic(&coord, &line); + min_dist = min_dist.min(dist); + } + + // Only check interior rings if they exist + if polygon.interiors_ext().next().is_some() { + for interior in polygon.interiors_ext() { + for line in interior.lines() { + let dist = distance_coord_to_line_generic(&coord, &line); + min_dist = min_dist.min(dist); + } + } + } + + min_dist + } else { + F::zero() + } +} + +/// Line to Line distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &Line>` implementation: +/// +/// 1. **Intersection Check**: First uses the `Intersects` trait to check if the +/// lines intersect, returning zero if they do. +/// +/// 2. **Four-Point Distance**: If no intersection, computes the minimum distance +/// by checking all four possible point-to-line segment distances: +/// +/// 3. **Minimum Selection**: Uses the same chained min() operations to find +/// the shortest distance among all four calculations. +/// +/// The generic trait interface provides the same coordinate access while +/// maintaining identical distance computation logic. +pub fn distance_line_to_line_generic(line1: &L1, line2: &L2) -> F +where + F: GeoFloat, + L1: LineTraitExt, + L2: LineTraitExt, +{ + let start1 = line1.start_coord(); + let end1 = line1.end_coord(); + let start2 = line2.start_coord(); + let end2 = line2.end_coord(); + + // Check if lines intersect using generic intersects + if line1.intersects(line2) { + return F::zero(); + } + + // Find minimum distance between all endpoint combinations + let dist1 = distance_coord_to_line_generic(&start1, line2); + let dist2 = distance_coord_to_line_generic(&end1, line2); + let dist3 = distance_coord_to_line_generic(&start2, line1); + let dist4 = distance_coord_to_line_generic(&end2, line1); + + dist1.min(dist2).min(dist3).min(dist4) +} + +/// Line to LineString distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &LineString>` implementation: +/// +/// 1. **Segment Iteration**: Maps over each line segment in the LineString +/// using the same `lines()` iterator approach. +/// +/// 2. **Line-to-Line Distance**: For each segment, calls the same line-to-line +/// distance function that handles intersection detection and four-point +/// distance calculations. +/// +/// 3. **Minimum Selection**: Uses the same fold pattern with F::max_value() +/// as the starting accumulator and min() reduction to find the shortest +/// distance across all segments. +/// +/// The generic trait interface provides equivalent LineString iteration while +/// maintaining identical distance computation logic. +pub fn distance_line_to_linestring_generic(line: &L, linestring: &LS) -> F +where + F: GeoFloat, + L: LineTraitExt, + LS: LineStringTraitExt, +{ + linestring + .lines() + .map(|ls_line| distance_line_to_line_generic(line, &ls_line)) + .fold(Float::max_value(), |acc, dist| acc.min(dist)) +} + +/// Line to Polygon distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &Polygon>` implementation: +/// +/// 1. **Line-to-LineString Conversion**: Converts the line segment into a +/// two-point LineString containing the start and end coordinates. +/// +/// 2. **Delegation to LineString-Polygon**: Uses the same delegation pattern +/// as the concrete implementation by calling the LineString-to-Polygon +/// distance function. +/// +/// 3. **Identical Logic Path**: This ensures the same containment checks, +/// intersection detection, and ring distance calculations are applied +/// as in the concrete implementation. +/// +/// The conversion approach maintains algorithmic equivalence while leveraging +/// the more comprehensive LineString-to-Polygon distance logic. +pub fn distance_line_to_polygon_generic(line: &L, polygon: &Poly) -> F +where + F: GeoFloat, + L: LineTraitExt, + Poly: PolygonTraitExt, +{ + // Convert line to linestring and use existing linestring-to-polygon function + let line_coords = vec![line.start_coord(), line.end_coord()]; + let line_as_ls = LineString::from(line_coords); + distance_linestring_to_polygon_generic(&line_as_ls, polygon) +} + +/// LineString to LineString distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &LineString>` implementation: +/// +/// 1. **Cartesian Product**: Uses flat_map to create all possible combinations +/// of line segments between the two LineStrings, matching the nested +/// iteration pattern of the concrete implementation. +/// +/// 2. **Line-to-Line Distance**: For each segment pair, applies the same +/// line-to-line distance function with intersection detection and +/// four-point distance calculations. +/// +/// 3. **Minimum Selection**: Uses the same fold pattern with F::max_value() +/// as the starting accumulator and min() reduction to find the shortest +/// distance across all segment combinations. +/// +/// The generic trait interface provides equivalent segment iteration while +/// maintaining identical pairwise distance computation logic. +pub fn distance_linestring_to_linestring_generic(ls1: &LS1, ls2: &LS2) -> F +where + F: GeoFloat, + LS1: LineStringTraitExt, + LS2: LineStringTraitExt, +{ + ls1.lines() + .flat_map(|line1| { + ls2.lines() + .map(move |line2| distance_line_to_line_generic(&line1, &line2)) + }) + .fold(Float::max_value(), |acc, dist| acc.min(dist)) +} + +/// LineString to Polygon distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &Polygon>` implementation: +/// +/// 1. **Intersection Check**: First uses the `Intersects` trait to check if +/// the LineString intersects the polygon, returning zero if they do. +/// +/// 2. **Containment-Based Logic**: Implements the same containment logic as +/// the concrete implementation: +/// - If polygon has holes AND first point of LineString is inside exterior +/// ring, only check distance to interior rings (holes) +/// - Otherwise, check distance to exterior ring only +/// +/// 3. **Ray Casting Algorithm**: Uses identical ray casting point-in-polygon +/// test to determine if the first LineString point is inside the exterior. +/// +/// 4. **Direct Nested Loop Approach**: Unlike simpler functions that use +/// `nearest_neighbour_distance`, this function implements the distance +/// calculation directly with nested loops over LineString and polygon +/// ring segments. This matches the concrete implementation's approach +/// which requires the specialized containment logic for polygons with holes. +/// +/// 5. **Early Exit**: Includes the same zero-distance early exit optimization +/// when any line segments intersect during the nested iteration. +/// +/// Note: +/// The direct nested loop approach (rather than delegating to helper functions) +/// is necessary to maintain the exact containment-based ring selection logic +/// that the concrete implementation uses for polygons with holes. +/// We have seen sufficient performance improvements in benchmarks by avoiding +/// the overhead of additional function calls and iterator abstractions. +/// +pub fn distance_linestring_to_polygon_generic(linestring: &LS, polygon: &Poly) -> F +where + F: GeoFloat, + LS: LineStringTraitExt, + Poly: PolygonTraitExt, +{ + // Early intersect check + if polygon.intersects(linestring) { + return F::zero(); + } + + if let Some(exterior) = polygon.exterior_ext() { + // Check containment logic: if polygon has holes AND first point of LineString is inside exterior ring, + // then only consider distance to holes (interior rings). Otherwise, consider distance to exterior. + let has_holes = polygon.interiors_ext().next().is_some(); + + let first_point_inside = if has_holes { + // Check if first point of LineString is inside the exterior ring + if let Some(first_coord) = linestring.coords().next() { + // Simple point-in-polygon test using ray casting + let point_x = first_coord.x(); + let point_y = first_coord.y(); + let mut inside = false; + let ring_coords: Vec<_> = exterior.coords().collect(); + let n = ring_coords.len(); + + if n > 0 { + let mut j = n - 1; + for i in 0..n { + let xi = ring_coords[i].x(); + let yi = ring_coords[i].y(); + let xj = ring_coords[j].x(); + let yj = ring_coords[j].y(); + + if ((yi > point_y) != (yj > point_y)) + && (point_x < (xj - xi) * (point_y - yi) / (yj - yi) + xi) + { + inside = !inside; + } + j = i; + } + } + inside + } else { + false // Empty LineString + } + } else { + false + }; + + if has_holes && first_point_inside { + // LineString is inside polygon: only check distance to interior rings (holes) + let mut min_dist: F = Float::max_value(); + for interior in polygon.interiors_ext() { + for line1 in linestring.lines() { + for line2 in interior.lines() { + let line_dist = distance_line_to_line_generic(&line1, &line2); + min_dist = min_dist.min(line_dist); + + if line_dist == F::zero() { + return F::zero(); + } + } + } + } + min_dist + } else { + // LineString is outside polygon or polygon has no holes: check distance to exterior ring only + let mut min_dist: F = Float::max_value(); + for line1 in linestring.lines() { + for line2 in exterior.lines() { + let line_dist = distance_line_to_line_generic(&line1, &line2); + min_dist = min_dist.min(line_dist); + + if line_dist == F::zero() { + return F::zero(); + } + } + } + min_dist + } + } else { + F::zero() + } +} + +/// Polygon to Polygon distance +/// +/// # Algorithm Equivalence +/// +/// This generic implementation is algorithmically identical to the concrete +/// `Distance, &Polygon>` implementation: +/// +/// 1. **Intersection Check**: First uses the `Intersects` trait to check if +/// the polygons intersect, returning zero if they do. +/// +/// 2. **Fast Path Optimization**: If neither polygon has holes, directly +/// delegates to LineString-to-LineString distance between exterior rings. +/// +/// 3. **Symmetric Containment Logic**: Implements the same bidirectional +/// containment checks as the concrete implementation: +/// - If polygon1 has holes AND polygon2's first point is inside polygon1's +/// exterior, check distance from polygon2's exterior to polygon1's holes +/// - If polygon2 has holes AND polygon1's first point is inside polygon2's +/// exterior, check distance from polygon1's exterior to polygon2's holes +/// +/// 4. **Mixed Approach**: Uses `nearest_neighbour_distance` for the contained +/// polygon cases (for efficiency when checking against multiple holes), +/// but delegates to `distance_linestring_to_linestring_generic` for the +/// default exterior-to-exterior case. +/// +/// 5. **Point-in-Polygon Test**: Uses the same `ring_contains_coord` helper +/// function for containment detection as the concrete implementation. +/// +/// The mixed approach (using both helper functions and direct delegation) +/// matches the concrete implementation's optimization strategy for different +/// geometric configurations. +pub fn distance_polygon_to_polygon_generic(polygon1: &P1, polygon2: &P2) -> F +where + F: GeoFloat, + P1: PolygonTraitExt, + P2: PolygonTraitExt, +{ + // Check if polygons intersect using generic intersects + if polygon1.intersects(polygon2) { + return F::zero(); + } + + if let (Some(ext1), Some(ext2)) = (polygon1.exterior_ext(), polygon2.exterior_ext()) { + let has_interiors1 = polygon1.interiors_ext().next().is_some(); + let has_interiors2 = polygon2.interiors_ext().next().is_some(); + + // Fast path: if no interiors in either polygon, skip containment logic entirely + if !has_interiors1 && !has_interiors2 { + return distance_linestring_to_linestring_generic(&ext1, &ext2); + } + + // Symmetric containment logic matching concrete implementation exactly + // Check if polygon_b is contained within polygon_a (has holes) + if has_interiors1 { + if let Some(first_coord_b) = ext2.coords_ext().next() { + let ext1_ls = LineString::from( + ext1.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + + let coord_b = Coord::from((first_coord_b.x(), first_coord_b.y())); + if ring_contains_coord(&ext1_ls, coord_b) { + // polygon_b is inside polygon_a: check distance to polygon_a's holes + let ext2_concrete = LineString::from( + ext2.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + + let mut mindist: F = Float::max_value(); + for ring in polygon1.interiors_ext() { + let ring_concrete = LineString::from( + ring.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + mindist = + mindist.min(nearest_neighbour_distance(&ext2_concrete, &ring_concrete)); + } + return mindist; + } + } + } + + // Check if polygon_a is contained within polygon_b (has holes) + if has_interiors2 { + if let Some(first_coord_a) = ext1.coords_ext().next() { + let ext2_ls = LineString::from( + ext2.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + + let coord_a = Coord::from((first_coord_a.x(), first_coord_a.y())); + if ring_contains_coord(&ext2_ls, coord_a) { + // polygon_a is inside polygon_b: check distance to polygon_b's holes + let ext1_concrete = LineString::from( + ext1.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + + let mut mindist: F = Float::max_value(); + for ring in polygon2.interiors_ext() { + let ring_concrete = LineString::from( + ring.coords_ext() + .map(|c| (c.x(), c.y())) + .collect::>(), + ); + mindist = + mindist.min(nearest_neighbour_distance(&ext1_concrete, &ring_concrete)); + } + return mindist; + } + } + } + + // Default case - distance between exterior rings + distance_linestring_to_linestring_generic(&ext1, &ext2) + } else { + F::zero() + } +} + +/// Triangle to Point distance +pub fn distance_triangle_to_point_generic(triangle: &T, point: &P) -> F +where + F: GeoFloat, + T: TriangleTraitExt, + P: PointTraitExt, +{ + // Convert triangle to polygon and use existing point-to-polygon function + let tri_poly = triangle.to_polygon(); + distance_point_to_polygon_generic(point, &tri_poly) +} + +// ┌────────────────────────────────────────────────────────────┐ +// │ Symmetric Distance Function Generator Macro │ +// └────────────────────────────────────────────────────────────┘ + +/// Macro to generate symmetric distance functions +/// For distance operations that are symmetric (distance(a, b) == distance(b, a)), +/// this macro generates the reverse function that calls the primary implementation +macro_rules! symmetric_distance_generic_impl { + ($func_name_ab:ident, $func_name_ba:ident, $trait_a:ident, $trait_b:ident) => { + #[allow(dead_code)] + pub fn $func_name_ba(b: &B, a: &A) -> F + where + F: GeoFloat, + A: $trait_a, + B: $trait_b, + { + $func_name_ab(a, b) + } + }; +} + +// Generate symmetric distance functions +symmetric_distance_generic_impl!( + distance_point_to_linestring_generic, + distance_linestring_to_point_generic, + PointTraitExt, + LineStringTraitExt +); + +symmetric_distance_generic_impl!( + distance_point_to_polygon_generic, + distance_polygon_to_point_generic, + PointTraitExt, + PolygonTraitExt +); + +symmetric_distance_generic_impl!( + distance_linestring_to_polygon_generic, + distance_polygon_to_linestring_generic, + LineStringTraitExt, + PolygonTraitExt +); + +symmetric_distance_generic_impl!( + distance_line_to_linestring_generic, + distance_linestring_to_line_generic, + LineTraitExt, + LineStringTraitExt +); + +symmetric_distance_generic_impl!( + distance_line_to_polygon_generic, + distance_polygon_to_line_generic, + LineTraitExt, + PolygonTraitExt +); + +symmetric_distance_generic_impl!( + distance_triangle_to_point_generic, + distance_point_to_triangle_generic, + TriangleTraitExt, + PointTraitExt +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::{coord, Line, LineString, Point, Polygon, Triangle}; + use approx::assert_relative_eq; + use geo::algorithm::line_measures::{Distance, Euclidean}; + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for point_distance_generic function │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_point_distance_generic_basic() { + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(3.0, 4.0); + + let distance = distance_point_to_point_generic(&p1, &p2); + assert_relative_eq!(distance, 5.0); // 3-4-5 triangle + + // Test symmetry + let distance_reverse = distance_point_to_point_generic(&p2, &p1); + assert_relative_eq!(distance, distance_reverse); + } + + #[test] + fn test_point_distance_generic_same_point() { + let p = Point::new(2.5, -1.5); + let distance = distance_point_to_point_generic(&p, &p); + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_point_distance_generic_negative_coordinates() { + let p1 = Point::new(-2.0, -3.0); + let p2 = Point::new(1.0, 1.0); + + let distance = distance_point_to_point_generic(&p1, &p2); + assert_relative_eq!(distance, 5.0); // sqrt((1-(-2))^2 + (1-(-3))^2) = sqrt(9+16) = 5 + } + + #[test] + fn test_point_distance_generic_empty_points() { + // Test with empty points (no coordinates) + let empty_point: Point = Point::new(f64::NAN, f64::NAN); + let regular_point = Point::new(1.0, 1.0); + + // When either point has no valid coordinates, distance should be 0 + let distance = distance_point_to_point_generic(&empty_point, ®ular_point); + assert!(distance.is_nan() || distance == 0.0); // Implementation dependent + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for line_segment_distance_generic function │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_line_segment_distance_generic_point_on_line() { + let coord = coord! { x: 2.0, y: 0.0 }; + let line = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 4.0, y: 0.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_line_segment_distance_generic_perpendicular() { + let coord = coord! { x: 2.0, y: 3.0 }; + let line = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 4.0, y: 0.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + assert_relative_eq!(distance, 3.0); + } + + #[test] + fn test_line_segment_distance_generic_beyond_endpoint() { + let coord = coord! { x: 6.0, y: 0.0 }; + let line = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 4.0, y: 0.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + assert_relative_eq!(distance, 2.0); // Distance to closest endpoint (4,0) + } + + #[test] + fn test_line_segment_distance_generic_before_startpoint() { + let coord = coord! { x: -2.0, y: 0.0 }; + let line = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 4.0, y: 0.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + assert_relative_eq!(distance, 2.0); // Distance to start point (0,0) + } + + #[test] + fn test_line_segment_distance_generic_zero_length_line() { + let coord = coord! { x: 2.0, y: 3.0 }; + let line = Line::new(coord! { x: 1.0, y: 1.0 }, coord! { x: 1.0, y: 1.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + let expected = ((2.0 - 1.0).powi(2) + (3.0 - 1.0).powi(2)).sqrt(); + assert_relative_eq!(distance, expected); + } + + #[test] + fn test_line_segment_distance_generic_diagonal_line() { + let coord = coord! { x: 0.0, y: 2.0 }; + let line = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 2.0, y: 2.0 }); + + let distance = distance_coord_to_line_generic(&coord, &line); + // Point (0,2) to line from (0,0) to (2,2) - should be sqrt(2) + assert_relative_eq!(distance, std::f64::consts::SQRT_2, epsilon = 1e-10); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for nearest_neighbour_distance function │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_nearest_neighbour_distance_basic() { + let ls1 = LineString::from(vec![(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + let ls2 = LineString::from(vec![(3.0, 0.0), (5.0, 0.0), (5.0, 2.0)]); + + let distance = nearest_neighbour_distance(&ls1, &ls2); + assert_relative_eq!(distance, 1.0); // Distance between (2,0)-(2,2) and (3,0)-(5,0) + } + + #[test] + fn test_nearest_neighbour_distance_intersecting() { + let ls1 = LineString::from(vec![(0.0, 0.0), (4.0, 0.0)]); + let ls2 = LineString::from(vec![(2.0, -1.0), (2.0, 1.0)]); + + let distance = nearest_neighbour_distance(&ls1, &ls2); + // The linestrings intersect at (2,0), so distance should be 0.0 + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_nearest_neighbour_distance_parallel_lines() { + let ls1 = LineString::from(vec![(0.0, 0.0), (4.0, 0.0)]); + let ls2 = LineString::from(vec![(0.0, 2.0), (4.0, 2.0)]); + + let distance = nearest_neighbour_distance(&ls1, &ls2); + assert_relative_eq!(distance, 2.0); // Perpendicular distance between parallel lines + } + + #[test] + fn test_nearest_neighbour_distance_single_segment_each() { + let ls1 = LineString::from(vec![(0.0, 0.0), (1.0, 0.0)]); + let ls2 = LineString::from(vec![(2.0, 1.0), (3.0, 1.0)]); + + let distance = nearest_neighbour_distance(&ls1, &ls2); + let expected = ((2.0 - 1.0).powi(2) + (1.0 - 0.0).powi(2)).sqrt(); + assert_relative_eq!(distance, expected); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for ring_contains_coord function │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_ring_contains_coord_inside() { + let ring = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let coord = coord! { x: 2.0, y: 2.0 }; + + assert!(ring_contains_coord(&ring, coord)); + } + + #[test] + fn test_ring_contains_coord_outside() { + let ring = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let coord = coord! { x: 5.0, y: 2.0 }; + + assert!(!ring_contains_coord(&ring, coord)); + } + + #[test] + fn test_ring_contains_coord_on_boundary() { + let ring = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let coord = coord! { x: 2.0, y: 0.0 }; + + assert!(!ring_contains_coord(&ring, coord)); // On boundary = false + } + + #[test] + fn test_ring_contains_coord_triangle() { + let ring = LineString::from(vec![(0.0, 0.0), (3.0, 0.0), (1.5, 2.0), (0.0, 0.0)]); + let inside_coord = coord! { x: 1.5, y: 0.5 }; + let outside_coord = coord! { x: 3.0, y: 3.0 }; + + assert!(ring_contains_coord(&ring, inside_coord)); + assert!(!ring_contains_coord(&ring, outside_coord)); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_point_to_linestring_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_point_to_linestring_generic_basic() { + let point = Point::new(1.0, 2.0); + let linestring = LineString::from(vec![(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + + let distance = distance_point_to_linestring_generic(&point, &linestring); + assert_relative_eq!(distance, 1.0); // Distance to closest segment + } + + #[test] + fn test_distance_point_to_linestring_generic_empty() { + let point = Point::new(1.0, 2.0); + let linestring = LineString::::new(vec![]); + + let distance = distance_point_to_linestring_generic(&point, &linestring); + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_distance_point_to_linestring_generic_single_point() { + let point = Point::new(1.0, 2.0); + let linestring = LineString::from(vec![(0.0, 0.0)]); + + let distance = distance_point_to_linestring_generic(&point, &linestring); + assert_relative_eq!(distance, 0.0); // Single point linestring + } + + #[test] + fn test_distance_point_to_linestring_generic_on_linestring() { + let point = Point::new(1.0, 0.0); + let linestring = LineString::from(vec![(0.0, 0.0), (2.0, 0.0)]); + + let distance = distance_point_to_linestring_generic(&point, &linestring); + assert_relative_eq!(distance, 0.0); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_point_to_polygon_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_point_to_polygon_generic_outside() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + let point = Point::new(6.0, 2.0); + + let distance = distance_point_to_polygon_generic(&point, &polygon); + assert_relative_eq!(distance, 2.0); // Distance to right edge + } + + #[test] + fn test_distance_point_to_polygon_generic_inside() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + let point = Point::new(2.0, 2.0); + + let distance = distance_point_to_polygon_generic(&point, &polygon); + assert_relative_eq!(distance, 0.0); // Inside polygon + } + + #[test] + fn test_distance_point_to_polygon_generic_on_boundary() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + let point = Point::new(2.0, 0.0); + + let distance = distance_point_to_polygon_generic(&point, &polygon); + assert_relative_eq!(distance, 0.0); // On boundary + } + + #[test] + fn test_distance_point_to_polygon_generic_with_hole() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (6.0, 0.0), + (6.0, 6.0), + (0.0, 6.0), + (0.0, 0.0), + ]); + let interior = LineString::from(vec![ + (2.0, 2.0), + (4.0, 2.0), + (4.0, 4.0), + (2.0, 4.0), + (2.0, 2.0), + ]); + let polygon = Polygon::new(exterior, vec![interior]); + let point = Point::new(3.0, 3.0); // Inside the hole + + let distance = distance_point_to_polygon_generic(&point, &polygon); + assert_relative_eq!(distance, 1.0); // Distance to closest hole edge + } + + #[test] + fn test_distance_point_to_polygon_generic_empty() { + let empty_polygon = Polygon::new(LineString::::new(vec![]), vec![]); + let point = Point::new(1.0, 1.0); + + let distance = distance_point_to_polygon_generic(&point, &empty_polygon); + assert_relative_eq!(distance, 0.0); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_line_to_line_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_line_to_line_generic_parallel() { + let line1 = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 2.0, y: 0.0 }); + let line2 = Line::new(coord! { x: 0.0, y: 3.0 }, coord! { x: 2.0, y: 3.0 }); + + let distance = distance_line_to_line_generic(&line1, &line2); + assert_relative_eq!(distance, 3.0); + } + + #[test] + fn test_distance_line_to_line_generic_intersecting() { + let line1 = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 2.0, y: 0.0 }); + let line2 = Line::new(coord! { x: 1.0, y: -1.0 }, coord! { x: 1.0, y: 1.0 }); + + let distance = distance_line_to_line_generic(&line1, &line2); + assert_relative_eq!(distance, 0.0, epsilon = 1e-10); + } + + #[test] + fn test_distance_line_to_line_generic_skew() { + let line1 = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 1.0, y: 0.0 }); + let line2 = Line::new(coord! { x: 2.0, y: 1.0 }, coord! { x: 3.0, y: 1.0 }); + + let distance = distance_line_to_line_generic(&line1, &line2); + let expected = ((2.0 - 1.0).powi(2) + (1.0 - 0.0).powi(2)).sqrt(); + assert_relative_eq!(distance, expected); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_linestring_to_polygon_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_linestring_to_polygon_generic_outside() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (2.0, 0.0), + (2.0, 2.0), + (0.0, 2.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + let linestring = LineString::from(vec![(3.0, 0.0), (4.0, 1.0)]); + + let distance = distance_linestring_to_polygon_generic(&linestring, &polygon); + assert_relative_eq!(distance, 1.0); // Distance to right edge of polygon + } + + #[test] + fn test_distance_linestring_to_polygon_generic_intersecting() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (2.0, 0.0), + (2.0, 2.0), + (0.0, 2.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + let linestring = LineString::from(vec![(-1.0, 1.0), (3.0, 1.0)]); + + let distance = distance_linestring_to_polygon_generic(&linestring, &polygon); + // The linestring intersects the polygon, so distance should be 0.0 + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_distance_linestring_to_polygon_generic_empty_polygon() { + let empty_polygon = Polygon::new(LineString::::new(vec![]), vec![]); + let linestring = LineString::from(vec![(0.0, 0.0), (1.0, 1.0)]); + + let distance = distance_linestring_to_polygon_generic(&linestring, &empty_polygon); + assert_relative_eq!(distance, 0.0); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_polygon_to_polygon_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_polygon_to_polygon_generic_separate() { + let exterior1 = LineString::from(vec![ + (0.0, 0.0), + (2.0, 0.0), + (2.0, 2.0), + (0.0, 2.0), + (0.0, 0.0), + ]); + let polygon1 = Polygon::new(exterior1, vec![]); + + let exterior2 = LineString::from(vec![ + (4.0, 0.0), + (6.0, 0.0), + (6.0, 2.0), + (4.0, 2.0), + (4.0, 0.0), + ]); + let polygon2 = Polygon::new(exterior2, vec![]); + + let distance = distance_polygon_to_polygon_generic(&polygon1, &polygon2); + assert_relative_eq!(distance, 2.0); // Distance between closest edges + } + + #[test] + fn test_distance_polygon_to_polygon_generic_intersecting() { + let exterior1 = LineString::from(vec![ + (0.0, 0.0), + (3.0, 0.0), + (3.0, 3.0), + (0.0, 3.0), + (0.0, 0.0), + ]); + let polygon1 = Polygon::new(exterior1, vec![]); + + let exterior2 = LineString::from(vec![ + (1.0, 1.0), + (4.0, 1.0), + (4.0, 4.0), + (1.0, 4.0), + (1.0, 1.0), + ]); + let polygon2 = Polygon::new(exterior2, vec![]); + + let distance = distance_polygon_to_polygon_generic(&polygon1, &polygon2); + assert_relative_eq!(distance, 0.0, epsilon = 1e-10); // Polygons intersect + } + + #[test] + fn test_distance_polygon_to_polygon_generic_one_in_others_hole() { + let exterior = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let interior = LineString::from(vec![ + (2.0, 2.0), + (8.0, 2.0), + (8.0, 8.0), + (2.0, 8.0), + (2.0, 2.0), + ]); + let polygon_with_hole = Polygon::new(exterior, vec![interior]); + + let small_exterior = LineString::from(vec![ + (4.0, 4.0), + (6.0, 4.0), + (6.0, 6.0), + (4.0, 6.0), + (4.0, 4.0), + ]); + let small_polygon = Polygon::new(small_exterior, vec![]); + + let distance = distance_polygon_to_polygon_generic(&polygon_with_hole, &small_polygon); + assert_relative_eq!(distance, 2.0); // Distance to hole boundary + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for symmetric distance functions │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_symmetric_distance_point_linestring() { + let point = Point::new(1.0, 2.0); + let linestring = LineString::from(vec![(0.0, 0.0), (2.0, 0.0)]); + + let dist1 = distance_point_to_linestring_generic(&point, &linestring); + let dist2 = distance_linestring_to_point_generic(&linestring, &point); + + assert_relative_eq!(dist1, dist2); + assert_relative_eq!(dist1, 2.0); + } + + #[test] + fn test_symmetric_distance_point_polygon() { + let point = Point::new(5.0, 2.0); + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + + let dist1 = distance_point_to_polygon_generic(&point, &polygon); + let dist2 = distance_polygon_to_point_generic(&polygon, &point); + + assert_relative_eq!(dist1, dist2); + assert_relative_eq!(dist1, 1.0); + } + + #[test] + fn test_symmetric_distance_linestring_polygon() { + let linestring = LineString::from(vec![(5.0, 1.0), (6.0, 2.0)]); + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + + let dist1 = distance_linestring_to_polygon_generic(&linestring, &polygon); + let dist2 = distance_polygon_to_linestring_generic(&polygon, &linestring); + + assert_relative_eq!(dist1, dist2); + assert_relative_eq!(dist1, 1.0); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for line-to-linestring and line-to-polygon functions │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_line_to_linestring_generic() { + let line = Line::new(coord! { x: 0.0, y: 3.0 }, coord! { x: 2.0, y: 3.0 }); + let linestring = LineString::from(vec![(0.0, 0.0), (2.0, 0.0), (2.0, 2.0)]); + + let distance = distance_line_to_linestring_generic(&line, &linestring); + assert_relative_eq!(distance, 1.0); // Distance to closest segment + } + + #[test] + fn test_distance_line_to_polygon_generic() { + let line = Line::new(coord! { x: 5.0, y: 1.0 }, coord! { x: 6.0, y: 2.0 }); + let exterior = LineString::from(vec![ + (0.0, 0.0), + (4.0, 0.0), + (4.0, 4.0), + (0.0, 4.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(exterior, vec![]); + + let distance = distance_line_to_polygon_generic(&line, &polygon); + assert_relative_eq!(distance, 1.0); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Tests for distance_triangle_to_point_generic │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_distance_triangle_to_point_generic() { + let triangle = Triangle::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + coord! { x: 1.5, y: 3.0 }, + ); + let point = Point::new(1.5, 1.0); // Inside triangle + + let distance = distance_triangle_to_point_generic(&triangle, &point); + assert_relative_eq!(distance, 0.0); + } + + #[test] + fn test_distance_triangle_to_point_generic_outside() { + let triangle = Triangle::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + coord! { x: 1.5, y: 3.0 }, + ); + let point = Point::new(5.0, 0.0); // Outside triangle + + let distance = distance_triangle_to_point_generic(&triangle, &point); + assert_relative_eq!(distance, 2.0); // Distance to right vertex + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Edge case tests │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_empty_geometries_edge_cases() { + // Empty LineString + let empty_ls = LineString::::new(vec![]); + let point = Point::new(1.0, 1.0); + + let dist = distance_point_to_linestring_generic(&point, &empty_ls); + assert_relative_eq!(dist, 0.0); + + // Empty Polygon + let empty_poly = Polygon::new(LineString::::new(vec![]), vec![]); + let dist2 = distance_point_to_polygon_generic(&point, &empty_poly); + assert_relative_eq!(dist2, 0.0); + } + + #[test] + fn test_degenerate_geometries() { + // Single point LineString + let single_point_ls = LineString::from(vec![(1.0, 1.0)]); + let point = Point::new(2.0, 2.0); + + let dist = distance_point_to_linestring_generic(&point, &single_point_ls); + assert_relative_eq!(dist, 0.0); // Should handle gracefully + + // Two identical points in LineString + let two_same_points_ls = LineString::from(vec![(1.0, 1.0), (1.0, 1.0)]); + let dist2 = distance_point_to_linestring_generic(&point, &two_same_points_ls); + assert_relative_eq!(dist2, std::f64::consts::SQRT_2); // Distance to the point + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Performance comparison tests (basic) │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_generic_vs_concrete_point_distance() { + let p1 = Point::new(-72.1235, 42.3521); + let p2 = Point::new(72.1260, 70.612); + + // Test generic implementation + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + // Test concrete implementation via Euclidean trait + let concrete_dist = Euclidean.distance(&p1, &p2); + + // Both should give the same result + assert_relative_eq!(generic_dist, concrete_dist, epsilon = 1e-10); + assert_relative_eq!(generic_dist, 146.99163308930207); + } + + #[test] + fn test_cross_validation_with_existing_tests() { + // Test cases from existing distance.rs tests to ensure compatibility + let o1 = Point::new(8.0, 0.0); + let p1 = Point::new(7.2, 2.0); + let p2 = Point::new(6.0, 1.0); + + // Create line from p1 to p2 + let line_seg = Line::new( + coord! { x: p1.x(), y: p1.y() }, + coord! { x: p2.x(), y: p2.y() }, + ); + + if let Some(o1_coord) = o1.coord_ext() { + let generic_dist = distance_coord_to_line_generic(&o1_coord, &line_seg); + + // This should match the expected value from the original test + assert_relative_eq!(generic_dist, 2.0485900789263356, epsilon = 1e-10); + } + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Property-based tests with random inputs │ + // └────────────────────────────────────────────────────────────┘ + + fn generate_random_point(seed: u64) -> Point { + // Simple LCG for deterministic "random" numbers + let mut rng = seed; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let x = ((rng >> 16) as i16) as f64 * 0.001; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let y = ((rng >> 16) as i16) as f64 * 0.001; + Point::new(x, y) + } + + fn generate_random_line(seed: u64) -> Line { + let mut rng = seed; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let x1 = ((rng >> 16) as i16) as f64 * 0.001; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let y1 = ((rng >> 16) as i16) as f64 * 0.001; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let x2 = ((rng >> 16) as i16) as f64 * 0.001; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let y2 = ((rng >> 16) as i16) as f64 * 0.001; + Line::new(coord! { x: x1, y: y1 }, coord! { x: x2, y: y2 }) + } + + fn generate_random_linestring(seed: u64, num_points: usize) -> LineString { + let mut rng = seed; + let mut points = Vec::new(); + for _ in 0..num_points { + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let x = ((rng >> 16) as i16) as f64 * 0.001; + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let y = ((rng >> 16) as i16) as f64 * 0.001; + points.push((x, y)); + } + LineString::from(points) + } + + fn generate_random_polygon(seed: u64, num_exterior_points: usize) -> Polygon { + let mut rng = seed; + let mut points = Vec::new(); + + // Generate points around a circle to ensure a valid polygon + let center_x = 0.0; + let center_y = 0.0; + let radius = 10.0; + + for i in 0..num_exterior_points { + let angle = 2.0 * std::f64::consts::PI * i as f64 / num_exterior_points as f64; + // Add some random noise + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let noise = ((rng >> 16) as i16) as f64 * 0.0001; + let x = center_x + (radius + noise) * angle.cos(); + let y = center_y + (radius + noise) * angle.sin(); + points.push((x, y)); + } + + // Close the polygon + if !points.is_empty() { + points.push(points[0]); + } + + Polygon::new(LineString::from(points), vec![]) + } + + #[test] + fn test_random_point_to_point_distance() { + // Test point-to-point distance with random inputs + for i in 0..100 { + let seed1 = 12345 + i * 17; + let seed2 = 54321 + i * 23; + + let p1 = generate_random_point(seed1); + let p2 = generate_random_point(seed2); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-12, + max_relative = 1e-12 + ); + } + } + + #[test] + fn test_random_point_to_linestring_distance() { + // Test point-to-linestring distance with random inputs + for i in 0..100 { + let seed1 = 11111 + i * 31; + let seed2 = 22222 + i * 37; + + let point = generate_random_point(seed1); + let linestring = generate_random_linestring(seed2, 3 + (i % 5) as usize); // 3-7 points + + let concrete_dist = Euclidean.distance(&point, &linestring); + let generic_dist = distance_point_to_linestring_generic(&point, &linestring); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-12, + max_relative = 1e-12 + ); + } + } + + #[test] + fn test_random_point_to_polygon_distance() { + // Test point-to-polygon distance with random inputs + for i in 0..100 { + let seed1 = 33333 + i * 41; + let seed2 = 44444 + i * 43; + + let point = generate_random_point(seed1); + let polygon = generate_random_polygon(seed2, 4 + (i % 4) as usize); // 4-7 sides + + let concrete_dist = Euclidean.distance(&point, &polygon); + let generic_dist = distance_point_to_polygon_generic(&point, &polygon); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-10, + max_relative = 1e-10 + ); + } + } + + #[test] + fn test_random_line_to_line_distance() { + // Test line-to-line distance with random inputs + for i in 0..100 { + let seed1 = 55555 + i * 47; + let seed2 = 66666 + i * 53; + + let line1 = generate_random_line(seed1); + let line2 = generate_random_line(seed2); + + let concrete_dist = Euclidean.distance(&line1, &line2); + let generic_dist = distance_line_to_line_generic(&line1, &line2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-12, + max_relative = 1e-12 + ); + } + } + + #[test] + fn test_random_linestring_to_linestring_distance() { + // Test linestring-to-linestring distance with random inputs + for i in 0..100 { + let seed1 = 77777 + i * 59; + let seed2 = 88888 + i * 61; + + let ls1 = generate_random_linestring(seed1, 3 + (i % 3) as usize); // 3-5 points + let ls2 = generate_random_linestring(seed2, 3 + ((i + 1) % 3) as usize); // 3-5 points + + let concrete_dist = Euclidean.distance(&ls1, &ls2); + // Use our actual generic implementation via nearest_neighbour_distance + let generic_dist = nearest_neighbour_distance(&ls1, &ls2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-10, + max_relative = 1e-10 + ); + } + } + + #[test] + fn test_random_polygon_to_polygon_distance() { + // Test polygon-to-polygon distance with random inputs + for i in 0..100 { + let seed1 = 99999 + i * 67; + let seed2 = 10101 + i * 71; + + let poly1 = generate_random_polygon(seed1, 4 + (i % 3) as usize); // 4-6 sides + let poly2 = generate_random_polygon(seed2, 4 + ((i + 1) % 3) as usize); // 4-6 sides + + let concrete_dist = Euclidean.distance(&poly1, &poly2); + let generic_dist = distance_polygon_to_polygon_generic(&poly1, &poly2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-8, + max_relative = 1e-8 + ); + } + } + + #[test] + fn test_random_line_to_polygon_distance() { + // Test line-to-polygon distance with random inputs + for i in 0..100 { + let seed1 = 12121 + i * 73; + let seed2 = 13131 + i * 79; + + let line = generate_random_line(seed1); + let polygon = generate_random_polygon(seed2, 4 + (i % 3) as usize); // 4-6 sides + + let concrete_dist = Euclidean.distance(&line, &polygon); + let generic_dist = distance_line_to_polygon_generic(&line, &polygon); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-10, + max_relative = 1e-10 + ); + } + } + + #[test] + fn test_random_linestring_to_polygon_distance() { + // Test linestring-to-polygon distance with random inputs + for i in 0..100 { + let seed1 = 14141 + i * 83; + let seed2 = 15151 + i * 89; + + let linestring = generate_random_linestring(seed1, 3 + (i % 3) as usize); // 3-5 points + let polygon = generate_random_polygon(seed2, 4 + (i % 3) as usize); // 4-6 sides + + let concrete_dist = Euclidean.distance(&linestring, &polygon); + let generic_dist = distance_linestring_to_polygon_generic(&linestring, &polygon); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-8, + max_relative = 1e-8 + ); + } + } + + #[test] + fn test_random_symmetry_properties() { + // Test symmetry properties with random inputs + for i in 0..100 { + let seed1 = 16161 + i * 97; + let seed2 = 17171 + i * 101; + + let point = generate_random_point(seed1); + let linestring = generate_random_linestring(seed2, 4); + + // Test point-linestring symmetry + let dist1 = distance_point_to_linestring_generic(&point, &linestring); + let dist2 = distance_linestring_to_point_generic(&linestring, &point); + assert_relative_eq!(dist1, dist2, epsilon = 1e-12); + + // Test with polygon + if i % 2 == 0 { + let polygon = generate_random_polygon(seed1 + seed2, 5); + let dist3 = distance_point_to_polygon_generic(&point, &polygon); + let dist4 = distance_polygon_to_point_generic(&polygon, &point); + assert_relative_eq!(dist3, dist4, epsilon = 1e-10); + } + } + } + + #[test] + fn test_random_edge_cases_and_boundaries() { + // Test edge cases with specific patterns + for i in 0..100 { + // Same point distance should be zero + let point = generate_random_point(12345 + i); + let same_point_dist = distance_point_to_point_generic(&point, &point); + assert_relative_eq!(same_point_dist, 0.0); + + // Zero-length line segment + let coord = coord! { x: point.x(), y: point.y() }; + let zero_line = Line::new(coord, coord); + let dist_to_zero_line = distance_coord_to_line_generic(&coord, &zero_line); + assert_relative_eq!(dist_to_zero_line, 0.0); + + // Point on line segment should have zero distance + let seed = 54321 + i * 13; + let line = generate_random_line(seed); + let start_coord = line.start_coord(); + let dist_to_start = distance_coord_to_line_generic(&start_coord, &line); + assert_relative_eq!(dist_to_start, 0.0, epsilon = 1e-12); + } + } + + #[test] + fn test_random_large_coordinates() { + // Test with large coordinate values to check numerical stability + for i in 0..100 { + let mut rng: u64 = 98765 + i * 107; + + // Generate large coordinates + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let scale = 1e6 + (rng % 1000000) as f64; + + let p1 = Point::new(scale, scale * 0.5); + let p2 = Point::new(scale * 1.1, scale * 0.7); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-10, + max_relative = 1e-10 + ); + } + } + + #[test] + fn test_random_small_coordinates() { + // Test with very small coordinate values to check numerical precision + for i in 0..100 { + let mut rng: u64 = 13579 + i * 109; + + // Generate small coordinates + rng = rng.wrapping_mul(1103515245).wrapping_add(12345); + let scale = 1e-6 * (1.0 + (rng % 100) as f64 * 0.01); + + let p1 = Point::new(scale, scale * 0.5); + let p2 = Point::new(scale * 1.1, scale * 0.7); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!( + concrete_dist, + generic_dist, + epsilon = 1e-15, + max_relative = 1e-12 + ); + } + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Geometric Edge Cases Tests │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_collinear_linestring_geometries() { + // Test linestrings where all points are collinear + let collinear_ls1 = LineString::from(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0)]); + let collinear_ls2 = LineString::from(vec![(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)]); + + let concrete_dist = Euclidean.distance(&collinear_ls1, &collinear_ls2); + let generic_dist = nearest_neighbour_distance(&collinear_ls1, &collinear_ls2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + // Distance should be sqrt(2)/2 (perpendicular distance between parallel lines) + assert_relative_eq!( + concrete_dist, + std::f64::consts::SQRT_2 / 2.0, + epsilon = 1e-10 + ); + } + + #[test] + fn test_degenerate_triangle_as_line() { + // Triangle where all three points are collinear (degenerate triangle) + let degenerate_triangle = Triangle::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 1.0 }, + coord! { x: 2.0, y: 2.0 }, + ); + let point = Point::new(0.0, 1.0); + + let concrete_dist = Euclidean.distance(°enerate_triangle, &point); + let generic_dist = distance_triangle_to_point_generic(°enerate_triangle, &point); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + // Distance should be sqrt(2)/2 (distance from point to line y=x) + assert_relative_eq!( + concrete_dist, + std::f64::consts::SQRT_2 / 2.0, + epsilon = 1e-10 + ); + } + + #[test] + fn test_self_intersecting_polygon() { + // Create a bowtie/figure-8 shaped self-intersecting polygon + let self_intersecting = LineString::from(vec![ + (0.0, 0.0), + (2.0, 2.0), + (2.0, 0.0), + (0.0, 2.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(self_intersecting, vec![]); + let point = Point::new(3.0, 1.0); // Outside the polygon + + let concrete_dist = Euclidean.distance(&point, &polygon); + let generic_dist = distance_point_to_polygon_generic(&point, &polygon); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + assert_relative_eq!(concrete_dist, 1.0, epsilon = 1e-10); // Distance to closest edge + } + + #[test] + fn test_nearly_touching_geometries() { + // Test geometries separated by very small distances + let epsilon_dist = 1e-12; + + let line1 = Line::new(coord! { x: 0.0, y: 0.0 }, coord! { x: 1.0, y: 0.0 }); + let line2 = Line::new( + coord! { x: 0.0, y: epsilon_dist }, + coord! { x: 1.0, y: epsilon_dist }, + ); + + let concrete_dist = Euclidean.distance(&line1, &line2); + let generic_dist = distance_line_to_line_generic(&line1, &line2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-15); + assert_relative_eq!(concrete_dist, epsilon_dist, epsilon = 1e-15); + } + + #[test] + fn test_very_close_but_separate_polygons() { + // Two polygons separated by extremely small distance + let tiny_gap = 1e-14; + + let poly1_exterior = LineString::from(vec![ + (0.0, 0.0), + (1.0, 0.0), + (1.0, 1.0), + (0.0, 1.0), + (0.0, 0.0), + ]); + let poly1 = Polygon::new(poly1_exterior, vec![]); + + let poly2_exterior = LineString::from(vec![ + (1.0 + tiny_gap, 0.0), + (2.0 + tiny_gap, 0.0), + (2.0 + tiny_gap, 1.0), + (1.0 + tiny_gap, 1.0), + (1.0 + tiny_gap, 0.0), + ]); + let poly2 = Polygon::new(poly2_exterior, vec![]); + + let concrete_dist = Euclidean.distance(&poly1, &poly2); + let generic_dist = distance_polygon_to_polygon_generic(&poly1, &poly2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-15); + assert_relative_eq!(concrete_dist, tiny_gap, epsilon = 1e-16); + } + + #[test] + fn test_overlapping_but_not_intersecting_linestrings() { + // LineStrings that overlap in projection but are at different heights + let ls1 = LineString::from(vec![(0.0, 0.0), (2.0, 0.0)]); + let ls2 = LineString::from(vec![(1.0, 1e-13), (3.0, 1e-13)]); + + let concrete_dist = Euclidean.distance(&ls1, &ls2); + let generic_dist = nearest_neighbour_distance(&ls1, &ls2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-15); + assert_relative_eq!(concrete_dist, 1e-13, epsilon = 1e-16); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Numerical Precision Tests │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_very_close_but_non_zero_distances() { + // Test extremely small but non-zero distances to check floating-point precision + let test_cases = [1e-15, 1e-14, 1e-13, 1e-12, 1e-11, 1e-10]; + + for &tiny_dist in &test_cases { + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(tiny_dist, 0.0); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-16); + assert_relative_eq!(concrete_dist, tiny_dist, epsilon = 1e-16); + assert!( + concrete_dist > 0.0, + "Distance should be positive for tiny_dist = {tiny_dist}" + ); + } + } + + #[test] + fn test_numerical_precision_near_floating_point_limits() { + // Test with coordinates that produce distances near floating-point precision limits + let base = 1.0; + let tiny_offset = f64::EPSILON * 10.0; // Slightly above machine epsilon + + let p1 = Point::new(base, base); + let p2 = Point::new(base + tiny_offset, base); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-15); + assert!(concrete_dist > 0.0); + assert!(concrete_dist < 1e-14); // Should be very small but measurable + } + + #[test] + fn test_precision_with_large_coordinate_differences() { + // Test with one geometry having small coordinates and another having large coordinates + let small_point = Point::new(1e-10, 1e-10); + let large_polygon = Polygon::new( + LineString::from(vec![ + (1e8, 1e8), + (1e8 + 1.0, 1e8), + (1e8 + 1.0, 1e8 + 1.0), + (1e8, 1e8 + 1.0), + (1e8, 1e8), + ]), + vec![], + ); + + let concrete_dist = Euclidean.distance(&small_point, &large_polygon); + let generic_dist = distance_point_to_polygon_generic(&small_point, &large_polygon); + + assert_relative_eq!(concrete_dist, generic_dist, max_relative = 1e-10); + assert!(concrete_dist > 1e7); // Should be very large distance + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Robustness Tests │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_nan_coordinate_handling() { + // Test behavior with NaN coordinates + let nan_point = Point::new(f64::NAN, 0.0); + let normal_point = Point::new(1.0, 1.0); + + let distance = distance_point_to_point_generic(&nan_point, &normal_point); + + // Distance involving NaN should be NaN + assert!( + distance.is_nan(), + "Distance with NaN coordinate should be NaN" + ); + } + + #[test] + fn test_infinity_coordinate_handling() { + // Test behavior with infinite coordinates + let inf_point = Point::new(f64::INFINITY, 0.0); + let normal_point = Point::new(1.0, 1.0); + + let distance = distance_point_to_point_generic(&inf_point, &normal_point); + + // Distance involving infinity should be infinity + assert!( + distance.is_infinite(), + "Distance with infinite coordinate should be infinite" + ); + } + + #[test] + fn test_negative_infinity_coordinate_handling() { + // Test behavior with negative infinite coordinates + let neg_inf_point = Point::new(f64::NEG_INFINITY, 0.0); + let normal_point = Point::new(1.0, 1.0); + + let distance = distance_point_to_point_generic(&neg_inf_point, &normal_point); + + // Distance involving negative infinity should be infinity + assert!( + distance.is_infinite(), + "Distance with negative infinite coordinate should be infinite" + ); + } + + #[test] + fn test_mixed_special_values() { + // Test combinations of NaN and infinity + let nan_point = Point::new(f64::NAN, f64::INFINITY); + let inf_point = Point::new(f64::INFINITY, f64::NEG_INFINITY); + + let distance = distance_point_to_point_generic(&nan_point, &inf_point); + + // Any operation involving NaN should result in NaN or Infinity depending on the math + // Since we're using hypot which can handle NaN differently, let's test that it's either NaN or infinite + assert!( + distance.is_nan() || distance.is_infinite(), + "Distance involving NaN and Infinity should be NaN or Infinite, got: {distance}" + ); + } + + #[test] + fn test_subnormal_number_handling() { + // Test with subnormal (denormalized) numbers + let subnormal = f64::MIN_POSITIVE / 2.0; // This creates a subnormal number + assert!(subnormal > 0.0 && subnormal < f64::MIN_POSITIVE); + + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(subnormal, 0.0); + + let concrete_dist = Euclidean.distance(&p1, &p2); + let generic_dist = distance_point_to_point_generic(&p1, &p2); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-16); + assert_relative_eq!(concrete_dist, subnormal, epsilon = 1e-16); + assert!(concrete_dist > 0.0); + } + + #[test] + fn test_zero_vs_negative_zero() { + // Test behavior with positive zero vs negative zero + let p1 = Point::new(0.0, 0.0); + let p2 = Point::new(-0.0, -0.0); // Negative zero + + let distance = distance_point_to_point_generic(&p1, &p2); + + // Distance between +0 and -0 should be exactly 0 + assert_eq!( + distance, 0.0, + "Distance between +0 and -0 should be exactly 0" + ); + } + + // ┌────────────────────────────────────────────────────────────┐ + // │ Algorithmic Correctness Validation Tests │ + // └────────────────────────────────────────────────────────────┘ + + #[test] + fn test_linestring_inside_polygon_with_holes_correctness() { + // This test exposes the algorithmic difference between generic and concrete implementations + + // Create a polygon with a hole + let outer = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let hole = LineString::from(vec![ + (3.0, 3.0), + (7.0, 3.0), + (7.0, 7.0), + (3.0, 7.0), + (3.0, 3.0), + ]); + let polygon = Polygon::new(outer, vec![hole]); + + // LineString that is INSIDE the polygon but OUTSIDE the hole + let linestring_inside = LineString::from(vec![(1.0, 1.0), (2.0, 2.0)]); + + let concrete_dist = Euclidean.distance(&linestring_inside, &polygon); + let generic_dist = distance_linestring_to_polygon_generic(&linestring_inside, &polygon); + + // The results should be identical + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + } + + #[test] + fn test_linestring_outside_polygon_with_holes_correctness() { + // Test case where LineString is completely outside the polygon + + let outer = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let hole = LineString::from(vec![ + (3.0, 3.0), + (7.0, 3.0), + (7.0, 7.0), + (3.0, 7.0), + (3.0, 3.0), + ]); + let polygon = Polygon::new(outer, vec![hole]); + + // LineString that is OUTSIDE the polygon entirely + let linestring_outside = LineString::from(vec![(12.0, 12.0), (13.0, 13.0)]); + + let concrete_dist = Euclidean.distance(&linestring_outside, &polygon); + let generic_dist = distance_linestring_to_polygon_generic(&linestring_outside, &polygon); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + } + + #[test] + fn test_linestring_crossing_polygon_boundary_correctness() { + // Test case where LineString crosses the polygon boundary + + let outer = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let polygon = Polygon::new(outer, vec![]); + + // LineString that crosses the polygon boundary (should intersect) + let linestring_crossing = LineString::from(vec![(-1.0, 5.0), (11.0, 5.0)]); + + let concrete_dist = Euclidean.distance(&linestring_crossing, &polygon); + let generic_dist = distance_linestring_to_polygon_generic(&linestring_crossing, &polygon); + + // Both should be 0.0 since they intersect + assert_eq!( + concrete_dist, 0.0, + "Concrete should return 0 for intersecting geometries" + ); + assert_eq!( + generic_dist, 0.0, + "Generic should return 0 for intersecting geometries" + ); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + } + + #[test] + fn test_containment_logic_specific() { + // This test specifically checks the containment logic for polygons with holes + use geo_types::{LineString, Polygon}; + + // Create a larger polygon with a hole + let exterior = LineString::from(vec![ + (0.0, 0.0), + (20.0, 0.0), + (20.0, 20.0), + (0.0, 20.0), + (0.0, 0.0), + ]); + let hole = LineString::from(vec![ + (8.0, 8.0), + (12.0, 8.0), + (12.0, 12.0), + (8.0, 12.0), + (8.0, 8.0), + ]); + let polygon = Polygon::new(exterior, vec![hole]); + + // LineString that is INSIDE the polygon but OUTSIDE the hole, + // Create a small LineString very close to itself to avoid intersection + let inside_linestring = LineString::from(vec![(5.0, 5.0), (5.1, 5.1)]); + + let concrete_distance = Euclidean.distance(&inside_linestring, &polygon); + let generic_distance = distance_linestring_to_polygon_generic(&inside_linestring, &polygon); + + // Check if LineString actually intersects with the polygon + use crate::algorithm::Intersects; + let _does_intersect = inside_linestring.intersects(&polygon); + + assert_relative_eq!(concrete_distance, generic_distance, epsilon = 1e-10); + } + + #[test] + fn test_polygon_to_polygon_symmetric_containment_correctness() { + // Test that both A contains B and B contains A cases work correctly + use geo_types::{LineString, Polygon}; + + // Case 1: Large polygon with hole contains small polygon + let large_exterior = LineString::from(vec![ + (0.0, 0.0), + (20.0, 0.0), + (20.0, 20.0), + (0.0, 20.0), + (0.0, 0.0), + ]); + let large_hole = LineString::from(vec![ + (8.0, 8.0), + (12.0, 8.0), + (12.0, 12.0), + (8.0, 12.0), + (8.0, 8.0), + ]); + let large_polygon = Polygon::new(large_exterior, vec![large_hole]); + + // Small polygon inside the large polygon (but outside the hole) + let small_exterior = LineString::from(vec![ + (2.0, 2.0), + (6.0, 2.0), + (6.0, 6.0), + (2.0, 6.0), + (2.0, 2.0), + ]); + let small_polygon = Polygon::new(small_exterior, vec![]); + + // Test A contains B: large polygon with hole contains small polygon + let concrete_dist_ab = Euclidean.distance(&small_polygon, &large_polygon); + let generic_dist_ab = distance_polygon_to_polygon_generic(&small_polygon, &large_polygon); + + // Test B contains A: small polygon contains large polygon (should be distance between exteriors) + let concrete_dist_ba = Euclidean.distance(&large_polygon, &small_polygon); + let generic_dist_ba = distance_polygon_to_polygon_generic(&large_polygon, &small_polygon); + + // Both directions should match between concrete and generic + assert_relative_eq!(concrete_dist_ab, generic_dist_ab, epsilon = 1e-10); + assert_relative_eq!(concrete_dist_ba, generic_dist_ba, epsilon = 1e-10); + + // The distances should be the same due to symmetry + assert_relative_eq!(concrete_dist_ab, concrete_dist_ba, epsilon = 1e-10); + assert_relative_eq!(generic_dist_ab, generic_dist_ba, epsilon = 1e-10); + } + + #[test] + fn test_polygon_to_polygon_both_have_holes_correctness() { + // Test case where both polygons have holes + use geo_types::{LineString, Polygon}; + + // Polygon A with hole + let exterior_a = LineString::from(vec![ + (0.0, 0.0), + (10.0, 0.0), + (10.0, 10.0), + (0.0, 10.0), + (0.0, 0.0), + ]); + let hole_a = LineString::from(vec![ + (3.0, 3.0), + (7.0, 3.0), + (7.0, 7.0), + (3.0, 7.0), + (3.0, 3.0), + ]); + let polygon_a = Polygon::new(exterior_a, vec![hole_a]); + + // Polygon B with hole (separate from A) + let exterior_b = LineString::from(vec![ + (15.0, 0.0), + (25.0, 0.0), + (25.0, 10.0), + (15.0, 10.0), + (15.0, 0.0), + ]); + let hole_b = LineString::from(vec![ + (18.0, 3.0), + (22.0, 3.0), + (22.0, 7.0), + (18.0, 7.0), + (18.0, 3.0), + ]); + let polygon_b = Polygon::new(exterior_b, vec![hole_b]); + + // Neither polygon contains the other, so should calculate distance between exteriors + let concrete_dist = Euclidean.distance(&polygon_a, &polygon_b); + let generic_dist = distance_polygon_to_polygon_generic(&polygon_a, &polygon_b); + + assert_relative_eq!(concrete_dist, generic_dist, epsilon = 1e-10); + + // Test symmetry + let concrete_dist_reverse = Euclidean.distance(&polygon_b, &polygon_a); + let generic_dist_reverse = distance_polygon_to_polygon_generic(&polygon_b, &polygon_a); + + assert_relative_eq!(concrete_dist_reverse, generic_dist_reverse, epsilon = 1e-10); + assert_relative_eq!(concrete_dist, concrete_dist_reverse, epsilon = 1e-10); + } + + #[test] + fn test_point_to_linestring_containment_optimization() { + // Test that the containment check optimization works correctly + use geo_types::{LineString, Point}; + + // Create a LineString + let linestring = LineString::from(vec![(0.0, 0.0), (5.0, 0.0), (5.0, 5.0), (10.0, 5.0)]); + + // Point ON the LineString (should return 0 due to containment check) + let point_on_line = Point::new(2.5, 0.0); // On first segment + let concrete_dist_on = Euclidean.distance(&point_on_line, &linestring); + let generic_dist_on = distance_point_to_linestring_generic(&point_on_line, &linestring); + + // Both should be exactly 0 due to containment + assert_eq!(concrete_dist_on, 0.0); + assert_eq!(generic_dist_on, 0.0); + assert_relative_eq!(concrete_dist_on, generic_dist_on, epsilon = 1e-10); + + // Point ON a vertex (should return 0) + let point_on_vertex = Point::new(5.0, 0.0); + let concrete_dist_vertex = Euclidean.distance(&point_on_vertex, &linestring); + let generic_dist_vertex = + distance_point_to_linestring_generic(&point_on_vertex, &linestring); + + assert_eq!(concrete_dist_vertex, 0.0); + assert_eq!(generic_dist_vertex, 0.0); + assert_relative_eq!(concrete_dist_vertex, generic_dist_vertex, epsilon = 1e-10); + + // Point NOT on the LineString (should calculate actual distance) + let point_off_line = Point::new(2.5, 3.0); + let concrete_dist_off = Euclidean.distance(&point_off_line, &linestring); + let generic_dist_off = distance_point_to_linestring_generic(&point_off_line, &linestring); + + // Should be greater than 0 and both implementations should match + assert!(concrete_dist_off > 0.0); + assert!(generic_dist_off > 0.0); + assert_relative_eq!(concrete_dist_off, generic_dist_off, epsilon = 1e-10); + } + + #[test] + fn test_line_segment_distance_algorithm_equivalence() { + // Test that the updated generic algorithm produces identical results to concrete + use geo_types::{coord, Line, Point}; + + // Test cases covering different scenarios + let test_cases = vec![ + // Point, Line start, Line end + ( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // Before start + ( + coord! { x: 2.0, y: 1.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // Perpendicular + ( + coord! { x: 4.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // Beyond end + ( + coord! { x: 2.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // On line + ( + coord! { x: 1.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // On start point + ( + coord! { x: 3.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + coord! { x: 3.0, y: 0.0 }, + ), // On end point + ( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 1.0 }, + coord! { x: 1.0, y: 1.0 }, + ), // Degenerate line + ( + coord! { x: 2.5, y: 3.0 }, + coord! { x: 0.0, y: 0.0 }, + coord! { x: 5.0, y: 5.0 }, + ), // Diagonal line + ]; + + for (point_coord, start_coord, end_coord) in test_cases { + let point = Point::from(point_coord); + let line = Line::new(start_coord, end_coord); + + // Test concrete implementation + let concrete_distance = Euclidean.distance(&point, &line); + + // Test generic implementation + let generic_distance = distance_coord_to_line_generic(&point_coord, &line); + + // They should be identical now + assert_relative_eq!(concrete_distance, generic_distance, epsilon = 1e-15); + } + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/mod.rs new file mode 100644 index 00000000..07dde9dc --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/metric_spaces/mod.rs @@ -0,0 +1,24 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Generic Metric Spaces (currently Euclidean) +//! +//! Ported (and contains copied code) from `geo::algorithm::line_measures::metric_spaces`: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +pub mod euclidean; +pub use euclidean::DistanceExt; +pub use euclidean::Euclidean; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/line_measures/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/mod.rs new file mode 100644 index 00000000..d0025bb9 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/line_measures/mod.rs @@ -0,0 +1,29 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Generic line measurement algorithms (distance, length, perimeter, metric spaces) +//! +//! Ported (and contains copied code) from `geo::algorithm::line_measures` and related modules: +//! . +//! Original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +mod distance; +pub use distance::{Distance, DistanceExt}; + +mod length; +pub use length::LengthMeasurableExt; + +pub mod metric_spaces; +pub use metric_spaces::Euclidean; diff --git a/rust/sedona-geo-generic-alg/src/algorithm/map_coords.rs b/rust/sedona-geo-generic-alg/src/algorithm/map_coords.rs new file mode 100644 index 00000000..8705bf57 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/map_coords.rs @@ -0,0 +1,1101 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! 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; + +use core::borrow::Borrow; +use sedona_geo_traits_ext::*; +use Coord; + +/// Map a function over all the coordinates in an object, returning a new one +pub trait MapCoords { + type Output; + + /// Apply a function to all the coordinates in a geometric object, returning a new object. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::MapCoords; + /// use sedona_geo_generic_alg::{Coord, Point}; + /// use approx::assert_relative_eq; + /// + /// let p1 = Point::new(10., 20.); + /// let p2 = p1.map_coords(|Coord { x, y }| Coord { x: x + 1000., y: y * 2. }); + /// + /// assert_relative_eq!(p2, Point::new(1010., 40.), epsilon = 1e-6); + /// ``` + /// + /// Note that the input and output numeric types need not match. + /// + /// For example, consider OpenStreetMap's coordinate encoding scheme, which, to save space, + /// encodes latitude/longitude as 32bit signed integers from the floating point values + /// to six decimal places (eg. lat/lon * 1000000). + /// + /// ``` + /// # use geo::{Coord, Point}; + /// # use geo::MapCoords; + /// # use approx::assert_relative_eq; + /// + /// let SCALE_FACTOR: f64 = 1000000.0; + /// let floating_point_geom: Point = Point::new(10.15f64, 20.05f64); + /// let fixed_point_geom: Point = floating_point_geom.map_coords(|Coord { x, y }| { + /// Coord { x: (x * SCALE_FACTOR) as i32, y: (y * SCALE_FACTOR) as i32 } + /// }); + /// + /// assert_eq!(fixed_point_geom.x(), 10150000); + /// ``` + /// + /// If you want *only* to convert between numeric types (i32 -> f64) without further + /// transformation, consider using [`Convert`](crate::Convert). + fn map_coords(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output + where + T: CoordNum, + NT: CoordNum; + + /// Map a fallible function over all the coordinates in a geometry, returning a Result + /// + /// # Examples + /// + /// ``` + /// use approx::assert_relative_eq; + /// use sedona_geo_generic_alg::MapCoords; + /// use sedona_geo_generic_alg::{Coord, Point}; + /// + /// let p1 = Point::new(10., 20.); + /// let p2 = p1 + /// .try_map_coords(|Coord { x, y }| -> Result<_, std::convert::Infallible> { + /// Ok(Coord { x: x + 1000., y: y * 2. }) + /// }).unwrap(); + /// + /// assert_relative_eq!(p2, Point::new(1010., 40.), epsilon = 1e-6); + /// ``` + fn try_map_coords( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result + where + T: CoordNum, + NT: CoordNum; +} + +pub trait MapCoordsInPlace { + /// Apply a function to all the coordinates in a geometric object, in place + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::MapCoordsInPlace; + /// use sedona_geo_generic_alg::{Coord, Point}; + /// use approx::assert_relative_eq; + /// + /// let mut p = Point::new(10., 20.); + /// p.map_coords_in_place(|Coord { x, y }| Coord { x: x + 1000., y: y * 2. }); + /// + /// assert_relative_eq!(p, Point::new(1010., 40.), epsilon = 1e-6); + /// ``` + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) + where + T: CoordNum; + + /// Map a fallible function over all the coordinates in a geometry, in place, returning a `Result`. + /// + /// Upon encountering an `Err` from the function, `try_map_coords_in_place` immediately returns + /// and the geometry is potentially left in a partially mapped state. + /// + /// # Examples + /// + /// ``` + /// use sedona_geo_generic_alg::MapCoordsInPlace; + /// use sedona_geo_generic_alg::Coord; + /// + /// let mut p1 = geo::point!{x: 10u32, y: 20u32}; + /// + /// p1.try_map_coords_in_place(|Coord { x, y }| -> Result<_, &str> { + /// Ok(Coord { + /// x: x.checked_add(1000).ok_or("Overflow")?, + /// y: y.checked_mul(2).ok_or("Overflow")?, + /// }) + /// })?; + /// + /// assert_eq!( + /// p1, + /// geo::point!{x: 1010u32, y: 40u32}, + /// ); + /// # Ok::<(), &str>(()) + /// ``` + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> + where + T: CoordNum; +} + +// Generic implementation using trait-based approach +impl MapCoords for G +where + T: CoordNum, + NT: CoordNum, + G: GeoTraitExtWithTypeTag + MapCoordsTrait, +{ + type Output = >::Output; + + fn map_coords(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + self.map_coords_trait(func) + } + + fn try_map_coords( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + self.try_map_coords_trait(func) + } +} + +pub trait MapCoordsTrait +where + T: CoordNum, + NT: CoordNum, +{ + type Output; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output; + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result; +} + +//-----------------------// +// Point implementations // +//-----------------------// + +impl MapCoordsTrait for P +where + T: CoordNum, + NT: CoordNum, + P: PointTraitExt, +{ + type Output = Point; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + if let Some(coord) = self.geo_coord() { + Point(func(coord)) + } else { + Point::new(NT::zero(), NT::zero()) + } + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + if let Some(coord) = self.geo_coord() { + Ok(Point(func(coord)?)) + } else { + Ok(Point::new(NT::zero(), NT::zero())) + } + } +} + +impl MapCoordsInPlace for Point { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord) { + self.0 = func(self.0); + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + self.0 = func(self.0)?; + Ok(()) + } +} + +//----------------------// +// Line implementations // +//----------------------// + +impl MapCoordsTrait for L +where + T: CoordNum, + NT: CoordNum, + L: LineTraitExt, +{ + type Output = Line; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + Line::new(func(self.start_coord()), func(self.end_coord())) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + Ok(Line::new( + func(self.start_coord())?, + func(self.end_coord())?, + )) + } +} + +impl MapCoordsInPlace for Line { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord) { + self.start = func(self.start); + self.end = func(self.end); + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + self.start = func(self.start)?; + self.end = func(self.end)?; + + Ok(()) + } +} + +//----------------------------// +// LineString implementations // +//----------------------------// + +impl MapCoordsTrait for LS +where + T: CoordNum, + NT: CoordNum, + LS: LineStringTraitExt, +{ + type Output = LineString; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + let coords = self.coord_iter().map(func).collect::>(); + LineString::new(coords) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + let coords = self.coord_iter().map(func).collect::, E>>()?; + Ok(LineString::new(coords)) + } +} + +impl MapCoordsInPlace for LineString { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord) { + for p in &mut self.0 { + *p = func(*p); + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + for p in &mut self.0 { + *p = func(*p)?; + } + Ok(()) + } +} + +//-------------------------// +// Polygon implementations // +//-------------------------// + +impl MapCoordsTrait for P +where + T: CoordNum, + NT: CoordNum, + P: PolygonTraitExt, +{ + type Output = Polygon; + + 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![]), + }; + + let interiors = self + .interiors_ext() + .map(|line| line.map_coords(func)) + .collect(); + + Polygon::new(exterior, interiors) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + let exterior = match self.exterior_ext() { + Some(ext) => ext.try_map_coords(func)?, + None => LineString::new(vec![]), + }; + + let interiors = self + .interiors_ext() + .map(|line| line.try_map_coords(func)) + .collect::, E>>()?; + + Ok(Polygon::new(exterior, interiors)) + } +} + +impl MapCoordsInPlace for Polygon { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + self.exterior_mut(|line_string| { + line_string.map_coords_in_place(func); + }); + + self.interiors_mut(|line_strings| { + for line_string in line_strings { + line_string.map_coords_in_place(func); + } + }); + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + let mut result = Ok(()); + + self.exterior_mut(|line_string| { + if let Err(e) = line_string.try_map_coords_in_place(&func) { + result = Err(e); + } + }); + + if result.is_ok() { + self.interiors_mut(|line_strings| { + for line_string in line_strings { + if let Err(e) = line_string.try_map_coords_in_place(&func) { + result = Err(e); + break; + } + } + }); + } + + result + } +} + +//----------------------------// +// MultiPoint implementations // +//----------------------------// + +impl MapCoordsTrait for MP +where + T: CoordNum, + NT: CoordNum, + MP: MultiPointTraitExt, +{ + type Output = MultiPoint; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + MultiPoint::new(self.points_ext().map(|p| p.map_coords(func)).collect()) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + Ok(MultiPoint::new( + self.points_ext() + .map(|p| p.try_map_coords(func)) + .collect::, E>>()?, + )) + } +} + +impl MapCoordsInPlace for MultiPoint { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + for p in &mut self.0 { + p.map_coords_in_place(func); + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + for p in &mut self.0 { + p.try_map_coords_in_place(&func)?; + } + Ok(()) + } +} + +//---------------------------------// +// MultiLineString implementations // +//---------------------------------// + +impl MapCoordsTrait for MLS +where + T: CoordNum, + NT: CoordNum, + MLS: MultiLineStringTraitExt, +{ + type Output = MultiLineString; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + MultiLineString::new( + self.line_strings_ext() + .map(|l| l.map_coords(func)) + .collect(), + ) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + Ok(MultiLineString::new( + self.line_strings_ext() + .map(|l| l.try_map_coords(func)) + .collect::, E>>()?, + )) + } +} + +impl MapCoordsInPlace for MultiLineString { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + for p in &mut self.0 { + p.map_coords_in_place(func); + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + for p in &mut self.0 { + p.try_map_coords_in_place(&func)?; + } + Ok(()) + } +} + +//------------------------------// +// MultiPolygon implementations // +//------------------------------// + +impl MapCoordsTrait for MP +where + T: CoordNum, + NT: CoordNum, + MP: MultiPolygonTraitExt, +{ + type Output = MultiPolygon; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + MultiPolygon::new(self.polygons_ext().map(|p| p.map_coords(func)).collect()) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + Ok(MultiPolygon::new( + self.polygons_ext() + .map(|p| p.try_map_coords(func)) + .collect::, E>>()?, + )) + } +} + +impl MapCoordsInPlace for MultiPolygon { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + for p in &mut self.0 { + p.map_coords_in_place(func); + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + for p in &mut self.0 { + p.try_map_coords_in_place(&func)?; + } + Ok(()) + } +} + +//--------------------------// +// Geometry implementations // +//--------------------------// + +impl MapCoordsTrait for G +where + T: CoordNum, + NT: CoordNum, + G: GeometryTraitExt, +{ + type Output = Geometry; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + if self.is_collection() { + let collection = GeometryCollection::new_from( + self.geometries_ext() + .map(|g| g.borrow().map_coords(func)) + .collect(), + ); + Geometry::GeometryCollection(collection) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(x) => Geometry::Point(x.map_coords_trait(func)), + GeometryTypeExt::Line(x) => Geometry::Line(x.map_coords_trait(func)), + GeometryTypeExt::LineString(x) => Geometry::LineString(x.map_coords_trait(func)), + GeometryTypeExt::Polygon(x) => Geometry::Polygon(x.map_coords_trait(func)), + GeometryTypeExt::MultiPoint(x) => Geometry::MultiPoint(x.map_coords_trait(func)), + GeometryTypeExt::MultiLineString(x) => { + Geometry::MultiLineString(x.map_coords_trait(func)) + } + GeometryTypeExt::MultiPolygon(x) => { + Geometry::MultiPolygon(x.map_coords_trait(func)) + } + GeometryTypeExt::Rect(x) => Geometry::Rect(x.map_coords_trait(func)), + GeometryTypeExt::Triangle(x) => Geometry::Triangle(x.map_coords_trait(func)), + } + } + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + if self.is_collection() { + let geoms = self + .geometries_ext() + .map(|g| g.borrow().try_map_coords(func)) + .collect::, E>>()?; + let collection = GeometryCollection::new_from(geoms); + Ok(Geometry::GeometryCollection(collection)) + } else { + match self.as_type_ext() { + GeometryTypeExt::Point(x) => Ok(Geometry::Point(x.try_map_coords_trait(func)?)), + GeometryTypeExt::Line(x) => Ok(Geometry::Line(x.try_map_coords_trait(func)?)), + GeometryTypeExt::LineString(x) => { + Ok(Geometry::LineString(x.try_map_coords_trait(func)?)) + } + GeometryTypeExt::Polygon(x) => Ok(Geometry::Polygon(x.try_map_coords_trait(func)?)), + GeometryTypeExt::MultiPoint(x) => { + Ok(Geometry::MultiPoint(x.try_map_coords_trait(func)?)) + } + GeometryTypeExt::MultiLineString(x) => { + Ok(Geometry::MultiLineString(x.try_map_coords_trait(func)?)) + } + GeometryTypeExt::MultiPolygon(x) => { + Ok(Geometry::MultiPolygon(x.try_map_coords_trait(func)?)) + } + GeometryTypeExt::Rect(x) => Ok(Geometry::Rect(x.try_map_coords_trait(func)?)), + GeometryTypeExt::Triangle(x) => { + Ok(Geometry::Triangle(x.try_map_coords_trait(func)?)) + } + } + } + } +} + +impl MapCoordsInPlace for Geometry { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + match *self { + Geometry::Point(ref mut x) => x.map_coords_in_place(func), + Geometry::Line(ref mut x) => x.map_coords_in_place(func), + Geometry::LineString(ref mut x) => x.map_coords_in_place(func), + Geometry::Polygon(ref mut x) => x.map_coords_in_place(func), + Geometry::MultiPoint(ref mut x) => x.map_coords_in_place(func), + Geometry::MultiLineString(ref mut x) => x.map_coords_in_place(func), + Geometry::MultiPolygon(ref mut x) => x.map_coords_in_place(func), + Geometry::GeometryCollection(ref mut x) => x.map_coords_in_place(func), + Geometry::Rect(ref mut x) => x.map_coords_in_place(func), + Geometry::Triangle(ref mut x) => x.map_coords_in_place(func), + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + match *self { + Geometry::Point(ref mut x) => x.try_map_coords_in_place(func), + Geometry::Line(ref mut x) => x.try_map_coords_in_place(func), + Geometry::LineString(ref mut x) => x.try_map_coords_in_place(func), + Geometry::Polygon(ref mut x) => x.try_map_coords_in_place(func), + Geometry::MultiPoint(ref mut x) => x.try_map_coords_in_place(func), + Geometry::MultiLineString(ref mut x) => x.try_map_coords_in_place(func), + Geometry::MultiPolygon(ref mut x) => x.try_map_coords_in_place(func), + Geometry::GeometryCollection(ref mut x) => x.try_map_coords_in_place(func), + Geometry::Rect(ref mut x) => x.try_map_coords_in_place(func), + Geometry::Triangle(ref mut x) => x.try_map_coords_in_place(func), + } + } +} + +//------------------------------------// +// GeometryCollection implementations // +//------------------------------------// + +impl MapCoordsTrait for GC +where + T: CoordNum, + NT: CoordNum, + GC: GeometryCollectionTraitExt, +{ + type Output = GeometryCollection; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + GeometryCollection::new_from(self.geometries_ext().map(|g| g.map_coords(func)).collect()) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E> + Copy, + ) -> Result { + Ok(GeometryCollection::new_from( + self.geometries_ext() + .map(|g| g.try_map_coords(func)) + .collect::, E>>()?, + )) + } +} + +impl MapCoordsInPlace for GeometryCollection { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord + Copy) { + for p in &mut self.0 { + p.map_coords_in_place(func); + } + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + for p in &mut self.0 { + p.try_map_coords_in_place(&func)?; + } + Ok(()) + } +} + +//----------------------// +// Rect implementations // +//----------------------// + +impl MapCoordsTrait for R +where + T: CoordNum, + NT: CoordNum, + R: RectTraitExt, +{ + type Output = Rect; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + Rect::new(func(self.min_coord()), func(self.max_coord())) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result { + Ok(Rect::new(func(self.min_coord())?, func(self.max_coord())?)) + } +} + +impl MapCoordsInPlace for Rect { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord) { + let mut new_rect = Rect::new(func(self.min()), func(self.max())); + ::std::mem::swap(self, &mut new_rect); + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + let mut new_rect = Rect::new(func(self.min())?, func(self.max())?); + ::std::mem::swap(self, &mut new_rect); + Ok(()) + } +} + +//--------------------------// +// Triangle implementations // +//--------------------------// + +impl MapCoordsTrait for TT +where + T: CoordNum, + NT: CoordNum, + TT: TriangleTraitExt, +{ + type Output = Triangle; + + fn map_coords_trait(&self, func: impl Fn(Coord) -> Coord + Copy) -> Self::Output { + Triangle::new( + func(self.first_coord()), + func(self.second_coord()), + func(self.third_coord()), + ) + } + + fn try_map_coords_trait( + &self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result { + Ok(Triangle::new( + func(self.first_coord())?, + func(self.second_coord())?, + func(self.third_coord())?, + )) + } +} + +impl MapCoordsInPlace for Triangle { + fn map_coords_in_place(&mut self, func: impl Fn(Coord) -> Coord) { + let mut new_triangle = Triangle::new(func(self.0), func(self.1), func(self.2)); + + ::std::mem::swap(self, &mut new_triangle); + } + + fn try_map_coords_in_place( + &mut self, + func: impl Fn(Coord) -> Result, E>, + ) -> Result<(), E> { + let mut new_triangle = Triangle::new(func(self.0)?, func(self.1)?, func(self.2)?); + + ::std::mem::swap(self, &mut new_triangle); + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::{MapCoords, MapCoordsInPlace}; + use crate::{ + coord, polygon, Coord, Geometry, GeometryCollection, Line, LineString, MultiLineString, + MultiPoint, MultiPolygon, Point, Polygon, Rect, + }; + + #[test] + fn point() { + let p = Point::new(10., 10.); + let new_p = p.map_coords(|Coord { x, y }| (x + 10., y + 100.).into()); + assert_relative_eq!(new_p.x(), 20.); + assert_relative_eq!(new_p.y(), 110.); + } + + #[test] + fn point_inplace() { + let mut p2 = Point::new(10f32, 10f32); + p2.map_coords_in_place(|Coord { x, y }| (x + 10., y + 100.).into()); + assert_relative_eq!(p2.x(), 20.); + assert_relative_eq!(p2.y(), 110.); + } + + #[test] + fn rect_inplace() { + let mut rect = Rect::new((10, 10), (20, 20)); + rect.map_coords_in_place(|Coord { x, y }| (x + 10, y + 20).into()); + assert_eq!(rect.min(), coord! { x: 20, y: 30 }); + assert_eq!(rect.max(), coord! { x: 30, y: 40 }); + } + + #[test] + fn rect_inplace_normalized() { + let mut rect = Rect::new((2, 2), (3, 3)); + // Rect's enforce that rect.min is up and left of p2. Here we test that the points are + // normalized into a valid rect, regardless of the order they are mapped. + rect.map_coords_in_place(|pt| { + match pt.x_y() { + // old min point maps to new max point + (2, 2) => (4, 4).into(), + // old max point maps to new min point + (3, 3) => (1, 1).into(), + _ => panic!("unexpected point"), + } + }); + + assert_eq!(rect.min(), coord! { x: 1, y: 1 }); + assert_eq!(rect.max(), coord! { x: 4, y: 4 }); + } + + #[test] + fn rect_map_coords() { + let rect = Rect::new((10, 10), (20, 20)); + let another_rect = rect.map_coords(|Coord { x, y }| (x + 10, y + 20).into()); + assert_eq!(another_rect.min(), coord! { x: 20, y: 30 }); + assert_eq!(another_rect.max(), coord! { x: 30, y: 40 }); + } + + #[test] + fn rect_try_map_coords() { + let rect = Rect::new((10i32, 10), (20, 20)); + let result = rect.try_map_coords(|Coord { x, y }| -> Result<_, &'static str> { + Ok(( + x.checked_add(10).ok_or("overflow")?, + y.checked_add(20).ok_or("overflow")?, + ) + .into()) + }); + assert!(result.is_ok()); + } + + #[test] + fn rect_try_map_coords_normalized() { + let rect = Rect::new((2, 2), (3, 3)); + // Rect's enforce that rect.min is up and left of p2. Here we test that the points are + // normalized into a valid rect, regardless of the order they are mapped. + let result: Result<_, std::convert::Infallible> = rect.try_map_coords(|pt| { + match pt.x_y() { + // old min point maps to new max point + (2, 2) => Ok((4, 4).into()), + // old max point maps to new min point + (3, 3) => Ok((1, 1).into()), + _ => panic!("unexpected point"), + } + }); + let new_rect = result.unwrap(); + assert_eq!(new_rect.min(), coord! { x: 1, y: 1 }); + assert_eq!(new_rect.max(), coord! { x: 4, y: 4 }); + } + + #[test] + fn line() { + let line = Line::from([(0., 0.), (1., 2.)]); + assert_relative_eq!( + line.map_coords(|Coord { x, y }| (x * 2., y).into()), + Line::from([(0., 0.), (2., 2.)]), + epsilon = 1e-6 + ); + } + + #[test] + fn linestring() { + let line1: LineString = LineString::from(vec![(0., 0.), (1., 2.)]); + let line2 = line1.map_coords(|Coord { x, y }| (x + 10., y - 100.).into()); + assert_relative_eq!(line2.0[0], Coord::from((10., -100.)), epsilon = 1e-6); + assert_relative_eq!(line2.0[1], Coord::from((11., -98.)), epsilon = 1e-6); + } + + #[test] + fn polygon() { + let exterior = LineString::from(vec![(0., 0.), (1., 1.), (1., 0.), (0., 0.)]); + let interiors = vec![LineString::from(vec![ + (0.1, 0.1), + (0.9, 0.9), + (0.9, 0.1), + (0.1, 0.1), + ])]; + let p = Polygon::new(exterior, interiors); + + let p2 = p.map_coords(|Coord { x, y }| (x + 10., y - 100.).into()); + + let exterior2 = + LineString::from(vec![(10., -100.), (11., -99.), (11., -100.), (10., -100.)]); + let interiors2 = vec![LineString::from(vec![ + (10.1, -99.9), + (10.9, -99.1), + (10.9, -99.9), + (10.1, -99.9), + ])]; + let expected_p2 = Polygon::new(exterior2, interiors2); + + assert_relative_eq!(p2, expected_p2, epsilon = 1e-6); + } + + #[test] + fn multipoint() { + let p1 = Point::new(10., 10.); + let p2 = Point::new(0., -100.); + let mp = MultiPoint::new(vec![p1, p2]); + + assert_eq!( + mp.map_coords(|Coord { x, y }| (x + 10., y + 100.).into()), + MultiPoint::new(vec![Point::new(20., 110.), Point::new(10., 0.)]) + ); + } + + #[test] + fn multilinestring() { + let line1: LineString = LineString::from(vec![(0., 0.), (1., 2.)]); + let line2: LineString = LineString::from(vec![(-1., 0.), (0., 0.), (1., 2.)]); + let mline = MultiLineString::new(vec![line1, line2]); + let mline2 = mline.map_coords(|Coord { x, y }| (x + 10., y - 100.).into()); + assert_relative_eq!( + mline2, + MultiLineString::new(vec![ + LineString::from(vec![(10., -100.), (11., -98.)]), + LineString::from(vec![(9., -100.), (10., -100.), (11., -98.)]), + ]), + epsilon = 1e-6 + ); + } + + #[test] + fn multipolygon() { + let poly1 = polygon![ + (x: 0., y: 0.), + (x: 10., y: 0.), + (x: 10., y: 10.), + (x: 0., y: 10.), + (x: 0., y: 0.), + ]; + let poly2 = polygon![ + exterior: [ + (x: 11., y: 11.), + (x: 20., y: 11.), + (x: 20., y: 20.), + (x: 11., y: 20.), + (x: 11., y: 11.), + ], + interiors: [ + [ + (x: 13., y: 13.), + (x: 13., y: 17.), + (x: 17., y: 17.), + (x: 17., y: 13.), + (x: 13., y: 13.), + ] + ], + ]; + + let mp = MultiPolygon::new(vec![poly1, poly2]); + let mp2 = mp.map_coords(|Coord { x, y }| (x * 2., y + 100.).into()); + assert_eq!(mp2.0.len(), 2); + assert_relative_eq!( + mp2.0[0], + polygon![ + (x: 0., y: 100.), + (x: 20., y: 100.), + (x: 20., y: 110.), + (x: 0., y: 110.), + (x: 0., y: 100.), + ], + ); + assert_relative_eq!( + mp2.0[1], + polygon![ + exterior: [ + (x: 22., y: 111.), + (x: 40., y: 111.), + (x: 40., y: 120.), + (x: 22., y: 120.), + (x: 22., y: 111.), + ], + interiors: [ + [ + (x: 26., y: 113.), + (x: 26., y: 117.), + (x: 34., y: 117.), + (x: 34., y: 113.), + (x: 26., y: 113.), + ], + ], + ], + ); + } + + #[test] + fn geometrycollection() { + let p1 = Geometry::Point(Point::new(10., 10.)); + let line1 = Geometry::LineString(LineString::from(vec![(0., 0.), (1., 2.)])); + + let gc = GeometryCollection::new_from(vec![p1, line1]); + let expected = GeometryCollection::new_from(vec![ + Geometry::Point(Point::new(20., 110.)), + Geometry::LineString(LineString::from(vec![(10., 100.), (11., 102.)])), + ]); + + assert_eq!( + gc.map_coords(|Coord { x, y }| (x + 10., y + 100.).into()), + expected + ); + assert_eq!( + Geometry::GeometryCollection(gc) + .map_coords(|Coord { x, y }| (x + 10., y + 100.).into()), + Geometry::GeometryCollection(expected) + ); + } + + #[test] + fn convert_type() { + let p1: Point = Point::new(1., 2.); + let p2: Point = p1.map_coords(|Coord { x, y }| (x as f32, y as f32).into()); + assert_relative_eq!(p2.x(), 1f32); + assert_relative_eq!(p2.y(), 2f32); + } + + #[test] + fn test_fallible() { + let f = |Coord { x, y }| -> Result<_, &'static str> { + if relative_ne!(x, 2.0) { + Ok((x * 2., y + 100.).into()) + } else { + Err("Ugh") + } + }; + // this should produce an error + let bad_ls: LineString<_> = vec![ + Point::new(1.0, 1.0), + Point::new(2.0, 2.0), + Point::new(3.0, 3.0), + ] + .into(); + // this should be fine + let good_ls: LineString<_> = vec![ + Point::new(1.0, 1.0), + Point::new(2.1, 2.0), + Point::new(3.0, 3.0), + ] + .into(); + let bad = bad_ls.try_map_coords(f); + assert!(bad.is_err()); + let good = good_ls.try_map_coords(f); + assert!(good.is_ok()); + assert_relative_eq!( + good.unwrap(), + vec![ + Point::new(2., 101.), + Point::new(4.2, 102.), + Point::new(6.0, 103.), + ] + .into() + ); + } + + #[test] + fn rect_map_invert_coords() { + let rect = Rect::new(coord! { x: 0., y: 0. }, coord! { x: 1., y: 1. }); + + // This call should not panic even though Rect::new + // constructor panics if min coords > max coords + rect.map_coords(|Coord { x, y }| (-x, -y).into()); + } +} diff --git a/rust/sedona-geo-generic-alg/src/algorithm/mod.rs b/rust/sedona-geo-generic-alg/src/algorithm/mod.rs new file mode 100644 index 00000000..59d4b40d --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/algorithm/mod.rs @@ -0,0 +1,63 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Collection of generic geometry algorithms ported from the `geo` crate. +//! +//! All submodules in this directory are ported (and contain copied code) from the +//! `geo` crate at commit `5d667f844716a3d0a17aa60bc0a58528cb5808c3`: +//! . +//! The original code is dual-licensed under Apache-2.0 or MIT; used here under Apache-2.0. +/// Kernels to compute various predicates +pub mod kernels; +pub use kernels::{Kernel, Orientation}; + +/// Calculate the area of the surface of a `Geometry`. +pub mod area; +pub use area::Area; + +/// Calculate the bounding rectangle of a `Geometry`. +pub mod bounding_rect; +pub use bounding_rect::BoundingRect; + +/// Calculate the centroid of a `Geometry`. +pub mod centroid; +pub use centroid::Centroid; + +/// Determine whether a `Coord` lies inside, outside, or on the boundary of a geometry. +pub mod coordinate_position; +pub use coordinate_position::CoordinatePosition; + +/// Dimensionality of a geometry and its boundary, based on OGC-SFA. +pub mod dimensions; +pub use dimensions::HasDimensions; + +/// Calculate the length of a planar line between two `Geometries`. +pub mod euclidean_length; +#[allow(deprecated)] +pub use euclidean_length::EuclideanLength; + +/// Determine whether `Geometry` `A` intersects `Geometry` `B`. +pub mod intersects; +pub use intersects::Intersects; + +pub mod line_measures; +pub use line_measures::metric_spaces::Euclidean; + +pub use line_measures::{Distance, DistanceExt, LengthMeasurableExt}; + +/// Apply a function to all `Coord`s of a `Geometry`. +pub mod map_coords; +pub use map_coords::{MapCoords, MapCoordsInPlace}; diff --git a/rust/sedona-geo-generic-alg/src/geometry.rs b/rust/sedona-geo-generic-alg/src/geometry.rs new file mode 100644 index 00000000..0f3be17f --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/geometry.rs @@ -0,0 +1,26 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Geometry type re-exports +//! +//! Ported (and contains copied code pattern) from the `geo` crate (geometry module) at commit +//! `5d667f844716a3d0a17aa60bc0a58528cb5808c3`: +//! . +//! This file is a thin wrapper that publicly re-exports geometry types from `geo-types` to +//! mirror the upstream API surface. Original upstream code is dual-licensed under Apache-2.0 or MIT; +//! used here under Apache-2.0. +//! This module makes all geometry types available +pub use geo_types::geometry::*; diff --git a/rust/sedona-geo-generic-alg/src/lib.rs b/rust/sedona-geo-generic-alg/src/lib.rs new file mode 100644 index 00000000..6f981542 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/lib.rs @@ -0,0 +1,149 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Crate root for generic geometry algorithms ported from `geo`. +//! +//! Substantial portions of this crate (algorithm modules, trait patterns, and API surface) are +//! ported (and contain copied code) from the `geo` crate at commit +//! `5d667f844716a3d0a17aa60bc0a58528cb5808c3`: +//! . +//! The original upstream project is dual-licensed under Apache-2.0 or MIT; the copied/ported code +//! here is used under the Apache-2.0 license consistent with this repository. +//! This top-level file orchestrates module exposure and numeric traits mirroring upstream design. +pub use crate::algorithm::*; +use std::cmp::Ordering; + +pub use geo_types::{coord, line_string, point, polygon, wkt, CoordFloat, CoordNum}; + +pub mod geometry; +pub use geometry::*; + +/// This module includes all the functions of geometric calculations +pub mod algorithm; +mod utils; +use crate::kernels::{RobustKernel, SimpleKernel}; + +#[cfg(test)] +#[macro_use] +extern crate approx; + +#[cfg(test)] +#[macro_use] +extern crate log; + +/// A prelude which re-exports the traits for manipulating objects in this +/// crate. Typically imported with `use geo::prelude::*`. +pub mod prelude { + pub use crate::algorithm::*; +} + +/// A common numeric trait used for geo algorithms +/// +/// Different numeric types have different tradeoffs. `geo` strives to utilize generics to allow +/// users to choose their numeric types. If you are writing a function which you'd like to be +/// generic over all the numeric types supported by geo, you probably want to constrain +/// your function input to `GeoFloat`. For methods which work for integers, and not just floating +/// point, see [`GeoNum`]. +pub trait GeoFloat: + GeoNum + num_traits::Float + num_traits::Signed + num_traits::Bounded + float_next_after::NextAfter +{ +} +impl GeoFloat for T where + T: GeoNum + + num_traits::Float + + num_traits::Signed + + num_traits::Bounded + + float_next_after::NextAfter +{ +} + +/// A trait for methods which work for both integers **and** floating point +pub trait GeoNum: CoordNum { + type Ker: Kernel; + + /// Return the ordering between self and other. + /// + /// For integers, this should behave just like [`Ord`]. + /// + /// For floating point numbers, unlike the standard partial comparison between floating point numbers, this comparison + /// always produces an ordering. + /// + /// See [f64::total_cmp](https://doc.rust-lang.org/src/core/num/f64.rs.html#1432) for details. + fn total_cmp(&self, other: &Self) -> Ordering; +} + +macro_rules! impl_geo_num_for_float { + ($t: ident) => { + impl GeoNum for $t { + type Ker = RobustKernel; + fn total_cmp(&self, other: &Self) -> Ordering { + self.total_cmp(other) + } + } + }; +} +macro_rules! impl_geo_num_for_int { + ($t: ident) => { + impl GeoNum for $t { + type Ker = SimpleKernel; + fn total_cmp(&self, other: &Self) -> Ordering { + self.cmp(other) + } + } + }; +} + +// This is the list of primitives that we support. +impl_geo_num_for_float!(f32); +impl_geo_num_for_float!(f64); +impl_geo_num_for_int!(i16); +impl_geo_num_for_int!(i32); +impl_geo_num_for_int!(i64); +impl_geo_num_for_int!(i128); +impl_geo_num_for_int!(isize); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn total_ord_float() { + assert_eq!(GeoNum::total_cmp(&3.0f64, &2.0f64), Ordering::Greater); + assert_eq!(GeoNum::total_cmp(&2.0f64, &2.0f64), Ordering::Equal); + assert_eq!(GeoNum::total_cmp(&1.0f64, &2.0f64), Ordering::Less); + assert_eq!(GeoNum::total_cmp(&1.0f64, &f64::NAN), Ordering::Less); + assert_eq!(GeoNum::total_cmp(&f64::NAN, &f64::NAN), Ordering::Equal); + assert_eq!(GeoNum::total_cmp(&f64::INFINITY, &f64::NAN), Ordering::Less); + } + + #[test] + fn total_ord_int() { + assert_eq!(GeoNum::total_cmp(&3i32, &2i32), Ordering::Greater); + assert_eq!(GeoNum::total_cmp(&2i32, &2i32), Ordering::Equal); + assert_eq!(GeoNum::total_cmp(&1i32, &2i32), Ordering::Less); + } + + #[test] + fn numeric_types() { + let _n_i16 = Point::new(1i16, 2i16); + let _n_i32 = Point::new(1i32, 2i32); + let _n_i64 = Point::new(1i64, 2i64); + let _n_i128 = Point::new(1i128, 2i128); + let _n_isize = Point::new(1isize, 2isize); + let _n_f32 = Point::new(1.0f32, 2.0f32); + let _n_f64 = Point::new(1.0f64, 2.0f64); + } +} diff --git a/rust/sedona-geo-generic-alg/src/utils.rs b/rust/sedona-geo-generic-alg/src/utils.rs new file mode 100644 index 00000000..a4029266 --- /dev/null +++ b/rust/sedona-geo-generic-alg/src/utils.rs @@ -0,0 +1,58 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +//! Internal utility functions, types, and data structures. +//! +//! Some helper logic (naming / minimal patterns) corresponds to simple utilities present in the +//! upstream `geo` crate at commit `5d667f844716a3d0a17aa60bc0a58528cb5808c3`. +//! (Example: partial_min / partial_max helpers). Where identical or trivially adapted, they are +//! used here under the upstream dual-license (Apache-2.0 or MIT); incorporated under Apache-2.0. +//! Upstream repository: . + +// The Rust standard library has `max` for `Ord`, but not for `PartialOrd` +pub fn partial_max(a: T, b: T) -> T { + if a > b { + a + } else { + b + } +} + +// The Rust standard library has `min` for `Ord`, but not for `PartialOrd` +pub fn partial_min(a: T, b: T) -> T { + if a < b { + a + } else { + b + } +} + +#[cfg(test)] +mod test { + use super::{partial_max, partial_min}; + + #[test] + fn test_partial_max() { + assert_eq!(5, partial_max(5, 4)); + assert_eq!(5, partial_max(5, 5)); + } + + #[test] + fn test_partial_min() { + assert_eq!(4, partial_min(5, 4)); + assert_eq!(4, partial_min(4, 4)); + } +}