Extract dataset workflow orchestration
This commit is contained in:
@@ -0,0 +1,486 @@
|
||||
import { FormEvent, useMemo, useState } from 'react'
|
||||
import { datasetsApi, jobsApi } from '../services/api'
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
JobRead,
|
||||
RasterMetadataResponse,
|
||||
RasterPreviewResponse,
|
||||
RasterStatsResponse,
|
||||
VectorSummary,
|
||||
} from '../types'
|
||||
import { formatError } from '../lib/formatError'
|
||||
|
||||
interface DatasetWorkflowOptions {
|
||||
selectedProjectId: string | null
|
||||
datasets: DatasetCreateResponse[]
|
||||
loadProjectData: (projectId: string) => Promise<{ datasets: DatasetCreateResponse[] } | null>
|
||||
setErrorMessage: (message: string | null) => void
|
||||
isVectorDatasetType: (datasetType: string) => boolean
|
||||
}
|
||||
|
||||
function toRasterMetadata(metadata: Record<string, unknown> | null | undefined): RasterMetadataResponse | null {
|
||||
if (!metadata) {
|
||||
return null
|
||||
}
|
||||
return metadata as unknown as RasterMetadataResponse
|
||||
}
|
||||
|
||||
export function useDatasetWorkflow({
|
||||
selectedProjectId,
|
||||
datasets,
|
||||
loadProjectData,
|
||||
setErrorMessage,
|
||||
isVectorDatasetType,
|
||||
}: DatasetWorkflowOptions) {
|
||||
const [selectedDatasetId, setSelectedDatasetId] = useState<string | null>(null)
|
||||
const [selectedDataset, setSelectedDataset] = useState<DatasetCreateResponse | null>(null)
|
||||
const [selectedDatasetSummary, setSelectedDatasetSummary] = useState<VectorSummary | null>(null)
|
||||
const [selectedRasterMetadata, setSelectedRasterMetadata] = useState<RasterMetadataResponse | null>(null)
|
||||
const [selectedRasterStats, setSelectedRasterStats] = useState<RasterStatsResponse | null>(null)
|
||||
const [datasetContent, setDatasetContent] = useState<GeoJSON.FeatureCollection | null>(null)
|
||||
const [jobs, setJobs] = useState<JobRead[]>([])
|
||||
const [rasterPreview, setRasterPreview] = useState<RasterPreviewResponse | null>(null)
|
||||
const [selectedIntersectTargetId, setSelectedIntersectTargetId] = useState('')
|
||||
const [selectedClipAreaId, setSelectedClipAreaId] = useState('')
|
||||
const [rasterTileSize, setRasterTileSize] = useState(512)
|
||||
const [rasterTileOverlap, setRasterTileOverlap] = useState(64)
|
||||
const [rasterTileOutputName, setRasterTileOutputName] = useState('')
|
||||
const [rasterReprojectCrs, setRasterReprojectCrs] = useState('EPSG:31370')
|
||||
const [rasterReprojectResampling, setRasterReprojectResampling] = useState('nearest')
|
||||
const [ndviNirBand, setNdviNirBand] = useState(4)
|
||||
const [ndviRedBand, setNdviRedBand] = useState(3)
|
||||
const [ndwiGreenBand, setNdwiGreenBand] = useState(2)
|
||||
const [ndwiNirBand, setNdwiNirBand] = useState(4)
|
||||
const [ndbiSwirBand, setNdbiSwirBand] = useState(5)
|
||||
const [ndbiNirBand, setNdbiNirBand] = useState(4)
|
||||
const [loadingDatasetDetails, setLoadingDatasetDetails] = useState(false)
|
||||
const [datasetDetailError, setDatasetDetailError] = useState<string | null>(null)
|
||||
const [datasetForm, setDatasetForm] = useState({
|
||||
datasetType: 'vector',
|
||||
source: 'user_upload',
|
||||
datasetRole: 'source',
|
||||
sourceName: '',
|
||||
referenceLayerName: '',
|
||||
sourceMetadataJson: '',
|
||||
provenanceMetadataJson: '',
|
||||
areaId: '',
|
||||
file: null as File | null,
|
||||
})
|
||||
|
||||
const isRasterTileInputValid = useMemo(
|
||||
() => rasterTileSize > 0 && rasterTileOverlap >= 0 && rasterTileOverlap < rasterTileSize,
|
||||
[rasterTileSize, rasterTileOverlap],
|
||||
)
|
||||
|
||||
const loadDatasetJobs = async (projectId: string, datasetId: string) => {
|
||||
const response = await jobsApi.list(projectId, { dataset_id: datasetId, limit: 20, offset: 0 })
|
||||
setJobs(response.items)
|
||||
}
|
||||
|
||||
const loadDatasetDetails = async (projectId: string, dataset: DatasetCreateResponse) => {
|
||||
setLoadingDatasetDetails(true)
|
||||
setDatasetDetailError(null)
|
||||
setSelectedDataset(dataset)
|
||||
setSelectedDatasetSummary(null)
|
||||
setSelectedRasterMetadata(null)
|
||||
setSelectedRasterStats(null)
|
||||
setDatasetContent(null)
|
||||
setRasterPreview(null)
|
||||
setSelectedDatasetId(dataset.id)
|
||||
setJobs([])
|
||||
try {
|
||||
if (isVectorDatasetType(dataset.dataset_type)) {
|
||||
const [content, summary] = await Promise.all([
|
||||
datasetsApi.getContent(projectId, dataset.id),
|
||||
datasetsApi.vectorSummary(projectId, dataset.id),
|
||||
])
|
||||
setDatasetContent(content)
|
||||
setSelectedDatasetSummary(summary)
|
||||
} else if (dataset.dataset_type === 'raster') {
|
||||
try {
|
||||
const rasterInspection = await datasetsApi.rasterInspect(projectId, dataset.id)
|
||||
setSelectedRasterMetadata(toRasterMetadata(rasterInspection.metadata))
|
||||
} catch (error) {
|
||||
setSelectedRasterMetadata(null)
|
||||
setDatasetDetailError(formatError(error, 'Raster metadata unavailable'))
|
||||
}
|
||||
}
|
||||
await loadDatasetJobs(projectId, dataset.id)
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Unable to load dataset detail'))
|
||||
} finally {
|
||||
setLoadingDatasetDetails(false)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadDataset = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!selectedProjectId || !datasetForm.file) {
|
||||
setErrorMessage('Select project and upload a file')
|
||||
return
|
||||
}
|
||||
if (!datasetForm.datasetRole) {
|
||||
setErrorMessage('Select dataset role')
|
||||
return
|
||||
}
|
||||
if (datasetForm.datasetRole === 'reference' && datasetForm.datasetType !== 'vector' && datasetForm.datasetType !== 'geojson') {
|
||||
setErrorMessage('Reference role requires vector dataset upload')
|
||||
return
|
||||
}
|
||||
if (datasetForm.sourceMetadataJson) {
|
||||
try {
|
||||
const parsedSourceMetadata = JSON.parse(datasetForm.sourceMetadataJson)
|
||||
if (parsedSourceMetadata === null || typeof parsedSourceMetadata !== 'object') {
|
||||
setErrorMessage('Source metadata must be a JSON object')
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
setErrorMessage('Source metadata must be valid JSON')
|
||||
return
|
||||
}
|
||||
}
|
||||
if (datasetForm.provenanceMetadataJson) {
|
||||
try {
|
||||
const parsedProvenanceMetadata = JSON.parse(datasetForm.provenanceMetadataJson)
|
||||
if (parsedProvenanceMetadata === null || typeof parsedProvenanceMetadata !== 'object') {
|
||||
setErrorMessage('Provenance metadata must be a JSON object')
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
setErrorMessage('Provenance metadata must be valid JSON')
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
await datasetsApi.upload(selectedProjectId, {
|
||||
file: datasetForm.file,
|
||||
datasetType: datasetForm.datasetType,
|
||||
source: datasetForm.source,
|
||||
datasetRole: datasetForm.datasetRole,
|
||||
sourceName: datasetForm.sourceName || undefined,
|
||||
referenceLayerName: datasetForm.referenceLayerName || undefined,
|
||||
sourceMetadataJson: datasetForm.sourceMetadataJson || undefined,
|
||||
provenanceMetadataJson: datasetForm.provenanceMetadataJson || undefined,
|
||||
areaId: datasetForm.areaId || undefined,
|
||||
})
|
||||
setDatasetForm((previous) => ({ ...previous, file: null }))
|
||||
await loadProjectData(selectedProjectId)
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : 'Failed to upload dataset')
|
||||
}
|
||||
}
|
||||
|
||||
const refreshSelectedDatasetAfterJob = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
await Promise.all([loadDatasetJobs(selectedProjectId, selectedDatasetId), loadProjectData(selectedProjectId)])
|
||||
const refreshed = datasets.find((dataset) => dataset.id === selectedDatasetId)
|
||||
if (refreshed) {
|
||||
loadDatasetDetails(selectedProjectId, refreshed).catch(() => null)
|
||||
}
|
||||
}
|
||||
|
||||
const runVectorClip = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
if (!selectedClipAreaId) {
|
||||
setDatasetDetailError('Select an area for clipping')
|
||||
return
|
||||
}
|
||||
setDatasetDetailError(null)
|
||||
try {
|
||||
await datasetsApi.vectorClip(selectedProjectId, selectedDatasetId, {
|
||||
area_id: selectedClipAreaId,
|
||||
output_name: `${selectedDataset?.name.replace(/\.geojson$/, '')}-clipped`,
|
||||
})
|
||||
await refreshSelectedDatasetAfterJob()
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Vector clip failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const runVectorBuffer = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
setDatasetDetailError(null)
|
||||
try {
|
||||
await datasetsApi.vectorBuffer(selectedProjectId, selectedDatasetId, {
|
||||
distance_m: 25,
|
||||
dissolve: false,
|
||||
})
|
||||
await refreshSelectedDatasetAfterJob()
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Vector buffer failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const runVectorIntersect = async (availableVectorTargets: DatasetCreateResponse[]) => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
const targetId = selectedIntersectTargetId || availableVectorTargets[0]?.id
|
||||
if (!targetId) {
|
||||
setDatasetDetailError('Select an intersect target dataset')
|
||||
return
|
||||
}
|
||||
setDatasetDetailError(null)
|
||||
try {
|
||||
await datasetsApi.vectorIntersect(selectedProjectId, selectedDatasetId, {
|
||||
other_dataset_id: targetId,
|
||||
})
|
||||
await refreshSelectedDatasetAfterJob()
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Vector intersect failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const runRasterInspect = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const response = await datasetsApi.inspectRaster(selectedProjectId, selectedDatasetId)
|
||||
setSelectedRasterMetadata(toRasterMetadata(response.metadata))
|
||||
setDatasetDetailError(null)
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Raster inspect unavailable'))
|
||||
}
|
||||
}
|
||||
|
||||
const runRasterPreview = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const response = await datasetsApi.rasterPreview(selectedProjectId, selectedDatasetId)
|
||||
setRasterPreview(response)
|
||||
if (response.metadata) {
|
||||
setSelectedRasterMetadata(toRasterMetadata(response.metadata))
|
||||
}
|
||||
setDatasetDetailError(null)
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Raster preview unavailable'))
|
||||
setRasterPreview(null)
|
||||
}
|
||||
}
|
||||
|
||||
const runRasterStats = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const response = await datasetsApi.rasterStats(selectedProjectId, selectedDatasetId)
|
||||
setSelectedRasterStats(response)
|
||||
setDatasetDetailError(null)
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Raster stats unavailable'))
|
||||
setSelectedRasterStats(null)
|
||||
}
|
||||
}
|
||||
|
||||
const runRasterReproject = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
const targetCrs = rasterReprojectCrs.trim()
|
||||
if (!targetCrs) {
|
||||
setDatasetDetailError('Target CRS is required for raster reproject')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await datasetsApi.rasterReproject(selectedProjectId, selectedDatasetId, {
|
||||
target_crs: targetCrs,
|
||||
resampling: rasterReprojectResampling,
|
||||
})
|
||||
await refreshSelectedDatasetAfterJob()
|
||||
setDatasetDetailError(null)
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Raster reproject failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const runRasterClip = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
if (!selectedClipAreaId) {
|
||||
setDatasetDetailError('Select an area for raster clip')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await datasetsApi.rasterClip(selectedProjectId, selectedDatasetId, {
|
||||
area_id: selectedClipAreaId,
|
||||
output_name: `raster-clipped-${selectedDatasetId}`,
|
||||
})
|
||||
await refreshSelectedDatasetAfterJob()
|
||||
setDatasetDetailError(null)
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Raster clip failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const runRasterTile = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await datasetsApi.rasterTile(selectedProjectId, selectedDatasetId, {
|
||||
tile_size: rasterTileSize,
|
||||
overlap: rasterTileOverlap,
|
||||
output_name: rasterTileOutputName || undefined,
|
||||
})
|
||||
await refreshSelectedDatasetAfterJob()
|
||||
setDatasetDetailError(null)
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Raster tile failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const runRasterNdvi = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await datasetsApi.rasterNdvi(selectedProjectId, selectedDatasetId, {
|
||||
nir_band: ndviNirBand,
|
||||
red_band: ndviRedBand,
|
||||
})
|
||||
await refreshSelectedDatasetAfterJob()
|
||||
setDatasetDetailError(null)
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Raster NDVI failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const runRasterNdwi = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await datasetsApi.rasterNdwi(selectedProjectId, selectedDatasetId, {
|
||||
green_band: ndwiGreenBand,
|
||||
nir_band: ndwiNirBand,
|
||||
})
|
||||
await refreshSelectedDatasetAfterJob()
|
||||
setDatasetDetailError(null)
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Raster NDWI failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const runRasterNdbi = async () => {
|
||||
if (!selectedProjectId || !selectedDatasetId) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await datasetsApi.rasterNdbi(selectedProjectId, selectedDatasetId, {
|
||||
swir_band: ndbiSwirBand,
|
||||
nir_band: ndbiNirBand,
|
||||
})
|
||||
await refreshSelectedDatasetAfterJob()
|
||||
setDatasetDetailError(null)
|
||||
} catch (error) {
|
||||
setDatasetDetailError(formatError(error, 'Raster NDBI failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const pickDerivedDataset = async (datasetId: string) => {
|
||||
if (!selectedProjectId) {
|
||||
return
|
||||
}
|
||||
const target = datasets.find((item) => item.id === datasetId)
|
||||
if (target) {
|
||||
await loadDatasetDetails(selectedProjectId, target)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshMetadata = async (datasetId: string) => {
|
||||
if (!selectedProjectId) {
|
||||
return
|
||||
}
|
||||
const refreshed = await datasetsApi.refreshMetadata(selectedProjectId, datasetId)
|
||||
await loadProjectData(selectedProjectId)
|
||||
if (selectedDataset?.id === datasetId) {
|
||||
setSelectedDataset(refreshed)
|
||||
if (isVectorDatasetType(refreshed.dataset_type)) {
|
||||
loadDatasetDetails(selectedProjectId, refreshed).catch(() => null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resetDatasetForProject = () => {
|
||||
setSelectedDatasetId(null)
|
||||
setSelectedDataset(null)
|
||||
setSelectedDatasetSummary(null)
|
||||
setSelectedRasterMetadata(null)
|
||||
setSelectedRasterStats(null)
|
||||
setDatasetContent(null)
|
||||
setRasterPreview(null)
|
||||
setJobs([])
|
||||
}
|
||||
|
||||
return {
|
||||
selectedDatasetId,
|
||||
selectedDataset,
|
||||
selectedDatasetSummary,
|
||||
selectedRasterMetadata,
|
||||
selectedRasterStats,
|
||||
datasetContent,
|
||||
jobs,
|
||||
rasterPreview,
|
||||
selectedIntersectTargetId,
|
||||
selectedClipAreaId,
|
||||
rasterTileSize,
|
||||
rasterTileOverlap,
|
||||
rasterTileOutputName,
|
||||
rasterReprojectCrs,
|
||||
rasterReprojectResampling,
|
||||
ndviNirBand,
|
||||
ndviRedBand,
|
||||
ndwiGreenBand,
|
||||
ndwiNirBand,
|
||||
ndbiSwirBand,
|
||||
ndbiNirBand,
|
||||
loadingDatasetDetails,
|
||||
datasetDetailError,
|
||||
datasetForm,
|
||||
isRasterTileInputValid,
|
||||
loadDatasetJobs,
|
||||
loadDatasetDetails,
|
||||
uploadDataset,
|
||||
runVectorClip,
|
||||
runVectorBuffer,
|
||||
runVectorIntersect,
|
||||
runRasterInspect,
|
||||
runRasterPreview,
|
||||
runRasterStats,
|
||||
runRasterReproject,
|
||||
runRasterClip,
|
||||
runRasterTile,
|
||||
runRasterNdvi,
|
||||
runRasterNdwi,
|
||||
runRasterNdbi,
|
||||
pickDerivedDataset,
|
||||
refreshMetadata,
|
||||
resetDatasetForProject,
|
||||
setSelectedDatasetId,
|
||||
setSelectedDataset,
|
||||
setSelectedIntersectTargetId,
|
||||
setSelectedClipAreaId,
|
||||
setRasterTileSize,
|
||||
setRasterTileOverlap,
|
||||
setRasterTileOutputName,
|
||||
setRasterReprojectCrs,
|
||||
setRasterReprojectResampling,
|
||||
setNdviNirBand,
|
||||
setNdviRedBand,
|
||||
setNdwiGreenBand,
|
||||
setNdwiNirBand,
|
||||
setNdbiSwirBand,
|
||||
setNdbiNirBand,
|
||||
setDatasetForm,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user