diff --git a/src/app/event-handlers.ts b/src/app/event-handlers.ts index bd17d9078..cef87f22e 100644 --- a/src/app/event-handlers.ts +++ b/src/app/event-handlers.ts @@ -1499,6 +1499,10 @@ export class EventHandlerManager implements AppModule { } else { this.ctx.map?.switchToFlat(); } + if (this.ctx.mapLayers.resilienceScore && !this.ctx.map?.isDeckGLActive?.()) { + this.ctx.mapLayers = { ...this.ctx.mapLayers, resilienceScore: false }; + saveToStorage(STORAGE_KEYS.mapLayers, this.ctx.mapLayers); + } }); }); } diff --git a/src/app/panel-layout.ts b/src/app/panel-layout.ts index d0ac4468d..ef53be7b6 100644 --- a/src/app/panel-layout.ts +++ b/src/app/panel-layout.ts @@ -718,6 +718,11 @@ export class PanelLayoutManager implements AppModule { timeRange: '7d', }, preferGlobe); + if (this.ctx.mapLayers.resilienceScore && !this.ctx.map.isDeckGLActive?.()) { + this.ctx.mapLayers = { ...this.ctx.mapLayers, resilienceScore: false }; + saveToStorage(STORAGE_KEYS.mapLayers, this.ctx.mapLayers); + } + this.ctx.map.initEscalationGetters(); this.ctx.currentTimeRange = this.ctx.map.getTimeRange(); @@ -1405,7 +1410,10 @@ export class PanelLayoutManager implements AppModule { } if (layers) { - const normalized = normalizeExclusiveChoropleths(layers, this.ctx.mapLayers); + let normalized = normalizeExclusiveChoropleths(layers, this.ctx.mapLayers); + if (normalized.resilienceScore && !this.ctx.map.isDeckGLActive?.()) { + normalized = { ...normalized, resilienceScore: false }; + } this.ctx.mapLayers = normalized; saveToStorage(STORAGE_KEYS.mapLayers, normalized); this.ctx.map.setLayers(normalized); diff --git a/src/app/search-manager.ts b/src/app/search-manager.ts index ff382dcc2..b77178768 100644 --- a/src/app/search-manager.ts +++ b/src/app/search-manager.ts @@ -490,9 +490,13 @@ export class SearchManager implements AppModule { if (!(layerKey in this.ctx.mapLayers)) return; const variantAllowed = getAllowedLayerKeys((SITE_VARIANT || 'full') as MapVariant); if (!variantAllowed.has(layerKey)) return; - this.ctx.mapLayers[layerKey] = !this.ctx.mapLayers[layerKey]; + let newValue = !this.ctx.mapLayers[layerKey]; + if (newValue && layerKey === 'resilienceScore' && !this.ctx.map?.isDeckGLActive?.()) { + newValue = false; + } + this.ctx.mapLayers[layerKey] = newValue; saveToStorage(STORAGE_KEYS.mapLayers, this.ctx.mapLayers); - if (this.ctx.mapLayers[layerKey]) { + if (newValue) { this.ctx.map?.enableLayer(layerKey); } else { this.ctx.map?.setLayers(this.ctx.mapLayers); diff --git a/src/components/MapContainer.ts b/src/components/MapContainer.ts index d8921b830..643550deb 100644 --- a/src/components/MapContainer.ts +++ b/src/components/MapContainer.ts @@ -149,9 +149,12 @@ export class MapContainer { this.isMobile = isMobileDevice(); this.useGlobe = preferGlobe && this.hasWebGLSupport(); - // Use deck.gl on desktop with WebGL support, SVG on mobile this.useDeckGL = !this.useGlobe && this.shouldUseDeckGL(); + if (!this.useDeckGL && this.initialState.layers?.resilienceScore) { + this.initialState = { ...this.initialState, layers: { ...this.initialState.layers, resilienceScore: false } }; + } + this.init(); } @@ -394,8 +397,9 @@ export class MapContainer { } public setLayers(layers: MapLayers): void { - if (this.useGlobe) { this.globeMap?.setLayers(layers); return; } - if (this.useDeckGL) { this.deckGLMap?.setLayers(layers); } else { this.svgMap?.setLayers(layers); } + const sanitized = !this.useDeckGL && layers.resilienceScore ? { ...layers, resilienceScore: false } : layers; + if (this.useGlobe) { this.globeMap?.setLayers(sanitized); return; } + if (this.useDeckGL) { this.deckGLMap?.setLayers(sanitized); } else { this.svgMap?.setLayers(sanitized); } } public getState(): MapContainerState { @@ -831,6 +835,7 @@ export class MapContainer { // Layer enable/disable and trigger methods public enableLayer(layer: keyof MapLayers): void { + if (layer === 'resilienceScore' && !this.useDeckGL) return; if (this.useGlobe) { this.globeMap?.enableLayer(layer); return; } if (this.useDeckGL) { this.deckGLMap?.enableLayer(layer); diff --git a/tests/resilience-map-layer.test.mts b/tests/resilience-map-layer.test.mts index 2b33cad35..ef8e09fd9 100644 --- a/tests/resilience-map-layer.test.mts +++ b/tests/resilience-map-layer.test.mts @@ -69,6 +69,68 @@ describe('resilience choropleth thresholds', () => { }); }); +describe('resilience non-DeckGL sanitization', () => { + function simulateSanitize(layers: Record, isDeckGLActive: boolean) { + if (layers.resilienceScore && !isDeckGLActive) { + return { ...layers, resilienceScore: false }; + } + return { ...layers }; + } + + it('strips resilienceScore from layer state when DeckGL is not active', () => { + const layers = { ...baseLayers(), resilienceScore: true }; + const result = simulateSanitize(layers, false); + assert.equal(result.resilienceScore, false); + }); + + it('preserves resilienceScore when DeckGL is active', () => { + const layers = { ...baseLayers(), resilienceScore: true }; + const result = simulateSanitize(layers, true); + assert.equal(result.resilienceScore, true); + }); + + it('does not affect other layers when stripping resilienceScore', () => { + const layers = { ...baseLayers(), resilienceScore: true, ciiChoropleth: true, flights: true }; + const result = simulateSanitize(layers, false); + assert.equal(result.resilienceScore, false); + assert.equal(result.ciiChoropleth, true); + assert.equal(result.flights, true); + }); + + it('URL restore with resilienceScore=true on non-DeckGL produces false in sanitized state', () => { + const urlLayers = { ...baseLayers(), resilienceScore: true }; + const normalized = normalizeExclusiveChoropleths(urlLayers, null); + const sanitized = simulateSanitize(normalized, false); + assert.equal(sanitized.resilienceScore, false); + }); + + it('mode switch from DeckGL to globe strips resilienceScore', () => { + const deckGlState = { ...baseLayers(), resilienceScore: true }; + const afterSwitch = simulateSanitize(deckGlState, false); + assert.equal(afterSwitch.resilienceScore, false); + }); + + function baseLayers() { + return { + conflicts: false, bases: false, cables: false, pipelines: false, + hotspots: false, ais: false, nuclear: false, irradiators: false, + radiationWatch: false, sanctions: false, weather: false, economic: false, + waterways: false, outages: false, cyberThreats: false, datacenters: false, + protests: false, flights: false, military: false, natural: false, + spaceports: false, minerals: false, fires: false, ucdpEvents: false, + displacement: false, climate: false, startupHubs: false, cloudRegions: false, + accelerators: false, techHQs: false, techEvents: false, stockExchanges: false, + financialCenters: false, centralBanks: false, commodityHubs: false, + gulfInvestments: false, positiveEvents: false, kindness: false, + happiness: false, speciesRecovery: false, renewableInstallations: false, + tradeRoutes: false, iranAttacks: false, gpsJamming: false, satellites: false, + ciiChoropleth: false, resilienceScore: false, dayNight: false, + miningSites: false, processingPlants: false, commodityPorts: false, + webcams: false, weatherRadar: false, diseaseOutbreaks: false, + }; + } +}); + describe('resilience choropleth exclusivity', () => { function baseLayers() { return {