Improve map overlay ergonomics
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user