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/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;
|