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;