diff --git a/backend/app/api/routes/datasets.py b/backend/app/api/routes/datasets.py index f6361bf2..e52bb70f 100644 --- a/backend/app/api/routes/datasets.py +++ b/backend/app/api/routes/datasets.py @@ -331,12 +331,19 @@ def select_vector_features( "limit": payload.limit, } full_dataset_area = False + preclipped_partition_filter = None if selection_area is not None: full_dataset_area = VectorFeatureService.can_use_full_area_fast_path(dataset, selection_area.id) + if not full_dataset_area: + preclipped_partition_filter = VectorFeatureService.preclipped_partition_filter( + dataset, + getattr(selection_area, "name", None), + ) selection_kwargs.update( selection_geometry=selection_area.geometry, selection_area_id=selection_area.id, full_dataset_area=full_dataset_area, + preclipped_partition_filter=preclipped_partition_filter, ) result = VectorFeatureService.select_features_by_bbox(db, **selection_kwargs) if VectorFeatureService.supports_selection_summary(dataset): @@ -348,6 +355,7 @@ def select_vector_features( if selection_area is not None: summary_kwargs["selection_geometry"] = selection_area.geometry summary_kwargs["full_dataset_area"] = full_dataset_area + summary_kwargs["preclipped_partition_filter"] = preclipped_partition_filter result["summary"] = VectorFeatureService.summarize_features_by_bbox(db, **summary_kwargs) return envelope(VectorSelectionResponse(**result).model_dump(exclude_none=True)) diff --git a/backend/app/services/vector_feature_service.py b/backend/app/services/vector_feature_service.py index 04158e07..e7ed4291 100644 --- a/backend/app/services/vector_feature_service.py +++ b/backend/app/services/vector_feature_service.py @@ -37,6 +37,10 @@ SEMANTIC_METRICS_DISABLED_OPERATOR_TOOLS = { "provision_regional_historical_landuse.py", } +PRECLIPPED_MUNICIPALITY_PARTITION_OPERATOR_TOOLS = { + "provision_regional_bwk_natura2000.py", +} + SEMANTIC_SELECTION_METRICS: dict[str, tuple[dict[str, Any], ...]] = { "buildings": ( @@ -181,6 +185,30 @@ class VectorFeatureService: provenance = dataset.provenance_metadata if isinstance(dataset.provenance_metadata, dict) else {} return provenance.get("operator_tool") in FULL_AREA_CLIPPED_OPERATOR_TOOLS + @staticmethod + def preclipped_partition_filter(dataset: Dataset, selection_area_name: str | None) -> tuple[str, str] | None: + provenance = dataset.provenance_metadata if isinstance(dataset.provenance_metadata, dict) else {} + if provenance.get("operator_tool") not in PRECLIPPED_MUNICIPALITY_PARTITION_OPERATOR_TOOLS: + return None + source_metadata = dataset.source_metadata if isinstance(dataset.source_metadata, dict) else {} + if ( + source_metadata.get("partitioned_source_audit") is not True + or source_metadata.get("geometry_clipped_to_area") is not True + ): + return None + normalized_name = str(selection_area_name or "").strip() + prefix = "Gemeente " + suffixes = (" - officiële grens", " - officiele grens") + if not normalized_name.startswith(prefix): + return None + municipality = normalized_name[len(prefix):] + for suffix in suffixes: + if municipality.endswith(suffix): + municipality = municipality[: -len(suffix)] + break + municipality = municipality.strip() + return ("municipality", municipality) if municipality else None + @staticmethod def _feature_row(dataset_id: UUID, feature: dict[str, Any], index: int, feature_class: str | None) -> VectorFeature | None: geometry_payload = feature.get("geometry") @@ -289,6 +317,7 @@ class VectorFeatureService: selection_geometry: Any | None = None, selection_area_id: UUID | None = None, full_dataset_area: bool = False, + preclipped_partition_filter: tuple[str, str] | None = None, ) -> dict[str, Any]: normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox) safe_limit = max(1, min(int(limit), 1000)) @@ -303,7 +332,10 @@ class VectorFeatureService: ) query = db.query(VectorFeature).filter(VectorFeature.dataset_id == dataset_id) - if not full_dataset_area: + if preclipped_partition_filter is not None: + partition_property, partition_value = preclipped_partition_filter + query = query.filter(VectorFeature.properties_json.op("->>")(partition_property) == partition_value) + elif not full_dataset_area: query = query.filter(ST_Intersects(VectorFeature.geometry, selection_shape)) if hasattr(query, "count"): total_feature_count = int(query.count()) @@ -327,6 +359,7 @@ class VectorFeatureService: total_feature_count=total_feature_count, selection_geometry=selection_geometry, full_dataset_area=full_dataset_area, + preclipped_partition_filter=preclipped_partition_filter, ) result = { @@ -354,6 +387,7 @@ class VectorFeatureService: total_feature_count: int | None = None, selection_geometry: Any | None = None, full_dataset_area: bool = False, + preclipped_partition_filter: tuple[str, str] | None = None, ) -> dict[str, Any]: normalized_bbox = VectorFeatureService._normalize_selection_bbox(bbox) selection_shape = selection_geometry @@ -366,8 +400,14 @@ class VectorFeatureService: 4326, ) selection_filter = (VectorFeature.dataset_id == dataset.id,) - if not full_dataset_area: + if preclipped_partition_filter is not None: + partition_property, partition_value = preclipped_partition_filter + selection_filter += ( + VectorFeature.properties_json.op("->>")(partition_property) == partition_value, + ) + elif not full_dataset_area: selection_filter += (ST_Intersects(VectorFeature.geometry, selection_shape),) + selection_is_preclipped = full_dataset_area or preclipped_partition_filter is not None feature_count = total_feature_count if feature_count is None: feature_count = int(db.query(func.count(VectorFeature.id)).filter(*selection_filter).scalar() or 0) @@ -438,7 +478,7 @@ class VectorFeatureService: selection_filter=selection_filter, selection_shape=selection_shape, feature_count=feature_count, - full_dataset_area=full_dataset_area, + full_dataset_area=selection_is_preclipped, ) for metric_config in metric_configs ] diff --git a/backend/tests/test_sprint210_regional_bwk_natura2000.py b/backend/tests/test_sprint210_regional_bwk_natura2000.py index 45bcec34..607d79b3 100644 --- a/backend/tests/test_sprint210_regional_bwk_natura2000.py +++ b/backend/tests/test_sprint210_regional_bwk_natura2000.py @@ -9,6 +9,9 @@ import sys import pytest from shapely.geometry import box, mapping, shape +from app.models import Dataset +from app.services.vector_feature_service import VectorFeatureService + ROOT = Path(__file__).resolve().parents[2] SCRIPTS = ROOT / "scripts" @@ -240,3 +243,28 @@ def test_regional_operator_is_packaged_release_checked_and_exact_area_is_preferr assert '"provision_regional_bwk_natura2000.py"' in service assert "dataset.area_id === selectedAreaId ? 10_000_000" in workspace assert "largestBwkSnapshot" in catalog + + +def test_regional_bwk_uses_only_canonical_preclipped_municipality_partitions() -> None: + dataset = Dataset( + name="regional-bwk.geojson", + dataset_type="vector", + status="ready", + source_metadata={ + "partitioned_source_audit": True, + "geometry_clipped_to_area": True, + }, + provenance_metadata={"operator_tool": "provision_regional_bwk_natura2000.py"}, + ) + + assert VectorFeatureService.preclipped_partition_filter( + dataset, "Gemeente Mol - officiële grens" + ) == ("municipality", "Mol") + assert VectorFeatureService.preclipped_partition_filter( + dataset, "Vervoerregio Kempen - officiële operationele grens" + ) is None + + dataset.provenance_metadata = {"operator_tool": "unrelated_operator.py"} + assert VectorFeatureService.preclipped_partition_filter( + dataset, "Gemeente Mol - officiële grens" + ) is None