Files
geointel/frontend/src/hooks/useDatasetWorkflow.ts
T
Codex aaf1299a5d
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled
Improve useful default workbench context
2026-06-22 23:19:35 +02:00

512 lines
16 KiB
TypeScript

import { FormEvent, useEffect, useMemo, useState } from 'react'
import { datasetsApi, jobsApi } from '../services/api'
import type {
AreaRead,
DatasetCreateResponse,
JobRead,
RasterMetadataResponse,
RasterPreviewResponse,
RasterStatsResponse,
VectorSummary,
} from '../types'
import { formatError } from '../lib/formatError'
interface DatasetWorkflowOptions {
selectedProjectId: string | null
areas: AreaRead[]
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,
areas,
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],
)
useEffect(() => {
if (areas.length === 0) {
setSelectedClipAreaId('')
} else if (!selectedClipAreaId || !areas.some((area) => area.id === selectedClipAreaId)) {
setSelectedClipAreaId(areas[0].id)
}
}, [areas, selectedClipAreaId])
useEffect(() => {
if (!selectedProjectId || selectedDatasetId || datasets.length === 0) {
return
}
// Auto-open the first usable dataset so Data, Map and Exports start with real context.
const defaultDataset =
datasets.find((dataset) => isVectorDatasetType(dataset.dataset_type) && dataset.status === 'ready') ??
datasets.find((dataset) => dataset.status === 'ready') ??
datasets[0]
if (defaultDataset) {
loadDatasetDetails(selectedProjectId, defaultDataset).catch(() => null)
}
}, [datasets, isVectorDatasetType, selectedDatasetId, selectedProjectId])
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,
}
}