fix: align GIS theme styling on ultrawide
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-14 18:08:43 +02:00
parent 0bb2c8fd65
commit 3a2d1fae74
7 changed files with 119 additions and 44 deletions
+2
View File
@@ -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. - 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. - 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. - 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) ## Sprint 187 Temporal Mol explorer (2026-07-14)
@@ -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") 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") 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") 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 "py_compile scripts/provision_official_landuse_timeseries.py" in readiness
assert "COPY scripts/provision_official_landuse_timeseries.py" in dockerfile assert "COPY scripts/provision_official_landuse_timeseries.py" in dockerfile
assert "department_omgeving_land_use' ? 90_000" in workspace assert "department_omgeving_land_use' ? 90_000" in workspace
assert "activeTemporalSeriesGroups.length > 1" 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( assert "Moderne landgebruikskaart (10 m)" in (ROOT / "scripts/provision_official_landuse_timeseries.py").read_text(
encoding="utf-8" encoding="utf-8"
) )
+6 -2
View File
@@ -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. - 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. - 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. - 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: 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.
+62 -41
View File
@@ -7,6 +7,8 @@ import type { MapViewportState, VectorSelectionBBox } from '../types'
interface GeoMapProps { interface GeoMapProps {
data: GeoJSON.FeatureCollection | null data: GeoJSON.FeatureCollection | null
dataFillColor?: string
dataLineColor?: string
areaData?: GeoJSON.FeatureCollection | null areaData?: GeoJSON.FeatureCollection | null
selectedFeature?: GeoJSON.Feature | null selectedFeature?: GeoJSON.Feature | null
selectionData?: GeoJSON.FeatureCollection | 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 { function defaultMapStyle(): string | maplibregl.StyleSpecification {
return import.meta.env.VITE_MAP_STYLE_URL || DEFAULT_ROAD_BASEMAP_STYLE return import.meta.env.VITE_MAP_STYLE_URL || DEFAULT_ROAD_BASEMAP_STYLE
} }
@@ -103,6 +147,8 @@ function bboxToFeatureCollection(
function GeoMap({ function GeoMap({
data, data,
dataFillColor = '#f97316',
dataLineColor = '#ea580c',
areaData = null, areaData = null,
selectedFeature = null, selectedFeature = null,
selectionData = null, selectionData = null,
@@ -326,26 +372,7 @@ function GeoMap({
type: 'fill', type: 'fill',
source: 'dataset', source: 'dataset',
paint: { paint: {
'fill-color': [ 'fill-color': datasetFillColor(dataFillColor),
'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-opacity': 0.4, 'fill-opacity': 0.4,
}, },
}) })
@@ -354,26 +381,7 @@ function GeoMap({
type: 'line', type: 'line',
source: 'dataset', source: 'dataset',
paint: { paint: {
'line-color': [ 'line-color': datasetLineColor(dataLineColor),
'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-width': ['interpolate', ['linear'], ['zoom'], 8, 0.25, 12, 0.8, 16, 2], '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(() => { useEffect(() => {
const map = mapRef.current const map = mapRef.current
+13 -1
View File
@@ -70,6 +70,15 @@ const DATA_THEMES: DataTheme[] = [
}, },
] ]
const DATA_THEME_MAP_STYLES: Record<DataThemeId, { fill: string; line: string }> = {
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 { function datasetSearchText(dataset: DatasetCreateResponse): string {
return [ return [
dataset.name, dataset.name,
@@ -563,6 +572,7 @@ export function MapWorkspace({
[availableMapDatasets], [availableMapDatasets],
) )
const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0] 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 activeThemeDataset = themeDatasetMap[activeTheme.id]
const activeTemporalSeriesGroups = useMemo( const activeTemporalSeriesGroups = useMemo(
() => listThemeTemporalSeries(availableMapDatasets, activeTheme), () => listThemeTemporalSeries(availableMapDatasets, activeTheme),
@@ -1056,6 +1066,8 @@ export function MapWorkspace({
<div className={bboxSelectionMode ? 'geo-map-canvas geo-map-canvas-drawing' : 'geo-map-canvas'}> <div className={bboxSelectionMode ? 'geo-map-canvas geo-map-canvas-drawing' : 'geo-map-canvas'}>
<GeoMap <GeoMap
data={analysisMode === 'evolution' && temporalComparison?.geojson.features.length ? temporalComparison.geojson : mapFeatureCollection} data={analysisMode === 'evolution' && temporalComparison?.geojson.features.length ? temporalComparison.geojson : mapFeatureCollection}
dataFillColor={activeThemeMapStyle.fill}
dataLineColor={activeThemeMapStyle.line}
areaData={areaFeatureCollection} areaData={areaFeatureCollection}
selectedFeature={selectedFeature} selectedFeature={selectedFeature}
selectionData={analysisMode === 'current' ? mapSelectionResult?.geojson ?? null : null} selectionData={analysisMode === 'current' ? mapSelectionResult?.geojson ?? null : null}
@@ -1082,7 +1094,7 @@ export function MapWorkspace({
</> </>
) : ( ) : (
<> <>
<span><i className="geo-legend-layer" /> {activeTheme.shortLabel}</span> <span><i className={`geo-legend-layer geo-legend-layer-${activeTheme.id}`} /> {activeTheme.shortLabel}</span>
<span><i className="geo-legend-selection" /> Selectie</span> <span><i className="geo-legend-selection" /> Selectie</span>
</> </>
)} )}
+25
View File
@@ -5790,6 +5790,31 @@ section {
background: rgba(212, 95, 61, 0.24); 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 { .geo-map-legend .geo-legend-selection {
border-color: #6b4aaa; border-color: #6b4aaa;
background: rgba(107, 74, 170, 0.18); background: rgba(107, 74, 170, 0.18);
+5
View File
@@ -1202,6 +1202,11 @@ details.ai-lab-model-surface > summary strong {
/* Responsive shell */ /* Responsive shell */
@media (min-width: 1800px) { @media (min-width: 1800px) {
.workbench-main > .geo-explorer {
width: 100%;
max-width: none;
}
.workbench-layout { .workbench-layout {
grid-template-columns: 13.25rem minmax(0, 1fr); grid-template-columns: 13.25rem minmax(0, 1fr);
} }