Improve operational GIS map workflow
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user