1837 lines
71 KiB
TypeScript
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
|