GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
86 lines
2.8 KiB
Python
86 lines
2.8 KiB
Python
"""Choosing which raster cells a drawn selection covers.
|
|
|
|
``rasterio.features.geometry_mask`` selects a cell when the cell *centre* falls
|
|
inside the geometry. That is the right rule for a selection spanning many
|
|
cells, and the wrong one for a small selection: a rectangle smaller than a cell,
|
|
or one landing between four centres, selects nothing. The analysis then reports
|
|
zeros, which on screen reads as "we measured this area and found nothing"
|
|
rather than "this selection is finer than the source raster".
|
|
|
|
Falling back to every touched cell keeps a small selection answerable, at the
|
|
cost of analysing more ground than was drawn. That trade is only honest if it
|
|
is stated, so the fallback is reported alongside the result.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RasterCellSelection:
|
|
mask: Any
|
|
mode: str
|
|
expanded_to_touched_cells: bool
|
|
warning: str | None
|
|
|
|
|
|
def select_cells(
|
|
geometry,
|
|
*,
|
|
out_shape: tuple[int, int],
|
|
transform,
|
|
cell_area_m2: float | None = None,
|
|
) -> RasterCellSelection:
|
|
"""Mask the cells a selection covers, widening only when it covers none.
|
|
|
|
``geometry`` must already be in the raster's own CRS.
|
|
"""
|
|
|
|
from rasterio.features import geometry_mask
|
|
from shapely.geometry import mapping
|
|
|
|
shapes = [mapping(geometry)]
|
|
mask = geometry_mask(shapes, out_shape=out_shape, transform=transform, invert=True)
|
|
if mask.any():
|
|
return RasterCellSelection(
|
|
mask=mask,
|
|
mode="cell_centre",
|
|
expanded_to_touched_cells=False,
|
|
warning=None,
|
|
)
|
|
|
|
touched = geometry_mask(
|
|
shapes,
|
|
out_shape=out_shape,
|
|
transform=transform,
|
|
invert=True,
|
|
all_touched=True,
|
|
)
|
|
if not touched.any():
|
|
# The selection does not reach the raster at all. Widening the rule
|
|
# must never manufacture coverage that is genuinely absent.
|
|
return RasterCellSelection(
|
|
mask=mask,
|
|
mode="cell_centre",
|
|
expanded_to_touched_cells=False,
|
|
warning=None,
|
|
)
|
|
|
|
cell_count = int(touched.sum())
|
|
if cell_area_m2:
|
|
analysed_ha = cell_count * float(cell_area_m2) / 10_000.0
|
|
detail = f"{cell_count} rastercel{'' if cell_count == 1 else 'len'} ({analysed_ha:.1f} ha)"
|
|
else:
|
|
detail = f"{cell_count} rastercel{'' if cell_count == 1 else 'len'}"
|
|
return RasterCellSelection(
|
|
mask=touched,
|
|
mode="all_touched",
|
|
expanded_to_touched_cells=True,
|
|
warning=(
|
|
f"De selectie is kleiner dan één rastercel van deze bron. Het resultaat geldt voor {detail} "
|
|
"die de selectie raken, dus voor een groter gebied dan getekend."
|
|
),
|
|
)
|