diff --git a/CHANGELOG.md b/CHANGELOG.md index 51191e45..7601aadb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ # Changelog +## Sprint 183 Mol map source clarity and live AI validation (2026-07-14) + +- Added an explicit Database/Analysis result map-content mode so an automatically loaded detection result can no longer mask a newly selected persisted municipality layer. +- Kept the Map workspace database-first and made dataset selection switch back to the selected PostGIS layer without discarding available detection, segmentation or change-analysis results. +- Restored the premium UI's viewport status row so visible/total feature counts and the 1,000-feature truncation warning remain readable. +- Rebuilt and deployed the all-in-one Tower runtime, then passed PostGIS 3.6 connectivity, schema/index, single-head migration, frontend proxy and browser runtime checks. +- Ran the bounded Mol-center raster -> tiles -> configured YOLO -> persisted Detection -> GRB QA -> export chain against the live runtime: 36 detections, 17 matches, precision 0.4722, recall 0.0455, F1 0.0829 and mean IoU 0.5958. These honest metrics confirm the workflow while keeping the current model below production-quality acceptance. + ## Sprint 182 Municipality viewport delivery and bounded AI handoff (2026-07-14) - Replaced full-file loading for vector datasets above 5,000 features with debounced, zoom-aware requests to the existing persisted PostGIS bbox-selection endpoint. diff --git a/backend/tests/test_sprint183_map_layer_source_mode.py b/backend/tests/test_sprint183_map_layer_source_mode.py new file mode 100644 index 00000000..3264aa1f --- /dev/null +++ b/backend/tests/test_sprint183_map_layer_source_mode.py @@ -0,0 +1,17 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_map_source_mode_keeps_database_layers_distinct_from_analysis_results() -> None: + app = (ROOT / "frontend/src/App.tsx").read_text(encoding="utf-8") + workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8") + + assert "const [mapContentMode, setMapContentMode]" in app + assert "mapContentMode === 'analysis' && analysisMapLayerAvailable" in app + assert "setMapContentMode('dataset')" in app + assert 'aria-label="Map content source"' in workspace + assert "onSetMapContentMode('dataset')" in workspace + assert "onSetMapContentMode('analysis')" in workspace + assert "disabled={!analysisLayerAvailable}" in workspace diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index c866cc37..3d518fb7 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -7510,3 +7510,49 @@ Open: guard, suppressed the incorrect empty-layer state and loaded an actual viewport slice with `2 visible of 36,941 total features`. No browser warnings or errors were recorded. + +# Sprint 183 - Mol map source clarity and live AI validation + +## Runtime evidence + +- Deployed commit `1ba479b` to the all-in-one Tower runtime at + `http://192.168.10.150:1202`. The deployment rebuilt the updated + `postgres:16-bookworm` GIS runtime and installed the configured CPU AI image. +- Live migration smoke passed against PostGIS `3.6` with required runtime + schema objects, geometry indexes and the single Alembic head + `202606120900`. `/health` reported service, backend version and database as + healthy. +- Reused project `d74c1f87-29c0-4c67-adfc-560764f2b80e` and created bounded + area `0a38dd7a-d55a-4453-ae59-db8579ed8170` for + `Mol Centrum - AI analysezone 500m`. +- Persisted raster dataset `e9a292e2-57d2-4a85-9df6-32580ba6ea98` and GRB + reference dataset `bfc83882-2acc-470d-baec-ee828b4428f8` through the + canonical upload and vector persistence paths. +- Ran local model asset + `geointel-building-yolov8s-smallbld-minpx3-img640-ft30-pt` against the + persisted tile manifest. Analysis run + `97c204cc-bc75-4139-aab5-25397dd0d87e` persisted `36` detections. +- Quality check `1edb81ec-6799-41c1-ae25-9145f52407bd` persisted `17` matches, + `19` false positives and `357` false negatives. Persisted metrics were + precision `0.472222`, recall `0.045455`, F1 `0.082927` and mean IoU + `0.595803`; the workflow works, but these figures do not justify production + model acceptance. +- Detection export `d18946c4-b7c5-4262-83ad-6c406c6aa59c` was persisted. + +## Map-source correction + +- Live browser review exposed that the automatically selected detection run + could mask an explicitly selected database layer and leave the header at + `36 features`. Added a Database/Analysis result source mode and made + database selection return to Database mode. +- Local browser verification then selected the complete + `mol_grb_gbg_buildings.geojson` dataset, rendered a real `1,000`-feature + PostGIS viewport slice over the OpenStreetMap road basemap, displayed the + truncation/zoom guidance, switched to the 36-feature detection overlay and + back without state loss, and recorded no console errors. + +## Next pass + +- Clip detection QA reference populations to actual raster/tile coverage and + add box-to-building-footprint matching diagnostics before any further model + training or promotion decision. diff --git a/docs/TODO.md b/docs/TODO.md index 9d628552..f5fc1d18 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -21,6 +21,9 @@ This file now starts with the current implementation status. Older preparation/b - [x] Make Mol the primary workbench, AOI and operator-sample context while preserving broader Kempen coverage. - [x] Add a Mol multi-zone operational pack with independent positive holdouts, background control and persisted map-ready AOIs. - [x] Execute the Mol pack on live Tower/PostGIS with real orthophotos, GRB references, configured YOLO, persisted QA/QC and a zero-building background control. +- [x] Persist and stream the complete official Mol municipality boundary plus all 36,941 clipped GRB buildings through the Map workspace. +- [x] Run a bounded Mol-center raster, tile, configured-YOLO, persisted detection, GRB QA and export workflow on live Tower/PostGIS. +- [x] Separate persisted database layers from available analysis-result overlays with an explicit Map content mode. - [x] Persist combined Mol operator evidence under the Unraid storage mount so reports survive all-in-one container replacement. - [x] Visually review Mol Postel and Donk false-positive/false-negative evidence, classify the dominant error modes and only then decide whether another model-training pass is justified. - [ ] Clip detection QA populations to persisted raster/tile coverage and add box-to-footprint matching diagnostics before reconsidering model training. diff --git a/frontend/README.md b/frontend/README.md index 167dcb0e..0e7f84b9 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -22,6 +22,8 @@ Operational GIS testing is now available directly in the Map workspace. Users ca Large vector layers use viewport delivery instead of downloading one unbounded GeoJSON file. Datasets above 5,000 features load their persisted PostGIS geometries from the existing bbox-selection endpoint at zoom level 14 or closer, with a 1,000-feature cap per view. The Map workspace shows visible versus total feature counts and asks the operator to zoom further when the response is truncated. Small layers, AI result overlays, selections and QA evidence keep their existing complete-FeatureCollection behavior. +The Map workspace keeps database layers and analysis-result overlays explicit. `Map content: Database` is the default and is selected automatically whenever an operator chooses a persisted database layer. `Analysis result` is enabled only when a detection, segmentation or change-result GeoJSON is available. Switching modes changes only the rendered source; it does not delete persisted analysis results or dataset state. + QA/QC and Exports follow the same calmer density model. QA/QC keeps metric evidence, feature ids and raw findings available but compresses provenance and history surfaces so review starts from the selected check and map evidence actions. Exports uses denser handoff cards, latest-artifact cards and history filters so artifact creation and download paths are easier to scan. When project data loads and no dataset is selected yet, the workbench auto-opens the first ready vector dataset. This gives Data, Map and Exports an immediately usable default context while preserving explicit user selection once the user picks another dataset. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e1572791..42ca7c42 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -56,11 +56,11 @@ const workspaceNavGroups: Array<{ label: string; keys: WorkspaceKey[] }> = [ function App(): JSX.Element { const [activeWorkspace, setActiveWorkspace] = useState('overview') const [inspectorOpen, setInspectorOpen] = useState(false) + const [mapContentMode, setMapContentMode] = useState<'dataset' | 'analysis'>('dataset') useEffect(() => { window.scrollTo({ top: 0, left: 0 }) setInspectorOpen(false) }, [activeWorkspace]) - const { projects, selectedProject, @@ -83,6 +83,10 @@ function App(): JSX.Element { setProjectForm, setAreaForm, } = useProjectWorkspace() + const selectProject = (projectId: string) => { + setMapContentMode('dataset') + setSelectedProjectId(projectId) + } const { selectedDatasetId, selectedDataset, @@ -351,7 +355,8 @@ function App(): JSX.Element { featureCount: selectedDatasetSummary?.feature_count ?? selectedDataset?.feature_count ?? null, isVectorDatasetType, }) - const analysisMapLayerActive = Boolean(changeDetectionResult?.geojson || segmentationGeoJson || detectionGeoJson) + 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 = () => { @@ -410,9 +415,9 @@ function App(): JSX.Element { setSelectedMapFeature, } = useMapWorkspaceState({ areas, - changeDetectionGeoJson: changeDetectionResult?.geojson ?? null, - segmentationGeoJson, - detectionGeoJson, + changeDetectionGeoJson: analysisMapLayerActive ? changeDetectionResult?.geojson ?? null : null, + segmentationGeoJson: analysisMapLayerActive ? segmentationGeoJson : null, + detectionGeoJson: analysisMapLayerActive ? detectionGeoJson : null, datasetContent: datasetMapContent, selectedDataset, datasetLayerActive: Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type)), @@ -479,7 +484,7 @@ function App(): JSX.Element { loadSegmentationRuns, loadQualityChecks, loadExports, - setSelectedProjectId, + setSelectedProjectId: selectProject, setSelectedDatasetId, setSelectedMapAreaId, setQaCandidateDatasetId, @@ -521,28 +526,28 @@ function App(): JSX.Element { const selectedArea = areas.find((area) => area.id === selectedMapAreaId) ?? null const activeWorkspaceItem = workspaceNavItems.find((item) => item.key === activeWorkspace) ?? workspaceNavItems[0] const mapLayerSourceLabel = useMemo(() => { - if (changeDetectionResult?.geojson) { + if (analysisMapLayerActive && changeDetectionResult?.geojson) { return 'Change detection' } - if (segmentationGeoJson) { + if (analysisMapLayerActive && segmentationGeoJson) { return 'Segmentation run' } - if (detectionGeoJson) { + if (analysisMapLayerActive && detectionGeoJson) { return 'Detection run' } if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) { return `${selectedDataset.dataset_type} dataset` } return 'No active vector or result layer' - }, [changeDetectionResult?.geojson, datasetMapContent, detectionGeoJson, segmentationGeoJson, selectedDataset, viewportVectorLayer.enabled]) + }, [analysisMapLayerActive, changeDetectionResult?.geojson, datasetMapContent, detectionGeoJson, segmentationGeoJson, selectedDataset, viewportVectorLayer.enabled]) const mapLayerProvenance = useMemo(() => { - if (changeDetectionResult?.geojson) { + if (analysisMapLayerActive && changeDetectionResult?.geojson) { return `source ${changeSourceDatasetId || 'n/a'} -> target ${changeTargetDatasetId || 'n/a'}` } - if (segmentationGeoJson) { + if (analysisMapLayerActive && segmentationGeoJson) { return selectedSegmentationRunId ? `analysis run ${selectedSegmentationRunId}` : 'segmentation results loaded' } - if (detectionGeoJson) { + if (analysisMapLayerActive && detectionGeoJson) { return selectedDetectionRunId ? `analysis run ${selectedDetectionRunId}` : 'detection results loaded' } if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) { @@ -550,6 +555,7 @@ function App(): JSX.Element { } return 'Open a dataset, detection run, segmentation run or change result to draw it here.' }, [ + analysisMapLayerActive, changeDetectionResult?.geojson, changeSourceDatasetId, changeTargetDatasetId, @@ -565,6 +571,7 @@ function App(): JSX.Element { if (selectedProjectId) { loadDatasetDetails(selectedProjectId, dataset) } + setMapContentMode('dataset') setMapLayerVisible(true) setActiveWorkspace('map') } @@ -867,7 +874,7 @@ function App(): JSX.Element { demoWorkflowMessage={demoWorkflowMessage} onCreateProject={createProject} onUpdateProjectForm={setProjectForm} - onSelectProject={setSelectedProjectId} + onSelectProject={selectProject} onLoadDemoWorkflow={loadDemoWorkflow} /> @@ -925,6 +932,8 @@ function App(): JSX.Element { viewportVectorStatus={viewportVectorLayerActive ? viewportVectorLayer.statusMessage : null} viewportVectorTone={viewportVectorLayer.error ? 'error' : viewportVectorLayer.truncated ? 'warning' : viewportVectorLayer.loading || viewportVectorLayer.zoomRequired ? 'pending' : 'ready'} fitMapDataOnChange={!viewportVectorLayerActive} + mapContentMode={analysisMapLayerActive ? 'analysis' : 'dataset'} + analysisLayerAvailable={analysisMapLayerAvailable} selectedMapFeature={selectedMapFeature} mapSelectionBbox={mapSelectionBbox} mapSelectionResult={mapSelectionResult} @@ -952,6 +961,7 @@ function App(): JSX.Element { onSetAreaLayerOpacity={setAreaLayerOpacity} onSetMapLayerVisible={setMapLayerVisible} onSetMapLayerOpacity={setMapLayerOpacity} + onSetMapContentMode={setMapContentMode} onSelectMapFeature={setSelectedMapFeature} onMapViewportChange={viewportVectorLayer.setViewport} onSetMapSelectionBbox={setMapSelectionBbox} diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index 5fd78ca8..106d5c9f 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -193,6 +193,8 @@ interface MapWorkspaceProps { viewportVectorStatus: string | null viewportVectorTone: 'ready' | 'pending' | 'warning' | 'error' fitMapDataOnChange: boolean + mapContentMode: 'dataset' | 'analysis' + analysisLayerAvailable: boolean selectedMapFeature: GeoJSON.Feature | null selectedFeature?: GeoJSON.Feature | null mapSelectionBbox: VectorSelectionBBox | null @@ -220,6 +222,7 @@ interface MapWorkspaceProps { onSetAreaLayerOpacity: (opacity: number) => void onSetMapLayerVisible: (visible: boolean) => void onSetMapLayerOpacity: (opacity: number) => void + onSetMapContentMode: (mode: 'dataset' | 'analysis') => void onSelectMapFeature: (feature: GeoJSON.Feature | null) => void onMapViewportChange: (viewport: MapViewportState) => void onSetMapSelectionBbox: (bbox: VectorSelectionBBox | null) => void @@ -256,6 +259,8 @@ export function MapWorkspace({ viewportVectorStatus, viewportVectorTone, fitMapDataOnChange, + mapContentMode, + analysisLayerAvailable, selectedMapFeature, selectedFeature = selectedMapFeature, mapSelectionBbox, @@ -283,6 +288,7 @@ export function MapWorkspace({ onSetAreaLayerOpacity, onSetMapLayerVisible, onSetMapLayerOpacity, + onSetMapContentMode, onSelectMapFeature, onMapViewportChange, onSetMapSelectionBbox, @@ -508,6 +514,26 @@ export function MapWorkspace({ ) : null}
+
+ Map content +
+ + +
+