Improve map overlay ergonomics
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-18 23:31:00 +02:00
parent 6fa41e11e4
commit 705b53fbfa
8 changed files with 218 additions and 0 deletions
+9
View File
@@ -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.
@@ -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
+26
View File
@@ -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.
+2
View File
@@ -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.
+1
View File
@@ -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
+42
View File
@@ -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}
@@ -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 (
<section data-testid="map-workspace">
<div className="panel-title-row">
@@ -121,6 +132,26 @@ export function MapWorkspace({
<span>{mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector/result layer loaded'}</span>
</div>
</div>
<div className="layer-provenance-rail" aria-label="Active map layer provenance">
<div>
<span>Layer source</span>
<strong>{mapLayerSourceLabel}</strong>
</div>
<div>
<span>Provenance</span>
<strong>{mapLayerProvenance}</strong>
</div>
<div>
<span>Draw state</span>
<strong>{mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No active vector or result layer'}</strong>
</div>
</div>
{!mapFeatureCollection ? (
<div className="empty-state map-empty-state">
<strong>No active vector or result layer</strong>
<p>Open a dataset, detection run, segmentation run or change result to draw it here.</p>
</div>
) : null}
<GeoMap
data={mapFeatureCollection}
areaData={areaFeatureCollection}
@@ -137,6 +168,16 @@ export function MapWorkspace({
</div>
{selectedMapFeature ? (
<>
{featureSummaryEntries.length > 0 ? (
<div className="feature-summary-grid" aria-label="Selected feature summary">
{featureSummaryEntries.map(([key, value]) => (
<div className="feature-property-chip" key={key}>
<span>{key}</span>
<strong>{String(value)}</strong>
</div>
))}
</div>
) : null}
<pre className="job-result">{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}</pre>
</>
) : (
+65
View File
@@ -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,