diff --git a/Cargo.lock b/Cargo.lock index 93db6d4f..d9a3c0a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2659,8 +2659,9 @@ dependencies = [ [[package]] name = "geo-types" -version = "0.7.16" -source = "git+https://github.com/wherobots/geo.git?branch=generic-alg#66ff85949a82549b0d28fb2d4fae01e3ea19ca83" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a4dcd69d35b2c87a7c83bce9af69fd65c9d68d3833a0ded568983928f3fc99" dependencies = [ "approx", "num-traits", @@ -6583,3 +6584,8 @@ dependencies = [ "cc", "pkg-config", ] + +[[patch.unused]] +name = "geo-types" +version = "0.7.16" +source = "git+https://github.com/wherobots/geo.git?branch=generic-alg#66ff85949a82549b0d28fb2d4fae01e3ea19ca83" diff --git a/rust/sedona-geo-traits-ext/Cargo.toml b/rust/sedona-geo-traits-ext/Cargo.toml new file mode 100644 index 00000000..9a2a4651 --- /dev/null +++ b/rust/sedona-geo-traits-ext/Cargo.toml @@ -0,0 +1,42 @@ +# 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-traits-ext" +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-traits extended for implementing generic algorithms" +readme = "README.md" +edition = "2021" + +[workspace] + +[dependencies] +geo-traits = "0.3.0" +geo-types = "0.7.17" +num-traits = { version = "0.2", default-features = false, features = ["libm"] } +wkb = "0.9.1" +byteorder = "1" + +[dev-dependencies] +wkt = "0.14.0" +rstest = "0.24.0" + +[patch.crates-io] +wkb = { git = "https://github.com/georust/wkb.git", rev = "130eb0c2b343bc9299aeafba6d34c2a6e53f3b6a" } diff --git a/rust/sedona-geo-traits-ext/README.md b/rust/sedona-geo-traits-ext/README.md new file mode 100644 index 00000000..ce5ab0d3 --- /dev/null +++ b/rust/sedona-geo-traits-ext/README.md @@ -0,0 +1,37 @@ + + +# Geo-Traits Extended + +This crate extends the `geo-traits` crate with additional traits and +implementations. The goal is to provide a set of traits that are useful for +implementing algorithms in `geo-generic-alg` crate. Most of the methods are +inspired by the `geo-types` crate, but are implemented as traits for the +`geo-traits` types. Some methods returns concrete types defined in `geo-types`, +these methods are only for computing tiny, intermediate results during +algorithm execution. By adding methods in `geo-types` to `geo-traits-ext`, +we can port algorithms in `geo` crate for concrete `geo-types` types to generic +`geo-traits-ext` types more easily. + +`geo-traits-ext` traits also has an associated `Tag` type to workaround the +single orphan rule in Rust. For instance, we cannot write blanket `AreaTrait` +implementations for both `LineStringTrait` and `PolygonTrait` because we +cannot show that there would be no type implementing both `LineStringTrait` and +`PolygonTrait`. By adding an associated `Tag` type, we can write blanket +implementations for `AreaTrait` and `AreaTrait`, since +`AreaTrait` and `AreaTrait` are different types. +Please refer to the source code of `sedona-geo-generic-alg` for more details. diff --git a/rust/sedona-geo-traits-ext/src/coord.rs b/rust/sedona-geo-traits-ext/src/coord.rs new file mode 100644 index 00000000..cd7472d3 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/coord.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. +//! Extend CoordTrait traits for the `geo-traits` crate + +use geo_traits::{CoordTrait, UnimplementedCoord}; +use geo_types::{Coord, CoordNum}; + +use crate::{CoordTag, GeoTraitExtWithTypeTag}; + +/// Extension methods that bridge [`CoordTrait`] with concrete `geo-types` helpers. +pub trait CoordTraitExt: CoordTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + #[inline] + /// Converts this coordinate into the concrete [`geo_types::Coord`]. + fn geo_coord(&self) -> Coord { + Coord { + x: self.x(), + y: self.y(), + } + } +} + +impl CoordTraitExt for Coord +where + T: CoordNum, +{ + fn geo_coord(&self) -> Coord { + *self + } +} + +impl GeoTraitExtWithTypeTag for Coord { + type Tag = CoordTag; +} + +impl CoordTraitExt for &Coord +where + T: CoordNum, +{ + fn geo_coord(&self) -> Coord { + **self + } +} + +impl GeoTraitExtWithTypeTag for &Coord { + type Tag = CoordTag; +} + +impl CoordTraitExt for UnimplementedCoord where T: CoordNum {} + +impl GeoTraitExtWithTypeTag for UnimplementedCoord { + type Tag = CoordTag; +} diff --git a/rust/sedona-geo-traits-ext/src/geometry.rs b/rust/sedona-geo-traits-ext/src/geometry.rs new file mode 100644 index 00000000..d665cbb2 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/geometry.rs @@ -0,0 +1,365 @@ +// 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. +// Extend GeometryTrait traits for the `geo-traits` crate + +use core::{borrow::Borrow, panic}; + +use geo_traits::*; +use geo_types::*; + +use crate::*; + +#[allow(clippy::type_complexity)] +/// Extension trait that augments [`geo_traits::GeometryTrait`] with Sedona's +/// additional helpers and type tagging support. +/// +/// The trait adds accessors that mirror the behavior of `geo-types::Geometry` +/// while keeping the code ergonomic when working through trait objects. +/// Implementations must also opt into [`GeoTraitExtWithTypeTag`] so geometries +/// can be introspected using [`GeometryTag`]. +pub trait GeometryTraitExt: GeometryTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + /// Extension-aware point type exposed by this geometry. + type PointTypeExt<'a>: 'a + PointTraitExt::T> + where + Self: 'a; + + /// Extension-aware line string type exposed by this geometry. + type LineStringTypeExt<'a>: 'a + LineStringTraitExt::T> + where + Self: 'a; + + /// Extension-aware polygon type exposed by this geometry. + type PolygonTypeExt<'a>: 'a + PolygonTraitExt::T> + where + Self: 'a; + + /// Extension-aware multi point type exposed by this geometry. + type MultiPointTypeExt<'a>: 'a + MultiPointTraitExt::T> + where + Self: 'a; + + /// Extension-aware multi line string type exposed by this geometry. + type MultiLineStringTypeExt<'a>: 'a + MultiLineStringTraitExt::T> + where + Self: 'a; + + /// Extension-aware multi polygon type exposed by this geometry. + type MultiPolygonTypeExt<'a>: 'a + MultiPolygonTraitExt::T> + where + Self: 'a; + + /// Extension-aware triangle type exposed by this geometry. + type TriangleTypeExt<'a>: 'a + TriangleTraitExt::T> + where + Self: 'a; + + /// Extension-aware rectangle type exposed by this geometry. + type RectTypeExt<'a>: 'a + RectTraitExt::T> + where + Self: 'a; + + /// Extension-aware line type exposed by this geometry. + type LineTypeExt<'a>: 'a + LineTraitExt::T> + where + Self: 'a; + + // Note that we don't have a GeometryCollectionTypeExt here, because it would introduce recursive GATs + // such as G::GeometryCollectionTypeExt::GeometryTypeExt::GeometryCollectionTypeExt::... and easily + // trigger a Rust compiler bug: https://github.com/rust-lang/rust/issues/128887 and https://github.com/rust-lang/rust/issues/131960. + // See also https://github.com/geoarrow/geoarrow-rs/issues/1339. + // + // Although this could be worked around by not implementing generic functions using trait-based approach and use + // function-based approach instead, see https://github.com/geoarrow/geoarrow-rs/pull/956 and https://github.com/georust/wkb/pull/77, + // we are not certain if there will be other issues caused by recursive GATs in the future. So we decided to completely get rid + // of recursive GATs. + + /// Reference type yielded when iterating over nested geometries inside a collection. + type InnerGeometryRef<'a>: 'a + Borrow + where + Self: 'a; + + /// Returns true if this geometry is a GeometryCollection + #[inline] + fn is_collection(&self) -> bool { + matches!(self.as_type(), GeometryType::GeometryCollection(_)) + } + + /// Returns the number of geometries inside this GeometryCollection + #[inline] + fn num_geometries_ext(&self) -> usize { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Not a GeometryCollection"); + }; + gc.num_geometries() + } + + /// Cast this geometry to a [`GeometryTypeExt`] enum, which allows for downcasting to a specific + /// type. This does not work when the geometry is a GeometryCollection. Please use `is_collection` + /// to check if the geometry is NOT a GeometryCollection first before calling this method. + fn as_type_ext( + &self, + ) -> GeometryTypeExt< + '_, + Self::PointTypeExt<'_>, + Self::LineStringTypeExt<'_>, + Self::PolygonTypeExt<'_>, + Self::MultiPointTypeExt<'_>, + Self::MultiLineStringTypeExt<'_>, + Self::MultiPolygonTypeExt<'_>, + Self::RectTypeExt<'_>, + Self::TriangleTypeExt<'_>, + Self::LineTypeExt<'_>, + >; + + /// Returns a geometry by index, or None if the index is out of bounds. This method only works with + /// GeometryCollection. Please use `is_collection` to check if the geometry is a GeometryCollection first before + /// calling this method. + fn geometry_ext(&self, i: usize) -> Option>; + + /// Returns a geometry by index without bounds checking. This method only works with GeometryCollection. + /// Please use `is_collection` to check if the geometry is a GeometryCollection first before calling this method. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of geometries. + /// Otherwise, this function may cause undefined behavior. + unsafe fn geometry_unchecked_ext(&self, i: usize) -> Self::InnerGeometryRef<'_>; + + /// Returns an iterator over the geometries in this GeometryCollection. This method only works with + /// GeometryCollection. Please use `is_collection` to check if the geometry is a GeometryCollection first before + /// calling this method. + fn geometries_ext(&self) -> impl Iterator>; +} + +#[derive(Debug)] +/// Borrowed view into a concrete geometry type implementing the extension traits. +pub enum GeometryTypeExt<'a, P, LS, Y, MP, ML, MY, R, TT, L> +where + P: PointTraitExt, + LS: LineStringTraitExt, + Y: PolygonTraitExt, + MP: MultiPointTraitExt, + ML: MultiLineStringTraitExt, + MY: MultiPolygonTraitExt, + R: RectTraitExt, + TT: TriangleTraitExt, + L: LineTraitExt, +

::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, + ::T: CoordNum, +{ + Point(&'a P), + LineString(&'a LS), + Polygon(&'a Y), + MultiPoint(&'a MP), + MultiLineString(&'a ML), + MultiPolygon(&'a MY), + Rect(&'a R), + Triangle(&'a TT), + Line(&'a L), +} + +#[macro_export] +/// Forwards [`GeometryTraitExt`] associated types and methods to the +/// underlying [`geo_traits::GeometryTrait`] implementation while retaining the +/// extension trait wrappers. +macro_rules! forward_geometry_trait_ext_funcs { + ($t:ty) => { + type PointTypeExt<'__g_inner> + = ::PointType<'__g_inner> + where + Self: '__g_inner; + + type LineStringTypeExt<'__g_inner> + = ::LineStringType<'__g_inner> + where + Self: '__g_inner; + + type PolygonTypeExt<'__g_inner> + = ::PolygonType<'__g_inner> + where + Self: '__g_inner; + + type MultiPointTypeExt<'__g_inner> + = ::MultiPointType<'__g_inner> + where + Self: '__g_inner; + + type MultiLineStringTypeExt<'__g_inner> + = ::MultiLineStringType<'__g_inner> + where + Self: '__g_inner; + + type MultiPolygonTypeExt<'__g_inner> + = ::MultiPolygonType<'__g_inner> + where + Self: '__g_inner; + + type RectTypeExt<'__g_inner> + = ::RectType<'__g_inner> + where + Self: '__g_inner; + + type TriangleTypeExt<'__g_inner> + = ::TriangleType<'__g_inner> + where + Self: '__g_inner; + + type LineTypeExt<'__g_inner> + = ::LineType<'__g_inner> + where + Self: '__g_inner; + + fn as_type_ext( + &self, + ) -> GeometryTypeExt< + '_, + Self::PointTypeExt<'_>, + Self::LineStringTypeExt<'_>, + Self::PolygonTypeExt<'_>, + Self::MultiPointTypeExt<'_>, + Self::MultiLineStringTypeExt<'_>, + Self::MultiPolygonTypeExt<'_>, + Self::RectTypeExt<'_>, + Self::TriangleTypeExt<'_>, + Self::LineTypeExt<'_>, + > { + match self.as_type() { + GeometryType::Point(p) => GeometryTypeExt::Point(p), + GeometryType::LineString(ls) => GeometryTypeExt::LineString(ls), + GeometryType::Polygon(p) => GeometryTypeExt::Polygon(p), + GeometryType::MultiPoint(mp) => GeometryTypeExt::MultiPoint(mp), + GeometryType::MultiLineString(mls) => GeometryTypeExt::MultiLineString(mls), + GeometryType::MultiPolygon(mp) => GeometryTypeExt::MultiPolygon(mp), + GeometryType::GeometryCollection(_) => { + panic!("GeometryCollection is not supported in GeometryTraitExt::as_type_ext") + } + GeometryType::Rect(r) => GeometryTypeExt::Rect(r), + GeometryType::Triangle(t) => GeometryTypeExt::Triangle(t), + GeometryType::Line(l) => GeometryTypeExt::Line(l), + } + } + }; +} + +impl GeometryTraitExt for Geometry +where + T: CoordNum, +{ + forward_geometry_trait_ext_funcs!(T); + + type InnerGeometryRef<'a> + = &'a Geometry + where + Self: 'a; + + fn geometry_ext(&self, i: usize) -> Option<&Geometry> { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Not a GeometryCollection"); + }; + gc.geometry(i) + } + + unsafe fn geometry_unchecked_ext(&self, i: usize) -> &Geometry { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Not a GeometryCollection"); + }; + gc.geometry_unchecked(i) + } + + fn geometries_ext(&self) -> impl Iterator> { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Not a GeometryCollection"); + }; + gc.geometries() + } +} + +impl GeoTraitExtWithTypeTag for Geometry { + type Tag = GeometryTag; +} + +impl<'a, T> GeometryTraitExt for &'a Geometry +where + T: CoordNum, +{ + forward_geometry_trait_ext_funcs!(T); + + type InnerGeometryRef<'b> + = &'a Geometry + where + Self: 'b; + + fn geometry_ext(&self, i: usize) -> Option<&'a Geometry> { + let g = *self; + g.geometry_ext(i) + } + + unsafe fn geometry_unchecked_ext(&self, i: usize) -> &'a Geometry { + let g = *self; + g.geometry_unchecked_ext(i) + } + + fn geometries_ext(&self) -> impl Iterator> { + let g = *self; + g.geometries_ext() + } +} + +impl GeoTraitExtWithTypeTag for &Geometry { + type Tag = GeometryTag; +} + +impl GeometryTraitExt for UnimplementedGeometry +where + T: CoordNum, +{ + forward_geometry_trait_ext_funcs!(T); + + type InnerGeometryRef<'a> + = &'a UnimplementedGeometry + where + Self: 'a; + + fn geometry_ext(&self, _i: usize) -> Option> { + unimplemented!() + } + + unsafe fn geometry_unchecked_ext(&self, _i: usize) -> Self::InnerGeometryRef<'_> { + unimplemented!() + } + + fn geometries_ext(&self) -> impl Iterator> { + unimplemented!(); + + // For making the type checker happy + #[allow(unreachable_code)] + core::iter::empty() + } +} + +impl GeoTraitExtWithTypeTag for UnimplementedGeometry { + type Tag = GeometryTag; +} diff --git a/rust/sedona-geo-traits-ext/src/geometry_collection.rs b/rust/sedona-geo-traits-ext/src/geometry_collection.rs new file mode 100644 index 00000000..50f6c6f3 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/geometry_collection.rs @@ -0,0 +1,113 @@ +// 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. +// Extend GeometryCollectionTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryCollectionTrait, GeometryTrait, UnimplementedGeometryCollection}; +use geo_types::{CoordNum, GeometryCollection}; + +use crate::{GeoTraitExtWithTypeTag, GeometryCollectionTag, GeometryTraitExt}; + +/// Extension trait that enriches [`geo_traits::GeometryCollectionTrait`] with +/// Sedona-specific conveniences. +/// +/// The trait exposes accessor methods that return geometry values wrapped in +/// [`GeometryTraitExt`], enabling downstream consumers to leverage the unified +/// extension API regardless of the backing geometry type. +pub trait GeometryCollectionTraitExt: + GeometryCollectionTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + /// Extension-aware geometry type yielded by accessor methods. + type GeometryTypeExt<'a>: 'a + GeometryTraitExt::T> + where + Self: 'a; + + /// Returns the geometry at index `i`, wrapped with [`GeometryTraitExt`]. + fn geometry_ext(&self, i: usize) -> Option>; + + /// Returns a geometry by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of geometries. + /// Otherwise, this function may cause undefined behavior. + unsafe fn geometry_unchecked_ext(&self, i: usize) -> Self::GeometryTypeExt<'_>; + + /// Iterates over all geometries in the collection with extension wrappers applied. + fn geometries_ext(&self) -> impl Iterator>; +} + +#[macro_export] +/// Forwards [`GeometryCollectionTraitExt`] methods to the underlying +/// [`geo_traits::GeometryCollectionTrait`] implementation while preserving the +/// extension trait wrappers. +macro_rules! forward_geometry_collection_trait_ext_funcs { + () => { + type GeometryTypeExt<'__gc_inner> + = ::GeometryType<'__gc_inner> + where + Self: '__gc_inner; + + #[inline] + fn geometry_ext(&self, i: usize) -> Option> { + ::geometry(self, i) + } + + #[inline] + unsafe fn geometry_unchecked_ext(&self, i: usize) -> Self::GeometryTypeExt<'_> { + unsafe { ::geometry_unchecked(self, i) } + } + + #[inline] + fn geometries_ext(&self) -> impl Iterator> { + ::geometries(self) + } + }; +} + +impl GeometryCollectionTraitExt for GeometryCollection +where + T: CoordNum, +{ + forward_geometry_collection_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for GeometryCollection { + type Tag = GeometryCollectionTag; +} + +impl GeometryCollectionTraitExt for &GeometryCollection +where + T: CoordNum, +{ + forward_geometry_collection_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &GeometryCollection { + type Tag = GeometryCollectionTag; +} + +impl GeometryCollectionTraitExt for UnimplementedGeometryCollection +where + T: CoordNum, +{ + forward_geometry_collection_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedGeometryCollection { + type Tag = GeometryCollectionTag; +} diff --git a/rust/sedona-geo-traits-ext/src/lib.rs b/rust/sedona-geo-traits-ext/src/lib.rs new file mode 100644 index 00000000..12729e79 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/lib.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. +//! Extended traits for the `geo-traits` crate +//! +//! This crate extends the `geo-traits` crate with additional traits and +//! implementations. The goal is to provide a set of traits that are useful for +//! implementing algorithms on top of the `geo` crate. Most of the methods are +//! inspired by the `geo-types` crate, but are implemented as traits on the +//! `geo-traits` types. Some methods returns concrete types defined in `geo-types`, +//! these methods are only for computing tiny, intermediate results during +//! algorithm execution. +//! +//! The crate is designed to support migration of the `geo` crate to use the +//! traits defined in `geo-traits` by providing generic implementations of the +//! geospatial algorithms, rather than implementing algorithms on concrete types +//! defined in `geo-types`. +//! +//! The crate is currently under active development and the API is subject to +//! change. + +pub use coord::CoordTraitExt; +pub use geometry::{GeometryTraitExt, GeometryTypeExt}; +pub use geometry_collection::GeometryCollectionTraitExt; +pub use line::LineTraitExt; +pub use line_string::LineStringTraitExt; +pub use multi_line_string::MultiLineStringTraitExt; +pub use multi_point::MultiPointTraitExt; +pub use multi_polygon::MultiPolygonTraitExt; +pub use point::PointTraitExt; +pub use polygon::PolygonTraitExt; +pub use rect::RectTraitExt; +pub use triangle::TriangleTraitExt; + +mod coord; +mod geometry; +mod geometry_collection; +mod line; +mod line_string; +mod multi_line_string; +mod multi_point; +mod multi_polygon; +mod point; +mod polygon; +mod rect; +mod triangle; + +pub use type_tag::*; +mod type_tag; + +pub mod wkb_ext; diff --git a/rust/sedona-geo-traits-ext/src/line.rs b/rust/sedona-geo-traits-ext/src/line.rs new file mode 100644 index 00000000..94665a19 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/line.rs @@ -0,0 +1,140 @@ +// 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. +// Extend LineTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, LineTrait, UnimplementedLine}; +use geo_types::{CoordNum, Line}; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, LineTag}; + +/// Extra helpers for [`LineTrait`] implementers that mirror `geo-types` APIs. +pub trait LineTraitExt: LineTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type CoordTypeExt<'a>: 'a + CoordTraitExt::T> + where + Self: 'a; + + /// Returns the start coordinate as an extension trait instance. + fn start_ext(&self) -> Self::CoordTypeExt<'_>; + /// Returns the end coordinate as an extension trait instance. + fn end_ext(&self) -> Self::CoordTypeExt<'_>; + /// Returns both start and end coordinates in a fixed-size array. + fn coords_ext(&self) -> [Self::CoordTypeExt<'_>; 2]; + + #[inline] + /// Returns the start coordinate converted to [`geo_types::Coord`]. + fn start_coord(&self) -> geo_types::Coord<::T> { + self.start_ext().geo_coord() + } + + #[inline] + /// Returns the end coordinate converted to [`geo_types::Coord`]. + fn end_coord(&self) -> geo_types::Coord<::T> { + self.end_ext().geo_coord() + } + + #[inline] + /// Returns the line converted to a [`geo_types::Line`]. + fn geo_line(&self) -> Line<::T> { + Line::new(self.start_coord(), self.end_coord()) + } +} + +#[macro_export] +/// Forwards [`LineTraitExt`] methods to an underlying [`LineTrait`] implementation. +macro_rules! forward_line_trait_ext_funcs { + () => { + type CoordTypeExt<'__l_inner> + = ::CoordType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn start_ext(&self) -> Self::CoordTypeExt<'_> { + ::start(self) + } + + #[inline] + fn end_ext(&self) -> Self::CoordTypeExt<'_> { + ::end(self) + } + + #[inline] + fn coords_ext(&self) -> [Self::CoordTypeExt<'_>; 2] { + [self.start_ext(), self.end_ext()] + } + }; +} + +impl LineTraitExt for Line +where + T: CoordNum, +{ + forward_line_trait_ext_funcs!(); + + fn start_coord(&self) -> geo_types::Coord { + self.start + } + + fn end_coord(&self) -> geo_types::Coord { + self.end + } + + fn geo_line(&self) -> geo_types::Line { + *self + } +} + +impl GeoTraitExtWithTypeTag for Line { + type Tag = LineTag; +} + +impl LineTraitExt for &Line +where + T: CoordNum, +{ + forward_line_trait_ext_funcs!(); + + fn start_coord(&self) -> geo_types::Coord { + self.start + } + + fn end_coord(&self) -> geo_types::Coord { + self.end + } + + fn geo_line(&self) -> geo_types::Line { + **self + } +} + +impl GeoTraitExtWithTypeTag for &Line { + type Tag = LineTag; +} + +impl LineTraitExt for UnimplementedLine +where + T: CoordNum, +{ + forward_line_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedLine { + type Tag = LineTag; +} diff --git a/rust/sedona-geo-traits-ext/src/line_string.rs b/rust/sedona-geo-traits-ext/src/line_string.rs new file mode 100644 index 00000000..86c07c97 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/line_string.rs @@ -0,0 +1,234 @@ +// 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. +// Extend LineStringTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, LineStringTrait, UnimplementedLineString}; +use geo_types::{Coord, CoordNum, Line, LineString, Triangle}; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, LineStringTag}; + +/// Additional convenience methods for [`LineStringTrait`] implementers that mirror `geo-types`. +pub trait LineStringTraitExt: + LineStringTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type CoordTypeExt<'a>: 'a + CoordTraitExt::T> + where + Self: 'a; + + /// Returns the coordinate at the provided index. + fn coord_ext(&self, i: usize) -> Option>; + + /// Returns a coordinate by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of coordinates. + /// Otherwise, this function may cause undefined behavior. + unsafe fn coord_unchecked_ext(&self, i: usize) -> Self::CoordTypeExt<'_>; + + /// Returns an iterator over all coordinates as extension trait instances. + fn coords_ext(&self) -> impl Iterator>; + + /// Returns a coordinate by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of coordinates. + /// Otherwise, this function may cause undefined behavior. + #[inline] + unsafe fn geo_coord_unchecked(&self, i: usize) -> Coord { + self.coord_unchecked_ext(i).geo_coord() + } + + /// Return an iterator yielding one [`Line`] for each line segment + /// in the [`LineString`][`geo_types::LineString`]. + #[inline] + fn lines(&'_ self) -> impl ExactSizeIterator::T>> + '_ { + let num_coords = self.num_coords(); + (0..num_coords.saturating_sub(1)).map(|i| unsafe { + let coord1 = self.geo_coord_unchecked(i); + let coord2 = self.geo_coord_unchecked(i + 1); + Line::new(coord1, coord2) + }) + } + + /// Return an iterator yielding one [`Line`] for each line segment in the [`LineString`][`geo_types::LineString`], + /// starting from the **end** point of the LineString, working towards the start. + /// + /// Note: This is like [`Self::lines`], but the sequence **and** the orientation of + /// segments are reversed. + #[inline] + fn rev_lines(&'_ self) -> impl ExactSizeIterator::T>> + '_ { + let num_coords = self.num_coords(); + (1..num_coords).rev().map(|i| unsafe { + let coord1 = self.geo_coord_unchecked(i); + let coord2 = self.geo_coord_unchecked(i - 1); + Line::new(coord2, coord1) + }) + } + + /// An iterator which yields the coordinates of a [`LineString`][`geo_types::LineString`] as [Triangle]s + #[inline] + fn triangles( + &'_ self, + ) -> impl ExactSizeIterator::T>> + '_ { + let num_coords = self.num_coords(); + let end = num_coords.saturating_sub(2); + (0..end).map(|i| unsafe { + let coord1 = self.geo_coord_unchecked(i); + let coord2 = self.geo_coord_unchecked(i + 1); + let coord3 = self.geo_coord_unchecked(i + 2); + Triangle::new(coord1, coord2, coord3) + }) + } + + /// Returns an iterator yielding the coordinates of this line string as [`geo_types::Coord`] values. + #[inline] + fn coord_iter(&self) -> impl Iterator::T>> { + self.coords_ext().map(|c| c.geo_coord()) + } + + #[inline] + /// Returns true when the line string is closed (its first and last coordinates are equal). + fn is_closed(&self) -> bool { + let num_coords = self.num_coords(); + if num_coords <= 1 { + true + } else { + let (first, last) = unsafe { + ( + self.geo_coord_unchecked(0), + self.geo_coord_unchecked(num_coords - 1), + ) + }; + first == last + } + } +} + +#[macro_export] +/// Forwards [`LineStringTraitExt`] methods to an underlying [`LineStringTrait`] implementation. +macro_rules! forward_line_string_trait_ext_funcs { + () => { + type CoordTypeExt<'__l_inner> + = ::CoordType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn coord_ext(&self, i: usize) -> Option> { + ::coord(self, i) + } + + #[inline] + unsafe fn coord_unchecked_ext(&self, i: usize) -> Self::CoordTypeExt<'_> { + ::coord_unchecked(self, i) + } + + #[inline] + fn coords_ext(&self) -> impl Iterator> { + ::coords(self) + } + }; +} + +impl LineStringTraitExt for LineString +where + T: CoordNum, +{ + forward_line_string_trait_ext_funcs!(); + + unsafe fn geo_coord_unchecked(&self, i: usize) -> Coord { + *self.0.get_unchecked(i) + } + + // Delegate to the `geo-types` implementation for less performance overhead + fn lines(&'_ self) -> impl ExactSizeIterator::T>> + '_ { + self.lines() + } + + fn rev_lines(&'_ self) -> impl ExactSizeIterator::T>> + '_ { + self.rev_lines() + } + + fn triangles( + &'_ self, + ) -> impl ExactSizeIterator::T>> + '_ { + self.triangles() + } + + fn is_closed(&self) -> bool { + self.is_closed() + } + + fn coord_iter(&self) -> impl Iterator::T>> { + self.0.iter().copied() + } +} + +impl GeoTraitExtWithTypeTag for LineString { + type Tag = LineStringTag; +} + +impl LineStringTraitExt for &LineString +where + T: CoordNum, +{ + forward_line_string_trait_ext_funcs!(); + + unsafe fn geo_coord_unchecked(&self, i: usize) -> Coord { + *self.0.get_unchecked(i) + } + + // Delegate to the `geo-types` implementation for less performance overhead + fn lines(&'_ self) -> impl ExactSizeIterator::T>> + '_ { + (*self).lines() + } + + fn rev_lines(&'_ self) -> impl ExactSizeIterator::T>> + '_ { + (*self).rev_lines() + } + + fn triangles( + &'_ self, + ) -> impl ExactSizeIterator::T>> + '_ { + (*self).triangles() + } + + fn is_closed(&self) -> bool { + (*self).is_closed() + } + + fn coord_iter(&self) -> impl Iterator::T>> { + self.0.iter().copied() + } +} + +impl GeoTraitExtWithTypeTag for &LineString { + type Tag = LineStringTag; +} + +impl LineStringTraitExt for UnimplementedLineString +where + T: CoordNum, +{ + forward_line_string_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedLineString { + type Tag = LineStringTag; +} diff --git a/rust/sedona-geo-traits-ext/src/multi_line_string.rs b/rust/sedona-geo-traits-ext/src/multi_line_string.rs new file mode 100644 index 00000000..af4f3713 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/multi_line_string.rs @@ -0,0 +1,123 @@ +// 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. +// Extend MultiLineStringTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, MultiLineStringTrait, UnimplementedMultiLineString}; +use geo_types::{CoordNum, MultiLineString}; + +use crate::{GeoTraitExtWithTypeTag, LineStringTraitExt, MultiLineStringTag}; + +/// Extension trait that layers additional ergonomics on +/// [`geo_traits::MultiLineStringTrait`]. +/// +/// implementers gain access to extension-aware iterators and helper methods +/// that mirror the behavior of `geo-types::MultiLineString`, while still being +/// consumable through the trait abstractions provided by `geo-traits`. +pub trait MultiLineStringTraitExt: + MultiLineStringTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + /// Extension-friendly line string type returned by accessor methods. + type LineStringTypeExt<'a>: 'a + LineStringTraitExt::T> + where + Self: 'a; + + /// Returns the line string at index `i` with the extension trait applied. + /// + /// This is analogous to [`geo_traits::MultiLineStringTrait::line_string`] + /// but ensures the result implements [`LineStringTraitExt`]. + fn line_string_ext(&self, i: usize) -> Option>; + + /// Returns a line string by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of line strings. + /// Otherwise, this function may cause undefined behavior. + unsafe fn line_string_unchecked_ext(&self, i: usize) -> Self::LineStringTypeExt<'_>; + + /// Iterates over all line strings with extension-aware wrappers applied. + fn line_strings_ext(&self) -> impl Iterator>; + + /// Returns `true` when the multi line string is empty or every component is closed. + #[inline] + fn is_closed(&self) -> bool { + // Note: Unlike JTS et al, we consider an empty MultiLineString as closed. + self.line_strings_ext().all(|ls| ls.is_closed()) + } +} + +#[macro_export] +/// Forwards [`MultiLineStringTraitExt`] methods to the underlying +/// [`geo_traits::MultiLineStringTrait`] implementation while keeping the +/// extension trait wrappers intact. +macro_rules! forward_multi_line_string_trait_ext_funcs { + () => { + type LineStringTypeExt<'__l_inner> + = ::InnerLineStringType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn line_string_ext(&self, i: usize) -> Option> { + ::line_string(self, i) + } + + #[inline] + unsafe fn line_string_unchecked_ext(&self, i: usize) -> Self::LineStringTypeExt<'_> { + ::line_string_unchecked(self, i) + } + + #[inline] + fn line_strings_ext(&self) -> impl Iterator> { + ::line_strings(self) + } + }; +} + +impl MultiLineStringTraitExt for MultiLineString +where + T: CoordNum, +{ + forward_multi_line_string_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for MultiLineString { + type Tag = MultiLineStringTag; +} + +impl MultiLineStringTraitExt for &MultiLineString +where + T: CoordNum, +{ + forward_multi_line_string_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &MultiLineString { + type Tag = MultiLineStringTag; +} + +impl MultiLineStringTraitExt for UnimplementedMultiLineString +where + T: CoordNum, +{ + forward_multi_line_string_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedMultiLineString { + type Tag = MultiLineStringTag; +} diff --git a/rust/sedona-geo-traits-ext/src/multi_point.rs b/rust/sedona-geo-traits-ext/src/multi_point.rs new file mode 100644 index 00000000..bdd40c88 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/multi_point.rs @@ -0,0 +1,168 @@ +// 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. +// Extend MultiPointTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, MultiPointTrait, UnimplementedMultiPoint}; +use geo_types::{Coord, CoordNum, MultiPoint}; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, MultiPointTag, PointTraitExt}; + +/// Extension trait that augments [`geo_traits::MultiPointTrait`] with richer +/// ergonomics and accessors. +/// +/// The trait keeps parity with the APIs provided by `geo-types::MultiPoint` +/// while still working with trait objects that only implement +/// [`geo_traits::MultiPointTrait`]. It also wires the geometry up with a +/// [`MultiPointTag`](crate::MultiPointTag) so the type can participate in the +/// shared `GeoTraitExtWithTypeTag` machinery. +pub trait MultiPointTraitExt: + MultiPointTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + /// Extension-aware point type returned from accessors on this multi point. + type PointTypeExt<'a>: 'a + PointTraitExt::T> + where + Self: 'a; + + /// Returns the point at index `i`, wrapped in the extension trait. + /// + /// This mirrors [`geo_traits::MultiPointTrait::point`] but guarantees the + /// returned point implements [`PointTraitExt`]. + fn point_ext(&self, i: usize) -> Option>; + + /// Returns a point by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of points. + /// Otherwise, this function may cause undefined behavior. + unsafe fn point_unchecked_ext(&self, i: usize) -> Self::PointTypeExt<'_>; + + /// Returns a coordinate by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of points. + /// Otherwise, this function may cause undefined behavior. + /// Returns the coordinate at index `i` without bounds checking. + /// + /// This helper is primarily used by iterator adapters that need direct + /// coordinate access while still honoring the [`PointTraitExt`] abstraction. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of points. + /// Otherwise, this function may cause undefined behavior. + #[inline] + unsafe fn geo_coord_unchecked(&self, i: usize) -> Option::T>> { + let point = unsafe { self.point_unchecked_ext(i) }; + point.coord_ext().map(|c| c.geo_coord()) + } + + /// Returns an iterator over all points, each wrapped in [`PointTraitExt`]. + fn points_ext(&self) -> impl DoubleEndedIterator>; + + /// Iterates over the coordinates contained in this multi point. + /// + /// For trait-based implementations this is derived from + /// [`points_ext`](Self::points_ext), while concrete `geo-types::MultiPoint` + /// instances provide a specialized iterator that avoids intermediate + /// allocations. + #[inline] + fn coord_iter(&self) -> impl DoubleEndedIterator::T>> { + self.points_ext().flat_map(|p| p.geo_coord()) + } +} + +#[macro_export] +/// Forwards [`MultiPointTraitExt`] methods to the underlying +/// [`geo_traits::MultiPointTrait`] implementation while maintaining the +/// extension trait wrappers. +macro_rules! forward_multi_point_trait_ext_funcs { + () => { + type PointTypeExt<'__l_inner> + = ::InnerPointType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn point_ext(&self, i: usize) -> Option> { + ::point(self, i) + } + + #[inline] + unsafe fn point_unchecked_ext(&self, i: usize) -> Self::PointTypeExt<'_> { + ::point_unchecked(self, i) + } + + #[inline] + fn points_ext(&self) -> impl DoubleEndedIterator> { + ::points(self) + } + }; +} + +impl MultiPointTraitExt for MultiPoint +where + T: CoordNum, +{ + forward_multi_point_trait_ext_funcs!(); + + /// Specialized coordinate accessor for `geo_types::MultiPoint`. + unsafe fn geo_coord_unchecked(&self, i: usize) -> Option> { + Some(self.0.get_unchecked(i).0) + } + + // Specialized implementation for geo_types::MultiPoint to reduce performance overhead + fn coord_iter(&self) -> impl DoubleEndedIterator::T>> { + self.0.iter().map(|p| p.0) + } +} + +impl GeoTraitExtWithTypeTag for MultiPoint { + type Tag = MultiPointTag; +} + +impl MultiPointTraitExt for &MultiPoint +where + T: CoordNum, +{ + forward_multi_point_trait_ext_funcs!(); + + /// Specialized coordinate accessor for `&geo_types::MultiPoint`. + unsafe fn geo_coord_unchecked(&self, i: usize) -> Option> { + Some(self.0.get_unchecked(i).0) + } + + // Specialized implementation for geo_types::MultiPoint to reduce performance overhead + fn coord_iter(&self) -> impl DoubleEndedIterator::T>> { + self.0.iter().map(|p| p.0) + } +} + +impl GeoTraitExtWithTypeTag for &MultiPoint { + type Tag = MultiPointTag; +} + +impl MultiPointTraitExt for UnimplementedMultiPoint +where + T: CoordNum, +{ + forward_multi_point_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedMultiPoint { + type Tag = MultiPointTag; +} diff --git a/rust/sedona-geo-traits-ext/src/multi_polygon.rs b/rust/sedona-geo-traits-ext/src/multi_polygon.rs new file mode 100644 index 00000000..9b90afad --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/multi_polygon.rs @@ -0,0 +1,113 @@ +// 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. +// Extend MultiPolygonTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, MultiPolygonTrait, UnimplementedMultiPolygon}; +use geo_types::{CoordNum, MultiPolygon}; + +use crate::{GeoTraitExtWithTypeTag, MultiPolygonTag, PolygonTraitExt}; + +/// Extension trait that enriches [`geo_traits::MultiPolygonTrait`] with Sedona +/// conveniences. +/// +/// Implementations can expose polygon members through the +/// [`PolygonTraitExt`] abstraction, ensuring consistent access to exterior and +/// interior rings regardless of the backing geometry type. +pub trait MultiPolygonTraitExt: + MultiPolygonTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + /// Extension-aware polygon type yielded by accessor methods. + type PolygonTypeExt<'a>: 'a + PolygonTraitExt::T> + where + Self: 'a; + + /// Returns the polygon at index `i`, wrapped with [`PolygonTraitExt`]. + fn polygon_ext(&self, i: usize) -> Option>; + + /// Returns a polygon by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of polygons. + /// Otherwise, this function may cause undefined behavior. + unsafe fn polygon_unchecked_ext(&self, i: usize) -> Self::PolygonTypeExt<'_>; + + /// Iterates over all polygon members, each wrapped with the extension trait. + fn polygons_ext(&self) -> impl Iterator>; +} + +#[macro_export] +/// Forwards [`MultiPolygonTraitExt`] methods to the underlying +/// [`geo_traits::MultiPolygonTrait`] implementation while preserving the +/// extension trait wrappers. +macro_rules! forward_multi_polygon_trait_ext_funcs { + () => { + type PolygonTypeExt<'__l_inner> + = ::InnerPolygonType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn polygon_ext(&self, i: usize) -> Option> { + ::polygon(self, i) + } + + #[inline] + unsafe fn polygon_unchecked_ext(&self, i: usize) -> Self::PolygonTypeExt<'_> { + ::polygon_unchecked(self, i) + } + + #[inline] + fn polygons_ext(&self) -> impl Iterator> { + ::polygons(self) + } + }; +} + +impl MultiPolygonTraitExt for MultiPolygon +where + T: CoordNum, +{ + forward_multi_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for MultiPolygon { + type Tag = MultiPolygonTag; +} + +impl MultiPolygonTraitExt for &MultiPolygon +where + T: CoordNum, +{ + forward_multi_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &MultiPolygon { + type Tag = MultiPolygonTag; +} + +impl MultiPolygonTraitExt for UnimplementedMultiPolygon +where + T: CoordNum, +{ + forward_multi_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedMultiPolygon { + type Tag = MultiPolygonTag; +} diff --git a/rust/sedona-geo-traits-ext/src/point.rs b/rust/sedona-geo-traits-ext/src/point.rs new file mode 100644 index 00000000..35489618 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/point.rs @@ -0,0 +1,113 @@ +// 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. +// Extend PointTrait traits for the `geo-traits` crate + +use geo_traits::{CoordTrait, GeometryTrait, PointTrait, UnimplementedPoint}; +use geo_types::{Coord, CoordNum, Point}; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, PointTag}; + +/// Extension methods that expose `geo-types` conveniences for [`PointTrait`] implementers. +pub trait PointTraitExt: PointTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + type CoordTypeExt<'a>: 'a + CoordTraitExt::T> + where + Self: 'a; + + /// Returns the underlying coordinate view for this point, if available. + fn coord_ext(&self) -> Option>; + + #[inline] + /// Converts the point into a concrete [`geo_types::Point`]. + fn geo_point(&self) -> Option::T>> { + self.coord_ext() + .map(|coord| Point::new(coord.x(), coord.y())) + } + + #[inline] + /// Converts the point into a concrete [`geo_types::Coord`]. + fn geo_coord(&self) -> Option::T>> { + self.coord_ext().map(|coord| coord.geo_coord()) + } +} + +#[macro_export] +/// Forwards [`PointTraitExt`] methods to the wrapped [`PointTrait`] implementation. +macro_rules! forward_point_trait_ext_funcs { + () => { + type CoordTypeExt<'__l_inner> + = ::CoordType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn coord_ext(&self) -> Option> { + ::coord(self) + } + }; +} + +impl PointTraitExt for Point +where + T: CoordNum, +{ + forward_point_trait_ext_funcs!(); + + fn geo_point(&self) -> Option> { + Some(*self) + } + + fn geo_coord(&self) -> Option> { + Some(self.0) + } +} + +impl GeoTraitExtWithTypeTag for Point { + type Tag = PointTag; +} + +impl PointTraitExt for &Point +where + T: CoordNum, +{ + forward_point_trait_ext_funcs!(); + + fn geo_point(&self) -> Option> { + Some(**self) + } + + fn geo_coord(&self) -> Option> { + Some(self.0) + } +} + +impl GeoTraitExtWithTypeTag for &Point { + type Tag = PointTag; +} + +impl PointTraitExt for UnimplementedPoint +where + T: CoordNum, +{ + forward_point_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedPoint { + type Tag = PointTag; +} diff --git a/rust/sedona-geo-traits-ext/src/polygon.rs b/rust/sedona-geo-traits-ext/src/polygon.rs new file mode 100644 index 00000000..52a00a56 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/polygon.rs @@ -0,0 +1,125 @@ +// 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. +// Extend PolygonTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, PolygonTrait, UnimplementedPolygon}; +use geo_types::{CoordNum, Polygon}; + +use crate::{GeoTraitExtWithTypeTag, LineStringTraitExt, PolygonTag}; + +/// Extension trait that augments [`geo_traits::PolygonTrait`] with +/// extension-aware accessors over exterior and interior rings. +/// +/// Implementations are able to return ring references that also implement +/// [`LineStringTraitExt`], bringing parity with `geo-types::Polygon` while +/// remaining ergonomic through trait objects. +pub trait PolygonTraitExt: PolygonTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + /// Type of ring returned from accessor methods, wrapped with + /// [`LineStringTraitExt`]. + type RingTypeExt<'a>: 'a + LineStringTraitExt::T> + where + Self: 'a; + + /// Returns the exterior ring with the extension trait applied. + fn exterior_ext(&self) -> Option>; + + /// Iterates over each interior ring with the extension trait applied. + fn interiors_ext( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator>; + + /// Returns the `i`th interior ring, if present, wrapped with the extension trait. + fn interior_ext(&self, i: usize) -> Option>; + + /// Returns an interior ring by index without bounds checking. + /// + /// # Safety + /// The caller must ensure that `i` is a valid index less than the number of interior rings. + /// Otherwise, this function may cause undefined behavior. + unsafe fn interior_unchecked_ext(&self, i: usize) -> Self::RingTypeExt<'_>; +} + +#[macro_export] +/// Forwards [`PolygonTraitExt`] methods to the underlying +/// [`geo_traits::PolygonTrait`] implementation while preserving the extension +/// trait wrappers. +macro_rules! forward_polygon_trait_ext_funcs { + () => { + type RingTypeExt<'__l_inner> + = ::RingType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn exterior_ext(&self) -> Option> { + ::exterior(self) + } + + #[inline] + fn interiors_ext( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator> { + ::interiors(self) + } + + #[inline] + fn interior_ext(&self, i: usize) -> Option> { + ::interior(self, i) + } + + #[inline] + unsafe fn interior_unchecked_ext(&self, i: usize) -> Self::RingTypeExt<'_> { + ::interior_unchecked(self, i) + } + }; +} + +impl PolygonTraitExt for Polygon +where + T: CoordNum, +{ + forward_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for Polygon { + type Tag = PolygonTag; +} + +impl PolygonTraitExt for &Polygon +where + T: CoordNum, +{ + forward_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &Polygon { + type Tag = PolygonTag; +} + +impl PolygonTraitExt for UnimplementedPolygon +where + T: CoordNum, +{ + forward_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedPolygon { + type Tag = PolygonTag; +} diff --git a/rust/sedona-geo-traits-ext/src/rect.rs b/rust/sedona-geo-traits-ext/src/rect.rs new file mode 100644 index 00000000..335179d2 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/rect.rs @@ -0,0 +1,331 @@ +// 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. +// Extend RectTrait traits for the `geo-traits` crate + +use geo_traits::{CoordTrait, GeometryTrait, RectTrait, UnimplementedRect}; +use geo_types::{coord, Coord, CoordFloat, CoordNum, Line, LineString, Polygon, Rect}; +use num_traits::One; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, RectTag}; + +static RECT_INVALID_BOUNDS_ERROR: &str = "Failed to create Rect: 'min' coordinate's x/y value must be smaller or equal to the 'max' x/y value"; + +/// Extension trait that augments [`geo_traits::RectTrait`] with additional +/// helpers for working with axis-aligned bounding boxes. +pub trait RectTraitExt: RectTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + /// Extension-aware coordinate type returned from accessors. + type CoordTypeExt<'a>: 'a + CoordTraitExt::T> + where + Self: 'a; + + /// Returns the minimum corner using the extension trait wrapper. + fn min_ext(&self) -> Self::CoordTypeExt<'_>; + + /// Returns the maximum corner using the extension trait wrapper. + fn max_ext(&self) -> Self::CoordTypeExt<'_>; + + #[inline] + /// Returns the minimum corner as a `geo-types::Coord`. + fn min_coord(&self) -> Coord<::T> { + self.min_ext().geo_coord() + } + + #[inline] + /// Returns the maximum corner as a `geo-types::Coord`. + fn max_coord(&self) -> Coord<::T> { + self.max_ext().geo_coord() + } + + #[inline] + /// Constructs a [`geo_types::Rect`] from the extension trait accessors. + fn geo_rect(&self) -> Rect<::T> { + Rect::new(self.min_coord(), self.max_coord()) + } + + #[inline] + /// Returns the width of the rectangle. + fn width(&self) -> ::T { + self.max().x() - self.min().x() + } + + #[inline] + /// Returns the height of the rectangle. + fn height(&self) -> ::T { + self.max().y() - self.min().y() + } + + /// Converts the rectangle into a polygon with four corners. + fn to_polygon(&self) -> Polygon<::T> + where + ::T: Clone, + { + let min_coord = self.min_coord(); + let max_coord = self.max_coord(); + + let min_x = min_coord.x; + let min_y = min_coord.y; + let max_x = max_coord.x; + let max_y = max_coord.y; + + let line_string = LineString::new(vec![ + Coord { x: min_x, y: min_y }, + Coord { x: min_x, y: max_y }, + Coord { x: max_x, y: max_y }, + Coord { x: max_x, y: min_y }, + Coord { x: min_x, y: min_y }, + ]); + + Polygon::new(line_string, vec![]) + } + + /// Returns the four outer edges as line segments. + fn to_lines(&self) -> [Line<::T>; 4] { + let min_coord = self.min_coord(); + let max_coord = self.max_coord(); + [ + Line::new( + coord! { + x: max_coord.x, + y: min_coord.y, + }, + coord! { + x: max_coord.x, + y: max_coord.y, + }, + ), + Line::new( + coord! { + x: max_coord.x, + y: max_coord.y, + }, + coord! { + x: min_coord.x, + y: max_coord.y, + }, + ), + Line::new( + coord! { + x: min_coord.x, + y: max_coord.y, + }, + coord! { + x: min_coord.x, + y: min_coord.y, + }, + ), + Line::new( + coord! { + x: min_coord.x, + y: min_coord.y, + }, + coord! { + x: max_coord.x, + y: min_coord.y, + }, + ), + ] + } + + /// Converts the rectangle into a closed line string in counter-clockwise order. + fn to_line_string(&self) -> LineString<::T> + where + ::T: Clone, + { + let min_coord = self.min_coord(); + let max_coord = self.max_coord(); + + let min_x = min_coord.x; + let min_y = min_coord.y; + let max_x = max_coord.x; + let max_y = max_coord.y; + + LineString::new(vec![ + Coord { x: min_x, y: min_y }, + Coord { x: min_x, y: max_y }, + Coord { x: max_x, y: max_y }, + Coord { x: max_x, y: min_y }, + Coord { x: min_x, y: min_y }, + ]) + } + + #[inline] + /// Returns `true` if the rectangle has non-decreasing bounds. + fn has_valid_bounds(&self) -> bool { + let min_coord = self.min_coord(); + let max_coord = self.max_coord(); + min_coord.x <= max_coord.x && min_coord.y <= max_coord.y + } + + #[inline] + /// Panics when the rectangle bounds are invalid. + fn assert_valid_bounds(&self) { + if !self.has_valid_bounds() { + panic!("{}", RECT_INVALID_BOUNDS_ERROR); + } + } + + #[inline] + /// Returns `true` if `coord` lies inside or on the rectangle boundary. + fn contains_point(&self, coord: &Coord<::T>) -> bool + where + ::T: PartialOrd, + { + let min_coord = self.min_coord(); + let max_coord = self.max_coord(); + + let min_x = min_coord.x; + let min_y = min_coord.y; + let max_x = max_coord.x; + let max_y = max_coord.y; + + (min_x <= coord.x && coord.x <= max_x) && (min_y <= coord.y && coord.y <= max_y) + } + + #[inline] + /// Returns `true` if `rect` is fully contained within `self`. + fn contains_rect(&self, rect: &Self) -> bool + where + ::T: PartialOrd, + { + let self_min = self.min_coord(); + let self_max = self.max_coord(); + let other_min = rect.min_coord(); + let other_max = rect.max_coord(); + + let self_min_x = self_min.x; + let self_min_y = self_min.y; + let self_max_x = self_max.x; + let self_max_y = self_max.y; + + let other_min_x = other_min.x; + let other_min_y = other_min.y; + let other_max_x = other_max.x; + let other_max_y = other_max.y; + + (self_min_x <= other_min_x && other_max_x <= self_max_x) + && (self_min_y <= other_min_y && other_max_y <= self_max_y) + } + + #[inline] + /// Returns the rectangle centroid as a coordinate. + fn center(&self) -> Coord<::T> + where + ::T: CoordFloat, + { + let two = ::T::one() + ::T::one(); + coord! { + x: (self.max_coord().x + self.min_coord().x) / two, + y: (self.max_coord().y + self.min_coord().y) / two, + } + } +} + +#[macro_export] +/// Forwards [`RectTraitExt`] methods to the underlying +/// [`geo_traits::RectTrait`] implementation while keeping the extension trait +/// wrappers intact. +macro_rules! forward_rect_trait_ext_funcs { + () => { + type CoordTypeExt<'__l_inner> + = ::CoordType<'__l_inner> + where + Self: '__l_inner; + + fn min_ext(&self) -> Self::CoordTypeExt<'_> { + ::min(self) + } + + fn max_ext(&self) -> Self::CoordTypeExt<'_> { + ::max(self) + } + }; +} + +impl RectTraitExt for Rect +where + T: CoordNum, +{ + forward_rect_trait_ext_funcs!(); + + fn min_coord(&self) -> Coord { + Rect::min(*self) + } + + fn max_coord(&self) -> Coord { + Rect::max(*self) + } + + fn geo_rect(&self) -> Rect { + *self + } + + fn to_lines(&self) -> [Line<::T>; 4] { + self.to_lines() + } +} + +impl GeoTraitExtWithTypeTag for Rect { + type Tag = RectTag; +} + +impl RectTraitExt for &Rect +where + T: CoordNum, +{ + forward_rect_trait_ext_funcs!(); + + fn min_coord(&self) -> Coord { + Rect::min(**self) + } + + fn max_coord(&self) -> Coord { + Rect::max(**self) + } + + fn geo_rect(&self) -> Rect { + **self + } + + fn to_polygon(&self) -> Polygon<::T> + where + ::T: Clone, + { + (*self).to_polygon() + } + + fn to_lines(&self) -> [Line<::T>; 4] { + (*self).to_lines() + } +} + +impl GeoTraitExtWithTypeTag for &Rect { + type Tag = RectTag; +} + +impl RectTraitExt for UnimplementedRect +where + T: CoordNum, +{ + forward_rect_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedRect { + type Tag = RectTag; +} diff --git a/rust/sedona-geo-traits-ext/src/triangle.rs b/rust/sedona-geo-traits-ext/src/triangle.rs new file mode 100644 index 00000000..08fb868e --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/triangle.rs @@ -0,0 +1,200 @@ +// 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. +// Extend TriangleTrait traits for the `geo-traits` crate + +use geo_traits::{GeometryTrait, TriangleTrait, UnimplementedTriangle}; +use geo_types::{polygon, Coord, CoordNum, Line, Polygon, Triangle}; + +use crate::{CoordTraitExt, GeoTraitExtWithTypeTag, TriangleTag}; + +/// Extension trait that augments [`geo_traits::TriangleTrait`] with convenient +/// coordinate accessors and adapters. +pub trait TriangleTraitExt: TriangleTrait + GeoTraitExtWithTypeTag +where + ::T: CoordNum, +{ + /// Extension-aware coordinate type returned from triangle accessors. + type CoordTypeExt<'a>: 'a + CoordTraitExt::T> + where + Self: 'a; + + /// Returns the first vertex with the extension trait applied. + fn first_ext(&self) -> Self::CoordTypeExt<'_>; + /// Returns the second vertex with the extension trait applied. + fn second_ext(&self) -> Self::CoordTypeExt<'_>; + /// Returns the third vertex with the extension trait applied. + fn third_ext(&self) -> Self::CoordTypeExt<'_>; + /// Returns all three vertices as extension-aware coordinates. + fn coords_ext(&self) -> [Self::CoordTypeExt<'_>; 3]; + + #[inline] + /// Returns the first vertex as a `geo-types::Coord`. + fn first_coord(&self) -> Coord<::T> { + self.first_ext().geo_coord() + } + + #[inline] + /// Returns the second vertex as a `geo-types::Coord`. + fn second_coord(&self) -> Coord<::T> { + self.second_ext().geo_coord() + } + + #[inline] + /// Returns the third vertex as a `geo-types::Coord`. + fn third_coord(&self) -> Coord<::T> { + self.third_ext().geo_coord() + } + + #[inline] + /// Returns the triangle vertices as an array of coordinates. + fn to_array(&self) -> [Coord<::T>; 3] { + [self.first_coord(), self.second_coord(), self.third_coord()] + } + + #[inline] + /// Returns the three edges as line segments in traversal order. + fn to_lines(&self) -> [Line<::T>; 3] { + [ + Line::new(self.first_coord(), self.second_coord()), + Line::new(self.second_coord(), self.third_coord()), + Line::new(self.third_coord(), self.first_coord()), + ] + } + + #[inline] + /// Converts the triangle into a polygon whose shell walks the triangle vertices. + fn to_polygon(&self) -> Polygon<::T> { + polygon![ + self.first_coord(), + self.second_coord(), + self.third_coord(), + self.first_coord(), + ] + } + + #[inline] + /// Iterates over the triangle vertices as coordinates. + fn coord_iter(&self) -> impl Iterator::T>> { + [self.first_coord(), self.second_coord(), self.third_coord()].into_iter() + } +} + +#[macro_export] +/// Forwards [`TriangleTraitExt`] methods to the underlying +/// [`geo_traits::TriangleTrait`] implementation while returning extension trait +/// wrappers. +macro_rules! forward_triangle_trait_ext_funcs { + () => { + type CoordTypeExt<'__l_inner> + = ::CoordType<'__l_inner> + where + Self: '__l_inner; + + #[inline] + fn first_ext(&self) -> Self::CoordTypeExt<'_> { + ::first(self) + } + + #[inline] + fn second_ext(&self) -> Self::CoordTypeExt<'_> { + ::second(self) + } + + #[inline] + fn third_ext(&self) -> Self::CoordTypeExt<'_> { + ::third(self) + } + + #[inline] + fn coords_ext(&self) -> [Self::CoordTypeExt<'_>; 3] { + [self.first_ext(), self.second_ext(), self.third_ext()] + } + }; +} + +impl TriangleTraitExt for Triangle +where + T: CoordNum, +{ + forward_triangle_trait_ext_funcs!(); + + fn first_coord(&self) -> Coord<::T> { + self.0 + } + + fn second_coord(&self) -> Coord<::T> { + self.1 + } + + fn third_coord(&self) -> Coord<::T> { + self.2 + } + + fn to_array(&self) -> [Coord<::T>; 3] { + self.to_array() + } + + fn to_lines(&self) -> [Line<::T>; 3] { + self.to_lines() + } +} + +impl GeoTraitExtWithTypeTag for Triangle { + type Tag = TriangleTag; +} + +impl TriangleTraitExt for &Triangle +where + T: CoordNum, +{ + forward_triangle_trait_ext_funcs!(); + + fn first_coord(&self) -> Coord<::T> { + self.0 + } + + fn second_coord(&self) -> Coord<::T> { + self.1 + } + + fn third_coord(&self) -> Coord<::T> { + self.2 + } + + fn to_array(&self) -> [Coord<::T>; 3] { + (*self).to_array() + } + + fn to_lines(&self) -> [Line<::T>; 3] { + (*self).to_lines() + } +} + +impl GeoTraitExtWithTypeTag for &Triangle { + type Tag = TriangleTag; +} + +impl TriangleTraitExt for UnimplementedTriangle +where + T: CoordNum, +{ + forward_triangle_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for UnimplementedTriangle { + type Tag = TriangleTag; +} diff --git a/rust/sedona-geo-traits-ext/src/type_tag.rs b/rust/sedona-geo-traits-ext/src/type_tag.rs new file mode 100644 index 00000000..f6b1fdf8 --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/type_tag.rs @@ -0,0 +1,66 @@ +// 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 tags for dispatching algorithm traits to the corresponding implementation + +/// Marker trait implemented by all geometry type tags used for dispatch. +pub trait GeoTypeTag {} + +/// Tag that identifies coordinate-like values. +pub struct CoordTag; +/// Tag that identifies point-like geometries. +pub struct PointTag; +/// Tag that identifies line-string-like geometries. +pub struct LineStringTag; +/// Tag that identifies polygon-like geometries. +pub struct PolygonTag; +/// Tag that identifies multi-point-like geometries. +pub struct MultiPointTag; +/// Tag that identifies multi-line-string-like geometries. +pub struct MultiLineStringTag; +/// Tag that identifies multi-polygon-like geometries. +pub struct MultiPolygonTag; +/// Tag that identifies geometry-collection-like geometries. +pub struct GeometryCollectionTag; +/// Tag that identifies generic geometry values. +pub struct GeometryTag; +/// Tag that identifies line-segment-like geometries. +pub struct LineTag; +/// Tag that identifies rectangle-like geometries. +pub struct RectTag; +/// Tag that identifies triangle-like geometries. +pub struct TriangleTag; + +impl GeoTypeTag for CoordTag {} +impl GeoTypeTag for PointTag {} +impl GeoTypeTag for LineStringTag {} +impl GeoTypeTag for PolygonTag {} +impl GeoTypeTag for MultiPointTag {} +impl GeoTypeTag for MultiLineStringTag {} +impl GeoTypeTag for MultiPolygonTag {} +impl GeoTypeTag for GeometryCollectionTag {} +impl GeoTypeTag for GeometryTag {} +impl GeoTypeTag for LineTag {} +impl GeoTypeTag for RectTag {} +impl GeoTypeTag for TriangleTag {} + +/// Helper trait implemented by extension traits to expose their geometry tag. +/// Each geometry type could only implement this trait once, so each geometry type +/// has one unique tag. This helps us work around the single-orphan rule of Rust +/// trait system and help us smoothly refactor the existing algorithms in georust/geo. +pub trait GeoTraitExtWithTypeTag { + type Tag: GeoTypeTag; +} diff --git a/rust/sedona-geo-traits-ext/src/wkb_ext.rs b/rust/sedona-geo-traits-ext/src/wkb_ext.rs new file mode 100644 index 00000000..4c84dbea --- /dev/null +++ b/rust/sedona-geo-traits-ext/src/wkb_ext.rs @@ -0,0 +1,557 @@ +// 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 std::marker::PhantomData; + +use crate::*; +use byteorder::{BigEndian, ByteOrder, LittleEndian}; +use geo_traits::{ + GeometryCollectionTrait, GeometryTrait, GeometryType, LineStringTrait, MultiLineStringTrait, + MultiPointTrait, MultiPolygonTrait, PointTrait, PolygonTrait, +}; +use geo_types::{Coord as GeoCoord, Line}; +use wkb::reader::{ + Coord, Dimension, GeometryCollection, LineString, LinearRing, MultiLineString, MultiPoint, + MultiPolygon, Point, Polygon, Wkb, +}; +use wkb::Endianness; + +// ┌──────────────────────────────────────────────────────────┐ +// │ Coord │ +// └──────────────────────────────────────────────────────────┘ +impl CoordTraitExt for Coord<'_> { + #[inline] + fn geo_coord(&self) -> geo_types::Coord { + let coord_slice = self.coord_slice(); + unsafe { + let x_bytes = std::slice::from_raw_parts(coord_slice.as_ptr(), 8); + let y_bytes = std::slice::from_raw_parts(coord_slice.as_ptr().add(8), 8); + match self.byte_order() { + Endianness::BigEndian => { + let x = BigEndian::read_f64(x_bytes); + let y = BigEndian::read_f64(y_bytes); + geo_types::Coord { x, y } + } + Endianness::LittleEndian => { + let x = LittleEndian::read_f64(x_bytes); + let y = LittleEndian::read_f64(y_bytes); + geo_types::Coord { x, y } + } + } + } + } +} + +impl GeoTraitExtWithTypeTag for Coord<'_> { + type Tag = CoordTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Point │ +// └──────────────────────────────────────────────────────────┘ + +impl PointTraitExt for Point<'_> { + forward_point_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for Point<'_> { + type Tag = PointTag; +} + +impl PointTraitExt for &Point<'_> { + forward_point_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &Point<'_> { + type Tag = PointTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ LineString │ +// └──────────────────────────────────────────────────────────┘ + +impl LineStringTraitExt for LineString<'_> { + forward_line_string_trait_ext_funcs!(); + + #[inline(always)] + fn lines(&'_ self) -> impl ExactSizeIterator> + '_ { + let buf = self.coords_slice(); + let dim_size = dimension_size(self.dimension()); + let num_coords = self.num_coords(); + match self.byte_order() { + Endianness::LittleEndian => { + EndianLineIter::LE(LineIter::new(buf, num_coords, dim_size)) + } + Endianness::BigEndian => EndianLineIter::BE(LineIter::new(buf, num_coords, dim_size)), + } + } + + #[inline(always)] + fn coord_iter(&self) -> impl Iterator> { + let buf = self.coords_slice(); + let dim_size = dimension_size(self.dimension()); + let num_coords = self.num_coords(); + match self.byte_order() { + Endianness::LittleEndian => { + EndianCoordIter::LE(CoordIter::new(buf, num_coords, dim_size)) + } + Endianness::BigEndian => EndianCoordIter::BE(CoordIter::new(buf, num_coords, dim_size)), + } + } +} + +impl GeoTraitExtWithTypeTag for LineString<'_> { + type Tag = LineStringTag; +} + +impl LineStringTraitExt for &LineString<'_> { + forward_line_string_trait_ext_funcs!(); + + #[inline(always)] + fn lines(&'_ self) -> impl ExactSizeIterator> + '_ { + (*self).lines() + } + + #[inline(always)] + fn coord_iter(&self) -> impl Iterator> { + (*self).coord_iter() + } +} + +impl GeoTraitExtWithTypeTag for &LineString<'_> { + type Tag = LineStringTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ LinearRing │ +// └──────────────────────────────────────────────────────────┘ + +impl LineStringTraitExt for LinearRing<'_> { + forward_line_string_trait_ext_funcs!(); + + #[inline(always)] + fn lines(&'_ self) -> impl ExactSizeIterator> + '_ { + let buf = self.coords_slice(); + let dim_size = dimension_size(self.dimension()); + let num_coords = self.num_coords(); + match self.byte_order() { + Endianness::LittleEndian => { + EndianLineIter::LE(LineIter::new(buf, num_coords, dim_size)) + } + Endianness::BigEndian => EndianLineIter::BE(LineIter::new(buf, num_coords, dim_size)), + } + } + + #[inline(always)] + fn coord_iter(&self) -> impl Iterator> { + let buf = self.coords_slice(); + let dim_size = dimension_size(self.dimension()); + let num_coords = self.num_coords(); + match self.byte_order() { + Endianness::LittleEndian => { + EndianCoordIter::LE(CoordIter::new(buf, num_coords, dim_size)) + } + Endianness::BigEndian => EndianCoordIter::BE(CoordIter::new(buf, num_coords, dim_size)), + } + } +} + +impl GeoTraitExtWithTypeTag for LinearRing<'_> { + type Tag = LineStringTag; +} + +impl LineStringTraitExt for &LinearRing<'_> { + forward_line_string_trait_ext_funcs!(); + + #[inline(always)] + fn lines(&'_ self) -> impl ExactSizeIterator> + '_ { + (*self).lines() + } + + #[inline(always)] + fn coord_iter(&self) -> impl Iterator> { + (*self).coord_iter() + } +} + +impl GeoTraitExtWithTypeTag for &LinearRing<'_> { + type Tag = LineStringTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Polygon │ +// └──────────────────────────────────────────────────────────┘ + +impl PolygonTraitExt for Polygon<'_> { + forward_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for Polygon<'_> { + type Tag = PolygonTag; +} + +impl PolygonTraitExt for &Polygon<'_> { + forward_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for &Polygon<'_> { + type Tag = PolygonTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ MultiPoint │ +// └──────────────────────────────────────────────────────────┘ + +impl MultiPointTraitExt for MultiPoint<'_> { + forward_multi_point_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for MultiPoint<'_> { + type Tag = MultiPointTag; +} + +impl<'a, 'b> MultiPointTraitExt for &'b MultiPoint<'a> +where + 'a: 'b, +{ + forward_multi_point_trait_ext_funcs!(); +} + +impl<'a, 'b> GeoTraitExtWithTypeTag for &'b MultiPoint<'a> +where + 'a: 'b, +{ + type Tag = MultiPointTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ MultiLineString │ +// └──────────────────────────────────────────────────────────┘ + +impl MultiLineStringTraitExt for MultiLineString<'_> { + forward_multi_line_string_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for MultiLineString<'_> { + type Tag = MultiLineStringTag; +} + +impl<'a, 'b> MultiLineStringTraitExt for &'b MultiLineString<'a> +where + 'a: 'b, +{ + forward_multi_line_string_trait_ext_funcs!(); +} + +impl<'a, 'b> GeoTraitExtWithTypeTag for &'b MultiLineString<'a> +where + 'a: 'b, +{ + type Tag = MultiLineStringTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ MultiPolygon │ +// └──────────────────────────────────────────────────────────┘ + +impl MultiPolygonTraitExt for MultiPolygon<'_> { + forward_multi_polygon_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for MultiPolygon<'_> { + type Tag = MultiPolygonTag; +} + +impl<'a, 'b> MultiPolygonTraitExt for &'b MultiPolygon<'a> +where + 'a: 'b, +{ + forward_multi_polygon_trait_ext_funcs!(); +} + +impl<'a, 'b> GeoTraitExtWithTypeTag for &'b MultiPolygon<'a> +where + 'a: 'b, +{ + type Tag = MultiPolygonTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ GeometryCollection │ +// └──────────────────────────────────────────────────────────┘ + +impl GeometryCollectionTraitExt for GeometryCollection<'_> { + forward_geometry_collection_trait_ext_funcs!(); +} + +impl GeoTraitExtWithTypeTag for GeometryCollection<'_> { + type Tag = GeometryCollectionTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Wkb/Geometry │ +// └──────────────────────────────────────────────────────────┘ + +impl<'a> GeometryTraitExt for Wkb<'a> { + forward_geometry_trait_ext_funcs!(f64); + + type InnerGeometryRef<'b> + = &'b Wkb<'a> + where + Self: 'b; + + #[inline] + fn geometry_ext(&self, i: usize) -> Option> { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + return None; + }; + gc.geometry(i) + } + + #[inline] + unsafe fn geometry_unchecked_ext(&self, i: usize) -> Self::InnerGeometryRef<'_> { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Called geometry_unchecked_ext on a non-GeometryCollection geometry"); + }; + gc.geometry_unchecked(i) + } + + #[inline] + fn geometries_ext(&self) -> impl Iterator> { + let GeometryType::GeometryCollection(gc) = self.as_type() else { + panic!("Called geometries_ext on a non-GeometryCollection geometry"); + }; + gc.geometries() + } +} + +impl<'a, 'b> GeometryTraitExt for &'b Wkb<'a> +where + 'a: 'b, +{ + forward_geometry_trait_ext_funcs!(f64); + + type InnerGeometryRef<'c> + = &'b Wkb<'a> + where + Self: 'c; + + #[inline] + fn geometry_ext(&self, i: usize) -> Option> { + (*self).geometry_ext(i) + } + + #[inline] + unsafe fn geometry_unchecked_ext(&self, i: usize) -> Self::InnerGeometryRef<'_> { + (*self).geometry_unchecked_ext(i) + } + + #[inline] + fn geometries_ext(&self) -> impl Iterator> { + (*self).geometries_ext() + } +} + +impl GeoTraitExtWithTypeTag for Wkb<'_> { + type Tag = GeometryTag; +} + +impl<'a, 'b> GeoTraitExtWithTypeTag for &'b Wkb<'a> +where + 'a: 'b, +{ + type Tag = GeometryTag; +} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Iterators │ +// └──────────────────────────────────────────────────────────┘ + +/// Iterator over coordinates in a WKB buffer using a compile-time endianness. +pub struct CoordIter<'a, B: ByteOrder> { + buf: &'a [u8], + current_offset: usize, + remaining: usize, + dim_size: usize, + _marker: PhantomData, +} + +impl<'a, B: ByteOrder> CoordIter<'a, B> { + #[inline] + /// Creates a new coordinate iterator over the provided buffer. + pub fn new(buf: &'a [u8], num_coords: usize, dim_size: usize) -> Self { + Self { + buf, + current_offset: 0, + remaining: num_coords, + dim_size, + _marker: PhantomData, + } + } +} + +impl Iterator for CoordIter<'_, B> { + type Item = GeoCoord; + + #[inline] + fn next(&mut self) -> Option { + if self.remaining == 0 { + return None; + } + + // SAFETY: We're reading raw memory from the buffer at calculated offsets. + // This assumes the buffer contains valid data and offsets are within bounds. + let coord = unsafe { + let x_bytes = std::slice::from_raw_parts(self.buf.as_ptr().add(self.current_offset), 8); + let y_bytes = + std::slice::from_raw_parts(self.buf.as_ptr().add(self.current_offset + 8), 8); + let x = B::read_f64(x_bytes); + let y = B::read_f64(y_bytes); + GeoCoord { x, y } + }; + + self.current_offset += self.dim_size * 8; + self.remaining -= 1; + Some(coord) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) + } +} + +impl ExactSizeIterator for CoordIter<'_, B> {} + +/// Iterator over line segments derived from sequential coordinates in a WKB buffer. +pub struct LineIter<'a, B: ByteOrder> { + coord_iter: CoordIter<'a, B>, + prev_coord: Option>, +} + +impl<'a, B: ByteOrder> LineIter<'a, B> { + #[inline] + /// Creates a new line iterator over the provided buffer. + pub fn new(buf: &'a [u8], num_coords: usize, dim_size: usize) -> Self { + Self { + coord_iter: CoordIter::new(buf, num_coords, dim_size), + prev_coord: None, + } + } +} + +impl Iterator for LineIter<'_, B> { + type Item = Line; + + #[inline] + fn next(&mut self) -> Option { + let current_coord = self.coord_iter.next()?; + + match self.prev_coord { + Some(prev_coord) => { + let line = Line::new(prev_coord, current_coord); + self.prev_coord = Some(current_coord); + Some(line) + } + None => { + // Grab the next coordinate to form the first line segment + let next_coord = self.coord_iter.next()?; + let line = Line::new(current_coord, next_coord); + self.prev_coord = Some(next_coord); + Some(line) + } + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let (min, max) = self.coord_iter.size_hint(); + (min.saturating_sub(1), max.map(|m| m.saturating_sub(1))) + } +} + +impl ExactSizeIterator for LineIter<'_, B> {} + +/// Wrapper around [`CoordIter`] that selects the concrete endianness at runtime. +/// +/// The dispatch in the iterator methods is static and can be inlined by the +/// compiler, so callers do not pay the cost of dynamic allocation. +pub enum EndianCoordIter<'a> { + LE(CoordIter<'a, LittleEndian>), + BE(CoordIter<'a, BigEndian>), +} + +impl Iterator for EndianCoordIter<'_> { + type Item = GeoCoord; + + #[inline] + fn next(&mut self) -> Option { + // We rely on compiler optimization to hoist the match out of the loop, so that + // there's no performance overhead of checking the endianness inside the loop. + match self { + EndianCoordIter::LE(iter) => iter.next(), + EndianCoordIter::BE(iter) => iter.next(), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self { + EndianCoordIter::LE(iter) => iter.size_hint(), + EndianCoordIter::BE(iter) => iter.size_hint(), + } + } +} + +/// Wrapper around [`LineIter`] that selects the concrete endianness at runtime. +pub enum EndianLineIter<'a> { + LE(LineIter<'a, LittleEndian>), + BE(LineIter<'a, BigEndian>), +} + +impl Iterator for EndianLineIter<'_> { + type Item = Line; + + #[inline] + fn next(&mut self) -> Option { + match self { + EndianLineIter::LE(iter) => iter.next(), + EndianLineIter::BE(iter) => iter.next(), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self { + EndianLineIter::LE(iter) => iter.size_hint(), + EndianLineIter::BE(iter) => iter.size_hint(), + } + } +} + +impl ExactSizeIterator for EndianLineIter<'_> {} + +// ┌──────────────────────────────────────────────────────────┐ +// │ Utils. │ +// └──────────────────────────────────────────────────────────┘ +/// Returns the dimensionality (number of ordinates) represented by a WKB [`Dimension`]. +fn dimension_size(dim: Dimension) -> usize { + match dim { + Dimension::Xy => 2, + Dimension::Xyz | Dimension::Xym => 3, + Dimension::Xyzm => 4, + } +} diff --git a/rust/sedona-geo-traits-ext/tests/wkb_ext_tests.rs b/rust/sedona-geo-traits-ext/tests/wkb_ext_tests.rs new file mode 100644 index 00000000..775d68c3 --- /dev/null +++ b/rust/sedona-geo-traits-ext/tests/wkb_ext_tests.rs @@ -0,0 +1,230 @@ +// 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. + +//! Tests for the WKB extension traits implemented in `wkb_ext`. + +use geo_traits::GeometryTrait; +use rstest::rstest; +use sedona_geo_traits_ext::*; +use std::str::FromStr; +use wkb::{reader::Wkb, Endianness}; +use wkt::Wkt; + +/// Helper to create WKB from WKT string using the wkb writer +fn wkb_from_wkt(wkt_str: &str) -> Vec { + wkb_from_wkt_with_endianness(wkt_str, wkb::Endianness::LittleEndian) +} + +/// Helper to create WKB from WKT string using the wkb writer +fn wkb_from_wkt_with_endianness(wkt_str: &str, endianness: wkb::Endianness) -> Vec { + let geometry = Wkt::::from_str(wkt_str).unwrap(); + let mut buf = Vec::new(); + let options = wkb::writer::WriteOptions { endianness }; + wkb::writer::write_geometry(&mut buf, &geometry, &options).unwrap(); + buf +} + +#[rstest] +fn test_geo_coord( + #[values(Endianness::LittleEndian, Endianness::BigEndian)] endianness: Endianness, +) { + let buf = wkb_from_wkt_with_endianness("POINT (1.0 2.0)", endianness); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::Point(pt) = wkb.as_type() else { + panic!("expected point") + }; + let coord = pt.geo_coord().unwrap(); + assert_eq!(coord.x, 1.0); + assert_eq!(coord.y, 2.0); + + let buf = wkb_from_wkt_with_endianness("POINT EMPTY", endianness); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::Point(pt) = wkb.as_type() else { + panic!("expected point") + }; + let coord = pt.geo_coord(); + assert!(coord.is_none()); +} + +#[rstest] +fn test_linestring_iterators( + #[values(Endianness::LittleEndian, Endianness::BigEndian)] endianness: Endianness, +) { + let buf = wkb_from_wkt_with_endianness("LINESTRING(0 0, 1 1, 2 1.5)", endianness); + let wkb = Wkb::try_new(&buf).unwrap(); + let GeometryTypeExt::LineString(ls) = wkb.as_type_ext() else { + panic!("expected linestring") + }; + + let coords = &[(0.0, 0.0), (1.0, 1.0), (2.0, 1.5)]; + let v: Vec<_> = ls.coord_iter().collect(); + assert_eq!(v.len(), coords.len()); + for (got, (ex_x, ex_y)) in v.iter().zip(coords.iter()) { + assert!((got.x - ex_x).abs() < 1e-9); + assert!((got.y - ex_y).abs() < 1e-9); + } + let segs: Vec<_> = ls.lines().collect(); + assert_eq!(segs.len(), coords.len() - 1); + assert_eq!(segs[0].start.x, 0.0); + assert_eq!(segs[0].end.x, 1.0); + + // Empty linestring + let buf = wkb_from_wkt_with_endianness("LINESTRING EMPTY", endianness); + let wkb = Wkb::try_new(&buf).unwrap(); + let GeometryTypeExt::LineString(ls) = wkb.as_type_ext() else { + panic!("expected linestring") + }; + assert_eq!(ls.coord_iter().count(), 0); + assert_eq!(ls.lines().count(), 0); +} + +#[test] +fn test_geometry_collection_ext() { + let buf = wkb_from_wkt("GEOMETRYCOLLECTION(POINT(0 0), POINT(1 1))"); + let wkb = Wkb::try_new(&buf).unwrap(); + + // GeometryTraitExt is implemented for Wkb in wkb_ext. Use those helpers. + assert!(wkb.is_collection()); + assert_eq!(wkb.num_geometries_ext(), 2); + + let child0 = wkb.geometry_ext(0).unwrap(); + let GeometryTypeExt::Point(_) = child0.as_type_ext() else { + panic!("child0 expected point"); + }; + + // Iterate via geometries_ext + let types: Vec<_> = wkb + .geometries_ext() + .map(|g| match g.as_type_ext() { + GeometryTypeExt::Point(_) => "P", + _ => "?", + }) + .collect(); + assert_eq!(types, vec!["P", "P"]); +} + +#[test] +fn test_linestring_rev_lines() { + // Empty linestring + let buf = wkb_from_wkt("LINESTRING EMPTY"); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else { + panic!("expected linestring") + }; + assert_eq!(ls.rev_lines().count(), 0); + + // Two-point linestring: 1 segment + let buf = wkb_from_wkt("LINESTRING(0 0, 1 1)"); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else { + panic!("expected linestring") + }; + let forward: Vec<_> = ls.lines().collect(); + let reverse: Vec<_> = ls.rev_lines().collect(); + assert_eq!(forward.len(), 1); + assert_eq!(reverse.len(), 1); + assert_eq!(forward[0].start.x, reverse[0].start.x); + assert_eq!(forward[0].end.x, reverse[0].end.x); + + // Multi-point linestring: rev_lines should produce segments in reverse order + let buf = wkb_from_wkt("LINESTRING(0 0, 2 0, 2 2, 0 2)"); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else { + panic!("expected linestring") + }; + let forward: Vec<_> = ls.lines().collect(); + let reverse: Vec<_> = ls.rev_lines().collect(); + assert_eq!(forward.len(), 3); + assert_eq!(reverse.len(), 3); + for i in 0..forward.len() { + let f_rev = &forward[forward.len() - 1 - i]; + let r = &reverse[i]; + assert_eq!(f_rev.start.x, r.start.x); + assert_eq!(f_rev.end.x, r.end.x); + } +} + +#[test] +fn test_linestring_is_closed() { + // Empty line string is considered closed + let buf = wkb_from_wkt("LINESTRING EMPTY"); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else { + panic!("expected linestring") + }; + assert!(ls.is_closed()); + + // Non-closed line string + let buf = wkb_from_wkt("LINESTRING(0 0, 1 0, 2 0)"); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else { + panic!("expected linestring") + }; + assert!(!ls.is_closed()); + + // Closed linestring (square ring) with repeated first/last + let buf = wkb_from_wkt("LINESTRING(0 0, 1 0, 1 1, 0 1, 0 0)"); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else { + panic!("expected linestring") + }; + assert!(ls.is_closed()); +} + +#[test] +fn test_linestring_triangles() { + // Empty - no triangles + let buf = wkb_from_wkt("LINESTRING EMPTY"); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else { + panic!("expected linestring") + }; + assert_eq!(ls.triangles().count(), 0); + + // Two points - no triangles (need at least 3) + let buf = wkb_from_wkt("LINESTRING(0 0, 1 1)"); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else { + panic!("expected linestring") + }; + assert_eq!(ls.triangles().count(), 0); + + // Three points - one triangle + let buf = wkb_from_wkt("LINESTRING(0 0, 1 0, 1 1)"); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else { + panic!("expected linestring") + }; + assert_eq!(ls.triangles().count(), 1); + + // Four points - two triangles + let buf = wkb_from_wkt("LINESTRING(0 0, 2 0, 2 2, 0 2)"); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else { + panic!("expected linestring") + }; + assert_eq!(ls.triangles().count(), 2); + + // Single point - degenerate case + let buf = wkb_from_wkt("LINESTRING(5 5)"); + let wkb = Wkb::try_new(&buf).unwrap(); + let geo_traits::GeometryType::LineString(ls) = wkb.as_type() else { + panic!("expected linestring") + }; + assert_eq!(ls.triangles().count(), 0); + assert_eq!(ls.lines().len(), 0); +}