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 88a49e1e2a [GH-2709] Implement GeoSeries: line_merge, 
count_coordinates, count_geometries, count_interior_rings, concave_hull, 
minimum_rotated_rectangle, exterior, extract_unique_points, 
remove_repeated_points (#2710)
88a49e1e2a is described below

commit 88a49e1e2a8ccbafd4a1d6e1c9c5265d58f6111f
Author: Jia Yu <[email protected]>
AuthorDate: Wed Mar 11 02:19:47 2026 -0700

    [GH-2709] Implement GeoSeries: line_merge, count_coordinates, 
count_geometries, count_interior_rings, concave_hull, 
minimum_rotated_rectangle, exterior, extract_unique_points, 
remove_repeated_points (#2710)
---
 python/sedona/spark/geopandas/base.py              | 279 +++++++++++++++++++--
 python/sedona/spark/geopandas/geoseries.py         |  81 +++---
 python/tests/geopandas/test_geoseries.py           | 189 +++++++++++++-
 .../tests/geopandas/test_match_geopandas_series.py |  61 ++++-
 .../streaming/spark/test_constructor_functions.py  |   6 +-
 5 files changed, 547 insertions(+), 69 deletions(-)

diff --git a/python/sedona/spark/geopandas/base.py 
b/python/sedona/spark/geopandas/base.py
index 0308b4d9be..168a7738b0 100644
--- a/python/sedona/spark/geopandas/base.py
+++ b/python/sedona/spark/geopandas/base.py
@@ -313,14 +313,94 @@ class GeoFrame(metaclass=ABCMeta):
         """
         return _delegate_to_geometry_column("is_empty", self)
 
-    # def count_coordinates(self):
-    #     raise NotImplementedError("This method is not implemented yet.")
+    def count_coordinates(self):
+        """Return a ``Series`` of ``dtype('int')`` with the number of
+        coordinate tuples in each geometry.
 
-    # def count_geometries(self):
-    #     raise NotImplementedError("This method is not implemented yet.")
+        Returns
+        -------
+        Series (int)
 
-    # def count_interior_rings(self):
-    #     raise NotImplementedError("This method is not implemented yet.")
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import Point, LineString, Polygon
+        >>> s = GeoSeries(
+        ...     [
+        ...         Point(0, 0),
+        ...         LineString([(0, 0), (1, 1), (2, 2)]),
+        ...         Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+        ...     ]
+        ... )
+        >>> s.count_coordinates()
+        0    1
+        1    3
+        2    5
+        dtype: int32
+
+        """
+        return _delegate_to_geometry_column("count_coordinates", self)
+
+    def count_geometries(self):
+        """Return a ``Series`` of ``dtype('int')`` with the number of
+        geometries in each multi-geometry or geometry collection.
+
+        For non-multi geometries, returns 1.
+
+        Returns
+        -------
+        Series (int)
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import Point, MultiPoint, MultiLineString
+        >>> s = GeoSeries(
+        ...     [
+        ...         Point(0, 0),
+        ...         MultiPoint([(0, 0), (1, 1)]),
+        ...         MultiLineString([[(0, 0), (1, 1)], [(2, 2), (3, 3)]]),
+        ...     ]
+        ... )
+        >>> s.count_geometries()
+        0    1
+        1    2
+        2    2
+        dtype: int32
+
+        """
+        return _delegate_to_geometry_column("count_geometries", self)
+
+    def count_interior_rings(self):
+        """Return a ``Series`` of ``dtype('int')`` with the number of
+        interior rings (holes) in each polygon geometry.
+
+        Returns 0 for polygons without holes and for non-polygon geometries.
+
+        Returns
+        -------
+        Series (int)
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import Point, Polygon
+        >>> s = GeoSeries(
+        ...     [
+        ...         Polygon([(0, 0), (10, 0), (10, 10), (0, 10)],
+        ...                 [[(1, 1), (2, 1), (2, 2), (1, 2)]]),
+        ...         Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+        ...         Point(0, 0),
+        ...     ]
+        ... )
+        >>> s.count_interior_rings()
+        0    1
+        1    0
+        2    0
+        dtype: int32
+
+        """
+        return _delegate_to_geometry_column("count_interior_rings", self)
 
     @property
     def is_simple(self):
@@ -609,8 +689,41 @@ class GeoFrame(metaclass=ABCMeta):
         """
         return _delegate_to_geometry_column("centroid", self)
 
-    # def concave_hull(self, ratio=0.0, allow_holes=False):
-    #     raise NotImplementedError("This method is not implemented yet.")
+    def concave_hull(self, ratio=0.0, allow_holes=False):
+        """Return the concave hull of each geometry.
+
+        The concave hull of a geometry is a possibly concave geometry that
+        encloses the input geometry.
+
+        Parameters
+        ----------
+        ratio : float, default 0.0
+            A value between 0 and 1 controlling the concaveness of the hull.
+            1 produces the convex hull; 0 produces a hull with maximum
+            concaveness.
+        allow_holes : bool, default False
+            If True, the concave hull may contain holes.
+
+        Returns
+        -------
+        GeoSeries
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import MultiPoint
+        >>> s = GeoSeries(
+        ...     [MultiPoint([(0, 0), (1, 0), (0.5, 0.5), (1, 1), (0, 1)])]
+        ... )
+        >>> s.concave_hull(ratio=0.3)
+        0    POLYGON ((0 0, 0 1, 0.5 0.5, 1 1, 1 0, 0 0))
+        dtype: geometry
+
+        See Also
+        --------
+        GeoSeries.convex_hull : convex hull geometry
+        """
+        return _delegate_to_geometry_column("concave_hull", self, ratio, 
allow_holes)
 
     @property
     def convex_hull(self):
@@ -707,15 +820,87 @@ class GeoFrame(metaclass=ABCMeta):
         """
         return _delegate_to_geometry_column("envelope", self)
 
-    # def minimum_rotated_rectangle(self):
-    #     raise NotImplementedError("This method is not implemented yet.")
+    def minimum_rotated_rectangle(self):
+        """Return the minimum rotated rectangle (oriented envelope) that
+        encloses each geometry.
 
-    # @property
-    # def exterior(self):
-    #     raise NotImplementedError("This method is not implemented yet.")
+        Unlike ``envelope``, the rectangle may be rotated to better fit the
+        geometry.
 
-    # def extract_unique_points(self):
-    #     raise NotImplementedError("This method is not implemented yet.")
+        Returns
+        -------
+        GeoSeries
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import MultiPoint
+        >>> s = GeoSeries(
+        ...     [MultiPoint([(0, 0), (1, 0), (0.5, 1)])]
+        ... )
+        >>> s.minimum_rotated_rectangle()
+        0    POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))
+        dtype: geometry
+
+        See Also
+        --------
+        GeoSeries.envelope : axis-aligned bounding rectangle
+        GeoSeries.convex_hull : convex hull geometry
+        """
+        return _delegate_to_geometry_column("minimum_rotated_rectangle", self)
+
+    @property
+    def exterior(self):
+        """Return the outer boundary of each polygon geometry.
+
+        Returns a ``GeoSeries`` of LineStrings representing the exterior ring
+        of each polygon. For non-polygon geometries, returns ``None``.
+
+        .. note::
+           Sedona's ``ST_ExteriorRing`` returns a ``LINESTRING`` rather than
+           a ``LINEARRING``.  The coordinates are identical to those of the
+           exterior ring but the geometry type differs from geopandas, which
+           returns a ``LINEARRING``.
+
+        Returns
+        -------
+        GeoSeries
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import Polygon
+        >>> s = GeoSeries(
+        ...     [Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])]
+        ... )
+        >>> s.exterior
+        0    LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)
+        dtype: geometry
+
+        """
+        return _delegate_to_geometry_column("exterior", self)
+
+    def extract_unique_points(self):
+        """Return a ``GeoSeries`` of MultiPoints representing all distinct
+        vertices of each geometry.
+
+        Returns
+        -------
+        GeoSeries
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import LineString
+        >>> s = GeoSeries(
+        ...     [LineString([(0, 0), (1, 1), (0, 0)])]
+        ... )
+        >>> s.extract_unique_points()
+        0    MULTIPOINT ((0 0), (1 1))
+        dtype: geometry
+
+        """
+        return _delegate_to_geometry_column("extract_unique_points", self)
 
     # def offset_curve(self, distance, quad_segs=8, join_style="round", 
mitre_limit=5.0):
     #     raise NotImplementedError("This method is not implemented yet.")
@@ -724,8 +909,33 @@ class GeoFrame(metaclass=ABCMeta):
     # def interiors(self):
     #     raise NotImplementedError("This method is not implemented yet.")
 
-    # def remove_repeated_points(self, tolerance=0.0):
-    #     raise NotImplementedError("This method is not implemented yet.")
+    def remove_repeated_points(self, tolerance=0.0):
+        """Return a ``GeoSeries`` with duplicate points removed.
+
+        Parameters
+        ----------
+        tolerance : float, default 0.0
+            Remove vertices that are within ``tolerance`` distance of one
+            another. A tolerance of 0.0 removes only exactly repeated
+            coordinates.
+
+        Returns
+        -------
+        GeoSeries
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import LineString
+        >>> s = GeoSeries(
+        ...     [LineString([(0, 0), (0, 0), (1, 1), (1, 1), (2, 2)])]
+        ... )
+        >>> s.remove_repeated_points()
+        0    LINESTRING (0 0, 1 1, 2 2)
+        dtype: geometry
+
+        """
+        return _delegate_to_geometry_column("remove_repeated_points", self, 
tolerance)
 
     # def set_precision(self, grid_size, mode="valid_output"):
     #     raise NotImplementedError("This method is not implemented yet.")
@@ -1093,8 +1303,39 @@ class GeoFrame(metaclass=ABCMeta):
         """
         return _delegate_to_geometry_column("force_3d", self, z)
 
-    # def line_merge(self, directed=False):
-    #     raise NotImplementedError("This method is not implemented yet.")
+    def line_merge(self, directed=False):
+        """Return merged LineStrings.
+
+        Returns a ``GeoSeries`` of (Multi)LineStrings, where connected
+        LineStrings are merged together into single LineStrings.
+
+        Parameters
+        ----------
+        directed : bool, default False
+            Only ``directed=False`` is supported. Passing ``directed=True``
+            will raise ``NotImplementedError``.
+
+        Returns
+        -------
+        GeoSeries
+
+        Examples
+        --------
+        >>> from sedona.spark.geopandas import GeoSeries
+        >>> from shapely.geometry import MultiLineString
+        >>> s = GeoSeries(
+        ...     [
+        ...         MultiLineString([[(0, 0), (1, 1)], [(1, 1), (2, 2)]]),
+        ...         MultiLineString([[(0, 0), (1, 1)], [(2, 2), (3, 3)]]),
+        ...     ]
+        ... )
+        >>> s.line_merge()
+        0                     LINESTRING (0 0, 1 1, 2 2)
+        1    MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))
+        dtype: geometry
+
+        """
+        return _delegate_to_geometry_column("line_merge", self, directed)
 
     # @property
     # def unary_union(self):
diff --git a/python/sedona/spark/geopandas/geoseries.py 
b/python/sedona/spark/geopandas/geoseries.py
index 67221b080b..0319a41be4 100644
--- a/python/sedona/spark/geopandas/geoseries.py
+++ b/python/sedona/spark/geopandas/geoseries.py
@@ -792,30 +792,27 @@ class GeoSeries(GeoFrame, pspd.Series):
         return _to_bool(result)
 
     def count_coordinates(self):
-        # Implementation of the abstract method.
-        raise NotImplementedError(
-            _not_implemented_error(
-                "count_coordinates",
-                "Counts the number of coordinate tuples in each geometry.",
-            )
+        spark_expr = stf.ST_NPoints(self.spark.column)
+        return self._query_geometry_column(
+            spark_expr,
+            returns_geom=False,
         )
 
     def count_geometries(self):
-        # Implementation of the abstract method.
-        raise NotImplementedError(
-            _not_implemented_error(
-                "count_geometries",
-                "Counts the number of geometries in each multi-geometry or 
collection.",
-            )
+        spark_expr = stf.ST_NumGeometries(self.spark.column)
+        return self._query_geometry_column(
+            spark_expr,
+            returns_geom=False,
         )
 
     def count_interior_rings(self):
-        # Implementation of the abstract method.
-        raise NotImplementedError(
-            _not_implemented_error(
-                "count_interior_rings",
-                "Counts the number of interior rings (holes) in each polygon.",
-            )
+        # Sedona's ST_NumInteriorRings returns NULL for non-polygon geometries
+        # (including MultiPolygon). GeoPandas semantics require 0 for
+        # non-polygon and empty geometries, so we wrap with coalesce.
+        spark_expr = F.coalesce(stf.ST_NumInteriorRings(self.spark.column), 
F.lit(0))
+        return self._query_geometry_column(
+            spark_expr,
+            returns_geom=False,
         )
 
     def dwithin(self, other, distance, align=None):
@@ -972,8 +969,11 @@ class GeoSeries(GeoFrame, pspd.Series):
         )
 
     def concave_hull(self, ratio=0.0, allow_holes=False):
-        # Implementation of the abstract method.
-        raise NotImplementedError("This method is not implemented yet.")
+        spark_expr = stf.ST_ConcaveHull(self.spark.column, ratio, allow_holes)
+        return self._query_geometry_column(
+            spark_expr,
+            returns_geom=True,
+        )
 
     @property
     def convex_hull(self) -> "GeoSeries":
@@ -1000,17 +1000,26 @@ class GeoSeries(GeoFrame, pspd.Series):
         )
 
     def minimum_rotated_rectangle(self):
-        # Implementation of the abstract method.
-        raise NotImplementedError("This method is not implemented yet.")
+        spark_expr = stf.ST_OrientedEnvelope(self.spark.column)
+        return self._query_geometry_column(
+            spark_expr,
+            returns_geom=True,
+        )
 
     @property
     def exterior(self):
-        # Implementation of the abstract method.
-        raise NotImplementedError("This method is not implemented yet.")
+        spark_expr = stf.ST_ExteriorRing(self.spark.column)
+        return self._query_geometry_column(
+            spark_expr,
+            returns_geom=True,
+        )
 
     def extract_unique_points(self):
-        # Implementation of the abstract method.
-        raise NotImplementedError("This method is not implemented yet.")
+        spark_expr = stf.ST_Points(self.spark.column)
+        return self._query_geometry_column(
+            spark_expr,
+            returns_geom=True,
+        )
 
     def offset_curve(self, distance, quad_segs=8, join_style="round", 
mitre_limit=5.0):
         # Implementation of the abstract method.
@@ -1022,8 +1031,12 @@ class GeoSeries(GeoFrame, pspd.Series):
         raise NotImplementedError("This method is not implemented yet.")
 
     def remove_repeated_points(self, tolerance=0.0):
-        # Implementation of the abstract method.
-        raise NotImplementedError("This method is not implemented yet.")
+        args = (self.spark.column, tolerance) if tolerance else 
(self.spark.column,)
+        spark_expr = stf.ST_RemoveRepeatedPoints(*args)
+        return self._query_geometry_column(
+            spark_expr,
+            returns_geom=True,
+        )
 
     def set_precision(self, grid_size, mode="valid_output"):
         # Implementation of the abstract method.
@@ -1114,8 +1127,16 @@ class GeoSeries(GeoFrame, pspd.Series):
         )
 
     def line_merge(self, directed=False):
-        # Implementation of the abstract method.
-        raise NotImplementedError("This method is not implemented yet.")
+        if directed:
+            raise NotImplementedError(
+                "Sedona does not support directed line_merge; "
+                "the 'directed' argument must be False."
+            )
+        spark_expr = stf.ST_LineMerge(self.spark.column)
+        return self._query_geometry_column(
+            spark_expr,
+            returns_geom=True,
+        )
 
     # 
============================================================================
     # GEOMETRIC OPERATIONS
diff --git a/python/tests/geopandas/test_geoseries.py 
b/python/tests/geopandas/test_geoseries.py
index 2e9c559a9f..bcbcc979c8 100644
--- a/python/tests/geopandas/test_geoseries.py
+++ b/python/tests/geopandas/test_geoseries.py
@@ -748,13 +748,54 @@ e": "Feature", "properties": {}, "geometry": {"type": 
"Point", "coordinates": [3
         self.check_pd_series_equal(df_result, expected)
 
     def test_count_coordinates(self):
-        pass
+        s = GeoSeries(
+            [
+                Point(0, 0),
+                LineString([(0, 0), (1, 1), (2, 2)]),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+        result = s.count_coordinates()
+        expected = pd.Series([1, 3, 5], dtype="int32")
+        self.check_pd_series_equal(result, expected)
+
+        # Check that GeoDataFrame works too
+        df_result = s.to_geoframe().count_coordinates()
+        self.check_pd_series_equal(df_result, expected)
 
     def test_count_geometries(self):
-        pass
+        s = GeoSeries(
+            [
+                Point(0, 0),
+                MultiPoint([(0, 0), (1, 1)]),
+                MultiLineString([[(0, 0), (1, 1)], [(2, 2), (3, 3)]]),
+            ]
+        )
+        result = s.count_geometries()
+        expected = pd.Series([1, 2, 2], dtype="int32")
+        self.check_pd_series_equal(result, expected)
+
+        # Check that GeoDataFrame works too
+        df_result = s.to_geoframe().count_geometries()
+        self.check_pd_series_equal(df_result, expected)
 
     def test_count_interior_rings(self):
-        pass
+        s = GeoSeries(
+            [
+                Polygon(
+                    [(0, 0), (10, 0), (10, 10), (0, 10)],
+                    [[(1, 1), (2, 1), (2, 2), (1, 2)]],
+                ),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+            ]
+        )
+        result = s.count_interior_rings()
+        expected = pd.Series([1, 0], dtype="int32")
+        self.check_pd_series_equal(result, expected)
+
+        # Check that GeoDataFrame works too
+        df_result = s.to_geoframe().count_interior_rings()
+        self.check_pd_series_equal(df_result, expected)
 
     def test_dwithin(self):
         s = GeoSeries(
@@ -1238,7 +1279,28 @@ e": "Feature", "properties": {}, "geometry": {"type": 
"Point", "coordinates": [3
         self.check_sgpd_equals_gpd(result, expected)
 
     def test_concave_hull(self):
-        pass
+        s = GeoSeries(
+            [
+                MultiPoint([(0, 0), (1, 0), (0.5, 0.5), (1, 1), (0, 1)]),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+                Point(0, 0),
+                None,
+            ]
+        )
+        expected = gpd.GeoSeries(
+            [
+                MultiPoint([(0, 0), (1, 0), (0.5, 0.5), (1, 1), (0, 1)]),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+                Point(0, 0),
+                None,
+            ]
+        ).concave_hull(ratio=0.5)
+        result = s.concave_hull(ratio=0.5)
+        self.check_sgpd_equals_gpd(result, expected)
+
+        # Check that GeoDataFrame works too
+        df_result = s.to_geoframe().concave_hull(ratio=0.5)
+        self.check_sgpd_equals_gpd(df_result, expected)
 
     def test_convex_hull(self):
         s = GeoSeries(
@@ -1297,13 +1359,80 @@ e": "Feature", "properties": {}, "geometry": {"type": 
"Point", "coordinates": [3
         self.check_sgpd_equals_gpd(df_result, expected)
 
     def test_minimum_rotated_rectangle(self):
-        pass
+        s = GeoSeries(
+            [
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+                LineString([(0, 0), (2, 1)]),
+                Point(0, 0),
+                None,
+            ]
+        )
+        expected = gpd.GeoSeries(
+            [
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+                LineString([(0, 0), (2, 1)]),
+                Point(0, 0),
+                None,
+            ]
+        ).minimum_rotated_rectangle()
+        result = s.minimum_rotated_rectangle()
+        self.check_sgpd_equals_gpd(result, expected)
+
+        # Check that GeoDataFrame works too
+        df_result = s.to_geoframe().minimum_rotated_rectangle()
+        self.check_sgpd_equals_gpd(df_result, expected)
 
     def test_exterior(self):
-        pass
+        s = GeoSeries(
+            [
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+                Polygon(
+                    [(0, 0), (10, 0), (10, 10), (0, 10)],
+                    [[(1, 1), (2, 1), (2, 2), (1, 2)]],
+                ),
+                None,
+            ]
+        )
+        expected = gpd.GeoSeries(
+            [
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+                Polygon(
+                    [(0, 0), (10, 0), (10, 10), (0, 10)],
+                    [[(1, 1), (2, 1), (2, 2), (1, 2)]],
+                ),
+                None,
+            ]
+        ).exterior
+        result = s.exterior
+        self.check_sgpd_equals_gpd(result, expected)
+
+        # Check that GeoDataFrame works too
+        df_result = s.to_geoframe().exterior
+        self.check_sgpd_equals_gpd(df_result, expected)
 
     def test_extract_unique_points(self):
-        pass
+        s = GeoSeries(
+            [
+                LineString([(0, 0), (1, 1), (0, 0)]),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+                Point(0, 0),
+                None,
+            ]
+        )
+        expected = gpd.GeoSeries(
+            [
+                LineString([(0, 0), (1, 1), (0, 0)]),
+                Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+                Point(0, 0),
+                None,
+            ]
+        ).extract_unique_points()
+        result = s.extract_unique_points()
+        self.check_sgpd_equals_gpd(result, expected)
+
+        # Check that GeoDataFrame works too
+        df_result = s.to_geoframe().extract_unique_points()
+        self.check_sgpd_equals_gpd(df_result, expected)
 
     def test_offset_curve(self):
         pass
@@ -1312,7 +1441,28 @@ e": "Feature", "properties": {}, "geometry": {"type": 
"Point", "coordinates": [3
         pass
 
     def test_remove_repeated_points(self):
-        pass
+        s = GeoSeries(
+            [
+                LineString([(0, 0), (0, 0), (1, 1), (1, 1), (2, 2)]),
+                Polygon([(0, 0), (1, 0), (1, 0), (1, 1), (0, 1)]),
+                Point(0, 0),
+                None,
+            ]
+        )
+        expected = gpd.GeoSeries(
+            [
+                LineString([(0, 0), (0, 0), (1, 1), (1, 1), (2, 2)]),
+                Polygon([(0, 0), (1, 0), (1, 0), (1, 1), (0, 1)]),
+                Point(0, 0),
+                None,
+            ]
+        ).remove_repeated_points()
+        result = s.remove_repeated_points()
+        self.check_sgpd_equals_gpd(result, expected)
+
+        # Check that GeoDataFrame works too
+        df_result = s.to_geoframe().remove_repeated_points()
+        self.check_sgpd_equals_gpd(df_result, expected)
 
     def test_set_precision(self):
         pass
@@ -1667,7 +1817,28 @@ e": "Feature", "properties": {}, "geometry": {"type": 
"Point", "coordinates": [3
         self.check_sgpd_equals_gpd(result, expected)
 
     def test_line_merge(self):
-        pass
+        s = GeoSeries(
+            [
+                MultiLineString([[(0, 0), (1, 1)], [(1, 1), (2, 2)]]),
+                MultiLineString([[(0, 0), (1, 1)], [(2, 2), (3, 3)]]),
+                LineString([(0, 0), (1, 1)]),
+                None,
+            ]
+        )
+        expected = gpd.GeoSeries(
+            [
+                MultiLineString([[(0, 0), (1, 1)], [(1, 1), (2, 2)]]),
+                MultiLineString([[(0, 0), (1, 1)], [(2, 2), (3, 3)]]),
+                LineString([(0, 0), (1, 1)]),
+                None,
+            ]
+        ).line_merge()
+        result = s.line_merge()
+        self.check_sgpd_equals_gpd(result, expected)
+
+        # Check that GeoDataFrame works too
+        df_result = s.to_geoframe().line_merge()
+        self.check_sgpd_equals_gpd(df_result, expected)
 
     def test_unary_union(self):
         pass
diff --git a/python/tests/geopandas/test_match_geopandas_series.py 
b/python/tests/geopandas/test_match_geopandas_series.py
index abac9b453f..29de459214 100644
--- a/python/tests/geopandas/test_match_geopandas_series.py
+++ b/python/tests/geopandas/test_match_geopandas_series.py
@@ -549,13 +549,22 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
             self.check_pd_series_equal(sgpd_result, gpd_result)
 
     def test_count_coordinates(self):
-        pass
+        for geom in self.geoms:
+            sgpd_result = GeoSeries(geom).count_coordinates()
+            gpd_result = gpd.GeoSeries(geom).count_coordinates()
+            self.check_pd_series_equal(sgpd_result, gpd_result)
 
     def test_count_geometries(self):
-        pass
+        for geom in self.geoms:
+            sgpd_result = GeoSeries(geom).count_geometries()
+            gpd_result = gpd.GeoSeries(geom).count_geometries()
+            self.check_pd_series_equal(sgpd_result, gpd_result)
 
     def test_count_interior_rings(self):
-        pass
+        for geom in self.geoms:
+            sgpd_result = GeoSeries(geom).count_interior_rings()
+            gpd_result = gpd.GeoSeries(geom).count_interior_rings()
+            self.check_pd_series_equal(sgpd_result, gpd_result)
 
     def test_dwithin(self):
         if parse_version(gpd.__version__) < parse_version("1.0.0"):
@@ -723,7 +732,10 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
             self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
 
     def test_concave_hull(self):
-        pass
+        for geom in self.geoms:
+            sgpd_result = GeoSeries(geom).concave_hull(ratio=0.5)
+            gpd_result = gpd.GeoSeries(geom).concave_hull(ratio=0.5)
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
 
     def test_convex_hull(self):
         for geom in self.geoms:
@@ -749,13 +761,38 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
             self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
 
     def test_minimum_rotated_rectangle(self):
-        pass
+        # Sedona (ST_OrientedEnvelope) and geopandas may return different
+        # but geometrically valid oriented envelopes, so we compare areas.
+        for geom in self.geoms:
+            sgpd_result = GeoSeries(geom).minimum_rotated_rectangle()
+            gpd_result = gpd.GeoSeries(geom).minimum_rotated_rectangle()
+            sgpd_gdf = sgpd_result.to_geopandas()
+            for a, e in zip(sgpd_gdf, gpd_result):
+                if (a is None or a.is_empty) and (e is None or e.is_empty):
+                    continue
+                assert (
+                    abs(a.area - e.area) < 1e-6
+                ), f"area mismatch: {a.area} vs {e.area}"
 
     def test_exterior(self):
-        pass
+        for geom in [self.polygons, self.multipolygons]:
+            sgpd_result = GeoSeries(geom).exterior
+            gpd_result = gpd.GeoSeries(geom).exterior
+            # Sedona returns LINESTRING, geopandas returns LINEARRING;
+            # compare coordinates instead of geometry type.
+            sgpd_gdf = sgpd_result.to_geopandas()
+            for a, e in zip(sgpd_gdf, gpd_result):
+                if (a is None or a.is_empty) and (e is None or e.is_empty):
+                    continue
+                assert list(a.coords) == list(
+                    e.coords
+                ), f"exterior coords mismatch: {a} vs {e}"
 
     def test_extract_unique_points(self):
-        pass
+        for geom in self.geoms:
+            sgpd_result = GeoSeries(geom).extract_unique_points()
+            gpd_result = gpd.GeoSeries(geom).extract_unique_points()
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
 
     def test_offset_curve(self):
         pass
@@ -764,7 +801,10 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
         pass
 
     def test_remove_repeated_points(self):
-        pass
+        for geom in self.geoms:
+            sgpd_result = GeoSeries(geom).remove_repeated_points()
+            gpd_result = gpd.GeoSeries(geom).remove_repeated_points()
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
 
     def test_set_precision(self):
         pass
@@ -931,7 +971,10 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
         self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
 
     def test_line_merge(self):
-        pass
+        for geom in [self.multilinestrings]:
+            sgpd_result = GeoSeries(geom).line_merge()
+            gpd_result = gpd.GeoSeries(geom).line_merge()
+            self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
 
     def test_unary_union(self):
         pass
diff --git a/python/tests/streaming/spark/test_constructor_functions.py 
b/python/tests/streaming/spark/test_constructor_functions.py
index 227c807411..6cf84749f1 100644
--- a/python/tests/streaming/spark/test_constructor_functions.py
+++ b/python/tests/streaming/spark/test_constructor_functions.py
@@ -252,9 +252,11 @@ SEDONA_LISTED_SQL_FUNCTIONS = [
         SuiteContainer.empty()
         .with_function_name("ST_LineMerge")
         .with_arguments(
-            ["ST_GeomFromText('LINESTRING(-29 -27,-30 -29.7,-36 -31,-45 
-33,-46 -32)')"]
+            [
+                "ST_GeomFromText('MULTILINESTRING((-29 -27,-30 -29.7,-36 
-31),(-36 -31,-45 -33,-46 -32))')"
+            ]
         )
-        .with_expected_result(0.0)
+        .with_expected_result(19.652212220711906)
         .with_transform("ST_LENGTH")
     ),
     (

Reply via email to