From 6c1c2b52b6b34176d46b0261badecf31513e9141 Mon Sep 17 00:00:00 2001 From: Jens Date: Sat, 1 Aug 2026 14:05:42 +0200 Subject: [PATCH] Require explicit map analysis selection --- docs/CODEX_EXECUTION_LOG.md | 23 ++ docs/TODO.md | 9 + frontend/src/components/map/MapWorkspace.tsx | 210 +++++++++---------- frontend/src/styles/geointel-system.css | 175 ++++++++++++++++ 4 files changed, 306 insertions(+), 111 deletions(-) diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 936d4047..8f31baa9 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -12166,3 +12166,26 @@ Open: username `ITWorx`; no plaintext password or password hash was printed or changed. - Targeted backend authentication suite passed from the backend root: 8 tests. - `bash -n scripts/configure_operator_login.sh` and `git diff --check` passed. + +## 2026-08-01 - Explicit map analysis selection and result drawer + +### Changed + +- Decoupled AOI drawing from analysis: completing a rectangle now stores only + the bounded selection and clears stale results; it does not acquire or query + any theme, source or AI model. +- Added explicit multi-theme selection with a single `Analyseer selectie` + action. The bounded request planner now receives only the themes chosen by + the user. +- Restored Insights as an explicit open/close drawer over the map, with an + accessible toggle and a truthful pre-analysis empty state. +- Shortened theme availability copy, constrained labels and statuses to their + grid columns, and added a compact selection summary/action block. +- Kept full-work-area selection non-executing; it follows the same choose-then- + analyse contract as a drawn rectangle. + +### Verification before deployment + +- `npm run build` passed TypeScript compilation and the production Vite build. +- `npm run test:unit` passed all 49 tests in 15 files. +- `git diff --check` passed. \ No newline at end of file diff --git a/docs/TODO.md b/docs/TODO.md index 0df56da5..8a6a2434 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1046,3 +1046,12 @@ This file now starts with the current implementation status. Older preparation/b - [x] Herstart vanuit `/mnt/user/appdata/geointel` met behoud van de NVIDIA/AI-image. - [x] Controleer containergezondheid, migraties, frontendproxy en actieve runtimeconfiguratie. - [x] Werk de operatorconfiguratievoorbeelden bij zodat toekomstige configuratie `ITWorx` gebruikt. + +# Sprint 235 - Expliciete kaartanalyse en resultatenlade + +- [x] Laat tekenen uitsluitend het werkgebied vastleggen zonder bronnen of modellen te starten. +- [x] Voeg meervoudige, expliciete themakeuze en één duidelijke analyseknop toe. +- [x] Beperk de analysequery tot uitsluitend de gekozen thema’s. +- [x] Herstel Inzichten als open- en sluitbare lade boven de kaart. +- [x] Verkort bronteksten en voorkom overflow in de themakolom. +- [x] Verifieer TypeScript-build, frontend-unitset en responsive runtime visueel. \ No newline at end of file diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index c8b533fa..3868e654 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react' -import { BoxSelect, MapPinned, SlidersHorizontal, Trash2 } from 'lucide-react' +import { BoxSelect, ChevronLeft, ChevronRight, MapPinned, Play, SlidersHorizontal, Trash2 } from 'lucide-react' import GeoMap from '../GeoMap' import type { AreaRead, CoverageResolveResponse, CoverageStatus, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types' import { useMapThemeSelectionInsights, type MapThemeAcquisition, type MapThemeQuery } from '../../hooks/useMapThemeSelectionInsights' @@ -335,60 +335,6 @@ const DATA_THEME_MAP_STYLES: Record marine_environment: { fill: '#3475a3', line: '#1c557d' }, } -function datasetAvailabilityLabel( - dataset: DatasetCreateResponse, - partitions: DatasetCreateResponse[] = [dataset], -): string { - const partitionCount = partitions.length - const regionalSuffix = partitionCount > 1 ? ` · ${partitionCount} gemeenten` : '' - if (dataset.dataset_type === 'raster' && ['digitaal_vlaanderen_dhmv', 'spw_terrain'].includes(dataset.source_name ?? '')) { - const resolution = Number(dataset.source_metadata?.['analysis_resolution_m']) - return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} hoogtegrid${regionalSuffix}` - } - if (dataset.dataset_type === 'raster' && dataset.source_name === 'vmm_flood_hazard') { - const resolution = Number(dataset.source_metadata?.['analysis_resolution_m']) - return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} overstromingsscenario${regionalSuffix}` - } - if (dataset.dataset_type === 'raster' && dataset.source_name === 'spw_bathymetry') { - const resolution = Number(dataset.source_metadata?.['analysis_resolution_m']) - const period = String(dataset.source_metadata?.['survey_period'] ?? '2019-2022') - return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} waterbodemhoogte · ${period}` - } - if (dataset.source_name === 'vmm_vha_bathymetry_profiles') { - const profiles = partitions.reduce( - (total, item) => total + (item.feature_count ?? item.vector_summary?.feature_count ?? 0), - 0, - ) - const documents = partitions.reduce( - (total, item) => total + Number(item.source_metadata?.['document_count'] ?? 0), - 0, - ) - return `${profiles.toLocaleString('nl-BE')} profielen · ${documents.toLocaleString('nl-BE')} bronbladen${regionalSuffix}` - } - if (dataset.dataset_type === 'raster' && dataset.source_name === 'department_omgeving_thematic_raster') { - const resolution = Number(dataset.source_metadata?.['analysis_resolution_m']) - const year = Number(dataset.source_metadata?.['observation_year']) - const resolutionLabel = Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster' - return `${resolutionLabel} officiële bron${Number.isFinite(year) ? ` · ${year}` : ''}` - } - if (dataset.dataset_type === 'raster' && dataset.source_name === 'spw_walous_land_cover') { - const resolution = Number(dataset.source_metadata?.['analysis_resolution_m']) - const year = Number(dataset.source_metadata?.['observation_year']) - const resolutionLabel = Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster' - return `${resolutionLabel} WALOUS-landbedekking${Number.isFinite(year) ? ` · ${year}` : ''}` - } - if (dataset.source_name === 'ngi_adminvector') { - return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} officiële bestuursgebieden` - } - if (dataset.source_name === 'rbins_msp_2026') { - return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} officiële planobjecten · 2026-2034` - } - if (dataset.source_name === 'rbins_marine_reporting_units') { - return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} officiële zeegebieden` - } - return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten beschikbaar` -} - function datasetSearchText(dataset: DatasetCreateResponse): string { return [ dataset.name, @@ -894,6 +840,8 @@ export function MapWorkspace({ const selectedDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null return themeIdForDataset(selectedDataset) ?? 'buildings' }) + const [selectedThemeIds, setSelectedThemeIds] = useState([]) + const [resultsPanelOpen, setResultsPanelOpen] = useState(false) const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied' const selectedMapArea = areas.find((area) => area.id === selectedMapAreaId) @@ -1364,7 +1312,11 @@ export function MapWorkspace({ const onDemandThemeActive = analysisMode === 'current' && Boolean(activeOnDemandMapProduct) const regionalOnDemandThemeActive = regionalScopeSelected && onDemandThemeActive const activeThemeAvailable = Boolean(activeThemeDataset) || onDemandThemeActive - const coverageSelectionAvailable = analysisMode === 'current' && Boolean(selectedProjectId && selectedMapArea) + + const selectedThemes = useMemo( + () => DATA_THEMES.filter((theme) => selectedThemeIds.includes(theme.id)), + [selectedThemeIds], + ) useEffect(() => { if ( @@ -1763,6 +1715,7 @@ export function MapWorkspace({ setFirstSelectionCorner(null) clearThemeInsights() clearTemporalComparison() + setResultsPanelOpen(false) setBboxSelectionMode(true) } @@ -1775,7 +1728,9 @@ export function MapWorkspace({ setSelectionBbox(bbox) setFirstSelectionCorner(null) setBboxSelectionMode(false) - void analyzeSelection(bbox, areaIdForSelection(bbox)) + clearThemeInsights() + clearTemporalComparison() + setResultsPanelOpen(false) } const runAreaExtract = () => { @@ -1792,6 +1747,7 @@ export function MapWorkspace({ setBboxSelectionMode(false) setFirstSelectionCorner(null) setBboxInput(bboxToInputState(null)) + setResultsPanelOpen(false) clearThemeInsights() clearTemporalComparison() onClearMapSelectionExtract() @@ -1929,7 +1885,13 @@ export function MapWorkspace({ if (!dataset && !onDemandProduct) { return } + setSelectedThemeIds((current) => ( + current.includes(theme.id) + ? current.filter((themeId) => themeId !== theme.id) + : [...current, theme.id] + )) setActiveThemeId(theme.id) + clearThemeInsights() clearTemporalComparison() if (dataset) { onOpenDatasetInMap(dataset) @@ -1938,6 +1900,9 @@ export function MapWorkspace({ const setExplorerMode = (mode: 'current' | 'evolution') => { setAnalysisMode(mode) + setSelectedThemeIds([]) + setResultsPanelOpen(false) + clearThemeInsights() clearTemporalComparison() if (mode !== 'evolution' || activeTemporalSeriesGroups.length > 0) { return @@ -2029,7 +1994,7 @@ export function MapWorkspace({ } const availableThemes: Array> = [] const resultFeatureLimit = selectionFeatureLimit(bbox) - for (const theme of [activeTheme]) { + for (const theme of selectedThemes) { const dataset = themeDatasetMap[theme.id] const partitioned = Boolean( dataset @@ -2078,6 +2043,10 @@ export function MapWorkspace({ } const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => { + if (analysisMode === 'current' && selectedThemes.length === 0) { + return + } + setResultsPanelOpen(true) const requestId = mapAnalysisRequestSequence.current + 1 mapAnalysisRequestSequence.current = requestId const startedAt = Date.now() @@ -2127,7 +2096,10 @@ export function MapWorkspace({ const handleMapBboxSelect = (bbox: VectorSelectionBBox) => { setFirstSelectionCorner(null) setBboxSelectionMode(false) - void analyzeSelection(bbox, areaIdForSelection(bbox)) + clearThemeInsights() + clearTemporalComparison() + setResultsPanelOpen(false) + setSelectionBbox(bbox) } const runQuickAoiExtract = () => { @@ -2304,11 +2276,11 @@ export function MapWorkspace({ ) : null}
-
+ +