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

Reply via email to