import { useEffect, useMemo, useState } from 'react' import GeoMap from '../GeoMap' import type { AreaRead, DatasetCreateResponse, MapViewportState, QaComparisonResult, VectorSelectionBBox, VectorSelectionResponse } from '../../types' import { featureCollectionBounds } from '../../lib/geojsonBounds' const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson' const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson' function collectGeometryPoints(geometry: GeoJSON.Geometry | null | undefined): Array<[number, number]> { const points: Array<[number, number]> = [] const walk = (coords: unknown) => { if (!Array.isArray(coords)) { return } if (coords.length >= 2 && typeof coords[0] === 'number' && typeof coords[1] === 'number') { points.push([coords[0], coords[1]]) return } for (const item of coords) { walk(item) } } if ('coordinates' in (geometry ?? {})) { walk((geometry as GeoJSON.Geometry & { coordinates: unknown }).coordinates) } return points } function formatCoordinate(value: number): string { return Number.isFinite(value) ? value.toFixed(6) : 'n/a' } function getFeatureGeometrySummary(feature: GeoJSON.Feature | null) { const points = collectGeometryPoints(feature?.geometry) if (!feature?.geometry || points.length === 0) { return { bboxLabel: 'n/a', coordinateCount: 0, geometryType: feature?.geometry?.type ?? 'none', } } const xs = points.map((point) => point[0]) const ys = points.map((point) => point[1]) const bboxLabel = `${formatCoordinate(Math.min(...xs))}, ${formatCoordinate(Math.min(...ys))} -> ${formatCoordinate( Math.max(...xs), )}, ${formatCoordinate(Math.max(...ys))}` return { bboxLabel, coordinateCount: points.length, geometryType: feature.geometry.type, } } function getFeatureCollectionBBox(collection: GeoJSON.FeatureCollection | null): VectorSelectionBBox | null { const bounds = featureCollectionBounds(collection) if (!bounds) { return null } return { min_x: bounds.minX, min_y: bounds.minY, max_x: bounds.maxX, max_y: bounds.maxY, crs: 'EPSG:4326', } } function getFeatureBBox(feature: GeoJSON.Feature | null): VectorSelectionBBox | null { const points = collectGeometryPoints(feature?.geometry) if (points.length === 0) { return null } const xs = points.map((point) => point[0]) const ys = points.map((point) => point[1]) return { min_x: Math.min(...xs), min_y: Math.min(...ys), max_x: Math.max(...xs), max_y: Math.max(...ys), crs: 'EPSG:4326', } } function normalizeBboxFromCorners(first: [number, number], second: [number, number]): VectorSelectionBBox { return { min_x: Math.min(first[0], second[0]), min_y: Math.min(first[1], second[1]), max_x: Math.max(first[0], second[0]), max_y: Math.max(first[1], second[1]), crs: 'EPSG:4326', } } function formatBboxLabel(bbox: VectorSelectionBBox | null): string { if (!bbox) { return 'n/a' } return `${formatCoordinate(bbox.min_x)}, ${formatCoordinate(bbox.min_y)} -> ${formatCoordinate(bbox.max_x)}, ${formatCoordinate(bbox.max_y)}` } function bboxToInputState(bbox: VectorSelectionBBox | null) { return { min_x: bbox ? String(bbox.min_x) : '', min_y: bbox ? String(bbox.min_y) : '', max_x: bbox ? String(bbox.max_x) : '', max_y: bbox ? String(bbox.max_y) : '', } } function parseBboxInput(input: ReturnType): VectorSelectionBBox | null { const min_x = Number(input.min_x) const min_y = Number(input.min_y) const max_x = Number(input.max_x) const max_y = Number(input.max_y) if (![min_x, min_y, max_x, max_y].every(Number.isFinite) || min_x >= max_x || min_y >= max_y) { return null } return { min_x, min_y, max_x, max_y, crs: 'EPSG:4326' } } function selectedFeatureCollection(feature: GeoJSON.Feature): GeoJSON.FeatureCollection { return { type: 'FeatureCollection', features: [feature], } } function safeFileStem(value: unknown): string { const stem = String(value ?? 'selected-feature') .trim() .toLowerCase() .replace(/[^a-z0-9._-]+/g, '-') .replace(/^-+|-+$/g, '') return stem || 'selected-feature' } function fallbackCopyText(text: string): void { const textarea = document.createElement('textarea') textarea.value = text textarea.setAttribute('readonly', 'true') textarea.style.position = 'fixed' textarea.style.left = '-9999px' document.body.appendChild(textarea) textarea.select() document.execCommand('copy') document.body.removeChild(textarea) } function copyText(text: string): void { if (navigator.clipboard?.writeText) { void navigator.clipboard.writeText(text).catch(() => fallbackCopyText(text)) return } fallbackCopyText(text) } function downloadJsonFile(filename: string, payload: unknown): void { const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/geo+json' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = filename document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) } interface MapWorkspaceProps { areas: AreaRead[] selectedMapAreaId: string areaFeatureCollection: GeoJSON.FeatureCollection | null mapFeatureCollection: GeoJSON.FeatureCollection | null qualityEvidenceGeoJson?: GeoJSON.FeatureCollection | null qualityEvidenceFeatureCount?: number qualityEvidenceLoading?: boolean qualityEvidenceError?: string | null qualityEvidenceWarnings?: string[] mapLayerLabel: string mapLayerSourceLabel: string mapLayerProvenance: string mapLayerVisible: boolean mapLayerOpacity: number areaLayerVisible: boolean areaLayerOpacity: number mapFeatureCount: number areaFeatureCount: number viewportVectorEnabled: boolean viewportVectorStatus: string | null viewportVectorTone: 'ready' | 'pending' | 'warning' | 'error' fitMapDataOnChange: boolean selectedMapFeature: GeoJSON.Feature | null selectedFeature?: GeoJSON.Feature | null mapSelectionBbox: VectorSelectionBBox | null mapSelectionResult: VectorSelectionResponse | null mapSelectionLoading: boolean mapSelectionError: string | null selectionExporting: boolean selectionExportError: string | null latestSelectionExportPath: string | null selectionDatasetSaving: boolean selectionDatasetError: string | null latestSelectionDataset: DatasetCreateResponse | null latestSelectionDatasetName: string | null mapQaReferenceDatasets: DatasetCreateResponse[] selectedMapQaReferenceDatasetId: string mapSelectionQaRunning: boolean mapSelectionQaError: string | null mapSelectionQaResult: QaComparisonResult | null latestMapSelectionQualityCheckId: string | null availableMapDatasets: DatasetCreateResponse[] selectedMapDatasetId: string onSelectMapArea: (areaId: string) => void onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void onSetAreaLayerVisible: (visible: boolean) => void onSetAreaLayerOpacity: (opacity: number) => void onSetMapLayerVisible: (visible: boolean) => void onSetMapLayerOpacity: (opacity: number) => void onSelectMapFeature: (feature: GeoJSON.Feature | null) => void onMapViewportChange: (viewport: MapViewportState) => void onSetMapSelectionBbox: (bbox: VectorSelectionBBox | null) => void onRunMapSelectionExtract: (bbox: VectorSelectionBBox) => Promise onClearMapSelectionExtract: () => void onExportMapSelection: (bbox: VectorSelectionBBox) => Promise onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox) => Promise onSelectMapQaReferenceDataset: (datasetId: string) => void onRunMapSelectionQa: (candidateDataset?: DatasetCreateResponse | null) => Promise onOpenMapSelectionQualityEvidence: () => void onClearQualityEvidence?: () => void } export function MapWorkspace({ areas, selectedMapAreaId, areaFeatureCollection, mapFeatureCollection, qualityEvidenceGeoJson = null, qualityEvidenceFeatureCount = 0, qualityEvidenceLoading = false, qualityEvidenceError = null, qualityEvidenceWarnings = [], mapLayerLabel, mapLayerSourceLabel, mapLayerProvenance, mapLayerVisible, mapLayerOpacity, areaLayerVisible, areaLayerOpacity, mapFeatureCount, areaFeatureCount, viewportVectorEnabled, viewportVectorStatus, viewportVectorTone, fitMapDataOnChange, selectedMapFeature, selectedFeature = selectedMapFeature, mapSelectionBbox, mapSelectionResult, mapSelectionLoading, mapSelectionError, selectionExporting, selectionExportError, latestSelectionExportPath, selectionDatasetSaving, selectionDatasetError, latestSelectionDataset, latestSelectionDatasetName, mapQaReferenceDatasets, selectedMapQaReferenceDatasetId, mapSelectionQaRunning, mapSelectionQaError, mapSelectionQaResult, latestMapSelectionQualityCheckId, availableMapDatasets, selectedMapDatasetId, onSelectMapArea, onOpenDatasetInMap, onSetAreaLayerVisible, onSetAreaLayerOpacity, onSetMapLayerVisible, onSetMapLayerOpacity, onSelectMapFeature, onMapViewportChange, onSetMapSelectionBbox, onRunMapSelectionExtract, onClearMapSelectionExtract, onExportMapSelection, onDeriveMapSelectionDataset, onSelectMapQaReferenceDataset, onRunMapSelectionQa, onOpenMapSelectionQualityEvidence, onClearQualityEvidence, }: MapWorkspaceProps): JSX.Element { const [bboxSelectionMode, setBboxSelectionMode] = useState(false) const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null) const [bboxInput, setBboxInput] = useState(bboxToInputState(mapSelectionBbox)) const [fullWorkflowRunning, setFullWorkflowRunning] = useState(false) const [fullWorkflowStatus, setFullWorkflowStatus] = useState('Ready to run persisted GIS workflow.') const [fullWorkflowError, setFullWorkflowError] = useState(null) const [fullWorkflowMode, setFullWorkflowMode] = useState<'new' | 'reuse'>('new') const selectedMapArea = areas.find((area) => area.id === selectedMapAreaId) const featureProperties = selectedMapFeature?.properties ?? null const featureSummaryEntries = featureProperties ? Object.entries(featureProperties) .filter(([, value]) => value !== null && value !== undefined && typeof value !== 'object') .slice(0, 6) : [] const featureExtractionEntries = featureProperties ? Object.entries(featureProperties).slice(0, 48) : [] const featureGeometrySummary = getFeatureGeometrySummary(selectedMapFeature) const selectedFeatureGeoJson = selectedMapFeature ? selectedFeatureCollection(selectedMapFeature) : null const selectedFeatureBbox = useMemo(() => getFeatureBBox(selectedMapFeature), [selectedMapFeature]) const activeLayerBbox = useMemo(() => getFeatureCollectionBBox(mapFeatureCollection), [mapFeatureCollection]) const selectedAreaBbox = useMemo(() => getFeatureCollectionBBox(areaFeatureCollection), [areaFeatureCollection]) const currentSelectionBbox = parseBboxInput(bboxInput) const areaSelectionFeatures = mapSelectionResult?.geojson.features ?? [] const areaSelectionPreviewFeatures = areaSelectionFeatures.slice(0, 12) const selectedFeatureStem = safeFileStem( featureProperties?.['name'] ?? featureProperties?.['id'] ?? featureProperties?.['source_feature_id'] ?? 'selected-feature', ) const selectedFeatureFilename = selectedFeatureStem === 'selected-feature' ? DEFAULT_SELECTED_FEATURE_FILENAME : `${selectedFeatureStem}.geojson` const selectedMapDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null const usesDefaultOsmBasemap = !import.meta.env.VITE_MAP_STYLE_URL useEffect(() => { setBboxInput(bboxToInputState(mapSelectionBbox)) }, [mapSelectionBbox]) const downloadSelectedMapFeature = () => { if (!selectedFeatureGeoJson) { return } downloadJsonFile(selectedFeatureFilename, selectedFeatureGeoJson) } const copySelectedMapFeatureProperties = () => { copyText(JSON.stringify(featureProperties ?? {}, null, 2)) } const setSelectionBbox = (bbox: VectorSelectionBBox | null) => { onSetMapSelectionBbox(bbox) setBboxInput(bboxToInputState(bbox)) } const startBboxSelection = () => { setFirstSelectionCorner(null) setBboxSelectionMode(true) } const handleMapCoordinateSelect = (coordinate: [number, number]) => { if (!firstSelectionCorner) { setFirstSelectionCorner(coordinate) return } const bbox = normalizeBboxFromCorners(firstSelectionCorner, coordinate) setSelectionBbox(bbox) setFirstSelectionCorner(null) setBboxSelectionMode(false) } const runAreaExtract = () => { const bbox = parseBboxInput(bboxInput) if (!bbox) { return } onRunMapSelectionExtract(bbox) } const clearAreaSelection = () => { setBboxSelectionMode(false) setFirstSelectionCorner(null) setBboxInput(bboxToInputState(null)) onClearMapSelectionExtract() } const downloadAreaSelection = () => { if (!mapSelectionResult) { return } downloadJsonFile(DEFAULT_AREA_SELECTION_FILENAME, mapSelectionResult.geojson) } const copyAreaSelection = () => { copyText(JSON.stringify(mapSelectionResult?.geojson ?? { type: 'FeatureCollection', features: [] }, null, 2)) } const saveAreaSelectionExport = () => { const bbox = parseBboxInput(bboxInput) if (!bbox) { return } onExportMapSelection(bbox) } const saveAreaSelectionDataset = () => { const bbox = parseBboxInput(bboxInput) if (!bbox) { return } onDeriveMapSelectionDataset(bbox) } const openSelectedDatabaseLayer = (datasetId: string) => { const dataset = availableMapDatasets.find((item) => item.id === datasetId) if (dataset) { onOpenDatasetInMap(dataset) } } const runQuickAoiExtract = () => { const bbox = selectedAreaBbox ?? activeLayerBbox if (!bbox) { return } setSelectionBbox(bbox) onRunMapSelectionExtract(bbox) } const runFullGisWorkflow = async () => { const bbox = currentSelectionBbox ?? selectedAreaBbox ?? activeLayerBbox if (fullWorkflowMode === 'reuse') { if (!latestSelectionDataset) { setFullWorkflowError('Save a map selection dataset before reusing the latest result.') return } if (!selectedMapQaReferenceDatasetId) { setFullWorkflowError('Select a reference dataset before reusing the latest result for QA/QC.') return } setFullWorkflowRunning(true) setFullWorkflowError(null) try { setFullWorkflowStatus('Reusing latest saved dataset for QA/QC...') const qaResult = await onRunMapSelectionQa(latestSelectionDataset) setFullWorkflowStatus(qaResult ? 'Reused latest saved dataset and completed QA/QC.' : 'Latest saved dataset reused, but QA/QC did not complete.') } catch (error) { setFullWorkflowError(error instanceof Error ? error.message : 'Full GIS workflow failed.') setFullWorkflowStatus('Workflow stopped.') } finally { setFullWorkflowRunning(false) } return } if (!selectedMapDataset || !bbox) { setFullWorkflowError('Select a database layer and AOI/layer extent before running the full workflow.') return } setFullWorkflowRunning(true) setFullWorkflowError(null) try { setFullWorkflowStatus('1/4 Querying persisted vector_features...') setSelectionBbox(bbox) const selection = await onRunMapSelectionExtract(bbox) if (!selection) { setFullWorkflowError('Persisted vector query did not complete.') setFullWorkflowStatus('Stopped at query.') return } setFullWorkflowStatus('2/4 Saving derived result dataset...') const derived = await onDeriveMapSelectionDataset(bbox) if (!derived) { setFullWorkflowError('Derived result dataset was not created.') setFullWorkflowStatus('Stopped at dataset save.') return } setFullWorkflowStatus('3/4 Saving GeoJSON export artifact...') await onExportMapSelection(bbox) if (selectedMapQaReferenceDatasetId) { setFullWorkflowStatus('4/4 Running QA/QC against selected reference...') const qaResult = await onRunMapSelectionQa(derived) setFullWorkflowStatus(qaResult ? 'Full GIS workflow complete with QA/QC result.' : 'Dataset/export complete; QA/QC did not complete.') } else { setFullWorkflowStatus('Dataset/export complete. Select a reference dataset to add QA/QC.') } } catch (error) { setFullWorkflowError(error instanceof Error ? error.message : 'Full GIS workflow failed.') setFullWorkflowStatus('Workflow stopped.') } finally { setFullWorkflowRunning(false) } } return (

Spatial review

Map workspace

{mapFeatureCollection ? `${mapFeatureCount} features` : viewportVectorEnabled ? 'zoom to load' : 'no layer'}
{usesDefaultOsmBasemap ? (
Local/demo basemap Public OpenStreetMap tiles are active. Configure VITE_MAP_STYLE_URL for production or heavier use.
) : null}
onSetAreaLayerOpacity(Number(event.target.value))} data-testid="map-area-opacity" />
onSetMapLayerOpacity(Number(event.target.value))} data-testid="map-layer-opacity" />
{mapLayerLabel} {selectedMapDataset ? `DB layer: ${selectedMapDataset.name}` : 'No database layer selected'} {areaFeatureCollection ? `${areaFeatureCount} AOI loaded` : 'No AOI loaded'} {mapFeatureCollection ? `${mapFeatureCount} features loaded` : viewportVectorEnabled ? 'Database layer selected; visible features load by viewport' : 'No vector/result layer loaded'} {viewportVectorEnabled && viewportVectorStatus ? ( {viewportVectorStatus} ) : null}
Layer details {mapFeatureCollection ? `${mapFeatureCount} rendered features` : viewportVectorEnabled ? 'Viewport layer selected' : 'No active layer'}
Area of interest {selectedMapArea?.name ?? 'No area selected'} {areaFeatureCollection ? `${areaFeatureCount} AOI features loaded` : 'AOI overlay disabled'}
Active layer {mapLayerLabel} {mapLayerSourceLabel}
Feature state {mapFeatureCollection ? `${mapFeatureCount} rendered features` : viewportVectorEnabled ? 'Awaiting viewport detail' : 'No layer rendered'} {mapLayerProvenance}
QA/QC evidence {qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} evidence features` : 'No evidence overlay'} {qualityEvidenceLoading ? 'Loading persisted evidence' : 'Matches, false positives and false negatives'}
Layer source {mapLayerSourceLabel}
Provenance {mapLayerProvenance}
Draw state {mapFeatureCollection ? `${mapFeatureCount} rendered features` : viewportVectorEnabled ? 'Viewport delivery active' : 'No active vector or result layer'}
QA evidence overlay {qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} rendered` : 'off'}
{qualityEvidenceGeoJson || qualityEvidenceError || qualityEvidenceWarnings.length > 0 ? (
QA/QC evidence overlay {qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} persisted features` : 'Not loaded'} {qualityEvidenceError ?

{qualityEvidenceError}

: null} {qualityEvidenceWarnings.length > 0 ? (

{qualityEvidenceWarnings.length} evidence id{qualityEvidenceWarnings.length === 1 ? '' : 's'} could not be resolved.

) : null}
Match candidate Match reference False positive False negative
{onClearQualityEvidence ? ( ) : null}
) : null} {!mapFeatureCollection && !viewportVectorEnabled ? (
No active vector or result layer

Open a dataset, detection run, segmentation run or change result to draw it here.

{availableMapDatasets.length > 0 ? ( <>

Open a ready vector dataset

{availableMapDatasets.map((dataset) => ( ))}
) : (

No ready vector datasets available yet. Upload or seed a vector dataset first.

)}
) : null}

Operational GIS run

Database selection test

{mapSelectionResult ? `${mapSelectionResult.feature_count} hits` : 'ready'}

Select a persisted vector layer, then query stored vector_features from PostGIS with the AOI or layer extent.

Database layer {selectedMapDataset?.name ?? 'Select a layer'}
AOI bbox {selectedAreaBbox ? 'available' : 'missing'}
Layer bbox {activeLayerBbox ? 'available' : 'missing'}
Result {mapSelectionResult ? `${mapSelectionResult.feature_count} features` : 'not run'}
{mapSelectionError ?

{mapSelectionError}

: null}
1 Layer {selectedMapDataset ? selectedMapDataset.name : 'Select database layer'}
2 Extent {currentSelectionBbox ? 'AOI/layer bbox ready' : 'Use AOI or layer extent'}
3 Query {mapSelectionResult ? `${mapSelectionResult.feature_count} persisted features` : 'Run PostGIS selection'}
4 Dataset {latestSelectionDatasetName ?? 'Save query result'}
5 QA/QC {mapSelectionQaResult ? `F1 ${mapSelectionQaResult.f1_score ?? 'n/a'}` : 'Compare with reference'}
6 Export {latestSelectionExportPath ? 'GeoJSON artifact ready' : 'Save handoff artifact'}
Query, save, QA and export {fullWorkflowStatus}
{selectionDatasetError ?

{selectionDatasetError}

: null} {selectionExportError ?

{selectionExportError}

: null} {mapSelectionQaError ?

{mapSelectionQaError}

: null} {fullWorkflowError ?

{fullWorkflowError}

: null}
Advanced selection and inspection BBox, feature extract and raw properties

Persisted vector query

Area selection

{mapSelectionResult ? `${mapSelectionResult.feature_count} selected` : bboxSelectionMode ? 'selecting' : 'ready'}
{bboxSelectionMode ? (firstSelectionCorner ? 'Click the opposite corner' : 'Click the first corner on the map') : 'BBox EPSG:4326'} {formatBboxLabel(currentSelectionBbox)}
{mapSelectionError ?

{mapSelectionError}

: null} {mapSelectionResult ? (
Features {mapSelectionResult.feature_count}
Limit {mapSelectionResult.limit}
Truncated {mapSelectionResult.truncated ? 'yes' : 'no'}
Source vector_features
{selectionExportError ?

{selectionExportError}

: null} {latestSelectionExportPath ? (

Saved selection artifact: {latestSelectionExportPath}

) : null} {selectionDatasetError ?

{selectionDatasetError}

: null} {latestSelectionDatasetName ? (

Saved derived dataset: {latestSelectionDatasetName}

) : null} {latestSelectionDatasetName ? (
{mapSelectionQaError ?

{mapSelectionQaError}

: null} {mapSelectionQaResult ? (

QA evidence

Saved selection comparison

Precision {mapSelectionQaResult.precision ?? 'n/a'}
Recall {mapSelectionQaResult.recall ?? 'n/a'}
F1 {mapSelectionQaResult.f1_score ?? 'n/a'}
Mean IoU {mapSelectionQaResult.mean_iou ?? 'n/a'}
Matches {mapSelectionQaResult.matches}
False positives {mapSelectionQaResult.false_positives}
False negatives {mapSelectionQaResult.false_negatives}
Quality check id {latestMapSelectionQualityCheckId ?? 'not persisted'}
{mapSelectionQaResult.warnings.length > 0 ? (
Map selection QA warnings
    {mapSelectionQaResult.warnings.map((warning) => (
  • {warning}
  • ))}
) : null}
) : null}
) : null} {areaSelectionPreviewFeatures.length > 0 ? (
{areaSelectionPreviewFeatures.map((feature, index) => ( ))}
Feature Class Source id
{String(feature.properties?.['name'] ?? feature.properties?.['vector_feature_id'] ?? feature.id ?? index + 1)} {String(feature.properties?.['feature_class'] ?? 'n/a')} {String(feature.properties?.['source_feature_id'] ?? 'n/a')}
) : (

No persisted vector features intersect this selection.

)}
) : null}

Selected feature

{'Selection & extract'}

{selectedMapFeature ? 'ready' : 'waiting'}
{selectedMapFeature ? ( <>
Geometry {featureGeometrySummary.geometryType}
Coordinates {featureGeometrySummary.coordinateCount}
Properties {featureExtractionEntries.length}
BBox EPSG:4326 {featureGeometrySummary.bboxLabel}
{featureExtractionEntries.length > 0 ? (
{featureExtractionEntries.map(([key, value]) => ( ))}
Property Value
{key} {typeof value === 'object' ? JSON.stringify(value) : String(value)}
) : (

The selected feature has geometry but no persisted properties.

)} ) : (
No feature selected

Click a visible vector, detection, segmentation or change feature on the map to extract its attributes and GeoJSON.

)}

Feature inspector

{selectedMapFeature?.geometry?.type ?? 'none'}
{selectedMapFeature ? ( <> {featureSummaryEntries.length > 0 ? (
{featureSummaryEntries.map(([key, value]) => (
{key} {String(value)}
))}
) : null}
{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}
) : (

Click a visible map feature to inspect its properties.

)}
) }