fix: keep Mol database layers explicit on map
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user