From 705b53fbfad4589397b9bd7b5b70dd736bee461f Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 18 Jun 2026 23:31:00 +0200 Subject: [PATCH] Improve map overlay ergonomics --- CHANGELOG.md | 9 +++ .../test_sprint63_map_overlay_ergonomics.py | 32 +++++++++ docs/CODEX_EXECUTION_LOG.md | 26 ++++++++ docs/TODO.md | 2 + frontend/README.md | 1 + frontend/src/App.tsx | 42 ++++++++++++ frontend/src/components/map/MapWorkspace.tsx | 41 ++++++++++++ frontend/src/styles/app.css | 65 +++++++++++++++++++ 8 files changed, 218 insertions(+) create mode 100644 backend/tests/test_sprint63_map_overlay_ergonomics.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a3ccffa..93051b9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ # Changelog +## Sprint 63 Map overlay ergonomics (2026-06-18) + +- Added an active layer provenance rail to the Map workspace, showing layer source, provenance and draw state from existing frontend state. +- Added clear empty guidance when no vector/result layer is active on the map. +- Added scan-friendly selected-feature property chips before the raw JSON inspector. +- Tightened panel title alignment after the visual polish pass exposed a generic CSS selector specificity issue. +- Added static regression coverage for the map provenance and feature-summary UI contracts. +- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed. + ## Sprint 62 Workbench visual polish (2026-06-18) - Added a compact workspace command bar for fast switching between the primary workbench surfaces. diff --git a/backend/tests/test_sprint63_map_overlay_ergonomics.py b/backend/tests/test_sprint63_map_overlay_ergonomics.py new file mode 100644 index 00000000..3df34a28 --- /dev/null +++ b/backend/tests/test_sprint63_map_overlay_ergonomics.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_map_workspace_exposes_layer_provenance_and_feature_summary() -> None: + app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text( + encoding="utf-8" + ) + css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") + + assert "mapLayerSourceLabel" in app + assert "mapLayerProvenance" in app + assert "layer-provenance-rail" in map_workspace + assert "feature-summary-grid" in map_workspace + assert "feature-property-chip" in map_workspace + assert ".layer-provenance-rail" in css + assert ".feature-summary-grid" in css + assert ".feature-property-chip" in css + + +def test_map_workspace_has_clear_empty_result_layer_guidance() -> None: + map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text( + encoding="utf-8" + ) + + assert "No active vector or result layer" in map_workspace + assert "Open a dataset, detection run, segmentation run or change result to draw it here." in map_workspace diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 1dbecc0e..ef870748 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -2434,3 +2434,29 @@ Limitations: Next recommended pass: - Continue with map/result overlay ergonomics, especially making selected dataset/result provenance easier to see beside the map. + +## Sprint 63 Map overlay ergonomics (2026-06-18) + +Changed: +- Added active map layer source/provenance/draw-state context in the Map workspace using existing selected dataset, detection, segmentation and change-detection frontend state. +- Added a clear map empty-state when no vector/result layer is active. +- Added selected-feature property chips before the raw JSON feature inspector so common properties are scan-friendly. +- Tightened panel-title alignment with a scoped CSS override after the broader visual polish exposed a specificity issue in the existing `section > div:not(...)` rule. +- Added `backend/tests/test_sprint63_map_overlay_ergonomics.py`. +- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`. + +Tested: +- Red step: `cd backend && python -m pytest tests/test_sprint63_map_overlay_ergonomics.py -q` failed on missing map provenance/feature-summary UI. +- `cd backend && python -m pytest tests/test_sprint63_map_overlay_ergonomics.py tests/test_sprint30_workbench_components.py tests/test_sprint53_selection_ergonomics.py -q` (`11 passed`) +- `cd frontend && npm run typecheck` +- `cd frontend && npm run build` +- Local Chrome/Playwright check against `http://127.0.0.1:5175` with live API proxy: Map workspace opened, provenance rail present, no console warnings/errors, no horizontal page overflow. + +Open: +- Run full readiness and deploy Tower after this pass. + +Limitations: +- This is a frontend ergonomics pass only. It does not add new map layers, backend endpoints, provider fetching or geospatial processing. + +Next recommended pass: +- Continue with export/report handoff polish or add a live browser smoke that explicitly visits every workspace after deployment. diff --git a/docs/TODO.md b/docs/TODO.md index 6519c172..2213403c 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -62,6 +62,7 @@ This file now starts with the current implementation status. Older preparation/b - [x] Decide next V1 stabilization focus: golden dataset expansion or frontend visual polish backlog. - [x] Expand golden QA/QC benchmark coverage across partial, perfect, no-overlap and MultiPolygon scenarios. - [x] Improve workbench shell visual polish, mobile navigation density and AI result readability. +- [x] Improve Map workspace layer provenance, empty guidance and selected-feature summary readability. ## Sprint 8 status @@ -334,3 +335,4 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add backend error-envelope audit for expected user-error paths. - [x] Expand golden datasets beyond the original single building QA fixture pair. - [x] Add workbench visual polish pass for command bar, panel surfaces, empty states and mobile nav density. +- [x] Add map/result overlay ergonomics for active layer provenance and feature property summaries. diff --git a/frontend/README.md b/frontend/README.md index 3bbf6318..3c6dd942 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -232,6 +232,7 @@ The workbench now uses a task-based shell instead of a single long panel stack. - Existing API calls, hooks, MapLibre rendering and QA/AI/export flows are unchanged. - Stable navigation test anchors use `data-testid="workspace-nav-{workspace}"`. - Mobile workbench navigation uses horizontal rails for the primary nav and command bar, avoiding a tall menu stack before the active workspace content. +- The Map workspace shows active layer source/provenance/draw-state context and selected-feature property chips before the raw JSON inspector. ## Raster dependency visibility diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dbc34ec9..c3ef02e7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -389,6 +389,46 @@ 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) { + return 'Change detection' + } + if (segmentationGeoJson) { + return 'Segmentation run' + } + if (detectionGeoJson) { + return 'Detection run' + } + if (datasetContent && selectedDataset) { + return `${selectedDataset.dataset_type} dataset` + } + return 'No active vector or result layer' + }, [changeDetectionResult?.geojson, datasetContent, detectionGeoJson, segmentationGeoJson, selectedDataset]) + const mapLayerProvenance = useMemo(() => { + if (changeDetectionResult?.geojson) { + return `source ${changeSourceDatasetId || 'n/a'} -> target ${changeTargetDatasetId || 'n/a'}` + } + if (segmentationGeoJson) { + return selectedSegmentationRunId ? `analysis run ${selectedSegmentationRunId}` : 'segmentation results loaded' + } + if (detectionGeoJson) { + return selectedDetectionRunId ? `analysis run ${selectedDetectionRunId}` : 'detection results loaded' + } + if (datasetContent && selectedDataset) { + return `${selectedDataset.dataset_role ?? 'source'} / ${selectedDataset.source_name ?? selectedDataset.source}` + } + return 'Open a dataset, detection run, segmentation run or change result to draw it here.' + }, [ + changeDetectionResult?.geojson, + changeSourceDatasetId, + changeTargetDatasetId, + datasetContent, + detectionGeoJson, + segmentationGeoJson, + selectedDataset, + selectedDetectionRunId, + selectedSegmentationRunId, + ]) const openDatasetInMap = (dataset: DatasetCreateResponse) => { if (selectedProjectId) { loadDatasetDetails(selectedProjectId, dataset) @@ -565,6 +605,8 @@ function App(): JSX.Element { areaFeatureCollection={areaFeatureCollection} mapFeatureCollection={mapFeatureCollection} mapLayerLabel={mapLayerLabel} + mapLayerSourceLabel={mapLayerSourceLabel} + mapLayerProvenance={mapLayerProvenance} mapLayerVisible={mapLayerVisible} mapLayerOpacity={mapLayerOpacity} areaLayerVisible={areaLayerVisible} diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index 2683d1ec..b5757e86 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -7,6 +7,8 @@ interface MapWorkspaceProps { areaFeatureCollection: GeoJSON.FeatureCollection | null mapFeatureCollection: GeoJSON.FeatureCollection | null mapLayerLabel: string + mapLayerSourceLabel: string + mapLayerProvenance: string mapLayerVisible: boolean mapLayerOpacity: number areaLayerVisible: boolean @@ -28,6 +30,8 @@ export function MapWorkspace({ areaFeatureCollection, mapFeatureCollection, mapLayerLabel, + mapLayerSourceLabel, + mapLayerProvenance, mapLayerVisible, mapLayerOpacity, areaLayerVisible, @@ -42,6 +46,13 @@ export function MapWorkspace({ onSetMapLayerOpacity, onSelectMapFeature, }: MapWorkspaceProps): JSX.Element { + const featureProperties = selectedMapFeature?.properties ?? null + const featureSummaryEntries = featureProperties + ? Object.entries(featureProperties) + .filter(([, value]) => value !== null && value !== undefined && typeof value !== 'object') + .slice(0, 6) + : [] + return (
@@ -121,6 +132,26 @@ export function MapWorkspace({ {mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector/result layer loaded'}
+
+
+ Layer source + {mapLayerSourceLabel} +
+
+ Provenance + {mapLayerProvenance} +
+
+ Draw state + {mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No active vector or result layer'} +
+
+ {!mapFeatureCollection ? ( +
+ No active vector or result layer +

Open a dataset, detection run, segmentation run or change result to draw it here.

+
+ ) : null} {selectedMapFeature ? ( <> + {featureSummaryEntries.length > 0 ? ( +
+ {featureSummaryEntries.map(([key, value]) => ( +
+ {key} + {String(value)} +
+ ))} +
+ ) : null}
{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}
) : ( diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index b4cdc8ce..a94314ee 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -797,6 +797,14 @@ section > div:not(.map-controls):not(.feature-inspector):not(.quick-action-grid) gap: 0.55rem; } +.panel-title-row { + display: flex; +} + +section > .panel-title-row { + display: flex !important; +} + section button + button { margin-top: 0.45rem; } @@ -1178,6 +1186,10 @@ button.entity-card { line-height: 1.4; } +.map-empty-state { + margin-bottom: 0.85rem; +} + .export-action-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -1287,6 +1299,58 @@ button.entity-card { font-size: 0.84rem; } +.layer-provenance-rail { + display: grid; + grid-template-columns: 0.85fr minmax(0, 1.5fr) 0.85fr; + gap: 0.65rem; + margin-bottom: 0.85rem; +} + +.layer-provenance-rail > div, +.feature-property-chip { + min-width: 0; + border: 1px solid var(--line); + border-radius: 7px; + padding: 0.58rem 0.68rem; + background: #ffffff; +} + +.layer-provenance-rail span, +.feature-property-chip span { + display: block; + color: var(--muted); + font-size: 0.68rem; + font-weight: 800; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.layer-provenance-rail strong, +.feature-property-chip strong { + display: block; + margin-top: 0.2rem; + overflow: hidden; + font-size: 0.84rem; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +.layer-provenance-rail strong { + display: -webkit-box; + min-height: 2.1rem; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + white-space: normal; +} + +.feature-summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); + gap: 0.55rem; + margin-bottom: 0.75rem; +} + .lab-block + .lab-block { margin-top: 0.85rem; } @@ -1508,6 +1572,7 @@ button.entity-card { .dataset-upload-form, .map-toolbar, + .layer-provenance-rail, .lab-form-grid, .quality-summary-grid, .quality-score-row,