diff --git a/changelog.d/847-cyclone-shared-selection.md b/changelog.d/847-cyclone-shared-selection.md new file mode 100644 index 000000000..a2f58ca71 --- /dev/null +++ b/changelog.d/847-cyclone-shared-selection.md @@ -0,0 +1,4 @@ +- Explicit cyclone selections publish only the currently selected NHC/CPHC + storm into shared context. Switching storms evicts the previous identity + instead of leaving stale coordinates active (kvnloo, #847; reviewed by + quanticsoul4772). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 22ce1eb3b..8e2662bf4 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -218,6 +218,11 @@ not storm size or the full hazard area. Successful empty and unavailable states are distinct. One owned native Cesium data source preserves polygon holes and geographic seams and releases on disable; forecast animation is not implied. Cyclone markers, labels, tracks and cones are hidden beyond the horizon on every map source. +Explicit cyclone selections publish one shared `weather-cyclones` record +with official advisory provenance and source position. Switching storms removes +the prior record; automatic row selection never claims global context, and +refreshes do not re-announce selection. Clearing or losing an explicitly selected +storm removes shared context rather than assigning a fallback. Consult the linked official advisory for safety decisions. Voice and HUD snapshots reuse the existing feedState classifier. Analyst follow-ups retain their original data provenance; current-view results append provenance without replacing legacy fields. HUD context and deterministic telemetry include non-nominal feed state. diff --git a/src/layers/cyclones/index.js b/src/layers/cyclones/index.js index 2738bebe0..ae4e5175a 100644 --- a/src/layers/cyclones/index.js +++ b/src/layers/cyclones/index.js @@ -79,8 +79,12 @@ export function createCyclonesLayer({ return null; } selectedContextEntity ||= { show: true }; + const contextId = `cyclone:${storm.id}`; + context.removeEntityContextsForLayer?.('weather-cyclones', { + retainIds: new Set([contextId]), + }); const record = context.registerEntityContext?.(selectedContextEntity, { - id: `cyclone:${storm.id}`, + id: contextId, layerId: 'weather-cyclones', layerName: 'Cyclone advisories', source: 'NOAA NHC / CPHC', diff --git a/src/layers/cyclones/index.test.mjs b/src/layers/cyclones/index.test.mjs index 7d796bef7..d1b7fefd4 100644 --- a/src/layers/cyclones/index.test.mjs +++ b/src/layers/cyclones/index.test.mjs @@ -419,11 +419,13 @@ test('explicit cyclone selection publishes shared context without auto-claiming selectedEvents += 1; return selectedContext; }, - removeEntityContextsForLayer(layerId) { + removeEntityContextsForLayer(layerId, { retainIds } = {}) { for (const [id, record] of records) { - if (record.layerId === layerId) records.delete(id); + if (record.layerId === layerId && !retainIds?.has(id)) + records.delete(id); } - if (selectedContext?.layerId === layerId) selectedContext = null; + if (selectedContext && !records.has(selectedContext.id)) + selectedContext = null; }, }; const h = harness({ getSnapshot: async () => next }, { context }); @@ -446,10 +448,18 @@ test('explicit cyclone selection publishes shared context without auto-claiming assert.equal(selectedContext?.longitude, -125.8); assert.equal(selectedContext?.properties.advisoryNumber, '10'); + h.layer.setParams({ stormId: 'ep152026' }); + assert.deepEqual([...records.keys()], ['cyclone:ep152026']); + assert.equal(selectedContext?.id, 'cyclone:ep152026'); + h.layer.setParams({ stormId: 'ep162026' }); + assert.deepEqual([...records.keys()], ['cyclone:ep162026']); + assert.equal(selectedContext?.id, 'cyclone:ep162026'); + assert.equal(selectedEvents, 3, 'each explicit storm switch announces once'); + await h.layer.update(); assert.equal( selectedEvents, - 1, + 3, 'refresh updates the selected record without re-announcing the click', ); @@ -463,7 +473,7 @@ test('explicit cyclone selection publishes shared context without auto-claiming assert.equal(records.size, 0); h.layer.setParams({ stormId: 'ep152026' }); - assert.equal(selectedEvents, 2); + assert.equal(selectedEvents, 4); h.layer.setParams({ clear: true }); assert.equal(selectedContext, null); assert.equal(records.size, 0);