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