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
+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;
}