Add V1 map workbench controls
This commit is contained in:
+69
-4
@@ -94,6 +94,9 @@ function App(): JSX.Element {
|
||||
const [selectedRasterMetadata, setSelectedRasterMetadata] = useState<RasterMetadataResponse | null>(null)
|
||||
const [selectedRasterStats, setSelectedRasterStats] = useState<RasterStatsResponse | null>(null)
|
||||
const [datasetContent, setDatasetContent] = useState<GeoJSON.FeatureCollection | null>(null)
|
||||
const [mapLayerVisible, setMapLayerVisible] = useState(true)
|
||||
const [mapLayerOpacity, setMapLayerOpacity] = useState(0.4)
|
||||
const [selectedMapFeature, setSelectedMapFeature] = useState<GeoJSON.Feature | null>(null)
|
||||
const [jobs, setJobs] = useState<JobRead[]>([])
|
||||
const [providerCapabilities, setProviderCapabilities] = useState<ProviderCapability[]>([])
|
||||
const [loadingCapabilities, setLoadingCapabilities] = useState(false)
|
||||
@@ -244,6 +247,22 @@ function App(): JSX.Element {
|
||||
() => changeDetectionResult?.geojson ?? segmentationGeoJson ?? detectionGeoJson ?? datasetContent,
|
||||
[changeDetectionResult, segmentationGeoJson, detectionGeoJson, datasetContent],
|
||||
)
|
||||
const mapLayerLabel = useMemo(() => {
|
||||
if (changeDetectionResult) {
|
||||
return 'Change detection result'
|
||||
}
|
||||
if (segmentationGeoJson) {
|
||||
return 'Segmentation result'
|
||||
}
|
||||
if (detectionGeoJson) {
|
||||
return 'Detection result'
|
||||
}
|
||||
if (datasetContent && selectedDataset) {
|
||||
return selectedDataset.name
|
||||
}
|
||||
return 'No active vector layer'
|
||||
}, [changeDetectionResult, datasetContent, detectionGeoJson, segmentationGeoJson, selectedDataset])
|
||||
const mapFeatureCount = mapFeatureCollection?.features.length ?? 0
|
||||
const isRasterTileInputValid = useMemo(
|
||||
() => rasterTileSize > 0 && rasterTileOverlap >= 0 && rasterTileOverlap < rasterTileSize,
|
||||
[rasterTileSize, rasterTileOverlap],
|
||||
@@ -624,6 +643,10 @@ function App(): JSX.Element {
|
||||
loadSegmentationModels().catch(() => null)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedMapFeature(null)
|
||||
}, [mapFeatureCollection])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedProjectId) {
|
||||
setAreas([])
|
||||
@@ -1300,8 +1323,8 @@ function App(): JSX.Element {
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<header>
|
||||
<h1>GeoIntel Kempen Sprint 9</h1>
|
||||
<p>Sprint 9: raster/vector workbench with detection and segmentation foundations.</p>
|
||||
<h1>GeoIntel Kempen V1 Workbench</h1>
|
||||
<p>Raster/vector workbench with QA/QC, exports, change detection and AI foundation layers.</p>
|
||||
</header>
|
||||
|
||||
{errorMessage ? <p className="error">{errorMessage}</p> : null}
|
||||
@@ -1826,8 +1849,50 @@ function App(): JSX.Element {
|
||||
|
||||
<section>
|
||||
<h2>Map workspace</h2>
|
||||
<p>{selectedDatasetId ? `Showing dataset ${selectedDatasetId}` : 'No vector dataset selected'}</p>
|
||||
<GeoMap data={mapFeatureCollection} />
|
||||
<p>{mapLayerLabel}</p>
|
||||
<div className="map-controls">
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
checked={mapLayerVisible}
|
||||
disabled={!mapFeatureCollection}
|
||||
type="checkbox"
|
||||
onChange={(event) => setMapLayerVisible(event.target.checked)}
|
||||
/>
|
||||
Layer visible
|
||||
</label>
|
||||
<label>
|
||||
Layer opacity
|
||||
<input
|
||||
disabled={!mapFeatureCollection}
|
||||
max="1"
|
||||
min="0.05"
|
||||
step="0.05"
|
||||
type="range"
|
||||
value={mapLayerOpacity}
|
||||
onChange={(event) => setMapLayerOpacity(Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<div className="map-status">
|
||||
{mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector layer loaded'}
|
||||
</div>
|
||||
</div>
|
||||
<GeoMap
|
||||
data={mapFeatureCollection}
|
||||
visible={mapLayerVisible}
|
||||
opacity={mapLayerOpacity}
|
||||
onFeatureSelect={setSelectedMapFeature}
|
||||
/>
|
||||
<div className="feature-inspector">
|
||||
<h3>Feature inspector</h3>
|
||||
{selectedMapFeature ? (
|
||||
<>
|
||||
<p>Geometry: {selectedMapFeature.geometry?.type ?? 'n/a'}</p>
|
||||
<pre className="job-result">{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}</pre>
|
||||
</>
|
||||
) : (
|
||||
<p className="muted">Click a visible map feature to inspect its properties.</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,9 @@ import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
|
||||
interface GeoMapProps {
|
||||
data: GeoJSON.FeatureCollection | null
|
||||
visible?: boolean
|
||||
opacity?: number
|
||||
onFeatureSelect?: (feature: GeoJSON.Feature | null) => void
|
||||
}
|
||||
|
||||
function collectCoordinates(featureCollection: GeoJSON.FeatureCollection): maplibregl.LngLatBoundsLike | null {
|
||||
@@ -40,9 +43,14 @@ function collectCoordinates(featureCollection: GeoJSON.FeatureCollection): mapli
|
||||
]
|
||||
}
|
||||
|
||||
function GeoMap({ data }: GeoMapProps): JSX.Element {
|
||||
function GeoMap({ data, visible = true, opacity = 0.4, onFeatureSelect }: GeoMapProps): JSX.Element {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const mapRef = useRef<maplibregl.Map | null>(null)
|
||||
const onFeatureSelectRef = useRef<GeoMapProps['onFeatureSelect']>(onFeatureSelect)
|
||||
|
||||
useEffect(() => {
|
||||
onFeatureSelectRef.current = onFeatureSelect
|
||||
}, [onFeatureSelect])
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || mapRef.current) {
|
||||
@@ -56,6 +64,21 @@ function GeoMap({ data }: GeoMapProps): JSX.Element {
|
||||
zoom: 9,
|
||||
})
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right')
|
||||
map.on('click', (event) => {
|
||||
if (!map.getLayer('dataset-fill') || !map.getLayer('dataset-line')) {
|
||||
onFeatureSelectRef.current?.(null)
|
||||
return
|
||||
}
|
||||
const features = map.queryRenderedFeatures(event.point, {
|
||||
layers: ['dataset-fill', 'dataset-line'],
|
||||
})
|
||||
if (features.length === 0) {
|
||||
onFeatureSelectRef.current?.(null)
|
||||
return
|
||||
}
|
||||
const feature = features[0] as unknown as GeoJSON.Feature
|
||||
onFeatureSelectRef.current?.(feature)
|
||||
})
|
||||
mapRef.current = map
|
||||
|
||||
return () => {
|
||||
@@ -136,6 +159,22 @@ function GeoMap({ data }: GeoMapProps): JSX.Element {
|
||||
}
|
||||
}, [data])
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
const visibility = visible ? 'visible' : 'none'
|
||||
if (map.getLayer('dataset-fill')) {
|
||||
map.setLayoutProperty('dataset-fill', 'visibility', visibility)
|
||||
map.setPaintProperty('dataset-fill', 'fill-opacity', opacity)
|
||||
}
|
||||
if (map.getLayer('dataset-line')) {
|
||||
map.setLayoutProperty('dataset-line', 'visibility', visibility)
|
||||
map.setPaintProperty('dataset-line', 'line-opacity', visible ? 1 : 0)
|
||||
}
|
||||
}, [visible, opacity, data])
|
||||
|
||||
return <div className="map-container" ref={containerRef} />
|
||||
}
|
||||
|
||||
|
||||
@@ -152,3 +152,22 @@ ul {
|
||||
border: 1px solid #94a3b8;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.map-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 0.75rem;
|
||||
align-items: end;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.map-status {
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.feature-inspector {
|
||||
margin-top: 0.75rem;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user