Initial GeoIntel V1 foundation
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import maplibregl from 'maplibre-gl'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
|
||||
interface GeoMapProps {
|
||||
data: GeoJSON.FeatureCollection | null
|
||||
}
|
||||
|
||||
function collectCoordinates(featureCollection: GeoJSON.FeatureCollection): maplibregl.LngLatBoundsLike | null {
|
||||
const coordinates: [number, number][] = []
|
||||
const walk = (coords: unknown) => {
|
||||
if (!Array.isArray(coords)) {
|
||||
return
|
||||
}
|
||||
if (coords.length === 2 && typeof coords[0] === 'number' && typeof coords[1] === 'number') {
|
||||
coordinates.push([coords[0], coords[1]])
|
||||
return
|
||||
}
|
||||
for (const item of coords) {
|
||||
walk(item)
|
||||
}
|
||||
}
|
||||
|
||||
for (const feature of featureCollection.features) {
|
||||
const geometry = feature.geometry as any
|
||||
if (geometry && geometry.coordinates) {
|
||||
walk(geometry.coordinates)
|
||||
}
|
||||
}
|
||||
|
||||
if (coordinates.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const xs = coordinates.map((point) => point[0])
|
||||
const ys = coordinates.map((point) => point[1])
|
||||
return [
|
||||
[Math.min(...xs), Math.min(...ys)],
|
||||
[Math.max(...xs), Math.max(...ys)],
|
||||
]
|
||||
}
|
||||
|
||||
function GeoMap({ data }: GeoMapProps): JSX.Element {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const mapRef = useRef<maplibregl.Map | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || mapRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container: containerRef.current,
|
||||
style: import.meta.env.VITE_MAP_STYLE_URL || 'https://demotiles.maplibre.org/style.json',
|
||||
center: [5.3, 51.3],
|
||||
zoom: 9,
|
||||
})
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right')
|
||||
mapRef.current = map
|
||||
|
||||
return () => {
|
||||
map.remove()
|
||||
mapRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
|
||||
if (map.getSource('dataset')) {
|
||||
if (data) {
|
||||
;(map.getSource('dataset') as maplibregl.GeoJSONSource).setData(data)
|
||||
} else {
|
||||
if (map.getLayer('dataset-fill')) {
|
||||
map.removeLayer('dataset-fill')
|
||||
}
|
||||
if (map.getLayer('dataset-line')) {
|
||||
map.removeLayer('dataset-line')
|
||||
}
|
||||
map.removeSource('dataset')
|
||||
return
|
||||
}
|
||||
} else if (data) {
|
||||
map.addSource('dataset', { type: 'geojson', data })
|
||||
map.addLayer({
|
||||
id: 'dataset-fill',
|
||||
type: 'fill',
|
||||
source: 'dataset',
|
||||
paint: { 'fill-color': '#f97316', 'fill-opacity': 0.4 },
|
||||
})
|
||||
map.addLayer({
|
||||
id: 'dataset-line',
|
||||
type: 'line',
|
||||
source: 'dataset',
|
||||
paint: { 'line-color': '#ea580c', 'line-width': 2 },
|
||||
})
|
||||
}
|
||||
|
||||
if (data) {
|
||||
const collection = data
|
||||
if (collection.type === 'FeatureCollection' && collection.features.length > 0) {
|
||||
const bounds = collectCoordinates(collection)
|
||||
if (bounds) {
|
||||
map.fitBounds(bounds, { padding: 40 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [data])
|
||||
|
||||
return <div className="map-container" ref={containerRef} />
|
||||
}
|
||||
|
||||
export default GeoMap
|
||||
@@ -0,0 +1,241 @@
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
DetectionModelCapability,
|
||||
DetectionQaResult,
|
||||
DetectionRead,
|
||||
DetectionRunRead,
|
||||
DetectionRunResponse,
|
||||
} from '../../types'
|
||||
|
||||
interface DetectionLabProps {
|
||||
detectionModels: DetectionModelCapability[]
|
||||
loadingDetectionModels: boolean
|
||||
detectionModelError: string | null
|
||||
selectedDetectionDatasetId: string
|
||||
selectedDetectionModelId: string
|
||||
detectionTileManifestPath: string
|
||||
detectionConfidenceThreshold: number
|
||||
runningDetection: boolean
|
||||
detectionRunResult: DetectionRunResponse | null
|
||||
detectionRunError: string | null
|
||||
detectionRuns: DetectionRunRead[]
|
||||
selectedDetectionRunId: string
|
||||
detectionItems: DetectionRead[]
|
||||
detectionClassFilter: string
|
||||
detectionMinConfidenceFilter: number
|
||||
loadingDetectionResults: boolean
|
||||
detectionReferenceDatasetId: string
|
||||
detectionQaResult: DetectionQaResult | null
|
||||
detectionQaError: string | null
|
||||
runningDetectionQa: boolean
|
||||
selectedProjectId: string | null
|
||||
rasterDatasets: DatasetCreateResponse[]
|
||||
referenceDatasets: DatasetCreateResponse[]
|
||||
onLoadModels: () => void
|
||||
onSelectDataset: (datasetId: string) => void
|
||||
onSelectModel: (modelId: string) => void
|
||||
onSetConfidenceThreshold: (value: number) => void
|
||||
onSetTileManifestPath: (value: string) => void
|
||||
onRunDetection: () => void
|
||||
onLoadRuns: () => void
|
||||
onSelectRun: (runId: string) => void
|
||||
onSetClassFilter: (value: string) => void
|
||||
onSetMinConfidenceFilter: (value: number) => void
|
||||
onLoadResults: () => void
|
||||
onSelectReferenceDataset: (datasetId: string) => void
|
||||
onRunQa: () => void
|
||||
}
|
||||
|
||||
export function DetectionLab({
|
||||
detectionModels,
|
||||
loadingDetectionModels,
|
||||
detectionModelError,
|
||||
selectedDetectionDatasetId,
|
||||
selectedDetectionModelId,
|
||||
detectionTileManifestPath,
|
||||
detectionConfidenceThreshold,
|
||||
runningDetection,
|
||||
detectionRunResult,
|
||||
detectionRunError,
|
||||
detectionRuns,
|
||||
selectedDetectionRunId,
|
||||
detectionItems,
|
||||
detectionClassFilter,
|
||||
detectionMinConfidenceFilter,
|
||||
loadingDetectionResults,
|
||||
detectionReferenceDatasetId,
|
||||
detectionQaResult,
|
||||
detectionQaError,
|
||||
runningDetectionQa,
|
||||
selectedProjectId,
|
||||
rasterDatasets,
|
||||
referenceDatasets,
|
||||
onLoadModels,
|
||||
onSelectDataset,
|
||||
onSelectModel,
|
||||
onSetConfidenceThreshold,
|
||||
onSetTileManifestPath,
|
||||
onRunDetection,
|
||||
onLoadRuns,
|
||||
onSelectRun,
|
||||
onSetClassFilter,
|
||||
onSetMinConfidenceFilter,
|
||||
onLoadResults,
|
||||
onSelectReferenceDataset,
|
||||
onRunQa,
|
||||
}: DetectionLabProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<h2>Detection Lab</h2>
|
||||
<button type="button" onClick={onLoadModels} disabled={loadingDetectionModels}>
|
||||
Refresh detection models
|
||||
</button>
|
||||
{loadingDetectionModels ? <p>Loading detection models...</p> : null}
|
||||
{detectionModelError ? <p className="error">{detectionModelError}</p> : null}
|
||||
{detectionModels.length === 0 && !loadingDetectionModels ? <p>No detection models reported by backend</p> : null}
|
||||
<ul>
|
||||
{detectionModels.map((model) => (
|
||||
<li key={model.model_id}>
|
||||
<strong>{model.display_name}</strong>
|
||||
<div>model: {model.model_id}</div>
|
||||
<div>framework: {model.framework}</div>
|
||||
<div>task: {model.task_type}</div>
|
||||
<div>status: {model.status}</div>
|
||||
<div>configured: {model.configured ? 'yes' : 'no'}</div>
|
||||
<div>classes: {model.supported_classes.join(', ')}</div>
|
||||
<div>limitation: {model.limitation_message}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<select value={selectedDetectionDatasetId} onChange={(event) => onSelectDataset(event.target.value)}>
|
||||
<option value="">Select raster dataset</option>
|
||||
{rasterDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select value={selectedDetectionModelId} onChange={(event) => onSelectModel(event.target.value)}>
|
||||
{detectionModels.map((model) => (
|
||||
<option key={model.model_id} value={model.model_id}>
|
||||
{model.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={detectionConfidenceThreshold}
|
||||
onChange={(event) => onSetConfidenceThreshold(Number(event.target.value))}
|
||||
/>
|
||||
{selectedDetectionModelId === 'yolo-configured' ? (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Raster tile manifest path"
|
||||
value={detectionTileManifestPath}
|
||||
onChange={(event) => onSetTileManifestPath(event.target.value)}
|
||||
/>
|
||||
) : null}
|
||||
<button type="button" onClick={onRunDetection} disabled={runningDetection || !selectedProjectId || rasterDatasets.length === 0}>
|
||||
Run detection
|
||||
</button>
|
||||
</div>
|
||||
{detectionRunError ? <p className="error">{detectionRunError}</p> : null}
|
||||
{detectionRunResult ? (
|
||||
<div>
|
||||
<p>Status: {detectionRunResult.status}</p>
|
||||
<p>Message: {detectionRunResult.message}</p>
|
||||
<p>Analysis run: {detectionRunResult.analysis_run_id}</p>
|
||||
<p>Job: {detectionRunResult.job_id}</p>
|
||||
<p>Detections: {detectionRunResult.detection_count}</p>
|
||||
{detectionRunResult.error_code ? <p className="error">Code: {detectionRunResult.error_code}</p> : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<h3>Detection results</h3>
|
||||
<button type="button" onClick={onLoadRuns} disabled={!selectedProjectId}>
|
||||
Refresh detection runs
|
||||
</button>
|
||||
<select value={selectedDetectionRunId} onChange={(event) => onSelectRun(event.target.value)}>
|
||||
<option value="">Select detection run</option>
|
||||
{detectionRuns.map((run) => (
|
||||
<option key={run.id} value={run.id}>
|
||||
{run.model_name || 'detection'} - {run.status} - {run.id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Class filter"
|
||||
value={detectionClassFilter}
|
||||
onChange={(event) => onSetClassFilter(event.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={detectionMinConfidenceFilter}
|
||||
onChange={(event) => onSetMinConfidenceFilter(Number(event.target.value))}
|
||||
/>
|
||||
<button type="button" onClick={onLoadResults} disabled={!selectedDetectionRunId || loadingDetectionResults}>
|
||||
Load detections
|
||||
</button>
|
||||
{loadingDetectionResults ? <p>Loading detection results...</p> : null}
|
||||
<p>Detections loaded: {detectionItems.length}</p>
|
||||
{detectionItems.length > 0 ? (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Confidence</th>
|
||||
<th>Model</th>
|
||||
<th>Source tile</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{detectionItems.map((detection) => (
|
||||
<tr key={detection.id}>
|
||||
<td>{detection.class_name}</td>
|
||||
<td>{detection.confidence.toFixed(2)}</td>
|
||||
<td>{detection.model_name}</td>
|
||||
<td>{detection.source_tile_path || 'n/a'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<h3>Detection QA</h3>
|
||||
<select value={detectionReferenceDatasetId} onChange={(event) => onSelectReferenceDataset(event.target.value)}>
|
||||
<option value="">Select reference dataset</option>
|
||||
{referenceDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" onClick={onRunQa} disabled={runningDetectionQa || !selectedDetectionRunId || !detectionReferenceDatasetId}>
|
||||
Compare detections to reference
|
||||
</button>
|
||||
{detectionQaError ? <p className="error">{detectionQaError}</p> : null}
|
||||
{detectionQaResult ? (
|
||||
<div>
|
||||
<p>Status: {detectionQaResult.status}</p>
|
||||
<p>Quality check: {detectionQaResult.quality_check_id}</p>
|
||||
<p>Precision: {detectionQaResult.precision?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>Recall: {detectionQaResult.recall?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>F1: {detectionQaResult.f1_score?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>Mean IoU: {detectionQaResult.mean_iou?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>False positives: {detectionQaResult.false_positives}</p>
|
||||
<p>False negatives: {detectionQaResult.false_negatives}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { DatasetCreateResponse, ExportCreateResponse, ExportRead } from '../../types'
|
||||
|
||||
interface ExportCenterProps {
|
||||
selectedProjectId: string | null
|
||||
selectedDataset: DatasetCreateResponse | null
|
||||
selectedDetectionRunId: string
|
||||
selectedSegmentationRunId: string
|
||||
exports: ExportRead[]
|
||||
latestExport: ExportCreateResponse | null
|
||||
exportError: string | null
|
||||
loadingExports: boolean
|
||||
exporting: boolean
|
||||
onRefresh: () => void
|
||||
onExportDataset: () => void
|
||||
onExportDetectionRun: () => void
|
||||
onExportSegmentationRun: () => void
|
||||
onExportProjectMetadata: () => void
|
||||
onExportProjectReport: () => void
|
||||
onPreviewContent: (exportId: string) => void
|
||||
onDownload: (exportId: string) => void
|
||||
}
|
||||
|
||||
function isVectorDatasetType(datasetType: string): boolean {
|
||||
return datasetType === 'vector' || datasetType === 'geojson'
|
||||
}
|
||||
|
||||
export function ExportCenter({
|
||||
selectedProjectId,
|
||||
selectedDataset,
|
||||
selectedDetectionRunId,
|
||||
selectedSegmentationRunId,
|
||||
exports,
|
||||
latestExport,
|
||||
exportError,
|
||||
loadingExports,
|
||||
exporting,
|
||||
onRefresh,
|
||||
onExportDataset,
|
||||
onExportDetectionRun,
|
||||
onExportSegmentationRun,
|
||||
onExportProjectMetadata,
|
||||
onExportProjectReport,
|
||||
onPreviewContent,
|
||||
onDownload,
|
||||
}: ExportCenterProps): JSX.Element {
|
||||
const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type))
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Export Center</h2>
|
||||
<button type="button" onClick={onRefresh} disabled={!selectedProjectId || loadingExports}>
|
||||
Refresh exports
|
||||
</button>
|
||||
<button type="button" onClick={onExportProjectMetadata} disabled={!selectedProjectId || exporting}>
|
||||
Export project metadata JSON
|
||||
</button>
|
||||
<button type="button" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
|
||||
Export project report HTML
|
||||
</button>
|
||||
<button type="button" onClick={onExportDataset} disabled={!canExportDataset || exporting}>
|
||||
Export selected vector GeoJSON
|
||||
</button>
|
||||
<button type="button" onClick={onExportDetectionRun} disabled={!selectedDetectionRunId || exporting}>
|
||||
Export selected detection run GeoJSON
|
||||
</button>
|
||||
<button type="button" onClick={onExportSegmentationRun} disabled={!selectedSegmentationRunId || exporting}>
|
||||
Export selected segmentation run GeoJSON
|
||||
</button>
|
||||
{exportError ? <p className="error">{exportError}</p> : null}
|
||||
{latestExport ? (
|
||||
<p>
|
||||
Latest export: {latestExport.export_type} {'->'} {latestExport.path}
|
||||
</p>
|
||||
) : null}
|
||||
{exports.length === 0 ? <p>No exports registered yet.</p> : null}
|
||||
<ul>
|
||||
{exports.map((item) => (
|
||||
<li key={item.id}>
|
||||
<strong>{item.export_type}</strong>
|
||||
<div>status: {item.status}</div>
|
||||
<div>path: {item.storage_path}</div>
|
||||
<div>export id: {item.id}</div>
|
||||
<button type="button" onClick={() => onPreviewContent(item.id)}>
|
||||
Preview JSON content
|
||||
</button>
|
||||
<button type="button" onClick={() => onDownload(item.id)}>
|
||||
Download artifact
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { FormEvent } from 'react'
|
||||
import type { AreaRead, ProjectRead } from '../../types'
|
||||
|
||||
interface AreaFormState {
|
||||
name: string
|
||||
geometry: string
|
||||
crs: string
|
||||
}
|
||||
|
||||
interface AreaPanelProps {
|
||||
areas: AreaRead[]
|
||||
selectedProject: ProjectRead | null
|
||||
selectedProjectId: string | null
|
||||
loadingAreas: boolean
|
||||
areaForm: AreaFormState
|
||||
onCreateArea: (event: FormEvent<HTMLFormElement>) => void
|
||||
onUpdateAreaForm: (areaForm: AreaFormState) => void
|
||||
}
|
||||
|
||||
export function AreaPanel({
|
||||
areas,
|
||||
selectedProject,
|
||||
selectedProjectId,
|
||||
loadingAreas,
|
||||
areaForm,
|
||||
onCreateArea,
|
||||
onUpdateAreaForm,
|
||||
}: AreaPanelProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<h2>Area manager</h2>
|
||||
<p>{selectedProject ? `Selected project: ${selectedProject.name}` : 'Select a project first'}</p>
|
||||
|
||||
<form onSubmit={onCreateArea}>
|
||||
<input
|
||||
value={areaForm.name}
|
||||
onChange={(event) => onUpdateAreaForm({ ...areaForm, name: event.target.value })}
|
||||
placeholder="AOI name"
|
||||
/>
|
||||
<input
|
||||
value={areaForm.crs}
|
||||
onChange={(event) => onUpdateAreaForm({ ...areaForm, crs: event.target.value })}
|
||||
placeholder="EPSG:4326"
|
||||
/>
|
||||
<textarea
|
||||
value={areaForm.geometry}
|
||||
onChange={(event) => onUpdateAreaForm({ ...areaForm, geometry: event.target.value })}
|
||||
rows={4}
|
||||
/>
|
||||
<button type="submit" disabled={!selectedProjectId}>
|
||||
Create area
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{loadingAreas ? <p>Loading areas...</p> : null}
|
||||
{areas.length === 0 ? <p>No areas yet</p> : null}
|
||||
<ul>
|
||||
{areas.map((area) => (
|
||||
<li key={area.id}>
|
||||
{area.name} · {area.area_m2 ? `${area.area_m2.toFixed(2)} m²` : 'n/a'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { FormEvent } from 'react'
|
||||
import type { ProjectCreate, ProjectRead } from '../../types'
|
||||
|
||||
interface ProjectPanelProps {
|
||||
projects: ProjectRead[]
|
||||
selectedProjectId: string | null
|
||||
loadingProjects: boolean
|
||||
projectForm: ProjectCreate
|
||||
loadingDemoWorkflow: boolean
|
||||
demoWorkflowMessage: string | null
|
||||
onCreateProject: (event: FormEvent<HTMLFormElement>) => void
|
||||
onUpdateProjectForm: (projectForm: ProjectCreate) => void
|
||||
onSelectProject: (projectId: string) => void
|
||||
onLoadDemoWorkflow: () => void
|
||||
}
|
||||
|
||||
export function ProjectPanel({
|
||||
projects,
|
||||
selectedProjectId,
|
||||
loadingProjects,
|
||||
projectForm,
|
||||
loadingDemoWorkflow,
|
||||
demoWorkflowMessage,
|
||||
onCreateProject,
|
||||
onUpdateProjectForm,
|
||||
onSelectProject,
|
||||
onLoadDemoWorkflow,
|
||||
}: ProjectPanelProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<h2>Projects</h2>
|
||||
{loadingProjects ? <p>Loading projects...</p> : null}
|
||||
|
||||
<form onSubmit={onCreateProject}>
|
||||
<input
|
||||
value={projectForm.name}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, name: event.target.value })}
|
||||
placeholder="Project name"
|
||||
/>
|
||||
<input
|
||||
value={projectForm.description ?? ''}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, description: event.target.value })}
|
||||
placeholder="Description"
|
||||
/>
|
||||
<input
|
||||
value={projectForm.region ?? 'Kempen'}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, region: event.target.value })}
|
||||
placeholder="Region"
|
||||
/>
|
||||
<button type="submit">Create project</button>
|
||||
</form>
|
||||
|
||||
<div className="demo-actions">
|
||||
<button type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow}>
|
||||
{loadingDemoWorkflow ? 'Loading demo...' : 'Load demo workflow'}
|
||||
</button>
|
||||
{demoWorkflowMessage ? <p>{demoWorkflowMessage}</p> : null}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{projects.map((project) => (
|
||||
<li key={project.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelectProject(project.id)}
|
||||
aria-pressed={project.id === selectedProjectId}
|
||||
>
|
||||
{project.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{projects.length === 0 ? <li>No projects yet</li> : null}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { ProviderCapability } from '../../types'
|
||||
|
||||
interface ProviderPanelProps {
|
||||
providers: ProviderCapability[]
|
||||
loadingCapabilities: boolean
|
||||
capabilitiesError: string | null
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export function ProviderPanel({
|
||||
providers,
|
||||
loadingCapabilities,
|
||||
capabilitiesError,
|
||||
onRefresh,
|
||||
}: ProviderPanelProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<h2>Provider Capabilities</h2>
|
||||
<button type="button" onClick={onRefresh} disabled={loadingCapabilities}>
|
||||
Refresh providers
|
||||
</button>
|
||||
{loadingCapabilities ? <p>Loading provider capabilities...</p> : null}
|
||||
{capabilitiesError ? <p className="error">{capabilitiesError}</p> : null}
|
||||
{providers.length === 0 && !loadingCapabilities ? <p>No providers reported by backend</p> : null}
|
||||
<ul>
|
||||
{providers.map((provider) => (
|
||||
<li key={provider.provider_name}>
|
||||
<strong>{provider.display_name}</strong>
|
||||
<div>provider: {provider.provider_name}</div>
|
||||
<div>authority: {provider.authority_level}</div>
|
||||
<div>status: {provider.status}</div>
|
||||
<div>configured: {provider.configured ? 'yes' : 'no'}</div>
|
||||
<div>layers: {provider.supported_layers.join(', ')}</div>
|
||||
<div>geometry: {provider.supported_geometry_types.join(', ')}</div>
|
||||
<div>query modes: {provider.supported_query_modes.join(', ')}</div>
|
||||
<div>limitation: {provider.limitation_message}</div>
|
||||
<div>attribution: {provider.attribution}</div>
|
||||
<div>license: {provider.license_note}</div>
|
||||
{!provider.configured && provider.not_configured_reason ? (
|
||||
<div>reason: {provider.not_configured_reason}</div>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
SegmentationModelCapability,
|
||||
SegmentationQaResult,
|
||||
SegmentationRead,
|
||||
SegmentationRunRead,
|
||||
SegmentationRunResponse,
|
||||
} from '../../types'
|
||||
|
||||
interface SegmentationLabProps {
|
||||
segmentationModels: SegmentationModelCapability[]
|
||||
loadingSegmentationModels: boolean
|
||||
segmentationModelError: string | null
|
||||
selectedSegmentationDatasetId: string
|
||||
selectedSegmentationModelId: string
|
||||
segmentationConfidenceThreshold: number
|
||||
runningSegmentation: boolean
|
||||
segmentationRunResult: SegmentationRunResponse | null
|
||||
segmentationRunError: string | null
|
||||
segmentationRuns: SegmentationRunRead[]
|
||||
selectedSegmentationRunId: string
|
||||
segmentationItems: SegmentationRead[]
|
||||
segmentationClassFilter: string
|
||||
segmentationMinConfidenceFilter: number
|
||||
loadingSegmentationResults: boolean
|
||||
segmentationReferenceDatasetId: string
|
||||
segmentationQaResult: SegmentationQaResult | null
|
||||
segmentationQaError: string | null
|
||||
runningSegmentationQa: boolean
|
||||
selectedProjectId: string | null
|
||||
rasterDatasets: DatasetCreateResponse[]
|
||||
referenceDatasets: DatasetCreateResponse[]
|
||||
selectedSegmentationModelConfigured: boolean
|
||||
selectedSegmentationModelLimitation: string | null
|
||||
onLoadModels: () => void
|
||||
onSelectDataset: (datasetId: string) => void
|
||||
onSelectModel: (modelId: string) => void
|
||||
onSetConfidenceThreshold: (value: number) => void
|
||||
onRunSegmentation: () => void
|
||||
onLoadRuns: () => void
|
||||
onSelectRun: (runId: string) => void
|
||||
onSetClassFilter: (value: string) => void
|
||||
onSetMinConfidenceFilter: (value: number) => void
|
||||
onLoadResults: () => void
|
||||
onSelectReferenceDataset: (datasetId: string) => void
|
||||
onRunQa: () => void
|
||||
}
|
||||
|
||||
export function SegmentationLab({
|
||||
segmentationModels,
|
||||
loadingSegmentationModels,
|
||||
segmentationModelError,
|
||||
selectedSegmentationDatasetId,
|
||||
selectedSegmentationModelId,
|
||||
segmentationConfidenceThreshold,
|
||||
runningSegmentation,
|
||||
segmentationRunResult,
|
||||
segmentationRunError,
|
||||
segmentationRuns,
|
||||
selectedSegmentationRunId,
|
||||
segmentationItems,
|
||||
segmentationClassFilter,
|
||||
segmentationMinConfidenceFilter,
|
||||
loadingSegmentationResults,
|
||||
segmentationReferenceDatasetId,
|
||||
segmentationQaResult,
|
||||
segmentationQaError,
|
||||
runningSegmentationQa,
|
||||
selectedProjectId,
|
||||
rasterDatasets,
|
||||
referenceDatasets,
|
||||
selectedSegmentationModelConfigured,
|
||||
selectedSegmentationModelLimitation,
|
||||
onLoadModels,
|
||||
onSelectDataset,
|
||||
onSelectModel,
|
||||
onSetConfidenceThreshold,
|
||||
onRunSegmentation,
|
||||
onLoadRuns,
|
||||
onSelectRun,
|
||||
onSetClassFilter,
|
||||
onSetMinConfidenceFilter,
|
||||
onLoadResults,
|
||||
onSelectReferenceDataset,
|
||||
onRunQa,
|
||||
}: SegmentationLabProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<h2>Segmentation Lab</h2>
|
||||
<button type="button" onClick={onLoadModels} disabled={loadingSegmentationModels}>
|
||||
Refresh segmentation models
|
||||
</button>
|
||||
{loadingSegmentationModels ? <p>Loading segmentation models...</p> : null}
|
||||
{segmentationModelError ? <p className="error">{segmentationModelError}</p> : null}
|
||||
{segmentationModels.length === 0 && !loadingSegmentationModels ? <p>No segmentation models reported by backend</p> : null}
|
||||
<ul>
|
||||
{segmentationModels.map((model) => (
|
||||
<li key={model.model_id}>
|
||||
<strong>{model.display_name}</strong>
|
||||
<div>model: {model.model_id}</div>
|
||||
<div>framework: {model.framework}</div>
|
||||
<div>task: {model.task_type}</div>
|
||||
<div>status: {model.status}</div>
|
||||
<div>configured: {model.configured ? 'yes' : 'no'}</div>
|
||||
<div>classes: {model.supported_classes.join(', ')}</div>
|
||||
<div>limitation: {model.limitation_message}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<select value={selectedSegmentationDatasetId} onChange={(event) => onSelectDataset(event.target.value)}>
|
||||
<option value="">Select raster dataset</option>
|
||||
{rasterDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select value={selectedSegmentationModelId} onChange={(event) => onSelectModel(event.target.value)}>
|
||||
{segmentationModels.map((model) => (
|
||||
<option key={model.model_id} value={model.model_id}>
|
||||
{model.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={segmentationConfidenceThreshold}
|
||||
onChange={(event) => onSetConfidenceThreshold(Number(event.target.value))}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRunSegmentation}
|
||||
disabled={runningSegmentation || !selectedProjectId || rasterDatasets.length === 0 || !selectedSegmentationModelConfigured}
|
||||
>
|
||||
Run segmentation
|
||||
</button>
|
||||
</div>
|
||||
{!selectedSegmentationModelConfigured ? (
|
||||
<p>{selectedSegmentationModelLimitation ?? 'Select a configured segmentation model'}</p>
|
||||
) : null}
|
||||
{segmentationRunError ? <p className="error">{segmentationRunError}</p> : null}
|
||||
{segmentationRunResult ? (
|
||||
<div>
|
||||
<p>Status: {segmentationRunResult.status}</p>
|
||||
<p>Message: {segmentationRunResult.message}</p>
|
||||
<p>Analysis run: {segmentationRunResult.analysis_run_id}</p>
|
||||
<p>Job: {segmentationRunResult.job_id}</p>
|
||||
<p>Segmentations: {segmentationRunResult.segmentation_count}</p>
|
||||
{segmentationRunResult.error_code ? <p className="error">Code: {segmentationRunResult.error_code}</p> : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<h3>Segmentation results</h3>
|
||||
<button type="button" onClick={onLoadRuns} disabled={!selectedProjectId}>
|
||||
Refresh segmentation runs
|
||||
</button>
|
||||
<select value={selectedSegmentationRunId} onChange={(event) => onSelectRun(event.target.value)}>
|
||||
<option value="">Select segmentation run</option>
|
||||
{segmentationRuns.map((run) => (
|
||||
<option key={run.id} value={run.id}>
|
||||
{run.model_name || 'segmentation'} - {run.status} - {run.id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Class filter"
|
||||
value={segmentationClassFilter}
|
||||
onChange={(event) => onSetClassFilter(event.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={segmentationMinConfidenceFilter}
|
||||
onChange={(event) => onSetMinConfidenceFilter(Number(event.target.value))}
|
||||
/>
|
||||
<button type="button" onClick={onLoadResults} disabled={!selectedSegmentationRunId || loadingSegmentationResults}>
|
||||
Load segmentations
|
||||
</button>
|
||||
{loadingSegmentationResults ? <p>Loading segmentation results...</p> : null}
|
||||
<p>Segmentations loaded: {segmentationItems.length}</p>
|
||||
{segmentationItems.length > 0 ? (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Confidence</th>
|
||||
<th>Area m2</th>
|
||||
<th>Model</th>
|
||||
<th>Tile</th>
|
||||
<th>Mask path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{segmentationItems.map((segmentation) => (
|
||||
<tr key={segmentation.id}>
|
||||
<td>{segmentation.class_name}</td>
|
||||
<td>{segmentation.confidence?.toFixed(2) ?? 'n/a'}</td>
|
||||
<td>{segmentation.area_m2?.toFixed(2) ?? 'n/a'}</td>
|
||||
<td>{segmentation.model_name}</td>
|
||||
<td>{segmentation.source_tile_path || (segmentation.tile_index ?? 'n/a')}</td>
|
||||
<td>{segmentation.mask_path || 'n/a'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<h3>Segmentation QA</h3>
|
||||
<select value={segmentationReferenceDatasetId} onChange={(event) => onSelectReferenceDataset(event.target.value)}>
|
||||
<option value="">Select reference dataset</option>
|
||||
{referenceDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" onClick={onRunQa} disabled={runningSegmentationQa || !selectedSegmentationRunId || !segmentationReferenceDatasetId}>
|
||||
Compare segmentations to reference
|
||||
</button>
|
||||
{segmentationQaError ? <p className="error">{segmentationQaError}</p> : null}
|
||||
{segmentationQaResult ? (
|
||||
<div>
|
||||
<p>Status: {segmentationQaResult.status}</p>
|
||||
<p>Quality check: {segmentationQaResult.quality_check_id}</p>
|
||||
<p>Precision: {segmentationQaResult.precision?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>Recall: {segmentationQaResult.recall?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>F1: {segmentationQaResult.f1_score?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>Mean IoU: {segmentationQaResult.mean_iou?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>False positives: {segmentationQaResult.false_positives}</p>
|
||||
<p>False negatives: {segmentationQaResult.false_negatives}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user