@atmosx/event-product-parser 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
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
+ export class TextAlerts {
19
+
20
+ /**
21
+ * @function getTracking
22
+ * @description
23
+ * Generates a unique tracking identifier for an event using the sender's ICAO
24
+ * and some attributes.
25
+ *
26
+ * @private
27
+ * @static
28
+ * @param {types.EventProperties} properties
29
+ * @returns {string}
30
+ */
31
+ private static getTracking(properties: types.EventProperties): string {
32
+ return `${properties.sender_icao}-${properties.raw.attributes.ttaaii}-${properties?.raw?.attributes?.id.slice(-4) ?? 'N/A'}`;
33
+ }
34
+
35
+ /**
36
+ * @function getEvent
37
+ * @description
38
+ * Determines the event name from a text message and its AWIPS attributes.
39
+ * If the message contains a known offshore event keyword, that offshore
40
+ * event is returned. Otherwise, the event type from the AWIPS attributes
41
+ * is formatted into a human-readable string with each word capitalized.
42
+ *
43
+ * @private
44
+ * @static
45
+ * @param {string} message
46
+ * @param {types.StanzaAttributes} metadata
47
+ * @returns {string}
48
+ */
49
+ private static getEvent(message: string, metadata: types.StanzaAttributes): string {
50
+ const offshoreEvent = Object.keys(loader.definitions.offshore).find(event => message.toLowerCase().includes(event.toLowerCase()));
51
+ if (offshoreEvent != undefined ) return Object.keys(loader.definitions.offshore).find(event => message.toLowerCase().includes(event.toLowerCase()));
52
+ return metadata.awipsType.type.split(`-`).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(` `)
53
+ }
54
+
55
+ /**
56
+ * @function event
57
+ * @description
58
+ * Processes a compiled text-based NOAA Stanza message and extracts relevant
59
+ * event information. Splits the message into multiple segments based on
60
+ * markers such as "$$", "ISSUED TIME...", or separator lines, generates
61
+ * base properties, headers, event names, and tracking information for
62
+ * each segment, then validates and emits the processed events.
63
+ *
64
+ * @public
65
+ * @static
66
+ * @async
67
+ * @param {types.StanzaCompiled} validated
68
+ * @returns {Promise<void>}
69
+ */
70
+ public static async event(validated: types.StanzaCompiled): Promise<void> {
71
+ let processed = [] as unknown[];
72
+ const messages = validated?.message?.split(/(?=\$\$)/g)?.map(msg => msg.trim())?.filter(msg => msg && msg !== "$$");
73
+ if (!messages || messages.length == 0) { return }
74
+ for (let i = 0; i < messages.length; i++) {
75
+ const tick = performance.now();
76
+ const message = messages[i]
77
+ const attributes = validated as types.StanzaAttributes;
78
+ const baseProperties = await EventParser.getBaseProperties(message, attributes) as types.EventProperties;
79
+ const getHeader = EventParser.getHeader({ ...validated.attributes, ...baseProperties.raw } as types.StanzaAttributes, baseProperties)
80
+ const getEvent = this.getEvent(message, attributes);
81
+ processed.push({
82
+ properties: {
83
+ event: getEvent,
84
+ parent: getEvent,
85
+ action_type: `Issued`,
86
+ ...baseProperties,
87
+ details: {
88
+ type: "Feature",
89
+ performance: performance.now() - tick,
90
+ source: `text-parser`,
91
+ tracking: this.getTracking(baseProperties),
92
+ header: getHeader,
93
+ pvtec: null,
94
+ hvtec: null,
95
+ history: [{ description: baseProperties.description, issued: baseProperties.issued, type: `Issued` }],
96
+ },
97
+ },
98
+ })
99
+ }
100
+ EventParser.validateEvents(processed);
101
+ }
102
+ }
103
+
104
+ export default TextAlerts;
@@ -0,0 +1,107 @@
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 UgcParser from '../ugc';
17
+ import EventParser from '../events';
18
+
19
+ export class UGCAlerts {
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} properties
30
+ * @returns {string}
31
+ */
32
+ private static getTracking(properties: types.EventProperties): string {
33
+ return `${properties.sender_icao}-${properties.raw.attributes.ttaaii}-${properties?.raw?.attributes?.id.slice(-4) ?? 'N/A'}`;
34
+ }
35
+
36
+ /**
37
+ * @function getEvent
38
+ * @description
39
+ * Determines the human-readable event name from a message and AWIPS attributes.
40
+ * - Checks if the message contains any predefined offshore event keywords
41
+ * and returns the matching offshore event if found.
42
+ * - Otherwise, returns a formatted event type string from the provided attributes,
43
+ * capitalizing the first letter of each word.
44
+ *
45
+ * @private
46
+ * @static
47
+ * @param {string} message
48
+ * @param {Record<string, any>} metadata
49
+ * @returns {string}
50
+ */
51
+ private static getEvent(message: string, metadata: types.StanzaAttributes): string {
52
+ const offshoreEvent = Object.keys(loader.definitions.offshore).find(event => message.toLowerCase().includes(event.toLowerCase()));
53
+ if (offshoreEvent != undefined ) return Object.keys(loader.definitions.offshore).find(event => message.toLowerCase().includes(event.toLowerCase()));
54
+ return metadata.awipsType.type.split(`-`).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(` `)
55
+ }
56
+
57
+ /**
58
+ * @function event
59
+ * @description
60
+ * Processes a validated stanza message, extracting UGC entries and
61
+ * computing base properties for non-VTEC events. Each extracted event
62
+ * is enriched with metadata, performance timing, and history information,
63
+ * then filtered and emitted via `EventParser.validateEvents`.
64
+ *
65
+ * @static
66
+ * @async
67
+ * @param {types.StanzaCompiled} validated
68
+ * @returns {Promise<void>}
69
+ */
70
+ public static async event(validated: types.StanzaCompiled): Promise<void> {
71
+ let processed = [] as unknown[];
72
+ const messages = validated?.message?.split(/(?=\$\$)/g)?.map(msg => msg.trim())?.filter(msg => msg && msg !== "$$");
73
+ if (!messages || messages.length == 0) { return }
74
+ for (let i = 0; i < messages.length; i++) {
75
+ const tick = performance.now();
76
+ const message = messages[i]
77
+ const getUGC = await UgcParser.ugcExtractor(message) as types.UGCEntry
78
+ if (getUGC != null) {
79
+ const attributes = validated as types.StanzaAttributes;
80
+ const baseProperties = await EventParser.getBaseProperties(message, attributes, getUGC) as types.EventProperties;
81
+ const getHeader = EventParser.getHeader({ ...attributes, ...baseProperties.raw } as types.StanzaAttributes, baseProperties);
82
+ const getEvent = this.getEvent(message, attributes);
83
+ processed.push({
84
+ type: "Feature",
85
+ properties: {
86
+ event: getEvent,
87
+ parent: getEvent,
88
+ action_type: `Issued`,
89
+ ...baseProperties,
90
+ details: {
91
+ performance: performance.now() - tick,
92
+ source: `ugc-parser`,
93
+ tracking: this.getTracking(baseProperties),
94
+ header: getHeader,
95
+ pvtec: null,
96
+ hvtec: null,
97
+ history: [{ description: baseProperties.description, issued: baseProperties.issued, type: `Issued` }],
98
+ }
99
+ },
100
+ })
101
+ }
102
+ }
103
+ EventParser.validateEvents(processed);
104
+ }
105
+ }
106
+
107
+ export default UGCAlerts;
@@ -0,0 +1,76 @@
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 pVtecParser from '../pvtec';
16
+ import hVtecParser from '../hvtec';
17
+ import UgcParser from '../ugc';
18
+ import EventParser from '../events';
19
+
20
+ export class VTECAlerts {
21
+
22
+ /**
23
+ * @function event
24
+ * @description
25
+ * Processes a validated stanza message, extracting VTEC and UGC entries,
26
+ * computing base properties, generating headers, and preparing structured
27
+ * event objects for downstream handling. Each extracted event is enriched
28
+ * with metadata, performance timing, and history information.
29
+ *
30
+ * @static
31
+ * @async
32
+ * @param {types.StanzaCompiled} validated
33
+ * @returns {Promise<void>}
34
+ */
35
+ public static async event(validated: types.StanzaCompiled): Promise<void> {
36
+ let processed = [] as unknown[];
37
+ const messages = validated?.message?.split(/(?=\$\$)/g)?.map(msg => msg.trim())?.filter(msg => msg && msg !== "$$");
38
+ if (!messages || messages.length == 0) { return }
39
+ for (let i = 0; i < messages.length; i++) {
40
+ const tick = performance.now();
41
+ const message = messages[i]
42
+ const attributes = validated as types.StanzaAttributes;
43
+ const getPVTEC = await pVtecParser.pVtecExtractor(message) as types.PVtecEntry[]
44
+ const getHVTEC = await hVtecParser.HVtecExtractor(message) as types.HVtecEntry
45
+ const getUGC = await UgcParser.ugcExtractor(message) as types.UGCEntry
46
+ if (getPVTEC != null && getUGC != null) {
47
+ for (let j = 0; j < getPVTEC.length; j++) {
48
+ const pVtec = getPVTEC[j];
49
+ const baseProperties = await EventParser.getBaseProperties(message, attributes, getUGC, pVtec, getHVTEC) as types.EventProperties;
50
+ const getHeader = EventParser.getHeader({ ...validated.attributes, ...baseProperties.raw } as types.StanzaAttributes, baseProperties, pVtec);
51
+ processed.push({
52
+ type: "Feature",
53
+ properties: {
54
+ event: pVtec.event,
55
+ parent: pVtec.event,
56
+ action_type: pVtec.status,
57
+ ...baseProperties,
58
+ details: {
59
+ performance: performance.now() - tick,
60
+ source: `pvtec-parser`,
61
+ tracking: pVtec.tracking,
62
+ header: getHeader,
63
+ pvtec: pVtec.raw,
64
+ hvtec: getHVTEC != null ? getHVTEC.raw : null,
65
+ history: [{ description: baseProperties.description, issued: baseProperties.issued, type: pVtec.status }],
66
+ },
67
+ },
68
+ })
69
+ }
70
+ }
71
+ }
72
+ EventParser.validateEvents(processed);
73
+ }
74
+ }
75
+
76
+ export default VTECAlerts;
@@ -0,0 +1,392 @@
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 './@events/vtec';
19
+ import UGCAlerts from './@events/ugc';
20
+ import TextAlerts from './@events/text';
21
+ import CAPAlerts from './@events/cap';
22
+ import APIAlerts from './@events/api';
23
+
24
+ export class EventParser {
25
+
26
+ /**
27
+ * @function getBaseProperties
28
+ * @description
29
+ * Extracts and compiles the core properties of a weather
30
+ * alert message into a structured object. Combines parsed
31
+ * textual data, UGC information, VTEC entries, and additional
32
+ * metadata for downstream use.
33
+ *
34
+ * @static
35
+ * @async
36
+ * @param {string} message
37
+ * @param {types.StanzaCompiled} metadata
38
+ * @param {types.UGCEntry} [ugc=null]
39
+ * @param {types.PVtecEntry} [pVtec=null]
40
+ * @param {types.HVtecEntry} [hVtec=null]
41
+ * @returns {Promise<Record<string, any>>}
42
+ */
43
+ public static async getBaseProperties(message: string, metadata: types.DefaultAttributesType, ugc: types.UGCEntry = null, pVtec: types.PVtecEntry = null, hVtec: types.HVtecEntry = null): Promise<Record<string, any>> {
44
+ const settings = loader.settings as types.ClientSettingsTypes;
45
+ const definitions = {
46
+ tornado: TextParser.textProductToString(message, `TORNADO...`) ?? TextParser.textProductToString(message, `WATERSPOUT...`) ?? null,
47
+ hail: TextParser.textProductToString(message, `MAX HAIL SIZE...`, [`IN`]) ?? TextParser.textProductToString(message, `HAIL...`, [`IN`]) ?? null,
48
+ gusts: TextParser.textProductToString(message, `MAX WIND GUST...`) ?? TextParser.textProductToString(message, `WIND...`) ?? null,
49
+ flood: TextParser.textProductToString(message, `FLASH FLOOD...`) ?? null,
50
+ damage: TextParser.textProductToString(message, `DAMAGE THREAT...`) ?? null,
51
+ source: TextParser.textProductToString(message, `SOURCE...`, [`.`]) ?? null,
52
+ description: TextParser.textProductToDescription(message, pVtec?.raw ?? null),
53
+ polygon: TextParser.textProductToPolygon(message),
54
+ wmo: message.match(loader.definitions.regular_expressions.wmo)?.[0] ?? null,
55
+ mdTorIntensity: TextParser.textProductToString(message, `MOST PROBABLE PEAK TORNADO INTENSITY...`) ?? null,
56
+ mdWindGusts: TextParser.textProductToString(message, `MOST PROBABLE PEAK WIND GUST...`) ?? null,
57
+ mdHailSize: TextParser.textProductToString(message, `MOST PROBABLE PEAK HAIL SIZE...`) ?? null,
58
+ };
59
+ const getOffice = this.getICAO(pVtec, metadata, definitions.wmo);
60
+ const getCorrectIssued = this.getCorrectIssuedDate(metadata);
61
+ const getCorrectExpiry = this.getCorrectExpiryDate(pVtec, ugc);
62
+ const base = {
63
+ locations: ugc?.locations.join(`; `) ?? `No Location Specified (UGC Missing)`,
64
+ issued: getCorrectIssued,
65
+ expires: getCorrectExpiry,
66
+ geocode: {
67
+ UGC: ugc?.zones ?? [],
68
+ generated: definitions.polygon.length > 0 ? Buffer.from(JSON.stringify([definitions.polygon])).toString('base64') : null
69
+ },
70
+ description: definitions.description,
71
+ instruction: null,
72
+ sender_name: getOffice.name,
73
+ sender_icao: getOffice.icao,
74
+ raw: {...Object.fromEntries(Object.entries(metadata).filter(([key]) => key !== 'message'))},
75
+ parameters: {
76
+ wmo: Array.isArray(definitions.wmo) ? definitions.wmo[0] : (definitions.wmo ?? null),
77
+ source: definitions.source,
78
+ max_hail_size: definitions.hail,
79
+ max_wind_gust: definitions.gusts,
80
+ damage_threat: definitions.damage,
81
+ tornado_detection: definitions.tornado,
82
+ flood_detection: definitions.flood,
83
+ discussion_tornado_intensity: definitions.mdTorIntensity,
84
+ discussion_wind_intensity: definitions.mdWindGusts,
85
+ discussion_hail_intensity: definitions.mdHailSize,
86
+ },
87
+ };
88
+ return base;
89
+ }
90
+
91
+ /**
92
+ * @function getEventGeometry
93
+ * @description
94
+ * Determines the geometry of an event using polygon data fromEntries
95
+ * in the message or UGC shapefile coordinates if enabled in settings. Falls
96
+ * back to null if no geometry can be determined.
97
+ *
98
+ * @static
99
+ * @param {string} generated
100
+ * @param {types.UGCEntry} [ugc=null]
101
+ * @returns {Promise<types.geometry>}
102
+ */
103
+ public static async getEventGeometry(generated: string, ugc: types.UGCEntry = null, isUnion: boolean = true) : Promise<types.geometry> {
104
+ const settings = loader.settings as types.ClientSettingsTypes;
105
+ let geometry = { type: "Polygon", coordinates: generated != null ? JSON.parse(Buffer.from(generated, 'base64').toString('utf-8')) : null };
106
+ if (settings.global_settings.shapefile_coordinates && generated == null && ugc != null) {
107
+ const coordinates = await UGCParser.getCoordinates(ugc.zones, isUnion) as any;
108
+ geometry = coordinates
109
+ }
110
+ return geometry;
111
+ }
112
+
113
+ /**
114
+ * @function betterParsedEventName
115
+ * @description
116
+ * Enhances the parsing of an event name using additional criteria
117
+ * from its description and parameters. Can optionally use
118
+ * the original parent event name instead.
119
+ *
120
+ * @static
121
+ * @param {types.EventCompiled} event
122
+ * @param {boolean} [betterParsing=false]
123
+ * @param {boolean} [useParentEvents=false]
124
+ * @returns {string}
125
+ */
126
+ public static betterParsedEventName(event: types.EventCompiled, betterParsing?: boolean, useParentEvents?: boolean): string {
127
+ let eventName = event?.properties?.event ?? `Unknown Event`;
128
+ const defEventTable = loader.definitions.enhancedEvents;
129
+ const properties = event?.properties;
130
+ const parameters = properties?.parameters;
131
+ const description = (properties?.description ?? `Unknown Description`).toLowerCase();
132
+ const damageThreatTag = parameters?.damage_threat ?? null;
133
+ const tornadoThreatTag = parameters?.tornado_detection ?? null;
134
+ if (!betterParsing) { return eventName }
135
+ for (const eventGroup of defEventTable) {
136
+ const [baseEvent, conditions] = Object.entries(eventGroup)[0] as [string, Record<string, types.EnhancedEventCondition>];
137
+ if (eventName === baseEvent) {
138
+ for (const [specificEvent, condition] of Object.entries(conditions)) {
139
+ let conditionMet = false;
140
+ if (condition.description) {
141
+ conditionMet = description.includes(condition.description.toLowerCase());
142
+ if (!conditionMet) continue;
143
+ }
144
+ if (!conditionMet && condition.condition) {
145
+ const tagToCheck = baseEvent.includes('Tornado') ? tornadoThreatTag : damageThreatTag;
146
+ conditionMet = condition.condition(tagToCheck);
147
+ }
148
+ if (conditionMet) {
149
+ eventName = specificEvent;
150
+ break;
151
+ }
152
+ }
153
+ if (baseEvent === 'Severe Thunderstorm Warning' && tornadoThreatTag === 'POSSIBLE' && !eventName.includes('(TPROB)')) {
154
+ eventName += ' (TPROB)';
155
+ }
156
+ break;
157
+ }
158
+ }
159
+ return useParentEvents ? event?.properties?.event : eventName;
160
+ }
161
+
162
+ /**
163
+ * @function validateEvents
164
+ * @description
165
+ * Processes an array of event objects and filters them based on
166
+ * global and EAS filtering settings, and
167
+ * other criteria such as expired or test products. Valid events
168
+ * trigger relevant event emitters.
169
+ *
170
+ * @static
171
+ * @param {unknown[]} events
172
+ * @returns {Promise<void>}
173
+ */
174
+ public static async validateEvents(events: unknown[]): Promise<void> {
175
+ if (events.length == 0) return;
176
+ const filteringSettings = loader.settings?.global_settings?.filtering;
177
+ const easSettings = loader.settings?.global_settings?.eas_settings;
178
+ const globalSettings = loader.settings?.global_settings;
179
+ const sets = {} as Record<string, Set<string>>;
180
+ const bools = {} as Record<string, boolean>;
181
+ const megered = {...filteringSettings, ...easSettings, ...globalSettings };
182
+ for (const key in megered) {
183
+ const setting = megered[key];
184
+ if (Array.isArray(setting)) { sets[key] = new Set(setting.map(item => item.toLowerCase())); }
185
+ if (typeof setting === 'boolean') { bools[key] = setting; }
186
+ }
187
+ const filtered = events.filter((event: types.EventCompiled) => {
188
+ const originalEvent = this.buildDefaultSignature(event);
189
+ const props = originalEvent?.properties;
190
+ const ugcs = props?.geocode?.UGC ?? [];
191
+ const { details, ...properties } = originalEvent.properties;
192
+ originalEvent.properties.parent = originalEvent.properties.event;
193
+ originalEvent.properties.event = this.betterParsedEventName(originalEvent, bools?.better_event_parsing, bools?.parent_events_only);
194
+ originalEvent.properties.hash = loader.packages.crypto.createHash('md5').update(JSON.stringify(properties)).digest('hex');
195
+ if (originalEvent.properties.is_test == true) {
196
+ loader.cache.events.emit(`onTest`, originalEvent);
197
+ if (bools?.ignore_test_products) {
198
+ return false;
199
+ }
200
+ }
201
+ if (originalEvent.properties.is_cancelled == true) {
202
+ loader.cache.events.emit(`onExpired`, originalEvent);
203
+ if (bools?.check_expired) {
204
+ return false;
205
+ }
206
+ }
207
+ loader.cache.events.emit(`on${originalEvent.properties.event.replace(/\s+/g, '')}`, originalEvent);
208
+ for (const key in sets) {
209
+ const setting = sets[key];
210
+ if (key === 'events' && setting.size > 0 && !setting.has(originalEvent.properties.event.toLowerCase())) {
211
+ loader.cache.events.emit(`onFilteredEvent`, originalEvent); return false
212
+ }
213
+ if (key === 'ignored_events' && setting.size > 0 && setting.has(originalEvent.properties.event.toLowerCase())) {
214
+ loader.cache.events.emit(`onIgnoredEvent`, originalEvent); return false
215
+ }
216
+ if (key === 'filtered_icao' && setting.size > 0 && props.sender_icao != null && !setting.has(props.sender_icao.toLowerCase())) {
217
+ loader.cache.events.emit(`onFilteredICAO`, originalEvent); return false
218
+ }
219
+ if (key === 'ignored_icao' && setting.size > 0 && props.sender_icao != null && setting.has(props.sender_icao.toLowerCase())) {
220
+ loader.cache.events.emit(`onIgnoredICAO`, originalEvent); return false
221
+ }
222
+ if (key === 'ugc_filter' && setting.size > 0 && ugcs.length > 0 && !ugcs.some((ugc: string) => setting.has(ugc.toLowerCase()))) {
223
+ loader.cache.events.emit(`onFilteredUGC`, originalEvent); return false
224
+ }
225
+ if (key === 'state_filter' && setting.size > 0 && ugcs.length > 0 && !ugcs.some((ugc: string) => setting.has(ugc.substring(0, 2).toLowerCase()))) {
226
+ loader.cache.events.emit(`onFilteredState`, originalEvent); return false
227
+ }
228
+ }
229
+ return true;
230
+ })
231
+ for (const event of filtered as types.EventCompiled[]) {
232
+ if (!loader.settings.global_settings.ignore_geometry_parsing) {
233
+ const geometry = await this.getEventGeometry(event.properties.geocode.generated, {
234
+ zones: event.properties.geocode != null ? event.properties.geocode.UGC : null
235
+ })
236
+ event.geometry = geometry;
237
+ }
238
+ }
239
+ if (filtered.length > 0) { loader.cache.events.emit(`onEvents`, filtered); }
240
+ }
241
+
242
+ /**
243
+ * @function getHeader
244
+ * @description
245
+ * Constructs a standardized alert header string using provided
246
+ * stanza attributes, event properties, and optional VTEC data.
247
+ *
248
+ * @static
249
+ * @param {types.StanzaAttributes} attributes
250
+ * @param {types.EventProperties} [properties]
251
+ * @param {types.PVtecEntry} [pVtec]
252
+ * @returns {string}
253
+ */
254
+ public static getHeader(attributes: types.StanzaAttributes, properties?: types.EventProperties, pVtec?: types.PVtecEntry): string {
255
+ const parent = `ATSX`
256
+ const alertType = attributes?.awipsType?.type ?? attributes?.getAwip?.prefix ?? `XX`;
257
+ const ugc = properties?.geocode?.UGC != null ? properties?.geocode?.UGC.join(`-`) : `000000`;
258
+ const status = pVtec?.status ?? 'Issued';
259
+ const issued = properties?.issued != null ? new Date(properties?.issued)?.toISOString().replace(/[-:]/g, '').split('.')[0] : new Date().toISOString().replace(/[-:]/g, '').split('.')[0];
260
+ const sender = properties?.sender_icao ?? `XXXX`;
261
+ const header = `ZCZC-${parent}-${alertType}-${ugc}-${status}-${issued}-${sender}-`;
262
+ return header
263
+ }
264
+
265
+ /**
266
+ * @function eventHandler
267
+ * @description
268
+ * Routes a validated stanza object to the appropriate alert handler
269
+ * based on its type flags: API, CAP, pVTEC (Primary VTEC), UGC, or plain text.
270
+ *
271
+ * @static
272
+ * @param {types.StanzaCompiled} metadata
273
+ * @returns {void}
274
+ */
275
+ public static eventHandler(metadata: types.StanzaCompiled): Promise<void> {
276
+ const settings = loader.settings as types.ClientSettingsTypes;
277
+ const preferences = settings.noaa_weather_wire_service_settings.preferences;
278
+ if (metadata.isApi) return APIAlerts.event(metadata)
279
+ if (metadata.isCap) return CAPAlerts.event(metadata)
280
+ if (!preferences.disable_vtec && !metadata.isCap && metadata.isPVtec && metadata.isUGC) return VTECAlerts.event(metadata);
281
+ if (!preferences.disable_ugc && !metadata.isCap && !metadata.isPVtec && metadata.isUGC) return UGCAlerts.event(metadata);
282
+ if (!preferences.disable_text && !metadata.isCap && !metadata.isPVtec && !metadata.isUGC) return TextAlerts.event(metadata);
283
+ return;
284
+ }
285
+
286
+ /**
287
+ * @function getICAO
288
+ * @description
289
+ * Determines the ICAO code and corresponding name for an event.
290
+ * Priority is given to the VTEC tracking code, then the attributes' `cccc` property,
291
+ * and finally the WMO code if available. Returns null if none are found.
292
+ *
293
+ * @private
294
+ * @static
295
+ * @param {types.PVtecEntry | null} pVtec
296
+ * @param {Record<string, string>} metadata
297
+ * @param {RegExpMatchArray | string | null} WMO
298
+ * @returns {{ icao: string; name: string }}
299
+ */
300
+ private static getICAO(pVtec: types.PVtecEntry, metadata: types.DefaultAttributesType, WMO: RegExpMatchArray | string | null): { icao: string; name: string } {
301
+ const icao = pVtec != null ? pVtec?.tracking.split(`-`)[0] : (metadata.attributes?.cccc || (WMO != null ? (Array.isArray(WMO) ? WMO[0] : WMO) : null));
302
+ const name = loader.definitions.ICAO?.[icao] ?? null;
303
+ return { icao, name };
304
+ }
305
+
306
+ /**
307
+ * @function getCorrectIssuedDate
308
+ * @description
309
+ * Determines the issued date for an event based on the provided attributes.
310
+ * Falls back to the current date and time if no valid issue date is available.
311
+ *
312
+ * @private
313
+ * @static
314
+ * @param {Record<string, string>} metadata
315
+ * @returns {string}
316
+ */
317
+ private static getCorrectIssuedDate(metadata: types.DefaultAttributesType): string {
318
+ const time = metadata.attributes.issue != null ?
319
+ new Date(metadata.attributes.issue).toISOString() : (metadata.attributes?.issue != null ?
320
+ new Date(metadata.attributes.issue).toISOString() :
321
+ new Date().toISOString());
322
+ return time;
323
+ }
324
+
325
+ /**
326
+ * @function getCorrectExpiryDate
327
+ * @description
328
+ * Determines the most appropriate expiry date for an event using VTEC or UGC data.
329
+ * Falls back to one hour from the current time if no valid expiry is available.
330
+ *
331
+ * @private
332
+ * @static
333
+ * @param {types.PVtecEntry} pVtec
334
+ * @param {types.UGCEntry} ugc
335
+ * @returns {string}
336
+ */
337
+ private static getCorrectExpiryDate(pVtec: types.PVtecEntry, ugc: types.UGCEntry): string | Date {
338
+ const time = pVtec?.expires && !isNaN(new Date(pVtec.expires).getTime()) ?
339
+ new Date(pVtec.expires).toISOString() : (ugc?.expiry != null ? new Date(ugc.expiry).toISOString() : new Date(new Date().getTime() + 1 * 60 * 60 * 1000))
340
+ if (isNaN(new Date(time).getTime())) {
341
+ return `Until Further Notice`
342
+ }
343
+ return time;
344
+ }
345
+
346
+ /**
347
+ * @function buildDefaultSignature
348
+ * @description
349
+ * Populates default properties for an event object, including action type flags,
350
+ * tags, and status updates. Determines if the event is issued, updated, or cancelled
351
+ * based on correlations, description content, VTEC codes, and expiration time.
352
+ *
353
+ * @private
354
+ * @static
355
+ * @param {any} event
356
+ * @returns {types.EventCompiled}
357
+ */
358
+ private static buildDefaultSignature(event: types.EventCompiled): types.EventCompiled {
359
+ const props = event.properties ?? {};
360
+ const statusCorrelation = loader.definitions.correlations.find((c: { type: string }) => c.type === props.action_type);
361
+ const defEventTags = loader.definitions.tags;
362
+ const tags = Object.entries(defEventTags).filter(([key]) => props?.description?.toLowerCase().includes(key.toLowerCase())).map(([, value]) => value)
363
+ props.tags = tags.length > 0 ? tags : [];
364
+ if (statusCorrelation) {
365
+ props.action_type = statusCorrelation.forward ?? props.action_type;
366
+ props.is_updated = !!statusCorrelation.update; props.is_issued = !!statusCorrelation.new;
367
+ props.is_cancelled = !!statusCorrelation.cancel;
368
+ } else {
369
+ props.is_issued = true
370
+ }
371
+ if (props.description) {
372
+ const detectedPhrase = loader.definitions.cancelSignatures.find(sig => props.description.toLowerCase().includes(sig.toLowerCase()));
373
+ if (detectedPhrase) {
374
+ props.is_cancelled = true;
375
+ }
376
+ }
377
+ if (props.details?.pvtec) {
378
+ const getType = props.details.pvtec.split(`.`)[0]?.replace(`/`, ``)
379
+ const isTestProduct = loader.definitions.productTypes[getType] == `Test Product`
380
+ const isTestSig = [`This is a test message`, `THIS_MESSAGE_IS_FOR_TEST_PURPOSES_ONLY`]
381
+ if (isTestProduct || isTestSig.some(sig => props?.description?.toLowerCase().includes(sig.toLowerCase()) || props?.instruction?.toLowerCase().includes(sig.toLowerCase()))) {
382
+ props.is_test = true
383
+ }
384
+ }
385
+ if (new Date(props?.expires).getTime() < new Date().getTime()) {
386
+ props.is_cancelled = true
387
+ }
388
+ return event;
389
+ }
390
+ }
391
+
392
+ export default EventParser