Complete regional raster exploration
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-16 14:21:32 +02:00
parent 153cff06c0
commit 45dc730e76
24 changed files with 1162 additions and 123 deletions
+93 -1
View File
@@ -18,7 +18,7 @@ from app.core.errors import AppError
from app.db.session import get_db
from app.main import app
from app.models import Area, Dataset, DatasetVersion, Job, Project
from app.schemas.dhmv import DhmvAcquireRequest, TerrainSelectionRequest
from app.schemas.dhmv import DhmvAcquireRequest, TerrainPartitionSelectionRequest, TerrainSelectionRequest
from app.services.dhmv_acquisition_service import DhmvAcquisitionService
from app.services.terrain_analysis_service import TerrainAnalysisService
@@ -39,6 +39,9 @@ class FakeQuery:
def first(self):
return self.result
def all(self):
return self.result if isinstance(self.result, list) else []
class FakeSession:
def __init__(self, rows=None, query_result=None):
@@ -115,6 +118,23 @@ def elevation_tiff(*, left: float, top: float, width: int, height: int, resoluti
return memory.read()
def constant_elevation_tiff(*, left: float, top: float, value: float) -> bytes:
values = np.full((20, 20), value, dtype="float32")
with MemoryFile() as memory:
with memory.open(
driver="GTiff",
width=20,
height=20,
count=1,
dtype="float32",
crs="EPSG:31370",
transform=from_origin(left, top, 5.0, 5.0),
nodata=-9999.0,
) as output:
output.write(values, 1)
return memory.read()
def edge_elevation_tiff(*, left: float, top: float, x_resolution: float, y_resolution: float = 5.0) -> bytes:
rows, columns = np.indices((20, 20))
values = (20.0 + columns * 0.5 + rows).astype("float32")
@@ -359,6 +379,61 @@ def test_terrain_analysis_returns_governed_elevation_relief_and_slope(tmp_path)
assert "Waterdiepte" in result["limitation_message"]
def test_partitioned_terrain_analysis_is_exact_across_municipality_boundaries(tmp_path) -> None:
project_id = uuid4()
transformer = Transformer.from_crs("EPSG:31370", "EPSG:4326", always_xy=True)
min_x, min_y = transformer.transform(200_000, 210_000)
middle_x, _ = transformer.transform(200_100, 210_000)
max_x, max_y = transformer.transform(200_200, 210_100)
paths = [tmp_path / "left-terrain.tif", tmp_path / "right-terrain.tif"]
paths[0].write_bytes(constant_elevation_tiff(left=200_000, top=210_100, value=10.0))
paths[1].write_bytes(constant_elevation_tiff(left=200_100, top=210_100, value=20.0))
datasets = [
Dataset(
id=uuid4(),
project_id=project_id,
area_id=uuid4(),
name=path.name,
dataset_type="raster",
source="official WCS",
source_name="digitaal_vlaanderen_dhmv",
source_metadata={
"product_key": "dtm_1m",
"surface_model": "terrain",
"bbox_epsg4326": [left, min_y, right, max_y],
},
status="ready",
storage_path=str(path),
)
for path, left, right in (
(paths[0], min_x, middle_x),
(paths[1], middle_x, max_x),
)
]
db = FakeSession(query_result=datasets)
payload = TerrainPartitionSelectionRequest(
bbox={"min_x": min_x, "min_y": min_y, "max_x": max_x, "max_y": max_y, "crs": "EPSG:4326"},
product_key="dtm_1m",
)
result = TerrainAnalysisService.analyze_partitions(
db,
project_id,
payload,
settings=Settings(_env_file=None),
)
metrics = {item["metric_key"]: item["metric_value"] for item in result["summary"]["metrics"]}
assert result["partition_count"] == 2
assert set(result["dataset_ids"]) == {str(dataset.id) for dataset in datasets}
assert result["sample_count"] >= 790
assert metrics["terrain_elevation_mean_m"] == pytest.approx(15.0, abs=0.1)
assert metrics["terrain_elevation_min_m"] == 10.0
assert metrics["terrain_elevation_max_m"] == 20.0
assert metrics["terrain_elevation_p90_m"] == 20.0
assert "2 persistente gemeentelijke rasterpartities" in result["limitation_message"]
def test_terrain_analysis_rejects_non_dhmv_raster(tmp_path) -> None:
project_id = uuid4()
dataset_id = uuid4()
@@ -425,6 +500,16 @@ def test_dhmv_endpoints_use_canonical_envelopes(monkeypatch) -> None:
"unsupported_metrics": ["water_depth_m", "water_volume_m3"],
},
)
monkeypatch.setattr(
TerrainAnalysisService,
"analyze_partitions",
lambda *_args, **_kwargs: {
"dataset_id": str(output_dataset_id),
"dataset_ids": [str(output_dataset_id)],
"partition_count": 1,
"sample_count": 100,
},
)
app.dependency_overrides[get_db] = lambda: db
try:
products = TestClient(app).get(f"/api/v1/projects/{project_id}/datasets/dhmv/products")
@@ -436,6 +521,10 @@ def test_dhmv_endpoints_use_canonical_envelopes(monkeypatch) -> None:
f"/api/v1/projects/{project_id}/datasets/{output_dataset_id}/raster/terrain/select",
json={"bbox": lambert_bbox_payload().bbox.model_dump()},
)
regional_terrain = TestClient(app).post(
f"/api/v1/projects/{project_id}/datasets/raster/terrain/select",
json={"bbox": lambert_bbox_payload().bbox.model_dump(), "product_key": "dtm_1m"},
)
finally:
app.dependency_overrides.clear()
@@ -450,6 +539,9 @@ def test_dhmv_endpoints_use_canonical_envelopes(monkeypatch) -> None:
assert set(terrain.json()) == {"data"}
assert terrain.json()["data"]["sample_count"] == 100
assert terrain.json()["data"]["unsupported_metrics"] == ["water_depth_m", "water_volume_m3"]
assert regional_terrain.status_code == 200
assert set(regional_terrain.json()) == {"data"}
assert regional_terrain.json()["data"]["partition_count"] == 1
assert any(isinstance(item, Job) for item in db.added)
@@ -16,7 +16,11 @@ from app.core.errors import AppError
from app.db.session import get_db
from app.main import app
from app.models import Dataset, Job, Project
from app.schemas.flood_hazard import FloodHazardAcquireRequest, FloodHazardSelectionRequest
from app.schemas.flood_hazard import (
FloodHazardAcquireRequest,
FloodHazardPartitionSelectionRequest,
FloodHazardSelectionRequest,
)
from app.schemas.assistant import AssistantQueryRequest
from app.services.geo_assistant_service import GeoAssistantService
from app.services.flood_hazard_acquisition_service import FloodHazardAcquisitionService
@@ -120,6 +124,23 @@ def edge_depth_tiff(*, left: float, top: float, x_resolution: float, y_resolutio
return memory.read()
def normalized_depth_tiff(*, left: float, top: float, value: float) -> bytes:
values = np.full((20, 20), value, dtype="float32")
with MemoryFile() as memory:
with memory.open(
driver="GTiff",
width=20,
height=20,
count=1,
dtype="float32",
crs="EPSG:31370",
transform=from_origin(left, top, 5.0, 5.0),
nodata=-9999.0,
) as output:
output.write(values, 1)
return memory.read()
def test_flood_hazard_registry_is_complete_and_semantically_honest() -> None:
products = FloodHazardAcquisitionService.list_products()
@@ -274,6 +295,61 @@ def test_flood_hazard_analysis_reports_scenario_metrics_without_claiming_waterbo
assert "geen gelijktijdig" in result["limitation_message"]
def test_partitioned_flood_analysis_is_exact_across_municipality_boundaries(tmp_path) -> None:
project_id = uuid4()
transformer = Transformer.from_crs("EPSG:31370", "EPSG:4326", always_xy=True)
min_x, min_y = transformer.transform(200_000, 210_000)
middle_x, _ = transformer.transform(200_100, 210_000)
max_x, max_y = transformer.transform(200_200, 210_100)
paths = [tmp_path / "left-flood.tif", tmp_path / "right-flood.tif"]
paths[0].write_bytes(normalized_depth_tiff(left=200_000, top=210_100, value=1.0))
paths[1].write_bytes(normalized_depth_tiff(left=200_100, top=210_100, value=2.0))
datasets = [
Dataset(
id=uuid4(),
project_id=project_id,
area_id=uuid4(),
name=path.name,
dataset_type="raster",
source="VMM",
source_name=FloodHazardAcquisitionService.PROVIDER,
source_metadata={
"product_key": "pluviaal_current_t100",
"normalized_value_unit": "m",
"bbox_epsg4326": [left, min_y, right, max_y],
},
status="ready",
storage_path=str(path),
)
for path, left, right in (
(paths[0], min_x, middle_x),
(paths[1], middle_x, max_x),
)
]
db = FakeSession(query_result=datasets)
payload = FloodHazardPartitionSelectionRequest(
bbox={"min_x": min_x, "min_y": min_y, "max_x": max_x, "max_y": max_y, "crs": "EPSG:4326"},
product_key="pluviaal_current_t100",
)
result = FloodHazardAnalysisService.analyze_partitions(
db,
project_id,
payload,
settings=Settings(_env_file=None),
)
metrics = {item["metric_key"]: item["metric_value"] for item in result["summary"]["metrics"]}
assert result["partition_count"] == 2
assert set(result["dataset_ids"]) == {str(dataset.id) for dataset in datasets}
assert result["inundated_cell_count"] >= 790
assert result["inundated_fraction"] == pytest.approx(1.0)
assert metrics["modelled_depth_mean_m"] == pytest.approx(1.5, abs=0.01)
assert metrics["modelled_depth_p90_m"] == 2.0
assert metrics["modelled_inundated_area_ha"] == pytest.approx(2.0, abs=0.03)
assert "2 persistente gemeentelijke rasterpartities" in result["limitation_message"]
def test_flood_hazard_renderer_returns_transparent_png(tmp_path) -> None:
project_id = uuid4()
dataset_id = uuid4()
@@ -314,6 +390,16 @@ def test_flood_hazard_api_uses_canonical_envelopes(monkeypatch) -> None:
"unsupported_metrics": ["permanent_water_volume_m3"],
},
)
monkeypatch.setattr(
FloodHazardAnalysisService,
"analyze_partitions",
lambda *_args, **_kwargs: {
"dataset_id": str(output_dataset_id),
"dataset_ids": [str(output_dataset_id)],
"partition_count": 1,
"inundated_cell_count": 4,
},
)
app.dependency_overrides[get_db] = lambda: db
try:
client = TestClient(app)
@@ -326,6 +412,10 @@ def test_flood_hazard_api_uses_canonical_envelopes(monkeypatch) -> None:
f"/api/v1/projects/{project_id}/datasets/{output_dataset_id}/raster/flood-hazard/select",
json={"bbox": flood_payload().bbox.model_dump()},
)
regional_selection = client.post(
f"/api/v1/projects/{project_id}/datasets/raster/flood-hazard/select",
json={"bbox": flood_payload().bbox.model_dump(), "product_key": "pluviaal_current_t100"},
)
finally:
app.dependency_overrides.clear()
@@ -334,6 +424,8 @@ def test_flood_hazard_api_uses_canonical_envelopes(monkeypatch) -> None:
assert acquisition.status_code == 200 and set(acquisition.json()) == {"data"}
assert acquisition.json()["data"]["job_type"] == "raster.flood_hazard.acquire"
assert selection.status_code == 200 and set(selection.json()) == {"data"}
assert regional_selection.status_code == 200 and set(regional_selection.json()) == {"data"}
assert regional_selection.json()["data"]["partition_count"] == 1
assert any(isinstance(item, Job) for item in db.added)
@@ -0,0 +1,52 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_partitioned_raster_routes_are_canonical_and_documented() -> None:
routes = (ROOT / "backend/app/api/routes/datasets.py").read_text(encoding="utf-8")
contracts = (ROOT / "docs/API_CONTRACTS.md").read_text(encoding="utf-8")
for path in (
"/datasets/raster/terrain/select",
"/datasets/raster/flood-hazard/select",
):
assert f'@router.post("{path}", response_model=dict)' in routes
assert path in contracts
assert "envelope(TerrainAnalysisService.analyze_partitions" in routes
assert "envelope(FloodHazardAnalysisService.analyze_partitions" in routes
def test_regional_map_uses_logical_partition_groups_and_exact_analysis() -> None:
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
hook = (ROOT / "frontend/src/hooks/useMapThemeSelectionInsights.ts").read_text(encoding="utf-8")
api = (ROOT / "frontend/src/services/api/datasets.ts").read_text(encoding="utf-8")
assert "regionalScopeSelected" in workspace
assert "rasterPartitionsForDataset" in workspace
assert "imageOverlays={activeImageOverlays}" in workspace
assert "de juiste gemeentelijke rasters worden automatisch gecombineerd" in workspace
assert "selectTerrainPartitions" in hook
assert "selectFloodHazardPartitions" in hook
assert "/datasets/raster/terrain/select" in api
assert "/datasets/raster/flood-hazard/select" in api
def test_maplibre_supports_multiple_persisted_raster_overlays() -> None:
map_source = (ROOT / "frontend/src/components/GeoMap.tsx").read_text(encoding="utf-8")
assert "imageOverlays?: MapImageOverlay[]" in map_source
assert "imageOverlayIdsRef" in map_source
assert "imageOverlays.forEach" in map_source
assert "bounded-raster-" in map_source
def test_regional_analysis_does_not_create_an_authoritative_mosaic() -> None:
service = (ROOT / "backend/app/services/raster_partition_analysis_service.py").read_text(encoding="utf-8")
storage = (ROOT / "docs/STORAGE_ARCHITECTURE.md").read_text(encoding="utf-8")
assert "from rasterio.merge import merge" in service
assert "DatasetService" not in service
assert "12-million-cell limit" in storage
assert "does not create another authoritative raster" in storage