Skip to content

Commit 31ceb14

Browse files
authored
[GH-2230] Implement GeoSeries: reverse, normalize, representative_point (#2701)
1 parent 21c31a7 commit 31ceb14

4 files changed

Lines changed: 170 additions & 18 deletions

File tree

python/sedona/spark/geopandas/base.py

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -730,8 +730,35 @@ def envelope(self):
730730
# def set_precision(self, grid_size, mode="valid_output"):
731731
# raise NotImplementedError("This method is not implemented yet.")
732732

733-
# def representative_point(self):
734-
# raise NotImplementedError("This method is not implemented yet.")
733+
def representative_point(self):
734+
"""Return a point that is guaranteed to be within each geometry.
735+
736+
Returns a ``GeoSeries`` of (cheaply computed) points that are guaranteed
737+
to be within each geometry.
738+
739+
Returns
740+
-------
741+
GeoSeries
742+
743+
Examples
744+
--------
745+
>>> from sedona.spark.geopandas import GeoSeries
746+
>>> from shapely.geometry import Polygon, LineString, Point
747+
>>> s = GeoSeries(
748+
... [
749+
... Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
750+
... LineString([(0, 0), (1, 1), (1, 0)]),
751+
... Point(0, 0),
752+
... ]
753+
... )
754+
>>> s.representative_point()
755+
0 POINT (0.5 0.5)
756+
1 POINT (1 0.5)
757+
2 POINT (0 0)
758+
dtype: geometry
759+
760+
"""
761+
return _delegate_to_geometry_column("representative_point", self)
735762

736763
def minimum_bounding_circle(self):
737764
"""Return a ``GeoSeries`` of geometries representing the minimum bounding
@@ -803,8 +830,35 @@ def minimum_bounding_radius(self):
803830
# def minimum_clearance(self):
804831
# raise NotImplementedError("This method is not implemented yet.")
805832

806-
# def normalize(self):
807-
# raise NotImplementedError("This method is not implemented yet.")
833+
def normalize(self):
834+
"""Return a ``GeoSeries`` of normalized geometries.
835+
836+
Normalization reorganizes the coordinates in a consistent order,
837+
which can be useful for comparison purposes.
838+
839+
Returns
840+
-------
841+
GeoSeries
842+
843+
Examples
844+
--------
845+
>>> from sedona.spark.geopandas import GeoSeries
846+
>>> from shapely.geometry import Polygon, LineString, Point
847+
>>> s = GeoSeries(
848+
... [
849+
... Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
850+
... LineString([(0, 0), (1, 1)]),
851+
... Point(0, 0),
852+
... ]
853+
... )
854+
>>> s.normalize()
855+
0 POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))
856+
1 LINESTRING (0 0, 1 1)
857+
2 POINT (0 0)
858+
dtype: geometry
859+
860+
"""
861+
return _delegate_to_geometry_column("normalize", self)
808862

809863
def make_valid(self, *, method="linework", keep_collapsed=True):
810864
"""Repairs invalid geometries.
@@ -869,8 +923,32 @@ def make_valid(self, *, method="linework", keep_collapsed=True):
869923
"make_valid", self, method=method, keep_collapsed=keep_collapsed
870924
)
871925

872-
# def reverse(self):
873-
# raise NotImplementedError("This method is not implemented yet.")
926+
def reverse(self):
927+
"""Return a ``GeoSeries`` with the coordinate order reversed.
928+
929+
Returns
930+
-------
931+
GeoSeries
932+
933+
Examples
934+
--------
935+
>>> from sedona.spark.geopandas import GeoSeries
936+
>>> from shapely.geometry import LineString, Point
937+
>>> s = GeoSeries(
938+
... [
939+
... LineString([(0, 0), (1, 1), (2, 2)]),
940+
... LineString([(0, 0), (1, 0), (1, 1)]),
941+
... Point(0, 0),
942+
... ]
943+
... )
944+
>>> s.reverse()
945+
0 LINESTRING (2 2, 1 1, 0 0)
946+
1 LINESTRING (1 1, 1 0, 0 0)
947+
2 POINT (0 0)
948+
dtype: geometry
949+
950+
"""
951+
return _delegate_to_geometry_column("reverse", self)
874952

875953
def segmentize(self, max_segment_length):
876954
"""Returns a ``GeoSeries`` with vertices added to line segments based on

python/sedona/spark/geopandas/geoseries.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,8 +1030,11 @@ def set_precision(self, grid_size, mode="valid_output"):
10301030
raise NotImplementedError("This method is not implemented yet.")
10311031

10321032
def representative_point(self):
1033-
# Implementation of the abstract method.
1034-
raise NotImplementedError("This method is not implemented yet.")
1033+
spark_expr = stf.ST_PointOnSurface(self.spark.column)
1034+
return self._query_geometry_column(
1035+
spark_expr,
1036+
returns_geom=True,
1037+
)
10351038

10361039
def minimum_bounding_circle(self) -> "GeoSeries":
10371040
spark_expr = stf.ST_MinimumBoundingCircle(self.spark.column)
@@ -1053,8 +1056,11 @@ def minimum_clearance(self):
10531056
raise NotImplementedError("This method is not implemented yet.")
10541057

10551058
def normalize(self):
1056-
# Implementation of the abstract method.
1057-
raise NotImplementedError("This method is not implemented yet.")
1059+
spark_expr = stf.ST_Normalize(self.spark.column)
1060+
return self._query_geometry_column(
1061+
spark_expr,
1062+
returns_geom=True,
1063+
)
10581064

10591065
def make_valid(self, *, method="linework", keep_collapsed=True) -> "GeoSeries":
10601066
if method != "structure":
@@ -1069,8 +1075,11 @@ def make_valid(self, *, method="linework", keep_collapsed=True) -> "GeoSeries":
10691075
)
10701076

10711077
def reverse(self):
1072-
# Implementation of the abstract method.
1073-
raise NotImplementedError("This method is not implemented yet.")
1078+
spark_expr = stf.ST_Reverse(self.spark.column)
1079+
return self._query_geometry_column(
1080+
spark_expr,
1081+
returns_geom=True,
1082+
)
10741083

10751084
def segmentize(self, max_segment_length):
10761085
other_series, extended = self._make_series_of_val(max_segment_length)

python/tests/geopandas/test_geoseries.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,21 @@ def test_set_precision(self):
13181318
pass
13191319

13201320
def test_representative_point(self):
1321-
pass
1321+
geoms = [
1322+
Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
1323+
LineString([(0, 0), (1, 1), (1, 0)]),
1324+
Point(0, 0),
1325+
None,
1326+
]
1327+
s = GeoSeries(geoms)
1328+
expected = gpd.GeoSeries(geoms).representative_point()
1329+
1330+
result = s.representative_point()
1331+
self.check_sgpd_equals_gpd(result, expected)
1332+
1333+
# Check that GeoDataFrame works too
1334+
df_result = s.to_geoframe().representative_point()
1335+
self.check_sgpd_equals_gpd(df_result, expected)
13221336

13231337
def test_minimum_bounding_circle(self):
13241338
s = GeoSeries(
@@ -1374,7 +1388,28 @@ def test_minimum_clearance(self):
13741388
pass
13751389

13761390
def test_normalize(self):
1377-
pass
1391+
s = GeoSeries(
1392+
[
1393+
Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
1394+
LineString([(0, 0), (1, 1)]),
1395+
Point(0, 0),
1396+
None,
1397+
]
1398+
)
1399+
result = s.normalize()
1400+
expected = gpd.GeoSeries(
1401+
[
1402+
shapely.normalize(Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])),
1403+
shapely.normalize(LineString([(0, 0), (1, 1)])),
1404+
shapely.normalize(Point(0, 0)),
1405+
None,
1406+
]
1407+
)
1408+
self.check_sgpd_equals_gpd(result, expected)
1409+
1410+
# Check that GeoDataFrame works too
1411+
df_result = s.to_geoframe().normalize()
1412+
self.check_sgpd_equals_gpd(df_result, expected)
13781413

13791414
def test_make_valid(self):
13801415
s = sgpd.GeoSeries(
@@ -1431,7 +1466,28 @@ def test_make_valid(self):
14311466
self.check_sgpd_equals_gpd(df_result, expected)
14321467

14331468
def test_reverse(self):
1434-
pass
1469+
s = GeoSeries(
1470+
[
1471+
LineString([(0, 0), (1, 1), (2, 2)]),
1472+
LineString([(0, 0), (1, 0), (1, 1)]),
1473+
Point(0, 0),
1474+
None,
1475+
]
1476+
)
1477+
result = s.reverse()
1478+
expected = gpd.GeoSeries(
1479+
[
1480+
LineString([(2, 2), (1, 1), (0, 0)]),
1481+
LineString([(1, 1), (1, 0), (0, 0)]),
1482+
Point(0, 0),
1483+
None,
1484+
]
1485+
)
1486+
self.check_sgpd_equals_gpd(result, expected)
1487+
1488+
# Check that GeoDataFrame works too
1489+
df_result = s.to_geoframe().reverse()
1490+
self.check_sgpd_equals_gpd(df_result, expected)
14351491

14361492
def test_segmentize(self):
14371493
s = GeoSeries(

python/tests/geopandas/test_match_geopandas_series.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -770,7 +770,10 @@ def test_set_precision(self):
770770
pass
771771

772772
def test_representative_point(self):
773-
pass
773+
for geom in self.geoms:
774+
sgpd_result = GeoSeries(geom).representative_point()
775+
gpd_result = gpd.GeoSeries(geom).representative_point()
776+
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
774777

775778
def test_minimum_bounding_circle(self):
776779
for geom in self.geoms:
@@ -788,7 +791,10 @@ def test_minimum_clearance(self):
788791
pass
789792

790793
def test_normalize(self):
791-
pass
794+
for geom in self.geoms:
795+
sgpd_result = GeoSeries(geom).normalize()
796+
gpd_result = gpd.GeoSeries(geom).normalize()
797+
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
792798

793799
def test_make_valid(self):
794800
import shapely
@@ -818,7 +824,10 @@ def test_make_valid(self):
818824
GeoSeries([Point(0, 0)]).make_valid(method="linework")
819825

820826
def test_reverse(self):
821-
pass
827+
for geom in self.geoms:
828+
sgpd_result = GeoSeries(geom).reverse()
829+
gpd_result = gpd.GeoSeries(geom).reverse()
830+
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
822831

823832
@pytest.mark.skipif(
824833
parse_version(gpd.__version__) < parse_version("0.14.0"),

0 commit comments

Comments
 (0)