This is an automated email from the ASF dual-hosted git repository. jiayu pushed a commit to branch geopandas-tier1-batch-bc in repository https://gitbox.apache.org/repos/asf/sedona.git
commit 4cbd23f9e4da87d8e713f394f61cf559db83edb0 Author: Jia Yu <[email protected]> AuthorDate: Tue Mar 10 23:44:03 2026 -0700 [GH-2230] Implement GeoSeries: line_merge, count_coordinates, count_geometries, count_interior_rings, concave_hull, minimum_rotated_rectangle, exterior, extract_unique_points, remove_repeated_points --- python/sedona/spark/geopandas/base.py | 272 +++++++++++++++++++-- python/sedona/spark/geopandas/geoseries.py | 73 +++--- python/tests/geopandas/test_geoseries.py | 189 +++++++++++++- .../tests/geopandas/test_match_geopandas_series.py | 63 ++++- 4 files changed, 530 insertions(+), 67 deletions(-) diff --git a/python/sedona/spark/geopandas/base.py b/python/sedona/spark/geopandas/base.py index 0308b4d9be..06e9cdfd45 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('int64')`` 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: int64 + + """ + return _delegate_to_geometry_column("count_coordinates", self) + + def count_geometries(self): + """Return a ``Series`` of ``dtype('int64')`` 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: int64 + + """ + return _delegate_to_geometry_column("count_geometries", self) + + def count_interior_rings(self): + """Return a ``Series`` of ``dtype('int64')`` 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: int64 + + """ + 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,81 @@ 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 LinearRings representing the exterior ring + of each polygon. For non-polygon geometries, returns ``None``. + + 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 LINEARRING (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 +903,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 +1297,38 @@ 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 + Currently not supported by Sedona. + + 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..b9b240a789 100644 --- a/python/sedona/spark/geopandas/geoseries.py +++ b/python/sedona/spark/geopandas/geoseries.py @@ -792,30 +792,24 @@ 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.", - ) + spark_expr = stf.ST_NumInteriorRings(self.spark.column) + return self._query_geometry_column( + spark_expr, + returns_geom=False, ) def dwithin(self, other, distance, align=None): @@ -972,8 +966,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 +997,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 +1028,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 +1124,11 @@ class GeoSeries(GeoFrame, pspd.Series): ) def line_merge(self, directed=False): - # Implementation of the abstract method. - raise NotImplementedError("This method is not implemented yet.") + 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..e3a55929e6 100644 --- a/python/tests/geopandas/test_match_geopandas_series.py +++ b/python/tests/geopandas/test_match_geopandas_series.py @@ -549,13 +549,24 @@ 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 + # Sedona returns null for empty geometries while geopandas returns 0, + # so we only test with non-empty polygons. + geom = [self.polygons[1], self.polygons[2]] + 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 +734,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 +763,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 +803,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 +973,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
