Files
geointel/frontend/src/App.tsx
T
Jens c76a746cd7
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s
Update
2026-07-27 23:28:43 +02:00

1442 lines
58 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from 'react'
import { CircleAlert, LogOut, 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'
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 { 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>(['map', 'analysis'])
const guestWorkspaceGroups: WorkspaceNavigationGroup[] = [
{ label: 'Demowerkruimte', keys: ['map', 'analysis'] },
]
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 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,
})
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),
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: isGuest,
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: isGuest,
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(target === 'exports' ? 'analysis' : '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"}>
<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>
{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>
{isGuest ? (
<div className="guest-mode-banner" role="status">
<ShieldCheck aria-hidden="true" />
<div>
<strong>Tijdelijke demowerkruimte</strong>
<span>U verkent vooraf geladen voorbeelddata. Wijzigingen, nieuwe analyses en operatorfuncties zijn uitgeschakeld.</span>
</div>
<span className="guest-mode-badge">Alleen-lezen</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>
{!isGuest && 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}
/>
<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={isGuest}
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(isGuest ? 'analysis' : 'assistant')}
onOpenExports={() => setActiveWorkspace(isGuest ? 'analysis' : '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(isGuest ? 'map' : 'ai')}
/>
{!isGuest ? (
<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 className="guest-readonly-card">
<ShieldCheck aria-hidden="true" />
<div>
<strong>Controleerbaar voorbeeldresultaat</strong>
<span>Deze kwaliteitsweergave gebruikt de vooraf berekende demo. Nieuwe vergelijkingen zijn alleen beschikbaar voor operators.</span>
</div>
</div>
)}
</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
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 && !isGuest ? (
<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