Extract dataset presentational components
This commit is contained in:
@@ -7,6 +7,14 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Sprint 29 dataset component decomposition (2026-06-17)
|
||||||
|
|
||||||
|
- Moved dataset upload/list UI into `DatasetPanel`.
|
||||||
|
- Moved dataset details and job list UI into `DatasetDetailPanel`.
|
||||||
|
- Split raster and vector controls into `RasterControls` and `VectorControls`.
|
||||||
|
- Added regression coverage to verify `App.tsx` wires the new presentational dataset components without taking dataset markup back inline.
|
||||||
|
- No API contracts, backend behavior, migrations, product features, provider fetching, AI behavior or UI redesign were introduced.
|
||||||
|
|
||||||
## Sprint 28 dataset workflow hook hardening (2026-06-17)
|
## Sprint 28 dataset workflow hook hardening (2026-06-17)
|
||||||
|
|
||||||
- Moved dataset selection, upload, detail loading, dataset jobs and raster/vector operation orchestration from `App.tsx` into `useDatasetWorkflow`.
|
- Moved dataset selection, upload, detail loading, dataset jobs and raster/vector operation orchestration from `App.tsx` into `useDatasetWorkflow`.
|
||||||
|
|||||||
@@ -41,19 +41,32 @@ def test_dataset_workflow_hook_owns_dataset_api_calls() -> None:
|
|||||||
|
|
||||||
def test_app_still_wires_dataset_ui_callbacks() -> None:
|
def test_app_still_wires_dataset_ui_callbacks() -> None:
|
||||||
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
dataset_panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetPanel.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
detail_panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetDetailPanel.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
assert "<form onSubmit={uploadDataset}" in app
|
assert "<DatasetPanel" in app
|
||||||
assert "onClick={() => loadDatasetDetails(selectedProjectId ?? '', dataset)}" in app
|
assert "onUploadDataset={uploadDataset}" in app
|
||||||
assert "onClick={() => refreshMetadata(dataset.id)}" in app
|
assert "onLoadDatasetDetails={loadDatasetDetails}" in app
|
||||||
assert "onClick={runRasterInspect}" in app
|
assert "onRefreshMetadata={refreshMetadata}" in app
|
||||||
assert "onClick={runRasterPreview}" in app
|
assert "<DatasetDetailPanel" in app
|
||||||
assert "onClick={runRasterStats}" in app
|
assert "onRunRasterInspect={runRasterInspect}" in app
|
||||||
assert "onClick={runRasterReproject}" in app
|
assert "onRunRasterPreview={runRasterPreview}" in app
|
||||||
assert "onClick={runRasterClip}" in app
|
assert "onRunRasterStats={runRasterStats}" in app
|
||||||
assert "onClick={runRasterTile}" in app
|
assert "onRunRasterReproject={runRasterReproject}" in app
|
||||||
assert "onClick={runRasterNdvi}" in app
|
assert "onRunRasterClip={runRasterClip}" in app
|
||||||
assert "onClick={runRasterNdwi}" in app
|
assert "onRunRasterTile={runRasterTile}" in app
|
||||||
assert "onClick={runRasterNdbi}" in app
|
assert "onRunRasterNdvi={runRasterNdvi}" in app
|
||||||
assert "onClick={runVectorClip}" in app
|
assert "onRunRasterNdwi={runRasterNdwi}" in app
|
||||||
assert "onClick={runVectorBuffer}" in app
|
assert "onRunRasterNdbi={runRasterNdbi}" in app
|
||||||
assert "onClick={() => runVectorIntersect(availableVectorTargets)}" in app
|
assert "onRunVectorClip={runVectorClip}" in app
|
||||||
|
assert "onRunVectorBuffer={runVectorBuffer}" in app
|
||||||
|
assert "onRunVectorIntersect={() => runVectorIntersect(availableVectorTargets)}" in app
|
||||||
|
assert "<form onSubmit={onUploadDataset}" in dataset_panel
|
||||||
|
assert "onClick={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)}" in dataset_panel
|
||||||
|
assert "onClick={() => onRefreshMetadata(dataset.id)}" in dataset_panel
|
||||||
|
assert "onRunRasterInspect={onRunRasterInspect}" in detail_panel
|
||||||
|
assert "onRunVectorIntersect={onRunVectorIntersect}" in detail_panel
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_uses_dataset_presentational_components() -> None:
|
||||||
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "from './components/datasets/DatasetPanel'" in app
|
||||||
|
assert "from './components/datasets/DatasetDetailPanel'" in app
|
||||||
|
assert "<DatasetPanel" in app
|
||||||
|
assert "<DatasetDetailPanel" in app
|
||||||
|
assert "<form onSubmit={uploadDataset}" not in app
|
||||||
|
assert "<h3>Raster operations</h3>" not in app
|
||||||
|
assert "<h3>Vector operations</h3>" not in app
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataset_panel_owns_upload_and_list_markup() -> None:
|
||||||
|
panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetPanel.tsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "Upload dataset" in panel
|
||||||
|
assert "Refresh metadata" in panel
|
||||||
|
assert "Select / details" in panel
|
||||||
|
assert "onLoadDatasetDetails" in panel
|
||||||
|
assert "onRefreshMetadata" in panel
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataset_detail_panel_composes_raster_and_vector_controls() -> None:
|
||||||
|
detail_panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetDetailPanel.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
raster_controls = (ROOT / "frontend" / "src" / "components" / "datasets" / "RasterControls.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
vector_controls = (ROOT / "frontend" / "src" / "components" / "datasets" / "VectorControls.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "<RasterControls" in detail_panel
|
||||||
|
assert "<VectorControls" in detail_panel
|
||||||
|
assert "Jobs" in detail_panel
|
||||||
|
assert "Raster operations" in raster_controls
|
||||||
|
assert "Compute NDVI" in raster_controls
|
||||||
|
assert "Generate tiles" in raster_controls
|
||||||
|
assert "Vector operations" in vector_controls
|
||||||
|
assert "Run intersect" in vector_controls
|
||||||
@@ -1,3 +1,27 @@
|
|||||||
|
## Sprint 29 dataset component decomposition (2026-06-17)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Moved dataset upload/list rendering from `frontend/src/App.tsx` into `frontend/src/components/datasets/DatasetPanel.tsx`.
|
||||||
|
- Moved dataset detail and job-list rendering into `frontend/src/components/datasets/DatasetDetailPanel.tsx`.
|
||||||
|
- Split raster controls and vector controls into `frontend/src/components/datasets/RasterControls.tsx` and `frontend/src/components/datasets/VectorControls.tsx`.
|
||||||
|
- Updated Sprint 28 regression tests for the new component boundary and added Sprint 29 component wiring tests.
|
||||||
|
- Updated frontend README, TODO and changelog docs.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `python -m pytest backend/tests/test_sprint29_dataset_components.py backend/tests/test_sprint28_dataset_workflow_hook.py backend/tests/test_sprint27_frontend_workflow_hooks.py` passed: 10 tests.
|
||||||
|
- `python -m compileall backend/app` passed.
|
||||||
|
- `cd backend && python -m pytest` passed: 170 tests.
|
||||||
|
- `cd frontend && npm run typecheck` passed.
|
||||||
|
- `cd frontend && npm run build` passed.
|
||||||
|
- `bash scripts/run_readiness_check.sh` passed: 170 backend tests, frontend typecheck/build, Alembic head check and script syntax checks.
|
||||||
|
- `cd backend && python -m alembic heads` passed: single head `202606120900`.
|
||||||
|
- `cd backend && python -m alembic upgrade head --sql` passed.
|
||||||
|
- `bash -n scripts/live_migration_smoke.sh` passed.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- No API contracts, backend behavior, migrations, product features, provider fetching, AI behavior or UI redesign changed.
|
||||||
|
- Next maintainability pass should split change detection, QA/QC results and map workspace controls into focused presentational components.
|
||||||
|
|
||||||
## Sprint 28 dataset workflow hook hardening (2026-06-17)
|
## Sprint 28 dataset workflow hook hardening (2026-06-17)
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
|
|||||||
+2
-1
@@ -39,7 +39,8 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Detection and segmentation workflow hook extraction beyond Sprint 10.
|
- [x] Detection and segmentation workflow hook extraction beyond Sprint 10.
|
||||||
- [x] Export and QA/QC workflow hook extraction beyond Sprint 10.
|
- [x] Export and QA/QC workflow hook extraction beyond Sprint 10.
|
||||||
- [x] Dataset, raster and vector workflow hook extraction beyond Sprint 10.
|
- [x] Dataset, raster and vector workflow hook extraction beyond Sprint 10.
|
||||||
- [ ] Further frontend component decomposition for dataset detail, raster controls and vector controls.
|
- [x] Dataset detail, raster controls and vector controls component decomposition.
|
||||||
|
- [ ] Further frontend component decomposition for change detection, QA/QC results and map workspace controls.
|
||||||
|
|
||||||
## Sprint 8 status
|
## Sprint 8 status
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,13 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow.
|
|||||||
- Project-scoped dataset listing remains in `App.tsx` because it is still part of the shared project/area load boundary.
|
- Project-scoped dataset listing remains in `App.tsx` because it is still part of the shared project/area load boundary.
|
||||||
- Dataset details, raster controls, vector controls and job list behavior are unchanged; the UI still receives the same callbacks and state.
|
- Dataset details, raster controls, vector controls and job list behavior are unchanged; the UI still receives the same callbacks and state.
|
||||||
|
|
||||||
|
## Sprint 29 maintainability updates
|
||||||
|
|
||||||
|
- Dataset upload/list rendering moved into `src/components/datasets/DatasetPanel.tsx`.
|
||||||
|
- Dataset details and job-list rendering moved into `src/components/datasets/DatasetDetailPanel.tsx`.
|
||||||
|
- Raster controls and vector controls now live in `src/components/datasets/RasterControls.tsx` and `src/components/datasets/VectorControls.tsx`.
|
||||||
|
- `App.tsx` still owns cross-module orchestration and passes the same `useDatasetWorkflow` state/actions into these presentational components.
|
||||||
|
|
||||||
## Release hardening updates
|
## Release hardening updates
|
||||||
|
|
||||||
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
||||||
|
|||||||
+66
-367
@@ -6,6 +6,8 @@ import { datasetsApi } from './services/api/datasets'
|
|||||||
import { projectsApi } from './services/api/projects'
|
import { projectsApi } from './services/api/projects'
|
||||||
import { analysisApi, demoApi, externalApi } from './services/api'
|
import { analysisApi, demoApi, externalApi } from './services/api'
|
||||||
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
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 { DetectionLab } from './components/detection/DetectionLab'
|
||||||
import { ExportCenter } from './components/exports/ExportCenter'
|
import { ExportCenter } from './components/exports/ExportCenter'
|
||||||
import { AreaPanel } from './components/project/AreaPanel'
|
import { AreaPanel } from './components/project/AreaPanel'
|
||||||
@@ -36,31 +38,6 @@ function isVectorDatasetType(datasetType: string): boolean {
|
|||||||
return datasetType === 'vector' || datasetType === 'geojson'
|
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 {
|
function App(): JSX.Element {
|
||||||
const [projects, setProjects] = useState<ProjectRead[]>([])
|
const [projects, setProjects] = useState<ProjectRead[]>([])
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
||||||
@@ -368,14 +345,6 @@ function App(): JSX.Element {
|
|||||||
}, [changeDetectionResult, datasetContent, detectionGeoJson, segmentationGeoJson, selectedDataset])
|
}, [changeDetectionResult, datasetContent, detectionGeoJson, segmentationGeoJson, selectedDataset])
|
||||||
const mapFeatureCount = mapFeatureCollection?.features.length ?? 0
|
const mapFeatureCount = mapFeatureCollection?.features.length ?? 0
|
||||||
const areaFeatureCount = areaFeatureCollection?.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 () => {
|
const loadProjects = async () => {
|
||||||
setLoadingProjects(true)
|
setLoadingProjects(true)
|
||||||
setErrorMessage(null)
|
setErrorMessage(null)
|
||||||
@@ -798,344 +767,74 @@ function App(): JSX.Element {
|
|||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<section>
|
<DatasetPanel
|
||||||
<h2>Datasets</h2>
|
selectedProjectId={selectedProjectId}
|
||||||
<form onSubmit={uploadDataset}>
|
areas={areas}
|
||||||
<select
|
datasets={datasets}
|
||||||
value={datasetForm.datasetType}
|
datasetForm={datasetForm}
|
||||||
onChange={(event) => setDatasetForm((previous) => ({ ...previous, datasetType: event.target.value }))}
|
loadingDatasets={loadingDatasets}
|
||||||
>
|
onDatasetFormChange={setDatasetForm}
|
||||||
<option value="vector">vector</option>
|
onUploadDataset={uploadDataset}
|
||||||
<option value="geojson">geojson</option>
|
onLoadDatasetDetails={loadDatasetDetails}
|
||||||
<option value="raster">raster</option>
|
onRefreshMetadata={refreshMetadata}
|
||||||
</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>
|
</main>
|
||||||
|
|
||||||
<section>
|
<DatasetDetailPanel
|
||||||
<h2>Dataset details</h2>
|
areas={areas}
|
||||||
{selectedDatasetId ? <p>Selected dataset: {selectedDatasetId}</p> : <p>No dataset selected</p>}
|
availableVectorTargets={availableVectorTargets}
|
||||||
{selectedDataset ? (
|
selectedDatasetId={selectedDatasetId}
|
||||||
<div>
|
selectedDataset={selectedDataset}
|
||||||
<p>
|
selectedDatasetSummary={selectedDatasetSummary}
|
||||||
<strong>{selectedDataset.name}</strong>
|
selectedRasterMetadata={selectedRasterMetadata}
|
||||||
</p>
|
selectedRasterStats={selectedRasterStats}
|
||||||
<p>Type: {selectedDataset.dataset_type}</p>
|
rasterPreview={rasterPreview}
|
||||||
<p>Status: {selectedDataset.status}</p>
|
rasterUnavailableMessage={rasterUnavailableMessage}
|
||||||
<p>Original file: {selectedDataset.original_filename ?? 'n/a'}</p>
|
selectedClipAreaId={selectedClipAreaId}
|
||||||
<p>Stored file: {selectedDataset.stored_filename ?? 'n/a'}</p>
|
selectedIntersectTargetId={selectedIntersectTargetId}
|
||||||
<p>Content type: {selectedDataset.content_type ?? 'n/a'}</p>
|
rasterTileSize={rasterTileSize}
|
||||||
<p>File size: {formatBytes(selectedDataset.size_bytes)}</p>
|
rasterTileOverlap={rasterTileOverlap}
|
||||||
<p>SHA256: {selectedDataset.checksum_sha256 ?? 'n/a'}</p>
|
rasterTileOutputName={rasterTileOutputName}
|
||||||
<p>Feature count: {selectedDatasetSummary?.feature_count ?? selectedDataset.feature_count ?? 'n/a'}</p>
|
rasterReprojectCrs={rasterReprojectCrs}
|
||||||
<p>BBox: {formatBounds(selectedDatasetSummary?.bounds_json ?? selectedDataset.bounds_json)}</p>
|
rasterReprojectResampling={rasterReprojectResampling}
|
||||||
{selectedDataset.dataset_type === 'raster' ? (
|
ndviNirBand={ndviNirBand}
|
||||||
<div>
|
ndviRedBand={ndviRedBand}
|
||||||
<p>Raster driver: {selectedRasterMetadata?.driver ?? 'n/a'}</p>
|
ndwiGreenBand={ndwiGreenBand}
|
||||||
<p>Raster size: {selectedRasterMetadata ? `${selectedRasterMetadata.width} x ${selectedRasterMetadata.height}` : 'n/a'}</p>
|
ndwiNirBand={ndwiNirBand}
|
||||||
<p>Raster checksum: {selectedRasterMetadata?.checksum_sha256 ?? 'n/a'}</p>
|
ndbiSwirBand={ndbiSwirBand}
|
||||||
<p>
|
ndbiNirBand={ndbiNirBand}
|
||||||
Profile: CRS {selectedRasterMetadata?.crs ?? 'n/a'} | bands {selectedRasterMetadata?.band_count ?? 'n/a'} | dtype {
|
isRasterTileInputValid={isRasterTileInputValid}
|
||||||
(selectedRasterMetadata?.dtype as string[] | undefined)?.join(', ') ?? 'n/a'}
|
jobs={jobs}
|
||||||
</p>
|
loadingDatasetDetails={loadingDatasetDetails}
|
||||||
<p>Bounds: {formatRasterBounds(selectedRasterMetadata?.bounds)}</p>
|
datasetDetailError={datasetDetailError}
|
||||||
<p>Resolution: {selectedRasterMetadata?.resolution ? selectedRasterMetadata.resolution.join(', ') : 'n/a'}</p>
|
isVectorDatasetType={isVectorDatasetType}
|
||||||
{rasterUnavailableMessage ? <p className="error">Raster unavailable: {rasterUnavailableMessage}</p> : null}
|
onSetSelectedClipAreaId={setSelectedClipAreaId}
|
||||||
<h3>Raster operations</h3>
|
onSetSelectedIntersectTargetId={setSelectedIntersectTargetId}
|
||||||
<p>Available operations: inspect, stats, reproject, preview, clip by selected area, tile generation.</p>
|
onSetRasterTileSize={setRasterTileSize}
|
||||||
<p>Preview: {rasterPreview?.preview.path ?? 'not generated'}</p>
|
onSetRasterTileOverlap={setRasterTileOverlap}
|
||||||
<p>Preview size: {rasterPreview?.preview.width ?? 'n/a'} x {rasterPreview?.preview.height ?? 'n/a'}</p>
|
onSetRasterTileOutputName={setRasterTileOutputName}
|
||||||
<button type="button" onClick={runRasterInspect}>
|
onSetRasterReprojectCrs={setRasterReprojectCrs}
|
||||||
Inspect raster metadata
|
onSetRasterReprojectResampling={setRasterReprojectResampling}
|
||||||
</button>
|
onSetNdviNirBand={setNdviNirBand}
|
||||||
<button type="button" onClick={runRasterPreview} disabled={!selectedDatasetId}>
|
onSetNdviRedBand={setNdviRedBand}
|
||||||
Generate preview
|
onSetNdwiGreenBand={setNdwiGreenBand}
|
||||||
</button>
|
onSetNdwiNirBand={setNdwiNirBand}
|
||||||
<button type="button" onClick={runRasterStats}>
|
onSetNdbiSwirBand={setNdbiSwirBand}
|
||||||
Compute band statistics
|
onSetNdbiNirBand={setNdbiNirBand}
|
||||||
</button>
|
onRunRasterInspect={runRasterInspect}
|
||||||
{selectedRasterStats ? (
|
onRunRasterPreview={runRasterPreview}
|
||||||
<div>
|
onRunRasterStats={runRasterStats}
|
||||||
<h4>Band statistics</h4>
|
onRunRasterReproject={runRasterReproject}
|
||||||
<p>Generated: {selectedRasterStats.generated_at ?? 'n/a'}</p>
|
onRunRasterClip={runRasterClip}
|
||||||
<ul>
|
onRunRasterTile={runRasterTile}
|
||||||
{selectedRasterStats.bands.map((band) => (
|
onRunRasterNdvi={runRasterNdvi}
|
||||||
<li key={band.band_index}>
|
onRunRasterNdwi={runRasterNdwi}
|
||||||
Band {band.band_index}: min {band.min ?? 'n/a'}, max {band.max ?? 'n/a'}, mean {band.mean ?? 'n/a'}, std {band.std ?? 'n/a'},
|
onRunRasterNdbi={runRasterNdbi}
|
||||||
valid {band.valid_pixel_count}, nodata ratio {(band.nodata_ratio * 100).toFixed(2)}%, dtype {band.dtype ?? 'n/a'}
|
onRunVectorClip={runVectorClip}
|
||||||
</li>
|
onRunVectorBuffer={runVectorBuffer}
|
||||||
))}
|
onRunVectorIntersect={() => runVectorIntersect(availableVectorTargets)}
|
||||||
</ul>
|
onPickDerivedDataset={pickDerivedDataset}
|
||||||
</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>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Map workspace</h2>
|
<h2>Map workspace</h2>
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import type {
|
||||||
|
AreaRead,
|
||||||
|
DatasetCreateResponse,
|
||||||
|
JobRead,
|
||||||
|
RasterMetadataResponse,
|
||||||
|
RasterPreviewResponse,
|
||||||
|
RasterStatsResponse,
|
||||||
|
VectorSummary,
|
||||||
|
} from '../../types'
|
||||||
|
import { RasterControls } from './RasterControls'
|
||||||
|
import { VectorControls } from './VectorControls'
|
||||||
|
|
||||||
|
interface DatasetDetailPanelProps {
|
||||||
|
areas: AreaRead[]
|
||||||
|
availableVectorTargets: DatasetCreateResponse[]
|
||||||
|
selectedDatasetId: string | null
|
||||||
|
selectedDataset: DatasetCreateResponse | null
|
||||||
|
selectedDatasetSummary: VectorSummary | null
|
||||||
|
selectedRasterMetadata: RasterMetadataResponse | null
|
||||||
|
selectedRasterStats: RasterStatsResponse | null
|
||||||
|
rasterPreview: RasterPreviewResponse | null
|
||||||
|
rasterUnavailableMessage: string | null
|
||||||
|
selectedClipAreaId: string
|
||||||
|
selectedIntersectTargetId: string
|
||||||
|
rasterTileSize: number
|
||||||
|
rasterTileOverlap: number
|
||||||
|
rasterTileOutputName: string
|
||||||
|
rasterReprojectCrs: string
|
||||||
|
rasterReprojectResampling: string
|
||||||
|
ndviNirBand: number
|
||||||
|
ndviRedBand: number
|
||||||
|
ndwiGreenBand: number
|
||||||
|
ndwiNirBand: number
|
||||||
|
ndbiSwirBand: number
|
||||||
|
ndbiNirBand: number
|
||||||
|
isRasterTileInputValid: boolean
|
||||||
|
jobs: JobRead[]
|
||||||
|
loadingDatasetDetails: boolean
|
||||||
|
datasetDetailError: string | null
|
||||||
|
isVectorDatasetType: (datasetType: string) => boolean
|
||||||
|
onSetSelectedClipAreaId: (value: string) => void
|
||||||
|
onSetSelectedIntersectTargetId: (value: string) => void
|
||||||
|
onSetRasterTileSize: (value: number) => void
|
||||||
|
onSetRasterTileOverlap: (value: number) => void
|
||||||
|
onSetRasterTileOutputName: (value: string) => void
|
||||||
|
onSetRasterReprojectCrs: (value: string) => void
|
||||||
|
onSetRasterReprojectResampling: (value: string) => void
|
||||||
|
onSetNdviNirBand: (value: number) => void
|
||||||
|
onSetNdviRedBand: (value: number) => void
|
||||||
|
onSetNdwiGreenBand: (value: number) => void
|
||||||
|
onSetNdwiNirBand: (value: number) => void
|
||||||
|
onSetNdbiSwirBand: (value: number) => void
|
||||||
|
onSetNdbiNirBand: (value: number) => void
|
||||||
|
onRunRasterInspect: () => void
|
||||||
|
onRunRasterPreview: () => void
|
||||||
|
onRunRasterStats: () => void
|
||||||
|
onRunRasterReproject: () => void
|
||||||
|
onRunRasterClip: () => void
|
||||||
|
onRunRasterTile: () => void
|
||||||
|
onRunRasterNdvi: () => void
|
||||||
|
onRunRasterNdwi: () => void
|
||||||
|
onRunRasterNdbi: () => void
|
||||||
|
onRunVectorClip: () => void
|
||||||
|
onRunVectorBuffer: () => void
|
||||||
|
onRunVectorIntersect: () => void
|
||||||
|
onPickDerivedDataset: (datasetId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
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)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DatasetDetailPanel({
|
||||||
|
areas,
|
||||||
|
availableVectorTargets,
|
||||||
|
selectedDatasetId,
|
||||||
|
selectedDataset,
|
||||||
|
selectedDatasetSummary,
|
||||||
|
selectedRasterMetadata,
|
||||||
|
selectedRasterStats,
|
||||||
|
rasterPreview,
|
||||||
|
rasterUnavailableMessage,
|
||||||
|
selectedClipAreaId,
|
||||||
|
selectedIntersectTargetId,
|
||||||
|
rasterTileSize,
|
||||||
|
rasterTileOverlap,
|
||||||
|
rasterTileOutputName,
|
||||||
|
rasterReprojectCrs,
|
||||||
|
rasterReprojectResampling,
|
||||||
|
ndviNirBand,
|
||||||
|
ndviRedBand,
|
||||||
|
ndwiGreenBand,
|
||||||
|
ndwiNirBand,
|
||||||
|
ndbiSwirBand,
|
||||||
|
ndbiNirBand,
|
||||||
|
isRasterTileInputValid,
|
||||||
|
jobs,
|
||||||
|
loadingDatasetDetails,
|
||||||
|
datasetDetailError,
|
||||||
|
isVectorDatasetType,
|
||||||
|
onSetSelectedClipAreaId,
|
||||||
|
onSetSelectedIntersectTargetId,
|
||||||
|
onSetRasterTileSize,
|
||||||
|
onSetRasterTileOverlap,
|
||||||
|
onSetRasterTileOutputName,
|
||||||
|
onSetRasterReprojectCrs,
|
||||||
|
onSetRasterReprojectResampling,
|
||||||
|
onSetNdviNirBand,
|
||||||
|
onSetNdviRedBand,
|
||||||
|
onSetNdwiGreenBand,
|
||||||
|
onSetNdwiNirBand,
|
||||||
|
onSetNdbiSwirBand,
|
||||||
|
onSetNdbiNirBand,
|
||||||
|
onRunRasterInspect,
|
||||||
|
onRunRasterPreview,
|
||||||
|
onRunRasterStats,
|
||||||
|
onRunRasterReproject,
|
||||||
|
onRunRasterClip,
|
||||||
|
onRunRasterTile,
|
||||||
|
onRunRasterNdvi,
|
||||||
|
onRunRasterNdwi,
|
||||||
|
onRunRasterNdbi,
|
||||||
|
onRunVectorClip,
|
||||||
|
onRunVectorBuffer,
|
||||||
|
onRunVectorIntersect,
|
||||||
|
onPickDerivedDataset,
|
||||||
|
}: DatasetDetailPanelProps) {
|
||||||
|
return (
|
||||||
|
<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' ? (
|
||||||
|
<RasterControls
|
||||||
|
areas={areas}
|
||||||
|
selectedDatasetId={selectedDatasetId}
|
||||||
|
selectedRasterMetadata={selectedRasterMetadata}
|
||||||
|
selectedRasterStats={selectedRasterStats}
|
||||||
|
rasterPreview={rasterPreview}
|
||||||
|
rasterUnavailableMessage={rasterUnavailableMessage}
|
||||||
|
selectedClipAreaId={selectedClipAreaId}
|
||||||
|
rasterTileSize={rasterTileSize}
|
||||||
|
rasterTileOverlap={rasterTileOverlap}
|
||||||
|
rasterTileOutputName={rasterTileOutputName}
|
||||||
|
rasterReprojectCrs={rasterReprojectCrs}
|
||||||
|
rasterReprojectResampling={rasterReprojectResampling}
|
||||||
|
ndviNirBand={ndviNirBand}
|
||||||
|
ndviRedBand={ndviRedBand}
|
||||||
|
ndwiGreenBand={ndwiGreenBand}
|
||||||
|
ndwiNirBand={ndwiNirBand}
|
||||||
|
ndbiSwirBand={ndbiSwirBand}
|
||||||
|
ndbiNirBand={ndbiNirBand}
|
||||||
|
isRasterTileInputValid={isRasterTileInputValid}
|
||||||
|
onSetSelectedClipAreaId={onSetSelectedClipAreaId}
|
||||||
|
onSetRasterTileSize={onSetRasterTileSize}
|
||||||
|
onSetRasterTileOverlap={onSetRasterTileOverlap}
|
||||||
|
onSetRasterTileOutputName={onSetRasterTileOutputName}
|
||||||
|
onSetRasterReprojectCrs={onSetRasterReprojectCrs}
|
||||||
|
onSetRasterReprojectResampling={onSetRasterReprojectResampling}
|
||||||
|
onSetNdviNirBand={onSetNdviNirBand}
|
||||||
|
onSetNdviRedBand={onSetNdviRedBand}
|
||||||
|
onSetNdwiGreenBand={onSetNdwiGreenBand}
|
||||||
|
onSetNdwiNirBand={onSetNdwiNirBand}
|
||||||
|
onSetNdbiSwirBand={onSetNdbiSwirBand}
|
||||||
|
onSetNdbiNirBand={onSetNdbiNirBand}
|
||||||
|
onRunRasterInspect={onRunRasterInspect}
|
||||||
|
onRunRasterPreview={onRunRasterPreview}
|
||||||
|
onRunRasterStats={onRunRasterStats}
|
||||||
|
onRunRasterReproject={onRunRasterReproject}
|
||||||
|
onRunRasterClip={onRunRasterClip}
|
||||||
|
onRunRasterTile={onRunRasterTile}
|
||||||
|
onRunRasterNdvi={onRunRasterNdvi}
|
||||||
|
onRunRasterNdwi={onRunRasterNdwi}
|
||||||
|
onRunRasterNdbi={onRunRasterNdbi}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isVectorDatasetType(selectedDataset.dataset_type) ? (
|
||||||
|
<VectorControls
|
||||||
|
areas={areas}
|
||||||
|
availableVectorTargets={availableVectorTargets}
|
||||||
|
selectedClipAreaId={selectedClipAreaId}
|
||||||
|
selectedIntersectTargetId={selectedIntersectTargetId}
|
||||||
|
onSetSelectedClipAreaId={onSetSelectedClipAreaId}
|
||||||
|
onSetSelectedIntersectTargetId={onSetSelectedIntersectTargetId}
|
||||||
|
onRunVectorClip={onRunVectorClip}
|
||||||
|
onRunVectorBuffer={onRunVectorBuffer}
|
||||||
|
onRunVectorIntersect={onRunVectorIntersect}
|
||||||
|
/>
|
||||||
|
) : 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={() => onPickDerivedDataset(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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import type { Dispatch, FormEvent, SetStateAction } from 'react'
|
||||||
|
import type { AreaRead, DatasetCreateResponse } from '../../types'
|
||||||
|
|
||||||
|
interface DatasetFormState {
|
||||||
|
datasetType: string
|
||||||
|
source: string
|
||||||
|
datasetRole: string
|
||||||
|
sourceName: string
|
||||||
|
referenceLayerName: string
|
||||||
|
sourceMetadataJson: string
|
||||||
|
provenanceMetadataJson: string
|
||||||
|
areaId: string
|
||||||
|
file: File | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DatasetPanelProps {
|
||||||
|
selectedProjectId: string | null
|
||||||
|
areas: AreaRead[]
|
||||||
|
datasets: DatasetCreateResponse[]
|
||||||
|
datasetForm: DatasetFormState
|
||||||
|
loadingDatasets: boolean
|
||||||
|
onDatasetFormChange: Dispatch<SetStateAction<DatasetFormState>>
|
||||||
|
onUploadDataset: (event: FormEvent) => void
|
||||||
|
onLoadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => void
|
||||||
|
onRefreshMetadata: (datasetId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
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)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DatasetPanel({
|
||||||
|
selectedProjectId,
|
||||||
|
areas,
|
||||||
|
datasets,
|
||||||
|
datasetForm,
|
||||||
|
loadingDatasets,
|
||||||
|
onDatasetFormChange,
|
||||||
|
onUploadDataset,
|
||||||
|
onLoadDatasetDetails,
|
||||||
|
onRefreshMetadata,
|
||||||
|
}: DatasetPanelProps) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h2>Datasets</h2>
|
||||||
|
<form onSubmit={onUploadDataset}>
|
||||||
|
<select
|
||||||
|
value={datasetForm.datasetType}
|
||||||
|
onChange={(event) => onDatasetFormChange((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) => onDatasetFormChange((previous) => ({ ...previous, source: event.target.value }))}
|
||||||
|
placeholder="user_upload"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={datasetForm.areaId}
|
||||||
|
onChange={(event) => onDatasetFormChange((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) => onDatasetFormChange((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={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)}>
|
||||||
|
Select / details
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onRefreshMetadata(dataset.id)}
|
||||||
|
disabled={dataset.dataset_type === 'raster'}
|
||||||
|
>
|
||||||
|
Refresh metadata
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
import type { AreaRead, RasterMetadataResponse, RasterPreviewResponse, RasterStatsResponse } from '../../types'
|
||||||
|
|
||||||
|
interface RasterControlsProps {
|
||||||
|
areas: AreaRead[]
|
||||||
|
selectedDatasetId: string | null
|
||||||
|
selectedRasterMetadata: RasterMetadataResponse | null
|
||||||
|
selectedRasterStats: RasterStatsResponse | null
|
||||||
|
rasterPreview: RasterPreviewResponse | null
|
||||||
|
rasterUnavailableMessage: string | null
|
||||||
|
selectedClipAreaId: string
|
||||||
|
rasterTileSize: number
|
||||||
|
rasterTileOverlap: number
|
||||||
|
rasterTileOutputName: string
|
||||||
|
rasterReprojectCrs: string
|
||||||
|
rasterReprojectResampling: string
|
||||||
|
ndviNirBand: number
|
||||||
|
ndviRedBand: number
|
||||||
|
ndwiGreenBand: number
|
||||||
|
ndwiNirBand: number
|
||||||
|
ndbiSwirBand: number
|
||||||
|
ndbiNirBand: number
|
||||||
|
isRasterTileInputValid: boolean
|
||||||
|
onSetSelectedClipAreaId: (value: string) => void
|
||||||
|
onSetRasterTileSize: (value: number) => void
|
||||||
|
onSetRasterTileOverlap: (value: number) => void
|
||||||
|
onSetRasterTileOutputName: (value: string) => void
|
||||||
|
onSetRasterReprojectCrs: (value: string) => void
|
||||||
|
onSetRasterReprojectResampling: (value: string) => void
|
||||||
|
onSetNdviNirBand: (value: number) => void
|
||||||
|
onSetNdviRedBand: (value: number) => void
|
||||||
|
onSetNdwiGreenBand: (value: number) => void
|
||||||
|
onSetNdwiNirBand: (value: number) => void
|
||||||
|
onSetNdbiSwirBand: (value: number) => void
|
||||||
|
onSetNdbiNirBand: (value: number) => void
|
||||||
|
onRunRasterInspect: () => void
|
||||||
|
onRunRasterPreview: () => void
|
||||||
|
onRunRasterStats: () => void
|
||||||
|
onRunRasterReproject: () => void
|
||||||
|
onRunRasterClip: () => void
|
||||||
|
onRunRasterTile: () => void
|
||||||
|
onRunRasterNdvi: () => void
|
||||||
|
onRunRasterNdwi: () => void
|
||||||
|
onRunRasterNdbi: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RasterControls({
|
||||||
|
areas,
|
||||||
|
selectedDatasetId,
|
||||||
|
selectedRasterMetadata,
|
||||||
|
selectedRasterStats,
|
||||||
|
rasterPreview,
|
||||||
|
rasterUnavailableMessage,
|
||||||
|
selectedClipAreaId,
|
||||||
|
rasterTileSize,
|
||||||
|
rasterTileOverlap,
|
||||||
|
rasterTileOutputName,
|
||||||
|
rasterReprojectCrs,
|
||||||
|
rasterReprojectResampling,
|
||||||
|
ndviNirBand,
|
||||||
|
ndviRedBand,
|
||||||
|
ndwiGreenBand,
|
||||||
|
ndwiNirBand,
|
||||||
|
ndbiSwirBand,
|
||||||
|
ndbiNirBand,
|
||||||
|
isRasterTileInputValid,
|
||||||
|
onSetSelectedClipAreaId,
|
||||||
|
onSetRasterTileSize,
|
||||||
|
onSetRasterTileOverlap,
|
||||||
|
onSetRasterTileOutputName,
|
||||||
|
onSetRasterReprojectCrs,
|
||||||
|
onSetRasterReprojectResampling,
|
||||||
|
onSetNdviNirBand,
|
||||||
|
onSetNdviRedBand,
|
||||||
|
onSetNdwiGreenBand,
|
||||||
|
onSetNdwiNirBand,
|
||||||
|
onSetNdbiSwirBand,
|
||||||
|
onSetNdbiNirBand,
|
||||||
|
onRunRasterInspect,
|
||||||
|
onRunRasterPreview,
|
||||||
|
onRunRasterStats,
|
||||||
|
onRunRasterReproject,
|
||||||
|
onRunRasterClip,
|
||||||
|
onRunRasterTile,
|
||||||
|
onRunRasterNdvi,
|
||||||
|
onRunRasterNdwi,
|
||||||
|
onRunRasterNdbi,
|
||||||
|
}: RasterControlsProps) {
|
||||||
|
return (
|
||||||
|
<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={onRunRasterInspect}>
|
||||||
|
Inspect raster metadata
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onRunRasterPreview} disabled={!selectedDatasetId}>
|
||||||
|
Generate preview
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onRunRasterStats}>
|
||||||
|
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) => onSetRasterReprojectCrs(event.target.value)}
|
||||||
|
placeholder="EPSG:31370"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Resampling
|
||||||
|
<select value={rasterReprojectResampling} onChange={(event) => onSetRasterReprojectResampling(event.target.value)}>
|
||||||
|
<option value="nearest">nearest</option>
|
||||||
|
<option value="bilinear">bilinear</option>
|
||||||
|
<option value="cubic">cubic</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button type="button" onClick={onRunRasterReproject}>
|
||||||
|
Reproject raster
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Clip area
|
||||||
|
<select value={selectedClipAreaId} onChange={(event) => onSetSelectedClipAreaId(event.target.value)}>
|
||||||
|
{areas.map((area) => (
|
||||||
|
<option key={area.id} value={area.id}>
|
||||||
|
{area.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button type="button" onClick={onRunRasterClip} 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) => onSetRasterTileSize(Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Overlap
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={rasterTileOverlap}
|
||||||
|
onChange={(event) => onSetRasterTileOverlap(Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Tile basename
|
||||||
|
<input
|
||||||
|
value={rasterTileOutputName}
|
||||||
|
onChange={(event) => onSetRasterTileOutputName(event.target.value)}
|
||||||
|
placeholder="optional"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button type="button" onClick={onRunRasterTile} 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) => onSetNdviNirBand(Number(event.target.value))} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Red band
|
||||||
|
<input type="number" min={1} value={ndviRedBand} onChange={(event) => onSetNdviRedBand(Number(event.target.value))} />
|
||||||
|
</label>
|
||||||
|
<button type="button" onClick={onRunRasterNdvi}>
|
||||||
|
Compute NDVI
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>NDWI</p>
|
||||||
|
<label>
|
||||||
|
Nir band
|
||||||
|
<input type="number" min={1} value={ndwiNirBand} onChange={(event) => onSetNdwiNirBand(Number(event.target.value))} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Green band
|
||||||
|
<input type="number" min={1} value={ndwiGreenBand} onChange={(event) => onSetNdwiGreenBand(Number(event.target.value))} />
|
||||||
|
</label>
|
||||||
|
<button type="button" onClick={onRunRasterNdwi}>
|
||||||
|
Compute NDWI
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>NDBI</p>
|
||||||
|
<label>
|
||||||
|
Swir band
|
||||||
|
<input type="number" min={1} value={ndbiSwirBand} onChange={(event) => onSetNdbiSwirBand(Number(event.target.value))} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Nir band
|
||||||
|
<input type="number" min={1} value={ndbiNirBand} onChange={(event) => onSetNdbiNirBand(Number(event.target.value))} />
|
||||||
|
</label>
|
||||||
|
<button type="button" onClick={onRunRasterNdbi}>
|
||||||
|
Compute NDBI
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { AreaRead, DatasetCreateResponse } from '../../types'
|
||||||
|
|
||||||
|
interface VectorControlsProps {
|
||||||
|
areas: AreaRead[]
|
||||||
|
availableVectorTargets: DatasetCreateResponse[]
|
||||||
|
selectedClipAreaId: string
|
||||||
|
selectedIntersectTargetId: string
|
||||||
|
onSetSelectedClipAreaId: (value: string) => void
|
||||||
|
onSetSelectedIntersectTargetId: (value: string) => void
|
||||||
|
onRunVectorClip: () => void
|
||||||
|
onRunVectorBuffer: () => void
|
||||||
|
onRunVectorIntersect: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VectorControls({
|
||||||
|
areas,
|
||||||
|
availableVectorTargets,
|
||||||
|
selectedClipAreaId,
|
||||||
|
selectedIntersectTargetId,
|
||||||
|
onSetSelectedClipAreaId,
|
||||||
|
onSetSelectedIntersectTargetId,
|
||||||
|
onRunVectorClip,
|
||||||
|
onRunVectorBuffer,
|
||||||
|
onRunVectorIntersect,
|
||||||
|
}: VectorControlsProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Vector operations</h3>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Clip area
|
||||||
|
<select value={selectedClipAreaId} onChange={(event) => onSetSelectedClipAreaId(event.target.value)}>
|
||||||
|
{areas.map((area) => (
|
||||||
|
<option key={area.id} value={area.id}>
|
||||||
|
{area.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button type="button" onClick={onRunVectorClip} disabled={areas.length === 0}>
|
||||||
|
Run clip
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onRunVectorBuffer}>
|
||||||
|
Run buffer (25m)
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Intersect target
|
||||||
|
<select value={selectedIntersectTargetId} onChange={(event) => onSetSelectedIntersectTargetId(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={onRunVectorIntersect}>
|
||||||
|
Run intersect
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user