Improve operational GIS map workflow
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-04 23:29:33 +02:00
parent 531c4e4e1d
commit 21374ed187
10 changed files with 246 additions and 4 deletions
+4
View File
@@ -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.
+1
View File
@@ -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}
+33 -1
View File
@@ -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)
})
@@ -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 (
<section className="map-workspace-shell" data-testid="map-workspace">
<div className="panel-title-row">
@@ -419,6 +438,22 @@ export function MapWorkspace({
<div className="map-control-surface" aria-label="Map workspace controls">
<div className="map-toolbar">
<label>
Database layer
<select
value={selectedMapDatasetId}
onChange={(event) => openSelectedDatabaseLayer(event.target.value)}
disabled={availableMapDatasets.length === 0}
data-testid="map-database-layer-select"
>
<option value="">Select persisted vector layer</option>
{availableMapDatasets.map((dataset) => (
<option key={dataset.id} value={dataset.id}>
{dataset.name}
</option>
))}
</select>
</label>
<label>
Area
<select
@@ -483,6 +518,7 @@ export function MapWorkspace({
</div>
<div className="map-status">
<strong>{mapLayerLabel}</strong>
<span>{selectedMapDataset ? `DB layer: ${selectedMapDataset.name}` : 'No database layer selected'}</span>
<span>{areaFeatureCollection ? `${areaFeatureCount} AOI loaded` : 'No AOI loaded'}</span>
<span>{mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector/result layer loaded'}</span>
</div>
@@ -572,6 +608,63 @@ export function MapWorkspace({
</div>
<div className="map-inspection-surface">
<div className="gis-test-run-surface" aria-label="Operational GIS test run">
<div className="panel-title-row">
<div>
<p className="eyebrow">Operational GIS run</p>
<h3>Database selection test</h3>
</div>
<span className="count-pill">{mapSelectionResult ? `${mapSelectionResult.feature_count} hits` : 'ready'}</span>
</div>
<p className="muted">
Select a persisted vector layer, then query stored vector_features from PostGIS with the AOI or layer extent.
</p>
<div className="gis-test-run-grid">
<div>
<span>Database layer</span>
<strong>{selectedMapDataset?.name ?? 'Select a layer'}</strong>
</div>
<div>
<span>AOI bbox</span>
<strong>{selectedAreaBbox ? 'available' : 'missing'}</strong>
</div>
<div>
<span>Layer bbox</span>
<strong>{activeLayerBbox ? 'available' : 'missing'}</strong>
</div>
<div>
<span>Result</span>
<strong>{mapSelectionResult ? `${mapSelectionResult.feature_count} features` : 'not run'}</strong>
</div>
</div>
<div className="feature-extract-actions">
<button
className="primary-action"
disabled={!selectedMapDataset || !mapFeatureCollection || (!selectedAreaBbox && !activeLayerBbox) || mapSelectionLoading}
type="button"
onClick={runQuickAoiExtract}
>
{mapSelectionLoading ? 'Running test...' : 'Run AOI/layer query'}
</button>
<button
className="secondary-action"
disabled={!selectedAreaBbox}
type="button"
onClick={() => setSelectionBbox(selectedAreaBbox)}
>
Use AOI extent
</button>
<button
className="secondary-action"
disabled={!activeLayerBbox}
type="button"
onClick={() => setSelectionBbox(activeLayerBbox)}
>
Use layer extent
</button>
</div>
{mapSelectionError ? <p className="error">{mapSelectionError}</p> : null}
</div>
<div className="bbox-select-surface" aria-label="Area selection and extract">
<div className="panel-title-row">
<div>
+44 -2
View File
@@ -4658,7 +4658,7 @@ section {
}
.map-workspace-shell .map-toolbar {
grid-template-columns: minmax(11rem, 1.1fr) repeat(2, minmax(9rem, 1fr)) minmax(11rem, 1.1fr);
grid-template-columns: minmax(13rem, 1.25fr) minmax(10rem, 1fr) repeat(2, minmax(8rem, 0.85fr)) minmax(11rem, 1fr);
gap: 0.5rem;
}
@@ -4714,12 +4714,13 @@ section {
.map-inspection-surface {
order: 6;
display: grid;
grid-template-columns: minmax(18rem, 1fr) minmax(18rem, 1fr);
grid-template-columns: minmax(18rem, 0.85fr) minmax(18rem, 1fr) minmax(18rem, 1fr);
gap: 0.65rem;
padding: 0.62rem;
}
.bbox-select-surface,
.gis-test-run-surface,
.feature-extract-surface,
.map-inspection-surface .feature-inspector {
margin: 0;
@@ -4727,6 +4728,47 @@ section {
padding: 0.62rem;
}
.gis-test-run-surface {
display: grid;
gap: 0.58rem;
border: 1px solid rgba(15, 118, 110, 0.24);
border-left: 3px solid var(--accent);
background: #f8fbf9;
}
.gis-test-run-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.42rem;
}
.gis-test-run-grid > div {
min-width: 0;
border: 1px solid var(--line);
border-radius: 5px;
padding: 0.42rem 0.48rem;
background: #ffffff;
}
.gis-test-run-grid span {
display: block;
color: var(--muted);
font-size: 0.6rem;
font-weight: 800;
letter-spacing: 0.055em;
text-transform: uppercase;
}
.gis-test-run-grid strong {
display: block;
overflow: hidden;
margin-top: 0.16rem;
font-size: 0.8rem;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.bbox-select-surface {
border-left-width: 3px;
}