@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.
- package/LICENSE +17 -0
- package/README.md +269 -0
- package/dist/cjs/index.cjs +7554 -0
- package/dist/esm/index.mjs +7542 -0
- package/dist/index.d.mts +1024 -0
- package/dist/index.d.ts +1024 -0
- package/package.json +55 -0
- package/src/@dictionaries/awips.ts +358 -0
- package/src/@dictionaries/events.ts +168 -0
- package/src/@dictionaries/icao.ts +250 -0
- package/src/@dictionaries/signatures.ts +139 -0
- package/src/@parsers/@events/api.ts +146 -0
- package/src/@parsers/@events/cap.ts +123 -0
- package/src/@parsers/@events/text.ts +104 -0
- package/src/@parsers/@events/ugc.ts +107 -0
- package/src/@parsers/@events/vtec.ts +76 -0
- package/src/@parsers/events.ts +392 -0
- package/src/@parsers/hvtec.ts +46 -0
- package/src/@parsers/pvtec.ts +72 -0
- package/src/@parsers/stanza.ts +97 -0
- package/src/@parsers/text.ts +165 -0
- package/src/@parsers/ugc.ts +247 -0
- package/src/@submodules/database.ts +162 -0
- package/src/@submodules/eas.ts +490 -0
- package/src/@submodules/utils.ts +222 -0
- package/src/@submodules/xmpp.ts +142 -0
- package/src/bootstrap.ts +190 -0
- package/src/index.ts +218 -0
- package/src/types.ts +259 -0
|
@@ -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
|