1442 lines
58 KiB
TypeScript
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
|