512 lines
16 KiB
TypeScript
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,
|
|
}
|
|
}
|