This is an automated email from the ASF dual-hosted git repository.
kontinuation pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git
The following commit(s) were added to refs/heads/main by this push:
new 2051517 feat: Add sedona-geo-traits-ext to sedona-db (#194)
2051517 is described below
commit 2051517207918263194925d9dc6792446e94e2f4
Author: Kristin Cowalcijk <[email protected]>
AuthorDate: Thu Oct 9 13:35:58 2025 +0800
feat: Add sedona-geo-traits-ext to sedona-db (#194)
This is part of the forked dependency elimination plan:
https://github.com/apache/sedona-db/pull/165. This PR depends on
https://github.com/apache/sedona-db/pull/193.
This PR moves geo-traits-ext from wherobots/geo to sedona-db and renamed it
to sedona-geo-traits-ext. Currently, it is a standalone crate and can be
compiled using `cd rust/sedona-geo-traits-ext && cargo build`. We'll update the
Cargo.toml files in the final step to make it live.
---
Cargo.lock | 10 +-
rust/sedona-geo-traits-ext/Cargo.toml | 42 ++
rust/sedona-geo-traits-ext/README.md | 37 ++
rust/sedona-geo-traits-ext/src/coord.rs | 69 +++
rust/sedona-geo-traits-ext/src/geometry.rs | 365 ++++++++++++++
.../src/geometry_collection.rs | 113 +++++
rust/sedona-geo-traits-ext/src/lib.rs | 64 +++
rust/sedona-geo-traits-ext/src/line.rs | 140 ++++++
rust/sedona-geo-traits-ext/src/line_string.rs | 234 +++++++++
.../sedona-geo-traits-ext/src/multi_line_string.rs | 123 +++++
rust/sedona-geo-traits-ext/src/multi_point.rs | 168 +++++++
rust/sedona-geo-traits-ext/src/multi_polygon.rs | 113 +++++
rust/sedona-geo-traits-ext/src/point.rs | 113 +++++
rust/sedona-geo-traits-ext/src/polygon.rs | 125 +++++
rust/sedona-geo-traits-ext/src/rect.rs | 331 ++++++++++++
rust/sedona-geo-traits-ext/src/triangle.rs | 200 ++++++++
rust/sedona-geo-traits-ext/src/type_tag.rs | 66 +++
rust/sedona-geo-traits-ext/src/wkb_ext.rs | 557 +++++++++++++++++++++
rust/sedona-geo-traits-ext/tests/wkb_ext_tests.rs | 230 +++++++++
19 files changed, 3098 insertions(+), 2 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 93db6d4..d9a3c0a 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 0000000..9a2a465
--- /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 <[email protected]>"]
+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 0000000..ce5ab0d
--- /dev/null
+++ b/rust/sedona-geo-traits-ext/README.md
@@ -0,0 +1,37 @@
+<!-- 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. -->
+
+# 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<LineStringTag>` and `AreaTrait<PolygonTag>`,
since
+`AreaTrait<LineStringTag>` and `AreaTrait<PolygonTag>` 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 0000000..cd7472d
--- /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<Tag = CoordTag>
+where
+ <Self as CoordTrait>::T: CoordNum,
+{
+ #[inline]
+ /// Converts this coordinate into the concrete [`geo_types::Coord`].
+ fn geo_coord(&self) -> Coord<Self::T> {
+ Coord {
+ x: self.x(),
+ y: self.y(),
+ }
+ }
+}
+
+impl<T> CoordTraitExt for Coord<T>
+where
+ T: CoordNum,
+{
+ fn geo_coord(&self) -> Coord<T> {
+ *self
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for Coord<T> {
+ type Tag = CoordTag;
+}
+
+impl<T> CoordTraitExt for &Coord<T>
+where
+ T: CoordNum,
+{
+ fn geo_coord(&self) -> Coord<T> {
+ **self
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &Coord<T> {
+ type Tag = CoordTag;
+}
+
+impl<T> CoordTraitExt for UnimplementedCoord<T> where T: CoordNum {}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for UnimplementedCoord<T> {
+ 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 0000000..d665cbb
--- /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<Tag =
GeometryTag>
+where
+ <Self as GeometryTrait>::T: CoordNum,
+{
+ /// Extension-aware point type exposed by this geometry.
+ type PointTypeExt<'a>: 'a + PointTraitExt<T = <Self as GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Extension-aware line string type exposed by this geometry.
+ type LineStringTypeExt<'a>: 'a + LineStringTraitExt<T = <Self as
GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Extension-aware polygon type exposed by this geometry.
+ type PolygonTypeExt<'a>: 'a + PolygonTraitExt<T = <Self as
GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Extension-aware multi point type exposed by this geometry.
+ type MultiPointTypeExt<'a>: 'a + MultiPointTraitExt<T = <Self as
GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Extension-aware multi line string type exposed by this geometry.
+ type MultiLineStringTypeExt<'a>: 'a + MultiLineStringTraitExt<T = <Self as
GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Extension-aware multi polygon type exposed by this geometry.
+ type MultiPolygonTypeExt<'a>: 'a + MultiPolygonTraitExt<T = <Self as
GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Extension-aware triangle type exposed by this geometry.
+ type TriangleTypeExt<'a>: 'a + TriangleTraitExt<T = <Self as
GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Extension-aware rectangle type exposed by this geometry.
+ type RectTypeExt<'a>: 'a + RectTraitExt<T = <Self as GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Extension-aware line type exposed by this geometry.
+ type LineTypeExt<'a>: 'a + LineTraitExt<T = <Self as GeometryTrait>::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<Self>
+ 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<Self::InnerGeometryRef<'_>>;
+
+ /// 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<Item =
Self::InnerGeometryRef<'_>>;
+}
+
+#[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,
+ <P as GeometryTrait>::T: CoordNum,
+ <LS as GeometryTrait>::T: CoordNum,
+ <Y as GeometryTrait>::T: CoordNum,
+ <MP as GeometryTrait>::T: CoordNum,
+ <ML as GeometryTrait>::T: CoordNum,
+ <MY as GeometryTrait>::T: CoordNum,
+ <R as GeometryTrait>::T: CoordNum,
+ <TT as GeometryTrait>::T: CoordNum,
+ <L as GeometryTrait>::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>
+ = <Self as GeometryTrait>::PointType<'__g_inner>
+ where
+ Self: '__g_inner;
+
+ type LineStringTypeExt<'__g_inner>
+ = <Self as GeometryTrait>::LineStringType<'__g_inner>
+ where
+ Self: '__g_inner;
+
+ type PolygonTypeExt<'__g_inner>
+ = <Self as GeometryTrait>::PolygonType<'__g_inner>
+ where
+ Self: '__g_inner;
+
+ type MultiPointTypeExt<'__g_inner>
+ = <Self as GeometryTrait>::MultiPointType<'__g_inner>
+ where
+ Self: '__g_inner;
+
+ type MultiLineStringTypeExt<'__g_inner>
+ = <Self as GeometryTrait>::MultiLineStringType<'__g_inner>
+ where
+ Self: '__g_inner;
+
+ type MultiPolygonTypeExt<'__g_inner>
+ = <Self as GeometryTrait>::MultiPolygonType<'__g_inner>
+ where
+ Self: '__g_inner;
+
+ type RectTypeExt<'__g_inner>
+ = <Self as GeometryTrait>::RectType<'__g_inner>
+ where
+ Self: '__g_inner;
+
+ type TriangleTypeExt<'__g_inner>
+ = <Self as GeometryTrait>::TriangleType<'__g_inner>
+ where
+ Self: '__g_inner;
+
+ type LineTypeExt<'__g_inner>
+ = <Self as GeometryTrait>::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<T> GeometryTraitExt for Geometry<T>
+where
+ T: CoordNum,
+{
+ forward_geometry_trait_ext_funcs!(T);
+
+ type InnerGeometryRef<'a>
+ = &'a Geometry<T>
+ where
+ Self: 'a;
+
+ fn geometry_ext(&self, i: usize) -> Option<&Geometry<T>> {
+ let GeometryType::GeometryCollection(gc) = self.as_type() else {
+ panic!("Not a GeometryCollection");
+ };
+ gc.geometry(i)
+ }
+
+ unsafe fn geometry_unchecked_ext(&self, i: usize) -> &Geometry<T> {
+ let GeometryType::GeometryCollection(gc) = self.as_type() else {
+ panic!("Not a GeometryCollection");
+ };
+ gc.geometry_unchecked(i)
+ }
+
+ fn geometries_ext(&self) -> impl Iterator<Item = &Geometry<T>> {
+ let GeometryType::GeometryCollection(gc) = self.as_type() else {
+ panic!("Not a GeometryCollection");
+ };
+ gc.geometries()
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for Geometry<T> {
+ type Tag = GeometryTag;
+}
+
+impl<'a, T> GeometryTraitExt for &'a Geometry<T>
+where
+ T: CoordNum,
+{
+ forward_geometry_trait_ext_funcs!(T);
+
+ type InnerGeometryRef<'b>
+ = &'a Geometry<T>
+ where
+ Self: 'b;
+
+ fn geometry_ext(&self, i: usize) -> Option<&'a Geometry<T>> {
+ let g = *self;
+ g.geometry_ext(i)
+ }
+
+ unsafe fn geometry_unchecked_ext(&self, i: usize) -> &'a Geometry<T> {
+ let g = *self;
+ g.geometry_unchecked_ext(i)
+ }
+
+ fn geometries_ext(&self) -> impl Iterator<Item = &'a Geometry<T>> {
+ let g = *self;
+ g.geometries_ext()
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &Geometry<T> {
+ type Tag = GeometryTag;
+}
+
+impl<T> GeometryTraitExt for UnimplementedGeometry<T>
+where
+ T: CoordNum,
+{
+ forward_geometry_trait_ext_funcs!(T);
+
+ type InnerGeometryRef<'a>
+ = &'a UnimplementedGeometry<T>
+ where
+ Self: 'a;
+
+ fn geometry_ext(&self, _i: usize) -> Option<Self::InnerGeometryRef<'_>> {
+ unimplemented!()
+ }
+
+ unsafe fn geometry_unchecked_ext(&self, _i: usize) ->
Self::InnerGeometryRef<'_> {
+ unimplemented!()
+ }
+
+ fn geometries_ext(&self) -> impl Iterator<Item =
Self::InnerGeometryRef<'_>> {
+ unimplemented!();
+
+ // For making the type checker happy
+ #[allow(unreachable_code)]
+ core::iter::empty()
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for UnimplementedGeometry<T> {
+ 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 0000000..50f6c6f
--- /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<Tag =
GeometryCollectionTag>
+where
+ <Self as GeometryTrait>::T: CoordNum,
+{
+ /// Extension-aware geometry type yielded by accessor methods.
+ type GeometryTypeExt<'a>: 'a + GeometryTraitExt<T = <Self as
GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Returns the geometry at index `i`, wrapped with [`GeometryTraitExt`].
+ fn geometry_ext(&self, i: usize) -> Option<Self::GeometryTypeExt<'_>>;
+
+ /// 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<Item =
Self::GeometryTypeExt<'_>>;
+}
+
+#[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>
+ = <Self as GeometryCollectionTrait>::GeometryType<'__gc_inner>
+ where
+ Self: '__gc_inner;
+
+ #[inline]
+ fn geometry_ext(&self, i: usize) -> Option<Self::GeometryTypeExt<'_>> {
+ <Self as GeometryCollectionTrait>::geometry(self, i)
+ }
+
+ #[inline]
+ unsafe fn geometry_unchecked_ext(&self, i: usize) ->
Self::GeometryTypeExt<'_> {
+ unsafe { <Self as
GeometryCollectionTrait>::geometry_unchecked(self, i) }
+ }
+
+ #[inline]
+ fn geometries_ext(&self) -> impl Iterator<Item =
Self::GeometryTypeExt<'_>> {
+ <Self as GeometryCollectionTrait>::geometries(self)
+ }
+ };
+}
+
+impl<T> GeometryCollectionTraitExt for GeometryCollection<T>
+where
+ T: CoordNum,
+{
+ forward_geometry_collection_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for GeometryCollection<T> {
+ type Tag = GeometryCollectionTag;
+}
+
+impl<T> GeometryCollectionTraitExt for &GeometryCollection<T>
+where
+ T: CoordNum,
+{
+ forward_geometry_collection_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &GeometryCollection<T> {
+ type Tag = GeometryCollectionTag;
+}
+
+impl<T> GeometryCollectionTraitExt for UnimplementedGeometryCollection<T>
+where
+ T: CoordNum,
+{
+ forward_geometry_collection_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for
UnimplementedGeometryCollection<T> {
+ 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 0000000..12729e7
--- /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 0000000..94665a1
--- /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<Tag = LineTag>
+where
+ <Self as GeometryTrait>::T: CoordNum,
+{
+ type CoordTypeExt<'a>: 'a + CoordTraitExt<T = <Self as GeometryTrait>::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<<Self as GeometryTrait>::T> {
+ self.start_ext().geo_coord()
+ }
+
+ #[inline]
+ /// Returns the end coordinate converted to [`geo_types::Coord`].
+ fn end_coord(&self) -> geo_types::Coord<<Self as GeometryTrait>::T> {
+ self.end_ext().geo_coord()
+ }
+
+ #[inline]
+ /// Returns the line converted to a [`geo_types::Line`].
+ fn geo_line(&self) -> Line<<Self as GeometryTrait>::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>
+ = <Self as LineTrait>::CoordType<'__l_inner>
+ where
+ Self: '__l_inner;
+
+ #[inline]
+ fn start_ext(&self) -> Self::CoordTypeExt<'_> {
+ <Self as LineTrait>::start(self)
+ }
+
+ #[inline]
+ fn end_ext(&self) -> Self::CoordTypeExt<'_> {
+ <Self as LineTrait>::end(self)
+ }
+
+ #[inline]
+ fn coords_ext(&self) -> [Self::CoordTypeExt<'_>; 2] {
+ [self.start_ext(), self.end_ext()]
+ }
+ };
+}
+
+impl<T> LineTraitExt for Line<T>
+where
+ T: CoordNum,
+{
+ forward_line_trait_ext_funcs!();
+
+ fn start_coord(&self) -> geo_types::Coord<T> {
+ self.start
+ }
+
+ fn end_coord(&self) -> geo_types::Coord<T> {
+ self.end
+ }
+
+ fn geo_line(&self) -> geo_types::Line<T> {
+ *self
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for Line<T> {
+ type Tag = LineTag;
+}
+
+impl<T> LineTraitExt for &Line<T>
+where
+ T: CoordNum,
+{
+ forward_line_trait_ext_funcs!();
+
+ fn start_coord(&self) -> geo_types::Coord<T> {
+ self.start
+ }
+
+ fn end_coord(&self) -> geo_types::Coord<T> {
+ self.end
+ }
+
+ fn geo_line(&self) -> geo_types::Line<T> {
+ **self
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &Line<T> {
+ type Tag = LineTag;
+}
+
+impl<T> LineTraitExt for UnimplementedLine<T>
+where
+ T: CoordNum,
+{
+ forward_line_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for UnimplementedLine<T> {
+ 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 0000000..86c07c9
--- /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<Tag = LineStringTag>
+where
+ <Self as GeometryTrait>::T: CoordNum,
+{
+ type CoordTypeExt<'a>: 'a + CoordTraitExt<T = <Self as GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Returns the coordinate at the provided index.
+ fn coord_ext(&self, i: usize) -> Option<Self::CoordTypeExt<'_>>;
+
+ /// 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<Item = Self::CoordTypeExt<'_>>;
+
+ /// 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::T> {
+ 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<Item = Line<<Self as
GeometryTrait>::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<Item = Line<<Self as
GeometryTrait>::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<Item = Triangle<<Self as GeometryTrait>::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<Item = Coord<<Self as
GeometryTrait>::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>
+ = <Self as LineStringTrait>::CoordType<'__l_inner>
+ where
+ Self: '__l_inner;
+
+ #[inline]
+ fn coord_ext(&self, i: usize) -> Option<Self::CoordTypeExt<'_>> {
+ <Self as LineStringTrait>::coord(self, i)
+ }
+
+ #[inline]
+ unsafe fn coord_unchecked_ext(&self, i: usize) ->
Self::CoordTypeExt<'_> {
+ <Self as LineStringTrait>::coord_unchecked(self, i)
+ }
+
+ #[inline]
+ fn coords_ext(&self) -> impl Iterator<Item = Self::CoordTypeExt<'_>> {
+ <Self as LineStringTrait>::coords(self)
+ }
+ };
+}
+
+impl<T> LineStringTraitExt for LineString<T>
+where
+ T: CoordNum,
+{
+ forward_line_string_trait_ext_funcs!();
+
+ unsafe fn geo_coord_unchecked(&self, i: usize) -> Coord<Self::T> {
+ *self.0.get_unchecked(i)
+ }
+
+ // Delegate to the `geo-types` implementation for less performance overhead
+ fn lines(&'_ self) -> impl ExactSizeIterator<Item = Line<<Self as
GeometryTrait>::T>> + '_ {
+ self.lines()
+ }
+
+ fn rev_lines(&'_ self) -> impl ExactSizeIterator<Item = Line<<Self as
GeometryTrait>::T>> + '_ {
+ self.rev_lines()
+ }
+
+ fn triangles(
+ &'_ self,
+ ) -> impl ExactSizeIterator<Item = Triangle<<Self as GeometryTrait>::T>> +
'_ {
+ self.triangles()
+ }
+
+ fn is_closed(&self) -> bool {
+ self.is_closed()
+ }
+
+ fn coord_iter(&self) -> impl Iterator<Item = Coord<<Self as
GeometryTrait>::T>> {
+ self.0.iter().copied()
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for LineString<T> {
+ type Tag = LineStringTag;
+}
+
+impl<T> LineStringTraitExt for &LineString<T>
+where
+ T: CoordNum,
+{
+ forward_line_string_trait_ext_funcs!();
+
+ unsafe fn geo_coord_unchecked(&self, i: usize) -> Coord<Self::T> {
+ *self.0.get_unchecked(i)
+ }
+
+ // Delegate to the `geo-types` implementation for less performance overhead
+ fn lines(&'_ self) -> impl ExactSizeIterator<Item = Line<<Self as
GeometryTrait>::T>> + '_ {
+ (*self).lines()
+ }
+
+ fn rev_lines(&'_ self) -> impl ExactSizeIterator<Item = Line<<Self as
GeometryTrait>::T>> + '_ {
+ (*self).rev_lines()
+ }
+
+ fn triangles(
+ &'_ self,
+ ) -> impl ExactSizeIterator<Item = Triangle<<Self as GeometryTrait>::T>> +
'_ {
+ (*self).triangles()
+ }
+
+ fn is_closed(&self) -> bool {
+ (*self).is_closed()
+ }
+
+ fn coord_iter(&self) -> impl Iterator<Item = Coord<<Self as
GeometryTrait>::T>> {
+ self.0.iter().copied()
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &LineString<T> {
+ type Tag = LineStringTag;
+}
+
+impl<T> LineStringTraitExt for UnimplementedLineString<T>
+where
+ T: CoordNum,
+{
+ forward_line_string_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for UnimplementedLineString<T> {
+ 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 0000000..af4f371
--- /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<Tag = MultiLineStringTag>
+where
+ <Self as GeometryTrait>::T: CoordNum,
+{
+ /// Extension-friendly line string type returned by accessor methods.
+ type LineStringTypeExt<'a>: 'a + LineStringTraitExt<T = <Self as
GeometryTrait>::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<Self::LineStringTypeExt<'_>>;
+
+ /// 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<Item =
Self::LineStringTypeExt<'_>>;
+
+ /// 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>
+ = <Self as MultiLineStringTrait>::InnerLineStringType<'__l_inner>
+ where
+ Self: '__l_inner;
+
+ #[inline]
+ fn line_string_ext(&self, i: usize) ->
Option<Self::LineStringTypeExt<'_>> {
+ <Self as MultiLineStringTrait>::line_string(self, i)
+ }
+
+ #[inline]
+ unsafe fn line_string_unchecked_ext(&self, i: usize) ->
Self::LineStringTypeExt<'_> {
+ <Self as MultiLineStringTrait>::line_string_unchecked(self, i)
+ }
+
+ #[inline]
+ fn line_strings_ext(&self) -> impl Iterator<Item =
Self::LineStringTypeExt<'_>> {
+ <Self as MultiLineStringTrait>::line_strings(self)
+ }
+ };
+}
+
+impl<T> MultiLineStringTraitExt for MultiLineString<T>
+where
+ T: CoordNum,
+{
+ forward_multi_line_string_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for MultiLineString<T> {
+ type Tag = MultiLineStringTag;
+}
+
+impl<T> MultiLineStringTraitExt for &MultiLineString<T>
+where
+ T: CoordNum,
+{
+ forward_multi_line_string_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &MultiLineString<T> {
+ type Tag = MultiLineStringTag;
+}
+
+impl<T> MultiLineStringTraitExt for UnimplementedMultiLineString<T>
+where
+ T: CoordNum,
+{
+ forward_multi_line_string_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for UnimplementedMultiLineString<T> {
+ 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 0000000..bdd40c8
--- /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<Tag = MultiPointTag>
+where
+ <Self as GeometryTrait>::T: CoordNum,
+{
+ /// Extension-aware point type returned from accessors on this multi point.
+ type PointTypeExt<'a>: 'a + PointTraitExt<T = <Self as GeometryTrait>::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<Self::PointTypeExt<'_>>;
+
+ /// 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<Coord<<Self as
GeometryTrait>::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<Item =
Self::PointTypeExt<'_>>;
+
+ /// 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<Item = Coord<<Self as
GeometryTrait>::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>
+ = <Self as MultiPointTrait>::InnerPointType<'__l_inner>
+ where
+ Self: '__l_inner;
+
+ #[inline]
+ fn point_ext(&self, i: usize) -> Option<Self::PointTypeExt<'_>> {
+ <Self as MultiPointTrait>::point(self, i)
+ }
+
+ #[inline]
+ unsafe fn point_unchecked_ext(&self, i: usize) ->
Self::PointTypeExt<'_> {
+ <Self as MultiPointTrait>::point_unchecked(self, i)
+ }
+
+ #[inline]
+ fn points_ext(&self) -> impl DoubleEndedIterator<Item =
Self::PointTypeExt<'_>> {
+ <Self as MultiPointTrait>::points(self)
+ }
+ };
+}
+
+impl<T> MultiPointTraitExt for MultiPoint<T>
+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<Coord<T>> {
+ Some(self.0.get_unchecked(i).0)
+ }
+
+ // Specialized implementation for geo_types::MultiPoint to reduce
performance overhead
+ fn coord_iter(&self) -> impl DoubleEndedIterator<Item = Coord<<Self as
GeometryTrait>::T>> {
+ self.0.iter().map(|p| p.0)
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for MultiPoint<T> {
+ type Tag = MultiPointTag;
+}
+
+impl<T> MultiPointTraitExt for &MultiPoint<T>
+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<Coord<T>> {
+ Some(self.0.get_unchecked(i).0)
+ }
+
+ // Specialized implementation for geo_types::MultiPoint to reduce
performance overhead
+ fn coord_iter(&self) -> impl DoubleEndedIterator<Item = Coord<<Self as
GeometryTrait>::T>> {
+ self.0.iter().map(|p| p.0)
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &MultiPoint<T> {
+ type Tag = MultiPointTag;
+}
+
+impl<T> MultiPointTraitExt for UnimplementedMultiPoint<T>
+where
+ T: CoordNum,
+{
+ forward_multi_point_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for UnimplementedMultiPoint<T> {
+ 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 0000000..9b90afa
--- /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<Tag = MultiPolygonTag>
+where
+ <Self as GeometryTrait>::T: CoordNum,
+{
+ /// Extension-aware polygon type yielded by accessor methods.
+ type PolygonTypeExt<'a>: 'a + PolygonTraitExt<T = <Self as
GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Returns the polygon at index `i`, wrapped with [`PolygonTraitExt`].
+ fn polygon_ext(&self, i: usize) -> Option<Self::PolygonTypeExt<'_>>;
+
+ /// 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<Item = Self::PolygonTypeExt<'_>>;
+}
+
+#[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>
+ = <Self as MultiPolygonTrait>::InnerPolygonType<'__l_inner>
+ where
+ Self: '__l_inner;
+
+ #[inline]
+ fn polygon_ext(&self, i: usize) -> Option<Self::PolygonTypeExt<'_>> {
+ <Self as MultiPolygonTrait>::polygon(self, i)
+ }
+
+ #[inline]
+ unsafe fn polygon_unchecked_ext(&self, i: usize) ->
Self::PolygonTypeExt<'_> {
+ <Self as MultiPolygonTrait>::polygon_unchecked(self, i)
+ }
+
+ #[inline]
+ fn polygons_ext(&self) -> impl Iterator<Item =
Self::PolygonTypeExt<'_>> {
+ <Self as MultiPolygonTrait>::polygons(self)
+ }
+ };
+}
+
+impl<T> MultiPolygonTraitExt for MultiPolygon<T>
+where
+ T: CoordNum,
+{
+ forward_multi_polygon_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for MultiPolygon<T> {
+ type Tag = MultiPolygonTag;
+}
+
+impl<T> MultiPolygonTraitExt for &MultiPolygon<T>
+where
+ T: CoordNum,
+{
+ forward_multi_polygon_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &MultiPolygon<T> {
+ type Tag = MultiPolygonTag;
+}
+
+impl<T> MultiPolygonTraitExt for UnimplementedMultiPolygon<T>
+where
+ T: CoordNum,
+{
+ forward_multi_polygon_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for UnimplementedMultiPolygon<T> {
+ 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 0000000..3548961
--- /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<Tag = PointTag>
+where
+ <Self as GeometryTrait>::T: CoordNum,
+{
+ type CoordTypeExt<'a>: 'a + CoordTraitExt<T = <Self as GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Returns the underlying coordinate view for this point, if available.
+ fn coord_ext(&self) -> Option<Self::CoordTypeExt<'_>>;
+
+ #[inline]
+ /// Converts the point into a concrete [`geo_types::Point`].
+ fn geo_point(&self) -> Option<Point<<Self as GeometryTrait>::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<Coord<<Self as GeometryTrait>::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>
+ = <Self as PointTrait>::CoordType<'__l_inner>
+ where
+ Self: '__l_inner;
+
+ #[inline]
+ fn coord_ext(&self) -> Option<Self::CoordTypeExt<'_>> {
+ <Self as PointTrait>::coord(self)
+ }
+ };
+}
+
+impl<T> PointTraitExt for Point<T>
+where
+ T: CoordNum,
+{
+ forward_point_trait_ext_funcs!();
+
+ fn geo_point(&self) -> Option<Point<T>> {
+ Some(*self)
+ }
+
+ fn geo_coord(&self) -> Option<Coord<T>> {
+ Some(self.0)
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for Point<T> {
+ type Tag = PointTag;
+}
+
+impl<T> PointTraitExt for &Point<T>
+where
+ T: CoordNum,
+{
+ forward_point_trait_ext_funcs!();
+
+ fn geo_point(&self) -> Option<Point<T>> {
+ Some(**self)
+ }
+
+ fn geo_coord(&self) -> Option<Coord<T>> {
+ Some(self.0)
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &Point<T> {
+ type Tag = PointTag;
+}
+
+impl<T> PointTraitExt for UnimplementedPoint<T>
+where
+ T: CoordNum,
+{
+ forward_point_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for UnimplementedPoint<T> {
+ 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 0000000..52a00a5
--- /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<Tag =
PolygonTag>
+where
+ <Self as GeometryTrait>::T: CoordNum,
+{
+ /// Type of ring returned from accessor methods, wrapped with
+ /// [`LineStringTraitExt`].
+ type RingTypeExt<'a>: 'a + LineStringTraitExt<T = <Self as
GeometryTrait>::T>
+ where
+ Self: 'a;
+
+ /// Returns the exterior ring with the extension trait applied.
+ fn exterior_ext(&self) -> Option<Self::RingTypeExt<'_>>;
+
+ /// Iterates over each interior ring with the extension trait applied.
+ fn interiors_ext(
+ &self,
+ ) -> impl DoubleEndedIterator + ExactSizeIterator<Item =
Self::RingTypeExt<'_>>;
+
+ /// Returns the `i`th interior ring, if present, wrapped with the
extension trait.
+ fn interior_ext(&self, i: usize) -> Option<Self::RingTypeExt<'_>>;
+
+ /// 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>
+ = <Self as PolygonTrait>::RingType<'__l_inner>
+ where
+ Self: '__l_inner;
+
+ #[inline]
+ fn exterior_ext(&self) -> Option<Self::RingTypeExt<'_>> {
+ <Self as PolygonTrait>::exterior(self)
+ }
+
+ #[inline]
+ fn interiors_ext(
+ &self,
+ ) -> impl DoubleEndedIterator + ExactSizeIterator<Item =
Self::RingTypeExt<'_>> {
+ <Self as PolygonTrait>::interiors(self)
+ }
+
+ #[inline]
+ fn interior_ext(&self, i: usize) -> Option<Self::RingTypeExt<'_>> {
+ <Self as PolygonTrait>::interior(self, i)
+ }
+
+ #[inline]
+ unsafe fn interior_unchecked_ext(&self, i: usize) ->
Self::RingTypeExt<'_> {
+ <Self as PolygonTrait>::interior_unchecked(self, i)
+ }
+ };
+}
+
+impl<T> PolygonTraitExt for Polygon<T>
+where
+ T: CoordNum,
+{
+ forward_polygon_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for Polygon<T> {
+ type Tag = PolygonTag;
+}
+
+impl<T> PolygonTraitExt for &Polygon<T>
+where
+ T: CoordNum,
+{
+ forward_polygon_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &Polygon<T> {
+ type Tag = PolygonTag;
+}
+
+impl<T> PolygonTraitExt for UnimplementedPolygon<T>
+where
+ T: CoordNum,
+{
+ forward_polygon_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for UnimplementedPolygon<T> {
+ 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 0000000..335179d
--- /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<Tag = RectTag>
+where
+ <Self as GeometryTrait>::T: CoordNum,
+{
+ /// Extension-aware coordinate type returned from accessors.
+ type CoordTypeExt<'a>: 'a + CoordTraitExt<T = <Self as GeometryTrait>::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<<Self as GeometryTrait>::T> {
+ self.min_ext().geo_coord()
+ }
+
+ #[inline]
+ /// Returns the maximum corner as a `geo-types::Coord`.
+ fn max_coord(&self) -> Coord<<Self as GeometryTrait>::T> {
+ self.max_ext().geo_coord()
+ }
+
+ #[inline]
+ /// Constructs a [`geo_types::Rect`] from the extension trait accessors.
+ fn geo_rect(&self) -> Rect<<Self as GeometryTrait>::T> {
+ Rect::new(self.min_coord(), self.max_coord())
+ }
+
+ #[inline]
+ /// Returns the width of the rectangle.
+ fn width(&self) -> <Self as GeometryTrait>::T {
+ self.max().x() - self.min().x()
+ }
+
+ #[inline]
+ /// Returns the height of the rectangle.
+ fn height(&self) -> <Self as GeometryTrait>::T {
+ self.max().y() - self.min().y()
+ }
+
+ /// Converts the rectangle into a polygon with four corners.
+ fn to_polygon(&self) -> Polygon<<Self as GeometryTrait>::T>
+ where
+ <Self as GeometryTrait>::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<<Self as GeometryTrait>::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<<Self as GeometryTrait>::T>
+ where
+ <Self as GeometryTrait>::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<<Self as GeometryTrait>::T>) -> bool
+ where
+ <Self as GeometryTrait>::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
+ <Self as GeometryTrait>::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<<Self as GeometryTrait>::T>
+ where
+ <Self as GeometryTrait>::T: CoordFloat,
+ {
+ let two = <Self as GeometryTrait>::T::one() + <Self as
GeometryTrait>::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>
+ = <Self as RectTrait>::CoordType<'__l_inner>
+ where
+ Self: '__l_inner;
+
+ fn min_ext(&self) -> Self::CoordTypeExt<'_> {
+ <Self as RectTrait>::min(self)
+ }
+
+ fn max_ext(&self) -> Self::CoordTypeExt<'_> {
+ <Self as RectTrait>::max(self)
+ }
+ };
+}
+
+impl<T> RectTraitExt for Rect<T>
+where
+ T: CoordNum,
+{
+ forward_rect_trait_ext_funcs!();
+
+ fn min_coord(&self) -> Coord<T> {
+ Rect::min(*self)
+ }
+
+ fn max_coord(&self) -> Coord<T> {
+ Rect::max(*self)
+ }
+
+ fn geo_rect(&self) -> Rect<T> {
+ *self
+ }
+
+ fn to_lines(&self) -> [Line<<Self as GeometryTrait>::T>; 4] {
+ self.to_lines()
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for Rect<T> {
+ type Tag = RectTag;
+}
+
+impl<T> RectTraitExt for &Rect<T>
+where
+ T: CoordNum,
+{
+ forward_rect_trait_ext_funcs!();
+
+ fn min_coord(&self) -> Coord<T> {
+ Rect::min(**self)
+ }
+
+ fn max_coord(&self) -> Coord<T> {
+ Rect::max(**self)
+ }
+
+ fn geo_rect(&self) -> Rect<T> {
+ **self
+ }
+
+ fn to_polygon(&self) -> Polygon<<Self as GeometryTrait>::T>
+ where
+ <Self as GeometryTrait>::T: Clone,
+ {
+ (*self).to_polygon()
+ }
+
+ fn to_lines(&self) -> [Line<<Self as GeometryTrait>::T>; 4] {
+ (*self).to_lines()
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &Rect<T> {
+ type Tag = RectTag;
+}
+
+impl<T> RectTraitExt for UnimplementedRect<T>
+where
+ T: CoordNum,
+{
+ forward_rect_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for UnimplementedRect<T> {
+ 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 0000000..08fb868
--- /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<Tag =
TriangleTag>
+where
+ <Self as GeometryTrait>::T: CoordNum,
+{
+ /// Extension-aware coordinate type returned from triangle accessors.
+ type CoordTypeExt<'a>: 'a + CoordTraitExt<T = <Self as GeometryTrait>::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<<Self as GeometryTrait>::T> {
+ self.first_ext().geo_coord()
+ }
+
+ #[inline]
+ /// Returns the second vertex as a `geo-types::Coord`.
+ fn second_coord(&self) -> Coord<<Self as GeometryTrait>::T> {
+ self.second_ext().geo_coord()
+ }
+
+ #[inline]
+ /// Returns the third vertex as a `geo-types::Coord`.
+ fn third_coord(&self) -> Coord<<Self as GeometryTrait>::T> {
+ self.third_ext().geo_coord()
+ }
+
+ #[inline]
+ /// Returns the triangle vertices as an array of coordinates.
+ fn to_array(&self) -> [Coord<<Self as GeometryTrait>::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<<Self as GeometryTrait>::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<<Self as GeometryTrait>::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<Item = Coord<<Self as
GeometryTrait>::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>
+ = <Self as TriangleTrait>::CoordType<'__l_inner>
+ where
+ Self: '__l_inner;
+
+ #[inline]
+ fn first_ext(&self) -> Self::CoordTypeExt<'_> {
+ <Self as TriangleTrait>::first(self)
+ }
+
+ #[inline]
+ fn second_ext(&self) -> Self::CoordTypeExt<'_> {
+ <Self as TriangleTrait>::second(self)
+ }
+
+ #[inline]
+ fn third_ext(&self) -> Self::CoordTypeExt<'_> {
+ <Self as TriangleTrait>::third(self)
+ }
+
+ #[inline]
+ fn coords_ext(&self) -> [Self::CoordTypeExt<'_>; 3] {
+ [self.first_ext(), self.second_ext(), self.third_ext()]
+ }
+ };
+}
+
+impl<T> TriangleTraitExt for Triangle<T>
+where
+ T: CoordNum,
+{
+ forward_triangle_trait_ext_funcs!();
+
+ fn first_coord(&self) -> Coord<<Self as GeometryTrait>::T> {
+ self.0
+ }
+
+ fn second_coord(&self) -> Coord<<Self as GeometryTrait>::T> {
+ self.1
+ }
+
+ fn third_coord(&self) -> Coord<<Self as GeometryTrait>::T> {
+ self.2
+ }
+
+ fn to_array(&self) -> [Coord<<Self as GeometryTrait>::T>; 3] {
+ self.to_array()
+ }
+
+ fn to_lines(&self) -> [Line<<Self as GeometryTrait>::T>; 3] {
+ self.to_lines()
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for Triangle<T> {
+ type Tag = TriangleTag;
+}
+
+impl<T> TriangleTraitExt for &Triangle<T>
+where
+ T: CoordNum,
+{
+ forward_triangle_trait_ext_funcs!();
+
+ fn first_coord(&self) -> Coord<<Self as GeometryTrait>::T> {
+ self.0
+ }
+
+ fn second_coord(&self) -> Coord<<Self as GeometryTrait>::T> {
+ self.1
+ }
+
+ fn third_coord(&self) -> Coord<<Self as GeometryTrait>::T> {
+ self.2
+ }
+
+ fn to_array(&self) -> [Coord<<Self as GeometryTrait>::T>; 3] {
+ (*self).to_array()
+ }
+
+ fn to_lines(&self) -> [Line<<Self as GeometryTrait>::T>; 3] {
+ (*self).to_lines()
+ }
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for &Triangle<T> {
+ type Tag = TriangleTag;
+}
+
+impl<T> TriangleTraitExt for UnimplementedTriangle<T>
+where
+ T: CoordNum,
+{
+ forward_triangle_trait_ext_funcs!();
+}
+
+impl<T: CoordNum> GeoTraitExtWithTypeTag for UnimplementedTriangle<T> {
+ 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 0000000..f6b1fdf
--- /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 0000000..4c84dbe
--- /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<f64> {
+ 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<Item = Line<f64>> + '_ {
+ 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<Item = GeoCoord<f64>> {
+ 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<Item = Line<f64>> + '_ {
+ (*self).lines()
+ }
+
+ #[inline(always)]
+ fn coord_iter(&self) -> impl Iterator<Item = GeoCoord<f64>> {
+ (*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<Item = Line<f64>> + '_ {
+ 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<Item = GeoCoord<f64>> {
+ 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<Item = Line<f64>> + '_ {
+ (*self).lines()
+ }
+
+ #[inline(always)]
+ fn coord_iter(&self) -> impl Iterator<Item = GeoCoord<f64>> {
+ (*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<Self::InnerGeometryRef<'_>> {
+ 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<Item =
Self::InnerGeometryRef<'_>> {
+ 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::InnerGeometryRef<'_>> {
+ (*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<Item =
Self::InnerGeometryRef<'_>> {
+ (*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<B>,
+}
+
+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<B: ByteOrder> Iterator for CoordIter<'_, B> {
+ type Item = GeoCoord<f64>;
+
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ 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<usize>) {
+ (self.remaining, Some(self.remaining))
+ }
+}
+
+impl<B: ByteOrder> 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<GeoCoord<f64>>,
+}
+
+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<B: ByteOrder> Iterator for LineIter<'_, B> {
+ type Item = Line<f64>;
+
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ 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<usize>) {
+ let (min, max) = self.coord_iter.size_hint();
+ (min.saturating_sub(1), max.map(|m| m.saturating_sub(1)))
+ }
+}
+
+impl<B: ByteOrder> 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<f64>;
+
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ // 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<usize>) {
+ 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<f64>;
+
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ match self {
+ EndianLineIter::LE(iter) => iter.next(),
+ EndianLineIter::BE(iter) => iter.next(),
+ }
+ }
+
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ 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 0000000..775d68c
--- /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<u8> {
+ 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<u8> {
+ let geometry = Wkt::<f64>::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);
+}