From 70157400d93a097f1f89fba99dddd9bcb75892e2 Mon Sep 17 00:00:00 2001 From: Jens Date: Sun, 23 Aug 2026 15:00:57 +0200 Subject: [PATCH] Breng de opstartkosten terug en geef de werkstand een schakelaar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit De inlogpagina haalde 1,46 MB over de lijn, waaronder de volledige kaartmotor van 213 kB terwijl er geen kaart staat. De werkbank is nu een aparte brok die pas na aanmelden geladen wordt: de eerste JS-bundel gaat van 476 naar 30 kB, en met de beeldwerk uit een eerdere commit komt de inlogpagina op 344 kB. Bij het opstarten gingen 27 verzoeken de deur uit in drie golven. Alles werd vooraf opgehaald, ook voor werkbladen die de gebruiker misschien nooit opent, en twee daarvan werden dubbel opgevraagd door effecten die negentig milliseconde na elkaar liepen. Gelijktijdige en vlak opeenvolgende identieke GET-verzoeken delen nu één belofte, en elk werkblad haalt zijn eigen gegevens pas op wanneer het geopend wordt. Nu zestien verzoeken. Typen in het themazoekveld kostte 53 ms mediaan met uitschieters tot 127 ms, omdat die filterwaarde in het viewmodel van de hele werkruimte leefde. Het veld is een eigen component geworden dat zijn invoer zelf bijhoudt. React.memo om de kaart is geprobeerd en weer verwijderd: van de negentien props worden er te veel per render opnieuw gemaakt, dus de vergelijking sloeg nooit over. Verder: donker blijft de standaard maar er is nu een weg terug naar licht, de kaartondergrond schakelt mee zonder de datalagen te herbouwen, de onderste hero-afbeelding wordt pas opgehaald als hij in beeld komt, en de werkbank heeft een h1 zodat schermlezers een ankerpunt hebben. De tests op de bootstrap legden het oude gedrag vast en toetsen nu het nieuwe, van drie naar zeven gevallen. Co-Authored-By: Claude Opus 5 --- frontend/src/App.tsx | 1474 +--------------- frontend/src/WorkbenchApp.tsx | 1512 +++++++++++++++++ frontend/src/components/GeoMap.tsx | 99 +- .../components/auth/LandingProjectStory.tsx | 4 +- .../src/components/map/MapExplorerView.tsx | 12 +- .../components/map/ThemeSearchField.test.tsx | 54 + .../src/components/map/ThemeSearchField.tsx | 55 + .../src/hooks/useDeferredBackground.test.tsx | 51 + frontend/src/hooks/useDeferredBackground.ts | 45 + .../src/hooks/useWorkbenchBootstrap.test.tsx | 83 +- frontend/src/hooks/useWorkbenchBootstrap.ts | 75 +- frontend/src/hooks/useWorkbenchTheme.test.tsx | 62 + frontend/src/hooks/useWorkbenchTheme.ts | 56 + frontend/src/main.tsx | 10 + frontend/src/services/api/client.test.ts | 99 ++ frontend/src/services/api/client.ts | 43 +- 16 files changed, 2202 insertions(+), 1532 deletions(-) create mode 100644 frontend/src/WorkbenchApp.tsx create mode 100644 frontend/src/components/map/ThemeSearchField.test.tsx create mode 100644 frontend/src/components/map/ThemeSearchField.tsx create mode 100644 frontend/src/hooks/useDeferredBackground.test.tsx create mode 100644 frontend/src/hooks/useDeferredBackground.ts create mode 100644 frontend/src/hooks/useWorkbenchTheme.test.tsx create mode 100644 frontend/src/hooks/useWorkbenchTheme.ts create mode 100644 frontend/src/services/api/client.test.ts 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 {