Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions xrspatial/rasterize.py
Original file line number Diff line number Diff line change
Expand Up @@ -1951,6 +1951,11 @@ def _parse_input(geometries, column=None, columns=None):
if isinstance(geometries, gpd.GeoDataFrame):
geom_list = geometries.geometry.tolist()
total_bounds = tuple(geometries.total_bounds)
# GeoPandas returns (nan, nan, nan, nan) for an empty frame.
# Treat as "no inferred bounds" so the caller's explicit-bounds
# guard fires instead of producing a raster with nan coords.
if any(not np.isfinite(v) for v in total_bounds):
total_bounds = None
if columns is not None:
props_array = geometries[columns].values.astype(np.float64)
else:
Expand Down Expand Up @@ -2217,19 +2222,28 @@ def rasterize(
if final_bounds is None:
final_bounds = inferred_bounds
if final_bounds is None and geom_list:
# Compute bounds lazily only when not supplied by the caller
# Compute bounds lazily only when not supplied by the caller.
# Drop empty/invalid geoms whose bbox is nan so they don't poison
# the inferred extent.
geom_bboxes = _geometry_bboxes(geom_list)
if len(geom_bboxes) > 0:
final_bounds = (geom_bboxes[:, 0].min(),
geom_bboxes[:, 1].min(),
geom_bboxes[:, 2].max(),
geom_bboxes[:, 3].max())
finite = np.all(np.isfinite(geom_bboxes), axis=1)
geom_bboxes = geom_bboxes[finite]
if len(geom_bboxes) > 0:
final_bounds = (geom_bboxes[:, 0].min(),
geom_bboxes[:, 1].min(),
geom_bboxes[:, 2].max(),
geom_bboxes[:, 3].max())
if final_bounds is None:
raise ValueError(
"bounds must be provided when geometries are empty or have "
"no spatial extent")

xmin, ymin, xmax, ymax = final_bounds
if not all(np.isfinite(v) for v in (xmin, ymin, xmax, ymax)):
raise ValueError(
f"Invalid bounds: all of (xmin, ymin, xmax, ymax) must be "
f"finite, got {(xmin, ymin, xmax, ymax)!r}")
if xmin >= xmax or ymin >= ymax:
raise ValueError(
f"Invalid bounds: xmin ({xmin}) must be < xmax ({xmax}) and "
Expand Down
43 changes: 43 additions & 0 deletions xrspatial/tests/test_rasterize.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,49 @@ def test_invalid_chunks(self, bad):
width=10, height=10, bounds=(0, 0, 1, 1),
chunks=bad)

def test_empty_geometry_does_not_poison_inferred_bounds(self):
# Issue #2065: an empty geometry returns nan from .bounds; the
# caller used .min()/.max() unfiltered, so a single empty geom
# poisoned the inferred extent and produced a raster with nan
# x/y coords. Drop the empty and infer from the rest.
empty = Polygon()
result = rasterize(
[(empty, 99), (box(0, 0, 1, 1), 1)],
width=2, height=2)
assert np.all(np.isfinite(result.x.values))
assert np.all(np.isfinite(result.y.values))
assert np.all(result.values == 1)

def test_only_empty_geometry_requires_explicit_bounds(self):
empty = Polygon()
with pytest.raises(ValueError, match="bounds must be provided"):
rasterize([(empty, 1.0)], width=2, height=2)

def test_empty_geodataframe_requires_explicit_bounds(self):
# Issue #2065: total_bounds on an empty frame is (nan,nan,nan,nan).
# That used to bypass the empty-bounds guard.
import geopandas as gpd
empty_gdf = gpd.GeoDataFrame(
{'value': []}, geometry=gpd.GeoSeries([]))
with pytest.raises(ValueError, match="bounds must be provided"):
rasterize(empty_gdf, width=2, height=2, column='value')

def test_explicit_nan_bounds_rejected(self):
with pytest.raises(ValueError, match="must be finite"):
rasterize([(box(0, 0, 1, 1), 1.0)], width=2, height=2,
bounds=(0, 0, float('nan'), 1))

def test_empty_multipolygon_filtered_from_inferred_bounds(self):
# Same shape as the empty-Polygon case but exercises the
# MultiPolygon branch in shapely's is_empty / bounds path.
empty_mp = MultiPolygon()
result = rasterize(
[(empty_mp, 99), (box(0, 0, 1, 1), 1)],
width=2, height=2)
assert np.all(np.isfinite(result.x.values))
assert np.all(np.isfinite(result.y.values))
assert np.all(result.values == 1)


# ---------------------------------------------------------------------------
# all_touched mode
Expand Down
Loading