from __future__ import annotations import importlib.util import json import sys from pathlib import Path from uuid import uuid4 import pytest from shapely.geometry import box, shape from shapely.ops import transform as transform_geometry from app.models import Dataset from app.schemas.operations import VectorSelectionSummary from app.services.vector_feature_service import VectorFeatureService ROOT = Path(__file__).resolve().parents[2] BBOX = {"min_x": 5.0, "min_y": 51.1, "max_x": 5.2, "max_y": 51.3, "crs": "EPSG:4326"} def load_operator(): script_path = ROOT / "scripts" / "provision_mol_bwk_natura2000.py" spec = importlib.util.spec_from_file_location("bwk_natura2000_operator", script_path) assert spec is not None assert spec.loader is not None module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module class FakeResponse: ok = True status_code = 200 text = "" def __init__(self, payload: dict, url: str): self.payload = payload self.url = url self.content = json.dumps(payload).encode("utf-8") def raise_for_status(self): return None def json(self): return self.payload class FakeSession: def __init__(self, responses: list[FakeResponse]): self.responses = iter(responses) self.calls: list[tuple[str, dict | None]] = [] def get(self, url, *, params=None, timeout): # noqa: ANN001, ARG002 self.calls.append((url, params)) return next(self.responses) class ScalarQuery: def __init__(self, value: float): self.value = value def filter(self, *args): # noqa: ANN002, ARG002 return self def scalar(self): return self.value class SequenceScalarSession: def __init__(self, values: list[float]): self.values = iter(values) def query(self, *args): # noqa: ANN002, ARG002 return ScalarQuery(next(self.values)) def test_official_bwk_contract_and_class_labels_are_fixed() -> None: module = load_operator() assert module.WFS_URL == "https://geo.api.vlaanderen.be/BWK/wfs" assert module.TYPE_NAME == "BWK:Bwkhab" assert module.SOURCE_VERSION == "2025" assert module.ATTRIBUTION == "Bron: INBO" assert module.EVALUATION_LABELS == { "z": "Biologisch zeer waardevol", "w": "Biologisch waardevol", "m": "Biologisch minder waardevol", "wz": "Complex van waardevolle en zeer waardevolle elementen", "mwz": "Complex van minder waardevolle, waardevolle en zeer waardevolle elementen", "mz": "Complex van minder waardevolle en zeer waardevolle elementen", "mw": "Complex van minder waardevolle en waardevolle elementen", } def test_wfs_pagination_follows_server_next_links() -> None: module = load_operator() page_one = { "type": "FeatureCollection", "features": [{"id": "one"}], "numberReturned": 1, "links": [{"rel": "next", "href": "https://geo.api.vlaanderen.be/BWK/wfs?STARTINDEX=1"}], } page_two = {"type": "FeatureCollection", "features": [], "numberReturned": 0, "links": []} session = FakeSession( [ FakeResponse(page_one, "https://geo.api.vlaanderen.be/BWK/wfs?first"), FakeResponse(page_two, "https://geo.api.vlaanderen.be/BWK/wfs?STARTINDEX=1"), ] ) pages = list(module.iter_wfs_pages(session, (5.0, 51.1, 5.2, 51.3), page_limit=1000, timeout=30)) assert len(pages) == 2 assert session.calls[0][1]["sortBy"] == "UIDN" assert session.calls[0][1]["srsName"] == "EPSG:4326" assert session.calls[1] == ("https://geo.api.vlaanderen.be/BWK/wfs?STARTINDEX=1", None) def test_wfs_page_limit_without_next_link_uses_controlled_start_index_fallback() -> None: module = load_operator() session = FakeSession( [ FakeResponse( {"type": "FeatureCollection", "features": [{"id": "one"}], "numberReturned": 1}, "https://geo.api.vlaanderen.be/BWK/wfs", ), FakeResponse( {"type": "FeatureCollection", "features": [], "numberReturned": 0}, "https://geo.api.vlaanderen.be/BWK/wfs?startIndex=1", ), ] ) pages = list(module.iter_wfs_pages(session, (5.0, 51.1, 5.2, 51.3), page_limit=1, timeout=30)) assert len(pages) == 2 assert session.calls[1][1]["startIndex"] == "1" def test_feature_is_clipped_in_lambert72_and_keeps_bwk_habitat_provenance() -> None: module = load_operator() boundary_wgs84 = box(5.10, 51.20, 5.11, 51.21) boundary_lambert72 = transform_geometry(module.TO_LAMBERT72.transform, boundary_wgs84) feature = { "type": "Feature", "id": "Bwkhab.42", "geometry": mapping_box(5.095, 51.195, 5.105, 51.205), "properties": { "UIDN": 42, "EVAL": "wz", "BWKLABEL": "qb + qs", "EENH1": "qb", "EENH2": "qs", "HERK": "225", "HAB1": "9190", "PHAB1": 60, "HAB2": "rbbppm", "PHAB2": 30, "HAB3": "gh", "PHAB3": 10, "HABLEGENDE": "phab", "HERKHAB": "225", "HERKPHAB": "a", }, } normalized, was_clipped = module.normalize_feature(feature, boundary_lambert72) assert normalized is not None assert was_clipped is True normalized_geometry = shape(normalized["geometry"]) assert normalized_geometry.difference(boundary_wgs84.buffer(1e-7)).area < 1e-12 properties = normalized["properties"] assert properties["bwk_evaluation_code"] == "wz" assert properties["bwk_evaluation_label"].startswith("Complex van waardevolle") assert properties["natura2000_codes"] == "9190" assert properties["regional_biotope_codes"] == "rbbppm" assert properties["natura2000_area_ha"] == pytest.approx(properties["clipped_area_ha"] * 0.6) assert properties["regional_biotope_area_ha"] == pytest.approx(properties["clipped_area_ha"] * 0.3) assert properties["habitat_share_origin_code"] == "a" def mapping_box(min_x: float, min_y: float, max_x: float, max_y: float) -> dict: return { "type": "Polygon", "coordinates": [[ [min_x, min_y], [max_x, min_y], [max_x, max_y], [min_x, max_y], [min_x, min_y], ]], } def test_uncertain_habitat_status_is_not_presented_as_confirmed_habitat() -> None: module = load_operator() entries, natura_share, regional_share, uncertain_share = module.habitat_breakdown( {"HAB1": "gh", "PHAB1": 100, "HABLEGENDE": "ohab"} ) assert entries == [{"code": "gh", "share_percent": 100.0}] assert natura_share == 0 assert regional_share == 0 assert uncertain_share == 100 def test_nature_value_summary_returns_separate_official_classes_and_habitat_metrics() -> None: module = load_operator() dataset = Dataset( id=uuid4(), project_id=uuid4(), name="bwk_natura2000_2025_mol.geojson", dataset_type="vector", dataset_role="reference", source_name="inbo_bwk_natura2000", reference_layer_name="nature_value", source_metadata={ "theme": "nature_value", "semantic_metrics": False, "selection_aggregation": { "metric_key": "bwk_mapped_area", "method": "intersection_area", "label": "BWK-gekarteerde oppervlakte", "unit": "ha", "geometry_dimension": 2, }, "selection_metrics": module.selection_metrics(), }, ) result = VectorFeatureService.summarize_features_by_bbox( SequenceScalarSession([100_000.0, 10_000.0, 20_000.0, 30_000.0, 40_000.0, 5.5, 2.5, 1.5]), dataset=dataset, bbox=BBOX, total_feature_count=125, full_dataset_area=True, ) assert result["primary_metric_key"] == "bwk_mapped_area" assert result["metric_value"] == 10.0 metrics = {item["metric_key"]: item for item in result["metrics"]} assert metrics["bwk_very_valuable_area"]["metric_value"] == 1.0 assert metrics["bwk_valuable_area"]["metric_value"] == 2.0 assert metrics["bwk_less_valuable_area"]["metric_value"] == 3.0 assert metrics["bwk_mixed_value_area"]["metric_value"] == 4.0 assert metrics["natura2000_area"]["metric_value"] == 5.5 assert metrics["natura2000_area"]["is_estimate"] is True assert metrics["regional_biotope_area"]["metric_value"] == 2.5 assert metrics["uncertain_habitat_area"]["metric_value"] == 1.5 assert metrics["feature_count"]["metric_value"] == 125 VectorSelectionSummary(**result) def test_operator_is_packaged_readiness_checked_and_wired_to_map() -> None: dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8") readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8") service = (ROOT / "backend" / "app" / "services" / "vector_feature_service.py").read_text(encoding="utf-8") map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8") source_catalog = (ROOT / "frontend" / "src" / "components" / "datasets" / "SourceCatalogPanel.tsx").read_text(encoding="utf-8") assert "COPY scripts/provision_mol_bwk_natura2000.py" in dockerfile assert "py_compile scripts/provision_mol_bwk_natura2000.py" in readiness assert '"provision_mol_bwk_natura2000.py"' in service assert "id: 'nature_value'" in map_workspace assert "Natuurwaarde" in map_workspace assert "source.key === 'bwk'" in source_catalog