import { useEffect, useMemo, useRef, useState } from 'react' import { segmentationApi } from '../services/api' import type { DatasetCreateResponse, JobRead, QualityCheckRead, SegmentationModelCapability, SegmentationQaResult, SegmentationRead, SegmentationRunRead, SegmentationRunResponse, } from '../types' import { formatError } from '../lib/formatError' import { analysisRunIdFromSegmentationJob, completedSegmentationResponse, SegmentationJobError, waitForSegmentationJob, } from '../services/segmentationJob' interface SegmentationWorkflowOptions { selectedProjectId: string | null rasterDatasets: DatasetCreateResponse[] qaIouThreshold: number loadProjectData: (projectId: string) => Promise loadQualityChecks: (projectId?: string | null) => Promise } function isAbortError(error: unknown): boolean { return error instanceof Error && error.name === 'AbortError' } function abortedError(): Error { const error = new Error('Het volgen van de segmentatietaak is gestopt') error.name = 'AbortError' return error } export function useSegmentationWorkflow({ selectedProjectId, rasterDatasets, qaIouThreshold, loadProjectData, loadQualityChecks, }: SegmentationWorkflowOptions) { const [segmentationModels, setSegmentationModels] = useState([]) const [loadingSegmentationModels, setLoadingSegmentationModels] = useState(false) const [segmentationModelError, setSegmentationModelError] = useState(null) const [selectedSegmentationDatasetId, setSelectedSegmentationDatasetId] = useState('') const [selectedSegmentationModelId, setSelectedSegmentationModelId] = useState('segmentation-placeholder') const [segmentationTileManifestPath, setSegmentationTileManifestPath] = useState('') const [segmentationConfidenceThreshold, setSegmentationConfidenceThreshold] = useState(0.5) const [runningSegmentation, setRunningSegmentation] = useState(false) const [segmentationJob, setSegmentationJob] = useState(null) const [segmentationRunResult, setSegmentationRunResult] = useState(null) const [segmentationRunError, setSegmentationRunError] = useState(null) const [segmentationRuns, setSegmentationRuns] = useState([]) const [selectedSegmentationRunId, setSelectedSegmentationRunId] = useState('') const [segmentationItems, setSegmentationItems] = useState([]) const [segmentationTotal, setSegmentationTotal] = useState(0) const [segmentationTruncated, setSegmentationTruncated] = useState(false) const [segmentationGeoJson, setSegmentationGeoJson] = useState(null) const [segmentationClassFilter, setSegmentationClassFilter] = useState('') const [segmentationMinConfidenceFilter, setSegmentationMinConfidenceFilter] = useState(0) const [loadingSegmentationResults, setLoadingSegmentationResults] = useState(false) const [segmentationReferenceDatasetId, setSegmentationReferenceDatasetId] = useState('') const [segmentationQaResult, setSegmentationQaResult] = useState(null) const [segmentationQaError, setSegmentationQaError] = useState(null) const [runningSegmentationQa, setRunningSegmentationQa] = useState(false) const activeSegmentationControllerRef = useRef(null) const selectedProjectIdRef = useRef(selectedProjectId) const segmentationExecutionSequence = useRef(0) const segmentationRunsRequestSequence = useRef(0) const segmentationResultsRequestSequence = useRef(0) const segmentationQaRequestSequence = useRef(0) selectedProjectIdRef.current = selectedProjectId useEffect(() => { activeSegmentationControllerRef.current?.abort() activeSegmentationControllerRef.current = null segmentationExecutionSequence.current += 1 segmentationRunsRequestSequence.current += 1 segmentationResultsRequestSequence.current += 1 segmentationQaRequestSequence.current += 1 setSelectedSegmentationDatasetId('') setSegmentationRuns([]) setSelectedSegmentationRunId('') setSegmentationItems([]) setSegmentationTotal(0) setSegmentationTruncated(false) setSegmentationGeoJson(null) setSegmentationRunResult(null) setSegmentationRunError(null) setSegmentationJob(null) setRunningSegmentation(false) setLoadingSegmentationResults(false) setSegmentationTileManifestPath('') setSegmentationQaResult(null) setSegmentationQaError(null) setRunningSegmentationQa(false) return () => { activeSegmentationControllerRef.current?.abort() } }, [selectedProjectId]) const selectedSegmentationModel = useMemo( () => segmentationModels.find((model) => model.model_id === selectedSegmentationModelId) ?? null, [segmentationModels, selectedSegmentationModelId], ) const loadSegmentationModels = async () => { setLoadingSegmentationModels(true) setSegmentationModelError(null) try { const response = await segmentationApi.listModels() setSegmentationModels(response.models) const selectionStillAvailable = response.models.some((model) => model.model_id === selectedSegmentationModelId) const selectionConfigured = response.models.some( (model) => model.model_id === selectedSegmentationModelId && model.configured, ) if ((!selectionStillAvailable || !selectionConfigured) && response.models.length > 0) { const configuredModel = response.models.find( (model) => model.configured && model.model_id !== 'fixture-segmenter', ) setSelectedSegmentationModelId( configuredModel?.model_id ?? (selectionStillAvailable ? selectedSegmentationModelId : response.models[0].model_id), ) } } catch (error) { setSegmentationModelError(formatError(error, 'De segmentatiemodellen konden niet worden geladen')) } finally { setLoadingSegmentationModels(false) } } const loadSegmentationRuns = async (projectId = selectedProjectId) => { const sequence = segmentationRunsRequestSequence.current + 1 segmentationRunsRequestSequence.current = sequence if (!projectId) { setSegmentationRuns([]) setSelectedSegmentationRunId('') return } try { const response = await segmentationApi.listRuns({ project_id: projectId }) if ( segmentationRunsRequestSequence.current !== sequence || selectedProjectIdRef.current !== projectId ) return setSegmentationRuns(response.items) setSelectedSegmentationRunId((current) => ( response.items.some((run) => run.id === current) ? current : response.items[0]?.id ?? '' )) } catch (error) { if ( segmentationRunsRequestSequence.current === sequence && selectedProjectIdRef.current === projectId ) { setSegmentationRunError(formatError(error, 'De segmentatieruns konden niet worden geladen')) } } } const loadSegmentationResults = async (analysisRunId = selectedSegmentationRunId) => { const sequence = segmentationResultsRequestSequence.current + 1 segmentationResultsRequestSequence.current = sequence const requestProjectId = selectedProjectIdRef.current if (!analysisRunId || !requestProjectId) { setSegmentationItems([]) setSegmentationTotal(0) setSegmentationTruncated(false) setSegmentationGeoJson(null) setLoadingSegmentationResults(false) return } setLoadingSegmentationResults(true) setSegmentationRunError(null) setSegmentationItems([]) setSegmentationTotal(0) setSegmentationTruncated(false) setSegmentationGeoJson(null) try { const params = { project_id: requestProjectId, class_name: segmentationClassFilter || null, min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null, } const [segmentationsResponse, geoJsonResponse] = await Promise.all([ segmentationApi.listSegmentations(analysisRunId, params), segmentationApi.getRunGeoJson(analysisRunId, params), ]) if ( segmentationResultsRequestSequence.current !== sequence || selectedProjectIdRef.current !== requestProjectId ) return if (segmentationsResponse.items.some((item) => ( item.project_id !== requestProjectId || item.analysis_run_id !== analysisRunId ))) { throw new Error('De server retourneerde segmentaties uit een andere werkruimte of analyserun') } setSegmentationItems(segmentationsResponse.items) setSegmentationTotal(segmentationsResponse.total) setSegmentationTruncated(Boolean(segmentationsResponse.truncated)) setSegmentationGeoJson(geoJsonResponse) } catch (error) { if ( segmentationResultsRequestSequence.current === sequence && selectedProjectIdRef.current === requestProjectId ) { setSegmentationRunError(formatError(error, 'De segmentatieresultaten konden niet worden geladen')) } } finally { if ( segmentationResultsRequestSequence.current === sequence && selectedProjectIdRef.current === requestProjectId ) { setLoadingSegmentationResults(false) } } } const runSegmentation = async () => { if (!selectedProjectId) { setSegmentationRunError('Kies eerst een werkruimte') return } const datasetId = selectedSegmentationDatasetId || rasterDatasets[0]?.id if (!datasetId) { setSegmentationRunError('Kies eerst een rasterbron') return } if (!selectedSegmentationModel?.configured) { setSegmentationRunError('Het gekozen segmentatiemodel is niet geconfigureerd') return } if (selectedSegmentationModelId === 'fixture-segmenter') { setSegmentationRunError('Het fixturemodel is uitsluitend beschikbaar voor expliciete geautomatiseerde tests') return } if (!segmentationTileManifestPath.trim()) { setSegmentationRunError('Koppel eerst het beeldtegelmanifest van het gekozen rasterbestand') return } if ( (activeSegmentationControllerRef.current && !activeSegmentationControllerRef.current.signal.aborted) || segmentationJob?.status === 'queued' || segmentationJob?.status === 'running' ) { setSegmentationRunError('Er wordt al een GPU-segmentatietaak verwerkt. Wacht tot die taak klaar is.') return } const projectId = selectedProjectId const parameters: Record = {} const request = { project_id: projectId, dataset_id: datasetId, model_id: selectedSegmentationModelId, confidence_threshold: segmentationConfidenceThreshold, tile_manifest_path: segmentationTileManifestPath.trim() || null, parameters_json: parameters, } const controller = new AbortController() const executionSequence = segmentationExecutionSequence.current + 1 segmentationExecutionSequence.current = executionSequence activeSegmentationControllerRef.current = controller const assertExecutionCurrent = () => { if ( controller.signal.aborted || segmentationExecutionSequence.current !== executionSequence || selectedProjectIdRef.current !== projectId ) { throw abortedError() } } setSegmentationRunError(null) setSegmentationRunResult(null) setRunningSegmentation(true) setSegmentationJob(null) try { const queuedJob = await segmentationApi.runAsync(request) assertExecutionCurrent() setSegmentationJob(queuedJob) const completedJob = await waitForSegmentationJob({ projectId, initialJob: queuedJob, signal: controller.signal, onStatus: (job) => { if ( segmentationExecutionSequence.current === executionSequence && selectedProjectIdRef.current === projectId ) { setSegmentationJob(job) } }, }) assertExecutionCurrent() const explicitAnalysisRunId = analysisRunIdFromSegmentationJob(completedJob) const run = explicitAnalysisRunId ? await segmentationApi.getRun(explicitAnalysisRunId, projectId) : (await segmentationApi.listRuns({ project_id: projectId, dataset_id: datasetId })).items .find((candidate) => candidate.job_id === completedJob.id) assertExecutionCurrent() if (!run) { throw new SegmentationJobError( 'De GPU-taak is voltooid, maar de bijbehorende bewaarde segmentatierun ontbreekt.', 'SEGMENTATION_RUN_RESULT_NOT_FOUND', completedJob.id, ) } const result = completedSegmentationResponse(request, completedJob, run) setSegmentationRunError(null) setSegmentationRunResult(result) setSelectedSegmentationRunId(result.analysis_run_id) await loadSegmentationRuns(projectId) assertExecutionCurrent() await loadSegmentationResults(result.analysis_run_id) assertExecutionCurrent() await loadProjectData(projectId) } catch (error) { if ( !isAbortError(error) && segmentationExecutionSequence.current === executionSequence && selectedProjectIdRef.current === projectId ) { setSegmentationRunError(formatError(error, 'De segmentatie is mislukt')) } } finally { if (activeSegmentationControllerRef.current === controller) { activeSegmentationControllerRef.current = null } if ( segmentationExecutionSequence.current === executionSequence && selectedProjectIdRef.current === projectId ) { setRunningSegmentation(false) } } } const runSegmentationQa = async () => { if (!selectedSegmentationRunId) { setSegmentationQaError('Kies eerst een segmentatierun') return } if (!segmentationReferenceDatasetId) { setSegmentationQaError('Kies eerst een referentiebron') return } const projectId = selectedProjectIdRef.current if (!projectId) { setSegmentationQaError('Kies eerst een werkruimte') return } const analysisRunId = selectedSegmentationRunId const referenceDatasetId = segmentationReferenceDatasetId const sequence = segmentationQaRequestSequence.current + 1 segmentationQaRequestSequence.current = sequence setSegmentationQaError(null) setSegmentationQaResult(null) setRunningSegmentationQa(true) try { const result = await segmentationApi.compareWithReference(analysisRunId, projectId, { reference_dataset_id: referenceDatasetId, iou_threshold: qaIouThreshold, class_name: segmentationClassFilter || null, min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null, }) if ( segmentationQaRequestSequence.current !== sequence || selectedProjectIdRef.current !== projectId ) return setSegmentationQaResult(result) await loadQualityChecks(projectId) } catch (error) { if ( segmentationQaRequestSequence.current === sequence && selectedProjectIdRef.current === projectId ) { setSegmentationQaError(formatError(error, 'De segmentatiecontrole is mislukt')) } } finally { if ( segmentationQaRequestSequence.current === sequence && selectedProjectIdRef.current === projectId ) { setRunningSegmentationQa(false) } } } const resetSegmentationForProject = () => { activeSegmentationControllerRef.current?.abort() activeSegmentationControllerRef.current = null segmentationExecutionSequence.current += 1 segmentationRunsRequestSequence.current += 1 segmentationResultsRequestSequence.current += 1 segmentationQaRequestSequence.current += 1 setSelectedSegmentationDatasetId('') setSegmentationRuns([]) setSelectedSegmentationRunId('') setSegmentationItems([]) setSegmentationTotal(0) setSegmentationTruncated(false) setSegmentationGeoJson(null) setSegmentationRunResult(null) setSegmentationRunError(null) setSegmentationJob(null) setRunningSegmentation(false) setLoadingSegmentationResults(false) setSegmentationTileManifestPath('') setSegmentationQaResult(null) setSegmentationQaError(null) setRunningSegmentationQa(false) } return { segmentationModels, loadingSegmentationModels, segmentationModelError, selectedSegmentationDatasetId, selectedSegmentationModelId, selectedSegmentationModel, segmentationTileManifestPath, segmentationConfidenceThreshold, runningSegmentation, segmentationJob, segmentationRunResult, segmentationRunError, segmentationRuns, selectedSegmentationRunId, segmentationItems, segmentationTotal, segmentationTruncated, segmentationGeoJson, segmentationClassFilter, segmentationMinConfidenceFilter, loadingSegmentationResults, segmentationReferenceDatasetId, segmentationQaResult, segmentationQaError, runningSegmentationQa, loadSegmentationModels, loadSegmentationRuns, loadSegmentationResults, runSegmentation, runSegmentationQa, resetSegmentationForProject, setSelectedSegmentationDatasetId, setSelectedSegmentationModelId, setSegmentationTileManifestPath, setSegmentationConfidenceThreshold, setSelectedSegmentationRunId, setSegmentationClassFilter, setSegmentationMinConfidenceFilter, setSegmentationReferenceDatasetId, } }