From b35f135fea4a0d9cbaa4c6a713ff5eead0eaa8de Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Jul 2026 17:51:24 +0200 Subject: [PATCH] feat: add official modern Mol land-use series --- CHANGELOG.md | 9 + backend/README.md | 12 +- ...t_sprint188_official_landuse_timeseries.py | 155 ++++ deploy/unraid/Dockerfile.all-in-one | 1 + docs/CODEX_EXECUTION_LOG.md | 24 + docs/DATASET_STRATEGY.md | 27 + docs/DATA_SOURCES.md | 35 + docs/TODO.md | 5 +- frontend/README.md | 4 + frontend/src/components/map/MapWorkspace.tsx | 79 +- frontend/src/styles/app.css | 4 + scripts/README.md | 44 +- .../provision_official_landuse_timeseries.py | 817 ++++++++++++++++++ scripts/run_readiness_check.sh | 3 + 14 files changed, 1199 insertions(+), 20 deletions(-) create mode 100644 backend/tests/test_sprint188_official_landuse_timeseries.py create mode 100644 scripts/provision_official_landuse_timeseries.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ef0e05b2..d5067be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ # Changelog +## Sprint 188 Official modern Mol land-use series (2026-07-14) + +- Added an explicit, reusable MercatorNet WCS operator for official Departement Omgeving land-use snapshots in 2013, 2016, 2019, 2022 and 2025. +- Validated categorical integer GeoTIFF input at 10 m in EPSG:31370, retained raw rasters/checksums/manifests and polygonized only documented class 12 (`Bos`). +- Imported forest polygons through the existing DatasetService/VectorFeatureService path with canonical EPSG:4326 geometry and hectare intersection metrics; no startup fetch or direct PostGIS write was added. +- Kept the modern 2013-2025 series separate from historical 1778/1873/1969 cartography and added a compact temporal-series selector when both are available. +- Made the map-first forest theme prefer the authoritative modern source and its latest 2025 snapshot while preserving historical access. +- Added focused raster, CRS, class, provenance, packaging and frontend contract tests plus readiness compilation coverage. + ## Sprint 187 Temporal Mol explorer (2026-07-14) - Added first-class temporal dataset metadata and immutable dataset-version provenance for uploaded and derived vector/raster datasets. diff --git a/backend/README.md b/backend/README.md index 95528d12..2d30546c 100644 --- a/backend/README.md +++ b/backend/README.md @@ -947,12 +947,15 @@ snapshots explicitly: ```bash docker exec geointel python /app/scripts/provision_mol_population_history.py docker exec geointel python /app/scripts/provision_mol_historical_landuse.py +docker exec geointel python /app/scripts/provision_official_landuse_timeseries.py ``` The first command imports Statbel sector population for 2021-2025. The second -imports Digitaal Vlaanderen historical land use for 1778, 1873 and 1969. Both -are idempotent, use the normal API/DatasetService flow and retain fetched -artifacts in persistent operator storage. They never run on app startup. +imports Digitaal Vlaanderen historical land use for 1778, 1873 and 1969. The +third imports the Departement Omgeving 10 m forest class for 2013, 2016, 2019, +2022 and 2025. All commands are idempotent, use the normal +API/DatasetService flow and retain fetched artifacts in persistent operator +storage. They never run on app startup. Historical land-use work can be bounded explicitly: @@ -964,6 +967,9 @@ docker exec geointel python /app/scripts/provision_mol_historical_landuse.py --y `POST /api/v1/projects/{project_id}/temporal/compare` compares two snapshots inside one EPSG:4326 bbox. Partial statistical sectors are estimates; old map editions without stable identities do not produce invented object changes. +Modern raster-derived forest polygons have the same identity limitation. Their +area is measured in EPSG:31370 and is exact within the 10 m source +representation, not a cadastral forest survey. ## Helpful repository scripts diff --git a/backend/tests/test_sprint188_official_landuse_timeseries.py b/backend/tests/test_sprint188_official_landuse_timeseries.py new file mode 100644 index 00000000..1eaac8bc --- /dev/null +++ b/backend/tests/test_sprint188_official_landuse_timeseries.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path +from types import SimpleNamespace +import sys + +import numpy as np +from pyproj import Transformer +import rasterio +from rasterio.transform import from_origin +from shapely.geometry import Polygon, shape +from shapely.ops import transform + + +ROOT = Path(__file__).resolve().parents[2] + + +def load_provisioner(): + script_path = ROOT / "scripts" / "provision_official_landuse_timeseries.py" + spec = importlib.util.spec_from_file_location("official_landuse_provisioner", script_path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_official_landuse_wcs_contract_is_categorical_and_deterministic() -> None: + module = load_provisioner() + + params = module.build_wcs_params(2025, (196594.1, 205064.2, 210910.7, 223974.9)) + + assert module.SUPPORTED_YEARS == (2013, 2016, 2019, 2022, 2025) + assert module.LAND_USE_CLASSES[12] == "Bos" + assert params == { + "SERVICE": "WCS", + "VERSION": "1.0.0", + "REQUEST": "GetCoverage", + "COVERAGE": "lu:lu_landgebruik_vlaa_2025_v3", + "CRS": "EPSG:31370", + "BBOX": "196590.000,205060.000,210920.000,223980.000", + "RESX": "10", + "RESY": "10", + "FORMAT": "image/tiff", + "RESPONSE_CRS": "EPSG:31370", + } + assert module.series_key(module.THEMES[0], "Mol") == "department-omgeving:land-use:forest:mol" + + +def test_official_landuse_polygonization_clips_and_preserves_provenance(tmp_path: Path) -> None: + module = load_provisioner() + raster_path = tmp_path / "landuse.tif" + values = np.array( + [ + [1, 1, 1, 1, 1, 1], + [1, 12, 12, 1, 1, 1], + [1, 12, 12, 1, 12, 1], + [1, 1, 1, 1, 12, 1], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + ], + dtype="int32", + ) + with rasterio.open( + raster_path, + "w", + driver="GTiff", + width=6, + height=6, + count=1, + dtype="int32", + crs="EPSG:31370", + transform=from_origin(200000, 210000, 10, 10), + nodata=-9999, + ) as destination: + destination.write(values, 1) + + to_wgs84 = Transformer.from_crs(31370, 4326, always_xy=True) + boundary_metric = Polygon( + [(200005, 209945), (200055, 209945), (200055, 209995), (200005, 209995), (200005, 209945)] + ) + boundary = transform(to_wgs84.transform, boundary_metric) + + payload, stats = module.polygonize_snapshot( + raster_path=raster_path, + boundary=boundary, + year=2025, + theme=module.THEMES[0], + municipality_name="Mol", + nis_code="13025", + scope_key="mol", + max_features=100, + ) + + assert payload["type"] == "FeatureCollection" + assert payload["crs"]["properties"]["name"] == "EPSG:4326" + assert payload["source_coverage_id"] == "lu:lu_landgebruik_vlaa_2025_v3" + assert stats["source_pixel_count"] == 6 + assert stats["source_pixel_area_m2"] == 600 + assert stats["feature_count"] == 2 + assert 0 < stats["polygon_area_m2"] <= 600 + assert stats["class_histogram"] == {"1": 30, "12": 6} + for feature in payload["features"]: + geometry = shape(feature["geometry"]) + properties = feature["properties"] + assert geometry.is_valid + assert geometry.within(boundary.buffer(1e-9)) + assert properties["source_name"] == "department_omgeving_land_use" + assert properties["land_use_class_ids"] == [12] + assert properties["source_resolution_m"] == 10.0 + assert properties["source_raster_sha256"] == stats["raster_sha256"] + + +def test_official_landuse_metadata_keeps_modern_series_separate(tmp_path: Path) -> None: + module = load_provisioner() + theme = module.THEMES[0] + snapshot = module.PreparedSnapshot( + year=2022, + theme=theme, + raster_path=tmp_path / "source.tif", + vector_path=tmp_path / "forest.geojson", + manifest_path=tmp_path / "forest.manifest.json", + feature_count=42, + raster_sha256="a" * 64, + vector_sha256="b" * 64, + ) + args = SimpleNamespace(scope_key="mol", municipality_name="Mol", nis_code="13025") + + source = module.build_source_metadata(args, snapshot) + provenance = module.build_provenance_metadata(args, snapshot) + + assert source["temporal_series_label"] == "Moderne landgebruikskaart (10 m)" + assert source["selection_aggregation"]["method"] == "intersection_area" + assert source["identity_stable"] is False + assert source["land_use_class_names"] == ["Bos"] + assert "10 m" in source["selection_aggregation"]["warning"] + assert provenance["operator_explicit_fetch"] is True + assert provenance["coverage_id"] == "lu:lu_landgebruik_vlaa_2022_v3" + assert "historical-landuse" not in module.series_key(theme, "mol") + + +def test_official_landuse_operator_is_packaged_and_readiness_checked() -> None: + readiness = (ROOT / "scripts/run_readiness_check.sh").read_text(encoding="utf-8") + dockerfile = (ROOT / "deploy/unraid/Dockerfile.all-in-one").read_text(encoding="utf-8") + workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8") + + assert "py_compile scripts/provision_official_landuse_timeseries.py" in readiness + assert "COPY scripts/provision_official_landuse_timeseries.py" in dockerfile + assert "department_omgeving_land_use' ? 90_000" in workspace + assert "activeTemporalSeriesGroups.length > 1" in workspace + assert "Moderne landgebruikskaart (10 m)" in (ROOT / "scripts/provision_official_landuse_timeseries.py").read_text( + encoding="utf-8" + ) diff --git a/deploy/unraid/Dockerfile.all-in-one b/deploy/unraid/Dockerfile.all-in-one index 623f9e82..8b2b6079 100644 --- a/deploy/unraid/Dockerfile.all-in-one +++ b/deploy/unraid/Dockerfile.all-in-one @@ -76,6 +76,7 @@ COPY scripts/provision_mol_municipality_workspace.py /app/scripts/provision_mol_ COPY scripts/provision_mol_context_layers.py /app/scripts/provision_mol_context_layers.py COPY scripts/provision_mol_population_history.py /app/scripts/provision_mol_population_history.py COPY scripts/provision_mol_historical_landuse.py /app/scripts/provision_mol_historical_landuse.py +COPY scripts/provision_official_landuse_timeseries.py /app/scripts/provision_official_landuse_timeseries.py COPY scripts/export_operator_yolo_tile_dataset.py /app/scripts/export_operator_yolo_tile_dataset.py COPY scripts/audit_operator_yolo_dataset_quality.py /app/scripts/audit_operator_yolo_dataset_quality.py COPY scripts/render_operator_yolo_label_qa_contact_sheets.py /app/scripts/render_operator_yolo_label_qa_contact_sheets.py diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 9dcd32ab..d7c9662c 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -7819,3 +7819,27 @@ Remaining source limitations: - The latest official population snapshot in this workspace is 2025. - The forest/green evolution series currently represents the available historical land-use editions through 1969; it must not be presented as current forest cover. - Partial-sector population results remain area-weighted estimates because no finer authoritative population surface has been ingested. + +## Sprint 188 Official modern Mol land-use series (2026-07-14) + +Implemented: +- Added `provision_official_landuse_timeseries.py` for explicit MercatorNet WCS subsets of the official Departement Omgeving version 3 land-use maps for 2013, 2016, 2019, 2022 and 2025. +- Validated one-band integer GeoTIFF input, EPSG:31370, 10 m resolution and the documented 1-19 class domain before processing. +- Preserved every raw raster, exact request URL, catalogue URL, raster/vector checksum, class histogram and processing manifest in operator storage. +- Polygonized documented class 12 (`Bos`) in metric CRS, clipped it to the official Mol boundary, normalized it to EPSG:4326 and uploaded it through the existing API/DatasetService/vector-feature path. +- Added source-governed hectare aggregation metadata, 10 m/non-cadastral limitations and unstable raster-polygon identity declarations. +- Kept `department-omgeving:land-use:forest:mol` separate from the historical land-use series and added a compact frontend series selector. + +Source validation: +- Live WCS capabilities exposed all five expected coverages through `lu:lu_landgebruik_vlaa__v3`. +- A full-Mol fetch-only run produced 3,768 / 3,823 / 3,953 / 3,615 / 3,676 forest polygons for 2013 / 2016 / 2019 / 2022 / 2025 without truncation. +- Polygonized forest area was 3,723.17 / 3,615.39 / 3,592.27 / 3,648.98 / 3,626.56 ha. These are measurements within the official 10 m representation, not cadastral forest areas. +- Every raster cell touching the municipality is considered before exact vector clipping. The 916 NoData edge cells in each WCS subset were explicitly excluded and recorded rather than assigned a class. + +Validation: +- New focused backend suite passed 4 tests. +- Frontend TypeScript typecheck passed after temporal-series selection wiring. +- Full readiness and live Tower/PostGIS/browser validation remain the final steps of this pass. + +Next: +- Deploy and provision all five modern snapshots on Tower, then verify current forest selection, both temporal series and a drawn rectangle in the internal browser. diff --git a/docs/DATASET_STRATEGY.md b/docs/DATASET_STRATEGY.md index 8989789e..c3825e2d 100644 --- a/docs/DATASET_STRATEGY.md +++ b/docs/DATASET_STRATEGY.md @@ -61,6 +61,33 @@ OSM is used for fast, broad, fallback context. ### OSM caveat OSM is community-maintained and may be incomplete. UI and reports must describe it as contextual/fallback data, not official ground truth. +## Official modern land-use strategy + +The Departement Omgeving version 3 land-use maps are the authoritative modern +source series for categorical land use. GeoIntel retrieves only explicit +boundary subsets from MercatorNet WCS; no whole-Flanders raster is fetched at +startup or during an interactive map query. + +- Source years: 2013, 2016, 2019, 2022 and 2025. +- Native representation: one integer category per 10 m cell in EPSG:31370. +- Preserved evidence: raw GeoTIFF, WCS request, catalogue URL, checksum, class + histogram and processing manifest. +- Operational representation: source cells polygonized in EPSG:31370, clipped + to the requested boundary and normalized to EPSG:4326 `vector_features`. +- Boundary behavior: retain every source cell touching the boundary, then + perform the exact metric geometry intersection; exclude and report NoData. +- Selection metric: exact polygon intersection area within the source's 10 m + representation, reported in hectares with a non-cadastral warning. +- Identity: raster polygons are not stable source objects; temporal object + lineage is unavailable. + +Modern land-use series must remain separate from the 1778, 1873 and 1969 +historical cartographic series. Comparisons are valid within each persisted +series and must retain the source methodology warning. Extension from Mol to +Kempen reuses the same operator with an explicit approved boundary, project, +area and scope key; an ambiguous regional label is never converted into an +invented boundary. + ## User-uploaded raster strategy V1 must support controlled local datasets because public raster access and model compatibility can be difficult. diff --git a/docs/DATA_SOURCES.md b/docs/DATA_SOURCES.md index d7d032e4..31cd1e6e 100644 --- a/docs/DATA_SOURCES.md +++ b/docs/DATA_SOURCES.md @@ -63,6 +63,41 @@ The operator uses standards-compliant WFS 2.0 XML POST requests. This keeps the spatial/class filters server-side without exposing a long XML filter in a GET query, which the public gateway rejects. +### Mol modern land use + +`scripts/provision_official_landuse_timeseries.py` uses the public Departement +Omgeving/MercatorNet WCS to retrieve the harmonized version 3 land-use maps for +2013, 2016, 2019, 2022 and 2025. The source is a categorical 10 m GeoTIFF in +Belgian Lambert 72 (`EPSG:31370`) with 19 documented classes. GeoIntel currently +derives only class `12` (`Bos`) as the operational forest theme. + +Every source raster is clipped against the explicit official boundary, +validated for integer classes, CRS and resolution, checksummed and retained in +persistent operator storage. Forest cells are polygonized and clipped in +`EPSG:31370`, then transformed to canonical `EPSG:4326` and uploaded through +DatasetService/VectorFeatureService. The raw raster remains the provenance +artifact; the persisted polygons provide rectangle selection, PostGIS area +aggregation and temporal comparison without a parallel query path. +All cells touching the requested boundary are considered before the exact +metric geometry clip; NoData cells are excluded and counted in the manifest. + +The modern series key is +`department-omgeving:land-use:forest:mol`. It remains separate from +`digitaal-vlaanderen:historical-landuse:forest:mol`: the 1778-1969 +cartographic editions and the harmonized 2013-2025 10 m land-use maps are not +presented as one continuous measurement method. Raster-derived polygon +identities are unstable, so GeoIntel compares hectares and never invents +added/removed forest objects. + +Official catalogues: + +- https://www.vlaanderen.be/datavindplaats/catalogus/landgebruik-vlaanderen-toestand-2013 +- https://www.vlaanderen.be/datavindplaats/catalogus/landgebruik-vlaanderen-toestand-2016 +- https://www.vlaanderen.be/datavindplaats/catalogus/landgebruik-vlaanderen-toestand-2019 +- https://www.vlaanderen.be/datavindplaats/catalogus/landgebruik-vlaanderen-toestand-2022 +- https://www.vlaanderen.be/datavindplaats/catalogus/landgebruik-vlaanderen-toestand-2025 +- https://www.vlaanderen.be/statistiek-vlaanderen/ruimtegebruik/landgebruik/metadata-landgebruik + ## OSM - Naam: OpenStreetMap diff --git a/docs/TODO.md b/docs/TODO.md index ff979872..300eefdd 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -6,8 +6,9 @@ - [x] Add drag rectangle selection with automatic persisted theme queries. - [x] Separate exact bbox intersection totals from the bounded map preview. - [x] Add official Mol GRB roads, water and parcels provisioning support. -- [ ] Select and validate an official Mol population/statistical-sector source before enabling the population theme. -- [ ] Select and validate an authoritative Flemish land-cover source before enabling the forest/green theme. +- [x] Select, validate and provision official Statbel 2021-2025 population/statistical-sector snapshots for Mol. +- [x] Select, validate and provision official Departement Omgeving 2013-2025 forest snapshots from the 10 m land-use map. +- [ ] Define the exact administrative Kempen scope before provisioning municipality or regional copies of the proven source series. This file now starts with the current implementation status. Older preparation/backlog sections are preserved below as historical planning context and should not be treated as the live sprint board without checking `docs/CODEX_EXECUTION_LOG.md`. diff --git a/frontend/README.md b/frontend/README.md index ed87b39b..59b05d94 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -10,6 +10,10 @@ current, while `Evolution` lets the operator compare an earlier and later snapshot from the same series over a drawn rectangle. Results show units, absolute/percentage change, estimate status and source limitations. Added/removed/modified overlays only appear for stable source identities. +When a theme has multiple valid methodologies, a compact series selector keeps +them explicit. Forest therefore defaults to the official modern 2013-2025 +10 m series, while the separate 1778-1969 historical map series remains +selectable and is never merged into the same trend. Selection results use dataset-specific PostGIS summaries. Object layers show intersecting counts, population shows inhabitants with partial-sector diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index 03a34c93..a4043830 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -7,6 +7,7 @@ import { useTemporalComparison } from '../../hooks/useTemporalComparison' const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson' const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson' +const EMPTY_TEMPORAL_SERIES: DatasetCreateResponse[] = [] type DataThemeId = 'buildings' | 'population' | 'forest' | 'water' | 'roads' | 'parcels' @@ -18,6 +19,12 @@ interface DataTheme { tokens: string[] } +interface TemporalSeriesGroup { + key: string + label: string + items: DatasetCreateResponse[] +} + const DATA_THEMES: DataTheme[] = [ { id: 'buildings', @@ -90,6 +97,7 @@ function pickThemeDataset(datasets: DatasetCreateResponse[], theme: DataTheme): const score = (dataset: DatasetCreateResponse) => (dataset.reference_layer_name && theme.tokens.includes(dataset.reference_layer_name.toLowerCase()) ? 1_000_000 : 0) + (dataset.source_name === 'grb' ? 100_000 : 0) + + (dataset.source_name === 'department_omgeving_land_use' ? 90_000 : 0) + (dataset.dataset_role === 'reference' ? 10_000 : 0) + (dataset.observed_at ? new Date(dataset.observed_at).getTime() / 100_000_000 : 0) + (dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0) @@ -98,7 +106,21 @@ function pickThemeDataset(datasets: DatasetCreateResponse[], theme: DataTheme): return candidates[0] ?? null } -function pickThemeTemporalSeries(datasets: DatasetCreateResponse[], theme: DataTheme): DatasetCreateResponse[] { +function temporalSeriesLabel(items: DatasetCreateResponse[]): string { + const configuredLabel = items.find((item) => typeof item.source_metadata?.['temporal_series_label'] === 'string') + ?.source_metadata?.['temporal_series_label'] + if (typeof configuredLabel === 'string' && configuredLabel.trim()) { + return configuredLabel + } + const first = items[0] + const source = first?.source_name ?? first?.source ?? 'Tijdreeks' + const firstYear = first?.observed_at ? new Date(first.observed_at).getUTCFullYear() : null + const last = items[items.length - 1] + const lastYear = last?.observed_at ? new Date(last.observed_at).getUTCFullYear() : null + return firstYear && lastYear ? `${source} (${firstYear}-${lastYear})` : source +} + +function listThemeTemporalSeries(datasets: DatasetCreateResponse[], theme: DataTheme): TemporalSeriesGroup[] { const groups = new Map() for (const dataset of datasets) { if (!datasetMatchesTheme(dataset, theme) || !dataset.temporal_series_key || !dataset.observed_at) { @@ -108,16 +130,21 @@ function pickThemeTemporalSeries(datasets: DatasetCreateResponse[], theme: DataT items.push(dataset) groups.set(dataset.temporal_series_key, items) } - return Array.from(groups.values()) - .filter((items) => items.length >= 2) + return Array.from(groups.entries()) + .filter(([, items]) => items.length >= 2) + .map(([key, items]) => { + const ordered = [...items].sort( + (left, right) => new Date(left.observed_at ?? 0).getTime() - new Date(right.observed_at ?? 0).getTime(), + ) + return { key, label: temporalSeriesLabel(ordered), items: ordered } + }) .sort((left, right) => { - if (right.length !== left.length) { - return right.length - left.length + if (right.items.length !== left.items.length) { + return right.items.length - left.items.length } - const latest = (items: DatasetCreateResponse[]) => Math.max(...items.map((item) => new Date(item.observed_at ?? 0).getTime())) + const latest = (group: TemporalSeriesGroup) => Math.max(...group.items.map((item) => new Date(item.observed_at ?? 0).getTime())) return latest(right) - latest(left) - })[0] - ?.sort((left, right) => new Date(left.observed_at ?? 0).getTime() - new Date(right.observed_at ?? 0).getTime()) ?? [] + }) } function formatObservationDate(value: string | null | undefined): string { @@ -496,6 +523,7 @@ export function MapWorkspace({ clearTemporalComparison, } = useTemporalComparison(selectedProjectId) const [analysisMode, setAnalysisMode] = useState<'current' | 'evolution'>('current') + const [selectedTemporalSeriesKey, setSelectedTemporalSeriesKey] = useState('') const [earlierDatasetId, setEarlierDatasetId] = useState('') const [laterDatasetId, setLaterDatasetId] = useState('') const [bboxSelectionMode, setBboxSelectionMode] = useState(false) @@ -536,10 +564,13 @@ export function MapWorkspace({ ) const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0] const activeThemeDataset = themeDatasetMap[activeTheme.id] - const activeTemporalSeries = useMemo( - () => pickThemeTemporalSeries(availableMapDatasets, activeTheme), + const activeTemporalSeriesGroups = useMemo( + () => listThemeTemporalSeries(availableMapDatasets, activeTheme), [activeTheme, availableMapDatasets], ) + const activeTemporalSeriesGroup = activeTemporalSeriesGroups.find((group) => group.key === selectedTemporalSeriesKey) + ?? activeTemporalSeriesGroups[0] + const activeTemporalSeries = activeTemporalSeriesGroup?.items ?? EMPTY_TEMPORAL_SERIES const themeResults = useMemo( () => themeInsights.flatMap((insight) => { @@ -592,6 +623,14 @@ export function MapWorkspace({ setBboxInput(bboxToInputState(mapSelectionBbox)) }, [mapSelectionBbox]) + useEffect(() => { + setSelectedTemporalSeriesKey((current) => + activeTemporalSeriesGroups.some((group) => group.key === current) + ? current + : activeTemporalSeriesGroups[0]?.key ?? '', + ) + }, [activeTemporalSeriesGroups]) + useEffect(() => { const first = activeTemporalSeries[0] const last = activeTemporalSeries[activeTemporalSeries.length - 1] @@ -912,7 +951,7 @@ export function MapWorkspace({ {analysisMode === 'evolution' ? 'Tijdreeks' : 'Actieve bron'} {analysisMode === 'evolution' - ? activeTemporalSeries[0]?.temporal_series_key ?? 'Geen tijdreeks beschikbaar' + ? activeTemporalSeriesGroup?.label ?? 'Geen tijdreeks beschikbaar' : activeThemeDataset?.name ?? 'Geen databron beschikbaar'} @@ -928,6 +967,22 @@ export function MapWorkspace({ {analysisMode === 'evolution' ? (
+ {activeTemporalSeriesGroups.length > 1 ? ( + + ) : null}