Improve map overlay ergonomics
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-18 23:31:00 +02:00
parent 6fa41e11e4
commit 705b53fbfa
8 changed files with 218 additions and 0 deletions
+42
View File
@@ -389,6 +389,46 @@ function App(): JSX.Element {
const selectedArea = areas.find((area) => area.id === selectedMapAreaId) ?? null
const activeWorkspaceItem = workspaceNavItems.find((item) => item.key === activeWorkspace) ?? workspaceNavItems[0]
const mapLayerSourceLabel = useMemo(() => {
if (changeDetectionResult?.geojson) {
return 'Change detection'
}
if (segmentationGeoJson) {
return 'Segmentation run'
}
if (detectionGeoJson) {
return 'Detection run'
}
if (datasetContent && selectedDataset) {
return `${selectedDataset.dataset_type} dataset`
}
return 'No active vector or result layer'
}, [changeDetectionResult?.geojson, datasetContent, detectionGeoJson, segmentationGeoJson, selectedDataset])
const mapLayerProvenance = useMemo(() => {
if (changeDetectionResult?.geojson) {
return `source ${changeSourceDatasetId || 'n/a'} -> target ${changeTargetDatasetId || 'n/a'}`
}
if (segmentationGeoJson) {
return selectedSegmentationRunId ? `analysis run ${selectedSegmentationRunId}` : 'segmentation results loaded'
}
if (detectionGeoJson) {
return selectedDetectionRunId ? `analysis run ${selectedDetectionRunId}` : 'detection results loaded'
}
if (datasetContent && selectedDataset) {
return `${selectedDataset.dataset_role ?? 'source'} / ${selectedDataset.source_name ?? selectedDataset.source}`
}
return 'Open a dataset, detection run, segmentation run or change result to draw it here.'
}, [
changeDetectionResult?.geojson,
changeSourceDatasetId,
changeTargetDatasetId,
datasetContent,
detectionGeoJson,
segmentationGeoJson,
selectedDataset,
selectedDetectionRunId,
selectedSegmentationRunId,
])
const openDatasetInMap = (dataset: DatasetCreateResponse) => {
if (selectedProjectId) {
loadDatasetDetails(selectedProjectId, dataset)
@@ -565,6 +605,8 @@ function App(): JSX.Element {
areaFeatureCollection={areaFeatureCollection}
mapFeatureCollection={mapFeatureCollection}
mapLayerLabel={mapLayerLabel}
mapLayerSourceLabel={mapLayerSourceLabel}
mapLayerProvenance={mapLayerProvenance}
mapLayerVisible={mapLayerVisible}
mapLayerOpacity={mapLayerOpacity}
areaLayerVisible={areaLayerVisible}
@@ -7,6 +7,8 @@ interface MapWorkspaceProps {
areaFeatureCollection: GeoJSON.FeatureCollection | null
mapFeatureCollection: GeoJSON.FeatureCollection | null
mapLayerLabel: string
mapLayerSourceLabel: string
mapLayerProvenance: string
mapLayerVisible: boolean
mapLayerOpacity: number
areaLayerVisible: boolean
@@ -28,6 +30,8 @@ export function MapWorkspace({
areaFeatureCollection,
mapFeatureCollection,
mapLayerLabel,
mapLayerSourceLabel,
mapLayerProvenance,
mapLayerVisible,
mapLayerOpacity,
areaLayerVisible,
@@ -42,6 +46,13 @@ export function MapWorkspace({
onSetMapLayerOpacity,
onSelectMapFeature,
}: MapWorkspaceProps): JSX.Element {
const featureProperties = selectedMapFeature?.properties ?? null
const featureSummaryEntries = featureProperties
? Object.entries(featureProperties)
.filter(([, value]) => value !== null && value !== undefined && typeof value !== 'object')
.slice(0, 6)
: []
return (
<section data-testid="map-workspace">
<div className="panel-title-row">
@@ -121,6 +132,26 @@ export function MapWorkspace({
<span>{mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector/result layer loaded'}</span>
</div>
</div>
<div className="layer-provenance-rail" aria-label="Active map layer provenance">
<div>
<span>Layer source</span>
<strong>{mapLayerSourceLabel}</strong>
</div>
<div>
<span>Provenance</span>
<strong>{mapLayerProvenance}</strong>
</div>
<div>
<span>Draw state</span>
<strong>{mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No active vector or result layer'}</strong>
</div>
</div>
{!mapFeatureCollection ? (
<div className="empty-state map-empty-state">
<strong>No active vector or result layer</strong>
<p>Open a dataset, detection run, segmentation run or change result to draw it here.</p>
</div>
) : null}
<GeoMap
data={mapFeatureCollection}
areaData={areaFeatureCollection}
@@ -137,6 +168,16 @@ export function MapWorkspace({
</div>
{selectedMapFeature ? (
<>
{featureSummaryEntries.length > 0 ? (
<div className="feature-summary-grid" aria-label="Selected feature summary">
{featureSummaryEntries.map(([key, value]) => (
<div className="feature-property-chip" key={key}>
<span>{key}</span>
<strong>{String(value)}</strong>
</div>
))}
</div>
) : null}
<pre className="job-result">{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}</pre>
</>
) : (
+65
View File
@@ -797,6 +797,14 @@ section > div:not(.map-controls):not(.feature-inspector):not(.quick-action-grid)
gap: 0.55rem;
}
.panel-title-row {
display: flex;
}
section > .panel-title-row {
display: flex !important;
}
section button + button {
margin-top: 0.45rem;
}
@@ -1178,6 +1186,10 @@ button.entity-card {
line-height: 1.4;
}
.map-empty-state {
margin-bottom: 0.85rem;
}
.export-action-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1287,6 +1299,58 @@ button.entity-card {
font-size: 0.84rem;
}
.layer-provenance-rail {
display: grid;
grid-template-columns: 0.85fr minmax(0, 1.5fr) 0.85fr;
gap: 0.65rem;
margin-bottom: 0.85rem;
}
.layer-provenance-rail > div,
.feature-property-chip {
min-width: 0;
border: 1px solid var(--line);
border-radius: 7px;
padding: 0.58rem 0.68rem;
background: #ffffff;
}
.layer-provenance-rail span,
.feature-property-chip span {
display: block;
color: var(--muted);
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.layer-provenance-rail strong,
.feature-property-chip strong {
display: block;
margin-top: 0.2rem;
overflow: hidden;
font-size: 0.84rem;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.layer-provenance-rail strong {
display: -webkit-box;
min-height: 2.1rem;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
white-space: normal;
}
.feature-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
gap: 0.55rem;
margin-bottom: 0.75rem;
}
.lab-block + .lab-block {
margin-top: 0.85rem;
}
@@ -1508,6 +1572,7 @@ button.entity-card {
.dataset-upload-form,
.map-toolbar,
.layer-provenance-rail,
.lab-form-grid,
.quality-summary-grid,
.quality-score-row,