Files
geointel/frontend/src/App.tsx
T
JensandClaude Opus 5 9614307669 send the map selection to change detection and show the modified class
The comparison hook runs before the selection state is declared in App, so the
selection is read through a getter at the moment the run starts rather than
captured at render. The panel gains the modified count, which until now was
folded into removed plus added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:56:37 +02:00

1485 lines
60 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from 'react'
import { CircleAlert, LogOut, MonitorUp, ShieldCheck, UserRound } from 'lucide-react'
import '@fontsource/manrope/latin-500.css'
import '@fontsource/manrope/latin-600.css'
import '@fontsource/manrope/latin-700.css'
import '@fontsource/public-sans/latin-400.css'
import '@fontsource/public-sans/latin-500.css'
import '@fontsource/public-sans/latin-600.css'
import './styles/app.css'
import './styles/premium.css'
import './styles/atlas-workbench.css'
import './styles/atlas-premium-v2.css'
import './styles/professionalization.css'
// Laatst geladen en leidend: het geconsolideerde designsysteem.
// Zie de kop van geointel-system.css voor de reden.
import './styles/geointel-system.css'
import { LandingPage } from './components/auth/LandingPage'
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
import { GeoAssistantPanel } from './components/assistant/GeoAssistantPanel'
import { DatasetPanel } from './components/datasets/DatasetPanel'
import { SourceCatalogPanel } from './components/datasets/SourceCatalogPanel'
import { DetectionLab } from './components/detection/DetectionLab'
import { ExportCenter } from './components/exports/ExportCenter'
import { ExportPreview } from './components/exports/ExportPreview'
import { WorkbenchInspector } from './components/inspector/WorkbenchInspector'
import { MapWorkspace } from './components/map/MapWorkspace'
import { OverviewWorkspace, type WorkspaceKey } from './components/overview/OverviewWorkspace'
import { AreaPanel } from './components/project/AreaPanel'
import { ProjectPanel } from './components/project/ProjectPanel'
import { QualityResultsPanel } from './components/quality/QualityResultsPanel'
import type { DatasetCreateResponse } from './types'
import { ProviderPanel } from './components/providers/ProviderPanel'
import { SegmentationLab } from './components/segmentation/SegmentationLab'
import {
WorkbenchNavigation,
type WorkspaceNavigationGroup,
type WorkspaceNavigationItem,
} from './components/shell/WorkbenchNavigation'
import { WorkspaceSignal } from './components/shell/WorkspaceSignal'
import { useSecondaryDisplay } from './components/shell/SecondaryDisplay'
import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow'
import { useDemoWorkflow } from './hooks/useDemoWorkflow'
import { useCoverageResolver } from './hooks/useCoverageResolver'
import { useDetectionWorkflow } from './hooks/useDetectionWorkflow'
import { useDatasetWorkflow } from './hooks/useDatasetWorkflow'
import { useExportWorkflow } from './hooks/useExportWorkflow'
import { useMapSelectionDataset } from './hooks/useMapSelectionDataset'
import { useMapSelectionQa } from './hooks/useMapSelectionQa'
import { useMapWorkspaceState } from './hooks/useMapWorkspaceState'
import { useOperatorSession } from './hooks/useOperatorSession'
import { useMapSelectionExtract } from './hooks/useMapSelectionExtract'
import { useMapOrthophotoAnalysis } from './hooks/useMapOrthophotoAnalysis'
import { isDetectionImageryDataset, isMapRasterDataset } from './lib/datasetCapabilities'
import { useProviderCapabilities } from './hooks/useProviderCapabilities'
import { useProjectWorkspace } from './hooks/useProjectWorkspace'
import { useQualityWorkflow } from './hooks/useQualityWorkflow'
import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow'
import { useSourceFreshness } from './hooks/useSourceFreshness'
import { useWorkbenchBootstrap } from './hooks/useWorkbenchBootstrap'
import { useViewportVectorLayer } from './hooks/useViewportVectorLayer'
import { getDatasetDisplayName } from './lib/datasetDisplay'
import {
FLANDERS_WORKSPACE_LABEL,
FLANDERS_WORKSPACE_PROJECT_NAME,
NATIONAL_WORKSPACE_LABEL,
NATIONAL_WORKSPACE_PROJECT_NAME,
REGIONAL_WORKSPACE_LABEL,
REGIONAL_WORKSPACE_PROJECT_NAME,
} from './config/primaryFocus'
function isVectorDatasetType(datasetType: string): boolean {
return datasetType === 'vector' || datasetType === 'geojson'
}
const workspaceNavItems: WorkspaceNavigationItem[] = [
{ key: 'overview', label: 'Status', description: 'Beschikbaarheid en aandachtspunten' },
{ key: 'data', label: 'Bronnen', description: 'Gebieden en ingeladen gegevens' },
{ key: 'map', label: 'Kaart', description: 'Selecteren, uitlezen en vergelijken' },
{ key: 'assistant', label: 'AI-vragen', description: 'Vraag de lokale assistent over het actieve gebied' },
{ key: 'analysis', label: 'Kwaliteit', description: 'Resultaten controleren' },
{ key: 'ai', label: 'Beeldanalyse', description: 'Gebouwen herkennen op luchtbeelden' },
{ key: 'exports', label: 'Downloads', description: 'Resultaten bewaren en delen' },
{ key: 'system', label: 'Systeem', description: 'Bronkoppelingen en operationele status' },
]
const workspaceNavGroups: WorkspaceNavigationGroup[] = [
{ label: 'Verkennen', keys: ['map', 'data'] },
{ label: 'Vragen', keys: ['assistant'] },
{ label: 'Analyseren', keys: ['analysis', 'ai'] },
{ label: 'Afronden', keys: ['exports'] },
{ label: 'Beheer', keys: ['overview', 'system'] },
]
const guestWorkspaceKeys = new Set<WorkspaceKey>([
'overview',
'data',
'map',
'assistant',
'analysis',
'ai',
'exports',
])
const guestWorkspaceGroups: WorkspaceNavigationGroup[] = [
{ label: 'Verkennen', keys: ['map', 'data'] },
{ label: 'Vragen', keys: ['assistant'] },
{ label: 'Analyseren', keys: ['analysis', 'ai'] },
{ label: 'Afronden', keys: ['exports'] },
{ label: 'Demo', keys: ['overview'] },
]
interface WorkbenchAppProps {
username: string | null
accessMode: 'open' | 'operator' | 'guest'
loggingOut: boolean
onLogout: () => void
}
function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchAppProps): JSX.Element {
const isGuest = accessMode === 'guest'
const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('map')
const [inspectorOpen, setInspectorOpen] = useState(false)
const [guestDemoReady, setGuestDemoReady] = useState(!isGuest)
const secondaryDisplay = useSecondaryDisplay()
const guestDemoStartedRef = useRef(false)
const visibleWorkspaceItems = useMemo(
() => isGuest ? workspaceNavItems.filter((item) => guestWorkspaceKeys.has(item.key)) : workspaceNavItems,
[isGuest],
)
const visibleWorkspaceGroups = isGuest ? guestWorkspaceGroups : workspaceNavGroups
const [mapContentMode, setMapContentMode] = useState<'dataset' | 'analysis'>('dataset')
const [mapContextSourceLabel, setMapContextSourceLabel] = useState<string | null>(null)
const [mapContextLayerLabel, setMapContextLayerLabel] = useState<string | null>(null)
const workbenchMainRef = useRef<HTMLElement | null>(null)
const previousWorkspaceRef = useRef<WorkspaceKey>(activeWorkspace)
useEffect(() => {
if (isGuest && !guestWorkspaceKeys.has(activeWorkspace)) {
setActiveWorkspace('map')
}
}, [activeWorkspace, isGuest])
useEffect(() => {
workbenchMainRef.current?.scrollTo({ top: 0, left: 0 })
if (previousWorkspaceRef.current !== activeWorkspace) {
workbenchMainRef.current?.focus({ preventScroll: true })
}
previousWorkspaceRef.current = activeWorkspace
setInspectorOpen(false)
}, [activeWorkspace])
const {
projects,
selectedProject,
selectedProjectId,
areas,
datasets,
loadingProjects,
loadingAreas,
loadingDatasets,
archivingProjectId,
errorMessage,
projectForm,
areaForm,
loadProjects,
loadProjectData,
createProject,
createArea,
activateMunicipality,
archiveProject,
resetProjectData,
setSelectedProjectId,
setErrorMessage,
setProjectForm,
setAreaForm,
} = useProjectWorkspace()
const sourceFreshness = useSourceFreshness(selectedProjectId)
const selectProject = (projectId: string) => {
setMapContentMode('dataset')
setSelectedProjectId(projectId)
}
const {
selectedDatasetId,
selectedDataset,
selectedDatasetSummary,
selectedRasterMetadata,
selectedRasterStats,
datasetContent,
jobs,
rasterPreview,
latestRasterTileManifestPath,
latestRasterTileManifest,
selectedIntersectTargetId,
selectedClipAreaId,
rasterTileSize,
rasterTileOverlap,
rasterTileOutputName,
rasterReprojectCrs,
rasterReprojectResampling,
ndviNirBand,
ndviRedBand,
ndwiGreenBand,
ndwiNirBand,
ndbiSwirBand,
ndbiNirBand,
loadingDatasetDetails,
datasetDetailError,
datasetForm,
isRasterTileInputValid,
loadDatasetJobs,
loadDatasetDetails,
uploadDataset,
runVectorClip,
runVectorBuffer,
runVectorIntersect,
runRasterInspect,
runRasterPreview,
runRasterStats,
runRasterReproject,
runRasterClip,
runRasterTile,
runRasterNdvi,
runRasterNdwi,
runRasterNdbi,
pickDerivedDataset,
refreshMetadata,
resetDatasetForProject,
setSelectedDatasetId,
setSelectedIntersectTargetId,
setSelectedClipAreaId,
setRasterTileSize,
setRasterTileOverlap,
setRasterTileOutputName,
setRasterReprojectCrs,
setRasterReprojectResampling,
setNdviNirBand,
setNdviRedBand,
setNdwiGreenBand,
setNdwiNirBand,
setNdbiSwirBand,
setNdbiNirBand,
setDatasetForm,
} = useDatasetWorkflow({
selectedProjectId,
areas,
datasets,
loadProjectData,
setErrorMessage,
isVectorDatasetType,
})
const rasterUnavailableMessage = useMemo(() => {
if (!selectedDataset || selectedDataset.dataset_type !== 'raster' || !selectedDataset.metadata_json) {
return null
}
const metadata = selectedDataset.metadata_json as Record<string, unknown>
const processingCode = metadata['processing_code']
if (processingCode === 'RASTER_PROCESSING_UNAVAILABLE') {
return String(metadata['processing_error'] ?? 'Raster processing unavailable.')
}
return null
}, [selectedDataset])
const availableVectorTargets = useMemo(
() => datasets.filter((item) => item.id !== selectedDatasetId && isVectorDatasetType(item.dataset_type)),
[datasets, selectedDatasetId],
)
const availableVectorDatasets = useMemo(() => datasets.filter((item) => isVectorDatasetType(item.dataset_type)), [datasets])
const availableMapDatasets = useMemo(
() => {
const vectors = datasets.filter((dataset) => isVectorDatasetType(dataset.dataset_type) && dataset.status === 'ready')
const rasters = datasets.filter(isMapRasterDataset)
return [...vectors, ...rasters]
},
[datasets],
)
const referenceDatasets = useMemo(
() => availableVectorDatasets.filter((item) => item.dataset_role === 'reference'),
[availableVectorDatasets],
)
const candidateDatasets = useMemo(
() => availableVectorDatasets.filter((item) => item.dataset_role !== 'reference'),
[availableVectorDatasets],
)
const rasterDatasets = useMemo(() => datasets.filter((item) => item.dataset_type === 'raster'), [datasets])
const detectionRasterDatasets = useMemo(
() => rasterDatasets.filter(isDetectionImageryDataset),
[rasterDatasets],
)
const {
providers,
loadingCapabilities,
capabilitiesError,
loadCapabilities,
} = useProviderCapabilities()
const {
changeSourceDatasetId,
changeTargetDatasetId,
changeIouThreshold,
changeIncludeUnchanged,
runningChangeDetection,
changeDetectionResult,
changeDetectionError,
runChangeDetection,
setChangeSourceDatasetId,
setChangeTargetDatasetId,
setChangeIouThreshold,
setChangeIncludeUnchanged,
} = useChangeDetectionWorkflow({
selectedProjectId,
availableVectorDatasets,
loadDatasetJobs,
// Bind the comparison to whatever the operator has selected on the map.
getSelection: () => ({ bbox: mapSelectionBbox ?? null, areaId: selectedMapAreaId ?? null }),
})
const {
qaCandidateDatasetId,
qaReferenceDatasetId,
qaAreaId,
qaIouThreshold,
qaRunning,
qaResult,
qaError,
qualityChecks,
qualityChecksError,
qualityEvidenceGeoJson,
qualityEvidenceLoading,
qualityEvidenceError,
loadQualityChecks,
loadQualityEvidenceGeoJson,
clearQualityEvidenceGeoJson,
runQaComparison,
setQaCandidateDatasetId,
setQaReferenceDatasetId,
setQaAreaId,
setQaIouThreshold,
} = useQualityWorkflow({
selectedProjectId,
loadProjectData,
})
const {
detectionModels,
modelAssets,
loadingDetectionModels,
detectionModelError,
modelAssetError,
selectedDetectionDatasetId,
selectedDetectionModelId,
selectedModelAssetId,
detectionTileManifestPath,
detectionConfidenceThreshold,
runningDetection,
detectionRunResult,
detectionRunError,
detectionRuns,
selectedDetectionRunId,
detectionItems,
detectionGeoJson,
detectionClassFilter,
detectionMinConfidenceFilter,
loadingDetectionResults,
detectionReferenceDatasetId,
detectionQaResult,
detectionQaError,
runningDetectionQa,
yoloPreflight,
loadingYoloPreflight,
yoloPreflightError,
calibrationThresholdText,
runningDetectionCalibration,
detectionCalibrationRows,
detectionCalibrationError,
detectionWorkflowStage,
loadDetectionModels,
loadYoloPreflight,
loadDetectionRuns,
loadDetectionResults,
runDetection,
uploadDetectionRaster,
prepareAndRunDetection,
compareDetectionRunWithReference,
runDetectionQa,
runDetectionCalibration,
applyDetectionOperatorProfile,
resetDetectionForProject,
setSelectedDetectionDatasetId,
setSelectedDetectionModelId,
setSelectedModelAssetId,
setDetectionTileManifestPath,
setDetectionConfidenceThreshold,
setSelectedDetectionRunId,
setDetectionClassFilter,
setDetectionMinConfidenceFilter,
setDetectionReferenceDatasetId,
setCalibrationThresholdText,
} = useDetectionWorkflow({
selectedProjectId,
rasterDatasets: detectionRasterDatasets,
qaIouThreshold,
loadProjectData,
loadQualityChecks,
})
const useRasterTileManifestForDetection = () => {
const manifestPath = latestRasterTileManifestPath.trim()
if (!manifestPath || !selectedDataset || !isDetectionImageryDataset(selectedDataset)) {
return
}
setDetectionTileManifestPath(manifestPath)
setSelectedDetectionDatasetId(selectedDataset.id)
setSelectedDetectionModelId('yolo-configured')
setDetectionConfidenceThreshold(0.25)
loadYoloPreflight(manifestPath).catch(() => null)
setActiveWorkspace('ai')
}
const {
segmentationModels,
loadingSegmentationModels,
segmentationModelError,
selectedSegmentationDatasetId,
selectedSegmentationModelId,
selectedSegmentationModel,
segmentationTileManifestPath,
segmentationConfidenceThreshold,
runningSegmentation,
segmentationRunResult,
segmentationRunError,
segmentationRuns,
selectedSegmentationRunId,
segmentationItems,
segmentationGeoJson,
segmentationClassFilter,
segmentationMinConfidenceFilter,
loadingSegmentationResults,
segmentationReferenceDatasetId,
segmentationQaResult,
segmentationQaError,
runningSegmentationQa,
loadSegmentationModels,
loadSegmentationRuns,
loadSegmentationResults,
runSegmentation,
runSegmentationQa,
resetSegmentationForProject,
setSelectedSegmentationDatasetId,
setSelectedSegmentationModelId,
setSegmentationTileManifestPath,
setSegmentationConfidenceThreshold,
setSelectedSegmentationRunId,
setSegmentationClassFilter,
setSegmentationMinConfidenceFilter,
setSegmentationReferenceDatasetId,
} = useSegmentationWorkflow({
selectedProjectId,
rasterDatasets,
qaIouThreshold,
loadProjectData,
loadQualityChecks,
})
const viewportVectorLayer = useViewportVectorLayer({
selectedProjectId,
selectedDataset,
featureCount: selectedDatasetSummary?.feature_count ?? selectedDataset?.feature_count ?? null,
isVectorDatasetType,
})
const analysisMapLayerAvailable = Boolean(changeDetectionResult?.geojson || segmentationGeoJson || detectionGeoJson)
const analysisMapLayerActive = mapContentMode === 'analysis' && analysisMapLayerAvailable
const viewportVectorLayerActive = viewportVectorLayer.enabled && !analysisMapLayerActive
const datasetMapContent = viewportVectorLayer.enabled ? viewportVectorLayer.data : datasetContent
const useRasterTileManifestForSegmentation = () => {
const manifestPath = latestRasterTileManifestPath.trim()
if (!manifestPath || !selectedDataset || selectedDataset.dataset_type !== 'raster') {
return
}
setSegmentationTileManifestPath(manifestPath)
setSelectedSegmentationDatasetId(selectedDataset.id)
setActiveWorkspace('ai')
}
const {
exports,
latestExport,
exportError,
loadingExports,
exporting,
selectionExporting,
selectionExportError,
latestSelectionExport,
exportPreview,
loadExports,
exportSelectedDatasetGeoJson,
exportSelectedDetectionRunGeoJson,
exportSelectedSegmentationRunGeoJson,
exportMapSelectionGeoJson,
persistMapResult,
exportProjectMetadata,
exportProjectReport,
previewExportContent,
downloadExportArtifact,
resetExportsForProject,
} = useExportWorkflow({
selectedProjectId,
selectedDataset,
selectedDetectionRunId,
selectedSegmentationRunId,
isVectorDatasetType,
})
const {
mapLayerVisible,
mapLayerOpacity,
selectedMapAreaId,
areaLayerVisible,
areaLayerOpacity,
selectedMapFeature,
areaFeatureCollection,
mapFeatureCollection,
mapLayerLabel,
mapFeatureCount,
areaFeatureCount,
setMapLayerVisible,
setMapLayerOpacity,
setSelectedMapAreaId,
setAreaLayerVisible,
setAreaLayerOpacity,
setSelectedMapFeature,
} = useMapWorkspaceState({
areas,
changeDetectionGeoJson: analysisMapLayerActive ? changeDetectionResult?.geojson ?? null : null,
segmentationGeoJson: analysisMapLayerActive ? segmentationGeoJson : null,
detectionGeoJson: analysisMapLayerActive ? detectionGeoJson : null,
datasetContent: datasetMapContent,
selectedDataset,
datasetLayerActive: Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type)),
})
const {
mapSelectionBbox,
mapSelectionResult,
mapSelectionLoading,
mapSelectionError,
runMapSelectionExtract,
resetMapSelectionExtract,
setMapSelectionBbox,
} = useMapSelectionExtract({
selectedProjectId,
selectedDataset,
isVectorDatasetType,
})
const {
coverage: mapCoverage,
loadingCoverage: mapCoverageLoading,
coverageError: mapCoverageError,
coverageDurationMs: mapCoverageDurationMs,
coverageBudgetExceeded: mapCoverageBudgetExceeded,
} = useCoverageResolver({
projectId: selectedProjectId,
bbox: mapSelectionBbox,
})
const {
selectionDatasetSaving,
selectionDatasetError,
latestSelectionDataset,
deriveMapSelectionDataset,
} = useMapSelectionDataset({
selectedProjectId,
selectedDataset,
isVectorDatasetType,
loadProjectData,
loadDatasetDetails,
setMapLayerVisible,
})
const {
selectedMapQaReferenceDatasetId,
mapSelectionQaRunning,
mapSelectionQaError,
mapSelectionQaResult,
latestMapSelectionQualityCheckId,
runMapSelectionQa,
setSelectedMapQaReferenceDatasetId,
} = useMapSelectionQa({
selectedProjectId,
latestSelectionDataset,
loadQualityChecks,
loadProjectData,
})
const mapOrthophotoAnalysis = useMapOrthophotoAnalysis({
selectedProjectId,
selectedAreaId: areas.find((area) => /vervoerregio|operationele grens/i.test(area.name))?.id ?? selectedMapAreaId,
selectionBbox: mapSelectionBbox,
datasets,
loadProjectData,
prepareAndRunDetection: (datasetId) => prepareAndRunDetection(datasetId, 'yolo-configured'),
compareDetectionRunWithReference: (analysisRunId, referenceDatasetId, iouThreshold) =>
compareDetectionRunWithReference(analysisRunId, referenceDatasetId, false, iouThreshold),
loadQualityEvidence: (qualityCheckId) => loadQualityEvidenceGeoJson(qualityCheckId),
onAnalysisReady: () => {
setMapContentMode('analysis')
setMapLayerVisible(true)
setActiveWorkspace('map')
},
})
const openMapSelectionQualityEvidence = () => {
setActiveWorkspace('analysis')
}
const openQualityEvidenceOnMap = async (qualityCheckId: string) => {
const result = await loadQualityEvidenceGeoJson(qualityCheckId)
if (result) {
setMapLayerVisible(true)
setActiveWorkspace('map')
}
}
const {
loadingDemoWorkflow,
demoWorkflowMessage,
loadDemoWorkflow,
} = useDemoWorkflow({
restrictedMode: false,
loadProjects,
loadProjectData,
loadDatasetDetails,
loadDetectionRuns,
loadSegmentationRuns,
loadQualityChecks,
loadExports,
setSelectedProjectId: selectProject,
setSelectedDatasetId,
setSelectedMapAreaId,
setQaCandidateDatasetId,
setQaReferenceDatasetId,
setQaAreaId,
setSelectedDetectionDatasetId,
setDetectionReferenceDatasetId,
setSelectedSegmentationDatasetId,
setSegmentationReferenceDatasetId,
setErrorMessage,
})
useEffect(() => {
if (!isGuest || guestDemoStartedRef.current) return
guestDemoStartedRef.current = true
setGuestDemoReady(false)
void loadDemoWorkflow().finally(() => setGuestDemoReady(true))
}, [isGuest, loadDemoWorkflow])
useWorkbenchBootstrap({
restrictedMode: false,
selectedProjectId,
selectedDetectionRunId,
detectionClassFilter,
detectionMinConfidenceFilter,
selectedSegmentationRunId,
segmentationClassFilter,
segmentationMinConfidenceFilter,
loadProjects,
loadCapabilities,
loadDetectionModels,
loadSegmentationModels,
loadProjectData,
loadDetectionRuns,
loadSegmentationRuns,
loadQualityChecks,
loadExports,
loadDetectionResults,
loadSegmentationResults,
resetProjectData,
resetDatasetForProject,
resetDetectionForProject,
resetSegmentationForProject,
resetExportsForProject,
})
const selectedArea = areas.find((area) => area.id === selectedMapAreaId) ?? null
const activeWorkspaceItem = visibleWorkspaceItems.find((item) => item.key === activeWorkspace) ?? visibleWorkspaceItems[0]
const mapLayerSourceLabel = useMemo(() => {
if (analysisMapLayerActive && changeDetectionResult?.geojson) {
return 'Veranderingsanalyse'
}
if (analysisMapLayerActive && segmentationGeoJson) {
return 'Segmentatierun'
}
if (analysisMapLayerActive && detectionGeoJson) {
return 'Detectierun'
}
if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) {
return selectedDataset.dataset_type === 'raster' ? 'Rasterdatabron' : 'Vectordatabron'
}
return 'Geen actieve gegevens- of analyselaag'
}, [analysisMapLayerActive, changeDetectionResult?.geojson, datasetMapContent, detectionGeoJson, segmentationGeoJson, selectedDataset, viewportVectorLayer.enabled])
const mapLayerProvenance = useMemo(() => {
if (analysisMapLayerActive && changeDetectionResult?.geojson) {
return 'Vergelijking van twee bewaarde kaartlagen'
}
if (analysisMapLayerActive && segmentationGeoJson) {
return selectedSegmentationRunId ? 'Bewaarde segmentatieronde' : 'Segmentatieresultaten geladen'
}
if (analysisMapLayerActive && detectionGeoJson) {
return selectedDetectionRunId ? 'Bewaarde detectieronde' : 'Detectieresultaten geladen'
}
if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) {
return `Bewaarde ${selectedDataset.dataset_role === 'reference' ? 'referentielaag' : 'databron'}`
}
return 'Open een databron, detectieronde, segmentatieronde of veranderingsresultaat om het hier te tekenen.'
}, [
analysisMapLayerActive,
changeDetectionResult?.geojson,
changeSourceDatasetId,
changeTargetDatasetId,
datasetMapContent,
detectionGeoJson,
segmentationGeoJson,
selectedDataset,
selectedDetectionRunId,
selectedSegmentationRunId,
viewportVectorLayer.enabled,
])
const openDatasetInMap = (dataset: DatasetCreateResponse) => {
if (selectedProjectId) {
loadDatasetDetails(selectedProjectId, dataset)
}
setMapContentMode('dataset')
setMapLayerVisible(true)
setActiveWorkspace('map')
}
const runGuidedDetection = async () => {
const completed = await prepareAndRunDetection()
if (completed) {
setMapContentMode('analysis')
setMapLayerVisible(true)
setActiveWorkspace('map')
}
}
const openDetectionResultsOnMap = () => {
if (!detectionGeoJson) {
return
}
setMapContentMode('analysis')
setMapLayerVisible(true)
setActiveWorkspace('map')
}
const openDatasetExport = (dataset: DatasetCreateResponse) => {
if (selectedProjectId) {
loadDatasetDetails(selectedProjectId, dataset)
}
setActiveWorkspace('exports')
}
const openWorkflowGuidanceStep = (target: WorkspaceKey) => {
if (isGuest && !guestWorkspaceKeys.has(target)) {
setActiveWorkspace('map')
return
}
if (target === 'map' && availableMapDatasets.length > 0 && !mapFeatureCollection) {
openDatasetInMap(availableMapDatasets[0])
return
}
if (target === 'exports' && availableMapDatasets.length > 0 && !selectedDataset) {
openDatasetExport(availableMapDatasets[0])
return
}
setActiveWorkspace(target)
}
const hasAnalysisOutput =
qualityChecks.length > 0
|| Boolean(changeDetectionResult)
|| detectionItems.length > 0
|| segmentationItems.length > 0
const projectContextLabel = selectedProject?.name === NATIONAL_WORKSPACE_PROJECT_NAME
? NATIONAL_WORKSPACE_LABEL
: selectedProject?.name === REGIONAL_WORKSPACE_PROJECT_NAME
? REGIONAL_WORKSPACE_LABEL
: selectedProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME
? FLANDERS_WORKSPACE_LABEL
: selectedProject?.name ?? 'Geen werkruimte'
const areaContextLabel = selectedArea?.name ?? (areas.length > 0 ? 'Kies een gebied' : 'Geen gebied')
const bathymetryProfileContextActive = Boolean(
activeWorkspace === 'map'
&& selectedDataset?.source_name === 'vmm_vha_bathymetry_profiles',
)
const bathymetryRasterContextActive = Boolean(
activeWorkspace === 'map'
&& selectedDataset?.source_name === 'spw_bathymetry',
)
const bathymetryContextActive = bathymetryProfileContextActive || bathymetryRasterContextActive
const regionalBathymetryContextActive = Boolean(
bathymetryProfileContextActive
&& selectedDataset?.source_metadata?.regional_partitions_complete === true
&& selectedArea
&& !/^Gemeente\s/i.test(selectedArea.name),
)
const regionalBathymetryManifest = String(
selectedDataset?.source_metadata?.partition_manifest_sha256 ?? '',
)
const regionalBathymetryPartitions = regionalBathymetryContextActive
? availableMapDatasets.filter(
(dataset) =>
dataset.source_name === 'vmm_vha_bathymetry_profiles'
&& dataset.source_metadata?.regional_partitions_complete === true
&& String(dataset.source_metadata?.partition_manifest_sha256 ?? '') === regionalBathymetryManifest,
)
: []
const regionalBathymetryProfileCount = regionalBathymetryPartitions.reduce(
(total, dataset) => total + (dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0),
0,
)
const bathymetryProfileCount = regionalBathymetryContextActive
? regionalBathymetryProfileCount
: selectedDataset?.feature_count ?? selectedDataset?.vector_summary?.feature_count ?? 0
const workspaceDataLoading = loadingProjects || loadingAreas || loadingDatasets
const workspaceStatusMessage = errorMessage
? `Werkruimtefout: ${errorMessage}`
: workspaceDataLoading
? 'Werkruimte en databronnen worden geladen.'
: selectedProject
? `${projectContextLabel} is geladen met ${datasets.length} databronnen en ${areas.length} gebieden.`
: 'Geen werkruimte geselecteerd.'
const datasetContextLabel = activeWorkspace === 'map' && mapContextSourceLabel
? mapContextSourceLabel
: analysisMapLayerActive && mapFeatureCollection
? `${mapLayerLabel} · controle vereist`
: selectedDataset
? getDatasetDisplayName(selectedDataset)
: workspaceDataLoading
? 'Bronnen worden geladen'
: datasets.length > 0
? 'Kies een bron'
: 'Geen bron'
const layerContextLabel = activeWorkspace === 'map' && mapContextLayerLabel
? mapContextLayerLabel
: bathymetryContextActive
? bathymetryRasterContextActive
? `${Number(selectedDataset?.source_metadata?.analysis_resolution_m ?? 0.5).toLocaleString('nl-BE')} m raster · mDNG · ${String(selectedDataset?.source_metadata?.survey_period ?? '2019-2022')}`
: regionalBathymetryContextActive
? `${bathymetryProfileCount.toLocaleString('nl-BE')} profielen · ${regionalBathymetryPartitions.length} gemeenten`
: `${bathymetryProfileCount.toLocaleString('nl-BE')} profielen`
: mapFeatureCollection
? `${mapFeatureCount.toLocaleString('nl-BE')} objecten`
: workspaceDataLoading
? 'Kaart wordt voorbereid'
: viewportVectorLayerActive
? 'Kaartlaag gekozen'
: activeWorkspace === 'map' && selectedDataset?.dataset_type === 'raster'
? 'Rasterlaag actief'
: 'Geen actieve laag'
return (
<div className={`${isGuest ? 'app-shell workbench-shell workbench-shell-guest' : 'app-shell workbench-shell'}${secondaryDisplay.isOpen ? ' workbench-shell-dual-display' : ''}`}>
<a
className="skip-link"
href="#workspace-main"
onClick={() => {
window.requestAnimationFrame(() => workbenchMainRef.current?.focus({ preventScroll: true }))
}}
>
Ga naar de werkruimte
</a>
<p className="sr-only" role="status" aria-live="polite">{workspaceStatusMessage}</p>
{errorMessage && activeWorkspace !== 'map' ? (
<div className="workspace-notice" role="alert">
<CircleAlert aria-hidden="true" />
<div>
<strong>Werkruimte vraagt aandacht</strong>
<span>{errorMessage}</span>
</div>
</div>
) : null}
<div className="workbench-layout">
<WorkbenchNavigation
activeWorkspace={activeWorkspace}
groups={visibleWorkspaceGroups}
items={visibleWorkspaceItems}
onSelect={setActiveWorkspace}
/>
<div className="workbench-stage">
<header className="workbench-topbar">
<div className="mobile-brand">
<img className="brand-mark geointel-mark" src="/geointel-icon.svg" alt="" aria-hidden="true" />
<strong>GeoIntel</strong>
</div>
<div className="context-bar" aria-label="Actieve werkcontext">
<div>
<span>Werkruimte</span>
<strong title={projectContextLabel}>{projectContextLabel}</strong>
</div>
<div>
<span>Gebied</span>
<strong title={areaContextLabel}>{areaContextLabel}</strong>
</div>
<div>
<span>Bron</span>
<strong title={datasetContextLabel}>{datasetContextLabel}</strong>
</div>
<div>
<span>Kaartlaag</span>
<strong title={layerContextLabel}>{layerContextLabel}</strong>
</div>
</div>
<div className="context-health" aria-label="Systeemstatus">
<span aria-hidden="true" />
<strong>{workspaceDataLoading ? 'Laden' : errorMessage ? 'Aandacht' : 'Gereed'}</strong>
</div>
<button
type="button"
className={secondaryDisplay.isOpen ? 'secondary-display-launch secondary-display-launch-active' : 'secondary-display-launch'}
aria-pressed={secondaryDisplay.isOpen}
aria-describedby={secondaryDisplay.error ? 'secondary-display-error' : undefined}
onClick={() => {
setActiveWorkspace('map')
if (secondaryDisplay.isOpen) secondaryDisplay.close()
else secondaryDisplay.open()
}}
>
<MonitorUp aria-hidden="true" />
<span>{secondaryDisplay.isOpen ? 'Sluit tweede scherm' : 'Open tweede scherm'}</span>
</button>
{username || isGuest ? (
<div
className={isGuest ? 'context-account context-account-guest' : 'context-account'}
aria-label={isGuest ? 'Tijdelijke gastensessie' : 'Aangemelde gebruiker'}
>
{isGuest ? <ShieldCheck aria-hidden="true" /> : <UserRound aria-hidden="true" />}
<span title={username ?? undefined}>{isGuest ? 'Gastmodus' : username}</span>
<button type="button" onClick={onLogout} disabled={loggingOut}>
<LogOut aria-hidden="true" />
<span>{loggingOut ? 'Afsluiten…' : isGuest ? 'Demo afsluiten' : 'Uitloggen'}</span>
</button>
</div>
) : null}
</header>
{secondaryDisplay.error ? (
<div className="secondary-display-error" id="secondary-display-error" role="alert">
<CircleAlert aria-hidden="true" />
<span>{secondaryDisplay.error}</span>
</div>
) : null}
{isGuest ? (
<div className="guest-mode-banner" role="status">
<ShieldCheck aria-hidden="true" />
<div>
<strong>Tijdelijke demowerkruimte</strong>
<span>Alle analysemodellen en werkfuncties zijn beschikbaar. Beheer, instellingen en blijvende gegevenswijzigingen blijven afgeschermd.</span>
</div>
<span className="guest-mode-badge">Analyse-toegang</span>
</div>
) : null}
{isGuest && (!guestDemoReady || loadingDemoWorkflow) ? (
<div className="guest-demo-loading" role="status" aria-live="polite">
<span aria-hidden="true" />
<strong>Demodata en kwaliteitsbewijs worden voorbereid…</strong>
</div>
) : null}
<div className="workbench-content">
<main
ref={workbenchMainRef}
className={activeWorkspace === 'map' ? 'workbench-main workbench-main-map' : 'workbench-main'}
id="workspace-main"
tabIndex={-1}
aria-busy={workspaceDataLoading}
>
{activeWorkspace !== 'map' ? <div className="workspace-heading">
<div className="workspace-heading-copy">
<p className="eyebrow">{selectedProject?.region ?? 'Belgie en Belgische Noordzee'}</p>
<h2>{activeWorkspaceItem.label}</h2>
</div>
<WorkspaceSignal workspace={activeWorkspace} />
<div className="workspace-heading-actions">
<p>{activeWorkspaceItem.description}</p>
{activeWorkspace !== 'overview' ? (
<button
type="button"
className={inspectorOpen ? 'inspector-toggle inspector-toggle-active' : 'inspector-toggle'}
onClick={() => setInspectorOpen((current) => !current)}
aria-expanded={inspectorOpen}
aria-controls="workbench-inspector"
>
{inspectorOpen ? 'Context sluiten' : 'Context bekijken'}
</button>
) : null}
</div>
</div> : null}
{activeWorkspace === 'overview' ? (
<OverviewWorkspace
selectedProject={selectedProject}
selectedProjectId={selectedProjectId}
areas={areas}
datasets={datasets}
qualityChecks={qualityChecks}
exports={exports}
activeLayerFeatureCount={mapFeatureCount}
selectedAreaHasGeometry={Boolean(areaFeatureCollection)}
hasAnalysisOutput={hasAnalysisOutput}
sourceFreshness={sourceFreshness}
onOpenWorkspace={openWorkflowGuidanceStep}
/>
) : null}
{activeWorkspace === 'data' ? (
<div className="workspace-grid workspace-grid-data">
<SourceCatalogPanel
datasets={datasets}
projectId={selectedProjectId ?? undefined}
loading={loadingDatasets}
/>
{isGuest ? (
<div className="guest-readonly-card">
<ShieldCheck aria-hidden="true" />
<div>
<strong>Bronnen bekijken zonder beheerrechten</strong>
<span>De demo gebruikt dezelfde beschikbare databronnen. Projecten, gebieden en uploads worden alleen door de operator beheerd.</span>
</div>
</div>
) : <>
<ProjectPanel
projects={projects}
selectedProjectId={selectedProjectId}
loadingProjects={loadingProjects}
archivingProjectId={archivingProjectId}
projectForm={projectForm}
loadingDemoWorkflow={loadingDemoWorkflow}
demoWorkflowMessage={demoWorkflowMessage}
onCreateProject={createProject}
onUpdateProjectForm={setProjectForm}
onSelectProject={selectProject}
onArchiveProject={archiveProject}
onLoadDemoWorkflow={loadDemoWorkflow}
/>
<AreaPanel
areas={areas}
selectedProject={selectedProject}
selectedProjectId={selectedProjectId}
loadingAreas={loadingAreas}
areaForm={areaForm}
selectedMapAreaId={selectedMapAreaId}
onCreateArea={createArea}
onUpdateAreaForm={setAreaForm}
onSelectMapArea={setSelectedMapAreaId}
/>
<DatasetPanel
selectedProjectId={selectedProjectId}
selectedDatasetId={selectedDatasetId}
areas={areas}
datasets={datasets}
datasetForm={datasetForm}
loadingDatasets={loadingDatasets}
onDatasetFormChange={setDatasetForm}
onUploadDataset={uploadDataset}
onLoadDatasetDetails={loadDatasetDetails}
onOpenInspector={() => setInspectorOpen(true)}
onRefreshMetadata={refreshMetadata}
onOpenDatasetInMap={openDatasetInMap}
onOpenDatasetExport={openDatasetExport}
/>
</>}
</div>
) : null}
<div className="workspace-persistent-map" hidden={activeWorkspace !== 'map'}>
<MapWorkspace
readOnly={false}
secondaryResultsContainer={secondaryDisplay.container}
selectedProjectId={selectedProjectId}
projects={projects}
areas={areas}
selectedMapAreaId={selectedMapAreaId}
areaFeatureCollection={areaFeatureCollection}
mapFeatureCollection={mapFeatureCollection}
qualityEvidenceGeoJson={qualityEvidenceGeoJson?.geojson ?? null}
qualityEvidenceFeatureCount={qualityEvidenceGeoJson?.feature_count ?? 0}
qualityEvidenceLoading={qualityEvidenceLoading}
qualityEvidenceError={qualityEvidenceError}
qualityEvidenceWarnings={qualityEvidenceGeoJson?.warnings ?? []}
mapLayerLabel={mapLayerLabel}
mapLayerSourceLabel={mapLayerSourceLabel}
mapLayerProvenance={mapLayerProvenance}
mapLayerVisible={mapLayerVisible}
mapLayerOpacity={mapLayerOpacity}
areaLayerVisible={areaLayerVisible}
areaLayerOpacity={areaLayerOpacity}
mapFeatureCount={mapFeatureCount}
areaFeatureCount={areaFeatureCount}
viewportVectorEnabled={viewportVectorLayerActive}
viewportVectorStatus={viewportVectorLayerActive ? viewportVectorLayer.statusMessage : null}
viewportVectorTone={viewportVectorLayer.error ? 'error' : viewportVectorLayer.truncated ? 'warning' : viewportVectorLayer.loading || viewportVectorLayer.zoomRequired ? 'pending' : 'ready'}
fitMapDataOnChange={!viewportVectorLayerActive}
mapContentMode={analysisMapLayerActive ? 'analysis' : 'dataset'}
analysisLayerAvailable={analysisMapLayerAvailable}
selectedMapFeature={selectedMapFeature}
mapSelectionBbox={mapSelectionBbox}
mapSelectionResult={mapSelectionResult}
mapSelectionLoading={mapSelectionLoading}
mapSelectionError={mapSelectionError}
coverage={mapCoverage}
coverageLoading={mapCoverageLoading}
coverageError={mapCoverageError}
coverageDurationMs={mapCoverageDurationMs}
coverageBudgetExceeded={mapCoverageBudgetExceeded}
workspaceLoading={workspaceDataLoading}
workspaceError={errorMessage}
selectionExporting={selectionExporting}
selectionExportError={selectionExportError}
latestSelectionExportPath={latestSelectionExport?.path ?? null}
selectionDatasetSaving={selectionDatasetSaving}
selectionDatasetError={selectionDatasetError}
latestSelectionDataset={latestSelectionDataset}
latestSelectionDatasetName={latestSelectionDataset?.name ?? null}
mapQaReferenceDatasets={referenceDatasets}
selectedMapQaReferenceDatasetId={selectedMapQaReferenceDatasetId}
mapSelectionQaRunning={mapSelectionQaRunning}
mapSelectionQaError={mapSelectionQaError}
mapSelectionQaResult={mapSelectionQaResult}
latestMapSelectionQualityCheckId={latestMapSelectionQualityCheckId}
orthophotoAnalysisStage={mapOrthophotoAnalysis.stage}
orthophotoAnalysisStatus={mapOrthophotoAnalysis.status}
orthophotoAnalysisError={mapOrthophotoAnalysis.error}
orthophotoAnalysisRunning={mapOrthophotoAnalysis.running}
orthophotoAnalysisQuality={mapOrthophotoAnalysis.lastQuality}
orthophotoAnalysisDetectionCount={mapOrthophotoAnalysis.lastDetectionCount}
orthophotoProducts={mapOrthophotoAnalysis.products}
selectedOrthophotoProductKey={mapOrthophotoAnalysis.selectedProductKey}
orthophotoResult={mapOrthophotoAnalysis.lastResult}
orthophotoImageUrl={mapOrthophotoAnalysis.imageUrl}
availableMapDatasets={availableMapDatasets}
selectedMapDatasetId={selectedDataset && availableMapDatasets.some((dataset) => dataset.id === selectedDataset.id) ? selectedDataset.id : ''}
selectedFeature={selectedMapFeature}
onSelectMapArea={setSelectedMapAreaId}
onActivateMunicipality={async (niscode) => {
const area = await activateMunicipality(niscode)
if (area) setSelectedMapAreaId(area.id)
return area
}}
onSetContextSourceLabel={setMapContextSourceLabel}
onSetContextLayerLabel={setMapContextLayerLabel}
onOpenDatasetInMap={openDatasetInMap}
onSetAreaLayerVisible={setAreaLayerVisible}
onSetAreaLayerOpacity={setAreaLayerOpacity}
onSetMapLayerVisible={setMapLayerVisible}
onSetMapLayerOpacity={setMapLayerOpacity}
onSetMapContentMode={setMapContentMode}
onSelectMapFeature={setSelectedMapFeature}
onMapViewportChange={viewportVectorLayer.setViewport}
onSetMapSelectionBbox={setMapSelectionBbox}
onRunMapSelectionExtract={runMapSelectionExtract}
onClearMapSelectionExtract={resetMapSelectionExtract}
onExportMapSelection={exportMapSelectionGeoJson}
onPersistMapResult={persistMapResult}
onDeriveMapSelectionDataset={deriveMapSelectionDataset}
onSelectMapQaReferenceDataset={setSelectedMapQaReferenceDatasetId}
onRunMapSelectionQa={runMapSelectionQa}
onOpenMapSelectionQualityEvidence={openMapSelectionQualityEvidence}
onRunOrthophotoAnalysis={mapOrthophotoAnalysis.run}
onSelectOrthophotoProduct={mapOrthophotoAnalysis.setSelectedProductKey}
onClearQualityEvidence={clearQualityEvidenceGeoJson}
onRefreshProjectData={() => (
selectedProjectId ? loadProjectData(selectedProjectId) : Promise.resolve(null)
)}
onOpenAssistant={() => setActiveWorkspace('assistant')}
onOpenExports={() => setActiveWorkspace('exports')}
/>
</div>
{activeWorkspace === 'analysis' ? (
<div className="workspace-grid workspace-grid-analysis">
<QualityResultsPanel
selectedProjectId={selectedProjectId}
qualityChecks={qualityChecks}
qualityChecksError={qualityChecksError}
candidateDatasets={candidateDatasets}
referenceDatasets={referenceDatasets}
onRefresh={() => loadQualityChecks()}
onOpenEvidenceMap={openQualityEvidenceOnMap}
evidenceLoading={qualityEvidenceLoading}
evidenceError={qualityEvidenceError}
onOpenMapWorkspace={() => setActiveWorkspace('map')}
onOpenAnalysisWorkspace={() => setActiveWorkspace('ai')}
/>
<details className="secondary-analysis-disclosure">
<summary>
<span>Historische vectorlagen vergelijken</span>
<strong>Geavanceerd</strong>
</summary>
<ChangeDetectionPanel
vectorDatasets={availableVectorDatasets}
sourceDatasetId={changeSourceDatasetId}
targetDatasetId={changeTargetDatasetId}
iouThreshold={changeIouThreshold}
includeUnchanged={changeIncludeUnchanged}
running={runningChangeDetection}
result={changeDetectionResult}
error={changeDetectionError}
onSourceDatasetChange={setChangeSourceDatasetId}
onTargetDatasetChange={setChangeTargetDatasetId}
onIouThresholdChange={setChangeIouThreshold}
onIncludeUnchangedChange={setChangeIncludeUnchanged}
onRun={runChangeDetection}
/>
</details>
</div>
) : null}
{activeWorkspace === 'assistant' ? (
<div className="workspace-grid workspace-grid-assistant">
<GeoAssistantPanel
selectedProjectId={selectedProjectId}
selectedAreaId={mapSelectionResult?.selection_area_id ?? (!mapSelectionBbox ? selectedArea?.id ?? null : null)}
selectedAreaName={selectedArea?.name ?? null}
selectionBbox={mapSelectionBbox}
/>
</div>
) : null}
{activeWorkspace === 'ai' ? (
<div className="workspace-grid workspace-grid-ai">
<DetectionLab
managementLocked={isGuest}
detectionModels={detectionModels}
modelAssets={modelAssets}
loadingDetectionModels={loadingDetectionModels}
detectionModelError={detectionModelError}
modelAssetError={modelAssetError}
selectedDetectionDatasetId={selectedDetectionDatasetId}
selectedDetectionModelId={selectedDetectionModelId}
selectedModelAssetId={selectedModelAssetId}
detectionTileManifestPath={detectionTileManifestPath}
detectionConfidenceThreshold={detectionConfidenceThreshold}
runningDetection={runningDetection}
detectionRunResult={detectionRunResult}
detectionRunError={detectionRunError}
detectionRuns={detectionRuns}
qualityChecks={qualityChecks}
calibrationThresholdText={calibrationThresholdText}
runningDetectionCalibration={runningDetectionCalibration}
detectionCalibrationRows={detectionCalibrationRows}
detectionCalibrationError={detectionCalibrationError}
detectionWorkflowStage={detectionWorkflowStage}
selectedDetectionRunId={selectedDetectionRunId}
detectionItems={detectionItems}
detectionClassFilter={detectionClassFilter}
detectionMinConfidenceFilter={detectionMinConfidenceFilter}
loadingDetectionResults={loadingDetectionResults}
detectionReferenceDatasetId={detectionReferenceDatasetId}
detectionQaResult={detectionQaResult}
detectionQaError={detectionQaError}
runningDetectionQa={runningDetectionQa}
yoloPreflight={yoloPreflight}
loadingYoloPreflight={loadingYoloPreflight}
yoloPreflightError={yoloPreflightError}
selectedProjectId={selectedProjectId}
rasterDatasets={detectionRasterDatasets}
referenceDatasets={referenceDatasets}
onLoadModels={loadDetectionModels}
onRefreshYoloPreflight={() => loadYoloPreflight()}
onSelectDataset={setSelectedDetectionDatasetId}
onSelectModel={setSelectedDetectionModelId}
onSelectModelAsset={setSelectedModelAssetId}
onSetConfidenceThreshold={setDetectionConfidenceThreshold}
onSetTileManifestPath={setDetectionTileManifestPath}
onRunDetection={runDetection}
onUploadRaster={uploadDetectionRaster}
onPrepareAndRunDetection={runGuidedDetection}
onOpenResultsOnMap={openDetectionResultsOnMap}
onLoadRuns={() => loadDetectionRuns()}
onSelectRun={setSelectedDetectionRunId}
onSetClassFilter={setDetectionClassFilter}
onSetMinConfidenceFilter={setDetectionMinConfidenceFilter}
onLoadResults={() => loadDetectionResults()}
onSelectReferenceDataset={setDetectionReferenceDatasetId}
onRunQa={runDetectionQa}
onSetCalibrationThresholdText={setCalibrationThresholdText}
onRunCalibration={runDetectionCalibration}
onOpenCalibrationEvidence={openQualityEvidenceOnMap}
onApplyOperatorProfile={applyDetectionOperatorProfile}
/>
<details className="secondary-analysis-disclosure segmentation-disclosure">
<summary>
<span>Segmentatie van beeldvlakken</span>
<strong>Nog niet geconfigureerd</strong>
</summary>
<SegmentationLab
segmentationModels={segmentationModels}
loadingSegmentationModels={loadingSegmentationModels}
segmentationModelError={segmentationModelError}
selectedSegmentationDatasetId={selectedSegmentationDatasetId}
selectedSegmentationModelId={selectedSegmentationModelId}
segmentationTileManifestPath={segmentationTileManifestPath}
segmentationConfidenceThreshold={segmentationConfidenceThreshold}
runningSegmentation={runningSegmentation}
segmentationRunResult={segmentationRunResult}
segmentationRunError={segmentationRunError}
segmentationRuns={segmentationRuns}
selectedSegmentationRunId={selectedSegmentationRunId}
segmentationItems={segmentationItems}
segmentationClassFilter={segmentationClassFilter}
segmentationMinConfidenceFilter={segmentationMinConfidenceFilter}
loadingSegmentationResults={loadingSegmentationResults}
segmentationReferenceDatasetId={segmentationReferenceDatasetId}
segmentationQaResult={segmentationQaResult}
segmentationQaError={segmentationQaError}
runningSegmentationQa={runningSegmentationQa}
selectedProjectId={selectedProjectId}
rasterDatasets={rasterDatasets}
referenceDatasets={referenceDatasets}
selectedSegmentationModelConfigured={Boolean(selectedSegmentationModel?.configured)}
selectedSegmentationModelLimitation={selectedSegmentationModel?.limitation_message ?? null}
onLoadModels={loadSegmentationModels}
onSelectDataset={setSelectedSegmentationDatasetId}
onSelectModel={setSelectedSegmentationModelId}
onSetTileManifestPath={setSegmentationTileManifestPath}
onSetConfidenceThreshold={setSegmentationConfidenceThreshold}
onRunSegmentation={runSegmentation}
onLoadRuns={() => loadSegmentationRuns()}
onSelectRun={setSelectedSegmentationRunId}
onSetClassFilter={setSegmentationClassFilter}
onSetMinConfidenceFilter={setSegmentationMinConfidenceFilter}
onLoadResults={() => loadSegmentationResults()}
onSelectReferenceDataset={setSegmentationReferenceDatasetId}
onRunQa={runSegmentationQa}
/>
</details>
</div>
) : null}
{activeWorkspace === 'exports' ? (
<div className="workspace-grid workspace-grid-exports">
<ExportCenter
selectedProjectId={selectedProjectId}
selectedDataset={selectedDataset}
selectedDetectionRunId={selectedDetectionRunId}
selectedSegmentationRunId={selectedSegmentationRunId}
exports={exports}
latestExport={latestExport}
exportError={exportError}
loadingExports={loadingExports}
exporting={exporting}
onRefresh={() => loadExports()}
onExportDataset={exportSelectedDatasetGeoJson}
onExportDetectionRun={exportSelectedDetectionRunGeoJson}
onExportSegmentationRun={exportSelectedSegmentationRunGeoJson}
onExportProjectMetadata={exportProjectMetadata}
onExportProjectReport={exportProjectReport}
onPreviewContent={previewExportContent}
onDownload={downloadExportArtifact}
/>
<ExportPreview content={exportPreview} />
</div>
) : null}
{activeWorkspace === 'system' ? (
<ProviderPanel
selectedProjectId={selectedProjectId}
providers={providers}
loadingCapabilities={loadingCapabilities}
capabilitiesError={capabilitiesError}
onRefresh={loadCapabilities}
onOpenSources={() => setActiveWorkspace('data')}
onOpenStatus={() => setActiveWorkspace('overview')}
onOpenMap={() => setActiveWorkspace('map')}
/>
) : null}
</main>
{inspectorOpen ? (
<aside className="workbench-inspector" id="workbench-inspector" aria-label="Details van de huidige selectie">
<WorkbenchInspector
selectedProject={selectedProject}
selectedArea={selectedArea}
selectedMapFeature={selectedMapFeature}
areas={areas}
datasetsCount={datasets.length}
qualityChecks={qualityChecks}
exports={exports}
latestExport={latestExport}
detectionRuns={detectionRuns}
selectedDetectionRunId={selectedDetectionRunId}
detectionItems={detectionItems}
segmentationRuns={segmentationRuns}
selectedSegmentationRunId={selectedSegmentationRunId}
segmentationItems={segmentationItems}
onOpenDataWorkspace={() => setActiveWorkspace('data')}
onOpenMapWorkspace={() => setActiveWorkspace('map')}
onOpenQualityWorkspace={() => setActiveWorkspace('analysis')}
onOpenExportsWorkspace={() => setActiveWorkspace('exports')}
onOpenAiWorkspace={() => setActiveWorkspace('ai')}
onClose={() => setInspectorOpen(false)}
datasetDetailProps={{
areas,
availableVectorTargets,
selectedDatasetId,
selectedDataset,
selectedDatasetSummary,
selectedRasterMetadata,
selectedRasterStats,
rasterPreview,
latestRasterTileManifestPath,
latestRasterTileManifest,
rasterUnavailableMessage,
selectedClipAreaId,
selectedIntersectTargetId,
rasterTileSize,
rasterTileOverlap,
rasterTileOutputName,
rasterReprojectCrs,
rasterReprojectResampling,
ndviNirBand,
ndviRedBand,
ndwiGreenBand,
ndwiNirBand,
ndbiSwirBand,
ndbiNirBand,
isRasterTileInputValid,
jobs,
loadingDatasetDetails,
datasetDetailError,
isVectorDatasetType,
onSetSelectedClipAreaId: setSelectedClipAreaId,
onSetSelectedIntersectTargetId: setSelectedIntersectTargetId,
onSetRasterTileSize: setRasterTileSize,
onSetRasterTileOverlap: setRasterTileOverlap,
onSetRasterTileOutputName: setRasterTileOutputName,
onSetRasterReprojectCrs: setRasterReprojectCrs,
onSetRasterReprojectResampling: setRasterReprojectResampling,
onSetNdviNirBand: setNdviNirBand,
onSetNdviRedBand: setNdviRedBand,
onSetNdwiGreenBand: setNdwiGreenBand,
onSetNdwiNirBand: setNdwiNirBand,
onSetNdbiSwirBand: setNdbiSwirBand,
onSetNdbiNirBand: setNdbiNirBand,
onRunRasterInspect: runRasterInspect,
onRunRasterPreview: runRasterPreview,
onRunRasterStats: runRasterStats,
onRunRasterReproject: runRasterReproject,
onRunRasterClip: runRasterClip,
onRunRasterTile: runRasterTile,
onUseTileManifestForDetection: useRasterTileManifestForDetection,
onUseTileManifestForSegmentation: useRasterTileManifestForSegmentation,
onRunRasterNdvi: runRasterNdvi,
onRunRasterNdwi: runRasterNdwi,
onRunRasterNdbi: runRasterNdbi,
onRunVectorClip: runVectorClip,
onRunVectorBuffer: runVectorBuffer,
onRunVectorIntersect: () => runVectorIntersect(availableVectorTargets),
onPickDerivedDataset: pickDerivedDataset,
}}
/>
</aside>
) : null}
</div>
</div>
</div>
</div>
)
}
function App(): JSX.Element {
const { session, sessionError, loggingOut, handleAuthenticated, handleLogout } = useOperatorSession()
if (session === null) {
return (
<div className="landing-auth-loading" role="status" aria-live="polite">
<div><span aria-hidden="true" /><strong>GeoIntel wordt voorbereid…</strong></div>
</div>
)
}
if (!session.authenticated) {
return (
<LandingPage
serviceError={sessionError}
guestAccessEnabled={session.guest_access_enabled}
onAuthenticated={handleAuthenticated}
/>
)
}
return (
<WorkbenchApp
username={session.username}
accessMode={session.role ?? (session.authentication_required ? 'operator' : 'open')}
loggingOut={loggingOut}
onLogout={handleLogout}
/>
)
}
export default App