diff --git a/ibis/backends/duckdb/registry.py b/ibis/backends/duckdb/registry.py index 6a76315cf4ad5..0e4a53d51ffeb 100644 --- a/ibis/backends/duckdb/registry.py +++ b/ibis/backends/duckdb/registry.py @@ -87,6 +87,12 @@ def _envelope(t, op): return sa.func.st_envelope(arg, type_=Geometry_WKB) +def _geo_buffer(t, op): + arg = t.translate(op.arg) + radius = t.translate(op.radius) + return sa.func.st_buffer(arg, radius, type_=Geometry_WKB) + + def _geo_unary_union(t, op): arg = t.translate(op.arg) return sa.func.st_union_agg(arg, type_=Geometry_WKB) @@ -562,7 +568,8 @@ def _to_json_collection(t, op): ops.GeoPoint: _geo_point, ops.GeoAsText: unary(sa.func.ST_AsText), ops.GeoArea: unary(sa.func.ST_Area), - # ops.GeoBuffer: fixed_arity(sa.func.ST_Buffer, 2), duckdb sup 2 or 3? + # ops.GeoBuffer: fixed_arity(sa.func.ST_Buffer, 2), + ops.GeoBuffer: _geo_buffer, # ops.GeoCentroid: unary(sa.func.ST_Centroid), ops.GeoCentroid: _centroid, ops.GeoContains: fixed_arity(sa.func.ST_Contains, 2), diff --git a/ibis/backends/duckdb/tests/test_geospatial.py b/ibis/backends/duckdb/tests/test_geospatial.py index bbb14548be7d0..d8582ca4d0e67 100644 --- a/ibis/backends/duckdb/tests/test_geospatial.py +++ b/ibis/backends/duckdb/tests/test_geospatial.py @@ -1,6 +1,7 @@ from __future__ import annotations import geopandas as gpd +import geopandas.testing as gtm import numpy.testing as npt import pandas.testing as tm import pyarrow as pa @@ -231,12 +232,6 @@ def test_geospatial_y(zones, zones_gdf): tm.assert_series_equal(y.to_pandas(), gp_cen.y, check_names=False) -# def test_geospatial_buffer() -# GeoBUffer in alchemy supports 2 arguments, but duckdb is a unary? -# actually docs are wrong, it takes 2 or 3 args -# looks like we have fixed_arity(sa.func.ST_Buffer, 2) - - @pytest.mark.broken(["duckdb"], raises=AssertionError, reason="no match with geopandas") def test_geospatial_unary_union(zones, zones_gdf): unary_union = zones.geom.unary_union().name("unary_union") @@ -245,3 +240,46 @@ def test_geospatial_unary_union(zones, zones_gdf): # we should try to use equals_exact but not even equals pass assert shapely.equals(shapely.from_wkb(unary_union.to_pandas()), gp_unary_union) + + +def test_geospatial_buffer_point(zones, zones_gdf): + # when we add a buffer to a single point things work + cen = zones.geom.centroid().name("centroid") + gp_cen = zones_gdf.geometry.centroid + + buffer = cen.buffer(100.0) + # geopandas resolution default is 16, while duckdb is 8. + gp_buffer = gp_cen.buffer(100.0, resolution=8) + + gtm.assert_geoseries_equal( + gpd.GeoSeries.from_wkb(buffer.to_pandas().values), gp_buffer, check_crs=False + ) + + # check areas are the same + tm.assert_series_equal(buffer.area().to_pandas(), gp_buffer.area, check_names=False) + + +@pytest.mark.broken(["duckdb"], raises=AssertionError, reason="no match with geopandas") +def test_geospatial_buffer(zones, zones_gdf): + # When generating buffer from a more complex geometry, we get a match on the geometry + # but errors in the area for ~7% of the series. + + buffer = zones.geom.buffer(100.0) + # geopandas resolution default is 16, while duckdb is 8. + gp_buffer = zones_gdf.geometry.buffer(100.0, resolution=8) + + # comparing the geometries directly works + gtm.assert_geoseries_equal( + gpd.GeoSeries.from_wkb(buffer.to_pandas().values), gp_buffer, check_crs=False + ) + + # when we check the area we have problems + # Notice that buffer.area().to_pandas() != gpd.GeoSeries.from_wkb(buffer.to_pandas().values).area + # Not clear which area is the correct one, geopandas in their area docs says + # Area may be invalid for a geographic CRS using degrees as units; + # use GeoSeries.to_crs() to project geometries to a planar CRS before using this function. + # gp_buffere.crs says axis info is ellipsoidal. But it is not clear how to project in this case + # when trying to_crs() with different epsg, I always get inf. + tm.assert_series_equal( + buffer.area().to_pandas(), gp_buffer.area, check_names=False, check_exact=False + )