atmosx-nwws-parser 1.0.19 → 1.0.22

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 (44) hide show
  1. package/README.md +182 -64
  2. package/dist/cjs/index.cjs +3799 -0
  3. package/dist/esm/index.mjs +3757 -0
  4. package/package.json +49 -37
  5. package/src/bootstrap.ts +196 -0
  6. package/src/database.ts +148 -0
  7. package/src/dictionaries/awips.ts +342 -0
  8. package/src/dictionaries/events.ts +142 -0
  9. package/src/dictionaries/icao.ts +237 -0
  10. package/src/dictionaries/offshore.ts +12 -0
  11. package/src/dictionaries/signatures.ts +107 -0
  12. package/src/eas.ts +493 -0
  13. package/src/index.ts +229 -0
  14. package/src/parsers/events/api.ts +151 -0
  15. package/src/parsers/events/cap.ts +138 -0
  16. package/src/parsers/events/text.ts +106 -0
  17. package/src/parsers/events/ugc.ts +109 -0
  18. package/src/parsers/events/vtec.ts +78 -0
  19. package/src/parsers/events.ts +367 -0
  20. package/src/parsers/hvtec.ts +46 -0
  21. package/src/parsers/pvtec.ts +71 -0
  22. package/src/parsers/stanza.ts +132 -0
  23. package/src/parsers/text.ts +166 -0
  24. package/src/parsers/ugc.ts +197 -0
  25. package/src/types.ts +251 -0
  26. package/src/utils.ts +314 -0
  27. package/src/xmpp.ts +144 -0
  28. package/test.js +58 -34
  29. package/tsconfig.json +12 -5
  30. package/tsup.config.ts +14 -0
  31. package/bootstrap.ts +0 -122
  32. package/dist/bootstrap.js +0 -153
  33. package/dist/src/events.js +0 -585
  34. package/dist/src/helper.js +0 -463
  35. package/dist/src/stanza.js +0 -147
  36. package/dist/src/text-parser.js +0 -119
  37. package/dist/src/ugc.js +0 -214
  38. package/dist/src/vtec.js +0 -125
  39. package/src/events.ts +0 -394
  40. package/src/helper.ts +0 -298
  41. package/src/stanza.ts +0 -102
  42. package/src/text-parser.ts +0 -120
  43. package/src/ugc.ts +0 -122
  44. package/src/vtec.ts +0 -99
@@ -0,0 +1,3799 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __defProps = Object.defineProperties;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
11
+ var __pow = Math.pow;
12
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
13
+ var __spreadValues = (a, b) => {
14
+ for (var prop in b || (b = {}))
15
+ if (__hasOwnProp.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ if (__getOwnPropSymbols)
18
+ for (var prop of __getOwnPropSymbols(b)) {
19
+ if (__propIsEnum.call(b, prop))
20
+ __defNormalProp(a, prop, b[prop]);
21
+ }
22
+ return a;
23
+ };
24
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
25
+ var __objRest = (source, exclude) => {
26
+ var target = {};
27
+ for (var prop in source)
28
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
29
+ target[prop] = source[prop];
30
+ if (source != null && __getOwnPropSymbols)
31
+ for (var prop of __getOwnPropSymbols(source)) {
32
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
33
+ target[prop] = source[prop];
34
+ }
35
+ return target;
36
+ };
37
+ var __export = (target, all) => {
38
+ for (var name in all)
39
+ __defProp(target, name, { get: all[name], enumerable: true });
40
+ };
41
+ var __copyProps = (to, from, except, desc) => {
42
+ if (from && typeof from === "object" || typeof from === "function") {
43
+ for (let key of __getOwnPropNames(from))
44
+ if (!__hasOwnProp.call(to, key) && key !== except)
45
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
46
+ }
47
+ return to;
48
+ };
49
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
50
+ // If the importer is in node compatibility mode or this is not an ESM
51
+ // file that has been converted to a CommonJS file using a Babel-
52
+ // compatible transform (i.e. "__esModule" has not been set), then set
53
+ // "default" to the CommonJS "module.exports" for node compatibility.
54
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
55
+ mod
56
+ ));
57
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
58
+ var __async = (__this, __arguments, generator) => {
59
+ return new Promise((resolve, reject) => {
60
+ var fulfilled = (value) => {
61
+ try {
62
+ step(generator.next(value));
63
+ } catch (e) {
64
+ reject(e);
65
+ }
66
+ };
67
+ var rejected = (value) => {
68
+ try {
69
+ step(generator.throw(value));
70
+ } catch (e) {
71
+ reject(e);
72
+ }
73
+ };
74
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
75
+ step((generator = generator.apply(__this, __arguments)).next());
76
+ });
77
+ };
78
+
79
+ // src/index.ts
80
+ var index_exports = {};
81
+ __export(index_exports, {
82
+ AlertManager: () => AlertManager,
83
+ Database: () => database_default,
84
+ EAS: () => eas_default,
85
+ EventParser: () => events_default,
86
+ HVtecParser: () => hvtec_default,
87
+ PVtecParser: () => pvtec_default,
88
+ StanzaParser: () => stanza_default,
89
+ TextParser: () => text_default,
90
+ UGCParser: () => ugc_default,
91
+ Utils: () => utils_default,
92
+ default: () => index_default
93
+ });
94
+ module.exports = __toCommonJS(index_exports);
95
+
96
+ // src/bootstrap.ts
97
+ var fs = __toESM(require("fs"));
98
+ var path = __toESM(require("path"));
99
+ var events = __toESM(require("events"));
100
+ var xmpp = __toESM(require("@xmpp/client"));
101
+ var shapefile = __toESM(require("shapefile"));
102
+ var xml2js = __toESM(require("xml2js"));
103
+ var jobs = __toESM(require("croner"));
104
+ var turf = __toESM(require("turf"));
105
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"));
106
+ var import_axios = __toESM(require("axios"));
107
+ var import_crypto = __toESM(require("crypto"));
108
+ var import_os = __toESM(require("os"));
109
+ var import_say = __toESM(require("say"));
110
+ var import_child_process = __toESM(require("child_process"));
111
+
112
+ // src/dictionaries/events.ts
113
+ var EVENTS = {
114
+ "AF": "Ashfall",
115
+ "AS": "Air Stagnation",
116
+ "BH": "Beach Hazard",
117
+ "BW": "Brisk Wind",
118
+ "BZ": "Blizzard",
119
+ "CF": "Coastal Flood",
120
+ "DF": "Debris Flow",
121
+ "DS": "Dust Storm",
122
+ "EC": "Extreme Cold",
123
+ "EH": "Excessive Heat",
124
+ "XH": "Extreme Heat",
125
+ "EW": "Extreme Wind",
126
+ "FA": "Areal Flood",
127
+ "FF": "Flash Flood",
128
+ "FG": "Dense Fog",
129
+ "FL": "Flood",
130
+ "FR": "Frost",
131
+ "FW": "Fire Weather",
132
+ "FZ": "Freeze",
133
+ "GL": "Gale",
134
+ "HF": "Hurricane Force Wind",
135
+ "HT": "Heat",
136
+ "HU": "Hurricane",
137
+ "HW": "High Wind",
138
+ "HY": "Hydrologic",
139
+ "HZ": "Hard Freeze",
140
+ "IS": "Ice Storm",
141
+ "LE": "Lake Effect Snow",
142
+ "LO": "Low Water",
143
+ "LS": "Lakeshore Flood",
144
+ "LW": "Lake Wind",
145
+ "MA": "Special Marine",
146
+ "EQ": "Earthquake",
147
+ "MF": "Dense Fog",
148
+ "MH": "Ashfall",
149
+ "MS": "Dense Smoke",
150
+ "RB": "Small Craft for Rough Bar",
151
+ "RP": "Rip Current Risk",
152
+ "SC": "Small Craft",
153
+ "SE": "Hazardous Seas",
154
+ "SI": "Small Craft for Winds",
155
+ "SM": "Dense Smoke",
156
+ "SQ": "Snow Squall",
157
+ "SR": "Storm",
158
+ "SS": "Storm Surge",
159
+ "SU": "High Surf",
160
+ "SV": "Severe Thunderstorm",
161
+ "SW": "Small Craft for Hazardous Seas",
162
+ "TO": "Tornado",
163
+ "TR": "Tropical Storm",
164
+ "TS": "Tsunami",
165
+ "TY": "Typhoon",
166
+ "SP": "Special Weather",
167
+ "UP": "Heavy Freezing Spray",
168
+ "WC": "Wind Chill",
169
+ "WI": "Wind",
170
+ "WS": "Winter Storm",
171
+ "WW": "Winter Weather",
172
+ "ZF": "Freezing Fog",
173
+ "ZR": "Freezing Rain",
174
+ "ZY": "Freezing Spray"
175
+ };
176
+ var ACTIONS = {
177
+ "W": "Warning",
178
+ "F": "Forecast",
179
+ "A": "Watch",
180
+ "O": "Outlook",
181
+ "Y": "Advisory",
182
+ "N": "Synopsis",
183
+ "S": "Statement"
184
+ };
185
+ var STATUS = {
186
+ "NEW": "Issued",
187
+ "CON": "Updated",
188
+ "EXT": "Extended",
189
+ "EXA": "Extended",
190
+ "EXB": "Extended",
191
+ "UPG": "Upgraded",
192
+ "COR": "Correction",
193
+ "ROU": "Routine",
194
+ "CAN": "Cancelled",
195
+ "EXP": "Expired"
196
+ };
197
+ var TYPES = {
198
+ "O": "Operational Product",
199
+ "T": "Test Product",
200
+ "E": "Experimental Product",
201
+ "X": "Experimental Product (Non-Operational)"
202
+ };
203
+ var STATUS_CORRELATIONS = [
204
+ { type: "Update", forward: "Updated", cancel: false, update: true, new: false },
205
+ { type: "Cancel", forward: "Cancelled", cancel: true, update: false, new: false },
206
+ { type: "Alert", forward: "Issued", cancel: false, update: false, new: true },
207
+ { type: "Updated", forward: "Updated", cancel: false, update: true, new: false },
208
+ { type: "Expired", forward: "Expired", cancel: true, update: false, new: false },
209
+ { type: "Issued", forward: "Issued", cancel: false, update: false, new: true },
210
+ { type: "Extended", forward: "Updated", cancel: false, update: true, new: false },
211
+ { type: "Correction", forward: "Updated", cancel: false, update: true, new: false },
212
+ { type: "Upgraded", forward: "Upgraded", cancel: false, update: true, new: false },
213
+ { type: "Cancelled", forward: "Cancelled", cancel: true, update: false, new: false },
214
+ { type: "Routine", forward: "Routine", cancel: false, update: true, new: false }
215
+ ];
216
+ var CAUSES = {
217
+ "SM": "Snow Melt",
218
+ "RS": "Rain/Snow Melt",
219
+ "ER": "Excessive Rain",
220
+ "DM": "Dam/Levee Failure",
221
+ "IJ": "Ice Jam",
222
+ "GO": "Glacier Lake Outburst",
223
+ "IC": "Ice",
224
+ "FS": "Flash Flood / Storm Surge",
225
+ "FT": "Tidal Effects",
226
+ "ET": "Elevated Upstream Flow",
227
+ "MC": "Other Multiple Causes",
228
+ "WT": "Wind and/or Tidal Effects",
229
+ "DR": "Reservoir Release",
230
+ "UU": "Unknown",
231
+ "OT": "Other Effects"
232
+ };
233
+ var RECORDS = {
234
+ "NO": "No Record Expected",
235
+ "NR": "Near Record or possible record",
236
+ "UU": "Unknown history of records",
237
+ "OO": "Other"
238
+ };
239
+ var SEVERITY = {
240
+ N: "Not Expected",
241
+ 0: "Areal Flood or FF Product",
242
+ 1: "Minor",
243
+ 2: "Moderate",
244
+ 3: "Major",
245
+ U: "Unknown"
246
+ };
247
+
248
+ // src/dictionaries/offshore.ts
249
+ var OFFSHORE = {
250
+ "Special Weather Statement": "Special Weather Statement",
251
+ "Hurricane Warning": "Hurricane Warning",
252
+ "Hurricane Force Wind Warning": "Hurricane Force Wind Warning",
253
+ "Hurricane Watch": "Hurricane Watch",
254
+ "Tropical Storm Warning": "Tropical Storm Warning",
255
+ "Tropical Storm Watch": "Tropical Storm Watch",
256
+ "High Wind Warning": "High Wind Warning",
257
+ "Gale Warning": "Gale Warning",
258
+ "Small Craft Advisory": "Small Craft Advisory",
259
+ "Small Craft Warning": "Small Craft Warning"
260
+ };
261
+
262
+ // src/dictionaries/awips.ts
263
+ var AWIPS = {
264
+ ABV: `rawinsonde-data-above-100-millibars`,
265
+ ADA: `alarm-alert-administrative-message`,
266
+ ADM: `alert-administrative-message`,
267
+ ADR: `nws-administrative-message`,
268
+ ADV: `space-environment-advisory`,
269
+ AFD: `area-forecast-discussion`,
270
+ AFM: `area-forecast-matrices`,
271
+ AFP: `area-forecast-product`,
272
+ AFW: `fire-weather-matrix`,
273
+ AGF: `agricultural-forecast`,
274
+ AGO: `agricultural-observations`,
275
+ ALT: `space-environment-alert`,
276
+ AQA: `air-quality-alert`,
277
+ AQI: `air-quality-index-statement`,
278
+ ASA: `air-stagnation-advisory`,
279
+ AVA: `avalanche-watch`,
280
+ AVG: `avalanche-weather-guidance`,
281
+ AVW: `avalanche-warning`,
282
+ AWO: `area-weather-outlook`,
283
+ AWS: `area-weather-summary`,
284
+ AWU: `area-weather-update`,
285
+ AWW: `airport-weather-warning`,
286
+ BLU: `blue-alert`,
287
+ BOY: `buoy-report`,
288
+ BRG: `coast-guard-observations`,
289
+ BRT: `hourly-roundup-for-weather-radio`,
290
+ CAE: `child-abduction-emergency`,
291
+ CCF: `coded-city-forecast`,
292
+ CDW: `civil-danger-warning`,
293
+ CEM: `civil-emergency-message`,
294
+ CF6: `monthly-daily-climate-data`,
295
+ CFP: `convective-forecast-product`,
296
+ CFW: `coastal-flood-warnings-watches-statements`,
297
+ CGR: `coast-guard-surface-report`,
298
+ CHG: `computer-hurricane-guidance`,
299
+ CLA: `climatological-report-annual`,
300
+ CLI: `climatological-report-daily`,
301
+ CLM: `climatological-report-monthly`,
302
+ CLQ: `climatological-report-quarterly`,
303
+ CLS: `climatological-report-seasonal`,
304
+ CLT: `climate-report`,
305
+ CMM: `coded-climatological-monthly-means`,
306
+ COD: `coded-analysis-and-forecasts`,
307
+ CPF: `great-lakes-port-forecast`,
308
+ CUR: `space-environment-products-routine`,
309
+ CWA: `center-weather-advisory`,
310
+ CWF: `coastal-waters-forecast`,
311
+ CWS: `center-weather-statement`,
312
+ DAY: `space-environment-product-daily`,
313
+ DDO: `daily-dispersion-outlook`,
314
+ DGT: `drought-information-statement`,
315
+ DMO: `practice-demo-warning`,
316
+ DSA: `unnumbered-depression-advisory`,
317
+ DSM: `asos-daily-summary`,
318
+ DSW: `dust-storm-warning`,
319
+ EFP: `extended-forecast-3-to-5-day`,
320
+ EOL: `six-to-ten-day-weather-outlook-local`,
321
+ EQI: `tsunami-bulletin`,
322
+ EQR: `earthquake-report`,
323
+ EQW: `earthquake-warning`,
324
+ ESF: `flood-potential-outlook`,
325
+ ESG: `extended-streamflow-guidance`,
326
+ ESP: `extended-streamflow-prediction`,
327
+ ESS: `water-supply-outlook`,
328
+ EVI: `evacuation-immediate`,
329
+ EWW: `extreme-wind-warning`,
330
+ FA0: `aviation-area-forecast-pacific`,
331
+ FA1: `aviation-area-forecast-northeast`,
332
+ FA2: `aviation-area-forecast-southeast`,
333
+ FA3: `aviation-area-forecast-north-central`,
334
+ FA4: `aviation-area-forecast-south-central`,
335
+ FA5: `aviation-area-forecast-rocky-mountains`,
336
+ FA6: `aviation-area-forecast-west-coast`,
337
+ FA7: `aviation-area-forecast-juneau-ak`,
338
+ FA8: `aviation-area-forecast-anchorage-ak`,
339
+ FA9: `aviation-area-forecast-fairbanks-ak`,
340
+ FD0: `winds-aloft-forecast-24hr-high-altitude`,
341
+ FD1: `winds-aloft-forecast-6hr`,
342
+ FD2: `winds-aloft-forecast-12hr`,
343
+ FD3: `winds-aloft-forecast-24hr`,
344
+ FD4: `winds-aloft-forecast`,
345
+ FD5: `winds-aloft-forecast`,
346
+ FD6: `winds-aloft-forecast`,
347
+ FD7: `winds-aloft-forecast`,
348
+ FD8: `winds-aloft-forecast-6hr-high-altitude`,
349
+ FD9: `winds-aloft-forecast-12hr-high-altitude`,
350
+ FDI: `fire-danger-indices`,
351
+ FFA: `flash-flood-watch`,
352
+ FFG: `flash-flood-guidance`,
353
+ FFH: `headwater-guidance`,
354
+ FFS: `flash-flood-statement`,
355
+ FFW: `flash-flood-warning`,
356
+ FLN: `national-flood-summary`,
357
+ FLS: `flood-statement`,
358
+ FLW: `flood-warning`,
359
+ FOF: `upper-wind-fallout-forecast`,
360
+ FRW: `fire-warning`,
361
+ FSH: `marine-fisheries-service-message`,
362
+ FTM: `radar-outage-notification`,
363
+ FTP: `temp-pop-guidance`,
364
+ FWA: `fire-weather-administrative-message`,
365
+ FWD: `fire-weather-outlook-discussion`,
366
+ FWF: `fire-weather-forecast`,
367
+ FWL: `land-management-forecast`,
368
+ FWM: `miscellaneous-fire-weather-product`,
369
+ FWN: `fire-weather-notification`,
370
+ FWO: `fire-weather-observation`,
371
+ FWS: `fire-weather-spot-forecast`,
372
+ FZL: `freezing-level-data`,
373
+ GLF: `great-lakes-forecast`,
374
+ GLS: `great-lakes-storm-summary`,
375
+ GRE: `green`,
376
+ HD1: `rfc-qpf-data-product`,
377
+ HD2: `rfc-qpf-data-product`,
378
+ HD3: `rfc-qpf-data-product`,
379
+ HD4: `rfc-qpf-data-product`,
380
+ HD7: `rfc-qpf-data-product`,
381
+ HD8: `rfc-qpf-data-product`,
382
+ HD9: `rfc-qpf-data-product`,
383
+ HLS: `hurricane-local-statement`,
384
+ HMD: `hydrometeorological-discussion`,
385
+ HML: `ahps-xml-product`,
386
+ HMW: `hazardous-materials-warning`,
387
+ HP1: `rfc-qpf-verification-product`,
388
+ HP2: `rfc-qpf-verification-product`,
389
+ HP3: `rfc-qpf-verification-product`,
390
+ HP4: `rfc-qpf-verification-product`,
391
+ HP5: `rfc-qpf-verification-product`,
392
+ HP6: `rfc-qpf-verification-product`,
393
+ HP7: `rfc-qpf-verification-product`,
394
+ HP8: `rfc-qpf-verification-product`,
395
+ HRR: `weather-roundup`,
396
+ HSF: `high-seas-forecast`,
397
+ HWO: `hazardous-weather-outlook`,
398
+ HWR: `hourly-weather-roundup`,
399
+ HYD: `daily-hydrometeorological-products`,
400
+ HYM: `monthly-hydrometeorological-product`,
401
+ ICE: `ice-forecast`,
402
+ IDM: `ice-drift-vectors`,
403
+ INI: `administrative-message`,
404
+ IOB: `ice-observation`,
405
+ KPA: `keep-alive-message`,
406
+ LAE: `local-area-emergency`,
407
+ LCD: `preliminary-local-climatological-data`,
408
+ LCO: `local-cooperative-observation`,
409
+ LEW: `law-enforcement-warning`,
410
+ LFP: `local-forecast`,
411
+ LKE: `lake-stages`,
412
+ LLS: `low-level-sounding`,
413
+ LOW: `low-temperatures`,
414
+ LSR: `local-storm-report`,
415
+ LTG: `lightning-data`,
416
+ MAN: `rawinsonde-mandatory-levels`,
417
+ MAP: `mean-areal-precipitation`,
418
+ MAW: `amended-marine-forecast`,
419
+ MFM: `marine-forecast-matrix`,
420
+ MIM: `marine-interpretation-message`,
421
+ MIS: `miscellaneous-local-product`,
422
+ MOB: `marine-observations`,
423
+ MON: `space-environment-product-monthly`,
424
+ MRP: `marine-product-techniques-development`,
425
+ MSM: `asos-monthly-summary-message`,
426
+ MTR: `metar-observation`,
427
+ MTT: `metar-test-message`,
428
+ MVF: `marine-verification-coded-message`,
429
+ MWS: `marine-weather-statement`,
430
+ MWW: `marine-weather-message`,
431
+ NOU: `weather-reconnaissance-flights`,
432
+ NOW: `short-term-forecast`,
433
+ NOX: `data-management-message`,
434
+ NPW: `non-precipitation-warning`,
435
+ NSH: `nearshore-marine-forecast`,
436
+ NUW: `nuclear-power-plant-warning`,
437
+ NWR: `noaa-weather-radio-forecast`,
438
+ OAV: `other-aviation-products`,
439
+ OBS: `observations`,
440
+ OFA: `offshore-aviation-forecast`,
441
+ OFF: `offshore-forecast`,
442
+ OMR: `other-marine-products`,
443
+ OPU: `other-public-products`,
444
+ OSO: `other-surface-observations`,
445
+ OSW: `ocean-surface-winds`,
446
+ OUA: `other-upper-air-data`,
447
+ OZF: `zone-forecast`,
448
+ PFM: `point-forecast-matrices`,
449
+ PFW: `fire-weather-point-forecast-matrices`,
450
+ PLS: `plain-language-ship-report`,
451
+ PMD: `prognostic-meteorological-discussion`,
452
+ PNS: `public-information-statement`,
453
+ POE: `probability-of-exceedance`,
454
+ PRB: `heat-index-forecast-tables`,
455
+ PRC: `pilot-report-collective`,
456
+ PRE: `preliminary-forecasts`,
457
+ PSH: `post-storm-hurricane-report`,
458
+ PTS: `probabilistic-outlook-points`,
459
+ PWO: `public-severe-weather-outlook`,
460
+ PWS: `tropical-cyclone-probabilities`,
461
+ QPF: `quantitative-precipitation-forecast`,
462
+ QPS: `quantitative-precipitation-statement`,
463
+ RDF: `revised-digital-forecast`,
464
+ REC: `recreational-report`,
465
+ RER: `record-report`,
466
+ RET: `eas-activation-request`,
467
+ RFD: `rangeland-fire-danger-forecast`,
468
+ RFI: `rfi-observation`,
469
+ RFR: `route-forecast`,
470
+ RFW: `red-flag-warning`,
471
+ RHW: `radiological-hazard-warning`,
472
+ RMT: `required-monthly-test`,
473
+ RNS: `rain-information-statement`,
474
+ RR1: `hydro-met-data-report-part-1`,
475
+ RR2: `hydro-met-data-report-part-2`,
476
+ RR3: `hydro-met-data-report-part-3`,
477
+ RR4: `hydro-met-data-report-part-4`,
478
+ RR5: `hydro-met-data-report-part-5`,
479
+ RR6: `hydro-met-data-report-part-6`,
480
+ RR7: `hydro-met-data-report-part-7`,
481
+ RR8: `hydro-met-data-report-part-8`,
482
+ RR9: `hydro-met-data-report-part-9`,
483
+ RRA: `automated-hydrologic-observation-report`,
484
+ RRM: `miscellaneous-hydrologic-data`,
485
+ RRS: `hads-data`,
486
+ RRY: `asos-hourly-test-message`,
487
+ RSD: `daily-snotel-data`,
488
+ RSM: `monthly-snotel-data`,
489
+ RTP: `regional-temp-precip-table`,
490
+ RVA: `river-summary`,
491
+ RVD: `daily-river-forecast`,
492
+ RVF: `river-forecast`,
493
+ RVI: `river-ice-statement`,
494
+ RVM: `miscellaneous-river-product`,
495
+ RVR: `river-recreation-statement`,
496
+ RVS: `river-statement`,
497
+ RWR: `regional-weather-roundup`,
498
+ RWS: `regional-weather-summary`,
499
+ RWT: `required-weekly-test`,
500
+ SAB: `special-avalanche-bulletin`,
501
+ SAF: `agricultural-weather-forecast`,
502
+ SAG: `snow-avalanche-guidance`,
503
+ SAT: `apt-prediction`,
504
+ SAW: `preliminary-notice-of-watch`,
505
+ SCC: `storm-summary`,
506
+ SCD: `supplementary-climatological-data`,
507
+ SCN: `soil-climate-analysis-network`,
508
+ SCP: `satellite-cloud-product`,
509
+ SCS: `selected-cities-summary`,
510
+ SDO: `supplementary-data-observation`,
511
+ SDS: `special-dispersion-statement`,
512
+ SEL: `severe-local-storm-watch`,
513
+ SEV: `spc-watch-point-information`,
514
+ SFP: `state-forecast`,
515
+ SFT: `tabular-state-forecast`,
516
+ SGL: `rawinsonde-significant-levels`,
517
+ SHP: `surface-ship-report`,
518
+ SIG: `international-sigmet`,
519
+ SIM: `satellite-interpretation-message`,
520
+ SLS: `severe-local-storm-outline`,
521
+ SMF: `smoke-management-weather-forecast`,
522
+ SMW: `special-marine-warning`,
523
+ SOO: `science-operations-officer-product`,
524
+ SPE: `satellite-precipitation-estimates`,
525
+ SPF: `storm-strike-probability-bulletin`,
526
+ SPS: `special-weather-statement`,
527
+ SPW: `shelter-in-place-warning`,
528
+ SQW: `snow-squall-warning`,
529
+ SRD: `surf-discussion`,
530
+ SRF: `surf-forecast`,
531
+ SRG: `soaring-guidance`,
532
+ SSM: `synoptic-surface-observation`,
533
+ STA: `weather-statistical-summary`,
534
+ STD: `satellite-tropical-disturbance-summary`,
535
+ STO: `road-condition-report`,
536
+ STP: `state-temp-precip-table`,
537
+ STQ: `spot-forecast-request`,
538
+ SUM: `space-weather-message`,
539
+ SVR: `severe-thunderstorm-warning`,
540
+ SVS: `severe-weather-statement`,
541
+ SWO: `severe-storm-outlook`,
542
+ SWS: `state-weather-summary`,
543
+ SYN: `regional-weather-synopsis`,
544
+ TAF: `terminal-aerodrome-forecast`,
545
+ TAP: `terminal-alerting-products`,
546
+ TAV: `travelers-forecast-table`,
547
+ TCA: `tropical-cyclone-advisory`,
548
+ TCD: `tropical-cyclone-discussion`,
549
+ TCE: `tropical-cyclone-position-estimate`,
550
+ TCM: `tropical-cyclone-marine-aviation-advisory`,
551
+ TCP: `public-tropical-cyclone-advisory`,
552
+ TCS: `satellite-tropical-cyclone-summary`,
553
+ TCU: `tropical-cyclone-update`,
554
+ TCV: `tropical-cyclone-break-points`,
555
+ TIB: `tsunami-bulletin`,
556
+ TID: `tide-report`,
557
+ TMA: `tsunami-tide-seismic-acknowledgement`,
558
+ TOE: `telephone-outage-emergency`,
559
+ TOR: `tornado-warning`,
560
+ TPT: `temperature-precipitation-table`,
561
+ TSU: `tsunami-watch-warning`,
562
+ TUV: `ultraviolet-index`,
563
+ TVL: `travelers-forecast`,
564
+ TWB: `transcribed-weather-broadcast`,
565
+ TWD: `tropical-weather-discussion`,
566
+ TWO: `tropical-weather-outlook`,
567
+ TWS: `tropical-weather-summary`,
568
+ URN: `aircraft-reconnaissance`,
569
+ UVI: `ultraviolet-index`,
570
+ VAA: `volcanic-activity-advisory`,
571
+ VER: `forecast-verification-statistics`,
572
+ VFT: `taf-verification-product`,
573
+ VOW: `volcano-warning`,
574
+ WA0: `airmet-pacific`,
575
+ WA1: `airmet-northeast`,
576
+ WA2: `airmet-southeast`,
577
+ WA3: `airmet-north-central`,
578
+ WA4: `airmet-south-central`,
579
+ WA5: `airmet-rocky-mountains`,
580
+ WA6: `airmet-west-coast`,
581
+ WA7: `airmet-juneau-ak`,
582
+ WA8: `airmet-anchorage-ak`,
583
+ WA9: `airmet-fairbanks-ak`,
584
+ WAR: `space-environment-warning`,
585
+ WAT: `space-environment-watch`,
586
+ WCN: `weather-watch-clearance-notification`,
587
+ WCR: `weekly-weather-and-crop-report`,
588
+ WDA: `weekly-data-for-agriculture`,
589
+ WDU: `warning-decision-update`,
590
+ WEK: `space-environment-product-weekly`,
591
+ WOU: `watch-outline-update`,
592
+ WS1: `sigmet-northeast`,
593
+ WS2: `sigmet-southeast`,
594
+ WS3: `sigmet-north-central`,
595
+ WS4: `sigmet-south-central`,
596
+ WS5: `sigmet-rocky-mountains`,
597
+ WS6: `sigmet-west-coast`,
598
+ WST: `tropical-cyclone-sigmet`,
599
+ WSV: `volcanic-activity-sigmet`,
600
+ WSW: `winter-weather-warning`,
601
+ WWA: `watch-status-report`,
602
+ WWP: `watch-probabilities`,
603
+ ZFP: `zone-forecast-product`
604
+ };
605
+
606
+ // src/dictionaries/signatures.ts
607
+ var TAGS = {
608
+ "LICKELY BECOME SLICK AND HAZARDOUS": "Slick and Hazardous Roads",
609
+ "SLIPPERY ROAD CONDITIONS": "Slippery Roads",
610
+ "BLOWING SNOW WHICH COULD REDUCE VISIBILITY": "Blowing Snow Reducing Visibility",
611
+ "TRAVEL COULD BE VERY DIFFICULT": "Difficult Travel Conditions",
612
+ "DIFFICULT TRAVEL CONDITIONS": "Difficult Travel Conditions",
613
+ "EXPECT DISRUPTIONS": "Expect Disruptions to Travel",
614
+ "A LARGE AND EXTREMELY DANGEROUS TORNADO": "Large and Dangerous Tornado",
615
+ "THIS IS A PARTICULARLY DANGEROUS SITUATION": "Particularly Dangerous Situation",
616
+ "RADAR INDICATED ROTATION": "Radar Indicated Tornado",
617
+ "WEATHER SPOTTERS CONFIRMED TORNADO": "Confirmed by Storm Spotters",
618
+ "A SEVERE THUNDERSTORM CAPABLE OF PRODUCING A TORNADO": "Developing Tornado",
619
+ "LAW ENFORCEMENT CONFIRMED TORNADO": "Reported by Law Enforcement",
620
+ "A TORNADO IS ON THE GROUND": "Confirmed Tornado",
621
+ "WEATHER SPOTTERS REPORTED FUNNEL CLOUD": "Confirmed Funnel Cloud by Storm Spotters",
622
+ "PUBLIC CONFIRMED TORNADO": "Public reports of Tornado",
623
+ "RADAR CONFIRMED": "Radar Confirmed",
624
+ "TORNADO WAS REPORTED BRIEFLY ON THE GROUND": "Tornado no longer on ground",
625
+ "SPOTTERS INDICATE THAT A FUNNEL CLOUD CONTINUES WITH THIS STORM": "Funnel Cloud Continues",
626
+ "A TORNADO MAY DEVELOP AT ANY TIME": "Potentional still exists for Tornado to form",
627
+ "LIFE-THREATENING SITUATION": "Life Threating Situation",
628
+ "COMPLETE DESTRUCTION IS POSSIBLE": "Extremly Damaging Tornado",
629
+ "POTENTIALLY DEADLY TORNADO": "Deadly Tornado",
630
+ "RADAR INDICATED": "Radar Indicated",
631
+ "HAIL DAMAGE TO VEHICLES IS EXPECTED": "Damaging to Vehicles",
632
+ "EXPECT WIND DAMAGE": "Wind Damage",
633
+ "FREQUENT LIGHTNING": "Frequent Lightning",
634
+ "PEOPLE AND ANIMALS OUTDOORS WILL BE INJURED": "Capable of Injuring People and Animals",
635
+ "TRAINED WEATHER SPOTTERS": "Confirmed by Storm Spotters",
636
+ "SOURCE...PUBLIC": "Confirmed by Public",
637
+ "SMALL CRAFT COULD BE DAMAGED": "Potential Damage to Small Craft",
638
+ "A TORNADO WATCH REMAINS IN EFFECT": "Active Tornado Watch",
639
+ "TENNIS BALL SIZE HAIL": "Tennis Ball Size Hail",
640
+ "BASEBALL SIZE HAIL": "Baseball Size Hail",
641
+ "GOLF BALL SIZE HAIL": "Golf Ball Size Hail",
642
+ "QUARTER SIZE HAIL": "Quarter Size Hail",
643
+ "PING PONG BALL SIZE HAIL": "Ping Pong Ball Size Hail",
644
+ "NICKEL SIZE HAIL": "Nickel Size Hail",
645
+ "DOPPLER RADAR.": "Confirmed by Radar",
646
+ "DOPPLER RADAR AND AUTOMATED GAUGES.": "Confirmed by Radar and Gauges",
647
+ "FLASH FLOODING CAUSED BY THUNDERSTORMS.": "Caused by Thunderstorm",
648
+ "SOURCE...EMERGENCY MANAGEMENT.": "Confirmed by Emergency Management",
649
+ "FLASH FLOODING CAUSED BY HEAVY RAIN.": "Caused by heavy rain",
650
+ "SOURCE...LAW ENFORCEMENT REPORTED.": "Confirmed by Law Enforcement"
651
+ };
652
+ var CANCEL_SIGNATURES = [
653
+ "THIS_MESSAGE_IS_FOR_TEST_PURPOSES_ONLY",
654
+ "this is a test",
655
+ "subsided sufficiently for the advisory to be cancelled",
656
+ "has been cancelled",
657
+ "will be allowed to expire",
658
+ "has diminished",
659
+ "and no longer",
660
+ "has been replaced",
661
+ "The threat has ended",
662
+ "has weakened below severe"
663
+ ];
664
+ var MESSAGE_SIGNATURES = [
665
+ { regex: /\*/g, replacement: "." },
666
+ { regex: /\bUTC\b/g, replacement: "Coordinated Universal Time" },
667
+ { regex: /\bGMT\b/g, replacement: "Greenwich Mean Time" },
668
+ { regex: /\bEST\b(?!\w)/g, replacement: "Eastern Standard Time" },
669
+ { regex: /\bEDT\b(?!\w)/g, replacement: "Eastern Daylight Time" },
670
+ { regex: /\bCST\b(?!\w)/g, replacement: "Central Standard Time" },
671
+ { regex: /\bCDT\b(?!\w)/g, replacement: "Central Daylight Time" },
672
+ { regex: /\bMST\b(?!\w)/g, replacement: "Mountain Standard Time" },
673
+ { regex: /\bMDT\b(?!\w)/g, replacement: "Mountain Daylight Time" },
674
+ { regex: /\bPST\b(?!\w)/g, replacement: "Pacific Standard Time" },
675
+ { regex: /\bPDT\b(?!\w)/g, replacement: "Pacific Daylight Time" },
676
+ { regex: /\bAKST\b(?!\w)/g, replacement: "Alaska Standard Time" },
677
+ { regex: /\bAKDT\b(?!\w)/g, replacement: "Alaska Daylight Time" },
678
+ { regex: /\bHST\b(?!\w)/g, replacement: "Hawaii Standard Time" },
679
+ { regex: /\bHDT\b(?!\w)/g, replacement: "Hawaii Daylight Time" },
680
+ { regex: /\bmph\b(?!\w)/g, replacement: "miles per hour" },
681
+ { regex: /\bkm\/h\b(?!\w)/g, replacement: "kilometers per hour" },
682
+ { regex: /\bkmh\b(?!\w)/g, replacement: "kilometers per hour" },
683
+ { regex: /\bkt\b(?!\w)/g, replacement: "knots" },
684
+ { regex: /\bNE\b(?!\w)/g, replacement: "northeast" },
685
+ { regex: /\bNW\b(?!\w)/g, replacement: "northwest" },
686
+ { regex: /\bSE\b(?!\w)/g, replacement: "southeast" },
687
+ { regex: /\bSW\b(?!\w)/g, replacement: "southwest" },
688
+ { regex: /\bNM\b(?!\w)/g, replacement: "nautical miles" },
689
+ { regex: /\bdeg\b(?!\w)/g, replacement: "degrees" },
690
+ { regex: /\btstm\b(?!\w)/g, replacement: "thunderstorm" },
691
+ { regex: /\bmm\b(?!\w)/g, replacement: "millimeters" },
692
+ { regex: /\bcm\b(?!\w)/g, replacement: "centimeters" },
693
+ { regex: /\bin.\b(?!\w)/g, replacement: "inches" },
694
+ { regex: /\bft\b(?!\w)/g, replacement: "feet" },
695
+ { regex: /\bmi\b(?!\w)/g, replacement: "miles" },
696
+ { regex: /\bhr\b(?!\w)/g, replacement: "hour" },
697
+ { regex: /\bhourly\b(?!\w)/g, replacement: "per hour" },
698
+ { regex: /\bkg\b(?!\w)/g, replacement: "kilograms" },
699
+ { regex: /\bg\/kg\b(?!\w)/g, replacement: "grams per kilogram" },
700
+ { regex: /\bmb\b(?!\w)/g, replacement: "millibars" },
701
+ { regex: /\bhPa\b(?!\w)/g, replacement: "hectopascals" },
702
+ { regex: /\bPa\b(?!\w)/g, replacement: "pascals" },
703
+ { regex: /\bKPa\b(?!\w)/g, replacement: "kilopascals" },
704
+ { regex: /\bC\/hr\b(?!\w)/g, replacement: "degrees Celsius per hour" },
705
+ { regex: /\bF\/hr\b(?!\w)/g, replacement: "degrees Fahrenheit per hour" },
706
+ { regex: /\bC\/min\b(?!\w)/g, replacement: "degrees Celsius per minute" },
707
+ { regex: /\bF\/min\b(?!\w)/g, replacement: "degrees Fahrenheit per minute" },
708
+ { regex: /\bC\b(?!\w)/g, replacement: "degrees Celsius" },
709
+ { regex: /\bF\b(?!\w)/g, replacement: "degrees Fahrenheit" }
710
+ ];
711
+
712
+ // src/dictionaries/icao.ts
713
+ var ICAOs = {
714
+ "KLCH": "Lake Charles, LA",
715
+ "TSTL": "St. Louis, MO",
716
+ "PABC": "Bethel, AK",
717
+ "TCMH": "Columbus, OH",
718
+ "KEPZ": "El Paso, TX",
719
+ "KCYS": "Cheyenne, WY",
720
+ "KJKL": "Jackson, KY",
721
+ "KPAH": "Paducah, KY",
722
+ "KEMX": "Tucson, AZ",
723
+ "KMHX": "Morehead City, NC",
724
+ "PAPD": "Fairbanks, AK",
725
+ "KDLH": "Duluth, MN",
726
+ "TADW": "Andrews Air Force Base, MD",
727
+ "KOKX": "Brookhaven, NY",
728
+ "KLZK": "Little Rock, AR",
729
+ "KHGX": "Houston, TX",
730
+ "TMSY": "New Orleans, LA",
731
+ "KDGX": "Jackson/Brandon, MS",
732
+ "KCTP": "Caribou, ME",
733
+ "KAMA": "Amarillo, TX",
734
+ "PGUA": "Andersen AFB, GU",
735
+ "KAPX": "Gaylord, MI",
736
+ "PAHG": "Kenai, AK",
737
+ "KLWX": "Sterling, VA",
738
+ "HWPA2": "Homer, AK",
739
+ "KGRK": "Fort Hood, TX",
740
+ "KAKQ": "Wakefield, VA",
741
+ "ROCO2": "Norman, OK",
742
+ "KCLX": "Charleston, SC",
743
+ "TPHX": "Phoenix, AZ",
744
+ "KNKX": "San Diego, CA",
745
+ "TDEN": "Denver, CO",
746
+ "TLAS": "Las Vegas, NV",
747
+ "KBUF": "Buffalo, NY",
748
+ "KTLX": "Norman, OK",
749
+ "KILX": "Lincoln, IL",
750
+ "KHDC": "Hammond, LA",
751
+ "KVWX": "Evansville, IN",
752
+ "TCLT": "Charlotte, NC",
753
+ "TEWR": "Newark, NJ",
754
+ "KFSD": "Sioux Falls, SD",
755
+ "KEAX": "Pleasant Hill, MO",
756
+ "KICX": "Cedar City, UT",
757
+ "KHTX": "Huntsville, AL",
758
+ "PACG": "Sitka, AK",
759
+ "KSOX": "Santa Ana Mountains, CA",
760
+ "TPBI": "West Palm Beach, FL",
761
+ "TSLC": "Salt Lake City, UT",
762
+ "KGLD": "Goodland, KS",
763
+ "TRDU": "Raleigh-Durham, NC",
764
+ "KATX": "Seattle, WA",
765
+ "TICH": "Wichita, KS",
766
+ "TSDF": "Louisville, KY",
767
+ "TBOS": "Boston, MA",
768
+ "TDCA": "Washington, DC",
769
+ "KUEX": "Grand Island, NE",
770
+ "TLKA2": "Talkeetna, AK",
771
+ "KBGM": "Binghamton, NY",
772
+ "TLVE": "Cleveland, OH",
773
+ "KCAE": "Columbia, SC",
774
+ "KDVN": "Quad Cities, IA",
775
+ "KABR": "Aberdeen, SD",
776
+ "KBYX": "Key West, FL",
777
+ "KMPX": "Minneapolis, MN",
778
+ "KCRP": "Corpus Christi, TX",
779
+ "KCBW": "Caribou, ME",
780
+ "KMRX": "Knoxville, TN",
781
+ "KSHV": "Shreveport, LA",
782
+ "KIWA": "Phoenix, AZ",
783
+ "KRGX": "Reno, NV",
784
+ "PHKM": "Kamuela, HI",
785
+ "KABX": "Albuquerque, NM",
786
+ "KBMX": "Birmingham, AL",
787
+ "TMDW": "Chicago Midway, IL",
788
+ "KVAX": "Moody AFB, GA",
789
+ "KHDX": "Holloman AFB, NM",
790
+ "KBRO": "Brownsville, TX",
791
+ "KTWX": "Topeka, KS",
792
+ "KRTX": "Portland, OR",
793
+ "KCXX": "Burlington, VT",
794
+ "KFCX": "Roanoke, VA",
795
+ "KFFC": "Atlanta, GA",
796
+ "KBOX": "Boston, MA",
797
+ "KTLH": "Tallahassee, FL",
798
+ "KPUX": "Pueblo, CO",
799
+ "KFDR": "Altus AFB, OK",
800
+ "KGJX": "Grand Junction, CO",
801
+ "KDTX": "Detroit, MI",
802
+ "PHWA": "Waimea, HI",
803
+ "KMQT": "Marquette, MI",
804
+ "KSJT": "San Angelo, TX",
805
+ "KUDX": "Rapid City, SD",
806
+ "TIAH": "Houston, TX",
807
+ "KSRX": "Fort Smith, AR",
808
+ "TJFK": "New York City, NY",
809
+ "KDDC": "Dodge City, KS",
810
+ "PAKC": "King Salmon, AK",
811
+ "PAIH": "Middleton Island, AK",
812
+ "RODN": "Kadena AB, JA",
813
+ "TBWI": "Baltimore/Washington, MD",
814
+ "KIWX": "Northern Indiana, IN",
815
+ "KFDX": "Cannon AFB, NM",
816
+ "TMIA": "Miami, FL",
817
+ "KICT": "Wichita, KS",
818
+ "TMKE": "Milwaukee, WI",
819
+ "TFLL": "Fort Lauderdale, FL",
820
+ "KARX": "La Crosse, WI",
821
+ "KLRX": "Elko, NV",
822
+ "KDAX": "Sacramento, CA",
823
+ "KGRB": "Green Bay, WI",
824
+ "KLGX": "Langley Hill, WA",
825
+ "KFTG": "Denver, CO",
826
+ "KMKX": "Milwaukee, WI",
827
+ "TTUL": "Tulsa, OK",
828
+ "TDFW": "Dallas/Fort Worth, TX",
829
+ "TTPA": "Tampa Bay, FL",
830
+ "TDAL": "Dallas Love Field, TX",
831
+ "KDFX": "Laughlin AFB, TX",
832
+ "KSFX": "Pocatello, ID",
833
+ "KMTX": "Salt Lake City, UT",
834
+ "PAEC": "Nome, AK",
835
+ "RKSG": "Camp Humphreys, KR",
836
+ "KOAX": "Omaha, NE",
837
+ "PHMO": "Molokai, HI",
838
+ "TDTW": "Detroit, MI",
839
+ "THOU": "Houston, TX",
840
+ "AWPA2": "Anchorage, AK",
841
+ "KTYX": "Fort Drum, NY",
842
+ "KCCX": "State College, PA",
843
+ "TMSP": "Minneapolis, MN",
844
+ "KMVX": "Grand Forks, ND",
845
+ "KBIS": "Bismarck, ND",
846
+ "KBBX": "Beale AFB, CA",
847
+ "KVBX": "Vandenberg AFB, CA",
848
+ "KPOE": "Fort Polk, LA",
849
+ "KMOB": "Mobile, AL",
850
+ "KJGX": "Robins AFB, GA",
851
+ "KMUX": "San Francisco, CA",
852
+ "TMCI": "Kansas City, MO",
853
+ "KLSX": "St. Louis, MO",
854
+ "KMAX": "Medford, OR",
855
+ "KRAX": "Raleigh/Durham, NC",
856
+ "KINX": "Tulsa, OK",
857
+ "RKJK": "Kunsan AB, KR",
858
+ "KSGF": "Springfield, MO",
859
+ "TDAY": "Dayton, OH",
860
+ "KDOX": "Dover AFB, DE",
861
+ "KGGW": "Glasgow, MT",
862
+ "KAMX": "Miami, FL",
863
+ "KENX": "Albany, NY",
864
+ "KTFX": "Great Falls, MT",
865
+ "KPBZ": "Pittsburgh, PA",
866
+ "KMAF": "Midland/Odessa, TX",
867
+ "KPDT": "Pendleton, OR",
868
+ "KLNX": "North Platte, NE",
869
+ "KEOX": "Fort Rucker, AL",
870
+ "KGSP": "Greer, SC",
871
+ "KHPX": "Fort Campbell, KY",
872
+ "KGRR": "Grand Rapids, MI",
873
+ "KLOT": "Chicago, IL",
874
+ "TPIT": "Pittsburgh, PA",
875
+ "KEYX": "Edwards AFB, CA",
876
+ "TIAD": "Dulles, VA",
877
+ "KFWS": "Dallas/Fort Worth, TX",
878
+ "KMLB": "Melbourne, FL",
879
+ "KMBX": "Minot AFB, ND",
880
+ "KDMX": "Des Moines, IA",
881
+ "KEVX": "Eglin AFB, FL",
882
+ "TBNA": "Nashville, TN",
883
+ "KDYX": "Dyess AFB, TX",
884
+ "TOKC": "Oklahoma City, OK",
885
+ "PHKI": "South Kauai, HI",
886
+ "TMCO": "Orlando, FL",
887
+ "KDIX": "Philadelphia, PA",
888
+ "TORD": "Chicago, IL",
889
+ "KYUX": "Yuma, AZ",
890
+ "KVNX": "Vance AFB, OK",
891
+ "TJUA": "San Juan, PR",
892
+ "TATL": "Atlanta, GA",
893
+ "KVTX": "Los Angeles, CA",
894
+ "KIND": "Indianapolis, IN",
895
+ "KCBX": "Boise, ID",
896
+ "KGYX": "Portland, ME",
897
+ "KMXX": "Maxwell AFB, AL",
898
+ "TSJU": "San Juan, PR",
899
+ "KHNX": "San Joaquin Valley, CA",
900
+ "KLVX": "Louisville, KY",
901
+ "KMSX": "Missoula, MT",
902
+ "KJAX": "Jacksonville, FL",
903
+ "KNQA": "Memphis, TN",
904
+ "KRIW": "Riverton/Lander, WY",
905
+ "TCVG": "Covington, KY",
906
+ "KBLX": "Billings, MT",
907
+ "TPHL": "Philadelphia, PA",
908
+ "KRLX": "Charleston, WV",
909
+ "TMEM": "Memphis, TN",
910
+ "KCLE": "Cleveland, OH",
911
+ "KBHX": "Eureka, CA",
912
+ "KLBB": "Lubbock, TX",
913
+ "KOTX": "Spokane, WA",
914
+ "KEWX": "Austin/San Antonio, TX",
915
+ "KGWX": "Columbus AFB, MS",
916
+ "KESX": "Las Vegas, NV",
917
+ "KTBW": "Tampa, FL",
918
+ "KOHX": "Nashville, TN",
919
+ "KLTX": "Wilmington, NC",
920
+ "KFSX": "Flagstaff, AZ",
921
+ "TIDS": "Indianapolis, IN",
922
+ "KILN": "Cincinnati, OH",
923
+ "PAFG": "Fairbanks, AK",
924
+ "KPQR": "Portland, OR",
925
+ "KILM": "Wilmington, NC",
926
+ "KEKA": "Eureka, CA",
927
+ "KCHS": "Charleston, SC",
928
+ "KPHI": "Philadelphia/Mt. Holly, NJ",
929
+ "KUNR": "Rapid City, SD",
930
+ "KMFL": "Miami, FL",
931
+ "TJSJ": "San Juan, PR",
932
+ "KFGF": "Grand Forks, ND",
933
+ "KSEW": "Seattle, WA",
934
+ "PAFC": "Anchorage, AK",
935
+ "KLMK": "Louisville, KY",
936
+ "PHFO": "Honolulu, HI",
937
+ "KLIX": "New Orleans/Baton Rouge, LA",
938
+ "KBOI": "Boise, ID",
939
+ "KPIH": "Pocatello, ID",
940
+ "KMTR": "San Francisco/Monterey, CA",
941
+ "KGJT": "Grand Junction, CO",
942
+ "PAAQ": "Anchorage, AK",
943
+ "KABQ": "Albuquerque, NM",
944
+ "KTAE": "Tallahassee, FL",
945
+ "KCAR": "Caribou, ME",
946
+ "KMFR": "Medford, OR",
947
+ "PGUM": "Guam, GU",
948
+ "PAJK": "Juneau, AK"
949
+ };
950
+
951
+ // src/bootstrap.ts
952
+ var packages = {
953
+ fs,
954
+ path,
955
+ events,
956
+ xmpp,
957
+ shapefile,
958
+ xml2js,
959
+ sqlite3: import_better_sqlite3.default,
960
+ jobs,
961
+ axios: import_axios.default,
962
+ crypto: import_crypto.default,
963
+ os: import_os.default,
964
+ say: import_say.default,
965
+ child: import_child_process.default,
966
+ turf
967
+ };
968
+ var cache = {
969
+ isReady: true,
970
+ sigHalt: false,
971
+ isConnected: false,
972
+ attemptingReconnect: false,
973
+ totalReconnects: 0,
974
+ lastStanza: null,
975
+ session: null,
976
+ lastConnect: null,
977
+ db: null,
978
+ lastWarn: null,
979
+ totalLocationWarns: 0,
980
+ events: new events.EventEmitter(),
981
+ isProcessingAudioQueue: false,
982
+ audioQueue: [],
983
+ currentLocations: {}
984
+ };
985
+ var settings = {
986
+ database: path.join(process.cwd(), "shapefiles.db"),
987
+ is_wire: true,
988
+ journal: true,
989
+ noaa_weather_wire_service_settings: {
990
+ reconnection_settings: {
991
+ enabled: true,
992
+ interval: 60
993
+ },
994
+ credentials: {
995
+ username: null,
996
+ password: null,
997
+ nickname: "AtmosphericX Standalone Parser"
998
+ },
999
+ cache: {
1000
+ enabled: false,
1001
+ max_file_size: 5,
1002
+ max_db_history: 5e3,
1003
+ directory: null
1004
+ },
1005
+ preferences: {
1006
+ disable_ugc: false,
1007
+ disable_vtec: false,
1008
+ disable_text: false,
1009
+ cap_only: false
1010
+ }
1011
+ },
1012
+ national_weather_service_settings: {
1013
+ interval: 15,
1014
+ endpoint: `https://api.weather.gov/alerts/active`
1015
+ },
1016
+ global_settings: {
1017
+ parent_events_only: true,
1018
+ better_event_parsing: true,
1019
+ shapefile_coordinates: false,
1020
+ shapefile_skip: 15,
1021
+ filtering: {
1022
+ events: [],
1023
+ filtered_icao: [],
1024
+ ignored_icao: [`KWNS`],
1025
+ ignored_events: [`Xx`, `Test Message`],
1026
+ ugc_filter: [],
1027
+ state_filter: [],
1028
+ check_expired: true,
1029
+ ignore_text_products: true,
1030
+ location: {
1031
+ unit: `miles`
1032
+ }
1033
+ },
1034
+ eas_settings: {
1035
+ directory: null,
1036
+ intro_wav: null
1037
+ }
1038
+ }
1039
+ };
1040
+ var definitions = {
1041
+ events: EVENTS,
1042
+ actions: ACTIONS,
1043
+ status: STATUS,
1044
+ productTypes: TYPES,
1045
+ correlations: STATUS_CORRELATIONS,
1046
+ offshore: OFFSHORE,
1047
+ awips: AWIPS,
1048
+ causes: CAUSES,
1049
+ records: RECORDS,
1050
+ severity: SEVERITY,
1051
+ cancelSignatures: CANCEL_SIGNATURES,
1052
+ messageSignatures: MESSAGE_SIGNATURES,
1053
+ tags: TAGS,
1054
+ ICAO: ICAOs,
1055
+ enhancedEvents: [
1056
+ { "Tornado Warning": {
1057
+ "Tornado Emergency": { description: "tornado emergency", condition: (tornadoThreatTag) => tornadoThreatTag === "OBSERVED" },
1058
+ "PDS Tornado Warning": { description: "particularly dangerous situation", condition: (damageThreatTag) => damageThreatTag === "CONSIDERABLE" },
1059
+ "Confirmed Tornado Warning": { condition: (tornadoThreatTag) => tornadoThreatTag === "OBSERVED" },
1060
+ "Radar Indicated Tornado Warning": { condition: (tornadoThreatTag) => tornadoThreatTag !== "OBSERVED" }
1061
+ } },
1062
+ { "Tornado Watch": {
1063
+ "PDS Tornado Watch": { description: "particularly dangerous situation" }
1064
+ } },
1065
+ { "Flash Flood Warning": {
1066
+ "Flash Flood Emergency": { description: "flash flood emergency" },
1067
+ "Considerable Flash Flood Warning": { condition: (damageThreatTag) => damageThreatTag === "CONSIDERABLE" }
1068
+ } },
1069
+ { "Severe Thunderstorm Warning": {
1070
+ "EDS Severe Thunderstorm Warning": { description: "extremely dangerous situation" },
1071
+ "Destructive Severe Thunderstorm Warning": { condition: (damageThreatTag) => damageThreatTag === "DESTRUCTIVE" },
1072
+ "Considerable Severe Thunderstorm Warning": { condition: (damageThreatTag) => damageThreatTag === "CONSIDERABLE" }
1073
+ } }
1074
+ ],
1075
+ regular_expressions: {
1076
+ pvtec: new RegExp(`[OTEX].(NEW|CON|EXT|EXA|EXB|UPG|CAN|EXP|COR|ROU).[A-Z]{4}.[A-Z]{2}.[WAYSFON].[0-9]{4}.[0-9]{6}T[0-9]{4}Z-[0-9]{6}T[0-9]{4}Z`, "g"),
1077
+ hvtec: new RegExp(`[a-zA-Z0-9]{4}.[A-Z0-9].[A-Z]{2}.[0-9]{6}T[0-9]{4}Z.[0-9]{6}T[0-9]{4}Z.[0-9]{6}T[0-9]{4}Z.[A-Z]{2}`, "imu"),
1078
+ wmo: new RegExp(`[A-Z0-9]{6}\\s[A-Z]{4}\\s\\d{6}`, "imu"),
1079
+ ugc1: new RegExp(`(\\w{2}[CZ](\\d{3}((-|>)\\s?(\\n\\n)?))+)`, "imu"),
1080
+ ugc2: new RegExp(`(\\d{6}(-|>)\\s?(\\n\\n)?)`, "imu"),
1081
+ ugc3: new RegExp(`(\\d{6})(?=-|$)`, "imu"),
1082
+ dateline: new RegExp(`\\d{3,4}\\s*(AM|PM)?\\s*[A-Z]{2,4}\\s+[A-Z]{3,}\\s+[A-Z]{3,}\\s+\\d{1,2}\\s+\\d{4}`, "gim")
1083
+ },
1084
+ shapefiles: [
1085
+ { id: `C`, file: `USCounties` },
1086
+ { id: `Z`, file: `ForecastZones` },
1087
+ { id: `Z`, file: `FireZones` },
1088
+ { id: `Z`, file: `OffShoreZones` },
1089
+ { id: `Z`, file: `FireCounties` },
1090
+ { id: `Z`, file: `Marine` }
1091
+ ],
1092
+ messages: {
1093
+ shapefile_creation: `DO NOT CLOSE THIS PROJECT UNTIL THE SHAPEFILES ARE DONE COMPLETING!
1094
+ THIS COULD TAKE A WHILE DEPENDING ON THE SPEED OF YOUR STORAGE!!
1095
+ IF YOU CLOSE YOUR PROJECT, THE SHAPEFILES WILL NOT BE CREATED AND YOU WILL NEED TO DELETE ${settings.database} AND RESTART TO CREATE THEM AGAIN!`,
1096
+ shapefile_creation_finished: `SHAPEFILES HAVE BEEN SUCCESSFULLY CREATED AND THE DATABASE IS READY FOR USE!`,
1097
+ not_ready: `You can NOT create another instance without shutting down the current one first, please make sure to call the stop() method first!`,
1098
+ invalid_nickname: `The nickname you provided is invalid, please provide a valid nickname to continue.`,
1099
+ eas_no_directory: `You have not set a directory for EAS audio files to be saved to, please set the 'directory' setting in the global settings to enable EAS audio generation.`,
1100
+ invalid_coordinates: `The coordinates you provided are invalid, please provide valid latitude and longitude values. Attempted: {lat}, {lon}.`,
1101
+ no_current_locations: `No current location has been set, operations will be haulted until a location is set or location filtering is disabled.`,
1102
+ disabled_location_warning: `Exceeded maximum warnings for invalid or missing lat/lon coordinates. Location filtering has been ignored until you set valid coordinates or disable location filtering.`,
1103
+ reconnect_too_fast: `The client is attempting to reconnect too fast. This may be due to network instability. Reconnection attempt has been halted for safety.`,
1104
+ dump_cache: `Found {count} cached alert files and will begin dumping them shortly...`,
1105
+ dump_cache_complete: `Completed dumping all cached alert files.`,
1106
+ eas_missing_festival: `Festival TTS engine is not installed or not found in PATH. Please install Festival to enable EAS audio generation on Linux and macOS systems.`
1107
+ }
1108
+ };
1109
+
1110
+ // src/parsers/stanza.ts
1111
+ var StanzaParser = class {
1112
+ /**
1113
+ * @function validate
1114
+ * @description
1115
+ * Validates and parses a stanza message, extracting its attributes and metadata.
1116
+ * Handles both raw message strings (for debug/testing) and actual stanza objects.
1117
+ * Determines whether the message is a CAP alert, contains VTEC codes, or contains UGCs,
1118
+ * and identifies the AWIPS product type and prefix.
1119
+ *
1120
+ * @static
1121
+ * @param {any} stanza
1122
+ * @param {boolean | types.StanzaAttributes} [isDebug=false]
1123
+ * @returns {{
1124
+ * message: string;
1125
+ * attributes: types.StanzaAttributes;
1126
+ * isCap: boolean,
1127
+ * isPVtec: boolean;
1128
+ * isCapDescription: boolean;
1129
+ * awipsType: Record<string, string>;
1130
+ * isApi: boolean;
1131
+ * ignore: boolean;
1132
+ * isUGC?: boolean;
1133
+ * }}
1134
+ */
1135
+ static validate(stanza, isDebug = false) {
1136
+ var _a;
1137
+ if (isDebug !== false) {
1138
+ const vTypes = isDebug;
1139
+ const message = stanza;
1140
+ const attributes = vTypes;
1141
+ const isCap = (_a = vTypes.isCap) != null ? _a : message.includes(`<?xml`);
1142
+ const isCapDescription = message.includes(`<areaDesc>`);
1143
+ const isPVtec = message.match(definitions.regular_expressions.pvtec) != null;
1144
+ const isUGC = message.match(definitions.regular_expressions.ugc1) != null;
1145
+ const awipsType = this.getType(attributes);
1146
+ return { message, attributes, isCap, isPVtec, isUGC, isCapDescription, awipsType, isApi: false, ignore: false };
1147
+ }
1148
+ if (stanza.is(`message`)) {
1149
+ let cb = stanza.getChild(`x`);
1150
+ if (cb && cb.children) {
1151
+ let message = unescape(cb.children[0]);
1152
+ let attributes = cb.attrs;
1153
+ if (attributes.awipsid && attributes.awipsid.length > 1) {
1154
+ const isCap = message.includes(`<?xml`);
1155
+ const isCapDescription = message.includes(`<areaDesc>`);
1156
+ const isPVtec = message.match(definitions.regular_expressions.pvtec) != null;
1157
+ const isUGC = message.match(definitions.regular_expressions.ugc1) != null;
1158
+ const awipsType = this.getType(attributes);
1159
+ this.cache(message, { attributes, isCap, isPVtec, awipsType });
1160
+ return { message, attributes, isCap, isPVtec, isUGC, isCapDescription, awipsType, isApi: false, ignore: false };
1161
+ }
1162
+ }
1163
+ }
1164
+ return { message: null, attributes: null, isApi: null, isCap: null, isPVtec: null, isUGC: null, isCapDescription: null, awipsType: null, ignore: true };
1165
+ }
1166
+ /**
1167
+ * @function getType
1168
+ * @description
1169
+ * Determines the AWIPS product type and prefix from a stanza's attributes.
1170
+ * Returns a default type of 'XX' if the attributes are missing or the AWIPS ID
1171
+ * does not match any known definitions.
1172
+ *
1173
+ * @private
1174
+ * @static
1175
+ * @param {unknown} attributes
1176
+ * @returns {Record<string, string>}
1177
+ */
1178
+ static getType(attributes) {
1179
+ const attrs = attributes;
1180
+ if (!(attrs == null ? void 0 : attrs.awipsid)) return { type: "XX", prefix: "XX" };
1181
+ const awipsDefs = definitions.awips;
1182
+ for (const [prefix, type] of Object.entries(awipsDefs)) {
1183
+ if (attrs.awipsid.startsWith(prefix)) {
1184
+ return { type, prefix };
1185
+ }
1186
+ }
1187
+ return { type: "XX", prefix: "XX" };
1188
+ }
1189
+ /**
1190
+ * @function cache
1191
+ * @description
1192
+ * Saves a compiled stanza message to the local cache directory.
1193
+ * Ensures the message contains "STANZA ATTRIBUTES..." metadata and timestamps,
1194
+ * and appends the formatted entry to both a category-specific file and a general cache file.
1195
+ *
1196
+ * @private
1197
+ * @static
1198
+ * @async
1199
+ * @param {unknown} compiled
1200
+ * @returns {Promise<void>}
1201
+ */
1202
+ static cache(message, compiled) {
1203
+ return __async(this, null, function* () {
1204
+ if (!compiled) return;
1205
+ const data = compiled;
1206
+ const settings2 = settings;
1207
+ const { fs: fs2, path: path2 } = packages;
1208
+ if (!message || !settings2.noaa_weather_wire_service_settings.cache.directory) return;
1209
+ const cacheDir = settings2.noaa_weather_wire_service_settings.cache.directory;
1210
+ if (!fs2.existsSync(cacheDir)) fs2.mkdirSync(cacheDir, { recursive: true });
1211
+ const prefix = `category-${data.awipsType.prefix}-${data.awipsType.type}s`;
1212
+ const suffix = `${data.isCap ? "cap" : "raw"}${data.isPVtec ? "-vtec" : ""}`;
1213
+ const categoryFile = path2.join(cacheDir, `${prefix}-${suffix}.bin`);
1214
+ const cacheFile = path2.join(cacheDir, `cache-${suffix}.bin`);
1215
+ const entry = `[SoF]
1216
+ STANZA ATTRIBUTES...${JSON.stringify(compiled)}
1217
+ [EoF]
1218
+ ${message}`;
1219
+ yield Promise.all([
1220
+ fs2.promises.appendFile(categoryFile, entry, "utf8"),
1221
+ fs2.promises.appendFile(cacheFile, entry, "utf8")
1222
+ ]);
1223
+ });
1224
+ }
1225
+ };
1226
+ var stanza_default = StanzaParser;
1227
+
1228
+ // src/parsers/text.ts
1229
+ var TextParser = class {
1230
+ /**
1231
+ * @function textProductToString
1232
+ * @description
1233
+ * Searches a text product message for a line containing a specific value,
1234
+ * extracts the substring immediately following that value, and optionally
1235
+ * removes additional specified strings. Cleans up the extracted string by
1236
+ * trimming whitespace and removing any remaining occurrences of the search
1237
+ * value or '<' characters.
1238
+ *
1239
+ * @static
1240
+ * @param {string} message
1241
+ * @param {string} value
1242
+ * @param {string[]} [removal=[]]
1243
+ * @returns {string | null}
1244
+ */
1245
+ static textProductToString(message, value, removal = []) {
1246
+ const lines = message.split("\n");
1247
+ for (const line of lines) {
1248
+ if (line.includes(value)) {
1249
+ let result = line.slice(line.indexOf(value) + value.length).trim();
1250
+ for (const str of removal) {
1251
+ result = result.split(str).join("");
1252
+ }
1253
+ result = result.replace(value, "").replace("<", "").trim();
1254
+ return result || null;
1255
+ }
1256
+ }
1257
+ return null;
1258
+ }
1259
+ /**
1260
+ * @function textProductToPolygon
1261
+ * @description
1262
+ * Parses a text product message to extract polygon coordinates based on
1263
+ * LAT...LON data. Coordinates are converted to [latitude, longitude] pairs
1264
+ * with longitude negated (assumes Western Hemisphere). If the polygon has
1265
+ * more than two points, the first point is repeated at the end to close it.
1266
+ *
1267
+ * @static
1268
+ * @param {string} message
1269
+ * @returns {[number, number][]}
1270
+ */
1271
+ static textProductToPolygon(message) {
1272
+ const coordinates = [];
1273
+ const latLonMatch = message.match(/LAT\.{3}LON\s+([\d\s]+)/i);
1274
+ if (!latLonMatch || !latLonMatch[1]) return coordinates;
1275
+ const coordStrings = latLonMatch[1].replace(/\n/g, " ").trim().split(/\s+/);
1276
+ for (let i = 0; i < coordStrings.length - 1; i += 2) {
1277
+ const lat = parseFloat(coordStrings[i]) / 100;
1278
+ const lon = -parseFloat(coordStrings[i + 1]) / 100;
1279
+ if (!isNaN(lat) && !isNaN(lon)) {
1280
+ coordinates.push([lon, lat]);
1281
+ }
1282
+ }
1283
+ if (coordinates.length > 2) {
1284
+ coordinates.push(coordinates[0]);
1285
+ }
1286
+ return coordinates;
1287
+ }
1288
+ /**
1289
+ * @function textProductToDescription
1290
+ * @description
1291
+ * Extracts a clean description portion from a text product message, optionally
1292
+ * removing a handle and any extra metadata such as "STANZA ATTRIBUTES...".
1293
+ * Also trims and normalizes whitespace.
1294
+ *
1295
+ * @static
1296
+ * @param {string} message
1297
+ * @param {string | null} [handle=null]
1298
+ * @returns {string}
1299
+ */
1300
+ static textProductToDescription(message, handle = null) {
1301
+ const original = message;
1302
+ const discoveredDates = Array.from(message.matchAll(definitions.regular_expressions.dateline));
1303
+ if (discoveredDates.length) {
1304
+ const lastMatch = discoveredDates[discoveredDates.length - 1][0];
1305
+ const startIdx = message.lastIndexOf(lastMatch);
1306
+ if (startIdx !== -1) {
1307
+ const endIdx = message.indexOf("&&", startIdx);
1308
+ message = message.substring(startIdx + lastMatch.length, endIdx !== -1 ? endIdx : void 0).trimStart();
1309
+ if (message.startsWith("/")) message = message.slice(1).trimStart();
1310
+ if (handle && message.includes(handle)) {
1311
+ const handleIdx = message.indexOf(handle);
1312
+ message = message.substring(handleIdx + handle.length).trimStart();
1313
+ if (message.startsWith("/")) message = message.slice(1).trimStart();
1314
+ }
1315
+ }
1316
+ } else if (handle) {
1317
+ const handleIdx = message.indexOf(handle);
1318
+ if (handleIdx !== -1) {
1319
+ let afterHandle = message.substring(handleIdx + handle.length).trimStart();
1320
+ if (afterHandle.startsWith("/")) afterHandle = afterHandle.slice(1).trimStart();
1321
+ const latEnd = afterHandle.indexOf("&&");
1322
+ message = latEnd !== -1 ? afterHandle.substring(0, latEnd).trim() : afterHandle.trim();
1323
+ }
1324
+ }
1325
+ return message.replace(/\s+/g, " ").trim().startsWith("STANZA ATTRIBUTES...") ? original : message.split("STANZA ATTRIBUTES...")[0].trim();
1326
+ }
1327
+ /**
1328
+ * @function getXmlValues
1329
+ * @description
1330
+ * Recursively extracts specified values from a parsed XML-like object.
1331
+ * Searches both object keys and array items for matching keys (case-insensitive)
1332
+ * and returns the corresponding values. If multiple unique values are found for
1333
+ * a key, an array is returned; if one value is found, it returns that value;
1334
+ * if none are found, returns `null`.
1335
+ *
1336
+ * @static
1337
+ * @param {any} parsed
1338
+ * @param {string[]} valuesToExtract
1339
+ * @returns {Record<string, string | string[] | null>}
1340
+ */
1341
+ static getXmlValues(parsed, valuesToExtract) {
1342
+ const extracted = {};
1343
+ const findValueByKey = (obj, searchKey) => {
1344
+ const results = [];
1345
+ if (obj === null || typeof obj !== "object") {
1346
+ return results;
1347
+ }
1348
+ const searchKeyLower = searchKey.toLowerCase();
1349
+ for (const key in obj) {
1350
+ if (obj.hasOwnProperty(key) && key.toLowerCase() === searchKeyLower) {
1351
+ results.push(obj[key]);
1352
+ }
1353
+ }
1354
+ if (Array.isArray(obj)) {
1355
+ for (const item of obj) {
1356
+ if (item.valueName && item.valueName.toLowerCase() === searchKeyLower && item.value !== void 0) {
1357
+ results.push(item.value);
1358
+ }
1359
+ const nestedResults = findValueByKey(item, searchKey);
1360
+ results.push(...nestedResults);
1361
+ }
1362
+ }
1363
+ for (const key in obj) {
1364
+ if (obj.hasOwnProperty(key)) {
1365
+ const nestedResults = findValueByKey(obj[key], searchKey);
1366
+ results.push(...nestedResults);
1367
+ }
1368
+ }
1369
+ return results;
1370
+ };
1371
+ for (const key of valuesToExtract) {
1372
+ const values = findValueByKey(parsed.alert, key);
1373
+ const uniqueValues = [...new Set(values)];
1374
+ extracted[key] = uniqueValues.length === 0 ? null : uniqueValues.length === 1 ? uniqueValues[0] : uniqueValues;
1375
+ }
1376
+ return extracted;
1377
+ }
1378
+ };
1379
+ var text_default = TextParser;
1380
+
1381
+ // src/parsers/ugc.ts
1382
+ var UGCParser = class {
1383
+ /**
1384
+ * @function ugcExtractor
1385
+ * @description
1386
+ * Extracts UGC (Universal Geographic Code) information from a message.
1387
+ * This includes parsing the header, resolving zones, calculating the expiry
1388
+ * date, and retrieving associated location names from the database.
1389
+ *
1390
+ * @static
1391
+ * @async
1392
+ * @param {string} message
1393
+ * @returns {Promise<types.UGCEntry | null>}
1394
+ */
1395
+ static ugcExtractor(message) {
1396
+ return __async(this, null, function* () {
1397
+ const header = this.getHeader(message);
1398
+ if (!header) return null;
1399
+ const zones = this.getZones(header);
1400
+ if (zones.length === 0) return null;
1401
+ const expiry = this.getExpiry(message);
1402
+ const locations = yield this.getLocations(zones);
1403
+ return {
1404
+ zones,
1405
+ locations,
1406
+ expiry
1407
+ };
1408
+ });
1409
+ }
1410
+ /**
1411
+ * @function getHeader
1412
+ * @description
1413
+ * Extracts the UGC header from a message by locating patterns defined in
1414
+ * `ugc1` and `ugc2` regular expressions. Removes all whitespace and the
1415
+ * trailing character from the matched header.
1416
+ *
1417
+ * @static
1418
+ * @param {string} message
1419
+ * @returns {string | null}
1420
+ */
1421
+ static getHeader(message) {
1422
+ const start = message.search(definitions.regular_expressions.ugc1);
1423
+ const subMessage = message.substring(start);
1424
+ const end = subMessage.search(definitions.regular_expressions.ugc2);
1425
+ const full = subMessage.substring(0, end).replace(/\s+/g, "").slice(0, -1);
1426
+ return full || null;
1427
+ }
1428
+ /**
1429
+ * @function getExpiry
1430
+ * @description
1431
+ * Extracts an expiration date from a message using the UGC3 format.
1432
+ * The function parses day, hour, and minute from the message and constructs
1433
+ * a Date object in the current month and year. Returns `null` if no valid
1434
+ * expiration is found.
1435
+ *
1436
+ * @static
1437
+ * @param {string} message
1438
+ * @returns {Date | null}
1439
+ */
1440
+ static getExpiry(message) {
1441
+ const start = message.match(definitions.regular_expressions.ugc3);
1442
+ const day = parseInt(start[0].substring(0, 2), 10);
1443
+ const hour = parseInt(start[0].substring(2, 4), 10);
1444
+ const minute = parseInt(start[0].substring(4, 6), 10);
1445
+ const now = /* @__PURE__ */ new Date();
1446
+ const expires = new Date(now.getUTCFullYear(), now.getUTCMonth(), day, hour, minute, 0);
1447
+ return expires;
1448
+ }
1449
+ /**
1450
+ * @function getLocations
1451
+ * @description
1452
+ * Retrieves human-readable location names for an array of zone identifiers
1453
+ * from the shapefiles database. If a zone is not found, the zone ID itself
1454
+ * is returned. Duplicate locations are removed and the result is sorted.
1455
+ *
1456
+ * @static
1457
+ * @async
1458
+ * @param {string[]} zones
1459
+ * @returns {Promise<string[]>}
1460
+ */
1461
+ static getLocations(zones) {
1462
+ return __async(this, null, function* () {
1463
+ const uniqueZones = Array.from(new Set(zones.map((z) => z.trim())));
1464
+ const placeholders = uniqueZones.map(() => "?").join(",");
1465
+ const rows = yield cache.db.prepare(
1466
+ `SELECT id, location FROM shapefiles WHERE id IN (${placeholders})`
1467
+ ).all(...uniqueZones);
1468
+ const locationMap = /* @__PURE__ */ new Map();
1469
+ for (const row of rows) {
1470
+ locationMap.set(row.id, row.location);
1471
+ }
1472
+ const locations = uniqueZones.map((id) => {
1473
+ var _a;
1474
+ return (_a = locationMap.get(id)) != null ? _a : id;
1475
+ });
1476
+ return locations.sort();
1477
+ });
1478
+ }
1479
+ /**
1480
+ * @function getCoordinates
1481
+ * @description
1482
+ * Retrieves geographic coordinates for an array of zone identifiers
1483
+ * from the shapefiles database. Returns the coordinates of the first
1484
+ * polygon found for any matching zone.
1485
+ *
1486
+ * @static
1487
+ * @param {string[]} zones
1488
+ * @returns {[number, number][]}
1489
+ */
1490
+ static getCoordinates(zones) {
1491
+ const polygons = [];
1492
+ for (const zone of zones.map((z) => z.trim())) {
1493
+ const row = cache.db.prepare(`SELECT geometry FROM shapefiles WHERE id = ?`).get(zone);
1494
+ if (row !== void 0) {
1495
+ const geometry = JSON.parse(row.geometry);
1496
+ if ((geometry == null ? void 0 : geometry.type) === "Polygon") {
1497
+ polygons.push({
1498
+ type: "Feature",
1499
+ geometry,
1500
+ properties: {}
1501
+ });
1502
+ }
1503
+ }
1504
+ }
1505
+ if (polygons.length === 0) return null;
1506
+ let merged = polygons[0];
1507
+ for (let i = 1; i < polygons.length; i++) {
1508
+ merged = packages.turf.union(merged, polygons[i]);
1509
+ }
1510
+ const outerRing = merged.geometry.type === "Polygon" ? merged.geometry.coordinates[0] : merged.geometry.coordinates[0][0];
1511
+ const skip = settings.global_settings.shapefile_skip;
1512
+ const skippedRing = outerRing.filter((_, index) => index % skip === 0);
1513
+ return [skippedRing];
1514
+ }
1515
+ /**
1516
+ * @function getZones
1517
+ * @description
1518
+ * Parses a UGC header string and returns an array of individual zone
1519
+ * identifiers. Handles ranges indicated with `>` and preserves the
1520
+ * state and format prefixes.
1521
+ *
1522
+ * @static
1523
+ * @param {string} header
1524
+ * @returns {string[]}
1525
+ */
1526
+ static getZones(header) {
1527
+ const ugcSplit = header.split("-");
1528
+ const zones = [];
1529
+ let state = ugcSplit[0].substring(0, 2);
1530
+ const format = ugcSplit[0].substring(2, 3);
1531
+ for (const part of ugcSplit) {
1532
+ if (/^[A-Z]/.test(part)) {
1533
+ state = part.substring(0, 2);
1534
+ if (part.includes(">")) {
1535
+ const [start, end] = part.split(">");
1536
+ const startNum = parseInt(start.substring(3), 10);
1537
+ const endNum = parseInt(end, 10);
1538
+ for (let j = startNum; j <= endNum; j++) {
1539
+ zones.push(`${state}${format}${j.toString().padStart(3, "0")}`);
1540
+ }
1541
+ } else {
1542
+ zones.push(part);
1543
+ }
1544
+ continue;
1545
+ }
1546
+ if (part.includes(">")) {
1547
+ const [start, end] = part.split(">");
1548
+ const startNum = parseInt(start, 10);
1549
+ const endNum = parseInt(end, 10);
1550
+ for (let j = startNum; j <= endNum; j++) {
1551
+ zones.push(`${state}${format}${j.toString().padStart(3, "0")}`);
1552
+ }
1553
+ } else {
1554
+ zones.push(`${state}${format}${part}`);
1555
+ }
1556
+ }
1557
+ return zones.filter((item) => item !== "");
1558
+ }
1559
+ };
1560
+ var ugc_default = UGCParser;
1561
+
1562
+ // src/parsers/pvtec.ts
1563
+ var PVtecParser = class {
1564
+ /**
1565
+ * @function pVtecExtractor
1566
+ * @description
1567
+ * Extracts VTEC entries from a raw NWWS message string and returns
1568
+ * structured objects containing type, tracking, event, status,
1569
+ * WMO identifiers, and expiry date.
1570
+ *
1571
+ * @static
1572
+ * @param {string} message
1573
+ * @returns {Promise<types.VtecEntry[] | null>}
1574
+ */
1575
+ static pVtecExtractor(message) {
1576
+ return __async(this, null, function* () {
1577
+ var _a, _b;
1578
+ const matches = (_a = message.match(definitions.regular_expressions.pvtec)) != null ? _a : [];
1579
+ const pVtecs = [];
1580
+ for (const pvtec of matches) {
1581
+ const parts = pvtec.split(".");
1582
+ if (parts.length < 7) continue;
1583
+ const dates = parts[6].split("-");
1584
+ pVtecs.push({
1585
+ raw: pvtec,
1586
+ type: definitions.productTypes[parts[0]],
1587
+ tracking: `${parts[2]}-${parts[3]}-${parts[4]}-${parts[5]}`,
1588
+ event: `${definitions.events[parts[3]]} ${definitions.actions[parts[4]]}`,
1589
+ status: definitions.status[parts[1]],
1590
+ wmo: ((_b = message.match(definitions.regular_expressions.wmo)) == null ? void 0 : _b[0]) || `N/A`,
1591
+ expires: this.parseExpiryDate(dates)
1592
+ });
1593
+ }
1594
+ return pVtecs.length > 0 ? pVtecs : null;
1595
+ });
1596
+ }
1597
+ /**
1598
+ * @function parseExpiryDate
1599
+ * @description
1600
+ * Converts a NWWS VTEC/expiry timestamp string into a formatted local ISO date string
1601
+ * with an Eastern Time offset (-04:00). Returns `Invalid Date Format` if the input
1602
+ * is `000000T0000Z`.
1603
+ *
1604
+ * @private
1605
+ * @static
1606
+ * @param {string[]} args
1607
+ * @returns {string}
1608
+ */
1609
+ static parseExpiryDate(args) {
1610
+ if (args[1] == `000000T0000Z`) return `Invalid Date Format`;
1611
+ const expires = `${(/* @__PURE__ */ new Date()).getFullYear().toString().substring(0, 2)}${args[1].substring(0, 2)}-${args[1].substring(2, 4)}-${args[1].substring(4, 6)}T${args[1].substring(7, 9)}:${args[1].substring(9, 11)}:00`;
1612
+ const local = new Date(new Date(expires).getTime() - 4 * 60 * 6e4);
1613
+ const pad = (n) => n.toString().padStart(2, "0");
1614
+ return `${local.getFullYear()}-${pad(local.getMonth() + 1)}-${pad(local.getDate())}T${pad(local.getHours())}:${pad(local.getMinutes())}:00.000-04:00`;
1615
+ }
1616
+ };
1617
+ var pvtec_default = PVtecParser;
1618
+
1619
+ // src/parsers/hvtec.ts
1620
+ var HVtecParser = class {
1621
+ /**
1622
+ * @function HVtecExtractor
1623
+ * @description
1624
+ * Extracts VTEC entries from a raw NWWS message string and returns
1625
+ * structured objects containing type, tracking, event, status,
1626
+ * WMO identifiers, and expiry date.
1627
+ *
1628
+ * @static
1629
+ * @param {string} message
1630
+ * @returns {Promise<types.HtecEntry[] | null>}
1631
+ */
1632
+ static HVtecExtractor(message) {
1633
+ return __async(this, null, function* () {
1634
+ const matches = message.match(definitions.regular_expressions.hvtec);
1635
+ if (!matches || matches.length !== 1) return null;
1636
+ const hvtec = matches[0];
1637
+ const parts = hvtec.split(".");
1638
+ if (parts.length < 7) return null;
1639
+ const hvtecs = [{
1640
+ severity: definitions.severity[parts[1]],
1641
+ cause: definitions.causes[parts[2]],
1642
+ record: definitions.records[parts[6]],
1643
+ raw: hvtec
1644
+ }];
1645
+ return hvtecs;
1646
+ });
1647
+ }
1648
+ };
1649
+ var hvtec_default = HVtecParser;
1650
+
1651
+ // src/parsers/events/vtec.ts
1652
+ var VTECAlerts = class {
1653
+ /**
1654
+ * @function event
1655
+ * @description
1656
+ * Processes a validated stanza message, extracting VTEC and UGC entries,
1657
+ * computing base properties, generating headers, and preparing structured
1658
+ * event objects for downstream handling. Each extracted event is enriched
1659
+ * with metadata, performance timing, and history information.
1660
+ *
1661
+ * @static
1662
+ * @async
1663
+ * @param {types.StanzaCompiled} validated
1664
+ * @returns {Promise<void>}
1665
+ */
1666
+ static event(validated) {
1667
+ return __async(this, null, function* () {
1668
+ var _a, _b;
1669
+ let processed = [];
1670
+ const blocks = (_a = validated.message.split(/\[SoF\]/gim)) == null ? void 0 : _a.map((msg) => msg.trim());
1671
+ for (const block of blocks) {
1672
+ const cachedAttribute = block.match(/STANZA ATTRIBUTES\.\.\.(\{.*\})/);
1673
+ const messages = (_b = block.split(/(?=\$\$)/g)) == null ? void 0 : _b.map((msg) => msg.trim());
1674
+ if (!messages || messages.length == 0) return;
1675
+ for (let i = 0; i < messages.length; i++) {
1676
+ const tick = performance.now();
1677
+ const message = messages[i];
1678
+ const attributes = cachedAttribute != null ? JSON.parse(cachedAttribute[1]) : validated;
1679
+ const getPVTEC = yield pvtec_default.pVtecExtractor(message);
1680
+ const getHVTEC = yield hvtec_default.HVtecExtractor(message);
1681
+ const getUGC = yield ugc_default.ugcExtractor(message);
1682
+ if (getPVTEC != null && getUGC != null) {
1683
+ for (let j = 0; j < getPVTEC.length; j++) {
1684
+ const pVtec = getPVTEC[j];
1685
+ const baseProperties = yield events_default.getBaseProperties(message, attributes, getUGC, pVtec, getHVTEC);
1686
+ const baseGeometry = yield events_default.getEventGeometry(message, getUGC);
1687
+ const getHeader = events_default.getHeader(__spreadValues(__spreadValues({}, validated.attributes), baseProperties.raw), baseProperties, pVtec);
1688
+ processed.push({
1689
+ type: "Feature",
1690
+ properties: __spreadValues({ event: pVtec.event, parent: pVtec.event, action_type: pVtec.status }, baseProperties),
1691
+ details: {
1692
+ performance: performance.now() - tick,
1693
+ source: `pvtec-parser`,
1694
+ tracking: pVtec.tracking,
1695
+ header: getHeader,
1696
+ pvtec: pVtec.raw,
1697
+ hvtec: getHVTEC != null ? getHVTEC.raw : `N/A`,
1698
+ history: [{ description: baseProperties.description, issued: baseProperties.issued, type: pVtec.status }]
1699
+ },
1700
+ geometry: baseGeometry
1701
+ });
1702
+ }
1703
+ }
1704
+ }
1705
+ }
1706
+ events_default.validateEvents(processed);
1707
+ });
1708
+ }
1709
+ };
1710
+ var vtec_default = VTECAlerts;
1711
+
1712
+ // src/parsers/events/ugc.ts
1713
+ var UGCAlerts = class {
1714
+ /**
1715
+ * @function getTracking
1716
+ * @description
1717
+ * Generates a unique tracking identifier for an event using the sender's ICAO
1718
+ * and some attributes.
1719
+ *
1720
+ * @private
1721
+ * @static
1722
+ * @param {types.EventProperties} baseProperties
1723
+ * @returns {string}
1724
+ */
1725
+ static getTracking(baseProperties) {
1726
+ return `${baseProperties.sender_icao}-${baseProperties.raw.attributes.ttaaii}-${baseProperties.raw.attributes.id.slice(-4)}`;
1727
+ }
1728
+ /**
1729
+ * @function getEvent
1730
+ * @description
1731
+ * Determines the human-readable event name from a message and AWIPS attributes.
1732
+ * - Checks if the message contains any predefined offshore event keywords
1733
+ * and returns the matching offshore event if found.
1734
+ * - Otherwise, returns a formatted event type string from the provided attributes,
1735
+ * capitalizing the first letter of each word.
1736
+ *
1737
+ * @private
1738
+ * @static
1739
+ * @param {string} message
1740
+ * @param {Record<string, any>} attributes
1741
+ * @returns {string}
1742
+ */
1743
+ static getEvent(message, metadata) {
1744
+ const offshoreEvent = Object.keys(definitions.offshore).find((event) => message.toLowerCase().includes(event.toLowerCase()));
1745
+ if (offshoreEvent != void 0) return Object.keys(definitions.offshore).find((event) => message.toLowerCase().includes(event.toLowerCase()));
1746
+ return metadata.awipsType.type.split(`-`).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(` `);
1747
+ }
1748
+ /**
1749
+ * @function event
1750
+ * @description
1751
+ * Processes a validated stanza message, extracting UGC entries and
1752
+ * computing base properties for non-VTEC events. Each extracted event
1753
+ * is enriched with metadata, performance timing, and history information,
1754
+ * then filtered and emitted via `EventParser.validateEvents`.
1755
+ *
1756
+ * @static
1757
+ * @async
1758
+ * @param {types.StanzaCompiled} validated
1759
+ * @returns {Promise<void>}
1760
+ */
1761
+ static event(validated) {
1762
+ return __async(this, null, function* () {
1763
+ var _a, _b;
1764
+ let processed = [];
1765
+ const blocks = (_a = validated.message.split(/\[SoF\]/gim)) == null ? void 0 : _a.map((msg) => msg.trim());
1766
+ for (const block of blocks) {
1767
+ const cachedAttribute = block.match(/STANZA ATTRIBUTES\.\.\.(\{.*\})/);
1768
+ const messages = (_b = block.split(/(?=\$\$)/g)) == null ? void 0 : _b.map((msg) => msg.trim());
1769
+ if (!messages || messages.length == 0) return;
1770
+ for (let i = 0; i < messages.length; i++) {
1771
+ const tick = performance.now();
1772
+ const message = messages[i];
1773
+ const getUGC = yield ugc_default.ugcExtractor(message);
1774
+ if (getUGC != null) {
1775
+ const attributes = cachedAttribute != null ? JSON.parse(cachedAttribute[1]) : validated;
1776
+ const baseProperties = yield events_default.getBaseProperties(message, attributes, getUGC);
1777
+ const baseGeometry = yield events_default.getEventGeometry(message, getUGC);
1778
+ const getHeader = events_default.getHeader(__spreadValues(__spreadValues({}, attributes), baseProperties.raw), baseProperties);
1779
+ const getEvent = this.getEvent(message, attributes);
1780
+ processed.push({
1781
+ type: "Feature",
1782
+ properties: __spreadValues({ event: getEvent, parent: getEvent, action_type: `Issued` }, baseProperties),
1783
+ details: {
1784
+ performance: performance.now() - tick,
1785
+ source: `ugc-parser`,
1786
+ tracking: this.getTracking(baseProperties),
1787
+ header: getHeader,
1788
+ pvtec: `N/A`,
1789
+ hvtec: `N/A`,
1790
+ history: [{ description: baseProperties.description, issued: baseProperties.issued, type: `Issued` }]
1791
+ },
1792
+ geometry: baseGeometry
1793
+ });
1794
+ }
1795
+ }
1796
+ }
1797
+ events_default.validateEvents(processed);
1798
+ });
1799
+ }
1800
+ };
1801
+ var ugc_default2 = UGCAlerts;
1802
+
1803
+ // src/parsers/events/text.ts
1804
+ var TextAlerts = class {
1805
+ /**
1806
+ * @function getTracking
1807
+ * @description
1808
+ * Generates a unique tracking identifier for an event using the sender's ICAO
1809
+ * and some attributes.
1810
+ *
1811
+ * @private
1812
+ * @static
1813
+ * @param {types.EventProperties} baseProperties
1814
+ * @returns {string}
1815
+ */
1816
+ static getTracking(properties) {
1817
+ return `${properties.sender_icao}-${properties.raw.attributes.ttaaii}-${properties.raw.attributes.id.slice(-4)}`;
1818
+ }
1819
+ /**
1820
+ * @function getEvent
1821
+ * @description
1822
+ * Determines the event name from a text message and its AWIPS attributes.
1823
+ * If the message contains a known offshore event keyword, that offshore
1824
+ * event is returned. Otherwise, the event type from the AWIPS attributes
1825
+ * is formatted into a human-readable string with each word capitalized.
1826
+ *
1827
+ * @private
1828
+ * @static
1829
+ * @param {string} message
1830
+ * @param {types.StanzaAttributes} metadata
1831
+ * @returns {string}
1832
+ */
1833
+ static getEvent(message, metadata) {
1834
+ const offshoreEvent = Object.keys(definitions.offshore).find((event) => message.toLowerCase().includes(event.toLowerCase()));
1835
+ if (offshoreEvent != void 0) return Object.keys(definitions.offshore).find((event) => message.toLowerCase().includes(event.toLowerCase()));
1836
+ return metadata.awipsType.type.split(`-`).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(` `);
1837
+ }
1838
+ /**
1839
+ * @function event
1840
+ * @description
1841
+ * Processes a compiled text-based NOAA Stanza message and extracts relevant
1842
+ * event information. Splits the message into multiple segments based on
1843
+ * markers such as "$$", "ISSUED TIME...", or separator lines, generates
1844
+ * base properties, headers, event names, and tracking information for
1845
+ * each segment, then validates and emits the processed events.
1846
+ *
1847
+ * @public
1848
+ * @static
1849
+ * @async
1850
+ * @param {types.StanzaCompiled} validated
1851
+ * @returns {Promise<void>}
1852
+ */
1853
+ static event(validated) {
1854
+ return __async(this, null, function* () {
1855
+ var _a, _b;
1856
+ let processed = [];
1857
+ const blocks = (_a = validated.message.split(/\[SoF\]/gim)) == null ? void 0 : _a.map((msg) => msg.trim());
1858
+ for (const block of blocks) {
1859
+ const cachedAttribute = block.match(/STANZA ATTRIBUTES\.\.\.(\{.*\})/);
1860
+ const messages = (_b = block.split(/(?=\$\$)/g)) == null ? void 0 : _b.map((msg) => msg.trim());
1861
+ if (!messages || messages.length == 0) return;
1862
+ for (let i = 0; i < messages.length; i++) {
1863
+ const tick = performance.now();
1864
+ const message = messages[i];
1865
+ const attributes = cachedAttribute != null ? JSON.parse(cachedAttribute[1]) : validated;
1866
+ const baseProperties = yield events_default.getBaseProperties(message, attributes);
1867
+ const baseGeometry = yield events_default.getEventGeometry(message);
1868
+ const getHeader = events_default.getHeader(__spreadValues(__spreadValues({}, validated.attributes), baseProperties.raw), baseProperties);
1869
+ const getEvent = this.getEvent(message, attributes);
1870
+ processed.push({
1871
+ properties: __spreadValues({ event: getEvent, parent: getEvent, action_type: `Issued` }, baseProperties),
1872
+ details: {
1873
+ type: "Feature",
1874
+ performance: performance.now() - tick,
1875
+ source: `text-parser`,
1876
+ tracking: this.getTracking(baseProperties),
1877
+ header: getHeader,
1878
+ pvtec: `N/A`,
1879
+ hvtec: `N/A`,
1880
+ history: [{ description: baseProperties.description, issued: baseProperties.issued, type: `Issued` }]
1881
+ },
1882
+ geometry: baseGeometry
1883
+ });
1884
+ }
1885
+ }
1886
+ events_default.validateEvents(processed);
1887
+ });
1888
+ }
1889
+ };
1890
+ var text_default2 = TextAlerts;
1891
+
1892
+ // src/parsers/events/cap.ts
1893
+ var CapAlerts = class {
1894
+ /**
1895
+ * @function getTracking
1896
+ * @description
1897
+ * Generates a unique tracking identifier for a CAP alert based on extracted XML values.
1898
+ * If VTEC information is available, it constructs the tracking ID from the VTEC components.
1899
+ * Otherwise, it uses the WMO identifier along with TTAI and CCCC attributes.
1900
+ *
1901
+ * @private
1902
+ * @static
1903
+ * @param {Record<string, string>} extracted
1904
+ * @returns {string}
1905
+ */
1906
+ static getTracking(extracted, metadata) {
1907
+ return extracted.vtec ? (() => {
1908
+ const vtecValue = Array.isArray(extracted.vtec) ? extracted.vtec[0] : extracted.vtec;
1909
+ const splitPVTEC = vtecValue.split(".");
1910
+ return `${splitPVTEC[2]}-${splitPVTEC[3]}-${splitPVTEC[4]}-${splitPVTEC[5]}`;
1911
+ })() : `${extracted.wmoidentifier.substring(extracted.wmoidentifier.length - 4)}-${metadata.attributes.ttaaii}-${metadata.attributes.id.slice(-4)}`;
1912
+ }
1913
+ /**
1914
+ * @function event
1915
+ * @description
1916
+ * Processes validated CAP alert messages, extracting relevant information and compiling it into structured event objects.
1917
+ *
1918
+ * @public
1919
+ * @static
1920
+ * @async
1921
+ * @param {types.StanzaCompiled} validated
1922
+ * @returns {*}
1923
+ */
1924
+ static event(validated) {
1925
+ return __async(this, null, function* () {
1926
+ var _a, _b;
1927
+ let processed = [];
1928
+ const tick = performance.now();
1929
+ const settings2 = settings;
1930
+ const blocks = (_a = validated.message.split(/\[SoF\]/gim)) == null ? void 0 : _a.map((msg) => msg.trim());
1931
+ for (const block of blocks) {
1932
+ const cachedAttribute = block.match(/STANZA ATTRIBUTES\.\.\.(\{.*\})/);
1933
+ const messages = (_b = block.split(/(?=\$\$)/g)) == null ? void 0 : _b.map((msg) => msg.trim());
1934
+ if (!messages || messages.length == 0) return;
1935
+ for (let i = 0; i < messages.length; i++) {
1936
+ let message = messages[i];
1937
+ const attributes = cachedAttribute != null ? JSON.parse(cachedAttribute[1]) : validated;
1938
+ message = message.substring(message.indexOf(`<?xml version="1.0"`), message.lastIndexOf(`>`) + 1);
1939
+ const parser = new packages.xml2js.Parser({ explicitArray: false, mergeAttrs: true, trim: true });
1940
+ const parsed = yield parser.parseStringPromise(message);
1941
+ if (parsed == null || parsed.alert == null) continue;
1942
+ const extracted = text_default.getXmlValues(parsed, [
1943
+ `vtec`,
1944
+ `wmoidentifier`,
1945
+ `ugc`,
1946
+ `areadesc`,
1947
+ `expires`,
1948
+ `sent`,
1949
+ `msgtype`,
1950
+ `description`,
1951
+ `event`,
1952
+ `sendername`,
1953
+ `tornadodetection`,
1954
+ `polygon`,
1955
+ `maxHailSize`,
1956
+ `maxWindGust`,
1957
+ `thunderstormdamagethreat`,
1958
+ `tornadodamagethreat`,
1959
+ `waterspoutdetection`,
1960
+ `flooddetection`
1961
+ ]);
1962
+ const getHeader = events_default.getHeader(__spreadValues({}, validated.attributes));
1963
+ const getSource = text_default.textProductToString(extracted.description, `SOURCE...`, [`.`]) || `N/A`;
1964
+ processed.push({
1965
+ type: "Feature",
1966
+ properties: {
1967
+ locations: extracted.areadesc || `N/A`,
1968
+ event: extracted.event || `N/A`,
1969
+ issued: extracted.sent ? new Date(extracted.sent).toLocaleString() : `N/A`,
1970
+ expires: extracted.expires ? new Date(extracted.expires).toLocaleString() : `N/A`,
1971
+ parent: extracted.event || `N/A`,
1972
+ action_type: extracted.msgtype || `N/A`,
1973
+ description: extracted.description || `N/A`,
1974
+ sender_name: extracted.sendername || `N/A`,
1975
+ sender_icao: extracted.wmoidentifier ? extracted.wmoidentifier.substring(extracted.wmoidentifier.length - 4) : `N/A`,
1976
+ attributes,
1977
+ geocode: {
1978
+ UGC: [extracted.ugc]
1979
+ },
1980
+ metadata: { attributes },
1981
+ technical: {
1982
+ vtec: extracted.vtec || `N/A`,
1983
+ ugc: extracted.ugc || `N/A`,
1984
+ hvtec: `N/A`
1985
+ },
1986
+ parameters: {
1987
+ wmo: extracted.wmoidentifier || `N/A`,
1988
+ source: getSource,
1989
+ max_hail_size: extracted.maxHailSize || `N/A`,
1990
+ max_wind_gust: extracted.maxWindGust || `N/A`,
1991
+ damage_threat: extracted.thunderstormdamagethreat || `N/A`,
1992
+ tornado_detection: extracted.tornadodetection || extracted.waterspoutdetection || `N/A`,
1993
+ flood_detection: extracted.flooddetection || `N/A`,
1994
+ discussion_tornado_intensity: `N/A`,
1995
+ discussion_wind_intensity: `N/A`,
1996
+ discussion_hail_intensity: `N/A`
1997
+ }
1998
+ },
1999
+ details: {
2000
+ performance: performance.now() - tick,
2001
+ source: `cap-parser`,
2002
+ tracking: this.getTracking(extracted, attributes),
2003
+ header: getHeader,
2004
+ pvtec: extracted.vtec || `N/A`,
2005
+ hvtec: `N/A`,
2006
+ history: [{ description: extracted.description || `N/A`, issued: extracted.sent ? new Date(extracted.sent).toLocaleString() : `N/A`, type: extracted.msgtype || `N/A` }]
2007
+ },
2008
+ geometry: extracted.polygon ? {
2009
+ type: "Polygon",
2010
+ coordinates: [
2011
+ extracted.polygon.split(" ").map((coord) => {
2012
+ const [lon, lat] = coord.split(",").map((num) => parseFloat(num));
2013
+ return [lat, lon];
2014
+ })
2015
+ ]
2016
+ } : yield events_default.getEventGeometry(``, { zones: [extracted.ugc] })
2017
+ });
2018
+ }
2019
+ }
2020
+ events_default.validateEvents(processed);
2021
+ });
2022
+ }
2023
+ };
2024
+ var cap_default = CapAlerts;
2025
+
2026
+ // src/parsers/events/api.ts
2027
+ var APIAlerts = class {
2028
+ /**
2029
+ * @function getTracking
2030
+ * @description
2031
+ * Generates a unique tracking identifier for a CAP alert based on extracted XML values.
2032
+ * If VTEC information is available, it constructs the tracking ID from the VTEC components.
2033
+ * Otherwise, it uses the WMO identifier along with TTAI and CCCC attributes.
2034
+ *
2035
+ * @private
2036
+ * @static
2037
+ * @param {Record<string, string>} extracted
2038
+ * @returns {string}
2039
+ */
2040
+ static getTracking(extracted) {
2041
+ return extracted.pVtec ? (() => {
2042
+ const vtecValue = Array.isArray(extracted.pVtec) ? extracted.pVtec[0] : extracted.pVtec;
2043
+ const splitPVTEC = vtecValue.split(".");
2044
+ return `${splitPVTEC[2]}-${splitPVTEC[3]}-${splitPVTEC[4]}-${splitPVTEC[5]}`;
2045
+ })() : (() => {
2046
+ var _a;
2047
+ const wmoMatch = (_a = extracted.wmoidentifier) == null ? void 0 : _a.match(/([A-Z]{4}\d{2})\s+([A-Z]{4})/);
2048
+ const id = (wmoMatch == null ? void 0 : wmoMatch[1]) || "N/A";
2049
+ const station = (wmoMatch == null ? void 0 : wmoMatch[2]) || "N/A";
2050
+ return `${station}-${id}`;
2051
+ })();
2052
+ }
2053
+ /**
2054
+ * @function getICAO
2055
+ * @description
2056
+ * Extracts the sender's ICAO code and corresponding name from a VTEC string.
2057
+ *
2058
+ * @private
2059
+ * @static
2060
+ * @param {string} pVtec
2061
+ * @returns {{ icao: any; name: any; }}
2062
+ */
2063
+ static getICAO(pVtec) {
2064
+ var _a, _b;
2065
+ const icao = pVtec ? pVtec.split(`.`)[2] : `N/A`;
2066
+ const name = (_b = (_a = definitions.ICAO) == null ? void 0 : _a[icao]) != null ? _b : `N/A`;
2067
+ return { icao, name };
2068
+ }
2069
+ /**
2070
+ * @function event
2071
+ * @description
2072
+ * Processes validated API alert messages, extracting relevant information and compiling it into structured event objects.
2073
+ *
2074
+ * @public
2075
+ * @static
2076
+ * @async
2077
+ * @param {types.StanzaCompiled} validated
2078
+ * @returns {*}
2079
+ */
2080
+ static event(validated) {
2081
+ return __async(this, null, function* () {
2082
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, __, _$, _aa, _ba, _ca, _da, _ea, _fa, _ga, _ha;
2083
+ let processed = [];
2084
+ const settings2 = settings;
2085
+ const messages = Object.values(JSON.parse(validated.message).features);
2086
+ for (let feature of messages) {
2087
+ const tick = performance.now();
2088
+ const getPVTEC = (_d = (_c = (_b = (_a = feature == null ? void 0 : feature.properties) == null ? void 0 : _a.parameters) == null ? void 0 : _b.VTEC) == null ? void 0 : _c[0]) != null ? _d : null;
2089
+ const getWmo = (_h = (_g = (_f = (_e = feature == null ? void 0 : feature.properties) == null ? void 0 : _e.parameters) == null ? void 0 : _f.WMOidentifier) == null ? void 0 : _g[0]) != null ? _h : null;
2090
+ const getUgc = (_k = (_j = (_i = feature == null ? void 0 : feature.properties) == null ? void 0 : _i.geocode) == null ? void 0 : _j.UGC) != null ? _k : null;
2091
+ const getHeadline = (_o = (_n = (_m = (_l = feature == null ? void 0 : feature.properties) == null ? void 0 : _l.parameters) == null ? void 0 : _m.NWSheadline) == null ? void 0 : _n[0]) != null ? _o : "";
2092
+ const getDescription = `${getHeadline} ${(_q = (_p = feature == null ? void 0 : feature.properties) == null ? void 0 : _p.description) != null ? _q : ``}`;
2093
+ const getAWIP = (_u = (_t = (_s = (_r = feature == null ? void 0 : feature.properties) == null ? void 0 : _r.parameters) == null ? void 0 : _s.AWIPSidentifier) == null ? void 0 : _t[0]) != null ? _u : null;
2094
+ const getHeader = events_default.getHeader(__spreadValues({}, { getAwip: { prefix: getAWIP == null ? void 0 : getAWIP.slice(0, -3) } }));
2095
+ const getSource = text_default.textProductToString(getDescription, `SOURCE...`, [`.`]) || `N/A`;
2096
+ const getOffice = this.getICAO(getPVTEC || ``);
2097
+ processed.push({
2098
+ type: "Feature",
2099
+ properties: {
2100
+ locations: (_w = (_v = feature == null ? void 0 : feature.properties) == null ? void 0 : _v.areaDesc) != null ? _w : `N/A`,
2101
+ event: (_y = (_x = feature == null ? void 0 : feature.properties) == null ? void 0 : _x.event) != null ? _y : `N/A`,
2102
+ issued: ((_z = feature == null ? void 0 : feature.properties) == null ? void 0 : _z.sent) ? new Date((_A = feature == null ? void 0 : feature.properties) == null ? void 0 : _A.sent).toLocaleString() : `N/A`,
2103
+ expires: ((_B = feature == null ? void 0 : feature.properties) == null ? void 0 : _B.expires) ? new Date((_C = feature == null ? void 0 : feature.properties) == null ? void 0 : _C.expires).toLocaleString() : `N/A`,
2104
+ parent: (_E = (_D = feature == null ? void 0 : feature.properties) == null ? void 0 : _D.event) != null ? _E : `N/A`,
2105
+ action_type: (_G = (_F = feature == null ? void 0 : feature.properties) == null ? void 0 : _F.messageType) != null ? _G : `N/A`,
2106
+ description: (_I = (_H = feature == null ? void 0 : feature.properties) == null ? void 0 : _H.description) != null ? _I : `N/A`,
2107
+ sender_name: getOffice.name || `N/A`,
2108
+ sender_icao: getOffice.icao || `N/A`,
2109
+ attributes: validated.attributes,
2110
+ geocode: {
2111
+ UGC: (_L = (_K = (_J = feature == null ? void 0 : feature.properties) == null ? void 0 : _J.geocode) == null ? void 0 : _K.UGC) != null ? _L : [`XX000`]
2112
+ },
2113
+ metadata: {},
2114
+ technical: {
2115
+ vtec: getPVTEC || `N/A`,
2116
+ ugc: getUgc ? getUgc.join(`,`) : `N/A`,
2117
+ hvtec: `N/A`
2118
+ },
2119
+ parameters: {
2120
+ wmo: ((_O = (_N = (_M = feature == null ? void 0 : feature.properties) == null ? void 0 : _M.parameters) == null ? void 0 : _N.WMOidentifier) == null ? void 0 : _O[0]) || getWmo || `N/A`,
2121
+ source: getSource,
2122
+ max_hail_size: ((_Q = (_P = feature == null ? void 0 : feature.properties) == null ? void 0 : _P.parameters) == null ? void 0 : _Q.maxHailSize) || `N/A`,
2123
+ max_wind_gust: ((_S = (_R = feature == null ? void 0 : feature.properties) == null ? void 0 : _R.parameters) == null ? void 0 : _S.maxWindGust) || `N/A`,
2124
+ damage_threat: ((_U = (_T = feature == null ? void 0 : feature.properties) == null ? void 0 : _T.parameters) == null ? void 0 : _U.thunderstormDamageThreat) || [`N/A`],
2125
+ tornado_detection: ((_W = (_V = feature == null ? void 0 : feature.properties) == null ? void 0 : _V.parameters) == null ? void 0 : _W.tornadoDetection) || [`N/A`],
2126
+ flood_detection: ((_Y = (_X = feature == null ? void 0 : feature.properties) == null ? void 0 : _X.parameters) == null ? void 0 : _Y.floodDetection) || [`N/A`],
2127
+ discussion_tornado_intensity: "N/A",
2128
+ peakWindGust: `N/A`,
2129
+ peakHailSize: `N/A`
2130
+ }
2131
+ },
2132
+ details: {
2133
+ performance: performance.now() - tick,
2134
+ source: `api-parser`,
2135
+ tracking: this.getTracking({ pVtec: getPVTEC, wmoidentifier: getWmo, ugc: getUgc ? getUgc.join(`,`) : null }),
2136
+ header: getHeader,
2137
+ pvtec: getPVTEC || `N/A`,
2138
+ history: [{
2139
+ description: (__ = (_Z = feature == null ? void 0 : feature.properties) == null ? void 0 : _Z.description) != null ? __ : `N/A`,
2140
+ action: (_aa = (_$ = feature == null ? void 0 : feature.properties) == null ? void 0 : _$.messageType) != null ? _aa : `N/A`,
2141
+ time: ((_ba = feature == null ? void 0 : feature.properties) == null ? void 0 : _ba.sent) ? new Date((_ca = feature == null ? void 0 : feature.properties) == null ? void 0 : _ca.sent).toLocaleString() : `N/A`
2142
+ }]
2143
+ },
2144
+ geometry: ((_ea = (_da = feature == null ? void 0 : feature.geometry) == null ? void 0 : _da.coordinates) == null ? void 0 : _ea[0]) != null ? {
2145
+ type: "Polygon",
2146
+ coordinates: [
2147
+ (_ha = (_ga = (_fa = feature == null ? void 0 : feature.geometry) == null ? void 0 : _fa.coordinates) == null ? void 0 : _ga[0]) == null ? void 0 : _ha.map((coord) => {
2148
+ const [lat, lon] = Array.isArray(coord) ? coord : [0, 0];
2149
+ return [lat, lon];
2150
+ })
2151
+ ]
2152
+ } : yield events_default.getEventGeometry(``, { zones: getUgc })
2153
+ });
2154
+ }
2155
+ events_default.validateEvents(processed);
2156
+ });
2157
+ }
2158
+ };
2159
+ var api_default = APIAlerts;
2160
+
2161
+ // src/parsers/events.ts
2162
+ var EventParser = class {
2163
+ /**
2164
+ * @function getBaseProperties
2165
+ * @description
2166
+ * Extracts and compiles the core properties of a weather
2167
+ * alert message into a structured object. Combines parsed
2168
+ * textual data, UGC information, VTEC entries, and additional
2169
+ * metadata for downstream use.
2170
+ *
2171
+ * @static
2172
+ * @async
2173
+ * @param {string} message
2174
+ * @param {types.StanzaCompiled} validated
2175
+ * @param {types.UGCEntry} [ugc=null]
2176
+ * @param {types.PVtecEntry} [pVtec=null]
2177
+ * @param {types.HVtecEntry} [hVtec=null]
2178
+ * @returns {Promise<Record<string, any>>}
2179
+ */
2180
+ static getBaseProperties(message, metadata, ugc = null, pVtec = null, hVtec = null) {
2181
+ return __async(this, null, function* () {
2182
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
2183
+ const settings2 = settings;
2184
+ const definitions2 = {
2185
+ tornado: (_b = (_a = text_default.textProductToString(message, `TORNADO...`)) != null ? _a : text_default.textProductToString(message, `WATERSPOUT...`)) != null ? _b : `N/A`,
2186
+ hail: (_d = (_c = text_default.textProductToString(message, `MAX HAIL SIZE...`, [`IN`])) != null ? _c : text_default.textProductToString(message, `HAIL...`, [`IN`])) != null ? _d : `N/A`,
2187
+ gusts: (_f = (_e = text_default.textProductToString(message, `MAX WIND GUST...`)) != null ? _e : text_default.textProductToString(message, `WIND...`)) != null ? _f : `N/A`,
2188
+ flood: (_g = text_default.textProductToString(message, `FLASH FLOOD...`)) != null ? _g : `N/A`,
2189
+ damage: (_h = text_default.textProductToString(message, `DAMAGE THREAT...`)) != null ? _h : `N/A`,
2190
+ source: (_i = text_default.textProductToString(message, `SOURCE...`, [`.`])) != null ? _i : `N/A`,
2191
+ description: text_default.textProductToDescription(message, (_j = pVtec == null ? void 0 : pVtec.raw) != null ? _j : null),
2192
+ wmo: (_l = (_k = message.match(definitions.regular_expressions.wmo)) == null ? void 0 : _k[0]) != null ? _l : `N/A`,
2193
+ mdTorIntensity: (_m = text_default.textProductToString(message, `MOST PROBABLE PEAK TORNADO INTENSITY...`)) != null ? _m : `N/A`,
2194
+ mdWindGusts: (_n = text_default.textProductToString(message, `MOST PROBABLE PEAK WIND GUST...`)) != null ? _n : `N/A`,
2195
+ mdHailSize: (_o = text_default.textProductToString(message, `MOST PROBABLE PEAK HAIL SIZE...`)) != null ? _o : `N/A`
2196
+ };
2197
+ const getOffice = this.getICAO(pVtec, metadata, definitions2.wmo);
2198
+ const getCorrectIssued = this.getCorrectIssuedDate(metadata);
2199
+ const getCorrectExpiry = this.getCorrectExpiryDate(pVtec, ugc);
2200
+ const base = {
2201
+ locations: (_p = ugc == null ? void 0 : ugc.locations.join(`; `)) != null ? _p : `No Location Specified (UGC Missing)`,
2202
+ issued: getCorrectIssued,
2203
+ expires: getCorrectExpiry,
2204
+ geocode: { UGC: (_q = ugc == null ? void 0 : ugc.zones) != null ? _q : [`XX000`] },
2205
+ description: definitions2.description,
2206
+ sender_name: getOffice.name,
2207
+ sender_icao: getOffice.icao,
2208
+ raw: __spreadValues({}, Object.fromEntries(Object.entries(metadata).filter(([key]) => key !== "message"))),
2209
+ parameters: {
2210
+ wmo: Array.isArray(definitions2.wmo) ? definitions2.wmo[0] : (_r = definitions2.wmo) != null ? _r : `N/A`,
2211
+ source: definitions2.source,
2212
+ max_hail_size: definitions2.hail,
2213
+ max_wind_gust: definitions2.gusts,
2214
+ damage_threat: definitions2.damage,
2215
+ tornado_detection: definitions2.tornado,
2216
+ flood_detection: definitions2.flood,
2217
+ discussion_tornado_intensity: definitions2.mdTorIntensity,
2218
+ discussion_wind_intensity: definitions2.mdWindGusts,
2219
+ discussion_hail_intensity: definitions2.mdHailSize
2220
+ }
2221
+ };
2222
+ return base;
2223
+ });
2224
+ }
2225
+ /**
2226
+ * @function getEventGeometry
2227
+ * @description
2228
+ * Determines the geometry of an event using polygon data fromEntries
2229
+ * in the message or UGC shapefile coordinates if enabled in settings. Falls
2230
+ * back to null if no geometry can be determined.
2231
+ *
2232
+ * @static
2233
+ * @param {string} message
2234
+ * @param {types.UGCEntry} [ugc=null]
2235
+ * @returns {Promise<types.geometry>}
2236
+ */
2237
+ static getEventGeometry(message, ugc = null) {
2238
+ return __async(this, null, function* () {
2239
+ const settings2 = settings;
2240
+ const polygonText = text_default.textProductToPolygon(message);
2241
+ let geometry = null;
2242
+ geometry = polygonText.length > 0 ? { type: "Polygon", coordinates: polygonText } : null;
2243
+ if (settings2.global_settings.shapefile_coordinates && polygonText.length == 0 && ugc != null) {
2244
+ const coordinates = yield ugc_default.getCoordinates(ugc.zones);
2245
+ geometry = { type: "Polygon", coordinates };
2246
+ }
2247
+ return geometry;
2248
+ });
2249
+ }
2250
+ /**
2251
+ * @function betterParsedEventName
2252
+ * @description
2253
+ * Enhances the parsing of an event name using additional criteria
2254
+ * from its description and parameters. Can optionally use
2255
+ * the original parent event name instead.
2256
+ *
2257
+ * @static
2258
+ * @param {types.EventCompiled} event
2259
+ * @param {boolean} [betterParsing=false]
2260
+ * @param {boolean} [useParentEvents=false]
2261
+ * @returns {string}
2262
+ */
2263
+ static betterParsedEventName(event, betterParsing, useParentEvents) {
2264
+ var _a, _b, _c, _d, _e, _f;
2265
+ let eventName = (_b = (_a = event == null ? void 0 : event.properties) == null ? void 0 : _a.event) != null ? _b : `Unknown Event`;
2266
+ const defEventTable = definitions.enhancedEvents;
2267
+ const properties = event == null ? void 0 : event.properties;
2268
+ const parameters = properties == null ? void 0 : properties.parameters;
2269
+ const description = (_c = properties == null ? void 0 : properties.description) != null ? _c : `Unknown Description`;
2270
+ const damageThreatTag = (_d = parameters == null ? void 0 : parameters.damage_threat) != null ? _d : `N/A`;
2271
+ const tornadoThreatTag = (_e = parameters == null ? void 0 : parameters.tornado_detection) != null ? _e : `N/A`;
2272
+ if (!betterParsing) {
2273
+ return eventName;
2274
+ }
2275
+ for (const eventGroup of defEventTable) {
2276
+ const [baseEvent, conditions] = Object.entries(eventGroup)[0];
2277
+ if (eventName === baseEvent) {
2278
+ for (const [specificEvent, condition] of Object.entries(conditions)) {
2279
+ const conditionMet = condition.description && description.includes(condition.description.toLowerCase()) || condition.condition && condition.condition(damageThreatTag || tornadoThreatTag);
2280
+ if (conditionMet) {
2281
+ eventName = specificEvent;
2282
+ break;
2283
+ }
2284
+ }
2285
+ if (baseEvent === "Severe Thunderstorm Warning" && tornadoThreatTag === "POSSIBLE" && !eventName.includes("(TPROB)")) eventName += " (TPROB)";
2286
+ break;
2287
+ }
2288
+ }
2289
+ return useParentEvents ? (_f = event == null ? void 0 : event.properties) == null ? void 0 : _f.event : eventName;
2290
+ }
2291
+ /**
2292
+ * @function validateEvents
2293
+ * @description
2294
+ * Processes an array of event objects and filters them based on
2295
+ * global and EAS filtering settings, location constraints, and
2296
+ * other criteria such as expired or test products. Valid events
2297
+ * trigger relevant event emitters.
2298
+ *
2299
+ * @static
2300
+ * @param {unknown[]} events
2301
+ * @returns {void}
2302
+ */
2303
+ static validateEvents(events2) {
2304
+ var _a, _b, _c, _d, _e;
2305
+ if (events2.length == 0) return;
2306
+ const filteringSettings = (_b = (_a = settings) == null ? void 0 : _a.global_settings) == null ? void 0 : _b.filtering;
2307
+ const locationSettings = filteringSettings == null ? void 0 : filteringSettings.location;
2308
+ const easSettings = (_d = (_c = settings) == null ? void 0 : _c.global_settings) == null ? void 0 : _d.eas_settings;
2309
+ const globalSettings = (_e = settings) == null ? void 0 : _e.global_settings;
2310
+ const sets = {};
2311
+ const bools = {};
2312
+ const megered = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, filteringSettings), easSettings), globalSettings), locationSettings);
2313
+ for (const key in megered) {
2314
+ const setting = megered[key];
2315
+ if (Array.isArray(setting)) {
2316
+ sets[key] = new Set(setting.map((item) => item.toLowerCase()));
2317
+ }
2318
+ if (typeof setting === "boolean") {
2319
+ bools[key] = setting;
2320
+ }
2321
+ }
2322
+ const filtered = events2.filter((alert) => {
2323
+ var _a2, _b2;
2324
+ const originalEvent = this.buildDefaultSignature(alert);
2325
+ const props = originalEvent == null ? void 0 : originalEvent.properties;
2326
+ const ugcs = (_b2 = (_a2 = props == null ? void 0 : props.geocode) == null ? void 0 : _a2.UGC) != null ? _b2 : [];
2327
+ const _c2 = originalEvent, { details } = _c2, eventWithoutPerformance = __objRest(_c2, ["details"]);
2328
+ originalEvent.properties.parent = originalEvent.properties.event;
2329
+ originalEvent.properties.event = this.betterParsedEventName(originalEvent, bools == null ? void 0 : bools.better_event_parsing, bools == null ? void 0 : bools.parent_events_only);
2330
+ originalEvent.hash = packages.crypto.createHash("md5").update(JSON.stringify(eventWithoutPerformance)).digest("hex");
2331
+ originalEvent.properties.distance = this.getLocationDistances(props, originalEvent.geometry, locationSettings == null ? void 0 : locationSettings.unit);
2332
+ if (originalEvent.properties.is_test == true && (bools == null ? void 0 : bools.ignore_text_products)) return false;
2333
+ if ((bools == null ? void 0 : bools.check_expired) && originalEvent.properties.is_cancelled == true) return false;
2334
+ for (const key in sets) {
2335
+ const setting = sets[key];
2336
+ if (key === "events" && setting.size > 0 && !setting.has(originalEvent.properties.event.toLowerCase())) return false;
2337
+ if (key === "ignored_events" && setting.size > 0 && setting.has(originalEvent.properties.event.toLowerCase())) return false;
2338
+ if (key === "filtered_icao" && setting.size > 0 && props.sender_icao != null && !setting.has(props.sender_icao.toLowerCase())) return false;
2339
+ if (key === "ignored_icao" && setting.size > 0 && props.sender_icao != null && setting.has(props.sender_icao.toLowerCase())) return false;
2340
+ if (key === "ugc_filter" && setting.size > 0 && ugcs.length > 0 && !ugcs.some((ugc) => setting.has(ugc.toLowerCase()))) return false;
2341
+ if (key === "state_filter" && setting.size > 0 && ugcs.length > 0 && !ugcs.some((ugc) => setting.has(ugc.substring(0, 2).toLowerCase()))) return false;
2342
+ }
2343
+ cache.events.emit(`on${originalEvent.properties.parent.replace(/\s+/g, "")}`);
2344
+ cache.events.emit(`on${originalEvent.properties.event.replace(/\s+/g, "")}`);
2345
+ return true;
2346
+ });
2347
+ if (filtered.length > 0) {
2348
+ cache.events.emit(`onEvents`, filtered);
2349
+ }
2350
+ }
2351
+ /**
2352
+ * @function getHeader
2353
+ * @description
2354
+ * Constructs a standardized alert header string using provided
2355
+ * stanza attributes, event properties, and optional VTEC data.
2356
+ *
2357
+ * @static
2358
+ * @param {types.StanzaAttributes} attributes
2359
+ * @param {types.EventProperties} [properties]
2360
+ * @param {types.PVtecEntry} [pVtec]
2361
+ * @returns {string}
2362
+ */
2363
+ static getHeader(attributes, properties, pVtec) {
2364
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
2365
+ const parent = `ATSX`;
2366
+ const alertType = (_d = (_c = (_a = attributes == null ? void 0 : attributes.awipsType) == null ? void 0 : _a.type) != null ? _c : (_b = attributes == null ? void 0 : attributes.getAwip) == null ? void 0 : _b.prefix) != null ? _d : `XX`;
2367
+ const ugc = ((_e = properties == null ? void 0 : properties.geocode) == null ? void 0 : _e.UGC) != null ? (_f = properties == null ? void 0 : properties.geocode) == null ? void 0 : _f.UGC.join(`-`) : `000000`;
2368
+ const status = (_g = pVtec == null ? void 0 : pVtec.status) != null ? _g : "Issued";
2369
+ const issued = (properties == null ? void 0 : properties.issued) != null ? (_h = new Date(properties == null ? void 0 : properties.issued)) == null ? void 0 : _h.toISOString().replace(/[-:]/g, "").split(".")[0] : (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").split(".")[0];
2370
+ const sender = (_i = properties == null ? void 0 : properties.sender_icao) != null ? _i : `XXXX`;
2371
+ const header = `ZCZC-${parent}-${alertType}-${ugc}-${status}-${issued}-${sender}-`;
2372
+ return header;
2373
+ }
2374
+ /**
2375
+ * @function eventHandler
2376
+ * @description
2377
+ * Routes a validated stanza object to the appropriate alert handler
2378
+ * based on its type flags: API, CAP, pVTEC (Primary VTEC), UGC, or plain text.
2379
+ *
2380
+ * @static
2381
+ * @param {types.StanzaCompiled} validated
2382
+ * @returns {void}
2383
+ */
2384
+ static eventHandler(metadata) {
2385
+ const settings2 = settings;
2386
+ const preferences = settings2.noaa_weather_wire_service_settings.preferences;
2387
+ if (metadata.isApi) return api_default.event(metadata);
2388
+ if (metadata.isCap) return cap_default.event(metadata);
2389
+ if (!preferences.disable_vtec && !metadata.isCap && metadata.isPVtec && metadata.isUGC) return vtec_default.event(metadata);
2390
+ if (!preferences.disable_ugc && !metadata.isCap && !metadata.isPVtec && metadata.isUGC) return ugc_default2.event(metadata);
2391
+ if (!preferences.disable_text && !metadata.isCap && !metadata.isPVtec && !metadata.isUGC) return text_default2.event(metadata);
2392
+ return;
2393
+ }
2394
+ /**
2395
+ * @function getICAO
2396
+ * @description
2397
+ * Determines the ICAO code and corresponding name for an event.
2398
+ * Priority is given to the VTEC tracking code, then the attributes' `cccc` property,
2399
+ * and finally the WMO code if available. Returns "N/A" if none are found.
2400
+ *
2401
+ * @private
2402
+ * @static
2403
+ * @param {types.PVtecEntry | null} pVtec
2404
+ * @param {Record<string, string>} attributes
2405
+ * @param {RegExpMatchArray | string | null} WMO
2406
+ * @returns {{ icao: string; name: string }}
2407
+ */
2408
+ static getICAO(pVtec, metadata, WMO) {
2409
+ var _a, _b, _c;
2410
+ const icao = pVtec != null ? pVtec == null ? void 0 : pVtec.tracking.split(`-`)[0] : ((_a = metadata.attributes) == null ? void 0 : _a.cccc) || (WMO != null ? Array.isArray(WMO) ? WMO[0] : WMO : `N/A`);
2411
+ const name = (_c = (_b = definitions.ICAO) == null ? void 0 : _b[icao]) != null ? _c : `N/A`;
2412
+ return { icao, name };
2413
+ }
2414
+ /**
2415
+ * @function getCorrectIssuedDate
2416
+ * @description
2417
+ * Determines the issued date for an event based on the provided attributes.
2418
+ * Falls back to the current date and time if no valid issue date is available.
2419
+ *
2420
+ * @private
2421
+ * @static
2422
+ * @param {Record<string, string>} attributes
2423
+ * @returns {string}
2424
+ */
2425
+ static getCorrectIssuedDate(metadata) {
2426
+ var _a;
2427
+ const time = metadata.attributes.issue != null ? new Date(metadata.attributes.issue).toLocaleString() : ((_a = metadata.attributes) == null ? void 0 : _a.issue) != null ? new Date(metadata.attributes.issue).toLocaleString() : (/* @__PURE__ */ new Date()).toLocaleString();
2428
+ if (time == `Invalid Date`) return (/* @__PURE__ */ new Date()).toLocaleString();
2429
+ return time;
2430
+ }
2431
+ /**
2432
+ * @function getCorrectExpiryDate
2433
+ * @description
2434
+ * Determines the most appropriate expiry date for an event using VTEC or UGC data.
2435
+ * Falls back to one hour from the current time if no valid expiry is available.
2436
+ *
2437
+ * @private
2438
+ * @static
2439
+ * @param {types.PVtecEntry} pVtec
2440
+ * @param {types.UGCEntry} ugc
2441
+ * @returns {string}
2442
+ */
2443
+ static getCorrectExpiryDate(pVtec, ugc) {
2444
+ const time = (pVtec == null ? void 0 : pVtec.expires) && !isNaN(new Date(pVtec.expires).getTime()) ? new Date(pVtec.expires).toLocaleString() : (ugc == null ? void 0 : ugc.expiry) != null ? new Date(ugc.expiry).toLocaleString() : new Date((/* @__PURE__ */ new Date()).getTime() + 1 * 60 * 60 * 1e3).toLocaleString();
2445
+ if (time == `Invalid Date`) return `Until Further Notice`;
2446
+ return time;
2447
+ }
2448
+ /**
2449
+ * @function getLocationDistances
2450
+ * @description
2451
+ * Calculates distances from an event's geometry to all current tracked locations.
2452
+ * Optionally filters locations by a maximum distance.
2453
+ *
2454
+ * @private
2455
+ * @static
2456
+ * @param {types.EventProperties} [properties]
2457
+ * @param {types.EventCompiled} [event]
2458
+ * @param {string} [unit='miles']
2459
+ * @returns {Record<string, { distance: number, unit: string}>}
2460
+ */
2461
+ static getLocationDistances(properties, geometry, unit = "miles") {
2462
+ if (geometry != null) {
2463
+ for (const key in cache.currentLocations) {
2464
+ const coordinates = cache.currentLocations[key];
2465
+ const singleCoord = geometry.coordinates;
2466
+ const center = singleCoord.reduce((acc, [lat, lon]) => [acc[0] + lat, acc[1] + lon], [0, 0]).map((sum) => sum / singleCoord.length);
2467
+ const validUnit = unit === "miles" || unit === "kilometers" ? unit : "miles";
2468
+ const distance = utils_default.calculateDistance({ lat: coordinates.lat, lon: coordinates.lon }, { lat: center[0], lon: center[1] }, validUnit);
2469
+ if (!properties.distance) {
2470
+ properties.distance = {};
2471
+ }
2472
+ properties.distance[key] = { unit, distance };
2473
+ }
2474
+ return properties.distance;
2475
+ }
2476
+ return {};
2477
+ }
2478
+ /**
2479
+ * @function buildDefaultSignature
2480
+ * @description
2481
+ * Populates default properties for an event object, including action type flags,
2482
+ * tags, and status updates. Determines if the event is issued, updated, or cancelled
2483
+ * based on correlations, description content, VTEC codes, and expiration time.
2484
+ *
2485
+ * @private
2486
+ * @static
2487
+ * @param {any} event
2488
+ * @returns {any}
2489
+ */
2490
+ static buildDefaultSignature(event) {
2491
+ var _a, _b;
2492
+ const props = (_a = event.properties) != null ? _a : {};
2493
+ const statusCorrelation = definitions.correlations.find((c) => c.type === props.action_type);
2494
+ const defEventTags = definitions.tags;
2495
+ const tags = Object.entries(defEventTags).filter(([key]) => props == null ? void 0 : props.description.toLowerCase().includes(key.toLowerCase())).map(([, value]) => value);
2496
+ props.tags = tags.length > 0 ? tags : [`N/A`];
2497
+ const setAction = (type) => {
2498
+ props.is_cancelled = type === `C`;
2499
+ props.is_updated = type === `U`;
2500
+ props.is_issued = type === `I`;
2501
+ };
2502
+ if (statusCorrelation) {
2503
+ props.action_type = (_b = statusCorrelation.forward) != null ? _b : props.action_type;
2504
+ props.is_updated = !!statusCorrelation.update;
2505
+ props.is_issued = !!statusCorrelation.new;
2506
+ props.is_cancelled = !!statusCorrelation.cancel;
2507
+ } else {
2508
+ setAction(`I`);
2509
+ }
2510
+ if (props.description) {
2511
+ const detectedPhrase = definitions.cancelSignatures.find((sig) => props.description.toLowerCase().includes(sig.toLowerCase()));
2512
+ if (detectedPhrase) {
2513
+ setAction(`C`);
2514
+ }
2515
+ }
2516
+ if (event.pvtec) {
2517
+ const getType = event.pvtec.split(`.`)[0];
2518
+ const isTestProduct = definitions.productTypes[getType] == `Test Product`;
2519
+ if (isTestProduct) {
2520
+ setAction(`C`);
2521
+ props.is_test = true;
2522
+ }
2523
+ }
2524
+ if (new Date(props == null ? void 0 : props.expires).getTime() < (/* @__PURE__ */ new Date()).getTime()) {
2525
+ setAction(`C`);
2526
+ }
2527
+ return event;
2528
+ }
2529
+ };
2530
+ var events_default = EventParser;
2531
+
2532
+ // src/database.ts
2533
+ var Database = class {
2534
+ /**
2535
+ * @function stanzaCacheImport
2536
+ * @description
2537
+ * Inserts a single NWWS stanza into the database cache. If the total number
2538
+ * of stanzas exceeds the configured maximum history, it deletes the oldest
2539
+ * entries to maintain the limit. Duplicate stanzas are ignored.
2540
+ *
2541
+ * @static
2542
+ * @async
2543
+ * @param {string} stanza
2544
+ * The raw stanza XML or text to store in the database.
2545
+ *
2546
+ * @returns {Promise<void>}
2547
+ * Resolves when the stanza has been inserted and any necessary pruning
2548
+ * of old stanzas has been performed.
2549
+ *
2550
+ * @example
2551
+ * await Database.stanzaCacheImport("<alert>...</alert>");
2552
+ */
2553
+ static stanzaCacheImport(stanza) {
2554
+ return __async(this, null, function* () {
2555
+ const settings2 = settings;
2556
+ try {
2557
+ const db = cache.db;
2558
+ if (!db) return;
2559
+ db.prepare(`INSERT OR IGNORE INTO stanzas (stanza) VALUES (?)`).run(stanza);
2560
+ const countRow = db.prepare(`SELECT COUNT(*) AS total FROM stanzas`).get();
2561
+ const totalRows = countRow.total;
2562
+ const maxHistory = settings2.noaa_weather_wire_service_settings.cache.max_db_history;
2563
+ if (totalRows > maxHistory) {
2564
+ const rowsToDelete = Math.floor((totalRows - maxHistory) / 2);
2565
+ if (rowsToDelete > 0) {
2566
+ db.prepare(`
2567
+ DELETE FROM stanzas
2568
+ WHERE rowid IN (
2569
+ SELECT rowid
2570
+ FROM stanzas
2571
+ ORDER BY rowid ASC
2572
+ LIMIT ?
2573
+ )
2574
+ `).run(rowsToDelete);
2575
+ }
2576
+ }
2577
+ } catch (error) {
2578
+ const msg = error instanceof Error ? error.message : String(error);
2579
+ utils_default.warn(`Failed to import stanza into cache: ${msg}`);
2580
+ }
2581
+ });
2582
+ }
2583
+ /**
2584
+ * @function loadDatabase
2585
+ * @description
2586
+ * Initializes the application's SQLite database, creating necessary tables
2587
+ * for storing stanzas and shapefiles. If the shapefiles table is empty,
2588
+ * it imports predefined shapefiles from disk, processes their features,
2589
+ * and populates the database. Emits warnings during the import process.
2590
+ *
2591
+ * @static
2592
+ * @async
2593
+ * @returns {Promise<void>}
2594
+ * Resolves when the database and shapefiles have been initialized.
2595
+ *
2596
+ * @example
2597
+ * await Database.loadDatabase();
2598
+ * console.log('Database initialized and shapefiles imported.');
2599
+ */
2600
+ static loadDatabase() {
2601
+ return __async(this, null, function* () {
2602
+ const settings2 = settings;
2603
+ try {
2604
+ const { fs: fs2, path: path2, sqlite3: sqlite32, shapefile: shapefile2 } = packages;
2605
+ if (!fs2.existsSync(settings2.database)) fs2.writeFileSync(settings2.database, "");
2606
+ cache.db = new sqlite32(settings2.database);
2607
+ cache.db.prepare(`
2608
+ CREATE TABLE IF NOT EXISTS stanzas (
2609
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2610
+ stanza TEXT
2611
+ )
2612
+ `).run();
2613
+ cache.db.prepare(`
2614
+ CREATE TABLE IF NOT EXISTS shapefiles (
2615
+ id TEXT PRIMARY KEY,
2616
+ location TEXT,
2617
+ geometry TEXT
2618
+ )
2619
+ `).run();
2620
+ const shapefileCount = cache.db.prepare(`SELECT COUNT(*) AS count FROM shapefiles`).get().count;
2621
+ if (shapefileCount === 0) {
2622
+ utils_default.warn(definitions.messages.shapefile_creation);
2623
+ for (const shape of definitions.shapefiles) {
2624
+ const filepath = path2.resolve(__dirname, "../../shapefiles", shape.file);
2625
+ const { features } = yield shapefile2.read(filepath, filepath);
2626
+ utils_default.warn(`Importing ${features.length} entries from ${shape.file}...`);
2627
+ const insertStmt = cache.db.prepare(`
2628
+ INSERT OR REPLACE INTO shapefiles (id, location, geometry) VALUES (?, ?, ?)
2629
+ `);
2630
+ const insertTransaction = cache.db.transaction((entries) => {
2631
+ for (const feature of entries) {
2632
+ const { properties, geometry } = feature;
2633
+ let final, location;
2634
+ if (properties.FIPS) {
2635
+ final = `${properties.STATE}${shape.id}${properties.FIPS.substring(2)}`;
2636
+ location = `${properties.COUNTYNAME}, ${properties.STATE}`;
2637
+ } else if (properties.FULLSTAID) {
2638
+ final = `${properties.ST}${shape.id}${properties.WFO}`;
2639
+ location = `${properties.CITY}, ${properties.STATE}`;
2640
+ } else if (properties.STATE) {
2641
+ final = `${properties.STATE}${shape.id}${properties.ZONE}`;
2642
+ location = `${properties.NAME}, ${properties.STATE}`;
2643
+ } else {
2644
+ final = properties.ID;
2645
+ location = properties.NAME;
2646
+ }
2647
+ insertStmt.run(final, location, JSON.stringify(geometry));
2648
+ }
2649
+ });
2650
+ insertTransaction(features);
2651
+ }
2652
+ utils_default.warn(definitions.messages.shapefile_creation_finished);
2653
+ }
2654
+ } catch (error) {
2655
+ const msg = error instanceof Error ? error.message : String(error);
2656
+ utils_default.warn(`Failed to load database: ${msg}`);
2657
+ }
2658
+ });
2659
+ }
2660
+ };
2661
+ var database_default = Database;
2662
+
2663
+ // src/xmpp.ts
2664
+ var Xmpp = class {
2665
+ /**
2666
+ * @function isSessionReconnectionEligible
2667
+ * @description
2668
+ * Checks if the XMPP session has been inactive longer than the given interval
2669
+ * and, if so, attempts a controlled reconnection.
2670
+ *
2671
+ * @async
2672
+ * @static
2673
+ * @param {number} currentInterval
2674
+ * @returns {Promise<void>}
2675
+ */
2676
+ static isSessionReconnectionEligible(currentInterval) {
2677
+ return __async(this, null, function* () {
2678
+ const settings2 = settings;
2679
+ const lastStanzaElapsed = Date.now() - cache.lastStanza;
2680
+ const threshold = currentInterval * 1e3;
2681
+ if (!cache.isConnected && !cache.sigHalt || !cache.session) {
2682
+ return;
2683
+ }
2684
+ if (lastStanzaElapsed < threshold) {
2685
+ return;
2686
+ }
2687
+ if (cache.attemptingReconnect) {
2688
+ return;
2689
+ }
2690
+ cache.attemptingReconnect = true;
2691
+ cache.isConnected = false;
2692
+ cache.totalReconnects += 1;
2693
+ try {
2694
+ cache.events.emit("onReconnection", {
2695
+ reconnects: cache.totalReconnects,
2696
+ lastStanza: lastStanzaElapsed,
2697
+ lastName: settings2.noaa_weather_wire_service_settings.credentials.nickname
2698
+ });
2699
+ yield cache.session.stop().catch(() => {
2700
+ });
2701
+ yield cache.session.start().catch(() => {
2702
+ });
2703
+ } catch (err) {
2704
+ utils_default.warn(`XMPP reconnection failed: ${err.message}`);
2705
+ } finally {
2706
+ cache.attemptingReconnect = false;
2707
+ }
2708
+ });
2709
+ }
2710
+ /**
2711
+ * @function deploySession
2712
+ * @description
2713
+ * Initializes the NOAA Weather Wire Service (NWWS-OI) XMPP client session and
2714
+ * manages its lifecycle events including connection, disconnection, errors,
2715
+ * and message handling.
2716
+ *
2717
+ * @async
2718
+ * @static
2719
+ * @returns {Promise<void>}
2720
+ */
2721
+ static deploySession() {
2722
+ return __async(this, null, function* () {
2723
+ var _a, _b;
2724
+ const settings2 = settings;
2725
+ (_b = (_a = settings2.noaa_weather_wire_service_settings.credentials).nickname) != null ? _b : _a.nickname = settings2.noaa_weather_wire_service_settings.credentials.username;
2726
+ cache.session = packages.xmpp.client({
2727
+ service: "xmpp://nwws-oi.weather.gov",
2728
+ domain: "nwws-oi.weather.gov",
2729
+ username: settings2.noaa_weather_wire_service_settings.credentials.username,
2730
+ password: settings2.noaa_weather_wire_service_settings.credentials.password
2731
+ });
2732
+ cache.session.on("online", (address) => __async(null, null, function* () {
2733
+ const now = Date.now();
2734
+ if (cache.lastConnect && now - cache.lastConnect < 1e4) {
2735
+ cache.sigHalt = true;
2736
+ utils_default.warn(definitions.messages.reconnect_too_fast);
2737
+ yield utils_default.sleep(2e3);
2738
+ yield cache.session.stop().catch(() => {
2739
+ });
2740
+ return;
2741
+ }
2742
+ cache.isConnected = true;
2743
+ cache.sigHalt = false;
2744
+ cache.lastConnect = now;
2745
+ cache.session.send(packages.xmpp.xml("presence", {
2746
+ to: `nwws@conference.nwws-oi.weather.gov/${settings2.noaa_weather_wire_service_settings.credentials.nickname}`,
2747
+ xmlns: "http://jabber.org/protocol/muc"
2748
+ }));
2749
+ cache.events.emit("onConnection", settings2.noaa_weather_wire_service_settings.credentials.nickname);
2750
+ if (cache.attemptingReconnect) return;
2751
+ cache.attemptingReconnect = true;
2752
+ yield utils_default.sleep(15e3);
2753
+ cache.attemptingReconnect = false;
2754
+ }));
2755
+ cache.session.on("offline", () => {
2756
+ cache.isConnected = false;
2757
+ cache.sigHalt = true;
2758
+ utils_default.warn("XMPP connection went offline");
2759
+ });
2760
+ cache.session.on("error", (error) => {
2761
+ cache.isConnected = false;
2762
+ cache.sigHalt = true;
2763
+ utils_default.warn(`XMPP connection error: ${error.message}`);
2764
+ });
2765
+ cache.session.on("stanza", (stanza) => __async(null, null, function* () {
2766
+ var _a2;
2767
+ try {
2768
+ cache.lastStanza = Date.now();
2769
+ if (stanza.is("message")) {
2770
+ const validate = stanza_default.validate(stanza);
2771
+ const skipMessage = validate.ignore || validate.isCap && !settings2.noaa_weather_wire_service_settings.preferences.cap_only || !validate.isCap && settings2.noaa_weather_wire_service_settings.preferences.cap_only || validate.isCap && !validate.isCapDescription;
2772
+ if (skipMessage) return;
2773
+ yield events_default.eventHandler(validate);
2774
+ yield database_default.stanzaCacheImport(JSON.stringify(validate));
2775
+ cache.events.emit("onMessage", validate);
2776
+ }
2777
+ if (stanza.is("presence") && ((_a2 = stanza.attrs.from) == null ? void 0 : _a2.startsWith("nwws@conference.nwws-oi.weather.gov/"))) {
2778
+ const occupant = stanza.attrs.from.split("/").slice(1).join("/");
2779
+ cache.events.emit("onOccupant", {
2780
+ occupant,
2781
+ type: stanza.attrs.type === "unavailable" ? "unavailable" : "available"
2782
+ });
2783
+ }
2784
+ } catch (err) {
2785
+ utils_default.warn(`Error processing stanza: ${err.message}`);
2786
+ }
2787
+ }));
2788
+ try {
2789
+ yield cache.session.start();
2790
+ } catch (err) {
2791
+ utils_default.warn(`Failed to start XMPP session: ${err.message}`);
2792
+ }
2793
+ });
2794
+ }
2795
+ };
2796
+ var xmpp_default = Xmpp;
2797
+
2798
+ // src/utils.ts
2799
+ var Utils = class _Utils {
2800
+ /**
2801
+ * @function sleep
2802
+ * @description
2803
+ * Pauses execution for a specified number of milliseconds.
2804
+ *
2805
+ * @static
2806
+ * @async
2807
+ * @param {number} ms
2808
+ * @returns {Promise<void>}
2809
+ */
2810
+ static sleep(ms) {
2811
+ return __async(this, null, function* () {
2812
+ return new Promise((resolve) => setTimeout(resolve, ms));
2813
+ });
2814
+ }
2815
+ /**
2816
+ * @function warn
2817
+ * @description
2818
+ * Emits a log event and prints a warning to the console. Throttles repeated
2819
+ * warnings within a short interval unless `force` is `true`.
2820
+ *
2821
+ * @static
2822
+ * @param {string} message
2823
+ * @param {boolean} [force=false]
2824
+ */
2825
+ static warn(message, force = false) {
2826
+ cache.events.emit("log", message);
2827
+ if (!settings.journal) return;
2828
+ if (cache.lastWarn != null && Date.now() - cache.lastWarn < 500 && !force) return;
2829
+ cache.lastWarn = Date.now();
2830
+ console.warn(`\x1B[33m[ATMOSX-PARSER]\x1B[0m [${(/* @__PURE__ */ new Date()).toLocaleString()}] ${message}`);
2831
+ }
2832
+ /**
2833
+ * @function loadCollectionCache
2834
+ * @description
2835
+ * Loads cached NWWS messages from disk, validates them, and passes them
2836
+ * to the event parser. Honors CAP preferences and ignores empty or
2837
+ * incompatible files.
2838
+ *
2839
+ * @static
2840
+ * @async
2841
+ */
2842
+ static loadCollectionCache() {
2843
+ return __async(this, null, function* () {
2844
+ try {
2845
+ const settings2 = settings;
2846
+ if (settings2.noaa_weather_wire_service_settings.cache.enabled && settings2.noaa_weather_wire_service_settings.cache.directory) {
2847
+ if (!packages.fs.existsSync(settings2.noaa_weather_wire_service_settings.cache.directory)) return;
2848
+ const cacheDir = settings2.noaa_weather_wire_service_settings.cache.directory;
2849
+ const getAllFiles = packages.fs.readdirSync(cacheDir).filter((file) => file.endsWith(".bin") && file.startsWith("cache-"));
2850
+ this.warn(definitions.messages.dump_cache.replace(`{count}`, getAllFiles.length.toString()), true);
2851
+ for (const file of getAllFiles) {
2852
+ const filepath = packages.path.join(cacheDir, file);
2853
+ const readFile = packages.fs.readFileSync(filepath, { encoding: "utf-8" });
2854
+ const readSize = packages.fs.statSync(filepath).size;
2855
+ if (readSize == 0) {
2856
+ continue;
2857
+ }
2858
+ const isCap = readFile.includes(`<?xml`);
2859
+ if (isCap && !settings2.noaa_weather_wire_service_settings.preferences.cap_only) continue;
2860
+ if (!isCap && settings2.noaa_weather_wire_service_settings.preferences.cap_only) continue;
2861
+ const validate = stanza_default.validate(readFile, { isCap, raw: true });
2862
+ yield events_default.eventHandler(validate);
2863
+ }
2864
+ this.warn(definitions.messages.dump_cache_complete, true);
2865
+ }
2866
+ } catch (error) {
2867
+ _Utils.warn(`Failed to load cache: ${error.stack}`);
2868
+ }
2869
+ });
2870
+ }
2871
+ /**
2872
+ * @function loadGeoJsonData
2873
+ * @description
2874
+ * Fetches GeoJSON data from the National Weather Service endpoint and
2875
+ * passes it to the event parser for processing.
2876
+ *
2877
+ * @static
2878
+ * @async
2879
+ */
2880
+ static loadGeoJsonData() {
2881
+ return __async(this, null, function* () {
2882
+ try {
2883
+ const settings2 = settings;
2884
+ const response = yield this.createHttpRequest(
2885
+ settings2.national_weather_service_settings.endpoint
2886
+ );
2887
+ if (response.error) return;
2888
+ events_default.eventHandler({
2889
+ message: JSON.stringify(response.message),
2890
+ attributes: {},
2891
+ isCap: true,
2892
+ isApi: true,
2893
+ isPVtec: false,
2894
+ isUGC: false,
2895
+ isCapDescription: false,
2896
+ awipsType: { type: "api", prefix: "AP" },
2897
+ ignore: false
2898
+ });
2899
+ } catch (error) {
2900
+ const msg = error instanceof Error ? error.message : String(error);
2901
+ _Utils.warn(`Failed to load National Weather Service GeoJSON Data: ${msg}`);
2902
+ }
2903
+ });
2904
+ }
2905
+ /**
2906
+ * @function createHttpRequest
2907
+ * @description
2908
+ * Performs an HTTP GET request with default headers and timeout, returning
2909
+ * either the response data or an error message.
2910
+ *
2911
+ * @static
2912
+ * @template T
2913
+ * @param {string} url
2914
+ * @param {types.HTTPSettings} [options]
2915
+ * @returns {Promise<{ error: boolean; message: T | string }>}
2916
+ */
2917
+ static createHttpRequest(url, options) {
2918
+ return __async(this, null, function* () {
2919
+ var _a;
2920
+ const defaultOptions = {
2921
+ timeout: 1e4,
2922
+ headers: {
2923
+ "User-Agent": "AtmosphericX",
2924
+ "Accept": "application/geo+json, text/plain, */*; q=0.9",
2925
+ "Accept-Language": "en-US,en;q=0.9"
2926
+ }
2927
+ };
2928
+ const requestOptions = __spreadProps(__spreadValues(__spreadValues({}, defaultOptions), options), {
2929
+ headers: __spreadValues(__spreadValues({}, defaultOptions.headers), (_a = options == null ? void 0 : options.headers) != null ? _a : {})
2930
+ });
2931
+ try {
2932
+ const resp = yield packages.axios.get(url, {
2933
+ headers: requestOptions.headers,
2934
+ timeout: requestOptions.timeout,
2935
+ maxRedirects: 0,
2936
+ validateStatus: (status) => status === 200 || status === 500
2937
+ });
2938
+ return { error: false, message: resp.data };
2939
+ } catch (err) {
2940
+ const msg = err instanceof Error ? err.message : String(err);
2941
+ return { error: true, message: msg };
2942
+ }
2943
+ });
2944
+ }
2945
+ /**
2946
+ * @function garbageCollectionCache
2947
+ * @description
2948
+ * Deletes cache files exceeding the specified size limit to free disk space.
2949
+ * Recursively traverses the cache directory and removes files larger than
2950
+ * the given maximum.
2951
+ *
2952
+ * @static
2953
+ * @param {number} maxFileMegabytes
2954
+ */
2955
+ static garbageCollectionCache(maxFileMegabytes) {
2956
+ try {
2957
+ const settings2 = settings;
2958
+ const cacheDir = settings2.noaa_weather_wire_service_settings.cache.directory;
2959
+ if (!cacheDir) return;
2960
+ const { fs: fs2, path: path2 } = packages;
2961
+ if (!fs2.existsSync(cacheDir)) return;
2962
+ const maxBytes = maxFileMegabytes * 1024 * 1024;
2963
+ const stackDirs = [cacheDir];
2964
+ const files = [];
2965
+ while (stackDirs.length) {
2966
+ const currentDir = stackDirs.pop();
2967
+ fs2.readdirSync(currentDir).forEach((file) => {
2968
+ const fullPath = path2.join(currentDir, file);
2969
+ const stat = fs2.statSync(fullPath);
2970
+ if (stat.isDirectory()) stackDirs.push(fullPath);
2971
+ else files.push({ file: fullPath, size: stat.size });
2972
+ });
2973
+ }
2974
+ files.forEach((f) => {
2975
+ if (f.size > maxBytes) fs2.unlinkSync(f.file);
2976
+ });
2977
+ } catch (error) {
2978
+ const msg = error instanceof Error ? error.message : String(error);
2979
+ _Utils.warn(`Failed to perform garbage collection: ${msg}`);
2980
+ }
2981
+ }
2982
+ /**
2983
+ * @function handleCronJob
2984
+ * @description
2985
+ * Performs scheduled tasks for NWWS XMPP session maintenance or GeoJSON data
2986
+ * updates depending on the job type.
2987
+ *
2988
+ * @static
2989
+ * @param {boolean} isWire
2990
+ */
2991
+ static handleCronJob(isWire) {
2992
+ try {
2993
+ const settings2 = settings;
2994
+ const cache2 = settings2.noaa_weather_wire_service_settings.cache;
2995
+ const reconnections = settings2.noaa_weather_wire_service_settings.reconnection_settings;
2996
+ if (isWire) {
2997
+ if (cache2.enabled) {
2998
+ void this.garbageCollectionCache(cache2.max_file_size);
2999
+ }
3000
+ if (reconnections.enabled) {
3001
+ void xmpp_default.isSessionReconnectionEligible(reconnections.interval);
3002
+ }
3003
+ } else {
3004
+ void this.loadGeoJsonData();
3005
+ }
3006
+ } catch (error) {
3007
+ const msg = error instanceof Error ? error.message : String(error);
3008
+ _Utils.warn(`Failed to perform scheduled tasks (${isWire ? "NWWS" : "GeoJSON"}): ${msg}`);
3009
+ }
3010
+ }
3011
+ /**
3012
+ * @function mergeClientSettings
3013
+ * @description
3014
+ * Recursively merges a ClientSettings object into a target object,
3015
+ * preserving nested structures and overriding existing values.
3016
+ *
3017
+ * @static
3018
+ * @param {Record<string, unknown>} target
3019
+ * @param {types.ClientSettingsTypes} settings
3020
+ * @returns {Record<string, unknown>}
3021
+ */
3022
+ static mergeClientSettings(target, settings2) {
3023
+ for (const key in settings2) {
3024
+ if (!Object.prototype.hasOwnProperty.call(settings2, key)) continue;
3025
+ const value = settings2[key];
3026
+ if (value && typeof value === "object" && !Array.isArray(value)) {
3027
+ if (!target[key] || typeof target[key] !== "object" || Array.isArray(target[key])) {
3028
+ target[key] = {};
3029
+ }
3030
+ this.mergeClientSettings(target[key], value);
3031
+ } else {
3032
+ target[key] = value;
3033
+ }
3034
+ }
3035
+ return target;
3036
+ }
3037
+ /**
3038
+ * @function calculateDistance
3039
+ * @description
3040
+ * Calculates the great-circle distance between two geographic coordinates
3041
+ * using the haversine formula.
3042
+ *
3043
+ * @static
3044
+ * @param {types.Coordinates} coord1
3045
+ * @param {types.Coordinates} coord2
3046
+ * @param {'miles' | 'kilometers'} [unit='miles']
3047
+ * @returns {number}
3048
+ */
3049
+ static calculateDistance(coord1, coord2, unit = "miles") {
3050
+ if (!coord1 || !coord2) return 0;
3051
+ const { lat: lat1, lon: lon1 } = coord1;
3052
+ const { lat: lat2, lon: lon2 } = coord2;
3053
+ if ([lat1, lon1, lat2, lon2].some((v) => typeof v !== "number")) return 0;
3054
+ const toRad = (deg) => deg * Math.PI / 180;
3055
+ const R = unit === "miles" ? 3958.8 : 6371;
3056
+ const dLat = toRad(lat2 - lat1);
3057
+ const dLon = toRad(lon2 - lon1);
3058
+ const a = __pow(Math.sin(dLat / 2), 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * __pow(Math.sin(dLon / 2), 2);
3059
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
3060
+ return Math.round(R * c * 100) / 100;
3061
+ }
3062
+ /**
3063
+ * @function isReadyToProcess
3064
+ * @description
3065
+ * Determines whether processing can continue based on the current
3066
+ * tracked locations and filter state. Emits limited warnings if no
3067
+ * locations are available.
3068
+ *
3069
+ * @static
3070
+ * @returns {boolean}
3071
+ */
3072
+ static isReadyToProcess() {
3073
+ const totalTracks = Object.keys(cache.currentLocations).length;
3074
+ if (totalTracks > 0) {
3075
+ cache.totalLocationWarns = 0;
3076
+ return true;
3077
+ }
3078
+ if (totalTracks == 0) {
3079
+ return true;
3080
+ }
3081
+ ;
3082
+ if (cache.totalLocationWarns < 3) {
3083
+ _Utils.warn(definitions.messages.no_current_locations);
3084
+ cache.totalLocationWarns++;
3085
+ return false;
3086
+ }
3087
+ _Utils.warn(definitions.messages.disabled_location_warning, true);
3088
+ return true;
3089
+ }
3090
+ };
3091
+ var utils_default = Utils;
3092
+
3093
+ // src/eas.ts
3094
+ var EAS = class {
3095
+ /**
3096
+ * @function generateEASAudio
3097
+ * @description
3098
+ * Generates an EAS (Emergency Alert System) audio file for a given message
3099
+ * and SAME/VTEC code. The audio is composed of optional intro tones, SAME
3100
+ * headers, attention tones, TTS narration of the message, and repeated
3101
+ * SAME headers. The resulting audio is processed for NWR-style broadcast
3102
+ * quality and saved as a WAV file.
3103
+ *
3104
+ * @static
3105
+ * @async
3106
+ * @param {string} message
3107
+ * @param {string} header
3108
+ * @returns {Promise<string | null>}
3109
+ */
3110
+ static generateEASAudio(message, header) {
3111
+ return new Promise((resolve) => __async(this, null, function* () {
3112
+ const settings2 = settings;
3113
+ const assetsDir = settings2.global_settings.eas_settings.directory;
3114
+ const rngFile = `${header.replace(/[^a-zA-Z0-9]/g, `_`)}`.substring(0, 32).replace(/^_+|_+$/g, "");
3115
+ const os2 = packages.os.platform();
3116
+ for (const { regex, replacement } of definitions.messageSignatures) {
3117
+ message = message.replace(regex, replacement);
3118
+ }
3119
+ if (!assetsDir) {
3120
+ utils_default.warn(definitions.messages.eas_no_directory);
3121
+ return resolve(null);
3122
+ }
3123
+ if (!packages.fs.existsSync(assetsDir)) {
3124
+ packages.fs.mkdirSync(assetsDir);
3125
+ }
3126
+ const tmpTTS = packages.path.join(assetsDir, `/tmp/${rngFile}.wav`);
3127
+ const outTTS = packages.path.join(assetsDir, `/output/${rngFile}.wav`);
3128
+ const voice = process.platform === "win32" ? "Microsoft David Desktop" : "en-US-GuyNeural";
3129
+ if (!packages.fs.existsSync(packages.path.join(assetsDir, `/tmp`))) {
3130
+ packages.fs.mkdirSync(packages.path.join(assetsDir, `/tmp`), { recursive: true });
3131
+ }
3132
+ if (!packages.fs.existsSync(packages.path.join(assetsDir, `/output`))) {
3133
+ packages.fs.mkdirSync(packages.path.join(assetsDir, `/output`), { recursive: true });
3134
+ }
3135
+ if (os2 == "win32") {
3136
+ packages.say.export(message, voice, 1, tmpTTS);
3137
+ }
3138
+ if (os2 == "linux") {
3139
+ message = message.replace(/[\r\n]+/g, " ");
3140
+ const festivalCommand = `echo "${message.replace(/"/g, '\\"')}" | text2wave -o "${tmpTTS}"`;
3141
+ packages.child.execSync(festivalCommand);
3142
+ }
3143
+ yield utils_default.sleep(3500);
3144
+ let ttsBuffer = null;
3145
+ while (!packages.fs.existsSync(tmpTTS) || (ttsBuffer = packages.fs.readFileSync(tmpTTS)).length === 0) {
3146
+ yield utils_default.sleep(25);
3147
+ }
3148
+ const ttsWav = this.parseWavPCM16(ttsBuffer);
3149
+ const ttsSamples = this.resamplePCM16(ttsWav.samples, ttsWav.sampleRate, 8e3);
3150
+ const ttsRadio = this.applyNWREffect(ttsSamples, 8e3);
3151
+ let toneRadio = null;
3152
+ if (packages.fs.existsSync(settings2.global_settings.eas_settings.intro_wav)) {
3153
+ const toneBuffer = packages.fs.readFileSync(settings2.global_settings.eas_settings.intro_wav);
3154
+ const toneWav = this.parseWavPCM16(toneBuffer);
3155
+ if (toneWav == null) {
3156
+ console.log(`[EAS] Intro tone WAV file is not valid PCM 16-bit format.`);
3157
+ return resolve(null);
3158
+ }
3159
+ const toneSamples = toneWav.sampleRate !== 8e3 ? this.resamplePCM16(toneWav.samples, toneWav.sampleRate, 8e3) : toneWav.samples;
3160
+ toneRadio = this.applyNWREffect(toneSamples, 8e3);
3161
+ }
3162
+ let build = toneRadio != null ? [toneRadio, this.generateSilence(0.5, 8e3)] : [];
3163
+ build.push(this.generateSAMEHeader(header, 3, 8e3, { preMarkSec: 1.1, gapSec: 0.5 }), this.generateSilence(0.5, 8e3), this.generateAttentionTone(8, 8e3), this.generateSilence(0.5, 8e3), ttsRadio);
3164
+ for (let i = 0; i < 3; i++) {
3165
+ build.push(this.generateSAMEHeader(header, 1, 8e3, { preMarkSec: 0.5, gapSec: 0.1 }));
3166
+ build.push(this.generateSilence(0.5, 8e3));
3167
+ }
3168
+ const allSamples = this.concatPCM16(build);
3169
+ const finalSamples = this.addNoise(allSamples, 2e-3);
3170
+ const outBuffer = this.encodeWavPCM16(Array.from(finalSamples).map((v) => ({ value: v })), 8e3);
3171
+ packages.fs.writeFileSync(outTTS, outBuffer);
3172
+ try {
3173
+ packages.fs.unlinkSync(tmpTTS);
3174
+ } catch (error) {
3175
+ if (error.code !== "EBUSY") {
3176
+ throw error;
3177
+ }
3178
+ }
3179
+ return resolve(outTTS);
3180
+ }));
3181
+ }
3182
+ /**
3183
+ * @function encodeWavPCM16
3184
+ * @description
3185
+ * Encodes an array of 16-bit PCM samples into a standard WAV file buffer.
3186
+ * Produces mono audio with 16 bits per sample and a specified sample rate.
3187
+ *
3188
+ * The input `samples` array should be an array of objects containing a
3189
+ * numeric `value` property representing the PCM sample.
3190
+ *
3191
+ * @private
3192
+ * @static
3193
+ * @param {Record<string, number>[]} samples
3194
+ * @param {number} [sampleRate=8000]
3195
+ * @returns {Buffer}
3196
+ */
3197
+ static encodeWavPCM16(samples, sampleRate = 8e3) {
3198
+ const bytesPerSample = 2;
3199
+ const blockAlign = 1 * bytesPerSample;
3200
+ const byteRate = sampleRate * blockAlign;
3201
+ const subchunk2Size = samples.length * bytesPerSample;
3202
+ const chunkSize = 36 + subchunk2Size;
3203
+ const buffer = Buffer.alloc(44 + subchunk2Size);
3204
+ let o = 0;
3205
+ buffer.write("RIFF", o);
3206
+ o += 4;
3207
+ buffer.writeUInt32LE(chunkSize, o);
3208
+ o += 4;
3209
+ buffer.write("WAVE", o);
3210
+ o += 4;
3211
+ buffer.write("fmt ", o);
3212
+ o += 4;
3213
+ buffer.writeUInt32LE(16, o);
3214
+ o += 4;
3215
+ buffer.writeUInt16LE(1, o);
3216
+ o += 2;
3217
+ buffer.writeUInt16LE(1, o);
3218
+ o += 2;
3219
+ buffer.writeUInt32LE(sampleRate, o);
3220
+ o += 4;
3221
+ buffer.writeUInt32LE(byteRate, o);
3222
+ o += 4;
3223
+ buffer.writeUInt16LE(blockAlign, o);
3224
+ o += 2;
3225
+ buffer.writeUInt16LE(16, o);
3226
+ o += 2;
3227
+ buffer.write("data", o);
3228
+ o += 4;
3229
+ buffer.writeUInt32LE(subchunk2Size, o);
3230
+ o += 4;
3231
+ for (let i = 0; i < samples.length; i++, o += 2) {
3232
+ buffer.writeInt16LE(samples[i].value, o);
3233
+ }
3234
+ return buffer;
3235
+ }
3236
+ /**
3237
+ * @function parseWavPCM16
3238
+ * @description
3239
+ * Parses a WAV buffer containing 16-bit PCM mono audio and extracts
3240
+ * the sample data along with format information.
3241
+ *
3242
+ * Only supports PCM format (audioFormat = 1), 16 bits per sample,
3243
+ * and single-channel (mono) audio. Returns `null` if the buffer
3244
+ * is invalid or does not meet these requirements.
3245
+ *
3246
+ * @private
3247
+ * @static
3248
+ * @param {Buffer} buffer
3249
+ * @returns { { samples: Int16Array; sampleRate: number; channels: number; bitsPerSample: number } | null }
3250
+ */
3251
+ static parseWavPCM16(buffer) {
3252
+ if (buffer.toString("ascii", 0, 4) !== "RIFF" || buffer.toString("ascii", 8, 12) !== "WAVE") {
3253
+ return null;
3254
+ }
3255
+ let fmt = null;
3256
+ let data = null;
3257
+ let i = 12;
3258
+ while (i + 8 <= buffer.length) {
3259
+ const id = buffer.toString("ascii", i, i + 4);
3260
+ const size = buffer.readUInt32LE(i + 4);
3261
+ const start = i + 8;
3262
+ const end = start + size;
3263
+ if (id === "fmt ") fmt = buffer.slice(start, end);
3264
+ if (id === "data") data = buffer.slice(start, end);
3265
+ i = end + size % 2;
3266
+ }
3267
+ if (!fmt || !data) return null;
3268
+ const audioFormat = fmt.readUInt16LE(0);
3269
+ const channels = fmt.readUInt16LE(2);
3270
+ const sampleRate = fmt.readUInt32LE(4);
3271
+ const bitsPerSample = fmt.readUInt16LE(14);
3272
+ if (audioFormat !== 1 || bitsPerSample !== 16 || channels !== 1) {
3273
+ return null;
3274
+ }
3275
+ const samples = new Int16Array(data.buffer, data.byteOffset, data.length / 2);
3276
+ return { samples: new Int16Array(samples), sampleRate, channels, bitsPerSample };
3277
+ }
3278
+ /**
3279
+ * @function concatPCM16
3280
+ * @description
3281
+ * Concatenates multiple Int16Array PCM audio buffers into a single
3282
+ * contiguous Int16Array.
3283
+ *
3284
+ * @private
3285
+ * @static
3286
+ * @param {Int16Array[]} arrays
3287
+ * @returns {Int16Array}
3288
+ */
3289
+ static concatPCM16(arrays) {
3290
+ let total = 0;
3291
+ for (const a of arrays) total += a.length;
3292
+ const out = new Int16Array(total);
3293
+ let o = 0;
3294
+ for (const a of arrays) {
3295
+ out.set(a, o);
3296
+ o += a.length;
3297
+ }
3298
+ return out;
3299
+ }
3300
+ /**
3301
+ * @function pcm16toFloat
3302
+ * @description
3303
+ * Converts a PCM16 Int16Array audio buffer to a Float32Array
3304
+ * with normalized values in the range [-1, 1).
3305
+ *
3306
+ * @private
3307
+ * @static
3308
+ * @param {Int16Array} int16
3309
+ * @returns {Float32Array}
3310
+ */
3311
+ static pcm16toFloat(int16) {
3312
+ const out = new Float32Array(int16.length);
3313
+ for (let i = 0; i < int16.length; i++) out[i] = int16[i] / 32768;
3314
+ return out;
3315
+ }
3316
+ /**
3317
+ * @function floatToPcm16
3318
+ * @description
3319
+ * Converts a Float32Array of audio samples in the range [-1, 1]
3320
+ * to a PCM16 Int16Array.
3321
+ *
3322
+ * @private
3323
+ * @static
3324
+ * @param {Float32Array} float32
3325
+ * @returns {Int16Array}
3326
+ */
3327
+ static floatToPcm16(float32) {
3328
+ const out = new Int16Array(float32.length);
3329
+ for (let i = 0; i < float32.length; i++) {
3330
+ let v = Math.max(-1, Math.min(1, float32[i]));
3331
+ out[i] = Math.round(v * 32767);
3332
+ }
3333
+ return out;
3334
+ }
3335
+ /**
3336
+ * @function resamplePCM16
3337
+ * @description
3338
+ * Resamples a PCM16 audio buffer from an original sample rate to a
3339
+ * target sample rate using linear interpolation.
3340
+ *
3341
+ * @private
3342
+ * @static
3343
+ * @param {Int16Array} int16
3344
+ * @param {number} originalRate
3345
+ * @param {number} targetRate
3346
+ * @returns {Int16Array}
3347
+ */
3348
+ static resamplePCM16(int16, originalRate, targetRate) {
3349
+ if (originalRate === targetRate) return int16;
3350
+ const ratio = targetRate / originalRate;
3351
+ const outLen = Math.max(1, Math.round(int16.length * ratio));
3352
+ const out = new Int16Array(outLen);
3353
+ for (let i = 0; i < outLen; i++) {
3354
+ const pos = i / ratio;
3355
+ const i0 = Math.floor(pos);
3356
+ const i1 = Math.min(i0 + 1, int16.length - 1);
3357
+ const frac = pos - i0;
3358
+ const v = int16[i0] * (1 - frac) + int16[i1] * frac;
3359
+ out[i] = Math.round(v);
3360
+ }
3361
+ return out;
3362
+ }
3363
+ /**
3364
+ * @function generateSilence
3365
+ * @description
3366
+ * Generates a PCM16 audio buffer containing silence for a specified
3367
+ * duration.
3368
+ *
3369
+ * @private
3370
+ * @static
3371
+ * @param {number} ms
3372
+ * @param {number} [sampleRate=8000]
3373
+ * @returns {Int16Array}
3374
+ */
3375
+ static generateSilence(ms, sampleRate = 8e3) {
3376
+ return new Int16Array(Math.floor(ms * sampleRate));
3377
+ }
3378
+ /**
3379
+ * @function generateAttentionTone
3380
+ * @description
3381
+ * Generates a dual-frequency Attention Tone (853 Hz and 960 Hz) used in
3382
+ * EAS/SAME alerts. Produces a PCM16 buffer of the specified duration.
3383
+ *
3384
+ * @private
3385
+ * @static
3386
+ * @param {number} ms
3387
+ * @param {number} [sampleRate=8000]
3388
+ * @returns {Int16Array}
3389
+ */
3390
+ static generateAttentionTone(ms, sampleRate = 8e3) {
3391
+ const len = Math.floor(ms * sampleRate);
3392
+ const out = new Int16Array(len);
3393
+ const f1 = 853;
3394
+ const f2 = 960;
3395
+ const twoPi = Math.PI * 2;
3396
+ const amp = 0.1;
3397
+ const fadeLen = Math.floor(sampleRate * 0);
3398
+ for (let i = 0; i < len; i++) {
3399
+ const t = i / sampleRate;
3400
+ const s = Math.sin(twoPi * f1 * t) + Math.sin(twoPi * f2 * t);
3401
+ let gain = 1;
3402
+ if (i < fadeLen) gain = i / fadeLen;
3403
+ else if (i > len - fadeLen) gain = (len - i) / fadeLen;
3404
+ const v = Math.max(-1, Math.min(1, s / 2 * amp * gain));
3405
+ out[i] = Math.round(v * 32767);
3406
+ }
3407
+ return out;
3408
+ }
3409
+ /**
3410
+ * @function applyNWREffect
3411
+ * @description
3412
+ * Applies a National Weather Radio (NWR)-style audio effect to a PCM16
3413
+ * buffer, including high-pass and low-pass filtering, soft clipping
3414
+ * compression, and optional bit reduction to simulate vintage broadcast
3415
+ * characteristics.
3416
+ *
3417
+ * @private
3418
+ * @static
3419
+ * @param {Int16Array} int16
3420
+ * @param {number} [sampleRate=8000]
3421
+ * @returns {Int16Array}
3422
+ */
3423
+ static applyNWREffect(int16, sampleRate = 8e3) {
3424
+ const hpCut = 3555;
3425
+ const lpCut = 1600;
3426
+ const noiseLevel = 0;
3427
+ const crushBits = 8;
3428
+ const x = this.pcm16toFloat(int16);
3429
+ const dt = 1 / sampleRate;
3430
+ const rcHP = 1 / (2 * Math.PI * hpCut);
3431
+ const aHP = rcHP / (rcHP + dt);
3432
+ let yHP = 0, xPrev = 0;
3433
+ for (let i = 0; i < x.length; i++) {
3434
+ const xi = x[i];
3435
+ yHP = aHP * (yHP + xi - xPrev);
3436
+ xPrev = xi;
3437
+ x[i] = yHP;
3438
+ }
3439
+ const rcLP = 1 / (2 * Math.PI * lpCut);
3440
+ const aLP = dt / (rcLP + dt);
3441
+ let yLP = 0;
3442
+ for (let i = 0; i < x.length; i++) {
3443
+ yLP = yLP + aLP * (x[i] - yLP);
3444
+ x[i] = yLP;
3445
+ }
3446
+ const compGain = 2;
3447
+ const norm = Math.tanh(compGain);
3448
+ for (let i = 0; i < x.length; i++) x[i] = Math.tanh(x[i] * compGain) / norm;
3449
+ const levels = Math.pow(2, crushBits) - 1;
3450
+ return this.floatToPcm16(x);
3451
+ }
3452
+ /**
3453
+ * @function addNoise
3454
+ * @description
3455
+ * Adds random noise to a PCM16 audio buffer and normalizes the signal
3456
+ * to prevent clipping. Useful for simulating real-world signal conditions
3457
+ * or reducing digital artifacts.
3458
+ *
3459
+ * @private
3460
+ * @static
3461
+ * @param {Int16Array} int16
3462
+ * @param {number} [noiseLevel=0.02]
3463
+ * @returns {Int16Array}
3464
+ */
3465
+ static addNoise(int16, noiseLevel = 0.02) {
3466
+ const x = this.pcm16toFloat(int16);
3467
+ for (let i = 0; i < x.length; i++) x[i] += (Math.random() * 2 - 1) * noiseLevel;
3468
+ let peak = 0;
3469
+ for (let i = 0; i < x.length; i++) peak = Math.max(peak, Math.abs(x[i]));
3470
+ if (peak > 1) for (let i = 0; i < x.length; i++) x[i] *= 0.98 / peak;
3471
+ return this.floatToPcm16(x);
3472
+ }
3473
+ /**
3474
+ * @function asciiTo8N1Bits
3475
+ * @description
3476
+ * Converts an ASCII string into a sequence of bits using the 8N1 framing
3477
+ * convention (1 start bit, 8 data bits, 2 stop bits) commonly used in
3478
+ * serial and EAS transmissions.
3479
+ *
3480
+ * @private
3481
+ * @static
3482
+ * @param {string} str
3483
+ * @returns {number[]}
3484
+ */
3485
+ static asciiTo8N1Bits(str) {
3486
+ const bits = [];
3487
+ for (let i = 0; i < str.length; i++) {
3488
+ const c = str.charCodeAt(i) & 255;
3489
+ bits.push(0);
3490
+ for (let b = 0; b < 8; b++) bits.push(c >> b & 1);
3491
+ bits.push(1, 1);
3492
+ }
3493
+ return bits;
3494
+ }
3495
+ /**
3496
+ * @function generateAFSK
3497
+ * @description
3498
+ * Converts a sequence of bits into AFSK-modulated PCM16 audio data for EAS
3499
+ * alerts. Applies a fade-in and fade-out to reduce clicks and generates
3500
+ * the audio at the specified sample rate.
3501
+ *
3502
+ * @private
3503
+ * @static
3504
+ * @param {number[]} bits
3505
+ * @param {number} [sampleRate=8000]
3506
+ * @returns {Int16Array}
3507
+ */
3508
+ static generateAFSK(bits, sampleRate = 8e3) {
3509
+ const baud = 520.83;
3510
+ const markFreq = 2083.3;
3511
+ const spaceFreq = 1562.5;
3512
+ const amplitude = 0.6;
3513
+ const twoPi = Math.PI * 2;
3514
+ const result = [];
3515
+ let phase = 0;
3516
+ let frac = 0;
3517
+ for (let b = 0; b < bits.length; b++) {
3518
+ const bit = bits[b];
3519
+ const freq = bit ? markFreq : spaceFreq;
3520
+ const samplesPerBit = sampleRate / baud + frac;
3521
+ const n = Math.round(samplesPerBit);
3522
+ frac = samplesPerBit - n;
3523
+ const inc = twoPi * freq / sampleRate;
3524
+ for (let i = 0; i < n; i++) {
3525
+ result.push(Math.round(Math.sin(phase) * amplitude * 32767));
3526
+ phase += inc;
3527
+ if (phase > twoPi) phase -= twoPi;
3528
+ }
3529
+ }
3530
+ const fadeSamples = Math.floor(sampleRate * 2e-3);
3531
+ for (let i = 0; i < fadeSamples; i++) {
3532
+ const gain = i / fadeSamples;
3533
+ result[i] = Math.round(result[i] * gain);
3534
+ result[result.length - 1 - i] = Math.round(result[result.length - 1 - i] * gain);
3535
+ }
3536
+ return Int16Array.from(result);
3537
+ }
3538
+ /**
3539
+ * @function generateSAMEHeader
3540
+ * @description
3541
+ * Generates a SAME (Specific Area Message Encoding) audio header for
3542
+ * EAS alerts. Converts a VTEC string into AFSK-modulated PCM16 audio,
3543
+ * optionally repeating the signal with pre-mark and gap intervals.
3544
+ *
3545
+ * @private
3546
+ * @static
3547
+ * @param {string} vtec
3548
+ * @param {number} repeats
3549
+ * @param {number} [sampleRate=8000]
3550
+ * @param {{preMarkSec?: number, gapSec?: number}} [options={}]
3551
+ * @returns {Int16Array}
3552
+ */
3553
+ static generateSAMEHeader(vtec, repeats, sampleRate = 8e3, options = {}) {
3554
+ var _a, _b;
3555
+ const preMarkSec = (_a = options.preMarkSec) != null ? _a : 0.3;
3556
+ const gapSec = (_b = options.gapSec) != null ? _b : 0.1;
3557
+ const bursts = [];
3558
+ const gap = this.generateSilence(gapSec, sampleRate);
3559
+ for (let i = 0; i < repeats; i++) {
3560
+ const bodyBits = this.asciiTo8N1Bits(vtec);
3561
+ const body = this.generateAFSK(bodyBits, sampleRate);
3562
+ const extendedBodyDuration = Math.round(preMarkSec * sampleRate);
3563
+ const extendedBody = new Int16Array(extendedBodyDuration + gap.length);
3564
+ for (let j = 0; j < extendedBodyDuration; j++) {
3565
+ extendedBody[j] = Math.round(body[j % body.length] * 0.2);
3566
+ }
3567
+ extendedBody.set(gap, extendedBodyDuration);
3568
+ bursts.push(extendedBody);
3569
+ if (i !== repeats - 1) bursts.push(gap);
3570
+ }
3571
+ return this.concatPCM16(bursts);
3572
+ }
3573
+ };
3574
+ var eas_default = EAS;
3575
+
3576
+ // src/index.ts
3577
+ var AlertManager = class {
3578
+ constructor(metadata) {
3579
+ this.start(metadata);
3580
+ }
3581
+ /**
3582
+ * @function setDisplayName
3583
+ * @description
3584
+ * Sets the display nickname for the NWWS XMPP session. Trims the provided
3585
+ * name and validates it, emitting a warning if the name is empty or invalid.
3586
+ *
3587
+ * @param {string} [name]
3588
+ */
3589
+ setDisplayName(name) {
3590
+ const settings2 = settings;
3591
+ const trimmed = name == null ? void 0 : name.trim();
3592
+ if (!trimmed) {
3593
+ utils_default.warn(definitions.messages.invalid_nickname);
3594
+ return;
3595
+ }
3596
+ settings2.noaa_weather_wire_service_settings.credentials.nickname = trimmed;
3597
+ }
3598
+ /**
3599
+ * @function setCurrentLocation
3600
+ * @description
3601
+ * Sets the current location with a name and geographic coordinates.
3602
+ * Validates the coordinates before updating the cache, emitting warnings
3603
+ * if values are missing or invalid.
3604
+ *
3605
+ * @param {string} locationName
3606
+ * @param {types.Coordinates} [coordinates]
3607
+ */
3608
+ setCurrentLocation(locationName, coordinates) {
3609
+ if (!coordinates) {
3610
+ utils_default.warn(`Coordinates not provided for location: ${locationName}`);
3611
+ return;
3612
+ }
3613
+ const { lat, lon } = coordinates;
3614
+ if (typeof lat !== "number" || typeof lon !== "number" || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
3615
+ utils_default.warn(definitions.messages.invalid_coordinates.replace("{lat}", String(lat)).replace("{lon}", String(lon)));
3616
+ return;
3617
+ }
3618
+ cache.currentLocations[locationName] = coordinates;
3619
+ }
3620
+ /**
3621
+ * @function createEasAudio
3622
+ * @description
3623
+ * Generates an EAS (Emergency Alert System) audio file using the provided
3624
+ * description and header.
3625
+ *
3626
+ * @async
3627
+ * @param {string} description
3628
+ * @param {string} header
3629
+ * @returns {Promise<Buffer>}
3630
+ */
3631
+ createEasAudio(description, header) {
3632
+ return __async(this, null, function* () {
3633
+ return yield eas_default.generateEASAudio(description, header);
3634
+ });
3635
+ }
3636
+ /**
3637
+ * @function getAllAlertTypes
3638
+ * @description
3639
+ * Generates a list of all possible alert types by combining defined
3640
+ * event names with action names.
3641
+ *
3642
+ * @returns {string[]}
3643
+ */
3644
+ getAllAlertTypes() {
3645
+ const events2 = new Set(Object.values(definitions.events));
3646
+ const actions = new Set(Object.values(definitions.actions));
3647
+ return Array.from(events2).flatMap(
3648
+ (event) => Array.from(actions).map((action) => `${event} ${action}`)
3649
+ );
3650
+ }
3651
+ /**
3652
+ * @function searchStanzaDatabase
3653
+ * @description
3654
+ * Searches the stanza database for entries containing the specified query.
3655
+ * Escapes SQL wildcard characters and returns results in descending order
3656
+ * by ID, up to the specified limit.
3657
+ *
3658
+ * @async
3659
+ * @param {string} query
3660
+ * @param {number} [limit=250]
3661
+ * @returns {Promise<any[]>}
3662
+ */
3663
+ searchStanzaDatabase(query, limit = 250) {
3664
+ return __async(this, null, function* () {
3665
+ const escapeLike = (s) => s.replace(/[%_]/g, "\\$&");
3666
+ const rows = yield cache.db.prepare(`SELECT * FROM stanzas WHERE stanza LIKE ? ESCAPE '\\' ORDER BY id DESC LIMIT ${limit}`).all(`%${escapeLike(query)}%`);
3667
+ return rows;
3668
+ });
3669
+ }
3670
+ /**
3671
+ * @function setSettings
3672
+ * @description
3673
+ * Merges the provided client settings into the current configuration,
3674
+ * preserving nested structures.
3675
+ *
3676
+ * @async
3677
+ * @param {types.ClientSettingsTypes} settings
3678
+ * @returns {Promise<void>}
3679
+ */
3680
+ setSettings(settings2) {
3681
+ return __async(this, null, function* () {
3682
+ utils_default.mergeClientSettings(settings, settings2);
3683
+ });
3684
+ }
3685
+ /**
3686
+ * @function on
3687
+ * @description
3688
+ * Registers a callback for a specific event and returns a function
3689
+ * to unregister the listener.
3690
+ *
3691
+ * @param {string} event
3692
+ * @param {(...args: any[]) => void} callback
3693
+ * @returns {() => void}
3694
+ */
3695
+ on(event, callback) {
3696
+ cache.events.on(event, callback);
3697
+ return () => cache.events.off(event, callback);
3698
+ }
3699
+ /**
3700
+ * @function start
3701
+ * @description
3702
+ * Initializes the client with the provided settings, starts the NWWS XMPP
3703
+ * session if applicable, loads cached messages, and sets up scheduled
3704
+ * tasks (cron jobs) for ongoing processing.
3705
+ *
3706
+ * @async
3707
+ * @param {types.ClientSettingsTypes} metadata
3708
+ * @returns {Promise<void>}
3709
+ */
3710
+ start(metadata) {
3711
+ return __async(this, null, function* () {
3712
+ if (!cache.isReady) {
3713
+ utils_default.warn(definitions.messages.not_ready);
3714
+ return;
3715
+ }
3716
+ this.setSettings(metadata);
3717
+ const settings2 = settings;
3718
+ this.isNoaaWeatherWireService = settings2.is_wire;
3719
+ cache.isReady = false;
3720
+ while (!utils_default.isReadyToProcess()) {
3721
+ yield utils_default.sleep(2e3);
3722
+ }
3723
+ yield database_default.loadDatabase();
3724
+ if (this.isNoaaWeatherWireService) {
3725
+ (() => __async(this, null, function* () {
3726
+ try {
3727
+ yield xmpp_default.deploySession();
3728
+ yield utils_default.loadCollectionCache();
3729
+ } catch (err) {
3730
+ const msg = err instanceof Error ? err.message : String(err);
3731
+ utils_default.warn(`Failed to initialize NWWS services: ${msg}`);
3732
+ }
3733
+ }))();
3734
+ }
3735
+ utils_default.handleCronJob(this.isNoaaWeatherWireService);
3736
+ if (this.job) {
3737
+ try {
3738
+ this.job.stop();
3739
+ } catch (e) {
3740
+ utils_default.warn(`Failed to stop existing cron job.`);
3741
+ }
3742
+ this.job = null;
3743
+ }
3744
+ const interval = !this.isNoaaWeatherWireService ? settings2.national_weather_service_settings.interval : 5;
3745
+ this.job = new packages.jobs.Cron(`*/${interval} * * * * *`, () => {
3746
+ utils_default.handleCronJob(this.isNoaaWeatherWireService);
3747
+ });
3748
+ });
3749
+ }
3750
+ /**
3751
+ * @function stop
3752
+ * @description
3753
+ * Stops active scheduled tasks (cron job) and, if connected, the NWWS
3754
+ * XMPP session. Updates relevant cache flags to indicate the session
3755
+ * is no longer active.
3756
+ *
3757
+ * @async
3758
+ * @returns {Promise<void>}
3759
+ */
3760
+ stop() {
3761
+ return __async(this, null, function* () {
3762
+ cache.isReady = true;
3763
+ if (this.job) {
3764
+ try {
3765
+ this.job.stop();
3766
+ } catch (e) {
3767
+ utils_default.warn(`Failed to stop cron job.`);
3768
+ }
3769
+ this.job = null;
3770
+ }
3771
+ const session = cache.session;
3772
+ if (session && this.isNoaaWeatherWireService) {
3773
+ try {
3774
+ yield session.stop();
3775
+ } catch (e) {
3776
+ utils_default.warn(`Failed to stop XMPP session.`);
3777
+ }
3778
+ cache.sigHalt = true;
3779
+ cache.isConnected = false;
3780
+ cache.session = null;
3781
+ this.isNoaaWeatherWireService = false;
3782
+ }
3783
+ });
3784
+ }
3785
+ };
3786
+ var index_default = AlertManager;
3787
+ // Annotate the CommonJS export names for ESM import in node:
3788
+ 0 && (module.exports = {
3789
+ AlertManager,
3790
+ Database,
3791
+ EAS,
3792
+ EventParser,
3793
+ HVtecParser,
3794
+ PVtecParser,
3795
+ StanzaParser,
3796
+ TextParser,
3797
+ UGCParser,
3798
+ Utils
3799
+ });