Add bounded GRB map acquisition
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 21:29:26 +02:00
parent 47cde21e17
commit 488da4cc83
28 changed files with 1644 additions and 134 deletions
+67 -60
View File
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'
import GeoMap from '../GeoMap'
import type { AreaRead, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types'
import { useMapThemeSelectionInsights, type MapThemeAcquisition, type MapThemeQuery } from '../../hooks/useMapThemeSelectionInsights'
import { useOfficialRasterProducts } from '../../hooks/useOfficialRasterProducts'
import { useOfficialMapProducts } from '../../hooks/useOfficialMapProducts'
import { useTemporalComparison } from '../../hooks/useTemporalComparison'
import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay'
import { TemporalTrendChart } from './TemporalTrendChart'
@@ -54,10 +54,9 @@ interface TemporalSeriesGroup {
items: DatasetCreateResponse[]
}
interface OnDemandRasterProduct extends MapThemeAcquisition {
interface OnDemandMapProduct extends MapThemeAcquisition {
theme: DataThemeId
nativeResolutionM: number
referenceLabel: string
availabilityLabel: string
attribution: string
limitationMessage: string
}
@@ -669,10 +668,10 @@ export function MapWorkspace({
clearThemeInsights,
} = useMapThemeSelectionInsights<DataThemeId>(selectedProjectId, onRefreshProjectData)
const {
products: officialRasterProducts,
loading: officialRasterProductsLoading,
error: officialRasterProductsError,
} = useOfficialRasterProducts(flandersScopeSelected ? selectedProjectId : null)
products: officialMapProducts,
loading: officialMapProductsLoading,
error: officialMapProductsError,
} = useOfficialMapProducts(flandersScopeSelected ? selectedProjectId : null)
const {
temporalComparison,
temporalComparisonLoading,
@@ -760,10 +759,10 @@ export function MapWorkspace({
)
if (selectedFloodHazard) {
result.flood_hazard = selectedFloodHazard
} else if (flandersScopeSelected && officialRasterProducts.floodHazard.length > 0) {
} else if (flandersScopeSelected && officialMapProducts.floodHazard.length > 0) {
result.flood_hazard = null
}
if (flandersScopeSelected && officialRasterProducts.dhmv.length > 0) {
if (flandersScopeSelected && officialMapProducts.dhmv.length > 0) {
result.elevation = availableMapDatasets.find(
(dataset) =>
dataset.source_name === 'digitaal_vlaanderen_dhmv'
@@ -776,8 +775,8 @@ export function MapWorkspace({
availableMapDatasets,
flandersScopeSelected,
floodHazardDatasets,
officialRasterProducts.dhmv.length,
officialRasterProducts.floodHazard.length,
officialMapProducts.dhmv.length,
officialMapProducts.floodHazard.length,
regionalScopeSelected,
selectedDhmvProductKey,
selectedFloodHazardDatasetId,
@@ -800,38 +799,47 @@ export function MapWorkspace({
[availableMapDatasets, regionalScopeSelected, selectedMapAreaId, themeDatasetMap],
)
const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0]
const onDemandRasterProductMap = useMemo(
const onDemandProductMap = useMemo(
() => {
const result = new Map<DataThemeId, OnDemandRasterProduct>()
const result = new Map<DataThemeId, OnDemandMapProduct>()
if (!flandersScopeSelected) {
return result
}
for (const product of officialRasterProducts.thematic) {
for (const product of officialMapProducts.thematic) {
result.set(product.theme, {
kind: 'thematic_raster',
productKey: product.key,
displayName: product.display_name,
theme: product.theme,
nativeResolutionM: product.native_resolution_m,
referenceLabel: String(product.observation_year),
availabilityLabel: `${product.native_resolution_m} m · ${product.observation_year} · laad bij selectie`,
attribution: product.attribution,
limitationMessage: product.limitation_message,
})
}
const dhmvProduct = officialRasterProducts.dhmv.find((product) => product.key === selectedDhmvProductKey)
for (const product of officialMapProducts.grb) {
result.set(product.key, {
kind: 'grb',
productKey: product.key,
displayName: product.display_name,
theme: product.key,
availabilityLabel: 'officiële vectorbron · laad bij selectie',
attribution: product.attribution,
limitationMessage: product.limitation_message,
})
}
const dhmvProduct = officialMapProducts.dhmv.find((product) => product.key === selectedDhmvProductKey)
if (dhmvProduct) {
result.set('elevation', {
kind: 'dhmv',
productKey: dhmvProduct.key,
displayName: dhmvProduct.display_name,
theme: 'elevation',
nativeResolutionM: dhmvProduct.native_resolution_m,
referenceLabel: dhmvProduct.acquisition_period,
availabilityLabel: `${dhmvProduct.native_resolution_m} m · ${dhmvProduct.acquisition_period} · laad bij selectie`,
attribution: dhmvProduct.attribution,
limitationMessage: dhmvProduct.limitation_message,
})
}
const floodProduct = officialRasterProducts.floodHazard.find(
const floodProduct = officialMapProducts.floodHazard.find(
(product) => product.key === selectedFloodHazardProductKey,
)
if (floodProduct) {
@@ -840,8 +848,7 @@ export function MapWorkspace({
productKey: floodProduct.key,
displayName: floodProduct.display_name,
theme: 'flood_hazard',
nativeResolutionM: floodProduct.native_resolution_m,
referenceLabel: `${floodProduct.climate_context} · T${floodProduct.return_period_years}`,
availabilityLabel: `${floodProduct.native_resolution_m} m · ${floodProduct.climate_context} · T${floodProduct.return_period_years} · laad bij selectie`,
attribution: floodProduct.attribution,
limitationMessage: floodProduct.limitation_message,
})
@@ -850,12 +857,12 @@ export function MapWorkspace({
},
[
flandersScopeSelected,
officialRasterProducts,
officialMapProducts,
selectedDhmvProductKey,
selectedFloodHazardProductKey,
],
)
const activeOnDemandRasterProduct = onDemandRasterProductMap.get(activeTheme.id) ?? null
const activeOnDemandMapProduct = onDemandProductMap.get(activeTheme.id) ?? null
const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id]
const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection)
const selectedOrthophotoProduct = orthophotoProducts.find((item) => item.key === selectedOrthophotoProductKey) ?? null
@@ -875,24 +882,24 @@ export function MapWorkspace({
const regionalRasterThemeActive = regionalScopeSelected && isPartitionedRaster(activeThemeDataset)
const regionalBathymetryThemeActive = regionalScopeSelected && isPartitionedBathymetry(activeThemeDataset)
const regionalPartitionedThemeActive = regionalRasterThemeActive || regionalBathymetryThemeActive
const onDemandRasterThemeActive = analysisMode === 'current' && Boolean(activeOnDemandRasterProduct)
const regionalOnDemandRasterThemeActive = regionalScopeSelected && onDemandRasterThemeActive
const activeThemeAvailable = Boolean(activeThemeDataset) || onDemandRasterThemeActive
const onDemandThemeActive = analysisMode === 'current' && Boolean(activeOnDemandMapProduct)
const regionalOnDemandThemeActive = regionalScopeSelected && onDemandThemeActive
const activeThemeAvailable = Boolean(activeThemeDataset) || onDemandThemeActive
useEffect(() => {
if (
!flandersScopeSelected
|| analysisMode !== 'current'
|| activeThemeAvailable
|| (onDemandRasterProductMap.size === 0 && !officialRasterProductsError)
|| (onDemandProductMap.size === 0 && !officialMapProductsError)
) {
return
}
const fallbackTheme = DATA_THEMES.find((theme) =>
theme.id === 'space_occupation'
&& Boolean(themeDatasetMap[theme.id] || onDemandRasterProductMap.get(theme.id)),
&& Boolean(themeDatasetMap[theme.id] || onDemandProductMap.get(theme.id)),
) ?? DATA_THEMES.find((theme) =>
Boolean(themeDatasetMap[theme.id] || onDemandRasterProductMap.get(theme.id)),
Boolean(themeDatasetMap[theme.id] || onDemandProductMap.get(theme.id)),
)
if (!fallbackTheme) {
return
@@ -906,8 +913,8 @@ export function MapWorkspace({
activeThemeAvailable,
analysisMode,
flandersScopeSelected,
officialRasterProductsError,
onDemandRasterProductMap,
officialMapProductsError,
onDemandProductMap,
onOpenDatasetInMap,
themeDatasetMap,
])
@@ -999,12 +1006,12 @@ export function MapWorkspace({
useEffect(() => {
const contextSourceLabel = analysisMode === 'evolution' ? activeTemporalSeriesGroup?.label ?? null
: regionalBathymetryThemeActive ? 'VHA-dwarsprofielen Vlaanderen'
: activeOnDemandRasterProduct?.displayName ?? null
: activeOnDemandMapProduct?.displayName ?? null
onSetContextSourceLabel(contextSourceLabel)
return () => onSetContextSourceLabel(null)
}, [
activeTemporalSeriesGroup?.label,
activeOnDemandRasterProduct?.displayName,
activeOnDemandMapProduct?.displayName,
analysisMode,
onSetContextSourceLabel,
regionalBathymetryThemeActive,
@@ -1112,7 +1119,7 @@ export function MapWorkspace({
}
return
}
if (flandersScopeSelected && officialRasterProducts.floodHazard.length > 0) {
if (flandersScopeSelected && officialMapProducts.floodHazard.length > 0) {
setSelectedFloodHazardDatasetId('')
return
}
@@ -1126,7 +1133,7 @@ export function MapWorkspace({
}, [
flandersScopeSelected,
floodHazardDatasets,
officialRasterProducts.floodHazard.length,
officialMapProducts.floodHazard.length,
selectedFloodHazardDatasetId,
selectedFloodHazardProductKey,
])
@@ -1350,7 +1357,7 @@ export function MapWorkspace({
? temporalGroup?.items[temporalGroup.items.length - 1] ?? null
: themeDatasetMap[theme.id]
const onDemandProduct = analysisMode === 'current'
? onDemandRasterProductMap.get(theme.id)
? onDemandProductMap.get(theme.id)
: null
if (!dataset && !onDemandProduct) {
return
@@ -1381,7 +1388,7 @@ export function MapWorkspace({
const availableThemes: Array<MapThemeQuery<DataThemeId>> = []
for (const theme of DATA_THEMES) {
const onDemandProduct = analysisMode === 'current'
? onDemandRasterProductMap.get(theme.id)
? onDemandProductMap.get(theme.id)
: null
if (onDemandProduct) {
availableThemes.push({
@@ -1410,7 +1417,7 @@ export function MapWorkspace({
const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => {
setSelectionBbox(bbox)
const tasks: Array<Promise<unknown>> = [loadAllThemeResults(bbox, areaId)]
if (!regionalPartitionedThemeActive && !onDemandRasterThemeActive) {
if (!regionalPartitionedThemeActive && !onDemandThemeActive) {
tasks.push(onRunMapSelectionExtract(bbox, areaId))
}
if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) {
@@ -1571,7 +1578,7 @@ export function MapWorkspace({
<div className="geo-theme-list">
{DATA_THEMES.map((theme) => {
const dataset = themeDatasetMap[theme.id]
const onDemandProduct = onDemandRasterProductMap.get(theme.id)
const onDemandProduct = onDemandProductMap.get(theme.id)
const partitions = themePartitionMap[theme.id]
const temporalGroups = themeTemporalSeriesMap[theme.id]
const temporalGroup = temporalGroups[0]
@@ -1603,7 +1610,7 @@ export function MapWorkspace({
: dataset
? datasetAvailabilityLabel(dataset, partitions)
: onDemandProduct
? `${onDemandProduct.nativeResolutionM} m · ${onDemandProduct.referenceLabel} · laad bij selectie`
? onDemandProduct.availabilityLabel
: 'Bron nog niet ingeladen'}
</small>
</span>
@@ -1628,7 +1635,7 @@ export function MapWorkspace({
? 'VHA-dwarsprofielen Vlaanderen'
: activeThemeDataset
? getDatasetDisplayName(activeThemeDataset)
: activeOnDemandRasterProduct?.displayName ?? 'Geen databron beschikbaar'}
: activeOnDemandMapProduct?.displayName ?? 'Geen databron beschikbaar'}
</strong>
<small>
{analysisOverlayActive
@@ -1641,19 +1648,19 @@ export function MapWorkspace({
? `${activeThemePartitions.length} gecontroleerde gemeentepartities · selectie wordt ruimtelijk samengevoegd`
: activeThemeDataset
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatDatasetObservation(activeThemeDataset)}`
: activeOnDemandRasterProduct
? `${activeOnDemandRasterProduct.attribution} · wordt alleen voor de gekozen selectie ingeladen`
: activeOnDemandMapProduct
? `${activeOnDemandMapProduct.attribution} · wordt alleen voor de gekozen selectie ingeladen`
: activeTheme.description}
</small>
</div>
{officialRasterProductsLoading && flandersScopeSelected ? (
<p className="geo-data-notice">Beschikbare Vlaamse rasterbronnen worden gecontroleerd</p>
{officialMapProductsLoading && flandersScopeSelected ? (
<p className="geo-data-notice">Beschikbare Vlaamse kaartbronnen worden gecontroleerd</p>
) : null}
{officialRasterProductsError && flandersScopeSelected ? (
<p className="error">{officialRasterProductsError}</p>
{officialMapProductsError && flandersScopeSelected ? (
<p className="error">{officialMapProductsError}</p>
) : null}
{analysisMode === 'current' && activeTheme.id === 'elevation' && officialRasterProducts.dhmv.length > 0 ? (
{analysisMode === 'current' && activeTheme.id === 'elevation' && officialMapProducts.dhmv.length > 0 ? (
<label className="geo-scope-select">
Hoogtemodel
<select
@@ -1674,7 +1681,7 @@ export function MapWorkspace({
}
}}
>
{officialRasterProducts.dhmv.map((product) => (
{officialMapProducts.dhmv.map((product) => (
<option key={product.key} value={product.key}>{product.display_name}</option>
))}
</select>
@@ -1682,7 +1689,7 @@ export function MapWorkspace({
</label>
) : null}
{analysisMode === 'current' && activeTheme.id === 'flood_hazard' && officialRasterProducts.floodHazard.length > 0 ? (
{analysisMode === 'current' && activeTheme.id === 'flood_hazard' && officialMapProducts.floodHazard.length > 0 ? (
<label className="geo-scope-select">
Overstromingsscenario
<select
@@ -1699,7 +1706,7 @@ export function MapWorkspace({
}
}}
>
{officialRasterProducts.floodHazard.map((product) => (
{officialMapProducts.floodHazard.map((product) => (
<option key={product.key} value={product.key}>{product.display_name}</option>
))}
</select>
@@ -1801,8 +1808,8 @@ export function MapWorkspace({
? 'Sleep nu een rechthoek op de kaart.'
: regionalRasterThemeActive
? 'Teken een rechthoek; de juiste gemeentelijke rasters worden automatisch gecombineerd.'
: onDemandRasterThemeActive
? 'Teken een rechthoek; officiële Vlaamse rasters worden begrensd opgehaald, bewaard en hergebruikt.'
: onDemandThemeActive
? 'Teken een rechthoek; officiële Vlaamse kaartbronnen worden begrensd opgehaald, bewaard en hergebruikt.'
: regionalBathymetryThemeActive
? 'Teken een rechthoek of analyseer Vlaanderen; alleen overlappende VHA-partities worden samengevoegd.'
: 'Sleep een rechthoek of analyseer het volledige werkgebied.'}
@@ -1820,16 +1827,16 @@ export function MapWorkspace({
</button>
<button
className="secondary-action"
disabled={!activeThemeAvailable || regionalRasterThemeActive || regionalOnDemandRasterThemeActive || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
disabled={!activeThemeAvailable || regionalRasterThemeActive || regionalOnDemandThemeActive || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
type="button"
title={
regionalRasterThemeActive || regionalOnDemandRasterThemeActive
? 'Teken een begrensde rechthoek voor een regionale rasteranalyse.'
regionalRasterThemeActive || regionalOnDemandThemeActive
? 'Teken een begrensde rechthoek voor deze regionale analyse.'
: undefined
}
onClick={() => selectedAreaBbox && void analyzeSelection(selectedAreaBbox, selectedMapArea?.id)}
>
{regionalRasterThemeActive || regionalOnDemandRasterThemeActive ? 'Selecteer een deelgebied' : 'Volledig werkgebied'}
{regionalRasterThemeActive || regionalOnDemandThemeActive ? 'Selecteer een deelgebied' : 'Volledig werkgebied'}
</button>
<button className="secondary-action" disabled={!mapSelectionBbox} type="button" onClick={clearAreaSelection}>
Wis selectie
@@ -2198,7 +2205,7 @@ export function MapWorkspace({
? `VHA-dwarsprofielen Vlaanderen · ${activeThemePartitions.length} gemeentepartities`
: activeThemeDataset
? getDatasetDisplayName(activeThemeDataset)
: activeOnDemandRasterProduct?.displayName
: activeOnDemandMapProduct?.displayName
?? 'niet beschikbaar'}
</span>
{usesDefaultOsmBasemap ? <span><strong>Ondergrond:</strong> OpenStreetMap</span> : null}
@@ -80,7 +80,8 @@ export function ProviderPanel({
<div className="provider-detail-stack">
<strong>Officiële referentiebronnen</strong>
<div>
Reeds ingeladen GRB-lagen blijven lokaal beschikbaar. De live GRB- en OSM-koppelingen staan uit en halen dus niet ongemerkt externe gegevens op.
GRB is beschikbaar voor expliciet begrensde kaartselecties en wordt daarna lokaal bewaard.
OSM blijft uitgeschakeld; geen enkele bron haalt ongemerkt gegevens op.
</div>
</div>
<ul className="system-provider-list">