@aguacerowx/mapsgl 0.0.32 → 0.0.42

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.
Files changed (35) hide show
  1. package/index.js +34 -2
  2. package/package.json +14 -3
  3. package/src/GridRenderLayer.js +105 -86
  4. package/src/MapManager.js +47 -15
  5. package/src/NexradSitesOverlay.js +148 -0
  6. package/src/NexradWeatherController.js +491 -0
  7. package/src/NwsWatchesWarningsOverlay.js +768 -0
  8. package/src/SatelliteShaderManager.js +999 -0
  9. package/src/WeatherLayerManager.js +866 -139
  10. package/src/WorkerPool.js +340 -0
  11. package/src/nexrad/MapboxRadarLayer.bundled.js +810 -0
  12. package/src/nexrad/MapboxRadarLayer.ts +784 -0
  13. package/src/nexrad/PreprocessedSweepParser.ts +226 -0
  14. package/src/nexrad/buildRadarRayGeometry.ts +97 -0
  15. package/src/nexrad/level3StormRelative.ts +116 -0
  16. package/src/nexrad/loadNexradSites.ts +41 -0
  17. package/src/nexrad/nexradArchiveCache.ts +64 -0
  18. package/src/nexrad/nexradCrossSectionSampleAtLatLon.bundled.js +91 -0
  19. package/src/nexrad/nexradCrossSectionSampleAtLatLon.ts +121 -0
  20. package/src/nexrad/nexradLevel3Products.ts +549 -0
  21. package/src/nexrad/nexradMapboxFrameOpts.js +106 -0
  22. package/src/nexrad/radarArchiveCore.bundled.js +4206 -0
  23. package/src/nexrad/radarArchiveCore.bundled.js.map +7 -0
  24. package/src/nexrad/radarArchiveCore.ts +1737 -0
  25. package/src/nexrad/radarDecode.worker.bundled.js +809 -0
  26. package/src/nexrad/radarDecode.worker.ts +227 -0
  27. package/src/nexrad/radarFrameGpuMatch.bundled.js +79 -0
  28. package/src/nexrad/radarFrameGpuMatch.ts +111 -0
  29. package/src/nwsAlertsSupport.js +860 -0
  30. package/src/nwsEventColorsDefaults.js +133 -0
  31. package/src/nwsSdkConstants.js +360 -0
  32. package/src/nwsWarningCustomizationKey.gen.js +496 -0
  33. package/src/satelliteDefaultColormaps.js +37 -0
  34. package/src/satelliteKtxWorker.js +225 -0
  35. package/src/satelliteShader.js +17 -0
@@ -0,0 +1,860 @@
1
+ /**
2
+ * NWWS HTTP + SSE merge pipeline (subset of aguacero-frontend WatchesWarningsLayer).
3
+ */
4
+
5
+ import { resolveNwsWarningCustomizationKey } from './nwsWarningCustomizationKey.gen.js';
6
+ import { NWS_ALERT_END_TIME_PROP_KEYS, NWS_WARNINGS_SUBTYPE_PARENT } from './nwsSdkConstants.js';
7
+
8
+ export const NWS_RENDER_PRIORITY_PROP = '_nws_render_priority';
9
+
10
+ export const SDK_ALLOWED_EVENT_SET = new Set([
11
+ 'Tornado Watch',
12
+ 'Tornado Warning',
13
+ 'Tornado Warning (Observed)',
14
+ 'Tornado Warning (PDS)',
15
+ 'Tornado Warning (Emergency)',
16
+ 'Severe Thunderstorm Watch',
17
+ 'Severe Thunderstorm Warning',
18
+ 'Severe Thunderstorm Warning (Considerable)',
19
+ 'Severe Thunderstorm Warning (Destructive)',
20
+ ]);
21
+
22
+ const NWS_EVENT_CANONICAL_MAP = new Map(
23
+ [...SDK_ALLOWED_EVENT_SET].map((name) => [normalizeNwsEventName(name), name])
24
+ );
25
+
26
+ function normalizeNwsEventName(raw) {
27
+ if (raw == null) return '';
28
+ return String(raw).trim().replace(/\s+/g, ' ').toLowerCase();
29
+ }
30
+
31
+ function canonicalizeNwsEventName(raw) {
32
+ const normalized = normalizeNwsEventName(raw);
33
+ if (!normalized) return '';
34
+ return NWS_EVENT_CANONICAL_MAP.get(normalized) ?? String(raw).trim();
35
+ }
36
+
37
+ export function getNwsAlertEventLabelFromProperties(p) {
38
+ if (!p) return '';
39
+ const explicitEventName = canonicalizeNwsEventName(p.event_name);
40
+ const raw = [p.event, p.phenomenon, p.event_name].find((v) => v != null && String(v).trim() !== '');
41
+ if (raw == null) return '';
42
+ const base = canonicalizeNwsEventName(raw);
43
+ if (!base) return '';
44
+ const resolved = resolveNwsWarningCustomizationKey(base, p);
45
+ if (
46
+ resolved === base &&
47
+ explicitEventName &&
48
+ explicitEventName !== base &&
49
+ NWS_WARNINGS_SUBTYPE_PARENT[explicitEventName] === base
50
+ ) {
51
+ return explicitEventName;
52
+ }
53
+ return resolved;
54
+ }
55
+
56
+ function normalizeNwsRankToken(raw) {
57
+ if (!raw) return '';
58
+ if (typeof raw === 'string') return raw.trim().toLowerCase();
59
+ return String(raw).trim().toLowerCase();
60
+ }
61
+
62
+ function computeNwsAlertRenderPriority(properties, precomputedEventName) {
63
+ if (!properties) return 0;
64
+ const eventName = normalizeNwsRankToken(precomputedEventName ?? getNwsAlertEventLabelFromProperties(properties));
65
+ const severity = normalizeNwsRankToken(properties.severity);
66
+ const urgency = normalizeNwsRankToken(properties.urgency);
67
+ const certainty = normalizeNwsRankToken(properties.certainty);
68
+ const response = normalizeNwsRankToken(properties.response);
69
+ const significance = normalizeNwsRankToken(properties.significance);
70
+
71
+ let score = 0;
72
+ if (eventName.includes('emergency')) score += 500;
73
+ else if (eventName.includes('warning')) score += 400;
74
+ else if (eventName.includes('watch')) score += 300;
75
+ else if (eventName.includes('advisory')) score += 200;
76
+ else if (eventName.includes('statement')) score += 120;
77
+
78
+ if (significance === 'w') score += 80;
79
+ else if (significance === 'a') score += 60;
80
+ else if (significance === 'y') score += 40;
81
+ else if (significance === 's') score += 20;
82
+
83
+ if (severity === 'extreme') score += 100;
84
+ else if (severity === 'severe') score += 75;
85
+ else if (severity === 'moderate') score += 50;
86
+ else if (severity === 'minor') score += 25;
87
+
88
+ if (urgency === 'immediate') score += 60;
89
+ else if (urgency === 'expected') score += 40;
90
+ else if (urgency === 'future') score += 20;
91
+
92
+ if (certainty === 'observed') score += 40;
93
+ else if (certainty === 'likely') score += 28;
94
+ else if (certainty === 'possible') score += 16;
95
+ else if (certainty === 'unlikely') score += 4;
96
+
97
+ if (response === 'immediate') score += 25;
98
+ else if (response === 'execute') score += 15;
99
+
100
+ return score;
101
+ }
102
+
103
+ function sortWarningsFeaturesForRender(features) {
104
+ if (!Array.isArray(features) || features.length <= 1) return features;
105
+ const copy = features.slice();
106
+ for (let i = 0; i < copy.length; i++) {
107
+ const f = copy[i];
108
+ const props = f?.properties;
109
+ if (props && typeof props[NWS_RENDER_PRIORITY_PROP] !== 'number') {
110
+ copy[i] = {
111
+ ...f,
112
+ properties: {
113
+ ...props,
114
+ [NWS_RENDER_PRIORITY_PROP]: computeNwsAlertRenderPriority(props),
115
+ },
116
+ };
117
+ }
118
+ }
119
+ copy.sort((a, b) => {
120
+ const pA = a?.properties?.[NWS_RENDER_PRIORITY_PROP] ?? 0;
121
+ const pB = b?.properties?.[NWS_RENDER_PRIORITY_PROP] ?? 0;
122
+ return pA - pB;
123
+ });
124
+ return copy;
125
+ }
126
+
127
+ export function getNwsTimeProp(properties, keys) {
128
+ if (!properties) return null;
129
+ for (let i = 0; i < keys.length; i++) {
130
+ const v = properties[keys[i]];
131
+ if (v != null && v !== '') return v;
132
+ }
133
+ return null;
134
+ }
135
+
136
+ export function parseNwsTimeToUnix(value) {
137
+ if (value == null || value === '') return null;
138
+ if (typeof value === 'number') {
139
+ if (!Number.isFinite(value)) return null;
140
+ return value > 1e12 ? Math.floor(value / 1000) : Math.floor(value);
141
+ }
142
+ const s = String(value).trim();
143
+ if (s.includes('T') || s.includes('-')) {
144
+ const t = Date.parse(s);
145
+ if (!Number.isNaN(t)) return Math.floor(t / 1000);
146
+ }
147
+ if (s.length === 10 && /^\d{10}$/.test(s)) {
148
+ return Number(s);
149
+ }
150
+ if (s.length === 13 && /^\d{13}$/.test(s)) {
151
+ return Math.floor(Number(s) / 1000);
152
+ }
153
+ const t = Date.parse(s);
154
+ if (!Number.isNaN(t)) return Math.floor(t / 1000);
155
+ return null;
156
+ }
157
+
158
+ const NWS_START_UNIX_KEYS = ['issued_at', 'issued', 'sent', 'onset', 'effective'];
159
+ const NWS_ACTIVE_START_UNIX_KEYS = ['onset', 'effective', 'issued_at', 'issued', 'sent'];
160
+
161
+ function getNwsStartUnixForMap(properties) {
162
+ const startStr = getNwsTimeProp(properties, NWS_START_UNIX_KEYS);
163
+ return parseNwsTimeToUnix(startStr) ?? 0;
164
+ }
165
+
166
+ function getNwsActiveStartUnix(properties) {
167
+ const startStr = getNwsTimeProp(properties, NWS_ACTIVE_START_UNIX_KEYS);
168
+ return parseNwsTimeToUnix(startStr) ?? 0;
169
+ }
170
+
171
+ export function getEarliestNwsReferenceSentRaw(properties) {
172
+ if (!properties) return null;
173
+ let refs = properties.references;
174
+ if (typeof refs === 'string') {
175
+ try {
176
+ refs = JSON.parse(refs);
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+ if (!Array.isArray(refs) || refs.length === 0) return null;
182
+ let best = null;
183
+ for (const r of refs) {
184
+ if (!r || typeof r !== 'object') continue;
185
+ const raw = r.sent ?? r.Sent;
186
+ if (raw == null || raw === '') continue;
187
+ const s = String(raw);
188
+ const t = new Date(s).getTime();
189
+ if (Number.isNaN(t)) continue;
190
+ if (!best || t < best.t) best = { t, raw: s };
191
+ }
192
+ return best?.raw ?? null;
193
+ }
194
+
195
+ let nextWarningFeatureId = 1;
196
+
197
+ function allocateNwsMapboxFeatureId(rawId) {
198
+ if (typeof rawId === 'number' && Number.isFinite(rawId) && rawId >= 0 && rawId <= Number.MAX_SAFE_INTEGER) {
199
+ return Math.trunc(rawId);
200
+ }
201
+ return nextWarningFeatureId++;
202
+ }
203
+
204
+ function ensureDistinctMapboxIdsOnFeatures(features) {
205
+ const used = new Set();
206
+ return features.map((f) => {
207
+ let id =
208
+ typeof f?.id === 'number' && Number.isFinite(f.id) && f.id >= 0 && f.id <= Number.MAX_SAFE_INTEGER
209
+ ? Math.trunc(f.id)
210
+ : null;
211
+ if (id == null) {
212
+ while (used.has(nextWarningFeatureId)) {
213
+ nextWarningFeatureId++;
214
+ }
215
+ id = nextWarningFeatureId++;
216
+ used.add(id);
217
+ return { ...f, id };
218
+ }
219
+ if (!used.has(id)) {
220
+ used.add(id);
221
+ return f;
222
+ }
223
+ while (used.has(nextWarningFeatureId)) {
224
+ nextWarningFeatureId++;
225
+ }
226
+ const newId = nextWarningFeatureId++;
227
+ used.add(newId);
228
+ return { ...f, id: newId };
229
+ });
230
+ }
231
+
232
+ export function enrichWarningsWithColors(geojson) {
233
+ if (!geojson?.features?.length) return geojson;
234
+ const features = geojson.features.map((f) => {
235
+ const p = f.properties || {};
236
+ const earliestRef = getEarliestNwsReferenceSentRaw(p);
237
+ const hasExplicitIssued = getNwsTimeProp(p, ['issued_at', 'issued']) != null;
238
+ const nwsIssuedPatch =
239
+ earliestRef && !hasExplicitIssued ? { _nws_issued_at: earliestRef } : null;
240
+
241
+ const eventName = getNwsAlertEventLabelFromProperties(p);
242
+ const renderPriority = computeNwsAlertRenderPriority(p, eventName);
243
+
244
+ const startUnix = getNwsStartUnixForMap(p);
245
+ const activeStartUnix = getNwsActiveStartUnix(p);
246
+ const endStr = getNwsTimeProp(p, [...NWS_ALERT_END_TIME_PROP_KEYS]);
247
+ const endUnix = parseNwsTimeToUnix(endStr) ?? 2147483647;
248
+
249
+ const mapboxFeatureId = allocateNwsMapboxFeatureId(f.id);
250
+
251
+ const hasEventName = !!eventName;
252
+ const eventNameUnchanged = !hasEventName || p.event_name === eventName;
253
+ const priorityUnchanged = p[NWS_RENDER_PRIORITY_PROP] === renderPriority;
254
+ const startUnchanged = p.start_unix === startUnix;
255
+ const activeStartUnchanged = p.active_start_unix === activeStartUnix;
256
+ const endUnchanged = p.end_unix === endUnix;
257
+ const idUnchanged = f.id === mapboxFeatureId;
258
+ const nwsIssuedUnchanged =
259
+ nwsIssuedPatch == null || String(p._nws_issued_at ?? '') === String(nwsIssuedPatch._nws_issued_at);
260
+
261
+ if (
262
+ eventNameUnchanged &&
263
+ priorityUnchanged &&
264
+ startUnchanged &&
265
+ activeStartUnchanged &&
266
+ endUnchanged &&
267
+ idUnchanged &&
268
+ nwsIssuedUnchanged
269
+ ) {
270
+ return f;
271
+ }
272
+ return {
273
+ ...f,
274
+ id: mapboxFeatureId,
275
+ properties: {
276
+ ...p,
277
+ ...(nwsIssuedPatch ?? {}),
278
+ ...(hasEventName ? { event_name: eventName } : {}),
279
+ [NWS_RENDER_PRIORITY_PROP]: renderPriority,
280
+ start_unix: startUnix,
281
+ active_start_unix: activeStartUnix,
282
+ end_unix: endUnix,
283
+ },
284
+ };
285
+ });
286
+ const distinctIds = ensureDistinctMapboxIdsOnFeatures(features);
287
+ return { type: 'FeatureCollection', features: sortWarningsFeaturesForRender(distinctIds) };
288
+ }
289
+
290
+ export function normalizeNwwsAlertId(value) {
291
+ if (value == null || value === '') return null;
292
+ const s = String(value).trim();
293
+ if (!s) return null;
294
+ const marker = '/alerts/';
295
+ const idx = s.indexOf(marker);
296
+ if (idx >= 0) {
297
+ const tail = s.slice(idx + marker.length).trim();
298
+ return tail || s;
299
+ }
300
+ return s;
301
+ }
302
+
303
+ export function getNwwsTrackerStatusFromProperties(properties) {
304
+ if (!properties) return null;
305
+ const raw = properties._trackerStatus ?? properties.trackerStatus ?? properties.tracker_status;
306
+ if (raw == null || raw === '') return null;
307
+ const s = String(raw).trim().toLowerCase();
308
+ if (!s) return null;
309
+ if (s === 'canceled') return 'cancelled';
310
+ if (s === 'cancel') return 'cancelled';
311
+ if (s === 'expire') return 'expired';
312
+ if (s === 'upgrade') return 'upgraded';
313
+ if (s === 'extend' || s === 'extended' || s === 'extension' || s === 'expanded' || s === 'expansion') {
314
+ return 'extended';
315
+ }
316
+ if (s === 'continuation' || s === 'continued' || s === 'correction' || s === 'continue') {
317
+ return 'continuation';
318
+ }
319
+ return s;
320
+ }
321
+
322
+ export function isNwwsTrackerStatusTerminal(status) {
323
+ return status === 'cancelled' || status === 'expired';
324
+ }
325
+
326
+ function isNwwsTrackerStatusUpgrade(status) {
327
+ return status === 'upgraded';
328
+ }
329
+
330
+ export function getNwsBaseAlertIdFromFeature(feature) {
331
+ const p = feature?.properties;
332
+ const base = normalizeNwwsAlertId(p?.base_alert_id ?? p?.base_alertid ?? p?.baseAlertId ?? p?.base_alertId);
333
+ if (base) return base;
334
+ return normalizeNwwsAlertId(p?.alert_id ?? p?.alertId ?? p?.id ?? feature?.id);
335
+ }
336
+
337
+ export function getNwsAlertInstanceIdFromFeature(feature) {
338
+ const p = feature?.properties ?? {};
339
+ const baseId = getNwsBaseAlertIdFromFeature(feature);
340
+ const direct = normalizeNwwsAlertId(
341
+ p.alert_id ?? p.alertId ?? p.unique_id ?? p.uniqueId ?? p.id ?? feature?.id
342
+ );
343
+ return direct ?? baseId;
344
+ }
345
+
346
+ export function isUsableNwwsAlertGeometry(geometry) {
347
+ if (geometry == null || typeof geometry !== 'object') return false;
348
+ const g = geometry;
349
+ const t = g.type;
350
+ if (!t || t === 'GeometryCollection') {
351
+ return Array.isArray(g.geometries) && g.geometries.length > 0;
352
+ }
353
+ const c = g.coordinates;
354
+ if (c == null) return false;
355
+ if (t === 'Point') return Array.isArray(c) && c.length >= 2;
356
+ if (t === 'LineString') return Array.isArray(c) && c.length >= 2;
357
+ if (t === 'MultiPoint' || t === 'MultiLineString' || t === 'Polygon') {
358
+ return Array.isArray(c) && c.length > 0;
359
+ }
360
+ if (t === 'MultiPolygon') {
361
+ return Array.isArray(c) && c.length > 0;
362
+ }
363
+ return true;
364
+ }
365
+
366
+ export function applyNwwsDeltaToCollection(data, delta) {
367
+ let features = [...data.features];
368
+ const cancelSet = new Set(delta.cancelIds);
369
+ const expireSet = new Set(delta.expireIds);
370
+ const removeIds = new Set([...expireSet, ...cancelSet]);
371
+
372
+ const replacedBaseIds = new Set();
373
+ for (const feat of delta.upserts) {
374
+ if (!feat || feat.type !== 'Feature') continue;
375
+ const baseId = getNwsBaseAlertIdFromFeature(feat);
376
+ if (baseId) replacedBaseIds.add(baseId);
377
+ }
378
+
379
+ const clearedGeometryByInstance = new Map();
380
+ const clearedMapboxIdByInstance = new Map();
381
+
382
+ if (replacedBaseIds.size > 0) {
383
+ features = features.filter((f) => {
384
+ const baseId = getNwsBaseAlertIdFromFeature(f);
385
+ if (!baseId || !replacedBaseIds.has(baseId)) return true;
386
+ const iid = getNwsAlertInstanceIdFromFeature(f) ?? baseId;
387
+ if (f.geometry) clearedGeometryByInstance.set(iid, f.geometry);
388
+ if (f.id != null) clearedMapboxIdByInstance.set(iid, f.id);
389
+ return false;
390
+ });
391
+ }
392
+
393
+ for (const feat of delta.upserts) {
394
+ if (!feat || feat.type !== 'Feature') continue;
395
+ const enriched = enrichWarningsWithColors({ type: 'FeatureCollection', features: [feat] }).features[0];
396
+ const trackerStatus = getNwwsTrackerStatusFromProperties(enriched?.properties);
397
+ if (isNwwsTrackerStatusTerminal(trackerStatus)) {
398
+ const terminalId = getNwsBaseAlertIdFromFeature(enriched);
399
+ if (terminalId) removeIds.add(terminalId);
400
+ continue;
401
+ }
402
+ const baseId = getNwsBaseAlertIdFromFeature(enriched);
403
+ const instanceId = getNwsAlertInstanceIdFromFeature(enriched) ?? baseId;
404
+
405
+ const clearedGeometry = instanceId ? clearedGeometryByInstance.get(instanceId) : undefined;
406
+ const clearedMapboxId = instanceId ? clearedMapboxIdByInstance.get(instanceId) : undefined;
407
+
408
+ features.push({
409
+ ...enriched,
410
+ id: clearedMapboxId ?? enriched.id,
411
+ geometry: isUsableNwwsAlertGeometry(enriched.geometry)
412
+ ? enriched.geometry
413
+ : clearedGeometry ?? enriched.geometry,
414
+ });
415
+ }
416
+
417
+ if (removeIds.size) {
418
+ features = features.filter((f) => {
419
+ const baseId = getNwsBaseAlertIdFromFeature(f);
420
+ if (baseId && removeIds.has(baseId)) return false;
421
+ const inst = getNwsAlertInstanceIdFromFeature(f);
422
+ if (inst && removeIds.has(inst)) return false;
423
+ return true;
424
+ });
425
+ }
426
+
427
+ features = ensureDistinctMapboxIdsOnFeatures(features);
428
+ return { type: 'FeatureCollection', features };
429
+ }
430
+
431
+ function pushIds(target, val) {
432
+ if (!Array.isArray(val)) return;
433
+ for (const x of val) {
434
+ if (x == null) continue;
435
+ if (typeof x === 'object') {
436
+ const o = x;
437
+ const id =
438
+ o.base_alert_id ??
439
+ o.base_alertid ??
440
+ o.baseAlertId ??
441
+ o.base_alertId ??
442
+ o.alert_id ??
443
+ o.alertId ??
444
+ o.id;
445
+ const normalizedId = normalizeNwwsAlertId(id);
446
+ if (normalizedId) target.push(normalizedId);
447
+ } else if (x !== '') {
448
+ const normalizedId = normalizeNwwsAlertId(x);
449
+ if (normalizedId) target.push(normalizedId);
450
+ }
451
+ }
452
+ }
453
+
454
+ export function parseNwwsWsDelta(raw) {
455
+ const upserts = [];
456
+ const cancelIds = [];
457
+ const expireIds = [];
458
+
459
+ const pushUpgradeReplacedIdsFromFeature = (feature) => {
460
+ const status = getNwwsTrackerStatusFromProperties(feature?.properties);
461
+ if (!isNwwsTrackerStatusUpgrade(status)) return;
462
+ const p = feature?.properties ?? {};
463
+ const direct =
464
+ p._upgradedFrom ??
465
+ p.upgradedFrom ??
466
+ p._replaces ??
467
+ p.replaces ??
468
+ p.replaced_alert_id ??
469
+ p.replacedAlertId;
470
+ const directNorm = normalizeNwwsAlertId(direct);
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
+ }
490
+ }
491
+ };
492
+
493
+ const collectUpsertFeatures = (val) => {
494
+ if (!val) return;
495
+ const collectMaybeFeatureObject = (obj) => {
496
+ if (!obj || typeof obj !== 'object') return;
497
+ if (obj.type !== 'Feature' && obj.geometry != null && obj.properties != null && typeof obj.properties === 'object') {
498
+ obj = { type: 'Feature', id: obj.id, geometry: obj.geometry, properties: obj.properties };
499
+ }
500
+ if (obj.type === 'Feature') {
501
+ const status = getNwwsTrackerStatusFromProperties(obj?.properties);
502
+ const baseId = getNwsBaseAlertIdFromFeature(obj);
503
+ if (status === 'cancelled' && baseId) {
504
+ cancelIds.push(baseId);
505
+ return;
506
+ }
507
+ if (status === 'expired' && baseId) {
508
+ expireIds.push(baseId);
509
+ return;
510
+ }
511
+ pushUpgradeReplacedIdsFromFeature(obj);
512
+ upserts.push(obj);
513
+ return;
514
+ }
515
+ if (obj.new != null) collectUpsertFeatures(obj.new);
516
+ if (obj.feature != null) collectUpsertFeatures(obj.feature);
517
+ if (obj.alert != null) collectUpsertFeatures(obj.alert);
518
+ if (obj.current != null) collectUpsertFeatures(obj.current);
519
+ };
520
+ if (Array.isArray(val)) {
521
+ for (const item of val) {
522
+ collectMaybeFeatureObject(item);
523
+ }
524
+ return;
525
+ }
526
+ const candidate = val;
527
+ if (candidate?.type === 'Feature') {
528
+ collectMaybeFeatureObject(candidate);
529
+ return;
530
+ }
531
+ if (candidate?.type === 'FeatureCollection' && Array.isArray(candidate.features)) {
532
+ for (const item of candidate.features) {
533
+ collectMaybeFeatureObject(item);
534
+ }
535
+ return;
536
+ }
537
+ if (candidate?.data != null) collectUpsertFeatures(candidate.data);
538
+ if (candidate?.payload != null) collectUpsertFeatures(candidate.payload);
539
+ if (candidate?.feature != null) collectUpsertFeatures(candidate.feature);
540
+ if (candidate?.features != null) collectUpsertFeatures(candidate.features);
541
+ if (candidate?.items != null) collectUpsertFeatures(candidate.items);
542
+ if (candidate?.records != null) collectUpsertFeatures(candidate.records);
543
+ if (candidate?.alerts != null) collectUpsertFeatures(candidate.alerts);
544
+ if (candidate?.upsert != null) collectUpsertFeatures(candidate.upsert);
545
+ if (candidate?.upserts != null) collectUpsertFeatures(candidate.upserts);
546
+ if (candidate?.insert != null) collectUpsertFeatures(candidate.insert);
547
+ if (candidate?.inserts != null) collectUpsertFeatures(candidate.inserts);
548
+ if (candidate?.created != null) collectUpsertFeatures(candidate.created);
549
+ if (candidate?.updated != null) collectUpsertFeatures(candidate.updated);
550
+ if (candidate?.delta != null) {
551
+ const deltaInner = candidate.delta;
552
+ collectUpsertFeatures(deltaInner?.upsert ?? deltaInner?.upserts ?? deltaInner?.features ?? deltaInner?.data);
553
+ }
554
+ if (candidate?.patch != null) collectUpsertFeatures(candidate.patch);
555
+ };
556
+
557
+ if (raw == null || typeof raw !== 'object') return { upserts, cancelIds, expireIds };
558
+ if (Array.isArray(raw)) {
559
+ collectUpsertFeatures(raw);
560
+ return { upserts, cancelIds, expireIds };
561
+ }
562
+ const msg = raw;
563
+ const collectBuckets = (container) => {
564
+ const action = container.type ?? container.action;
565
+
566
+ if (container.upsert != null) collectUpsertFeatures(container.upsert);
567
+ if (container.upserts != null) collectUpsertFeatures(container.upserts);
568
+ if (container.alert != null) collectUpsertFeatures(container.alert);
569
+ if (container.alerts != null) collectUpsertFeatures(container.alerts);
570
+ if (container.new != null) collectUpsertFeatures(container.new);
571
+ if (container.extended != null) collectUpsertFeatures(container.extended);
572
+ if (container.upgraded != null) collectUpsertFeatures(container.upgraded);
573
+ if (action === 'upsert' || action === 'Upsert') {
574
+ collectUpsertFeatures(container.features ?? container.items ?? container.data);
575
+ }
576
+ if (container.features != null && container.type !== 'FeatureCollection') {
577
+ collectUpsertFeatures(container.features);
578
+ }
579
+
580
+ if (container.cancel != null) pushIds(cancelIds, container.cancel);
581
+ if (container.cancels != null) pushIds(cancelIds, container.cancels);
582
+ if (container.cancelled != null) pushIds(cancelIds, container.cancelled);
583
+ if (container.canceled != null) pushIds(cancelIds, container.canceled);
584
+ if (action === 'cancel' || action === 'Cancel') {
585
+ pushIds(cancelIds, container.ids ?? container.base_alert_ids ?? container.items);
586
+ }
587
+
588
+ if (container.expire != null) pushIds(expireIds, container.expire);
589
+ if (container.expires != null) pushIds(expireIds, container.expires);
590
+ if (container.expired != null) pushIds(expireIds, container.expired);
591
+ if (action === 'expire' || action === 'Expire') {
592
+ pushIds(expireIds, container.ids ?? container.base_alert_ids ?? container.items);
593
+ }
594
+ };
595
+
596
+ collectBuckets(msg);
597
+ if (msg.diff != null && typeof msg.diff === 'object') {
598
+ collectBuckets(msg.diff);
599
+ }
600
+
601
+ return { upserts, cancelIds, expireIds };
602
+ }
603
+
604
+ export function filterNwwsTerminalStatusFeatures(fc) {
605
+ return {
606
+ type: 'FeatureCollection',
607
+ features: (fc.features ?? []).filter((feature) => {
608
+ const status = getNwwsTrackerStatusFromProperties(feature?.properties);
609
+ return !isNwwsTrackerStatusTerminal(status);
610
+ }),
611
+ };
612
+ }
613
+
614
+ /**
615
+ * Legacy filter: tornado + severe-thunderstorm family only (same as pre–all-alerts SDK).
616
+ * @param {import('geojson').FeatureCollection} fc
617
+ */
618
+ export function filterToConvectiveSdkEvents(fc) {
619
+ const out = [];
620
+ for (const f of fc.features ?? []) {
621
+ const ev = getNwsAlertEventLabelFromProperties(f?.properties ?? {});
622
+ if (ev && SDK_ALLOWED_EVENT_SET.has(ev)) {
623
+ out.push(f);
624
+ }
625
+ }
626
+ return { type: 'FeatureCollection', features: out };
627
+ }
628
+
629
+ /**
630
+ * Whether a resolved NWS `event_name` should be shown when the user chose {@link filterNwsAlertsByIncludedList}.
631
+ * Matches exact names; also matches when the user lists a parent product (e.g. "Tornado Warning") and the feature
632
+ * is a known subtype (e.g. "Tornado Warning (PDS)") via {@link NWS_WARNINGS_SUBTYPE_PARENT}.
633
+ * @param {string[] | null | undefined} includedAlerts
634
+ * @param {string} eventLabel
635
+ */
636
+ export function isNwsEventIncludedInAllowlist(includedAlerts, eventLabel) {
637
+ if (!includedAlerts?.length || !eventLabel) {
638
+ return false;
639
+ }
640
+ const set = new Set(
641
+ includedAlerts.map((x) => (x != null ? String(x).trim() : '')).filter(Boolean)
642
+ );
643
+ if (set.size === 0) {
644
+ return false;
645
+ }
646
+ const ev = String(eventLabel).trim();
647
+ if (set.has(ev)) {
648
+ return true;
649
+ }
650
+ const parent = NWS_WARNINGS_SUBTYPE_PARENT[ev];
651
+ if (parent && set.has(parent)) {
652
+ return true;
653
+ }
654
+ if (ev === 'Tornado Warning (Observed)' && set.has('Tornado Warning (Reported)')) {
655
+ return true;
656
+ }
657
+ return false;
658
+ }
659
+
660
+ /**
661
+ * @param {import('geojson').FeatureCollection} fc
662
+ * @param {string[]} includedAlerts
663
+ */
664
+ export function filterNwsAlertsByIncludedList(fc, includedAlerts) {
665
+ const out = [];
666
+ for (const f of fc.features ?? []) {
667
+ const ev = getNwsAlertEventLabelFromProperties(f?.properties ?? {});
668
+ if (isNwsEventIncludedInAllowlist(includedAlerts, ev)) {
669
+ out.push(f);
670
+ }
671
+ }
672
+ return { type: 'FeatureCollection', features: out };
673
+ }
674
+
675
+ /**
676
+ * @param {import('geojson').FeatureCollection} fc
677
+ * @param {'all' | 'user'} [scope]
678
+ * @param {string[] | undefined} [includedAlerts] - Used when `scope === 'user'`: exact NWS `event_name` strings to show.
679
+ */
680
+ export function filterNwsAlertsByScope(fc, scope = 'all', includedAlerts) {
681
+ if (scope === 'user') {
682
+ return filterNwsAlertsByIncludedList(fc, includedAlerts ?? []);
683
+ }
684
+ const features = (fc.features ?? []).filter((f) => {
685
+ const ev = getNwsAlertEventLabelFromProperties(f?.properties ?? {});
686
+ return !!(ev && String(ev).trim());
687
+ });
688
+ return { type: 'FeatureCollection', features };
689
+ }
690
+
691
+ /** @deprecated Use {@link filterToConvectiveSdkEvents} or {@link filterNwsAlertsByScope} with `scope: 'user'` and an explicit `includedAlerts` list. */
692
+ export function filterToSdkAllowedEvents(fc) {
693
+ return filterToConvectiveSdkEvents(fc);
694
+ }
695
+
696
+ function getNwsHttpAlertStableId(properties) {
697
+ if (!properties) return null;
698
+ const details = properties.details;
699
+ if (details && typeof details === 'object') {
700
+ const tr = details.tracking;
701
+ if (tr != null && String(tr).trim() !== '') return String(tr).trim();
702
+ }
703
+ const h = properties.hash;
704
+ if (h != null && String(h).trim() !== '') return String(h).trim();
705
+ return null;
706
+ }
707
+
708
+ function mapHttpActionToTrackerStatus(p, forLiveStream) {
709
+ if (!forLiveStream) return undefined;
710
+ if (p.is_cancelled === true) return 'cancelled';
711
+ const at = String(p.action_type ?? '').trim().toLowerCase();
712
+ if (at === 'cancelled' || at === 'canceled') return 'cancelled';
713
+ if (at === 'expired') return 'expired';
714
+ if (at === 'upgraded') return 'upgraded';
715
+ if (at === 'extended' || at === 'extension') return 'extended';
716
+ if (at === 'updated') return 'extended';
717
+ if (at === 'continuation' || at === 'continued') return 'continuation';
718
+ if (at === 'issued' || at === 'new') return 'issued';
719
+ if (at === '') return 'issued';
720
+ return 'issued';
721
+ }
722
+
723
+ export function normalizeNwwsHttpAlertFeature(raw, forLiveStream) {
724
+ if (!raw || typeof raw !== 'object' || raw.type !== 'Feature') return raw;
725
+ const p = { ...(raw.properties ?? {}) };
726
+ const stableId = getNwsHttpAlertStableId(p);
727
+ if (stableId) {
728
+ p.base_alert_id = stableId;
729
+ p.alert_id = stableId;
730
+ } else {
731
+ const fallback =
732
+ normalizeNwwsAlertId(raw.id) ?? normalizeNwwsAlertId(p.id) ?? normalizeNwwsAlertId(p.alert_id ?? p.alertId);
733
+ if (fallback) {
734
+ p.base_alert_id = fallback;
735
+ p.alert_id = fallback;
736
+ }
737
+ }
738
+ const tracker = mapHttpActionToTrackerStatus(p, forLiveStream);
739
+ if (tracker !== undefined) {
740
+ p._trackerStatus = tracker;
741
+ }
742
+ return { ...raw, properties: p };
743
+ }
744
+
745
+ export function unwrapNwwsFeatureCollectionRoot(parsed) {
746
+ if (Array.isArray(parsed)) {
747
+ return {
748
+ type: 'FeatureCollection',
749
+ features: parsed.map((f) => normalizeNwwsHttpAlertFeature(f, false)),
750
+ };
751
+ }
752
+ if (!parsed || typeof parsed !== 'object') return null;
753
+ const o = parsed;
754
+ if (o.type === 'FeatureCollection' && Array.isArray(o.features)) {
755
+ return {
756
+ type: 'FeatureCollection',
757
+ features: o.features.map((f) => normalizeNwwsHttpAlertFeature(f, false)),
758
+ };
759
+ }
760
+ for (const key of ['geojson', 'data', 'payload', 'body', 'collection', 'result', 'message', 'record']) {
761
+ const inner = o[key];
762
+ if (
763
+ inner &&
764
+ typeof inner === 'object' &&
765
+ inner.type === 'FeatureCollection' &&
766
+ Array.isArray(inner.features)
767
+ ) {
768
+ return {
769
+ type: 'FeatureCollection',
770
+ features: inner.features.map((f) => normalizeNwwsHttpAlertFeature(f, false)),
771
+ };
772
+ }
773
+ }
774
+ return null;
775
+ }
776
+
777
+ export function cloneNwwsFeatureCollectionStrippingVolatile(data) {
778
+ return {
779
+ type: 'FeatureCollection',
780
+ features: (data.features || []).flatMap((f) => {
781
+ const p = f?.properties;
782
+ if (p) {
783
+ const ex = p.nwws_expired;
784
+ if (ex === true || ex === 1 || String(ex).toLowerCase() === 'true') {
785
+ return [];
786
+ }
787
+ }
788
+ const copy = {
789
+ ...f,
790
+ properties: p ? { ...p } : {},
791
+ };
792
+ delete copy._lastActiveState;
793
+ if (copy.properties && 'nwws_expired' in copy.properties) {
794
+ delete copy.properties.nwws_expired;
795
+ }
796
+ if (copy.properties) {
797
+ delete copy.properties._nws_flash_until_unix;
798
+ delete copy.properties._nws_webhook_debug_flash;
799
+ }
800
+ return [copy];
801
+ }),
802
+ };
803
+ }
804
+
805
+ export function buildAlertClickPayload(feature) {
806
+ const properties = feature?.properties ? { ...feature.properties } : {};
807
+ const eventName = getNwsAlertEventLabelFromProperties(properties);
808
+ let tags = properties.tags;
809
+ if (typeof tags === 'string') {
810
+ try {
811
+ tags = JSON.parse(tags);
812
+ } catch {
813
+ tags = null;
814
+ }
815
+ }
816
+ if (!Array.isArray(tags)) {
817
+ tags = tags != null ? [String(tags)] : [];
818
+ }
819
+ const headline = properties.headline != null ? String(properties.headline) : '';
820
+ const name =
821
+ headline ||
822
+ eventName ||
823
+ (properties.event != null ? String(properties.event) : '') ||
824
+ (properties.event_name != null ? String(properties.event_name) : '');
825
+ const description =
826
+ properties.description != null
827
+ ? String(properties.description)
828
+ : properties.raw_text != null
829
+ ? String(properties.raw_text)
830
+ : '';
831
+ const summary = properties.summary != null ? String(properties.summary) : '';
832
+ const instruction = properties.instruction != null ? String(properties.instruction) : '';
833
+
834
+ const startUnix =
835
+ typeof properties.start_unix === 'number'
836
+ ? properties.start_unix
837
+ : getNwsStartUnixForMap(properties);
838
+ const endUnix =
839
+ typeof properties.end_unix === 'number'
840
+ ? properties.end_unix
841
+ : parseNwsTimeToUnix(getNwsTimeProp(properties, [...NWS_ALERT_END_TIME_PROP_KEYS]));
842
+ const activeStartUnix =
843
+ typeof properties.active_start_unix === 'number'
844
+ ? properties.active_start_unix
845
+ : getNwsActiveStartUnix(properties);
846
+
847
+ return {
848
+ name,
849
+ eventName,
850
+ headline,
851
+ summary,
852
+ description,
853
+ instruction,
854
+ tags,
855
+ startUnix,
856
+ endUnix,
857
+ activeStartUnix,
858
+ properties,
859
+ };
860
+ }