This is an automated email from the ASF dual-hosted git repository.
petern 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 807137225b [GH-2491] Implement force_2d (#2493)
807137225b is described below
commit 807137225b56731655e0f595ed21013fdfd969ff
Author: Krishna C Vemulakonda <[email protected]>
AuthorDate: Wed Nov 12 21:13:13 2025 -0700
[GH-2491] Implement force_2d (#2493)
---
python/sedona/spark/geopandas/base.py | 33 ++++++++++++++++++++--
python/sedona/spark/geopandas/geoseries.py | 6 ++--
python/tests/geopandas/test_geoseries.py | 33 +++++++++++++++++++++-
.../tests/geopandas/test_match_geopandas_series.py | 23 ++++++++++++++-
4 files changed, 88 insertions(+), 7 deletions(-)
diff --git a/python/sedona/spark/geopandas/base.py
b/python/sedona/spark/geopandas/base.py
index 82dd7dd53b..639785c2b6 100644
--- a/python/sedona/spark/geopandas/base.py
+++ b/python/sedona/spark/geopandas/base.py
@@ -861,8 +861,37 @@ class GeoFrame(metaclass=ABCMeta):
# def transform(self, transformation, include_z=False):
# raise NotImplementedError("This method is not implemented yet.")
- # def force_2d(self):
- # raise NotImplementedError("This method is not implemented yet.")
+ def force_2d(self):
+ """
+ Forces the dimensionality of each geometry to 2D.
+ Removes the Z and M coordinates (if present) from each geometry and
returns a
+ GeoSeries with 2D geometries. 2D inputs are returned unchanged.
+ Returns
+ -------
+ GeoSeries
+ Examples
+ --------
+ >>> from shapely import Polygon, LineString, Point
+ >>> from sedona.spark.geopandas import GeoSeries
+ >>> s = GeoSeries(
+ ... [
+ ... Point(0.5, 2.5, 0),
+ ... LineString([(1, 1, 1), (0, 1, 3), (1, 0, 2)]),
+ ... Polygon([(0, 0, 0), (0, 10, 0), (10, 10, 0)]),
+ ... ]
+ ... )
+ >>> s
+ 0 POINT Z (0.5 2.5 0)
+ 1 LINESTRING Z (1 1 1, 0 1 3, 1 0 2)
+ 2 POLYGON Z ((0 0 0, 0 10 0, 10 10 0, 0 0 0))
+ dtype: geometry
+ >>> s.force_2d()
+ 0 POINT (0.5 2.5)
+ 1 LINESTRING (1 1, 0 1, 1 0)
+ 2 POLYGON ((0 0, 0 10, 10 10, 0 0))
+ dtype: geometry
+ """
+ return _delegate_to_geometry_column("force_2d", self)
# def force_3d(self, z=0):
# raise NotImplementedError("This method is not implemented yet.")
diff --git a/python/sedona/spark/geopandas/geoseries.py
b/python/sedona/spark/geopandas/geoseries.py
index 0530b62e3d..0e60256aa7 100644
--- a/python/sedona/spark/geopandas/geoseries.py
+++ b/python/sedona/spark/geopandas/geoseries.py
@@ -1081,9 +1081,9 @@ class GeoSeries(GeoFrame, pspd.Series):
# Implementation of the abstract method.
raise NotImplementedError("This method is not implemented yet.")
- def force_2d(self):
- # Implementation of the abstract method.
- raise NotImplementedError("This method is not implemented yet.")
+ def force_2d(self) -> "GeoSeries":
+ spark_expr = stf.ST_Force_2D(self.spark.column)
+ return self._query_geometry_column(spark_expr, returns_geom=True)
def force_3d(self, z=0):
# Implementation of the abstract method.
diff --git a/python/tests/geopandas/test_geoseries.py
b/python/tests/geopandas/test_geoseries.py
index c1c373a2ff..efadad1794 100644
--- a/python/tests/geopandas/test_geoseries.py
+++ b/python/tests/geopandas/test_geoseries.py
@@ -1409,7 +1409,38 @@ e": "Feature", "properties": {}, "geometry": {"type":
"Point", "coordinates": [3
pass
def test_force_2d(self):
- pass
+ s = sgpd.GeoSeries(
+ [
+ Point(0, -1, 2.5), # 3D point
+ LineString([(0, 0, 1), (1, 1, 2)]), # 3D line
+ Polygon([(0, 0, 1), (1, 0, 2), (1, 1, 3), (0, 0, 1)]), # 3D
polygon
+ Point(5, 5), # already 2D
+ Polygon(), # empty geometry
+ None, # None preserved
+ shapely.wkt.loads("POINT M (1 2 3)"),
+ shapely.wkt.loads("LINESTRING ZM (1 2 3 4, 5 6 7 8)"),
+ ]
+ )
+
+ result = s.force_2d()
+
+ expected = gpd.GeoSeries(
+ [
+ Point(0, -1),
+ LineString([(0, 0), (1, 1)]),
+ Polygon([(0, 0), (1, 0), (1, 1), (0, 0)]),
+ Point(5, 5),
+ Polygon(),
+ None,
+ shapely.wkt.loads("POINT (1 2)"),
+ shapely.wkt.loads("LINESTRING (1 2, 5 6)"),
+ ]
+ )
+
+ self.check_sgpd_equals_gpd(result, expected)
+
+ df_result = s.to_geoframe().force_2d()
+ self.check_sgpd_equals_gpd(df_result, expected)
def test_force_3d(self):
pass
diff --git a/python/tests/geopandas/test_match_geopandas_series.py
b/python/tests/geopandas/test_match_geopandas_series.py
index 1a9f7ad4ef..16a9fee9de 100644
--- a/python/tests/geopandas/test_match_geopandas_series.py
+++ b/python/tests/geopandas/test_match_geopandas_series.py
@@ -833,7 +833,28 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
pass
def test_force_2d(self):
- pass
+ # force_2d was added from geopandas 1.0.0
+ if parse_version(gpd.__version__) < parse_version("1.0.0"):
+ pytest.skip("geopandas force_2d requires version 1.0.0 or higher")
+ # 1) No-op on existing 2D fixtures
+ for geom in self.geoms:
+ sgpd_result = GeoSeries(geom).force_2d()
+ gpd_result = gpd.GeoSeries(geom).force_2d()
+ self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+ # 2) Minimal 3D sample to verify Z is actually stripped
+ data = [
+ Point(0, -1, 2.5),
+ LineString([(0, 0, 1), (1, 1, 2)]),
+ Polygon([(0, 0, 1), (1, 0, 2), (1, 1, 3), (0, 0, 1)]),
+ Point(5, 5), # already 2D
+ Polygon(), # empty geometry
+ shapely.wkt.loads("POINT M (1 2 3)"),
+ shapely.wkt.loads("LINESTRING ZM (1 2 3 4, 5 6 7 8)"),
+ ]
+ sgpd_3d = GeoSeries(data).force_2d()
+ gpd_3d = gpd.GeoSeries(data).force_2d()
+ self.check_sgpd_equals_gpd(sgpd_3d, gpd_3d)
def test_force_3d(self):
pass