Files
geointel/frontend/src/hooks/useSegmentationWorkflow.ts
T

460 lines
17 KiB
TypeScript

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<unknown>
loadQualityChecks: (projectId?: string | null) => Promise<QualityCheckRead[] | void>
}
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<SegmentationModelCapability[]>([])
const [loadingSegmentationModels, setLoadingSegmentationModels] = useState(false)
const [segmentationModelError, setSegmentationModelError] = useState<string | null>(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<JobRead | null>(null)
const [segmentationRunResult, setSegmentationRunResult] = useState<SegmentationRunResponse | null>(null)
const [segmentationRunError, setSegmentationRunError] = useState<string | null>(null)
const [segmentationRuns, setSegmentationRuns] = useState<SegmentationRunRead[]>([])
const [selectedSegmentationRunId, setSelectedSegmentationRunId] = useState('')
const [segmentationItems, setSegmentationItems] = useState<SegmentationRead[]>([])
const [segmentationTotal, setSegmentationTotal] = useState(0)
const [segmentationTruncated, setSegmentationTruncated] = useState(false)
const [segmentationGeoJson, setSegmentationGeoJson] = useState<GeoJSON.FeatureCollection | null>(null)
const [segmentationClassFilter, setSegmentationClassFilter] = useState('')
const [segmentationMinConfidenceFilter, setSegmentationMinConfidenceFilter] = useState(0)
const [loadingSegmentationResults, setLoadingSegmentationResults] = useState(false)
const [segmentationReferenceDatasetId, setSegmentationReferenceDatasetId] = useState('')
const [segmentationQaResult, setSegmentationQaResult] = useState<SegmentationQaResult | null>(null)
const [segmentationQaError, setSegmentationQaError] = useState<string | null>(null)
const [runningSegmentationQa, setRunningSegmentationQa] = useState(false)
const activeSegmentationControllerRef = useRef<AbortController | null>(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<string, unknown> = {}
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,
}
}