diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bfaf10f..70ab8423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Added a compact frontend status strip for project, AOI, datasets, active map layer, QA/QC and exports. - The strip is driven by existing App state and suggests the next operator action in the V1 loop. +- Hardened the MapLibre component so GeoJSON sources/layers wait for the map style to finish loading before updates run. - Added regression coverage to ensure the strip remains wired without introducing new API calls. - No backend behavior, migrations, API contracts, provider downloads, AI inference or new dependencies were introduced. diff --git a/backend/tests/test_sprint19_map_workbench.py b/backend/tests/test_sprint19_map_workbench.py index 17c5e36c..716ab5a5 100644 --- a/backend/tests/test_sprint19_map_workbench.py +++ b/backend/tests/test_sprint19_map_workbench.py @@ -13,6 +13,8 @@ def test_geomap_exposes_v1_layer_controls_and_feature_inspection_contract() -> N assert "opacity?: number" in geomap assert "onFeatureSelect?: (feature: GeoJSON.Feature | null) => void" in geomap assert "queryRenderedFeatures" in geomap + assert "mapStyleReady" in geomap + assert "map.isStyleLoaded()" in geomap assert "setLayoutProperty('dataset-fill', 'visibility'" in geomap assert "setPaintProperty('dataset-fill', 'fill-opacity', opacity)" in geomap diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 49391cd5..6ddaa61a 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -3,7 +3,7 @@ Changed: - Added `frontend/src/components/WorkbenchStatusStrip.tsx` to summarize existing V1 state for project, AOI, datasets, active map layer, QA/QC and exports. - Wired the status strip into `frontend/src/App.tsx` using existing orchestration state only. -- Added compact status-strip styling and regression tests for the frontend wiring contract. +- Added compact status-strip styling and regression tests for the frontend wiring contract.`n- Hardened `frontend/src/components/GeoMap.tsx` so MapLibre source/layer updates wait for style readiness before adding sources. - Updated frontend README, TODO and changelog. Tested: diff --git a/frontend/README.md b/frontend/README.md index 616da9d9..771f9f00 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -162,6 +162,7 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow. - The strip summarizes existing connected state for project, AOI, datasets, active map layer, persisted QA/QC results and exports. - It suggests the next operator action based on missing V1 loop state without calling new APIs or adding backend behavior. - The status strip is implemented in `src/components/WorkbenchStatusStrip.tsx` and remains driven by `App.tsx` orchestration state. +- MapLibre source/layer updates now wait for style readiness to avoid runtime blank-screen failures during first render. ## Release hardening updates diff --git a/frontend/src/components/GeoMap.tsx b/frontend/src/components/GeoMap.tsx index dcecf07b..11719dcd 100644 --- a/frontend/src/components/GeoMap.tsx +++ b/frontend/src/components/GeoMap.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' import maplibregl from 'maplibre-gl' import 'maplibre-gl/dist/maplibre-gl.css' @@ -63,6 +63,7 @@ function GeoMap({ const containerRef = useRef(null) const mapRef = useRef(null) const onFeatureSelectRef = useRef(onFeatureSelect) + const [mapStyleReady, setMapStyleReady] = useState(false) useEffect(() => { onFeatureSelectRef.current = onFeatureSelect @@ -80,6 +81,9 @@ function GeoMap({ zoom: 9, }) map.addControl(new maplibregl.NavigationControl(), 'top-right') + map.on('load', () => { + setMapStyleReady(true) + }) map.on('click', (event) => { const layers = ['dataset-fill', 'dataset-line', 'area-fill', 'area-line'].filter((layerId) => map.getLayer(layerId)) if (layers.length === 0) { @@ -101,12 +105,13 @@ function GeoMap({ return () => { map.remove() mapRef.current = null + setMapStyleReady(false) } }, []) useEffect(() => { const map = mapRef.current - if (!map) { + if (!map || !mapStyleReady || !map.isStyleLoaded()) { return } @@ -174,11 +179,11 @@ function GeoMap({ } } } - }, [data]) + }, [data, mapStyleReady]) useEffect(() => { const map = mapRef.current - if (!map) { + if (!map || !mapStyleReady || !map.isStyleLoaded()) { return } @@ -231,11 +236,11 @@ function GeoMap({ map.fitBounds(bounds, { padding: 40 }) } } - }, [areaData, data]) + }, [areaData, data, mapStyleReady]) useEffect(() => { const map = mapRef.current - if (!map) { + if (!map || !mapStyleReady || !map.isStyleLoaded()) { return } const visibility = visible ? 'visible' : 'none' @@ -247,11 +252,11 @@ function GeoMap({ map.setLayoutProperty('dataset-line', 'visibility', visibility) map.setPaintProperty('dataset-line', 'line-opacity', visible ? 1 : 0) } - }, [visible, opacity, data]) + }, [visible, opacity, data, mapStyleReady]) useEffect(() => { const map = mapRef.current - if (!map) { + if (!map || !mapStyleReady || !map.isStyleLoaded()) { return } const visibility = areaVisible ? 'visible' : 'none' @@ -263,7 +268,7 @@ function GeoMap({ map.setLayoutProperty('area-line', 'visibility', visibility) map.setPaintProperty('area-line', 'line-opacity', areaVisible ? 1 : 0) } - }, [areaVisible, areaOpacity, areaData]) + }, [areaVisible, areaOpacity, areaData, mapStyleReady]) return
}