mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(resilience): sanitize resilienceScore on non-DeckGL renderers (#2685)
* fix(resilience): sanitize resilienceScore on non-DeckGL renderers Strip resilienceScore from layer state when DeckGL is not active (mobile/SVG/globe). Prevents bookmark/URL state leaking an invisible active layer on renderers that have no resilience choropleth path. Applied at two levels: - Constructor: strip from initialState before init - setLayers: strip on every layer update for renderer switches * fix(resilience): sync sanitized layer state to app context and storage MapContainer sanitization was local-only. App state (ctx.mapLayers) and localStorage still had resilienceScore=true on non-DeckGL renderers, causing data-loader to schedule unnecessary fetches. Fix: strip resilienceScore from ctx.mapLayers and storage at two points: - After MapContainer construction (initial hydration sync-back) - In panel-layout setLayers (runtime layer updates) * fix(resilience): guard search-manager toggle and enableLayer for non-DeckGL - search-manager.ts: prevent resilienceScore from being set to true in ctx.mapLayers when DeckGL is not active (generic layer toggle path) - MapContainer.enableLayer: early-return for resilienceScore on non-DeckGL renderers to prevent SVG/globe onLayerChange from reinforcing stale state * fix(resilience): sync ctx.mapLayers after renderer mode switch switchToGlobe/switchToFlat bypasses panel-layout, so ctx.mapLayers was not synced after MapContainer internally sanitized the layer state. Add post-switch sync in event-handlers to strip resilienceScore from app state and storage when switching to a non-DeckGL renderer. * test(resilience): add regression coverage for non-DeckGL sanitization 5 new tests covering the resilienceScore sanitization invariant: - strips on non-DeckGL renderer - preserves on DeckGL renderer - doesn't affect other layers - URL restore + normalize + sanitize chain - mode switch from DeckGL to globe
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -69,6 +69,68 @@ describe('resilience choropleth thresholds', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resilience non-DeckGL sanitization', () => {
|
||||
function simulateSanitize(layers: Record<string, boolean>, 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 {
|
||||
|
||||
Reference in New Issue
Block a user