From 21374ed187a8565b9170e58e479c840254394c42 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 23:29:33 +0200 Subject: [PATCH] Improve operational GIS map workflow --- CHANGELOG.md | 7 ++ ..._sprint116_operational_gis_map_workflow.py | 29 ++++++ docs/CODEX_EXECUTION_LOG.md | 27 ++++++ docs/ENVIRONMENT_SPEC.md | 8 +- docs/TODO.md | 1 + frontend/README.md | 4 + frontend/src/App.tsx | 1 + frontend/src/components/GeoMap.tsx | 34 ++++++- frontend/src/components/map/MapWorkspace.tsx | 93 +++++++++++++++++++ frontend/src/styles/app.css | 46 ++++++++- 10 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 backend/tests/test_sprint116_operational_gis_map_workflow.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 09e6705b..147c4420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ # Changelog +## Sprint 116 Operational GIS map workflow (2026-07-04) + +- Switched the default MapLibre basemap from demo tiles to an OpenStreetMap road raster basemap with visible attribution while keeping `VITE_MAP_STYLE_URL` as the production override. +- Added a persisted database layer selector to the Map workspace so users can directly load a ready vector dataset from stored project data. +- Added an Operational GIS run panel that reuses AOI or active layer extents to query persisted PostGIS `vector_features` through the existing bbox selection flow. +- Added static regression coverage for the road basemap, attribution, database layer selector and persisted selection run wiring. + ## Sprint 115 QA/QC and Exports usability layout pass (2026-07-04) - Made the QA/QC workspace calmer by compacting summary, handoff, drilldown, feature evidence and metric history surfaces. diff --git a/backend/tests/test_sprint116_operational_gis_map_workflow.py b/backend/tests/test_sprint116_operational_gis_map_workflow.py new file mode 100644 index 00000000..3f44173a --- /dev/null +++ b/backend/tests/test_sprint116_operational_gis_map_workflow.py @@ -0,0 +1,29 @@ +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_map_uses_road_basemap_with_attribution_and_env_override() -> None: + geo_map = (REPO_ROOT / "frontend/src/components/GeoMap.tsx").read_text(encoding="utf-8") + + assert "DEFAULT_ROAD_BASEMAP_STYLE" in geo_map + assert "https://tile.openstreetmap.org/{z}/{x}/{y}.png" in geo_map + assert "OpenStreetMap contributors" in geo_map + assert "VITE_MAP_STYLE_URL" in geo_map + assert "AttributionControl" in geo_map + + +def test_map_workspace_can_select_persisted_database_layer_and_run_query() -> None: + map_workspace = (REPO_ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8") + app_shell = (REPO_ROOT / "frontend/src/App.tsx").read_text(encoding="utf-8") + styles = (REPO_ROOT / "frontend/src/styles/app.css").read_text(encoding="utf-8") + + assert "map-database-layer-select" in map_workspace + assert "Select persisted vector layer" in map_workspace + assert "Operational GIS run" in map_workspace + assert "vector_features" in map_workspace + assert "Run AOI/layer query" in map_workspace + assert "selectedMapDatasetId=" in app_shell + assert ".gis-test-run-surface" in styles + assert ".gis-test-run-grid" in styles diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 3742101b..a19ddd0e 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -1,3 +1,30 @@ +## Sprint 116 Operational GIS map workflow (2026-07-04) + +Changed: +- Switched the default MapLibre basemap to an OpenStreetMap road raster style with visible attribution while preserving `VITE_MAP_STYLE_URL` as the override for managed/production map styles. +- Added a persisted database layer selector to the Map workspace so ready vector datasets can be opened directly from stored project data. +- Added an Operational GIS run panel that reuses the selected AOI bbox or active layer bbox and calls the existing persisted `vector_features` bbox selection workflow. +- Updated `CHANGELOG.md`, `docs/TODO.md`, `docs/ENVIRONMENT_SPEC.md` and `frontend/README.md`. +- Added regression coverage in `backend/tests/test_sprint116_operational_gis_map_workflow.py`. + +Validation: +- `cd frontend && npm run typecheck` passed. +- `python -m pytest backend\tests\test_sprint116_operational_gis_map_workflow.py backend\tests\test_sprint85_map_workspace_density.py backend\tests\test_sprint106_map_bbox_extract.py backend\tests\test_sprint107_map_selection_export.py backend\tests\test_sprint108_map_selection_derived_dataset.py -q` passed: 18 tests. +- `cd frontend && npm run build` passed. +- `python -m compileall backend/app` passed. +- `cd backend && python -m pytest -q` passed: 364 tests. +- `bash scripts/run_readiness_check.sh` passed: 364 backend tests plus frontend typecheck/build. +- `cd backend && python -m alembic heads` passed: `202606120900 (head)`. +- `cd backend && python -m alembic upgrade head --sql` passed. +- `bash -n scripts/live_migration_smoke.sh` passed. + +Limitations: +- The default OpenStreetMap tile service is appropriate for local V1 testing and demos, but production or heavier deployments should set `VITE_MAP_STYLE_URL` to a managed tile/style provider. +- This pass does not add live GRB/OSM fetching, new API routes, new migrations or fake data. + +Next recommended pass: +- Run a browser-facing map workflow smoke against the deployed app, then add a guided persisted-reference QA test run if the operational flow needs fewer manual steps. + ## Sprint 115 QA/QC and Exports usability layout pass (2026-07-04) Changed: diff --git a/docs/ENVIRONMENT_SPEC.md b/docs/ENVIRONMENT_SPEC.md index e96694ed..262a904b 100644 --- a/docs/ENVIRONMENT_SPEC.md +++ b/docs/ENVIRONMENT_SPEC.md @@ -31,7 +31,7 @@ OSM_OVERPASS_URL=https://overpass-api.de/api/interpreter ```env VITE_API_BASE_URL= VITE_API_PROXY_TARGET=http://localhost:8000 -VITE_MAP_STYLE_URL=https://demotiles.maplibre.org/style.json +VITE_MAP_STYLE_URL= ``` `VITE_API_BASE_URL` is intentionally empty by default so the browser calls the @@ -39,6 +39,12 @@ same origin as the frontend. In Docker Compose, nginx serves the built frontend and reverse proxies `/api` and `/health` to the backend container. For local Vite development, `VITE_API_PROXY_TARGET` can point to the local backend. +If `VITE_MAP_STYLE_URL` is empty, the frontend uses the built-in OpenStreetMap +road raster basemap with visible attribution. This is suitable for local V1 +testing and demos. For production, commercial use or heavier traffic, set +`VITE_MAP_STYLE_URL` to a managed MapLibre style URL from an appropriate tile +provider instead of relying on the public OpenStreetMap tile service. + ## Docker compose services - `db`: PostGIS image. diff --git a/docs/TODO.md b/docs/TODO.md index ea63d067..1e7c86d8 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -76,6 +76,7 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add Data workspace selected-summary and panel density polish. - [x] Add Map workspace panel hierarchy and layer-control density polish. - [x] Add selected map feature extraction with highlight, property table, copy and GeoJSON download. +- [x] Add operational GIS map workflow with road basemap, persisted database layer selection and AOI/layer `vector_features` query run. - [x] Add QA/QC workspace result hierarchy and filter density polish. - [x] Add Change Detection panel hierarchy and analysis workspace density polish. - [x] Add calm workbench layout pass to reduce duplicate navigation, heavy card styling and shell density. diff --git a/frontend/README.md b/frontend/README.md index d4390b77..d69c6899 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -8,6 +8,10 @@ The shell has a calmer V1 workbench density pass: the duplicated workspace comma Data and Map have an additional usability layout pass for the core daily workflow. The Data workspace uses compact catalog cards, shorter action buttons and tighter role summaries. The Map workspace prioritizes the MapLibre frame before dense controls, gives the map more height on desktop and compresses context/provenance/bbox extraction surfaces while preserving the existing selection, export and QA actions. +The Map workspace defaults to an OpenStreetMap road basemap with visible attribution so uploaded vectors, AOIs and QA overlays appear on a real street context. Set `VITE_MAP_STYLE_URL` to a managed MapLibre style URL to override this for production or high-volume deployments. + +Operational GIS testing is now available directly in the Map workspace. Users can choose a persisted vector database layer, load it on the map, reuse the selected AOI or active layer extent, and run the existing persisted `vector_features` bbox query without creating fake data or a parallel backend path. + 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 4be4f057..cb977df1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -868,6 +868,7 @@ function App(): JSX.Element { mapSelectionQaResult={mapSelectionQaResult} latestMapSelectionQualityCheckId={latestMapSelectionQualityCheckId} availableMapDatasets={availableMapDatasets} + selectedMapDatasetId={selectedDataset && isVectorDatasetType(selectedDataset.dataset_type) ? selectedDataset.id : ''} selectedFeature={selectedMapFeature} onSelectMapArea={setSelectedMapAreaId} onOpenDatasetInMap={openDatasetInMap} diff --git a/frontend/src/components/GeoMap.tsx b/frontend/src/components/GeoMap.tsx index 4b971a7c..9245c7d1 100644 --- a/frontend/src/components/GeoMap.tsx +++ b/frontend/src/components/GeoMap.tsx @@ -23,6 +23,30 @@ const EMPTY_FEATURE_COLLECTION: GeoJSON.FeatureCollection = { features: [], } +const DEFAULT_ROAD_BASEMAP_STYLE: maplibregl.StyleSpecification = { + version: 8, + sources: { + 'osm-standard': { + type: 'raster', + tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], + tileSize: 256, + attribution: '© OpenStreetMap contributors', + maxzoom: 19, + }, + }, + layers: [ + { + id: 'osm-standard', + type: 'raster', + source: 'osm-standard', + }, + ], +} + +function defaultMapStyle(): string | maplibregl.StyleSpecification { + return import.meta.env.VITE_MAP_STYLE_URL || DEFAULT_ROAD_BASEMAP_STYLE +} + function collectCoordinates(featureCollection: GeoJSON.FeatureCollection): maplibregl.LngLatBoundsLike | null { const coordinates: [number, number][] = [] const walk = (coords: unknown) => { @@ -137,11 +161,19 @@ function GeoMap({ const map = new maplibregl.Map({ container: containerRef.current, - style: import.meta.env.VITE_MAP_STYLE_URL || 'https://demotiles.maplibre.org/style.json', + style: defaultMapStyle(), center: [5.3, 51.3], zoom: 9, + attributionControl: false, }) map.addControl(new maplibregl.NavigationControl(), 'top-right') + map.addControl( + new maplibregl.AttributionControl({ + compact: true, + customAttribution: 'Basemap © OpenStreetMap contributors', + }), + 'bottom-right', + ) map.on('load', () => { setMapStyleReady(true) }) diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index bc895fce..ca7b0cff 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -209,6 +209,7 @@ interface MapWorkspaceProps { mapSelectionQaResult: QaComparisonResult | null latestMapSelectionQualityCheckId: string | null availableMapDatasets: DatasetCreateResponse[] + selectedMapDatasetId: string onSelectMapArea: (areaId: string) => void onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void onSetAreaLayerVisible: (visible: boolean) => void @@ -265,6 +266,7 @@ export function MapWorkspace({ mapSelectionQaResult, latestMapSelectionQualityCheckId, availableMapDatasets, + selectedMapDatasetId, onSelectMapArea, onOpenDatasetInMap, onSetAreaLayerVisible, @@ -305,6 +307,7 @@ export function MapWorkspace({ featureProperties?.['name'] ?? featureProperties?.['id'] ?? featureProperties?.['source_feature_id'] ?? 'selected-feature', ) const selectedFeatureFilename = selectedFeatureStem === 'selected-feature' ? DEFAULT_SELECTED_FEATURE_FILENAME : `${selectedFeatureStem}.geojson` + const selectedMapDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null useEffect(() => { setBboxInput(bboxToInputState(mapSelectionBbox)) @@ -384,6 +387,22 @@ export function MapWorkspace({ onDeriveMapSelectionDataset(bbox) } + const openSelectedDatabaseLayer = (datasetId: string) => { + const dataset = availableMapDatasets.find((item) => item.id === datasetId) + if (dataset) { + onOpenDatasetInMap(dataset) + } + } + + const runQuickAoiExtract = () => { + const bbox = selectedAreaBbox ?? activeLayerBbox + if (!bbox) { + return + } + setSelectionBbox(bbox) + onRunMapSelectionExtract(bbox) + } + return (
@@ -419,6 +438,22 @@ export function MapWorkspace({
+