From 59c1f7f3e30fbca783aa5b06c391648518f2bcae Mon Sep 17 00:00:00 2001 From: Fred Bunt Date: Sat, 22 Oct 2022 07:54:26 -0600 Subject: [PATCH] BUG: Fix handling of empty shapely geometries in clip() (#2595) --- CHANGELOG.md | 1 + geopandas/tools/clip.py | 7 ++++++- geopandas/tools/tests/test_clip.py | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e6b63f..8977f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Deprecations and compatibility notes: - resolve ``matplotlib.cm`` warning in ``.explore()`` (#2596) Bug fixes: +- Fix cryptic error message in ``geopandas.clip()`` when clipping with an empty geometry (#2589) - Accessing `gdf.geometry` where the active geometry column is missing, and a column named `"geometry"` is present will now raise an `AttributeError`, rather than returning `gdf["geometry"]` (#2575) - Combining GeoSeries/GeoDataFrames with ``pandas.concat`` will no longer silently diff --git a/geopandas/tools/clip.py b/geopandas/tools/clip.py index c10b12b..a088db8 100644 --- a/geopandas/tools/clip.py +++ b/geopandas/tools/clip.py @@ -7,6 +7,7 @@ A module to clip vector data using GeoPandas. """ import warnings +import numpy as np import pandas.api.types from shapely.geometry import Polygon, MultiPolygon, box @@ -168,7 +169,11 @@ def clip(gdf, mask, keep_geom_type=False): elif mask_is_list_like: box_mask = mask else: - box_mask = mask.bounds + # Avoid empty tuple returned by .bounds when geometry is empty. A tuple of + # all nan values is consistent with the behavior of + # {GeoSeries, GeoDataFrame}.total_bounds for empty geometries. + # TODO(shapely) can simpely use mask.bounds once relying on Shapely 2.0 + box_mask = mask.bounds if not mask.is_empty else (np.nan,) * 4 box_gdf = gdf.total_bounds if not ( ((box_mask[0] <= box_gdf[2]) and (box_gdf[0] <= box_mask[2])) diff --git a/geopandas/tools/tests/test_clip.py b/geopandas/tools/tests/test_clip.py index c643d5a..29006fd 100644 --- a/geopandas/tools/tests/test_clip.py +++ b/geopandas/tools/tests/test_clip.py @@ -450,3 +450,28 @@ def test_clip_single_multipoly_no_extra_geoms( multi = buffered_locations.dissolve(by="type").reset_index() clipped = clip(multi, masks) assert clipped.geom_type[0] == "Polygon" + + +@pytest.mark.filterwarnings("ignore:All-NaN slice encountered") +@pytest.mark.parametrize( + "mask", + [ + Polygon(), + (np.nan,) * 4, + (np.nan, 0, np.nan, 1), + GeoSeries([Polygon(), Polygon()], crs="EPSG:3857"), + GeoSeries([Polygon(), Polygon()], crs="EPSG:3857").to_frame(), + GeoSeries([], crs="EPSG:3857"), + GeoSeries([], crs="EPSG:3857").to_frame(), + ], +) +def test_clip_empty_mask(buffered_locations, mask): + """Test that clipping with empty mask returns an empty result.""" + clipped = clip(buffered_locations, mask) + assert_geodataframe_equal( + clipped, + GeoDataFrame([], columns=["geometry", "type"], crs="EPSG:3857"), + check_index_type=False, + ) + clipped = clip(buffered_locations.geometry, mask) + assert_geoseries_equal(clipped, GeoSeries([], crs="EPSG:3857"))