fix: keep Mol database layers explicit on map
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-14 11:01:36 +02:00
parent 1ba479b50b
commit aae6315981
9 changed files with 190 additions and 18 deletions
+2
View File
@@ -22,6 +22,8 @@ Operational GIS testing is now available directly in the Map workspace. Users ca
Large vector layers use viewport delivery instead of downloading one unbounded GeoJSON file. Datasets above 5,000 features load their persisted PostGIS geometries from the existing bbox-selection endpoint at zoom level 14 or closer, with a 1,000-feature cap per view. The Map workspace shows visible versus total feature counts and asks the operator to zoom further when the response is truncated. Small layers, AI result overlays, selections and QA evidence keep their existing complete-FeatureCollection behavior.
The Map workspace keeps database layers and analysis-result overlays explicit. `Map content: Database` is the default and is selected automatically whenever an operator chooses a persisted database layer. `Analysis result` is enabled only when a detection, segmentation or change-result GeoJSON is available. Switching modes changes only the rendered source; it does not delete persisted analysis results or dataset state.
QA/QC and Exports follow the same calmer density model. QA/QC keeps metric evidence, feature ids and raw findings available but compresses provenance and history surfaces so review starts from the selected check and map evidence actions. Exports uses denser handoff cards, latest-artifact cards and history filters so artifact creation and download paths are easier to scan.
When project data loads and no dataset is selected yet, the workbench auto-opens the first ready vector dataset. This gives Data, Map and Exports an immediately usable default context while preserving explicit user selection once the user picks another dataset.
+24 -14
View File
@@ -56,11 +56,11 @@ const workspaceNavGroups: Array<{ label: string; keys: WorkspaceKey[] }> = [
function App(): JSX.Element {
const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('overview')
const [inspectorOpen, setInspectorOpen] = useState(false)
const [mapContentMode, setMapContentMode] = useState<'dataset' | 'analysis'>('dataset')
useEffect(() => {
window.scrollTo({ top: 0, left: 0 })
setInspectorOpen(false)
}, [activeWorkspace])
const {
projects,
selectedProject,
@@ -83,6 +83,10 @@ function App(): JSX.Element {
setProjectForm,
setAreaForm,
} = useProjectWorkspace()
const selectProject = (projectId: string) => {
setMapContentMode('dataset')
setSelectedProjectId(projectId)
}
const {
selectedDatasetId,
selectedDataset,
@@ -351,7 +355,8 @@ function App(): JSX.Element {
featureCount: selectedDatasetSummary?.feature_count ?? selectedDataset?.feature_count ?? null,
isVectorDatasetType,
})
const analysisMapLayerActive = Boolean(changeDetectionResult?.geojson || segmentationGeoJson || detectionGeoJson)
const analysisMapLayerAvailable = Boolean(changeDetectionResult?.geojson || segmentationGeoJson || detectionGeoJson)
const analysisMapLayerActive = mapContentMode === 'analysis' && analysisMapLayerAvailable
const viewportVectorLayerActive = viewportVectorLayer.enabled && !analysisMapLayerActive
const datasetMapContent = viewportVectorLayer.enabled ? viewportVectorLayer.data : datasetContent
const useRasterTileManifestForSegmentation = () => {
@@ -410,9 +415,9 @@ function App(): JSX.Element {
setSelectedMapFeature,
} = useMapWorkspaceState({
areas,
changeDetectionGeoJson: changeDetectionResult?.geojson ?? null,
segmentationGeoJson,
detectionGeoJson,
changeDetectionGeoJson: analysisMapLayerActive ? changeDetectionResult?.geojson ?? null : null,
segmentationGeoJson: analysisMapLayerActive ? segmentationGeoJson : null,
detectionGeoJson: analysisMapLayerActive ? detectionGeoJson : null,
datasetContent: datasetMapContent,
selectedDataset,
datasetLayerActive: Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type)),
@@ -479,7 +484,7 @@ function App(): JSX.Element {
loadSegmentationRuns,
loadQualityChecks,
loadExports,
setSelectedProjectId,
setSelectedProjectId: selectProject,
setSelectedDatasetId,
setSelectedMapAreaId,
setQaCandidateDatasetId,
@@ -521,28 +526,28 @@ 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) {
if (analysisMapLayerActive && changeDetectionResult?.geojson) {
return 'Change detection'
}
if (segmentationGeoJson) {
if (analysisMapLayerActive && segmentationGeoJson) {
return 'Segmentation run'
}
if (detectionGeoJson) {
if (analysisMapLayerActive && detectionGeoJson) {
return 'Detection run'
}
if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) {
return `${selectedDataset.dataset_type} dataset`
}
return 'No active vector or result layer'
}, [changeDetectionResult?.geojson, datasetMapContent, detectionGeoJson, segmentationGeoJson, selectedDataset, viewportVectorLayer.enabled])
}, [analysisMapLayerActive, changeDetectionResult?.geojson, datasetMapContent, detectionGeoJson, segmentationGeoJson, selectedDataset, viewportVectorLayer.enabled])
const mapLayerProvenance = useMemo(() => {
if (changeDetectionResult?.geojson) {
if (analysisMapLayerActive && changeDetectionResult?.geojson) {
return `source ${changeSourceDatasetId || 'n/a'} -> target ${changeTargetDatasetId || 'n/a'}`
}
if (segmentationGeoJson) {
if (analysisMapLayerActive && segmentationGeoJson) {
return selectedSegmentationRunId ? `analysis run ${selectedSegmentationRunId}` : 'segmentation results loaded'
}
if (detectionGeoJson) {
if (analysisMapLayerActive && detectionGeoJson) {
return selectedDetectionRunId ? `analysis run ${selectedDetectionRunId}` : 'detection results loaded'
}
if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) {
@@ -550,6 +555,7 @@ function App(): JSX.Element {
}
return 'Open a dataset, detection run, segmentation run or change result to draw it here.'
}, [
analysisMapLayerActive,
changeDetectionResult?.geojson,
changeSourceDatasetId,
changeTargetDatasetId,
@@ -565,6 +571,7 @@ function App(): JSX.Element {
if (selectedProjectId) {
loadDatasetDetails(selectedProjectId, dataset)
}
setMapContentMode('dataset')
setMapLayerVisible(true)
setActiveWorkspace('map')
}
@@ -867,7 +874,7 @@ function App(): JSX.Element {
demoWorkflowMessage={demoWorkflowMessage}
onCreateProject={createProject}
onUpdateProjectForm={setProjectForm}
onSelectProject={setSelectedProjectId}
onSelectProject={selectProject}
onLoadDemoWorkflow={loadDemoWorkflow}
/>
@@ -925,6 +932,8 @@ function App(): JSX.Element {
viewportVectorStatus={viewportVectorLayerActive ? viewportVectorLayer.statusMessage : null}
viewportVectorTone={viewportVectorLayer.error ? 'error' : viewportVectorLayer.truncated ? 'warning' : viewportVectorLayer.loading || viewportVectorLayer.zoomRequired ? 'pending' : 'ready'}
fitMapDataOnChange={!viewportVectorLayerActive}
mapContentMode={analysisMapLayerActive ? 'analysis' : 'dataset'}
analysisLayerAvailable={analysisMapLayerAvailable}
selectedMapFeature={selectedMapFeature}
mapSelectionBbox={mapSelectionBbox}
mapSelectionResult={mapSelectionResult}
@@ -952,6 +961,7 @@ function App(): JSX.Element {
onSetAreaLayerOpacity={setAreaLayerOpacity}
onSetMapLayerVisible={setMapLayerVisible}
onSetMapLayerOpacity={setMapLayerOpacity}
onSetMapContentMode={setMapContentMode}
onSelectMapFeature={setSelectedMapFeature}
onMapViewportChange={viewportVectorLayer.setViewport}
onSetMapSelectionBbox={setMapSelectionBbox}
@@ -193,6 +193,8 @@ interface MapWorkspaceProps {
viewportVectorStatus: string | null
viewportVectorTone: 'ready' | 'pending' | 'warning' | 'error'
fitMapDataOnChange: boolean
mapContentMode: 'dataset' | 'analysis'
analysisLayerAvailable: boolean
selectedMapFeature: GeoJSON.Feature | null
selectedFeature?: GeoJSON.Feature | null
mapSelectionBbox: VectorSelectionBBox | null
@@ -220,6 +222,7 @@ interface MapWorkspaceProps {
onSetAreaLayerOpacity: (opacity: number) => void
onSetMapLayerVisible: (visible: boolean) => void
onSetMapLayerOpacity: (opacity: number) => void
onSetMapContentMode: (mode: 'dataset' | 'analysis') => void
onSelectMapFeature: (feature: GeoJSON.Feature | null) => void
onMapViewportChange: (viewport: MapViewportState) => void
onSetMapSelectionBbox: (bbox: VectorSelectionBBox | null) => void
@@ -256,6 +259,8 @@ export function MapWorkspace({
viewportVectorStatus,
viewportVectorTone,
fitMapDataOnChange,
mapContentMode,
analysisLayerAvailable,
selectedMapFeature,
selectedFeature = selectedMapFeature,
mapSelectionBbox,
@@ -283,6 +288,7 @@ export function MapWorkspace({
onSetAreaLayerOpacity,
onSetMapLayerVisible,
onSetMapLayerOpacity,
onSetMapContentMode,
onSelectMapFeature,
onMapViewportChange,
onSetMapSelectionBbox,
@@ -508,6 +514,26 @@ export function MapWorkspace({
</div>
) : null}
<div className="map-toolbar">
<div className="map-layer-mode" aria-label="Map content mode">
<span>Map content</span>
<div role="group" aria-label="Map content source">
<button
type="button"
aria-pressed={mapContentMode === 'dataset'}
onClick={() => onSetMapContentMode('dataset')}
>
Database
</button>
<button
type="button"
aria-pressed={mapContentMode === 'analysis'}
disabled={!analysisLayerAvailable}
onClick={() => onSetMapContentMode('analysis')}
>
Analysis result
</button>
</div>
</div>
<label>
Database layer
<select
+50 -1
View File
@@ -4969,7 +4969,7 @@ section {
}
.map-workspace-shell .map-toolbar {
grid-template-columns: minmax(13rem, 1.25fr) minmax(10rem, 1fr) repeat(2, minmax(8rem, 0.85fr)) minmax(11rem, 1fr);
grid-template-columns: minmax(10rem, 0.9fr) minmax(13rem, 1.25fr) minmax(10rem, 1fr) repeat(2, minmax(8rem, 0.85fr)) minmax(11rem, 1fr);
gap: 0.5rem;
}
@@ -4977,6 +4977,55 @@ section {
margin-top: 0;
}
.map-layer-mode {
display: grid;
align-content: start;
gap: 0.34rem;
min-width: 0;
}
.map-layer-mode > span {
color: var(--muted);
font-size: 0.7rem;
font-weight: 700;
}
.map-layer-mode > div {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
overflow: hidden;
border: 1px solid var(--line-strong);
border-radius: 6px;
background: var(--panel-soft);
}
.map-layer-mode button {
min-width: 0;
min-height: 2.2rem;
border: 0;
border-radius: 0;
padding: 0.36rem 0.42rem;
background: transparent;
color: var(--muted);
font-size: 0.68rem;
font-weight: 750;
line-height: 1.15;
}
.map-layer-mode button + button {
border-left: 1px solid var(--line-strong);
}
.map-layer-mode button[aria-pressed='true'] {
background: var(--accent-strong);
color: #ffffff;
}
.map-layer-mode button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.layer-control-card,
.map-status {
border-radius: 6px;
+14 -3
View File
@@ -936,7 +936,7 @@ details.ai-lab-model-surface > summary strong {
.map-workspace-shell .map-toolbar {
display: grid;
grid-template-columns: minmax(13rem, 1.3fr) minmax(10rem, 0.9fr) minmax(8rem, 0.65fr) minmax(8rem, 0.65fr) minmax(10rem, 0.9fr);
grid-template-columns: minmax(10rem, 0.85fr) minmax(13rem, 1.3fr) minmax(10rem, 0.9fr) minmax(8rem, 0.65fr) minmax(8rem, 0.65fr);
gap: 0.6rem;
align-items: end;
}
@@ -956,7 +956,10 @@ details.ai-lab-model-surface > summary strong {
.map-status {
display: grid;
min-height: 3.9rem;
grid-column: 1 / -1;
grid-template-columns: minmax(13rem, 1.35fr) repeat(3, minmax(10rem, 1fr));
gap: 0.25rem 0.85rem;
min-height: auto;
align-content: center;
border-left: 3px solid var(--accent);
border-radius: 4px;
@@ -969,7 +972,15 @@ details.ai-lab-model-surface > summary strong {
}
.map-status span {
display: none;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.map-status .viewport-vector-status {
grid-column: 1 / -1;
white-space: normal;
}
.basemap-policy-notice {