atmosx-nwws-parser 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +6 -0
  3. package/dist/cjs/bootstrap.cjs +1009 -0
  4. package/dist/cjs/database.cjs +1114 -0
  5. package/dist/cjs/dictionaries/awips.cjs +379 -0
  6. package/dist/cjs/dictionaries/events.cjs +139 -0
  7. package/dist/cjs/dictionaries/icao.cjs +265 -0
  8. package/dist/cjs/dictionaries/offshore.cjs +40 -0
  9. package/dist/cjs/dictionaries/signatures.cjs +132 -0
  10. package/dist/cjs/eas.cjs +2857 -0
  11. package/dist/cjs/helper.cjs +3014 -0
  12. package/dist/cjs/parsers/events.cjs +2857 -0
  13. package/dist/cjs/parsers/stanza.cjs +1108 -0
  14. package/dist/cjs/parsers/text.cjs +1142 -0
  15. package/dist/cjs/parsers/types/api.cjs +2857 -0
  16. package/dist/cjs/parsers/types/cap.cjs +2857 -0
  17. package/dist/cjs/parsers/types/text.cjs +2857 -0
  18. package/dist/cjs/parsers/types/ugc.cjs +2857 -0
  19. package/dist/cjs/parsers/types/vtec.cjs +2857 -0
  20. package/dist/cjs/parsers/ugc.cjs +1139 -0
  21. package/dist/cjs/parsers/vtec.cjs +1060 -0
  22. package/dist/cjs/types.cjs +17 -0
  23. package/dist/cjs/utils.cjs +2857 -0
  24. package/dist/cjs/xmpp.cjs +2857 -0
  25. package/dist/esm/bootstrap.mjs +972 -0
  26. package/dist/esm/database.mjs +1079 -0
  27. package/dist/esm/dictionaries/awips.mjs +355 -0
  28. package/dist/esm/dictionaries/events.mjs +111 -0
  29. package/dist/esm/dictionaries/icao.mjs +241 -0
  30. package/dist/esm/dictionaries/offshore.mjs +16 -0
  31. package/dist/esm/dictionaries/signatures.mjs +106 -0
  32. package/dist/esm/eas.mjs +2824 -0
  33. package/dist/esm/helper.mjs +2974 -0
  34. package/dist/esm/parsers/events.mjs +2824 -0
  35. package/dist/esm/parsers/stanza.mjs +1072 -0
  36. package/dist/esm/parsers/text.mjs +1106 -0
  37. package/dist/esm/parsers/types/api.mjs +2824 -0
  38. package/dist/esm/parsers/types/cap.mjs +2824 -0
  39. package/dist/esm/parsers/types/text.mjs +2824 -0
  40. package/dist/esm/parsers/types/ugc.mjs +2824 -0
  41. package/dist/esm/parsers/types/vtec.mjs +2824 -0
  42. package/dist/esm/parsers/ugc.mjs +1104 -0
  43. package/dist/esm/parsers/vtec.mjs +1025 -0
  44. package/dist/esm/types.mjs +0 -0
  45. package/dist/esm/utils.mjs +2824 -0
  46. package/dist/esm/xmpp.mjs +2824 -0
  47. package/package.json +47 -0
  48. package/shapefiles/FireCounties.dbf +0 -0
  49. package/shapefiles/FireCounties.shp +0 -0
  50. package/shapefiles/FireZones.dbf +0 -0
  51. package/shapefiles/FireZones.shp +0 -0
  52. package/shapefiles/ForecastZones.dbf +0 -0
  53. package/shapefiles/ForecastZones.shp +0 -0
  54. package/shapefiles/Marine.dbf +0 -0
  55. package/shapefiles/Marine.shp +0 -0
  56. package/shapefiles/OffShoreZones.dbf +0 -0
  57. package/shapefiles/OffShoreZones.shp +0 -0
  58. package/shapefiles/USCounties.dbf +0 -0
  59. package/shapefiles/USCounties.shp +0 -0
  60. package/src/bootstrap.ts +171 -0
  61. package/src/database.ts +99 -0
  62. package/src/dictionaries/awips.ts +351 -0
  63. package/src/dictionaries/events.ts +109 -0
  64. package/src/dictionaries/icao.ts +237 -0
  65. package/src/dictionaries/offshore.ts +12 -0
  66. package/src/dictionaries/signatures.ts +103 -0
  67. package/src/eas.ts +428 -0
  68. package/src/helper.ts +167 -0
  69. package/src/parsers/events.ts +289 -0
  70. package/src/parsers/stanza.ts +103 -0
  71. package/src/parsers/text.ts +167 -0
  72. package/src/parsers/types/api.ts +94 -0
  73. package/src/parsers/types/cap.ts +89 -0
  74. package/src/parsers/types/text.ts +54 -0
  75. package/src/parsers/types/ugc.ts +85 -0
  76. package/src/parsers/types/vtec.ts +60 -0
  77. package/src/parsers/ugc.ts +148 -0
  78. package/src/parsers/vtec.ts +66 -0
  79. package/src/types.ts +187 -0
  80. package/src/utils.ts +216 -0
  81. package/src/xmpp.ts +123 -0
  82. package/test.js +1 -0
  83. package/tsconfig.json +14 -0
  84. package/tsup.config.ts +11 -0
@@ -0,0 +1,289 @@
1
+ /*
2
+ _ _ __ __
3
+ /\ | | | | (_) \ \ / /
4
+ / \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
5
+ / /\ \| __| "_ ` _ \ / _ \/ __| "_ \| "_ \ / _ \ "__| |/ __| > <
6
+ / ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
7
+ /_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
8
+ | |
9
+ |_|
10
+
11
+ Written by: KiyoWx (k3yomi)
12
+ */
13
+
14
+ import * as loader from '../bootstrap';
15
+ import * as types from '../types';
16
+ import TextParser from './text';
17
+ import UGCParser from './ugc';
18
+ import VTECAlerts from './types/vtec';
19
+ import UGCAlerts from './types/ugc';
20
+ import TextAlerts from './types/text';
21
+ import CAPAlerts from './types/cap';
22
+ import APIAlerts from './types/api';
23
+
24
+
25
+ import EAS from '../eas';
26
+
27
+ export class EventParser {
28
+
29
+
30
+ /**
31
+ * getBaseProperties extracts and compiles the base properties of an alert message, including location, timing, description, sender information, and various parameters.
32
+ *
33
+ * @public
34
+ * @static
35
+ * @async
36
+ * @param {string} message
37
+ * @param {types.TypeCompiled} validated
38
+ * @param {types.UGCParsed} [ugc=null]
39
+ * @param {types.VTECParsed} [vtec=null]
40
+ * @returns {Promise<types.BaseProperties>}
41
+ */
42
+ public static async getBaseProperties(message: string, validated: types.TypeCompiled, ugc: types.UGCParsed = null, vtec: types.VTECParsed = null): Promise<types.BaseProperties> {
43
+ const settings = loader.settings as types.ClientSettings;
44
+ const definitions = {
45
+ tornado: TextParser.textProductToString(message, `TORNADO...`) || TextParser.textProductToString(message, `WATERSPOUT...`) || `N/A`,
46
+ hail: TextParser.textProductToString(message, `MAX HAIL SIZE...`, [`IN`]) || TextParser.textProductToString(message, `HAIL...`, [`IN`]) || `N/A`,
47
+ gusts: TextParser.textProductToString(message, `MAX WIND GUST...`) || TextParser.textProductToString(message, `WIND...`) || `N/A`,
48
+ flood: TextParser.textProductToString(message, `FLASH FLOOD...`) || `N/A`,
49
+ damage: TextParser.textProductToString(message, `DAMAGE THREAT...`) || `N/A`,
50
+ source: TextParser.textProductToString(message, `SOURCE...`, [`.`]) || `N/A`,
51
+ attributes: TextParser.textProductToString(message, `STANZA ATTRIBUTES...`) ? JSON.parse(TextParser.textProductToString(message, `STANZA ATTRIBUTES...`)) : null,
52
+ polygon: TextParser.textProductToPolygon(message),
53
+ description: TextParser.textProductToDescription(message, vtec?.raw ?? null),
54
+ wmo: message.match(new RegExp(loader.definitions.expressions.wmo, 'imu')),
55
+ mdTorIntensity: TextParser.textProductToString(message, `MOST PROBABLE PEAK TORNADO INTENSITY...`) || `N/A`,
56
+ mdWindGusts: TextParser.textProductToString(message, `MOST PROBABLE PEAK WIND GUST...`) || `N/A`,
57
+ mdHailSize: TextParser.textProductToString(message, `MOST PROBABLE PEAK HAIL SIZE...`) || `N/A`,
58
+ };
59
+ const getOffice = this.getICAO(vtec, validated.attributes ?? definitions.attributes ?? {}, definitions.wmo);
60
+ const getCorrectIssued = this.getCorrectIssuedDate(definitions.attributes ?? validated.attributes ?? {});
61
+ const getCorrectExpiry = this.getCorrectExpiryDate(vtec, ugc);
62
+ const getAwip = TextParser.awipTextToEvent(definitions.attributes?.awipsid ?? validated.awipsType.prefix);
63
+ const base = {
64
+ locations: ugc?.locations.join(`; `) || `No Location Specified (UGC Missing)`,
65
+ issued: getCorrectIssued,
66
+ expires: getCorrectExpiry,
67
+ geocode: { UGC: ugc?.zones || [`XX000`] },
68
+ description: definitions.description,
69
+ sender_name: getOffice.name,
70
+ sender_icao: getOffice.icao,
71
+ attributes: {
72
+ ...validated.attributes,
73
+ ...definitions.attributes,
74
+ getAwip,
75
+ },
76
+ parameters: {
77
+ wmo: Array.isArray(definitions.wmo) ? definitions.wmo[0] : (definitions.wmo ?? `N/A`),
78
+ source: definitions.source,
79
+ max_hail_size: definitions.hail,
80
+ max_wind_gust: definitions.gusts,
81
+ damage_threat: definitions.damage,
82
+ tornado_detection: definitions.tornado,
83
+ flood_detection: definitions.flood,
84
+ discussion_tornado_intensity: definitions.mdTorIntensity,
85
+ discussion_wind_intensity: definitions.mdWindGusts,
86
+ discussion_hail_intensity: definitions.mdHailSize,
87
+ },
88
+ geometry: definitions.polygon.length > 0 ? { type: "Polygon", coordinates: definitions.polygon } : null
89
+ };
90
+ if (settings.NoaaWeatherWireService.alertPreferences.isShapefileUGC && base.geometry == null && ugc != null) {
91
+ const coordinates = await UGCParser.getCoordinates(ugc.zones);
92
+ base.geometry = { type: "Polygon", coordinates };
93
+ }
94
+ return base;
95
+ }
96
+
97
+ /**
98
+ * enhanceEvent refines the event name based on specific conditions and tags found in the event's description and parameters.
99
+ *
100
+ * @public
101
+ * @static
102
+ * @param {types.TypeAlert} event
103
+ * @returns {{ eventName: any; tags: any; }}
104
+ */
105
+ public static enhanceEvent(event: types.TypeAlert) {
106
+ let eventName = event?.properties?.event ?? `Unknown Event`;
107
+ const defEventTable = loader.definitions.enhancedEvents;
108
+ const defEventTags = loader.definitions.tags
109
+
110
+ const properties = event?.properties;
111
+ const parameters = properties?.parameters;
112
+
113
+ const description = properties?.description ?? `Unknown Description`;
114
+ const damageThreatTag = parameters?.damage_threat ?? `N/A`;
115
+ const tornadoThreatTag = parameters?.tornado_detection ?? `N/A`;
116
+
117
+
118
+ for (const eventGroup of defEventTable) {
119
+ const [baseEvent, conditions] = Object.entries(eventGroup)[0] as [string, Record<string, types.EnhancedEventCondition>];
120
+ if (eventName === baseEvent) {
121
+ for (const [specificEvent, condition] of Object.entries(conditions)) {
122
+ const conditionMet = (condition.description && description.includes(condition.description.toLowerCase())) || (condition.condition && condition.condition(damageThreatTag || tornadoThreatTag));
123
+ if (conditionMet) { eventName = specificEvent; break; }
124
+ }
125
+ if (baseEvent === 'Severe Thunderstorm Warning' && damageThreatTag === 'POSSIBLE' && !eventName.includes('(TPROB)')) eventName += ' (TPROB)';
126
+ break;
127
+ }
128
+ }
129
+ const tags = Object.entries(defEventTags).filter(([key]) => description.includes(key.toLowerCase())).map(([, value]) => value)
130
+ return { eventName, tags: tags.length > 0 ? tags : [`N/A`] }
131
+ }
132
+
133
+ /**
134
+ * getCorrectExpiryDate determines the correct expiration date for an alert based on VTEC information or UGC zones.
135
+ *
136
+ * @public
137
+ * @static
138
+ * @param {unknown[]} events
139
+ */
140
+ public static validateEvents(events: unknown[]) {
141
+ if (events.length == 0) return;
142
+
143
+ const settings = loader.settings as types.ClientSettings;
144
+ const filteringSettings = loader.settings?.global?.alertFiltering;
145
+ const easSettings = loader.settings?.global?.easSettings;
146
+ const globalSettings = loader.settings?.global;
147
+ const sets = {} as Record<string, Set<string>>;
148
+ const bools = {} as Record<string, boolean>;
149
+ const megered = {...filteringSettings, ...easSettings, ...globalSettings};
150
+
151
+ for (const key in megered) {
152
+ const setting = megered[key];
153
+ if (Array.isArray(setting)) { sets[key] = new Set(setting.map(item => item.toLowerCase())); }
154
+ if (typeof setting === 'boolean') { bools[key] = setting; }
155
+ }
156
+
157
+ const filtered = events.filter((alert: any) => {
158
+ const originalEvent = alert
159
+ const props = originalEvent?.properties;
160
+ const ugcs = props?.geocode?.UGC ?? [];
161
+ if (bools?.betterEventParsing) {
162
+ const { eventName, tags } = this.enhanceEvent(originalEvent);
163
+ originalEvent.properties.event = eventName;
164
+ originalEvent.properties.tags = tags;
165
+ }
166
+ const eventCheck = bools?.useParentEvents ? props.parent?.toLowerCase() : props.event?.toLowerCase();
167
+ const statusCorrelation = loader.definitions.correlations.find((c: { type: string }) => c.type === originalEvent.properties.action_type);
168
+ for (const key in sets) {
169
+ const setting = sets[key];
170
+ if (key === 'filteredEvents' && setting.size > 0 && eventCheck != null && !setting.has(eventCheck)) return false;
171
+ if (key === 'ignoredEvents' && setting.size > 0 && eventCheck != null && setting.has(eventCheck)) return false;
172
+ if (key === 'filteredICOAs' && setting.size > 0 && props.sender_icao != null && !setting.has(props.sender_icao.toLowerCase())) return false;
173
+ if (key === 'ignoredICOAs' && setting.size > 0 && props.sender_icao != null && setting.has(props.sender_icao.toLowerCase())) return false;
174
+ if (key === 'ugcFilter' && setting.size > 0 && ugcs.length > 0 && !ugcs.some((ugc: string) => setting.has(ugc.toLowerCase()))) return false;
175
+ if (key === 'stateFilter' && setting.size > 0 && ugcs.length > 0 && !ugcs.some((ugc: string) => setting.has(ugc.substring(0, 2).toLowerCase()))) return false;
176
+ if (key === 'easAlerts' && setting.size > 0 && eventCheck != null && setting.has(eventCheck) && settings.isNWWS) { EAS.generateEASAudio(props.description, alert.header) }
177
+ }
178
+ for (const key in bools) {
179
+ const setting = bools[key];
180
+ if (key === 'checkExpired' && setting && new Date(props?.expires).getTime() < new Date().getTime()) return false;
181
+ }
182
+ originalEvent.properties.action_type = statusCorrelation ? statusCorrelation.forward : originalEvent.properties.action_type;
183
+ originalEvent.properties.is_cancelled = statusCorrelation ? (statusCorrelation.cancel == true && bools.checkExpired) : false;
184
+ if (props.description) {
185
+ const detectedPhrase = loader.definitions.cancelSignatures.find(sig => props.description.toLowerCase().includes(sig.toLowerCase()));
186
+ if (detectedPhrase && bools.checkExpired) { originalEvent.properties.action_type = 'Cancel'; originalEvent.properties.is_cancelled = true; return false; }
187
+ }
188
+ if (originalEvent.vtec) {
189
+ const getType = originalEvent.vtec.split(`.`)[0];
190
+ const isTestProduct = loader.definitions.productTypes[getType] == `Test Product`
191
+ if (isTestProduct) { return false; }
192
+ }
193
+ loader.cache.events.emit(`on${originalEvent.properties.parent.replace(/\s+/g, '')}`)
194
+ return true;
195
+ })
196
+ if (filtered.length > 0) { loader.cache.events.emit(`onAlerts`, filtered); }
197
+ }
198
+
199
+ /**
200
+ * getHeader constructs a standardized alert header string based on provided attributes, properties, and VTEC information.
201
+ *
202
+ * @public
203
+ * @static
204
+ * @param {types.TypeAttributes} attributes
205
+ * @param {?types.BaseProperties} [properties]
206
+ * @param {?types.VTECParsed} [vtec]
207
+ * @returns {string}
208
+ */
209
+ public static getHeader(attributes: types.TypeAttributes, properties?: types.BaseProperties, vtec?: types.VTECParsed) {
210
+ const parent = `ATSX`
211
+ const alertType = attributes?.awipsType?.type ?? attributes?.getAwip?.prefix ?? `XX`;
212
+ const ugc = properties?.geocode?.UGC != null ? properties?.geocode?.UGC.join(`-`) : `000000`;
213
+ const status = vtec?.status || 'Issued';
214
+ const issued = properties?.issued != null ? new Date(properties?.issued)?.toISOString().replace(/[-:]/g, '').split('.')[0] : new Date().toISOString().replace(/[-:]/g, '').split('.')[0];
215
+ const sender = properties?.sender_icao || `XXXX`;
216
+ const header = `ZCZC-${parent}-${alertType}-${ugc}-${status}-${issued}-${sender}-`;
217
+ return header
218
+ }
219
+
220
+ /**
221
+ * eventHandler routes the validated alert message to the appropriate event parser based on its type (API, CAP, VTEC, UGC, or plain text).
222
+ *
223
+ * @public
224
+ * @static
225
+ * @param {types.TypeCompiled} validated
226
+ * @returns {*}
227
+ */
228
+ public static eventHandler(validated: types.TypeCompiled) {
229
+ if (validated.isApi) return APIAlerts.event(validated)
230
+ if (validated.isCap) return CAPAlerts.event(validated)
231
+ if (!validated.isCap && validated.isVtec && validated.isUGC) return VTECAlerts.event(validated);
232
+ if (!validated.isCap && !validated.isVtec && validated.isUGC) return UGCAlerts.event(validated);
233
+ if (!validated.isCap && !validated.isVtec && !validated.isUGC) return TextAlerts.event(validated);
234
+ }
235
+
236
+ /**
237
+ * getICAO retrieves the ICAO code and corresponding office name based on VTEC tracking information, message attributes, or WMO code.
238
+ *
239
+ * @private
240
+ * @static
241
+ * @param {types.VTECParsed} vtec
242
+ * @param {Record<string, string>} attributes
243
+ * @param {(RegExpMatchArray | string | null)} WMO
244
+ * @returns {{ icao: any; name: any; }}
245
+ */
246
+ private static getICAO(vtec: types.VTECParsed, attributes: Record<string, string>, WMO: RegExpMatchArray | string | null) {
247
+ const icao = vtec != null ? vtec?.tracking.split(`-`)[0] : (attributes?.cccc ?? (WMO != null ? (Array.isArray(WMO) ? WMO[0] : WMO) : `N/A`));
248
+ const name = loader.definitions.ICAO?.[icao] ?? `N/A`;
249
+ return { icao, name };
250
+ }
251
+
252
+ /**
253
+ * getCorrectIssuedDate ensures the issued date is valid and falls back to the current date if not.
254
+ *
255
+ * @private
256
+ * @static
257
+ * @param {Record<string, string>} attributes
258
+ * @returns {*}
259
+ */
260
+ private static getCorrectIssuedDate(attributes: Record<string, string>) {
261
+ const time = attributes.issue != null ?
262
+ new Date(attributes.issue).toLocaleString() : (attributes?.issue != null ?
263
+ new Date(attributes.issue).toLocaleString() :
264
+ new Date().toLocaleString());
265
+ if (time == `Invalid Date`) return new Date().toLocaleString();
266
+ return time;
267
+ }
268
+
269
+ /**
270
+ * getCorrectExpiryDate determines the correct expiration date for an alert based on VTEC information or UGC zones.
271
+ *
272
+ * @private
273
+ * @static
274
+ * @param {types.VTECParsed} vtec
275
+ * @param {types.UGCParsed} ugc
276
+ * @returns {*}
277
+ */
278
+ private static getCorrectExpiryDate(vtec: types.VTECParsed, ugc: types.UGCParsed) {
279
+ const time = vtec?.expires && !isNaN(new Date(vtec.expires).getTime()) ?
280
+ new Date(vtec.expires).toLocaleString() :
281
+ (ugc?.expiry != null ? new Date(ugc.expiry).toLocaleString() :
282
+ new Date(new Date().getTime() + 1 * 60 * 60 * 1000).toLocaleString())
283
+ if (time == `Invalid Date`) return new Date(new Date().getTime() + 1 * 60 * 60 * 1000).toLocaleString();
284
+ return time;
285
+ }
286
+
287
+ }
288
+
289
+ export default EventParser
@@ -0,0 +1,103 @@
1
+ /*
2
+ _ _ __ __
3
+ /\ | | | | (_) \ \ / /
4
+ / \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
5
+ / /\ \| __| "_ ` _ \ / _ \/ __| "_ \| "_ \ / _ \ "__| |/ __| > <
6
+ / ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
7
+ /_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
8
+ | |
9
+ |_|
10
+
11
+ Written by: KiyoWx (k3yomi)
12
+ */
13
+
14
+
15
+ import * as loader from '../bootstrap';
16
+ import * as types from '../types';
17
+
18
+ export class StanzaParser {
19
+
20
+ /**
21
+ * validate handles the validation of incoming XMPP stanzas to ensure they contain valid alert data.
22
+ * You can also feed debug / cache data directly into this function by specifying the second parameter
23
+ * which is the attributes object that would normally be parsed from the XMPP stanza.
24
+ *
25
+ * @public
26
+ * @static
27
+ * @param {*} stanza
28
+ * @param {(boolean | types.TypeAttributes)} [isDebug=false]
29
+ * @returns {{ message: any; attributes: types.TypeAttributes; isCap: any; isVtec: boolean; isCapDescription: any; awipsType: any; isApi: boolean; ignore: boolean; }}
30
+ */
31
+ public static validate(stanza: any, isDebug: boolean | types.TypeAttributes = false) {
32
+ if (isDebug !== false) {
33
+ const vTypes = isDebug as types.TypeAttributes;
34
+ const message = stanza;
35
+ const attributes = vTypes;
36
+ const isCap = vTypes.isCap ?? message.includes(`<?xml`);
37
+ const isCapDescription = message.includes(`<areaDesc>`);
38
+ const isVtec = message.match(loader.definitions.expressions.vtec) != null;
39
+ const isUGC = message.match(loader.definitions.expressions.ugc1) != null;
40
+ const awipsType = this.getType(attributes);
41
+ return { message, attributes, isCap, isVtec, isUGC, isCapDescription, awipsType: awipsType, isApi: false, ignore: false };
42
+ }
43
+ if (stanza.is(`message`)) {
44
+ let cb = stanza.getChild(`x`)
45
+ if (cb && cb.children) {
46
+ let message = unescape(cb.children[0])
47
+ let attributes = cb.attrs
48
+ if (attributes.awipsid && attributes.awipsid.length > 1) {
49
+ const isCap = message.includes(`<?xml`);
50
+ const isCapDescription = message.includes(`<areaDesc>`);
51
+ const isVtec = message.match(loader.definitions.expressions.vtec) != null;
52
+ const isUGC = message.match(loader.definitions.expressions.ugc1) != null;
53
+ const awipsType = this.getType(attributes);
54
+ const isApi = false;
55
+ this.cache({ message, attributes, isCap, isVtec, awipsType: awipsType.type, awipsPrefix: awipsType.prefix });
56
+ return { message, attributes, isCap, isApi, isVtec, isUGC, isCapDescription, awipsType: awipsType, ignore: false };
57
+ }
58
+ }
59
+ }
60
+ return { message: null, attributes: null, isApi: null, isCap: null, isVtec: null, isUGC: null, isCapDescription: null, awipsType: null, ignore: true };
61
+ }
62
+
63
+ /**
64
+ * getType determines the AWIPS type of the alert based on its attributes, specifically the awipsid.
65
+ * If no matching type is found, it defaults to 'default'.
66
+ *
67
+ * @private
68
+ * @static
69
+ * @param {unknown} attributes
70
+ * @returns {*}
71
+ */
72
+ private static getType(attributes: unknown) {
73
+ const attrs = attributes as types.TypeAttributes;
74
+ if (!attrs || !attrs.awipsid) return {type: `XX`, prefix: `XX`};
75
+ for (const [prefix, type] of Object.entries(loader.definitions.awips)) {
76
+ if (attrs.awipsid.startsWith(prefix)) { return {type: type, prefix: prefix}; }
77
+ }
78
+ return {type: `XX`, prefix: `XX`};
79
+ }
80
+
81
+ /**
82
+ * cache stores the compiled alert data into a cache file if caching is enabled in the settings.
83
+ *
84
+ * @private
85
+ * @static
86
+ * @param {unknown} compiled
87
+ */
88
+ private static cache(compiled: unknown) {
89
+ const data = compiled as types.TypeCompiled;
90
+ const settings = loader.settings as types.ClientSettings;
91
+ if (!settings.NoaaWeatherWireService.cache.directory) return;
92
+ if (!loader.packages.fs.existsSync(settings.NoaaWeatherWireService.cache.directory)) { loader.packages.fs.mkdirSync(settings.NoaaWeatherWireService.cache.directory, { recursive: true }); }
93
+ data.message = data.message.replace(/\$\$/g, `\nSTANZA ATTRIBUTES...${JSON.stringify(data.attributes)}\nISSUED TIME...${new Date().toISOString()}\n$$$\n`);
94
+ if (!data.message.includes(`STANZA ATTRIBUTES...`)) {
95
+ data.message += `\nSTANZA ATTRIBUTES...${JSON.stringify(data.attributes)}\nISSUED TIME...${new Date().toISOString()}\n$$\n`;
96
+ }
97
+ loader.packages.fs.appendFileSync(`${settings.NoaaWeatherWireService.cache.directory}/category-${data.awipsPrefix}-${data.awipsType}s-${data.isCap ? `cap` : `raw`}${data.isVtec ? `-vtec` : ``}.bin`,`=================================================\n${new Date().toISOString().replace(/[:.]/g, '-')}\n=================================================\n${data.message}`, 'utf8');
98
+ loader.packages.fs.appendFileSync(`${settings.NoaaWeatherWireService.cache.directory}/cache-${data.isCap ? `cap` : `raw`}${data.isVtec ? `-vtec` : ``}.bin`,`=================================================\n${new Date().toISOString().replace(/[:.]/g, '-')}\n=================================================\n${data.message}`, 'utf8');
99
+ }
100
+
101
+ }
102
+
103
+ export default StanzaParser;
@@ -0,0 +1,167 @@
1
+ /*
2
+ _ _ __ __
3
+ /\ | | | | (_) \ \ / /
4
+ / \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
5
+ / /\ \| __| "_ ` _ \ / _ \/ __| "_ \| "_ \ / _ \ "__| |/ __| > <
6
+ / ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
7
+ /_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
8
+ | |
9
+ |_|
10
+
11
+ Written by: KiyoWx (k3yomi)
12
+ */
13
+
14
+ import * as loader from '../bootstrap';
15
+
16
+ export class TextParser {
17
+
18
+ /**
19
+ * textProductToString extracts a specific value from a text-based weather product message based on a given key and optional removal strings.
20
+ *
21
+ * @public
22
+ * @static
23
+ * @param {string} message
24
+ * @param {string} value
25
+ * @param {string[]} [removal=[]]
26
+ * @returns {(string | null)}
27
+ */
28
+ public static textProductToString(message: string,value: string,removal: string[] = []): string | null {
29
+ const lines = message.split('\n');
30
+ for (const line of lines) {
31
+ if (line.includes(value)) {
32
+ let result = line.slice(line.indexOf(value) + value.length).trim();
33
+ for (const str of removal) { result = result.split(str).join(''); }
34
+ result = result.replace(value, '').replace('<', '').trim();
35
+ return result || null;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * textProductToPolygon extracts geographical coordinates from a text-based weather product message and returns them as an array of [longitude, latitude] pairs.
43
+ *
44
+ * @public
45
+ * @static
46
+ * @param {string} message
47
+ * @returns {[number, number][]}
48
+ */
49
+ public static textProductToPolygon(message: string): [number, number][] {
50
+ const coordinates: [number, number][] = [];
51
+ const latLonMatch = message.match(/LAT\.{3}LON\s+([\d\s]+)/i);
52
+ if (!latLonMatch || !latLonMatch[1]) return coordinates;
53
+ const coordStrings = latLonMatch[1].replace(/\n/g, ' ').trim().split(/\s+/);
54
+ for (let i = 0; i < coordStrings.length - 1; i += 2) {
55
+ const lat = parseFloat(coordStrings[i]) / 100;
56
+ const lon = -parseFloat(coordStrings[i + 1]) / 100;
57
+ if (!isNaN(lat) && !isNaN(lon)) { coordinates.push([lon, lat]); }
58
+ }
59
+ if (coordinates.length > 2) { coordinates.push(coordinates[0]); }
60
+ return coordinates;
61
+ }
62
+
63
+ /**
64
+ * textProductToDescription extracts the main description from a text-based weather product message.
65
+ *
66
+ * @public
67
+ * @static
68
+ * @param {string} message
69
+ * @param {string} [handle=null]
70
+ * @returns {string}
71
+ */
72
+ public static textProductToDescription(message: string, handle: string = null): string {
73
+ const original = message;
74
+ const dateRegex = /\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;
75
+ const discoveredDates = Array.from(message.matchAll(dateRegex));
76
+ if (discoveredDates.length) {
77
+ const lastMatch = discoveredDates[discoveredDates.length - 1][0];
78
+ const startIdx = message.lastIndexOf(lastMatch);
79
+ if (startIdx !== -1) {
80
+ const endIdx = message.indexOf('&&', startIdx);
81
+ message = message.substring(startIdx + lastMatch.length, endIdx !== -1 ? endIdx : undefined).trimStart();
82
+ if (message.startsWith('/')) message = message.slice(1).trimStart();
83
+ if (handle && message.includes(handle)) {
84
+ const handleIdx = message.indexOf(handle);
85
+ message = message.substring(handleIdx + handle.length).trimStart();
86
+ if (message.startsWith('/')) message = message.slice(1).trimStart();
87
+ }
88
+ }
89
+ } else if (handle) {
90
+ const handleIdx = message.indexOf(handle);
91
+ if (handleIdx !== -1) {
92
+ let afterHandle = message.substring(handleIdx + handle.length).trimStart();
93
+ if (afterHandle.startsWith('/')) afterHandle = afterHandle.slice(1).trimStart();
94
+ const latEnd = afterHandle.indexOf('&&');
95
+ message = latEnd !== -1 ? afterHandle.substring(0, latEnd).trim() : afterHandle.trim();
96
+ }
97
+ }
98
+ return message.replace(/\s+/g, ' ').trim().startsWith('ISSUED TIME...') ? original : message.trim();
99
+ }
100
+
101
+ /**
102
+ * awipTextToEvent converts an AWIPS ID prefix from a text-based weather product message to its corresponding event type and prefix.
103
+ *
104
+ * @public
105
+ * @static
106
+ * @param {string} message
107
+ * @returns {{ type: any; prefix: any; }}
108
+ */
109
+ public static awipTextToEvent(message: string) {
110
+ for (const [prefix, type] of Object.entries(loader.definitions.awips)) {
111
+ if (message.startsWith(prefix)) {
112
+ return {type: type, prefix: prefix};
113
+ }
114
+ }
115
+ return {type: `XX`, prefix: `XX`};
116
+ }
117
+
118
+ /**
119
+ * getXmlValues recursively searches a parsed XML object for specified keys and extracts their values.
120
+ *
121
+ * @public
122
+ * @static
123
+ * @param {*} parsed
124
+ * @param {string[]} valuesToExtract
125
+ * @returns {Record<string, any>}
126
+ */
127
+ public static getXmlValues(parsed: any, valuesToExtract: string[]) {
128
+ const extracted: Record<string, any> = {};
129
+ const findValueByKey = (obj: any, searchKey: string): any[] => {
130
+ const results: any[] = [];
131
+ if (obj === null || typeof obj !== 'object') {
132
+ return results;
133
+ }
134
+ const searchKeyLower = searchKey.toLowerCase();
135
+ for (const key in obj) {
136
+ if (obj.hasOwnProperty(key) && key.toLowerCase() === searchKeyLower) {
137
+ results.push(obj[key]);
138
+ }
139
+ }
140
+ if (Array.isArray(obj)) {
141
+ for (const item of obj) {
142
+ if (item.valueName && item.valueName.toLowerCase() === searchKeyLower && item.value !== undefined) {
143
+ results.push(item.value);
144
+ }
145
+ const nestedResults = findValueByKey(item, searchKey);
146
+ results.push(...nestedResults);
147
+ }
148
+ }
149
+ for (const key in obj) {
150
+ if (obj.hasOwnProperty(key)) {
151
+ const nestedResults = findValueByKey(obj[key], searchKey);
152
+ results.push(...nestedResults);
153
+ }
154
+ }
155
+ return results;
156
+ };
157
+ for (const key of valuesToExtract) {
158
+ const values = findValueByKey(parsed.alert, key);
159
+ const uniqueValues = [...new Set(values)];
160
+ extracted[key] = uniqueValues.length === 0 ? null : (uniqueValues.length === 1 ? uniqueValues[0] : uniqueValues);
161
+ }
162
+ return extracted;
163
+ }
164
+
165
+ }
166
+
167
+ export default TextParser;
@@ -0,0 +1,94 @@
1
+ /*
2
+ _ _ __ __
3
+ /\ | | | | (_) \ \ / /
4
+ / \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
5
+ / /\ \| __| "_ ` _ \ / _ \/ __| "_ \| "_ \ / _ \ "__| |/ __| > <
6
+ / ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
7
+ /_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
8
+ | |
9
+ |_|
10
+
11
+ Written by: KiyoWx (k3yomi)
12
+ */
13
+
14
+ import * as types from '../../types';
15
+ import * as loader from '../../bootstrap';
16
+ import EventParser from '../events';
17
+ import TextParser from '../text';
18
+
19
+
20
+ export class APIAlerts {
21
+
22
+ private static getTracking(extracted: Record<string, string>) {
23
+ return extracted.vtec ? (() => {
24
+ const vtecValue = Array.isArray(extracted.vtec) ? extracted.vtec[0] : extracted.vtec;
25
+ const splitVTEC = vtecValue.split('.');
26
+ return `${splitVTEC[2]}-${splitVTEC[3]}-${splitVTEC[4]}-${splitVTEC[5]}`;
27
+ })() : `${extracted.wmoidentifier} (${extracted.ugc})`;
28
+ }
29
+
30
+ private static getICAO(vtec: string) {
31
+ const icao = vtec ? vtec.split(`.`)[2] : `N/A`;
32
+ const name = loader.definitions.ICAO?.[icao] ?? `N/A`;
33
+ return { icao, name };
34
+ }
35
+
36
+ public static async event(validated: types.TypeCompiled) {
37
+ let processed = [] as unknown[];
38
+ const messages = Object.values(JSON.parse(validated.message).features) as types.TypeAlert[];
39
+ for (let feature of messages) {
40
+ const tick = performance.now();
41
+ const getVTEC = feature?.properties?.parameters?.VTEC?.[0] ?? null;
42
+ const getWmo = feature?.properties?.parameters?.WMOidentifier[0] ?? null;
43
+ const getUgc = feature?.properties?.geocode?.UGC ?? null;
44
+ const getHeadline = feature?.properties?.parameters?.NWSheadline?.[0] ?? "";
45
+ const getDescription = `${getHeadline} ${feature?.properties?.description ?? ``}`
46
+ const getAWIP = feature?.properties?.parameters?.AWIPSidentifier?.[0] ?? null;
47
+ const getHeader = EventParser.getHeader({ ...{ getAwip: {prefix: getAWIP?.slice(0, -3) }},} as types.TypeAttributes);
48
+ const getSource = TextParser.textProductToString(getDescription, `SOURCE...`, [`.`]) || `N/A`;
49
+ const getOffice = this.getICAO(getVTEC || ``);
50
+ processed.push({
51
+ preformance: performance.now() - tick,
52
+ tracking: this.getTracking({ vtec: getVTEC, wmoidentifier: getWmo, ugc: getUgc ? getUgc.join(`,`) : null }),
53
+ header: getHeader,
54
+ vtec: getVTEC || `N/A`,
55
+ history: [{
56
+ description: feature?.properties?.description ?? `N/A`,
57
+ action: feature?.properties?.messageType ?? `N/A`,
58
+ time: feature?.properties?.sent ? new Date(feature?.properties?.sent).toLocaleString() : `N/A`
59
+ }],
60
+ properties: {
61
+ locations: feature?.properties?.areaDesc ?? `N/A`,
62
+ event: feature?.properties?.event ?? `N/A`,
63
+ issued: feature?.properties?.sent ? new Date(feature?.properties?.sent).toLocaleString() : `N/A`,
64
+ expires: feature?.properties?.expires ? new Date(feature?.properties?.expires).toLocaleString() : `N/A`,
65
+ parent: feature?.properties?.event ?? `N/A`,
66
+ action_type: feature?.properties?.messageType ?? `N/A`,
67
+ description: feature?.properties?.description ?? `N/A`,
68
+ sender_name: getOffice.name || `N/A`,
69
+ sender_icao: getOffice.icao || `N/A`,
70
+ attributes: validated.attributes,
71
+ geocode: {
72
+ UGC: feature?.properties?.geocode?.UGC ?? [`XX000`]
73
+ },
74
+ parameters: {
75
+ wmo: feature?.properties?.parameters?.WMOidentifier?.[0] || getWmo || `N/A`,
76
+ source: getSource,
77
+ max_hail_size: feature?.properties?.parameters?.maxHailSize || `N/A`,
78
+ max_wind_gust: feature?.properties?.parameters?.maxWindGust || `N/A`,
79
+ damage_threat: feature?.properties?.parameters?.thunderstormDamageThreat || [`N/A`],
80
+ tornado_detection: feature?.properties?.parameters?.tornadoDetection || [`N/A`],
81
+ flood_detection: feature?.properties?.parameters?.floodDetection || [`N/A`],
82
+ discussion_tornado_intensity: "N/A",
83
+ peakWindGust: `N/A`,
84
+ peakHailSize: `N/A`,
85
+ },
86
+ geometry: feature?.geometry ?? null,
87
+ }
88
+ })
89
+ }
90
+ EventParser.validateEvents(processed);
91
+ }
92
+ }
93
+
94
+ export default APIAlerts;