Add bounded Flanders thematic analysis
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-17 18:28:48 +02:00
parent 21103441cf
commit ce597ee0b4
17 changed files with 389 additions and 55 deletions
+16
View File
@@ -7,6 +7,22 @@
# Changelog # 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) ## Sprint 236 Flemish bathymetry partitions and safe North Sea probe (2026-07-17)
- Added dynamic provisioning of the complete official VRBG Flemish - Added dynamic provisioning of the complete official VRBG Flemish
+9
View File
@@ -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 tiles, product identifiers remain server-allowlisted and other raster
pipelines retain their smaller independent limits. 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 Provision the official DOV soil polygons for Mol through the existing vector
upload path: upload path:
@@ -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) raise AppError(code="THEMATIC_RASTER_SELECTION_OUTSIDE_AREA", message="Selection does not overlap the selected work area", status_code=422)
return intersection 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 @staticmethod
def _fetch(request_url: str, settings: Settings, opener: Callable[..., Any] | None = None) -> tuple[bytes, str]: 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"}) 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, "license_note": ThematicRasterAcquisitionService.LICENSE_NOTE,
"legend_min_label": product.legend_min_label, "legend_min_label": product.legend_min_label,
"legend_max_label": product.legend_max_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={ provenance_metadata={
"acquisition": "explicit_bounded_tiled_wcs_coverage", "acquisition": "explicit_bounded_tiled_wcs_coverage",
@@ -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: def test_evolution_theme_catalog_distinguishes_history_from_current_only_data() -> None:
workspace = read("frontend/src/components/map/MapWorkspace.tsx") 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 "meetmomenten" in workspace
assert "Tijdreeks" in workspace assert "Tijdreeks" in workspace
assert "Alleen huidige toestand" in workspace assert "Alleen huidige toestand" in workspace
@@ -17,7 +17,7 @@ from app.core.config import Settings
from app.core.errors import AppError from app.core.errors import AppError
from app.db.session import get_db from app.db.session import get_db
from app.main import app 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.thematic_raster import ThematicRasterAcquireRequest, ThematicRasterSelectionRequest
from app.schemas.assistant import AssistantQueryRequest from app.schemas.assistant import AssistantQueryRequest
from app.services.geo_assistant_service import GeoAssistantService 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" 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: def test_wcs_fetch_retries_an_incomplete_tile_without_accepting_partial_bytes(monkeypatch) -> None:
content = b"II*\x00complete-geotiff" content = b"II*\x00complete-geotiff"
responses = [IncompleteResponse(content), FakeResponse(content)] responses = [IncompleteResponse(content), FakeResponse(content)]
@@ -339,7 +339,8 @@ def test_theme_failures_name_the_source_and_reason() -> None:
root = Path(__file__).resolve().parents[2] root = Path(__file__).resolve().parents[2]
hook = (root / "frontend/src/hooks/useMapThemeSelectionInsights.ts").read_text(encoding="utf-8") 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 "reason: formatError(item.reason" in hook
assert "failure.dataset}: ${failure.reason}" in hook assert "failure.dataset}: ${failure.reason}" in hook
@@ -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<DataThemeId, ThematicRasterProductRead>" 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
+10
View File
@@ -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 exact Area and persists an ordinary raster Dataset and DatasetVersion. It does
not accept arbitrary URLs, coverage ids, resolutions or expressions. 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` ### POST `/api/v1/projects/{project_id}/datasets/{dataset_id}/raster/thematic/select`
Returns source-correct metrics for a bbox and optional exact Area mask: Returns source-correct metrics for a bbox and optional exact Area mask:
+28
View File
@@ -10154,3 +10154,31 @@ Validation:
display limit. A final shell follow-up replaces the representative display limit. A final shell follow-up replaces the representative
municipality count with regional profile/partition totals and uses the municipality count with regional profile/partition totals and uses the
end-user label `Vlaanderen (285 gemeenten)`. 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.
+14 -5
View File
@@ -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 register count. Accessibility and service scores are not live travel times or
object counts. 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 ## DOV digital soil map
- Service: `https://www.dov.vlaanderen.be/geoserver/wfs` - 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 VectorFeatureService. Arbitrary service URLs, browser-side fetches and startup
downloads remain forbidden. downloads remain forbidden.
The recommended next connector is a fixed allowlist of public Mercator The fixed allowlist of public Mercator thematic rasters for space occupation,
thematic rasters for space occupation, open space, population density, node open space, population density, node value and service level is operational
value and service level. The digital soil map should follow the existing for bounded Mol, Kempen and Flanders selections. The digital soil map follows
canonical vector persistence path. Watercourse/runoff additions remain in the the canonical vector persistence path for Mol and Kempen. Additional Flemish
roadmap but no longer precede these cross-domain gaps. vector themes still require governed regional operators; a catalogue record
alone never makes them operational.
## Bathymetry, inland profiles and maritime scope ## Bathymetry, inland profiles and maritime scope
+3
View File
@@ -43,6 +43,9 @@ geen open productroadmap meer.
kaart-, kwaliteits-, detectie- en segmentatiewerkstromen. kaart-, kwaliteits-, detectie- en segmentatiewerkstromen.
- [x] Begrens de grootste frontendverantwoordelijkheden met afzonderlijke - [x] Begrens de grootste frontendverantwoordelijkheden met afzonderlijke
overzichts-, modelbeheer- en kaarthelpermodules zonder gedrag te wijzigen. 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: Bewuste, niet-blokkerende grenzen:
+16 -1
View File
@@ -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. 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 Detection Lab only lists ready imagery rasters. Governed height, flood-hazard
and thematic policy rasters remain available in the map explorer but are and thematic policy rasters remain available in the map explorer but are
excluded from the `Luchtbeeld` selector. 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 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. 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. 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.
+3
View File
@@ -972,6 +972,9 @@ function App(): JSX.Element {
onRunOrthophotoAnalysis={mapOrthophotoAnalysis.run} onRunOrthophotoAnalysis={mapOrthophotoAnalysis.run}
onSelectOrthophotoProduct={mapOrthophotoAnalysis.setSelectedProductKey} onSelectOrthophotoProduct={mapOrthophotoAnalysis.setSelectedProductKey}
onClearQualityEvidence={clearQualityEvidenceGeoJson} onClearQualityEvidence={clearQualityEvidenceGeoJson}
onRefreshProjectData={() => (
selectedProjectId ? loadProjectData(selectedProjectId) : Promise.resolve(null)
)}
onOpenAssistant={() => setActiveWorkspace('assistant')} onOpenAssistant={() => setActiveWorkspace('assistant')}
onOpenExports={() => setActiveWorkspace('exports')} onOpenExports={() => setActiveWorkspace('exports')}
/> />
+100 -37
View File
@@ -1,13 +1,15 @@
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import GeoMap from '../GeoMap' import GeoMap from '../GeoMap'
import type { AreaRead, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types' import type { AreaRead, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, ThematicRasterProductRead, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types'
import { useMapThemeSelectionInsights } from '../../hooks/useMapThemeSelectionInsights' import { useMapThemeSelectionInsights, type MapThemeQuery } from '../../hooks/useMapThemeSelectionInsights'
import { useThematicRasterProducts } from '../../hooks/useThematicRasterProducts'
import { useTemporalComparison } from '../../hooks/useTemporalComparison' import { useTemporalComparison } from '../../hooks/useTemporalComparison'
import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay' import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay'
import { TemporalTrendChart } from './TemporalTrendChart' import { TemporalTrendChart } from './TemporalTrendChart'
import { terrainImageUrl } from '../../lib/terrainImage' import { terrainImageUrl } from '../../lib/terrainImage'
import { floodHazardImageUrl } from '../../lib/floodHazardImage' import { floodHazardImageUrl } from '../../lib/floodHazardImage'
import { thematicRasterImageUrl } from '../../lib/thematicRaster' import { thematicRasterImageUrl } from '../../lib/thematicRaster'
import { FLANDERS_WORKSPACE_PROJECT_NAME } from '../../config/primaryFocus'
import { import {
bboxToInputState, bboxToInputState,
bboxesEqual, bboxesEqual,
@@ -548,6 +550,7 @@ interface MapWorkspaceProps {
onRunOrthophotoAnalysis: (bbox: VectorSelectionBBox) => Promise<boolean> onRunOrthophotoAnalysis: (bbox: VectorSelectionBBox) => Promise<boolean>
onSelectOrthophotoProduct: (productKey: string) => void onSelectOrthophotoProduct: (productKey: string) => void
onClearQualityEvidence?: () => void onClearQualityEvidence?: () => void
onRefreshProjectData: () => Promise<unknown>
onOpenAssistant: () => void onOpenAssistant: () => void
onOpenExports: () => void onOpenExports: () => void
} }
@@ -632,6 +635,7 @@ export function MapWorkspace({
onRunOrthophotoAnalysis, onRunOrthophotoAnalysis,
onSelectOrthophotoProduct, onSelectOrthophotoProduct,
onClearQualityEvidence, onClearQualityEvidence,
onRefreshProjectData,
onOpenAssistant, onOpenAssistant,
onOpenExports, onOpenExports,
}: MapWorkspaceProps): JSX.Element { }: MapWorkspaceProps): JSX.Element {
@@ -640,13 +644,21 @@ export function MapWorkspace({
const selectedDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null const selectedDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null
return themeIdForDataset(selectedDataset) ?? 'buildings' 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 { const {
themeInsights, themeInsights,
themeInsightsLoading: themeResultsLoading, themeInsightsLoading: themeResultsLoading,
themeInsightsError: themeResultsError, themeInsightsError: themeResultsError,
loadThemeInsights, loadThemeInsights,
clearThemeInsights, clearThemeInsights,
} = useMapThemeSelectionInsights<DataThemeId>(selectedProjectId) } = useMapThemeSelectionInsights<DataThemeId>(selectedProjectId, onRefreshProjectData)
const {
products: thematicRasterProducts,
loading: thematicRasterProductsLoading,
error: thematicRasterProductsError,
} = useThematicRasterProducts(flandersScopeSelected ? selectedProjectId : null)
const { const {
temporalComparison, temporalComparison,
temporalComparisonLoading, temporalComparisonLoading,
@@ -747,6 +759,15 @@ export function MapWorkspace({
[availableMapDatasets, regionalScopeSelected, selectedMapAreaId, themeDatasetMap], [availableMapDatasets, regionalScopeSelected, selectedMapAreaId, themeDatasetMap],
) )
const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0] const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0]
const onDemandThematicProductMap = useMemo(
() => flandersScopeSelected
? new Map<DataThemeId, ThematicRasterProductRead>(
thematicRasterProducts.map((product) => [product.theme, product]),
)
: new Map<DataThemeId, ThematicRasterProductRead>(),
[flandersScopeSelected, thematicRasterProducts],
)
const activeOnDemandThematicProduct = onDemandThematicProductMap.get(activeTheme.id) ?? null
const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id] const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id]
const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection) const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection)
const selectedOrthophotoProduct = orthophotoProducts.find((item) => item.key === selectedOrthophotoProductKey) ?? null const selectedOrthophotoProduct = orthophotoProducts.find((item) => item.key === selectedOrthophotoProductKey) ?? null
@@ -766,6 +787,9 @@ export function MapWorkspace({
const regionalRasterThemeActive = regionalScopeSelected && isPartitionedRaster(activeThemeDataset) const regionalRasterThemeActive = regionalScopeSelected && isPartitionedRaster(activeThemeDataset)
const regionalBathymetryThemeActive = regionalScopeSelected && isPartitionedBathymetry(activeThemeDataset) const regionalBathymetryThemeActive = regionalScopeSelected && isPartitionedBathymetry(activeThemeDataset)
const regionalPartitionedThemeActive = regionalRasterThemeActive || regionalBathymetryThemeActive const regionalPartitionedThemeActive = regionalRasterThemeActive || regionalBathymetryThemeActive
const onDemandThematicThemeActive = analysisMode === 'current' && Boolean(activeOnDemandThematicProduct)
const regionalOnDemandThematicThemeActive = regionalScopeSelected && onDemandThematicThemeActive
const activeThemeAvailable = Boolean(activeThemeDataset) || onDemandThematicThemeActive
const terrainImageOverlays = useMemo( const terrainImageOverlays = useMemo(
() => () =>
activeTheme.id === 'elevation' && selectedProjectId activeTheme.id === 'elevation' && selectedProjectId
@@ -830,8 +854,6 @@ export function MapWorkspace({
: orthophotoImageOverlay ? [orthophotoImageOverlay] : [], : orthophotoImageOverlay ? [orthophotoImageOverlay] : [],
[floodHazardImageOverlays, orthophotoImageOverlay, terrainImageOverlays, thematicRasterImageOverlays], [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 municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
const themeTemporalSeriesMap = useMemo( const themeTemporalSeriesMap = useMemo(
() => () =>
@@ -855,11 +877,12 @@ export function MapWorkspace({
useEffect(() => { useEffect(() => {
const contextSourceLabel = analysisMode === 'evolution' ? activeTemporalSeriesGroup?.label ?? null const contextSourceLabel = analysisMode === 'evolution' ? activeTemporalSeriesGroup?.label ?? null
: regionalBathymetryThemeActive ? 'VHA-dwarsprofielen Vlaanderen' : regionalBathymetryThemeActive ? 'VHA-dwarsprofielen Vlaanderen'
: null : activeOnDemandThematicProduct?.display_name ?? null
onSetContextSourceLabel(contextSourceLabel) onSetContextSourceLabel(contextSourceLabel)
return () => onSetContextSourceLabel(null) return () => onSetContextSourceLabel(null)
}, [ }, [
activeTemporalSeriesGroup?.label, activeTemporalSeriesGroup?.label,
activeOnDemandThematicProduct?.display_name,
analysisMode, analysisMode,
onSetContextSourceLabel, onSetContextSourceLabel,
regionalBathymetryThemeActive, regionalBathymetryThemeActive,
@@ -873,7 +896,9 @@ export function MapWorkspace({
}), }),
[themeInsights], [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) ?? (!regionalPartitionedThemeActive && selectedMapDataset?.id === activeThemeDataset?.id ? mapSelectionResult : null)
const explorerMapFeatureCollection = regionalBathymetryThemeActive const explorerMapFeatureCollection = regionalBathymetryThemeActive
? null ? null
@@ -1066,15 +1091,15 @@ export function MapWorkspace({
if (!activeSelectionResult) { if (!activeSelectionResult) {
return return
} }
if (activeThemeDataset?.dataset_type === 'raster') { if (activeResultDataset?.dataset_type === 'raster') {
downloadJsonFile(`${activeTheme.id}-analysis.json`, { downloadJsonFile(`${activeTheme.id}-analysis.json`, {
project_id: selectedProjectId, project_id: selectedProjectId,
area_id: areaIdForSelection(mapSelectionBbox) ?? null, area_id: areaIdForSelection(mapSelectionBbox) ?? null,
area_name: selectedMapArea?.name ?? null, area_name: selectedMapArea?.name ?? null,
theme: activeTheme, theme: activeTheme,
dataset_id: activeThemeDataset.id, dataset_id: activeResultDataset.id,
dataset_name: activeThemeDataset.name, dataset_name: activeResultDataset.name,
source_name: activeThemeDataset.source_name, source_name: activeResultDataset.source_name,
result: activeSelectionResult, result: activeSelectionResult,
}) })
return return
@@ -1116,19 +1141,19 @@ export function MapWorkspace({
name: `${activeTheme.id}-evolution`, name: `${activeTheme.id}-evolution`,
} }
: null : null
: activeSelectionResult && activeThemeDataset : activeSelectionResult && activeResultDataset
? { ? {
project_id: selectedProjectId, project_id: selectedProjectId,
mode: 'current', mode: 'current',
bbox: { ...mapSelectionBbox, crs: 'EPSG:4326' }, bbox: { ...mapSelectionBbox, crs: 'EPSG:4326' },
dataset_id: activeThemeDataset.id, dataset_id: activeResultDataset.id,
area_id: areaId, area_id: areaId,
partitioned: regionalPartitionedThemeActive, partitioned: regionalPartitionedThemeActive,
product_key: regionalRasterThemeActive product_key: regionalRasterThemeActive
? String(activeThemeDataset.source_metadata?.['product_key'] ?? '') || undefined ? String(activeResultDataset.source_metadata?.['product_key'] ?? '') || undefined
: undefined, : undefined,
partition_scope_key: regionalBathymetryThemeActive partition_scope_key: regionalBathymetryThemeActive
? String(activeThemeDataset.source_metadata?.['partition_scope_key'] ?? 'flanders') ? String(activeResultDataset.source_metadata?.['partition_scope_key'] ?? 'flanders')
: undefined, : undefined,
theme_id: activeTheme.id, theme_id: activeTheme.id,
name: `${activeTheme.id}-analysis`, name: `${activeTheme.id}-analysis`,
@@ -1172,12 +1197,17 @@ export function MapWorkspace({
const dataset = analysisMode === 'evolution' const dataset = analysisMode === 'evolution'
? temporalGroup?.items[temporalGroup.items.length - 1] ?? null ? temporalGroup?.items[temporalGroup.items.length - 1] ?? null
: themeDatasetMap[theme.id] : themeDatasetMap[theme.id]
if (!dataset) { const onDemandProduct = analysisMode === 'current'
? onDemandThematicProductMap.get(theme.id)
: null
if (!dataset && !onDemandProduct) {
return return
} }
setActiveThemeId(theme.id) setActiveThemeId(theme.id)
clearTemporalComparison() clearTemporalComparison()
onOpenDatasetInMap(dataset) if (dataset) {
onOpenDatasetInMap(dataset)
}
} }
const setExplorerMode = (mode: 'current' | 'evolution') => { const setExplorerMode = (mode: 'current' | 'evolution') => {
@@ -1196,24 +1226,35 @@ export function MapWorkspace({
} }
const loadAllThemeResults = async (bbox: VectorSelectionBBox, areaId?: string) => { const loadAllThemeResults = async (bbox: VectorSelectionBBox, areaId?: string) => {
const availableThemes = DATA_THEMES.flatMap((theme) => { const availableThemes: Array<MapThemeQuery<DataThemeId>> = []
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] const dataset = themeDatasetMap[theme.id]
return dataset if (dataset) {
? [{ availableThemes.push({
themeId: theme.id, themeId: theme.id,
dataset, dataset,
partitioned: regionalScopeSelected partitioned: regionalScopeSelected
&& (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)), && (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)),
}] })
: [] }
}) }
await loadThemeInsights(bbox, availableThemes, areaId) await loadThemeInsights(bbox, availableThemes, areaId)
} }
const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => { const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => {
setSelectionBbox(bbox) setSelectionBbox(bbox)
const tasks: Array<Promise<unknown>> = [loadAllThemeResults(bbox, areaId)] const tasks: Array<Promise<unknown>> = [loadAllThemeResults(bbox, areaId)]
if (!regionalPartitionedThemeActive) { if (!regionalPartitionedThemeActive && !onDemandThematicThemeActive) {
tasks.push(onRunMapSelectionExtract(bbox, areaId)) tasks.push(onRunMapSelectionExtract(bbox, areaId))
} }
if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) { if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) {
@@ -1374,11 +1415,14 @@ export function MapWorkspace({
<div className="geo-theme-list"> <div className="geo-theme-list">
{DATA_THEMES.map((theme) => { {DATA_THEMES.map((theme) => {
const dataset = themeDatasetMap[theme.id] const dataset = themeDatasetMap[theme.id]
const onDemandProduct = onDemandThematicProductMap.get(theme.id)
const partitions = themePartitionMap[theme.id] const partitions = themePartitionMap[theme.id]
const temporalGroups = themeTemporalSeriesMap[theme.id] const temporalGroups = themeTemporalSeriesMap[theme.id]
const temporalGroup = temporalGroups[0] const temporalGroup = temporalGroups[0]
const evolutionAvailable = temporalGroups.some((group) => group.items.length >= 2) 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 active = activeThemeId === theme.id
const temporalRange = temporalGroup ? temporalRangeLabel(temporalGroup.items) : null const temporalRange = temporalGroup ? temporalRangeLabel(temporalGroup.items) : null
return ( return (
@@ -1402,13 +1446,15 @@ export function MapWorkspace({
: 'Bron nog niet ingeladen' : 'Bron nog niet ingeladen'
: dataset : dataset
? datasetAvailabilityLabel(dataset, partitions) ? datasetAvailabilityLabel(dataset, partitions)
: onDemandProduct
? `${onDemandProduct.native_resolution_m} m · ${onDemandProduct.observation_year} · laad bij selectie`
: 'Bron nog niet ingeladen'} : 'Bron nog niet ingeladen'}
</small> </small>
</span> </span>
<i> <i>
{analysisMode === 'evolution' {analysisMode === 'evolution'
? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt' ? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt'
: dataset ? 'Beschikbaar' : 'Ontbreekt'} : dataset ? 'Beschikbaar' : onDemandProduct ? 'Op aanvraag' : 'Ontbreekt'}
</i> </i>
</button> </button>
) )
@@ -1424,7 +1470,9 @@ export function MapWorkspace({
? activeTemporalSeriesGroup?.label ?? 'Nog geen historische reeks ingeladen' ? activeTemporalSeriesGroup?.label ?? 'Nog geen historische reeks ingeladen'
: regionalBathymetryThemeActive : regionalBathymetryThemeActive
? 'VHA-dwarsprofielen Vlaanderen' ? 'VHA-dwarsprofielen Vlaanderen'
: activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'} : activeThemeDataset
? getDatasetDisplayName(activeThemeDataset)
: activeOnDemandThematicProduct?.display_name ?? 'Geen databron beschikbaar'}
</strong> </strong>
<small> <small>
{analysisOverlayActive {analysisOverlayActive
@@ -1437,9 +1485,17 @@ export function MapWorkspace({
? `${activeThemePartitions.length} gecontroleerde gemeentepartities · selectie wordt ruimtelijk samengevoegd` ? `${activeThemePartitions.length} gecontroleerde gemeentepartities · selectie wordt ruimtelijk samengevoegd`
: activeThemeDataset : activeThemeDataset
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatDatasetObservation(activeThemeDataset)}` ? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatDatasetObservation(activeThemeDataset)}`
: activeOnDemandThematicProduct
? `${activeOnDemandThematicProduct.attribution} · wordt alleen voor de gekozen selectie ingeladen`
: activeTheme.description} : activeTheme.description}
</small> </small>
</div> </div>
{thematicRasterProductsLoading && flandersScopeSelected ? (
<p className="geo-data-notice">Beschikbare Vlaamse beleidsbronnen worden gecontroleerd</p>
) : null}
{thematicRasterProductsError && flandersScopeSelected ? (
<p className="error">{thematicRasterProductsError}</p>
) : null}
{analysisMode === 'current' && activeTheme.id === 'flood_hazard' && floodHazardDatasets.length > 0 ? ( {analysisMode === 'current' && activeTheme.id === 'flood_hazard' && floodHazardDatasets.length > 0 ? (
<label className="geo-scope-select"> <label className="geo-scope-select">
@@ -1537,6 +1593,8 @@ export function MapWorkspace({
? 'Sleep nu een rechthoek op de kaart.' ? 'Sleep nu een rechthoek op de kaart.'
: regionalRasterThemeActive : regionalRasterThemeActive
? 'Teken een rechthoek; de juiste gemeentelijke rasters worden automatisch gecombineerd.' ? 'Teken een rechthoek; de juiste gemeentelijke rasters worden automatisch gecombineerd.'
: onDemandThematicThemeActive
? 'Teken een rechthoek; officiële Vlaamse rasters worden begrensd opgehaald, bewaard en hergebruikt.'
: regionalBathymetryThemeActive : regionalBathymetryThemeActive
? 'Teken een rechthoek of analyseer Vlaanderen; alleen overlappende VHA-partities worden samengevoegd.' ? 'Teken een rechthoek of analyseer Vlaanderen; alleen overlappende VHA-partities worden samengevoegd.'
: 'Sleep een rechthoek of analyseer het volledige werkgebied.'} : 'Sleep een rechthoek of analyseer het volledige werkgebied.'}
@@ -1546,7 +1604,7 @@ export function MapWorkspace({
<div className="geo-map-actions"> <div className="geo-map-actions">
<button <button
className={bboxSelectionMode ? 'primary-action geo-draw-active' : 'primary-action'} className={bboxSelectionMode ? 'primary-action geo-draw-active' : 'primary-action'}
disabled={!activeThemeDataset || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || mapSelectionLoading || themeResultsLoading} disabled={!activeThemeAvailable || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || mapSelectionLoading || themeResultsLoading}
type="button" type="button"
onClick={startBboxSelection} onClick={startBboxSelection}
> >
@@ -1554,12 +1612,16 @@ export function MapWorkspace({
</button> </button>
<button <button
className="secondary-action" className="secondary-action"
disabled={!activeThemeDataset || regionalRasterThemeActive || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading} disabled={!activeThemeAvailable || regionalRasterThemeActive || regionalOnDemandThematicThemeActive || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
type="button" type="button"
title={regionalRasterThemeActive ? 'Teken een begrensde rechthoek voor een regionale rasteranalyse.' : undefined} title={
regionalRasterThemeActive || regionalOnDemandThematicThemeActive
? 'Teken een begrensde rechthoek voor een regionale rasteranalyse.'
: undefined
}
onClick={() => selectedAreaBbox && void analyzeSelection(selectedAreaBbox, selectedMapArea?.id)} onClick={() => selectedAreaBbox && void analyzeSelection(selectedAreaBbox, selectedMapArea?.id)}
> >
{regionalRasterThemeActive ? 'Selecteer een deelgebied' : 'Volledig werkgebied'} {regionalRasterThemeActive || regionalOnDemandThematicThemeActive ? 'Selecteer een deelgebied' : 'Volledig werkgebied'}
</button> </button>
<button className="secondary-action" disabled={!mapSelectionBbox} type="button" onClick={clearAreaSelection}> <button className="secondary-action" disabled={!mapSelectionBbox} type="button" onClick={clearAreaSelection}>
Wis selectie Wis selectie
@@ -1881,7 +1943,7 @@ export function MapWorkspace({
{analysisMode === 'current' ? ( {analysisMode === 'current' ? (
<div className="geo-result-actions"> <div className="geo-result-actions">
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={downloadActiveThemeResult}> <button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={downloadActiveThemeResult}>
{activeThemeDataset?.dataset_type === 'raster' ? 'Download analyse' : 'Download GeoJSON'} {activeResultDataset?.dataset_type === 'raster' ? 'Download analyse' : 'Download GeoJSON'}
</button> </button>
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={copyActiveThemeResult}>Kopieer gegevens</button> <button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={copyActiveThemeResult}>Kopieer gegevens</button>
</div> </div>
@@ -1928,7 +1990,8 @@ export function MapWorkspace({
? `VHA-dwarsprofielen Vlaanderen · ${activeThemePartitions.length} gemeentepartities` ? `VHA-dwarsprofielen Vlaanderen · ${activeThemePartitions.length} gemeentepartities`
: activeThemeDataset : activeThemeDataset
? getDatasetDisplayName(activeThemeDataset) ? getDatasetDisplayName(activeThemeDataset)
: 'niet beschikbaar'} : activeOnDemandThematicProduct?.display_name
?? 'niet beschikbaar'}
</span> </span>
{usesDefaultOsmBasemap ? <span><strong>Ondergrond:</strong> OpenStreetMap</span> : null} {usesDefaultOsmBasemap ? <span><strong>Ondergrond:</strong> OpenStreetMap</span> : null}
</footer> </footer>
@@ -8,16 +8,22 @@ import { thematicRasterSelectionToMapSelection } from '../lib/thematicRaster'
export interface MapThemeQuery<TThemeId extends string> { export interface MapThemeQuery<TThemeId extends string> {
themeId: TThemeId themeId: TThemeId
dataset: DatasetCreateResponse dataset?: DatasetCreateResponse
partitioned?: boolean partitioned?: boolean
thematicProductKey?: string
} }
export interface MapThemeInsight<TThemeId extends string> extends MapThemeQuery<TThemeId> { export interface MapThemeInsight<TThemeId extends string> {
themeId: TThemeId
dataset: DatasetCreateResponse
partitioned?: boolean
thematicProductKey?: string
result: VectorSelectionResponse result: VectorSelectionResponse
} }
export function useMapThemeSelectionInsights<TThemeId extends string>( export function useMapThemeSelectionInsights<TThemeId extends string>(
selectedProjectId: string | null, selectedProjectId: string | null,
onDatasetsChanged?: () => Promise<unknown>,
) { ) {
const [themeInsights, setThemeInsights] = useState<Array<MapThemeInsight<TThemeId>>>([]) const [themeInsights, setThemeInsights] = useState<Array<MapThemeInsight<TThemeId>>>([])
const [themeInsightsLoading, setThemeInsightsLoading] = useState(false) const [themeInsightsLoading, setThemeInsightsLoading] = useState(false)
@@ -55,10 +61,32 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
setThemeInsightsError(null) setThemeInsightsError(null)
try { try {
const settled = await Promise.allSettled( const settled = await Promise.allSettled(
queries.map(async ({ themeId, dataset, partitioned }) => ({ queries.map(async ({ themeId, dataset: existingDataset, partitioned, thematicProductKey }) => {
themeId, let dataset = existingDataset
dataset, if (thematicProductKey) {
result: dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv' const acquisition = await datasetsApi.acquireThematicRaster(selectedProjectId, {
bbox,
area_id: areaId,
product_key: thematicProductKey,
force_refresh: false,
})
if (acquisition.status !== 'success' || !acquisition.output_dataset_id) {
throw new Error(
acquisition.error_message
|| `De officiële rasterbron ${thematicProductKey} kon niet worden ingeladen.`,
)
}
dataset = await datasetsApi.get(selectedProjectId, acquisition.output_dataset_id)
}
if (!dataset) {
throw new Error(`Geen persistente databron beschikbaar voor thema ${themeId}.`)
}
return {
themeId,
dataset,
partitioned,
thematicProductKey,
result: dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv'
? terrainSelectionToMapSelection( ? terrainSelectionToMapSelection(
partitioned partitioned
? await datasetsApi.selectTerrainPartitions(selectedProjectId, { ? await datasetsApi.selectTerrainPartitions(selectedProjectId, {
@@ -100,13 +128,18 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
area_id: areaId, area_id: areaId,
limit: 1000, limit: 1000,
}), }),
})), }
}),
) )
const successful = settled.flatMap((item) => (item.status === 'fulfilled' ? [item.value] : [])) const successful = settled.flatMap((item) => (item.status === 'fulfilled' ? [item.value] : []))
const failures = settled.flatMap((item, index) => ( const failures = settled.flatMap((item, index) => (
item.status === 'rejected' item.status === 'rejected'
? [{ ? [{
dataset: queries[index]?.dataset.name ?? queries[index]?.themeId ?? 'Onbekende bron', dataset:
queries[index]?.dataset?.name
?? queries[index]?.thematicProductKey
?? queries[index]?.themeId
?? 'Onbekende bron',
reason: formatError(item.reason, 'Bron kon niet worden bevraagd.'), reason: formatError(item.reason, 'Bron kon niet worden bevraagd.'),
}] }]
: [] : []
@@ -116,6 +149,14 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
return [] return []
} }
setThemeInsights(successful) setThemeInsights(successful)
let refreshFailure: string | null = null
if (successful.some((item) => item.thematicProductKey) && onDatasetsChanged) {
try {
await onDatasetsChanged()
} catch (error) {
refreshFailure = formatError(error, 'De datasetlijst kon na de analyse niet worden vernieuwd.')
}
}
if (failureCount > 0) { if (failureCount > 0) {
const details = failures const details = failures
.slice(0, 4) .slice(0, 4)
@@ -125,6 +166,8 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
setThemeInsightsError( setThemeInsightsError(
`${failureCount} beschikbare databron${failureCount === 1 ? '' : 'nen'} kon niet worden bevraagd. ${details}${remainder}`, `${failureCount} beschikbare databron${failureCount === 1 ? '' : 'nen'} kon niet worden bevraagd. ${details}${remainder}`,
) )
} else if (refreshFailure) {
setThemeInsightsError(refreshFailure)
} }
return successful return successful
} catch (error) { } catch (error) {
@@ -0,0 +1,48 @@
import { useEffect, useState } from 'react'
import { datasetsApi } from '../services/api'
import { formatError } from '../lib/formatError'
import type { ThematicRasterProductRead } from '../types'
export function useThematicRasterProducts(selectedProjectId: string | null) {
const [products, setProducts] = useState<ThematicRasterProductRead[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
if (!selectedProjectId) {
setProducts([])
setLoading(false)
setError(null)
return () => {
cancelled = true
}
}
setLoading(true)
setError(null)
void datasetsApi.listThematicRasterProducts(selectedProjectId)
.then((response) => {
if (!cancelled) {
setProducts(response.items)
}
})
.catch((requestError) => {
if (!cancelled) {
setProducts([])
setError(formatError(requestError, 'De Vlaamse beleidsrasters konden niet worden geladen.'))
}
})
.finally(() => {
if (!cancelled) {
setLoading(false)
}
})
return () => {
cancelled = true
}
}, [selectedProjectId])
return { products, loading, error }
}
+2
View File
@@ -64,6 +64,8 @@ async function listProjectDatasets(projectId: string): Promise<DatasetListRespon
export const datasetsApi = { export const datasetsApi = {
list: listProjectDatasets, list: listProjectDatasets,
get: (projectId: string, datasetId: string): Promise<DatasetCreateResponse> =>
apiGet<DatasetCreateResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}`),
sourceFreshness: (projectId: string): Promise<SourceFreshnessReport> => sourceFreshness: (projectId: string): Promise<SourceFreshnessReport> =>
apiGet<SourceFreshnessReport>(`/api/v1/projects/${projectId}/datasets/source-freshness`), apiGet<SourceFreshnessReport>(`/api/v1/projects/${projectId}/datasets/source-freshness`),
sourceCatalogProbes: (projectId: string, refresh = false): Promise<SourceCatalogProbeReport> => sourceCatalogProbes: (projectId: string, refresh = false): Promise<SourceCatalogProbeReport> =>