From d47088b7edc46b4e490e3d125882f17085b9b55a Mon Sep 17 00:00:00 2001 From: ncclementi Date: Mon, 8 Jan 2024 17:40:02 -0500 Subject: [PATCH] feat(duckdb-geospatial): add support for flipping coordinates - Extend api to support ST_FlipCoordinates - Clarify convert (equivalent of st_transform) docs to mention it that coordinates are expected as XY (longitude/latitude) --- ibis/backends/duckdb/registry.py | 6 ++++++ ibis/backends/duckdb/tests/test_geospatial.py | 20 +++++++++++++++++++ ibis/expr/operations/geospatial.py | 7 +++++++ ibis/expr/types/geospatial.py | 18 ++++++++++++++++- 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/ibis/backends/duckdb/registry.py b/ibis/backends/duckdb/registry.py index 7b675acd369c..db815f6dd5be 100644 --- a/ibis/backends/duckdb/registry.py +++ b/ibis/backends/duckdb/registry.py @@ -61,6 +61,11 @@ def _centroid(t, op): return sa.func.st_centroid(arg, type_=Geometry_WKB) +def _geo_flip_coordinates(t, op): + arg = t.translate(op.arg) + return sa.func.st_flipcoordinates(arg, type_=Geometry_WKB) + + def _geo_end_point(t, op): arg = t.translate(op.arg) return sa.func.st_endpoint(arg, type_=Geometry_WKB) @@ -568,6 +573,7 @@ def _array_remove(t, op): ops.GeoX: unary(sa.func.ST_X), ops.GeoY: unary(sa.func.ST_Y), ops.GeoConvert: _geo_convert, + ops.GeoFlipCoordinates: _geo_flip_coordinates, # other ops ops.TimestampRange: fixed_arity(sa.func.range, 3), ops.RegexSplit: fixed_arity(sa.func.str_split_regex, 2), diff --git a/ibis/backends/duckdb/tests/test_geospatial.py b/ibis/backends/duckdb/tests/test_geospatial.py index 379a1fd8cdd5..364bc290611d 100644 --- a/ibis/backends/duckdb/tests/test_geospatial.py +++ b/ibis/backends/duckdb/tests/test_geospatial.py @@ -214,6 +214,26 @@ def test_geospatial_convert(geotable, gdf): ) +def test_geospatial_flip_coordinates(geotable): + flipped = geotable.geom.flip_coordinates() + + # flipped coords + point = shapely.geometry.Point(40, -100) + line_string = shapely.geometry.LineString([[0, 0], [1, 1], [1, 2], [2, 2]]) + polygon = shapely.geometry.Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) + + d = { + "name": ["Point", "LineString", "Polygon"], + "geometry": [point, line_string, polygon], + } + + flipped_gdf = gpd.GeoDataFrame(d) + + gtm.assert_geoseries_equal( + flipped.to_pandas(), flipped_gdf.geometry, check_crs=False + ) + + def test_create_table_geospatial_types(geotable, con): name = ibis.util.gen_name("geotable") diff --git a/ibis/expr/operations/geospatial.py b/ibis/expr/operations/geospatial.py index 856716011f92..21067dc1d161 100644 --- a/ibis/expr/operations/geospatial.py +++ b/ibis/expr/operations/geospatial.py @@ -487,3 +487,10 @@ class GeoAsText(GeoSpatialUnOp): """Return the Well-Known Text (WKT) representation of the input, without SRID metadata.""" dtype = dt.string + + +@public +class GeoFlipCoordinates(GeoSpatialUnOp): + """Returns a new geometry with the coordinates of the input geometry "flipped" so that x = y and y = x.""" + + dtype = dt.geometry diff --git a/ibis/expr/types/geospatial.py b/ibis/expr/types/geospatial.py index c9f97f000da5..557e009e20f9 100644 --- a/ibis/expr/types/geospatial.py +++ b/ibis/expr/types/geospatial.py @@ -674,7 +674,8 @@ def transform(self, srid: ir.IntegerValue) -> GeoSpatialValue: def convert( self, source: ir.StringValue, target: ir.StringValue | ir.IntegerValue ) -> GeoSpatialValue: - """Transform a geometry into a new SRID (CRS). + """Transform a geometry into a new SRID (CRS). The coordinates are assumed + to always be XY (Longitude-Latitude). Parameters ---------- @@ -687,6 +688,11 @@ def convert( ------- GeoSpatialValue Transformed geometry + + See Also + -------- + [`flip_coordinates`](#ibis.expr.types.geospatial.GeoSpatialValue.flip_coordinates) + """ return ops.GeoConvert(self, source, target).to_expr() @@ -748,6 +754,16 @@ def line_merge(self) -> ir.LineStringValue: """ return ops.GeoLineMerge(self).to_expr() + def flip_coordinates(self) -> GeoSpatialValue: + """Flip coordinates of a geometry so that x = y and y = x. + + Returns + ------- + GeoSpatialValue + New geometry with flipped coordinates + """ + return ops.GeoFlipCoordinates(self).to_expr() + @public class GeoSpatialScalar(NumericScalar, GeoSpatialValue):