fix(cyclones): accept advisories NHC publishes before their nominal time

NHC stamps an advisory with its nominal time and writes it about half an
hour early, so the 5-minute future limit rejected the whole feed each
advisory cycle and the layer showed no storms. Allow 90 minutes ahead.
This commit is contained in:
Bilawal Sidhu committed 2026-09-28 20:47:06 -05:00
1 parent e7707d9a0f
commit 9c15f093a1
3 files changed
+18 -1

No files matched your search

+5
View File
@@ -1,5 +1,10 @@
# Changelog # Changelog
- Cyclone advisories no longer go dark while NHC publishes an advisory ahead
of its nominal time: advisory and position times up to 90 minutes ahead of
the server clock are accepted (previously 5 minutes), so the whole feed is
not rejected for about half an hour each advisory cycle.
- Public Overpass instances are no longer used by default. Street Traffic - Public Overpass instances are no longer used by default. Street Traffic
roads come from TomTom flow tiles, OpenFreeMap vector tiles, or both, chosen roads come from TomTom flow tiles, OpenFreeMap vector tiles, or both, chosen
on the layer row (TomTom / OSM / Hybrid) or with `?trafficRoads=`. With a on the layer row (TomTom / OSM / Hybrid) or with `?trafficRoads=`. With a
+4 -1
View File
@@ -41,6 +41,9 @@ function number(value, min, max) {
const n = Number(value); const n = Number(value);
return Number.isFinite(n) && n >= min && n <= max ? n : null; return Number.isFinite(n) && n >= min && n <= max ? n : null;
} }
// NHC stamps an advisory with its nominal time (e.g. 03:00Z) and publishes it
// up to about half an hour before; allow that, but not a clock far ahead.
const EARLY_ADVISORY_MS = 90 * 60_000;
function iso(value, now) { function iso(value, now) {
if ( if (
typeof value !== 'string' || typeof value !== 'string' ||
@@ -50,7 +53,7 @@ function iso(value, now) {
const n = Date.parse(value); const n = Date.parse(value);
if ( if (
!Number.isFinite(n) || !Number.isFinite(n) ||
n > now + 5 * 60_000 || n > now + EARLY_ADVISORY_MS ||
now - n > 12 * HOUR || now - n > 12 * HOUR ||
new Date(n).toISOString().replace('.000Z', 'Z') !== new Date(n).toISOString().replace('.000Z', 'Z') !==
value.replace('.000Z', 'Z') value.replace('.000Z', 'Z')
+9
View File
@@ -162,6 +162,15 @@ test('malformed, duplicate, oversized, future and expired status is rejected rat
assert.deepEqual(parseCycloneStatus({ activeStorms: [] }, NOW), []); assert.deepEqual(parseCycloneStatus({ activeStorms: [] }, NOW), []);
}); });
test('advisories published before their nominal time are accepted, far-future ones are not', () => {
// NHC wrote the 03:00Z advisory at 02:34Z.
const early = Date.UTC(2026, 8, 16, 2, 34);
assert.equal(parseCycloneStatus(status(), early)[0].issuedAt, TIME);
assert.throws(() =>
parseCycloneStatus(status(), Date.UTC(2026, 8, 16, 1, 25)),
);
});
test('coherent geometry is retained with holes/dateline while older advisory geometry is omitted', () => { test('coherent geometry is retained with holes/dateline while older advisory geometry is omitted', () => {
const storms = parseCycloneStatus(status(), NOW); const storms = parseCycloneStatus(status(), NOW);
const parts = collections(); const parts = collections();