Add guided operational GIS workflow
This commit is contained in:
+3
-1
@@ -10,7 +10,9 @@ Data and Map have an additional usability layout pass for the core daily workflo
|
||||
|
||||
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.
|
||||
When the public OpenStreetMap fallback is active, the Map workspace shows a basemap usage notice. This keeps the local/demo default honest and reminds operators to configure a managed style URL before production or heavier tile traffic.
|
||||
|
||||
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, run the existing persisted `vector_features` bbox query, save the result as a derived dataset, export the selection GeoJSON, choose a reference dataset and launch QA/QC 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.
|
||||
|
||||
|
||||
@@ -308,6 +308,7 @@ export function MapWorkspace({
|
||||
)
|
||||
const selectedFeatureFilename = selectedFeatureStem === 'selected-feature' ? DEFAULT_SELECTED_FEATURE_FILENAME : `${selectedFeatureStem}.geojson`
|
||||
const selectedMapDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null
|
||||
const usesDefaultOsmBasemap = !import.meta.env.VITE_MAP_STYLE_URL
|
||||
|
||||
useEffect(() => {
|
||||
setBboxInput(bboxToInputState(mapSelectionBbox))
|
||||
@@ -437,6 +438,12 @@ export function MapWorkspace({
|
||||
</div>
|
||||
|
||||
<div className="map-control-surface" aria-label="Map workspace controls">
|
||||
{usesDefaultOsmBasemap ? (
|
||||
<div className="basemap-policy-notice" aria-label="Basemap usage notice">
|
||||
<strong>Local/demo basemap</strong>
|
||||
<span>Public OpenStreetMap tiles are active. Configure VITE_MAP_STYLE_URL for production or heavier use.</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="map-toolbar">
|
||||
<label>
|
||||
Database layer
|
||||
@@ -664,6 +671,92 @@ export function MapWorkspace({
|
||||
</button>
|
||||
</div>
|
||||
{mapSelectionError ? <p className="error">{mapSelectionError}</p> : null}
|
||||
<div className="guided-gis-flow" aria-label="Guided operational GIS workflow">
|
||||
<div className="guided-gis-steps">
|
||||
<div className={selectedMapDataset ? 'complete' : ''}>
|
||||
<span>1</span>
|
||||
<strong>Layer</strong>
|
||||
<small>{selectedMapDataset ? selectedMapDataset.name : 'Select database layer'}</small>
|
||||
</div>
|
||||
<div className={currentSelectionBbox ? 'complete' : ''}>
|
||||
<span>2</span>
|
||||
<strong>Extent</strong>
|
||||
<small>{currentSelectionBbox ? 'AOI/layer bbox ready' : 'Use AOI or layer extent'}</small>
|
||||
</div>
|
||||
<div className={mapSelectionResult ? 'complete' : ''}>
|
||||
<span>3</span>
|
||||
<strong>Query</strong>
|
||||
<small>{mapSelectionResult ? `${mapSelectionResult.feature_count} persisted features` : 'Run PostGIS selection'}</small>
|
||||
</div>
|
||||
<div className={latestSelectionDatasetName ? 'complete' : ''}>
|
||||
<span>4</span>
|
||||
<strong>Dataset</strong>
|
||||
<small>{latestSelectionDatasetName ?? 'Save query result'}</small>
|
||||
</div>
|
||||
<div className={mapSelectionQaResult ? 'complete' : ''}>
|
||||
<span>5</span>
|
||||
<strong>QA/QC</strong>
|
||||
<small>{mapSelectionQaResult ? `F1 ${mapSelectionQaResult.f1_score ?? 'n/a'}` : 'Compare with reference'}</small>
|
||||
</div>
|
||||
<div className={latestSelectionExportPath ? 'complete' : ''}>
|
||||
<span>6</span>
|
||||
<strong>Export</strong>
|
||||
<small>{latestSelectionExportPath ? 'GeoJSON artifact ready' : 'Save handoff artifact'}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="guided-gis-actions">
|
||||
<button
|
||||
className="secondary-action"
|
||||
disabled={!mapSelectionResult || !currentSelectionBbox || selectionDatasetSaving}
|
||||
type="button"
|
||||
onClick={saveAreaSelectionDataset}
|
||||
>
|
||||
{selectionDatasetSaving ? 'Saving dataset...' : 'Save result dataset'}
|
||||
</button>
|
||||
<button
|
||||
className="secondary-action"
|
||||
disabled={!mapSelectionResult || !currentSelectionBbox || selectionExporting}
|
||||
type="button"
|
||||
onClick={saveAreaSelectionExport}
|
||||
>
|
||||
{selectionExporting ? 'Saving export...' : 'Save GeoJSON export'}
|
||||
</button>
|
||||
<label>
|
||||
Reference
|
||||
<select
|
||||
value={selectedMapQaReferenceDatasetId}
|
||||
onChange={(event) => onSelectMapQaReferenceDataset(event.target.value)}
|
||||
disabled={!latestSelectionDatasetName || mapQaReferenceDatasets.length === 0 || mapSelectionQaRunning}
|
||||
>
|
||||
<option value="">Select reference</option>
|
||||
{mapQaReferenceDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
className="primary-action"
|
||||
disabled={!latestSelectionDatasetName || !selectedMapQaReferenceDatasetId || mapSelectionQaRunning}
|
||||
type="button"
|
||||
onClick={onRunMapSelectionQa}
|
||||
>
|
||||
{mapSelectionQaRunning ? 'Running QA...' : 'Run QA/QC'}
|
||||
</button>
|
||||
<button
|
||||
className="secondary-action"
|
||||
disabled={!latestMapSelectionQualityCheckId}
|
||||
type="button"
|
||||
onClick={onOpenMapSelectionQualityEvidence}
|
||||
>
|
||||
Open evidence
|
||||
</button>
|
||||
</div>
|
||||
{selectionDatasetError ? <p className="error">{selectionDatasetError}</p> : null}
|
||||
{selectionExportError ? <p className="error">{selectionExportError}</p> : null}
|
||||
{mapSelectionQaError ? <p className="error">{mapSelectionQaError}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bbox-select-surface" aria-label="Area selection and extract">
|
||||
<div className="panel-title-row">
|
||||
|
||||
+109
-2
@@ -4769,6 +4769,110 @@ section {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.basemap-policy-notice {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.28rem 0.55rem;
|
||||
align-items: center;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 6px;
|
||||
padding: 0.46rem 0.58rem;
|
||||
background: #fffbeb;
|
||||
color: #713f12;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.basemap-policy-notice strong {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.045em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.basemap-policy-notice span {
|
||||
color: #854d0e;
|
||||
}
|
||||
|
||||
.guided-gis-flow {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 0.54rem;
|
||||
}
|
||||
|
||||
.guided-gis-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.38rem;
|
||||
}
|
||||
|
||||
.guided-gis-steps > div {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.08rem 0.38rem;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.44rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.guided-gis-steps > div.complete {
|
||||
border-color: rgba(15, 118, 110, 0.26);
|
||||
background: #f0fdfa;
|
||||
}
|
||||
|
||||
.guided-gis-steps span {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
grid-row: span 2;
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
border-radius: 999px;
|
||||
background: var(--panel-soft);
|
||||
color: var(--muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.guided-gis-steps .complete span {
|
||||
background: var(--accent);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.guided-gis-steps strong,
|
||||
.guided-gis-steps small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.guided-gis-steps strong {
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.guided-gis-steps small {
|
||||
color: var(--muted);
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.guided-gis-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.38rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.guided-gis-actions label {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.guided-gis-actions button {
|
||||
min-height: 2.15rem;
|
||||
padding: 0.38rem 0.55rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.bbox-select-surface {
|
||||
border-left-width: 3px;
|
||||
}
|
||||
@@ -4829,7 +4933,8 @@ section {
|
||||
.dataset-action-grid,
|
||||
.map-context-summary,
|
||||
.map-workspace-shell .map-toolbar,
|
||||
.layer-provenance-rail {
|
||||
.layer-provenance-rail,
|
||||
.guided-gis-steps {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -4842,7 +4947,9 @@ section {
|
||||
.map-context-summary,
|
||||
.map-workspace-shell .map-toolbar,
|
||||
.layer-provenance-rail,
|
||||
.bbox-select-grid {
|
||||
.bbox-select-grid,
|
||||
.guided-gis-steps,
|
||||
.guided-gis-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user