This is an automated email from the ASF dual-hosted git repository.
jiayu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sedona.git
The following commit(s) were added to refs/heads/master by this push:
new 59944645d0 [GH-2041] Implement boundary, centroid, envelope (#2042)
59944645d0 is described below
commit 59944645d00403841733c461bb198a9d1a392b33
Author: Peter Nguyen <[email protected]>
AuthorDate: Fri Jul 11 23:22:58 2025 -0700
[GH-2041] Implement boundary, centroid, envelope (#2042)
* Implement boundary
* Implement centroid
* Implement envelope
* Skip boundary test for geom collection for shapely < 2.0.0
* Fix doc strings to use sedona instead of geoseries
* Support na values in to_geopandas
---
python/sedona/geopandas/geoseries.py | 130 +++++++++++++++++++--
python/tests/geopandas/test_geoseries.py | 55 ++++++++-
.../tests/geopandas/test_match_geopandas_series.py | 22 +++-
3 files changed, 190 insertions(+), 17 deletions(-)
diff --git a/python/sedona/geopandas/geoseries.py
b/python/sedona/geopandas/geoseries.py
index 97e35038b3..a4a913443e 100644
--- a/python/sedona/geopandas/geoseries.py
+++ b/python/sedona/geopandas/geoseries.py
@@ -537,7 +537,9 @@ class GeoSeries(GeoFrame, pspd.Series):
try:
return gpd.GeoSeries(
pd_series.map(
- lambda wkb: shapely.wkb.loads(bytes(wkb)) if wkb else None
+ lambda wkb: (
+ shapely.wkb.loads(bytes(wkb)) if not pd.isna(wkb) else
None
+ )
),
crs=self.crs,
)
@@ -1029,14 +1031,85 @@ class GeoSeries(GeoFrame, pspd.Series):
)
@property
- def boundary(self):
- # Implementation of the abstract method
- raise NotImplementedError("This method is not implemented yet.")
+ def boundary(self) -> "GeoSeries":
+ """Returns a ``GeoSeries`` of lower dimensional objects representing
+ each geometry's set-theoretic `boundary`.
+
+ Examples
+ --------
+
+ >>> from sedona.geopandas import GeoSeries
+ >>> from shapely.geometry import Polygon, LineString, Point
+ >>> s = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (1, 1), (0, 1)]),
+ ... LineString([(0, 0), (1, 1), (1, 0)]),
+ ... Point(0, 0),
+ ... ]
+ ... )
+ >>> s
+ 0 POLYGON ((0 0, 1 1, 0 1, 0 0))
+ 1 LINESTRING (0 0, 1 1, 1 0)
+ 2 POINT (0 0)
+ dtype: geometry
+
+ >>> s.boundary
+ 0 LINESTRING (0 0, 1 1, 0 1, 0 0)
+ 1 MULTIPOINT ((0 0), (1 0))
+ 2 GEOMETRYCOLLECTION EMPTY
+ dtype: geometry
+
+ See also
+ --------
+ GeoSeries.exterior : outer boundary (without interior rings)
+
+ """
+ col = self.get_first_geometry_column()
+ # Geopandas and shapely return NULL for GeometryCollections, so we
handle it separately
+ #
https://shapely.readthedocs.io/en/stable/reference/shapely.boundary.html
+ select = f"""
+ CASE
+ WHEN GeometryType(`{col}`) IN ('GEOMETRYCOLLECTION') THEN NULL
+ ELSE ST_Boundary(`{col}`)
+ END"""
+ return self._query_geometry_column(select, col, rename="boundary")
@property
- def centroid(self):
- # Implementation of the abstract method
- raise NotImplementedError("This method is not implemented yet.")
+ def centroid(self) -> "GeoSeries":
+ """Returns a ``GeoSeries`` of points representing the centroid of each
+ geometry.
+
+ Note that centroid does not have to be on or within original geometry.
+
+ Examples
+ --------
+
+ >>> from sedona.geopandas import GeoSeries
+ >>> from shapely.geometry import Polygon, LineString, Point
+ >>> s = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (1, 1), (0, 1)]),
+ ... LineString([(0, 0), (1, 1), (1, 0)]),
+ ... Point(0, 0),
+ ... ]
+ ... )
+ >>> s
+ 0 POLYGON ((0 0, 1 1, 0 1, 0 0))
+ 1 LINESTRING (0 0, 1 1, 1 0)
+ 2 POINT (0 0)
+ dtype: geometry
+
+ >>> s.centroid
+ 0 POINT (0.33333 0.66667)
+ 1 POINT (0.70711 0.5)
+ 2 POINT (0 0)
+ dtype: geometry
+
+ See also
+ --------
+ GeoSeries.representative_point : point guaranteed to be within each
geometry
+ """
+ return self._process_geometry_column("ST_Centroid", rename="centroid")
def concave_hull(self, ratio=0.0, allow_holes=False):
# Implementation of the abstract method
@@ -1056,9 +1129,46 @@ class GeoSeries(GeoFrame, pspd.Series):
raise NotImplementedError("This method is not implemented yet.")
@property
- def envelope(self):
- # Implementation of the abstract method
- raise NotImplementedError("This method is not implemented yet.")
+ def envelope(self) -> "GeoSeries":
+ """Returns a ``GeoSeries`` of geometries representing the envelope of
+ each geometry.
+
+ The envelope of a geometry is the bounding rectangle. That is, the
+ point or smallest rectangular polygon (with sides parallel to the
+ coordinate axes) that contains the geometry.
+
+ Examples
+ --------
+
+ >>> from sedona.geopandas import GeoSeries
+ >>> from shapely.geometry import Polygon, LineString, Point, MultiPoint
+ >>> s = GeoSeries(
+ ... [
+ ... Polygon([(0, 0), (1, 1), (0, 1)]),
+ ... LineString([(0, 0), (1, 1), (1, 0)]),
+ ... MultiPoint([(0, 0), (1, 1)]),
+ ... Point(0, 0),
+ ... ]
+ ... )
+ >>> s
+ 0 POLYGON ((0 0, 1 1, 0 1, 0 0))
+ 1 LINESTRING (0 0, 1 1, 1 0)
+ 2 MULTIPOINT ((0 0), (1 1))
+ 3 POINT (0 0)
+ dtype: geometry
+
+ >>> s.envelope
+ 0 POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))
+ 1 POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))
+ 2 POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))
+ 3 POINT (0 0)
+ dtype: geometry
+
+ See also
+ --------
+ GeoSeries.convex_hull : convex hull geometry
+ """
+ return self._process_geometry_column("ST_Envelope", rename="envelope")
def minimum_rotated_rectangle(self):
# Implementation of the abstract method
diff --git a/python/tests/geopandas/test_geoseries.py
b/python/tests/geopandas/test_geoseries.py
index 298b7b13ac..dfce6a268a 100644
--- a/python/tests/geopandas/test_geoseries.py
+++ b/python/tests/geopandas/test_geoseries.py
@@ -492,10 +492,42 @@ class TestGeoSeries(TestBase):
self.check_sgpd_equals_gpd(result, expected)
def test_boundary(self):
- pass
+ s = sgpd.GeoSeries(
+ [
+ Polygon([(0, 0), (1, 1), (0, 1)]),
+ LineString([(0, 0), (1, 1), (1, 0)]),
+ Point(0, 0),
+ GeometryCollection([Point(0, 0)]),
+ ]
+ )
+ result = s.boundary
+ expected = gpd.GeoSeries(
+ [
+ LineString([(0, 0), (1, 1), (0, 1), (0, 0)]),
+ MultiPoint([(0, 0), (1, 0)]),
+ GeometryCollection([]),
+ None,
+ ]
+ )
+ self.check_sgpd_equals_gpd(result, expected)
def test_centroid(self):
- pass
+ s = sgpd.GeoSeries(
+ [
+ Polygon([(0, 0), (1, 1), (0, 1)]),
+ LineString([(0, 0), (1, 1), (1, 0)]),
+ Point(0, 0),
+ ]
+ )
+ result = s.centroid
+ expected = gpd.GeoSeries(
+ [
+ Point(0.3333333333333333, 0.6666666666666666),
+ Point(0.7071067811865476, 0.5),
+ Point(0, 0),
+ ]
+ )
+ self.check_sgpd_equals_gpd(result, expected)
def test_concave_hull(self):
pass
@@ -510,7 +542,24 @@ class TestGeoSeries(TestBase):
pass
def test_envelope(self):
- pass
+ s = sgpd.GeoSeries(
+ [
+ Polygon([(0, 0), (1, 1), (0, 1)]),
+ LineString([(0, 0), (1, 1), (1, 0)]),
+ MultiPoint([(0, 0), (1, 1)]),
+ Point(0, 0),
+ ]
+ )
+ result = s.envelope
+ expected = gpd.GeoSeries(
+ [
+ Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]),
+ Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]),
+ Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]),
+ Point(0, 0),
+ ]
+ )
+ self.check_sgpd_equals_gpd(result, expected)
def test_minimum_rotated_rectangle(self):
pass
diff --git a/python/tests/geopandas/test_match_geopandas_series.py
b/python/tests/geopandas/test_match_geopandas_series.py
index 389597c651..aa534be9c9 100644
--- a/python/tests/geopandas/test_match_geopandas_series.py
+++ b/python/tests/geopandas/test_match_geopandas_series.py
@@ -23,7 +23,7 @@ import geopandas as gpd
import pyspark.pandas as ps
import pyspark
from pandas.testing import assert_series_equal
-
+import shapely
from shapely.geometry import (
Point,
Polygon,
@@ -479,10 +479,21 @@ class TestMatchGeopandasSeries(TestBase):
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
def test_boundary(self):
- pass
+ for _, geom in self.geoms:
+ # Shapely < 2.0 doesn't support GeometryCollection for boundary
operation
+ if shapely.__version__ < "2.0.0" and isinstance(
+ geom[0], GeometryCollection
+ ):
+ continue
+ sgpd_result = GeoSeries(geom).boundary
+ gpd_result = gpd.GeoSeries(geom).boundary
+ self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
def test_centroid(self):
- pass
+ for _, geom in self.geoms:
+ sgpd_result = GeoSeries(geom).centroid
+ gpd_result = gpd.GeoSeries(geom).centroid
+ self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
def test_concave_hull(self):
pass
@@ -497,7 +508,10 @@ class TestMatchGeopandasSeries(TestBase):
pass
def test_envelope(self):
- pass
+ for _, geom in self.geoms:
+ sgpd_result = GeoSeries(geom).envelope
+ gpd_result = gpd.GeoSeries(geom).envelope
+ self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
def test_minimum_rotated_rectangle(self):
pass