mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
Merge pull request #741 from bilawalsidhu/fix/cyclone-advisory-time
fix(cyclones): accept advisories NHC publishes before their nominal time
This commit is contained in:
4 files changed
+53
-2
No files matched your search
@@ -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.
|
||||||
|
|
||||||
- Flights age out vanished aircraft again. OpenSky's routine position-less rows
|
- Flights age out vanished aircraft again. OpenSky's routine position-less rows
|
||||||
no longer mark every snapshot partial, which had kept vanished and landed
|
no longer mark every snapshot partial, which had kept vanished and landed
|
||||||
aircraft at full opacity for up to five minutes; those rows retain only their
|
aircraft at full opacity for up to five minutes; those rows retain only their
|
||||||
|
|||||||
@@ -339,7 +339,12 @@ Cyclones combine fixed NHC/CPHC status and summary GIS endpoints through a bound
|
|||||||
same-origin provider. The status endpoint lacks browser CORS; each installation
|
same-origin provider. The status endpoint lacks browser CORS; each installation
|
||||||
fetches keyless public sources at runtime, with no centralized ingestion service.
|
fetches keyless public sources at runtime, with no centralized ingestion service.
|
||||||
The five-minute snapshot covers Atlantic and eastern/central North Pacific only.
|
The five-minute snapshot covers Atlantic and eastern/central North Pacific only.
|
||||||
Current positions, advisory issue time and position time stay distinct. Tracks,
|
Current positions, advisory issue time and position time stay distinct. NHC may
|
||||||
|
release an advisory before its nominal time (routinely up to about 30 minutes,
|
||||||
|
rarely about an hour), so advisory issue and position times up to and including
|
||||||
|
90 minutes ahead of the server clock are accepted; 90 minutes is this app's
|
||||||
|
tolerance, not an NHC limit. Later, non-canonical, or more than 12-hour-old
|
||||||
|
times still reject the status snapshot. Tracks,
|
||||||
forecast lead-hour points and cones render only when all source parts match the
|
forecast lead-hour points and cones render only when all source parts match the
|
||||||
status advisory; a newer status shows its position with geometry pending rather
|
status advisory; a newer status shows its position with geometry pending rather
|
||||||
than relabeling older geometry. The cone represents forecast center uncertainty,
|
than relabeling older geometry. The cone represents forecast center uncertainty,
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -162,6 +162,44 @@ 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)),
|
||||||
|
/invalid_cyclone_data/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('advisory and position times are bounded at 90 minutes ahead and 12 hours old', () => {
|
||||||
|
const MINUTE = 60_000;
|
||||||
|
const at = (offsetMs) => new Date(NOW + offsetMs).toISOString();
|
||||||
|
const fields = {
|
||||||
|
issuedAt: (value) =>
|
||||||
|
status({
|
||||||
|
forecastAdvisory: {
|
||||||
|
advNum: '010',
|
||||||
|
issuance: value,
|
||||||
|
url: 'https://www.nhc.noaa.gov/text/MIATCMEP5.shtml',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
positionAt: (value) => status({ lastUpdate: value }),
|
||||||
|
};
|
||||||
|
for (const [field, payload] of Object.entries(fields)) {
|
||||||
|
for (const offset of [90 * MINUTE, -12 * 60 * MINUTE]) {
|
||||||
|
const [storm] = parseCycloneStatus(payload(at(offset)), NOW);
|
||||||
|
assert.equal(storm[field], at(offset), `${field} ${offset}`);
|
||||||
|
}
|
||||||
|
for (const offset of [90 * MINUTE + 1, -12 * 60 * MINUTE - 1])
|
||||||
|
assert.throws(
|
||||||
|
() => parseCycloneStatus(payload(at(offset)), NOW),
|
||||||
|
/invalid_cyclone_data/,
|
||||||
|
`${field} ${offset}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
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();
|
||||||
|
|||||||
Reference in new issue
Block a user