Persist map area selection exports
This commit is contained in:
@@ -278,6 +278,7 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst
|
||||
- The Map workspace shows active layer source/provenance/draw-state context and selected-feature property chips before the raw JSON inspector.
|
||||
- When the Map workspace has no active result layer, it lists ready vector/GeoJSON datasets as direct quick actions so populated demo projects can jump straight from the empty state to map inspection.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -325,11 +325,15 @@ function App(): JSX.Element {
|
||||
exportError,
|
||||
loadingExports,
|
||||
exporting,
|
||||
selectionExporting,
|
||||
selectionExportError,
|
||||
latestSelectionExport,
|
||||
exportPreview,
|
||||
loadExports,
|
||||
exportSelectedDatasetGeoJson,
|
||||
exportSelectedDetectionRunGeoJson,
|
||||
exportSelectedSegmentationRunGeoJson,
|
||||
exportMapSelectionGeoJson,
|
||||
exportProjectMetadata,
|
||||
exportProjectReport,
|
||||
previewExportContent,
|
||||
@@ -802,6 +806,9 @@ function App(): JSX.Element {
|
||||
mapSelectionResult={mapSelectionResult}
|
||||
mapSelectionLoading={mapSelectionLoading}
|
||||
mapSelectionError={mapSelectionError}
|
||||
selectionExporting={selectionExporting}
|
||||
selectionExportError={selectionExportError}
|
||||
latestSelectionExportPath={latestSelectionExport?.path ?? null}
|
||||
availableMapDatasets={availableMapDatasets}
|
||||
selectedFeature={selectedMapFeature}
|
||||
onSelectMapArea={setSelectedMapAreaId}
|
||||
@@ -814,6 +821,7 @@ function App(): JSX.Element {
|
||||
onSetMapSelectionBbox={setMapSelectionBbox}
|
||||
onRunMapSelectionExtract={runMapSelectionExtract}
|
||||
onClearMapSelectionExtract={resetMapSelectionExtract}
|
||||
onExportMapSelection={exportMapSelectionGeoJson}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -191,6 +191,9 @@ interface MapWorkspaceProps {
|
||||
mapSelectionResult: VectorSelectionResponse | null
|
||||
mapSelectionLoading: boolean
|
||||
mapSelectionError: string | null
|
||||
selectionExporting: boolean
|
||||
selectionExportError: string | null
|
||||
latestSelectionExportPath: string | null
|
||||
availableMapDatasets: DatasetCreateResponse[]
|
||||
onSelectMapArea: (areaId: string) => void
|
||||
onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void
|
||||
@@ -202,6 +205,7 @@ interface MapWorkspaceProps {
|
||||
onSetMapSelectionBbox: (bbox: VectorSelectionBBox | null) => void
|
||||
onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => void
|
||||
onClearMapSelectionExtract: () => void
|
||||
onExportMapSelection: (bbox: VectorSelectionBBox) => void
|
||||
}
|
||||
|
||||
export function MapWorkspace({
|
||||
@@ -224,6 +228,9 @@ export function MapWorkspace({
|
||||
mapSelectionResult,
|
||||
mapSelectionLoading,
|
||||
mapSelectionError,
|
||||
selectionExporting,
|
||||
selectionExportError,
|
||||
latestSelectionExportPath,
|
||||
availableMapDatasets,
|
||||
onSelectMapArea,
|
||||
onOpenDatasetInMap,
|
||||
@@ -235,6 +242,7 @@ export function MapWorkspace({
|
||||
onSetMapSelectionBbox,
|
||||
onRunMapSelectionExtract,
|
||||
onClearMapSelectionExtract,
|
||||
onExportMapSelection,
|
||||
}: MapWorkspaceProps): JSX.Element {
|
||||
const [bboxSelectionMode, setBboxSelectionMode] = useState(false)
|
||||
const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null)
|
||||
@@ -322,6 +330,14 @@ export function MapWorkspace({
|
||||
copyText(JSON.stringify(mapSelectionResult?.geojson ?? { type: 'FeatureCollection', features: [] }, null, 2))
|
||||
}
|
||||
|
||||
const saveAreaSelectionExport = () => {
|
||||
const bbox = parseBboxInput(bboxInput)
|
||||
if (!bbox) {
|
||||
return
|
||||
}
|
||||
onExportMapSelection(bbox)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="map-workspace-shell" data-testid="map-workspace">
|
||||
<div className="panel-title-row">
|
||||
@@ -587,7 +603,19 @@ export function MapWorkspace({
|
||||
<button className="secondary-action" type="button" onClick={copyAreaSelection}>
|
||||
Copy area GeoJSON
|
||||
</button>
|
||||
<button
|
||||
className="secondary-action"
|
||||
disabled={!currentSelectionBbox || selectionExporting}
|
||||
type="button"
|
||||
onClick={saveAreaSelectionExport}
|
||||
>
|
||||
{selectionExporting ? 'Saving export...' : 'Save area export'}
|
||||
</button>
|
||||
</div>
|
||||
{selectionExportError ? <p className="error">{selectionExportError}</p> : null}
|
||||
{latestSelectionExportPath ? (
|
||||
<p className="muted">Saved selection artifact: {latestSelectionExportPath}</p>
|
||||
) : null}
|
||||
{areaSelectionPreviewFeatures.length > 0 ? (
|
||||
<div className="table-scroll feature-property-table" aria-label="Area selection feature table">
|
||||
<table>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { exportsApi } from '../services/api'
|
||||
import type { DatasetCreateResponse, ExportCreateResponse, ExportRead } from '../types'
|
||||
import type { DatasetCreateResponse, ExportCreateResponse, ExportRead, VectorSelectionBBox } from '../types'
|
||||
import { formatError } from '../lib/formatError'
|
||||
|
||||
interface ExportWorkflowOptions {
|
||||
@@ -23,6 +23,9 @@ export function useExportWorkflow({
|
||||
const [exportError, setExportError] = useState<string | null>(null)
|
||||
const [loadingExports, setLoadingExports] = useState(false)
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const [selectionExporting, setSelectionExporting] = useState(false)
|
||||
const [selectionExportError, setSelectionExportError] = useState<string | null>(null)
|
||||
const [latestSelectionExport, setLatestSelectionExport] = useState<ExportCreateResponse | null>(null)
|
||||
const [exportPreview, setExportPreview] = useState<Record<string, unknown> | null>(null)
|
||||
|
||||
const loadExports = async (projectId = selectedProjectId) => {
|
||||
@@ -106,6 +109,31 @@ 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
|
||||
}
|
||||
setSelectionExporting(true)
|
||||
setSelectionExportError(null)
|
||||
try {
|
||||
const response = await exportsApi.exportGeojson({
|
||||
dataset_id: selectedDataset.id,
|
||||
export_kind: 'vector_selection',
|
||||
bbox: { ...bbox, crs: 'EPSG:4326' },
|
||||
limit: 250,
|
||||
name: `${selectedDataset.name.replace(/\.(geo)?json$/i, '')}-selection`,
|
||||
})
|
||||
setLatestExport(response)
|
||||
setLatestSelectionExport(response)
|
||||
await loadExports(selectedDataset.project_id)
|
||||
} catch (error) {
|
||||
setSelectionExportError(formatError(error, 'Failed to save area export'))
|
||||
} finally {
|
||||
setSelectionExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const exportProjectMetadata = async () => {
|
||||
if (!selectedProjectId) {
|
||||
setExportError('Select a project before exporting metadata.')
|
||||
@@ -159,6 +187,8 @@ export function useExportWorkflow({
|
||||
const resetExportsForProject = () => {
|
||||
setExports([])
|
||||
setLatestExport(null)
|
||||
setLatestSelectionExport(null)
|
||||
setSelectionExportError(null)
|
||||
setExportPreview(null)
|
||||
}
|
||||
|
||||
@@ -168,11 +198,15 @@ export function useExportWorkflow({
|
||||
exportError,
|
||||
loadingExports,
|
||||
exporting,
|
||||
selectionExporting,
|
||||
selectionExportError,
|
||||
latestSelectionExport,
|
||||
exportPreview,
|
||||
loadExports,
|
||||
exportSelectedDatasetGeoJson,
|
||||
exportSelectedDetectionRunGeoJson,
|
||||
exportSelectedSegmentationRunGeoJson,
|
||||
exportMapSelectionGeoJson,
|
||||
exportProjectMetadata,
|
||||
exportProjectReport,
|
||||
previewExportContent,
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import { apiGet, apiPost, apiUrl } from './client'
|
||||
import type { ExportContentResponse, ExportCreateResponse, ExportKind, ExportListResponse, ExportRead } from '../../types'
|
||||
import type {
|
||||
ExportContentResponse,
|
||||
ExportCreateResponse,
|
||||
ExportKind,
|
||||
ExportListResponse,
|
||||
ExportRead,
|
||||
VectorSelectionBBox,
|
||||
} from '../../types'
|
||||
|
||||
export const exportsApi = {
|
||||
exportGeojson: (
|
||||
payload: { dataset_id?: string; analysis_run_id?: string; export_kind?: ExportKind; name?: string } | string,
|
||||
payload:
|
||||
| {
|
||||
dataset_id?: string
|
||||
analysis_run_id?: string
|
||||
export_kind?: ExportKind
|
||||
name?: string
|
||||
bbox?: VectorSelectionBBox
|
||||
limit?: number
|
||||
}
|
||||
| string,
|
||||
): Promise<ExportCreateResponse> => {
|
||||
const body = typeof payload === 'string' ? { dataset_id: payload, export_kind: 'dataset' } : payload
|
||||
return apiPost<ExportCreateResponse>(`/api/v1/exports/geojson`, body)
|
||||
|
||||
@@ -635,7 +635,7 @@ export interface QualityCheckListResponse {
|
||||
offset: number
|
||||
}
|
||||
|
||||
export type ExportKind = 'dataset' | 'detection_run' | 'segmentation_run'
|
||||
export type ExportKind = 'dataset' | 'detection_run' | 'segmentation_run' | 'vector_selection'
|
||||
|
||||
export interface ExportRead {
|
||||
id: string
|
||||
|
||||
Reference in New Issue
Block a user