feat: complete Wallonia land cover and terrain sources
This commit is contained in:
@@ -59,6 +59,8 @@ def test_configured_yolo_and_active_asset_are_selected_without_hiding_limitation
|
||||
assert "getYoloPreflight" in hook
|
||||
assert 'aria-label="Status gebouwdetectie"' in lab
|
||||
assert "Nog niet nationaal gevalideerd" in lab
|
||||
assert "selectedDetectionModel?.nationally_validated !== true" in lab
|
||||
assert "selectedDetectionModel?.validation_scope" in lab
|
||||
assert "vereisen lokale referentiedata en QA" in lab
|
||||
assert "Modelkalibratie voor beheerders" in lab
|
||||
|
||||
|
||||
@@ -232,6 +232,10 @@ def test_yolo_configured_model_reports_configured_with_local_model_and_dependenc
|
||||
assert model.configured is True
|
||||
assert model.status == "configured"
|
||||
assert model.version == settings.yolo_model_version
|
||||
assert model.nationally_validated is False
|
||||
assert model.operator_review_required is True
|
||||
assert model.validated_regions == ["flanders_mol_kempen"]
|
||||
assert "Mol and the Kempen" in (model.validation_scope or "")
|
||||
|
||||
|
||||
def test_yolo_dependency_check_uses_real_imports_not_find_spec() -> None:
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from uuid import uuid4
|
||||
|
||||
import numpy as np
|
||||
from fastapi.testclient import TestClient
|
||||
from pyproj import Transformer
|
||||
import rasterio
|
||||
from rasterio.transform import from_origin
|
||||
|
||||
from app.core.config import Settings
|
||||
from app.db.session import get_db
|
||||
from app.main import app
|
||||
from app.models import Dataset, Job, Project
|
||||
from app.schemas.dhmv import TerrainSelectionRequest
|
||||
from app.schemas.spw_terrain import SpwTerrainAcquireRequest
|
||||
from app.services.dataset_service import DatasetService
|
||||
from app.services.spw_terrain_service import SpwTerrainService
|
||||
from app.services.terrain_analysis_service import TerrainAnalysisService
|
||||
|
||||
|
||||
class FakeQuery:
|
||||
def filter(self, *_args):
|
||||
return self
|
||||
|
||||
def order_by(self, *_args):
|
||||
return self
|
||||
|
||||
def first(self):
|
||||
return None
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self, project, dataset=None):
|
||||
self.project = project
|
||||
self.dataset = dataset
|
||||
self.added = []
|
||||
|
||||
def get(self, model, row_id):
|
||||
if model is Project and row_id == self.project.id:
|
||||
return self.project
|
||||
if model is Dataset and self.dataset is not None and row_id == self.dataset.id:
|
||||
return self.dataset
|
||||
return next(
|
||||
(
|
||||
item
|
||||
for item in self.added
|
||||
if isinstance(item, model) and item.id == row_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
def query(self, _model):
|
||||
return FakeQuery()
|
||||
|
||||
def add(self, row):
|
||||
self.added.append(row)
|
||||
|
||||
def commit(self):
|
||||
return None
|
||||
|
||||
def rollback(self):
|
||||
return None
|
||||
|
||||
def refresh(self, row):
|
||||
return row
|
||||
|
||||
|
||||
def settings(source_dir: Path) -> Settings:
|
||||
return Settings(
|
||||
_env_file=None,
|
||||
SPW_TERRAIN_SOURCE_DIR=str(source_dir),
|
||||
SPW_TERRAIN_ANALYSIS_RESOLUTION_M=5,
|
||||
SPW_TERRAIN_MAX_SIDE_M=20_000,
|
||||
SPW_TERRAIN_MAX_PIXELS=1_000_000,
|
||||
DHMV_MAX_PIXELS=1_000_000,
|
||||
)
|
||||
|
||||
|
||||
def make_source(path: Path) -> list[float]:
|
||||
to_3812 = Transformer.from_crs("EPSG:4326", "EPSG:3812", always_xy=True)
|
||||
to_4326 = Transformer.from_crs("EPSG:3812", "EPSG:4326", always_xy=True)
|
||||
x, y = to_3812.transform(4.85, 50.45)
|
||||
values = np.linspace(100.0, 125.0, 40_000, dtype="float32").reshape(200, 200)
|
||||
with rasterio.open(
|
||||
path,
|
||||
"w",
|
||||
driver="GTiff",
|
||||
width=200,
|
||||
height=200,
|
||||
count=1,
|
||||
dtype="float32",
|
||||
crs="EPSG:3812",
|
||||
transform=from_origin(x, y + 200, 1, 1),
|
||||
nodata=-9999.0,
|
||||
) as target:
|
||||
target.write(values, 1)
|
||||
min_lon, min_lat = to_4326.transform(x, y)
|
||||
max_lon, max_lat = to_4326.transform(x + 200, y + 200)
|
||||
return [min_lon, min_lat, max_lon, max_lat]
|
||||
|
||||
|
||||
def test_spw_terrain_registry_reports_real_source_state(tmp_path: Path) -> None:
|
||||
before = SpwTerrainService.list_products(settings=settings(tmp_path))[0]
|
||||
assert before["status"] == "source_not_provisioned"
|
||||
|
||||
make_source(tmp_path / SpwTerrainService.SOURCE_FILENAME)
|
||||
after = SpwTerrainService.list_products(settings=settings(tmp_path))[0]
|
||||
|
||||
assert after["configured"] is True
|
||||
assert after["coverage_zones"] == ["wallonia"]
|
||||
assert after["source_crs"] == "EPSG:3812"
|
||||
assert after["vertical_reference"].endswith("(EPSG:5710)")
|
||||
|
||||
|
||||
def test_spw_terrain_acquisition_persists_bounded_dng_raster_and_provenance(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
bbox = make_source(tmp_path / SpwTerrainService.SOURCE_FILENAME)
|
||||
project = Project(id=uuid4(), name="Belgium")
|
||||
captured = {}
|
||||
|
||||
def persist(_db, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return SimpleNamespace(id=uuid4())
|
||||
|
||||
monkeypatch.setattr(DatasetService, "import_raster_bytes", persist)
|
||||
result = SpwTerrainService.acquire(
|
||||
FakeSession(project),
|
||||
project.id,
|
||||
SpwTerrainAcquireRequest(
|
||||
bbox={
|
||||
"min_x": bbox[0],
|
||||
"min_y": bbox[1],
|
||||
"max_x": bbox[2],
|
||||
"max_y": bbox[3],
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
resolution_m=5,
|
||||
force_refresh=True,
|
||||
),
|
||||
settings=settings(tmp_path),
|
||||
)
|
||||
|
||||
assert result["provider"] == "spw_terrain"
|
||||
assert result["resolution_m"] == 5
|
||||
assert result["valid_pixel_count"] > 0
|
||||
assert captured["source_metadata"]["vertical_unit_label"] == "m DNG"
|
||||
assert captured["source_metadata"]["coverage_zones"] == ["wallonia"]
|
||||
assert captured["provenance_metadata"]["resampling"] == "bilinear"
|
||||
assert captured["valid_from"].date().isoformat() == "2021-02-19"
|
||||
with rasterio.MemoryFile(captured["content"]) as memory:
|
||||
with memory.open() as derived:
|
||||
assert derived.crs.to_epsg() == 3812
|
||||
assert derived.res == (5.0, 5.0)
|
||||
assert derived.nodata == -9999.0
|
||||
|
||||
|
||||
def test_terrain_analysis_preserves_spw_vertical_datum_and_limitations(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
bbox = make_source(tmp_path / "derived.tif")
|
||||
project = Project(id=uuid4(), name="Belgium")
|
||||
dataset = Dataset(
|
||||
id=uuid4(),
|
||||
project_id=project.id,
|
||||
name="derived.tif",
|
||||
dataset_type="raster",
|
||||
source="SPW MNT",
|
||||
source_name="spw_terrain",
|
||||
source_metadata={
|
||||
"product_key": SpwTerrainService.PRODUCT_KEY,
|
||||
"surface_model": "terrain",
|
||||
"vertical_reference": SpwTerrainService.VERTICAL_REFERENCE,
|
||||
"vertical_unit_label": "m DNG",
|
||||
"limitation_message": SpwTerrainService.LIMITATION,
|
||||
},
|
||||
storage_path=str(tmp_path / "derived.tif"),
|
||||
status="ready",
|
||||
)
|
||||
result = TerrainAnalysisService.analyze(
|
||||
FakeSession(project, dataset),
|
||||
project.id,
|
||||
dataset.id,
|
||||
TerrainSelectionRequest(
|
||||
bbox={
|
||||
"min_x": bbox[0],
|
||||
"min_y": bbox[1],
|
||||
"max_x": bbox[2],
|
||||
"max_y": bbox[3],
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
),
|
||||
settings=settings(tmp_path),
|
||||
)
|
||||
|
||||
assert result["vertical_reference"] == SpwTerrainService.VERTICAL_REFERENCE
|
||||
assert result["summary"]["metric_unit"] == "m DNG"
|
||||
assert result["summary"]["metrics"][0]["metric_unit"] == "m DNG"
|
||||
assert result["limitation_message"] == SpwTerrainService.LIMITATION
|
||||
assert result["unsupported_metrics"] == ["water_depth_m", "water_volume_m3"]
|
||||
|
||||
|
||||
def test_spw_terrain_routes_use_canonical_envelopes(monkeypatch) -> None:
|
||||
project = Project(id=uuid4(), name="Belgium")
|
||||
db = FakeSession(project)
|
||||
monkeypatch.setattr(
|
||||
SpwTerrainService,
|
||||
"acquire",
|
||||
lambda *_args, **_kwargs: {
|
||||
"output_dataset_id": str(uuid4()),
|
||||
"provider": SpwTerrainService.PROVIDER,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
SpwTerrainService,
|
||||
"list_products",
|
||||
lambda *_args, **_kwargs: [
|
||||
{
|
||||
"key": SpwTerrainService.PRODUCT_KEY,
|
||||
"display_name": SpwTerrainService.DISPLAY_NAME,
|
||||
"surface_model": "terrain",
|
||||
"source_filename": SpwTerrainService.SOURCE_FILENAME,
|
||||
"native_resolution_m": 1,
|
||||
"analysis_resolution_m": 5,
|
||||
"source_crs": "EPSG:3812",
|
||||
"vertical_reference": SpwTerrainService.VERTICAL_REFERENCE,
|
||||
"acquisition_period": SpwTerrainService.ACQUISITION_PERIOD,
|
||||
"catalog_url": SpwTerrainService.CATALOG_URL,
|
||||
"attribution": SpwTerrainService.ATTRIBUTION,
|
||||
"license_note": SpwTerrainService.LICENSE_NOTE,
|
||||
"limitation_message": SpwTerrainService.LIMITATION,
|
||||
"coverage_zones": ["wallonia"],
|
||||
"configured": True,
|
||||
"status": "configured",
|
||||
}
|
||||
],
|
||||
)
|
||||
app.dependency_overrides[get_db] = lambda: db
|
||||
try:
|
||||
client = TestClient(app)
|
||||
products = client.get(
|
||||
f"/api/v1/projects/{project.id}/datasets/spw-terrain/products"
|
||||
)
|
||||
acquisition = client.post(
|
||||
f"/api/v1/projects/{project.id}/datasets/spw-terrain/acquire",
|
||||
json={
|
||||
"bbox": {
|
||||
"min_x": 4.8,
|
||||
"min_y": 50.4,
|
||||
"max_x": 4.9,
|
||||
"max_y": 50.5,
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
"product_key": SpwTerrainService.PRODUCT_KEY,
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert products.status_code == 200 and products.json()["data"]["total"] == 1
|
||||
assert (
|
||||
acquisition.status_code == 200
|
||||
and acquisition.json()["data"]["job_type"] == "raster.spw-terrain.acquire"
|
||||
)
|
||||
assert any(isinstance(item, Job) for item in db.added)
|
||||
@@ -16,7 +16,10 @@ from app.core.config import Settings
|
||||
from app.db.session import get_db
|
||||
from app.main import app
|
||||
from app.models import Dataset, Job, Project
|
||||
from app.schemas.thematic_raster import ThematicRasterAcquireRequest, ThematicRasterSelectionRequest
|
||||
from app.schemas.thematic_raster import (
|
||||
ThematicRasterAcquireRequest,
|
||||
ThematicRasterSelectionRequest,
|
||||
)
|
||||
from app.schemas.temporal import TemporalComparisonRequest
|
||||
from app.services.dataset_service import DatasetService
|
||||
from app.services.temporal_analysis_service import TemporalAnalysisService
|
||||
@@ -24,8 +27,12 @@ from app.services.walous_land_cover_service import WalousLandCoverService
|
||||
|
||||
|
||||
def load_provisioner():
|
||||
path = Path(__file__).resolve().parents[2] / "scripts" / "provision_walous_sources.py"
|
||||
spec = importlib.util.spec_from_file_location("walous_source_provisioner_test", path)
|
||||
path = (
|
||||
Path(__file__).resolve().parents[2] / "scripts" / "provision_walous_sources.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"walous_source_provisioner_test", path
|
||||
)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
@@ -54,7 +61,14 @@ class FakeSession:
|
||||
return self.project
|
||||
if model is Dataset and self.dataset is not None and row_id == self.dataset.id:
|
||||
return self.dataset
|
||||
match = next((item for item in self.added if isinstance(item, model) and item.id == row_id), None)
|
||||
match = next(
|
||||
(
|
||||
item
|
||||
for item in self.added
|
||||
if isinstance(item, model) and item.id == row_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if match is not None:
|
||||
return match
|
||||
return None
|
||||
@@ -80,12 +94,13 @@ def make_source(
|
||||
*,
|
||||
dtype: str = "uint8",
|
||||
nodata: int = 255,
|
||||
class_codes: list[int] | None = None,
|
||||
) -> tuple[list[float], np.ndarray]:
|
||||
to_3812 = Transformer.from_crs("EPSG:4326", "EPSG:3812", always_xy=True)
|
||||
to_4326 = Transformer.from_crs("EPSG:3812", "EPSG:4326", always_xy=True)
|
||||
x, y = to_3812.transform(4.85, 50.45)
|
||||
transform = from_origin(x, y + 100, 1, 1)
|
||||
class_codes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
class_codes = class_codes or [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
values = np.empty((100, len(class_codes) * 20), dtype=dtype)
|
||||
for index, class_code in enumerate(class_codes):
|
||||
values[:, index * 20 : (index + 1) * 20] = class_code
|
||||
@@ -118,20 +133,40 @@ def settings(source_dir: Path) -> Settings:
|
||||
|
||||
|
||||
def test_walous_registry_reports_real_provisioning_state(tmp_path: Path) -> None:
|
||||
before = {item["key"]: item for item in WalousLandCoverService.list_products(settings=settings(tmp_path))}
|
||||
before = {
|
||||
item["key"]: item
|
||||
for item in WalousLandCoverService.list_products(settings=settings(tmp_path))
|
||||
}
|
||||
assert before["walous_land_cover_2023"]["status"] == "source_not_provisioned"
|
||||
make_source(tmp_path / "walous_land_cover_2023_3812.tif")
|
||||
after = {item["key"]: item for item in WalousLandCoverService.list_products(settings=settings(tmp_path))}
|
||||
after = {
|
||||
item["key"]: item
|
||||
for item in WalousLandCoverService.list_products(settings=settings(tmp_path))
|
||||
}
|
||||
assert after["walous_land_cover_2023"]["configured"] is True
|
||||
assert after["walous_land_cover_2023"]["source_crs"] == "EPSG:3812"
|
||||
assert after["walous_land_cover_2023"]["native_resolution_m"] == 1.0
|
||||
assert after["walous_land_cover_2023"]["analysis_resolution_m"] == 10.0
|
||||
assert after["walous_land_cover_2023"]["coverage_zones"] == ["wallonia"]
|
||||
assert after["walous_land_cover_2023"]["included_source_values"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
assert after["walous_land_cover_2023"]["included_source_values"] == [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
80,
|
||||
90,
|
||||
]
|
||||
assert after["walous_land_cover_2023"]["source_value_unit"] == "walous_class_code"
|
||||
|
||||
|
||||
def test_walous_provisioner_accepts_official_non_contiguous_class_codes(tmp_path: Path) -> None:
|
||||
def test_walous_provisioner_accepts_official_non_contiguous_class_codes(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source_path = tmp_path / "walous_land_cover_2023_3812.tif"
|
||||
make_source(source_path)
|
||||
|
||||
@@ -140,7 +175,86 @@ def test_walous_provisioner_accepts_official_non_contiguous_class_codes(tmp_path
|
||||
assert validation["sample_classes"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
|
||||
|
||||
def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path: Path, monkeypatch) -> None:
|
||||
def test_walous_2018_registry_and_provisioner_accept_official_stacked_classes(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source_codes = sorted(WalousLandCoverService.WALOUS_2018_CLASS_CROSSWALK)
|
||||
source_path = tmp_path / "walous_land_cover_2018_3812.tif"
|
||||
make_source(source_path, class_codes=source_codes)
|
||||
|
||||
validation = load_provisioner().validate_raster(source_path)
|
||||
registry = {
|
||||
item["key"]: item
|
||||
for item in WalousLandCoverService.list_products(settings=settings(tmp_path))
|
||||
}
|
||||
|
||||
assert validation["sample_classes"] == source_codes
|
||||
assert validation["implicit_source_nodata_values"] == [0]
|
||||
assert registry["walous_land_cover_2018"]["configured"] is True
|
||||
assert registry["walous_land_cover_2018"]["observation_year"] == 2018
|
||||
assert "crosswalk" in registry["walous_land_cover_2018"]["limitation_message"]
|
||||
|
||||
|
||||
def test_walous_2018_acquisition_normalizes_stacked_classes_with_explicit_provenance(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
source_codes = sorted(WalousLandCoverService.WALOUS_2018_CLASS_CROSSWALK)
|
||||
bbox, _values = make_source(
|
||||
tmp_path / "walous_land_cover_2018_3812.tif",
|
||||
class_codes=source_codes,
|
||||
)
|
||||
project = Project(id=uuid4(), name="Belgium")
|
||||
captured = {}
|
||||
|
||||
def persist(_db, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return SimpleNamespace(id=uuid4())
|
||||
|
||||
monkeypatch.setattr(DatasetService, "import_raster_bytes", persist)
|
||||
result = WalousLandCoverService.acquire(
|
||||
FakeSession(project),
|
||||
project.id,
|
||||
ThematicRasterAcquireRequest(
|
||||
bbox={
|
||||
"min_x": bbox[0],
|
||||
"min_y": bbox[1],
|
||||
"max_x": bbox[2],
|
||||
"max_y": bbox[3],
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
product_key="walous_land_cover_2018",
|
||||
force_refresh=True,
|
||||
),
|
||||
settings=settings(tmp_path),
|
||||
)
|
||||
|
||||
assert result["observation_year"] == 2018
|
||||
assert captured["source_metadata"]["classes_present"] == [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
80,
|
||||
90,
|
||||
]
|
||||
assert captured["source_metadata"]["source_classes_present"] == source_codes
|
||||
assert captured["source_metadata"]["class_crosswalk"][62] == 2
|
||||
assert captured["source_metadata"]["class_crosswalk"][0] == 255
|
||||
assert (
|
||||
captured["source_metadata"]["attribution"]
|
||||
== "Service public de Wallonie (SPW), UCLouvain, ULB, ISSeP"
|
||||
)
|
||||
assert captured["observed_at"].date().isoformat() == "2018-12-31"
|
||||
|
||||
|
||||
def test_walous_acquisition_reads_real_classes_and_persists_provenance(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
bbox, _values = make_source(tmp_path / "walous_land_cover_2023_3812.tif")
|
||||
project = Project(id=uuid4(), name="Belgium")
|
||||
db = FakeSession(project)
|
||||
@@ -156,7 +270,13 @@ def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path:
|
||||
db,
|
||||
project.id,
|
||||
ThematicRasterAcquireRequest(
|
||||
bbox={"min_x": bbox[0], "min_y": bbox[1], "max_x": bbox[2], "max_y": bbox[3], "crs": "EPSG:4326"},
|
||||
bbox={
|
||||
"min_x": bbox[0],
|
||||
"min_y": bbox[1],
|
||||
"max_x": bbox[2],
|
||||
"max_y": bbox[3],
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
product_key="walous_land_cover_2023",
|
||||
force_refresh=True,
|
||||
),
|
||||
@@ -166,7 +286,19 @@ def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path:
|
||||
assert result["output_dataset_id"] == str(output_id)
|
||||
assert result["resolution_m"] == 10
|
||||
assert captured["source_name"] == "spw_walous_land_cover"
|
||||
assert captured["source_metadata"]["classes_present"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
assert captured["source_metadata"]["classes_present"] == [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
80,
|
||||
90,
|
||||
]
|
||||
assert captured["provenance_metadata"]["resampling"] == "nearest"
|
||||
assert captured["temporal_series_key"].startswith("spw:walous:land-cover:")
|
||||
assert captured["observed_at"].date().isoformat() == "2023-06-25"
|
||||
@@ -174,7 +306,9 @@ def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path:
|
||||
assert captured["valid_to"] == captured["observed_at"]
|
||||
|
||||
|
||||
def test_walous_acquisition_accepts_official_signed_int8_nodata(tmp_path: Path, monkeypatch) -> None:
|
||||
def test_walous_acquisition_accepts_official_signed_int8_nodata(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
bbox, _values = make_source(
|
||||
tmp_path / "walous_land_cover_2023_3812.tif",
|
||||
dtype="int8",
|
||||
@@ -193,7 +327,13 @@ def test_walous_acquisition_accepts_official_signed_int8_nodata(tmp_path: Path,
|
||||
FakeSession(project),
|
||||
project.id,
|
||||
ThematicRasterAcquireRequest(
|
||||
bbox={"min_x": bbox[0], "min_y": bbox[1], "max_x": bbox[2], "max_y": bbox[3], "crs": "EPSG:4326"},
|
||||
bbox={
|
||||
"min_x": bbox[0],
|
||||
"min_y": bbox[1],
|
||||
"max_x": bbox[2],
|
||||
"max_y": bbox[3],
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
product_key="walous_land_cover_2023",
|
||||
force_refresh=True,
|
||||
),
|
||||
@@ -201,14 +341,28 @@ def test_walous_acquisition_accepts_official_signed_int8_nodata(tmp_path: Path,
|
||||
)
|
||||
|
||||
assert result["output_dataset_id"] == str(output_id)
|
||||
assert captured["source_metadata"]["classes_present"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
assert captured["source_metadata"]["classes_present"] == [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
80,
|
||||
90,
|
||||
]
|
||||
with rasterio.MemoryFile(captured["content"]) as memory:
|
||||
with memory.open() as derived:
|
||||
assert derived.dtypes == ("uint8",)
|
||||
assert derived.nodata == 255
|
||||
|
||||
|
||||
def test_walous_analysis_returns_semantic_area_metrics(tmp_path: Path, monkeypatch) -> None:
|
||||
def test_walous_analysis_returns_semantic_area_metrics(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
bbox, _values = make_source(tmp_path / "walous_land_cover_2023_3812.tif")
|
||||
project = Project(id=uuid4(), name="Belgium")
|
||||
output_id = uuid4()
|
||||
@@ -221,7 +375,13 @@ def test_walous_analysis_returns_semantic_area_metrics(tmp_path: Path, monkeypat
|
||||
monkeypatch.setattr(DatasetService, "import_raster_bytes", persist)
|
||||
db = FakeSession(project)
|
||||
payload = ThematicRasterAcquireRequest(
|
||||
bbox={"min_x": bbox[0], "min_y": bbox[1], "max_x": bbox[2], "max_y": bbox[3], "crs": "EPSG:4326"},
|
||||
bbox={
|
||||
"min_x": bbox[0],
|
||||
"min_y": bbox[1],
|
||||
"max_x": bbox[2],
|
||||
"max_y": bbox[3],
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
product_key="walous_land_cover_2023",
|
||||
force_refresh=True,
|
||||
)
|
||||
@@ -247,7 +407,10 @@ def test_walous_analysis_returns_semantic_area_metrics(tmp_path: Path, monkeypat
|
||||
output_id,
|
||||
ThematicRasterSelectionRequest(bbox=payload.bbox),
|
||||
)
|
||||
metrics = {item["metric_key"]: item["metric_value"] for item in result["summary"]["metrics"]}
|
||||
metrics = {
|
||||
item["metric_key"]: item["metric_value"]
|
||||
for item in result["summary"]["metrics"]
|
||||
}
|
||||
|
||||
assert result["metric_kind"] == "categorical_area"
|
||||
assert metrics["land_cover_observed_area_ha"] > 0
|
||||
@@ -270,7 +433,10 @@ def test_walous_render_png_uses_governed_class_colours(tmp_path: Path) -> None:
|
||||
dataset_type="raster",
|
||||
source="SPW WALOUS",
|
||||
source_name="spw_walous_land_cover",
|
||||
source_metadata={"product_key": "walous_land_cover_2023", "bbox_epsg4326": bbox},
|
||||
source_metadata={
|
||||
"product_key": "walous_land_cover_2023",
|
||||
"bbox_epsg4326": bbox,
|
||||
},
|
||||
storage_path=str(tmp_path / "walous_land_cover_2023_3812.tif"),
|
||||
status="ready",
|
||||
)
|
||||
@@ -281,7 +447,9 @@ def test_walous_render_png_uses_governed_class_colours(tmp_path: Path) -> None:
|
||||
assert rendered.startswith(b"\x89PNG\r\n\x1a\n")
|
||||
|
||||
|
||||
def test_walous_temporal_comparison_reuses_persisted_raster_metrics(monkeypatch) -> None:
|
||||
def test_walous_temporal_comparison_reuses_persisted_raster_metrics(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
project_id = uuid4()
|
||||
earlier = Dataset(
|
||||
id=uuid4(),
|
||||
@@ -320,14 +488,16 @@ def test_walous_temporal_comparison_reuses_persisted_raster_metrics(monkeypatch)
|
||||
"metric_unit": "ha",
|
||||
"aggregation_method": "nearest_resampled_cells_times_cell_area",
|
||||
"primary_metric_key": "land_cover_observed_area_ha",
|
||||
"metrics": [{
|
||||
"metric_key": "land_cover_observed_area_ha",
|
||||
"metric_label": "Gekarteerde landbedekking",
|
||||
"metric_value": value,
|
||||
"metric_unit": "ha",
|
||||
"aggregation_method": "nearest_resampled_cells_times_cell_area",
|
||||
"is_estimate": True,
|
||||
}],
|
||||
"metrics": [
|
||||
{
|
||||
"metric_key": "land_cover_observed_area_ha",
|
||||
"metric_label": "Gekarteerde landbedekking",
|
||||
"metric_value": value,
|
||||
"metric_unit": "ha",
|
||||
"aggregation_method": "nearest_resampled_cells_times_cell_area",
|
||||
"is_estimate": True,
|
||||
}
|
||||
],
|
||||
},
|
||||
"limitation_message": "Cell-based estimate.",
|
||||
}
|
||||
@@ -336,10 +506,18 @@ def test_walous_temporal_comparison_reuses_persisted_raster_metrics(monkeypatch)
|
||||
payload = TemporalComparisonRequest(
|
||||
earlier_dataset_id=earlier.id,
|
||||
later_dataset_id=later.id,
|
||||
bbox={"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"},
|
||||
bbox={
|
||||
"min_x": 4.8,
|
||||
"min_y": 50.4,
|
||||
"max_x": 4.9,
|
||||
"max_y": 50.5,
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
)
|
||||
|
||||
result = TemporalAnalysisService.compare(TemporalSession(), project_id=project_id, payload=payload)
|
||||
result = TemporalAnalysisService.compare(
|
||||
TemporalSession(), project_id=project_id, payload=payload
|
||||
)
|
||||
|
||||
assert result.metric.earlier_value == 4.0
|
||||
assert result.metric.later_value == 5.5
|
||||
@@ -354,7 +532,10 @@ def test_walous_api_routes_use_canonical_envelopes(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
WalousLandCoverService,
|
||||
"acquire",
|
||||
lambda *_args, **_kwargs: {"output_dataset_id": str(dataset_id), "provider": WalousLandCoverService.PROVIDER},
|
||||
lambda *_args, **_kwargs: {
|
||||
"output_dataset_id": str(dataset_id),
|
||||
"provider": WalousLandCoverService.PROVIDER,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
WalousLandCoverService,
|
||||
@@ -364,7 +545,13 @@ def test_walous_api_routes_use_canonical_envelopes(monkeypatch) -> None:
|
||||
"product_key": "walous_land_cover_2023",
|
||||
"theme": "land_cover_use",
|
||||
"metric_kind": "categorical_area",
|
||||
"selection_bbox": {"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"},
|
||||
"selection_bbox": {
|
||||
"min_x": 4.8,
|
||||
"min_y": 50.4,
|
||||
"max_x": 4.9,
|
||||
"max_y": 50.5,
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
"selected_cell_count": 100,
|
||||
"valid_cell_count": 100,
|
||||
"coverage_ratio": 1.0,
|
||||
@@ -390,20 +577,37 @@ def test_walous_api_routes_use_canonical_envelopes(monkeypatch) -> None:
|
||||
acquisition = client.post(
|
||||
f"/api/v1/projects/{project.id}/datasets/walous/acquire",
|
||||
json={
|
||||
"bbox": {"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"},
|
||||
"bbox": {
|
||||
"min_x": 4.8,
|
||||
"min_y": 50.4,
|
||||
"max_x": 4.9,
|
||||
"max_y": 50.5,
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
"product_key": "walous_land_cover_2023",
|
||||
},
|
||||
)
|
||||
selection = client.post(
|
||||
f"/api/v1/projects/{project.id}/datasets/{dataset_id}/raster/walous/select",
|
||||
json={"bbox": {"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"}},
|
||||
json={
|
||||
"bbox": {
|
||||
"min_x": 4.8,
|
||||
"min_y": 50.4,
|
||||
"max_x": 4.9,
|
||||
"max_y": 50.5,
|
||||
"crs": "EPSG:4326",
|
||||
}
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert products.status_code == 200 and set(products.json()) == {"data"}
|
||||
assert products.json()["data"]["total"] == 2
|
||||
assert products.json()["data"]["total"] == 3
|
||||
assert acquisition.status_code == 200 and set(acquisition.json()) == {"data"}
|
||||
assert acquisition.json()["data"]["job_type"] == "raster.walous.acquire"
|
||||
assert selection.status_code == 200 and selection.json()["data"]["theme"] == "land_cover_use"
|
||||
assert (
|
||||
selection.status_code == 200
|
||||
and selection.json()["data"]["theme"] == "land_cover_use"
|
||||
)
|
||||
assert any(isinstance(item, Job) for item in db.added)
|
||||
|
||||
Reference in New Issue
Block a user