Add map selection QA shortcut
This commit is contained in:
@@ -280,6 +280,7 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst
|
||||
- The Map workspace can extract persisted vector features by area: open a ready vector dataset, use `Start map bbox` and click two map corners or enter EPSG:4326 bbox values, then run `Run area extract` to query backend `vector_features`. Results are highlighted on the map and can be downloaded as GeoJSON.
|
||||
- After an area extract, `Save area export` persists the selected FeatureCollection as a normal Export Center artifact (`vector_selection_geojson`) so the handoff remains in project export history.
|
||||
- `Save as dataset` persists the same selected FeatureCollection as a derived vector dataset, selects it in the workbench and keeps it queryable through backend `vector_features` for later QA/QC or analysis.
|
||||
- Once a map selection has been saved as a derived dataset, the Map workspace can run QA/QC against a selected reference dataset without switching workspaces. The action reuses the existing QA comparison endpoint and shows precision, recall, F1 and persisted quality-check status inline.
|
||||
- The Data catalog shows a compact selected/reference/candidate/source summary and scan-friendly badges. Persisted `reference` datasets are shown as Reference, non-reference vector/GeoJSON layers are shown as QA Candidates for workbench scanning, and raster/other uploads remain Source.
|
||||
- Dataset cards explain the recommended next action and use compact two-line action buttons for inspect, map, export/QA and metadata refresh. Disabled actions keep a visible reason, such as `Vector/GeoJSON only`.
|
||||
- Raster controls show the latest generated tile manifest path from persisted `raster.tile` jobs and can hand that path directly to Detection Lab or Segmentation Lab with the selected raster dataset.
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useDetectionWorkflow } from './hooks/useDetectionWorkflow'
|
||||
import { useDatasetWorkflow } from './hooks/useDatasetWorkflow'
|
||||
import { useExportWorkflow } from './hooks/useExportWorkflow'
|
||||
import { useMapSelectionDataset } from './hooks/useMapSelectionDataset'
|
||||
import { useMapSelectionQa } from './hooks/useMapSelectionQa'
|
||||
import { useMapWorkspaceState } from './hooks/useMapWorkspaceState'
|
||||
import { useMapSelectionExtract } from './hooks/useMapSelectionExtract'
|
||||
import { useProviderCapabilities } from './hooks/useProviderCapabilities'
|
||||
@@ -399,6 +400,19 @@ function App(): JSX.Element {
|
||||
loadDatasetDetails,
|
||||
setMapLayerVisible,
|
||||
})
|
||||
const {
|
||||
selectedMapQaReferenceDatasetId,
|
||||
mapSelectionQaRunning,
|
||||
mapSelectionQaError,
|
||||
mapSelectionQaResult,
|
||||
runMapSelectionQa,
|
||||
setSelectedMapQaReferenceDatasetId,
|
||||
} = useMapSelectionQa({
|
||||
selectedProjectId,
|
||||
latestSelectionDataset,
|
||||
loadQualityChecks,
|
||||
loadProjectData,
|
||||
})
|
||||
const {
|
||||
loadingDemoWorkflow,
|
||||
demoWorkflowMessage,
|
||||
@@ -826,6 +840,11 @@ function App(): JSX.Element {
|
||||
selectionDatasetSaving={selectionDatasetSaving}
|
||||
selectionDatasetError={selectionDatasetError}
|
||||
latestSelectionDatasetName={latestSelectionDataset?.name ?? null}
|
||||
mapQaReferenceDatasets={referenceDatasets}
|
||||
selectedMapQaReferenceDatasetId={selectedMapQaReferenceDatasetId}
|
||||
mapSelectionQaRunning={mapSelectionQaRunning}
|
||||
mapSelectionQaError={mapSelectionQaError}
|
||||
mapSelectionQaResult={mapSelectionQaResult}
|
||||
availableMapDatasets={availableMapDatasets}
|
||||
selectedFeature={selectedMapFeature}
|
||||
onSelectMapArea={setSelectedMapAreaId}
|
||||
@@ -840,6 +859,8 @@ function App(): JSX.Element {
|
||||
onClearMapSelectionExtract={resetMapSelectionExtract}
|
||||
onExportMapSelection={exportMapSelectionGeoJson}
|
||||
onDeriveMapSelectionDataset={deriveMapSelectionDataset}
|
||||
onSelectMapQaReferenceDataset={setSelectedMapQaReferenceDatasetId}
|
||||
onRunMapSelectionQa={runMapSelectionQa}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import GeoMap from '../GeoMap'
|
||||
import type { AreaRead, DatasetCreateResponse, VectorSelectionBBox, VectorSelectionResponse } from '../../types'
|
||||
import type { AreaRead, DatasetCreateResponse, QaComparisonResult, VectorSelectionBBox, VectorSelectionResponse } from '../../types'
|
||||
|
||||
const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson'
|
||||
const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson'
|
||||
@@ -197,6 +197,11 @@ interface MapWorkspaceProps {
|
||||
selectionDatasetSaving: boolean
|
||||
selectionDatasetError: string | null
|
||||
latestSelectionDatasetName: string | null
|
||||
mapQaReferenceDatasets: DatasetCreateResponse[]
|
||||
selectedMapQaReferenceDatasetId: string
|
||||
mapSelectionQaRunning: boolean
|
||||
mapSelectionQaError: string | null
|
||||
mapSelectionQaResult: QaComparisonResult | null
|
||||
availableMapDatasets: DatasetCreateResponse[]
|
||||
onSelectMapArea: (areaId: string) => void
|
||||
onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void
|
||||
@@ -210,6 +215,8 @@ interface MapWorkspaceProps {
|
||||
onClearMapSelectionExtract: () => void
|
||||
onExportMapSelection: (bbox: VectorSelectionBBox) => void
|
||||
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => void
|
||||
onSelectMapQaReferenceDataset: (datasetId: string) => void
|
||||
onRunMapSelectionQa: () => void
|
||||
}
|
||||
|
||||
export function MapWorkspace({
|
||||
@@ -238,6 +245,11 @@ export function MapWorkspace({
|
||||
selectionDatasetSaving,
|
||||
selectionDatasetError,
|
||||
latestSelectionDatasetName,
|
||||
mapQaReferenceDatasets,
|
||||
selectedMapQaReferenceDatasetId,
|
||||
mapSelectionQaRunning,
|
||||
mapSelectionQaError,
|
||||
mapSelectionQaResult,
|
||||
availableMapDatasets,
|
||||
onSelectMapArea,
|
||||
onOpenDatasetInMap,
|
||||
@@ -251,6 +263,8 @@ export function MapWorkspace({
|
||||
onClearMapSelectionExtract,
|
||||
onExportMapSelection,
|
||||
onDeriveMapSelectionDataset,
|
||||
onSelectMapQaReferenceDataset,
|
||||
onRunMapSelectionQa,
|
||||
}: MapWorkspaceProps): JSX.Element {
|
||||
const [bboxSelectionMode, setBboxSelectionMode] = useState(false)
|
||||
const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null)
|
||||
@@ -644,6 +658,54 @@ export function MapWorkspace({
|
||||
{latestSelectionDatasetName ? (
|
||||
<p className="muted">Saved derived dataset: {latestSelectionDatasetName}</p>
|
||||
) : null}
|
||||
{latestSelectionDatasetName ? (
|
||||
<div className="map-selection-qa-surface" aria-label="Map selection QA shortcut">
|
||||
<label>
|
||||
Reference dataset
|
||||
<select
|
||||
value={selectedMapQaReferenceDatasetId}
|
||||
onChange={(event) => onSelectMapQaReferenceDataset(event.target.value)}
|
||||
disabled={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={!selectedMapQaReferenceDatasetId || mapSelectionQaRunning}
|
||||
type="button"
|
||||
onClick={onRunMapSelectionQa}
|
||||
>
|
||||
{mapSelectionQaRunning ? 'Running QA...' : 'Run QA on saved dataset'}
|
||||
</button>
|
||||
{mapSelectionQaError ? <p className="error">{mapSelectionQaError}</p> : null}
|
||||
{mapSelectionQaResult ? (
|
||||
<div className="feature-extract-grid" aria-label="Map selection QA result">
|
||||
<div>
|
||||
<span>Precision</span>
|
||||
<strong>{mapSelectionQaResult.precision ?? 'n/a'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Recall</span>
|
||||
<strong>{mapSelectionQaResult.recall ?? 'n/a'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>F1</span>
|
||||
<strong>{mapSelectionQaResult.f1_score ?? 'n/a'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Quality check</span>
|
||||
<strong>{mapSelectionQaResult.status}</strong>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{areaSelectionPreviewFeatures.length > 0 ? (
|
||||
<div className="table-scroll feature-property-table" aria-label="Area selection feature table">
|
||||
<table>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useState } from 'react'
|
||||
import type { DatasetCreateResponse, JobRead, QaComparisonRequest, QaComparisonResult, QualityCheckRead } from '../types'
|
||||
import { formatError } from '../lib/formatError'
|
||||
import { qaApi } from '../services/api'
|
||||
|
||||
interface UseMapSelectionQaOptions {
|
||||
selectedProjectId: string | null
|
||||
latestSelectionDataset: DatasetCreateResponse | null
|
||||
loadQualityChecks: (projectId?: string | null) => Promise<QualityCheckRead[] | void>
|
||||
loadProjectData: (projectId: string) => Promise<unknown>
|
||||
}
|
||||
|
||||
export function useMapSelectionQa({
|
||||
selectedProjectId,
|
||||
latestSelectionDataset,
|
||||
loadQualityChecks,
|
||||
loadProjectData,
|
||||
}: UseMapSelectionQaOptions) {
|
||||
const [selectedMapQaReferenceDatasetId, setSelectedMapQaReferenceDatasetId] = useState('')
|
||||
const [mapSelectionQaRunning, setMapSelectionQaRunning] = useState(false)
|
||||
const [mapSelectionQaError, setMapSelectionQaError] = useState<string | null>(null)
|
||||
const [mapSelectionQaResult, setMapQaResult] = useState<QaComparisonResult | null>(null)
|
||||
|
||||
const runMapSelectionQa = async () => {
|
||||
if (!selectedProjectId) {
|
||||
setMapSelectionQaError('Select a project before running QA/QC.')
|
||||
return
|
||||
}
|
||||
if (!latestSelectionDataset) {
|
||||
setMapSelectionQaError('Save the map selection as a dataset before running QA/QC.')
|
||||
return
|
||||
}
|
||||
if (!selectedMapQaReferenceDatasetId) {
|
||||
setMapSelectionQaError('Select a reference dataset for QA/QC.')
|
||||
return
|
||||
}
|
||||
if (latestSelectionDataset.id === selectedMapQaReferenceDatasetId) {
|
||||
setMapSelectionQaError('Candidate and reference datasets must be different.')
|
||||
return
|
||||
}
|
||||
|
||||
setMapSelectionQaRunning(true)
|
||||
setMapSelectionQaError(null)
|
||||
setMapQaResult(null)
|
||||
try {
|
||||
const request: QaComparisonRequest = {
|
||||
candidate_dataset_id: latestSelectionDataset.id,
|
||||
reference_dataset_id: selectedMapQaReferenceDatasetId,
|
||||
iou_threshold: 0.5,
|
||||
area_id: latestSelectionDataset.area_id ?? null,
|
||||
}
|
||||
const job: JobRead = await qaApi.runQa(request)
|
||||
if (job.status === 'failed') {
|
||||
setMapSelectionQaError(job.error_message || 'Map selection QA/QC failed')
|
||||
return
|
||||
}
|
||||
const payload = job.result_json
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
setMapSelectionQaError('Map selection QA/QC result was not available')
|
||||
return
|
||||
}
|
||||
const parsed = payload as unknown as QaComparisonResult
|
||||
if (!parsed || typeof parsed.status !== 'string') {
|
||||
setMapSelectionQaError('Map selection QA/QC result format was unexpected')
|
||||
return
|
||||
}
|
||||
setMapQaResult(parsed)
|
||||
await loadQualityChecks(selectedProjectId)
|
||||
await loadProjectData(selectedProjectId)
|
||||
} catch (error) {
|
||||
setMapSelectionQaError(formatError(error, 'Map selection QA/QC failed'))
|
||||
} finally {
|
||||
setMapSelectionQaRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectedMapQaReferenceDatasetId,
|
||||
mapSelectionQaRunning,
|
||||
mapSelectionQaError,
|
||||
mapSelectionQaResult,
|
||||
runMapSelectionQa,
|
||||
setSelectedMapQaReferenceDatasetId,
|
||||
}
|
||||
}
|
||||
@@ -2669,6 +2669,37 @@ button.entity-card {
|
||||
padding-top: 0.68rem;
|
||||
}
|
||||
|
||||
.map-selection-qa-surface {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(11rem, 1fr) auto;
|
||||
gap: 0.58rem;
|
||||
align-items: end;
|
||||
min-width: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 0.68rem;
|
||||
}
|
||||
|
||||
.map-selection-qa-surface label {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.map-selection-qa-surface .primary-action {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.map-selection-qa-surface .error,
|
||||
.map-selection-qa-surface .feature-extract-grid {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.map-selection-qa-surface {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-extract-surface {
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
|
||||
Reference in New Issue
Block a user