feat: guide raster building analysis workflow
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-15 00:19:38 +02:00
parent d528677e03
commit 2f9898bc82
12 changed files with 598 additions and 64 deletions
+165 -15
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { detectionApi } from '../services/api'
import { datasetsApi, detectionApi } from '../services/api'
import type {
DatasetCreateResponse,
DetectionModelCapability,
@@ -7,6 +7,7 @@ import type {
DetectionRead,
DetectionRunRead,
DetectionRunResponse,
JobRead,
ModelAssetRead,
QualityCheckRead,
YoloPreflightResponse,
@@ -26,6 +27,17 @@ interface DetectionOperatorProfileSelection {
confidenceThreshold: number
}
export type DetectionWorkflowStage =
| 'idle'
| 'uploading'
| 'ready'
| 'tiling'
| 'validating'
| 'detecting'
| 'loading'
| 'complete'
| 'failed'
export interface DetectionCalibrationRunRow {
threshold: number
status: 'queued' | 'running' | 'success' | 'failed'
@@ -59,6 +71,21 @@ function parseCalibrationThresholds(value: string): number[] {
return thresholds
}
function tileManifestPathFromJob(job: JobRead): string | null {
const manifestPath = job.result_json?.manifest_path
return typeof manifestPath === 'string' && manifestPath.trim().length > 0 ? manifestPath.trim() : null
}
function rasterTileCount(metadata: Record<string, unknown>, tileSize: number, overlap: number): number | null {
const width = metadata.width
const height = metadata.height
if (typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0) {
return null
}
const step = tileSize - overlap
return Math.ceil(width / step) * Math.ceil(height / step)
}
export function useDetectionWorkflow({
selectedProjectId,
rasterDatasets,
@@ -97,6 +124,7 @@ export function useDetectionWorkflow({
const [runningDetectionCalibration, setRunningDetectionCalibration] = useState(false)
const [detectionCalibrationRows, setDetectionCalibrationRows] = useState<DetectionCalibrationRunRow[]>([])
const [detectionCalibrationError, setDetectionCalibrationError] = useState<string | null>(null)
const [detectionWorkflowStage, setDetectionWorkflowStage] = useState<DetectionWorkflowStage>('idle')
useEffect(() => {
if (!selectedDetectionDatasetId && rasterDatasets.length > 0) {
@@ -201,6 +229,25 @@ export function useDetectionWorkflow({
}
}
const executeDetection = async (projectId: string, datasetId: string, manifestPath: string | null) => {
const result = await detectionApi.run({
project_id: projectId,
dataset_id: datasetId,
model_id: selectedDetectionModelId,
model_asset_id: selectedModelAssetId || null,
confidence_threshold: detectionConfidenceThreshold,
tile_manifest_path: manifestPath,
parameters_json: {},
})
setDetectionRunResult(result)
setSelectedDetectionRunId(result.analysis_run_id)
setDetectionWorkflowStage('loading')
await loadDetectionRuns(projectId)
await loadDetectionResults(result.analysis_run_id)
await loadProjectData(projectId)
return result
}
const runDetection = async () => {
if (!selectedProjectId) {
setDetectionRunError('Select a project first')
@@ -214,23 +261,122 @@ export function useDetectionWorkflow({
setDetectionRunError(null)
setDetectionRunResult(null)
setRunningDetection(true)
setDetectionWorkflowStage('detecting')
try {
const result = await detectionApi.run({
project_id: selectedProjectId,
dataset_id: datasetId,
model_id: selectedDetectionModelId,
model_asset_id: selectedModelAssetId || null,
confidence_threshold: detectionConfidenceThreshold,
tile_manifest_path: detectionTileManifestPath.trim() || null,
parameters_json: {},
})
setDetectionRunResult(result)
setSelectedDetectionRunId(result.analysis_run_id)
await loadDetectionRuns(selectedProjectId)
await loadDetectionResults(result.analysis_run_id)
await loadProjectData(selectedProjectId)
await executeDetection(selectedProjectId, datasetId, detectionTileManifestPath.trim() || null)
setDetectionWorkflowStage('complete')
} catch (error) {
setDetectionRunError(formatError(error, 'Detection run failed'))
setDetectionWorkflowStage('failed')
} finally {
setRunningDetection(false)
}
}
const uploadDetectionRaster = async (file: File): Promise<boolean> => {
if (!selectedProjectId) {
setDetectionRunError('De regionale werkruimte is nog niet geladen')
return false
}
setDetectionRunError(null)
setDetectionWorkflowStage('uploading')
try {
const dataset = await datasetsApi.upload(selectedProjectId, {
file,
datasetType: 'raster',
source: 'user_upload',
datasetRole: 'source',
sourceName: 'manual',
sourceMetadataJson: JSON.stringify({ purpose: 'building_detection' }),
provenanceMetadataJson: JSON.stringify({ original_filename: file.name, acquisition: 'explicit_user_upload' }),
})
setSelectedDetectionDatasetId(dataset.id)
setDetectionTileManifestPath('')
setDetectionRunResult(null)
setDetectionWorkflowStage('ready')
await loadProjectData(selectedProjectId)
return true
} catch (error) {
setDetectionRunError(formatError(error, 'Het luchtbeeld kon niet worden toegevoegd'))
setDetectionWorkflowStage('failed')
return false
}
}
const prepareAndRunDetection = async (): Promise<boolean> => {
if (!selectedProjectId) {
setDetectionRunError('De regionale werkruimte is nog niet geladen')
return false
}
const datasetId = selectedDetectionDatasetId || rasterDatasets[0]?.id
if (!datasetId) {
setDetectionRunError('Kies of voeg eerst een gegeorefereerd luchtbeeld toe')
return false
}
const selectedModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId)
if (!selectedModel?.configured || selectedDetectionModelId === 'manual-fixture-detector') {
setDetectionRunError(selectedModel?.limitation_message ?? 'Het gekozen analysemodel is niet beschikbaar')
return false
}
if (selectedDetectionModelId === 'yolo-configured' && modelAssets.length > 0 && !selectedModelAssetId) {
setDetectionRunError('Kies eerst een lokaal modelbestand')
return false
}
setDetectionRunError(null)
setDetectionRunResult(null)
setRunningDetection(true)
try {
let manifestPath = detectionTileManifestPath.trim()
if (!manifestPath) {
setDetectionWorkflowStage('tiling')
const inspection = await datasetsApi.rasterInspect(selectedProjectId, datasetId)
const expectedTileCount = rasterTileCount(inspection.metadata, 512, 64)
const maxTiles = yoloPreflight?.max_tiles ?? 256
if (expectedTileCount === null) {
throw new Error('De afmetingen van het luchtbeeld konden niet veilig worden bepaald')
}
if (expectedTileCount > maxTiles) {
throw new Error(
`Dit luchtbeeld zou ${expectedTileCount} beeldtegels maken; het veilige maximum is ${maxTiles}. Knip het beeld eerst tot het gewenste werkgebied.`,
)
}
const tileJob = await datasetsApi.rasterTile(selectedProjectId, datasetId, {
tile_size: 512,
overlap: 64,
})
manifestPath = tileManifestPathFromJob(tileJob) ?? ''
if (!manifestPath) {
throw new Error(tileJob.error_message || 'De tegelvoorbereiding leverde geen geldig manifest op')
}
setDetectionTileManifestPath(manifestPath)
}
setDetectionWorkflowStage('validating')
const preflight = await detectionApi.getYoloPreflight({
tile_manifest_path: manifestPath,
model_asset_id: selectedModelAssetId || null,
})
setYoloPreflight(preflight)
setYoloPreflightError(null)
if (
!preflight.checks.manifest_valid ||
!preflight.checks.tile_paths_exist ||
!preflight.checks.tile_limit_ok ||
!preflight.checks.dependencies_available ||
!preflight.checks.model_file_exists
) {
throw new Error(preflight.message || 'De beeldtegels of modelruntime zijn niet startklaar')
}
setDetectionWorkflowStage('detecting')
await executeDetection(selectedProjectId, datasetId, manifestPath)
setDetectionWorkflowStage('complete')
return true
} catch (error) {
setDetectionRunError(formatError(error, 'De beeldanalyse is mislukt'))
setDetectionWorkflowStage('failed')
return false
} finally {
setRunningDetection(false)
}
@@ -371,6 +517,7 @@ export function useDetectionWorkflow({
setDetectionRunResult(null)
setDetectionCalibrationRows([])
setDetectionCalibrationError(null)
setDetectionWorkflowStage('idle')
}
return {
@@ -405,11 +552,14 @@ export function useDetectionWorkflow({
runningDetectionCalibration,
detectionCalibrationRows,
detectionCalibrationError,
detectionWorkflowStage,
loadDetectionModels,
loadYoloPreflight,
loadDetectionRuns,
loadDetectionResults,
runDetection,
uploadDetectionRaster,
prepareAndRunDetection,
runDetectionQa,
runDetectionCalibration,
applyDetectionOperatorProfile,