mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
Merge meirm/i18n-spanish: air-quality layer (AQHI), bikeshare Toronto, Panoramax count line
Merges cleanly; weatherPanel.test.mjs arrived unformatted and is run through prettier so format:check passes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
b68f09793e
61 files changed
+3209
-178
No files matched your search
@@ -90,6 +90,7 @@ const PANEL_GROUPS = [
|
||||
{
|
||||
label: msg('Weather'),
|
||||
ids: [
|
||||
'air-quality',
|
||||
'wind',
|
||||
'weather-radar',
|
||||
'weather-satellite',
|
||||
@@ -569,6 +570,7 @@ export class LayerPanel {
|
||||
'weather-satellite',
|
||||
'weather-lightning',
|
||||
'weather-cyclones',
|
||||
'air-quality',
|
||||
].includes(layer.id),
|
||||
)
|
||||
.map((layer) => ({
|
||||
|
||||
@@ -329,3 +329,31 @@ test('the unavailable chip may abbreviate, but its accessible name keeps the ful
|
||||
assert.equal(button.dataset.feedState, 'unavailable');
|
||||
assert.match(attrs.get('aria-label'), /^Active Fires: UNAVAILABLE\./);
|
||||
});
|
||||
|
||||
test('the Weather readout receives enabled air-quality alongside weather layers', async () => {
|
||||
const { LayerPanel } = await import('./layerPanel.js');
|
||||
const layers = ['wind', 'air-quality', 'other'].map((id) => ({
|
||||
id,
|
||||
icon: '*',
|
||||
enabled: true,
|
||||
}));
|
||||
layers.push({ id: 'weather-radar', icon: '*', enabled: false });
|
||||
const panel = new LayerPanel({
|
||||
getLayers: () => layers,
|
||||
getRowControls: (id) => ({ readout: true, summary: { label: id } }),
|
||||
});
|
||||
let entries = null;
|
||||
panel._weatherPanel = {
|
||||
update: (next) => {
|
||||
entries = next;
|
||||
},
|
||||
};
|
||||
panel._refreshWeatherPanel();
|
||||
assert.deepEqual(
|
||||
entries.map(({ id, summary }) => [id, summary.label]),
|
||||
[
|
||||
['wind', 'wind'],
|
||||
['air-quality', 'air-quality'],
|
||||
],
|
||||
);
|
||||
});
|
||||
@@ -257,29 +257,45 @@ function presentMeta(state) {
|
||||
);
|
||||
const hint = translateHint(state.coverage.hint);
|
||||
if (state.coverage.count > 0) {
|
||||
// A provider whose lines are not sequences carries its wording as data
|
||||
// (coverageLabel); its count line needs its own tn pair so every language
|
||||
// gets real plural forms. New wording: add a pair here and to the catalogs.
|
||||
const count = state.coverage.count;
|
||||
const countLine =
|
||||
state.coverage.label?.noun === 'streets'
|
||||
? tn(
|
||||
count,
|
||||
'{n} street in view · click a line for its nearest photo',
|
||||
'{n} streets in view · click a line for its nearest photo',
|
||||
{ n: formatNumber(count) },
|
||||
)
|
||||
: tn(
|
||||
count,
|
||||
'{n} sequence in view · click a line for its photos',
|
||||
'{n} sequences in view · click a line for its photos',
|
||||
{ n: formatNumber(count) },
|
||||
);
|
||||
const countLine = coverageCountLine(state.coverage);
|
||||
return [countLine, hint].filter(Boolean).join(' · ');
|
||||
}
|
||||
return hint;
|
||||
}
|
||||
|
||||
/**
|
||||
* The coverage count line. Providers carry their wording as data
|
||||
* (coverageLabel noun/action); each combination needs its own literal tn
|
||||
* pair so every language gets real plural forms — the noun cannot inflect
|
||||
* through a placeholder in Russian. New wording: add a pair here and to the
|
||||
* catalogs (es.json/ru.json).
|
||||
* @param {{count: number, label?: {noun?: string, action?: string}|null}} coverage
|
||||
*/
|
||||
function coverageCountLine(coverage) {
|
||||
const count = coverage.count;
|
||||
const values = { n: formatNumber(count) };
|
||||
if (coverage.label?.action === 'click a line for its nearest photo')
|
||||
return coverage.label.noun === 'streets'
|
||||
? tn(
|
||||
count,
|
||||
'{n} street in view · click a line for its nearest photo',
|
||||
'{n} streets in view · click a line for its nearest photo',
|
||||
values,
|
||||
)
|
||||
: tn(
|
||||
count,
|
||||
'{n} sequence in view · click a line for its nearest photo',
|
||||
'{n} sequences in view · click a line for its nearest photo',
|
||||
values,
|
||||
);
|
||||
return tn(
|
||||
count,
|
||||
'{n} sequence in view · click a line for its photos',
|
||||
'{n} sequences in view · click a line for its photos',
|
||||
values,
|
||||
);
|
||||
}
|
||||
|
||||
/** @param {{now?: number}} [options] Clock for the SINCE readout. */
|
||||
export function presentStreetLevelPanel(state, { now = Date.now() } = {}) {
|
||||
const enabled = state.enabled === true;
|
||||
|
||||
@@ -11,8 +11,9 @@ const ORDER = [
|
||||
'weather-radar',
|
||||
'weather-satellite',
|
||||
'weather-lightning',
|
||||
'air-quality',
|
||||
];
|
||||
const OBSERVED = new Set(ORDER.slice(2));
|
||||
const OBSERVED = new Set(ORDER.slice(2, 5));
|
||||
const utc = (time) =>
|
||||
Number.isFinite(Date.parse(time))
|
||||
? `${formatUtcDayMonth(time)} ${utcClock(time)}`
|
||||
@@ -166,7 +167,10 @@ export function createWeatherPanel({
|
||||
? t('LATEST · newest per product')
|
||||
: historyTime(state.target),
|
||||
});
|
||||
const models = active.map(({ id, icon, summary, legend = [], list }) => {
|
||||
const models = active.map((entry) => {
|
||||
const { id, icon, summary, legend = [], list } = entry;
|
||||
// Discrete classes (storm geometry, AQHI bands) list swatches, not a ramp.
|
||||
const categorical = entry.legendCategorical === true;
|
||||
let detail = summary.detail;
|
||||
const product = state.products.find((item) => item.id === id);
|
||||
if (OBSERVED.has(id)) {
|
||||
@@ -207,17 +211,15 @@ export function createWeatherPanel({
|
||||
id: 'legend',
|
||||
type: 'legend',
|
||||
legend: {
|
||||
categorical: id === 'weather-cyclones',
|
||||
categorical,
|
||||
colors: legend.map(({ color }) => color),
|
||||
labels: legend.map(({ label }) => label),
|
||||
units: id === 'weather-cyclones' ? '' : summary.units,
|
||||
units: categorical ? '' : summary.units,
|
||||
zeroIndex: legend.findIndex(({ label }) => label === '0'),
|
||||
},
|
||||
};
|
||||
const blocks = [
|
||||
...(id === 'weather-cyclones' && list?.items?.length
|
||||
? [{ id: 'storms', type: 'list', list }]
|
||||
: []),
|
||||
...(list?.items?.length ? [{ id: 'list', type: 'list', list }] : []),
|
||||
{ id: 'details', type: 'lines', lines },
|
||||
...(legend.length ? [legendBlock] : []),
|
||||
...(summary.settings?.length
|
||||
|
||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
||||
import { setLocale } from '../i18n/index.js';
|
||||
import { createWeatherPanel } from './weatherPanel.js';
|
||||
import { railFixture } from './railTestFixture.mjs';
|
||||
import { createAirQualityLayer } from '../layers/airQuality/index.js';
|
||||
const ticks = ['2026-09-22T01:00:00Z', '2026-09-22T01:05:00Z'];
|
||||
const wind = {
|
||||
id: 'wind',
|
||||
@@ -167,6 +168,7 @@ test('cyclone readout retains advisory, position, intensity, geometry, coverage,
|
||||
},
|
||||
],
|
||||
},
|
||||
legendCategorical: true,
|
||||
legend: [
|
||||
{ label: 'Advisory center / forecast track', color: '#7fe6ed' },
|
||||
{ label: 'Center-track uncertainty cone', color: '#7fe6ed44' },
|
||||
@@ -411,7 +413,7 @@ test('cyclones lead, and observed history owns one bordered group with only acti
|
||||
assert.deepEqual(opened(f), ['weather-cyclones']);
|
||||
assert.equal(
|
||||
card(f, cyclone.id).children[1].children[0].dataset.blockId,
|
||||
'storms',
|
||||
'list',
|
||||
);
|
||||
view.update([wind, satellite]);
|
||||
assert.equal(group.children[1].textContent, 'Satellite clouds');
|
||||
@@ -686,3 +688,67 @@ test('history time keeps the hour whatever the length of the localized date (K-6
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('air-quality readout renders its band legend, Canada coverage and worst-first list', async () => {
|
||||
const observedMs = Date.now() - 10 * 60_000;
|
||||
const reading = (stationId, value, band, name) => ({
|
||||
id: `eccc:${stationId}`,
|
||||
provider: 'eccc',
|
||||
scale: 'AQHI',
|
||||
value,
|
||||
band,
|
||||
stationId,
|
||||
name,
|
||||
zone: 'on',
|
||||
lat: 43.65,
|
||||
lon: -79.38,
|
||||
observedMs,
|
||||
color: '#4cc9f0',
|
||||
label: String(value),
|
||||
risk: `${band} risk`,
|
||||
note: null,
|
||||
});
|
||||
const layer = createAirQualityLayer({
|
||||
source: {
|
||||
getSnapshot: async () => [
|
||||
reading('B', 8, 'high', 'Hamilton'),
|
||||
reading('A', 2, 'low', 'Toronto'),
|
||||
],
|
||||
},
|
||||
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
|
||||
});
|
||||
layer.init({ dataSources: { add() {}, remove() {} } });
|
||||
layer.enable();
|
||||
await layer.update();
|
||||
const f = fixture();
|
||||
const view = createWeatherPanel(f);
|
||||
view.update([{ id: layer.id, icon: layer.icon, ...layer.getRowControls() }]);
|
||||
const aq = card(f, 'air-quality');
|
||||
assert.ok(aq, 'the air-quality card is shown');
|
||||
assert.equal(aq.dataset.open, 'true');
|
||||
assert.match(
|
||||
f.find((n) => n.classList?.contains?.('rail-card-badge'), aq).textContent,
|
||||
/Canada only/,
|
||||
);
|
||||
assert.match(line(f, 'air-quality', 'status').textContent, /2 stations/);
|
||||
const entries = f.find((n) => n.className === 'rail-card-legend-entries', aq);
|
||||
assert.equal(entries.hidden, false);
|
||||
assert.deepEqual(
|
||||
entries.children.map((entry) => entry.children[1].textContent),
|
||||
['Low', 'Moderate', 'High', 'Very High'],
|
||||
);
|
||||
assert.equal(
|
||||
f.find((n) => n.className === 'rail-card-ramp', aq).hidden,
|
||||
true,
|
||||
);
|
||||
const list = f.find((n) => n.dataset.blockId === 'list', aq);
|
||||
assert.equal(list.hidden, false);
|
||||
assert.deepEqual(
|
||||
list.children.map((item) =>
|
||||
item.children[0].children.map((part) => part.textContent).join(' '),
|
||||
),
|
||||
['AQHI 8 Hamilton · high risk', 'AQHI 2 Toronto · low risk'],
|
||||
);
|
||||
view.destroy();
|
||||
layer.destroy();
|
||||
});
|
||||
Reference in new issue
Block a user