@@ -1,5 +1,5 @@
import { useCallback , useEffect , useMemo , useRef , useState , type KeyboardEvent } from 'react'
import { BoxSelect , MapPinned , SlidersHorizontal , Trash2 } from 'lucide-react'
import { BoxSelect , ChevronLeft , ChevronRight , MapPinned , Play , SlidersHorizontal , Trash2 } from 'lucide-react'
import GeoMap from '../GeoMap'
import type { AreaRead , CoverageResolveResponse , CoverageStatus , DatasetCreateResponse , DetectionQaResult , MapResultExportRequest , MapViewportState , OrthophotoAcquisitionResult , OrthophotoProductRead , ProjectRead , QaComparisonResult , VectorSelectionBBox , VectorSelectionMetric , VectorSelectionResponse } from '../../types'
import { useMapThemeSelectionInsights , type MapThemeAcquisition , type MapThemeQuery } from '../../hooks/useMapThemeSelectionInsights'
@@ -335,60 +335,6 @@ const DATA_THEME_MAP_STYLES: Record<DataThemeId, { fill: string; line: string }>
marine_environment : { fill : '#3475a3' , line : '#1c557d' } ,
}
function datasetAvailabilityLabel (
dataset : DatasetCreateResponse ,
partitions : DatasetCreateResponse [ ] = [ dataset ] ,
) : string {
const partitionCount = partitions . length
const regionalSuffix = partitionCount > 1 ? ` · ${ partitionCount } gemeenten ` : ''
if ( dataset . dataset_type === 'raster' && [ 'digitaal_vlaanderen_dhmv' , 'spw_terrain' ] . includes ( dataset . source_name ? ? '' ) ) {
const resolution = Number ( dataset . source_metadata ? . [ 'analysis_resolution_m' ] )
return ` ${ Number . isFinite ( resolution ) ? ` ${ resolution . toLocaleString ( 'nl-BE' ) } m ` : 'Raster' } hoogtegrid ${ regionalSuffix } `
}
if ( dataset . dataset_type === 'raster' && dataset . source_name === 'vmm_flood_hazard' ) {
const resolution = Number ( dataset . source_metadata ? . [ 'analysis_resolution_m' ] )
return ` ${ Number . isFinite ( resolution ) ? ` ${ resolution . toLocaleString ( 'nl-BE' ) } m ` : 'Raster' } overstromingsscenario ${ regionalSuffix } `
}
if ( dataset . dataset_type === 'raster' && dataset . source_name === 'spw_bathymetry' ) {
const resolution = Number ( dataset . source_metadata ? . [ 'analysis_resolution_m' ] )
const period = String ( dataset . source_metadata ? . [ 'survey_period' ] ? ? '2019-2022' )
return ` ${ Number . isFinite ( resolution ) ? ` ${ resolution . toLocaleString ( 'nl-BE' ) } m ` : 'Raster' } waterbodemhoogte · ${ period } `
}
if ( dataset . source_name === 'vmm_vha_bathymetry_profiles' ) {
const profiles = partitions . reduce (
( total , item ) = > total + ( item . feature_count ? ? item . vector_summary ? . feature_count ? ? 0 ) ,
0 ,
)
const documents = partitions . reduce (
( total , item ) = > total + Number ( item . source_metadata ? . [ 'document_count' ] ? ? 0 ) ,
0 ,
)
return ` ${ profiles . toLocaleString ( 'nl-BE' ) } profielen · ${ documents . toLocaleString ( 'nl-BE' ) } bronbladen ${ regionalSuffix } `
}
if ( dataset . dataset_type === 'raster' && dataset . source_name === 'department_omgeving_thematic_raster' ) {
const resolution = Number ( dataset . source_metadata ? . [ 'analysis_resolution_m' ] )
const year = Number ( dataset . source_metadata ? . [ 'observation_year' ] )
const resolutionLabel = Number . isFinite ( resolution ) ? ` ${ resolution . toLocaleString ( 'nl-BE' ) } m ` : 'Raster'
return ` ${ resolutionLabel } officiële bron ${ Number . isFinite ( year ) ? ` · ${ year } ` : '' } `
}
if ( dataset . dataset_type === 'raster' && dataset . source_name === 'spw_walous_land_cover' ) {
const resolution = Number ( dataset . source_metadata ? . [ 'analysis_resolution_m' ] )
const year = Number ( dataset . source_metadata ? . [ 'observation_year' ] )
const resolutionLabel = Number . isFinite ( resolution ) ? ` ${ resolution . toLocaleString ( 'nl-BE' ) } m ` : 'Raster'
return ` ${ resolutionLabel } WALOUS-landbedekking ${ Number . isFinite ( year ) ? ` · ${ year } ` : '' } `
}
if ( dataset . source_name === 'ngi_adminvector' ) {
return ` ${ ( dataset . feature_count ? ? dataset . vector_summary ? . feature_count ? ? 0 ) . toLocaleString ( 'nl-BE' ) } officiële bestuursgebieden `
}
if ( dataset . source_name === 'rbins_msp_2026' ) {
return ` ${ ( dataset . feature_count ? ? dataset . vector_summary ? . feature_count ? ? 0 ) . toLocaleString ( 'nl-BE' ) } officiële planobjecten · 2026-2034 `
}
if ( dataset . source_name === 'rbins_marine_reporting_units' ) {
return ` ${ ( dataset . feature_count ? ? dataset . vector_summary ? . feature_count ? ? 0 ) . toLocaleString ( 'nl-BE' ) } officiële zeegebieden `
}
return ` ${ ( dataset . feature_count ? ? dataset . vector_summary ? . feature_count ? ? 0 ) . toLocaleString ( 'nl-BE' ) } objecten beschikbaar `
}
function datasetSearchText ( dataset : DatasetCreateResponse ) : string {
return [
dataset . name ,
@@ -894,6 +840,8 @@ export function MapWorkspace({
const selectedDataset = availableMapDatasets . find ( ( dataset ) = > dataset . id === selectedMapDatasetId ) ? ? null
return themeIdForDataset ( selectedDataset ) ? ? 'buildings'
} )
const [ selectedThemeIds , setSelectedThemeIds ] = useState < DataThemeId [ ] > ( [ ] )
const [ resultsPanelOpen , setResultsPanelOpen ] = useState ( false )
const activeScopeProject = projects . find ( ( project ) = > project . id === selectedProjectId ) ? ? null
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel ( activeScopeProject ) : 'Werkgebied'
const selectedMapArea = areas . find ( ( area ) = > area . id === selectedMapAreaId )
@@ -1364,7 +1312,11 @@ export function MapWorkspace({
const onDemandThemeActive = analysisMode === 'current' && Boolean ( activeOnDemandMapProduct )
const regionalOnDemandThemeActive = regionalScopeSelected && onDemandThemeActive
const activeThemeAvailable = Boolean ( activeThemeDataset ) || onDemandThemeActive
const coverageSelectionAvailable = analysisMode === 'current' && Boolean ( selectedProjectId && selectedMapArea )
const selectedThemes = useMemo (
( ) = > DATA_THEMES . filter ( ( theme ) = > selectedThemeIds . includes ( theme . id ) ) ,
[ selectedThemeIds ] ,
)
useEffect ( ( ) = > {
if (
@@ -1763,6 +1715,7 @@ export function MapWorkspace({
setFirstSelectionCorner ( null )
clearThemeInsights ( )
clearTemporalComparison ( )
setResultsPanelOpen ( false )
setBboxSelectionMode ( true )
}
@@ -1775,7 +1728,9 @@ export function MapWorkspace({
setSelectionBbox ( bbox )
setFirstSelectionCorner ( null )
setBboxSelectionMode ( false )
void analyzeSelection ( bbox , areaIdForSelection ( bbox ) )
clearThemeInsights ( )
clearTemporalComparison ( )
setResultsPanelOpen ( false )
}
const runAreaExtract = ( ) = > {
@@ -1792,6 +1747,7 @@ export function MapWorkspace({
setBboxSelectionMode ( false )
setFirstSelectionCorner ( null )
setBboxInput ( bboxToInputState ( null ) )
setResultsPanelOpen ( false )
clearThemeInsights ( )
clearTemporalComparison ( )
onClearMapSelectionExtract ( )
@@ -1929,7 +1885,13 @@ export function MapWorkspace({
if ( ! dataset && ! onDemandProduct ) {
return
}
setSelectedThemeIds ( ( current ) = > (
current . includes ( theme . id )
? current . filter ( ( themeId ) = > themeId !== theme . id )
: [ . . . current , theme . id ]
) )
setActiveThemeId ( theme . id )
clearThemeInsights ( )
clearTemporalComparison ( )
if ( dataset ) {
onOpenDatasetInMap ( dataset )
@@ -1938,6 +1900,9 @@ export function MapWorkspace({
const setExplorerMode = ( mode : 'current' | 'evolution' ) = > {
setAnalysisMode ( mode )
setSelectedThemeIds ( [ ] )
setResultsPanelOpen ( false )
clearThemeInsights ( )
clearTemporalComparison ( )
if ( mode !== 'evolution' || activeTemporalSeriesGroups . length > 0 ) {
return
@@ -2029,7 +1994,7 @@ export function MapWorkspace({
}
const availableThemes : Array < MapThemeQuery < DataThemeId > > = [ ]
const resultFeatureLimit = selectionFeatureLimit ( bbox )
for ( const theme of [ activeTheme ] ) {
for ( const theme of selectedThemes ) {
const dataset = themeDatasetMap [ theme . id ]
const partitioned = Boolean (
dataset
@@ -2078,6 +2043,10 @@ export function MapWorkspace({
}
const analyzeSelection = async ( bbox : VectorSelectionBBox , areaId? : string ) = > {
if ( analysisMode === 'current' && selectedThemes . length === 0 ) {
return
}
setResultsPanelOpen ( true )
const requestId = mapAnalysisRequestSequence . current + 1
mapAnalysisRequestSequence . current = requestId
const startedAt = Date . now ( )
@@ -2127,7 +2096,10 @@ export function MapWorkspace({
const handleMapBboxSelect = ( bbox : VectorSelectionBBox ) = > {
setFirstSelectionCorner ( null )
setBboxSelectionMode ( false )
void analyzeSelection ( bbox , areaIdForSelection ( bbox ) )
clearThemeInsights ( )
clearTemporalComparison ( )
setResultsPanelOpen ( false )
setSelectionBbox ( bbox )
}
const runQuickAoiExtract = ( ) = > {
@@ -2304,11 +2276,11 @@ export function MapWorkspace({
) : null }
< div className = "geo-explorer-layout" >
< aside className = "geo-theme-panel" aria-label = "Datathema kiezen" >
< aside className = "geo-theme-panel" aria-label = "Thema’ s en modellen kiezen" >
< div className = "geo-panel-heading" >
< div >
< h3 > Focus op de kaart < small > optioneel < / small > < / h3 >
< p > Dit bepaalt de zichtbare laag en hoofdmeting ; Inzichten controleert ook de overige beschikbare thema ’ s . < / p >
< h3 > Kies thema ’ s < / h3 >
< p > Alleen uw gekozen bronnen en modellen worden geanalyseerd . < / p >
< / div >
< / div >
< div className = "geo-loaded-scope" aria-label = "Ingeladen regiobereik" >
@@ -2328,15 +2300,13 @@ export function MapWorkspace({
{ DATA_THEMES . map ( ( theme ) = > {
const dataset = themeDatasetMap [ theme . id ]
const onDemandProduct = onDemandProductMap . get ( theme . id )
const partitions = themePartitionMap [ theme . id ]
const temporalGroups = themeTemporalSeriesMap [ theme . id ]
const temporalGroup = temporalGroups [ 0 ]
const evolutionAvailable = temporalGroups . some ( ( group ) = > group . items . length >= 2 )
const available = ! workspaceLoading && ( analysisMode === 'current'
? Boolean ( dataset || ( ! readOnly && onDemandProduct ) )
: Boolean ( dataset ) && evolutionAvailable )
const active = activeThemeId === theme . id
const temporalRange = temporalGroup ? temporalRangeLabel ( temporalGroup . items ) : null
const active = selectedThemeIds . includes ( theme . id )
return (
< button
className = { active ? 'geo-theme-option geo-theme-option-active' : 'geo-theme-option' }
@@ -2351,32 +2321,36 @@ export function MapWorkspace({
< strong > { theme . label } < / strong >
< small >
{ workspaceLoading
? 'Beschikbaarheid c ontroleren'
? 'C ontroleren… '
: analysisMode === 'evolution'
? evolutionAvailable
? ` ${ temporalGroup . items . length } meetmomenten ${ temporalRange ? ` · ${ temporalRange } ` : '' } `
? evolutionAvailable ? ` ${ temporalGroup . items . length } meetmomenten ` : 'Geen tijdreeks'
: dataset
? 'Alleen huidige toestand '
: 'Bron nog niet ingeladen'
: dataset
? ` ${ datasetAvailabilityLabel ( dataset , partitions ) } ${ onDemandProduct ? ' · zo nodig automatisch aangevuld' : '' } `
? 'Lokaal beschikbaar '
: onDemandProduct
? readOnly ? 'Niet opgenomen in deze demo' : onDemandProduct . availabilityLabel
: 'Bron nog niet ingeladen ' }
? readOnly ? 'Niet in demo' : 'Op aanvraag'
: 'Niet beschikbaar ' }
< / small >
< / span >
< i >
{ workspaceLoading
? 'Laden'
: analysisMode === 'evolution'
? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt'
: dataset ? 'Beschikbaar' : onDemandProduct ? readOnly ? 'Niet in demo' : 'Automatisch' : 'Ontbreekt' }
< / i >
< i > { active ? 'Gekozen' : available ? 'Kies' : '—' } < / i >
< / button >
)
} ) }
< / div >
< div className = "geo-theme-actions" aria-live = "polite" >
< span > { selectedThemes . length === 0 ? 'Nog niets gekozen' : ` ${ selectedThemes . length } gekozen ` } < / span >
< button
className = "primary-action"
type = "button"
disabled = { ! mapSelectionBbox || selectedThemes . length === 0 || themeResultsLoading || mapSelectionLoading }
onClick = { ( ) = > mapSelectionBbox && void analyzeSelection ( mapSelectionBbox , areaIdForSelection ( mapSelectionBbox ) ) }
>
< Play aria-hidden = "true" / >
Analyseer selectie
< / button >
< small > { mapSelectionBbox ? 'Start alleen de gekozen analyses.' : 'Teken eerst een gebied.' } < / small >
< / div >
< div className = "geo-source-summary" >
< span > { analysisOverlayActive ? 'Actieve analyselaag' : analysisMode === 'evolution' ? 'Tijdreeks' : 'Actieve bron' } < / span >
< strong >
@@ -2578,22 +2552,16 @@ export function MapWorkspace({
< p >
{ bboxSelectionMode
? 'Sleep nu een rechthoek op de kaart.'
: ! activeThemeAvailable
? 'Teken een rechthoek om de databeschikbaarheid voor dit gebied te controleren .'
: regionalRasterThemeActive
? 'Teken een rechthoek; de juiste gemeentelijke rasters worden automatisch gecombineerd.'
: onDemandThemeActive
? 'Teken een rechthoek; officiële regionale 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.' }
: mapSelectionBbox
? 'Gebied gekozen. Selecteer links één of meer thema’ s en start de analyse .'
: 'Teken een rechthoek of gebruik het volledige werkgebied. Er wordt nog niets automatisch geladen.' }
< / p >
< / div >
< / div >
< div className = "geo-map-actions" >
< button
className = { bboxSelectionMode ? 'primary-action geo-draw-active' : 'primary-action' }
disabled = { ( ! activeThemeAvailable && ! coverageSelectionAvailable ) || ( analysisMode === 'evolution' && activeTemporalSeries . length < 2 ) || mapSelectionLoading || themeResultsLoading }
disabled = { ! selectedProjectId || workspaceLoading || mapSelectionLoading || themeResultsLoading }
type = "button"
onClick = { startBboxSelection }
>
@@ -2602,17 +2570,19 @@ export function MapWorkspace({
< / button >
< button
className = "secondary-action"
disabled = { ( ! activeThemeAvailable && ! coverageSelectionAvailable ) || regionalRasterThemeActive || regionalOnDemandThemeActive || ( analysisMode === 'evolution' && activeTemporalSeries . length < 2 ) || ! selectedAreaBbox || mapSelectionLoading || themeResultsLoading }
disabled = { ! selectedAreaBbox || workspaceLoading || mapSelectionLoading || themeResultsLoading }
type = "button"
title = {
regionalRasterThemeActive || regionalOnDemandThemeActive
? 'Teken een begrensde rechthoek voor deze regionale analyse.'
: undefined
}
onClick = { ( ) = > selectedAreaBbox && void analyzeSelection ( selectedAreaBbox , selectedMapArea ? . id ) }
onClick = { ( ) = > {
if ( ! selectedAreaBbox ) return
clearThemeInsights ( )
clearTemporalComparison ( )
setResultsPanelOpen ( false )
setSelectionBbox ( selectedAreaBbox )
} }
>
< MapPinned aria-hidden = "true" / >
{ regionalRasterThemeActive || regionalOnDemandThemeActive ? 'Selecteer een deelgebied' : 'Volledig werkgebied' }
Volledig werkgebied
< / button >
< button className = "secondary-action" disabled = { ! mapSelectionBbox } type = "button" onClick = { clearAreaSelection } >
< Trash2 aria-hidden = "true" / >
@@ -2647,14 +2617,14 @@ export function MapWorkspace({
selectionMode = { bboxSelectionMode }
hasSelection = { Boolean ( mapSelectionBbox ) }
sourceLoading = { workspaceLoading || coverageLoading || officialMapProductsLoading }
sourceReady = { activeThemeAvailable && ! workspaceLoading && ! coverageLoading }
sourceReady = { selectedThemes . length > 0 && ! workspaceLoading }
processing = { liveJourneyProcessing }
validating = { liveJourneyValidating }
hasResult = { liveJourneyHasResult }
verified = { liveJourneyVerified }
error = { liveJourneyError }
selectionLabel = { mapSelectionBbox ? 'Begrensde kaartselectie' : selectedMapArea ? . name ? ? 'Nog geen gebied geselecteerd' }
sourceLabel = { activeThemeAvailable ? activeTheme . shortLabel : 'Dekking wordt gecontroleerd '}
sourceLabel = { selectedThemes . length > 0 ? ` ${ selectedThemes . length } gekozen ` : 'Kies thema’ s '}
statusMessage = { liveJourneyStatus }
resultLabel = { liveJourneyResultLabel }
/ >
@@ -2703,16 +2673,28 @@ export function MapWorkspace({
< / div >
< / div >
< button
className = { resultsPanelOpen ? 'geo-results-toggle geo-results-toggle-open' : 'geo-results-toggle' }
type = "button"
aria-controls = "geo-explorer-results"
aria-expanded = { resultsPanelOpen }
onClick = { ( ) = > setResultsPanelOpen ( ( open ) = > ! open ) }
>
{ resultsPanelOpen ? < ChevronRight aria-hidden = "true" / > : < ChevronLeft aria-hidden = "true" / > }
< span > { resultsPanelOpen ? 'Sluit inzichten' : 'Inzichten' } < / span >
< / button >
< aside
id = "geo-explorer-results"
className = " geo-results-panel"
className = { resultsPanelOpen ? 'geo-results-panel geo-results-panel-open' : ' geo-results-panel' }
aria-label = "Gebiedsanalyse"
aria-live = "polite"
aria-hidden = { ! resultsPanelOpen }
>
< div className = "geo-panel-heading" >
< div >
< h3 > Inzichten < / h3 >
< p > Gemeten resultaten en beschikbaarheid voor de volledige selectie . < / p >
< p > Resultaten van de gekozen analyses . < / p >
< / div >
< / div >
@@ -2781,12 +2763,17 @@ export function MapWorkspace({
{ ! mapSelectionBbox ? (
< div className = "geo-results-empty" >
< strong > Nog geen gebied geselecteerd < / strong >
< p > Teken een rechthoek op de kaart . De analyse start automatisch zodra je loslaat . < / p >
< p > Teken een rechthoek , kies thema ’ s en start daarna de analyse . < / p >
< / div >
) : mapSelectionLoading || themeResultsLoading || temporalComparisonLoading ? (
< div className = "geo-results-loading" role = "status" >
< span / >
< strong > De gekozen officiële bron wordt begrensd geladen en geanalyseerd … < / strong >
< strong > De gekozen bronnen worden begrensd geladen en geanalyseerd … < / strong >
< / div >
) : analysisMode === 'current' && themeInsights . length === 0 && ! mapSelectionResult ? (
< div className = "geo-results-empty" >
< strong > Nog niet geanalyseerd < / strong >
< p > Kies é é n of meer thema ’ s en gebruik “ Analyseer selectie ” . < / p >
< / div >
) : (
< >
@@ -2895,16 +2882,17 @@ export function MapWorkspace({
< div className = "geo-theme-results" >
< div className = "geo-results-title-row" >
< h4 > Gekozen thema < / h4 >
< span > { activeSelectionResult ? 'Uitgelezen' : 'Geen meting' } < / span >
< h4 > Gekozen thema ’ s < / h4 >
< span > { themeResults . length } / { selectedThemes . length } uitgelezen < / span >
< / div >
{ [ activeTheme ] . flatMap ( ( theme ) = > {
{ selectedThemes . flatMap ( ( theme ) = > {
const dataset = themeDatasetMap [ theme . id ]
const items = themeResults . filter ( ( result ) = > result . theme . id === theme . id )
const rows = items . length > 0 ? items : [ null ]
const themeSupportsCurrentSelection = selectionRelevantThemes . some ( ( candidate ) = > candidate . id === theme . id )
return rows . map ( ( item , index ) = > {
const resultDataset = item ? . dataset ? ? ( activeThemeSupportsCurrentSelection ? dataset : undefined )
const unavailableAtScale = ! activeThemeSupportsCurrentSelection
const resultDataset = item ? . dataset ? ? ( themeSupportsCurrentSelection ? dataset : undefined )
const unavailableAtScale = ! themeSupportsCurrentSelection
return (
< div className = "geo-theme-result-row" key = { ` ${ theme . id } : ${ resultDataset ? . id ? ? index } ` } >
< span className = { ` geo-theme-symbol geo-theme-symbol- ${ theme . id } ` } aria-hidden = "true" / >