Let a weather source describe its own products

A weather source can describe its own products (credit, units, coverage,
legend) through an optional `display` object in each product manifest:
source, credit, units, coverage, info, legend and extent. The weather
layer uses each valid field in the row summary, legend, info text,
coverage navigation, outside-coverage status, layer stats and imagery
credit, and keeps today's built-in value for any field that is absent or
invalid. Units and legend describe one scale, so they apply only as a
valid pair; otherwise both keep their built-in values, and the radar
info heading names the units in use.

Text is single-line and length-capped. It rejects control characters
(U+0000-U+001F, U+007F-U+009F), line and paragraph separators, and
invisible or format characters: soft hyphen (U+00AD), combining grapheme
joiner (U+034F), Arabic letter mark (U+061C), Mongolian vowel separator
(U+180E), zero-width and direction marks (U+200B-U+200F), bidi embedding
and override (U+202A-U+202E), word joiner, invisible operators and bidi
isolates (U+2060-U+2069), variation selectors (U+FE00-U+FE0F), the
byte-order mark (U+FEFF) and tag characters (U+E0000-U+E007F).

Legends need 2-16 stops whose labels are plain decimal numbers or short
text and whose colours are #rrggbb. A number label must read back
exactly as the legend shows it: within the safe integer range, with no
leading zeros and no digits lost to rounding. Extents must be finite, in
range and non-empty. Supplied source and units text is data: the layer
marks it so the panels show it untranslated, while built-in values are
translated as before.

The imagery credit is escaped as plain text and follows a refreshed
manifest even when the shown frame stays the same. A refresh changes
only the credit: the shown frame keeps the snapshot it was staged from,
so a later host switch restages it with its own time and bounds and the
refreshed credit. Manifests without `display`, including the bundled
nowCOAST ones, render exactly as before.
This commit is contained in:
Sameh Khamis committed 2026-10-31 22:42:01 -07:00
1 parent 3cd7d0fe15
commit 039b0828b7
23 files changed
+1243 -63

No files matched your search

+5 -1
View File
@@ -635,7 +635,11 @@ export class LayerPanel {
const stats = layer.stats || {};
const feedState = layerFeedState(stats);
const stateLabel = t(FEED_STATE_LABEL_KEYS[feedState]);
const source = sourceLabel(stats.source || layer.source);
// A layer marks source text that is data rather than a catalog name.
const source =
stats.sourceVerbatim === true && stats.source
? stats.source
: sourceLabel(stats.source || layer.source);
const lifecycleState =
layer.lifecycleState || (layer.enabled ? 'enabled' : 'disabled');
if (lifecycleState === 'enabling' || lifecycleState === 'disabling') {
+18
View File
@@ -87,3 +87,21 @@ for (const locale of ['es', 'ru']) {
}
});
}
test('a source a layer marks as data is shown untranslated in the row', () => {
setLocale('ru');
try {
const row = (stats) =>
LayerPanel.prototype._buildMetaText({
id: 'weather-lightning',
enabled: true,
stats: { count: 1, lastUpdate: Date.now(), ...stats },
});
assert.match(row({ source: 'Local ADS-B' }), /Локальный ADS-B/);
const verbatim = row({ source: 'Local ADS-B', sourceVerbatim: true });
assert.match(verbatim, /Local ADS-B/);
assert.doesNotMatch(verbatim, /Локальный/);
} finally {
setLocale('en');
}
});
+10 -4
View File
@@ -16,9 +16,10 @@ const order = (parent, nodes) =>
parent.insertBefore(node, parent.children[i] || null);
});
// "960 — 1050+ гПа": the unit stays with the last number.
const scaleText = (low, high, units) => {
const unit = unitLabel(units);
// "960 — 1050+ гПа": the unit stays with the last number. Units a source
// supplied as data are shown as given.
const scaleText = (low, high, units, verbatim) => {
const unit = verbatim ? units || '' : unitLabel(units);
const range = `${formatScaleLabel(low)} — ${formatScaleLabel(high)}`;
return unit ? `${range}\u00a0${unit}` : range;
};
@@ -309,7 +310,12 @@ export function createRailCardBlocks({ container, cardId, onParams }) {
block.scale,
'textContent',
visible
? scaleText(legend.labels[0], legend.labels.at(-1), legend.units)
? scaleText(
legend.labels[0],
legend.labels.at(-1),
legend.units,
legend.unitsVerbatim === true,
)
: '',
);
} else if (props.type === 'actions') {
+1
View File
@@ -215,6 +215,7 @@ export function createWeatherPanel({
colors: legend.map(({ color }) => color),
labels: legend.map(({ label }) => label),
units: categorical ? '' : summary.units,
...(summary.unitsVerbatim === true ? { unitsVerbatim: true } : {}),
zeroIndex: legend.findIndex(({ label }) => label === '0'),
},
};
+24
View File
@@ -752,3 +752,27 @@ test('air-quality readout renders its band legend, Canada coverage and worst-fir
view.destroy();
layer.destroy();
});
test('source-supplied legend units are shown as given; catalog units are translated', () => {
setLocale('ru');
try {
const f = fixture();
const view = createWeatherPanel(f);
const row = (summary) => ({
...radar,
summary: { ...radar.summary, units: 'km/h', ...summary },
legend: [
{ label: '1', color: '#000000' },
{ label: '2', color: '#ffffff' },
],
});
const scale = () =>
f.find((n) => n.className === 'rail-card-scale').textContent;
view.update([row()]);
assert.equal(scale(), '1 — 2 км/ч');
view.update([row({ unitsVerbatim: true })]);
assert.equal(scale(), '1 — 2 km/h');
view.destroy();
} finally {
setLocale('en');
}
});