atmosx-nwws-parser 1.0.19 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +182 -64
  2. package/dist/cjs/index.cjs +3799 -0
  3. package/dist/esm/index.mjs +3757 -0
  4. package/package.json +49 -37
  5. package/src/bootstrap.ts +196 -0
  6. package/src/database.ts +148 -0
  7. package/src/dictionaries/awips.ts +342 -0
  8. package/src/dictionaries/events.ts +142 -0
  9. package/src/dictionaries/icao.ts +237 -0
  10. package/src/dictionaries/offshore.ts +12 -0
  11. package/src/dictionaries/signatures.ts +107 -0
  12. package/src/eas.ts +493 -0
  13. package/src/index.ts +229 -0
  14. package/src/parsers/events/api.ts +151 -0
  15. package/src/parsers/events/cap.ts +138 -0
  16. package/src/parsers/events/text.ts +106 -0
  17. package/src/parsers/events/ugc.ts +109 -0
  18. package/src/parsers/events/vtec.ts +78 -0
  19. package/src/parsers/events.ts +367 -0
  20. package/src/parsers/hvtec.ts +46 -0
  21. package/src/parsers/pvtec.ts +71 -0
  22. package/src/parsers/stanza.ts +132 -0
  23. package/src/parsers/text.ts +166 -0
  24. package/src/parsers/ugc.ts +197 -0
  25. package/src/types.ts +251 -0
  26. package/src/utils.ts +314 -0
  27. package/src/xmpp.ts +144 -0
  28. package/test.js +58 -34
  29. package/tsconfig.json +12 -5
  30. package/tsup.config.ts +14 -0
  31. package/bootstrap.ts +0 -122
  32. package/dist/bootstrap.js +0 -153
  33. package/dist/src/events.js +0 -585
  34. package/dist/src/helper.js +0 -463
  35. package/dist/src/stanza.js +0 -147
  36. package/dist/src/text-parser.js +0 -119
  37. package/dist/src/ugc.js +0 -214
  38. package/dist/src/vtec.js +0 -125
  39. package/src/events.ts +0 -394
  40. package/src/helper.ts +0 -298
  41. package/src/stanza.ts +0 -102
  42. package/src/text-parser.ts +0 -120
  43. package/src/ugc.ts +0 -122
  44. package/src/vtec.ts +0 -99
package/src/types.ts ADDED
@@ -0,0 +1,251 @@
1
+ /*
2
+ _ _ __ __
3
+ /\ | | | | (_) \ \ / /
4
+ / \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
5
+ / /\ \| __| '_ ` _ \ / _ \/ __| '_ \| '_ \ / _ \ '__| |/ __| > <
6
+ / ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
7
+ /_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
8
+ | |
9
+ |_|
10
+
11
+ Written by: k3yomi@GitHub
12
+ */
13
+
14
+ // ----------- Settings ----------- //
15
+ interface LocalEasSettings {
16
+ directory?: string;
17
+ intro_wav?: string;
18
+ }
19
+
20
+ interface LocalLocationFilteringSettings {
21
+ unit?: 'miles' | 'kilometers';
22
+ }
23
+
24
+ interface LocalAlertFilteringSettings {
25
+ events?: string[];
26
+ filtered_icao?: string[];
27
+ ignored_icao?: string[];
28
+ ugc_filter?: string[];
29
+ state_filter?: string[];
30
+ ignored_events?: string[];
31
+ check_expired?: boolean;
32
+ location?: LocalLocationFilteringSettings;
33
+ }
34
+
35
+ interface LocalGlobalSettings {
36
+ parent_events_only?: boolean;
37
+ better_event_parsing?: boolean;
38
+ shapefile_coordinates?: boolean;
39
+ shapefile_skip?: number;
40
+ eas_settings?: LocalEasSettings;
41
+ filtering?: LocalAlertFilteringSettings;
42
+ }
43
+
44
+ interface LocalClientReconnectionSettings {
45
+ enabled?: boolean;
46
+ interval?: number;
47
+ }
48
+
49
+ interface LocalClientCredentialSettings {
50
+ username?: string;
51
+ password?: string;
52
+ nickname?: string;
53
+ }
54
+
55
+ interface LocalCacheSettings {
56
+ enabled?: boolean;
57
+ max_file_size?: number;
58
+ max_db_history?: number;
59
+ directory?: string;
60
+ }
61
+
62
+ interface LocalAlertPreferenceSettings {
63
+ disable_ugc?: boolean;
64
+ disable_vtec?: boolean;
65
+ disable_text?: boolean;
66
+ cap_only?: boolean;
67
+ }
68
+
69
+ interface LocalNoaaWeatherWireServiceSettings {
70
+ reconnection_settings?: LocalClientReconnectionSettings;
71
+ credentials?: LocalClientCredentialSettings;
72
+ cache?: LocalCacheSettings;
73
+ preferences?: LocalAlertPreferenceSettings;
74
+ }
75
+
76
+ interface LocalNationalWeatherServiceSettings {
77
+ interval?: number;
78
+ endpoint?: string;
79
+ }
80
+
81
+ // --- Exports --- //
82
+ export interface ClientSettingsTypes {
83
+ database?: string;
84
+ is_wire?: boolean;
85
+ journal?: boolean;
86
+ noaa_weather_wire_service_settings?: LocalNoaaWeatherWireServiceSettings;
87
+ national_weather_service_settings?: LocalNationalWeatherServiceSettings;
88
+ global_settings?: LocalGlobalSettings;
89
+ }
90
+
91
+
92
+ // ----------- Alert / Events ----------- //
93
+
94
+
95
+ interface LocalEventHistory {
96
+ description?: string;
97
+ issued?: string;
98
+ type?: string;
99
+ }
100
+
101
+
102
+ interface LocalEventParameters {
103
+ wmo?: string;
104
+ source?: string;
105
+ max_hail_size?: string;
106
+ max_wind_gust?: string;
107
+ damage_threat?: string;
108
+ tornado_detection?: string;
109
+ flood_detection?: string;
110
+ discussion_tornado_intensity?: string;
111
+ discussion_wind_intensity?: string;
112
+ discussion_hail_intensity?: string;
113
+ WMOidentifier?: string[];
114
+ VTEC?: string;
115
+ maxHailSize?: string;
116
+ maxWindGust?: string;
117
+ thunderstormDamageThreat?: string[];
118
+ tornadoDetection?: string[];
119
+ waterspoutDetection?: string[];
120
+ floodDetection?: string[];
121
+ AWIPSidentifier?: string[];
122
+ NWSheadline?: string[];
123
+ }
124
+
125
+ interface LocalEventProperties {
126
+ parent?: string;
127
+ event?: string;
128
+ locations?: string;
129
+ issued?: string;
130
+ expires?: string;
131
+ geocode?: { UGC: string[] };
132
+ description?: string;
133
+ sender_name?: string;
134
+ sender_icao?: string;
135
+ raw?: DefaultAttributesType;
136
+ parameters?: LocalEventParameters;
137
+ messageType?: string;
138
+ sent?: string;
139
+ areaDesc?: string;
140
+ distance?: Record<string, { distance: number, unit: string}>,
141
+ }
142
+
143
+ // --- Exports --- //
144
+
145
+ export interface StanzaAttributesType {
146
+ xmlns?: string;
147
+ id?: string;
148
+ issue?: string;
149
+ ttaaii?: string;
150
+ cccc?: string;
151
+ awipsid?: string;
152
+ }
153
+ export interface DefaultAttributesType {
154
+ attributes?: {
155
+ xmlns?: string;
156
+ id?: string;
157
+ issue?: string;
158
+ ttaaii?: string;
159
+ cccc?: string;
160
+ awipsid?: string;
161
+ }
162
+ getAwip?: Record<string, string>;
163
+ awipsType?: Record<string, string>;
164
+ isCap?: boolean;
165
+ raw?: boolean;
166
+ }
167
+
168
+ export interface StanzaCompiled {
169
+ message?: string;
170
+ attributes?: DefaultAttributesType;
171
+ isCap?: boolean;
172
+ isApi?: boolean;
173
+ isCapDescription?: boolean;
174
+ isPVtec?: boolean;
175
+ isUGC?: boolean;
176
+ getAwip?: Record<string, string>;
177
+ awipsType?: Record<string, string>;
178
+ awipsPrefix?: string;
179
+ ignore?: boolean;
180
+ awipsid?: string;
181
+ }
182
+
183
+ export interface PVtecEntry {
184
+ raw?: string;
185
+ type?: string;
186
+ tracking?: string;
187
+ event?: string;
188
+ status?: string;
189
+ wmo?: string;
190
+ expires?: Date | string;
191
+ }
192
+
193
+ export interface UGCEntry {
194
+ zones?: string[];
195
+ locations?: string[];
196
+ expiry?: Date | string;
197
+ polygon?: [number, number][];
198
+ }
199
+
200
+ export interface HVtecEntry {
201
+ severity?: string,
202
+ cause?: string,
203
+ record?: string,
204
+ raw?: string,
205
+ }
206
+
207
+ export interface geometry {
208
+ type?: string;
209
+ coordinates?: [number, number][];
210
+ }
211
+
212
+
213
+ export interface EventCompiled {
214
+ performance?: number;
215
+ tracking?: string;
216
+ header?: string;
217
+ pvtec?: string;
218
+ hvtec?: string;
219
+ history?: LocalEventHistory[];
220
+ properties?: LocalEventProperties
221
+ geometry?: { type?: string; coordinates?: [number, number][] } | null;
222
+ }
223
+
224
+ export type EventProperties = LocalEventProperties;
225
+ export type StanzaAttributes = DefaultAttributesType;
226
+
227
+
228
+ // ----------- Generic ----------- //
229
+
230
+ // --- Exports --- //
231
+ export type Coordinates = {
232
+ lon: number;
233
+ lat: number;
234
+ }
235
+
236
+ export type HTTPSettings = {
237
+ timeout?: number;
238
+ headers?: Record<string, string>;
239
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
240
+ body?: string;
241
+ }
242
+
243
+ export interface EnhancedEventCondition {
244
+ description?: string;
245
+ condition?: (value: string) => boolean;
246
+ }
247
+
248
+ export interface GenericHTTPResponse {
249
+ error?: boolean,
250
+ message?: { features: Record<string, any>[] } | string,
251
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,314 @@
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 StanzaParser from './parsers/stanza';
18
+ import EventParser from './parsers/events';
19
+ import Xmpp from './xmpp';
20
+
21
+ export class Utils {
22
+
23
+ /**
24
+ * @function sleep
25
+ * @description
26
+ * Pauses execution for a specified number of milliseconds.
27
+ *
28
+ * @static
29
+ * @async
30
+ * @param {number} ms
31
+ * @returns {Promise<void>}
32
+ */
33
+ public static async sleep(ms: number) {
34
+ return new Promise(resolve => setTimeout(resolve, ms));
35
+ }
36
+
37
+ /**
38
+ * @function warn
39
+ * @description
40
+ * Emits a log event and prints a warning to the console. Throttles repeated
41
+ * warnings within a short interval unless `force` is `true`.
42
+ *
43
+ * @static
44
+ * @param {string} message
45
+ * @param {boolean} [force=false]
46
+ */
47
+ public static warn(message: string, force: boolean = false) {
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 NWWS messages from disk, validates them, and passes them
59
+ * to the event parser. Honors CAP preferences and ignores empty or
60
+ * incompatible files.
61
+ *
62
+ * @static
63
+ * @async
64
+ */
65
+ public static async loadCollectionCache() {
66
+ try {
67
+ const settings = loader.settings as types.ClientSettingsTypes;
68
+ if (settings.noaa_weather_wire_service_settings.cache.enabled && settings.noaa_weather_wire_service_settings.cache.directory) {
69
+ if (!loader.packages.fs.existsSync(settings.noaa_weather_wire_service_settings.cache.directory)) return;
70
+ const cacheDir = settings.noaa_weather_wire_service_settings.cache.directory;
71
+ const getAllFiles = loader.packages.fs.readdirSync(cacheDir).filter((file: string) => file.endsWith('.bin') && file.startsWith('cache-'));
72
+ this.warn(loader.definitions.messages.dump_cache.replace(`{count}`, getAllFiles.length.toString()), true);
73
+ for (const file of getAllFiles) {
74
+ const filepath = loader.packages.path.join(cacheDir, file);
75
+ const readFile = loader.packages.fs.readFileSync(filepath, { encoding: 'utf-8' });
76
+ const readSize = loader.packages.fs.statSync(filepath).size;
77
+ if (readSize == 0) { continue; }
78
+ const isCap = readFile.includes(`<?xml`);
79
+ if (isCap && !settings.noaa_weather_wire_service_settings.preferences.cap_only) continue;
80
+ if (!isCap && settings.noaa_weather_wire_service_settings.preferences.cap_only) continue;
81
+ const validate = StanzaParser.validate(readFile, { isCap: isCap, raw: true });
82
+ await EventParser.eventHandler(validate);
83
+ }
84
+ this.warn(loader.definitions.messages.dump_cache_complete, true);
85
+ }
86
+ } catch (error: any) {
87
+ Utils.warn(`Failed to load cache: ${error.stack}`);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * @function loadGeoJsonData
93
+ * @description
94
+ * Fetches GeoJSON data from the National Weather Service endpoint and
95
+ * passes it to the event parser for processing.
96
+ *
97
+ * @static
98
+ * @async
99
+ */
100
+ public static async loadGeoJsonData() {
101
+ try {
102
+ const settings = loader.settings as types.ClientSettingsTypes;
103
+ const response = await this.createHttpRequest<types.GenericHTTPResponse >(
104
+ settings.national_weather_service_settings.endpoint
105
+ );
106
+ if (response.error) return;
107
+ EventParser.eventHandler({
108
+ message: JSON.stringify(response.message),
109
+ attributes: {},
110
+ isCap: true,
111
+ isApi: true,
112
+ isPVtec: false,
113
+ isUGC: false,
114
+ isCapDescription: false,
115
+ awipsType: { type: 'api', prefix: 'AP' },
116
+ ignore: false
117
+ });
118
+ } catch (error: unknown) {
119
+ const msg = error instanceof Error ? error.message : String(error);
120
+ Utils.warn(`Failed to load National Weather Service GeoJSON Data: ${msg}`);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * @function createHttpRequest
126
+ * @description
127
+ * Performs an HTTP GET request with default headers and timeout, returning
128
+ * either the response data or an error message.
129
+ *
130
+ * @static
131
+ * @template T
132
+ * @param {string} url
133
+ * @param {types.HTTPSettings} [options]
134
+ * @returns {Promise<{ error: boolean; message: T | string }>}
135
+ */
136
+ public static async createHttpRequest<T = unknown>(url: string, options?: types.HTTPSettings): Promise<{ error: boolean; message: T | string }> {
137
+ const defaultOptions = {
138
+ timeout: 10000,
139
+ headers: {
140
+ "User-Agent": "AtmosphericX",
141
+ "Accept": "application/geo+json, text/plain, */*; q=0.9",
142
+ "Accept-Language": "en-US,en;q=0.9"
143
+ }
144
+ };
145
+ const requestOptions = {
146
+ ...defaultOptions,
147
+ ...options,
148
+ headers: { ...defaultOptions.headers, ...(options?.headers ?? {}) }
149
+ };
150
+ try {
151
+ const resp = await loader.packages.axios.get<T>(url, {
152
+ headers: requestOptions.headers,
153
+ timeout: requestOptions.timeout,
154
+ maxRedirects: 0,
155
+ validateStatus: (status) => status === 200 || status === 500
156
+ });
157
+ return { error: false, message: resp.data };
158
+ } catch (err: unknown) {
159
+ const msg = err instanceof Error ? err.message : String(err);
160
+ return { error: true, message: msg };
161
+ }
162
+ }
163
+
164
+ /**
165
+ * @function garbageCollectionCache
166
+ * @description
167
+ * Deletes cache files exceeding the specified size limit to free disk space.
168
+ * Recursively traverses the cache directory and removes files larger than
169
+ * the given maximum.
170
+ *
171
+ * @static
172
+ * @param {number} maxFileMegabytes
173
+ */
174
+ public static garbageCollectionCache(maxFileMegabytes: number) {
175
+ try {
176
+ const settings = loader.settings as types.ClientSettingsTypes;
177
+ const cacheDir = settings.noaa_weather_wire_service_settings.cache.directory;
178
+ if (!cacheDir) return;
179
+ const { fs, path } = loader.packages;
180
+ if (!fs.existsSync(cacheDir)) return;
181
+ const maxBytes = maxFileMegabytes * 1024 * 1024;
182
+ const stackDirs: string[] = [cacheDir];
183
+ const files: { file: string; size: number }[] = [];
184
+ while (stackDirs.length) {
185
+ const currentDir = stackDirs.pop()!;
186
+ fs.readdirSync(currentDir).forEach(file => {
187
+ const fullPath = path.join(currentDir, file);
188
+ const stat = fs.statSync(fullPath);
189
+ if (stat.isDirectory()) stackDirs.push(fullPath);
190
+ else files.push({ file: fullPath, size: stat.size });
191
+ });
192
+ }
193
+ files.forEach(f => {
194
+ if (f.size > maxBytes) fs.unlinkSync(f.file);
195
+ });
196
+ } catch (error: unknown) {
197
+ const msg = error instanceof Error ? error.message : String(error);
198
+ Utils.warn(`Failed to perform garbage collection: ${msg}`);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * @function handleCronJob
204
+ * @description
205
+ * Performs scheduled tasks for NWWS XMPP session maintenance or GeoJSON data
206
+ * updates depending on the job type.
207
+ *
208
+ * @static
209
+ * @param {boolean} isWire
210
+ */
211
+ public static handleCronJob(isWire: boolean) {
212
+ try {
213
+ const settings = loader.settings as types.ClientSettingsTypes;
214
+ const cache = settings.noaa_weather_wire_service_settings.cache;
215
+ const reconnections = settings.noaa_weather_wire_service_settings.reconnection_settings;
216
+ if (isWire) {
217
+ if (cache.enabled) {
218
+ void this.garbageCollectionCache(cache.max_file_size);
219
+ }
220
+ if (reconnections.enabled) {
221
+ void Xmpp.isSessionReconnectionEligible(reconnections.interval);
222
+ }
223
+ } else {
224
+ void this.loadGeoJsonData();
225
+ }
226
+ } catch (error: unknown) {
227
+ const msg = error instanceof Error ? error.message : String(error);
228
+ Utils.warn(`Failed to perform scheduled tasks (${isWire ? 'NWWS' : 'GeoJSON'}): ${msg}`);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * @function mergeClientSettings
234
+ * @description
235
+ * Recursively merges a ClientSettings object into a target object,
236
+ * preserving nested structures and overriding existing values.
237
+ *
238
+ * @static
239
+ * @param {Record<string, unknown>} target
240
+ * @param {types.ClientSettingsTypes} settings
241
+ * @returns {Record<string, unknown>}
242
+ */
243
+ public static mergeClientSettings(target: Record<string, unknown>, settings: types.ClientSettingsTypes): Record<string, unknown> {
244
+ for (const key in settings) {
245
+ if (!Object.prototype.hasOwnProperty.call(settings, key)) continue;
246
+ const value = settings[key];
247
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
248
+ if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
249
+ target[key] = {};
250
+ }
251
+ this.mergeClientSettings(target[key] as Record<string, unknown>, value as types.ClientSettingsTypes);
252
+ } else {
253
+ target[key] = value;
254
+ }
255
+ }
256
+ return target;
257
+ }
258
+
259
+ /**
260
+ * @function calculateDistance
261
+ * @description
262
+ * Calculates the great-circle distance between two geographic coordinates
263
+ * using the haversine formula.
264
+ *
265
+ * @static
266
+ * @param {types.Coordinates} coord1
267
+ * @param {types.Coordinates} coord2
268
+ * @param {'miles' | 'kilometers'} [unit='miles']
269
+ * @returns {number}
270
+ */
271
+ public static calculateDistance(coord1: types.Coordinates, coord2: types.Coordinates, unit: 'miles' | 'kilometers' = 'miles'): number {
272
+ if (!coord1 || !coord2) return 0;
273
+ const { lat: lat1, lon: lon1 } = coord1;
274
+ const { lat: lat2, lon: lon2 } = coord2;
275
+ if ([lat1, lon1, lat2, lon2].some(v => typeof v !== 'number')) return 0;
276
+ const toRad = (deg: number) => deg * Math.PI / 180;
277
+ const R = unit === 'miles' ? 3958.8 : 6371;
278
+ const dLat = toRad(lat2 - lat1);
279
+ const dLon = toRad(lon2 - lon1);
280
+ const a =
281
+ Math.sin(dLat / 2) ** 2 +
282
+ Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
283
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
284
+ return Math.round(R * c * 100) / 100;
285
+ }
286
+
287
+ /**
288
+ * @function isReadyToProcess
289
+ * @description
290
+ * Determines whether processing can continue based on the current
291
+ * tracked locations and filter state. Emits limited warnings if no
292
+ * locations are available.
293
+ *
294
+ * @static
295
+ * @returns {boolean}
296
+ */
297
+ public static isReadyToProcess(): boolean {
298
+ const totalTracks = Object.keys(loader.cache.currentLocations).length;
299
+ if (totalTracks > 0) {
300
+ loader.cache.totalLocationWarns = 0;
301
+ return true;
302
+ }
303
+ if (totalTracks == 0) { return true };
304
+ if (loader.cache.totalLocationWarns < 3) {
305
+ Utils.warn(loader.definitions.messages.no_current_locations);
306
+ loader.cache.totalLocationWarns++;
307
+ return false;
308
+ }
309
+ Utils.warn(loader.definitions.messages.disabled_location_warning, true);
310
+ return true;
311
+ }
312
+ }
313
+
314
+ export default Utils;