import { FormEvent, useEffect, useMemo, useRef, useState } from 'react' import { datasetsApi, jobsApi } from '../services/api' import type { AreaRead, DatasetCreateResponse, JobRead, RasterTileHandoff, RasterMetadataResponse, RasterPreviewResponse, RasterStatsResponse, VectorSummary, } from '../types' import { formatError } from '../lib/formatError' import { isPrimaryFocusMunicipalityBoundaryDataset } from '../config/primaryFocus' import { VECTOR_VIEWPORT_FEATURE_THRESHOLD } from '../config/vectorDelivery' 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 | null | undefined): RasterMetadataResponse | null { if (!metadata) { return null } return metadata as unknown as RasterMetadataResponse } function isOperationalScopeBoundaryDataset(dataset: DatasetCreateResponse): boolean { return ( dataset.status === 'ready' && (dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson') && dataset.source_metadata?.layer_type === 'regional_boundary' ) } function extractRasterTileManifestPath(job: JobRead | null | undefined): string { const manifest = toRasterTileHandoff(job) return manifest?.manifest_path ?? '' } function optionalString(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value : null } function optionalNumber(value: unknown): number | null { return typeof value === 'number' && Number.isFinite(value) ? value : null } function toRasterTileHandoff(job: JobRead | null | undefined): RasterTileHandoff | null { const result = job?.result_json if (!result) { return null } const manifestPath = result.manifest_path if (typeof manifestPath !== 'string' || manifestPath.trim().length === 0) { return null } const manifest = typeof result.manifest === 'object' && result.manifest !== null ? result.manifest as Record : {} return { manifest_path: manifestPath, tile_set_id: optionalString(result.tile_set_id) ?? optionalString(manifest.tile_set_id), tile_size: optionalNumber(result.tile_size) ?? optionalNumber(manifest.tile_size), overlap: optionalNumber(result.overlap) ?? optionalNumber(manifest.overlap), count: optionalNumber(result.count) ?? optionalNumber(manifest.count), source_dataset_id: optionalString(result.source_dataset_id) ?? optionalString(manifest.source_dataset_id), created_at: optionalString(manifest.created_at), } } export function useDatasetWorkflow({ selectedProjectId, areas, datasets, loadProjectData, setErrorMessage, isVectorDatasetType, }: DatasetWorkflowOptions) { const [selectedDatasetId, setSelectedDatasetId] = useState(null) const [selectedDataset, setSelectedDataset] = useState(null) const [selectedDatasetSummary, setSelectedDatasetSummary] = useState(null) const [selectedRasterMetadata, setSelectedRasterMetadata] = useState(null) const [selectedRasterStats, setSelectedRasterStats] = useState(null) const [datasetContent, setDatasetContent] = useState(null) const [jobs, setJobs] = useState([]) const [rasterPreview, setRasterPreview] = useState(null) const [latestRasterTileManifestPath, setLatestRasterTileManifestPath] = useState('') const [latestRasterTileManifest, setLatestRasterTileManifest] = useState(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(null) const datasetDetailRequestSequence = useRef(0) 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(isOperationalScopeBoundaryDataset) ?? datasets.find(isPrimaryFocusMunicipalityBoundaryDataset) ?? 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, detailRequestId?: number) => { const response = await jobsApi.list(projectId, { dataset_id: datasetId, limit: 20, offset: 0 }) if (detailRequestId != null && detailRequestId !== datasetDetailRequestSequence.current) { return } setJobs(response.items) const latestRasterTileJob = response.items.find( (job) => job.job_type === 'raster.tile' && extractRasterTileManifestPath(job), ) const manifest = toRasterTileHandoff(latestRasterTileJob) const manifestPath = manifest ? manifest.manifest_path : '' setLatestRasterTileManifest(manifest) setLatestRasterTileManifestPath(manifestPath) } const loadDatasetDetails = async (projectId: string, dataset: DatasetCreateResponse) => { const detailRequestId = ++datasetDetailRequestSequence.current setLoadingDatasetDetails(true) setDatasetDetailError(null) setSelectedDataset(dataset) setSelectedDatasetSummary(null) setSelectedRasterMetadata(null) setSelectedRasterStats(null) setDatasetContent(null) setRasterPreview(null) setLatestRasterTileManifestPath('') setLatestRasterTileManifest(null) setSelectedDatasetId(dataset.id) setJobs([]) try { if (isVectorDatasetType(dataset.dataset_type)) { const summary = await datasetsApi.vectorSummary(projectId, dataset.id) if (detailRequestId !== datasetDetailRequestSequence.current) { return } setSelectedDatasetSummary(summary) const featureCount = summary.feature_count ?? dataset.feature_count if (featureCount == null || featureCount <= VECTOR_VIEWPORT_FEATURE_THRESHOLD) { const content = await datasetsApi.getContent(projectId, dataset.id) if (detailRequestId !== datasetDetailRequestSequence.current) { return } setDatasetContent(content) } } else if (dataset.dataset_type === 'raster') { try { const rasterInspection = await datasetsApi.rasterInspect(projectId, dataset.id) if (detailRequestId !== datasetDetailRequestSequence.current) { return } setSelectedRasterMetadata(toRasterMetadata(rasterInspection.metadata)) } catch (error) { if (detailRequestId !== datasetDetailRequestSequence.current) { return } setSelectedRasterMetadata(null) setDatasetDetailError(formatError(error, 'Raster metadata unavailable')) } } await loadDatasetJobs(projectId, dataset.id, detailRequestId) } catch (error) { if (detailRequestId === datasetDetailRequestSequence.current) { setDatasetDetailError(formatError(error, 'Unable to load dataset detail')) } } finally { if (detailRequestId === datasetDetailRequestSequence.current) { 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 { const job = await datasetsApi.rasterTile(selectedProjectId, selectedDatasetId, { tile_size: rasterTileSize, overlap: rasterTileOverlap, output_name: rasterTileOutputName || undefined, }) const manifest = toRasterTileHandoff(job) const manifestPath = manifest?.manifest_path ?? '' setLatestRasterTileManifest(manifest) setLatestRasterTileManifestPath(manifestPath) await refreshSelectedDatasetAfterJob() if (manifestPath) { setLatestRasterTileManifest(manifest) setLatestRasterTileManifestPath(manifestPath) } 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 = () => { datasetDetailRequestSequence.current += 1 setSelectedDatasetId(null) setSelectedDataset(null) setSelectedDatasetSummary(null) setSelectedRasterMetadata(null) setSelectedRasterStats(null) setDatasetContent(null) setRasterPreview(null) setLatestRasterTileManifestPath('') setLatestRasterTileManifest(null) setJobs([]) setLoadingDatasetDetails(false) setDatasetDetailError(null) } return { selectedDatasetId, selectedDataset, selectedDatasetSummary, selectedRasterMetadata, selectedRasterStats, datasetContent, jobs, rasterPreview, latestRasterTileManifestPath, latestRasterTileManifest, 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, } }