Merge pull request #1744 from bilawalsidhu/weather/source-names

A weather source can name its provider for each product
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-11-14 20:07:09 -08:00
commit bce737bf32
5 files changed
+77 -5

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
- A weather source can name its provider for each product with
`sourceName(kind)`; the Rain radar, Lightning density and Satellite clouds
rows show it until a manifest names its own, instead of always saying NOAA
nowCOAST (samehkhamis, #1744).
+6
View File
@@ -195,6 +195,12 @@ signal })` returns one tile or null for a transparent tile, each as an
`ImageBitmap`, `ImageData`, canvas or `VideoFrame` of exactly the requested
size, which the layer draws once and closes. Without them the layers read the
image and tile routes (`WeatherFrameFeed` in `src/layers/weather/source.js`).
It may also name its source with an optional `sourceName(kind)`, `kind` being
`'radar'`, `'lightning'` or `'satellite'`: the layer's row shows that name until
a manifest names its own, and NOAA nowCOAST when the source names none. The
name is read once, when the layer is built, and held to the same rule as a
manifest's `display.source`. The map credit still comes from each manifest's
`display.credit`, so a source that is not nowCOAST sets that too.
`application/services` accepts boundary, terrain, regional-context, weather and
summary services; its weather service supplies cockpit local conditions, not the
+9
View File
@@ -39,6 +39,15 @@ function text(value, max) {
: undefined;
}
/**
* A source name a weather feed supplies itself (`sourceName` on the feed),
* held to the same rule as a manifest's `display.source`; undefined when it
* is not a fit name.
*/
export function weatherSourceName(value) {
return text(value, TEXT_LIMITS.source);
}
/**
* Whether a label the legend formats as a number reads back exactly as given:
* within the safe integer range and with no digits lost to rounding, so
+21 -5
View File
@@ -2,7 +2,7 @@ import * as Cesium from 'cesium';
import { createWeatherRendering } from './rendering.js';
import { weatherFrameSource } from './source.js';
import { imageryHostStatus } from './imageryHost.js';
import { weatherDisplay, insideExtent } from './display.js';
import { weatherDisplay, weatherSourceName, insideExtent } from './display.js';
import {
RADAR_MAX_GAP_MS,
REGIONAL_INFRARED_MAX_GAP_MS,
@@ -55,7 +55,9 @@ const utc = (value) =>
/** A per-application observation layer, using the existing layer lifecycle and
* row controls. Its time is the world clock's, so a link made away from live
* opens its imagery at that time; one made live opens the latest. */
* opens its imagery at that time; one made live opens the latest. The feed
* may also name its source with `sourceName(kind)`, `kind` being 'radar',
* 'lightning' or 'satellite'; NOAA nowCOAST is named otherwise. */
export function createWeatherLayer({
feed,
clock,
@@ -72,6 +74,20 @@ export function createWeatherLayer({
const radar = id === 'weather-radar';
const lightning = id === 'weather-lightning';
const satellite = !radar && !lightning;
// A feed may name the source behind each product ('radar', 'lightning' or
// 'satellite'); its row then says so before a manifest names it. A name
// that fails, like a manifest's, leaves the built-in one.
let sourceName = null;
try {
sourceName =
weatherSourceName(
feed.sourceName?.(
radar ? 'radar' : lightning ? 'lightning' : 'satellite',
),
) ?? null;
} catch (cause) {
console.warn('[weather] source name unavailable:', cause);
}
let product = radar ? 'radar' : lightning ? 'lightning' : 'clouds-regional';
let opacity = 'strong';
let infrared = 100;
@@ -301,7 +317,7 @@ export function createWeatherLayer({
? 'Lightning density'
: 'Satellite clouds',
icon: radar ? '◉' : lightning ? 'ϟ' : '☁',
source: 'NOAA nowCOAST · OBSERVED',
source: sourceName ?? 'NOAA nowCOAST · OBSERVED',
updateInterval: lightning ? 600_000 : 120_000,
init(nextViewer) {
viewer = nextViewer;
@@ -870,10 +886,10 @@ export function createWeatherLayer({
loading,
error: error || rendering?.getDiagnostics().error || null,
stale: Boolean(manifest?.stale || observationDelayed()),
...(source === undefined
...(source === undefined && sourceName === null
? { source: 'NOAA nowCOAST' }
: // Source text is data: the panel shows it untranslated.
{ source, sourceVerbatim: true }),
{ source: source ?? sourceName, sourceVerbatim: true }),
observedAt: shownTime(),
};
},
+37
View File
@@ -2694,3 +2694,40 @@ test('the layer hands rendering a frame source only when its feed is one', () =>
assert.equal(typeof seen[1].getTile, 'function');
assert.equal(seen[1].getImage, undefined);
});
test('a feed names the source behind each product, before any manifest does', (t) => {
t.mock.method(console, 'warn', () => {});
const quiet = {
documentRef: target({ hidden: false }),
eventTarget: target(),
matchMedia: () => target({ matches: false }),
};
const asked = [];
const feed = {
getSnapshot: async () => snapshot,
sourceName(kind) {
asked.push(kind);
if (kind === 'satellite') throw new Error('no name');
return kind === 'radar' ? ' NOAA MRMS ' : 'NOAA\u200b GLM';
},
};
const radar = createWeatherLayer({ ...quiet, feed, id: 'weather-radar' });
assert.equal(radar.source, 'NOAA MRMS');
const stats = radar.getStats();
assert.equal(stats.source, 'NOAA MRMS');
assert.equal(stats.sourceVerbatim, true);
// A name a manifest could not carry, or a failing call, leaves nowCOAST, as
// does a feed with no names.
for (const id of ['weather-lightning', 'weather-satellite']) {
const layer = createWeatherLayer({ ...quiet, feed, id });
assert.equal(layer.source, 'NOAA nowCOAST · OBSERVED', id);
assert.equal(layer.getStats().source, 'NOAA nowCOAST', id);
assert.equal(layer.getStats().sourceVerbatim, undefined, id);
}
assert.deepEqual(asked, ['radar', 'lightning', 'satellite']);
const unnamed = createWeatherLayer({
...quiet,
feed: { getSnapshot: async () => snapshot },
});
assert.equal(unnamed.source, 'NOAA nowCOAST · OBSERVED');
});