diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index a6b956b1..0849d109 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -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() 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, - [availableMapDatasets], + [availableMapDatasets, selectedMapAreaId], ) const activeTemporalSeriesGroups = themeTemporalSeriesMap[activeTheme.id] const availableEvolutionThemes = DATA_THEMES.filter((theme) => diff --git a/frontend/src/components/map/mapWorkspaceUtils.test.ts b/frontend/src/components/map/mapWorkspaceUtils.test.ts index aef08956..7a0394fb 100644 --- a/frontend/src/components/map/mapWorkspaceUtils.test.ts +++ b/frontend/src/components/map/mapWorkspaceUtils.test.ts @@ -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, diff --git a/frontend/src/components/map/mapWorkspaceUtils.ts b/frontend/src/components/map/mapWorkspaceUtils.ts index 5daf91a4..e2174b2a 100644 --- a/frontend/src/components/map/mapWorkspaceUtils.ts +++ b/frontend/src/components/map/mapWorkspaceUtils.ts @@ -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, + 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'