diff --git a/CHANGELOG.md b/CHANGELOG.md index d5067be7..52954136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - 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. +- Provisioned all five snapshots in the live Tower/PostGIS workspace and verified idempotent reruns, canonical provenance and full-Mol temporal selection. +- Aligned MapLibre layer and legend colors with the selected data theme and let the primary geographic explorer use the full available ultrawide width. ## Sprint 187 Temporal Mol explorer (2026-07-14) diff --git a/backend/tests/test_sprint188_official_landuse_timeseries.py b/backend/tests/test_sprint188_official_landuse_timeseries.py index 039b3411..4c3fcd48 100644 --- a/backend/tests/test_sprint188_official_landuse_timeseries.py +++ b/backend/tests/test_sprint188_official_landuse_timeseries.py @@ -180,11 +180,17 @@ 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") + geo_map = (ROOT / "frontend/src/components/GeoMap.tsx").read_text(encoding="utf-8") + premium_css = (ROOT / "frontend/src/styles/premium.css").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 "dataFillColor={activeThemeMapStyle.fill}" in workspace + assert "forest: { fill: '#347950', line: '#225f3b' }" in workspace + assert "datasetFillColor(dataFillColor)" in geo_map + assert ".workbench-main > .geo-explorer" in premium_css assert "Moderne landgebruikskaart (10 m)" in (ROOT / "scripts/provision_official_landuse_timeseries.py").read_text( encoding="utf-8" ) diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index f894cb23..23b78c8d 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -7840,7 +7840,11 @@ Validation: - The first live import was rejected before any upload because the operator requested 500 datasets while the canonical API limit is 200. The operator now follows every 200-item page and a focused pagination regression test covers 405 records. - New focused backend suite passed 5 tests. - Full readiness passed 533 backend tests, one Alembic head, frontend TypeScript typecheck/build and all repository smoke gates. -- Tower rebuilt successfully and passed PostGIS 3.6, migration, container health and browser-proxy validation; live data provisioning remains the final step of this pass. +- Tower rebuilt successfully and passed PostGIS 3.6, migration, container health and browser-proxy validation. +- All five modern forest snapshots were persisted in the canonical Mol project: 3,768 / 3,823 / 3,953 / 3,615 / 3,676 vector features for 2013 / 2016 / 2019 / 2022 / 2025. A second operator run reused every existing dataset without uploading duplicates. +- The live full-Mol comparison returned 3,723.17 ha for 2013 and 3,626.56 ha for 2025, a measured change of -96.60 ha (-2.60%) within the documented 10 m raster representation. +- Internal-browser verification confirmed latest-state source selection, separate modern and historical series, exact warning text, valid MapLibre rendering and an empty warning/error console. +- A final presentation correction aligned the active MapLibre and legend colors with each data theme and removed the old 105rem content cap only for the primary explorer on ultrawide screens. 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. +- Define the exact municipality list or administrative boundary that GeoIntel will call `Kempen`, then reuse the proven source operators and temporal query path for that scope without changing Mol semantics. diff --git a/frontend/src/components/GeoMap.tsx b/frontend/src/components/GeoMap.tsx index 2ae82ab2..60a34175 100644 --- a/frontend/src/components/GeoMap.tsx +++ b/frontend/src/components/GeoMap.tsx @@ -7,6 +7,8 @@ import type { MapViewportState, VectorSelectionBBox } from '../types' interface GeoMapProps { data: GeoJSON.FeatureCollection | null + dataFillColor?: string + dataLineColor?: string areaData?: GeoJSON.FeatureCollection | null selectedFeature?: GeoJSON.Feature | null selectionData?: GeoJSON.FeatureCollection | null @@ -50,6 +52,48 @@ const DEFAULT_ROAD_BASEMAP_STYLE: maplibregl.StyleSpecification = { ], } +function datasetFillColor(fallbackColor: string): maplibregl.ExpressionSpecification { + return [ + 'case', + ['==', ['get', 'layer_type'], 'municipality_boundary'], + '#0f766e', + [ + 'match', + ['get', 'change_type'], + 'added', + '#16a34a', + 'removed', + '#dc2626', + 'modified', + '#d97706', + 'unchanged', + '#2563eb', + fallbackColor, + ], + ] +} + +function datasetLineColor(fallbackColor: string): maplibregl.ExpressionSpecification { + return [ + 'case', + ['==', ['get', 'layer_type'], 'municipality_boundary'], + '#0f5f59', + [ + 'match', + ['get', 'change_type'], + 'added', + '#15803d', + 'removed', + '#b91c1c', + 'modified', + '#b45309', + 'unchanged', + '#1d4ed8', + fallbackColor, + ], + ] +} + function defaultMapStyle(): string | maplibregl.StyleSpecification { return import.meta.env.VITE_MAP_STYLE_URL || DEFAULT_ROAD_BASEMAP_STYLE } @@ -103,6 +147,8 @@ function bboxToFeatureCollection( function GeoMap({ data, + dataFillColor = '#f97316', + dataLineColor = '#ea580c', areaData = null, selectedFeature = null, selectionData = null, @@ -326,26 +372,7 @@ function GeoMap({ type: 'fill', source: 'dataset', paint: { - 'fill-color': [ - 'case', - ['==', ['get', 'layer_type'], 'municipality_boundary'], - '#0f766e', - ['==', ['get', 'layer_type'], 'building'], - '#0891b2', - [ - 'match', - ['get', 'change_type'], - 'added', - '#16a34a', - 'removed', - '#dc2626', - 'modified', - '#d97706', - 'unchanged', - '#2563eb', - '#f97316', - ], - ], + 'fill-color': datasetFillColor(dataFillColor), 'fill-opacity': 0.4, }, }) @@ -354,26 +381,7 @@ function GeoMap({ type: 'line', source: 'dataset', paint: { - 'line-color': [ - 'case', - ['==', ['get', 'layer_type'], 'municipality_boundary'], - '#0f5f59', - ['==', ['get', 'layer_type'], 'building'], - '#0e7490', - [ - 'match', - ['get', 'change_type'], - 'added', - '#15803d', - 'removed', - '#b91c1c', - 'modified', - '#b45309', - 'unchanged', - '#1d4ed8', - '#ea580c', - ], - ], + 'line-color': datasetLineColor(dataLineColor), 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 0.25, 12, 0.8, 16, 2], }, }) @@ -388,7 +396,20 @@ function GeoMap({ } } } - }, [data, fitDataOnChange, mapStyleReady]) + }, [data, dataFillColor, dataLineColor, fitDataOnChange, mapStyleReady]) + + useEffect(() => { + const map = mapRef.current + if (!map || !mapStyleReady || !map.isStyleLoaded()) { + return + } + if (map.getLayer('dataset-fill')) { + map.setPaintProperty('dataset-fill', 'fill-color', datasetFillColor(dataFillColor)) + } + if (map.getLayer('dataset-line')) { + map.setPaintProperty('dataset-line', 'line-color', datasetLineColor(dataLineColor)) + } + }, [dataFillColor, dataLineColor, mapStyleReady]) useEffect(() => { const map = mapRef.current diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index a4043830..49ba851f 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -70,6 +70,15 @@ const DATA_THEMES: DataTheme[] = [ }, ] +const DATA_THEME_MAP_STYLES: Record = { + buildings: { fill: '#d45f3d', line: '#9f3e24' }, + population: { fill: '#7559a6', line: '#5b3f88' }, + forest: { fill: '#347950', line: '#225f3b' }, + water: { fill: '#2676a8', line: '#155b85' }, + roads: { fill: '#6b7280', line: '#4b5563' }, + parcels: { fill: '#a7792f', line: '#7d571f' }, +} + function datasetSearchText(dataset: DatasetCreateResponse): string { return [ dataset.name, @@ -563,6 +572,7 @@ export function MapWorkspace({ [availableMapDatasets], ) const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0] + const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id] const activeThemeDataset = themeDatasetMap[activeTheme.id] const activeTemporalSeriesGroups = useMemo( () => listThemeTemporalSeries(availableMapDatasets, activeTheme), @@ -1056,6 +1066,8 @@ export function MapWorkspace({
) : ( <> - {activeTheme.shortLabel} + {activeTheme.shortLabel} Selectie )} diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index c2ec61bc..38ddc72f 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -5790,6 +5790,31 @@ section { background: rgba(212, 95, 61, 0.24); } +.geo-map-legend .geo-legend-layer-population { + border-color: #5b3f88; + background: rgba(117, 89, 166, 0.24); +} + +.geo-map-legend .geo-legend-layer-forest { + border-color: #225f3b; + background: rgba(52, 121, 80, 0.24); +} + +.geo-map-legend .geo-legend-layer-water { + border-color: #155b85; + background: rgba(38, 118, 168, 0.24); +} + +.geo-map-legend .geo-legend-layer-roads { + border-color: #4b5563; + background: rgba(107, 114, 128, 0.24); +} + +.geo-map-legend .geo-legend-layer-parcels { + border-color: #7d571f; + background: rgba(167, 121, 47, 0.24); +} + .geo-map-legend .geo-legend-selection { border-color: #6b4aaa; background: rgba(107, 74, 170, 0.18); diff --git a/frontend/src/styles/premium.css b/frontend/src/styles/premium.css index 79e5afe4..a54e6168 100644 --- a/frontend/src/styles/premium.css +++ b/frontend/src/styles/premium.css @@ -1202,6 +1202,11 @@ details.ai-lab-model-surface > summary strong { /* Responsive shell */ @media (min-width: 1800px) { + .workbench-main > .geo-explorer { + width: 100%; + max-width: none; + } + .workbench-layout { grid-template-columns: 13.25rem minmax(0, 1fr); }