Add one-click GIS workflow run
This commit is contained in:
@@ -218,12 +218,12 @@ interface MapWorkspaceProps {
|
||||
onSetMapLayerOpacity: (opacity: number) => void
|
||||
onSelectMapFeature: (feature: GeoJSON.Feature | null) => void
|
||||
onSetMapSelectionBbox: (bbox: VectorSelectionBBox | null) => void
|
||||
onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => void
|
||||
onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => Promise<VectorSelectionResponse | null>
|
||||
onClearMapSelectionExtract: () => void
|
||||
onExportMapSelection: (bbox: VectorSelectionBBox) => void
|
||||
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => void
|
||||
onExportMapSelection: (bbox: VectorSelectionBBox) => Promise<unknown>
|
||||
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => Promise<DatasetCreateResponse | null>
|
||||
onSelectMapQaReferenceDataset: (datasetId: string) => void
|
||||
onRunMapSelectionQa: () => void
|
||||
onRunMapSelectionQa: (candidateDataset?: DatasetCreateResponse | null) => Promise<QaComparisonResult | null>
|
||||
onOpenMapSelectionQualityEvidence: () => void
|
||||
onClearQualityEvidence?: () => void
|
||||
}
|
||||
@@ -287,6 +287,9 @@ export function MapWorkspace({
|
||||
const [bboxSelectionMode, setBboxSelectionMode] = useState(false)
|
||||
const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null)
|
||||
const [bboxInput, setBboxInput] = useState(bboxToInputState(mapSelectionBbox))
|
||||
const [fullWorkflowRunning, setFullWorkflowRunning] = useState(false)
|
||||
const [fullWorkflowStatus, setFullWorkflowStatus] = useState('Ready to run persisted GIS workflow.')
|
||||
const [fullWorkflowError, setFullWorkflowError] = useState<string | null>(null)
|
||||
const selectedMapArea = areas.find((area) => area.id === selectedMapAreaId)
|
||||
const featureProperties = selectedMapFeature?.properties ?? null
|
||||
const featureSummaryEntries = featureProperties
|
||||
@@ -404,6 +407,51 @@ export function MapWorkspace({
|
||||
onRunMapSelectionExtract(bbox)
|
||||
}
|
||||
|
||||
const runFullGisWorkflow = async () => {
|
||||
const bbox = currentSelectionBbox ?? selectedAreaBbox ?? activeLayerBbox
|
||||
if (!selectedMapDataset || !bbox) {
|
||||
setFullWorkflowError('Select a database layer and AOI/layer extent before running the full workflow.')
|
||||
return
|
||||
}
|
||||
|
||||
setFullWorkflowRunning(true)
|
||||
setFullWorkflowError(null)
|
||||
try {
|
||||
setFullWorkflowStatus('1/4 Querying persisted vector_features...')
|
||||
setSelectionBbox(bbox)
|
||||
const selection = await onRunMapSelectionExtract(bbox)
|
||||
if (!selection) {
|
||||
setFullWorkflowError('Persisted vector query did not complete.')
|
||||
setFullWorkflowStatus('Stopped at query.')
|
||||
return
|
||||
}
|
||||
|
||||
setFullWorkflowStatus('2/4 Saving derived result dataset...')
|
||||
const derived = await onDeriveMapSelectionDataset(bbox)
|
||||
if (!derived) {
|
||||
setFullWorkflowError('Derived result dataset was not created.')
|
||||
setFullWorkflowStatus('Stopped at dataset save.')
|
||||
return
|
||||
}
|
||||
|
||||
setFullWorkflowStatus('3/4 Saving GeoJSON export artifact...')
|
||||
await onExportMapSelection(bbox)
|
||||
|
||||
if (selectedMapQaReferenceDatasetId) {
|
||||
setFullWorkflowStatus('4/4 Running QA/QC against selected reference...')
|
||||
const qaResult = await onRunMapSelectionQa(derived)
|
||||
setFullWorkflowStatus(qaResult ? 'Full GIS workflow complete with QA/QC result.' : 'Dataset/export complete; QA/QC did not complete.')
|
||||
} else {
|
||||
setFullWorkflowStatus('Dataset/export complete. Select a reference dataset to add QA/QC.')
|
||||
}
|
||||
} catch (error) {
|
||||
setFullWorkflowError(error instanceof Error ? error.message : 'Full GIS workflow failed.')
|
||||
setFullWorkflowStatus('Workflow stopped.')
|
||||
} finally {
|
||||
setFullWorkflowRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="map-workspace-shell" data-testid="map-workspace">
|
||||
<div className="panel-title-row">
|
||||
@@ -705,6 +753,18 @@ export function MapWorkspace({
|
||||
</div>
|
||||
</div>
|
||||
<div className="guided-gis-actions">
|
||||
<button
|
||||
className="primary-action guided-gis-full-run"
|
||||
disabled={!selectedMapDataset || !mapFeatureCollection || (!currentSelectionBbox && !selectedAreaBbox && !activeLayerBbox) || fullWorkflowRunning}
|
||||
type="button"
|
||||
onClick={runFullGisWorkflow}
|
||||
>
|
||||
{fullWorkflowRunning ? 'Running full workflow...' : 'Run full GIS workflow'}
|
||||
</button>
|
||||
<div className="guided-gis-batch-status" aria-live="polite">
|
||||
<strong>Query, save, QA and export</strong>
|
||||
<span>{fullWorkflowStatus}</span>
|
||||
</div>
|
||||
<button
|
||||
className="secondary-action"
|
||||
disabled={!mapSelectionResult || !currentSelectionBbox || selectionDatasetSaving}
|
||||
@@ -740,7 +800,7 @@ export function MapWorkspace({
|
||||
className="primary-action"
|
||||
disabled={!latestSelectionDatasetName || !selectedMapQaReferenceDatasetId || mapSelectionQaRunning}
|
||||
type="button"
|
||||
onClick={onRunMapSelectionQa}
|
||||
onClick={() => onRunMapSelectionQa()}
|
||||
>
|
||||
{mapSelectionQaRunning ? 'Running QA...' : 'Run QA/QC'}
|
||||
</button>
|
||||
@@ -756,6 +816,7 @@ export function MapWorkspace({
|
||||
{selectionDatasetError ? <p className="error">{selectionDatasetError}</p> : null}
|
||||
{selectionExportError ? <p className="error">{selectionExportError}</p> : null}
|
||||
{mapSelectionQaError ? <p className="error">{mapSelectionQaError}</p> : null}
|
||||
{fullWorkflowError ? <p className="error">{fullWorkflowError}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bbox-select-surface" aria-label="Area selection and extract">
|
||||
@@ -915,7 +976,7 @@ export function MapWorkspace({
|
||||
className="primary-action"
|
||||
disabled={!selectedMapQaReferenceDatasetId || mapSelectionQaRunning}
|
||||
type="button"
|
||||
onClick={onRunMapSelectionQa}
|
||||
onClick={() => onRunMapSelectionQa()}
|
||||
>
|
||||
{mapSelectionQaRunning ? 'Running QA...' : 'Run QA on saved dataset'}
|
||||
</button>
|
||||
|
||||
@@ -112,7 +112,7 @@ export function useExportWorkflow({
|
||||
const exportMapSelectionGeoJson = async (bbox: VectorSelectionBBox) => {
|
||||
if (!selectedDataset || !isVectorDatasetType(selectedDataset.dataset_type)) {
|
||||
setSelectionExportError('Select a vector dataset before saving an area export.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
setSelectionExporting(true)
|
||||
setSelectionExportError(null)
|
||||
@@ -127,8 +127,10 @@ export function useExportWorkflow({
|
||||
setLatestExport(response)
|
||||
setLatestSelectionExport(response)
|
||||
await loadExports(selectedDataset.project_id)
|
||||
return response
|
||||
} catch (error) {
|
||||
setSelectionExportError(formatError(error, 'Failed to save area export'))
|
||||
return null
|
||||
} finally {
|
||||
setSelectionExporting(false)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function useMapSelectionDataset({
|
||||
const deriveMapSelectionDataset = async (bbox: VectorSelectionBBox) => {
|
||||
if (!selectedProjectId || !selectedDataset || !isVectorDatasetType(selectedDataset.dataset_type)) {
|
||||
setSelectionDatasetError('Select a vector dataset before saving the area as a dataset.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
setSelectionDatasetSaving(true)
|
||||
setSelectionDatasetError(null)
|
||||
@@ -41,8 +41,10 @@ export function useMapSelectionDataset({
|
||||
await loadProjectData(selectedProjectId)
|
||||
await loadDatasetDetails(selectedProjectId, derived)
|
||||
setMapLayerVisible(true)
|
||||
return derived
|
||||
} catch (error) {
|
||||
setSelectionDatasetError(formatError(error, 'Failed to save area as dataset'))
|
||||
return null
|
||||
} finally {
|
||||
setSelectionDatasetSaving(false)
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@ export function useMapSelectionExtract({
|
||||
const runMapSelectionExtract = async (bbox: VectorSelectionBBox) => {
|
||||
if (!selectedProjectId || !selectedDataset) {
|
||||
setMapSelectionError('Open a vector dataset before extracting a map area.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
if (!isVectorDatasetType(selectedDataset.dataset_type)) {
|
||||
setMapSelectionError('Area extraction requires an active vector dataset.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
setMapSelectionLoading(true)
|
||||
@@ -44,9 +44,11 @@ export function useMapSelectionExtract({
|
||||
limit: 250,
|
||||
})
|
||||
setMapSelectionResult(response)
|
||||
return response
|
||||
} catch (error) {
|
||||
setMapSelectionResult(null)
|
||||
setMapSelectionError(formatError(error, 'Area extraction failed'))
|
||||
return null
|
||||
} finally {
|
||||
setMapSelectionLoading(false)
|
||||
}
|
||||
|
||||
@@ -22,56 +22,58 @@ export function useMapSelectionQa({
|
||||
const [mapSelectionQaResult, setMapQaResult] = useState<QaComparisonResult | null>(null)
|
||||
const [latestMapSelectionQualityCheckId, setLatestMapSelectionQualityCheckId] = useState<string | null>(null)
|
||||
|
||||
const runMapSelectionQa = async () => {
|
||||
const runMapSelectionQa = async (candidateDataset = latestSelectionDataset) => {
|
||||
if (!selectedProjectId) {
|
||||
setMapSelectionQaError('Select a project before running QA/QC.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
if (!latestSelectionDataset) {
|
||||
if (!candidateDataset) {
|
||||
setMapSelectionQaError('Save the map selection as a dataset before running QA/QC.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
if (!selectedMapQaReferenceDatasetId) {
|
||||
setMapSelectionQaError('Select a reference dataset for QA/QC.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
if (latestSelectionDataset.id === selectedMapQaReferenceDatasetId) {
|
||||
if (candidateDataset.id === selectedMapQaReferenceDatasetId) {
|
||||
setMapSelectionQaError('Candidate and reference datasets must be different.')
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
setMapSelectionQaRunning(true)
|
||||
setMapSelectionQaError(null)
|
||||
setMapQaResult(null)
|
||||
setLatestMapSelectionQualityCheckId(null)
|
||||
setLatestMapSelectionQualityCheckId(null)
|
||||
try {
|
||||
const request: QaComparisonRequest = {
|
||||
candidate_dataset_id: latestSelectionDataset.id,
|
||||
candidate_dataset_id: candidateDataset.id,
|
||||
reference_dataset_id: selectedMapQaReferenceDatasetId,
|
||||
iou_threshold: 0.5,
|
||||
area_id: latestSelectionDataset.area_id ?? null,
|
||||
area_id: candidateDataset.area_id ?? null,
|
||||
}
|
||||
const job: JobRead = await qaApi.runQa(request)
|
||||
if (job.status === 'failed') {
|
||||
setMapSelectionQaError(job.error_message || 'Map selection QA/QC failed')
|
||||
return
|
||||
return null
|
||||
}
|
||||
const payload = job.result_json
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
setMapSelectionQaError('Map selection QA/QC result was not available')
|
||||
return
|
||||
return null
|
||||
}
|
||||
const parsed = payload as unknown as QaComparisonResult
|
||||
if (!parsed || typeof parsed.status !== 'string') {
|
||||
setMapSelectionQaError('Map selection QA/QC result format was unexpected')
|
||||
return
|
||||
return null
|
||||
}
|
||||
setMapQaResult(parsed)
|
||||
setLatestMapSelectionQualityCheckId(parsed.quality_check_id ?? null)
|
||||
await loadQualityChecks(selectedProjectId)
|
||||
await loadProjectData(selectedProjectId)
|
||||
return parsed
|
||||
} catch (error) {
|
||||
setMapSelectionQaError(formatError(error, 'Map selection QA/QC failed'))
|
||||
return null
|
||||
} finally {
|
||||
setMapSelectionQaRunning(false)
|
||||
}
|
||||
|
||||
@@ -4873,6 +4873,40 @@ section {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.guided-gis-full-run {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.guided-gis-batch-status {
|
||||
display: grid;
|
||||
gap: 0.12rem;
|
||||
min-width: 0;
|
||||
border: 1px solid rgba(15, 118, 110, 0.22);
|
||||
border-radius: 6px;
|
||||
padding: 0.44rem 0.52rem;
|
||||
background: #f8fbf9;
|
||||
}
|
||||
|
||||
.guided-gis-batch-status strong {
|
||||
overflow: hidden;
|
||||
color: #0f766e;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 850;
|
||||
letter-spacing: 0.045em;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.guided-gis-batch-status span {
|
||||
overflow: hidden;
|
||||
color: var(--muted);
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bbox-select-surface {
|
||||
border-left-width: 3px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user