feat: complete governed Walloon coverage sources
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user