atmosx-nwws-parser 1.0.19 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -64
- package/dist/cjs/index.cjs +3799 -0
- package/dist/esm/index.mjs +3757 -0
- package/package.json +49 -37
- package/src/bootstrap.ts +196 -0
- package/src/database.ts +148 -0
- package/src/dictionaries/awips.ts +342 -0
- package/src/dictionaries/events.ts +142 -0
- package/src/dictionaries/icao.ts +237 -0
- package/src/dictionaries/offshore.ts +12 -0
- package/src/dictionaries/signatures.ts +107 -0
- package/src/eas.ts +493 -0
- package/src/index.ts +229 -0
- package/src/parsers/events/api.ts +151 -0
- package/src/parsers/events/cap.ts +138 -0
- package/src/parsers/events/text.ts +106 -0
- package/src/parsers/events/ugc.ts +109 -0
- package/src/parsers/events/vtec.ts +78 -0
- package/src/parsers/events.ts +367 -0
- package/src/parsers/hvtec.ts +46 -0
- package/src/parsers/pvtec.ts +71 -0
- package/src/parsers/stanza.ts +132 -0
- package/src/parsers/text.ts +166 -0
- package/src/parsers/ugc.ts +197 -0
- package/src/types.ts +251 -0
- package/src/utils.ts +314 -0
- package/src/xmpp.ts +144 -0
- package/test.js +58 -34
- package/tsconfig.json +12 -5
- package/tsup.config.ts +14 -0
- package/bootstrap.ts +0 -122
- package/dist/bootstrap.js +0 -153
- package/dist/src/events.js +0 -585
- package/dist/src/helper.js +0 -463
- package/dist/src/stanza.js +0 -147
- package/dist/src/text-parser.js +0 -119
- package/dist/src/ugc.js +0 -214
- package/dist/src/vtec.js +0 -125
- package/src/events.ts +0 -394
- package/src/helper.ts +0 -298
- package/src/stanza.ts +0 -102
- package/src/text-parser.ts +0 -120
- package/src/ugc.ts +0 -122
- package/src/vtec.ts +0 -99
|
@@ -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
|
+
};
|