You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ji...@apache.org on 2026/07/23 06:48:55 UTC
(sedona) branch master updated: [GH-3147] Implement distributed GeoPandas skew (#3148)
This is an automated email from the ASF dual-hosted git repository.
jiayuasu 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 31f9d536118 [GH-3147] Implement distributed GeoPandas skew (#3148)
31f9d536118 is described below
commit 31f9d53611852f57010742c11cec1be4cfd532cf
Author: Jia Yu <ji...@apache.org>
AuthorDate: Wed Jul 22 23:48:50 2026 -0700
[GH-3147] Implement distributed GeoPandas skew (#3148)
---
python/sedona/spark/geopandas/base.py | 46 +++++
python/sedona/spark/geopandas/geoseries.py | 191 ++++++++++--------
python/tests/geopandas/test_geoseries.py | 221 ++++++++++++++++++++-
.../tests/geopandas/test_match_geopandas_series.py | 81 ++++++++
4 files changed, 456 insertions(+), 83 deletions(-)
diff --git a/python/sedona/spark/geopandas/base.py b/python/sedona/spark/geopandas/base.py
index f0206c3f526..cd9d13905ae 100644
--- a/python/sedona/spark/geopandas/base.py
+++ b/python/sedona/spark/geopandas/base.py
@@ -1525,6 +1525,52 @@ class GeoFrame(metaclass=ABCMeta):
"""
return _delegate_to_geometry_column("scale", self, xfact, yfact, zfact, origin)
+ def skew(self, xs=0.0, ys=0.0, origin="center", use_radians=False):
+ """Return a ``GeoSeries`` with skewed geometries.
+
+ Each geometry is sheared independently along its x and y dimensions.
+ Negative angles shear in the opposite direction.
+
+ Parameters
+ ----------
+ xs : float, default 0.0
+ Shear angle for the x dimension, in degrees by default.
+ ys : float, default 0.0
+ Shear angle for the y dimension, in degrees by default.
+ origin : {"center", "centroid"}, Point, or tuple, default "center"
+ The skew origin. ``"center"`` uses each geometry's bounding-box
+ center and ``"centroid"`` uses each geometry's centroid. A 2D or
+ 3D Shapely Point or coordinate tuple may also be supplied. Skew is
+ a 2D operation, so an explicit origin's z coordinate is ignored.
+ use_radians : bool, default False
+ If True, interpret ``xs`` and ``ys`` as radians instead of degrees.
+
+ Returns
+ -------
+ GeoSeries
+ The skewed geometries.
+
+ Notes
+ -----
+ Existing z coordinates are preserved. Results for mixed 2D/3D
+ ``GeometryCollection`` objects, M or ZM ordinates, NaN z coordinates,
+ non-finite angles or origin coordinates, and angles near 90 degrees
+ may differ from GeoPandas because Sedona uses JTS while GeoPandas uses
+ Shapely. This method applies Sedona's distributed semantics and does
+ not materialize geometries locally to emulate Shapely.
+
+ Examples
+ --------
+ >>> from shapely.geometry import Point
+ >>> from sedona.spark.geopandas import GeoSeries
+ >>> s = GeoSeries([Point(1, 2), Point(-1, -2)])
+ >>> s.skew(xs=45, origin=(0, 0))
+ 0 POINT (3 2)
+ 1 POINT (-3 -2)
+ dtype: geometry
+ """
+ return _delegate_to_geometry_column("skew", self, xs, ys, origin, use_radians)
+
def force_2d(self):
"""Force the dimensionality of a geometry to 2D.
diff --git a/python/sedona/spark/geopandas/geoseries.py b/python/sedona/spark/geopandas/geoseries.py
index 2cd9dad8e35..6fa633ac622 100644
--- a/python/sedona/spark/geopandas/geoseries.py
+++ b/python/sedona/spark/geopandas/geoseries.py
@@ -99,6 +99,70 @@ IMPLEMENTATION_PRIORITY = {
}
+def _normalize_affine_scalar(value, error_message: str) -> float:
+ """Normalize an operation-wide affine parameter to a Python float."""
+ if (
+ value is None
+ or isinstance(value, (str, bytes, bytearray))
+ or not np.isscalar(value)
+ ):
+ raise TypeError(error_message)
+ try:
+ return float(value)
+ except (TypeError, ValueError, OverflowError) as exc:
+ raise TypeError(error_message) from exc
+
+
+def _interpret_origin(geometry: PySparkColumn, origin, with_z: bool):
+ """Resolve a local affinity origin to distributed coordinate columns."""
+ if isinstance(origin, str):
+ if origin == "center":
+ origin_geometry = stf.ST_Centroid(stf.ST_Envelope(geometry))
+ elif origin == "centroid":
+ origin_geometry = stf.ST_Centroid(geometry)
+ else:
+ raise ValueError(
+ "origin must be 'center', 'centroid', a Point, or a "
+ f"two- or three-element tuple, got {origin!r}"
+ )
+ origin_columns = (
+ stf.ST_X(origin_geometry),
+ stf.ST_Y(origin_geometry),
+ F.lit(0.0),
+ )
+ elif isinstance(origin, tuple):
+ if len(origin) not in (2, 3):
+ raise ValueError("'origin' tuple must contain two or three coordinates")
+ coordinates = [
+ _normalize_affine_scalar(
+ coordinate,
+ "'origin' tuple must contain only numeric coordinates",
+ )
+ for coordinate in origin
+ ]
+ if len(coordinates) == 2:
+ coordinates.append(0.0)
+ origin_columns = tuple(F.lit(value) for value in coordinates)
+ elif isinstance(origin, shapely.geometry.Point):
+ if origin.is_empty:
+ raise ValueError("'origin' Point must be a non-empty 2D or 3D Point")
+ coordinates = tuple(origin.coords[0])
+ if len(coordinates) not in (2, 3) or (
+ len(coordinates) == 3 and not origin.has_z
+ ):
+ raise ValueError("'origin' Point must be a non-empty 2D or 3D Point")
+ if len(coordinates) == 2:
+ coordinates += (0.0,)
+ origin_columns = tuple(F.lit(float(value)) for value in coordinates)
+ else:
+ raise TypeError(
+ "origin must be 'center', 'centroid', a Point, or a "
+ "two- or three-element tuple"
+ )
+
+ return origin_columns if with_z else origin_columns[:2]
+
+
def _not_implemented_error(method_name: str, additional_info: str = "") -> str:
"""
Generate a standardized NotImplementedError message.
@@ -1191,20 +1255,12 @@ class GeoSeries(GeoFrame, pspd.Series):
if len(matrix) not in (6, 12):
raise ValueError("'matrix' expects either 6 or 12 coefficients")
- coefficients = []
- for coefficient in matrix:
- if (
- coefficient is None
- or isinstance(coefficient, (str, bytes, bytearray))
- or not np.isscalar(coefficient)
- ):
- raise TypeError("'matrix' must contain only numeric coefficients")
- try:
- coefficients.append(float(coefficient))
- except (TypeError, ValueError, OverflowError) as exc:
- raise TypeError(
- "'matrix' must contain only numeric coefficients"
- ) from exc
+ coefficients = [
+ _normalize_affine_scalar(
+ coefficient, "'matrix' must contain only numeric coefficients"
+ )
+ for coefficient in matrix
+ ]
if len(coefficients) == 6:
a, b, d, e, xoff, yoff = coefficients
@@ -1266,76 +1322,12 @@ class GeoSeries(GeoFrame, pspd.Series):
return self._query_geometry_column(spark_expr, returns_geom=True)
def scale(self, xfact=1.0, yfact=1.0, zfact=1.0, origin="center") -> "GeoSeries":
- def normalize_factor(value, name):
- if (
- value is None
- or isinstance(value, (str, bytes, bytearray))
- or not np.isscalar(value)
- ):
- raise TypeError(f"'{name}' must be a numeric scalar")
- try:
- return float(value)
- except (TypeError, ValueError, OverflowError) as exc:
- raise TypeError(f"'{name}' must be a numeric scalar") from exc
-
- xfact = normalize_factor(xfact, "xfact")
- yfact = normalize_factor(yfact, "yfact")
- zfact = normalize_factor(zfact, "zfact")
+ xfact = _normalize_affine_scalar(xfact, "'xfact' must be a numeric scalar")
+ yfact = _normalize_affine_scalar(yfact, "'yfact' must be a numeric scalar")
+ zfact = _normalize_affine_scalar(zfact, "'zfact' must be a numeric scalar")
geometry = self.spark.column
- if isinstance(origin, str):
- if origin == "center":
- origin_geometry = stf.ST_Centroid(stf.ST_Envelope(geometry))
- elif origin == "centroid":
- origin_geometry = stf.ST_Centroid(geometry)
- else:
- raise ValueError(
- "origin must be 'center', 'centroid', a Point, or a "
- f"two- or three-element tuple, got {origin!r}"
- )
- origin_x = stf.ST_X(origin_geometry)
- origin_y = stf.ST_Y(origin_geometry)
- origin_z = F.lit(0.0)
- elif isinstance(origin, tuple):
- if len(origin) not in (2, 3):
- raise ValueError("'origin' tuple must contain two or three coordinates")
- coordinates = []
- for coordinate in origin:
- if (
- coordinate is None
- or isinstance(coordinate, (str, bytes, bytearray))
- or not np.isscalar(coordinate)
- ):
- raise TypeError(
- "'origin' tuple must contain only numeric coordinates"
- )
- try:
- coordinates.append(float(coordinate))
- except (TypeError, ValueError, OverflowError) as exc:
- raise TypeError(
- "'origin' tuple must contain only numeric coordinates"
- ) from exc
- if len(coordinates) == 2:
- coordinates.append(0.0)
- origin_x, origin_y, origin_z = (F.lit(value) for value in coordinates)
- elif isinstance(origin, shapely.geometry.Point):
- if origin.is_empty:
- raise ValueError("'origin' Point must be a non-empty 2D or 3D Point")
- coordinates = tuple(origin.coords[0])
- if len(coordinates) not in (2, 3) or (
- len(coordinates) == 3 and not origin.has_z
- ):
- raise ValueError("'origin' Point must be a non-empty 2D or 3D Point")
- if len(coordinates) == 2:
- coordinates += (0.0,)
- origin_x, origin_y, origin_z = (
- F.lit(float(value)) for value in coordinates
- )
- else:
- raise TypeError(
- "origin must be 'center', 'centroid', a Point, or a "
- "two- or three-element tuple"
- )
+ origin_x, origin_y, origin_z = _interpret_origin(geometry, origin, with_z=True)
xoff = origin_x - origin_x * F.lit(xfact)
yoff = origin_y - origin_y * F.lit(yfact)
@@ -1362,6 +1354,41 @@ class GeoSeries(GeoFrame, pspd.Series):
spark_expr = F.when(stf.ST_IsEmpty(geometry), geometry).otherwise(scaled)
return self._query_geometry_column(spark_expr, returns_geom=True)
+ def skew(self, xs=0.0, ys=0.0, origin="center", use_radians=False) -> "GeoSeries":
+ import math
+
+ xs = _normalize_affine_scalar(xs, "'xs' must be a numeric scalar")
+ ys = _normalize_affine_scalar(ys, "'ys' must be a numeric scalar")
+ if not isinstance(use_radians, (bool, np.bool_)):
+ raise TypeError("'use_radians' must be a boolean")
+
+ if not use_radians:
+ xs = xs * math.pi / 180.0
+ ys = ys * math.pi / 180.0
+ tan_x = math.tan(xs)
+ tan_y = math.tan(ys)
+ if abs(tan_x) < 2.5e-16:
+ tan_x = 0.0
+ if abs(tan_y) < 2.5e-16:
+ tan_y = 0.0
+
+ geometry = self.spark.column
+ origin_x, origin_y = _interpret_origin(geometry, origin, with_z=False)
+
+ xoff = -origin_y * F.lit(tan_x)
+ yoff = -origin_x * F.lit(tan_y)
+ skewed = stf.ST_Affine(
+ geometry,
+ 1.0,
+ tan_x,
+ tan_y,
+ 1.0,
+ xoff,
+ yoff,
+ )
+ spark_expr = F.when(stf.ST_IsEmpty(geometry), geometry).otherwise(skewed)
+ return self._query_geometry_column(spark_expr, returns_geom=True)
+
def force_2d(self) -> "GeoSeries":
spark_expr = stf.ST_Force_2D(self.spark.column)
return self._query_geometry_column(spark_expr, returns_geom=True)
diff --git a/python/tests/geopandas/test_geoseries.py b/python/tests/geopandas/test_geoseries.py
index b8f7eddae08..435e0518602 100644
--- a/python/tests/geopandas/test_geoseries.py
+++ b/python/tests/geopandas/test_geoseries.py
@@ -2252,7 +2252,7 @@ e": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": [3
):
source.scale(origin=origin)
- for origin in ((1, "2"), (1, None)):
+ for origin in ((1, "2"), (1, None), (1, 2, "invalid-z")):
with pytest.raises(TypeError, match="only numeric coordinates"):
source.scale(origin=origin)
@@ -2271,6 +2271,225 @@ e": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": [3
with pytest.raises(ValueError, match="origin must be"):
empty_source.scale(origin="invalid")
+ def test_skew_documented_examples(self):
+ geoms = [
+ Point(1, 1),
+ LineString([(1, -1), (1, 0)]),
+ Polygon([(3, -1), (4, 0), (3, 1), (3, -1)]),
+ ]
+ source = GeoSeries(geoms)
+
+ for kwargs in (
+ {"xs": 45, "ys": 30},
+ {"xs": 45, "ys": 30, "origin": (0, 0)},
+ ):
+ result = source.skew(**kwargs)
+ expected = gpd.GeoSeries(geoms).skew(**kwargs)
+ self.check_sgpd_equals_gpd(result, expected)
+
+ @pytest.mark.parametrize(
+ "xs,ys",
+ [
+ pytest.param(45, 0, id="x-only"),
+ pytest.param(0, 30, id="y-only"),
+ pytest.param(45, 30, id="both"),
+ pytest.param(-45, -30, id="negative"),
+ pytest.param(0, 0, id="zero"),
+ ],
+ )
+ def test_skew_angle_combinations(self, xs, ys):
+ geoms = [
+ Point(1, 2),
+ LineString([(0, 0), (2, 4)]),
+ Polygon([(0, 0), (4, 0), (0, 2), (0, 0)]),
+ ]
+ source = GeoSeries(geoms)
+ expected = gpd.GeoSeries(geoms).skew(xs, ys, origin=(0, 0))
+
+ result = source.skew(xs, ys, origin=(0, 0))
+
+ self.check_sgpd_equals_gpd(result, expected)
+
+ def test_skew_degrees_radians_and_180_degree_clamp(self):
+ import math
+
+ source = GeoSeries([LineString([(0, 0), (2, 4)])])
+
+ degrees = source.skew(45, 30, origin=(0, 0)).to_geopandas().iloc[0]
+ radians = (
+ source.skew(
+ math.pi / 4,
+ math.pi / 6,
+ origin=(0, 0),
+ use_radians=True,
+ )
+ .to_geopandas()
+ .iloc[0]
+ )
+ numpy_bool_radians = (
+ source.skew(
+ math.pi / 4,
+ math.pi / 6,
+ origin=(0, 0),
+ use_radians=np.bool_(True),
+ )
+ .to_geopandas()
+ .iloc[0]
+ )
+ clamped = source.skew(180, 180, origin=(0, 0)).to_geopandas().iloc[0]
+
+ expected = [(0.0, 0.0), (6.0, 5.1547005383792515)]
+ assert list(degrees.coords) == pytest.approx(expected)
+ assert list(radians.coords) == pytest.approx(expected)
+ assert list(numpy_bool_radians.coords) == pytest.approx(expected)
+ assert list(clamped.coords) == [(0.0, 0.0), (2.0, 4.0)]
+
+ def test_skew_center_and_centroid_origins(self):
+ source = GeoSeries([Polygon([(0, 0), (4, 0), (0, 2), (0, 0)])])
+
+ centered = source.skew(45, 30, origin="center").to_geopandas().iloc[0]
+ centroid = source.skew(45, 30, origin="centroid").to_geopandas().iloc[0]
+
+ assert centered.exterior.coords[0] == pytest.approx((-1.0, -1.1547005383792512))
+ assert centroid.exterior.coords[0] == pytest.approx(
+ (-2.0 / 3.0, -0.7698003589195008)
+ )
+
+ @pytest.mark.parametrize(
+ "origin",
+ [
+ pytest.param((1, 1), id="two-coordinate-tuple"),
+ pytest.param((1, 1, 999), id="three-coordinate-tuple"),
+ pytest.param(Point(1, 1), id="2d-point"),
+ pytest.param(Point(1, 1, 999), id="3d-point"),
+ ],
+ )
+ def test_skew_explicit_origins_ignore_z(self, origin):
+ source = GeoSeries([LineString([(1, 2, 3), (4, 6, 9)])])
+
+ result = source.skew(45, 45, origin=origin)
+
+ line = result.to_geopandas().iloc[0]
+ assert line.has_z
+ assert list(line.coords) == pytest.approx([(2, 2, 3), (9, 9, 9)])
+ assert [coordinate[2] for coordinate in line.coords] == [3.0, 9.0]
+
+ def test_skew_2d_stays_2d_with_3d_origin(self):
+ geoms = [
+ Point(1, 2),
+ LineString([(0, 0), (2, 1)]),
+ Polygon([(0, 0), (2, 0), (1, 1), (0, 0)]),
+ ]
+ source = GeoSeries(geoms)
+ expected = gpd.GeoSeries(geoms).skew(45, 30, origin=(1, 1, 999))
+
+ result = source.skew(45, 30, origin=(1, 1, 999))
+
+ self.check_sgpd_equals_gpd(result, expected)
+ assert not any(geom.has_z for geom in result.to_geopandas())
+
+ @pytest.mark.parametrize("origin", ["center", "centroid"])
+ def test_skew_preserves_metadata_empty_null_and_delegates(self, origin):
+ geoms = [
+ Point(1, 2),
+ LineString([(0, 0), (2, 1)]),
+ Point(),
+ LineString(),
+ Polygon(),
+ None,
+ ]
+ index = pd.Index(
+ ["point", "line", "empty-point", "empty-line", "empty-polygon", "null"],
+ name="feature_id",
+ )
+ source = GeoSeries(geoms, index=index, crs="EPSG:3857", name="geometry")
+ expected = gpd.GeoSeries(
+ geoms, index=index, crs="EPSG:3857", name="geometry"
+ ).skew(45, 30, origin=origin)
+
+ result = source.skew(45, 30, origin=origin)
+
+ self.check_sgpd_equals_gpd(result, expected)
+ assert result.name is None
+ assert result.crs == source.crs == expected.crs
+ actual = result.to_geopandas()
+ assert actual.loc["empty-point"].is_empty
+ assert actual.loc["empty-point"].geom_type == "Point"
+ assert actual.loc["empty-line"].is_empty
+ assert actual.loc["empty-line"].geom_type == "LineString"
+ assert actual.loc["empty-polygon"].is_empty
+ assert actual.loc["empty-polygon"].geom_type == "Polygon"
+ assert actual.loc["null"] is None
+
+ srids = result._internal.spark_frame.select(
+ stf.ST_SRID(result.spark.column).alias("srid")
+ ).collect()
+ assert {row.srid for row in srids if row.srid is not None} == {3857}
+
+ frame_result = source.to_geoframe().skew(45, 30, origin=origin)
+ assert isinstance(frame_result, GeoSeries)
+ self.check_sgpd_equals_gpd(frame_result, expected)
+ assert frame_result.crs == source.crs
+
+ def test_skew_validates_angles_units_and_origin(self):
+ source = GeoSeries([Point(1, 2)])
+
+ invalid_angles = [
+ ("xs", None),
+ ("ys", "30"),
+ ("xs", np.array([45])),
+ ("ys", [30]),
+ ("xs", 45 + 1j),
+ ]
+ for name, value in invalid_angles:
+ with pytest.raises(TypeError, match=rf"'{name}' must be a numeric scalar"):
+ source.skew(**{name: value})
+
+ for name in ("xs", "ys"):
+ with pytest.raises(TypeError, match=rf"'{name}' must be a numeric scalar"):
+ source.skew(**{name: ps.Series([45.0])})
+
+ for use_radians in (None, 0, 1, "true", [True], np.array(True)):
+ with pytest.raises(TypeError, match="'use_radians' must be a boolean"):
+ source.skew(use_radians=use_radians)
+
+ for origin in ("invalid", "CENTER"):
+ with pytest.raises(ValueError, match="origin must be"):
+ source.skew(origin=origin)
+
+ for origin in ((1,), (1, 2, 3, 4)):
+ with pytest.raises(
+ ValueError, match="tuple must contain two or three coordinates"
+ ):
+ source.skew(origin=origin)
+
+ for origin in ((1, "2"), (1, None), (1, 2, "ignored-by-skew")):
+ with pytest.raises(TypeError, match="only numeric coordinates"):
+ source.skew(origin=origin)
+
+ for origin in (
+ None,
+ [0, 0],
+ Polygon([(0, 0), (1, 0), (0, 1)]),
+ ps.Series([0.0, 0.0]),
+ ):
+ with pytest.raises(TypeError, match="origin must be"):
+ source.skew(origin=origin)
+
+ with pytest.raises(ValueError, match="Point must be a non-empty 2D or 3D"):
+ source.skew(origin=Point())
+
+ # Operation-wide arguments are validated even when there is no
+ # non-empty geometry on which to apply the transformation.
+ empty_source = GeoSeries([Polygon(), None])
+ for name in ("xs", "ys"):
+ with pytest.raises(TypeError, match=rf"'{name}' must be a numeric scalar"):
+ empty_source.skew(**{name: None})
+ with pytest.raises(TypeError, match="'use_radians' must be a boolean"):
+ empty_source.skew(use_radians=1)
+ with pytest.raises(ValueError, match="origin must be"):
+ empty_source.skew(origin="invalid")
+
def test_force_2d(self):
s = sgpd.GeoSeries(
[
diff --git a/python/tests/geopandas/test_match_geopandas_series.py b/python/tests/geopandas/test_match_geopandas_series.py
index ccd23157ca0..a46ce56543a 100644
--- a/python/tests/geopandas/test_match_geopandas_series.py
+++ b/python/tests/geopandas/test_match_geopandas_series.py
@@ -1126,6 +1126,87 @@ class TestMatchGeopandasSeries(TestGeopandasBase):
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+ @pytest.mark.parametrize(
+ "xs,ys",
+ [
+ (35.0, 0.0),
+ (0.0, -25.0),
+ (-15.0, 30.0),
+ ],
+ ids=["x-only-positive", "y-only-negative", "unequal-both"],
+ )
+ @pytest.mark.parametrize(
+ "origin",
+ ["center", "centroid", (2.5, -1.5)],
+ ids=["center", "centroid", "explicit"],
+ )
+ def test_skew_2d(self, xs, ys, origin):
+ geoms = [
+ self.points[2],
+ self.linestrings[2],
+ self.t1,
+ self.multipoints[2],
+ self.multilinestrings[2],
+ self.multipolygons[1],
+ self.geomcollection[1],
+ ]
+
+ sgpd_result = GeoSeries(geoms).skew(xs=xs, ys=ys, origin=origin)
+ gpd_result = gpd.GeoSeries(geoms).skew(xs=xs, ys=ys, origin=origin)
+
+ self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+ def test_skew_radians(self):
+ geoms = [
+ self.points[2],
+ self.linestrings[2],
+ self.t1,
+ self.multipoints[2],
+ self.multilinestrings[2],
+ self.multipolygons[1],
+ self.geomcollection[1],
+ ]
+ skew_kwargs = {
+ "xs": np.pi / 7,
+ "ys": -np.pi / 9,
+ "origin": (1.25, -2.5),
+ "use_radians": True,
+ }
+
+ sgpd_result = GeoSeries(geoms).skew(**skew_kwargs)
+ gpd_result = gpd.GeoSeries(geoms).skew(**skew_kwargs)
+
+ self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+
+ def test_skew_3d_preserves_z(self):
+ polygon = Polygon([(0, 0, 1), (3, 0, 2), (1, 2, 4), (0, 0, 1)])
+ geoms = [
+ Point(1, 2, 3),
+ LineString([(0, 0, 1), (2, 1, 4)]),
+ polygon,
+ MultiPoint([(0, 0, 1), (1, 2, 3)]),
+ MultiLineString([[(0, 0, 1), (2, 1, 3)], [(1, -1, 2), (3, 2, 4)]]),
+ MultiPolygon([polygon]),
+ GeometryCollection(
+ [Point(1, 2, 3), LineString([(0, 0, 1), (2, 1, 4)]), polygon]
+ ),
+ ]
+ skew_kwargs = {
+ "xs": 25.0,
+ "ys": -10.0,
+ "origin": (1.0, -2.0, 7.0),
+ }
+
+ sgpd_result = GeoSeries(geoms).skew(**skew_kwargs)
+ gpd_result = gpd.GeoSeries(geoms).skew(**skew_kwargs)
+
+ self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
+ actual_z = shapely.get_coordinates(
+ sgpd_result.to_geopandas().array, include_z=True
+ )[:, 2]
+ expected_z = shapely.get_coordinates(geoms, include_z=True)[:, 2]
+ np.testing.assert_array_equal(actual_z, expected_z)
+
def test_force_2d(self):
# force_2d was added from geopandas 1.0.0
if parse_version(gpd.__version__) < parse_version("1.0.0"):