Persist map selections as derived datasets
This commit is contained in:
@@ -19,6 +19,7 @@ import { useDemoWorkflow } from './hooks/useDemoWorkflow'
|
||||
import { useDetectionWorkflow } from './hooks/useDetectionWorkflow'
|
||||
import { useDatasetWorkflow } from './hooks/useDatasetWorkflow'
|
||||
import { useExportWorkflow } from './hooks/useExportWorkflow'
|
||||
import { useMapSelectionDataset } from './hooks/useMapSelectionDataset'
|
||||
import { useMapWorkspaceState } from './hooks/useMapWorkspaceState'
|
||||
import { useMapSelectionExtract } from './hooks/useMapSelectionExtract'
|
||||
import { useProviderCapabilities } from './hooks/useProviderCapabilities'
|
||||
@@ -385,6 +386,19 @@ function App(): JSX.Element {
|
||||
selectedDataset,
|
||||
isVectorDatasetType,
|
||||
})
|
||||
const {
|
||||
selectionDatasetSaving,
|
||||
selectionDatasetError,
|
||||
latestSelectionDataset,
|
||||
deriveMapSelectionDataset,
|
||||
} = useMapSelectionDataset({
|
||||
selectedProjectId,
|
||||
selectedDataset,
|
||||
isVectorDatasetType,
|
||||
loadProjectData,
|
||||
loadDatasetDetails,
|
||||
setMapLayerVisible,
|
||||
})
|
||||
const {
|
||||
loadingDemoWorkflow,
|
||||
demoWorkflowMessage,
|
||||
@@ -809,6 +823,9 @@ function App(): JSX.Element {
|
||||
selectionExporting={selectionExporting}
|
||||
selectionExportError={selectionExportError}
|
||||
latestSelectionExportPath={latestSelectionExport?.path ?? null}
|
||||
selectionDatasetSaving={selectionDatasetSaving}
|
||||
selectionDatasetError={selectionDatasetError}
|
||||
latestSelectionDatasetName={latestSelectionDataset?.name ?? null}
|
||||
availableMapDatasets={availableMapDatasets}
|
||||
selectedFeature={selectedMapFeature}
|
||||
onSelectMapArea={setSelectedMapAreaId}
|
||||
@@ -822,6 +839,7 @@ function App(): JSX.Element {
|
||||
onRunMapSelectionExtract={runMapSelectionExtract}
|
||||
onClearMapSelectionExtract={resetMapSelectionExtract}
|
||||
onExportMapSelection={exportMapSelectionGeoJson}
|
||||
onDeriveMapSelectionDataset={deriveMapSelectionDataset}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -194,6 +194,9 @@ interface MapWorkspaceProps {
|
||||
selectionExporting: boolean
|
||||
selectionExportError: string | null
|
||||
latestSelectionExportPath: string | null
|
||||
selectionDatasetSaving: boolean
|
||||
selectionDatasetError: string | null
|
||||
latestSelectionDatasetName: string | null
|
||||
availableMapDatasets: DatasetCreateResponse[]
|
||||
onSelectMapArea: (areaId: string) => void
|
||||
onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void
|
||||
@@ -206,6 +209,7 @@ interface MapWorkspaceProps {
|
||||
onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => void
|
||||
onClearMapSelectionExtract: () => void
|
||||
onExportMapSelection: (bbox: VectorSelectionBBox) => void
|
||||
onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => void
|
||||
}
|
||||
|
||||
export function MapWorkspace({
|
||||
@@ -231,6 +235,9 @@ export function MapWorkspace({
|
||||
selectionExporting,
|
||||
selectionExportError,
|
||||
latestSelectionExportPath,
|
||||
selectionDatasetSaving,
|
||||
selectionDatasetError,
|
||||
latestSelectionDatasetName,
|
||||
availableMapDatasets,
|
||||
onSelectMapArea,
|
||||
onOpenDatasetInMap,
|
||||
@@ -243,6 +250,7 @@ export function MapWorkspace({
|
||||
onRunMapSelectionExtract,
|
||||
onClearMapSelectionExtract,
|
||||
onExportMapSelection,
|
||||
onDeriveMapSelectionDataset,
|
||||
}: MapWorkspaceProps): JSX.Element {
|
||||
const [bboxSelectionMode, setBboxSelectionMode] = useState(false)
|
||||
const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null)
|
||||
@@ -338,6 +346,14 @@ export function MapWorkspace({
|
||||
onExportMapSelection(bbox)
|
||||
}
|
||||
|
||||
const saveAreaSelectionDataset = () => {
|
||||
const bbox = parseBboxInput(bboxInput)
|
||||
if (!bbox) {
|
||||
return
|
||||
}
|
||||
onDeriveMapSelectionDataset(bbox)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="map-workspace-shell" data-testid="map-workspace">
|
||||
<div className="panel-title-row">
|
||||
@@ -611,11 +627,23 @@ export function MapWorkspace({
|
||||
>
|
||||
{selectionExporting ? 'Saving export...' : 'Save area export'}
|
||||
</button>
|
||||
<button
|
||||
className="secondary-action"
|
||||
disabled={!currentSelectionBbox || selectionDatasetSaving}
|
||||
type="button"
|
||||
onClick={saveAreaSelectionDataset}
|
||||
>
|
||||
{selectionDatasetSaving ? 'Saving dataset...' : 'Save as dataset'}
|
||||
</button>
|
||||
</div>
|
||||
{selectionExportError ? <p className="error">{selectionExportError}</p> : null}
|
||||
{latestSelectionExportPath ? (
|
||||
<p className="muted">Saved selection artifact: {latestSelectionExportPath}</p>
|
||||
) : null}
|
||||
{selectionDatasetError ? <p className="error">{selectionDatasetError}</p> : null}
|
||||
{latestSelectionDatasetName ? (
|
||||
<p className="muted">Saved derived dataset: {latestSelectionDatasetName}</p>
|
||||
) : null}
|
||||
{areaSelectionPreviewFeatures.length > 0 ? (
|
||||
<div className="table-scroll feature-property-table" aria-label="Area selection feature table">
|
||||
<table>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useState } from 'react'
|
||||
import type { DatasetCreateResponse, VectorSelectionBBox } from '../types'
|
||||
import { formatError } from '../lib/formatError'
|
||||
import { datasetsApi } from '../services/api'
|
||||
|
||||
interface UseMapSelectionDatasetOptions {
|
||||
selectedProjectId: string | null
|
||||
selectedDataset: DatasetCreateResponse | null
|
||||
isVectorDatasetType: (datasetType: string) => boolean
|
||||
loadProjectData: (projectId: string) => Promise<unknown>
|
||||
loadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => Promise<void>
|
||||
setMapLayerVisible: (visible: boolean) => void
|
||||
}
|
||||
|
||||
export function useMapSelectionDataset({
|
||||
selectedProjectId,
|
||||
selectedDataset,
|
||||
isVectorDatasetType,
|
||||
loadProjectData,
|
||||
loadDatasetDetails,
|
||||
setMapLayerVisible,
|
||||
}: UseMapSelectionDatasetOptions) {
|
||||
const [selectionDatasetSaving, setSelectionDatasetSaving] = useState(false)
|
||||
const [selectionDatasetError, setSelectionDatasetError] = useState<string | null>(null)
|
||||
const [latestSelectionDataset, setLatestSelectionDataset] = useState<DatasetCreateResponse | null>(null)
|
||||
|
||||
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
|
||||
}
|
||||
setSelectionDatasetSaving(true)
|
||||
setSelectionDatasetError(null)
|
||||
try {
|
||||
const derived = await datasetsApi.deriveVectorSelection(selectedProjectId, selectedDataset.id, {
|
||||
bbox: { ...bbox, crs: 'EPSG:4326' },
|
||||
limit: 250,
|
||||
output_name: `${selectedDataset.name.replace(/\.(geo)?json$/i, '')}-selection-dataset`,
|
||||
})
|
||||
setLatestSelectionDataset(derived)
|
||||
await loadProjectData(selectedProjectId)
|
||||
await loadDatasetDetails(selectedProjectId, derived)
|
||||
setMapLayerVisible(true)
|
||||
} catch (error) {
|
||||
setSelectionDatasetError(formatError(error, 'Failed to save area as dataset'))
|
||||
} finally {
|
||||
setSelectionDatasetSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selectionDatasetSaving,
|
||||
selectionDatasetError,
|
||||
latestSelectionDataset,
|
||||
deriveMapSelectionDataset,
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
VectorBBoxResponse,
|
||||
VectorStatsResponse,
|
||||
VectorSelectionRequest,
|
||||
VectorSelectionDeriveRequest,
|
||||
VectorSelectionResponse,
|
||||
VectorSummary,
|
||||
RasterNdviRequest,
|
||||
@@ -74,6 +75,8 @@ export const datasetsApi = {
|
||||
apiGet<VectorStatsResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/vector/stats`),
|
||||
selectVectorFeatures: (projectId: string, datasetId: string, payload: VectorSelectionRequest): Promise<VectorSelectionResponse> =>
|
||||
apiPost<VectorSelectionResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/vector/select`, payload),
|
||||
deriveVectorSelection: (projectId: string, datasetId: string, payload: VectorSelectionDeriveRequest): Promise<DatasetCreateResponse> =>
|
||||
apiPost<DatasetCreateResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/vector/select/derive`, payload),
|
||||
vectorClip: (projectId: string, datasetId: string, payload: { area_id: string; output_name?: string }) =>
|
||||
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/${datasetId}/vector/clip`, payload),
|
||||
vectorBuffer: (projectId: string, datasetId: string, payload: { distance_m: number; dissolve?: boolean; output_name?: string }) =>
|
||||
|
||||
@@ -287,6 +287,10 @@ export interface VectorSelectionRequest {
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface VectorSelectionDeriveRequest extends VectorSelectionRequest {
|
||||
output_name?: string
|
||||
}
|
||||
|
||||
export interface VectorSelectionResponse {
|
||||
selection_bbox: VectorSelectionBBox
|
||||
feature_count: number
|
||||
|
||||
Reference in New Issue
Block a user