extract the map workspace's domain layer out of the component
MapWorkspace.tsx opened with ~590 lines of theme catalogue, dataset matching and label formatting above a 3.200-line component. None of it is React, all of it is independently testable, and both render paths read from it, so it belongs beside the pure helpers that already live in mapWorkspaceUtils. The contract tests that read MapWorkspace.tsx would have gone red for a move that changes no behaviour at all — 24 of them. That is the brittleness the frontend_contract helper exists to remove, so it gains read_map_workspace(): the workspace is one feature spread over several modules, and a contract belongs to the feature rather than to whichever file currently holds it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,12 +22,46 @@ ROOT = Path(__file__).resolve().parents[2]
|
|||||||
FRONTEND_SRC = ROOT / "frontend" / "src"
|
FRONTEND_SRC = ROOT / "frontend" / "src"
|
||||||
|
|
||||||
|
|
||||||
|
# The map workspace is one feature split across several modules: the container
|
||||||
|
# component, its domain layer and its pure helpers. A contract belongs to the
|
||||||
|
# feature, not to whichever file currently holds it, so splitting a 4.000-line
|
||||||
|
# component must not red the suite.
|
||||||
|
MAP_WORKSPACE_SOURCES = (
|
||||||
|
"components/map/MapWorkspace.tsx",
|
||||||
|
"components/map/mapWorkspaceThemes.ts",
|
||||||
|
"components/map/mapWorkspaceUtils.ts",
|
||||||
|
"components/map/MapExplorerView.tsx",
|
||||||
|
"components/map/MapAdvancedWorkbench.tsx",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def read_frontend(relative_path: str) -> str:
|
def read_frontend(relative_path: str) -> str:
|
||||||
"""Read one frontend source file relative to ``frontend/src``."""
|
"""Read one frontend source file relative to ``frontend/src``."""
|
||||||
|
|
||||||
return (FRONTEND_SRC / relative_path).read_text(encoding="utf-8")
|
return (FRONTEND_SRC / relative_path).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def read_frontend_area(*relative_paths: str) -> str:
|
||||||
|
"""Read several related sources as one body of code.
|
||||||
|
|
||||||
|
Files that do not exist are skipped, so this survives a module being split
|
||||||
|
further or merged back.
|
||||||
|
"""
|
||||||
|
|
||||||
|
parts: list[str] = []
|
||||||
|
for relative_path in relative_paths:
|
||||||
|
path = FRONTEND_SRC / relative_path
|
||||||
|
if path.is_file():
|
||||||
|
parts.append(path.read_text(encoding="utf-8"))
|
||||||
|
return chr(10).join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def read_map_workspace() -> str:
|
||||||
|
"""The whole map workspace feature, whichever modules it is split into."""
|
||||||
|
|
||||||
|
return read_frontend_area(*MAP_WORKSPACE_SOURCES)
|
||||||
|
|
||||||
|
|
||||||
def assert_wired(source: str, *identifiers: str, context: str = "frontend source") -> None:
|
def assert_wired(source: str, *identifiers: str, context: str = "frontend source") -> None:
|
||||||
"""Every identifier must appear in ``source``.
|
"""Every identifier must appear in ``source``.
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from app.models import Area, Dataset, Project
|
|||||||
from app.schemas.coverage import CoverageBBox
|
from app.schemas.coverage import CoverageBBox
|
||||||
from app.services.coverage_registry_service import CoverageRegistryService, THEMES, ZONES
|
from app.services.coverage_registry_service import CoverageRegistryService, THEMES, ZONES
|
||||||
from app.services.vector_feature_service import VectorFeatureService
|
from app.services.vector_feature_service import VectorFeatureService
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
class FakeQuery:
|
class FakeQuery:
|
||||||
@@ -151,9 +152,7 @@ def test_national_and_maritime_reference_layers_are_selection_analyzable() -> No
|
|||||||
def test_national_scope_operator_assigns_explicit_map_themes() -> None:
|
def test_national_scope_operator_assigns_explicit_map_themes() -> None:
|
||||||
root = Path(__file__).resolve().parents[2]
|
root = Path(__file__).resolve().parents[2]
|
||||||
operator = (root / "scripts" / "provision_belgium_north_sea_scope.py").read_text(encoding="utf-8")
|
operator = (root / "scripts" / "provision_belgium_north_sea_scope.py").read_text(encoding="utf-8")
|
||||||
map_workspace = (root / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(
|
map_workspace = read_map_workspace()
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert '"belgium_municipalities": "administrative"' in operator
|
assert '"belgium_municipalities": "administrative"' in operator
|
||||||
assert '"marine_legal_scopes": "marine_environment"' in operator
|
assert '"marine_legal_scopes": "marine_environment"' in operator
|
||||||
@@ -476,7 +475,7 @@ def test_frontend_prefers_materialized_national_workspace_and_resolves_drawn_bbo
|
|||||||
focus = (root / "frontend" / "src" / "config" / "primaryFocus.ts").read_text(encoding="utf-8")
|
focus = (root / "frontend" / "src" / "config" / "primaryFocus.ts").read_text(encoding="utf-8")
|
||||||
workspace_hook = (root / "frontend" / "src" / "hooks" / "useProjectWorkspace.ts").read_text(encoding="utf-8")
|
workspace_hook = (root / "frontend" / "src" / "hooks" / "useProjectWorkspace.ts").read_text(encoding="utf-8")
|
||||||
coverage_hook = (root / "frontend" / "src" / "hooks" / "useCoverageResolver.ts").read_text(encoding="utf-8")
|
coverage_hook = (root / "frontend" / "src" / "hooks" / "useCoverageResolver.ts").read_text(encoding="utf-8")
|
||||||
map_workspace = (root / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
map_workspace = read_map_workspace()
|
||||||
|
|
||||||
assert "Belgium and North Sea Workbench" in focus
|
assert "Belgium and North Sea Workbench" in focus
|
||||||
assert "nationalProject" in workspace_hook
|
assert "nationalProject" in workspace_hook
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -20,9 +21,7 @@ def test_rc9_ux_audit_is_wired_into_frontend_and_readiness() -> None:
|
|||||||
|
|
||||||
def test_rc9_loading_and_accessibility_states_are_explicit() -> None:
|
def test_rc9_loading_and_accessibility_states_are_explicit() -> None:
|
||||||
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
map_workspace = (
|
map_workspace = read_map_workspace()
|
||||||
ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx"
|
|
||||||
).read_text(encoding="utf-8")
|
|
||||||
geo_map = (ROOT / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(
|
geo_map = (ROOT / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(
|
||||||
encoding="utf-8"
|
encoding="utf-8"
|
||||||
)
|
)
|
||||||
@@ -42,9 +41,7 @@ def test_rc9_performance_budgets_are_documented_and_visible() -> None:
|
|||||||
ROOT / "frontend" / "src" / "lib" / "performanceBudget.ts"
|
ROOT / "frontend" / "src" / "lib" / "performanceBudget.ts"
|
||||||
).read_text(encoding="utf-8")
|
).read_text(encoding="utf-8")
|
||||||
docs = (ROOT / "docs" / "UX_PERFORMANCE_BUDGETS.md").read_text(encoding="utf-8")
|
docs = (ROOT / "docs" / "UX_PERFORMANCE_BUDGETS.md").read_text(encoding="utf-8")
|
||||||
map_workspace = (
|
map_workspace = read_map_workspace()
|
||||||
ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx"
|
|
||||||
).read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
assert "COVERAGE_RESPONSE_BUDGET_MS = 4_000" in budget
|
assert "COVERAGE_RESPONSE_BUDGET_MS = 4_000" in budget
|
||||||
assert "MAP_ANALYSIS_BUDGET_MS = 15_000" in budget
|
assert "MAP_ANALYSIS_BUDGET_MS = 15_000" in budget
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
def test_map_workspace_exposes_feature_extract_actions() -> None:
|
def test_map_workspace_exposes_feature_extract_actions() -> None:
|
||||||
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(
|
map_workspace = read_map_workspace()
|
||||||
encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "Selectie en extractie" in map_workspace
|
assert "Selectie en extractie" in map_workspace
|
||||||
assert "downloadSelectedMapFeature" in map_workspace
|
assert "downloadSelectedMapFeature" in map_workspace
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from shapely.geometry import Polygon, box
|
|||||||
from app.core.errors import AppError
|
from app.core.errors import AppError
|
||||||
from app.models import Dataset, VectorFeature
|
from app.models import Dataset, VectorFeature
|
||||||
from app.services.vector_feature_service import VectorFeatureService
|
from app.services.vector_feature_service import VectorFeatureService
|
||||||
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
|
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired, read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -338,7 +338,7 @@ def test_vector_select_route_rejects_area_from_another_project(monkeypatch) -> N
|
|||||||
|
|
||||||
def test_frontend_exposes_map_bbox_selection_contracts() -> None:
|
def test_frontend_exposes_map_bbox_selection_contracts() -> None:
|
||||||
api_client = (ROOT / "frontend" / "src" / "services" / "api" / "datasets.ts").read_text(encoding="utf-8")
|
api_client = (ROOT / "frontend" / "src" / "services" / "api" / "datasets.ts").read_text(encoding="utf-8")
|
||||||
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
map_workspace = read_map_workspace()
|
||||||
geomap = (ROOT / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(encoding="utf-8")
|
geomap = (ROOT / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(encoding="utf-8")
|
||||||
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
extract_hook = (ROOT / "frontend" / "src" / "hooks" / "useMapSelectionExtract.ts").read_text(encoding="utf-8")
|
extract_hook = (ROOT / "frontend" / "src" / "hooks" / "useMapSelectionExtract.ts").read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
|
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired, read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -15,7 +15,7 @@ def read(path: str) -> str:
|
|||||||
|
|
||||||
def test_map_first_explorer_is_the_default_product_flow() -> None:
|
def test_map_first_explorer_is_the_default_product_flow() -> None:
|
||||||
app = read("frontend/src/App.tsx")
|
app = read("frontend/src/App.tsx")
|
||||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
workspace = read_map_workspace()
|
||||||
|
|
||||||
assert "useState<WorkspaceKey>('map')" in app
|
assert "useState<WorkspaceKey>('map')" in app
|
||||||
assert "Gebied analyseren" in workspace
|
assert "Gebied analyseren" in workspace
|
||||||
@@ -36,7 +36,7 @@ def test_map_first_explorer_is_the_default_product_flow() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_map_rectangle_drag_is_wired_to_automatic_analysis() -> None:
|
def test_map_rectangle_drag_is_wired_to_automatic_analysis() -> None:
|
||||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
workspace = read_map_workspace()
|
||||||
geomap = read("frontend/src/components/GeoMap.tsx")
|
geomap = read("frontend/src/components/GeoMap.tsx")
|
||||||
styles = read("frontend/src/styles/app.css")
|
styles = read("frontend/src/styles/app.css")
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from app.schemas.temporal import TemporalComparisonRequest, TemporalObjectChange
|
|||||||
from app.services.dataset_service import DatasetService
|
from app.services.dataset_service import DatasetService
|
||||||
from app.services.temporal_analysis_service import TemporalAnalysisService
|
from app.services.temporal_analysis_service import TemporalAnalysisService
|
||||||
from app.services.vector_feature_service import VectorFeatureService
|
from app.services.vector_feature_service import VectorFeatureService
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).parents[2]
|
ROOT = Path(__file__).parents[2]
|
||||||
@@ -513,7 +514,7 @@ def test_legacy_grb_identity_requires_complete_partition_evidence() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
|
def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
|
||||||
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
workspace = read_map_workspace()
|
||||||
temporal_api = (ROOT / "frontend/src/services/api/temporal.ts").read_text(encoding="utf-8")
|
temporal_api = (ROOT / "frontend/src/services/api/temporal.ts").read_text(encoding="utf-8")
|
||||||
population = (ROOT / "scripts/provision_mol_population_history.py").read_text(encoding="utf-8")
|
population = (ROOT / "scripts/provision_mol_population_history.py").read_text(encoding="utf-8")
|
||||||
landuse = (ROOT / "scripts/provision_mol_historical_landuse.py").read_text(encoding="utf-8")
|
landuse = (ROOT / "scripts/provision_mol_historical_landuse.py").read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import rasterio
|
|||||||
from rasterio.transform import from_origin
|
from rasterio.transform import from_origin
|
||||||
from shapely.geometry import Polygon, shape
|
from shapely.geometry import Polygon, shape
|
||||||
from shapely.ops import transform
|
from shapely.ops import transform
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -179,7 +180,7 @@ def test_official_landuse_operator_paginates_within_api_limit() -> None:
|
|||||||
def test_official_landuse_operator_is_packaged_and_readiness_checked() -> None:
|
def test_official_landuse_operator_is_packaged_and_readiness_checked() -> None:
|
||||||
readiness = (ROOT / "scripts/run_readiness_check.sh").read_text(encoding="utf-8")
|
readiness = (ROOT / "scripts/run_readiness_check.sh").read_text(encoding="utf-8")
|
||||||
dockerfile = (ROOT / "deploy/unraid/Dockerfile.all-in-one").read_text(encoding="utf-8")
|
dockerfile = (ROOT / "deploy/unraid/Dockerfile.all-in-one").read_text(encoding="utf-8")
|
||||||
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
workspace = read_map_workspace()
|
||||||
geo_map = (ROOT / "frontend/src/components/GeoMap.tsx").read_text(encoding="utf-8")
|
geo_map = (ROOT / "frontend/src/components/GeoMap.tsx").read_text(encoding="utf-8")
|
||||||
premium_css = (ROOT / "frontend/src/styles/premium.css").read_text(encoding="utf-8")
|
premium_css = (ROOT / "frontend/src/styles/premium.css").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
|
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired, read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -10,7 +10,7 @@ def read(path: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def test_work_area_change_clears_stale_spatial_results_before_switching_area() -> None:
|
def test_work_area_change_clears_stale_spatial_results_before_switching_area() -> None:
|
||||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
workspace = read_map_workspace()
|
||||||
|
|
||||||
assert "const handleSelectMapArea = (areaId: string) => {" in workspace
|
assert "const handleSelectMapArea = (areaId: string) => {" in workspace
|
||||||
assert "clearAreaSelection()\n onSelectMapArea(areaId)" in workspace
|
assert "clearAreaSelection()\n onSelectMapArea(areaId)" in workspace
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import importlib.util
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -118,9 +119,7 @@ def test_readiness_compiles_false_negative_review_validator() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_map_explains_strict_and_diagnostic_detection_matching() -> None:
|
def test_map_explains_strict_and_diagnostic_detection_matching() -> None:
|
||||||
workspace = (
|
workspace = read_map_workspace()
|
||||||
ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx"
|
|
||||||
).read_text(encoding="utf-8")
|
|
||||||
# Both matching methods must be named; the exact label may change.
|
# Both matching methods must be named; the exact label may change.
|
||||||
assert "strikte" in workspace.casefold()
|
assert "strikte" in workspace.casefold()
|
||||||
assert "Rechthoekcontrole" in workspace
|
assert "Rechthoekcontrole" in workspace
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
|
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired, read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -10,7 +10,7 @@ def read(path: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def test_evolution_mode_falls_back_to_an_available_series() -> None:
|
def test_evolution_mode_falls_back_to_an_available_series() -> None:
|
||||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
workspace = read_map_workspace()
|
||||||
|
|
||||||
assert "themeTemporalSeriesMap" in workspace
|
assert "themeTemporalSeriesMap" in workspace
|
||||||
assert "availableEvolutionThemes[0]" in workspace
|
assert "availableEvolutionThemes[0]" in workspace
|
||||||
@@ -20,7 +20,7 @@ def test_evolution_mode_falls_back_to_an_available_series() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_evolution_theme_catalog_distinguishes_history_from_current_only_data() -> None:
|
def test_evolution_theme_catalog_distinguishes_history_from_current_only_data() -> None:
|
||||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
workspace = read_map_workspace()
|
||||||
|
|
||||||
assert "analysisMode === 'current'" in workspace
|
assert "analysisMode === 'current'" in workspace
|
||||||
# Theme availability depends on a dataset or an on-demand product;
|
# Theme availability depends on a dataset or an on-demand product;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from shapely.ops import transform as transform_geometry
|
|||||||
from app.models import Dataset
|
from app.models import Dataset
|
||||||
from app.schemas.operations import VectorSelectionSummary
|
from app.schemas.operations import VectorSelectionSummary
|
||||||
from app.services.vector_feature_service import VectorFeatureService
|
from app.services.vector_feature_service import VectorFeatureService
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -260,7 +261,7 @@ 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")
|
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")
|
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")
|
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")
|
map_workspace = read_map_workspace()
|
||||||
source_catalog = (ROOT / "frontend" / "src" / "components" / "datasets" / "SourceCatalogPanel.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 "COPY scripts/provision_mol_bwk_natura2000.py" in dockerfile
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from shapely.ops import transform as transform_geometry
|
|||||||
from app.models import Dataset
|
from app.models import Dataset
|
||||||
from app.schemas.operations import VectorSelectionSummary
|
from app.schemas.operations import VectorSelectionSummary
|
||||||
from app.services.vector_feature_service import VectorFeatureService
|
from app.services.vector_feature_service import VectorFeatureService
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -261,7 +262,7 @@ def test_operator_uses_canonical_upload_and_is_packaged_for_runtime() -> None:
|
|||||||
service = (ROOT / "backend" / "app" / "services" / "vector_feature_service.py").read_text(encoding="utf-8")
|
service = (ROOT / "backend" / "app" / "services" / "vector_feature_service.py").read_text(encoding="utf-8")
|
||||||
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
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")
|
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||||
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
map_workspace = read_map_workspace()
|
||||||
source_catalog = (ROOT / "frontend" / "src" / "components" / "datasets" / "SourceCatalogPanel.tsx").read_text(encoding="utf-8")
|
source_catalog = (ROOT / "frontend" / "src" / "components" / "datasets" / "SourceCatalogPanel.tsx").read_text(encoding="utf-8")
|
||||||
dataset_display = (ROOT / "frontend" / "src" / "lib" / "datasetDisplay.ts").read_text(encoding="utf-8")
|
dataset_display = (ROOT / "frontend" / "src" / "lib" / "datasetDisplay.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from app.models import Area, Dataset, DatasetVersion, Job, Project, SourceRegist
|
|||||||
from app.schemas.dhmv import DhmvAcquireRequest, TerrainPartitionSelectionRequest, TerrainSelectionRequest
|
from app.schemas.dhmv import DhmvAcquireRequest, TerrainPartitionSelectionRequest, TerrainSelectionRequest
|
||||||
from app.services.dhmv_acquisition_service import DhmvAcquisitionService
|
from app.services.dhmv_acquisition_service import DhmvAcquisitionService
|
||||||
from app.services.terrain_analysis_service import TerrainAnalysisService
|
from app.services.terrain_analysis_service import TerrainAnalysisService
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -616,7 +617,7 @@ def test_frontend_and_runtime_expose_dhmv_workflow() -> None:
|
|||||||
capabilities_source = (
|
capabilities_source = (
|
||||||
ROOT / "frontend" / "src" / "lib" / "datasetCapabilities.ts"
|
ROOT / "frontend" / "src" / "lib" / "datasetCapabilities.ts"
|
||||||
).read_text(encoding="utf-8")
|
).read_text(encoding="utf-8")
|
||||||
map_source = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
map_source = read_map_workspace()
|
||||||
hook_source = (ROOT / "frontend" / "src" / "hooks" / "useMapSelectionExtract.ts").read_text(encoding="utf-8")
|
hook_source = (ROOT / "frontend" / "src" / "hooks" / "useMapSelectionExtract.ts").read_text(encoding="utf-8")
|
||||||
service_source = (ROOT / "frontend" / "src" / "services" / "api" / "datasets.ts").read_text(encoding="utf-8")
|
service_source = (ROOT / "frontend" / "src" / "services" / "api" / "datasets.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from shapely.ops import transform as transform_geometry
|
|||||||
from app.models import Dataset
|
from app.models import Dataset
|
||||||
from app.schemas.operations import VectorSelectionSummary
|
from app.schemas.operations import VectorSelectionSummary
|
||||||
from app.services.vector_feature_service import VectorFeatureService
|
from app.services.vector_feature_service import VectorFeatureService
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -352,7 +353,7 @@ def test_operator_is_canonical_packaged_and_mol_scoped_in_explorer() -> None:
|
|||||||
service = (ROOT / "backend/app/services/vector_feature_service.py").read_text(encoding="utf-8")
|
service = (ROOT / "backend/app/services/vector_feature_service.py").read_text(encoding="utf-8")
|
||||||
dockerfile = (ROOT / "deploy/unraid/Dockerfile.all-in-one").read_text(encoding="utf-8")
|
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")
|
readiness = (ROOT / "scripts/run_readiness_check.sh").read_text(encoding="utf-8")
|
||||||
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
workspace = read_map_workspace()
|
||||||
catalog = (ROOT / "frontend/src/components/datasets/SourceCatalogPanel.tsx").read_text(encoding="utf-8")
|
catalog = (ROOT / "frontend/src/components/datasets/SourceCatalogPanel.tsx").read_text(encoding="utf-8")
|
||||||
display = (ROOT / "frontend/src/lib/datasetDisplay.ts").read_text(encoding="utf-8")
|
display = (ROOT / "frontend/src/lib/datasetDisplay.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from app.schemas.assistant import AssistantQueryRequest
|
|||||||
from app.services.geo_assistant_service import GeoAssistantService
|
from app.services.geo_assistant_service import GeoAssistantService
|
||||||
from app.services.flood_hazard_acquisition_service import FloodHazardAcquisitionService
|
from app.services.flood_hazard_acquisition_service import FloodHazardAcquisitionService
|
||||||
from app.services.flood_hazard_analysis_service import FloodHazardAnalysisService
|
from app.services.flood_hazard_analysis_service import FloodHazardAnalysisService
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -549,7 +550,7 @@ def test_flood_hazard_runtime_contract_is_packaged() -> None:
|
|||||||
operator = (ROOT / "scripts" / "provision_mol_flood_hazards.py").read_text(encoding="utf-8")
|
operator = (ROOT / "scripts" / "provision_mol_flood_hazards.py").read_text(encoding="utf-8")
|
||||||
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||||
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
||||||
frontend = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
frontend = read_map_workspace()
|
||||||
assert "/datasets/flood-hazard/acquire" in operator
|
assert "/datasets/flood-hazard/acquire" in operator
|
||||||
assert "/raster/flood-hazard/select" in operator
|
assert "/raster/flood-hazard/select" in operator
|
||||||
assert "concurrent_flood_volume_m3" in operator
|
assert "concurrent_flood_volume_m3" in operator
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from shapely.geometry import box, mapping, shape
|
|||||||
|
|
||||||
from app.models import Dataset
|
from app.models import Dataset
|
||||||
from app.services.vector_feature_service import VectorFeatureService
|
from app.services.vector_feature_service import VectorFeatureService
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -235,7 +236,7 @@ def test_regional_operator_is_packaged_release_checked_and_exact_area_is_preferr
|
|||||||
dockerfile = (ROOT / "deploy/unraid/Dockerfile.all-in-one").read_text(encoding="utf-8")
|
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")
|
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")
|
service = (ROOT / "backend/app/services/vector_feature_service.py").read_text(encoding="utf-8")
|
||||||
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
workspace = read_map_workspace()
|
||||||
catalog = (ROOT / "frontend/src/components/datasets/SourceCatalogPanel.tsx").read_text(encoding="utf-8")
|
catalog = (ROOT / "frontend/src/components/datasets/SourceCatalogPanel.tsx").read_text(encoding="utf-8")
|
||||||
model = (ROOT / "backend/app/models/entities.py").read_text(encoding="utf-8")
|
model = (ROOT / "backend/app/models/entities.py").read_text(encoding="utf-8")
|
||||||
migration = (
|
migration = (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from shapely.ops import transform as transform_geometry
|
|||||||
|
|
||||||
from app.models import Dataset
|
from app.models import Dataset
|
||||||
from app.services.vector_feature_service import VectorFeatureService
|
from app.services.vector_feature_service import VectorFeatureService
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -154,7 +155,7 @@ def test_soil_operator_contract_has_no_direct_persistence_and_is_packaged() -> N
|
|||||||
operator = (ROOT / "scripts" / "provision_mol_soil_map.py").read_text(encoding="utf-8")
|
operator = (ROOT / "scripts" / "provision_mol_soil_map.py").read_text(encoding="utf-8")
|
||||||
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
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")
|
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||||
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
map_workspace = read_map_workspace()
|
||||||
|
|
||||||
assert "/datasets/upload" in operator
|
assert "/datasets/upload" in operator
|
||||||
assert "vector_features" in operator
|
assert "vector_features" in operator
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
|
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired, read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -21,7 +21,7 @@ def test_partitioned_raster_routes_are_canonical_and_documented() -> None:
|
|||||||
|
|
||||||
def test_regional_map_uses_logical_partition_groups_and_exact_analysis() -> None:
|
def test_regional_map_uses_logical_partition_groups_and_exact_analysis() -> None:
|
||||||
app = (ROOT / "frontend/src/App.tsx").read_text(encoding="utf-8")
|
app = (ROOT / "frontend/src/App.tsx").read_text(encoding="utf-8")
|
||||||
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
workspace = read_map_workspace()
|
||||||
hook = (ROOT / "frontend/src/hooks/useMapThemeSelectionInsights.ts").read_text(encoding="utf-8")
|
hook = (ROOT / "frontend/src/hooks/useMapThemeSelectionInsights.ts").read_text(encoding="utf-8")
|
||||||
api = (ROOT / "frontend/src/services/api/datasets.ts").read_text(encoding="utf-8")
|
api = (ROOT / "frontend/src/services/api/datasets.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import pytest
|
|||||||
from app.core.errors import AppError
|
from app.core.errors import AppError
|
||||||
from app.models import Dataset
|
from app.models import Dataset
|
||||||
from app.services.grb_refresh_plan_service import GrbRefreshPlanService
|
from app.services.grb_refresh_plan_service import GrbRefreshPlanService
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
NOW = datetime(2026, 7, 16, 16, 0, tzinfo=timezone.utc)
|
NOW = datetime(2026, 7, 16, 16, 0, tzinfo=timezone.utc)
|
||||||
@@ -252,7 +253,7 @@ def test_refresh_api_and_frontend_remain_explicit_only() -> None:
|
|||||||
|
|
||||||
def test_map_theme_ranking_prefers_newer_observation_over_feature_count() -> None:
|
def test_map_theme_ranking_prefers_newer_observation_over_feature_count() -> None:
|
||||||
root = Path(__file__).resolve().parents[2]
|
root = Path(__file__).resolve().parents[2]
|
||||||
workspace = (root / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
workspace = read_map_workspace()
|
||||||
observed_sort = workspace.index("const observedAtDifference")
|
observed_sort = workspace.index("const observedAtDifference")
|
||||||
feature_tiebreaker = workspace.index("right.feature_count", observed_sort)
|
feature_tiebreaker = workspace.index("right.feature_count", observed_sort)
|
||||||
assert observed_sort < feature_tiebreaker
|
assert observed_sort < feature_tiebreaker
|
||||||
@@ -262,7 +263,7 @@ def test_map_theme_ranking_prefers_newer_observation_over_feature_count() -> Non
|
|||||||
|
|
||||||
def test_map_workspace_restores_theme_from_selected_dataset() -> None:
|
def test_map_workspace_restores_theme_from_selected_dataset() -> None:
|
||||||
root = Path(__file__).resolve().parents[2]
|
root = Path(__file__).resolve().parents[2]
|
||||||
workspace = (root / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
workspace = read_map_workspace()
|
||||||
assert "function themeIdForDataset(" in workspace
|
assert "function themeIdForDataset(" in workspace
|
||||||
assert "useState<DataThemeId>(() =>" in workspace
|
assert "useState<DataThemeId>(() =>" in workspace
|
||||||
assert "return themeIdForDataset(selectedDataset) ?? 'buildings'" in workspace
|
assert "return themeIdForDataset(selectedDataset) ?? 'buildings'" in workspace
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from app.services.storage_service import StorageService
|
|||||||
from app.services.source_registry_service import SourceRegistryService
|
from app.services.source_registry_service import SourceRegistryService
|
||||||
from app.services.temporal_analysis_service import TemporalAnalysisService
|
from app.services.temporal_analysis_service import TemporalAnalysisService
|
||||||
from app.services.thematic_raster_analysis_service import ThematicRasterAnalysisService
|
from app.services.thematic_raster_analysis_service import ThematicRasterAnalysisService
|
||||||
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
|
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired, read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
class FakeSession:
|
class FakeSession:
|
||||||
@@ -364,7 +364,7 @@ def test_map_result_export_endpoint_uses_canonical_envelope(monkeypatch) -> None
|
|||||||
|
|
||||||
def test_frontend_persists_map_result_before_opening_downloads() -> None:
|
def test_frontend_persists_map_result_before_opening_downloads() -> None:
|
||||||
root = Path(__file__).resolve().parents[2]
|
root = Path(__file__).resolve().parents[2]
|
||||||
workspace = (root / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
workspace = read_map_workspace()
|
||||||
hook = (root / "frontend/src/hooks/useExportWorkflow.ts").read_text(encoding="utf-8")
|
hook = (root / "frontend/src/hooks/useExportWorkflow.ts").read_text(encoding="utf-8")
|
||||||
api = (root / "frontend/src/services/api/exports.ts").read_text(encoding="utf-8")
|
api = (root / "frontend/src/services/api/exports.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from app.models import Area, Dataset, Job, Project
|
|||||||
from app.schemas.bathymetry import BathymetryProfileAcquireRequest
|
from app.schemas.bathymetry import BathymetryProfileAcquireRequest
|
||||||
from app.services.bathymetry_profile_acquisition_service import BathymetryProfileAcquisitionService
|
from app.services.bathymetry_profile_acquisition_service import BathymetryProfileAcquisitionService
|
||||||
from app.services.dataset_service import DatasetService
|
from app.services.dataset_service import DatasetService
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -360,7 +361,7 @@ def test_bathymetry_contract_and_expansion_roadmap_are_documented() -> None:
|
|||||||
assert "py_compile scripts/provision_mol_bathymetry_profiles.py" in readiness
|
assert "py_compile scripts/provision_mol_bathymetry_profiles.py" in readiness
|
||||||
assert "COPY scripts/provision_mol_bathymetry_profiles.py" in dockerfile
|
assert "COPY scripts/provision_mol_bathymetry_profiles.py" in dockerfile
|
||||||
operator = (ROOT / "scripts" / "provision_mol_bathymetry_profiles.py").read_text(encoding="utf-8")
|
operator = (ROOT / "scripts" / "provision_mol_bathymetry_profiles.py").read_text(encoding="utf-8")
|
||||||
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(encoding="utf-8")
|
map_workspace = read_map_workspace()
|
||||||
assert 'DEFAULT_PROJECT_NAME = "Kempen Regional Workbench"' in operator
|
assert 'DEFAULT_PROJECT_NAME = "Kempen Regional Workbench"' in operator
|
||||||
assert "regional_partitions_complete" in map_workspace
|
assert "regional_partitions_complete" in map_workspace
|
||||||
assert "historische profielen" in map_workspace
|
assert "historische profielen" in map_workspace
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ if str(SCRIPTS) not in sys.path:
|
|||||||
sys.path.insert(0, str(SCRIPTS))
|
sys.path.insert(0, str(SCRIPTS))
|
||||||
|
|
||||||
import provision_flanders_geographic_scope as flanders_scope # noqa: E402
|
import provision_flanders_geographic_scope as flanders_scope # noqa: E402
|
||||||
|
from tests.frontend_contract import read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
class BinaryResponse:
|
class BinaryResponse:
|
||||||
@@ -463,9 +464,7 @@ def test_frontend_uses_partitioned_bathymetry_selection_for_regional_scope() ->
|
|||||||
focus = (
|
focus = (
|
||||||
ROOT / "frontend" / "src" / "config" / "primaryFocus.ts"
|
ROOT / "frontend" / "src" / "config" / "primaryFocus.ts"
|
||||||
).read_text(encoding="utf-8")
|
).read_text(encoding="utf-8")
|
||||||
map_workspace = (
|
map_workspace = read_map_workspace()
|
||||||
ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx"
|
|
||||||
).read_text(encoding="utf-8")
|
|
||||||
theme_hook = (
|
theme_hook = (
|
||||||
ROOT / "frontend" / "src" / "hooks" / "useMapThemeSelectionInsights.ts"
|
ROOT / "frontend" / "src" / "hooks" / "useMapThemeSelectionInsights.ts"
|
||||||
).read_text(encoding="utf-8")
|
).read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
|
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired, read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -10,7 +10,7 @@ def read(path: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def test_flanders_workspace_exposes_governed_thematic_products_on_demand() -> None:
|
def test_flanders_workspace_exposes_governed_thematic_products_on_demand() -> None:
|
||||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
workspace = read_map_workspace()
|
||||||
product_hook = read("frontend/src/hooks/useOfficialMapProducts.ts")
|
product_hook = read("frontend/src/hooks/useOfficialMapProducts.ts")
|
||||||
selection_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
|
selection_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
|
||||||
api = read("frontend/src/services/api/datasets.ts")
|
api = read("frontend/src/services/api/datasets.ts")
|
||||||
@@ -30,7 +30,7 @@ def test_flanders_workspace_exposes_governed_thematic_products_on_demand() -> No
|
|||||||
|
|
||||||
|
|
||||||
def test_selection_reads_and_bounded_acquires_all_relevant_themes() -> None:
|
def test_selection_reads_and_bounded_acquires_all_relevant_themes() -> None:
|
||||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
workspace = read_map_workspace()
|
||||||
app = read("frontend/src/App.tsx")
|
app = read("frontend/src/App.tsx")
|
||||||
selection_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
|
selection_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ def test_selection_reads_and_bounded_acquires_all_relevant_themes() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_regional_on_demand_sources_require_a_bounded_drawn_selection() -> None:
|
def test_regional_on_demand_sources_require_a_bounded_drawn_selection() -> None:
|
||||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
workspace = read_map_workspace()
|
||||||
|
|
||||||
assert "regionalOnDemandThemeActive" in workspace
|
assert "regionalOnDemandThemeActive" in workspace
|
||||||
# Both regional source kinds need a bounded selection before acquiring.
|
# Both regional source kinds need a bounded selection before acquiring.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from app.models import Area, Dataset, Job, Project
|
|||||||
from app.schemas.grb import GrbAcquireRequest
|
from app.schemas.grb import GrbAcquireRequest
|
||||||
from app.services.dataset_service import DatasetService
|
from app.services.dataset_service import DatasetService
|
||||||
from app.services.grb_acquisition_service import GrbAcquisitionService
|
from app.services.grb_acquisition_service import GrbAcquisitionService
|
||||||
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired
|
from tests.frontend_contract import assert_calls, assert_mentions, assert_wired, read_map_workspace
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -383,7 +383,7 @@ def test_system_capabilities_reports_bounded_grb_integration() -> None:
|
|||||||
def test_grb_frontend_and_contracts_use_only_the_governed_backend_path() -> None:
|
def test_grb_frontend_and_contracts_use_only_the_governed_backend_path() -> None:
|
||||||
selection_hook = (ROOT / "frontend/src/hooks/useMapThemeSelectionInsights.ts").read_text(encoding="utf-8")
|
selection_hook = (ROOT / "frontend/src/hooks/useMapThemeSelectionInsights.ts").read_text(encoding="utf-8")
|
||||||
catalog_hook = (ROOT / "frontend/src/hooks/useOfficialMapProducts.ts").read_text(encoding="utf-8")
|
catalog_hook = (ROOT / "frontend/src/hooks/useOfficialMapProducts.ts").read_text(encoding="utf-8")
|
||||||
workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8")
|
workspace = read_map_workspace()
|
||||||
contracts = (ROOT / "docs/API_CONTRACTS.md").read_text(encoding="utf-8")
|
contracts = (ROOT / "docs/API_CONTRACTS.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "datasetsApi.acquireGrb" in selection_hook
|
assert "datasetsApi.acquireGrb" in selection_hook
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
bboxesEqual,
|
bboxesEqual,
|
||||||
copyText,
|
copyText,
|
||||||
datasetIntersectsSelection,
|
datasetIntersectsSelection,
|
||||||
deduplicateTemporalSnapshots,
|
|
||||||
downloadJsonFile,
|
downloadJsonFile,
|
||||||
formatArea,
|
formatArea,
|
||||||
formatBboxLabel,
|
formatBboxLabel,
|
||||||
@@ -35,14 +34,12 @@ import {
|
|||||||
getFeatureCollectionBBox,
|
getFeatureCollectionBBox,
|
||||||
getFeatureGeometrySummary,
|
getFeatureGeometrySummary,
|
||||||
isMunicipalityAreaName,
|
isMunicipalityAreaName,
|
||||||
isSelectionBoundedDataset,
|
|
||||||
normalizeBboxFromCorners,
|
normalizeBboxFromCorners,
|
||||||
operationalScopeProjectLabel,
|
operationalScopeProjectLabel,
|
||||||
parseBboxInput,
|
parseBboxInput,
|
||||||
persistedDatasetSupportsSelection,
|
persistedDatasetSupportsSelection,
|
||||||
productCoversZones,
|
productCoversZones,
|
||||||
readablePropertyName,
|
readablePropertyName,
|
||||||
resultCountLabel,
|
|
||||||
resultMetricLabel,
|
resultMetricLabel,
|
||||||
safeFileStem,
|
safeFileStem,
|
||||||
selectedAreaCoverageZones,
|
selectedAreaCoverageZones,
|
||||||
@@ -53,596 +50,34 @@ import {
|
|||||||
selectionFeatureLimit,
|
selectionFeatureLimit,
|
||||||
selectionMetricLabel,
|
selectionMetricLabel,
|
||||||
splitSelectionBbox,
|
splitSelectionBbox,
|
||||||
temporalDatasetAreaMatch,
|
|
||||||
} from './mapWorkspaceUtils'
|
} from './mapWorkspaceUtils'
|
||||||
|
import {
|
||||||
const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson'
|
COVERAGE_THEME_BY_MAP_THEME,
|
||||||
const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson'
|
DATA_THEMES,
|
||||||
const EMPTY_TEMPORAL_SERIES: DatasetCreateResponse[] = []
|
DATA_THEME_MAP_STYLES,
|
||||||
|
DEFAULT_AREA_SELECTION_FILENAME,
|
||||||
type DataThemeId =
|
DEFAULT_SELECTED_FEATURE_FILENAME,
|
||||||
| 'administrative'
|
EMPTY_TEMPORAL_SERIES,
|
||||||
| 'buildings'
|
coverageStatusLabel,
|
||||||
| 'land_cover'
|
coverageZoneLabel,
|
||||||
| 'space_occupation'
|
datasetCoversSelectedArea,
|
||||||
| 'open_space'
|
datasetProductKey,
|
||||||
| 'population'
|
floodScenarioLabel,
|
||||||
| 'forest'
|
formatDatasetObservation,
|
||||||
| 'nature_value'
|
formatObservationDate,
|
||||||
| 'agriculture'
|
isPartitionedBathymetry,
|
||||||
| 'soil'
|
isPartitionedRaster,
|
||||||
| 'water'
|
listThemeTemporalSeries,
|
||||||
| 'bathymetry'
|
pickThemeDataset,
|
||||||
| 'flood_hazard'
|
productSupportsSelection,
|
||||||
| 'elevation'
|
rasterPartitionsForDataset,
|
||||||
| 'accessibility'
|
themeIdForDataset,
|
||||||
| 'services'
|
type DataTheme,
|
||||||
| 'roads'
|
type DataThemeId,
|
||||||
| 'parcels'
|
type OnDemandMapProduct,
|
||||||
| 'maritime_planning'
|
type PlannedOnDemandMapProduct,
|
||||||
| 'marine_environment'
|
type TemporalSeriesGroup,
|
||||||
|
} from './mapWorkspaceThemes'
|
||||||
interface DataTheme {
|
|
||||||
id: DataThemeId
|
|
||||||
label: string
|
|
||||||
shortLabel: string
|
|
||||||
description: string
|
|
||||||
tokens: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TemporalSeriesGroup {
|
|
||||||
key: string
|
|
||||||
label: string
|
|
||||||
items: DatasetCreateResponse[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OnDemandMapProduct extends MapThemeAcquisition {
|
|
||||||
theme: DataThemeId
|
|
||||||
availabilityLabel: string
|
|
||||||
attribution: string
|
|
||||||
limitationMessage: string
|
|
||||||
coverageZones: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PlannedOnDemandMapProduct extends OnDemandMapProduct {
|
|
||||||
acquisitionBboxes: VectorSelectionBBox[]
|
|
||||||
}
|
|
||||||
|
|
||||||
function productSupportsSelection(product: OnDemandMapProduct, bbox: VectorSelectionBBox): boolean {
|
|
||||||
const scale = selectionAnalysisScale(bbox)
|
|
||||||
if (scale === 'overview') return true
|
|
||||||
const dimensions = selectionDimensions(bbox)
|
|
||||||
if (product.kind === 'dhmv' || product.kind === 'spw_terrain' || product.kind === 'flood_hazard') {
|
|
||||||
return dimensions.areaSquareMetres <= 280_000_000
|
|
||||||
}
|
|
||||||
if (product.kind === 'thematic_raster' || product.kind === 'walous') {
|
|
||||||
return dimensions.widthMetres <= 50_000
|
|
||||||
&& dimensions.heightMetres <= 50_000
|
|
||||||
&& dimensions.areaSquareMetres <= 2_800_000_000
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const DATA_THEMES: DataTheme[] = [
|
|
||||||
{
|
|
||||||
id: 'administrative',
|
|
||||||
label: 'Bestuurlijke indeling',
|
|
||||||
shortLabel: 'Bestuursgebieden',
|
|
||||||
description: 'Officiële lands-, gewest-, provincie- en gemeentegrenzen van het NGI.',
|
|
||||||
tokens: ['administrative', 'adminvector', 'belgium_land_boundary', 'belgium_regions', 'belgium_provinces', 'belgium_municipalities'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'buildings',
|
|
||||||
label: 'Bebouwing',
|
|
||||||
shortLabel: 'Gebouwen',
|
|
||||||
description: 'Gebouwen en gebouwcontouren uit GRB of een andere persistente bron.',
|
|
||||||
tokens: ['buildings', 'building', 'gebouwen', 'gebouw', 'bebouwing', 'gbg'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'land_cover',
|
|
||||||
label: 'Landbedekking',
|
|
||||||
shortLabel: 'Landbedekking',
|
|
||||||
description: 'Fysieke en biologische bodembedekking uit een officieel regionaal classificatieraster.',
|
|
||||||
tokens: ['land_cover', 'land_cover_use', 'landbedekking', 'walous', 'occupation du sol'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'space_occupation',
|
|
||||||
label: 'Ruimtebeslag',
|
|
||||||
shortLabel: 'Ruimtebeslag',
|
|
||||||
description: 'Officiële 10 m-beleidskaart van ruimte ingenomen door wonen, economie, infrastructuur en recreatie.',
|
|
||||||
tokens: ['space_occupation', 'ruimtebeslag', 'ruibes'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'open_space',
|
|
||||||
label: 'Open ruimte',
|
|
||||||
shortLabel: 'Open ruimte',
|
|
||||||
description: 'Officiële 10 m-beleidskaart van open ruimte buiten kernen en ruimtebeslag.',
|
|
||||||
tokens: ['open_space', 'open ruimte', 'openruimte'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'population',
|
|
||||||
label: 'Bevolking',
|
|
||||||
shortLabel: 'Inwoners',
|
|
||||||
description: 'Bevolkingscijfers of statistische raster- en vectorzones.',
|
|
||||||
tokens: ['population', 'bevolking', 'inwoners', 'inhabitants', 'census'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'forest',
|
|
||||||
label: 'Bos & groen',
|
|
||||||
shortLabel: 'Bos',
|
|
||||||
description: 'Bos, natuur en groenbedekking uit een ingeladen vectorbron.',
|
|
||||||
tokens: ['forest', 'forestry', 'woodland', 'bos', 'groen', 'vegetation'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'nature_value',
|
|
||||||
label: 'Natuurwaarde',
|
|
||||||
shortLabel: 'BWK-oppervlakte',
|
|
||||||
description: 'Biologische waardering, Natura 2000-habitat en regionaal belangrijke biotopen uit de BWK.',
|
|
||||||
tokens: ['nature_value', 'nature value', 'natuurwaarde', 'bwk', 'natura2000', 'natura 2000', 'biodiversity'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'agriculture',
|
|
||||||
label: 'Landbouw',
|
|
||||||
shortLabel: 'Landbouwgebruik',
|
|
||||||
description: 'Jaarlijkse officiële landbouwgebruikspercelen en hoofdteeltgroepen.',
|
|
||||||
tokens: ['agriculture', 'agricultural', 'landbouw', 'landbouwgebruik', 'agpa'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'soil',
|
|
||||||
label: 'Bodem',
|
|
||||||
shortLabel: 'Bodemkaart',
|
|
||||||
description: 'Officiele bodemkartering met bodemtype, textuur en drainageklasse waar de geselecteerde zone door een gekoppelde bron wordt gedekt.',
|
|
||||||
tokens: ['soil', 'bodem', 'bodemkaart', 'bodemtype', 'dov_soil_map'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'water',
|
|
||||||
label: 'Water',
|
|
||||||
shortLabel: 'Water',
|
|
||||||
description: 'Waterlopen, grachten, kanalen en wateroppervlakken.',
|
|
||||||
tokens: ['waterways', 'waterway', 'water', 'hydro', 'river', 'stream', 'canal', 'waterloop'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bathymetry',
|
|
||||||
label: 'Waterbodem',
|
|
||||||
shortLabel: 'Dwarsprofielen',
|
|
||||||
description: 'Officiële historische VHA-dwarsprofielen met meetvelden en brondocumenten.',
|
|
||||||
tokens: ['bathymetry', 'bathymetry_profiles', 'dwarsprofielen', 'waterbodem'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'flood_hazard',
|
|
||||||
label: 'Overstroming',
|
|
||||||
shortLabel: 'Overstroomd oppervlak',
|
|
||||||
description: 'Gemodelleerde maximale waterdiepte per VMM-kans- en klimaatscenario.',
|
|
||||||
tokens: ['flood_hazard', 'flood depth', 'flood_depth', 'overstroming', 'waterdiepte'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'elevation',
|
|
||||||
label: 'Hoogte & reliëf',
|
|
||||||
shortLabel: 'Hoogte',
|
|
||||||
description: 'Maaiveld- of oppervlaktehoogte, reliëf en helling uit DHMV II.',
|
|
||||||
tokens: ['dhmv', 'elevation', 'height', 'hoogte', 'terrain', 'surface', 'dtm', 'dsm', 'reliëf'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'accessibility',
|
|
||||||
label: 'Bereikbaarheid',
|
|
||||||
shortLabel: 'Knooppuntwaarde',
|
|
||||||
description: 'Knooppuntwaarde van collectief vervoer per hectare voor referentiejaar 2022.',
|
|
||||||
tokens: ['accessibility', 'bereikbaarheid', 'knooppuntwaarde', 'knptw'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'services',
|
|
||||||
label: 'Voorzieningen',
|
|
||||||
shortLabel: 'Voorzieningenniveau',
|
|
||||||
description: 'Genormaliseerde nabijheid van basis-, regionale en metropolitane voorzieningen in 2022.',
|
|
||||||
tokens: ['services', 'voorzieningen', 'voorzieningenniveau', 'totvznv'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'roads',
|
|
||||||
label: 'Wegen',
|
|
||||||
shortLabel: 'Wegen',
|
|
||||||
description: 'Wegen en wegsegmenten uit een persistente bron.',
|
|
||||||
tokens: ['roads', 'road', 'wegen', 'wegsegment', 'street'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'parcels',
|
|
||||||
label: 'Percelen',
|
|
||||||
shortLabel: 'Percelen',
|
|
||||||
description: 'Kadastrale of administratieve perceelcontouren.',
|
|
||||||
tokens: ['parcels', 'parcel', 'percelen', 'perceel', 'cadastre', 'kadaster'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'maritime_planning',
|
|
||||||
label: 'Maritieme planning',
|
|
||||||
shortLabel: 'Plan- en gebruikszones',
|
|
||||||
description: 'Officiële gebruiks- en beschermingszones uit het Belgisch Marien Ruimtelijk Plan 2026-2034.',
|
|
||||||
tokens: ['maritime_planning', 'marine_spatial_plan', 'rbins_msp', 'bmsp', 'imsp26'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'marine_environment',
|
|
||||||
label: 'Mariene rapportagezones',
|
|
||||||
shortLabel: 'Zeegebieden',
|
|
||||||
description: 'Officiële juridische en mariene rapportagegebieden voor het Belgische deel van de Noordzee.',
|
|
||||||
tokens: ['marine_environment', 'marine_legal_scopes', 'marine_reporting_units', 'rbins_marine_reporting'],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const COVERAGE_THEME_BY_MAP_THEME: Record<DataThemeId, string> = {
|
|
||||||
administrative: 'admin',
|
|
||||||
buildings: 'buildings',
|
|
||||||
land_cover: 'land_cover_use',
|
|
||||||
space_occupation: 'land_cover_use',
|
|
||||||
open_space: 'land_cover_use',
|
|
||||||
population: 'population',
|
|
||||||
forest: 'land_cover_use',
|
|
||||||
nature_value: 'nature',
|
|
||||||
agriculture: 'land_cover_use',
|
|
||||||
soil: 'soil',
|
|
||||||
water: 'surface_water',
|
|
||||||
bathymetry: 'bathymetry',
|
|
||||||
flood_hazard: 'flood_climate',
|
|
||||||
elevation: 'elevation',
|
|
||||||
accessibility: 'roads',
|
|
||||||
services: 'population',
|
|
||||||
roads: 'roads',
|
|
||||||
parcels: 'parcels',
|
|
||||||
maritime_planning: 'maritime_planning',
|
|
||||||
marine_environment: 'marine_environment',
|
|
||||||
}
|
|
||||||
|
|
||||||
function coverageStatusLabel(status: CoverageStatus): string {
|
|
||||||
const labels: Record<CoverageStatus, string> = {
|
|
||||||
operational: 'Beschikbaar',
|
|
||||||
partial: 'Gedeeltelijk',
|
|
||||||
not_configured: 'Niet gekoppeld',
|
|
||||||
unsupported: 'Niet ondersteund',
|
|
||||||
}
|
|
||||||
return labels[status]
|
|
||||||
}
|
|
||||||
|
|
||||||
function coverageZoneLabel(zone: string): string {
|
|
||||||
const labels: Record<string, string> = {
|
|
||||||
belgium: 'Belgie',
|
|
||||||
flanders: 'Vlaanderen',
|
|
||||||
wallonia: 'Wallonie',
|
|
||||||
brussels: 'Brussel',
|
|
||||||
belgian_north_sea: 'Belgische Noordzee',
|
|
||||||
territorial_sea: 'Territoriale zee',
|
|
||||||
exclusive_economic_zone: 'EEZ',
|
|
||||||
continental_shelf: 'Continentaal plat',
|
|
||||||
}
|
|
||||||
return labels[zone] ?? zone
|
|
||||||
}
|
|
||||||
|
|
||||||
const DATA_THEME_MAP_STYLES: Record<DataThemeId, { fill: string; line: string }> = {
|
|
||||||
administrative: { fill: '#5f6f7f', line: '#344554' },
|
|
||||||
buildings: { fill: '#d45f3d', line: '#9f3e24' },
|
|
||||||
land_cover: { fill: '#4f7b4f', line: '#315c39' },
|
|
||||||
space_occupation: { fill: '#be3e33', line: '#8f2c24' },
|
|
||||||
open_space: { fill: '#267a46', line: '#175c32' },
|
|
||||||
population: { fill: '#7559a6', line: '#5b3f88' },
|
|
||||||
forest: { fill: '#347950', line: '#225f3b' },
|
|
||||||
nature_value: { fill: '#9a4f64', line: '#74364a' },
|
|
||||||
agriculture: { fill: '#7b8f32', line: '#53671d' },
|
|
||||||
soil: { fill: '#9a7040', line: '#6f4c27' },
|
|
||||||
water: { fill: '#2676a8', line: '#155b85' },
|
|
||||||
bathymetry: { fill: '#0e7490', line: '#164e63' },
|
|
||||||
flood_hazard: { fill: '#1597c2', line: '#075985' },
|
|
||||||
elevation: { fill: '#a57a4b', line: '#315f59' },
|
|
||||||
accessibility: { fill: '#0f766e', line: '#115e59' },
|
|
||||||
services: { fill: '#b66d16', line: '#854d0e' },
|
|
||||||
roads: { fill: '#6b7280', line: '#4b5563' },
|
|
||||||
parcels: { fill: '#a7792f', line: '#7d571f' },
|
|
||||||
maritime_planning: { fill: '#2f7f8f', line: '#145d6a' },
|
|
||||||
marine_environment: { fill: '#3475a3', line: '#1c557d' },
|
|
||||||
}
|
|
||||||
|
|
||||||
function datasetSearchText(dataset: DatasetCreateResponse): string {
|
|
||||||
return [
|
|
||||||
dataset.name,
|
|
||||||
dataset.original_filename,
|
|
||||||
dataset.source,
|
|
||||||
dataset.source_name,
|
|
||||||
dataset.reference_layer_name,
|
|
||||||
dataset.metadata_json?.['layer_name'],
|
|
||||||
dataset.source_metadata?.['layer_name'],
|
|
||||||
dataset.source_metadata?.['theme'],
|
|
||||||
dataset.source_metadata?.['product_display_name'],
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')
|
|
||||||
.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
function datasetMatchesTheme(dataset: DatasetCreateResponse, theme: DataTheme): boolean {
|
|
||||||
// Governed raster products have one unambiguous semantic theme. Matching
|
|
||||||
// them by generic substrings (for example "water" in "waterdiepte") would
|
|
||||||
// make a flood scenario replace the permanent surface-water layer.
|
|
||||||
if (dataset.source_name === 'vmm_flood_hazard') {
|
|
||||||
return theme.id === 'flood_hazard'
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
|
|
||||||
return theme.id === 'bathymetry'
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'spw_bathymetry') {
|
|
||||||
return theme.id === 'bathymetry'
|
|
||||||
}
|
|
||||||
if (['digitaal_vlaanderen_dhmv', 'spw_terrain'].includes(dataset.source_name ?? '')) {
|
|
||||||
return theme.id === 'elevation'
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'department_omgeving_thematic_raster') {
|
|
||||||
return dataset.source_metadata?.['theme'] === theme.id
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'spw_walous_land_cover') {
|
|
||||||
return theme.id === 'land_cover'
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'dov_soil_map') {
|
|
||||||
return theme.id === 'soil'
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'ngi_adminvector') {
|
|
||||||
return theme.id === 'administrative'
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'rbins_msp_2026') {
|
|
||||||
return theme.id === 'maritime_planning'
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'rbins_marine_reporting_units') {
|
|
||||||
return theme.id === 'marine_environment'
|
|
||||||
}
|
|
||||||
const searchText = datasetSearchText(dataset)
|
|
||||||
return theme.tokens.some((token) => searchText.includes(token))
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPartitionedRaster(dataset: DatasetCreateResponse | null | undefined): boolean {
|
|
||||||
return Boolean(
|
|
||||||
dataset?.dataset_type === 'raster'
|
|
||||||
&& ['digitaal_vlaanderen_dhmv', 'spw_terrain', 'vmm_flood_hazard'].includes(dataset.source_name ?? ''),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPartitionedBathymetry(dataset: DatasetCreateResponse | null | undefined): boolean {
|
|
||||||
return Boolean(
|
|
||||||
dataset?.source_name === 'vmm_vha_bathymetry_profiles'
|
|
||||||
&& dataset.source_metadata?.['regional_partitions_complete'] === true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function datasetProductKey(dataset: DatasetCreateResponse): string {
|
|
||||||
return String(dataset.source_metadata?.['product_key'] ?? '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function datasetCoversSelectedArea(
|
|
||||||
dataset: DatasetCreateResponse,
|
|
||||||
selectedAreaId: string | null,
|
|
||||||
selectedAreaName: string | null | undefined,
|
|
||||||
regionalScope = false,
|
|
||||||
): boolean {
|
|
||||||
if (isSelectionBoundedDataset(dataset.source_metadata)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const selectedZones = selectedAreaCoverageZones(selectedAreaName)
|
|
||||||
const configuredZones = dataset.source_metadata?.['coverage_zones']
|
|
||||||
if (selectedZones && Array.isArray(configuredZones) && configuredZones.length > 0) {
|
|
||||||
const datasetZones = configuredZones.map((zone) => String(zone))
|
|
||||||
if (!selectedZones.some((zone) => datasetZones.includes(zone))) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const coverageScope = String(dataset.source_metadata?.['coverage_scope'] ?? '')
|
|
||||||
if (isPartitionedBathymetry(dataset)) {
|
|
||||||
return regionalScope
|
|
||||||
? true
|
|
||||||
: Boolean(selectedAreaId) && dataset.area_id === selectedAreaId
|
|
||||||
}
|
|
||||||
if (coverageScope !== 'municipality' || !dataset.area_id) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (regionalScope) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return Boolean(selectedAreaId) && dataset.area_id === selectedAreaId
|
|
||||||
}
|
|
||||||
|
|
||||||
function rasterPartitionsForDataset(
|
|
||||||
datasets: DatasetCreateResponse[],
|
|
||||||
representative: DatasetCreateResponse | null,
|
|
||||||
selectedAreaId: string | null,
|
|
||||||
selectedAreaName: string | null | undefined,
|
|
||||||
regionalScope: boolean,
|
|
||||||
): DatasetCreateResponse[] {
|
|
||||||
if (!representative) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
if (!regionalScope || (!isPartitionedRaster(representative) && !isPartitionedBathymetry(representative))) {
|
|
||||||
return [representative]
|
|
||||||
}
|
|
||||||
const productKey = datasetProductKey(representative)
|
|
||||||
const manifestSha256 = String(representative.source_metadata?.['partition_manifest_sha256'] ?? '')
|
|
||||||
return datasets
|
|
||||||
.filter(
|
|
||||||
(dataset) =>
|
|
||||||
dataset.source_name === representative.source_name
|
|
||||||
&& (
|
|
||||||
isPartitionedBathymetry(representative)
|
|
||||||
? String(dataset.source_metadata?.['partition_manifest_sha256'] ?? '') === manifestSha256
|
|
||||||
: datasetProductKey(dataset) === productKey
|
|
||||||
)
|
|
||||||
&& datasetCoversSelectedArea(dataset, selectedAreaId, selectedAreaName, true),
|
|
||||||
)
|
|
||||||
.sort((left, right) => String(left.area_id ?? '').localeCompare(String(right.area_id ?? '')))
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickThemeDataset(
|
|
||||||
datasets: DatasetCreateResponse[],
|
|
||||||
theme: DataTheme,
|
|
||||||
selectedAreaId: string | null,
|
|
||||||
selectedAreaName: string | null | undefined,
|
|
||||||
regionalScope = false,
|
|
||||||
): DatasetCreateResponse | null {
|
|
||||||
const candidates = datasets.filter(
|
|
||||||
(dataset) =>
|
|
||||||
datasetMatchesTheme(dataset, theme)
|
|
||||||
&& datasetCoversSelectedArea(dataset, selectedAreaId, selectedAreaName, regionalScope),
|
|
||||||
)
|
|
||||||
candidates.sort((left, right) => {
|
|
||||||
const priorityScore = (dataset: DatasetCreateResponse) =>
|
|
||||||
(dataset.area_id && dataset.area_id === selectedAreaId ? 10_000_000 : 0) +
|
|
||||||
(dataset.reference_layer_name && theme.tokens.includes(dataset.reference_layer_name.toLowerCase()) ? 1_000_000 : 0) +
|
|
||||||
(dataset.source_name === 'grb' ? 100_000 : 0) +
|
|
||||||
(dataset.source_name === 'department_omgeving_land_use' ? 90_000 : 0) +
|
|
||||||
(dataset.source_name === 'inbo_bwk_natura2000' ? 95_000 : 0) +
|
|
||||||
(dataset.source_name === 'agentschap_landbouw_zeevisserij_agricultural_parcels' ? 98_000 : 0) +
|
|
||||||
(dataset.source_name === 'department_omgeving_thematic_raster' ? 5_000_000 : 0) +
|
|
||||||
(dataset.source_name === 'spw_walous_land_cover' ? 5_000_000 : 0) +
|
|
||||||
(dataset.source_name === 'digitaal_vlaanderen_buildings_addresses_register' ? 5_000_000 : 0) +
|
|
||||||
(dataset.source_name === 'digitaal_vlaanderen_dhmv' ? 5_000_000 : 0) +
|
|
||||||
(dataset.source_name === 'spw_terrain' ? 5_000_000 : 0) +
|
|
||||||
(dataset.source_name === 'vmm_flood_hazard' ? 5_000_000 : 0) +
|
|
||||||
(dataset.source_name === 'vmm_vha_bathymetry_profiles' ? 5_000_000 : 0) +
|
|
||||||
(dataset.source_name === 'spw_bathymetry' ? 5_100_000 : 0) +
|
|
||||||
(dataset.source_metadata?.['product_key'] === 'dtm_1m' ? 1_000_000 : 0) +
|
|
||||||
(dataset.source_metadata?.['product_key'] === 'pluviaal_current_t100' ? 1_000_000 : 0) +
|
|
||||||
(dataset.dataset_role === 'reference' ? 10_000 : 0)
|
|
||||||
const priorityDifference = priorityScore(right) - priorityScore(left)
|
|
||||||
if (priorityDifference !== 0) return priorityDifference
|
|
||||||
|
|
||||||
const observedAtDifference = new Date(right.observed_at ?? 0).getTime() - new Date(left.observed_at ?? 0).getTime()
|
|
||||||
if (observedAtDifference !== 0) return observedAtDifference
|
|
||||||
|
|
||||||
const importedAtDifference = new Date(right.imported_at ?? 0).getTime() - new Date(left.imported_at ?? 0).getTime()
|
|
||||||
if (importedAtDifference !== 0) return importedAtDifference
|
|
||||||
|
|
||||||
return (right.feature_count ?? right.vector_summary?.feature_count ?? 0)
|
|
||||||
- (left.feature_count ?? left.vector_summary?.feature_count ?? 0)
|
|
||||||
})
|
|
||||||
return candidates[0] ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
function themeIdForDataset(dataset: DatasetCreateResponse | null): DataThemeId | null {
|
|
||||||
return dataset
|
|
||||||
? DATA_THEMES.find((theme) => datasetMatchesTheme(dataset, theme))?.id ?? null
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
|
|
||||||
function temporalSeriesLabel(items: DatasetCreateResponse[]): string {
|
|
||||||
const configuredLabel = items.find((item) => typeof item.source_metadata?.['temporal_series_label'] === 'string')
|
|
||||||
?.source_metadata?.['temporal_series_label']
|
|
||||||
if (typeof configuredLabel === 'string' && configuredLabel.trim()) {
|
|
||||||
return configuredLabel
|
|
||||||
}
|
|
||||||
const first = items[0]
|
|
||||||
const source = first ? getDatasetDisplayName(first) : 'Tijdreeks'
|
|
||||||
const range = temporalRangeLabel(items)
|
|
||||||
return range ? `${source} (${range})` : source
|
|
||||||
}
|
|
||||||
|
|
||||||
function listThemeTemporalSeries(
|
|
||||||
datasets: DatasetCreateResponse[],
|
|
||||||
theme: DataTheme,
|
|
||||||
selectedAreaId: string | null,
|
|
||||||
): TemporalSeriesGroup[] {
|
|
||||||
const groups = new Map<string, DatasetCreateResponse[]>()
|
|
||||||
for (const dataset of datasets) {
|
|
||||||
if (!datasetMatchesTheme(dataset, theme) || !dataset.temporal_series_key || !dataset.observed_at) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (temporalDatasetAreaMatch(dataset, selectedAreaId) === 'other') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const items = groups.get(dataset.temporal_series_key) ?? []
|
|
||||||
items.push(dataset)
|
|
||||||
groups.set(dataset.temporal_series_key, items)
|
|
||||||
}
|
|
||||||
const series = Array.from(groups.entries())
|
|
||||||
.map(([key, items]) => {
|
|
||||||
const ordered = deduplicateTemporalSnapshots(items)
|
|
||||||
return { key, label: temporalSeriesLabel(ordered), items: ordered }
|
|
||||||
})
|
|
||||||
.filter((group) => group.items.length >= 2)
|
|
||||||
|
|
||||||
const sourcesWithExactAreaSeries = new Set(
|
|
||||||
series
|
|
||||||
.filter((group) => group.items.some((item) => temporalDatasetAreaMatch(item, selectedAreaId) === 'exact'))
|
|
||||||
.map((group) => group.items[0]?.source_name)
|
|
||||||
.filter(Boolean),
|
|
||||||
)
|
|
||||||
|
|
||||||
return series
|
|
||||||
.filter((group) => {
|
|
||||||
const sourceName = group.items[0]?.source_name
|
|
||||||
if (!sourceName || !sourcesWithExactAreaSeries.has(sourceName)) return true
|
|
||||||
return group.items.some((item) => temporalDatasetAreaMatch(item, selectedAreaId) === 'exact')
|
|
||||||
})
|
|
||||||
.sort((left, right) => {
|
|
||||||
if (right.items.length !== left.items.length) {
|
|
||||||
return right.items.length - left.items.length
|
|
||||||
}
|
|
||||||
const latest = (group: TemporalSeriesGroup) => Math.max(...group.items.map((item) => new Date(item.observed_at ?? 0).getTime()))
|
|
||||||
return latest(right) - latest(left)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatObservationDate(value: string | null | undefined): string {
|
|
||||||
if (!value) {
|
|
||||||
return 'Geen peildatum'
|
|
||||||
}
|
|
||||||
return new Intl.DateTimeFormat('nl-BE', { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
function temporalRangeLabel(items: DatasetCreateResponse[]): string | null {
|
|
||||||
const firstValue = items[0]?.observed_at
|
|
||||||
const lastValue = items[items.length - 1]?.observed_at
|
|
||||||
if (!firstValue || !lastValue) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const first = new Date(firstValue)
|
|
||||||
const last = new Date(lastValue)
|
|
||||||
if (first.getTime() === last.getTime()) {
|
|
||||||
return formatObservationDate(firstValue)
|
|
||||||
}
|
|
||||||
if (first.getUTCFullYear() !== last.getUTCFullYear()) {
|
|
||||||
return `${first.getUTCFullYear()}-${last.getUTCFullYear()}`
|
|
||||||
}
|
|
||||||
if (first.getUTCMonth() === last.getUTCMonth()) {
|
|
||||||
const monthAndYear = new Intl.DateTimeFormat('nl-BE', { year: 'numeric', month: 'short' }).format(last)
|
|
||||||
return `${first.getUTCDate()}-${last.getUTCDate()} ${monthAndYear}`
|
|
||||||
}
|
|
||||||
return `${formatObservationDate(firstValue)} - ${formatObservationDate(lastValue)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDatasetObservation(dataset: DatasetCreateResponse): string {
|
|
||||||
if (dataset.source_name === 'vmm_flood_hazard') {
|
|
||||||
return `scenario ${String(dataset.source_metadata?.['climate_context'] ?? '')} · ${String(dataset.source_metadata?.['probability_class'] ?? '')}`
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
|
|
||||||
const firstMeasurement = String(dataset.source_metadata?.['measurement_date_min'] ?? '').slice(0, 4)
|
|
||||||
const lastMeasurement = String(dataset.source_metadata?.['measurement_date_max'] ?? '').slice(0, 4)
|
|
||||||
return firstMeasurement && lastMeasurement
|
|
||||||
? `historische profielen ${firstMeasurement}-${lastMeasurement}`
|
|
||||||
: 'historische profielmetingen'
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'spw_bathymetry') {
|
|
||||||
return `samengestelde waterbodemmeting ${String(dataset.source_metadata?.['survey_period'] ?? '2019-2022')} · mDNG`
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'department_omgeving_thematic_raster') {
|
|
||||||
const observationYear = Number(dataset.source_metadata?.['observation_year'])
|
|
||||||
if (Number.isFinite(observationYear)) {
|
|
||||||
return `referentiejaar ${observationYear}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dataset.source_name === 'spw_walous_land_cover') {
|
|
||||||
const observationYear = Number(dataset.source_metadata?.['observation_year'])
|
|
||||||
return Number.isFinite(observationYear) ? `WALOUS referentiejaar ${observationYear}` : 'WALOUS landbedekking'
|
|
||||||
}
|
|
||||||
const period = dataset.source_metadata?.['acquisition_period']
|
|
||||||
if (typeof period === 'string' && period.trim()) {
|
|
||||||
return `opnameperiode ${period}`
|
|
||||||
}
|
|
||||||
return formatObservationDate(dataset.observed_at)
|
|
||||||
}
|
|
||||||
|
|
||||||
function floodScenarioLabel(dataset: DatasetCreateResponse): string {
|
|
||||||
const configured = dataset.source_metadata?.['product_display_name']
|
|
||||||
return typeof configured === 'string' && configured.trim() ? configured : getDatasetDisplayName(dataset)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MapWorkspaceProps {
|
interface MapWorkspaceProps {
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
|
|||||||
@@ -0,0 +1,612 @@
|
|||||||
|
/**
|
||||||
|
* The map workspace's domain layer: which themes exist, which persisted dataset
|
||||||
|
* answers a theme, and how a dataset describes itself to an operator.
|
||||||
|
*
|
||||||
|
* This was ~590 lines at the top of MapWorkspace.tsx, above a 3.200-line
|
||||||
|
* component. None of it is React, all of it is testable on its own, and both
|
||||||
|
* render paths in that component read from it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CoverageStatus,
|
||||||
|
DatasetCreateResponse,
|
||||||
|
VectorSelectionBBox,
|
||||||
|
} from '../../types'
|
||||||
|
import type { MapThemeAcquisition } from '../../hooks/useMapThemeSelectionInsights'
|
||||||
|
import { getDatasetDisplayName } from '../../lib/datasetDisplay'
|
||||||
|
import {
|
||||||
|
deduplicateTemporalSnapshots,
|
||||||
|
isSelectionBoundedDataset,
|
||||||
|
selectedAreaCoverageZones,
|
||||||
|
selectionAnalysisScale,
|
||||||
|
selectionDimensions,
|
||||||
|
temporalDatasetAreaMatch,
|
||||||
|
} from './mapWorkspaceUtils'
|
||||||
|
|
||||||
|
export const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson'
|
||||||
|
export const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson'
|
||||||
|
export const EMPTY_TEMPORAL_SERIES: DatasetCreateResponse[] = []
|
||||||
|
|
||||||
|
export type DataThemeId =
|
||||||
|
| 'administrative'
|
||||||
|
| 'buildings'
|
||||||
|
| 'land_cover'
|
||||||
|
| 'space_occupation'
|
||||||
|
| 'open_space'
|
||||||
|
| 'population'
|
||||||
|
| 'forest'
|
||||||
|
| 'nature_value'
|
||||||
|
| 'agriculture'
|
||||||
|
| 'soil'
|
||||||
|
| 'water'
|
||||||
|
| 'bathymetry'
|
||||||
|
| 'flood_hazard'
|
||||||
|
| 'elevation'
|
||||||
|
| 'accessibility'
|
||||||
|
| 'services'
|
||||||
|
| 'roads'
|
||||||
|
| 'parcels'
|
||||||
|
| 'maritime_planning'
|
||||||
|
| 'marine_environment'
|
||||||
|
|
||||||
|
export interface DataTheme {
|
||||||
|
id: DataThemeId
|
||||||
|
label: string
|
||||||
|
shortLabel: string
|
||||||
|
description: string
|
||||||
|
tokens: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemporalSeriesGroup {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
items: DatasetCreateResponse[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnDemandMapProduct extends MapThemeAcquisition {
|
||||||
|
theme: DataThemeId
|
||||||
|
availabilityLabel: string
|
||||||
|
attribution: string
|
||||||
|
limitationMessage: string
|
||||||
|
coverageZones: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlannedOnDemandMapProduct extends OnDemandMapProduct {
|
||||||
|
acquisitionBboxes: VectorSelectionBBox[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function productSupportsSelection(product: OnDemandMapProduct, bbox: VectorSelectionBBox): boolean {
|
||||||
|
const scale = selectionAnalysisScale(bbox)
|
||||||
|
if (scale === 'overview') return true
|
||||||
|
const dimensions = selectionDimensions(bbox)
|
||||||
|
if (product.kind === 'dhmv' || product.kind === 'spw_terrain' || product.kind === 'flood_hazard') {
|
||||||
|
return dimensions.areaSquareMetres <= 280_000_000
|
||||||
|
}
|
||||||
|
if (product.kind === 'thematic_raster' || product.kind === 'walous') {
|
||||||
|
return dimensions.widthMetres <= 50_000
|
||||||
|
&& dimensions.heightMetres <= 50_000
|
||||||
|
&& dimensions.areaSquareMetres <= 2_800_000_000
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DATA_THEMES: DataTheme[] = [
|
||||||
|
{
|
||||||
|
id: 'administrative',
|
||||||
|
label: 'Bestuurlijke indeling',
|
||||||
|
shortLabel: 'Bestuursgebieden',
|
||||||
|
description: 'Officiële lands-, gewest-, provincie- en gemeentegrenzen van het NGI.',
|
||||||
|
tokens: ['administrative', 'adminvector', 'belgium_land_boundary', 'belgium_regions', 'belgium_provinces', 'belgium_municipalities'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'buildings',
|
||||||
|
label: 'Bebouwing',
|
||||||
|
shortLabel: 'Gebouwen',
|
||||||
|
description: 'Gebouwen en gebouwcontouren uit GRB of een andere persistente bron.',
|
||||||
|
tokens: ['buildings', 'building', 'gebouwen', 'gebouw', 'bebouwing', 'gbg'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'land_cover',
|
||||||
|
label: 'Landbedekking',
|
||||||
|
shortLabel: 'Landbedekking',
|
||||||
|
description: 'Fysieke en biologische bodembedekking uit een officieel regionaal classificatieraster.',
|
||||||
|
tokens: ['land_cover', 'land_cover_use', 'landbedekking', 'walous', 'occupation du sol'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'space_occupation',
|
||||||
|
label: 'Ruimtebeslag',
|
||||||
|
shortLabel: 'Ruimtebeslag',
|
||||||
|
description: 'Officiële 10 m-beleidskaart van ruimte ingenomen door wonen, economie, infrastructuur en recreatie.',
|
||||||
|
tokens: ['space_occupation', 'ruimtebeslag', 'ruibes'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'open_space',
|
||||||
|
label: 'Open ruimte',
|
||||||
|
shortLabel: 'Open ruimte',
|
||||||
|
description: 'Officiële 10 m-beleidskaart van open ruimte buiten kernen en ruimtebeslag.',
|
||||||
|
tokens: ['open_space', 'open ruimte', 'openruimte'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'population',
|
||||||
|
label: 'Bevolking',
|
||||||
|
shortLabel: 'Inwoners',
|
||||||
|
description: 'Bevolkingscijfers of statistische raster- en vectorzones.',
|
||||||
|
tokens: ['population', 'bevolking', 'inwoners', 'inhabitants', 'census'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'forest',
|
||||||
|
label: 'Bos & groen',
|
||||||
|
shortLabel: 'Bos',
|
||||||
|
description: 'Bos, natuur en groenbedekking uit een ingeladen vectorbron.',
|
||||||
|
tokens: ['forest', 'forestry', 'woodland', 'bos', 'groen', 'vegetation'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nature_value',
|
||||||
|
label: 'Natuurwaarde',
|
||||||
|
shortLabel: 'BWK-oppervlakte',
|
||||||
|
description: 'Biologische waardering, Natura 2000-habitat en regionaal belangrijke biotopen uit de BWK.',
|
||||||
|
tokens: ['nature_value', 'nature value', 'natuurwaarde', 'bwk', 'natura2000', 'natura 2000', 'biodiversity'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'agriculture',
|
||||||
|
label: 'Landbouw',
|
||||||
|
shortLabel: 'Landbouwgebruik',
|
||||||
|
description: 'Jaarlijkse officiële landbouwgebruikspercelen en hoofdteeltgroepen.',
|
||||||
|
tokens: ['agriculture', 'agricultural', 'landbouw', 'landbouwgebruik', 'agpa'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'soil',
|
||||||
|
label: 'Bodem',
|
||||||
|
shortLabel: 'Bodemkaart',
|
||||||
|
description: 'Officiele bodemkartering met bodemtype, textuur en drainageklasse waar de geselecteerde zone door een gekoppelde bron wordt gedekt.',
|
||||||
|
tokens: ['soil', 'bodem', 'bodemkaart', 'bodemtype', 'dov_soil_map'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'water',
|
||||||
|
label: 'Water',
|
||||||
|
shortLabel: 'Water',
|
||||||
|
description: 'Waterlopen, grachten, kanalen en wateroppervlakken.',
|
||||||
|
tokens: ['waterways', 'waterway', 'water', 'hydro', 'river', 'stream', 'canal', 'waterloop'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bathymetry',
|
||||||
|
label: 'Waterbodem',
|
||||||
|
shortLabel: 'Dwarsprofielen',
|
||||||
|
description: 'Officiële historische VHA-dwarsprofielen met meetvelden en brondocumenten.',
|
||||||
|
tokens: ['bathymetry', 'bathymetry_profiles', 'dwarsprofielen', 'waterbodem'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'flood_hazard',
|
||||||
|
label: 'Overstroming',
|
||||||
|
shortLabel: 'Overstroomd oppervlak',
|
||||||
|
description: 'Gemodelleerde maximale waterdiepte per VMM-kans- en klimaatscenario.',
|
||||||
|
tokens: ['flood_hazard', 'flood depth', 'flood_depth', 'overstroming', 'waterdiepte'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'elevation',
|
||||||
|
label: 'Hoogte & reliëf',
|
||||||
|
shortLabel: 'Hoogte',
|
||||||
|
description: 'Maaiveld- of oppervlaktehoogte, reliëf en helling uit DHMV II.',
|
||||||
|
tokens: ['dhmv', 'elevation', 'height', 'hoogte', 'terrain', 'surface', 'dtm', 'dsm', 'reliëf'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'accessibility',
|
||||||
|
label: 'Bereikbaarheid',
|
||||||
|
shortLabel: 'Knooppuntwaarde',
|
||||||
|
description: 'Knooppuntwaarde van collectief vervoer per hectare voor referentiejaar 2022.',
|
||||||
|
tokens: ['accessibility', 'bereikbaarheid', 'knooppuntwaarde', 'knptw'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'services',
|
||||||
|
label: 'Voorzieningen',
|
||||||
|
shortLabel: 'Voorzieningenniveau',
|
||||||
|
description: 'Genormaliseerde nabijheid van basis-, regionale en metropolitane voorzieningen in 2022.',
|
||||||
|
tokens: ['services', 'voorzieningen', 'voorzieningenniveau', 'totvznv'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'roads',
|
||||||
|
label: 'Wegen',
|
||||||
|
shortLabel: 'Wegen',
|
||||||
|
description: 'Wegen en wegsegmenten uit een persistente bron.',
|
||||||
|
tokens: ['roads', 'road', 'wegen', 'wegsegment', 'street'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'parcels',
|
||||||
|
label: 'Percelen',
|
||||||
|
shortLabel: 'Percelen',
|
||||||
|
description: 'Kadastrale of administratieve perceelcontouren.',
|
||||||
|
tokens: ['parcels', 'parcel', 'percelen', 'perceel', 'cadastre', 'kadaster'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'maritime_planning',
|
||||||
|
label: 'Maritieme planning',
|
||||||
|
shortLabel: 'Plan- en gebruikszones',
|
||||||
|
description: 'Officiële gebruiks- en beschermingszones uit het Belgisch Marien Ruimtelijk Plan 2026-2034.',
|
||||||
|
tokens: ['maritime_planning', 'marine_spatial_plan', 'rbins_msp', 'bmsp', 'imsp26'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'marine_environment',
|
||||||
|
label: 'Mariene rapportagezones',
|
||||||
|
shortLabel: 'Zeegebieden',
|
||||||
|
description: 'Officiële juridische en mariene rapportagegebieden voor het Belgische deel van de Noordzee.',
|
||||||
|
tokens: ['marine_environment', 'marine_legal_scopes', 'marine_reporting_units', 'rbins_marine_reporting'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const COVERAGE_THEME_BY_MAP_THEME: Record<DataThemeId, string> = {
|
||||||
|
administrative: 'admin',
|
||||||
|
buildings: 'buildings',
|
||||||
|
land_cover: 'land_cover_use',
|
||||||
|
space_occupation: 'land_cover_use',
|
||||||
|
open_space: 'land_cover_use',
|
||||||
|
population: 'population',
|
||||||
|
forest: 'land_cover_use',
|
||||||
|
nature_value: 'nature',
|
||||||
|
agriculture: 'land_cover_use',
|
||||||
|
soil: 'soil',
|
||||||
|
water: 'surface_water',
|
||||||
|
bathymetry: 'bathymetry',
|
||||||
|
flood_hazard: 'flood_climate',
|
||||||
|
elevation: 'elevation',
|
||||||
|
accessibility: 'roads',
|
||||||
|
services: 'population',
|
||||||
|
roads: 'roads',
|
||||||
|
parcels: 'parcels',
|
||||||
|
maritime_planning: 'maritime_planning',
|
||||||
|
marine_environment: 'marine_environment',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function coverageStatusLabel(status: CoverageStatus): string {
|
||||||
|
const labels: Record<CoverageStatus, string> = {
|
||||||
|
operational: 'Beschikbaar',
|
||||||
|
partial: 'Gedeeltelijk',
|
||||||
|
not_configured: 'Niet gekoppeld',
|
||||||
|
unsupported: 'Niet ondersteund',
|
||||||
|
}
|
||||||
|
return labels[status]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function coverageZoneLabel(zone: string): string {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
belgium: 'Belgie',
|
||||||
|
flanders: 'Vlaanderen',
|
||||||
|
wallonia: 'Wallonie',
|
||||||
|
brussels: 'Brussel',
|
||||||
|
belgian_north_sea: 'Belgische Noordzee',
|
||||||
|
territorial_sea: 'Territoriale zee',
|
||||||
|
exclusive_economic_zone: 'EEZ',
|
||||||
|
continental_shelf: 'Continentaal plat',
|
||||||
|
}
|
||||||
|
return labels[zone] ?? zone
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DATA_THEME_MAP_STYLES: Record<DataThemeId, { fill: string; line: string }> = {
|
||||||
|
administrative: { fill: '#5f6f7f', line: '#344554' },
|
||||||
|
buildings: { fill: '#d45f3d', line: '#9f3e24' },
|
||||||
|
land_cover: { fill: '#4f7b4f', line: '#315c39' },
|
||||||
|
space_occupation: { fill: '#be3e33', line: '#8f2c24' },
|
||||||
|
open_space: { fill: '#267a46', line: '#175c32' },
|
||||||
|
population: { fill: '#7559a6', line: '#5b3f88' },
|
||||||
|
forest: { fill: '#347950', line: '#225f3b' },
|
||||||
|
nature_value: { fill: '#9a4f64', line: '#74364a' },
|
||||||
|
agriculture: { fill: '#7b8f32', line: '#53671d' },
|
||||||
|
soil: { fill: '#9a7040', line: '#6f4c27' },
|
||||||
|
water: { fill: '#2676a8', line: '#155b85' },
|
||||||
|
bathymetry: { fill: '#0e7490', line: '#164e63' },
|
||||||
|
flood_hazard: { fill: '#1597c2', line: '#075985' },
|
||||||
|
elevation: { fill: '#a57a4b', line: '#315f59' },
|
||||||
|
accessibility: { fill: '#0f766e', line: '#115e59' },
|
||||||
|
services: { fill: '#b66d16', line: '#854d0e' },
|
||||||
|
roads: { fill: '#6b7280', line: '#4b5563' },
|
||||||
|
parcels: { fill: '#a7792f', line: '#7d571f' },
|
||||||
|
maritime_planning: { fill: '#2f7f8f', line: '#145d6a' },
|
||||||
|
marine_environment: { fill: '#3475a3', line: '#1c557d' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export function datasetSearchText(dataset: DatasetCreateResponse): string {
|
||||||
|
return [
|
||||||
|
dataset.name,
|
||||||
|
dataset.original_filename,
|
||||||
|
dataset.source,
|
||||||
|
dataset.source_name,
|
||||||
|
dataset.reference_layer_name,
|
||||||
|
dataset.metadata_json?.['layer_name'],
|
||||||
|
dataset.source_metadata?.['layer_name'],
|
||||||
|
dataset.source_metadata?.['theme'],
|
||||||
|
dataset.source_metadata?.['product_display_name'],
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function datasetMatchesTheme(dataset: DatasetCreateResponse, theme: DataTheme): boolean {
|
||||||
|
// Governed raster products have one unambiguous semantic theme. Matching
|
||||||
|
// them by generic substrings (for example "water" in "waterdiepte") would
|
||||||
|
// make a flood scenario replace the permanent surface-water layer.
|
||||||
|
if (dataset.source_name === 'vmm_flood_hazard') {
|
||||||
|
return theme.id === 'flood_hazard'
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
|
||||||
|
return theme.id === 'bathymetry'
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'spw_bathymetry') {
|
||||||
|
return theme.id === 'bathymetry'
|
||||||
|
}
|
||||||
|
if (['digitaal_vlaanderen_dhmv', 'spw_terrain'].includes(dataset.source_name ?? '')) {
|
||||||
|
return theme.id === 'elevation'
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'department_omgeving_thematic_raster') {
|
||||||
|
return dataset.source_metadata?.['theme'] === theme.id
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'spw_walous_land_cover') {
|
||||||
|
return theme.id === 'land_cover'
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'dov_soil_map') {
|
||||||
|
return theme.id === 'soil'
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'ngi_adminvector') {
|
||||||
|
return theme.id === 'administrative'
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'rbins_msp_2026') {
|
||||||
|
return theme.id === 'maritime_planning'
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'rbins_marine_reporting_units') {
|
||||||
|
return theme.id === 'marine_environment'
|
||||||
|
}
|
||||||
|
const searchText = datasetSearchText(dataset)
|
||||||
|
return theme.tokens.some((token) => searchText.includes(token))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPartitionedRaster(dataset: DatasetCreateResponse | null | undefined): boolean {
|
||||||
|
return Boolean(
|
||||||
|
dataset?.dataset_type === 'raster'
|
||||||
|
&& ['digitaal_vlaanderen_dhmv', 'spw_terrain', 'vmm_flood_hazard'].includes(dataset.source_name ?? ''),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPartitionedBathymetry(dataset: DatasetCreateResponse | null | undefined): boolean {
|
||||||
|
return Boolean(
|
||||||
|
dataset?.source_name === 'vmm_vha_bathymetry_profiles'
|
||||||
|
&& dataset.source_metadata?.['regional_partitions_complete'] === true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function datasetProductKey(dataset: DatasetCreateResponse): string {
|
||||||
|
return String(dataset.source_metadata?.['product_key'] ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function datasetCoversSelectedArea(
|
||||||
|
dataset: DatasetCreateResponse,
|
||||||
|
selectedAreaId: string | null,
|
||||||
|
selectedAreaName: string | null | undefined,
|
||||||
|
regionalScope = false,
|
||||||
|
): boolean {
|
||||||
|
if (isSelectionBoundedDataset(dataset.source_metadata)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const selectedZones = selectedAreaCoverageZones(selectedAreaName)
|
||||||
|
const configuredZones = dataset.source_metadata?.['coverage_zones']
|
||||||
|
if (selectedZones && Array.isArray(configuredZones) && configuredZones.length > 0) {
|
||||||
|
const datasetZones = configuredZones.map((zone) => String(zone))
|
||||||
|
if (!selectedZones.some((zone) => datasetZones.includes(zone))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const coverageScope = String(dataset.source_metadata?.['coverage_scope'] ?? '')
|
||||||
|
if (isPartitionedBathymetry(dataset)) {
|
||||||
|
return regionalScope
|
||||||
|
? true
|
||||||
|
: Boolean(selectedAreaId) && dataset.area_id === selectedAreaId
|
||||||
|
}
|
||||||
|
if (coverageScope !== 'municipality' || !dataset.area_id) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (regionalScope) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return Boolean(selectedAreaId) && dataset.area_id === selectedAreaId
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rasterPartitionsForDataset(
|
||||||
|
datasets: DatasetCreateResponse[],
|
||||||
|
representative: DatasetCreateResponse | null,
|
||||||
|
selectedAreaId: string | null,
|
||||||
|
selectedAreaName: string | null | undefined,
|
||||||
|
regionalScope: boolean,
|
||||||
|
): DatasetCreateResponse[] {
|
||||||
|
if (!representative) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (!regionalScope || (!isPartitionedRaster(representative) && !isPartitionedBathymetry(representative))) {
|
||||||
|
return [representative]
|
||||||
|
}
|
||||||
|
const productKey = datasetProductKey(representative)
|
||||||
|
const manifestSha256 = String(representative.source_metadata?.['partition_manifest_sha256'] ?? '')
|
||||||
|
return datasets
|
||||||
|
.filter(
|
||||||
|
(dataset) =>
|
||||||
|
dataset.source_name === representative.source_name
|
||||||
|
&& (
|
||||||
|
isPartitionedBathymetry(representative)
|
||||||
|
? String(dataset.source_metadata?.['partition_manifest_sha256'] ?? '') === manifestSha256
|
||||||
|
: datasetProductKey(dataset) === productKey
|
||||||
|
)
|
||||||
|
&& datasetCoversSelectedArea(dataset, selectedAreaId, selectedAreaName, true),
|
||||||
|
)
|
||||||
|
.sort((left, right) => String(left.area_id ?? '').localeCompare(String(right.area_id ?? '')))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickThemeDataset(
|
||||||
|
datasets: DatasetCreateResponse[],
|
||||||
|
theme: DataTheme,
|
||||||
|
selectedAreaId: string | null,
|
||||||
|
selectedAreaName: string | null | undefined,
|
||||||
|
regionalScope = false,
|
||||||
|
): DatasetCreateResponse | null {
|
||||||
|
const candidates = datasets.filter(
|
||||||
|
(dataset) =>
|
||||||
|
datasetMatchesTheme(dataset, theme)
|
||||||
|
&& datasetCoversSelectedArea(dataset, selectedAreaId, selectedAreaName, regionalScope),
|
||||||
|
)
|
||||||
|
candidates.sort((left, right) => {
|
||||||
|
const priorityScore = (dataset: DatasetCreateResponse) =>
|
||||||
|
(dataset.area_id && dataset.area_id === selectedAreaId ? 10_000_000 : 0) +
|
||||||
|
(dataset.reference_layer_name && theme.tokens.includes(dataset.reference_layer_name.toLowerCase()) ? 1_000_000 : 0) +
|
||||||
|
(dataset.source_name === 'grb' ? 100_000 : 0) +
|
||||||
|
(dataset.source_name === 'department_omgeving_land_use' ? 90_000 : 0) +
|
||||||
|
(dataset.source_name === 'inbo_bwk_natura2000' ? 95_000 : 0) +
|
||||||
|
(dataset.source_name === 'agentschap_landbouw_zeevisserij_agricultural_parcels' ? 98_000 : 0) +
|
||||||
|
(dataset.source_name === 'department_omgeving_thematic_raster' ? 5_000_000 : 0) +
|
||||||
|
(dataset.source_name === 'spw_walous_land_cover' ? 5_000_000 : 0) +
|
||||||
|
(dataset.source_name === 'digitaal_vlaanderen_buildings_addresses_register' ? 5_000_000 : 0) +
|
||||||
|
(dataset.source_name === 'digitaal_vlaanderen_dhmv' ? 5_000_000 : 0) +
|
||||||
|
(dataset.source_name === 'spw_terrain' ? 5_000_000 : 0) +
|
||||||
|
(dataset.source_name === 'vmm_flood_hazard' ? 5_000_000 : 0) +
|
||||||
|
(dataset.source_name === 'vmm_vha_bathymetry_profiles' ? 5_000_000 : 0) +
|
||||||
|
(dataset.source_name === 'spw_bathymetry' ? 5_100_000 : 0) +
|
||||||
|
(dataset.source_metadata?.['product_key'] === 'dtm_1m' ? 1_000_000 : 0) +
|
||||||
|
(dataset.source_metadata?.['product_key'] === 'pluviaal_current_t100' ? 1_000_000 : 0) +
|
||||||
|
(dataset.dataset_role === 'reference' ? 10_000 : 0)
|
||||||
|
const priorityDifference = priorityScore(right) - priorityScore(left)
|
||||||
|
if (priorityDifference !== 0) return priorityDifference
|
||||||
|
|
||||||
|
const observedAtDifference = new Date(right.observed_at ?? 0).getTime() - new Date(left.observed_at ?? 0).getTime()
|
||||||
|
if (observedAtDifference !== 0) return observedAtDifference
|
||||||
|
|
||||||
|
const importedAtDifference = new Date(right.imported_at ?? 0).getTime() - new Date(left.imported_at ?? 0).getTime()
|
||||||
|
if (importedAtDifference !== 0) return importedAtDifference
|
||||||
|
|
||||||
|
return (right.feature_count ?? right.vector_summary?.feature_count ?? 0)
|
||||||
|
- (left.feature_count ?? left.vector_summary?.feature_count ?? 0)
|
||||||
|
})
|
||||||
|
return candidates[0] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function themeIdForDataset(dataset: DatasetCreateResponse | null): DataThemeId | null {
|
||||||
|
return dataset
|
||||||
|
? DATA_THEMES.find((theme) => datasetMatchesTheme(dataset, theme))?.id ?? null
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function temporalSeriesLabel(items: DatasetCreateResponse[]): string {
|
||||||
|
const configuredLabel = items.find((item) => typeof item.source_metadata?.['temporal_series_label'] === 'string')
|
||||||
|
?.source_metadata?.['temporal_series_label']
|
||||||
|
if (typeof configuredLabel === 'string' && configuredLabel.trim()) {
|
||||||
|
return configuredLabel
|
||||||
|
}
|
||||||
|
const first = items[0]
|
||||||
|
const source = first ? getDatasetDisplayName(first) : 'Tijdreeks'
|
||||||
|
const range = temporalRangeLabel(items)
|
||||||
|
return range ? `${source} (${range})` : source
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listThemeTemporalSeries(
|
||||||
|
datasets: DatasetCreateResponse[],
|
||||||
|
theme: DataTheme,
|
||||||
|
selectedAreaId: string | null,
|
||||||
|
): TemporalSeriesGroup[] {
|
||||||
|
const groups = new Map<string, DatasetCreateResponse[]>()
|
||||||
|
for (const dataset of datasets) {
|
||||||
|
if (!datasetMatchesTheme(dataset, theme) || !dataset.temporal_series_key || !dataset.observed_at) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (temporalDatasetAreaMatch(dataset, selectedAreaId) === 'other') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const items = groups.get(dataset.temporal_series_key) ?? []
|
||||||
|
items.push(dataset)
|
||||||
|
groups.set(dataset.temporal_series_key, items)
|
||||||
|
}
|
||||||
|
const series = Array.from(groups.entries())
|
||||||
|
.map(([key, items]) => {
|
||||||
|
const ordered = deduplicateTemporalSnapshots(items)
|
||||||
|
return { key, label: temporalSeriesLabel(ordered), items: ordered }
|
||||||
|
})
|
||||||
|
.filter((group) => group.items.length >= 2)
|
||||||
|
|
||||||
|
const sourcesWithExactAreaSeries = new Set(
|
||||||
|
series
|
||||||
|
.filter((group) => group.items.some((item) => temporalDatasetAreaMatch(item, selectedAreaId) === 'exact'))
|
||||||
|
.map((group) => group.items[0]?.source_name)
|
||||||
|
.filter(Boolean),
|
||||||
|
)
|
||||||
|
|
||||||
|
return series
|
||||||
|
.filter((group) => {
|
||||||
|
const sourceName = group.items[0]?.source_name
|
||||||
|
if (!sourceName || !sourcesWithExactAreaSeries.has(sourceName)) return true
|
||||||
|
return group.items.some((item) => temporalDatasetAreaMatch(item, selectedAreaId) === 'exact')
|
||||||
|
})
|
||||||
|
.sort((left, right) => {
|
||||||
|
if (right.items.length !== left.items.length) {
|
||||||
|
return right.items.length - left.items.length
|
||||||
|
}
|
||||||
|
const latest = (group: TemporalSeriesGroup) => Math.max(...group.items.map((item) => new Date(item.observed_at ?? 0).getTime()))
|
||||||
|
return latest(right) - latest(left)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatObservationDate(value: string | null | undefined): string {
|
||||||
|
if (!value) {
|
||||||
|
return 'Geen peildatum'
|
||||||
|
}
|
||||||
|
return new Intl.DateTimeFormat('nl-BE', { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function temporalRangeLabel(items: DatasetCreateResponse[]): string | null {
|
||||||
|
const firstValue = items[0]?.observed_at
|
||||||
|
const lastValue = items[items.length - 1]?.observed_at
|
||||||
|
if (!firstValue || !lastValue) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const first = new Date(firstValue)
|
||||||
|
const last = new Date(lastValue)
|
||||||
|
if (first.getTime() === last.getTime()) {
|
||||||
|
return formatObservationDate(firstValue)
|
||||||
|
}
|
||||||
|
if (first.getUTCFullYear() !== last.getUTCFullYear()) {
|
||||||
|
return `${first.getUTCFullYear()}-${last.getUTCFullYear()}`
|
||||||
|
}
|
||||||
|
if (first.getUTCMonth() === last.getUTCMonth()) {
|
||||||
|
const monthAndYear = new Intl.DateTimeFormat('nl-BE', { year: 'numeric', month: 'short' }).format(last)
|
||||||
|
return `${first.getUTCDate()}-${last.getUTCDate()} ${monthAndYear}`
|
||||||
|
}
|
||||||
|
return `${formatObservationDate(firstValue)} - ${formatObservationDate(lastValue)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDatasetObservation(dataset: DatasetCreateResponse): string {
|
||||||
|
if (dataset.source_name === 'vmm_flood_hazard') {
|
||||||
|
return `scenario ${String(dataset.source_metadata?.['climate_context'] ?? '')} · ${String(dataset.source_metadata?.['probability_class'] ?? '')}`
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
|
||||||
|
const firstMeasurement = String(dataset.source_metadata?.['measurement_date_min'] ?? '').slice(0, 4)
|
||||||
|
const lastMeasurement = String(dataset.source_metadata?.['measurement_date_max'] ?? '').slice(0, 4)
|
||||||
|
return firstMeasurement && lastMeasurement
|
||||||
|
? `historische profielen ${firstMeasurement}-${lastMeasurement}`
|
||||||
|
: 'historische profielmetingen'
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'spw_bathymetry') {
|
||||||
|
return `samengestelde waterbodemmeting ${String(dataset.source_metadata?.['survey_period'] ?? '2019-2022')} · mDNG`
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'department_omgeving_thematic_raster') {
|
||||||
|
const observationYear = Number(dataset.source_metadata?.['observation_year'])
|
||||||
|
if (Number.isFinite(observationYear)) {
|
||||||
|
return `referentiejaar ${observationYear}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dataset.source_name === 'spw_walous_land_cover') {
|
||||||
|
const observationYear = Number(dataset.source_metadata?.['observation_year'])
|
||||||
|
return Number.isFinite(observationYear) ? `WALOUS referentiejaar ${observationYear}` : 'WALOUS landbedekking'
|
||||||
|
}
|
||||||
|
const period = dataset.source_metadata?.['acquisition_period']
|
||||||
|
if (typeof period === 'string' && period.trim()) {
|
||||||
|
return `opnameperiode ${period}`
|
||||||
|
}
|
||||||
|
return formatObservationDate(dataset.observed_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function floodScenarioLabel(dataset: DatasetCreateResponse): string {
|
||||||
|
const configured = dataset.source_metadata?.['product_display_name']
|
||||||
|
return typeof configured === 'string' && configured.trim() ? configured : getDatasetDisplayName(dataset)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user