diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3f255793..1d93f3b5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,1455 +1,15 @@ -import { useEffect, useMemo, useRef, useState } from 'react' -import { CircleAlert, LogOut, MonitorUp, ShieldCheck, UserRound } from 'lucide-react' -import '@fontsource/manrope/latin-500.css' -import '@fontsource/manrope/latin-600.css' -import '@fontsource/manrope/latin-700.css' -import '@fontsource/public-sans/latin-400.css' -import '@fontsource/public-sans/latin-500.css' -import '@fontsource/public-sans/latin-600.css' -import './styles/app.css' -import './styles/premium.css' -import './styles/atlas-workbench.css' -import './styles/atlas-premium-v2.css' -import './styles/professionalization.css' -// Laatst geladen en leidend: het geconsolideerde designsysteem. -// Zie de kop van geointel-system.css voor de reden. -import './styles/geointel-system.css' +import { Suspense, lazy } from 'react' import { LandingPage } from './components/auth/LandingPage' -import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel' -import { GeoAssistantPanel } from './components/assistant/GeoAssistantPanel' -import { DatasetPanel } from './components/datasets/DatasetPanel' -import { SourceCatalogPanel } from './components/datasets/SourceCatalogPanel' -import { DetectionLab } from './components/detection/DetectionLab' -import { ExportCenter } from './components/exports/ExportCenter' -import { ExportPreview } from './components/exports/ExportPreview' -import { WorkbenchInspector } from './components/inspector/WorkbenchInspector' -import { MapWorkspace } from './components/map/MapWorkspace' -import { OverviewWorkspace, type WorkspaceKey } from './components/overview/OverviewWorkspace' -import { AreaPanel } from './components/project/AreaPanel' -import { ProjectPanel } from './components/project/ProjectPanel' -import { QualityResultsPanel } from './components/quality/QualityResultsPanel' -import type { DatasetCreateResponse } from './types' -import { ProviderPanel } from './components/providers/ProviderPanel' -import { SegmentationLab } from './components/segmentation/SegmentationLab' -import { - WorkbenchNavigation, - type WorkspaceNavigationGroup, - type WorkspaceNavigationItem, -} from './components/shell/WorkbenchNavigation' -import { WorkspaceSignal } from './components/shell/WorkspaceSignal' -import { useSecondaryDisplay } from './components/shell/SecondaryDisplay' -import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow' -import { useDemoWorkflow } from './hooks/useDemoWorkflow' -import { useCoverageResolver } from './hooks/useCoverageResolver' -import { useDetectionWorkflow } from './hooks/useDetectionWorkflow' -import { useDatasetWorkflow } from './hooks/useDatasetWorkflow' -import { useExportWorkflow } from './hooks/useExportWorkflow' -import { useMapSelectionDataset } from './hooks/useMapSelectionDataset' -import { useMapSelectionQa } from './hooks/useMapSelectionQa' -import { useMapWorkspaceState } from './hooks/useMapWorkspaceState' import { useOperatorSession } from './hooks/useOperatorSession' -import { useMapSelectionExtract } from './hooks/useMapSelectionExtract' -import { useMapOrthophotoAnalysis } from './hooks/useMapOrthophotoAnalysis' -import { isDetectionImageryDataset, isMapRasterDataset } from './lib/datasetCapabilities' -import { useProviderCapabilities } from './hooks/useProviderCapabilities' -import { useProjectWorkspace } from './hooks/useProjectWorkspace' -import { useQualityWorkflow } from './hooks/useQualityWorkflow' -import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow' -import { useSourceFreshness } from './hooks/useSourceFreshness' -import { useWorkbenchBootstrap } from './hooks/useWorkbenchBootstrap' -import { useViewportVectorLayer } from './hooks/useViewportVectorLayer' -import { getDatasetDisplayName } from './lib/datasetDisplay' -import { - FLANDERS_WORKSPACE_LABEL, - FLANDERS_WORKSPACE_PROJECT_NAME, - NATIONAL_WORKSPACE_LABEL, - NATIONAL_WORKSPACE_PROJECT_NAME, - REGIONAL_WORKSPACE_LABEL, - REGIONAL_WORKSPACE_PROJECT_NAME, -} from './config/primaryFocus' -function isVectorDatasetType(datasetType: string): boolean { - return datasetType === 'vector' || datasetType === 'geojson' -} - -const workspaceNavItems: WorkspaceNavigationItem[] = [ - { key: 'overview', label: 'Status', description: 'Beschikbaarheid en aandachtspunten' }, - { key: 'data', label: 'Bronnen', description: 'Gebieden en ingeladen gegevens' }, - { key: 'map', label: 'Kaart', description: 'Selecteren, uitlezen en vergelijken' }, - { key: 'assistant', label: 'AI-vragen', description: 'Vraag de lokale assistent over het actieve gebied' }, - { key: 'analysis', label: 'Kwaliteit', description: 'Resultaten controleren' }, - { key: 'ai', label: 'Beeldanalyse', description: 'Gebouwen herkennen op luchtbeelden' }, - { key: 'exports', label: 'Downloads', description: 'Resultaten bewaren en delen' }, - { key: 'system', label: 'Systeem', description: 'Bronkoppelingen en operationele status' }, -] - -const workspaceNavGroups: WorkspaceNavigationGroup[] = [ - { label: 'Verkennen', keys: ['map', 'data'] }, - { label: 'Vragen', keys: ['assistant'] }, - { label: 'Analyseren', keys: ['analysis', 'ai'] }, - { label: 'Afronden', keys: ['exports'] }, - { label: 'Beheer', keys: ['overview', 'system'] }, -] - -const guestWorkspaceKeys = new Set([ - 'overview', - 'data', - 'map', - 'assistant', - 'analysis', - 'ai', - 'exports', -]) -const guestWorkspaceGroups: WorkspaceNavigationGroup[] = [ - { label: 'Verkennen', keys: ['map', 'data'] }, - { label: 'Vragen', keys: ['assistant'] }, - { label: 'Analyseren', keys: ['analysis', 'ai'] }, - { label: 'Afronden', keys: ['exports'] }, - { label: 'Demo', keys: ['overview'] }, -] - -interface WorkbenchAppProps { - username: string | null - accessMode: 'open' | 'operator' | 'guest' - loggingOut: boolean - onLogout: () => void -} - -function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchAppProps): JSX.Element { - const isGuest = accessMode === 'guest' - const [activeWorkspace, setActiveWorkspace] = useState('map') - const [inspectorOpen, setInspectorOpen] = useState(false) - const [guestDemoReady, setGuestDemoReady] = useState(!isGuest) - const secondaryDisplay = useSecondaryDisplay() - const guestDemoStartedRef = useRef(false) - const visibleWorkspaceItems = useMemo( - () => isGuest ? workspaceNavItems.filter((item) => guestWorkspaceKeys.has(item.key)) : workspaceNavItems, - [isGuest], - ) - const visibleWorkspaceGroups = isGuest ? guestWorkspaceGroups : workspaceNavGroups - const [mapContentMode, setMapContentMode] = useState<'dataset' | 'analysis'>('dataset') - const [mapContextSourceLabel, setMapContextSourceLabel] = useState(null) - const [mapContextLayerLabel, setMapContextLayerLabel] = useState(null) - const workbenchMainRef = useRef(null) - const previousWorkspaceRef = useRef(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 - const processingCode = metadata['processing_code'] - if (processingCode === 'RASTER_PROCESSING_UNAVAILABLE') { - return String(metadata['processing_error'] ?? 'Raster processing unavailable.') - } - return null - }, [selectedDataset]) - - const availableVectorTargets = useMemo( - () => datasets.filter((item) => item.id !== selectedDatasetId && isVectorDatasetType(item.dataset_type)), - [datasets, selectedDatasetId], - ) - const availableVectorDatasets = useMemo(() => datasets.filter((item) => isVectorDatasetType(item.dataset_type)), [datasets]) - const availableMapDatasets = useMemo( - () => { - const vectors = datasets.filter((dataset) => isVectorDatasetType(dataset.dataset_type) && dataset.status === 'ready') - const rasters = datasets.filter(isMapRasterDataset) - return [...vectors, ...rasters] - }, - [datasets], - ) - const referenceDatasets = useMemo( - () => availableVectorDatasets.filter((item) => item.dataset_role === 'reference'), - [availableVectorDatasets], - ) - const candidateDatasets = useMemo( - () => availableVectorDatasets.filter((item) => item.dataset_role !== 'reference'), - [availableVectorDatasets], - ) - const rasterDatasets = useMemo(() => datasets.filter((item) => item.dataset_type === 'raster'), [datasets]) - const detectionRasterDatasets = useMemo( - () => rasterDatasets.filter(isDetectionImageryDataset), - [rasterDatasets], - ) - const { - providers, - loadingCapabilities, - capabilitiesError, - loadCapabilities, - } = useProviderCapabilities() - const { - changeSourceDatasetId, - changeTargetDatasetId, - changeIouThreshold, - changeIncludeUnchanged, - runningChangeDetection, - changeDetectionResult, - changeDetectionError, - runChangeDetection, - setChangeSourceDatasetId, - setChangeTargetDatasetId, - setChangeIouThreshold, - setChangeIncludeUnchanged, - } = useChangeDetectionWorkflow({ - selectedProjectId, - availableVectorDatasets, - loadDatasetJobs, - // Bind the comparison to whatever the operator has selected on the map. - getSelection: () => ({ bbox: mapSelectionBbox ?? null, areaId: selectedMapAreaId ?? null }), - }) - const { - qaCandidateDatasetId, - qaReferenceDatasetId, - qaAreaId, - qaIouThreshold, - qaRunning, - qaResult, - qaError, - qualityChecks, - qualityChecksError, - qualityEvidenceGeoJson, - qualityEvidenceLoading, - qualityEvidenceError, - loadQualityChecks, - loadQualityEvidenceGeoJson, - clearQualityEvidenceGeoJson, - runQaComparison, - setQaCandidateDatasetId, - setQaReferenceDatasetId, - setQaAreaId, - setQaIouThreshold, - } = useQualityWorkflow({ - selectedProjectId, - loadProjectData, - }) - const { - detectionModels, - modelAssets, - loadingDetectionModels, - detectionModelError, - modelAssetError, - selectedDetectionDatasetId, - selectedDetectionModelId, - selectedModelAssetId, - detectionTileManifestPath, - detectionConfidenceThreshold, - runningDetection, - detectionRunResult, - detectionRunError, - detectionRuns, - selectedDetectionRunId, - detectionItems, - detectionTotal, - detectionTruncated, - detectionGeoJson, - detectionClassFilter, - detectionMinConfidenceFilter, - loadingDetectionResults, - detectionReferenceDatasetId, - detectionQaResult, - detectionQaError, - runningDetectionQa, - yoloPreflight, - loadingYoloPreflight, - yoloPreflightError, - calibrationThresholdText, - runningDetectionCalibration, - detectionCalibrationRows, - detectionCalibrationError, - detectionWorkflowStage, - loadDetectionModels, - loadYoloPreflight, - loadDetectionRuns, - loadDetectionResults, - runDetection, - uploadDetectionRaster, - prepareAndRunDetection, - compareDetectionRunWithReference, - runDetectionQa, - runDetectionCalibration, - applyDetectionOperatorProfile, - resetDetectionForProject, - setSelectedDetectionDatasetId, - setSelectedDetectionModelId, - setSelectedModelAssetId, - setDetectionTileManifestPath, - setDetectionConfidenceThreshold, - setSelectedDetectionRunId, - setDetectionClassFilter, - setDetectionMinConfidenceFilter, - setDetectionReferenceDatasetId, - setCalibrationThresholdText, - } = useDetectionWorkflow({ - selectedProjectId, - rasterDatasets: detectionRasterDatasets, - qaIouThreshold, - loadProjectData, - loadQualityChecks, - }) - const useRasterTileManifestForDetection = () => { - const manifestPath = latestRasterTileManifestPath.trim() - if (!manifestPath || !selectedDataset || !isDetectionImageryDataset(selectedDataset)) { - return - } - setDetectionTileManifestPath(manifestPath) - setSelectedDetectionDatasetId(selectedDataset.id) - setSelectedDetectionModelId('yolo-configured') - setDetectionConfidenceThreshold(0.25) - loadYoloPreflight(manifestPath).catch(() => null) - setActiveWorkspace('ai') - } - const { - segmentationModels, - loadingSegmentationModels, - segmentationModelError, - selectedSegmentationDatasetId, - selectedSegmentationModelId, - selectedSegmentationModel, - segmentationTileManifestPath, - segmentationConfidenceThreshold, - runningSegmentation, - segmentationRunResult, - segmentationRunError, - segmentationRuns, - selectedSegmentationRunId, - segmentationItems, - segmentationGeoJson, - segmentationClassFilter, - segmentationMinConfidenceFilter, - loadingSegmentationResults, - segmentationReferenceDatasetId, - segmentationQaResult, - segmentationQaError, - runningSegmentationQa, - loadSegmentationModels, - loadSegmentationRuns, - loadSegmentationResults, - runSegmentation, - runSegmentationQa, - resetSegmentationForProject, - setSelectedSegmentationDatasetId, - setSelectedSegmentationModelId, - setSegmentationTileManifestPath, - setSegmentationConfidenceThreshold, - setSelectedSegmentationRunId, - setSegmentationClassFilter, - setSegmentationMinConfidenceFilter, - setSegmentationReferenceDatasetId, - } = useSegmentationWorkflow({ - selectedProjectId, - rasterDatasets, - qaIouThreshold, - loadProjectData, - loadQualityChecks, - }) - const viewportVectorLayer = useViewportVectorLayer({ - selectedProjectId, - selectedDataset, - featureCount: selectedDatasetSummary?.feature_count ?? selectedDataset?.feature_count ?? null, - isVectorDatasetType, - }) - const analysisMapLayerAvailable = Boolean(changeDetectionResult?.geojson || segmentationGeoJson || detectionGeoJson) - const analysisMapLayerActive = mapContentMode === 'analysis' && analysisMapLayerAvailable - const viewportVectorLayerActive = viewportVectorLayer.enabled && !analysisMapLayerActive - const datasetMapContent = viewportVectorLayer.enabled ? viewportVectorLayer.data : datasetContent - const useRasterTileManifestForSegmentation = () => { - const manifestPath = latestRasterTileManifestPath.trim() - if (!manifestPath || !selectedDataset || selectedDataset.dataset_type !== 'raster') { - return - } - setSegmentationTileManifestPath(manifestPath) - setSelectedSegmentationDatasetId(selectedDataset.id) - setActiveWorkspace('ai') - } - const { - exports, - latestExport, - exportError, - loadingExports, - exporting, - selectionExporting, - selectionExportError, - latestSelectionExport, - exportPreview, - loadExports, - exportSelectedDatasetGeoJson, - exportSelectedDetectionRunGeoJson, - exportSelectedSegmentationRunGeoJson, - exportMapSelectionGeoJson, - persistMapResult, - exportProjectMetadata, - exportProjectReport, - previewExportContent, - downloadExportArtifact, - resetExportsForProject, - } = useExportWorkflow({ - selectedProjectId, - selectedDataset, - selectedDetectionRunId, - selectedSegmentationRunId, - isVectorDatasetType, - }) - const { - mapLayerVisible, - mapLayerOpacity, - selectedMapAreaId, - areaLayerVisible, - areaLayerOpacity, - selectedMapFeature, - areaFeatureCollection, - mapFeatureCollection, - mapLayerLabel, - mapFeatureCount, - areaFeatureCount, - setMapLayerVisible, - setMapLayerOpacity, - setSelectedMapAreaId, - setAreaLayerVisible, - setAreaLayerOpacity, - setSelectedMapFeature, - } = useMapWorkspaceState({ - areas, - changeDetectionGeoJson: analysisMapLayerActive ? changeDetectionResult?.geojson ?? null : null, - segmentationGeoJson: analysisMapLayerActive ? segmentationGeoJson : null, - detectionGeoJson: analysisMapLayerActive ? detectionGeoJson : null, - datasetContent: datasetMapContent, - selectedDataset, - datasetLayerActive: Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type)), - }) - const { - mapSelectionBbox, - mapSelectionResult, - mapSelectionLoading, - mapSelectionError, - runMapSelectionExtract, - resetMapSelectionExtract, - setMapSelectionBbox, - } = useMapSelectionExtract({ - selectedProjectId, - selectedDataset, - isVectorDatasetType, - }) - const { - coverage: mapCoverage, - loadingCoverage: mapCoverageLoading, - coverageError: mapCoverageError, - coverageDurationMs: mapCoverageDurationMs, - coverageBudgetExceeded: mapCoverageBudgetExceeded, - } = useCoverageResolver({ - projectId: selectedProjectId, - bbox: mapSelectionBbox, - }) - const { - selectionDatasetSaving, - selectionDatasetError, - latestSelectionDataset, - deriveMapSelectionDataset, - } = useMapSelectionDataset({ - selectedProjectId, - selectedDataset, - isVectorDatasetType, - loadProjectData, - loadDatasetDetails, - setMapLayerVisible, - }) - const { - selectedMapQaReferenceDatasetId, - mapSelectionQaRunning, - mapSelectionQaError, - mapSelectionQaResult, - latestMapSelectionQualityCheckId, - runMapSelectionQa, - setSelectedMapQaReferenceDatasetId, - } = useMapSelectionQa({ - selectedProjectId, - latestSelectionDataset, - loadQualityChecks, - loadProjectData, - }) - const mapOrthophotoAnalysis = useMapOrthophotoAnalysis({ - selectedProjectId, - selectedAreaId: areas.find((area) => /vervoerregio|operationele grens/i.test(area.name))?.id ?? selectedMapAreaId, - selectionBbox: mapSelectionBbox, - datasets, - loadProjectData, - prepareAndRunDetection: (datasetId) => prepareAndRunDetection(datasetId, 'yolo-configured'), - compareDetectionRunWithReference: (analysisRunId, referenceDatasetId, iouThreshold) => - compareDetectionRunWithReference(analysisRunId, referenceDatasetId, false, iouThreshold), - loadQualityEvidence: (qualityCheckId) => loadQualityEvidenceGeoJson(qualityCheckId), - onAnalysisReady: () => { - setMapContentMode('analysis') - setMapLayerVisible(true) - setActiveWorkspace('map') - }, - }) - const openMapSelectionQualityEvidence = () => { - setActiveWorkspace('analysis') - } - const openQualityEvidenceOnMap = async (qualityCheckId: string) => { - const result = await loadQualityEvidenceGeoJson(qualityCheckId) - if (result) { - setMapLayerVisible(true) - setActiveWorkspace('map') - } - } - const { - loadingDemoWorkflow, - demoWorkflowMessage, - loadDemoWorkflow, - } = useDemoWorkflow({ - restrictedMode: false, - loadProjects, - loadProjectData, - loadDatasetDetails, - loadDetectionRuns, - loadSegmentationRuns, - loadQualityChecks, - loadExports, - setSelectedProjectId: selectProject, - setSelectedDatasetId, - setSelectedMapAreaId, - setQaCandidateDatasetId, - setQaReferenceDatasetId, - setQaAreaId, - setSelectedDetectionDatasetId, - setDetectionReferenceDatasetId, - setSelectedSegmentationDatasetId, - setSegmentationReferenceDatasetId, - setErrorMessage, - }) - - useEffect(() => { - if (!isGuest || guestDemoStartedRef.current) return - guestDemoStartedRef.current = true - setGuestDemoReady(false) - void loadDemoWorkflow().finally(() => setGuestDemoReady(true)) - }, [isGuest, loadDemoWorkflow]) - - useWorkbenchBootstrap({ - restrictedMode: false, - selectedProjectId, - selectedDetectionRunId, - detectionClassFilter, - detectionMinConfidenceFilter, - selectedSegmentationRunId, - segmentationClassFilter, - segmentationMinConfidenceFilter, - loadProjects, - loadCapabilities, - loadDetectionModels, - loadSegmentationModels, - loadProjectData, - loadDetectionRuns, - loadSegmentationRuns, - loadQualityChecks, - loadExports, - loadDetectionResults, - loadSegmentationResults, - resetProjectData, - resetDatasetForProject, - resetDetectionForProject, - resetSegmentationForProject, - resetExportsForProject, - }) - - const selectedArea = areas.find((area) => area.id === selectedMapAreaId) ?? null - const activeWorkspaceItem = visibleWorkspaceItems.find((item) => item.key === activeWorkspace) ?? visibleWorkspaceItems[0] - const mapLayerSourceLabel = useMemo(() => { - if (analysisMapLayerActive && changeDetectionResult?.geojson) { - return 'Veranderingsanalyse' - } - if (analysisMapLayerActive && segmentationGeoJson) { - return 'Segmentatierun' - } - if (analysisMapLayerActive && detectionGeoJson) { - return 'Detectierun' - } - if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) { - return selectedDataset.dataset_type === 'raster' ? 'Rasterdatabron' : 'Vectordatabron' - } - return 'Geen actieve gegevens- of analyselaag' - }, [analysisMapLayerActive, changeDetectionResult?.geojson, datasetMapContent, detectionGeoJson, segmentationGeoJson, selectedDataset, viewportVectorLayer.enabled]) - const mapLayerProvenance = useMemo(() => { - if (analysisMapLayerActive && changeDetectionResult?.geojson) { - return 'Vergelijking van twee bewaarde kaartlagen' - } - if (analysisMapLayerActive && segmentationGeoJson) { - return selectedSegmentationRunId ? 'Bewaarde segmentatieronde' : 'Segmentatieresultaten geladen' - } - if (analysisMapLayerActive && detectionGeoJson) { - return selectedDetectionRunId ? 'Bewaarde detectieronde' : 'Detectieresultaten geladen' - } - if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) { - return `Bewaarde ${selectedDataset.dataset_role === 'reference' ? 'referentielaag' : 'databron'}` - } - return 'Open een databron, detectieronde, segmentatieronde of veranderingsresultaat om het hier te tekenen.' - }, [ - analysisMapLayerActive, - changeDetectionResult?.geojson, - changeSourceDatasetId, - changeTargetDatasetId, - datasetMapContent, - detectionGeoJson, - segmentationGeoJson, - selectedDataset, - selectedDetectionRunId, - selectedSegmentationRunId, - viewportVectorLayer.enabled, - ]) - const openDatasetInMap = (dataset: DatasetCreateResponse) => { - if (selectedProjectId) { - loadDatasetDetails(selectedProjectId, dataset) - } - setMapContentMode('dataset') - setMapLayerVisible(true) - setActiveWorkspace('map') - } - const runGuidedDetection = async () => { - const completed = await prepareAndRunDetection() - if (completed) { - setMapContentMode('analysis') - setMapLayerVisible(true) - setActiveWorkspace('map') - } - } - const openDetectionResultsOnMap = () => { - if (!detectionGeoJson) { - return - } - setMapContentMode('analysis') - setMapLayerVisible(true) - setActiveWorkspace('map') - } - const openDatasetExport = (dataset: DatasetCreateResponse) => { - if (selectedProjectId) { - loadDatasetDetails(selectedProjectId, dataset) - } - setActiveWorkspace('exports') - } - const openWorkflowGuidanceStep = (target: WorkspaceKey) => { - if (isGuest && !guestWorkspaceKeys.has(target)) { - setActiveWorkspace('map') - return - } - if (target === 'map' && availableMapDatasets.length > 0 && !mapFeatureCollection) { - openDatasetInMap(availableMapDatasets[0]) - return - } - if (target === 'exports' && availableMapDatasets.length > 0 && !selectedDataset) { - openDatasetExport(availableMapDatasets[0]) - return - } - setActiveWorkspace(target) - } - const hasAnalysisOutput = - qualityChecks.length > 0 - || Boolean(changeDetectionResult) - || detectionItems.length > 0 - || segmentationItems.length > 0 - const projectContextLabel = selectedProject?.name === NATIONAL_WORKSPACE_PROJECT_NAME - ? NATIONAL_WORKSPACE_LABEL - : selectedProject?.name === REGIONAL_WORKSPACE_PROJECT_NAME - ? REGIONAL_WORKSPACE_LABEL - : selectedProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME - ? FLANDERS_WORKSPACE_LABEL - : selectedProject?.name ?? 'Geen werkruimte' - const areaContextLabel = selectedArea?.name ?? (areas.length > 0 ? 'Kies een gebied' : 'Geen gebied') - const bathymetryProfileContextActive = Boolean( - activeWorkspace === 'map' - && selectedDataset?.source_name === 'vmm_vha_bathymetry_profiles', - ) - const bathymetryRasterContextActive = Boolean( - activeWorkspace === 'map' - && selectedDataset?.source_name === 'spw_bathymetry', - ) - const bathymetryContextActive = bathymetryProfileContextActive || bathymetryRasterContextActive - const regionalBathymetryContextActive = Boolean( - bathymetryProfileContextActive - && selectedDataset?.source_metadata?.regional_partitions_complete === true - && selectedArea - && !/^Gemeente\s/i.test(selectedArea.name), - ) - const regionalBathymetryManifest = String( - selectedDataset?.source_metadata?.partition_manifest_sha256 ?? '', - ) - const regionalBathymetryPartitions = regionalBathymetryContextActive - ? availableMapDatasets.filter( - (dataset) => - dataset.source_name === 'vmm_vha_bathymetry_profiles' - && dataset.source_metadata?.regional_partitions_complete === true - && String(dataset.source_metadata?.partition_manifest_sha256 ?? '') === regionalBathymetryManifest, - ) - : [] - const regionalBathymetryProfileCount = regionalBathymetryPartitions.reduce( - (total, dataset) => total + (dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0), - 0, - ) - const bathymetryProfileCount = regionalBathymetryContextActive - ? regionalBathymetryProfileCount - : selectedDataset?.feature_count ?? selectedDataset?.vector_summary?.feature_count ?? 0 - const workspaceDataLoading = loadingProjects || loadingAreas || loadingDatasets - const workspaceStatusMessage = errorMessage - ? `Werkruimtefout: ${errorMessage}` - : workspaceDataLoading - ? 'Werkruimte en databronnen worden geladen.' - : selectedProject - ? `${projectContextLabel} is geladen met ${datasets.length} databronnen en ${areas.length} gebieden.` - : 'Geen werkruimte geselecteerd.' - const datasetContextLabel = activeWorkspace === 'map' && mapContextSourceLabel - ? mapContextSourceLabel - : analysisMapLayerActive && mapFeatureCollection - ? `${mapLayerLabel} · controle vereist` - : selectedDataset - ? getDatasetDisplayName(selectedDataset) - : workspaceDataLoading - ? 'Bronnen worden geladen' - : datasets.length > 0 - ? 'Kies een bron' - : 'Geen bron' - const layerContextLabel = activeWorkspace === 'map' && mapContextLayerLabel - ? mapContextLayerLabel - : bathymetryContextActive - ? bathymetryRasterContextActive - ? `${Number(selectedDataset?.source_metadata?.analysis_resolution_m ?? 0.5).toLocaleString('nl-BE')} m raster · mDNG · ${String(selectedDataset?.source_metadata?.survey_period ?? '2019-2022')}` - : regionalBathymetryContextActive - ? `${bathymetryProfileCount.toLocaleString('nl-BE')} profielen · ${regionalBathymetryPartitions.length} gemeenten` - : `${bathymetryProfileCount.toLocaleString('nl-BE')} profielen` - : mapFeatureCollection - ? `${mapFeatureCount.toLocaleString('nl-BE')} objecten` - : workspaceDataLoading - ? 'Kaart wordt voorbereid' - : viewportVectorLayerActive - ? 'Kaartlaag gekozen' - : activeWorkspace === 'map' && selectedDataset?.dataset_type === 'raster' - ? 'Rasterlaag actief' - : 'Geen actieve laag' +// De werkbank wordt pas opgehaald wanneer iemand is aangemeld. Daardoor betaalt +// het inlogscherm niet voor de kaartmotor en de paneelopmaak. +const WorkbenchApp = lazy(() => import('./WorkbenchApp')) +function Voorbereiden({ tekst }: { tekst: string }): JSX.Element { return ( -
- { - window.requestAnimationFrame(() => workbenchMainRef.current?.focus({ preventScroll: true })) - }} - > - Ga naar de werkruimte - -

{workspaceStatusMessage}

- {errorMessage && activeWorkspace !== 'map' ? ( -
-
- ) : null} - -
- - -
-
-
- - GeoIntel -
-
-
- Werkruimte - {projectContextLabel} -
-
- Gebied - {areaContextLabel} -
-
- Bron - {datasetContextLabel} -
-
- Kaartlaag - {layerContextLabel} -
-
-
-
- - {username || isGuest ? ( -
- {isGuest ?
- ) : null} -
- - {secondaryDisplay.error ? ( - - ) : null} - - {isGuest ? ( -
-
- ) : null} - - {isGuest && (!guestDemoReady || loadingDemoWorkflow) ? ( -
-
- ) : null} - -
- -
- {activeWorkspace !== 'map' ?
-
-

{selectedProject?.region ?? 'Belgie en Belgische Noordzee'}

-

{activeWorkspaceItem.label}

-
- -
-

{activeWorkspaceItem.description}

- {activeWorkspace !== 'overview' ? ( - - ) : null} -
-
: null} - {activeWorkspace === 'overview' ? ( - - ) : null} - - {activeWorkspace === 'data' ? ( -
- - {isGuest ? ( -
-
- ) : <> - - - - - setInspectorOpen(true)} - onRefreshMetadata={refreshMetadata} - onOpenDatasetInMap={openDatasetInMap} - onOpenDatasetExport={openDatasetExport} - /> - } -
- ) : null} - - - - {activeWorkspace === 'analysis' ? ( -
- loadQualityChecks()} - onOpenEvidenceMap={openQualityEvidenceOnMap} - evidenceLoading={qualityEvidenceLoading} - evidenceError={qualityEvidenceError} - onOpenMapWorkspace={() => setActiveWorkspace('map')} - onOpenAnalysisWorkspace={() => setActiveWorkspace('ai')} - /> -
- - Historische vectorlagen vergelijken - Geavanceerd - - -
-
- ) : null} - - {activeWorkspace === 'assistant' ? ( -
- -
- ) : null} - - {activeWorkspace === 'ai' ? ( -
- 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} - /> - -
- - Segmentatie van beeldvlakken - Nog niet geconfigureerd - - loadSegmentationRuns()} - onSelectRun={setSelectedSegmentationRunId} - onSetClassFilter={setSegmentationClassFilter} - onSetMinConfidenceFilter={setSegmentationMinConfidenceFilter} - onLoadResults={() => loadSegmentationResults()} - onSelectReferenceDataset={setSegmentationReferenceDatasetId} - onRunQa={runSegmentationQa} - /> -
-
- ) : null} - - {activeWorkspace === 'exports' ? ( -
- loadExports()} - onExportDataset={exportSelectedDatasetGeoJson} - onExportDetectionRun={exportSelectedDetectionRunGeoJson} - onExportSegmentationRun={exportSelectedSegmentationRunGeoJson} - onExportProjectMetadata={exportProjectMetadata} - onExportProjectReport={exportProjectReport} - onPreviewContent={previewExportContent} - onDownload={downloadExportArtifact} - /> - -
- ) : null} - - {activeWorkspace === 'system' ? ( - setActiveWorkspace('data')} - onOpenStatus={() => setActiveWorkspace('overview')} - onOpenMap={() => setActiveWorkspace('map')} - /> - ) : null} -
- - {inspectorOpen ? ( - - ) : null} -
-
-
+
+
) } @@ -1458,11 +18,7 @@ function App(): JSX.Element { const { session, sessionError, loggingOut, handleAuthenticated, handleLogout } = useOperatorSession() if (session === null) { - return ( -
-
-
- ) + return } if (!session.authenticated) { @@ -1476,12 +32,14 @@ function App(): JSX.Element { } return ( - + }> + + ) } diff --git a/frontend/src/WorkbenchApp.tsx b/frontend/src/WorkbenchApp.tsx new file mode 100644 index 00000000..7f953679 --- /dev/null +++ b/frontend/src/WorkbenchApp.tsx @@ -0,0 +1,1512 @@ +/** + * De werkbank, apart van App. + * + * Dit stond in App.tsx, waardoor de kaartmotor (maplibre, 213 kB over de lijn) + * en alle paneelopmaak in de eerste bundel zaten en dus door iedereen werden + * gedownload op het inlogscherm, waar geen kaart staat. Als eigen module wordt + * het een aparte brok die pas na aanmelden geladen wordt. + * + * Om dezelfde reden staan de stylesheets van de werkbank hier en niet in + * main.tsx: ook die horen bij deze brok. + */ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { CircleAlert, LogOut, MonitorUp, Moon, ShieldCheck, Sun, UserRound } from 'lucide-react' +import './styles/app.css' +import './styles/premium.css' +import './styles/atlas-workbench.css' +import './styles/atlas-premium-v2.css' +import './styles/professionalization.css' +// Laatst geladen en leidend: het geconsolideerde designsysteem. +// Zie de kop van geointel-system.css voor de reden. +import './styles/geointel-system.css' +import { 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 { WorkspaceErrorBoundary } from './components/shell/WorkspaceErrorBoundary' +import { useSecondaryDisplay } from './components/shell/SecondaryDisplay' +import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow' +import { useDemoWorkflow } from './hooks/useDemoWorkflow' +import { useCoverageResolver } from './hooks/useCoverageResolver' +import { useDetectionWorkflow } from './hooks/useDetectionWorkflow' +import { useDatasetWorkflow } from './hooks/useDatasetWorkflow' +import { useExportWorkflow } from './hooks/useExportWorkflow' +import { useMapSelectionDataset } from './hooks/useMapSelectionDataset' +import { useMapSelectionQa } from './hooks/useMapSelectionQa' +import { useMapWorkspaceState } from './hooks/useMapWorkspaceState' +import { useOperatorSession } from './hooks/useOperatorSession' +import { useMapSelectionExtract } from './hooks/useMapSelectionExtract' +import { useMapOrthophotoAnalysis } from './hooks/useMapOrthophotoAnalysis' +import { isDetectionImageryDataset, isMapRasterDataset } from './lib/datasetCapabilities' +import { useProviderCapabilities } from './hooks/useProviderCapabilities' +import { useProjectWorkspace } from './hooks/useProjectWorkspace' +import { useQualityWorkflow } from './hooks/useQualityWorkflow' +import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow' +import { useSourceFreshness } from './hooks/useSourceFreshness' +import { useWorkbenchBootstrap } from './hooks/useWorkbenchBootstrap' +import { useWorkbenchTheme } from './hooks/useWorkbenchTheme' +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([ + 'overview', + 'data', + 'map', + 'assistant', + 'analysis', + 'ai', + 'exports', +]) +const guestWorkspaceGroups: WorkspaceNavigationGroup[] = [ + { label: 'Verkennen', keys: ['map', 'data'] }, + { label: 'Vragen', keys: ['assistant'] }, + { label: 'Analyseren', keys: ['analysis', 'ai'] }, + { label: 'Afronden', keys: ['exports'] }, + { label: 'Demo', keys: ['overview'] }, +] + +interface WorkbenchAppProps { + username: string | null + accessMode: 'open' | 'operator' | 'guest' + loggingOut: boolean + onLogout: () => void +} + +function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchAppProps): JSX.Element { + const isGuest = accessMode === 'guest' + const [activeWorkspace, setActiveWorkspace] = useState('map') + const [inspectorOpen, setInspectorOpen] = useState(false) + const [guestDemoReady, setGuestDemoReady] = useState(!isGuest) + const secondaryDisplay = useSecondaryDisplay() + const { theme, toggleTheme } = useWorkbenchTheme() + // Welke onderdelen niet geladen konden worden. Zonder dit zag de gebruiker + // een leeg paneel, ongeacht of er niets is of de verbinding wegviel. + const [laadfouten, setLaadfouten] = useState([]) + const meldLaadfout = useCallback((onderdeel: string) => { + setLaadfouten((huidig) => (huidig.includes(onderdeel) ? huidig : [...huidig, onderdeel])) + }, []) + 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(null) + const [mapContextLayerLabel, setMapContextLayerLabel] = useState(null) + const workbenchMainRef = useRef(null) + const previousWorkspaceRef = useRef(activeWorkspace) + // De werkbank draait donker, de landingspagina licht. Zie de sectie + // "Operationeel donker" in geointel-system.css. + useEffect(() => { + document.body.classList.add('workbench-body') + return () => document.body.classList.remove('workbench-body') + }, []) + 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 + const processingCode = metadata['processing_code'] + if (processingCode === 'RASTER_PROCESSING_UNAVAILABLE') { + return String(metadata['processing_error'] ?? 'Raster processing unavailable.') + } + return null + }, [selectedDataset]) + + const availableVectorTargets = useMemo( + () => datasets.filter((item) => item.id !== selectedDatasetId && isVectorDatasetType(item.dataset_type)), + [datasets, selectedDatasetId], + ) + const availableVectorDatasets = useMemo(() => datasets.filter((item) => isVectorDatasetType(item.dataset_type)), [datasets]) + const availableMapDatasets = useMemo( + () => { + const vectors = datasets.filter((dataset) => isVectorDatasetType(dataset.dataset_type) && dataset.status === 'ready') + const rasters = datasets.filter(isMapRasterDataset) + return [...vectors, ...rasters] + }, + [datasets], + ) + const referenceDatasets = useMemo( + () => availableVectorDatasets.filter((item) => item.dataset_role === 'reference'), + [availableVectorDatasets], + ) + const candidateDatasets = useMemo( + () => availableVectorDatasets.filter((item) => item.dataset_role !== 'reference'), + [availableVectorDatasets], + ) + const rasterDatasets = useMemo(() => datasets.filter((item) => item.dataset_type === 'raster'), [datasets]) + const detectionRasterDatasets = useMemo( + () => rasterDatasets.filter(isDetectionImageryDataset), + [rasterDatasets], + ) + const { + providers, + loadingCapabilities, + capabilitiesError, + loadCapabilities, + } = useProviderCapabilities() + const { + changeSourceDatasetId, + changeTargetDatasetId, + changeIouThreshold, + changeIncludeUnchanged, + runningChangeDetection, + changeDetectionResult, + changeDetectionError, + runChangeDetection, + setChangeSourceDatasetId, + setChangeTargetDatasetId, + setChangeIouThreshold, + setChangeIncludeUnchanged, + } = useChangeDetectionWorkflow({ + selectedProjectId, + availableVectorDatasets, + loadDatasetJobs, + // Bind the comparison to whatever the operator has selected on the map. + getSelection: () => ({ bbox: mapSelectionBbox ?? null, areaId: selectedMapAreaId ?? null }), + }) + const { + qaCandidateDatasetId, + qaReferenceDatasetId, + qaAreaId, + qaIouThreshold, + qaRunning, + qaResult, + qaError, + qualityChecks, + qualityChecksError, + qualityEvidenceGeoJson, + qualityEvidenceLoading, + qualityEvidenceError, + loadQualityChecks, + loadQualityEvidenceGeoJson, + clearQualityEvidenceGeoJson, + runQaComparison, + setQaCandidateDatasetId, + setQaReferenceDatasetId, + setQaAreaId, + setQaIouThreshold, + } = useQualityWorkflow({ + selectedProjectId, + loadProjectData, + }) + const { + detectionModels, + modelAssets, + loadingDetectionModels, + detectionModelError, + modelAssetError, + selectedDetectionDatasetId, + selectedDetectionModelId, + selectedModelAssetId, + detectionTileManifestPath, + detectionConfidenceThreshold, + runningDetection, + detectionRunResult, + detectionRunError, + detectionRuns, + selectedDetectionRunId, + detectionItems, + detectionTotal, + detectionTruncated, + 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(() => meldLaadfout('modelcontrole')) + setActiveWorkspace('ai') + } + const { + segmentationModels, + loadingSegmentationModels, + segmentationModelError, + selectedSegmentationDatasetId, + selectedSegmentationModelId, + selectedSegmentationModel, + segmentationTileManifestPath, + segmentationConfidenceThreshold, + runningSegmentation, + segmentationRunResult, + segmentationRunError, + segmentationRuns, + selectedSegmentationRunId, + segmentationItems, + segmentationGeoJson, + segmentationClassFilter, + segmentationMinConfidenceFilter, + loadingSegmentationResults, + segmentationReferenceDatasetId, + segmentationQaResult, + segmentationQaError, + runningSegmentationQa, + loadSegmentationModels, + loadSegmentationRuns, + loadSegmentationResults, + runSegmentation, + runSegmentationQa, + resetSegmentationForProject, + setSelectedSegmentationDatasetId, + setSelectedSegmentationModelId, + setSegmentationTileManifestPath, + setSegmentationConfidenceThreshold, + setSelectedSegmentationRunId, + setSegmentationClassFilter, + setSegmentationMinConfidenceFilter, + setSegmentationReferenceDatasetId, + } = useSegmentationWorkflow({ + selectedProjectId, + rasterDatasets, + qaIouThreshold, + loadProjectData, + loadQualityChecks, + }) + const viewportVectorLayer = useViewportVectorLayer({ + selectedProjectId, + selectedDataset, + featureCount: selectedDatasetSummary?.feature_count ?? selectedDataset?.feature_count ?? null, + isVectorDatasetType, + }) + const analysisMapLayerAvailable = Boolean(changeDetectionResult?.geojson || segmentationGeoJson || detectionGeoJson) + const analysisMapLayerActive = mapContentMode === 'analysis' && analysisMapLayerAvailable + const viewportVectorLayerActive = viewportVectorLayer.enabled && !analysisMapLayerActive + const datasetMapContent = viewportVectorLayer.enabled ? viewportVectorLayer.data : datasetContent + const useRasterTileManifestForSegmentation = () => { + const manifestPath = latestRasterTileManifestPath.trim() + if (!manifestPath || !selectedDataset || selectedDataset.dataset_type !== 'raster') { + return + } + setSegmentationTileManifestPath(manifestPath) + setSelectedSegmentationDatasetId(selectedDataset.id) + setActiveWorkspace('ai') + } + const { + exports, + latestExport, + exportError, + loadingExports, + exporting, + selectionExporting, + selectionExportError, + latestSelectionExport, + exportPreview, + loadExports, + exportSelectedDatasetGeoJson, + exportSelectedDetectionRunGeoJson, + exportSelectedSegmentationRunGeoJson, + exportMapSelectionGeoJson, + persistMapResult, + exportProjectMetadata, + exportProjectReport, + previewExportContent, + downloadExportArtifact, + resetExportsForProject, + } = useExportWorkflow({ + selectedProjectId, + selectedDataset, + selectedDetectionRunId, + selectedSegmentationRunId, + isVectorDatasetType, + }) + const { + mapLayerVisible, + mapLayerOpacity, + selectedMapAreaId, + areaLayerVisible, + areaLayerOpacity, + selectedMapFeature, + areaFeatureCollection, + mapFeatureCollection, + mapLayerLabel, + mapFeatureCount, + areaFeatureCount, + setMapLayerVisible, + setMapLayerOpacity, + setSelectedMapAreaId, + setAreaLayerVisible, + setAreaLayerOpacity, + setSelectedMapFeature, + } = useMapWorkspaceState({ + areas, + changeDetectionGeoJson: analysisMapLayerActive ? changeDetectionResult?.geojson ?? null : null, + segmentationGeoJson: analysisMapLayerActive ? segmentationGeoJson : null, + detectionGeoJson: analysisMapLayerActive ? detectionGeoJson : null, + datasetContent: datasetMapContent, + selectedDataset, + datasetLayerActive: Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type)), + }) + const { + mapSelectionBbox, + mapSelectionResult, + mapSelectionLoading, + mapSelectionError, + runMapSelectionExtract, + resetMapSelectionExtract, + setMapSelectionBbox, + } = useMapSelectionExtract({ + selectedProjectId, + selectedDataset, + isVectorDatasetType, + }) + const { + coverage: mapCoverage, + loadingCoverage: mapCoverageLoading, + coverageError: mapCoverageError, + coverageDurationMs: mapCoverageDurationMs, + coverageBudgetExceeded: mapCoverageBudgetExceeded, + } = useCoverageResolver({ + projectId: selectedProjectId, + bbox: mapSelectionBbox, + }) + const { + selectionDatasetSaving, + selectionDatasetError, + latestSelectionDataset, + deriveMapSelectionDataset, + } = useMapSelectionDataset({ + selectedProjectId, + selectedDataset, + isVectorDatasetType, + loadProjectData, + loadDatasetDetails, + setMapLayerVisible, + }) + const { + selectedMapQaReferenceDatasetId, + mapSelectionQaRunning, + mapSelectionQaError, + mapSelectionQaResult, + latestMapSelectionQualityCheckId, + runMapSelectionQa, + setSelectedMapQaReferenceDatasetId, + } = useMapSelectionQa({ + selectedProjectId, + latestSelectionDataset, + loadQualityChecks, + loadProjectData, + }) + const mapOrthophotoAnalysis = useMapOrthophotoAnalysis({ + selectedProjectId, + selectedAreaId: areas.find((area) => /vervoerregio|operationele grens/i.test(area.name))?.id ?? selectedMapAreaId, + selectionBbox: mapSelectionBbox, + datasets, + loadProjectData, + prepareAndRunDetection: (datasetId) => prepareAndRunDetection(datasetId, 'yolo-configured'), + compareDetectionRunWithReference: (analysisRunId, referenceDatasetId, iouThreshold) => + compareDetectionRunWithReference(analysisRunId, referenceDatasetId, false, iouThreshold), + loadQualityEvidence: (qualityCheckId) => loadQualityEvidenceGeoJson(qualityCheckId), + onAnalysisReady: () => { + setMapContentMode('analysis') + setMapLayerVisible(true) + setActiveWorkspace('map') + }, + }) + const openMapSelectionQualityEvidence = () => { + setActiveWorkspace('analysis') + } + const openQualityEvidenceOnMap = async (qualityCheckId: string) => { + const result = await loadQualityEvidenceGeoJson(qualityCheckId) + if (result) { + setMapLayerVisible(true) + setActiveWorkspace('map') + } + } + const { + loadingDemoWorkflow, + demoWorkflowMessage, + loadDemoWorkflow, + } = useDemoWorkflow({ + restrictedMode: false, + loadProjects, + loadProjectData, + loadDatasetDetails, + loadDetectionRuns, + loadSegmentationRuns, + loadQualityChecks, + loadExports, + setSelectedProjectId: selectProject, + setSelectedDatasetId, + setSelectedMapAreaId, + setQaCandidateDatasetId, + setQaReferenceDatasetId, + setQaAreaId, + setSelectedDetectionDatasetId, + setDetectionReferenceDatasetId, + setSelectedSegmentationDatasetId, + setSegmentationReferenceDatasetId, + setErrorMessage, + }) + + useEffect(() => { + if (!isGuest || guestDemoStartedRef.current) return + guestDemoStartedRef.current = true + setGuestDemoReady(false) + void loadDemoWorkflow().finally(() => setGuestDemoReady(true)) + }, [isGuest, loadDemoWorkflow]) + + useWorkbenchBootstrap({ + restrictedMode: false, + activeWorkspace, + onLoadError: meldLaadfout, + selectedProjectId, + selectedDetectionRunId, + detectionClassFilter, + detectionMinConfidenceFilter, + selectedSegmentationRunId, + segmentationClassFilter, + segmentationMinConfidenceFilter, + loadProjects, + loadCapabilities, + loadDetectionModels, + loadSegmentationModels, + loadProjectData, + loadDetectionRuns, + loadSegmentationRuns, + loadQualityChecks, + loadExports, + loadDetectionResults, + loadSegmentationResults, + resetProjectData, + resetDatasetForProject, + resetDetectionForProject, + resetSegmentationForProject, + resetExportsForProject, + }) + + const selectedArea = areas.find((area) => area.id === selectedMapAreaId) ?? null + const activeWorkspaceItem = visibleWorkspaceItems.find((item) => item.key === activeWorkspace) ?? visibleWorkspaceItems[0] + const mapLayerSourceLabel = useMemo(() => { + if (analysisMapLayerActive && changeDetectionResult?.geojson) { + return 'Veranderingsanalyse' + } + if (analysisMapLayerActive && segmentationGeoJson) { + return 'Segmentatierun' + } + if (analysisMapLayerActive && detectionGeoJson) { + return 'Detectierun' + } + if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) { + return selectedDataset.dataset_type === 'raster' ? 'Rasterdatabron' : 'Vectordatabron' + } + return 'Geen actieve gegevens- of analyselaag' + }, [analysisMapLayerActive, changeDetectionResult?.geojson, datasetMapContent, detectionGeoJson, segmentationGeoJson, selectedDataset, viewportVectorLayer.enabled]) + const mapLayerProvenance = useMemo(() => { + if (analysisMapLayerActive && changeDetectionResult?.geojson) { + return 'Vergelijking van twee bewaarde kaartlagen' + } + if (analysisMapLayerActive && segmentationGeoJson) { + return selectedSegmentationRunId ? 'Bewaarde segmentatieronde' : 'Segmentatieresultaten geladen' + } + if (analysisMapLayerActive && detectionGeoJson) { + return selectedDetectionRunId ? 'Bewaarde detectieronde' : 'Detectieresultaten geladen' + } + if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) { + return `Bewaarde ${selectedDataset.dataset_role === 'reference' ? 'referentielaag' : 'databron'}` + } + return 'Open een databron, detectieronde, segmentatieronde of veranderingsresultaat om het hier te tekenen.' + }, [ + analysisMapLayerActive, + changeDetectionResult?.geojson, + changeSourceDatasetId, + changeTargetDatasetId, + datasetMapContent, + detectionGeoJson, + segmentationGeoJson, + selectedDataset, + selectedDetectionRunId, + selectedSegmentationRunId, + viewportVectorLayer.enabled, + ]) + const openDatasetInMap = (dataset: DatasetCreateResponse) => { + if (selectedProjectId) { + loadDatasetDetails(selectedProjectId, dataset) + } + setMapContentMode('dataset') + setMapLayerVisible(true) + setActiveWorkspace('map') + } + const runGuidedDetection = async () => { + const completed = await prepareAndRunDetection() + if (completed) { + setMapContentMode('analysis') + setMapLayerVisible(true) + setActiveWorkspace('map') + } + } + const openDetectionResultsOnMap = () => { + if (!detectionGeoJson) { + return + } + setMapContentMode('analysis') + setMapLayerVisible(true) + setActiveWorkspace('map') + } + const openDatasetExport = (dataset: DatasetCreateResponse) => { + if (selectedProjectId) { + loadDatasetDetails(selectedProjectId, dataset) + } + setActiveWorkspace('exports') + } + const openWorkflowGuidanceStep = (target: WorkspaceKey) => { + if (isGuest && !guestWorkspaceKeys.has(target)) { + setActiveWorkspace('map') + return + } + if (target === 'map' && availableMapDatasets.length > 0 && !mapFeatureCollection) { + openDatasetInMap(availableMapDatasets[0]) + return + } + if (target === 'exports' && availableMapDatasets.length > 0 && !selectedDataset) { + openDatasetExport(availableMapDatasets[0]) + return + } + setActiveWorkspace(target) + } + const hasAnalysisOutput = + qualityChecks.length > 0 + || Boolean(changeDetectionResult) + || detectionItems.length > 0 + || segmentationItems.length > 0 + const projectContextLabel = selectedProject?.name === NATIONAL_WORKSPACE_PROJECT_NAME + ? NATIONAL_WORKSPACE_LABEL + : selectedProject?.name === REGIONAL_WORKSPACE_PROJECT_NAME + ? REGIONAL_WORKSPACE_LABEL + : selectedProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME + ? FLANDERS_WORKSPACE_LABEL + : selectedProject?.name ?? 'Geen werkruimte' + const areaContextLabel = selectedArea?.name ?? (areas.length > 0 ? 'Kies een gebied' : 'Geen gebied') + const bathymetryProfileContextActive = Boolean( + activeWorkspace === 'map' + && selectedDataset?.source_name === 'vmm_vha_bathymetry_profiles', + ) + const bathymetryRasterContextActive = Boolean( + activeWorkspace === 'map' + && selectedDataset?.source_name === 'spw_bathymetry', + ) + const bathymetryContextActive = bathymetryProfileContextActive || bathymetryRasterContextActive + const regionalBathymetryContextActive = Boolean( + bathymetryProfileContextActive + && selectedDataset?.source_metadata?.regional_partitions_complete === true + && selectedArea + && !/^Gemeente\s/i.test(selectedArea.name), + ) + const regionalBathymetryManifest = String( + selectedDataset?.source_metadata?.partition_manifest_sha256 ?? '', + ) + const regionalBathymetryPartitions = regionalBathymetryContextActive + ? availableMapDatasets.filter( + (dataset) => + dataset.source_name === 'vmm_vha_bathymetry_profiles' + && dataset.source_metadata?.regional_partitions_complete === true + && String(dataset.source_metadata?.partition_manifest_sha256 ?? '') === regionalBathymetryManifest, + ) + : [] + const regionalBathymetryProfileCount = regionalBathymetryPartitions.reduce( + (total, dataset) => total + (dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0), + 0, + ) + const bathymetryProfileCount = regionalBathymetryContextActive + ? regionalBathymetryProfileCount + : selectedDataset?.feature_count ?? selectedDataset?.vector_summary?.feature_count ?? 0 + const workspaceDataLoading = loadingProjects || loadingAreas || loadingDatasets + const workspaceStatusMessage = errorMessage + ? `Werkruimtefout: ${errorMessage}` + : workspaceDataLoading + ? 'Werkruimte en databronnen worden geladen.' + : selectedProject + ? `${projectContextLabel} is geladen met ${datasets.length} databronnen en ${areas.length} gebieden.` + : 'Geen werkruimte geselecteerd.' + const datasetContextLabel = activeWorkspace === 'map' && mapContextSourceLabel + ? mapContextSourceLabel + : analysisMapLayerActive && mapFeatureCollection + ? `${mapLayerLabel} · controle vereist` + : selectedDataset + ? getDatasetDisplayName(selectedDataset) + : workspaceDataLoading + ? 'Bronnen worden geladen' + : datasets.length > 0 + ? 'Kies een bron' + : 'Geen bron' + const layerContextLabel = activeWorkspace === 'map' && mapContextLayerLabel + ? mapContextLayerLabel + : bathymetryContextActive + ? bathymetryRasterContextActive + ? `${Number(selectedDataset?.source_metadata?.analysis_resolution_m ?? 0.5).toLocaleString('nl-BE')} m raster · mDNG · ${String(selectedDataset?.source_metadata?.survey_period ?? '2019-2022')}` + : regionalBathymetryContextActive + ? `${bathymetryProfileCount.toLocaleString('nl-BE')} profielen · ${regionalBathymetryPartitions.length} gemeenten` + : `${bathymetryProfileCount.toLocaleString('nl-BE')} profielen` + : mapFeatureCollection + ? `${mapFeatureCount.toLocaleString('nl-BE')} objecten` + : workspaceDataLoading + ? 'Kaart wordt voorbereid' + : viewportVectorLayerActive + ? 'Kaartlaag gekozen' + : activeWorkspace === 'map' && selectedDataset?.dataset_type === 'raster' + ? 'Rasterlaag actief' + : 'Geen actieve laag' + + return ( +
+ { + window.requestAnimationFrame(() => workbenchMainRef.current?.focus({ preventScroll: true })) + }} + > + Ga naar de werkruimte + + {/* De werkbank begon bij h2, dus schermlezers kregen geen paginatitel en + geen ankerpunt om vanaf te navigeren. Deze kop noemt waar je bent en + verandert mee met het werkblad. */} +

GeoIntel — {activeWorkspaceItem.label}

+

{workspaceStatusMessage}

+ {laadfouten.length > 0 ? ( +
+
+ ) : null} + {errorMessage && activeWorkspace !== 'map' ? ( +
+
+ ) : null} + +
+ + +
+
+
+ + GeoIntel +
+
+
+ Werkruimte + {projectContextLabel} +
+
+ Gebied + {areaContextLabel} +
+
+ Bron + {datasetContextLabel} +
+
+ Kaartlaag + {layerContextLabel} +
+
+
+
+ + + {username || isGuest ? ( +
+ {isGuest ?
+ ) : null} +
+ + {secondaryDisplay.error ? ( + + ) : null} + + {isGuest ? ( +
+
+ ) : null} + + {isGuest && (!guestDemoReady || loadingDemoWorkflow) ? ( +
+
+ ) : null} + +
+ + {/* Eén werkblad dat gooit mag niet de hele werkbank meenemen. */} + +
+ {activeWorkspace !== 'map' ?
+
+

{selectedProject?.region ?? 'Belgie en Belgische Noordzee'}

+

{activeWorkspaceItem.label}

+
+ +
+

{activeWorkspaceItem.description}

+ {activeWorkspace !== 'overview' ? ( + + ) : null} +
+
: null} + {activeWorkspace === 'overview' ? ( + + ) : null} + + {activeWorkspace === 'data' ? ( +
+ + {isGuest ? ( +
+
+ ) : <> + + + + + setInspectorOpen(true)} + onRefreshMetadata={refreshMetadata} + onOpenDatasetInMap={openDatasetInMap} + onOpenDatasetExport={openDatasetExport} + /> + } +
+ ) : null} + + + + {activeWorkspace === 'analysis' ? ( +
+ loadQualityChecks()} + onOpenEvidenceMap={openQualityEvidenceOnMap} + evidenceLoading={qualityEvidenceLoading} + evidenceError={qualityEvidenceError} + onOpenMapWorkspace={() => setActiveWorkspace('map')} + onOpenAnalysisWorkspace={() => setActiveWorkspace('ai')} + /> +
+ + Historische vectorlagen vergelijken + Geavanceerd + + +
+
+ ) : null} + + {activeWorkspace === 'assistant' ? ( +
+ +
+ ) : null} + + {activeWorkspace === 'ai' ? ( +
+ 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} + /> + +
+ + Segmentatie van beeldvlakken + Nog niet geconfigureerd + + loadSegmentationRuns()} + onSelectRun={setSelectedSegmentationRunId} + onSetClassFilter={setSegmentationClassFilter} + onSetMinConfidenceFilter={setSegmentationMinConfidenceFilter} + onLoadResults={() => loadSegmentationResults()} + onSelectReferenceDataset={setSegmentationReferenceDatasetId} + onRunQa={runSegmentationQa} + /> +
+
+ ) : null} + + {activeWorkspace === 'exports' ? ( +
+ loadExports()} + onExportDataset={exportSelectedDatasetGeoJson} + onExportDetectionRun={exportSelectedDetectionRunGeoJson} + onExportSegmentationRun={exportSelectedSegmentationRunGeoJson} + onExportProjectMetadata={exportProjectMetadata} + onExportProjectReport={exportProjectReport} + onPreviewContent={previewExportContent} + onDownload={downloadExportArtifact} + /> + +
+ ) : null} + + {activeWorkspace === 'system' ? ( + setActiveWorkspace('data')} + onOpenStatus={() => setActiveWorkspace('overview')} + onOpenMap={() => setActiveWorkspace('map')} + /> + ) : null} +
+
+ + {inspectorOpen ? ( + + ) : null} +
+
+
+
+ ) +} + +export default WorkbenchApp diff --git a/frontend/src/components/GeoMap.tsx b/frontend/src/components/GeoMap.tsx index 867064ef..3712be7c 100644 --- a/frontend/src/components/GeoMap.tsx +++ b/frontend/src/components/GeoMap.tsx @@ -5,6 +5,7 @@ import 'maplibre-gl/dist/maplibre-gl.css' import { NATIONAL_MAP_CENTER, NATIONAL_MAP_ZOOM } from '../config/primaryFocus' import { featureCollectionBounds } from '../lib/geojsonBounds' import type { MapImageOverlay, MapViewportState, VectorSelectionBBox } from '../types' +import { basemapGround, basemapPaint, huidigeWerkstand, mapSymbology } from './map/mapSymbology' interface GeoMapProps { data: GeoJSON.FeatureCollection | null @@ -46,10 +47,23 @@ const DEFAULT_ROAD_BASEMAP_STYLE: maplibregl.StyleSpecification = { }, }, layers: [ + { + // Grondtoon onder de tegels. Zonder deze laag flitst er wit tussen + // tegels die nog niet geladen zijn. + id: 'basemap-ground', + type: 'background', + paint: { 'background-color': basemapGround(huidigeWerkstand()) }, + }, { id: 'osm-standard', type: 'raster', source: 'osm-standard', + // De tegel wordt ontkleurd en gedempt tot een operationele ondergrond, + // zodat alleen de eigen data nog kleur draagt. Geen andere tegelbron en + // geen sleutel nodig; wie een echte vectorstijl heeft zet die via + // VITE_MAP_STYLE_URL en omzeilt dit blok volledig. Zie basemapPaint voor + // het verschil tussen de twee werkstanden. + paint: basemapPaint(huidigeWerkstand()), }, ], } @@ -58,18 +72,18 @@ function datasetFillColor(fallbackColor: string): ExpressionSpecification { return [ 'case', ['==', ['get', 'layer_type'], 'municipality_boundary'], - '#0f766e', + mapSymbology.boundary, [ 'match', ['get', 'change_type'], 'added', - '#16a34a', + mapSymbology.added, 'removed', - '#dc2626', + mapSymbology.removed, 'modified', - '#d97706', + mapSymbology.modified, 'unchanged', - '#2563eb', + mapSymbology.unchanged, fallbackColor, ], ] @@ -79,18 +93,18 @@ function datasetLineColor(fallbackColor: string): ExpressionSpecification { return [ 'case', ['==', ['get', 'layer_type'], 'municipality_boundary'], - '#0f5f59', + mapSymbology.boundaryStrong, [ 'match', ['get', 'change_type'], 'added', - '#15803d', + mapSymbology.added, 'removed', - '#b91c1c', + mapSymbology.removed, 'modified', - '#b45309', + mapSymbology.modified, 'unchanged', - '#1d4ed8', + mapSymbology.unchanged, fallbackColor, ], ] @@ -144,8 +158,8 @@ function bboxToFeatureCollection( function GeoMap({ data, - dataFillColor = '#f97316', - dataLineColor = '#ea580c', + dataFillColor = mapSymbology.dataFill, + dataLineColor = mapSymbology.dataLine, areaData = null, selectedFeature = null, selectionData = null, @@ -236,6 +250,27 @@ function GeoMap({ } }, [bboxSelectionMode]) + // De kaart wordt eenmalig opgebouwd, dus bij het wisselen van werkstand moet + // alleen de verf van de ondergrond mee. setPaintProperty laat alle datalagen + // ongemoeid; een volledige setStyle zou ze opnieuw moeten opbouwen. + useEffect(() => { + const pasAan = () => { + const map = mapRef.current + if (!map || !map.isStyleLoaded()) return + const werkstand = huidigeWerkstand() + if (!map.getLayer('osm-standard')) return + for (const [naam, waarde] of Object.entries(basemapPaint(werkstand))) { + map.setPaintProperty('osm-standard', naam as never, waarde as never) + } + if (map.getLayer('basemap-ground')) { + map.setPaintProperty('basemap-ground', 'background-color', basemapGround(werkstand)) + } + } + const waarnemer = new MutationObserver(pasAan) + waarnemer.observe(document.body, { attributes: true, attributeFilter: ['data-theme'] }) + return () => waarnemer.disconnect() + }, []) + useEffect(() => { if (!containerRef.current || mapRef.current) { return @@ -491,7 +526,7 @@ function GeoMap({ type: 'fill', source: 'area', paint: { - 'fill-color': '#0f766e', + 'fill-color': mapSymbology.boundary, 'fill-opacity': 0.18, }, }, @@ -503,7 +538,7 @@ function GeoMap({ type: 'line', source: 'area', paint: { - 'line-color': '#0f766e', + 'line-color': mapSymbology.boundary, 'line-width': 3, 'line-dasharray': [2, 1], }, @@ -576,7 +611,7 @@ function GeoMap({ source: 'selected-feature', filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon'], true, false], paint: { - 'fill-color': '#fde047', + 'fill-color': mapSymbology.selectionFill, 'fill-opacity': 0.32, }, }) @@ -586,7 +621,7 @@ function GeoMap({ source: 'selected-feature', filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false], paint: { - 'line-color': '#854d0e', + 'line-color': mapSymbology.selectionLine, 'line-width': 4, }, }) @@ -596,9 +631,9 @@ function GeoMap({ source: 'selected-feature', filter: ['match', ['geometry-type'], ['Point', 'MultiPoint'], true, false], paint: { - 'circle-color': '#fde047', + 'circle-color': mapSymbology.selectionFill, 'circle-radius': 7, - 'circle-stroke-color': '#854d0e', + 'circle-stroke-color': mapSymbology.selectionLine, 'circle-stroke-width': 2, }, }) @@ -620,7 +655,7 @@ function GeoMap({ type: 'fill', source: 'selection-bbox', paint: { - 'fill-color': '#38bdf8', + 'fill-color': mapSymbology.waterFill, 'fill-opacity': 0.12, }, }) @@ -629,7 +664,7 @@ function GeoMap({ type: 'line', source: 'selection-bbox', paint: { - 'line-color': '#0369a1', + 'line-color': mapSymbology.waterLine, 'line-width': 2, 'line-dasharray': [2, 1], }, @@ -656,7 +691,7 @@ function GeoMap({ source: 'selection-result', filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon'], true, false], paint: { - 'fill-color': '#7c3aed', + 'fill-color': mapSymbology.detectionFill, 'fill-opacity': 0.24, }, }) @@ -666,7 +701,7 @@ function GeoMap({ source: 'selection-result', filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'], true, false], paint: { - 'line-color': '#5b21b6', + 'line-color': mapSymbology.detectionLine, 'line-width': 3, }, }) @@ -676,9 +711,9 @@ function GeoMap({ source: 'selection-result', filter: ['match', ['geometry-type'], ['Point', 'MultiPoint'], true, false], paint: { - 'circle-color': '#7c3aed', + 'circle-color': mapSymbology.detectionFill, 'circle-radius': 6, - 'circle-stroke-color': '#ffffff', + 'circle-stroke-color': mapSymbology.pointStroke, 'circle-stroke-width': 2, }, }) @@ -701,14 +736,14 @@ function GeoMap({ 'match', ['get', 'qa_evidence_role'], 'match_candidate', - '#2563eb', + mapSymbology.unchanged, 'match_reference', - '#0f766e', + mapSymbology.boundary, 'false_positive', - '#dc2626', + mapSymbology.removed, 'false_negative', - '#d97706', - '#475569', + mapSymbology.modified, + mapSymbology.fallback, ] as ExpressionSpecification map.addLayer({ id: 'qa-evidence-fill', @@ -746,7 +781,7 @@ function GeoMap({ paint: { 'circle-color': evidenceColor, 'circle-radius': 7, - 'circle-stroke-color': '#ffffff', + 'circle-stroke-color': mapSymbology.pointStroke, 'circle-stroke-width': 2, }, }) @@ -762,4 +797,8 @@ function GeoMap({ ) } +// Geen React.memo hier. Het is geprobeerd en het scheelde niets: van de +// negentien props worden er te veel per render opnieuw gemaakt, dus de +// vergelijking slaat nooit over. Zinvol wordt dat pas wanneer die props +// gestabiliseerd zijn; tot die tijd is het schijnzekerheid. export default GeoMap diff --git a/frontend/src/components/auth/LandingProjectStory.tsx b/frontend/src/components/auth/LandingProjectStory.tsx index ecdf6bed..7fd5bd87 100644 --- a/frontend/src/components/auth/LandingProjectStory.tsx +++ b/frontend/src/components/auth/LandingProjectStory.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { BadgeCheck, Database, Download, MapPinned, ScanSearch } from 'lucide-react' import '../../styles/landing-project-story.css' +import { useDeferredBackground } from '../../hooks/useDeferredBackground' const stages = [ { @@ -46,6 +47,7 @@ const stages = [ ] as const export function LandingProjectStory(): JSX.Element { + const kaartRef = useDeferredBackground() const [activeIndex, setActiveIndex] = useState(0) const activeStage = stages[activeIndex] @@ -104,7 +106,7 @@ export function LandingProjectStory(): JSX.Element {