atmosx-nwws-parser 1.0.19 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +182 -64
  2. package/dist/cjs/index.cjs +3799 -0
  3. package/dist/esm/index.mjs +3757 -0
  4. package/package.json +49 -37
  5. package/src/bootstrap.ts +196 -0
  6. package/src/database.ts +148 -0
  7. package/src/dictionaries/awips.ts +342 -0
  8. package/src/dictionaries/events.ts +142 -0
  9. package/src/dictionaries/icao.ts +237 -0
  10. package/src/dictionaries/offshore.ts +12 -0
  11. package/src/dictionaries/signatures.ts +107 -0
  12. package/src/eas.ts +493 -0
  13. package/src/index.ts +229 -0
  14. package/src/parsers/events/api.ts +151 -0
  15. package/src/parsers/events/cap.ts +138 -0
  16. package/src/parsers/events/text.ts +106 -0
  17. package/src/parsers/events/ugc.ts +109 -0
  18. package/src/parsers/events/vtec.ts +78 -0
  19. package/src/parsers/events.ts +367 -0
  20. package/src/parsers/hvtec.ts +46 -0
  21. package/src/parsers/pvtec.ts +71 -0
  22. package/src/parsers/stanza.ts +132 -0
  23. package/src/parsers/text.ts +166 -0
  24. package/src/parsers/ugc.ts +197 -0
  25. package/src/types.ts +251 -0
  26. package/src/utils.ts +314 -0
  27. package/src/xmpp.ts +144 -0
  28. package/test.js +58 -34
  29. package/tsconfig.json +12 -5
  30. package/tsup.config.ts +14 -0
  31. package/bootstrap.ts +0 -122
  32. package/dist/bootstrap.js +0 -153
  33. package/dist/src/events.js +0 -585
  34. package/dist/src/helper.js +0 -463
  35. package/dist/src/stanza.js +0 -147
  36. package/dist/src/text-parser.js +0 -119
  37. package/dist/src/ugc.js +0 -214
  38. package/dist/src/vtec.js +0 -125
  39. package/src/events.ts +0 -394
  40. package/src/helper.ts +0 -298
  41. package/src/stanza.ts +0 -102
  42. package/src/text-parser.ts +0 -120
  43. package/src/ugc.ts +0 -122
  44. package/src/vtec.ts +0 -99
package/src/index.ts ADDED
@@ -0,0 +1,229 @@
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
+ import Utils from './utils';
18
+ import Xmpp from './xmpp';
19
+ import StanzaParser from './parsers/stanza';
20
+ import Database from './database';
21
+ import EAS from './eas';
22
+ import EventParser from './parsers/events';
23
+ import TextParser from './parsers/text';
24
+ import PVtecParser from './parsers/pvtec';
25
+ import HVtecParser from './parsers/hvtec';
26
+ import UGCParser from './parsers/ugc';
27
+
28
+ export class AlertManager {
29
+ isNoaaWeatherWireService: boolean
30
+ job: any
31
+ constructor(metadata: types.ClientSettingsTypes) { this.start(metadata) }
32
+
33
+ /**
34
+ * @function setDisplayName
35
+ * @description
36
+ * Sets the display nickname for the NWWS XMPP session. Trims the provided
37
+ * name and validates it, emitting a warning if the name is empty or invalid.
38
+ *
39
+ * @param {string} [name]
40
+ */
41
+ public setDisplayName(name?: string) {
42
+ const settings = loader.settings as types.ClientSettingsTypes;
43
+ const trimmed = name?.trim();
44
+ if (!trimmed) {
45
+ Utils.warn(loader.definitions.messages.invalid_nickname);
46
+ return;
47
+ }
48
+ settings.noaa_weather_wire_service_settings.credentials.nickname = trimmed;
49
+ }
50
+
51
+ /**
52
+ * @function setCurrentLocation
53
+ * @description
54
+ * Sets the current location with a name and geographic coordinates.
55
+ * Validates the coordinates before updating the cache, emitting warnings
56
+ * if values are missing or invalid.
57
+ *
58
+ * @param {string} locationName
59
+ * @param {types.Coordinates} [coordinates]
60
+ */
61
+ public setCurrentLocation(locationName: string, coordinates?: types.Coordinates): void {
62
+ if (!coordinates) {
63
+ Utils.warn(`Coordinates not provided for location: ${locationName}`);
64
+ return;
65
+ }
66
+ const { lat, lon } = coordinates;
67
+ if (typeof lat !== 'number' || typeof lon !== 'number' || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
68
+ Utils.warn(loader.definitions.messages.invalid_coordinates.replace('{lat}', String(lat)).replace('{lon}', String(lon)));
69
+ return;
70
+ }
71
+ loader.cache.currentLocations[locationName] = coordinates;
72
+ }
73
+
74
+ /**
75
+ * @function createEasAudio
76
+ * @description
77
+ * Generates an EAS (Emergency Alert System) audio file using the provided
78
+ * description and header.
79
+ *
80
+ * @async
81
+ * @param {string} description
82
+ * @param {string} header
83
+ * @returns {Promise<Buffer>}
84
+ */
85
+ public async createEasAudio(description: string, header: string) {
86
+ return await EAS.generateEASAudio(description, header);
87
+ }
88
+
89
+ /**
90
+ * @function getAllAlertTypes
91
+ * @description
92
+ * Generates a list of all possible alert types by combining defined
93
+ * event names with action names.
94
+ *
95
+ * @returns {string[]}
96
+ */
97
+ public getAllAlertTypes(): string[] {
98
+ const events = new Set(Object.values(loader.definitions.events));
99
+ const actions = new Set(Object.values(loader.definitions.actions));
100
+ return Array.from(events).flatMap(event =>
101
+ Array.from(actions).map(action => `${event} ${action}`)
102
+ );
103
+ }
104
+
105
+ /**
106
+ * @function searchStanzaDatabase
107
+ * @description
108
+ * Searches the stanza database for entries containing the specified query.
109
+ * Escapes SQL wildcard characters and returns results in descending order
110
+ * by ID, up to the specified limit.
111
+ *
112
+ * @async
113
+ * @param {string} query
114
+ * @param {number} [limit=250]
115
+ * @returns {Promise<any[]>}
116
+ */
117
+ public async searchStanzaDatabase(query: string, limit: number = 250) {
118
+ const escapeLike = (s: string) => s.replace(/[%_]/g, '\\$&');
119
+ const rows = await loader.cache.db
120
+ .prepare(`SELECT * FROM stanzas WHERE stanza LIKE ? ESCAPE '\\' ORDER BY id DESC LIMIT ${limit}`)
121
+ .all(`%${escapeLike(query)}%`);
122
+ return rows;
123
+ }
124
+
125
+ /**
126
+ * @function setSettings
127
+ * @description
128
+ * Merges the provided client settings into the current configuration,
129
+ * preserving nested structures.
130
+ *
131
+ * @async
132
+ * @param {types.ClientSettingsTypes} settings
133
+ * @returns {Promise<void>}
134
+ */
135
+ public async setSettings(settings: types.ClientSettingsTypes) {
136
+ Utils.mergeClientSettings(loader.settings, settings);
137
+ }
138
+
139
+ /**
140
+ * @function on
141
+ * @description
142
+ * Registers a callback for a specific event and returns a function
143
+ * to unregister the listener.
144
+ *
145
+ * @param {string} event
146
+ * @param {(...args: any[]) => void} callback
147
+ * @returns {() => void}
148
+ */
149
+ public on(event: string, callback: (...args: any[]) => void) {
150
+ loader.cache.events.on(event, callback);
151
+ return () => loader.cache.events.off(event, callback);
152
+ }
153
+
154
+ /**
155
+ * @function start
156
+ * @description
157
+ * Initializes the client with the provided settings, starts the NWWS XMPP
158
+ * session if applicable, loads cached messages, and sets up scheduled
159
+ * tasks (cron jobs) for ongoing processing.
160
+ *
161
+ * @async
162
+ * @param {types.ClientSettingsTypes} metadata
163
+ * @returns {Promise<void>}
164
+ */
165
+ public async start(metadata: types.ClientSettingsTypes): Promise<void> {
166
+ if (!loader.cache.isReady) {
167
+ Utils.warn(loader.definitions.messages.not_ready);
168
+ return;
169
+ }
170
+ this.setSettings(metadata);
171
+ const settings = loader.settings as types.ClientSettingsTypes;
172
+ this.isNoaaWeatherWireService = settings.is_wire;
173
+ loader.cache.isReady = false;
174
+ while (!Utils.isReadyToProcess()) {
175
+ await Utils.sleep(2000);
176
+ }
177
+ await Database.loadDatabase();
178
+ if (this.isNoaaWeatherWireService) {
179
+ (async () => {
180
+ try {
181
+ await Xmpp.deploySession();
182
+ await Utils.loadCollectionCache();
183
+ } catch (err: unknown) {
184
+ const msg = err instanceof Error ? err.message : String(err);
185
+ Utils.warn(`Failed to initialize NWWS services: ${msg}`);
186
+ }
187
+ })();
188
+ }
189
+ Utils.handleCronJob(this.isNoaaWeatherWireService);
190
+ if (this.job) {
191
+ try { this.job.stop(); } catch { Utils.warn(`Failed to stop existing cron job.`); }
192
+ this.job = null;
193
+ }
194
+ const interval = !this.isNoaaWeatherWireService ? settings.national_weather_service_settings.interval : 5;
195
+ this.job = new loader.packages.jobs.Cron(`*/${interval} * * * * *`, () => {
196
+ Utils.handleCronJob(this.isNoaaWeatherWireService);
197
+ });
198
+ }
199
+
200
+ /**
201
+ * @function stop
202
+ * @description
203
+ * Stops active scheduled tasks (cron job) and, if connected, the NWWS
204
+ * XMPP session. Updates relevant cache flags to indicate the session
205
+ * is no longer active.
206
+ *
207
+ * @async
208
+ * @returns {Promise<void>}
209
+ */
210
+ public async stop(): Promise<void> {
211
+ loader.cache.isReady = true;
212
+ if (this.job) {
213
+ try { this.job.stop(); } catch { Utils.warn(`Failed to stop cron job.`); }
214
+ this.job = null;
215
+ }
216
+ const session = loader.cache.session;
217
+ if (session && this.isNoaaWeatherWireService) {
218
+ try { await session.stop(); } catch { Utils.warn(`Failed to stop XMPP session.`); }
219
+ loader.cache.sigHalt = true;
220
+ loader.cache.isConnected = false;
221
+ loader.cache.session = null;
222
+ this.isNoaaWeatherWireService = false;
223
+ }
224
+ }
225
+
226
+ }
227
+
228
+ export default AlertManager;
229
+ export { StanzaParser, EventParser, TextParser, PVtecParser, HVtecParser, UGCParser, EAS, Database, Utils };
@@ -0,0 +1,151 @@
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
+ /**
23
+ * @function getTracking
24
+ * @description
25
+ * Generates a unique tracking identifier for a CAP alert based on extracted XML values.
26
+ * If VTEC information is available, it constructs the tracking ID from the VTEC components.
27
+ * Otherwise, it uses the WMO identifier along with TTAI and CCCC attributes.
28
+ *
29
+ * @private
30
+ * @static
31
+ * @param {Record<string, string>} extracted
32
+ * @returns {string}
33
+ */
34
+ private static getTracking(extracted: Record<string, string>) {
35
+ return extracted.pVtec ? (() => {
36
+ const vtecValue = Array.isArray(extracted.pVtec) ? extracted.pVtec[0] : extracted.pVtec;
37
+ const splitPVTEC = vtecValue.split('.');
38
+ return `${splitPVTEC[2]}-${splitPVTEC[3]}-${splitPVTEC[4]}-${splitPVTEC[5]}`;
39
+ })() : (() => {
40
+ const wmoMatch = extracted.wmoidentifier?.match(/([A-Z]{4}\d{2})\s+([A-Z]{4})/);
41
+ const id = wmoMatch?.[1] || 'N/A';
42
+ const station = wmoMatch?.[2] || 'N/A';
43
+ return `${station}-${id}`;
44
+ })();
45
+ }
46
+
47
+ /**
48
+ * @function getICAO
49
+ * @description
50
+ * Extracts the sender's ICAO code and corresponding name from a VTEC string.
51
+ *
52
+ * @private
53
+ * @static
54
+ * @param {string} pVtec
55
+ * @returns {{ icao: any; name: any; }}
56
+ */
57
+ private static getICAO(pVtec: string) {
58
+ const icao = pVtec ? pVtec.split(`.`)[2] : `N/A`;
59
+ const name = loader.definitions.ICAO?.[icao] ?? `N/A`;
60
+ return { icao, name };
61
+ }
62
+
63
+ /**
64
+ * @function event
65
+ * @description
66
+ * Processes validated API alert messages, extracting relevant information and compiling it into structured event objects.
67
+ *
68
+ * @public
69
+ * @static
70
+ * @async
71
+ * @param {types.StanzaCompiled} validated
72
+ * @returns {*}
73
+ */
74
+ public static async event(validated: types.StanzaCompiled) {
75
+ let processed = [] as unknown[];
76
+ const settings = loader.settings as types.ClientSettingsTypes;
77
+ const messages = Object.values(JSON.parse(validated.message).features) as types.EventCompiled[];
78
+ for (let feature of messages) {
79
+ const tick = performance.now();
80
+ const getPVTEC = feature?.properties?.parameters?.VTEC?.[0] ?? null;
81
+ const getWmo = feature?.properties?.parameters?.WMOidentifier?.[0] ?? null;
82
+ const getUgc = feature?.properties?.geocode?.UGC ?? null;
83
+ const getHeadline = feature?.properties?.parameters?.NWSheadline?.[0] ?? "";
84
+ const getDescription = `${getHeadline} ${feature?.properties?.description ?? ``}`
85
+ const getAWIP = feature?.properties?.parameters?.AWIPSidentifier?.[0] ?? null;
86
+ const getHeader = EventParser.getHeader({ ...{ getAwip: {prefix: getAWIP?.slice(0, -3) }},} as types.StanzaAttributes);
87
+ const getSource = TextParser.textProductToString(getDescription, `SOURCE...`, [`.`]) || `N/A`;
88
+ const getOffice = this.getICAO(getPVTEC || ``);
89
+ processed.push({
90
+ type: "Feature",
91
+ properties: {
92
+ locations: feature?.properties?.areaDesc ?? `N/A`,
93
+ event: feature?.properties?.event ?? `N/A`,
94
+ issued: feature?.properties?.sent ? new Date(feature?.properties?.sent).toLocaleString() : `N/A`,
95
+ expires: feature?.properties?.expires ? new Date(feature?.properties?.expires).toLocaleString() : `N/A`,
96
+ parent: feature?.properties?.event ?? `N/A`,
97
+ action_type: feature?.properties?.messageType ?? `N/A`,
98
+ description: feature?.properties?.description ?? `N/A`,
99
+ sender_name: getOffice.name || `N/A`,
100
+ sender_icao: getOffice.icao || `N/A`,
101
+ attributes: validated.attributes,
102
+ geocode: {
103
+ UGC: feature?.properties?.geocode?.UGC ?? [`XX000`]
104
+ },
105
+ metadata: {},
106
+ technical: {
107
+ vtec: getPVTEC || `N/A`,
108
+ ugc: getUgc ? getUgc.join(`,`) : `N/A`,
109
+ hvtec: `N/A`,
110
+ },
111
+ parameters: {
112
+ wmo: feature?.properties?.parameters?.WMOidentifier?.[0] || getWmo || `N/A`,
113
+ source: getSource,
114
+ max_hail_size: feature?.properties?.parameters?.maxHailSize || `N/A`,
115
+ max_wind_gust: feature?.properties?.parameters?.maxWindGust || `N/A`,
116
+ damage_threat: feature?.properties?.parameters?.thunderstormDamageThreat || [`N/A`],
117
+ tornado_detection: feature?.properties?.parameters?.tornadoDetection || [`N/A`],
118
+ flood_detection: feature?.properties?.parameters?.floodDetection || [`N/A`],
119
+ discussion_tornado_intensity: "N/A",
120
+ peakWindGust: `N/A`,
121
+ peakHailSize: `N/A`,
122
+ },
123
+ },
124
+ details: {
125
+ performance: performance.now() - tick,
126
+ source: `api-parser`,
127
+ tracking: this.getTracking({ pVtec: getPVTEC, wmoidentifier: getWmo, ugc: getUgc ? getUgc.join(`,`) : null }),
128
+ header: getHeader,
129
+ pvtec: getPVTEC || `N/A`,
130
+ history: [{
131
+ description: feature?.properties?.description ?? `N/A`,
132
+ action: feature?.properties?.messageType ?? `N/A`,
133
+ time: feature?.properties?.sent ? new Date(feature?.properties?.sent).toLocaleString() : `N/A`
134
+ }],
135
+ },
136
+ geometry: feature?.geometry?.coordinates?.[0] != null ? {
137
+ type: "Polygon",
138
+ coordinates: [
139
+ feature?.geometry?.coordinates?.[0]?.map((coord: number[] | any) => {
140
+ const [lat, lon] = Array.isArray(coord) ? coord : [0, 0];
141
+ return [lat, lon];
142
+ })
143
+ ]
144
+ } : await EventParser.getEventGeometry(``, {zones: getUgc})
145
+ })
146
+ }
147
+ EventParser.validateEvents(processed);
148
+ }
149
+ }
150
+
151
+ export default APIAlerts;
@@ -0,0 +1,138 @@
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 CapAlerts {
21
+
22
+ /**
23
+ * @function getTracking
24
+ * @description
25
+ * Generates a unique tracking identifier for a CAP alert based on extracted XML values.
26
+ * If VTEC information is available, it constructs the tracking ID from the VTEC components.
27
+ * Otherwise, it uses the WMO identifier along with TTAI and CCCC attributes.
28
+ *
29
+ * @private
30
+ * @static
31
+ * @param {Record<string, string>} extracted
32
+ * @returns {string}
33
+ */
34
+ private static getTracking(extracted: Record<string, string>, metadata: types.StanzaAttributes) {
35
+ return extracted.vtec ? (() => {
36
+ const vtecValue = Array.isArray(extracted.vtec) ? extracted.vtec[0] : extracted.vtec;
37
+ const splitPVTEC = vtecValue.split('.');
38
+ return `${splitPVTEC[2]}-${splitPVTEC[3]}-${splitPVTEC[4]}-${splitPVTEC[5]}`;
39
+ })() : `${extracted.wmoidentifier.substring(extracted.wmoidentifier.length - 4)}-${metadata.attributes.ttaaii}-${metadata.attributes.id.slice(-4)}`;
40
+ }
41
+
42
+ /**
43
+ * @function event
44
+ * @description
45
+ * Processes validated CAP alert messages, extracting relevant information and compiling it into structured event objects.
46
+ *
47
+ * @public
48
+ * @static
49
+ * @async
50
+ * @param {types.StanzaCompiled} validated
51
+ * @returns {*}
52
+ */
53
+ public static async event(validated: types.StanzaCompiled) {
54
+ let processed = [] as unknown[];
55
+ const tick = performance.now();
56
+ const settings = loader.settings as types.ClientSettingsTypes;
57
+ const blocks = validated.message.split(/\[SoF\]/gim)?.map(msg => msg.trim());
58
+ for (const block of blocks) {
59
+ const cachedAttribute = block.match(/STANZA ATTRIBUTES\.\.\.(\{.*\})/);
60
+ const messages = block.split(/(?=\$\$)/g)?.map(msg => msg.trim());
61
+ if (!messages || messages.length == 0) return;
62
+ for (let i = 0; i < messages.length; i++) {
63
+ let message = messages[i]
64
+ const attributes = cachedAttribute != null ? JSON.parse(cachedAttribute[1]) : validated;
65
+ message = message.substring(message.indexOf(`<?xml version="1.0"`), message.lastIndexOf(`>`) + 1);
66
+ const parser = new loader.packages.xml2js.Parser({ explicitArray: false, mergeAttrs: true, trim: true })
67
+ const parsed = await parser.parseStringPromise(message);
68
+ if (parsed == null || parsed.alert == null) continue;
69
+ const extracted = TextParser.getXmlValues(parsed, [
70
+ `vtec`, `wmoidentifier`, `ugc`, `areadesc`,
71
+ `expires`, `sent`, `msgtype`, `description`,
72
+ `event`, `sendername`, `tornadodetection`, `polygon`,
73
+ `maxHailSize`, `maxWindGust`, `thunderstormdamagethreat`,
74
+ `tornadodamagethreat`, `waterspoutdetection`, `flooddetection`,
75
+ ]);
76
+ const getHeader = EventParser.getHeader({ ...validated.attributes,} as types.StanzaAttributes);
77
+ const getSource = TextParser.textProductToString(extracted.description, `SOURCE...`, [`.`]) || `N/A`;
78
+ processed.push({
79
+ type: "Feature",
80
+ properties: {
81
+ locations: extracted.areadesc || `N/A`,
82
+ event: extracted.event || `N/A`,
83
+ issued: extracted.sent ? new Date(extracted.sent).toLocaleString() : `N/A`,
84
+ expires: extracted.expires ? new Date(extracted.expires).toLocaleString() : `N/A`,
85
+ parent: extracted.event || `N/A`,
86
+ action_type: extracted.msgtype || `N/A`,
87
+ description: extracted.description || `N/A`,
88
+ sender_name: extracted.sendername || `N/A`,
89
+ sender_icao: extracted.wmoidentifier ? extracted.wmoidentifier.substring(extracted.wmoidentifier.length - 4) : `N/A`,
90
+ attributes: attributes,
91
+ geocode: {
92
+ UGC: [extracted.ugc],
93
+ },
94
+ metadata: {attributes},
95
+ technical: {
96
+ vtec: extracted.vtec || `N/A`,
97
+ ugc: extracted.ugc || `N/A`,
98
+ hvtec: `N/A`,
99
+ },
100
+ parameters: {
101
+ wmo: extracted.wmoidentifier || `N/A`,
102
+ source: getSource,
103
+ max_hail_size: extracted.maxHailSize || `N/A`,
104
+ max_wind_gust: extracted.maxWindGust || `N/A`,
105
+ damage_threat: extracted.thunderstormdamagethreat || `N/A`,
106
+ tornado_detection: extracted.tornadodetection || extracted.waterspoutdetection || `N/A`,
107
+ flood_detection: extracted.flooddetection || `N/A`,
108
+ discussion_tornado_intensity: `N/A`,
109
+ discussion_wind_intensity: `N/A`,
110
+ discussion_hail_intensity: `N/A`,
111
+ },
112
+ },
113
+ details: {
114
+ performance: performance.now() - tick,
115
+ source: `cap-parser`,
116
+ tracking: this.getTracking(extracted, attributes),
117
+ header: getHeader,
118
+ pvtec: extracted.vtec || `N/A`,
119
+ hvtec: `N/A`,
120
+ history: [{ description: extracted.description || `N/A`, issued: extracted.sent ? new Date(extracted.sent).toLocaleString() : `N/A`, type: extracted.msgtype || `N/A` }],
121
+ },
122
+ geometry: extracted.polygon ? {
123
+ type: "Polygon",
124
+ coordinates: [
125
+ extracted.polygon.split(" ").map((coord: string) => {
126
+ const [lon, lat] = coord.split(",").map((num: string) => parseFloat(num));
127
+ return [lat, lon];
128
+ })
129
+ ]
130
+ } : await EventParser.getEventGeometry(``, {zones: [extracted.ugc]}),
131
+ })
132
+ }
133
+ }
134
+ EventParser.validateEvents(processed);
135
+ }
136
+ }
137
+
138
+ export default CapAlerts;
@@ -0,0 +1,106 @@
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
+
18
+
19
+ export class TextAlerts {
20
+
21
+ /**
22
+ * @function getTracking
23
+ * @description
24
+ * Generates a unique tracking identifier for an event using the sender's ICAO
25
+ * and some attributes.
26
+ *
27
+ * @private
28
+ * @static
29
+ * @param {types.EventProperties} baseProperties
30
+ * @returns {string}
31
+ */
32
+ private static getTracking(properties: types.EventProperties) {
33
+ return `${properties.sender_icao}-${properties.raw.attributes.ttaaii}-${properties.raw.attributes.id.slice(-4)}`
34
+ }
35
+
36
+ /**
37
+ * @function getEvent
38
+ * @description
39
+ * Determines the event name from a text message and its AWIPS attributes.
40
+ * If the message contains a known offshore event keyword, that offshore
41
+ * event is returned. Otherwise, the event type from the AWIPS attributes
42
+ * is formatted into a human-readable string with each word capitalized.
43
+ *
44
+ * @private
45
+ * @static
46
+ * @param {string} message
47
+ * @param {types.StanzaAttributes} metadata
48
+ * @returns {string}
49
+ */
50
+ private static getEvent(message: string, metadata: types.StanzaAttributes) {
51
+ const offshoreEvent = Object.keys(loader.definitions.offshore).find(event => message.toLowerCase().includes(event.toLowerCase()));
52
+ if (offshoreEvent != undefined ) return Object.keys(loader.definitions.offshore).find(event => message.toLowerCase().includes(event.toLowerCase()));
53
+ return metadata.awipsType.type.split(`-`).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(` `)
54
+ }
55
+
56
+ /**
57
+ * @function event
58
+ * @description
59
+ * Processes a compiled text-based NOAA Stanza message and extracts relevant
60
+ * event information. Splits the message into multiple segments based on
61
+ * markers such as "$$", "ISSUED TIME...", or separator lines, generates
62
+ * base properties, headers, event names, and tracking information for
63
+ * each segment, then validates and emits the processed events.
64
+ *
65
+ * @public
66
+ * @static
67
+ * @async
68
+ * @param {types.StanzaCompiled} validated
69
+ * @returns {Promise<void>}
70
+ */
71
+ public static async event(validated: types.StanzaCompiled) {
72
+ let processed = [] as unknown[];
73
+ const blocks = validated.message.split(/\[SoF\]/gim)?.map(msg => msg.trim());
74
+ for (const block of blocks) {
75
+ const cachedAttribute = block.match(/STANZA ATTRIBUTES\.\.\.(\{.*\})/);
76
+ const messages = block.split(/(?=\$\$)/g)?.map(msg => msg.trim());
77
+ if (!messages || messages.length == 0) return;
78
+ for (let i = 0; i < messages.length; i++) {
79
+ const tick = performance.now();
80
+ const message = messages[i]
81
+ const attributes = cachedAttribute != null ? JSON.parse(cachedAttribute[1]) : validated;
82
+ const baseProperties = await EventParser.getBaseProperties(message, attributes) as types.EventProperties;
83
+ const baseGeometry = await EventParser.getEventGeometry(message);
84
+ const getHeader = EventParser.getHeader({ ...validated.attributes, ...baseProperties.raw } as types.StanzaAttributes, baseProperties)
85
+ const getEvent = this.getEvent(message, attributes);
86
+ processed.push({
87
+ properties: { event: getEvent, parent: getEvent, action_type: `Issued`, ...baseProperties },
88
+ details: {
89
+ type: "Feature",
90
+ performance: performance.now() - tick,
91
+ source: `text-parser`,
92
+ tracking: this.getTracking(baseProperties),
93
+ header: getHeader,
94
+ pvtec: `N/A`,
95
+ hvtec: `N/A`,
96
+ history: [{ description: baseProperties.description, issued: baseProperties.issued, type: `Issued` }],
97
+ },
98
+ geometry: baseGeometry,
99
+ })
100
+ }
101
+ }
102
+ EventParser.validateEvents(processed);
103
+ }
104
+ }
105
+
106
+ export default TextAlerts;