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")
),
(