feat: complete governed Walloon coverage sources
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-22 03:39:08 +02:00
parent 17d4442e60
commit c2101ea8f9
37 changed files with 1813 additions and 45 deletions
@@ -22,7 +22,9 @@ from app.schemas.temporal import (
TemporalSeriesDataset,
TemporalSeriesRead,
)
from app.schemas.thematic_raster import ThematicRasterSelectionRequest
from app.services.vector_feature_service import VectorFeatureService
from app.services.walous_land_cover_service import WalousLandCoverService
class TemporalAnalysisService:
@@ -31,6 +33,7 @@ class TemporalAnalysisService:
"provision_regional_grb_buildings.py",
"provision_regional_grb_context.py",
}
SUPPORTED_RASTER_TEMPORAL_SOURCES = {WalousLandCoverService.PROVIDER}
@staticmethod
def _canonical_observation_snapshots(datasets: list[Dataset]) -> list[Dataset]:
@@ -141,6 +144,15 @@ class TemporalAnalysisService:
status_code=400,
)
if earlier.dataset_type == "raster" or later.dataset_type == "raster":
return TemporalAnalysisService._compare_walous_rasters(
db,
project_id=project_id,
payload=payload,
earlier=earlier,
later=later,
)
bbox = payload.bbox.model_dump()
selection_area = TemporalAnalysisService._get_selection_area(db, project_id, payload.area_id)
selection_geometry = None
@@ -259,6 +271,90 @@ class TemporalAnalysisService:
raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404)
return area
@staticmethod
def _compare_walous_rasters(
db: Session,
*,
project_id: UUID,
payload: TemporalComparisonRequest,
earlier: Dataset,
later: Dataset,
) -> TemporalComparisonResponse:
if {
earlier.dataset_type,
later.dataset_type,
} != {"raster"} or earlier.source_name != WalousLandCoverService.PROVIDER or later.source_name != WalousLandCoverService.PROVIDER:
raise AppError(
code="INCOMPATIBLE_TEMPORAL_DATASET_TYPES",
message="Raster evolution currently supports only two governed WALOUS land-cover snapshots",
status_code=400,
)
request = ThematicRasterSelectionRequest(bbox=payload.bbox, area_id=payload.area_id)
summaries: dict[UUID, dict[str, Any]] = {}
def summarize(dataset: Dataset) -> dict[str, Any]:
cached = summaries.get(dataset.id)
if cached is not None:
return cached
result = WalousLandCoverService.analyze(db, project_id, dataset.id, request)
summary = dict(result["summary"])
summary["warning"] = result.get("limitation_message")
summaries[dataset.id] = summary
return summary
earlier_summary = summarize(earlier)
later_summary = summarize(later)
metric_comparisons = TemporalAnalysisService._compare_summary_metrics(earlier_summary, later_summary)
if not metric_comparisons:
raise AppError(
code="INCOMPATIBLE_TEMPORAL_AGGREGATION",
message="WALOUS snapshots use incompatible aggregation semantics",
status_code=400,
)
primary_key = str(later_summary.get("primary_metric_key") or metric_comparisons[0].metric_key)
primary_metric = next(
(metric for metric in metric_comparisons if metric.metric_key == primary_key),
metric_comparisons[0],
)
timeline = TemporalAnalysisService._build_timeline(
db,
project_id=project_id,
series_key=str(earlier.temporal_series_key),
fallback_datasets=[earlier, later],
summarize=summarize,
)
warnings = [
"WALOUS-evolutie vergelijkt celgebaseerde landbedekkingsoppervlakten; individuele objectwijzigingen zijn niet beschikbaar.",
]
limitation = str(later_summary.get("warning") or earlier_summary.get("warning") or "").strip()
if limitation:
warnings.append(limitation)
return TemporalComparisonResponse(
temporal_series_key=str(earlier.temporal_series_key),
earlier=TemporalDatasetRef(
id=earlier.id,
name=earlier.name,
observed_at=earlier.observed_at,
source_version=earlier.source_version,
),
later=TemporalDatasetRef(
id=later.id,
name=later.name,
observed_at=later.observed_at,
source_version=later.source_version,
),
selection_bbox=payload.bbox,
selection_area_id=payload.area_id,
metric=primary_metric,
metrics=metric_comparisons,
timeline=timeline,
object_changes=TemporalObjectChanges(available=False),
geojson={"type": "FeatureCollection", "features": []},
warnings=warnings,
generated_at=datetime.now(timezone.utc),
)
@staticmethod
def _summary_metrics(summary: dict[str, Any]) -> list[dict[str, Any]]:
configured = summary.get("metrics")
@@ -370,10 +466,15 @@ class TemporalAnalysisService:
dataset = db.get(Dataset, dataset_id)
if not dataset or dataset.project_id != project_id:
raise AppError(code="DATASET_NOT_FOUND", message=f"{label} dataset not found", status_code=404)
if dataset.dataset_type not in {"vector", "geojson"}:
supported_vector = dataset.dataset_type in {"vector", "geojson"}
supported_raster = (
dataset.dataset_type == "raster"
and dataset.source_name in TemporalAnalysisService.SUPPORTED_RASTER_TEMPORAL_SOURCES
)
if not supported_vector and not supported_raster:
raise AppError(
code="DATASET_NOT_VECTOR",
message="Temporal selection comparison currently requires vector datasets",
code="TEMPORAL_DATASET_NOT_SUPPORTED",
message="Temporal comparison requires a vector series or a governed WALOUS raster series",
status_code=400,
)
if not dataset.temporal_series_key or not dataset.observed_at: