@aguacerowx/mapsgl 0.0.43 → 0.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/NwsWatchesWarningsOverlay.js +17 -0
- package/src/WeatherLayerManager.js +11 -3
- package/src/nwsAlertsSupport.js +184 -36
package/package.json
CHANGED
|
@@ -308,6 +308,23 @@ export class NwsWatchesWarningsOverlay {
|
|
|
308
308
|
: timeMode.timelineUnix;
|
|
309
309
|
const useOnsetWindow = this._nwsAlertSettings.activeOnlyRealtime === true;
|
|
310
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Weather-timeline alignment (MRMS / satellite / NEXRAD scrubber): without a valid reference
|
|
313
|
+
* time we must not show every alert at full opacity — previously `isActive` defaulted to
|
|
314
|
+
* true for all features when `referenceUnix` was still null (race before first timestamp).
|
|
315
|
+
*/
|
|
316
|
+
if (!timeMode.useRealtime && (referenceUnix == null || !Number.isFinite(referenceUnix))) {
|
|
317
|
+
for (const f of this._workingFc.features ?? []) {
|
|
318
|
+
if (f.id == null) continue;
|
|
319
|
+
try {
|
|
320
|
+
m.setFeatureState({ source: this._sourceId, id: f.id }, { active: false });
|
|
321
|
+
} catch {
|
|
322
|
+
/* ignore */
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
311
328
|
for (const f of this._workingFc.features ?? []) {
|
|
312
329
|
if (f.id == null) continue;
|
|
313
330
|
let isActive = true;
|
|
@@ -1007,7 +1007,11 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1007
1007
|
return;
|
|
1008
1008
|
}
|
|
1009
1009
|
|
|
1010
|
-
|
|
1010
|
+
// Number(null) === 0, which is falsy and causes _loadGridData to return null immediately
|
|
1011
|
+
// (the core guards with `if (!mrmsTimestamp) return null`). Treat null as NaN so that
|
|
1012
|
+
// primaryTimeForRebuild falls through to times[0] — the first real MRMS timestamp.
|
|
1013
|
+
const _rawFrameTime = state.isMRMS ? state.mrmsTimestamp : state.forecastHour;
|
|
1014
|
+
const currentFrameTime = _rawFrameTime == null ? NaN : Number(_rawFrameTime);
|
|
1011
1015
|
/** When the active timestep is unset/NaN, paint the first timeline step (legacy single-fetch behavior). */
|
|
1012
1016
|
let primaryTimeForRebuild = Number.NaN;
|
|
1013
1017
|
if (mode === 'rebuild') {
|
|
@@ -1180,7 +1184,10 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1180
1184
|
this._initialGridLoadPending = true;
|
|
1181
1185
|
|
|
1182
1186
|
const normalized = this._collectNormalizedTimelineSteps(state);
|
|
1183
|
-
|
|
1187
|
+
// Same null-guard as in _runParallelGridFrameLoads: Number(null) === 0, which is a finite
|
|
1188
|
+
// value that would inject Unix epoch into timesToLoad and mis-identify the primary frame.
|
|
1189
|
+
const _rawCft = state.isMRMS ? state.mrmsTimestamp : state.forecastHour;
|
|
1190
|
+
const currentFrameTime = _rawCft == null ? NaN : Number(_rawCft);
|
|
1184
1191
|
const timesSet = new Set(normalized);
|
|
1185
1192
|
if (!Number.isNaN(currentFrameTime)) {
|
|
1186
1193
|
timesSet.add(currentFrameTime);
|
|
@@ -1219,7 +1226,8 @@ export class WeatherLayerManager extends EventEmitter {
|
|
|
1219
1226
|
|
|
1220
1227
|
_preloadAllTimeSteps(state) {
|
|
1221
1228
|
const normalized = this._collectNormalizedTimelineSteps(state);
|
|
1222
|
-
const
|
|
1229
|
+
const _rawPft = state.isMRMS ? state.mrmsTimestamp : state.forecastHour;
|
|
1230
|
+
const currentFrameTime = _rawPft == null ? NaN : Number(_rawPft);
|
|
1223
1231
|
const stepsToPreload = normalized.filter(t => t !== currentFrameTime);
|
|
1224
1232
|
|
|
1225
1233
|
this._debugLog('_preloadAllTimeSteps', {
|
package/src/nwsAlertsSupport.js
CHANGED
|
@@ -323,8 +323,43 @@ export function isNwwsTrackerStatusTerminal(status) {
|
|
|
323
323
|
return status === 'cancelled' || status === 'expired';
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
/**
|
|
327
|
+
* CAP references / explicit replace fields for the previous bulletin(s) in the same event chain.
|
|
328
|
+
* Used so extensions/upgrades/continuations remove polygons keyed by superseded ids, not only by
|
|
329
|
+
* the new feature's `base_alert_id`.
|
|
330
|
+
*/
|
|
331
|
+
function collectNwwsReplacementReferenceIds(properties) {
|
|
332
|
+
if (!properties || typeof properties !== 'object') return [];
|
|
333
|
+
const out = [];
|
|
334
|
+
const pushRaw = (v) => {
|
|
335
|
+
if (v == null) return;
|
|
336
|
+
if (Array.isArray(v)) {
|
|
337
|
+
for (const x of v) pushRaw(x);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const n = normalizeNwwsAlertId(v);
|
|
341
|
+
if (n) out.push(n);
|
|
342
|
+
};
|
|
343
|
+
pushRaw(properties._upgradedFrom ?? properties.upgradedFrom);
|
|
344
|
+
pushRaw(properties._replaces ?? properties.replaces ?? properties.replaced_alert_id ?? properties.replacedAlertId);
|
|
345
|
+
let refs = properties.references;
|
|
346
|
+
if (typeof refs === 'string') {
|
|
347
|
+
try {
|
|
348
|
+
refs = JSON.parse(refs);
|
|
349
|
+
} catch {
|
|
350
|
+
refs = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (Array.isArray(refs)) {
|
|
354
|
+
for (const r of refs) {
|
|
355
|
+
if (typeof r === 'string') pushRaw(r);
|
|
356
|
+
else if (r && typeof r === 'object') {
|
|
357
|
+
const ro = r;
|
|
358
|
+
pushRaw(ro.identifier ?? ro.id ?? ro.alert_id ?? ro.alertId ?? ro.base_alert_id);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
328
363
|
}
|
|
329
364
|
|
|
330
365
|
export function getNwsBaseAlertIdFromFeature(feature) {
|
|
@@ -369,20 +404,60 @@ export function applyNwwsDeltaToCollection(data, delta) {
|
|
|
369
404
|
const expireSet = new Set(delta.expireIds);
|
|
370
405
|
const removeIds = new Set([...expireSet, ...cancelSet]);
|
|
371
406
|
|
|
407
|
+
for (const feat of delta.upserts) {
|
|
408
|
+
if (!feat || feat.type !== 'Feature') continue;
|
|
409
|
+
const norm = normalizeNwwsHttpAlertFeature(
|
|
410
|
+
{
|
|
411
|
+
type: 'Feature',
|
|
412
|
+
id: feat.id,
|
|
413
|
+
geometry: feat.geometry,
|
|
414
|
+
properties: { ...(feat.properties ?? {}) },
|
|
415
|
+
},
|
|
416
|
+
true
|
|
417
|
+
);
|
|
418
|
+
for (const refId of collectNwwsReplacementReferenceIds(norm.properties)) {
|
|
419
|
+
removeIds.add(refId);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
372
423
|
const replacedBaseIds = new Set();
|
|
424
|
+
// Secondary: VTEC-derived stable IDs from upserts. Used as a fallback when base_alert_id
|
|
425
|
+
// differs between the stored feature and the update (e.g. when the content hash changed but
|
|
426
|
+
// the VTEC event number did not). This handles sessions where old hash-keyed features already
|
|
427
|
+
// exist in the working collection when an update with a VTEC-keyed ID arrives.
|
|
428
|
+
const replacedVtecIds = new Set();
|
|
373
429
|
for (const feat of delta.upserts) {
|
|
374
430
|
if (!feat || feat.type !== 'Feature') continue;
|
|
375
|
-
const
|
|
431
|
+
const norm = normalizeNwwsHttpAlertFeature(
|
|
432
|
+
{
|
|
433
|
+
type: 'Feature',
|
|
434
|
+
id: feat.id,
|
|
435
|
+
geometry: feat.geometry,
|
|
436
|
+
properties: { ...(feat.properties ?? {}) },
|
|
437
|
+
},
|
|
438
|
+
true
|
|
439
|
+
);
|
|
440
|
+
const baseId = getNwsBaseAlertIdFromFeature(norm);
|
|
376
441
|
if (baseId) replacedBaseIds.add(baseId);
|
|
442
|
+
const vtecId = extractNwsVtecStableId(norm.properties);
|
|
443
|
+
if (vtecId) replacedVtecIds.add(vtecId);
|
|
377
444
|
}
|
|
378
445
|
|
|
379
446
|
const clearedGeometryByInstance = new Map();
|
|
380
447
|
const clearedMapboxIdByInstance = new Map();
|
|
381
448
|
|
|
382
|
-
if (replacedBaseIds.size > 0) {
|
|
449
|
+
if (replacedBaseIds.size > 0 || replacedVtecIds.size > 0) {
|
|
383
450
|
features = features.filter((f) => {
|
|
384
451
|
const baseId = getNwsBaseAlertIdFromFeature(f);
|
|
385
|
-
|
|
452
|
+
const matchesPrimary = baseId != null && replacedBaseIds.has(baseId);
|
|
453
|
+
const matchesVtec =
|
|
454
|
+
!matchesPrimary &&
|
|
455
|
+
replacedVtecIds.size > 0 &&
|
|
456
|
+
(() => {
|
|
457
|
+
const vtecId = extractNwsVtecStableId(f.properties);
|
|
458
|
+
return vtecId != null && replacedVtecIds.has(vtecId);
|
|
459
|
+
})();
|
|
460
|
+
if (!matchesPrimary && !matchesVtec) return true;
|
|
386
461
|
const iid = getNwsAlertInstanceIdFromFeature(f) ?? baseId;
|
|
387
462
|
if (f.geometry) clearedGeometryByInstance.set(iid, f.geometry);
|
|
388
463
|
if (f.id != null) clearedMapboxIdByInstance.set(iid, f.id);
|
|
@@ -392,7 +467,16 @@ export function applyNwwsDeltaToCollection(data, delta) {
|
|
|
392
467
|
|
|
393
468
|
for (const feat of delta.upserts) {
|
|
394
469
|
if (!feat || feat.type !== 'Feature') continue;
|
|
395
|
-
const
|
|
470
|
+
const normalizedFeat = normalizeNwwsHttpAlertFeature(
|
|
471
|
+
{
|
|
472
|
+
type: 'Feature',
|
|
473
|
+
id: feat.id,
|
|
474
|
+
geometry: feat.geometry,
|
|
475
|
+
properties: { ...(feat.properties ?? {}) },
|
|
476
|
+
},
|
|
477
|
+
true
|
|
478
|
+
);
|
|
479
|
+
const enriched = enrichWarningsWithColors({ type: 'FeatureCollection', features: [normalizedFeat] }).features[0];
|
|
396
480
|
const trackerStatus = getNwwsTrackerStatusFromProperties(enriched?.properties);
|
|
397
481
|
if (isNwwsTrackerStatusTerminal(trackerStatus)) {
|
|
398
482
|
const terminalId = getNwsBaseAlertIdFromFeature(enriched);
|
|
@@ -456,37 +540,19 @@ export function parseNwwsWsDelta(raw) {
|
|
|
456
540
|
const cancelIds = [];
|
|
457
541
|
const expireIds = [];
|
|
458
542
|
|
|
459
|
-
const
|
|
543
|
+
const pushSupersededIdsFromFeature = (feature) => {
|
|
460
544
|
const status = getNwwsTrackerStatusFromProperties(feature?.properties);
|
|
461
|
-
if (
|
|
545
|
+
if (status === 'cancelled' || status === 'expired') return;
|
|
546
|
+
|
|
462
547
|
const p = feature?.properties ?? {};
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (directNorm) cancelIds.push(directNorm);
|
|
472
|
-
|
|
473
|
-
const refs = p.references ?? feature?.references;
|
|
474
|
-
if (Array.isArray(refs)) {
|
|
475
|
-
for (const r of refs) {
|
|
476
|
-
if (r == null) continue;
|
|
477
|
-
if (typeof r === 'string') {
|
|
478
|
-
const id = normalizeNwwsAlertId(r);
|
|
479
|
-
if (id) cancelIds.push(id);
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
if (typeof r === 'object') {
|
|
483
|
-
const ro = r;
|
|
484
|
-
const id = normalizeNwwsAlertId(
|
|
485
|
-
ro.identifier ?? ro.id ?? ro.alert_id ?? ro.alertId ?? ro.base_alert_id
|
|
486
|
-
);
|
|
487
|
-
if (id) cancelIds.push(id);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
548
|
+
const self = new Set();
|
|
549
|
+
const b = getNwsBaseAlertIdFromFeature(feature);
|
|
550
|
+
const inst = getNwsAlertInstanceIdFromFeature(feature);
|
|
551
|
+
if (b) self.add(b);
|
|
552
|
+
if (inst) self.add(inst);
|
|
553
|
+
|
|
554
|
+
for (const id of collectNwwsReplacementReferenceIds(p)) {
|
|
555
|
+
if (!self.has(id)) cancelIds.push(id);
|
|
490
556
|
}
|
|
491
557
|
};
|
|
492
558
|
|
|
@@ -508,7 +574,7 @@ export function parseNwwsWsDelta(raw) {
|
|
|
508
574
|
expireIds.push(baseId);
|
|
509
575
|
return;
|
|
510
576
|
}
|
|
511
|
-
|
|
577
|
+
pushSupersededIdsFromFeature(obj);
|
|
512
578
|
upserts.push(obj);
|
|
513
579
|
return;
|
|
514
580
|
}
|
|
@@ -693,6 +759,68 @@ export function filterToSdkAllowedEvents(fc) {
|
|
|
693
759
|
return filterToConvectiveSdkEvents(fc);
|
|
694
760
|
}
|
|
695
761
|
|
|
762
|
+
function extractRawNwwsVtecString(properties) {
|
|
763
|
+
if (!properties) return null;
|
|
764
|
+
if (properties.vtec != null) return String(properties.vtec);
|
|
765
|
+
if (properties.VTEC != null) return String(properties.VTEC);
|
|
766
|
+
const details = properties.details;
|
|
767
|
+
if (details?.pvtec != null) return String(details.pvtec);
|
|
768
|
+
const params = properties.parameters;
|
|
769
|
+
if (params != null && typeof params === 'object' && !Array.isArray(params)) {
|
|
770
|
+
const direct = params.VTEC ?? params.vtec;
|
|
771
|
+
if (direct != null) {
|
|
772
|
+
return Array.isArray(direct) ? String(direct[0] ?? '') : String(direct);
|
|
773
|
+
}
|
|
774
|
+
for (const k of Object.keys(params)) {
|
|
775
|
+
if (String(k).toUpperCase() === 'VTEC') {
|
|
776
|
+
const x = params[k];
|
|
777
|
+
if (x != null) return Array.isArray(x) ? String(x[0] ?? '') : String(x);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function normalizeNwsChainTimeComponent(v) {
|
|
785
|
+
if (v == null || v === '') return null;
|
|
786
|
+
if (typeof v === 'number' && Number.isFinite(v)) {
|
|
787
|
+
return new Date(v > 1e12 ? v : v * 1000).toISOString();
|
|
788
|
+
}
|
|
789
|
+
const s = String(v).trim();
|
|
790
|
+
const ms = Date.parse(s);
|
|
791
|
+
if (!Number.isNaN(ms)) return new Date(ms).toISOString();
|
|
792
|
+
return s;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function getNwsOfficeEventIssuedChainId(properties) {
|
|
796
|
+
if (!properties) return null;
|
|
797
|
+
const office = String(properties.office ?? properties.sender_icao ?? properties.sender ?? '').trim();
|
|
798
|
+
const ev = String(properties.event ?? properties.event_name ?? '').trim();
|
|
799
|
+
const rawIssued = getNwsTimeProp(properties, ['issued', 'issued_at']);
|
|
800
|
+
if (!office || !ev || rawIssued == null || rawIssued === '') return null;
|
|
801
|
+
const issuedNorm = normalizeNwsChainTimeComponent(rawIssued);
|
|
802
|
+
if (!issuedNorm) return null;
|
|
803
|
+
return `${office}|${ev}|${issuedNorm}`;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Extracts a stable VTEC-derived alert ID (e.g. "KWNS.SV.A.0108") from raw feature properties.
|
|
808
|
+
* The VTEC event number is stable across CON/EXT/CAN actions for the same watch/warning,
|
|
809
|
+
* unlike content-based hashes which change on every update.
|
|
810
|
+
*/
|
|
811
|
+
function extractNwsVtecStableId(properties) {
|
|
812
|
+
const vtec = extractRawNwwsVtecString(properties);
|
|
813
|
+
if (vtec == null || vtec === '') return null;
|
|
814
|
+
const parts = String(vtec).split('.');
|
|
815
|
+
if (parts.length < 6) return null;
|
|
816
|
+
const office = parts[2]?.trim();
|
|
817
|
+
const ph = parts[3]?.trim();
|
|
818
|
+
const sig = parts[4]?.trim();
|
|
819
|
+
const seq = parts[5]?.trim();
|
|
820
|
+
if (!office || !ph || !sig || !seq) return null;
|
|
821
|
+
return `${office}.${ph}.${sig}.${seq}`;
|
|
822
|
+
}
|
|
823
|
+
|
|
696
824
|
function getNwsHttpAlertStableId(properties) {
|
|
697
825
|
if (!properties) return null;
|
|
698
826
|
const details = properties.details;
|
|
@@ -700,6 +828,20 @@ function getNwsHttpAlertStableId(properties) {
|
|
|
700
828
|
const tr = details.tracking;
|
|
701
829
|
if (tr != null && String(tr).trim() !== '') return String(tr).trim();
|
|
702
830
|
}
|
|
831
|
+
// Same DB key as backend — use if trim drops details/hash (see API trim payloads).
|
|
832
|
+
for (const k of ['nws_stable_id', 'stable_id', 'alert_chain_id']) {
|
|
833
|
+
const v = properties[k];
|
|
834
|
+
if (v != null && String(v).trim() !== '') return String(v).trim();
|
|
835
|
+
}
|
|
836
|
+
if (properties.tracking != null && typeof properties.tracking === 'string' && String(properties.tracking).trim() !== '') {
|
|
837
|
+
return String(properties.tracking).trim();
|
|
838
|
+
}
|
|
839
|
+
// Prefer VTEC-derived ID over hash: VTEC event number is stable across all CON/EXT/UPG
|
|
840
|
+
// actions while the content hash changes on every update, causing phantom duplicates on the map.
|
|
841
|
+
const vtecId = extractNwsVtecStableId(properties);
|
|
842
|
+
if (vtecId) return vtecId;
|
|
843
|
+
const chainId = getNwsOfficeEventIssuedChainId(properties);
|
|
844
|
+
if (chainId) return chainId;
|
|
703
845
|
const h = properties.hash;
|
|
704
846
|
if (h != null && String(h).trim() !== '') return String(h).trim();
|
|
705
847
|
return null;
|
|
@@ -733,6 +875,12 @@ export function normalizeNwwsHttpAlertFeature(raw, forLiveStream) {
|
|
|
733
875
|
if (fallback) {
|
|
734
876
|
p.base_alert_id = fallback;
|
|
735
877
|
p.alert_id = fallback;
|
|
878
|
+
} else {
|
|
879
|
+
const chain = getNwsOfficeEventIssuedChainId(p);
|
|
880
|
+
if (chain) {
|
|
881
|
+
p.base_alert_id = chain;
|
|
882
|
+
p.alert_id = chain;
|
|
883
|
+
}
|
|
736
884
|
}
|
|
737
885
|
}
|
|
738
886
|
const tracker = mapHttpActionToTrackerStatus(p, forLiveStream);
|