From 70494698bc9717001022c17d1dd1f371cb562fbd Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 31 Oct 2026 22:58:06 -0700 Subject: [PATCH] Keep a refreshed weather credit through frame installs and host switches A refreshed manifest could lose its credit in two ways. A frame still loading when the refresh arrived had no credit yet, so the refresh was skipped and the frame installed with the credit of the snapshot it was staged from. And a host switch restaged the shown frame, then re-applied the refreshed manifest captured when the restage began, overwriting any newer refresh that arrived while it loaded. Each frame now remembers the latest refreshed manifest of its product, whether or not its imagery is installed yet, and takes its credit from it, falling back to the snapshot it was staged from. That snapshot still supplies the time and bounds. A restage passes the newest refreshed manifest to the new frame up front instead of re-applying a captured one afterwards, and a refresh that arrives while a frame loads is kept for a later restage. --- src/layers/weather/display.test.mjs | 100 ++++++++++++++++++++++++++++ src/layers/weather/rendering.js | 49 +++++++++++--- 2 files changed, 138 insertions(+), 11 deletions(-) diff --git a/src/layers/weather/display.test.mjs b/src/layers/weather/display.test.mjs index f867dc197..e852a1ae8 100644 --- a/src/layers/weather/display.test.mjs +++ b/src/layers/weather/display.test.mjs @@ -805,6 +805,106 @@ test('a refreshed manifest leaves the shown snapshot for a host switch to restag rendering.clear(); }); +test('a refresh during a host-switch restage keeps the newest credit', async () => { + const globe = globeViewer(); + let host = { collection: globe.viewer.imageryLayers, kind: 'globe' }; + const rendering = createWeatherRendering({ + viewer: globe.viewer, + ...globe.options, + getHost: () => host, + createShell: ({ product }) => ({ + product, + setAlpha() {}, + setHidden() {}, + rehome: () => false, + cancelPrefetch() {}, + clear() {}, + async setFrame() { + return true; + }, + }), + }); + const first = { + ...base, + product: 'lightning', + display: { credit: 'First credit' }, + }; + const shown = rendering.setFrame(first, times[0]); + await globe.settle(); + assert.equal(await shown, true); + rendering.updateSnapshot({ ...first, display: { credit: 'Second credit' } }); + host = { collection: {}, kind: 'tileset' }; + assert.equal(rendering.rehome(), true); + await flush(); + host = { collection: globe.viewer.imageryLayers, kind: 'globe' }; + assert.equal(rendering.rehome(), true); + rendering.updateSnapshot({ ...first, display: { credit: 'Third credit' } }); + await globe.settle(); + assert.equal(globe.providers.length, 2); + assert.equal(creditOf(globe.providers), 'Third credit'); + rendering.clear(); +}); + +test('a refresh while a frame loads survives a later host switch', async () => { + const globe = globeViewer(); + let host = { collection: globe.viewer.imageryLayers, kind: 'globe' }; + const rendering = createWeatherRendering({ + viewer: globe.viewer, + ...globe.options, + getHost: () => host, + createShell: ({ product }) => ({ + product, + setAlpha() {}, + setHidden() {}, + rehome: () => false, + cancelPrefetch() {}, + clear() {}, + async setFrame() { + return true; + }, + }), + }); + const first = { + ...base, + product: 'lightning', + display: { credit: 'First credit' }, + }; + const shown = rendering.setFrame(first, times[0]); + rendering.updateSnapshot({ ...first, display: { credit: 'Second credit' } }); + await globe.settle(); + assert.equal(await shown, true); + assert.equal(creditOf(globe.providers), 'Second credit'); + host = { collection: {}, kind: 'tileset' }; + assert.equal(rendering.rehome(), true); + await flush(); + host = { collection: globe.viewer.imageryLayers, kind: 'globe' }; + assert.equal(rendering.rehome(), true); + await globe.settle(); + assert.equal(globe.providers.length, 2); + assert.equal(creditOf(globe.providers), 'Second credit'); + rendering.clear(); +}); + +test('a refresh before a frame is installed gives it the newest credit', async () => { + for (const product of ['lightning', 'clouds']) { + const globe = globeViewer(); + const rendering = createWeatherRendering({ + viewer: globe.viewer, + ...globe.options, + }); + const first = { ...base, product, display: { credit: 'First credit' } }; + const shown = rendering.setFrame(first, times[1]); + rendering.updateSnapshot({ + ...first, + display: { credit: 'Second credit' }, + }); + await globe.settle(); + assert.equal(await shown, true); + assert.equal(creditOf(globe.providers), 'Second credit', product); + rendering.clear(); + } +}); + test('a layer refresh with the same latest frame updates the imagery credit', async (t) => { t.mock.method(Date, 'now', () => Date.parse(times[1])); const globe = globeViewer(); diff --git a/src/layers/weather/rendering.js b/src/layers/weather/rendering.js index 05be9cebf..629101693 100644 --- a/src/layers/weather/rendering.js +++ b/src/layers/weather/rendering.js @@ -127,21 +127,24 @@ function createGlobeRendering({ ? 'NOAA nowCOAST · NWS/OAR MRMS' : 'NOAA nowCOAST · NESDIS GOES / global satellite partners'; } - function setCredit(frame, snapshot = frame.snapshot) { - const text = creditText(snapshot); + function setCredit(frame) { + const text = creditText(frame.creditSnapshot ?? frame.snapshot); if (frame.credit?.html === text) return false; frame.credit = new cesium.Credit(text, false); return true; } /** * Take the credit of a refreshed manifest of the same product. Each frame - * keeps the snapshot it was staged from, whose time and bounds still match. + * keeps the snapshot it was staged from, whose time and bounds still match, + * and remembers the refreshed one for its credit, including a frame that + * is still loading and takes its credit when installed. */ function updateSnapshot(snapshot) { let changed = false; for (const frame of new Set([current, incoming, ...retiring])) { if (!frame || frame.product !== snapshot.product) continue; - if (frame.credit && setCredit(frame, snapshot)) changed = true; + frame.creditSnapshot = snapshot; + if (frame.credit && setCredit(frame)) changed = true; } if (changed) viewer.scene.requestRender(); } @@ -251,7 +254,11 @@ function createGlobeRendering({ if (job && prefetchJob === job) prefetchJob = null; } }, - async setFrame(snapshot, time, { signal, infrared = 'filtered' } = {}) { + async setFrame( + snapshot, + time, + { signal, infrared = 'filtered', creditSnapshot } = {}, + ) { signal?.throwIfAborted(); cancelPrefetch(); rehome(); @@ -266,7 +273,7 @@ function createGlobeRendering({ current.kind === kind && current.infrared === infrared ) { - updateSnapshot(snapshot); + updateSnapshot(creditSnapshot ?? snapshot); return true; } lastError = null; @@ -275,6 +282,7 @@ function createGlobeRendering({ const global = snapshot.product === 'clouds'; const frame = { snapshot, + creditSnapshot, time, infrared, mosaic: global @@ -608,6 +616,8 @@ export function createWeatherRendering({ let hidden = false; let shown = null; let restaging = null; + // The last refreshed manifest, so a frame shown after it keeps its credit. + let refreshed = null; // A host without imagery keeps the active renderer, which retains its frame. const wanted = () => { @@ -635,11 +645,14 @@ export function createWeatherRendering({ if (restaging !== frame) return; restaging = null; if (!ok) onChange(); - else if (frame.latest && target === globe) - globe.updateSnapshot(frame.latest); }; + // The restaged frame takes the newest refreshed credit; a refresh while + // it loads reaches it through the globe's own updateSnapshot. target - .setFrame(frame.snapshot, frame.time, { infrared: frame.infrared }) + .setFrame(frame.snapshot, frame.time, { + infrared: frame.infrared, + ...(target === globe ? { creditSnapshot: shown?.latest } : {}), + }) .then(settle, () => settle(false)); } function switchHost() { @@ -681,14 +694,27 @@ export function createWeatherRendering({ switchHost(); restaging = null; const target = active === 'shell' ? shellFor(snapshot.product) : globe; + const before = refreshed; const ok = await target.setFrame(snapshot, time, options); - if (ok) - shown = { snapshot, time, infrared: options.infrared ?? 'filtered' }; + if (ok) { + // Keep a refresh that arrived while this frame loaded. + const latest = + refreshed !== before && refreshed?.product === snapshot.product + ? refreshed + : undefined; + shown = { + snapshot, + time, + infrared: options.infrared ?? 'filtered', + ...(latest ? { latest } : {}), + }; + } return ok; }, // The shown frame keeps its staged snapshot for a restage on another // host; only the refreshed credit is remembered and applied. updateSnapshot(snapshot) { + refreshed = snapshot; if (shown?.snapshot.product === snapshot.product) shown = { ...shown, latest: snapshot }; globe.updateSnapshot(snapshot); @@ -705,6 +731,7 @@ export function createWeatherRendering({ clear() { restaging = null; shown = null; + refreshed = null; hidden = false; shell?.clear(); shell = null;