Persist map selections as derived datasets
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-25 02:25:49 +02:00
parent 1e42302f62
commit b92faafa74
16 changed files with 587 additions and 2 deletions
+18
View File
@@ -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,
}
}
+3
View File
@@ -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 }) =>
+4
View File
@@ -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