fix: scope evolution series to work area
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-22 06:55:49 +02:00
parent 5a3e7c679b
commit 65749b1694
3 changed files with 44 additions and 4 deletions
+26 -4
View File
@@ -50,6 +50,7 @@ import {
selectionFeatureLimit,
selectionMetricLabel,
splitSelectionBbox,
temporalDatasetAreaMatch,
} from './mapWorkspaceUtils'
const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson'
@@ -584,22 +585,43 @@ function temporalSeriesLabel(items: DatasetCreateResponse[]): string {
return range ? `${source} (${range})` : source
}
function listThemeTemporalSeries(datasets: DatasetCreateResponse[], theme: DataTheme): TemporalSeriesGroup[] {
function listThemeTemporalSeries(
datasets: DatasetCreateResponse[],
theme: DataTheme,
selectedAreaId: string | null,
): TemporalSeriesGroup[] {
const groups = new Map<string, DatasetCreateResponse[]>()
for (const dataset of datasets) {
if (!datasetMatchesTheme(dataset, theme) || !dataset.temporal_series_key || !dataset.observed_at) {
continue
}
if (temporalDatasetAreaMatch(dataset, selectedAreaId) === 'other') {
continue
}
const items = groups.get(dataset.temporal_series_key) ?? []
items.push(dataset)
groups.set(dataset.temporal_series_key, items)
}
return Array.from(groups.entries())
const series = Array.from(groups.entries())
.map(([key, items]) => {
const ordered = deduplicateTemporalSnapshots(items)
return { key, label: temporalSeriesLabel(ordered), items: ordered }
})
.filter((group) => group.items.length >= 2)
const sourcesWithExactAreaSeries = new Set(
series
.filter((group) => group.items.some((item) => temporalDatasetAreaMatch(item, selectedAreaId) === 'exact'))
.map((group) => group.items[0]?.source_name)
.filter(Boolean),
)
return series
.filter((group) => {
const sourceName = group.items[0]?.source_name
if (!sourceName || !sourcesWithExactAreaSeries.has(sourceName)) return true
return group.items.some((item) => temporalDatasetAreaMatch(item, selectedAreaId) === 'exact')
})
.sort((left, right) => {
if (right.items.length !== left.items.length) {
return right.items.length - left.items.length
@@ -1442,9 +1464,9 @@ export function MapWorkspace({
const themeTemporalSeriesMap = useMemo(
() =>
Object.fromEntries(
DATA_THEMES.map((theme) => [theme.id, listThemeTemporalSeries(availableMapDatasets, theme)]),
DATA_THEMES.map((theme) => [theme.id, listThemeTemporalSeries(availableMapDatasets, theme, selectedMapAreaId)]),
) as Record<DataThemeId, TemporalSeriesGroup[]>,
[availableMapDatasets],
[availableMapDatasets, selectedMapAreaId],
)
const activeTemporalSeriesGroups = themeTemporalSeriesMap[activeTheme.id]
const availableEvolutionThemes = DATA_THEMES.filter((theme) =>
@@ -16,6 +16,7 @@ import {
selectionDimensions,
selectionFeatureLimit,
splitSelectionBbox,
temporalDatasetAreaMatch,
} from './mapWorkspaceUtils'
import type { VectorSelectionBBox, VectorSelectionResponse } from '../../types'
@@ -98,6 +99,12 @@ describe('map workspace selection guards', () => {
})).toBe(false)
})
it('distinguishes exact, unscoped and foreign temporal work-area snapshots', () => {
expect(temporalDatasetAreaMatch({ area_id: 'wallonia-golden' }, 'wallonia-golden')).toBe('exact')
expect(temporalDatasetAreaMatch({ area_id: 'other-area' }, 'wallonia-golden')).toBe('other')
expect(temporalDatasetAreaMatch({ area_id: null }, 'wallonia-golden')).toBe('unscoped')
})
it('classifies local, regional and overview selections before provider calls', () => {
const bbox = (widthDegrees: number, heightDegrees: number): VectorSelectionBBox => ({
min_x: 5,
@@ -1,5 +1,6 @@
import { featureCollectionBounds } from '../../lib/geojsonBounds'
import type {
DatasetCreateResponse,
ProjectRead,
VectorSelectionBBox,
VectorSelectionMetric,
@@ -50,6 +51,16 @@ export function isSelectionBoundedDataset(
&& sourceMetadata['bbox_epsg4326'].length === 4
}
export type TemporalAreaMatch = 'exact' | 'unscoped' | 'other'
export function temporalDatasetAreaMatch(
dataset: Pick<DatasetCreateResponse, 'area_id'>,
selectedAreaId: string | null,
): TemporalAreaMatch {
if (!selectedAreaId || !dataset.area_id) return 'unscoped'
return dataset.area_id === selectedAreaId ? 'exact' : 'other'
}
export function operationalScopeProjectLabel(project: ProjectRead): string {
if (project.name === MOL_PROJECT_NAME) {
return 'Mol'