Files
geointel/frontend/src/App.tsx
T
Codex 01f063b921
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled
Add vector change detection foundation
2026-06-16 23:55:52 +02:00

1837 lines
71 KiB
TypeScript

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<string, number> | 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<ProjectRead[]>([])
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
const [areas, setAreas] = useState<AreaRead[]>([])
const [datasets, setDatasets] = useState<DatasetCreateResponse[]>([])
const [selectedDatasetId, setSelectedDatasetId] = useState<string | null>(null)
const [selectedDataset, setSelectedDataset] = useState<DatasetCreateResponse | null>(null)
const [selectedDatasetSummary, setSelectedDatasetSummary] = useState<VectorSummary | null>(null)
const [selectedRasterMetadata, setSelectedRasterMetadata] = useState<RasterMetadataResponse | null>(null)
const [selectedRasterStats, setSelectedRasterStats] = useState<RasterStatsResponse | null>(null)
const [datasetContent, setDatasetContent] = useState<GeoJSON.FeatureCollection | null>(null)
const [jobs, setJobs] = useState<JobRead[]>([])
const [providerCapabilities, setProviderCapabilities] = useState<ProviderCapability[]>([])
const [loadingCapabilities, setLoadingCapabilities] = useState(false)
const [capabilitiesError, setCapabilitiesError] = useState<string | null>(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<ChangeDetectionSummary | null>(null)
const [changeDetectionError, setChangeDetectionError] = useState<string | null>(null)
const [detectionModels, setDetectionModels] = useState<DetectionModelCapability[]>([])
const [loadingDetectionModels, setLoadingDetectionModels] = useState(false)
const [detectionModelError, setDetectionModelError] = useState<string | null>(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<DetectionRunResponse | null>(null)
const [detectionRunError, setDetectionRunError] = useState<string | null>(null)
const [detectionRuns, setDetectionRuns] = useState<DetectionRunRead[]>([])
const [selectedDetectionRunId, setSelectedDetectionRunId] = useState('')
const [detectionItems, setDetectionItems] = useState<DetectionRead[]>([])
const [detectionGeoJson, setDetectionGeoJson] = useState<GeoJSON.FeatureCollection | null>(null)
const [detectionClassFilter, setDetectionClassFilter] = useState('')
const [detectionMinConfidenceFilter, setDetectionMinConfidenceFilter] = useState(0)
const [loadingDetectionResults, setLoadingDetectionResults] = useState(false)
const [detectionReferenceDatasetId, setDetectionReferenceDatasetId] = useState('')
const [detectionQaResult, setDetectionQaResult] = useState<DetectionQaResult | null>(null)
const [detectionQaError, setDetectionQaError] = useState<string | null>(null)
const [runningDetectionQa, setRunningDetectionQa] = useState(false)
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 [segmentationConfidenceThreshold, setSegmentationConfidenceThreshold] = useState(0.5)
const [runningSegmentation, setRunningSegmentation] = useState(false)
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 [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 [rasterPreview, setRasterPreview] = useState<RasterPreviewResponse | null>(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<QaComparisonResult | null>(null)
const [qaError, setQaError] = useState<string | null>(null)
const [qualityChecks, setQualityChecks] = useState<QualityCheckRead[]>([])
const [qualityChecksError, setQualityChecksError] = useState<string | null>(null)
const [exports, setExports] = useState<ExportRead[]>([])
const [latestExport, setLatestExport] = useState<ExportCreateResponse | null>(null)
const [exportError, setExportError] = useState<string | null>(null)
const [loadingExports, setLoadingExports] = useState(false)
const [exporting, setExporting] = useState(false)
const [exportPreview, setExportPreview] = useState<Record<string, unknown> | 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<string | null>(null)
const [loadingAreas, setLoadingAreas] = useState(false)
const [loadingDatasets, setLoadingDatasets] = useState(false)
const [loadingDatasetDetails, setLoadingDatasetDetails] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
const [datasetDetailError, setDatasetDetailError] = useState<string | null>(null)
const [projectForm, setProjectForm] = useState<ProjectCreate>({
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<string, unknown>
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<string, unknown> | 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 (
<div className="app-shell">
<header>
<h1>GeoIntel Kempen Sprint 9</h1>
<p>Sprint 9: raster/vector workbench with detection and segmentation foundations.</p>
</header>
{errorMessage ? <p className="error">{errorMessage}</p> : null}
<main className="workspace-grid">
<ProjectPanel
projects={projects}
selectedProjectId={selectedProjectId}
loadingProjects={loadingProjects}
projectForm={projectForm}
loadingDemoWorkflow={loadingDemoWorkflow}
demoWorkflowMessage={demoWorkflowMessage}
onCreateProject={createProject}
onUpdateProjectForm={setProjectForm}
onSelectProject={setSelectedProjectId}
onLoadDemoWorkflow={loadDemoWorkflow}
/>
<AreaPanel
areas={areas}
selectedProject={selectedProject}
selectedProjectId={selectedProjectId}
loadingAreas={loadingAreas}
areaForm={areaForm}
onCreateArea={createArea}
onUpdateAreaForm={setAreaForm}
/>
<ProviderPanel
providers={providers}
loadingCapabilities={loadingCapabilities}
capabilitiesError={capabilitiesError}
onRefresh={loadCapabilities}
/>
<ChangeDetectionPanel
vectorDatasets={availableVectorDatasets}
sourceDatasetId={changeSourceDatasetId}
targetDatasetId={changeTargetDatasetId}
iouThreshold={changeIouThreshold}
includeUnchanged={changeIncludeUnchanged}
running={runningChangeDetection}
result={changeDetectionResult}
error={changeDetectionError}
onSourceDatasetChange={setChangeSourceDatasetId}
onTargetDatasetChange={setChangeTargetDatasetId}
onIouThresholdChange={setChangeIouThreshold}
onIncludeUnchangedChange={setChangeIncludeUnchanged}
onRun={runChangeDetection}
/>
<DetectionLab
detectionModels={detectionModels}
loadingDetectionModels={loadingDetectionModels}
detectionModelError={detectionModelError}
selectedDetectionDatasetId={selectedDetectionDatasetId}
selectedDetectionModelId={selectedDetectionModelId}
detectionTileManifestPath={detectionTileManifestPath}
detectionConfidenceThreshold={detectionConfidenceThreshold}
runningDetection={runningDetection}
detectionRunResult={detectionRunResult}
detectionRunError={detectionRunError}
detectionRuns={detectionRuns}
selectedDetectionRunId={selectedDetectionRunId}
detectionItems={detectionItems}
detectionClassFilter={detectionClassFilter}
detectionMinConfidenceFilter={detectionMinConfidenceFilter}
loadingDetectionResults={loadingDetectionResults}
detectionReferenceDatasetId={detectionReferenceDatasetId}
detectionQaResult={detectionQaResult}
detectionQaError={detectionQaError}
runningDetectionQa={runningDetectionQa}
selectedProjectId={selectedProjectId}
rasterDatasets={rasterDatasets}
referenceDatasets={referenceDatasets}
onLoadModels={loadDetectionModels}
onSelectDataset={setSelectedDetectionDatasetId}
onSelectModel={setSelectedDetectionModelId}
onSetConfidenceThreshold={setDetectionConfidenceThreshold}
onSetTileManifestPath={setDetectionTileManifestPath}
onRunDetection={runDetection}
onLoadRuns={() => loadDetectionRuns()}
onSelectRun={setSelectedDetectionRunId}
onSetClassFilter={setDetectionClassFilter}
onSetMinConfidenceFilter={setDetectionMinConfidenceFilter}
onLoadResults={() => loadDetectionResults()}
onSelectReferenceDataset={setDetectionReferenceDatasetId}
onRunQa={runDetectionQa}
/>
<SegmentationLab
segmentationModels={segmentationModels}
loadingSegmentationModels={loadingSegmentationModels}
segmentationModelError={segmentationModelError}
selectedSegmentationDatasetId={selectedSegmentationDatasetId}
selectedSegmentationModelId={selectedSegmentationModelId}
segmentationConfidenceThreshold={segmentationConfidenceThreshold}
runningSegmentation={runningSegmentation}
segmentationRunResult={segmentationRunResult}
segmentationRunError={segmentationRunError}
segmentationRuns={segmentationRuns}
selectedSegmentationRunId={selectedSegmentationRunId}
segmentationItems={segmentationItems}
segmentationClassFilter={segmentationClassFilter}
segmentationMinConfidenceFilter={segmentationMinConfidenceFilter}
loadingSegmentationResults={loadingSegmentationResults}
segmentationReferenceDatasetId={segmentationReferenceDatasetId}
segmentationQaResult={segmentationQaResult}
segmentationQaError={segmentationQaError}
runningSegmentationQa={runningSegmentationQa}
selectedProjectId={selectedProjectId}
rasterDatasets={rasterDatasets}
referenceDatasets={referenceDatasets}
selectedSegmentationModelConfigured={Boolean(selectedSegmentationModel?.configured)}
selectedSegmentationModelLimitation={selectedSegmentationModel?.limitation_message ?? null}
onLoadModels={loadSegmentationModels}
onSelectDataset={setSelectedSegmentationDatasetId}
onSelectModel={setSelectedSegmentationModelId}
onSetConfidenceThreshold={setSegmentationConfidenceThreshold}
onRunSegmentation={runSegmentation}
onLoadRuns={() => loadSegmentationRuns()}
onSelectRun={setSelectedSegmentationRunId}
onSetClassFilter={setSegmentationClassFilter}
onSetMinConfidenceFilter={setSegmentationMinConfidenceFilter}
onLoadResults={() => loadSegmentationResults()}
onSelectReferenceDataset={setSegmentationReferenceDatasetId}
onRunQa={runSegmentationQa}
/>
<section>
<h2>QA/QC Results</h2>
<button type="button" onClick={() => loadQualityChecks()} disabled={!selectedProjectId}>
Refresh QA/QC results
</button>
{qualityChecksError ? <p className="error">{qualityChecksError}</p> : null}
{qualityChecks.length === 0 ? <p>No persisted QA/QC results yet</p> : null}
<ul>
{qualityChecks.map((check) => (
<li key={check.id}>
<strong>{check.check_type}</strong>
<div>status: {check.status}</div>
<div>score: {check.score ?? 'n/a'}</div>
<div>candidate: {check.candidate_dataset_id ?? 'n/a'}</div>
<div>reference: {check.reference_dataset_id}</div>
<div>quality check: {check.id}</div>
<ul>
{check.metrics.map((metric) => (
<li key={metric.id}>
{metric.metric_key}: {metric.metric_value ?? 'n/a'}
</li>
))}
</ul>
</li>
))}
</ul>
</section>
<ExportCenter
selectedProjectId={selectedProjectId}
selectedDataset={selectedDataset}
selectedDetectionRunId={selectedDetectionRunId}
selectedSegmentationRunId={selectedSegmentationRunId}
exports={exports}
latestExport={latestExport}
exportError={exportError}
loadingExports={loadingExports}
exporting={exporting}
onRefresh={() => loadExports()}
onExportDataset={exportSelectedDatasetGeoJson}
onExportDetectionRun={exportSelectedDetectionRunGeoJson}
onExportSegmentationRun={exportSelectedSegmentationRunGeoJson}
onExportProjectMetadata={exportProjectMetadata}
onExportProjectReport={exportProjectReport}
onPreviewContent={previewExportContent}
onDownload={downloadExportArtifact}
/>
{exportPreview ? (
<section>
<h2>Export Preview</h2>
<pre className="job-result">{JSON.stringify(exportPreview, null, 2)}</pre>
</section>
) : null}
<section>
<h2>Datasets</h2>
<form onSubmit={uploadDataset}>
<select
value={datasetForm.datasetType}
onChange={(event) => setDatasetForm((previous) => ({ ...previous, datasetType: event.target.value }))}
>
<option value="vector">vector</option>
<option value="geojson">geojson</option>
<option value="raster">raster</option>
</select>
<input
value={datasetForm.source}
onChange={(event) => setDatasetForm((previous) => ({ ...previous, source: event.target.value }))}
placeholder="user_upload"
/>
<select
value={datasetForm.areaId}
onChange={(event) => setDatasetForm((previous) => ({ ...previous, areaId: event.target.value }))}
>
<option value="">No area</option>
{areas.map((area) => (
<option key={area.id} value={area.id}>
{area.name}
</option>
))}
</select>
<input
type="file"
accept=".geojson,.json,.tif,.tiff,.geotiff"
onChange={(event) => setDatasetForm((previous) => ({ ...previous, file: event.target.files?.[0] ?? null }))}
/>
<button type="submit" disabled={!selectedProjectId}>
Upload dataset
</button>
</form>
{loadingDatasets ? <p>Loading datasets...</p> : null}
{datasets.length === 0 ? <p>No datasets yet</p> : null}
<ul>
{datasets.map((dataset) => (
<li key={dataset.id}>
<strong>{dataset.name}</strong>
<div>type: {dataset.dataset_type}</div>
<div>status: {dataset.status}</div>
<div>readiness: {dataset.status === 'ready' ? 'ready' : dataset.status === 'failed' ? 'failed' : 'pending'}</div>
<div>size: {formatBytes(dataset.size_bytes)}</div>
<div>features: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'n/a'}</div>
<div>bbox: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)}</div>
<button type="button" onClick={() => loadDatasetDetails(selectedProjectId ?? '', dataset)}>
Select / details
</button>
<button
type="button"
onClick={() => refreshMetadata(dataset.id)}
disabled={dataset.dataset_type === 'raster'}
>
Refresh metadata
</button>
</li>
))}
</ul>
</section>
</main>
<section>
<h2>Dataset details</h2>
{selectedDatasetId ? <p>Selected dataset: {selectedDatasetId}</p> : <p>No dataset selected</p>}
{selectedDataset ? (
<div>
<p>
<strong>{selectedDataset.name}</strong>
</p>
<p>Type: {selectedDataset.dataset_type}</p>
<p>Status: {selectedDataset.status}</p>
<p>Original file: {selectedDataset.original_filename ?? 'n/a'}</p>
<p>Stored file: {selectedDataset.stored_filename ?? 'n/a'}</p>
<p>Content type: {selectedDataset.content_type ?? 'n/a'}</p>
<p>File size: {formatBytes(selectedDataset.size_bytes)}</p>
<p>SHA256: {selectedDataset.checksum_sha256 ?? 'n/a'}</p>
<p>Feature count: {selectedDatasetSummary?.feature_count ?? selectedDataset.feature_count ?? 'n/a'}</p>
<p>BBox: {formatBounds(selectedDatasetSummary?.bounds_json ?? selectedDataset.bounds_json)}</p>
{selectedDataset.dataset_type === 'raster' ? (
<div>
<p>Raster driver: {selectedRasterMetadata?.driver ?? 'n/a'}</p>
<p>Raster size: {selectedRasterMetadata ? `${selectedRasterMetadata.width} x ${selectedRasterMetadata.height}` : 'n/a'}</p>
<p>Raster checksum: {selectedRasterMetadata?.checksum_sha256 ?? 'n/a'}</p>
<p>
Profile: CRS {selectedRasterMetadata?.crs ?? 'n/a'} | bands {selectedRasterMetadata?.band_count ?? 'n/a'} | dtype {
(selectedRasterMetadata?.dtype as string[] | undefined)?.join(', ') ?? 'n/a'}
</p>
<p>Bounds: {formatRasterBounds(selectedRasterMetadata?.bounds)}</p>
<p>Resolution: {selectedRasterMetadata?.resolution ? selectedRasterMetadata.resolution.join(', ') : 'n/a'}</p>
{rasterUnavailableMessage ? <p className="error">Raster unavailable: {rasterUnavailableMessage}</p> : null}
<h3>Raster operations</h3>
<p>Available operations: inspect, stats, reproject, preview, clip by selected area, tile generation.</p>
<p>Preview: {rasterPreview?.preview.path ?? 'not generated'}</p>
<p>Preview size: {rasterPreview?.preview.width ?? 'n/a'} x {rasterPreview?.preview.height ?? 'n/a'}</p>
<button type="button" onClick={runRasterInspect}>
Inspect raster metadata
</button>
<button type="button" onClick={runRasterPreview} disabled={!selectedDatasetId}>
Generate preview
</button>
<button type="button" onClick={runRasterStats}>
Compute band statistics
</button>
{selectedRasterStats ? (
<div>
<h4>Band statistics</h4>
<p>Generated: {selectedRasterStats.generated_at ?? 'n/a'}</p>
<ul>
{selectedRasterStats.bands.map((band) => (
<li key={band.band_index}>
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'}
</li>
))}
</ul>
</div>
) : null}
<div>
<label>
Reproject CRS
<input
value={rasterReprojectCrs}
onChange={(event) => setRasterReprojectCrs(event.target.value)}
placeholder="EPSG:31370"
/>
</label>
<label>
Resampling
<select value={rasterReprojectResampling} onChange={(event) => setRasterReprojectResampling(event.target.value)}>
<option value="nearest">nearest</option>
<option value="bilinear">bilinear</option>
<option value="cubic">cubic</option>
</select>
</label>
<button type="button" onClick={runRasterReproject}>
Reproject raster
</button>
</div>
<div>
<label>
Clip area
<select value={selectedClipAreaId} onChange={(event) => setSelectedClipAreaId(event.target.value)}>
{areas.map((area) => (
<option key={area.id} value={area.id}>
{area.name}
</option>
))}
</select>
</label>
<button type="button" onClick={runRasterClip} disabled={areas.length === 0}>
Clip raster by area
</button>
{areas.length === 0 ? <p className="error">Create an area before raster clipping.</p> : null}
</div>
<div>
<label>
Tile size
<input
type="number"
min={1}
value={rasterTileSize}
onChange={(event) => setRasterTileSize(Number(event.target.value))}
/>
</label>
<label>
Overlap
<input
type="number"
min={0}
value={rasterTileOverlap}
onChange={(event) => setRasterTileOverlap(Number(event.target.value))}
/>
</label>
<label>
Tile basename
<input
value={rasterTileOutputName}
onChange={(event) => setRasterTileOutputName(event.target.value)}
placeholder="optional"
/>
</label>
<button type="button" onClick={runRasterTile} disabled={!isRasterTileInputValid}>
Generate tiles
</button>
{!isRasterTileInputValid ? (
<p className="error">
Tile size must be {'>'} 0 and overlap must be {'>='} 0 and smaller than tile size.
</p>
) : null}
</div>
<h4>Spectral indices</h4>
<div>
<p>Use available band indexes from the raster file (1-based).</p>
<div>
<p>NDVI</p>
<label>
Nir band
<input
type="number"
min={1}
value={ndviNirBand}
onChange={(event) => setNdviNirBand(Number(event.target.value))}
/>
</label>
<label>
Red band
<input
type="number"
min={1}
value={ndviRedBand}
onChange={(event) => setNdviRedBand(Number(event.target.value))}
/>
</label>
<button type="button" onClick={runRasterNdvi}>
Compute NDVI
</button>
</div>
<div>
<p>NDWI</p>
<label>
Nir band
<input
type="number"
min={1}
value={ndwiNirBand}
onChange={(event) => setNdwiNirBand(Number(event.target.value))}
/>
</label>
<label>
Green band
<input
type="number"
min={1}
value={ndwiGreenBand}
onChange={(event) => setNdwiGreenBand(Number(event.target.value))}
/>
</label>
<button type="button" onClick={runRasterNdwi}>
Compute NDWI
</button>
</div>
<div>
<p>NDBI</p>
<label>
Swir band
<input
type="number"
min={1}
value={ndbiSwirBand}
onChange={(event) => setNdbiSwirBand(Number(event.target.value))}
/>
</label>
<label>
Nir band
<input
type="number"
min={1}
value={ndbiNirBand}
onChange={(event) => setNdbiNirBand(Number(event.target.value))}
/>
</label>
<button type="button" onClick={runRasterNdbi}>
Compute NDBI
</button>
</div>
</div>
</div>
) : null}
{isVectorDatasetType(selectedDataset.dataset_type) ? (
<div>
<h3>Vector operations</h3>
<div>
<label>
Clip area
<select value={selectedClipAreaId} onChange={(event) => setSelectedClipAreaId(event.target.value)}>
{areas.map((area) => (
<option key={area.id} value={area.id}>
{area.name}
</option>
))}
</select>
</label>
<button type="button" onClick={runVectorClip} disabled={areas.length === 0}>
Run clip
</button>
</div>
<button type="button" onClick={runVectorBuffer}>
Run buffer (25m)
</button>
<div>
<label>
Intersect target
<select value={selectedIntersectTargetId} onChange={(event) => setSelectedIntersectTargetId(event.target.value)}>
<option value="">auto first vector</option>
{availableVectorTargets.map((target) => (
<option key={target.id} value={target.id}>
{target.name}
</option>
))}
</select>
</label>
<button type="button" onClick={runVectorIntersect}>
Run intersect
</button>
</div>
</div>
) : null}
<h3>Jobs</h3>
{jobs.length === 0 ? <p>No jobs yet.</p> : null}
<ul>
{jobs.map((job) => (
<li key={job.id}>
<div>
{job.job_type} · {job.status}
</div>
{job.result_json ? (
<pre className="job-result">{JSON.stringify(job.result_json, null, 2)}</pre>
) : null}
{job.error_message ? <div className="error">error: {job.error_message}</div> : null}
{job.result_json?.output_dataset_id ? (
<button type="button" onClick={() => pickDerivedDataset(String(job.result_json?.output_dataset_id))}>
open derived dataset
</button>
) : null}
</li>
))}
</ul>
</div>
) : null}
{loadingDatasetDetails ? <p>Loading dataset details...</p> : null}
{datasetDetailError ? <p className="error">Dataset detail error: {datasetDetailError}</p> : null}
</section>
<section>
<h2>Map workspace</h2>
<p>{selectedDatasetId ? `Showing dataset ${selectedDatasetId}` : 'No vector dataset selected'}</p>
<GeoMap data={mapFeatureCollection} />
</section>
</div>
)
}
export default App