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 `ImageBitmap`, `ImageData`, canvas or `VideoFrame` of exactly the requested
size, which the layer draws once and closes. Without them the layers read the 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`). 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 `application/services` accepts boundary, terrain, regional-context, weather and
summary services; its weather service supplies cockpit local conditions, not the summary services; its weather service supplies cockpit local conditions, not the
+9
View File
@@ -39,6 +39,15 @@ function text(value, max) {
: undefined; : 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: * 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 * 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 { createWeatherRendering } from './rendering.js';
import { weatherFrameSource } from './source.js'; import { weatherFrameSource } from './source.js';
import { imageryHostStatus } from './imageryHost.js'; import { imageryHostStatus } from './imageryHost.js';
import { weatherDisplay, insideExtent } from './display.js'; import { weatherDisplay, weatherSourceName, insideExtent } from './display.js';
import { import {
RADAR_MAX_GAP_MS, RADAR_MAX_GAP_MS,
REGIONAL_INFRARED_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 /** 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 * 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({ export function createWeatherLayer({
feed, feed,
clock, clock,
@@ -72,6 +74,20 @@ export function createWeatherLayer({
const radar = id === 'weather-radar'; const radar = id === 'weather-radar';
const lightning = id === 'weather-lightning'; const lightning = id === 'weather-lightning';
const satellite = !radar && !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 product = radar ? 'radar' : lightning ? 'lightning' : 'clouds-regional';
let opacity = 'strong'; let opacity = 'strong';
let infrared = 100; let infrared = 100;
@@ -301,7 +317,7 @@ export function createWeatherLayer({
? 'Lightning density' ? 'Lightning density'
: 'Satellite clouds', : 'Satellite clouds',
icon: radar ? '◉' : lightning ? 'ϟ' : '☁', icon: radar ? '◉' : lightning ? 'ϟ' : '☁',
source: 'NOAA nowCOAST · OBSERVED', source: sourceName ?? 'NOAA nowCOAST · OBSERVED',
updateInterval: lightning ? 600_000 : 120_000, updateInterval: lightning ? 600_000 : 120_000,
init(nextViewer) { init(nextViewer) {
viewer = nextViewer; viewer = nextViewer;
@@ -870,10 +886,10 @@ export function createWeatherLayer({
loading, loading,
error: error || rendering?.getDiagnostics().error || null, error: error || rendering?.getDiagnostics().error || null,
stale: Boolean(manifest?.stale || observationDelayed()), stale: Boolean(manifest?.stale || observationDelayed()),
...(source === undefined ...(source === undefined && sourceName === null
? { source: 'NOAA nowCOAST' } ? { source: 'NOAA nowCOAST' }
: // Source text is data: the panel shows it untranslated. : // Source text is data: the panel shows it untranslated.
{ source, sourceVerbatim: true }), { source: source ?? sourceName, sourceVerbatim: true }),
observedAt: shownTime(), 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(typeof seen[1].getTile, 'function');
assert.equal(seen[1].getImage, undefined); 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');
});