Add bounded Flanders thematic analysis
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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> =>
|
||||||
|
|||||||
Reference in New Issue
Block a user