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