diff --git a/CHANGELOG.md b/CHANGELOG.md index 6122b6f9..35c58c77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ # Changelog +## Sprint 237 Bounded Flanders cross-domain profile (2026-07-17) + +- Exposed the five existing governed MercatorNet policy rasters as + `Op aanvraag` in the Flanders map workbench without adding a new provider or + browser-side external fetch path. +- Made one explicit municipality or rectangle selection acquire/reuse, + persist and analyse space occupation, open space, population density, node + value and service level through the canonical Job and Dataset services. +- Kept whole-Flanders raster acquisition disabled behind the existing 60 km + and 30 million cell guardrails while preserving complete-region vector and + partitioned bathymetry analysis. +- Corrected thematic Dataset metadata so a regional Area is no longer + mislabelled as municipality coverage merely because an `area_id` was used. +- Added source-contract and metadata regression coverage for the complete + on-demand flow. + ## Sprint 236 Flemish bathymetry partitions and safe North Sea probe (2026-07-17) - Added dynamic provisioning of the complete official VRBG Flemish diff --git a/backend/README.md b/backend/README.md index 8498efc3..73421d64 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1508,6 +1508,15 @@ Kempen work area fits. External WCS transfers remain split into fixed 10 km tiles, product identifiers remain server-allowlisted and other raster pipelines retain their smaller independent limits. +The Flanders browser workflow uses these same endpoints on demand. It never +performs a startup import or direct browser WCS request: an explicit +municipality or drawn rectangle starts five bounded acquisitions, followed by +the existing persisted-raster analyses. Exact request hashes reuse ready +Datasets. A full-Flanders raster request remains blocked by the same 60 km and +30 million cell limits. Dataset metadata labels only Areas named +`Gemeente ...` as `coverage_scope=municipality`; regional Area clipping is +stored as `bounded_selection`. + Provision the official DOV soil polygons for Mol through the existing vector upload path: diff --git a/backend/app/services/thematic_raster_acquisition_service.py b/backend/app/services/thematic_raster_acquisition_service.py index ebcffcd2..82c61ac5 100644 --- a/backend/app/services/thematic_raster_acquisition_service.py +++ b/backend/app/services/thematic_raster_acquisition_service.py @@ -304,6 +304,15 @@ class ThematicRasterAcquisitionService: raise AppError(code="THEMATIC_RASTER_SELECTION_OUTSIDE_AREA", message="Selection does not overlap the selected work area", status_code=422) return intersection + @staticmethod + def _coverage_scope(db, area_id: UUID | None) -> str: + if area_id is None: + return "bounded_selection" + area = db.get(Area, area_id) + if area and str(area.name).casefold().startswith("gemeente "): + return "municipality" + return "bounded_selection" + @staticmethod def _fetch(request_url: str, settings: Settings, opener: Callable[..., Any] | None = None) -> tuple[bytes, str]: request = Request(request_url, headers={"Accept": "image/tiff,*/*", "User-Agent": "GeoIntel/0.1 bounded-thematic-raster"}) @@ -568,7 +577,7 @@ class ThematicRasterAcquisitionService: "license_note": ThematicRasterAcquisitionService.LICENSE_NOTE, "legend_min_label": product.legend_min_label, "legend_max_label": product.legend_max_label, - "coverage_scope": "municipality" if payload.area_id else "bounded_selection", + "coverage_scope": ThematicRasterAcquisitionService._coverage_scope(db, payload.area_id), }, provenance_metadata={ "acquisition": "explicit_bounded_tiled_wcs_coverage", diff --git a/backend/tests/test_sprint200_temporal_explorer_handoff.py b/backend/tests/test_sprint200_temporal_explorer_handoff.py index 824a982b..844c7b3d 100644 --- a/backend/tests/test_sprint200_temporal_explorer_handoff.py +++ b/backend/tests/test_sprint200_temporal_explorer_handoff.py @@ -21,7 +21,9 @@ def test_evolution_mode_falls_back_to_an_available_series() -> None: def test_evolution_theme_catalog_distinguishes_history_from_current_only_data() -> None: workspace = read("frontend/src/components/map/MapWorkspace.tsx") - assert "analysisMode === 'current' || evolutionAvailable" in workspace + assert "analysisMode === 'current'" in workspace + assert "Boolean(dataset || onDemandProduct)" in workspace + assert "Boolean(dataset) && evolutionAvailable" in workspace assert "meetmomenten" in workspace assert "Tijdreeks" in workspace assert "Alleen huidige toestand" in workspace diff --git a/backend/tests/test_sprint213_thematic_rasters.py b/backend/tests/test_sprint213_thematic_rasters.py index 36317588..17944aa3 100644 --- a/backend/tests/test_sprint213_thematic_rasters.py +++ b/backend/tests/test_sprint213_thematic_rasters.py @@ -17,7 +17,7 @@ from app.core.config import Settings from app.core.errors import AppError from app.db.session import get_db from app.main import app -from app.models import Dataset, Job, Project +from app.models import Area, Dataset, Job, Project from app.schemas.thematic_raster import ThematicRasterAcquireRequest, ThematicRasterSelectionRequest from app.schemas.assistant import AssistantQueryRequest from app.services.geo_assistant_service import GeoAssistantService @@ -177,6 +177,20 @@ def test_complete_kempen_scope_fits_the_tiled_thematic_guardrails() -> None: assert exc_info.value.code == "THEMATIC_RASTER_SELECTION_TOO_LARGE" +def test_coverage_scope_only_labels_named_municipality_areas_as_municipality() -> None: + project_id = uuid4() + municipality_id = uuid4() + region_id = uuid4() + db = FakeSession({ + (Area, municipality_id): Area(id=municipality_id, project_id=project_id, name="Gemeente Mol"), + (Area, region_id): Area(id=region_id, project_id=project_id, name="Vlaanderen"), + }) + + assert ThematicRasterAcquisitionService._coverage_scope(db, municipality_id) == "municipality" + assert ThematicRasterAcquisitionService._coverage_scope(db, region_id) == "bounded_selection" + assert ThematicRasterAcquisitionService._coverage_scope(db, None) == "bounded_selection" + + def test_wcs_fetch_retries_an_incomplete_tile_without_accepting_partial_bytes(monkeypatch) -> None: content = b"II*\x00complete-geotiff" responses = [IncompleteResponse(content), FakeResponse(content)] diff --git a/backend/tests/test_sprint233_operational_completion.py b/backend/tests/test_sprint233_operational_completion.py index 5a0fecb8..b265897f 100644 --- a/backend/tests/test_sprint233_operational_completion.py +++ b/backend/tests/test_sprint233_operational_completion.py @@ -339,7 +339,8 @@ def test_theme_failures_name_the_source_and_reason() -> None: root = Path(__file__).resolve().parents[2] hook = (root / "frontend/src/hooks/useMapThemeSelectionInsights.ts").read_text(encoding="utf-8") - assert "dataset: queries[index]?.dataset.name" in hook + assert "queries[index]?.dataset?.name" in hook + assert "queries[index]?.thematicProductKey" in hook assert "reason: formatError(item.reason" in hook assert "failure.dataset}: ${failure.reason}" in hook diff --git a/backend/tests/test_sprint237_flanders_thematic_on_demand.py b/backend/tests/test_sprint237_flanders_thematic_on_demand.py new file mode 100644 index 00000000..383e3d3c --- /dev/null +++ b/backend/tests/test_sprint237_flanders_thematic_on_demand.py @@ -0,0 +1,59 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def read(path: str) -> str: + return (ROOT / path).read_text(encoding="utf-8") + + +def test_flanders_workspace_exposes_governed_thematic_products_on_demand() -> None: + workspace = read("frontend/src/components/map/MapWorkspace.tsx") + product_hook = read("frontend/src/hooks/useThematicRasterProducts.ts") + selection_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts") + api = read("frontend/src/services/api/datasets.ts") + + assert "activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME" in workspace + assert "new Map" in workspace + assert "'Op aanvraag'" in workspace + assert "thematicProductKey: thematicProduct.key" in workspace + assert "datasetsApi.listThematicRasterProducts" in product_hook + assert "datasetsApi.acquireThematicRaster" in selection_hook + assert "datasetsApi.selectThematicRaster" in selection_hook + assert "datasetsApi.get(selectedProjectId, acquisition.output_dataset_id)" in selection_hook + assert "/datasets/thematic-raster/acquire" in api + + +def test_selection_runs_all_available_themes_and_refreshes_persisted_datasets() -> None: + workspace = read("frontend/src/components/map/MapWorkspace.tsx") + app = read("frontend/src/App.tsx") + selection_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts") + + assert "for (const theme of DATA_THEMES)" in workspace + assert "await loadThemeInsights(bbox, availableThemes, areaId)" in workspace + assert "!regionalPartitionedThemeActive && !onDemandThematicThemeActive" in workspace + assert "onRefreshProjectData" in workspace + assert "selectedProjectId ? loadProjectData(selectedProjectId)" in app + assert "successful.some((item) => item.thematicProductKey)" in selection_hook + assert "await onDatasetsChanged()" in selection_hook + + +def test_regional_on_demand_rasters_require_a_bounded_drawn_selection() -> None: + workspace = read("frontend/src/components/map/MapWorkspace.tsx") + + assert "regionalOnDemandThematicThemeActive" in workspace + assert "regionalRasterThemeActive || regionalOnDemandThematicThemeActive" in workspace + assert "Teken een begrensde rechthoek voor een regionale rasteranalyse." in workspace + assert "officiële Vlaamse rasters worden begrensd opgehaald, bewaard en hergebruikt" in workspace + + +def test_frontend_does_not_contact_the_external_wcs_directly() -> None: + frontend_sources = "\n".join( + path.read_text(encoding="utf-8") + for path in (ROOT / "frontend/src").rglob("*") + if path.suffix in {".ts", ".tsx"} + ) + + assert "mercatornet.be" not in frontend_sources.casefold() + assert "GetCoverage" not in frontend_sources diff --git a/docs/API_CONTRACTS.md b/docs/API_CONTRACTS.md index 46316c37..f0220b85 100644 --- a/docs/API_CONTRACTS.md +++ b/docs/API_CONTRACTS.md @@ -384,6 +384,16 @@ WCS 1.0 tiles, validates EPSG:31370 and documented source values, masks the exact Area and persists an ordinary raster Dataset and DatasetVersion. It does not accept arbitrary URLs, coverage ids, resolutions or expressions. +The Flanders map workbench orchestrates this existing endpoint only after an +explicit municipality or rectangle selection. It requests all five governed +products for the same `bbox ∩ Area`, reuses the checksum-bound Dataset for an +identical request and then calls the normal selection endpoint. The browser +never contacts WCS directly. A whole-Flanders raster request is intentionally +unavailable in the map flow because it exceeds the bounded 60 km/30 million +cell guardrail; users must choose a municipality or draw a smaller rectangle. +An Area is recorded as `coverage_scope=municipality` only when its canonical +name starts with `Gemeente `; a regional Area remains `bounded_selection`. + ### POST `/api/v1/projects/{project_id}/datasets/{dataset_id}/raster/thematic/select` Returns source-correct metrics for a bbox and optional exact Area mask: diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 1c535d7b..770c0585 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -10154,3 +10154,31 @@ Validation: display limit. A final shell follow-up replaces the representative municipality count with regional profile/partition totals and uses the end-user label `Vlaanderen (285 gemeenten)`. + +## Sprint 237 - Bounded Flanders cross-domain profile (2026-07-17) + +Implemented: +- Reused the governed MercatorNet registry and canonical acquisition, + Dataset, analysis and image endpoints instead of introducing a second + provider path. +- Added a Flanders-only `Op aanvraag` state for ruimtebeslag 2025, open ruimte + 2022, inwonersdichtheid 2019, knooppuntwaarde 2022 and + voorzieningenniveau 2022. +- Made one explicit map rectangle or municipality analysis acquire/reuse all + five products for the same `bbox ∩ Area`, refresh the Dataset catalog and + keep the measured results available for switching, export and AI context. +- Kept the whole Flanders Area disabled for policy-raster acquisition under + the existing 60 km/30 million cell safety limits. Full-region vectors and + manifest-aware VHA analysis are unchanged. +- Corrected `coverage_scope` so only canonically named municipality Areas are + labelled municipality coverage; regional clipping remains a bounded + selection. + +Validation: +- A pre-implementation live probe acquired and persisted a real 143 x 224 + ruimtebeslag raster for a bounded Mol rectangle through Tower. The canonical + analysis returned 275.4 ha, 88.4563 percent and 311.34 ha valid raster area + with complete selection coverage. +- The complete local release gate passed 932 backend tests, backend + compilation, the 116-route contract audit, Alembic head `202607160001`, + frontend TypeScript typecheck and the production Vite build. diff --git a/docs/DATA_SOURCES.md b/docs/DATA_SOURCES.md index e9494ec5..a2569a65 100644 --- a/docs/DATA_SOURCES.md +++ b/docs/DATA_SOURCES.md @@ -73,6 +73,14 @@ publicly accessible land. Population is a 2019 raster estimate, not a current register count. Accessibility and service scores are not live travel times or object counts. +In `Flanders Regional Workbench` these five products are shown as +`Op aanvraag` until a user chooses a municipality or draws a bounded +rectangle. One selection acquires, persists and analyses the five products +through the canonical backend. Repeating the exact same product, bounds and +Area reuses the retained source evidence. GeoIntel does not preload 1,425 +municipality/product combinations and does not permit one unbounded +whole-Flanders WCS transfer. + ## DOV digital soil map - Service: `https://www.dov.vlaanderen.be/geoserver/wfs` @@ -704,11 +712,12 @@ provider output continues to use DatasetService and, for vectors, VectorFeatureService. Arbitrary service URLs, browser-side fetches and startup downloads remain forbidden. -The recommended next connector is a fixed allowlist of public Mercator -thematic rasters for space occupation, open space, population density, node -value and service level. The digital soil map should follow the existing -canonical vector persistence path. Watercourse/runoff additions remain in the -roadmap but no longer precede these cross-domain gaps. +The fixed allowlist of public Mercator thematic rasters for space occupation, +open space, population density, node value and service level is operational +for bounded Mol, Kempen and Flanders selections. The digital soil map follows +the canonical vector persistence path for Mol and Kempen. Additional Flemish +vector themes still require governed regional operators; a catalogue record +alone never makes them operational. ## Bathymetry, inland profiles and maritime scope diff --git a/docs/TODO.md b/docs/TODO.md index 97f8e744..151dc450 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -43,6 +43,9 @@ geen open productroadmap meer. kaart-, kwaliteits-, detectie- en segmentatiewerkstromen. - [x] Begrens de grootste frontendverantwoordelijkheden met afzonderlijke overzichts-, modelbeheer- en kaarthelpermodules zonder gedrag te wijzigen. +- [x] Maak de vijf bestaande officiële beleidsrasters veilig op aanvraag + beschikbaar voor elke begrensde selectie in Vlaanderen, met Dataset-cache, + semantische metrics en zonder 1.425 vooraf geladen rasters. Bewuste, niet-blokkerende grenzen: diff --git a/frontend/README.md b/frontend/README.md index cc9d3d08..b4d7ae70 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -71,6 +71,15 @@ but is not required for the primary choose-theme, draw-area, read-result flow. The primary workflow is deliberately short: choose a municipality or the complete region, choose a data theme, drag a rectangle on the MapLibre map and read the resulting PostGIS evidence. Releasing the drag runs the active theme query and every other available theme query for the same EPSG:4326 bbox. The active `area_id` constrains every drawn/manual selection to `bbox ∩ Area`; `Volledig werkgebied` uses a bbox enclosing the Area and therefore resolves to the exact persisted geometry. The result panel shows selection area, exact intersection totals, active-theme density, source identity and bounded feature properties. Map rendering remains capped at 1,000 features while `total_feature_count` reports the exact database count. +In the Flanders workbench, ruimtebeslag, open ruimte, population density, +node value and service level may appear as `Op aanvraag`. Drawing a rectangle +or choosing a municipality uses the existing backend Job/Dataset flow to +acquire and persist those five official rasters for the exact selection, then +shows all available semantic metrics together. Identical requests reuse the +persisted artifact. The complete Flanders Area is deliberately unavailable for +these rasters because it exceeds the backend safety ceiling; this does not +limit vector or partitioned bathymetry analysis of the complete region. + Detection Lab only lists ready imagery rasters. Governed height, flood-hazard and thematic policy rasters remain available in the map explorer but are excluded from the `Luchtbeeld` selector. @@ -92,7 +101,13 @@ local YOLO model, Detection persistence and automatic QA against ready GRB buildings. The panel shows all stages and errors; successful detections open as an explicit AI-result overlay. Rectangles must be 128-1,024 m per side. -The theme catalog currently recognizes buildings, population, forest/green, water, roads and parcels from dataset names and canonical `reference_layer_name` metadata. A theme is enabled only when a ready persisted vector dataset exists; otherwise it states `Bron nog niet ingeladen`. This prevents missing population or land-cover sources from appearing as zero-valued observations. The previous technical Map workspace remains available through `Geavanceerde werkbank` for derived datasets, QA/QC evidence and export operations. +The theme catalog recognizes the governed vector, raster and partitioned +sources through canonical source metadata. A theme is enabled only when a +ready persisted Dataset exists, except for the five allowlisted Flanders +policy rasters that explicitly show `Op aanvraag`. Other missing themes state +`Bron nog niet ingeladen`; they never appear as zero-valued observations. The +technical Map workspace remains available through `Geavanceerde werkbank` for +derived datasets, QA/QC evidence and export operations. The workbench uses a task-based shell instead of a single long panel stack. `App.tsx` still owns shared orchestration state, but Map is the default product entry and Overview, Data, QA/QC, AI Labs, Exports and System remain secondary workspaces with a persistent top context bar and an optional selection-detail drawer. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e4ca3a73..103212d4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -972,6 +972,9 @@ function App(): JSX.Element { onRunOrthophotoAnalysis={mapOrthophotoAnalysis.run} onSelectOrthophotoProduct={mapOrthophotoAnalysis.setSelectedProductKey} onClearQualityEvidence={clearQualityEvidenceGeoJson} + onRefreshProjectData={() => ( + selectedProjectId ? loadProjectData(selectedProjectId) : Promise.resolve(null) + )} onOpenAssistant={() => setActiveWorkspace('assistant')} onOpenExports={() => setActiveWorkspace('exports')} /> diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index d99980dd..088076c7 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -1,13 +1,15 @@ import { useEffect, useMemo, useState } from 'react' import GeoMap from '../GeoMap' -import type { AreaRead, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types' -import { useMapThemeSelectionInsights } from '../../hooks/useMapThemeSelectionInsights' +import type { AreaRead, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, ThematicRasterProductRead, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types' +import { useMapThemeSelectionInsights, type MapThemeQuery } from '../../hooks/useMapThemeSelectionInsights' +import { useThematicRasterProducts } from '../../hooks/useThematicRasterProducts' import { useTemporalComparison } from '../../hooks/useTemporalComparison' import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay' import { TemporalTrendChart } from './TemporalTrendChart' import { terrainImageUrl } from '../../lib/terrainImage' import { floodHazardImageUrl } from '../../lib/floodHazardImage' import { thematicRasterImageUrl } from '../../lib/thematicRaster' +import { FLANDERS_WORKSPACE_PROJECT_NAME } from '../../config/primaryFocus' import { bboxToInputState, bboxesEqual, @@ -548,6 +550,7 @@ interface MapWorkspaceProps { onRunOrthophotoAnalysis: (bbox: VectorSelectionBBox) => Promise onSelectOrthophotoProduct: (productKey: string) => void onClearQualityEvidence?: () => void + onRefreshProjectData: () => Promise onOpenAssistant: () => void onOpenExports: () => void } @@ -632,6 +635,7 @@ export function MapWorkspace({ onRunOrthophotoAnalysis, onSelectOrthophotoProduct, onClearQualityEvidence, + onRefreshProjectData, onOpenAssistant, onOpenExports, }: MapWorkspaceProps): JSX.Element { @@ -640,13 +644,21 @@ export function MapWorkspace({ const selectedDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null return themeIdForDataset(selectedDataset) ?? 'buildings' }) + const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null + const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied' + const flandersScopeSelected = activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME const { themeInsights, themeInsightsLoading: themeResultsLoading, themeInsightsError: themeResultsError, loadThemeInsights, clearThemeInsights, - } = useMapThemeSelectionInsights(selectedProjectId) + } = useMapThemeSelectionInsights(selectedProjectId, onRefreshProjectData) + const { + products: thematicRasterProducts, + loading: thematicRasterProductsLoading, + error: thematicRasterProductsError, + } = useThematicRasterProducts(flandersScopeSelected ? selectedProjectId : null) const { temporalComparison, temporalComparisonLoading, @@ -747,6 +759,15 @@ export function MapWorkspace({ [availableMapDatasets, regionalScopeSelected, selectedMapAreaId, themeDatasetMap], ) const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0] + const onDemandThematicProductMap = useMemo( + () => flandersScopeSelected + ? new Map( + thematicRasterProducts.map((product) => [product.theme, product]), + ) + : new Map(), + [flandersScopeSelected, thematicRasterProducts], + ) + const activeOnDemandThematicProduct = onDemandThematicProductMap.get(activeTheme.id) ?? null const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id] const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection) const selectedOrthophotoProduct = orthophotoProducts.find((item) => item.key === selectedOrthophotoProductKey) ?? null @@ -766,6 +787,9 @@ export function MapWorkspace({ const regionalRasterThemeActive = regionalScopeSelected && isPartitionedRaster(activeThemeDataset) const regionalBathymetryThemeActive = regionalScopeSelected && isPartitionedBathymetry(activeThemeDataset) const regionalPartitionedThemeActive = regionalRasterThemeActive || regionalBathymetryThemeActive + const onDemandThematicThemeActive = analysisMode === 'current' && Boolean(activeOnDemandThematicProduct) + const regionalOnDemandThematicThemeActive = regionalScopeSelected && onDemandThematicThemeActive + const activeThemeAvailable = Boolean(activeThemeDataset) || onDemandThematicThemeActive const terrainImageOverlays = useMemo( () => activeTheme.id === 'elevation' && selectedProjectId @@ -830,8 +854,6 @@ export function MapWorkspace({ : orthophotoImageOverlay ? [orthophotoImageOverlay] : [], [floodHazardImageOverlays, orthophotoImageOverlay, terrainImageOverlays, thematicRasterImageOverlays], ) - const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null - const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied' const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length const themeTemporalSeriesMap = useMemo( () => @@ -855,11 +877,12 @@ export function MapWorkspace({ useEffect(() => { const contextSourceLabel = analysisMode === 'evolution' ? activeTemporalSeriesGroup?.label ?? null : regionalBathymetryThemeActive ? 'VHA-dwarsprofielen Vlaanderen' - : null + : activeOnDemandThematicProduct?.display_name ?? null onSetContextSourceLabel(contextSourceLabel) return () => onSetContextSourceLabel(null) }, [ activeTemporalSeriesGroup?.label, + activeOnDemandThematicProduct?.display_name, analysisMode, onSetContextSourceLabel, regionalBathymetryThemeActive, @@ -873,7 +896,9 @@ export function MapWorkspace({ }), [themeInsights], ) - const activeSelectionResult = themeResults.find((item) => item.theme.id === activeThemeId)?.result + const activeThemeInsight = themeResults.find((item) => item.theme.id === activeThemeId) + const activeResultDataset = activeThemeInsight?.dataset ?? activeThemeDataset + const activeSelectionResult = activeThemeInsight?.result ?? (!regionalPartitionedThemeActive && selectedMapDataset?.id === activeThemeDataset?.id ? mapSelectionResult : null) const explorerMapFeatureCollection = regionalBathymetryThemeActive ? null @@ -1066,15 +1091,15 @@ export function MapWorkspace({ if (!activeSelectionResult) { return } - if (activeThemeDataset?.dataset_type === 'raster') { + if (activeResultDataset?.dataset_type === 'raster') { downloadJsonFile(`${activeTheme.id}-analysis.json`, { project_id: selectedProjectId, area_id: areaIdForSelection(mapSelectionBbox) ?? null, area_name: selectedMapArea?.name ?? null, theme: activeTheme, - dataset_id: activeThemeDataset.id, - dataset_name: activeThemeDataset.name, - source_name: activeThemeDataset.source_name, + dataset_id: activeResultDataset.id, + dataset_name: activeResultDataset.name, + source_name: activeResultDataset.source_name, result: activeSelectionResult, }) return @@ -1116,19 +1141,19 @@ export function MapWorkspace({ name: `${activeTheme.id}-evolution`, } : null - : activeSelectionResult && activeThemeDataset + : activeSelectionResult && activeResultDataset ? { project_id: selectedProjectId, mode: 'current', bbox: { ...mapSelectionBbox, crs: 'EPSG:4326' }, - dataset_id: activeThemeDataset.id, + dataset_id: activeResultDataset.id, area_id: areaId, partitioned: regionalPartitionedThemeActive, product_key: regionalRasterThemeActive - ? String(activeThemeDataset.source_metadata?.['product_key'] ?? '') || undefined + ? String(activeResultDataset.source_metadata?.['product_key'] ?? '') || undefined : undefined, partition_scope_key: regionalBathymetryThemeActive - ? String(activeThemeDataset.source_metadata?.['partition_scope_key'] ?? 'flanders') + ? String(activeResultDataset.source_metadata?.['partition_scope_key'] ?? 'flanders') : undefined, theme_id: activeTheme.id, name: `${activeTheme.id}-analysis`, @@ -1172,12 +1197,17 @@ export function MapWorkspace({ const dataset = analysisMode === 'evolution' ? temporalGroup?.items[temporalGroup.items.length - 1] ?? null : themeDatasetMap[theme.id] - if (!dataset) { + const onDemandProduct = analysisMode === 'current' + ? onDemandThematicProductMap.get(theme.id) + : null + if (!dataset && !onDemandProduct) { return } setActiveThemeId(theme.id) clearTemporalComparison() - onOpenDatasetInMap(dataset) + if (dataset) { + onOpenDatasetInMap(dataset) + } } const setExplorerMode = (mode: 'current' | 'evolution') => { @@ -1196,24 +1226,35 @@ export function MapWorkspace({ } const loadAllThemeResults = async (bbox: VectorSelectionBBox, areaId?: string) => { - const availableThemes = DATA_THEMES.flatMap((theme) => { + const availableThemes: Array> = [] + for (const theme of DATA_THEMES) { + const thematicProduct = analysisMode === 'current' + ? onDemandThematicProductMap.get(theme.id) + : null + if (thematicProduct) { + availableThemes.push({ + themeId: theme.id, + thematicProductKey: thematicProduct.key, + }) + continue + } const dataset = themeDatasetMap[theme.id] - return dataset - ? [{ - themeId: theme.id, - dataset, - partitioned: regionalScopeSelected - && (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)), - }] - : [] - }) + if (dataset) { + availableThemes.push({ + themeId: theme.id, + dataset, + partitioned: regionalScopeSelected + && (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)), + }) + } + } await loadThemeInsights(bbox, availableThemes, areaId) } const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => { setSelectionBbox(bbox) const tasks: Array> = [loadAllThemeResults(bbox, areaId)] - if (!regionalPartitionedThemeActive) { + if (!regionalPartitionedThemeActive && !onDemandThematicThemeActive) { tasks.push(onRunMapSelectionExtract(bbox, areaId)) } if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) { @@ -1374,11 +1415,14 @@ export function MapWorkspace({
{DATA_THEMES.map((theme) => { const dataset = themeDatasetMap[theme.id] + const onDemandProduct = onDemandThematicProductMap.get(theme.id) const partitions = themePartitionMap[theme.id] const temporalGroups = themeTemporalSeriesMap[theme.id] const temporalGroup = temporalGroups[0] const evolutionAvailable = temporalGroups.some((group) => group.items.length >= 2) - const available = Boolean(dataset) && (analysisMode === 'current' || evolutionAvailable) + const available = analysisMode === 'current' + ? Boolean(dataset || onDemandProduct) + : Boolean(dataset) && evolutionAvailable const active = activeThemeId === theme.id const temporalRange = temporalGroup ? temporalRangeLabel(temporalGroup.items) : null return ( @@ -1402,13 +1446,15 @@ export function MapWorkspace({ : 'Bron nog niet ingeladen' : dataset ? datasetAvailabilityLabel(dataset, partitions) + : onDemandProduct + ? `${onDemandProduct.native_resolution_m} m · ${onDemandProduct.observation_year} · laad bij selectie` : 'Bron nog niet ingeladen'} {analysisMode === 'evolution' ? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt' - : dataset ? 'Beschikbaar' : 'Ontbreekt'} + : dataset ? 'Beschikbaar' : onDemandProduct ? 'Op aanvraag' : 'Ontbreekt'} ) @@ -1424,7 +1470,9 @@ export function MapWorkspace({ ? activeTemporalSeriesGroup?.label ?? 'Nog geen historische reeks ingeladen' : regionalBathymetryThemeActive ? 'VHA-dwarsprofielen Vlaanderen' - : activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'} + : activeThemeDataset + ? getDatasetDisplayName(activeThemeDataset) + : activeOnDemandThematicProduct?.display_name ?? 'Geen databron beschikbaar'} {analysisOverlayActive @@ -1437,9 +1485,17 @@ export function MapWorkspace({ ? `${activeThemePartitions.length} gecontroleerde gemeentepartities · selectie wordt ruimtelijk samengevoegd` : activeThemeDataset ? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatDatasetObservation(activeThemeDataset)}` + : activeOnDemandThematicProduct + ? `${activeOnDemandThematicProduct.attribution} · wordt alleen voor de gekozen selectie ingeladen` : activeTheme.description}
+ {thematicRasterProductsLoading && flandersScopeSelected ? ( +

Beschikbare Vlaamse beleidsbronnen worden gecontroleerd…

+ ) : null} + {thematicRasterProductsError && flandersScopeSelected ? ( +

{thematicRasterProductsError}

+ ) : null} {analysisMode === 'current' && activeTheme.id === 'flood_hazard' && floodHazardDatasets.length > 0 ? (