import { FormEvent, useEffect, useMemo, useState } from 'react' import './styles/app.css' import GeoMap from './components/GeoMap' import { areasApi } from './services/api/areas' import { datasetsApi } from './services/api/datasets' import { projectsApi } from './services/api/projects' import { analysisApi, demoApi, detectionApi, jobsApi, externalApi, exportsApi, qaApi, segmentationApi } from './services/api' import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel' import { DetectionLab } from './components/detection/DetectionLab' import { ExportCenter } from './components/exports/ExportCenter' import { AreaPanel } from './components/project/AreaPanel' import { ProjectPanel } from './components/project/ProjectPanel' import type { ApiError, ChangeDetectionSummary, DatasetCreateResponse, DatasetListResponse, DetectionQaResult, DetectionRead, DetectionModelCapability, DetectionRunRead, DetectionRunResponse, JobRead, QaComparisonRequest, RasterMetadataResponse, RasterInspectResponse, RasterStatsResponse, RasterPreviewResponse, VectorSummary, ProjectRead, ProjectCreate, QualityCheckRead, ExportCreateResponse, ExportRead, AreaCreate, AreaListResponse, AreaRead, ProviderCapability, QaComparisonResult, SegmentationModelCapability, SegmentationQaResult, SegmentationRead, SegmentationRunRead, SegmentationRunResponse, } from './types' import { ProviderPanel } from './components/providers/ProviderPanel' import { SegmentationLab } from './components/segmentation/SegmentationLab' function isVectorDatasetType(datasetType: string): boolean { return datasetType === 'vector' || datasetType === 'geojson' } function formatBytes(value: number | null | undefined): string { if (!value && value !== 0) { return 'n/a' } const units = ['B', 'KB', 'MB', 'GB'] let size = value let index = 0 while (size >= 1024 && index < units.length - 1) { size /= 1024 index += 1 } return `${size.toFixed(1)} ${units[index]}` } function formatBounds(bounds: Record | null | undefined): string { if (!bounds) { return 'n/a' } const keys = ['min_x', 'min_y', 'max_x', 'max_y'] if (!keys.every((key) => key in bounds)) { return 'n/a' } return `${bounds.min_x?.toFixed(4)}, ${bounds.min_y?.toFixed(4)} -> ${bounds.max_x?.toFixed(4)}, ${bounds.max_y?.toFixed(4)}` } function formatError(error: unknown, fallback: string): string { if (error instanceof Error) { const code = (error as { code?: string }).code return code ? `${error.message} (${code})` : error.message } return fallback } function App(): JSX.Element { const [projects, setProjects] = useState([]) const [selectedProjectId, setSelectedProjectId] = useState(null) const [areas, setAreas] = useState([]) const [datasets, setDatasets] = useState([]) 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 [providerCapabilities, setProviderCapabilities] = useState([]) const [loadingCapabilities, setLoadingCapabilities] = useState(false) const [capabilitiesError, setCapabilitiesError] = useState(null) const [changeSourceDatasetId, setChangeSourceDatasetId] = useState('') const [changeTargetDatasetId, setChangeTargetDatasetId] = useState('') const [changeIouThreshold, setChangeIouThreshold] = useState(0.8) const [changeIncludeUnchanged, setChangeIncludeUnchanged] = useState(true) const [runningChangeDetection, setRunningChangeDetection] = useState(false) const [changeDetectionResult, setChangeDetectionResult] = useState(null) const [changeDetectionError, setChangeDetectionError] = useState(null) const [detectionModels, setDetectionModels] = useState([]) const [loadingDetectionModels, setLoadingDetectionModels] = useState(false) const [detectionModelError, setDetectionModelError] = useState(null) const [selectedDetectionDatasetId, setSelectedDetectionDatasetId] = useState('') const [selectedDetectionModelId, setSelectedDetectionModelId] = useState('yolo-placeholder') const [detectionTileManifestPath, setDetectionTileManifestPath] = useState('') const [detectionConfidenceThreshold, setDetectionConfidenceThreshold] = useState(0.5) const [runningDetection, setRunningDetection] = useState(false) const [detectionRunResult, setDetectionRunResult] = useState(null) const [detectionRunError, setDetectionRunError] = useState(null) const [detectionRuns, setDetectionRuns] = useState([]) const [selectedDetectionRunId, setSelectedDetectionRunId] = useState('') const [detectionItems, setDetectionItems] = useState([]) const [detectionGeoJson, setDetectionGeoJson] = useState(null) const [detectionClassFilter, setDetectionClassFilter] = useState('') const [detectionMinConfidenceFilter, setDetectionMinConfidenceFilter] = useState(0) const [loadingDetectionResults, setLoadingDetectionResults] = useState(false) const [detectionReferenceDatasetId, setDetectionReferenceDatasetId] = useState('') const [detectionQaResult, setDetectionQaResult] = useState(null) const [detectionQaError, setDetectionQaError] = useState(null) const [runningDetectionQa, setRunningDetectionQa] = useState(false) 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 [segmentationConfidenceThreshold, setSegmentationConfidenceThreshold] = useState(0.5) const [runningSegmentation, setRunningSegmentation] = useState(false) const [segmentationRunResult, setSegmentationRunResult] = useState(null) const [segmentationRunError, setSegmentationRunError] = useState(null) const [segmentationRuns, setSegmentationRuns] = useState([]) const [selectedSegmentationRunId, setSelectedSegmentationRunId] = useState('') const [segmentationItems, setSegmentationItems] = useState([]) 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 [rasterPreview, setRasterPreview] = useState(null) const [selectedIntersectTargetId, setSelectedIntersectTargetId] = useState('') const [selectedClipAreaId, setSelectedClipAreaId] = useState('') const [qaCandidateDatasetId, setQaCandidateDatasetId] = useState('') const [qaReferenceDatasetId, setQaReferenceDatasetId] = useState('') const [qaAreaId, setQaAreaId] = useState('') const [qaIouThreshold, setQaIouThreshold] = useState(0.5) const [qaRunning, setQaRunning] = useState(false) const [qaResult, setQaResult] = useState(null) const [qaError, setQaError] = useState(null) const [qualityChecks, setQualityChecks] = useState([]) const [qualityChecksError, setQualityChecksError] = useState(null) const [exports, setExports] = useState([]) const [latestExport, setLatestExport] = useState(null) const [exportError, setExportError] = useState(null) const [loadingExports, setLoadingExports] = useState(false) const [exporting, setExporting] = useState(false) const [exportPreview, setExportPreview] = useState | null>(null) 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 [loadingProjects, setLoadingProjects] = useState(false) const [loadingDemoWorkflow, setLoadingDemoWorkflow] = useState(false) const [demoWorkflowMessage, setDemoWorkflowMessage] = useState(null) const [loadingAreas, setLoadingAreas] = useState(false) const [loadingDatasets, setLoadingDatasets] = useState(false) const [loadingDatasetDetails, setLoadingDatasetDetails] = useState(false) const [errorMessage, setErrorMessage] = useState(null) const [datasetDetailError, setDatasetDetailError] = useState(null) const [projectForm, setProjectForm] = useState({ name: '', description: '', region: 'Kempen', }) const [areaForm, setAreaForm] = useState({ name: 'Demo AOI', geometry: '{"type":"MultiPolygon","coordinates":[[[[4.35,51.28],[4.55,51.28],[4.55,51.46],[4.35,51.46],[4.35,51.28]]]]}', crs: 'EPSG:4326', }) const [datasetForm, setDatasetForm] = useState({ datasetType: 'vector', source: 'user_upload', datasetRole: 'source', sourceName: '', referenceLayerName: '', sourceMetadataJson: '', provenanceMetadataJson: '', areaId: '', file: null as File | null, }) const selectedProject = useMemo( () => projects.find((project) => project.id === selectedProjectId) ?? null, [projects, selectedProjectId], ) const rasterUnavailableMessage = useMemo(() => { if (!selectedDataset || selectedDataset.dataset_type !== 'raster' || !selectedDataset.metadata_json) { return null } const metadata = selectedDataset.metadata_json as Record const processingCode = metadata['processing_code'] if (processingCode === 'RASTER_PROCESSING_UNAVAILABLE') { return String(metadata['processing_error'] ?? 'Raster processing unavailable.') } return null }, [selectedDataset]) const availableVectorTargets = useMemo( () => datasets.filter((item) => item.id !== selectedDatasetId && isVectorDatasetType(item.dataset_type)), [datasets, selectedDatasetId], ) const availableVectorDatasets = useMemo(() => datasets.filter((item) => isVectorDatasetType(item.dataset_type)), [datasets]) const referenceDatasets = useMemo( () => availableVectorDatasets.filter((item) => item.dataset_role === 'reference'), [availableVectorDatasets], ) const candidateDatasets = availableVectorDatasets const providers = useMemo(() => providerCapabilities, [providerCapabilities]) const rasterDatasets = useMemo(() => datasets.filter((item) => item.dataset_type === 'raster'), [datasets]) const selectedSegmentationModel = useMemo( () => segmentationModels.find((model) => model.model_id === selectedSegmentationModelId) ?? null, [segmentationModels, selectedSegmentationModelId], ) const mapFeatureCollection = useMemo( () => changeDetectionResult?.geojson ?? segmentationGeoJson ?? detectionGeoJson ?? datasetContent, [changeDetectionResult, segmentationGeoJson, detectionGeoJson, datasetContent], ) const isRasterTileInputValid = useMemo( () => rasterTileSize > 0 && rasterTileOverlap >= 0 && rasterTileOverlap < rasterTileSize, [rasterTileSize, rasterTileOverlap], ) const toRasterMetadata = (metadata: Record | null | undefined): RasterMetadataResponse | null => { if (!metadata) { return null } return metadata as unknown as RasterMetadataResponse } const formatRasterBounds = (bounds: number[] | undefined | null): string => { if (!bounds || bounds.length < 4) { return 'n/a' } const [minX, minY, maxX, maxY] = bounds return `${minX.toFixed(4)}, ${minY.toFixed(4)} -> ${maxX.toFixed(4)}, ${maxY.toFixed(4)}` } const loadProjects = async () => { setLoadingProjects(true) setErrorMessage(null) try { const response = await projectsApi.list() setProjects(response.items) if (!selectedProjectId && response.items.length > 0) { setSelectedProjectId(response.items[0].id) } } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'Failed to load projects') } finally { setLoadingProjects(false) } } const loadProjectData = async (projectId: string) => { setLoadingAreas(true) setLoadingDatasets(true) setErrorMessage(null) try { const [areaResponse, datasetResponse]: [AreaListResponse, DatasetListResponse] = await Promise.all([ areasApi.list(projectId), datasetsApi.list(projectId), ]) setAreas(areaResponse.items) setDatasets(datasetResponse.items) if (!selectedClipAreaId && areaResponse.items.length > 0) { setSelectedClipAreaId(areaResponse.items[0].id) } } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data') } finally { setLoadingAreas(false) setLoadingDatasets(false) } } const loadCapabilities = async () => { setLoadingCapabilities(true) setCapabilitiesError(null) try { const providerResponse = await externalApi.listProviders() setProviderCapabilities(providerResponse.providers) } catch (error) { setCapabilitiesError(error instanceof Error ? error.message : 'Failed to load external capabilities') } finally { setLoadingCapabilities(false) } } const loadDetectionModels = async () => { setLoadingDetectionModels(true) setDetectionModelError(null) try { const response = await detectionApi.listModels() setDetectionModels(response.models) if (!response.models.some((model) => model.model_id === selectedDetectionModelId) && response.models.length > 0) { setSelectedDetectionModelId(response.models[0].model_id) } } catch (error) { setDetectionModelError(formatError(error, 'Failed to load detection models')) } finally { setLoadingDetectionModels(false) } } const loadDetectionRuns = async (projectId = selectedProjectId) => { if (!projectId) { setDetectionRuns([]) return } try { const response = await detectionApi.listRuns({ project_id: projectId }) setDetectionRuns(response.items) if (!selectedDetectionRunId && response.items.length > 0) { setSelectedDetectionRunId(response.items[0].id) } } catch (error) { setDetectionRunError(formatError(error, 'Failed to load detection runs')) } } const loadDetectionResults = async (analysisRunId = selectedDetectionRunId) => { if (!analysisRunId) { setDetectionItems([]) setDetectionGeoJson(null) return } setLoadingDetectionResults(true) setDetectionRunError(null) try { const params = { class_name: detectionClassFilter || null, min_confidence: detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null, } const [detectionsResponse, geoJsonResponse] = await Promise.all([ detectionApi.listDetections(analysisRunId, params), detectionApi.getRunGeoJson(analysisRunId, params), ]) setDetectionItems(detectionsResponse.items) setDetectionGeoJson(geoJsonResponse) } catch (error) { setDetectionRunError(formatError(error, 'Failed to load detection results')) } finally { setLoadingDetectionResults(false) } } const loadSegmentationModels = async () => { setLoadingSegmentationModels(true) setSegmentationModelError(null) try { const response = await segmentationApi.listModels() setSegmentationModels(response.models) if (!response.models.some((model) => model.model_id === selectedSegmentationModelId) && response.models.length > 0) { setSelectedSegmentationModelId(response.models[0].model_id) } } catch (error) { setSegmentationModelError(formatError(error, 'Failed to load segmentation models')) } finally { setLoadingSegmentationModels(false) } } const loadSegmentationRuns = async (projectId = selectedProjectId) => { if (!projectId) { setSegmentationRuns([]) return } try { const response = await segmentationApi.listRuns({ project_id: projectId }) setSegmentationRuns(response.items) if (!selectedSegmentationRunId && response.items.length > 0) { setSelectedSegmentationRunId(response.items[0].id) } } catch (error) { setSegmentationRunError(formatError(error, 'Failed to load segmentation runs')) } } const loadSegmentationResults = async (analysisRunId = selectedSegmentationRunId) => { if (!analysisRunId) { setSegmentationItems([]) setSegmentationGeoJson(null) return } setLoadingSegmentationResults(true) setSegmentationRunError(null) try { const params = { class_name: segmentationClassFilter || null, min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null, } const [segmentationsResponse, geoJsonResponse] = await Promise.all([ segmentationApi.listSegmentations(analysisRunId, params), segmentationApi.getRunGeoJson(analysisRunId, params), ]) setSegmentationItems(segmentationsResponse.items) setSegmentationGeoJson(geoJsonResponse) } catch (error) { setSegmentationRunError(formatError(error, 'Failed to load segmentation results')) } finally { setLoadingSegmentationResults(false) } } const loadQualityChecks = async (projectId = selectedProjectId) => { if (!projectId) { setQualityChecks([]) return } setQualityChecksError(null) try { const response = await qaApi.listQualityChecks(projectId) setQualityChecks(response.items) } catch (error) { setQualityChecksError(formatError(error, 'Failed to load QA/QC results')) } } const loadExports = async (projectId = selectedProjectId) => { if (!projectId) { setExports([]) return } setLoadingExports(true) setExportError(null) try { const response = await exportsApi.listProjectExports(projectId) setExports(response.items) } catch (error) { setExportError(formatError(error, 'Failed to load exports')) } finally { setLoadingExports(false) } } const exportSelectedDatasetGeoJson = async () => { if (!selectedDataset || !isVectorDatasetType(selectedDataset.dataset_type)) { setExportError('Select a vector dataset before exporting GeoJSON.') return } setExporting(true) setExportError(null) try { const response = await exportsApi.exportGeojson({ dataset_id: selectedDataset.id, export_kind: 'dataset', name: selectedDataset.name.replace(/\.(geo)?json$/i, ''), }) setLatestExport(response) await loadExports(selectedDataset.project_id) } catch (error) { setExportError(formatError(error, 'Failed to export selected dataset')) } finally { setExporting(false) } } const exportSelectedDetectionRunGeoJson = async () => { if (!selectedDetectionRunId) { setExportError('Select a detection run before exporting GeoJSON.') return } setExporting(true) setExportError(null) try { const response = await exportsApi.exportGeojson({ analysis_run_id: selectedDetectionRunId, export_kind: 'detection_run', }) setLatestExport(response) await loadExports(selectedProjectId) } catch (error) { setExportError(formatError(error, 'Failed to export detection run')) } finally { setExporting(false) } } const exportSelectedSegmentationRunGeoJson = async () => { if (!selectedSegmentationRunId) { setExportError('Select a segmentation run before exporting GeoJSON.') return } setExporting(true) setExportError(null) try { const response = await exportsApi.exportGeojson({ analysis_run_id: selectedSegmentationRunId, export_kind: 'segmentation_run', }) setLatestExport(response) await loadExports(selectedProjectId) } catch (error) { setExportError(formatError(error, 'Failed to export segmentation run')) } finally { setExporting(false) } } const exportProjectMetadata = async () => { if (!selectedProjectId) { setExportError('Select a project before exporting metadata.') return } setExporting(true) setExportError(null) try { const response = await exportsApi.exportProjectMetadata(selectedProjectId) setLatestExport(response) await loadExports(selectedProjectId) } catch (error) { setExportError(formatError(error, 'Failed to export project metadata')) } finally { setExporting(false) } } const exportProjectReport = async () => { if (!selectedProjectId) { setExportError('Select a project before exporting a report.') return } setExporting(true) setExportError(null) try { const response = await exportsApi.exportProjectReport(selectedProjectId) setLatestExport(response) await loadExports(selectedProjectId) } catch (error) { setExportError(formatError(error, 'Failed to export project report')) } finally { setExporting(false) } } const previewExportContent = async (exportId: string) => { setExportError(null) try { const response = await exportsApi.getContent(exportId) setExportPreview(response.content) } catch (error) { setExportError(formatError(error, 'Failed to load export content')) } } const downloadExportArtifact = (exportId: string) => { window.open(exportsApi.downloadUrl(exportId), '_blank', 'noopener,noreferrer') } 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) } } useEffect(() => { loadProjects().catch(() => null) loadCapabilities().catch(() => null) loadDetectionModels().catch(() => null) loadSegmentationModels().catch(() => null) }, []) useEffect(() => { if (!selectedProjectId) { setAreas([]) setDatasets([]) setSelectedDatasetId(null) setSelectedDataset(null) setSelectedDatasetSummary(null) setSelectedRasterMetadata(null) setSelectedRasterStats(null) setJobs([]) setSelectedDetectionDatasetId('') setDetectionRuns([]) setSelectedDetectionRunId('') setDetectionItems([]) setDetectionGeoJson(null) setDetectionRunResult(null) setSelectedSegmentationDatasetId('') setSegmentationRuns([]) setSelectedSegmentationRunId('') setSegmentationItems([]) setSegmentationGeoJson(null) setSegmentationRunResult(null) setExports([]) setLatestExport(null) setExportPreview(null) return } loadProjectData(selectedProjectId).catch(() => null) loadDetectionRuns(selectedProjectId).catch(() => null) loadSegmentationRuns(selectedProjectId).catch(() => null) loadQualityChecks(selectedProjectId).catch(() => null) loadExports(selectedProjectId).catch(() => null) }, [selectedProjectId]) useEffect(() => { loadDetectionResults().catch(() => null) }, [selectedDetectionRunId, detectionClassFilter, detectionMinConfidenceFilter]) useEffect(() => { loadSegmentationResults().catch(() => null) }, [selectedSegmentationRunId, segmentationClassFilter, segmentationMinConfidenceFilter]) const createProject = async (event: FormEvent) => { event.preventDefault() if (!projectForm.name.trim()) { setErrorMessage('Project name is required') return } try { await projectsApi.create({ name: projectForm.name.trim(), description: projectForm.description?.trim() || undefined, region: projectForm.region?.trim() || 'Kempen', }) setProjectForm((previous) => ({ ...previous, name: '', description: '' })) await loadProjects() } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'Failed to create project') } } const loadDemoWorkflow = async () => { setLoadingDemoWorkflow(true) setDemoWorkflowMessage(null) setErrorMessage(null) try { const result = await demoApi.seedWorkflow() setSelectedProjectId(result.project_id) setSelectedDatasetId(result.candidate_dataset_id) setQaCandidateDatasetId(result.candidate_dataset_id) setQaReferenceDatasetId(result.reference_dataset_id) setQaAreaId(result.area_id) setDetectionReferenceDatasetId(result.reference_dataset_id) setSegmentationReferenceDatasetId(result.reference_dataset_id) setDemoWorkflowMessage(result.message) await loadProjects() await Promise.all([ loadProjectData(result.project_id), loadDetectionRuns(result.project_id), loadSegmentationRuns(result.project_id), loadQualityChecks(result.project_id), loadExports(result.project_id), ]) } catch (error) { setErrorMessage(formatError(error, 'Failed to load demo workflow')) } finally { setLoadingDemoWorkflow(false) } } const createArea = async (event: FormEvent) => { event.preventDefault() if (!selectedProjectId) { setErrorMessage('Select a project first') return } let geometry: AreaCreate['geometry'] try { geometry = JSON.parse(areaForm.geometry) as AreaCreate['geometry'] } catch { setErrorMessage('Invalid GeoJSON geometry JSON') return } try { await areasApi.create(selectedProjectId, { name: areaForm.name, crs: areaForm.crs, geometry, }) await loadProjectData(selectedProjectId) setAreaForm((previous) => ({ ...previous, name: '' })) } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'Failed to create area') } } 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 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 Promise.all([loadDatasetJobs(selectedProjectId, selectedDatasetId), loadProjectData(selectedProjectId)]) const refreshed = datasets.find((dataset) => dataset.id === selectedDatasetId) if (refreshed && selectedProjectId) { loadDatasetDetails(selectedProjectId, refreshed).catch(() => null) } } 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 Promise.all([loadDatasetJobs(selectedProjectId, selectedDatasetId), loadProjectData(selectedProjectId)]) const refreshed = datasets.find((dataset) => dataset.id === selectedDatasetId) if (refreshed && selectedProjectId) { loadDatasetDetails(selectedProjectId, refreshed).catch(() => null) } } catch (error) { setDatasetDetailError(formatError(error, 'Vector buffer failed')) } } const runVectorIntersect = async () => { 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 Promise.all([loadDatasetJobs(selectedProjectId, selectedDatasetId), loadProjectData(selectedProjectId)]) const refreshed = datasets.find((dataset) => dataset.id === selectedDatasetId) if (refreshed && selectedProjectId) { loadDatasetDetails(selectedProjectId, refreshed).catch(() => null) } } 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 Promise.all([loadDatasetJobs(selectedProjectId, selectedDatasetId), loadProjectData(selectedProjectId)]) const refreshed = datasets.find((dataset) => dataset.id === selectedDatasetId) if (refreshed && selectedProjectId) { loadDatasetDetails(selectedProjectId, refreshed).catch(() => null) } 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 Promise.all([loadDatasetJobs(selectedProjectId, selectedDatasetId), loadProjectData(selectedProjectId)]) const refreshed = datasets.find((dataset) => dataset.id === selectedDatasetId) if (refreshed && selectedProjectId) { loadDatasetDetails(selectedProjectId, refreshed).catch(() => null) } 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 Promise.all([loadDatasetJobs(selectedProjectId, selectedDatasetId), loadProjectData(selectedProjectId)]) const refreshed = datasets.find((dataset) => dataset.id === selectedDatasetId) if (refreshed && selectedProjectId) { loadDatasetDetails(selectedProjectId, refreshed).catch(() => null) } 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 Promise.all([loadDatasetJobs(selectedProjectId, selectedDatasetId), loadProjectData(selectedProjectId)]) const refreshed = datasets.find((dataset) => dataset.id === selectedDatasetId) if (refreshed && selectedProjectId) { loadDatasetDetails(selectedProjectId, refreshed).catch(() => null) } 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 Promise.all([loadDatasetJobs(selectedProjectId, selectedDatasetId), loadProjectData(selectedProjectId)]) const refreshed = datasets.find((dataset) => dataset.id === selectedDatasetId) if (refreshed && selectedProjectId) { loadDatasetDetails(selectedProjectId, refreshed).catch(() => null) } 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 Promise.all([loadDatasetJobs(selectedProjectId, selectedDatasetId), loadProjectData(selectedProjectId)]) const refreshed = datasets.find((dataset) => dataset.id === selectedDatasetId) if (refreshed && selectedProjectId) { loadDatasetDetails(selectedProjectId, refreshed).catch(() => null) } setDatasetDetailError(null) } catch (error) { setDatasetDetailError(formatError(error, 'Raster NDBI failed')) } } const runQaComparison = async () => { if (!selectedProjectId) { setQaError('Select a project first') return } if (!qaCandidateDatasetId) { setQaError('Select candidate dataset') return } if (!qaReferenceDatasetId) { setQaError('Select reference dataset') return } if (qaCandidateDatasetId === qaReferenceDatasetId) { setQaError('Candidate and reference datasets must be different') return } if (!Number.isFinite(qaIouThreshold) || qaIouThreshold < 0 || qaIouThreshold > 1) { setQaError('IoU threshold must be between 0 and 1') return } setQaError(null) setQaResult(null) setQaRunning(true) try { const request: QaComparisonRequest = { candidate_dataset_id: qaCandidateDatasetId, reference_dataset_id: qaReferenceDatasetId, iou_threshold: qaIouThreshold, area_id: qaAreaId || null, } const job: JobRead = await qaApi.runQa(request) if (job.status === 'failed') { setQaError(job.error_message || 'QA comparison failed') return } const payload = job.result_json if (!payload || typeof payload !== 'object') { setQaError('QA result was not available') return } const parsed = payload as unknown as QaComparisonResult if (!parsed || typeof parsed.status !== 'string') { setQaError('QA result format was unexpected') return } setQaResult(parsed) await loadQualityChecks(selectedProjectId) if (job.output_dataset_id) { await loadProjectData(selectedProjectId) } } catch (error) { setQaError(error instanceof Error ? error.message : 'QA comparison failed') } finally { setQaRunning(false) } } const runDetection = async () => { if (!selectedProjectId) { setDetectionRunError('Select a project first') return } const datasetId = selectedDetectionDatasetId || rasterDatasets[0]?.id if (!datasetId) { setDetectionRunError('Select a raster dataset') return } setDetectionRunError(null) setDetectionRunResult(null) setRunningDetection(true) try { const result = await detectionApi.run({ project_id: selectedProjectId, dataset_id: datasetId, model_id: selectedDetectionModelId, 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) } catch (error) { setDetectionRunError(formatError(error, 'Detection run failed')) } finally { setRunningDetection(false) } } const runChangeDetection = async () => { const sourceDatasetId = changeSourceDatasetId || availableVectorDatasets[0]?.id const targetDatasetId = changeTargetDatasetId || availableVectorDatasets.find((dataset) => dataset.id !== sourceDatasetId)?.id if (!sourceDatasetId || !targetDatasetId) { setChangeDetectionError('Select two vector datasets') return } if (sourceDatasetId === targetDatasetId) { setChangeDetectionError('Source and target datasets must differ') return } if (changeIouThreshold < 0 || changeIouThreshold > 1) { setChangeDetectionError('IoU threshold must be between 0 and 1') return } setChangeDetectionError(null) setChangeDetectionResult(null) setRunningChangeDetection(true) try { const job = await analysisApi.runChangeDetection({ source_dataset_id: sourceDatasetId, target_dataset_id: targetDatasetId, iou_threshold: changeIouThreshold, include_unchanged: changeIncludeUnchanged, }) if (job.status !== 'success') { throw new Error(job.error_message || 'Change detection job failed') } if (!job.result_json) { throw new Error('Change detection completed without result payload') } setChangeSourceDatasetId(sourceDatasetId) setChangeTargetDatasetId(targetDatasetId) setChangeDetectionResult(job.result_json) if (selectedProjectId) { await loadDatasetJobs(selectedProjectId, sourceDatasetId) } } catch (error) { setChangeDetectionError(formatError(error, 'Change detection failed')) } finally { setRunningChangeDetection(false) } } const runDetectionQa = async () => { if (!selectedDetectionRunId) { setDetectionQaError('Select a detection run') return } if (!detectionReferenceDatasetId) { setDetectionQaError('Select a reference dataset') return } setDetectionQaError(null) setDetectionQaResult(null) setRunningDetectionQa(true) try { const result = await detectionApi.compareWithReference(selectedDetectionRunId, { reference_dataset_id: detectionReferenceDatasetId, iou_threshold: qaIouThreshold, class_name: detectionClassFilter || null, min_confidence: detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null, }) setDetectionQaResult(result) await loadQualityChecks(selectedProjectId) } catch (error) { setDetectionQaError(formatError(error, 'Detection QA failed')) } finally { setRunningDetectionQa(false) } } const runSegmentation = async () => { if (!selectedProjectId) { setSegmentationRunError('Select a project first') return } const datasetId = selectedSegmentationDatasetId || rasterDatasets[0]?.id if (!datasetId) { setSegmentationRunError('Select a raster dataset') return } if (!selectedSegmentationModel?.configured) { setSegmentationRunError('Selected segmentation model is not configured') return } setSegmentationRunError(null) setSegmentationRunResult(null) setRunningSegmentation(true) try { const parameters = selectedSegmentationModelId === 'fixture-segmenter' ? { fixture_mode: true, fixture_segmentations: [] } : {} const result = await segmentationApi.run({ project_id: selectedProjectId, dataset_id: datasetId, model_id: selectedSegmentationModelId, confidence_threshold: segmentationConfidenceThreshold, parameters_json: parameters, }) setSegmentationRunResult(result) setSelectedSegmentationRunId(result.analysis_run_id) await loadSegmentationRuns(selectedProjectId) await loadSegmentationResults(result.analysis_run_id) await loadProjectData(selectedProjectId) } catch (error) { setSegmentationRunError(formatError(error, 'Segmentation run failed')) } finally { setRunningSegmentation(false) } } const runSegmentationQa = async () => { if (!selectedSegmentationRunId) { setSegmentationQaError('Select a segmentation run') return } if (!segmentationReferenceDatasetId) { setSegmentationQaError('Select a reference dataset') return } setSegmentationQaError(null) setSegmentationQaResult(null) setRunningSegmentationQa(true) try { const result = await segmentationApi.compareWithReference(selectedSegmentationRunId, { reference_dataset_id: segmentationReferenceDatasetId, iou_threshold: qaIouThreshold, class_name: segmentationClassFilter || null, min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null, }) setSegmentationQaResult(result) await loadQualityChecks(selectedProjectId) } catch (error) { setSegmentationQaError(formatError(error, 'Segmentation QA failed')) } finally { setRunningSegmentationQa(false) } } 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) } } } return (

GeoIntel Kempen Sprint 9

Sprint 9: raster/vector workbench with detection and segmentation foundations.

{errorMessage ?

{errorMessage}

: null}
loadDetectionRuns()} onSelectRun={setSelectedDetectionRunId} onSetClassFilter={setDetectionClassFilter} onSetMinConfidenceFilter={setDetectionMinConfidenceFilter} onLoadResults={() => loadDetectionResults()} onSelectReferenceDataset={setDetectionReferenceDatasetId} onRunQa={runDetectionQa} /> loadSegmentationRuns()} onSelectRun={setSelectedSegmentationRunId} onSetClassFilter={setSegmentationClassFilter} onSetMinConfidenceFilter={setSegmentationMinConfidenceFilter} onLoadResults={() => loadSegmentationResults()} onSelectReferenceDataset={setSegmentationReferenceDatasetId} onRunQa={runSegmentationQa} />

QA/QC Results

{qualityChecksError ?

{qualityChecksError}

: null} {qualityChecks.length === 0 ?

No persisted QA/QC results yet

: null}
    {qualityChecks.map((check) => (
  • {check.check_type}
    status: {check.status}
    score: {check.score ?? 'n/a'}
    candidate: {check.candidate_dataset_id ?? 'n/a'}
    reference: {check.reference_dataset_id}
    quality check: {check.id}
      {check.metrics.map((metric) => (
    • {metric.metric_key}: {metric.metric_value ?? 'n/a'}
    • ))}
  • ))}
loadExports()} onExportDataset={exportSelectedDatasetGeoJson} onExportDetectionRun={exportSelectedDetectionRunGeoJson} onExportSegmentationRun={exportSelectedSegmentationRunGeoJson} onExportProjectMetadata={exportProjectMetadata} onExportProjectReport={exportProjectReport} onPreviewContent={previewExportContent} onDownload={downloadExportArtifact} /> {exportPreview ? (

Export Preview

{JSON.stringify(exportPreview, null, 2)}
) : null}

Datasets

setDatasetForm((previous) => ({ ...previous, source: event.target.value }))} placeholder="user_upload" /> setDatasetForm((previous) => ({ ...previous, file: event.target.files?.[0] ?? null }))} />
{loadingDatasets ?

Loading datasets...

: null} {datasets.length === 0 ?

No datasets yet

: null}
    {datasets.map((dataset) => (
  • {dataset.name}
    type: {dataset.dataset_type}
    status: {dataset.status}
    readiness: {dataset.status === 'ready' ? 'ready' : dataset.status === 'failed' ? 'failed' : 'pending'}
    size: {formatBytes(dataset.size_bytes)}
    features: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'n/a'}
    bbox: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)}
  • ))}

Dataset details

{selectedDatasetId ?

Selected dataset: {selectedDatasetId}

:

No dataset selected

} {selectedDataset ? (

{selectedDataset.name}

Type: {selectedDataset.dataset_type}

Status: {selectedDataset.status}

Original file: {selectedDataset.original_filename ?? 'n/a'}

Stored file: {selectedDataset.stored_filename ?? 'n/a'}

Content type: {selectedDataset.content_type ?? 'n/a'}

File size: {formatBytes(selectedDataset.size_bytes)}

SHA256: {selectedDataset.checksum_sha256 ?? 'n/a'}

Feature count: {selectedDatasetSummary?.feature_count ?? selectedDataset.feature_count ?? 'n/a'}

BBox: {formatBounds(selectedDatasetSummary?.bounds_json ?? selectedDataset.bounds_json)}

{selectedDataset.dataset_type === 'raster' ? (

Raster driver: {selectedRasterMetadata?.driver ?? 'n/a'}

Raster size: {selectedRasterMetadata ? `${selectedRasterMetadata.width} x ${selectedRasterMetadata.height}` : 'n/a'}

Raster checksum: {selectedRasterMetadata?.checksum_sha256 ?? 'n/a'}

Profile: CRS {selectedRasterMetadata?.crs ?? 'n/a'} | bands {selectedRasterMetadata?.band_count ?? 'n/a'} | dtype { (selectedRasterMetadata?.dtype as string[] | undefined)?.join(', ') ?? 'n/a'}

Bounds: {formatRasterBounds(selectedRasterMetadata?.bounds)}

Resolution: {selectedRasterMetadata?.resolution ? selectedRasterMetadata.resolution.join(', ') : 'n/a'}

{rasterUnavailableMessage ?

Raster unavailable: {rasterUnavailableMessage}

: null}

Raster operations

Available operations: inspect, stats, reproject, preview, clip by selected area, tile generation.

Preview: {rasterPreview?.preview.path ?? 'not generated'}

Preview size: {rasterPreview?.preview.width ?? 'n/a'} x {rasterPreview?.preview.height ?? 'n/a'}

{selectedRasterStats ? (

Band statistics

Generated: {selectedRasterStats.generated_at ?? 'n/a'}

    {selectedRasterStats.bands.map((band) => (
  • Band {band.band_index}: min {band.min ?? 'n/a'}, max {band.max ?? 'n/a'}, mean {band.mean ?? 'n/a'}, std {band.std ?? 'n/a'}, valid {band.valid_pixel_count}, nodata ratio {(band.nodata_ratio * 100).toFixed(2)}%, dtype {band.dtype ?? 'n/a'}
  • ))}
) : null}
{areas.length === 0 ?

Create an area before raster clipping.

: null}
{!isRasterTileInputValid ? (

Tile size must be {'>'} 0 and overlap must be {'>='} 0 and smaller than tile size.

) : null}

Spectral indices

Use available band indexes from the raster file (1-based).

NDVI

NDWI

NDBI

) : null} {isVectorDatasetType(selectedDataset.dataset_type) ? (

Vector operations

) : null}

Jobs

{jobs.length === 0 ?

No jobs yet.

: null}
    {jobs.map((job) => (
  • {job.job_type} · {job.status}
    {job.result_json ? (
    {JSON.stringify(job.result_json, null, 2)}
    ) : null} {job.error_message ?
    error: {job.error_message}
    : null} {job.result_json?.output_dataset_id ? ( ) : null}
  • ))}
) : null} {loadingDatasetDetails ?

Loading dataset details...

: null} {datasetDetailError ?

Dataset detail error: {datasetDetailError}

: null}

Map workspace

{selectedDatasetId ? `Showing dataset ${selectedDatasetId}` : 'No vector dataset selected'}

) } export default App