Extract dataset presentational components
This commit is contained in:
+68
-369
@@ -6,6 +6,8 @@ import { datasetsApi } from './services/api/datasets'
|
||||
import { projectsApi } from './services/api/projects'
|
||||
import { analysisApi, demoApi, externalApi } from './services/api'
|
||||
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
||||
import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel'
|
||||
import { DatasetPanel } from './components/datasets/DatasetPanel'
|
||||
import { DetectionLab } from './components/detection/DetectionLab'
|
||||
import { ExportCenter } from './components/exports/ExportCenter'
|
||||
import { AreaPanel } from './components/project/AreaPanel'
|
||||
@@ -36,31 +38,6 @@ 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 App(): JSX.Element {
|
||||
const [projects, setProjects] = useState<ProjectRead[]>([])
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||
@@ -368,14 +345,6 @@ function App(): JSX.Element {
|
||||
}, [changeDetectionResult, datasetContent, detectionGeoJson, segmentationGeoJson, selectedDataset])
|
||||
const mapFeatureCount = mapFeatureCollection?.features.length ?? 0
|
||||
const areaFeatureCount = areaFeatureCollection?.features.length ?? 0
|
||||
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)
|
||||
@@ -798,344 +767,74 @@ function App(): JSX.Element {
|
||||
</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>
|
||||
<DatasetPanel
|
||||
selectedProjectId={selectedProjectId}
|
||||
areas={areas}
|
||||
datasets={datasets}
|
||||
datasetForm={datasetForm}
|
||||
loadingDatasets={loadingDatasets}
|
||||
onDatasetFormChange={setDatasetForm}
|
||||
onUploadDataset={uploadDataset}
|
||||
onLoadDatasetDetails={loadDatasetDetails}
|
||||
onRefreshMetadata={refreshMetadata}
|
||||
/>
|
||||
</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(availableVectorTargets)}>
|
||||
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>
|
||||
<DatasetDetailPanel
|
||||
areas={areas}
|
||||
availableVectorTargets={availableVectorTargets}
|
||||
selectedDatasetId={selectedDatasetId}
|
||||
selectedDataset={selectedDataset}
|
||||
selectedDatasetSummary={selectedDatasetSummary}
|
||||
selectedRasterMetadata={selectedRasterMetadata}
|
||||
selectedRasterStats={selectedRasterStats}
|
||||
rasterPreview={rasterPreview}
|
||||
rasterUnavailableMessage={rasterUnavailableMessage}
|
||||
selectedClipAreaId={selectedClipAreaId}
|
||||
selectedIntersectTargetId={selectedIntersectTargetId}
|
||||
rasterTileSize={rasterTileSize}
|
||||
rasterTileOverlap={rasterTileOverlap}
|
||||
rasterTileOutputName={rasterTileOutputName}
|
||||
rasterReprojectCrs={rasterReprojectCrs}
|
||||
rasterReprojectResampling={rasterReprojectResampling}
|
||||
ndviNirBand={ndviNirBand}
|
||||
ndviRedBand={ndviRedBand}
|
||||
ndwiGreenBand={ndwiGreenBand}
|
||||
ndwiNirBand={ndwiNirBand}
|
||||
ndbiSwirBand={ndbiSwirBand}
|
||||
ndbiNirBand={ndbiNirBand}
|
||||
isRasterTileInputValid={isRasterTileInputValid}
|
||||
jobs={jobs}
|
||||
loadingDatasetDetails={loadingDatasetDetails}
|
||||
datasetDetailError={datasetDetailError}
|
||||
isVectorDatasetType={isVectorDatasetType}
|
||||
onSetSelectedClipAreaId={setSelectedClipAreaId}
|
||||
onSetSelectedIntersectTargetId={setSelectedIntersectTargetId}
|
||||
onSetRasterTileSize={setRasterTileSize}
|
||||
onSetRasterTileOverlap={setRasterTileOverlap}
|
||||
onSetRasterTileOutputName={setRasterTileOutputName}
|
||||
onSetRasterReprojectCrs={setRasterReprojectCrs}
|
||||
onSetRasterReprojectResampling={setRasterReprojectResampling}
|
||||
onSetNdviNirBand={setNdviNirBand}
|
||||
onSetNdviRedBand={setNdviRedBand}
|
||||
onSetNdwiGreenBand={setNdwiGreenBand}
|
||||
onSetNdwiNirBand={setNdwiNirBand}
|
||||
onSetNdbiSwirBand={setNdbiSwirBand}
|
||||
onSetNdbiNirBand={setNdbiNirBand}
|
||||
onRunRasterInspect={runRasterInspect}
|
||||
onRunRasterPreview={runRasterPreview}
|
||||
onRunRasterStats={runRasterStats}
|
||||
onRunRasterReproject={runRasterReproject}
|
||||
onRunRasterClip={runRasterClip}
|
||||
onRunRasterTile={runRasterTile}
|
||||
onRunRasterNdvi={runRasterNdvi}
|
||||
onRunRasterNdwi={runRasterNdwi}
|
||||
onRunRasterNdbi={runRasterNdbi}
|
||||
onRunVectorClip={runVectorClip}
|
||||
onRunVectorBuffer={runVectorBuffer}
|
||||
onRunVectorIntersect={() => runVectorIntersect(availableVectorTargets)}
|
||||
onPickDerivedDataset={pickDerivedDataset}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<h2>Map workspace</h2>
|
||||
|
||||
Reference in New Issue
Block a user