Skip to content

Commit c7e42cb

Browse files
authored
[GH-2504] Geopandas: Implement force_3d (#2512)
1 parent e3941bf commit c7e42cb

File tree

4 files changed

+206
-7
lines changed

4 files changed

+206
-7
lines changed

python/sedona/spark/geopandas/base.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -949,8 +949,70 @@ def force_2d(self):
949949
"""
950950
return _delegate_to_geometry_column("force_2d", self)
951951

952-
# def force_3d(self, z=0):
953-
# raise NotImplementedError("This method is not implemented yet.")
952+
def force_3d(self, z=0.0):
953+
"""Force the dimensionality of a geometry to 3D.
954+
955+
2D geometries will get the provided Z coordinate; 3D geometries
956+
are unchanged (unless their Z coordinate is ``np.nan``).
957+
958+
Note: Sedona's behavior may differ from Geopandas' for M and ZM geometries.
959+
For M geometries, Sedona will replace the M coordinate and add the Z coordinate.
960+
For ZM geometries, Sedona will drop the M coordinate and retain the Z coordinate.
961+
962+
Parameters
963+
----------
964+
z : float | array_like (default 0)
965+
Z coordinate to be assigned
966+
967+
Returns
968+
-------
969+
GeoSeries
970+
971+
Examples
972+
--------
973+
>>> from shapely import Polygon, LineString, Point
974+
>>> from sedona.spark.geopandas import GeoSeries
975+
>>> s = GeoSeries(
976+
... [
977+
... Point(1, 2),
978+
... Point(0.5, 2.5, 2),
979+
... LineString([(1, 1), (0, 1), (1, 0)]),
980+
... Polygon([(0, 0), (0, 10), (10, 10)]),
981+
... ],
982+
... )
983+
>>> s
984+
0 POINT (1 2)
985+
1 POINT Z (0.5 2.5 2)
986+
2 LINESTRING (1 1, 0 1, 1 0)
987+
3 POLYGON ((0 0, 0 10, 10 10, 0 0))
988+
dtype: geometry
989+
990+
>>> s.force_3d()
991+
0 POINT Z (1 2 0)
992+
1 POINT Z (0.5 2.5 2)
993+
2 LINESTRING Z (1 1 0, 0 1 0, 1 0 0)
994+
3 POLYGON Z ((0 0 0, 0 10 0, 10 10 0, 0 0 0))
995+
dtype: geometry
996+
997+
Z coordinate can be specified as scalar:
998+
999+
>>> s.force_3d(4)
1000+
0 POINT Z (1 2 4)
1001+
1 POINT Z (0.5 2.5 2)
1002+
2 LINESTRING Z (1 1 4, 0 1 4, 1 0 4)
1003+
3 POLYGON Z ((0 0 4, 0 10 4, 10 10 4, 0 0 4))
1004+
dtype: geometry
1005+
1006+
Or as an array-like (one value per geometry):
1007+
1008+
>>> s.force_3d(range(4))
1009+
0 POINT Z (1 2 0)
1010+
1 POINT Z (0.5 2.5 2)
1011+
2 LINESTRING Z (1 1 2, 0 1 2, 1 0 2)
1012+
3 POLYGON Z ((0 0 3, 0 10 3, 10 10 3, 0 0 3))
1013+
dtype: geometry
1014+
"""
1015+
return _delegate_to_geometry_column("force_3d", self, z)
9541016

9551017
# def line_merge(self, directed=False):
9561018
# raise NotImplementedError("This method is not implemented yet.")

python/sedona/spark/geopandas/geoseries.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,9 +1093,17 @@ def force_2d(self) -> "GeoSeries":
10931093
spark_expr = stf.ST_Force_2D(self.spark.column)
10941094
return self._query_geometry_column(spark_expr, returns_geom=True)
10951095

1096-
def force_3d(self, z=0):
1097-
# Implementation of the abstract method.
1098-
raise NotImplementedError("This method is not implemented yet.")
1096+
def force_3d(self, z=0.0) -> "GeoSeries":
1097+
other_series, extended = self._make_series_of_val(z)
1098+
align = not extended
1099+
1100+
spark_expr = stf.ST_Force3D(F.col("L"), F.col("R"))
1101+
return self._row_wise_operation(
1102+
spark_expr,
1103+
other_series,
1104+
align=align,
1105+
returns_geom=True,
1106+
)
10991107

11001108
def line_merge(self, directed=False):
11011109
# Implementation of the abstract method.

python/tests/geopandas/test_geoseries.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1520,7 +1520,95 @@ def test_force_2d(self):
15201520
self.check_sgpd_equals_gpd(df_result, expected)
15211521

15221522
def test_force_3d(self):
1523-
pass
1523+
# 1. 2D geometries promoted to 3D with default z=0.0
1524+
s = sgpd.GeoSeries(
1525+
[
1526+
Point(1, 2),
1527+
Point(0.5, 2.5, 2),
1528+
Point(1, 1, np.nan),
1529+
LineString([(1, 1), (0, 1), (1, 0)]),
1530+
Polygon([(0, 0), (0, 10), (10, 10)]),
1531+
GeometryCollection(
1532+
[
1533+
Point(1, 1),
1534+
LineString([(1, 1), (0, 1), (1, 0)]),
1535+
]
1536+
),
1537+
]
1538+
)
1539+
# Promote 2D to 3D with z=0, keep 3D as is
1540+
expected = gpd.GeoSeries(
1541+
[
1542+
Point(1, 2, 0),
1543+
Point(0.5, 2.5, 2),
1544+
Point(1, 1, 0),
1545+
LineString([(1, 1, 0), (0, 1, 0), (1, 0, 0)]),
1546+
Polygon([(0, 0, 0), (0, 10, 0), (10, 10, 0), (0, 0, 0)]),
1547+
GeometryCollection(
1548+
[
1549+
Point(1, 1, 0),
1550+
LineString([(1, 1, 0), (0, 1, 0), (1, 0, 0)]),
1551+
]
1552+
),
1553+
]
1554+
)
1555+
result = s.force_3d()
1556+
self.check_sgpd_equals_gpd(result, expected)
1557+
1558+
# 2. 2D geometries promoted to 3D with scalar z
1559+
expected = gpd.GeoSeries(
1560+
[
1561+
Point(1, 2, 4),
1562+
Point(0.5, 2.5, 2),
1563+
Point(1, 1, 4),
1564+
LineString([(1, 1, 4), (0, 1, 4), (1, 0, 4)]),
1565+
Polygon([(0, 0, 4), (0, 10, 4), (10, 10, 4), (0, 0, 4)]),
1566+
GeometryCollection(
1567+
[
1568+
Point(1, 1, 4),
1569+
LineString([(1, 1, 4), (0, 1, 4), (1, 0, 4)]),
1570+
]
1571+
),
1572+
]
1573+
)
1574+
result = s.force_3d(4)
1575+
self.check_sgpd_equals_gpd(result, expected)
1576+
1577+
# 3. Array-like z: use ps.Series
1578+
z = [0, 2, 2, 3, 4, 5]
1579+
expected = gpd.GeoSeries(
1580+
[
1581+
Point(1, 2, 0),
1582+
Point(0.5, 2.5, 2),
1583+
Point(1, 1, 2),
1584+
LineString([(1, 1, 3), (0, 1, 3), (1, 0, 3)]),
1585+
Polygon([(0, 0, 4), (0, 10, 4), (10, 10, 4), (0, 0, 4)]),
1586+
GeometryCollection(
1587+
[
1588+
Point(1, 1, 5),
1589+
LineString([(1, 1, 5), (0, 1, 5), (1, 0, 5)]),
1590+
]
1591+
),
1592+
]
1593+
)
1594+
result = s.force_3d(z)
1595+
self.check_sgpd_equals_gpd(result, expected)
1596+
1597+
# 4. Ensure M and ZM geometries are handled correctly
1598+
s = sgpd.GeoSeries(
1599+
[
1600+
shapely.wkt.loads("POINT M (1 2 3)"),
1601+
shapely.wkt.loads("POINT ZM (1 2 3 4)"),
1602+
]
1603+
)
1604+
result = s.force_3d(7.5)
1605+
expected = gpd.GeoSeries(
1606+
[
1607+
shapely.wkt.loads("POINT Z (1 2 7.5)"),
1608+
shapely.wkt.loads("POINT Z (1 2 3)"),
1609+
]
1610+
)
1611+
self.check_sgpd_equals_gpd(result, expected)
15241612

15251613
def test_line_merge(self):
15261614
pass

python/tests/geopandas/test_match_geopandas_series.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,48 @@ def test_force_2d(self):
878878
self.check_sgpd_equals_gpd(sgpd_3d, gpd_3d)
879879

880880
def test_force_3d(self):
881-
pass
881+
# force_3d was added from geopandas 1.0.0
882+
if parse_version(gpd.__version__) < parse_version("1.0.0"):
883+
pytest.skip("geopandas force_3d requires version 1.0.0 or higher")
884+
# 1) Promote 2D to 3D with z = 4
885+
for geom in self.geoms:
886+
if isinstance(geom[0], (LinearRing)):
887+
continue
888+
sgpd_result = GeoSeries(geom).force_3d(4)
889+
gpd_result = gpd.GeoSeries(geom).force_3d(4)
890+
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
891+
892+
# 2) Minimal sample for various geometry types with custom z=7.5
893+
data = [
894+
Point(1, 2), # 2D
895+
Point(0.5, 2.5, 2), # 3D (Z)
896+
LineString([(1, 1), (0, 1), (1, 0)]), # 2D
897+
Polygon([(0, 0), (0, 10), (10, 10)]), # 2D
898+
]
899+
sgpd_result = GeoSeries(data).force_3d(7.5)
900+
gpd_result = gpd.GeoSeries(data).force_3d(7.5)
901+
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
902+
903+
# 3) Array-like z tests
904+
geoms = self.polygons
905+
lst = list(range(1, len(geoms) + 1))
906+
907+
# Traditional python list
908+
sgpd_result = GeoSeries(geoms).force_3d(lst)
909+
gpd_result = gpd.GeoSeries(geoms).force_3d(lst)
910+
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
911+
912+
# numpy array
913+
np_array = np.array(lst)
914+
sgpd_result = GeoSeries(geoms).force_3d(np_array)
915+
gpd_result = gpd.GeoSeries(geoms).force_3d(np_array)
916+
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
917+
918+
# pandas-on-Spark Series
919+
psser = ps.Series(lst)
920+
sgpd_result = GeoSeries(geoms).force_3d(psser)
921+
gpd_result = gpd.GeoSeries(geoms).force_3d(psser.to_pandas())
922+
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
882923

883924
def test_line_merge(self):
884925
pass

0 commit comments

Comments
 (0)