@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,222 @@
|
|
|
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 StanzaParser from '../@parsers/stanza';
|
|
17
|
+
import EventParser from '../@parsers/events';
|
|
18
|
+
import Xmpp from './xmpp';
|
|
19
|
+
|
|
20
|
+
export class Utils {
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @function sleep
|
|
24
|
+
* @description
|
|
25
|
+
* Pauses execution for a specified number of milliseconds.
|
|
26
|
+
*
|
|
27
|
+
* @static
|
|
28
|
+
* @async
|
|
29
|
+
* @param {number} ms
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
public static async sleep(ms: number): Promise<void> {
|
|
33
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @function warn
|
|
38
|
+
* @description
|
|
39
|
+
* Emits a log event and prints a warning to the console. Throttles repeated
|
|
40
|
+
* warnings within a short interval unless `force` is `true`.
|
|
41
|
+
*
|
|
42
|
+
* @static
|
|
43
|
+
* @param {string} message
|
|
44
|
+
* @param {boolean} [force=false]
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
public static warn(message: string, force: boolean = false): void {
|
|
48
|
+
loader.cache.events.emit('log', message)
|
|
49
|
+
if (!loader.settings.journal) return;
|
|
50
|
+
if (loader.cache.lastWarn != null && (Date.now() - loader.cache.lastWarn < 500) && !force) return;
|
|
51
|
+
loader.cache.lastWarn = Date.now();
|
|
52
|
+
console.warn(`\x1b[33m[ATMOSX-PARSER]\x1b[0m [${new Date().toLocaleString()}] ${message}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @function loadCollectionCache
|
|
57
|
+
* @description
|
|
58
|
+
* Loads cached stanzas from the database, validates them, and processes them through the event parser.
|
|
59
|
+
* Only processes stanzas that are not marked to be ignored and match the CAP preferences.
|
|
60
|
+
*
|
|
61
|
+
* @static
|
|
62
|
+
* @async
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
public static async loadCollectionCache(): Promise<void> {
|
|
66
|
+
try {
|
|
67
|
+
const settings = loader.settings as types.ClientSettingsTypes;
|
|
68
|
+
if (settings.noaa_weather_wire_service_settings.cache.enabled) {
|
|
69
|
+
const maxRows = settings.noaa_weather_wire_service_settings.cache.max_db_cache_size ?? 5000;
|
|
70
|
+
const rows = await loader.cache.db.prepare(`SELECT * FROM stanzas ORDER BY rowid DESC LIMIT ?`)
|
|
71
|
+
.all(maxRows) as { rowid: number; stanza: string }[];
|
|
72
|
+
this.warn(loader.definitions.messages.dump_cache.replace(`{count}`, rows.length.toString()), true);
|
|
73
|
+
const eventsToProcess = rows
|
|
74
|
+
.map(row => {return JSON.parse(row.stanza)})
|
|
75
|
+
.filter(validate => {
|
|
76
|
+
if (!validate) return false;
|
|
77
|
+
const skip = validate.ignore ||
|
|
78
|
+
(validate.isCap && !settings.noaa_weather_wire_service_settings.preferences.cap_only) ||
|
|
79
|
+
(!validate.isCap && settings.noaa_weather_wire_service_settings.preferences.cap_only) ||
|
|
80
|
+
(validate.isCap && !validate.isCapDescription);
|
|
81
|
+
return !skip;
|
|
82
|
+
});
|
|
83
|
+
await Promise.all(eventsToProcess.map(validate => EventParser.eventHandler(validate)));
|
|
84
|
+
this.warn(loader.definitions.messages.dump_cache_complete, true);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
Utils.warn(`Failed to load cache: ${error.stack}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @function loadGeoJsonData
|
|
94
|
+
* @description
|
|
95
|
+
* Fetches GeoJSON data from the National Weather Service endpoint and
|
|
96
|
+
* passes it to the event parser for processing.
|
|
97
|
+
*
|
|
98
|
+
* @static
|
|
99
|
+
* @async
|
|
100
|
+
* @returns {Promise<void>}
|
|
101
|
+
*/
|
|
102
|
+
public static async loadGeoJsonData(): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
const settings = loader.settings as types.ClientSettingsTypes;
|
|
105
|
+
const response = await this.createHttpRequest<types.GenericHTTPResponse >(
|
|
106
|
+
settings.national_weather_service_settings.endpoint
|
|
107
|
+
);
|
|
108
|
+
if (response.error) return;
|
|
109
|
+
EventParser.eventHandler({
|
|
110
|
+
message: JSON.stringify(response.message),
|
|
111
|
+
attributes: {},
|
|
112
|
+
isCap: true,
|
|
113
|
+
isApi: true,
|
|
114
|
+
isPVtec: false,
|
|
115
|
+
isUGC: false,
|
|
116
|
+
isCapDescription: false,
|
|
117
|
+
awipsType: { type: 'api', prefix: 'AP' },
|
|
118
|
+
ignore: false
|
|
119
|
+
});
|
|
120
|
+
} catch (error: unknown) {
|
|
121
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
122
|
+
Utils.warn(`Failed to load National Weather Service GeoJSON Data: ${msg}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @function createHttpRequest
|
|
128
|
+
* @description
|
|
129
|
+
* Performs an HTTP GET request with default headers and timeout, returning
|
|
130
|
+
* either the response data or an error message.
|
|
131
|
+
*
|
|
132
|
+
* @static
|
|
133
|
+
* @template T
|
|
134
|
+
* @param {string} url
|
|
135
|
+
* @param {types.HTTPSettings} [options]
|
|
136
|
+
* @returns {Promise<{ error: boolean; message: T | string }>}
|
|
137
|
+
*/
|
|
138
|
+
public static async createHttpRequest<T = unknown>(url: string, options?: types.HTTPSettings): Promise<{ error: boolean; message: T | string }> {
|
|
139
|
+
const defaultOptions = {
|
|
140
|
+
timeout: 10000,
|
|
141
|
+
headers: {
|
|
142
|
+
"User-Agent": "AtmosphericX",
|
|
143
|
+
"Accept": "application/geo+json, text/plain, */*; q=0.9",
|
|
144
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const requestOptions = {
|
|
148
|
+
...defaultOptions,
|
|
149
|
+
...options,
|
|
150
|
+
headers: { ...defaultOptions.headers, ...(options?.headers ?? {}) }
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
const resp = await loader.packages.axios.get<T>(url, {
|
|
154
|
+
headers: requestOptions.headers,
|
|
155
|
+
timeout: requestOptions.timeout,
|
|
156
|
+
maxRedirects: 0,
|
|
157
|
+
validateStatus: (status) => status === 200 || status === 500
|
|
158
|
+
});
|
|
159
|
+
return { error: false, message: resp.data };
|
|
160
|
+
} catch (err: unknown) {
|
|
161
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
162
|
+
return { error: true, message: msg };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @function handleCronJob
|
|
168
|
+
* @description
|
|
169
|
+
* Performs scheduled tasks for NWWS XMPP session maintenance or GeoJSON data
|
|
170
|
+
* updates depending on the job type.
|
|
171
|
+
*
|
|
172
|
+
* @static
|
|
173
|
+
* @param {boolean} isWire
|
|
174
|
+
* @returns {void}
|
|
175
|
+
*/
|
|
176
|
+
public static handleCronJob(isWire: boolean): void {
|
|
177
|
+
try {
|
|
178
|
+
const settings = loader.settings as types.ClientSettingsTypes;
|
|
179
|
+
const cache = settings.noaa_weather_wire_service_settings.cache;
|
|
180
|
+
const reconnections = settings.noaa_weather_wire_service_settings.reconnection_settings;
|
|
181
|
+
if (isWire) {
|
|
182
|
+
if (reconnections.enabled) {
|
|
183
|
+
void Xmpp.isSessionReconnectionEligible(reconnections.interval);
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
void this.loadGeoJsonData();
|
|
187
|
+
}
|
|
188
|
+
} catch (error: unknown) {
|
|
189
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
190
|
+
Utils.warn(`Failed to perform scheduled tasks (${isWire ? 'NWWS' : 'GeoJSON'}): ${msg}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @function mergeClientSettings
|
|
196
|
+
* @description
|
|
197
|
+
* Recursively merges a ClientSettings object into a target object,
|
|
198
|
+
* preserving nested structures and overriding existing values.
|
|
199
|
+
*
|
|
200
|
+
* @static
|
|
201
|
+
* @param {Record<string, unknown>} target
|
|
202
|
+
* @param {types.ClientSettingsTypes} settings
|
|
203
|
+
* @returns {Record<string, unknown>}
|
|
204
|
+
*/
|
|
205
|
+
public static mergeClientSettings(target: Record<string, unknown>, settings: types.ClientSettingsTypes): Record<string, unknown> {
|
|
206
|
+
for (const key in settings) {
|
|
207
|
+
if (!Object.prototype.hasOwnProperty.call(settings, key)) continue;
|
|
208
|
+
const value = settings[key];
|
|
209
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
210
|
+
if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
|
|
211
|
+
target[key] = {};
|
|
212
|
+
}
|
|
213
|
+
this.mergeClientSettings(target[key] as Record<string, unknown>, value as types.ClientSettingsTypes);
|
|
214
|
+
} else {
|
|
215
|
+
target[key] = value;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return target;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export default Utils;
|
|
@@ -0,0 +1,142 @@
|
|
|
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 Utils from './utils';
|
|
17
|
+
import StanzaParser from '../@parsers/stanza';
|
|
18
|
+
import Database from '../@submodules/database';
|
|
19
|
+
import EventParser from '../@parsers/events';
|
|
20
|
+
|
|
21
|
+
export class Xmpp {
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @function isSessionReconnectionEligible
|
|
25
|
+
* @description
|
|
26
|
+
* Checks if the XMPP session has been inactive longer than the given interval
|
|
27
|
+
* and, if so, attempts a controlled reconnection.
|
|
28
|
+
*
|
|
29
|
+
* @async
|
|
30
|
+
* @static
|
|
31
|
+
* @param {number} currentInterval
|
|
32
|
+
* @returns {Promise<void>}
|
|
33
|
+
*/
|
|
34
|
+
public static async isSessionReconnectionEligible(currentInterval: number): Promise<void> {
|
|
35
|
+
const settings = loader.settings as types.ClientSettingsTypes;
|
|
36
|
+
const lastStanzaElapsed = Date.now() - loader.cache.lastStanza;
|
|
37
|
+
const threshold = currentInterval * 1000;
|
|
38
|
+
if ((!loader.cache.isConnected && !loader.cache.sigHalt) || !loader.cache.session) { return; }
|
|
39
|
+
if (lastStanzaElapsed < threshold) { return; }
|
|
40
|
+
if (loader.cache.attemptingReconnect) { return; }
|
|
41
|
+
loader.cache.attemptingReconnect = true;
|
|
42
|
+
loader.cache.isConnected = false;
|
|
43
|
+
loader.cache.totalReconnects += 1;
|
|
44
|
+
try {
|
|
45
|
+
loader.cache.events.emit("onReconnection", {
|
|
46
|
+
reconnects: loader.cache.totalReconnects,
|
|
47
|
+
lastStanza: lastStanzaElapsed,
|
|
48
|
+
lastName: settings.noaa_weather_wire_service_settings.credentials.nickname,
|
|
49
|
+
});
|
|
50
|
+
await loader.cache.session.stop().catch(() => {});
|
|
51
|
+
await loader.cache.session.start().catch(() => {});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
Utils.warn(`XMPP reconnection failed: ${(err as Error).message}`);
|
|
54
|
+
} finally {
|
|
55
|
+
loader.cache.attemptingReconnect = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @function deploySession
|
|
61
|
+
* @description
|
|
62
|
+
* Initializes the NOAA Weather Wire Service (NWWS-OI) XMPP client session and
|
|
63
|
+
* manages its lifecycle events including connection, disconnection, errors,
|
|
64
|
+
* and message handling.
|
|
65
|
+
*
|
|
66
|
+
* @async
|
|
67
|
+
* @static
|
|
68
|
+
* @returns {Promise<void>}
|
|
69
|
+
*/
|
|
70
|
+
public static async deploySession(): Promise<void> {
|
|
71
|
+
const settings = loader.settings as types.ClientSettingsTypes;
|
|
72
|
+
settings.noaa_weather_wire_service_settings.credentials.nickname ??= settings.noaa_weather_wire_service_settings.credentials.username;
|
|
73
|
+
loader.cache.session = loader.packages.xmpp.client({
|
|
74
|
+
service: 'xmpp://nwws-oi.weather.gov',
|
|
75
|
+
domain: 'nwws-oi.weather.gov',
|
|
76
|
+
username: settings.noaa_weather_wire_service_settings.credentials.username,
|
|
77
|
+
password: settings.noaa_weather_wire_service_settings.credentials.password,
|
|
78
|
+
});
|
|
79
|
+
loader.cache.session.on('online', async (address: string) => {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
if (loader.cache.lastConnect && now - loader.cache.lastConnect < 10_000) {
|
|
82
|
+
loader.cache.sigHalt = true;
|
|
83
|
+
Utils.warn(loader.definitions.messages.reconnect_too_fast);
|
|
84
|
+
await Utils.sleep(2_000);
|
|
85
|
+
await loader.cache.session.stop().catch(() => {});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
loader.cache.isConnected = true;
|
|
89
|
+
loader.cache.sigHalt = false;
|
|
90
|
+
loader.cache.lastConnect = now;
|
|
91
|
+
loader.cache.session.send(loader.packages.xmpp.xml('presence', {
|
|
92
|
+
to: `nwws@conference.nwws-oi.weather.gov/${settings.noaa_weather_wire_service_settings.credentials.nickname}`,
|
|
93
|
+
xmlns: 'http://jabber.org/protocol/muc',
|
|
94
|
+
}));
|
|
95
|
+
loader.cache.events.emit('onConnection', settings.noaa_weather_wire_service_settings.credentials.nickname);
|
|
96
|
+
if (loader.cache.attemptingReconnect) return;
|
|
97
|
+
loader.cache.attemptingReconnect = true;
|
|
98
|
+
await Utils.sleep(15_000);
|
|
99
|
+
loader.cache.attemptingReconnect = false;
|
|
100
|
+
});
|
|
101
|
+
loader.cache.session.on('offline', () => {
|
|
102
|
+
loader.cache.isConnected = false;
|
|
103
|
+
loader.cache.sigHalt = true;
|
|
104
|
+
Utils.warn('XMPP connection went offline');
|
|
105
|
+
});
|
|
106
|
+
loader.cache.session.on('error', (error: Error) => {
|
|
107
|
+
loader.cache.isConnected = false;
|
|
108
|
+
loader.cache.sigHalt = true;
|
|
109
|
+
Utils.warn(`XMPP connection error: ${error.message}`);
|
|
110
|
+
});
|
|
111
|
+
loader.cache.session.on('stanza', async (stanza: any) => {
|
|
112
|
+
try {
|
|
113
|
+
loader.cache.lastStanza = Date.now();
|
|
114
|
+
if (stanza.is('message')) {
|
|
115
|
+
const validate = StanzaParser.validate(stanza);
|
|
116
|
+
const skipMessage = validate.ignore ||
|
|
117
|
+
(validate.isCap && !settings.noaa_weather_wire_service_settings.preferences.cap_only) ||
|
|
118
|
+
(!validate.isCap && settings.noaa_weather_wire_service_settings.preferences.cap_only) ||
|
|
119
|
+
(validate.isCap && !validate.isCapDescription);
|
|
120
|
+
if (skipMessage) return;
|
|
121
|
+
await EventParser.eventHandler(validate);
|
|
122
|
+
await Database.stanzaCacheImport(validate);
|
|
123
|
+
loader.cache.events.emit('onMessage', validate);
|
|
124
|
+
}
|
|
125
|
+
if (stanza.is('presence') && stanza.attrs.from?.startsWith('nwws@conference.nwws-oi.weather.gov/')) {
|
|
126
|
+
const occupant = stanza.attrs.from.split('/').slice(1).join('/');
|
|
127
|
+
loader.cache.events.emit('onOccupant', {
|
|
128
|
+
occupant,
|
|
129
|
+
type: stanza.attrs.type === 'unavailable' ? 'unavailable' : 'available',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
} catch (err: unknown) {
|
|
133
|
+
Utils.warn(`Error processing stanza: ${(err as Error).message}`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
try { await loader.cache.session.start(); } catch (err: unknown) {
|
|
137
|
+
Utils.warn(`Failed to start XMPP session: ${(err as Error).message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default Xmpp;
|
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/*
|
|
2
|
+
_ _ __ __
|
|
3
|
+
/\ | | | | (_) \ \ / /
|
|
4
|
+
/ \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
|
|
5
|
+
/ /\ \| __| '_ ` _ \ / _ \/ __| '_ \| '_ \ / _ \ '__| |/ __| > <
|
|
6
|
+
/ ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
|
|
7
|
+
/_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
|
|
8
|
+
| |
|
|
9
|
+
|_|
|
|
10
|
+
|
|
11
|
+
Written by: k3yomi@GitHub
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import * as events from 'events';
|
|
17
|
+
import * as xmpp from '@xmpp/client';
|
|
18
|
+
import * as shapefile from 'shapefile';
|
|
19
|
+
import * as xml2js from 'xml2js';
|
|
20
|
+
import * as jobs from 'croner';
|
|
21
|
+
import * as polygonClipping from 'polygon-clipping';
|
|
22
|
+
import sqlite3 from 'better-sqlite3';
|
|
23
|
+
import axios from 'axios';
|
|
24
|
+
import crypto from 'crypto';
|
|
25
|
+
import os from 'os';
|
|
26
|
+
import say from 'say';
|
|
27
|
+
import child from 'child_process';
|
|
28
|
+
import jszip from 'jszip';
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
import * as dictEvents from './@dictionaries/events';
|
|
32
|
+
import * as dictAwips from './@dictionaries/awips';
|
|
33
|
+
import * as dictSignatures from './@dictionaries/signatures';
|
|
34
|
+
import * as dictICAOs from './@dictionaries/icao';
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
export const packages = {
|
|
39
|
+
fs, path, events, xmpp,
|
|
40
|
+
shapefile, xml2js, sqlite3, jobs, axios,
|
|
41
|
+
crypto, os, say, child, polygonClipping, jszip
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const cache = {
|
|
45
|
+
isReady: true,
|
|
46
|
+
sigHalt: false,
|
|
47
|
+
isConnected: false,
|
|
48
|
+
attemptingReconnect: false,
|
|
49
|
+
totalReconnects: 0,
|
|
50
|
+
lastStanza: null,
|
|
51
|
+
session: null,
|
|
52
|
+
lastConnect: null,
|
|
53
|
+
db: null,
|
|
54
|
+
lastWarn: null,
|
|
55
|
+
totalLocationWarns: 0,
|
|
56
|
+
events: new events.EventEmitter(),
|
|
57
|
+
isProcessingAudioQueue: false,
|
|
58
|
+
audioQueue: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const settings = {
|
|
62
|
+
database: path.join(process.cwd(), 'shapefiles.db'),
|
|
63
|
+
is_wire: true,
|
|
64
|
+
journal: true,
|
|
65
|
+
noaa_weather_wire_service_settings: {
|
|
66
|
+
reconnection_settings: {
|
|
67
|
+
enabled: true,
|
|
68
|
+
interval: 60,
|
|
69
|
+
},
|
|
70
|
+
credentials: {
|
|
71
|
+
username: null,
|
|
72
|
+
password: null,
|
|
73
|
+
nickname: "AtmosphericX Standalone Parser",
|
|
74
|
+
},
|
|
75
|
+
cache: {
|
|
76
|
+
enabled: true,
|
|
77
|
+
max_db_history: 5000,
|
|
78
|
+
max_db_cache_size: 1000,
|
|
79
|
+
},
|
|
80
|
+
preferences: {
|
|
81
|
+
disable_ugc: false,
|
|
82
|
+
disable_vtec: false,
|
|
83
|
+
disable_text: false,
|
|
84
|
+
cap_only: false,
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
national_weather_service_settings: {
|
|
88
|
+
interval: 15,
|
|
89
|
+
endpoint: `https://api.weather.gov/alerts/active`,
|
|
90
|
+
},
|
|
91
|
+
global_settings: {
|
|
92
|
+
parent_events_only: true,
|
|
93
|
+
better_event_parsing: true,
|
|
94
|
+
ignore_geometry_parsing: false,
|
|
95
|
+
shapefile_coordinates: false,
|
|
96
|
+
shapefile_skip: 15,
|
|
97
|
+
filtering: {
|
|
98
|
+
events: [],
|
|
99
|
+
filtered_icao: [],
|
|
100
|
+
ignored_icao: [],
|
|
101
|
+
ignored_events: [`Xx`, `Test Message`],
|
|
102
|
+
ugc_filter: [],
|
|
103
|
+
state_filter: [],
|
|
104
|
+
check_expired: true,
|
|
105
|
+
ignore_test_products: true,
|
|
106
|
+
},
|
|
107
|
+
eas_settings: {
|
|
108
|
+
directory: null,
|
|
109
|
+
intro_wav: null,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
export const definitions = {
|
|
116
|
+
events: dictEvents.events,
|
|
117
|
+
actions: dictEvents.actions,
|
|
118
|
+
status: dictEvents.status,
|
|
119
|
+
productTypes: dictEvents.types,
|
|
120
|
+
correlations: dictEvents.status_correlations,
|
|
121
|
+
offshore: dictEvents.offshore,
|
|
122
|
+
awips: dictAwips.awips,
|
|
123
|
+
causes: dictEvents.causes,
|
|
124
|
+
records: dictEvents.records,
|
|
125
|
+
severity: dictEvents.severity,
|
|
126
|
+
cancelSignatures: dictSignatures.cancel_signatures,
|
|
127
|
+
messageSignatures: dictSignatures.message_signatures,
|
|
128
|
+
tags: dictSignatures.tags,
|
|
129
|
+
ICAO: dictICAOs.icaos,
|
|
130
|
+
enhancedEvents: [
|
|
131
|
+
{"Tornado Warning": {
|
|
132
|
+
"Tornado Emergency": { description: "tornado emergency", condition: (tornadoThreatTag: string) => tornadoThreatTag === 'OBSERVED'},
|
|
133
|
+
"PDS Tornado Warning": { description: "particularly dangerous situation", condition: (damageThreatTag: string) => damageThreatTag === 'CONSIDERABLE'},
|
|
134
|
+
"Confirmed Tornado Warning": { condition: (tornadoThreatTag: string) => tornadoThreatTag === 'OBSERVED'},
|
|
135
|
+
"Radar Indicated Tornado Warning": {condition: (tornadoThreatTag: string) => tornadoThreatTag !== 'OBSERVED'},
|
|
136
|
+
}},
|
|
137
|
+
{"Special Marine Warning": {
|
|
138
|
+
"Tornadic Special Marine Warning": {condition: (tornadoThreatTag: string) => tornadoThreatTag !== 'POSSIBLE'},
|
|
139
|
+
}},
|
|
140
|
+
{"Tornado Watch": {
|
|
141
|
+
"PDS Tornado Watch": { description: "particularly dangerous situation"}
|
|
142
|
+
}},
|
|
143
|
+
{"Flash Flood Warning": {
|
|
144
|
+
"Flash Flood Emergency": { description: "flash flood emergency", },
|
|
145
|
+
"Considerable Flash Flood Warning": { condition: (damageThreatTag: string) => damageThreatTag === 'CONSIDERABLE' },
|
|
146
|
+
}},
|
|
147
|
+
{"Severe Thunderstorm Warning": {
|
|
148
|
+
"EDS Severe Thunderstorm Warning": {description: "extremely dangerous situation"},
|
|
149
|
+
"Destructive Severe Thunderstorm Warning": {condition: (damageThreatTag: string) => damageThreatTag === 'DESTRUCTIVE'},
|
|
150
|
+
"Considerable Severe Thunderstorm Warning": {condition: (damageThreatTag: string) => damageThreatTag === 'CONSIDERABLE'},
|
|
151
|
+
}},
|
|
152
|
+
],
|
|
153
|
+
shapefiles_directory: [
|
|
154
|
+
{name: "us_counties", id: "C", link: "https://www.weather.gov/source/gis/Shapefiles/County/c_18mr25.zip"},
|
|
155
|
+
{name: "us_states_territories", id: "Z", link: "https://www.weather.gov/source/gis/Shapefiles/County/s_18mr25.zip"},
|
|
156
|
+
{name: "fire_weather_zones", id: "Z", link: "https://www.weather.gov/source/gis/Shapefiles/WSOM/fz18mr25.zip"},
|
|
157
|
+
{name: "costal_marine_zones", id: "Z", link: "https://www.weather.gov/source/gis/Shapefiles/WSOM/mz18mr25.zip"},
|
|
158
|
+
{name: "offshore_marine_zones", id: "Z", link: "https://www.weather.gov/source/gis/Shapefiles/WSOM/oz18mr25.zip"},
|
|
159
|
+
{name: "public_forecast_zones", id: "Z", link: "https://www.weather.gov/source/gis/Shapefiles/WSOM/z_18mr25.zip"},
|
|
160
|
+
{name: "county_warning_areas", id: "Z", link: "https://www.weather.gov/source/gis/Shapefiles/WSOM/w_18mr25.zip"},
|
|
161
|
+
{name: "river_forecast_boundaries", id: "Z", link: "https://www.weather.gov/source/gis/Shapefiles/Misc/rf05mr24.zip"},
|
|
162
|
+
],
|
|
163
|
+
regular_expressions: {
|
|
164
|
+
pvtec: new RegExp(`[OTEX].(NEW|CON|EXT|EXA|EXB|UPG|CAN|EXP|COR|ROU).[A-Z]{4}.[A-Z]{2}.[WAYSFON].[0-9]{4}.[0-9]{6}T[0-9]{4}Z-[0-9]{6}T[0-9]{4}Z`, "g"),
|
|
165
|
+
hvtec: new RegExp(`[a-zA-Z0-9]{4}.[A-Z0-9].[A-Z]{2}.[0-9]{6}T[0-9]{4}Z.[0-9]{6}T[0-9]{4}Z.[0-9]{6}T[0-9]{4}Z.[A-Z]{2}`, "imu"),
|
|
166
|
+
wmo: new RegExp(`[A-Z0-9]{6}\\s[A-Z]{4}\\s\\d{6}`, "imu"),
|
|
167
|
+
ugc1: new RegExp(`(\\w{2}[CZ](\\d{3}((-|>)\\s?(\\n\\n)?))+)`, "imu"),
|
|
168
|
+
ugc2: new RegExp(`(\\d{6}(-|>)\\s?(\\n\\n)?)`, "imu"),
|
|
169
|
+
ugc3: new RegExp(`(\\d{6})(?=-|$)`, "imu"),
|
|
170
|
+
dateline: new RegExp(`\\d{3,4}\\s*(AM|PM)?\\s*[A-Z]{2,4}\\s+[A-Z]{3,}\\s+[A-Z]{3,}\\s+\\d{1,2}\\s+\\d{4}`, "gim"),
|
|
171
|
+
},
|
|
172
|
+
messages: {
|
|
173
|
+
shapefile_creation: `DO NOT EXIT UNTIL THE SHAPEFILES ARE DONE COMPLETING! IF YOU CLOSE YOUR PROJECT, THE SHAPEFILES WILL NOT BE CREATED AND YOU WILL NEED TO DELETE ${settings.database} AND RESTART TO CREATE THEM AGAIN!`,
|
|
174
|
+
shapefile_creation_finished: `Shapefiles have finished completing and you can now use the parser.`,
|
|
175
|
+
not_ready: `You can not create another instance without shutting down the current one first, please make sure to call the stop() method first!`,
|
|
176
|
+
invalid_nickname: `The nickname you provided is invalid, please provide a valid nickname to continue.`,
|
|
177
|
+
eas_no_directory: `You have not set a directory for EAS audio files to be saved to, please set the 'directory' setting in the global settings to enable EAS audio generation.`,
|
|
178
|
+
reconnect_too_fast: `The client is attempting to reconnect too fast. This may be due to network instability. Reconnection attempt has been halted for safety.`,
|
|
179
|
+
dump_cache: `Found {count} cached events and will begin dumping them shortly. This may take a while depending on the number of cached events.`,
|
|
180
|
+
dump_cache_complete: `Completed dumping all cached alert files.`,
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
process.on('uncaughtException', (err: any) => {
|
|
186
|
+
if (err?.code === 'ETIMEDOUT') { return; }
|
|
187
|
+
if (err?.code === 'ECONNRESET') { return; }
|
|
188
|
+
if (err?.code === 'EHOSTUNREACH') { return; }
|
|
189
|
+
throw err;
|
|
190
|
+
})
|