atmosx-nwws-parser 1.0.20311 → 1.0.20314
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/dist/cjs/index.cjs +1965 -1349
- package/dist/esm/index.mjs +1963 -1347
- package/package.json +2 -2
- package/src/bootstrap.ts +59 -41
- package/src/database.ts +97 -48
- package/src/dictionaries/signatures.ts +43 -45
- package/src/eas.ts +144 -79
- package/src/index.ts +141 -82
- package/src/parsers/events.ts +195 -134
- package/src/parsers/stanza.ts +64 -35
- package/src/parsers/text.ts +43 -44
- package/src/parsers/types/api.ts +42 -8
- package/src/parsers/types/cap.ts +92 -62
- package/src/parsers/types/text.ts +71 -25
- package/src/parsers/types/ugc.ts +55 -37
- package/src/parsers/types/vtec.ts +35 -25
- package/src/parsers/ugc.ts +102 -58
- package/src/parsers/vtec.ts +27 -21
- package/src/types.ts +226 -171
- package/src/utils.ts +198 -99
- package/src/xmpp.ts +85 -64
package/dist/cjs/index.cjs
CHANGED
|
@@ -8,6 +8,7 @@ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
|
8
8
|
var __getProtoOf = Object.getPrototypeOf;
|
|
9
9
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
10
|
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
11
|
+
var __pow = Math.pow;
|
|
11
12
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
12
13
|
var __spreadValues = (a, b) => {
|
|
13
14
|
for (var prop in b || (b = {}))
|
|
@@ -85,9 +86,9 @@ __export(index_exports, {
|
|
|
85
86
|
StanzaParser: () => stanza_default,
|
|
86
87
|
TextParser: () => text_default,
|
|
87
88
|
UGCParser: () => ugc_default,
|
|
89
|
+
Utils: () => utils_default,
|
|
88
90
|
VtecParser: () => vtec_default,
|
|
89
|
-
default: () => index_default
|
|
90
|
-
types: () => types_exports
|
|
91
|
+
default: () => index_default
|
|
91
92
|
});
|
|
92
93
|
module.exports = __toCommonJS(index_exports);
|
|
93
94
|
|
|
@@ -98,12 +99,13 @@ var events = __toESM(require("events"));
|
|
|
98
99
|
var xmpp = __toESM(require("@xmpp/client"));
|
|
99
100
|
var shapefile = __toESM(require("shapefile"));
|
|
100
101
|
var xml2js = __toESM(require("xml2js"));
|
|
101
|
-
var
|
|
102
|
+
var jobs = __toESM(require("croner"));
|
|
102
103
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
103
104
|
var import_axios = __toESM(require("axios"));
|
|
104
105
|
var import_crypto = __toESM(require("crypto"));
|
|
105
106
|
var import_os = __toESM(require("os"));
|
|
106
107
|
var import_say = __toESM(require("say"));
|
|
108
|
+
var import_child_process = __toESM(require("child_process"));
|
|
107
109
|
|
|
108
110
|
// src/dictionaries/events.ts
|
|
109
111
|
var EVENTS = {
|
|
@@ -621,53 +623,51 @@ var CANCEL_SIGNATURES = [
|
|
|
621
623
|
"has weakened below severe"
|
|
622
624
|
];
|
|
623
625
|
var MESSAGE_SIGNATURES = [
|
|
624
|
-
{ regex: /\r?\n/g, replacement: " " },
|
|
625
|
-
{ regex: /\s+/g, replacement: " " },
|
|
626
626
|
{ regex: /\*/g, replacement: "." },
|
|
627
627
|
{ regex: /\bUTC\b/g, replacement: "Coordinated Universal Time" },
|
|
628
628
|
{ regex: /\bGMT\b/g, replacement: "Greenwich Mean Time" },
|
|
629
|
-
{ regex: /\bEST\b/g, replacement: "Eastern Standard Time" },
|
|
630
|
-
{ regex: /\bEDT\b/g, replacement: "Eastern Daylight Time" },
|
|
631
|
-
{ regex: /\bCST\b/g, replacement: "Central Standard Time" },
|
|
632
|
-
{ regex: /\bCDT\b/g, replacement: "Central Daylight Time" },
|
|
633
|
-
{ regex: /\bMST\b/g, replacement: "Mountain Standard Time" },
|
|
634
|
-
{ regex: /\bMDT\b/g, replacement: "Mountain Daylight Time" },
|
|
635
|
-
{ regex: /\bPST\b/g, replacement: "Pacific Standard Time" },
|
|
636
|
-
{ regex: /\bPDT\b/g, replacement: "Pacific Daylight Time" },
|
|
637
|
-
{ regex: /\bAKST\b/g, replacement: "Alaska Standard Time" },
|
|
638
|
-
{ regex: /\bAKDT\b/g, replacement: "Alaska Daylight Time" },
|
|
639
|
-
{ regex: /\bHST\b/g, replacement: "Hawaii Standard Time" },
|
|
640
|
-
{ regex: /\bHDT\b/g, replacement: "Hawaii Daylight Time" },
|
|
641
|
-
{ regex: /\bmph\b/g, replacement: "miles per hour" },
|
|
642
|
-
{ regex: /\bkm\/h\b/g, replacement: "kilometers per hour" },
|
|
643
|
-
{ regex: /\bkmh\b/g, replacement: "kilometers per hour" },
|
|
644
|
-
{ regex: /\bkt\b/g, replacement: "knots" },
|
|
645
|
-
{ regex: /\bNE\b/g, replacement: "northeast" },
|
|
646
|
-
{ regex: /\bNW\b/g, replacement: "northwest" },
|
|
647
|
-
{ regex: /\bSE\b/g, replacement: "southeast" },
|
|
648
|
-
{ regex: /\bSW\b/g, replacement: "southwest" },
|
|
649
|
-
{ regex: /\bNM\b/g, replacement: "nautical miles" },
|
|
650
|
-
{ regex: /\bdeg\b/g, replacement: "degrees" },
|
|
651
|
-
{ regex: /\btstm\b/g, replacement: "thunderstorm" },
|
|
652
|
-
{ regex: /\bmm\b/g, replacement: "millimeters" },
|
|
653
|
-
{ regex: /\bcm\b/g, replacement: "centimeters" },
|
|
654
|
-
{ regex: /\bin
|
|
655
|
-
{ regex: /\bft\b/g, replacement: "feet" },
|
|
656
|
-
{ regex: /\bmi\b/g, replacement: "miles" },
|
|
657
|
-
{ regex: /\bhr\b/g, replacement: "hour" },
|
|
658
|
-
{ regex: /\bhourly\b/g, replacement: "per hour" },
|
|
659
|
-
{ regex: /\bkg\b/g, replacement: "kilograms" },
|
|
660
|
-
{ regex: /\bg\/kg\b/g, replacement: "grams per kilogram" },
|
|
661
|
-
{ regex: /\bmb\b/g, replacement: "millibars" },
|
|
662
|
-
{ regex: /\bhPa\b/g, replacement: "hectopascals" },
|
|
663
|
-
{ regex: /\bPa\b/g, replacement: "pascals" },
|
|
664
|
-
{ regex: /\bKPa\b/g, replacement: "kilopascals" },
|
|
665
|
-
{ regex: /\bC\/hr\b/g, replacement: "degrees Celsius per hour" },
|
|
666
|
-
{ regex: /\bF\/hr\b/g, replacement: "degrees Fahrenheit per hour" },
|
|
667
|
-
{ regex: /\bC\/min\b/g, replacement: "degrees Celsius per minute" },
|
|
668
|
-
{ regex: /\bF\/min\b/g, replacement: "degrees Fahrenheit per minute" },
|
|
669
|
-
{ regex: /\bC\b/g, replacement: "degrees Celsius" },
|
|
670
|
-
{ regex: /\bF\b/g, replacement: "degrees Fahrenheit" }
|
|
629
|
+
{ regex: /\bEST\b(?!\w)/g, replacement: "Eastern Standard Time" },
|
|
630
|
+
{ regex: /\bEDT\b(?!\w)/g, replacement: "Eastern Daylight Time" },
|
|
631
|
+
{ regex: /\bCST\b(?!\w)/g, replacement: "Central Standard Time" },
|
|
632
|
+
{ regex: /\bCDT\b(?!\w)/g, replacement: "Central Daylight Time" },
|
|
633
|
+
{ regex: /\bMST\b(?!\w)/g, replacement: "Mountain Standard Time" },
|
|
634
|
+
{ regex: /\bMDT\b(?!\w)/g, replacement: "Mountain Daylight Time" },
|
|
635
|
+
{ regex: /\bPST\b(?!\w)/g, replacement: "Pacific Standard Time" },
|
|
636
|
+
{ regex: /\bPDT\b(?!\w)/g, replacement: "Pacific Daylight Time" },
|
|
637
|
+
{ regex: /\bAKST\b(?!\w)/g, replacement: "Alaska Standard Time" },
|
|
638
|
+
{ regex: /\bAKDT\b(?!\w)/g, replacement: "Alaska Daylight Time" },
|
|
639
|
+
{ regex: /\bHST\b(?!\w)/g, replacement: "Hawaii Standard Time" },
|
|
640
|
+
{ regex: /\bHDT\b(?!\w)/g, replacement: "Hawaii Daylight Time" },
|
|
641
|
+
{ regex: /\bmph\b(?!\w)/g, replacement: "miles per hour" },
|
|
642
|
+
{ regex: /\bkm\/h\b(?!\w)/g, replacement: "kilometers per hour" },
|
|
643
|
+
{ regex: /\bkmh\b(?!\w)/g, replacement: "kilometers per hour" },
|
|
644
|
+
{ regex: /\bkt\b(?!\w)/g, replacement: "knots" },
|
|
645
|
+
{ regex: /\bNE\b(?!\w)/g, replacement: "northeast" },
|
|
646
|
+
{ regex: /\bNW\b(?!\w)/g, replacement: "northwest" },
|
|
647
|
+
{ regex: /\bSE\b(?!\w)/g, replacement: "southeast" },
|
|
648
|
+
{ regex: /\bSW\b(?!\w)/g, replacement: "southwest" },
|
|
649
|
+
{ regex: /\bNM\b(?!\w)/g, replacement: "nautical miles" },
|
|
650
|
+
{ regex: /\bdeg\b(?!\w)/g, replacement: "degrees" },
|
|
651
|
+
{ regex: /\btstm\b(?!\w)/g, replacement: "thunderstorm" },
|
|
652
|
+
{ regex: /\bmm\b(?!\w)/g, replacement: "millimeters" },
|
|
653
|
+
{ regex: /\bcm\b(?!\w)/g, replacement: "centimeters" },
|
|
654
|
+
{ regex: /\bin.\b(?!\w)/g, replacement: "inches" },
|
|
655
|
+
{ regex: /\bft\b(?!\w)/g, replacement: "feet" },
|
|
656
|
+
{ regex: /\bmi\b(?!\w)/g, replacement: "miles" },
|
|
657
|
+
{ regex: /\bhr\b(?!\w)/g, replacement: "hour" },
|
|
658
|
+
{ regex: /\bhourly\b(?!\w)/g, replacement: "per hour" },
|
|
659
|
+
{ regex: /\bkg\b(?!\w)/g, replacement: "kilograms" },
|
|
660
|
+
{ regex: /\bg\/kg\b(?!\w)/g, replacement: "grams per kilogram" },
|
|
661
|
+
{ regex: /\bmb\b(?!\w)/g, replacement: "millibars" },
|
|
662
|
+
{ regex: /\bhPa\b(?!\w)/g, replacement: "hectopascals" },
|
|
663
|
+
{ regex: /\bPa\b(?!\w)/g, replacement: "pascals" },
|
|
664
|
+
{ regex: /\bKPa\b(?!\w)/g, replacement: "kilopascals" },
|
|
665
|
+
{ regex: /\bC\/hr\b(?!\w)/g, replacement: "degrees Celsius per hour" },
|
|
666
|
+
{ regex: /\bF\/hr\b(?!\w)/g, replacement: "degrees Fahrenheit per hour" },
|
|
667
|
+
{ regex: /\bC\/min\b(?!\w)/g, replacement: "degrees Celsius per minute" },
|
|
668
|
+
{ regex: /\bF\/min\b(?!\w)/g, replacement: "degrees Fahrenheit per minute" },
|
|
669
|
+
{ regex: /\bC\b(?!\w)/g, replacement: "degrees Celsius" },
|
|
670
|
+
{ regex: /\bF\b(?!\w)/g, replacement: "degrees Fahrenheit" }
|
|
671
671
|
];
|
|
672
672
|
|
|
673
673
|
// src/dictionaries/icao.ts
|
|
@@ -918,11 +918,12 @@ var packages = {
|
|
|
918
918
|
shapefile,
|
|
919
919
|
xml2js,
|
|
920
920
|
sqlite3: import_better_sqlite3.default,
|
|
921
|
-
|
|
921
|
+
jobs,
|
|
922
922
|
axios: import_axios.default,
|
|
923
923
|
crypto: import_crypto.default,
|
|
924
924
|
os: import_os.default,
|
|
925
|
-
say: import_say.default
|
|
925
|
+
say: import_say.default,
|
|
926
|
+
child: import_child_process.default
|
|
926
927
|
};
|
|
927
928
|
var cache = {
|
|
928
929
|
isReady: true,
|
|
@@ -934,55 +935,64 @@ var cache = {
|
|
|
934
935
|
session: null,
|
|
935
936
|
lastConnect: null,
|
|
936
937
|
db: null,
|
|
938
|
+
lastWarn: null,
|
|
939
|
+
totalLocationWarns: 0,
|
|
937
940
|
events: new events.EventEmitter(),
|
|
938
941
|
isProcessingAudioQueue: false,
|
|
939
|
-
audioQueue: []
|
|
942
|
+
audioQueue: [],
|
|
943
|
+
currentLocations: {}
|
|
940
944
|
};
|
|
941
945
|
var settings = {
|
|
942
946
|
database: path.join(process.cwd(), "shapefiles.db"),
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
947
|
+
is_wire: true,
|
|
948
|
+
journal: true,
|
|
949
|
+
noaa_weather_wire_service_settings: {
|
|
950
|
+
reconnection_settings: {
|
|
951
|
+
enabled: true,
|
|
952
|
+
interval: 60
|
|
949
953
|
},
|
|
950
|
-
|
|
954
|
+
credentials: {
|
|
951
955
|
username: null,
|
|
952
956
|
password: null,
|
|
953
957
|
nickname: "AtmosphericX Standalone Parser"
|
|
954
958
|
},
|
|
955
959
|
cache: {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
960
|
+
enabled: false,
|
|
961
|
+
max_file_size: 5,
|
|
962
|
+
max_db_history: 5e3,
|
|
959
963
|
directory: null
|
|
960
964
|
},
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
965
|
+
preferences: {
|
|
966
|
+
cap_only: false,
|
|
967
|
+
shapefile_coordinates: false
|
|
964
968
|
}
|
|
965
969
|
},
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
endpoint:
|
|
970
|
+
national_weather_service_settings: {
|
|
971
|
+
interval: 15,
|
|
972
|
+
endpoint: `https://api.weather.gov/alerts/active`
|
|
969
973
|
},
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
974
|
+
global_settings: {
|
|
975
|
+
parent_events_only: true,
|
|
976
|
+
better_event_parsing: true,
|
|
977
|
+
filtering: {
|
|
978
|
+
events: [],
|
|
979
|
+
filtered_icoa: [],
|
|
980
|
+
ignored_icoa: [`KWNS`],
|
|
981
|
+
ignored_events: [`Xx`, `Test Message`],
|
|
982
|
+
ugc_filter: [],
|
|
983
|
+
state_filter: [],
|
|
984
|
+
check_expired: true,
|
|
985
|
+
ignore_text_products: true,
|
|
986
|
+
location: {
|
|
987
|
+
max_distance: 100,
|
|
988
|
+
unit: `miles`,
|
|
989
|
+
filter: false
|
|
990
|
+
}
|
|
981
991
|
},
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
992
|
+
eas_settings: {
|
|
993
|
+
directory: null,
|
|
994
|
+
intro_wav: null,
|
|
995
|
+
festival_tts_voice: `kal_diphone`
|
|
986
996
|
}
|
|
987
997
|
}
|
|
988
998
|
};
|
|
@@ -1040,31 +1050,47 @@ var definitions = {
|
|
|
1040
1050
|
{ id: `Z`, file: `Marine` }
|
|
1041
1051
|
],
|
|
1042
1052
|
messages: {
|
|
1043
|
-
shapefile_creation: `
|
|
1053
|
+
shapefile_creation: `DO NOT CLOSE THIS PROJECT UNTIL THE SHAPEFILES ARE DONE COMPLETING!
|
|
1044
1054
|
THIS COULD TAKE A WHILE DEPENDING ON THE SPEED OF YOUR STORAGE!!
|
|
1045
1055
|
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!`,
|
|
1046
|
-
shapefile_creation_finished: `
|
|
1047
|
-
not_ready:
|
|
1048
|
-
invalid_nickname:
|
|
1049
|
-
eas_no_directory:
|
|
1056
|
+
shapefile_creation_finished: `SHAPEFILES HAVE BEEN SUCCESSFULLY CREATED AND THE DATABASE IS READY FOR USE!`,
|
|
1057
|
+
not_ready: `You can NOT create another instance without shutting down the current one first, please make sure to call the stop() method first!`,
|
|
1058
|
+
invalid_nickname: `The nickname you provided is invalid, please provide a valid nickname to continue.`,
|
|
1059
|
+
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.`,
|
|
1060
|
+
invalid_coordinates: `The coordinates you provided are invalid, please provide valid latitude and longitude values. Attempted: {lat}, {lon}.`,
|
|
1061
|
+
no_current_locations: `No current location has been set, operations will be haulted until a location is set or location filtering is disabled.`,
|
|
1062
|
+
disabled_location_warning: `Exceeded maximum warnings for invalid or missing lat/lon coordinates. Location filtering has been ignored until you set valid coordinates or disable location filtering.`,
|
|
1063
|
+
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.`,
|
|
1064
|
+
dump_cache: `Found {count} cached alert files and will begin dumping them shortly...`,
|
|
1065
|
+
dump_cache_complete: `Completed dumping all cached alert files.`,
|
|
1066
|
+
eas_missing_festival: `Festival TTS engine is not installed or not found in PATH. Please install Festival to enable EAS audio generation on Linux and macOS systems.`
|
|
1050
1067
|
}
|
|
1051
1068
|
};
|
|
1052
1069
|
|
|
1053
|
-
// src/types.ts
|
|
1054
|
-
var types_exports = {};
|
|
1055
|
-
|
|
1056
1070
|
// src/parsers/stanza.ts
|
|
1057
1071
|
var StanzaParser = class {
|
|
1058
1072
|
/**
|
|
1059
|
-
* validate
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1073
|
+
* @function validate
|
|
1074
|
+
* @description
|
|
1075
|
+
* Validates and parses a stanza message, extracting its attributes and metadata.
|
|
1076
|
+
* Handles both raw message strings (for debug/testing) and actual stanza objects.
|
|
1077
|
+
* Determines whether the message is a CAP alert, contains VTEC codes, or contains UGCs,
|
|
1078
|
+
* and identifies the AWIPS product type and prefix.
|
|
1062
1079
|
*
|
|
1063
|
-
* @public
|
|
1064
1080
|
* @static
|
|
1065
|
-
* @param {
|
|
1066
|
-
* @param {
|
|
1067
|
-
* @returns {{
|
|
1081
|
+
* @param {any} stanza
|
|
1082
|
+
* @param {boolean | types.StanzaAttributes} [isDebug=false]
|
|
1083
|
+
* @returns {{
|
|
1084
|
+
* message: string;
|
|
1085
|
+
* attributes: types.StanzaAttributes;
|
|
1086
|
+
* isCap: boolean,
|
|
1087
|
+
* isVtec: boolean;
|
|
1088
|
+
* isCapDescription: boolean;
|
|
1089
|
+
* awipsType: Record<string, string>;
|
|
1090
|
+
* isApi: boolean;
|
|
1091
|
+
* ignore: boolean;
|
|
1092
|
+
* isUGC?: boolean;
|
|
1093
|
+
* }}
|
|
1068
1094
|
*/
|
|
1069
1095
|
static validate(stanza, isDebug = false) {
|
|
1070
1096
|
var _a;
|
|
@@ -1090,67 +1116,71 @@ var StanzaParser = class {
|
|
|
1090
1116
|
const isVtec = message.match(definitions.expressions.vtec) != null;
|
|
1091
1117
|
const isUGC = message.match(definitions.expressions.ugc1) != null;
|
|
1092
1118
|
const awipsType = this.getType(attributes);
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
return { message, attributes, isCap, isApi, isVtec, isUGC, isCapDescription, awipsType, ignore: false };
|
|
1119
|
+
this.cache(message, { attributes, isCap, isVtec, awipsType });
|
|
1120
|
+
return { message, attributes, isCap, isVtec, isUGC, isCapDescription, awipsType, isApi: false, ignore: false };
|
|
1096
1121
|
}
|
|
1097
1122
|
}
|
|
1098
1123
|
}
|
|
1099
1124
|
return { message: null, attributes: null, isApi: null, isCap: null, isVtec: null, isUGC: null, isCapDescription: null, awipsType: null, ignore: true };
|
|
1100
1125
|
}
|
|
1101
1126
|
/**
|
|
1102
|
-
* getType
|
|
1103
|
-
*
|
|
1127
|
+
* @function getType
|
|
1128
|
+
* @description
|
|
1129
|
+
* Determines the AWIPS product type and prefix from a stanza's attributes.
|
|
1130
|
+
* Returns a default type of 'XX' if the attributes are missing or the AWIPS ID
|
|
1131
|
+
* does not match any known definitions.
|
|
1104
1132
|
*
|
|
1105
1133
|
* @private
|
|
1106
1134
|
* @static
|
|
1107
|
-
* @param {unknown} attributes
|
|
1108
|
-
* @returns {
|
|
1135
|
+
* @param {unknown} attributes
|
|
1136
|
+
* @returns {Record<string, string>}
|
|
1109
1137
|
*/
|
|
1110
1138
|
static getType(attributes) {
|
|
1111
1139
|
const attrs = attributes;
|
|
1112
|
-
if (!attrs
|
|
1113
|
-
|
|
1140
|
+
if (!(attrs == null ? void 0 : attrs.awipsid)) return { type: "XX", prefix: "XX" };
|
|
1141
|
+
const awipsDefs = definitions.awips;
|
|
1142
|
+
for (const [prefix, type] of Object.entries(awipsDefs)) {
|
|
1114
1143
|
if (attrs.awipsid.startsWith(prefix)) {
|
|
1115
1144
|
return { type, prefix };
|
|
1116
1145
|
}
|
|
1117
1146
|
}
|
|
1118
|
-
return { type:
|
|
1147
|
+
return { type: "XX", prefix: "XX" };
|
|
1119
1148
|
}
|
|
1120
1149
|
/**
|
|
1121
|
-
*
|
|
1150
|
+
* @function cache
|
|
1151
|
+
* @description
|
|
1152
|
+
* Saves a compiled stanza message to the local cache directory.
|
|
1153
|
+
* Ensures the message contains "STANZA ATTRIBUTES..." metadata and timestamps,
|
|
1154
|
+
* and appends the formatted entry to both a category-specific file and a general cache file.
|
|
1122
1155
|
*
|
|
1123
1156
|
* @private
|
|
1124
1157
|
* @static
|
|
1125
|
-
* @
|
|
1158
|
+
* @async
|
|
1159
|
+
* @param {unknown} compiled
|
|
1160
|
+
* @returns {Promise<void>}
|
|
1126
1161
|
*/
|
|
1127
|
-
static cache(compiled) {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
`);
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
STANZA ATTRIBUTES...${JSON.stringify(
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
${data.message}`, "utf8");
|
|
1150
|
-
packages.fs.appendFileSync(`${settings2.NoaaWeatherWireService.cache.directory}/cache-${data.isCap ? `cap` : `raw`}${data.isVtec ? `-vtec` : ``}.bin`, `=================================================
|
|
1151
|
-
${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}
|
|
1152
|
-
=================================================
|
|
1153
|
-
${data.message}`, "utf8");
|
|
1162
|
+
static cache(message, compiled) {
|
|
1163
|
+
return __async(this, null, function* () {
|
|
1164
|
+
if (!compiled) return;
|
|
1165
|
+
const data = compiled;
|
|
1166
|
+
const settings2 = settings;
|
|
1167
|
+
const { fs: fs2, path: path2 } = packages;
|
|
1168
|
+
if (!message || !settings2.noaa_weather_wire_service_settings.cache.directory) return;
|
|
1169
|
+
const cacheDir = settings2.noaa_weather_wire_service_settings.cache.directory;
|
|
1170
|
+
if (!fs2.existsSync(cacheDir)) fs2.mkdirSync(cacheDir, { recursive: true });
|
|
1171
|
+
const prefix = `category-${data.awipsType.prefix}-${data.awipsType.type}s`;
|
|
1172
|
+
const suffix = `${data.isCap ? "cap" : "raw"}${data.isVtec ? "-vtec" : ""}`;
|
|
1173
|
+
const categoryFile = path2.join(cacheDir, `${prefix}-${suffix}.bin`);
|
|
1174
|
+
const cacheFile = path2.join(cacheDir, `cache-${suffix}.bin`);
|
|
1175
|
+
const entry = `[SoF]
|
|
1176
|
+
STANZA ATTRIBUTES...${JSON.stringify(compiled)}
|
|
1177
|
+
[EoF]
|
|
1178
|
+
${message}`;
|
|
1179
|
+
yield Promise.all([
|
|
1180
|
+
fs2.promises.appendFile(categoryFile, entry, "utf8"),
|
|
1181
|
+
fs2.promises.appendFile(cacheFile, entry, "utf8")
|
|
1182
|
+
]);
|
|
1183
|
+
});
|
|
1154
1184
|
}
|
|
1155
1185
|
};
|
|
1156
1186
|
var stanza_default = StanzaParser;
|
|
@@ -1158,14 +1188,19 @@ var stanza_default = StanzaParser;
|
|
|
1158
1188
|
// src/parsers/text.ts
|
|
1159
1189
|
var TextParser = class {
|
|
1160
1190
|
/**
|
|
1161
|
-
* textProductToString
|
|
1191
|
+
* @function textProductToString
|
|
1192
|
+
* @description
|
|
1193
|
+
* Searches a text product message for a line containing a specific value,
|
|
1194
|
+
* extracts the substring immediately following that value, and optionally
|
|
1195
|
+
* removes additional specified strings. Cleans up the extracted string by
|
|
1196
|
+
* trimming whitespace and removing any remaining occurrences of the search
|
|
1197
|
+
* value or '<' characters.
|
|
1162
1198
|
*
|
|
1163
|
-
* @public
|
|
1164
1199
|
* @static
|
|
1165
|
-
* @param {string} message
|
|
1166
|
-
* @param {string} value
|
|
1167
|
-
* @param {string[]} [removal=[]]
|
|
1168
|
-
* @returns {
|
|
1200
|
+
* @param {string} message
|
|
1201
|
+
* @param {string} value
|
|
1202
|
+
* @param {string[]} [removal=[]]
|
|
1203
|
+
* @returns {string | null}
|
|
1169
1204
|
*/
|
|
1170
1205
|
static textProductToString(message, value, removal = []) {
|
|
1171
1206
|
const lines = message.split("\n");
|
|
@@ -1182,12 +1217,16 @@ var TextParser = class {
|
|
|
1182
1217
|
return null;
|
|
1183
1218
|
}
|
|
1184
1219
|
/**
|
|
1185
|
-
* textProductToPolygon
|
|
1220
|
+
* @function textProductToPolygon
|
|
1221
|
+
* @description
|
|
1222
|
+
* Parses a text product message to extract polygon coordinates based on
|
|
1223
|
+
* LAT...LON data. Coordinates are converted to [latitude, longitude] pairs
|
|
1224
|
+
* with longitude negated (assumes Western Hemisphere). If the polygon has
|
|
1225
|
+
* more than two points, the first point is repeated at the end to close it.
|
|
1186
1226
|
*
|
|
1187
|
-
* @public
|
|
1188
1227
|
* @static
|
|
1189
|
-
* @param {string} message
|
|
1190
|
-
* @returns {[number, number][]}
|
|
1228
|
+
* @param {string} message
|
|
1229
|
+
* @returns {[number, number][]}
|
|
1191
1230
|
*/
|
|
1192
1231
|
static textProductToPolygon(message) {
|
|
1193
1232
|
const coordinates = [];
|
|
@@ -1207,13 +1246,16 @@ var TextParser = class {
|
|
|
1207
1246
|
return coordinates;
|
|
1208
1247
|
}
|
|
1209
1248
|
/**
|
|
1210
|
-
* textProductToDescription
|
|
1249
|
+
* @function textProductToDescription
|
|
1250
|
+
* @description
|
|
1251
|
+
* Extracts a clean description portion from a text product message, optionally
|
|
1252
|
+
* removing a handle and any extra metadata such as "STANZA ATTRIBUTES...".
|
|
1253
|
+
* Also trims and normalizes whitespace.
|
|
1211
1254
|
*
|
|
1212
|
-
* @public
|
|
1213
1255
|
* @static
|
|
1214
|
-
* @param {string} message
|
|
1215
|
-
* @param {string} [handle=null]
|
|
1216
|
-
* @returns {string}
|
|
1256
|
+
* @param {string} message
|
|
1257
|
+
* @param {string | null} [handle=null]
|
|
1258
|
+
* @returns {string}
|
|
1217
1259
|
*/
|
|
1218
1260
|
static textProductToDescription(message, handle = null) {
|
|
1219
1261
|
const original = message;
|
|
@@ -1241,32 +1283,21 @@ var TextParser = class {
|
|
|
1241
1283
|
message = latEnd !== -1 ? afterHandle.substring(0, latEnd).trim() : afterHandle.trim();
|
|
1242
1284
|
}
|
|
1243
1285
|
}
|
|
1244
|
-
return message.replace(/\s+/g, " ").trim().startsWith("
|
|
1245
|
-
}
|
|
1246
|
-
/**
|
|
1247
|
-
* awipTextToEvent converts an AWIPS ID prefix from a text-based weather product message to its corresponding event type and prefix.
|
|
1248
|
-
*
|
|
1249
|
-
* @public
|
|
1250
|
-
* @static
|
|
1251
|
-
* @param {string} message
|
|
1252
|
-
* @returns {{ type: any; prefix: any; }}
|
|
1253
|
-
*/
|
|
1254
|
-
static awipTextToEvent(message) {
|
|
1255
|
-
for (const [prefix, type] of Object.entries(definitions.awips)) {
|
|
1256
|
-
if (message.startsWith(prefix)) {
|
|
1257
|
-
return { type, prefix };
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
return { type: `XX`, prefix: `XX` };
|
|
1286
|
+
return message.replace(/\s+/g, " ").trim().startsWith("STANZA ATTRIBUTES...") ? original : message.split("STANZA ATTRIBUTES...")[0].trim();
|
|
1261
1287
|
}
|
|
1262
1288
|
/**
|
|
1263
|
-
* getXmlValues
|
|
1289
|
+
* @function getXmlValues
|
|
1290
|
+
* @description
|
|
1291
|
+
* Recursively extracts specified values from a parsed XML-like object.
|
|
1292
|
+
* Searches both object keys and array items for matching keys (case-insensitive)
|
|
1293
|
+
* and returns the corresponding values. If multiple unique values are found for
|
|
1294
|
+
* a key, an array is returned; if one value is found, it returns that value;
|
|
1295
|
+
* if none are found, returns `null`.
|
|
1264
1296
|
*
|
|
1265
|
-
* @public
|
|
1266
1297
|
* @static
|
|
1267
|
-
* @param {
|
|
1268
|
-
* @param {string[]} valuesToExtract
|
|
1269
|
-
* @returns {Record<string,
|
|
1298
|
+
* @param {any} parsed
|
|
1299
|
+
* @param {string[]} valuesToExtract
|
|
1300
|
+
* @returns {Record<string, string | string[] | null>}
|
|
1270
1301
|
*/
|
|
1271
1302
|
static getXmlValues(parsed, valuesToExtract) {
|
|
1272
1303
|
const extracted = {};
|
|
@@ -1311,45 +1342,63 @@ var text_default = TextParser;
|
|
|
1311
1342
|
// src/parsers/ugc.ts
|
|
1312
1343
|
var UGCParser = class {
|
|
1313
1344
|
/**
|
|
1314
|
-
* ugcExtractor
|
|
1345
|
+
* @function ugcExtractor
|
|
1346
|
+
* @description
|
|
1347
|
+
* Extracts UGC (Universal Geographic Code) information from a message.
|
|
1348
|
+
* This includes parsing the header, resolving zones, calculating the expiry
|
|
1349
|
+
* date, and retrieving associated location names from the database.
|
|
1315
1350
|
*
|
|
1316
|
-
* @public
|
|
1317
1351
|
* @static
|
|
1318
1352
|
* @async
|
|
1319
|
-
* @param {string} message
|
|
1320
|
-
* @returns {
|
|
1353
|
+
* @param {string} message
|
|
1354
|
+
* @returns {Promise<types.UGCEntry | null>}
|
|
1321
1355
|
*/
|
|
1322
1356
|
static ugcExtractor(message) {
|
|
1323
1357
|
return __async(this, null, function* () {
|
|
1324
1358
|
const header = this.getHeader(message);
|
|
1359
|
+
if (!header) return null;
|
|
1325
1360
|
const zones = this.getZones(header);
|
|
1361
|
+
if (zones.length === 0) return null;
|
|
1326
1362
|
const expiry = this.getExpiry(message);
|
|
1327
1363
|
const locations = yield this.getLocations(zones);
|
|
1328
|
-
|
|
1329
|
-
|
|
1364
|
+
return {
|
|
1365
|
+
zones,
|
|
1366
|
+
locations,
|
|
1367
|
+
expiry
|
|
1368
|
+
};
|
|
1330
1369
|
});
|
|
1331
1370
|
}
|
|
1332
1371
|
/**
|
|
1333
|
-
* getHeader
|
|
1372
|
+
* @function getHeader
|
|
1373
|
+
* @description
|
|
1374
|
+
* Extracts the UGC header from a message by locating patterns defined in
|
|
1375
|
+
* `ugc1` and `ugc2` regular expressions. Removes all whitespace and the
|
|
1376
|
+
* trailing character from the matched header.
|
|
1334
1377
|
*
|
|
1335
|
-
* @public
|
|
1336
1378
|
* @static
|
|
1337
|
-
* @param {string} message
|
|
1338
|
-
* @returns {
|
|
1379
|
+
* @param {string} message
|
|
1380
|
+
* @returns {string | null}
|
|
1339
1381
|
*/
|
|
1340
1382
|
static getHeader(message) {
|
|
1341
1383
|
const start = message.search(new RegExp(definitions.expressions.ugc1, "gimu"));
|
|
1342
|
-
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1384
|
+
if (start === -1) return null;
|
|
1385
|
+
const subMessage = message.substring(start);
|
|
1386
|
+
const end = subMessage.search(new RegExp(definitions.expressions.ugc2, "gimu"));
|
|
1387
|
+
if (end === -1) return null;
|
|
1388
|
+
const full = subMessage.substring(0, end).replace(/\s+/g, "").slice(0, -1);
|
|
1389
|
+
return full || null;
|
|
1345
1390
|
}
|
|
1346
1391
|
/**
|
|
1347
|
-
* getExpiry
|
|
1392
|
+
* @function getExpiry
|
|
1393
|
+
* @description
|
|
1394
|
+
* Extracts an expiration date from a message using the UGC3 format.
|
|
1395
|
+
* The function parses day, hour, and minute from the message and constructs
|
|
1396
|
+
* a Date object in the current month and year. Returns `null` if no valid
|
|
1397
|
+
* expiration is found.
|
|
1348
1398
|
*
|
|
1349
|
-
* @public
|
|
1350
1399
|
* @static
|
|
1351
|
-
* @param {string} message
|
|
1352
|
-
* @returns {
|
|
1400
|
+
* @param {string} message
|
|
1401
|
+
* @returns {Date | null}
|
|
1353
1402
|
*/
|
|
1354
1403
|
static getExpiry(message) {
|
|
1355
1404
|
const start = message.match(new RegExp(definitions.expressions.ugc3, "gimu"));
|
|
@@ -1364,42 +1413,53 @@ var UGCParser = class {
|
|
|
1364
1413
|
return null;
|
|
1365
1414
|
}
|
|
1366
1415
|
/**
|
|
1367
|
-
* getLocations
|
|
1416
|
+
* @function getLocations
|
|
1417
|
+
* @description
|
|
1418
|
+
* Retrieves human-readable location names for an array of zone identifiers
|
|
1419
|
+
* from the shapefiles database. If a zone is not found, the zone ID itself
|
|
1420
|
+
* is returned. Duplicate locations are removed and the result is sorted.
|
|
1368
1421
|
*
|
|
1369
|
-
* @public
|
|
1370
1422
|
* @static
|
|
1371
1423
|
* @async
|
|
1372
|
-
* @param {
|
|
1373
|
-
* @returns {
|
|
1424
|
+
* @param {string[]} zones
|
|
1425
|
+
* @returns {Promise<string[]>}
|
|
1374
1426
|
*/
|
|
1375
1427
|
static getLocations(zones) {
|
|
1376
1428
|
return __async(this, null, function* () {
|
|
1377
1429
|
const locations = [];
|
|
1378
1430
|
for (let i = 0; i < zones.length; i++) {
|
|
1379
1431
|
const id = zones[i].trim();
|
|
1380
|
-
const located = yield cache.db.prepare(
|
|
1432
|
+
const located = yield cache.db.prepare(
|
|
1433
|
+
`
|
|
1434
|
+
SELECT location FROM shapefiles WHERE id = ?`
|
|
1435
|
+
).get(id);
|
|
1381
1436
|
located != void 0 ? locations.push(located.location) : locations.push(id);
|
|
1382
1437
|
}
|
|
1383
1438
|
return Array.from(new Set(locations)).sort();
|
|
1384
1439
|
});
|
|
1385
1440
|
}
|
|
1386
1441
|
/**
|
|
1387
|
-
* getCoordinates
|
|
1442
|
+
* @function getCoordinates
|
|
1443
|
+
* @description
|
|
1444
|
+
* Retrieves geographic coordinates for an array of zone identifiers
|
|
1445
|
+
* from the shapefiles database. Returns the coordinates of the first
|
|
1446
|
+
* polygon found for any matching zone.
|
|
1388
1447
|
*
|
|
1389
|
-
* @public
|
|
1390
1448
|
* @static
|
|
1391
|
-
* @param {
|
|
1392
|
-
* @returns {
|
|
1449
|
+
* @param {string[]} zones
|
|
1450
|
+
* @returns {[number, number][]}
|
|
1393
1451
|
*/
|
|
1394
1452
|
static getCoordinates(zones) {
|
|
1395
1453
|
let coordinates = [];
|
|
1396
1454
|
for (let i = 0; i < zones.length; i++) {
|
|
1397
1455
|
const id = zones[i].trim();
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1456
|
+
const row = cache.db.prepare(
|
|
1457
|
+
`SELECT geometry FROM shapefiles WHERE id = ?`
|
|
1458
|
+
).get(id);
|
|
1459
|
+
if (row != void 0) {
|
|
1460
|
+
let geometry = JSON.parse(row.geometry);
|
|
1401
1461
|
if ((geometry == null ? void 0 : geometry.type) === "Polygon") {
|
|
1402
|
-
coordinates.push(...geometry.coordinates[0].map((coord) => [coord[
|
|
1462
|
+
coordinates.push(...geometry.coordinates[0].map((coord) => [coord[1], coord[0]]));
|
|
1403
1463
|
break;
|
|
1404
1464
|
}
|
|
1405
1465
|
}
|
|
@@ -1407,31 +1467,46 @@ var UGCParser = class {
|
|
|
1407
1467
|
return coordinates;
|
|
1408
1468
|
}
|
|
1409
1469
|
/**
|
|
1410
|
-
* getZones
|
|
1470
|
+
* @function getZones
|
|
1471
|
+
* @description
|
|
1472
|
+
* Parses a UGC header string and returns an array of individual zone
|
|
1473
|
+
* identifiers. Handles ranges indicated with `>` and preserves the
|
|
1474
|
+
* state and format prefixes.
|
|
1411
1475
|
*
|
|
1412
|
-
* @public
|
|
1413
1476
|
* @static
|
|
1414
|
-
* @param {string} header
|
|
1415
|
-
* @returns {
|
|
1477
|
+
* @param {string} header
|
|
1478
|
+
* @returns {string[]}
|
|
1416
1479
|
*/
|
|
1417
1480
|
static getZones(header) {
|
|
1418
1481
|
const ugcSplit = header.split("-");
|
|
1419
1482
|
const zones = [];
|
|
1420
1483
|
let state = ugcSplit[0].substring(0, 2);
|
|
1421
|
-
|
|
1422
|
-
for (
|
|
1423
|
-
if (/^[A-Z]/.test(
|
|
1424
|
-
state =
|
|
1425
|
-
if (
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1484
|
+
const format = ugcSplit[0].substring(2, 3);
|
|
1485
|
+
for (const part of ugcSplit) {
|
|
1486
|
+
if (/^[A-Z]/.test(part)) {
|
|
1487
|
+
state = part.substring(0, 2);
|
|
1488
|
+
if (part.includes(">")) {
|
|
1489
|
+
const [start, end] = part.split(">");
|
|
1490
|
+
const startNum = parseInt(start.substring(3), 10);
|
|
1491
|
+
const endNum = parseInt(end, 10);
|
|
1492
|
+
for (let j = startNum; j <= endNum; j++) {
|
|
1493
|
+
zones.push(`${state}${format}${j.toString().padStart(3, "0")}`);
|
|
1494
|
+
}
|
|
1495
|
+
} else {
|
|
1496
|
+
zones.push(part);
|
|
1497
|
+
}
|
|
1429
1498
|
continue;
|
|
1430
1499
|
}
|
|
1431
|
-
if (
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1500
|
+
if (part.includes(">")) {
|
|
1501
|
+
const [start, end] = part.split(">");
|
|
1502
|
+
const startNum = parseInt(start, 10);
|
|
1503
|
+
const endNum = parseInt(end, 10);
|
|
1504
|
+
for (let j = startNum; j <= endNum; j++) {
|
|
1505
|
+
zones.push(`${state}${format}${j.toString().padStart(3, "0")}`);
|
|
1506
|
+
}
|
|
1507
|
+
} else {
|
|
1508
|
+
zones.push(`${state}${format}${part}`);
|
|
1509
|
+
}
|
|
1435
1510
|
}
|
|
1436
1511
|
return zones.filter((item) => item !== "");
|
|
1437
1512
|
}
|
|
@@ -1441,43 +1516,50 @@ var ugc_default = UGCParser;
|
|
|
1441
1516
|
// src/parsers/vtec.ts
|
|
1442
1517
|
var VtecParser = class {
|
|
1443
1518
|
/**
|
|
1444
|
-
* vtecExtractor
|
|
1519
|
+
* @function vtecExtractor
|
|
1520
|
+
* @description
|
|
1521
|
+
* Extracts VTEC entries from a raw NWWS message string and returns
|
|
1522
|
+
* structured objects containing type, tracking, event, status,
|
|
1523
|
+
* WMO identifiers, and expiry date.
|
|
1445
1524
|
*
|
|
1446
|
-
* @public
|
|
1447
1525
|
* @static
|
|
1448
|
-
* @
|
|
1449
|
-
* @
|
|
1450
|
-
* @returns {unknown}
|
|
1526
|
+
* @param {string} message
|
|
1527
|
+
* @returns {Promise<types.VtecEntry[] | null>}
|
|
1451
1528
|
*/
|
|
1452
1529
|
static vtecExtractor(message) {
|
|
1453
1530
|
return __async(this, null, function* () {
|
|
1454
|
-
|
|
1531
|
+
var _a;
|
|
1455
1532
|
const matches = message.match(new RegExp(definitions.expressions.vtec, "g"));
|
|
1456
1533
|
if (!matches) return null;
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
const parts = vtec.split(
|
|
1460
|
-
|
|
1534
|
+
const vtecs = [];
|
|
1535
|
+
for (const vtec of matches) {
|
|
1536
|
+
const parts = vtec.split(".");
|
|
1537
|
+
if (parts.length < 7) continue;
|
|
1538
|
+
const dates = parts[6].split("-");
|
|
1461
1539
|
vtecs.push({
|
|
1462
1540
|
raw: vtec,
|
|
1463
1541
|
type: definitions.productTypes[parts[0]],
|
|
1464
1542
|
tracking: `${parts[2]}-${parts[3]}-${parts[4]}-${parts[5]}`,
|
|
1465
1543
|
event: `${definitions.events[parts[3]]} ${definitions.actions[parts[4]]}`,
|
|
1466
1544
|
status: definitions.status[parts[1]],
|
|
1467
|
-
wmo: message.match(new RegExp(definitions.expressions.wmo, "gimu")),
|
|
1545
|
+
wmo: (_a = message.match(new RegExp(definitions.expressions.wmo, "gimu"))) != null ? _a : [],
|
|
1468
1546
|
expires: this.parseExpiryDate(dates)
|
|
1469
1547
|
});
|
|
1470
1548
|
}
|
|
1471
|
-
return vtecs;
|
|
1549
|
+
return vtecs.length ? vtecs : null;
|
|
1472
1550
|
});
|
|
1473
1551
|
}
|
|
1474
1552
|
/**
|
|
1475
|
-
* parseExpiryDate
|
|
1553
|
+
* @function parseExpiryDate
|
|
1554
|
+
* @description
|
|
1555
|
+
* Converts a NWWS VTEC/expiry timestamp string into a formatted local ISO date string
|
|
1556
|
+
* with an Eastern Time offset (-04:00). Returns `Invalid Date Format` if the input
|
|
1557
|
+
* is `000000T0000Z`.
|
|
1476
1558
|
*
|
|
1477
1559
|
* @private
|
|
1478
1560
|
* @static
|
|
1479
|
-
* @param {
|
|
1480
|
-
* @returns {string}
|
|
1561
|
+
* @param {string[]} args
|
|
1562
|
+
* @returns {string}
|
|
1481
1563
|
*/
|
|
1482
1564
|
static parseExpiryDate(args) {
|
|
1483
1565
|
if (args[1] == `000000T0000Z`) return `Invalid Date Format`;
|
|
@@ -1492,38 +1574,48 @@ var vtec_default = VtecParser;
|
|
|
1492
1574
|
// src/parsers/types/vtec.ts
|
|
1493
1575
|
var VTECAlerts = class {
|
|
1494
1576
|
/**
|
|
1495
|
-
*
|
|
1577
|
+
* @function event
|
|
1578
|
+
* @description
|
|
1579
|
+
* Processes a validated stanza message, extracting VTEC and UGC entries,
|
|
1580
|
+
* computing base properties, generating headers, and preparing structured
|
|
1581
|
+
* event objects for downstream handling. Each extracted event is enriched
|
|
1582
|
+
* with metadata, performance timing, and history information.
|
|
1496
1583
|
*
|
|
1497
|
-
* @public
|
|
1498
1584
|
* @static
|
|
1499
1585
|
* @async
|
|
1500
|
-
* @param {types.
|
|
1501
|
-
* @returns {
|
|
1586
|
+
* @param {types.StanzaCompiled} validated
|
|
1587
|
+
* @returns {Promise<void>}
|
|
1502
1588
|
*/
|
|
1503
1589
|
static event(validated) {
|
|
1504
1590
|
return __async(this, null, function* () {
|
|
1505
|
-
var _a;
|
|
1591
|
+
var _a, _b;
|
|
1506
1592
|
let processed = [];
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1593
|
+
const blocks = (_a = validated.message.split(/\[SoF\]/gim)) == null ? void 0 : _a.map((msg) => msg.trim());
|
|
1594
|
+
for (const block of blocks) {
|
|
1595
|
+
const cachedAttribute = block.match(/STANZA ATTRIBUTES\.\.\.(\{.*\})/);
|
|
1596
|
+
const messages = (_b = block.split(/(?=\$\$)/g)) == null ? void 0 : _b.map((msg) => msg.trim());
|
|
1597
|
+
if (!messages || messages.length == 0) return;
|
|
1598
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1599
|
+
const tick = performance.now();
|
|
1600
|
+
const message = messages[i];
|
|
1601
|
+
const attributes = cachedAttribute != null ? JSON.parse(cachedAttribute[1]) : validated;
|
|
1602
|
+
const getVTEC = yield vtec_default.vtecExtractor(message);
|
|
1603
|
+
const getUGC = yield ugc_default.ugcExtractor(message);
|
|
1604
|
+
if (getVTEC != null && getUGC != null) {
|
|
1605
|
+
for (let j = 0; j < getVTEC.length; j++) {
|
|
1606
|
+
const vtec = getVTEC[j];
|
|
1607
|
+
const getBaseProperties = yield events_default.getBaseProperties(message, attributes, getUGC, vtec);
|
|
1608
|
+
const getHeader = events_default.getHeader(__spreadValues(__spreadValues({}, validated.attributes), getBaseProperties.metadata), getBaseProperties, vtec);
|
|
1609
|
+
processed.push({
|
|
1610
|
+
performance: performance.now() - tick,
|
|
1611
|
+
source: `vtec-parser`,
|
|
1612
|
+
tracking: vtec.tracking,
|
|
1613
|
+
header: getHeader,
|
|
1614
|
+
vtec: vtec.raw,
|
|
1615
|
+
history: [{ description: getBaseProperties.description, issued: getBaseProperties.issued, type: vtec.status }],
|
|
1616
|
+
properties: __spreadValues({ event: vtec.event, parent: vtec.event, action_type: vtec.status }, getBaseProperties)
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1527
1619
|
}
|
|
1528
1620
|
}
|
|
1529
1621
|
}
|
|
@@ -1536,62 +1628,80 @@ var vtec_default2 = VTECAlerts;
|
|
|
1536
1628
|
// src/parsers/types/ugc.ts
|
|
1537
1629
|
var UGCAlerts = class {
|
|
1538
1630
|
/**
|
|
1539
|
-
* getTracking
|
|
1631
|
+
* @function getTracking
|
|
1632
|
+
* @description
|
|
1633
|
+
* Generates a unique tracking identifier for an event using the sender's ICAO
|
|
1634
|
+
* and some attributes.
|
|
1540
1635
|
*
|
|
1541
1636
|
* @private
|
|
1542
1637
|
* @static
|
|
1543
|
-
* @param {types.
|
|
1544
|
-
* @param {string[]} zones
|
|
1638
|
+
* @param {types.EventProperties} baseProperties
|
|
1545
1639
|
* @returns {string}
|
|
1546
1640
|
*/
|
|
1547
|
-
static getTracking(baseProperties
|
|
1548
|
-
return `${baseProperties.sender_icao}
|
|
1641
|
+
static getTracking(baseProperties) {
|
|
1642
|
+
return `${baseProperties.sender_icao}-${baseProperties.metadata.attributes.ttaaii}-${baseProperties.metadata.attributes.id.slice(-4)}`;
|
|
1549
1643
|
}
|
|
1550
1644
|
/**
|
|
1551
|
-
* getEvent
|
|
1645
|
+
* @function getEvent
|
|
1646
|
+
* @description
|
|
1647
|
+
* Determines the human-readable event name from a message and AWIPS attributes.
|
|
1648
|
+
* - Checks if the message contains any predefined offshore event keywords
|
|
1649
|
+
* and returns the matching offshore event if found.
|
|
1650
|
+
* - Otherwise, returns a formatted event type string from the provided attributes,
|
|
1651
|
+
* capitalizing the first letter of each word.
|
|
1552
1652
|
*
|
|
1553
1653
|
* @private
|
|
1554
1654
|
* @static
|
|
1555
|
-
* @param {string} message
|
|
1556
|
-
* @param {Record<string, any>} attributes
|
|
1557
|
-
* @returns {
|
|
1655
|
+
* @param {string} message
|
|
1656
|
+
* @param {Record<string, any>} attributes
|
|
1657
|
+
* @returns {string}
|
|
1558
1658
|
*/
|
|
1559
|
-
static getEvent(message,
|
|
1659
|
+
static getEvent(message, metadata) {
|
|
1560
1660
|
const offshoreEvent = Object.keys(definitions.offshore).find((event) => message.toLowerCase().includes(event.toLowerCase()));
|
|
1561
1661
|
if (offshoreEvent != void 0) return Object.keys(definitions.offshore).find((event) => message.toLowerCase().includes(event.toLowerCase()));
|
|
1562
|
-
return
|
|
1662
|
+
return metadata.awipsType.type.split(`-`).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(` `);
|
|
1563
1663
|
}
|
|
1564
1664
|
/**
|
|
1565
|
-
*
|
|
1665
|
+
* @function event
|
|
1666
|
+
* @description
|
|
1667
|
+
* Processes a validated stanza message, extracting UGC entries and
|
|
1668
|
+
* computing base properties for non-VTEC events. Each extracted event
|
|
1669
|
+
* is enriched with metadata, performance timing, and history information,
|
|
1670
|
+
* then filtered and emitted via `EventParser.validateEvents`.
|
|
1566
1671
|
*
|
|
1567
|
-
* @public
|
|
1568
1672
|
* @static
|
|
1569
1673
|
* @async
|
|
1570
|
-
* @param {types.
|
|
1571
|
-
* @returns {
|
|
1674
|
+
* @param {types.StanzaCompiled} validated
|
|
1675
|
+
* @returns {Promise<void>}
|
|
1572
1676
|
*/
|
|
1573
1677
|
static event(validated) {
|
|
1574
1678
|
return __async(this, null, function* () {
|
|
1575
|
-
var _a;
|
|
1679
|
+
var _a, _b;
|
|
1576
1680
|
let processed = [];
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
const
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
const
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1681
|
+
const blocks = (_a = validated.message.split(/\[SoF\]/gim)) == null ? void 0 : _a.map((msg) => msg.trim());
|
|
1682
|
+
for (const block of blocks) {
|
|
1683
|
+
const cachedAttribute = block.match(/STANZA ATTRIBUTES\.\.\.(\{.*\})/);
|
|
1684
|
+
const messages = (_b = block.split(/(?=\$\$)/g)) == null ? void 0 : _b.map((msg) => msg.trim());
|
|
1685
|
+
if (!messages || messages.length == 0) return;
|
|
1686
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1687
|
+
const tick = performance.now();
|
|
1688
|
+
const message = messages[i];
|
|
1689
|
+
const getUGC = yield ugc_default.ugcExtractor(message);
|
|
1690
|
+
if (getUGC != null) {
|
|
1691
|
+
const attributes = cachedAttribute != null ? JSON.parse(cachedAttribute[1]) : validated;
|
|
1692
|
+
const getBaseProperties = yield events_default.getBaseProperties(message, attributes, getUGC);
|
|
1693
|
+
const getHeader = events_default.getHeader(__spreadValues(__spreadValues({}, attributes), getBaseProperties.metadata), getBaseProperties);
|
|
1694
|
+
const getEvent = this.getEvent(message, attributes);
|
|
1695
|
+
processed.push({
|
|
1696
|
+
performance: performance.now() - tick,
|
|
1697
|
+
source: `ugc-parser`,
|
|
1698
|
+
tracking: this.getTracking(getBaseProperties),
|
|
1699
|
+
header: getHeader,
|
|
1700
|
+
vtec: `N/A`,
|
|
1701
|
+
history: [{ description: getBaseProperties.description, issued: getBaseProperties.issued, type: `Issued` }],
|
|
1702
|
+
properties: __spreadValues({ event: getEvent, parent: getEvent, action_type: `Issued` }, getBaseProperties)
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1595
1705
|
}
|
|
1596
1706
|
}
|
|
1597
1707
|
events_default.validateEvents(processed);
|
|
@@ -1601,126 +1711,202 @@ var UGCAlerts = class {
|
|
|
1601
1711
|
var ugc_default2 = UGCAlerts;
|
|
1602
1712
|
|
|
1603
1713
|
// src/parsers/types/text.ts
|
|
1604
|
-
var
|
|
1714
|
+
var TextAlerts = class {
|
|
1715
|
+
/**
|
|
1716
|
+
* @function getTracking
|
|
1717
|
+
* @description
|
|
1718
|
+
* Generates a unique tracking identifier for an event using the sender's ICAO
|
|
1719
|
+
* and some attributes.
|
|
1720
|
+
*
|
|
1721
|
+
* @private
|
|
1722
|
+
* @static
|
|
1723
|
+
* @param {types.EventProperties} baseProperties
|
|
1724
|
+
* @returns {string}
|
|
1725
|
+
*/
|
|
1605
1726
|
static getTracking(baseProperties) {
|
|
1606
|
-
return `${baseProperties.sender_icao}`;
|
|
1727
|
+
return `${baseProperties.sender_icao}-${baseProperties.metadata.attributes.ttaaii}-${baseProperties.metadata.attributes.id.slice(-4)}`;
|
|
1607
1728
|
}
|
|
1608
|
-
|
|
1729
|
+
/**
|
|
1730
|
+
* @function getEvent
|
|
1731
|
+
* @description
|
|
1732
|
+
* Determines the event name from a text message and its AWIPS attributes.
|
|
1733
|
+
* If the message contains a known offshore event keyword, that offshore
|
|
1734
|
+
* event is returned. Otherwise, the event type from the AWIPS attributes
|
|
1735
|
+
* is formatted into a human-readable string with each word capitalized.
|
|
1736
|
+
*
|
|
1737
|
+
* @private
|
|
1738
|
+
* @static
|
|
1739
|
+
* @param {string} message
|
|
1740
|
+
* @param {types.StanzaAttributes} metadata
|
|
1741
|
+
* @returns {string}
|
|
1742
|
+
*/
|
|
1743
|
+
static getEvent(message, metadata) {
|
|
1609
1744
|
const offshoreEvent = Object.keys(definitions.offshore).find((event) => message.toLowerCase().includes(event.toLowerCase()));
|
|
1610
|
-
if (offshoreEvent) return Object.keys(definitions.offshore).find((event) => message.toLowerCase().includes(event.toLowerCase()));
|
|
1611
|
-
return
|
|
1745
|
+
if (offshoreEvent != void 0) return Object.keys(definitions.offshore).find((event) => message.toLowerCase().includes(event.toLowerCase()));
|
|
1746
|
+
return metadata.awipsType.type.split(`-`).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(` `);
|
|
1612
1747
|
}
|
|
1748
|
+
/**
|
|
1749
|
+
* @function event
|
|
1750
|
+
* @description
|
|
1751
|
+
* Processes a compiled text-based NOAA Stanza message and extracts relevant
|
|
1752
|
+
* event information. Splits the message into multiple segments based on
|
|
1753
|
+
* markers such as "$$", "ISSUED TIME...", or separator lines, generates
|
|
1754
|
+
* base properties, headers, event names, and tracking information for
|
|
1755
|
+
* each segment, then validates and emits the processed events.
|
|
1756
|
+
*
|
|
1757
|
+
* @public
|
|
1758
|
+
* @static
|
|
1759
|
+
* @async
|
|
1760
|
+
* @param {types.StanzaCompiled} validated
|
|
1761
|
+
* @returns {Promise<void>}
|
|
1762
|
+
*/
|
|
1613
1763
|
static event(validated) {
|
|
1614
1764
|
return __async(this, null, function* () {
|
|
1615
|
-
var _a;
|
|
1765
|
+
var _a, _b;
|
|
1616
1766
|
let processed = [];
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1767
|
+
const blocks = (_a = validated.message.split(/\[SoF\]/gim)) == null ? void 0 : _a.map((msg) => msg.trim());
|
|
1768
|
+
for (const block of blocks) {
|
|
1769
|
+
const cachedAttribute = block.match(/STANZA ATTRIBUTES\.\.\.(\{.*\})/);
|
|
1770
|
+
const messages = (_b = block.split(/(?=\$\$)/g)) == null ? void 0 : _b.map((msg) => msg.trim());
|
|
1771
|
+
if (!messages || messages.length == 0) return;
|
|
1772
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1773
|
+
const tick = performance.now();
|
|
1774
|
+
const message = messages[i];
|
|
1775
|
+
const attributes = cachedAttribute != null ? JSON.parse(cachedAttribute[1]) : validated;
|
|
1776
|
+
const getBaseProperties = yield events_default.getBaseProperties(message, attributes);
|
|
1777
|
+
const getHeader = events_default.getHeader(__spreadValues(__spreadValues({}, validated.attributes), getBaseProperties.metadata), getBaseProperties);
|
|
1778
|
+
const getEvent = this.getEvent(message, attributes);
|
|
1779
|
+
processed.push({
|
|
1780
|
+
performance: performance.now() - tick,
|
|
1781
|
+
source: `text-parser`,
|
|
1782
|
+
tracking: this.getTracking(getBaseProperties),
|
|
1783
|
+
header: getHeader,
|
|
1784
|
+
vtec: `N/A`,
|
|
1785
|
+
history: [{ description: getBaseProperties.description, issued: getBaseProperties.issued, type: `Issued` }],
|
|
1786
|
+
properties: __spreadValues({ event: getEvent, parent: getEvent, action_type: `Issued` }, getBaseProperties)
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1633
1789
|
}
|
|
1634
1790
|
events_default.validateEvents(processed);
|
|
1635
1791
|
});
|
|
1636
1792
|
}
|
|
1637
1793
|
};
|
|
1638
|
-
var text_default2 =
|
|
1794
|
+
var text_default2 = TextAlerts;
|
|
1639
1795
|
|
|
1640
1796
|
// src/parsers/types/cap.ts
|
|
1641
1797
|
var CapAlerts = class {
|
|
1642
|
-
|
|
1798
|
+
/**
|
|
1799
|
+
* @function getTracking
|
|
1800
|
+
* @description
|
|
1801
|
+
* Generates a unique tracking identifier for a CAP alert based on extracted XML values.
|
|
1802
|
+
* If VTEC information is available, it constructs the tracking ID from the VTEC components.
|
|
1803
|
+
* Otherwise, it uses the WMO identifier along with TTAI and CCCC attributes.
|
|
1804
|
+
*
|
|
1805
|
+
* @private
|
|
1806
|
+
* @static
|
|
1807
|
+
* @param {Record<string, string>} extracted
|
|
1808
|
+
* @returns {string}
|
|
1809
|
+
*/
|
|
1810
|
+
static getTracking(extracted, metadata) {
|
|
1643
1811
|
return extracted.vtec ? (() => {
|
|
1644
1812
|
const vtecValue = Array.isArray(extracted.vtec) ? extracted.vtec[0] : extracted.vtec;
|
|
1645
1813
|
const splitVTEC = vtecValue.split(".");
|
|
1646
1814
|
return `${splitVTEC[2]}-${splitVTEC[3]}-${splitVTEC[4]}-${splitVTEC[5]}`;
|
|
1647
|
-
})() : `${extracted.wmoidentifier
|
|
1815
|
+
})() : `${extracted.wmoidentifier.substring(extracted.wmoidentifier.length - 4)}-${metadata.attributes.ttaaii}-${metadata.attributes.id.slice(-4)}`;
|
|
1648
1816
|
}
|
|
1817
|
+
/**
|
|
1818
|
+
* @function event
|
|
1819
|
+
* @description
|
|
1820
|
+
* Processes validated CAP alert messages, extracting relevant information and compiling it into structured event objects.
|
|
1821
|
+
*
|
|
1822
|
+
* @public
|
|
1823
|
+
* @static
|
|
1824
|
+
* @async
|
|
1825
|
+
* @param {types.StanzaCompiled} validated
|
|
1826
|
+
* @returns {*}
|
|
1827
|
+
*/
|
|
1649
1828
|
static event(validated) {
|
|
1650
1829
|
return __async(this, null, function* () {
|
|
1651
|
-
var _a;
|
|
1830
|
+
var _a, _b;
|
|
1652
1831
|
let processed = [];
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
for (
|
|
1656
|
-
const
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
`
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1832
|
+
const tick = performance.now();
|
|
1833
|
+
const blocks = (_a = validated.message.split(/\[SoF\]/gim)) == null ? void 0 : _a.map((msg) => msg.trim());
|
|
1834
|
+
for (const block of blocks) {
|
|
1835
|
+
const cachedAttribute = block.match(/STANZA ATTRIBUTES\.\.\.(\{.*\})/);
|
|
1836
|
+
const messages = (_b = block.split(/(?=\$\$)/g)) == null ? void 0 : _b.map((msg) => msg.trim());
|
|
1837
|
+
if (!messages || messages.length == 0) return;
|
|
1838
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1839
|
+
let message = messages[i];
|
|
1840
|
+
const attributes = cachedAttribute != null ? JSON.parse(cachedAttribute[1]) : validated;
|
|
1841
|
+
message = message.substring(message.indexOf(`<?xml version="1.0"`), message.lastIndexOf(`>`) + 1);
|
|
1842
|
+
const parser = new packages.xml2js.Parser({ explicitArray: false, mergeAttrs: true, trim: true });
|
|
1843
|
+
const parsed = yield parser.parseStringPromise(message);
|
|
1844
|
+
if (parsed == null || parsed.alert == null) continue;
|
|
1845
|
+
const extracted = text_default.getXmlValues(parsed, [
|
|
1846
|
+
`vtec`,
|
|
1847
|
+
`wmoidentifier`,
|
|
1848
|
+
`ugc`,
|
|
1849
|
+
`areadesc`,
|
|
1850
|
+
`expires`,
|
|
1851
|
+
`sent`,
|
|
1852
|
+
`msgtype`,
|
|
1853
|
+
`description`,
|
|
1854
|
+
`event`,
|
|
1855
|
+
`sendername`,
|
|
1856
|
+
`tornadodetection`,
|
|
1857
|
+
`polygon`,
|
|
1858
|
+
`maxHailSize`,
|
|
1859
|
+
`maxWindGust`,
|
|
1860
|
+
`thunderstormdamagethreat`,
|
|
1861
|
+
`tornadodamagethreat`,
|
|
1862
|
+
`waterspoutdetection`,
|
|
1863
|
+
`flooddetection`
|
|
1864
|
+
]);
|
|
1865
|
+
const getHeader = events_default.getHeader(__spreadValues({}, validated.attributes));
|
|
1866
|
+
const getSource = text_default.textProductToString(extracted.description, `SOURCE...`, [`.`]) || `N/A`;
|
|
1867
|
+
processed.push({
|
|
1868
|
+
performance: performance.now() - tick,
|
|
1869
|
+
source: `cap-parser`,
|
|
1870
|
+
tracking: this.getTracking(extracted, attributes),
|
|
1871
|
+
header: getHeader,
|
|
1872
|
+
vtec: extracted.vtec || `N/A`,
|
|
1873
|
+
history: [{ description: extracted.description || `N/A`, issued: extracted.sent ? new Date(extracted.sent).toLocaleString() : `N/A`, type: extracted.msgtype || `N/A` }],
|
|
1874
|
+
properties: {
|
|
1875
|
+
locations: extracted.areadesc || `N/A`,
|
|
1876
|
+
event: extracted.event || `N/A`,
|
|
1877
|
+
issued: extracted.sent ? new Date(extracted.sent).toLocaleString() : `N/A`,
|
|
1878
|
+
expires: extracted.expires ? new Date(extracted.expires).toLocaleString() : `N/A`,
|
|
1879
|
+
parent: extracted.event || `N/A`,
|
|
1880
|
+
action_type: extracted.msgtype || `N/A`,
|
|
1881
|
+
description: extracted.description || `N/A`,
|
|
1882
|
+
sender_name: extracted.sendername || `N/A`,
|
|
1883
|
+
sender_icao: extracted.wmoidentifier ? extracted.wmoidentifier.substring(extracted.wmoidentifier.length - 4) : `N/A`,
|
|
1884
|
+
attributes: validated.attributes,
|
|
1885
|
+
geocode: {
|
|
1886
|
+
UGC: [extracted.ugc]
|
|
1887
|
+
},
|
|
1888
|
+
parameters: {
|
|
1889
|
+
wmo: extracted.wmoidentifier || `N/A`,
|
|
1890
|
+
source: getSource,
|
|
1891
|
+
max_hail_size: extracted.maxHailSize || `N/A`,
|
|
1892
|
+
max_wind_gust: extracted.maxWindGust || `N/A`,
|
|
1893
|
+
damage_threat: extracted.thunderstormdamagethreat || `N/A`,
|
|
1894
|
+
tornado_detection: extracted.tornadodetection || extracted.waterspoutdetection || `N/A`,
|
|
1895
|
+
flood_detection: extracted.flooddetection || `N/A`,
|
|
1896
|
+
discussion_tornado_intensity: `N/A`,
|
|
1897
|
+
discussion_wind_intensity: `N/A`,
|
|
1898
|
+
discussion_hail_intensity: `N/A`
|
|
1899
|
+
},
|
|
1900
|
+
geometry: extracted.polygon ? {
|
|
1901
|
+
type: `Polygon`,
|
|
1902
|
+
coordinates: extracted.polygon.split(` `).map((coord) => {
|
|
1903
|
+
const [lat, lon] = coord.split(`,`).map((num) => parseFloat(num));
|
|
1904
|
+
return [lat, lon];
|
|
1905
|
+
})
|
|
1906
|
+
} : null
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1724
1910
|
}
|
|
1725
1911
|
events_default.validateEvents(processed);
|
|
1726
1912
|
});
|
|
@@ -1730,19 +1916,52 @@ var cap_default = CapAlerts;
|
|
|
1730
1916
|
|
|
1731
1917
|
// src/parsers/types/api.ts
|
|
1732
1918
|
var APIAlerts = class {
|
|
1919
|
+
/**
|
|
1920
|
+
* @function getTracking
|
|
1921
|
+
* @description
|
|
1922
|
+
* Generates a unique tracking identifier for a CAP alert based on extracted XML values.
|
|
1923
|
+
* If VTEC information is available, it constructs the tracking ID from the VTEC components.
|
|
1924
|
+
* Otherwise, it uses the WMO identifier along with TTAI and CCCC attributes.
|
|
1925
|
+
*
|
|
1926
|
+
* @private
|
|
1927
|
+
* @static
|
|
1928
|
+
* @param {Record<string, string>} extracted
|
|
1929
|
+
* @returns {string}
|
|
1930
|
+
*/
|
|
1733
1931
|
static getTracking(extracted) {
|
|
1734
1932
|
return extracted.vtec ? (() => {
|
|
1735
1933
|
const vtecValue = Array.isArray(extracted.vtec) ? extracted.vtec[0] : extracted.vtec;
|
|
1736
1934
|
const splitVTEC = vtecValue.split(".");
|
|
1737
1935
|
return `${splitVTEC[2]}-${splitVTEC[3]}-${splitVTEC[4]}-${splitVTEC[5]}`;
|
|
1738
|
-
})() : `${extracted.wmoidentifier}
|
|
1936
|
+
})() : `${extracted.wmoidentifier}`;
|
|
1739
1937
|
}
|
|
1938
|
+
/**
|
|
1939
|
+
* @function getICAO
|
|
1940
|
+
* @description
|
|
1941
|
+
* Extracts the sender's ICAO code and corresponding name from a VTEC string.
|
|
1942
|
+
*
|
|
1943
|
+
* @private
|
|
1944
|
+
* @static
|
|
1945
|
+
* @param {string} vtec
|
|
1946
|
+
* @returns {{ icao: any; name: any; }}
|
|
1947
|
+
*/
|
|
1740
1948
|
static getICAO(vtec) {
|
|
1741
1949
|
var _a, _b;
|
|
1742
1950
|
const icao = vtec ? vtec.split(`.`)[2] : `N/A`;
|
|
1743
1951
|
const name = (_b = (_a = definitions.ICAO) == null ? void 0 : _a[icao]) != null ? _b : `N/A`;
|
|
1744
1952
|
return { icao, name };
|
|
1745
1953
|
}
|
|
1954
|
+
/**
|
|
1955
|
+
* @function event
|
|
1956
|
+
* @description
|
|
1957
|
+
* Processes validated API alert messages, extracting relevant information and compiling it into structured event objects.
|
|
1958
|
+
*
|
|
1959
|
+
* @public
|
|
1960
|
+
* @static
|
|
1961
|
+
* @async
|
|
1962
|
+
* @param {types.StanzaCompiled} validated
|
|
1963
|
+
* @returns {*}
|
|
1964
|
+
*/
|
|
1746
1965
|
static event(validated) {
|
|
1747
1966
|
return __async(this, null, function* () {
|
|
1748
1967
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, __, _$, _aa, _ba, _ca, _da, _ea, _fa, _ga, _ha, _ia;
|
|
@@ -1761,6 +1980,7 @@ var APIAlerts = class {
|
|
|
1761
1980
|
const getOffice = this.getICAO(getVTEC || ``);
|
|
1762
1981
|
processed.push({
|
|
1763
1982
|
performance: performance.now() - tick,
|
|
1983
|
+
source: `api-parser`,
|
|
1764
1984
|
tracking: this.getTracking({ vtec: getVTEC, wmoidentifier: getWmo, ugc: getUgc ? getUgc.join(`,`) : null }),
|
|
1765
1985
|
header: getHeader,
|
|
1766
1986
|
vtec: getVTEC || `N/A`,
|
|
@@ -1799,7 +2019,7 @@ var APIAlerts = class {
|
|
|
1799
2019
|
type: ((_fa = feature == null ? void 0 : feature.geometry) == null ? void 0 : _fa.type) || "Polygon",
|
|
1800
2020
|
coordinates: (_ia = (_ha = (_ga = feature == null ? void 0 : feature.geometry) == null ? void 0 : _ga.coordinates) == null ? void 0 : _ha[0]) == null ? void 0 : _ia.map((coord) => {
|
|
1801
2021
|
const [lat, lon] = Array.isArray(coord) ? coord : [0, 0];
|
|
1802
|
-
return [
|
|
2022
|
+
return [lon, lat];
|
|
1803
2023
|
})
|
|
1804
2024
|
} : null
|
|
1805
2025
|
}
|
|
@@ -1811,1080 +2031,1410 @@ var APIAlerts = class {
|
|
|
1811
2031
|
};
|
|
1812
2032
|
var api_default = APIAlerts;
|
|
1813
2033
|
|
|
1814
|
-
// src/
|
|
1815
|
-
var
|
|
2034
|
+
// src/parsers/events.ts
|
|
2035
|
+
var EventParser = class {
|
|
1816
2036
|
/**
|
|
1817
|
-
*
|
|
2037
|
+
* @function getBaseProperties
|
|
2038
|
+
* @description
|
|
2039
|
+
* Extracts and compiles the core properties of a weather
|
|
2040
|
+
* alert message into a structured object. Combines parsed
|
|
2041
|
+
* textual data, UGC information, VTEC entries, and additional
|
|
2042
|
+
* metadata for downstream use.
|
|
1818
2043
|
*
|
|
1819
|
-
* @public
|
|
1820
2044
|
* @static
|
|
1821
|
-
* @
|
|
1822
|
-
* @param {string}
|
|
1823
|
-
* @
|
|
2045
|
+
* @async
|
|
2046
|
+
* @param {string} message
|
|
2047
|
+
* @param {types.StanzaCompiled} validated
|
|
2048
|
+
* @param {types.UGCEntry} [ugc=null]
|
|
2049
|
+
* @param {types.VtecEntry} [vtec=null]
|
|
2050
|
+
* @returns {Promise<Record<string, any>>}
|
|
1824
2051
|
*/
|
|
1825
|
-
static
|
|
1826
|
-
return
|
|
2052
|
+
static getBaseProperties(message, metadata, ugc = null, vtec = null) {
|
|
2053
|
+
return __async(this, null, function* () {
|
|
2054
|
+
var _a, _b;
|
|
1827
2055
|
const settings2 = settings;
|
|
1828
|
-
|
|
1829
|
-
message
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2056
|
+
const definitions2 = {
|
|
2057
|
+
tornado: text_default.textProductToString(message, `TORNADO...`) || text_default.textProductToString(message, `WATERSPOUT...`) || `N/A`,
|
|
2058
|
+
hail: text_default.textProductToString(message, `MAX HAIL SIZE...`, [`IN`]) || text_default.textProductToString(message, `HAIL...`, [`IN`]) || `N/A`,
|
|
2059
|
+
gusts: text_default.textProductToString(message, `MAX WIND GUST...`) || text_default.textProductToString(message, `WIND...`) || `N/A`,
|
|
2060
|
+
flood: text_default.textProductToString(message, `FLASH FLOOD...`) || `N/A`,
|
|
2061
|
+
damage: text_default.textProductToString(message, `DAMAGE THREAT...`) || `N/A`,
|
|
2062
|
+
source: text_default.textProductToString(message, `SOURCE...`, [`.`]) || `N/A`,
|
|
2063
|
+
polygon: text_default.textProductToPolygon(message),
|
|
2064
|
+
description: text_default.textProductToDescription(message, (_a = vtec == null ? void 0 : vtec.raw) != null ? _a : null),
|
|
2065
|
+
wmo: message.match(new RegExp(definitions.expressions.wmo, "imu")),
|
|
2066
|
+
mdTorIntensity: text_default.textProductToString(message, `MOST PROBABLE PEAK TORNADO INTENSITY...`) || `N/A`,
|
|
2067
|
+
mdWindGusts: text_default.textProductToString(message, `MOST PROBABLE PEAK WIND GUST...`) || `N/A`,
|
|
2068
|
+
mdHailSize: text_default.textProductToString(message, `MOST PROBABLE PEAK HAIL SIZE...`) || `N/A`
|
|
2069
|
+
};
|
|
2070
|
+
const getOffice = this.getICAO(vtec, metadata, definitions2.wmo);
|
|
2071
|
+
const getCorrectIssued = this.getCorrectIssuedDate(metadata);
|
|
2072
|
+
const getCorrectExpiry = this.getCorrectExpiryDate(vtec, ugc);
|
|
2073
|
+
const base = {
|
|
2074
|
+
locations: (ugc == null ? void 0 : ugc.locations.join(`; `)) || `No Location Specified (UGC Missing)`,
|
|
2075
|
+
issued: getCorrectIssued,
|
|
2076
|
+
expires: getCorrectExpiry,
|
|
2077
|
+
geocode: { UGC: (ugc == null ? void 0 : ugc.zones) || [`XX000`] },
|
|
2078
|
+
description: definitions2.description,
|
|
2079
|
+
sender_name: getOffice.name,
|
|
2080
|
+
sender_icao: getOffice.icao,
|
|
2081
|
+
metadata: __spreadValues({}, Object.fromEntries(Object.entries(metadata).filter(([key]) => key !== "message"))),
|
|
2082
|
+
parameters: {
|
|
2083
|
+
wmo: Array.isArray(definitions2.wmo) ? definitions2.wmo[0] : (_b = definitions2.wmo) != null ? _b : `N/A`,
|
|
2084
|
+
source: definitions2.source,
|
|
2085
|
+
max_hail_size: definitions2.hail,
|
|
2086
|
+
max_wind_gust: definitions2.gusts,
|
|
2087
|
+
damage_threat: definitions2.damage,
|
|
2088
|
+
tornado_detection: definitions2.tornado,
|
|
2089
|
+
flood_detection: definitions2.flood,
|
|
2090
|
+
discussion_tornado_intensity: definitions2.mdTorIntensity,
|
|
2091
|
+
discussion_wind_intensity: definitions2.mdWindGusts,
|
|
2092
|
+
discussion_hail_intensity: definitions2.mdHailSize
|
|
2093
|
+
},
|
|
2094
|
+
geometry: definitions2.polygon.length > 0 ? { type: "Polygon", coordinates: definitions2.polygon } : null
|
|
2095
|
+
};
|
|
2096
|
+
if (settings2.noaa_weather_wire_service_settings.preferences.shapefile_coordinates && base.geometry == null && ugc != null) {
|
|
2097
|
+
const coordinates = yield ugc_default.getCoordinates(ugc.zones);
|
|
2098
|
+
base.geometry = { type: "Polygon", coordinates };
|
|
1839
2099
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2100
|
+
return base;
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* @function betterParsedEventName
|
|
2105
|
+
* @description
|
|
2106
|
+
* Enhances the parsing of an event name using additional criteria
|
|
2107
|
+
* from its description and parameters. Can optionally use
|
|
2108
|
+
* the original parent event name instead.
|
|
2109
|
+
*
|
|
2110
|
+
* @static
|
|
2111
|
+
* @param {types.EventCompiled} event
|
|
2112
|
+
* @param {boolean} [betterParsing=false]
|
|
2113
|
+
* @param {boolean} [useParentEvents=false]
|
|
2114
|
+
* @returns {string}
|
|
2115
|
+
*/
|
|
2116
|
+
static betterParsedEventName(event, betterParsing, useParentEvents) {
|
|
2117
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2118
|
+
let eventName = (_b = (_a = event == null ? void 0 : event.properties) == null ? void 0 : _a.event) != null ? _b : `Unknown Event`;
|
|
2119
|
+
const defEventTable = definitions.enhancedEvents;
|
|
2120
|
+
const properties = event == null ? void 0 : event.properties;
|
|
2121
|
+
const parameters = properties == null ? void 0 : properties.parameters;
|
|
2122
|
+
const description = (_c = properties == null ? void 0 : properties.description) != null ? _c : `Unknown Description`;
|
|
2123
|
+
const damageThreatTag = (_d = parameters == null ? void 0 : parameters.damage_threat) != null ? _d : `N/A`;
|
|
2124
|
+
const tornadoThreatTag = (_e = parameters == null ? void 0 : parameters.tornado_detection) != null ? _e : `N/A`;
|
|
2125
|
+
if (!betterParsing) {
|
|
2126
|
+
return eventName;
|
|
2127
|
+
}
|
|
2128
|
+
for (const eventGroup of defEventTable) {
|
|
2129
|
+
const [baseEvent, conditions] = Object.entries(eventGroup)[0];
|
|
2130
|
+
if (eventName === baseEvent) {
|
|
2131
|
+
for (const [specificEvent, condition] of Object.entries(conditions)) {
|
|
2132
|
+
const conditionMet = condition.description && description.includes(condition.description.toLowerCase()) || condition.condition && condition.condition(damageThreatTag || tornadoThreatTag);
|
|
2133
|
+
if (conditionMet) {
|
|
2134
|
+
eventName = specificEvent;
|
|
2135
|
+
break;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
if (baseEvent === "Severe Thunderstorm Warning" && tornadoThreatTag === "POSSIBLE" && !eventName.includes("(TPROB)")) eventName += " (TPROB)";
|
|
2139
|
+
break;
|
|
1845
2140
|
}
|
|
1846
|
-
if (!packages.fs.existsSync(packages.path.join(assetsDir, `/output`))) {
|
|
1847
|
-
packages.fs.mkdirSync(packages.path.join(assetsDir, `/output`), { recursive: true });
|
|
1848
|
-
}
|
|
1849
|
-
packages.say.export(message, voice, 1, tmpTTS);
|
|
1850
|
-
yield utils_default.sleep(2500);
|
|
1851
|
-
let ttsBuffer = null;
|
|
1852
|
-
while (!packages.fs.existsSync(tmpTTS) || (ttsBuffer = packages.fs.readFileSync(tmpTTS)).length === 0) {
|
|
1853
|
-
yield utils_default.sleep(500);
|
|
1854
|
-
}
|
|
1855
|
-
const ttsWav = this.parseWavPCM16(ttsBuffer);
|
|
1856
|
-
const ttsSamples = this.resamplePCM16(ttsWav.samples, ttsWav.sampleRate, 8e3);
|
|
1857
|
-
const ttsRadio = this.applyNWREffect(ttsSamples, 8e3);
|
|
1858
|
-
let toneRadio = null;
|
|
1859
|
-
if (packages.fs.existsSync(settings2.global.easSettings.easIntroWav)) {
|
|
1860
|
-
const toneBuffer = packages.fs.readFileSync(settings2.global.easSettings.easIntroWav);
|
|
1861
|
-
const toneWav = this.parseWavPCM16(toneBuffer);
|
|
1862
|
-
const toneSamples = toneWav.sampleRate !== 8e3 ? this.resamplePCM16(toneWav.samples, toneWav.sampleRate, 8e3) : toneWav.samples;
|
|
1863
|
-
toneRadio = this.applyNWREffect(toneSamples, 8e3);
|
|
1864
|
-
}
|
|
1865
|
-
let build = toneRadio != null ? [toneRadio, this.generateSilence(0.5, 8e3)] : [];
|
|
1866
|
-
build.push(this.generateSAMEHeader(vtec, 3, 8e3, { preMarkSec: 1.1, gapSec: 0.5 }), this.generateSilence(0.5, 8e3), this.generateAttentionTone(8, 8e3), this.generateSilence(0.5, 8e3), ttsRadio);
|
|
1867
|
-
for (let i = 0; i < 3; i++) {
|
|
1868
|
-
build.push(this.generateSAMEHeader(vtec, 1, 8e3, { preMarkSec: 0.5, gapSec: 0.1 }));
|
|
1869
|
-
build.push(this.generateSilence(0.5, 8e3));
|
|
1870
|
-
}
|
|
1871
|
-
const allSamples = this.concatPCM16(build);
|
|
1872
|
-
const finalSamples = this.addNoise(allSamples, 2e-3);
|
|
1873
|
-
const outBuffer = this.encodeWavPCM16(Array.from(finalSamples).map((v) => ({ value: v })), 8e3);
|
|
1874
|
-
packages.fs.writeFileSync(outTTS, outBuffer);
|
|
1875
|
-
try {
|
|
1876
|
-
packages.fs.unlinkSync(tmpTTS);
|
|
1877
|
-
} catch (error) {
|
|
1878
|
-
if (error.code !== "EBUSY") {
|
|
1879
|
-
throw error;
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
return Promise.resolve(outTTS);
|
|
1883
|
-
}));
|
|
1884
|
-
}
|
|
1885
|
-
/**
|
|
1886
|
-
* encodeWavPCM16 encodes an array of samples into a WAV PCM 16-bit Buffer.
|
|
1887
|
-
*
|
|
1888
|
-
* @private
|
|
1889
|
-
* @static
|
|
1890
|
-
* @param {Record<string, number>[]} samples
|
|
1891
|
-
* @param {number} [sampleRate=8000]
|
|
1892
|
-
* @returns {Buffer}
|
|
1893
|
-
*/
|
|
1894
|
-
static encodeWavPCM16(samples, sampleRate = 8e3) {
|
|
1895
|
-
const bytesPerSample = 2;
|
|
1896
|
-
const blockAlign = 1 * bytesPerSample;
|
|
1897
|
-
const byteRate = sampleRate * blockAlign;
|
|
1898
|
-
const subchunk2Size = samples.length * bytesPerSample;
|
|
1899
|
-
const chunkSize = 36 + subchunk2Size;
|
|
1900
|
-
const buffer = Buffer.alloc(44 + subchunk2Size);
|
|
1901
|
-
let o = 0;
|
|
1902
|
-
buffer.write("RIFF", o);
|
|
1903
|
-
o += 4;
|
|
1904
|
-
buffer.writeUInt32LE(chunkSize, o);
|
|
1905
|
-
o += 4;
|
|
1906
|
-
buffer.write("WAVE", o);
|
|
1907
|
-
o += 4;
|
|
1908
|
-
buffer.write("fmt ", o);
|
|
1909
|
-
o += 4;
|
|
1910
|
-
buffer.writeUInt32LE(16, o);
|
|
1911
|
-
o += 4;
|
|
1912
|
-
buffer.writeUInt16LE(1, o);
|
|
1913
|
-
o += 2;
|
|
1914
|
-
buffer.writeUInt16LE(1, o);
|
|
1915
|
-
o += 2;
|
|
1916
|
-
buffer.writeUInt32LE(sampleRate, o);
|
|
1917
|
-
o += 4;
|
|
1918
|
-
buffer.writeUInt32LE(byteRate, o);
|
|
1919
|
-
o += 4;
|
|
1920
|
-
buffer.writeUInt16LE(blockAlign, o);
|
|
1921
|
-
o += 2;
|
|
1922
|
-
buffer.writeUInt16LE(16, o);
|
|
1923
|
-
o += 2;
|
|
1924
|
-
buffer.write("data", o);
|
|
1925
|
-
o += 4;
|
|
1926
|
-
buffer.writeUInt32LE(subchunk2Size, o);
|
|
1927
|
-
o += 4;
|
|
1928
|
-
for (let i = 0; i < samples.length; i++, o += 2) {
|
|
1929
|
-
buffer.writeInt16LE(samples[i].value, o);
|
|
1930
2141
|
}
|
|
1931
|
-
return
|
|
2142
|
+
return useParentEvents ? (_f = event == null ? void 0 : event.properties) == null ? void 0 : _f.event : eventName;
|
|
1932
2143
|
}
|
|
1933
2144
|
/**
|
|
1934
|
-
*
|
|
2145
|
+
* @function validateEvents
|
|
2146
|
+
* @description
|
|
2147
|
+
* Processes an array of event objects and filters them based on
|
|
2148
|
+
* global and EAS filtering settings, location constraints, and
|
|
2149
|
+
* other criteria such as expired or test products. Valid events
|
|
2150
|
+
* trigger relevant event emitters.
|
|
1935
2151
|
*
|
|
1936
|
-
* @private
|
|
1937
2152
|
* @static
|
|
1938
|
-
* @param {
|
|
1939
|
-
* @returns {
|
|
2153
|
+
* @param {unknown[]} events
|
|
2154
|
+
* @returns {void}
|
|
1940
2155
|
*/
|
|
1941
|
-
static
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
const
|
|
1953
|
-
if (
|
|
1954
|
-
|
|
1955
|
-
|
|
2156
|
+
static validateEvents(events2) {
|
|
2157
|
+
var _a, _b, _c, _d, _e;
|
|
2158
|
+
if (events2.length == 0) return;
|
|
2159
|
+
const filteringSettings = (_b = (_a = settings) == null ? void 0 : _a.global_settings) == null ? void 0 : _b.filtering;
|
|
2160
|
+
const locationSettings = filteringSettings == null ? void 0 : filteringSettings.location;
|
|
2161
|
+
const easSettings = (_d = (_c = settings) == null ? void 0 : _c.global_settings) == null ? void 0 : _d.eas_settings;
|
|
2162
|
+
const globalSettings = (_e = settings) == null ? void 0 : _e.global_settings;
|
|
2163
|
+
const sets = {};
|
|
2164
|
+
const bools = {};
|
|
2165
|
+
const megered = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, filteringSettings), easSettings), globalSettings), locationSettings);
|
|
2166
|
+
for (const key in megered) {
|
|
2167
|
+
const setting = megered[key];
|
|
2168
|
+
if (Array.isArray(setting)) {
|
|
2169
|
+
sets[key] = new Set(setting.map((item) => item.toLowerCase()));
|
|
2170
|
+
}
|
|
2171
|
+
if (typeof setting === "boolean") {
|
|
2172
|
+
bools[key] = setting;
|
|
2173
|
+
}
|
|
1956
2174
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2175
|
+
const filtered = events2.filter((alert) => {
|
|
2176
|
+
var _a2, _b2, _d2;
|
|
2177
|
+
const originalEvent = this.buildDefaultSignature(alert);
|
|
2178
|
+
const props = originalEvent == null ? void 0 : originalEvent.properties;
|
|
2179
|
+
const ugcs = (_b2 = (_a2 = props == null ? void 0 : props.geocode) == null ? void 0 : _a2.UGC) != null ? _b2 : [];
|
|
2180
|
+
const _c2 = originalEvent, { performance: performance2, header } = _c2, eventWithoutPerformance = __objRest(_c2, ["performance", "header"]);
|
|
2181
|
+
originalEvent.properties.parent = originalEvent.properties.event;
|
|
2182
|
+
originalEvent.properties.event = this.betterParsedEventName(originalEvent, bools == null ? void 0 : bools.better_event_parsing, bools == null ? void 0 : bools.parent_events_only);
|
|
2183
|
+
originalEvent.hash = packages.crypto.createHash("md5").update(JSON.stringify(eventWithoutPerformance)).digest("hex");
|
|
2184
|
+
originalEvent.properties.distance = this.getLocationDistances(props, bools == null ? void 0 : bools.filter, locationSettings == null ? void 0 : locationSettings.max_distance, locationSettings == null ? void 0 : locationSettings.unit);
|
|
2185
|
+
if (!((_d2 = originalEvent.properties.distance) == null ? void 0 : _d2.in_range) && (bools == null ? void 0 : bools.filter)) {
|
|
2186
|
+
return false;
|
|
2187
|
+
}
|
|
2188
|
+
if (originalEvent.properties.is_test == true && (bools == null ? void 0 : bools.ignore_text_products)) return false;
|
|
2189
|
+
if ((bools == null ? void 0 : bools.check_expired) && originalEvent.properties.is_cancelled == true) return false;
|
|
2190
|
+
for (const key in sets) {
|
|
2191
|
+
const setting = sets[key];
|
|
2192
|
+
if (key === "events" && setting.size > 0 && !setting.has(originalEvent.properties.event.toLowerCase())) return false;
|
|
2193
|
+
if (key === "ignored_events" && setting.size > 0 && setting.has(originalEvent.properties.event.toLowerCase())) return false;
|
|
2194
|
+
if (key === "filtered_icoa" && setting.size > 0 && props.sender_icao != null && !setting.has(props.sender_icao.toLowerCase())) return false;
|
|
2195
|
+
if (key === "ignored_icoa" && setting.size > 0 && props.sender_icao != null && setting.has(props.sender_icao.toLowerCase())) return false;
|
|
2196
|
+
if (key === "ugc_filter" && setting.size > 0 && ugcs.length > 0 && !ugcs.some((ugc) => setting.has(ugc.toLowerCase()))) return false;
|
|
2197
|
+
if (key === "state_filter" && setting.size > 0 && ugcs.length > 0 && !ugcs.some((ugc) => setting.has(ugc.substring(0, 2).toLowerCase()))) return false;
|
|
2198
|
+
}
|
|
2199
|
+
cache.events.emit(`on${originalEvent.properties.parent.replace(/\s+/g, "")}`);
|
|
2200
|
+
cache.events.emit(`on${originalEvent.properties.event.replace(/\s+/g, "")}`);
|
|
2201
|
+
return true;
|
|
2202
|
+
});
|
|
2203
|
+
if (filtered.length > 0) {
|
|
2204
|
+
cache.events.emit(`onAlerts`, filtered);
|
|
1964
2205
|
}
|
|
1965
|
-
const samples = new Int16Array(data.buffer, data.byteOffset, data.length / 2);
|
|
1966
|
-
return { samples: new Int16Array(samples), sampleRate, channels, bitsPerSample };
|
|
1967
2206
|
}
|
|
1968
2207
|
/**
|
|
1969
|
-
*
|
|
2208
|
+
* @function getHeader
|
|
2209
|
+
* @description
|
|
2210
|
+
* Constructs a standardized alert header string using provided
|
|
2211
|
+
* stanza attributes, event properties, and optional VTEC data.
|
|
1970
2212
|
*
|
|
1971
|
-
* @private
|
|
1972
2213
|
* @static
|
|
1973
|
-
* @param {
|
|
1974
|
-
* @
|
|
2214
|
+
* @param {types.StanzaAttributes} attributes
|
|
2215
|
+
* @param {types.EventProperties} [properties]
|
|
2216
|
+
* @param {types.VtecEntry} [vtec]
|
|
2217
|
+
* @returns {string}
|
|
1975
2218
|
*/
|
|
1976
|
-
static
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
}
|
|
1985
|
-
return
|
|
2219
|
+
static getHeader(attributes, properties, vtec) {
|
|
2220
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
2221
|
+
const parent = `ATSX`;
|
|
2222
|
+
const alertType = (_d = (_c = (_a = attributes == null ? void 0 : attributes.awipsType) == null ? void 0 : _a.type) != null ? _c : (_b = attributes == null ? void 0 : attributes.getAwip) == null ? void 0 : _b.prefix) != null ? _d : `XX`;
|
|
2223
|
+
const ugc = ((_e = properties == null ? void 0 : properties.geocode) == null ? void 0 : _e.UGC) != null ? (_f = properties == null ? void 0 : properties.geocode) == null ? void 0 : _f.UGC.join(`-`) : `000000`;
|
|
2224
|
+
const status = (vtec == null ? void 0 : vtec.status) || "Issued";
|
|
2225
|
+
const issued = (properties == null ? void 0 : properties.issued) != null ? (_g = new Date(properties == null ? void 0 : properties.issued)) == null ? void 0 : _g.toISOString().replace(/[-:]/g, "").split(".")[0] : (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").split(".")[0];
|
|
2226
|
+
const sender = (properties == null ? void 0 : properties.sender_icao) || `XXXX`;
|
|
2227
|
+
const header = `ZCZC-${parent}-${alertType}-${ugc}-${status}-${issued}-${sender}-`;
|
|
2228
|
+
return header;
|
|
1986
2229
|
}
|
|
1987
2230
|
/**
|
|
1988
|
-
*
|
|
2231
|
+
* @function eventHandler
|
|
2232
|
+
* @description
|
|
2233
|
+
* Routes a validated stanza object to the appropriate alert handler
|
|
2234
|
+
* based on its type flags: API, CAP, VTEC, UGC, or plain text.
|
|
1989
2235
|
*
|
|
1990
|
-
* @private
|
|
1991
2236
|
* @static
|
|
1992
|
-
* @param {
|
|
1993
|
-
* @returns {
|
|
2237
|
+
* @param {types.StanzaCompiled} validated
|
|
2238
|
+
* @returns {void}
|
|
1994
2239
|
*/
|
|
1995
|
-
static
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
return
|
|
2240
|
+
static eventHandler(metadata) {
|
|
2241
|
+
if (metadata.isApi) return api_default.event(metadata);
|
|
2242
|
+
if (metadata.isCap) return cap_default.event(metadata);
|
|
2243
|
+
if (!metadata.isCap && metadata.isVtec && metadata.isUGC) return vtec_default2.event(metadata);
|
|
2244
|
+
if (!metadata.isCap && !metadata.isVtec && metadata.isUGC) return ugc_default2.event(metadata);
|
|
2245
|
+
if (!metadata.isCap && !metadata.isVtec && !metadata.isUGC) return text_default2.event(metadata);
|
|
1999
2246
|
}
|
|
2000
2247
|
/**
|
|
2001
|
-
*
|
|
2248
|
+
* @function getICAO
|
|
2249
|
+
* @description
|
|
2250
|
+
* Determines the ICAO code and corresponding name for an event.
|
|
2251
|
+
* Priority is given to the VTEC tracking code, then the attributes' `cccc` property,
|
|
2252
|
+
* and finally the WMO code if available. Returns "N/A" if none are found.
|
|
2002
2253
|
*
|
|
2003
2254
|
* @private
|
|
2004
2255
|
* @static
|
|
2005
|
-
* @param {
|
|
2006
|
-
* @
|
|
2256
|
+
* @param {types.VtecEntry | null} vtec
|
|
2257
|
+
* @param {Record<string, string>} attributes
|
|
2258
|
+
* @param {RegExpMatchArray | string | null} WMO
|
|
2259
|
+
* @returns {{ icao: string; name: string }}
|
|
2007
2260
|
*/
|
|
2008
|
-
static
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
}
|
|
2014
|
-
return out;
|
|
2261
|
+
static getICAO(vtec, metadata, WMO) {
|
|
2262
|
+
var _a, _b, _c, _d;
|
|
2263
|
+
const icao = vtec != null ? vtec == null ? void 0 : vtec.tracking.split(`-`)[0] : (_b = (_a = metadata.attributes) == null ? void 0 : _a.cccc) != null ? _b : WMO != null ? Array.isArray(WMO) ? WMO[0] : WMO : `N/A`;
|
|
2264
|
+
const name = (_d = (_c = definitions.ICAO) == null ? void 0 : _c[icao]) != null ? _d : `N/A`;
|
|
2265
|
+
return { icao, name };
|
|
2015
2266
|
}
|
|
2016
2267
|
/**
|
|
2017
|
-
*
|
|
2268
|
+
* @function getCorrectIssuedDate
|
|
2269
|
+
* @description
|
|
2270
|
+
* Determines the issued date for an event based on the provided attributes.
|
|
2271
|
+
* Falls back to the current date and time if no valid issue date is available.
|
|
2018
2272
|
*
|
|
2019
2273
|
* @private
|
|
2020
2274
|
* @static
|
|
2021
|
-
* @param {
|
|
2022
|
-
* @
|
|
2023
|
-
* @param {number} targetRate
|
|
2024
|
-
* @returns {*}
|
|
2275
|
+
* @param {Record<string, string>} attributes
|
|
2276
|
+
* @returns {string}
|
|
2025
2277
|
*/
|
|
2026
|
-
static
|
|
2027
|
-
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
for (let i = 0; i < outLen; i++) {
|
|
2032
|
-
const pos = i / ratio;
|
|
2033
|
-
const i0 = Math.floor(pos);
|
|
2034
|
-
const i1 = Math.min(i0 + 1, int16.length - 1);
|
|
2035
|
-
const frac = pos - i0;
|
|
2036
|
-
const v = int16[i0] * (1 - frac) + int16[i1] * frac;
|
|
2037
|
-
out[i] = Math.round(v);
|
|
2038
|
-
}
|
|
2039
|
-
return out;
|
|
2278
|
+
static getCorrectIssuedDate(metadata) {
|
|
2279
|
+
var _a;
|
|
2280
|
+
const time = metadata.attributes.issue != null ? new Date(metadata.attributes.issue).toLocaleString() : ((_a = metadata.attributes) == null ? void 0 : _a.issue) != null ? new Date(metadata.attributes.issue).toLocaleString() : (/* @__PURE__ */ new Date()).toLocaleString();
|
|
2281
|
+
if (time == `Invalid Date`) return (/* @__PURE__ */ new Date()).toLocaleString();
|
|
2282
|
+
return time;
|
|
2040
2283
|
}
|
|
2041
2284
|
/**
|
|
2042
|
-
*
|
|
2285
|
+
* @function getCorrectExpiryDate
|
|
2286
|
+
* @description
|
|
2287
|
+
* Determines the most appropriate expiry date for an event using VTEC or UGC data.
|
|
2288
|
+
* Falls back to one hour from the current time if no valid expiry is available.
|
|
2043
2289
|
*
|
|
2044
2290
|
* @private
|
|
2045
2291
|
* @static
|
|
2046
|
-
* @param {
|
|
2047
|
-
* @param {
|
|
2048
|
-
* @returns {
|
|
2292
|
+
* @param {types.VtecEntry} vtec
|
|
2293
|
+
* @param {types.UGCEntry} ugc
|
|
2294
|
+
* @returns {string}
|
|
2049
2295
|
*/
|
|
2050
|
-
static
|
|
2051
|
-
|
|
2296
|
+
static getCorrectExpiryDate(vtec, ugc) {
|
|
2297
|
+
const time = (vtec == null ? void 0 : vtec.expires) && !isNaN(new Date(vtec.expires).getTime()) ? new Date(vtec.expires).toLocaleString() : (ugc == null ? void 0 : ugc.expiry) != null ? new Date(ugc.expiry).toLocaleString() : new Date((/* @__PURE__ */ new Date()).getTime() + 1 * 60 * 60 * 1e3).toLocaleString();
|
|
2298
|
+
if (time == `Invalid Date`) return new Date((/* @__PURE__ */ new Date()).getTime() + 1 * 60 * 60 * 1e3).toLocaleString();
|
|
2299
|
+
return time;
|
|
2052
2300
|
}
|
|
2053
2301
|
/**
|
|
2054
|
-
*
|
|
2302
|
+
* @function getLocationDistances
|
|
2303
|
+
* @description
|
|
2304
|
+
* Calculates distances from an event's geometry to all current tracked locations.
|
|
2305
|
+
* Optionally filters locations by a maximum distance.
|
|
2055
2306
|
*
|
|
2056
2307
|
* @private
|
|
2057
2308
|
* @static
|
|
2058
|
-
* @param {
|
|
2059
|
-
* @param {
|
|
2060
|
-
* @
|
|
2309
|
+
* @param {types.EventProperties} [properties]
|
|
2310
|
+
* @param {boolean} [isFiltered=false]
|
|
2311
|
+
* @param {number} [maxDistance]
|
|
2312
|
+
* @param {string} [unit='miles']
|
|
2313
|
+
* @returns {{ range?: Record<string, {unit: string, distance: number}>, in_range: boolean }}
|
|
2061
2314
|
*/
|
|
2062
|
-
static
|
|
2063
|
-
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2315
|
+
static getLocationDistances(properties, isFiltered, maxDistance, unit) {
|
|
2316
|
+
let inRange = false;
|
|
2317
|
+
const totalTracks = Object.keys(cache.currentLocations).length;
|
|
2318
|
+
if (properties.geometry != null) {
|
|
2319
|
+
for (const key in cache.currentLocations) {
|
|
2320
|
+
const coordinates = cache.currentLocations[key];
|
|
2321
|
+
const singleCoord = properties.geometry.coordinates;
|
|
2322
|
+
const center = singleCoord.reduce((acc, [lat, lon]) => [acc[0] + lat, acc[1] + lon], [0, 0]).map((sum) => sum / singleCoord.length);
|
|
2323
|
+
const validUnit = unit === "miles" || unit === "kilometers" ? unit : "miles";
|
|
2324
|
+
const distance = utils_default.calculateDistance({ lat: coordinates.lat, lon: coordinates.lon }, { lat: center[0], lon: center[1] }, validUnit);
|
|
2325
|
+
if (!properties.distance) {
|
|
2326
|
+
properties.distance = {};
|
|
2327
|
+
}
|
|
2328
|
+
properties.distance[key] = { unit, distance };
|
|
2329
|
+
}
|
|
2330
|
+
if (!isFiltered) {
|
|
2331
|
+
return { range: properties.distance, in_range: true };
|
|
2332
|
+
}
|
|
2333
|
+
for (const key in properties.distance) {
|
|
2334
|
+
if (properties.distance[key].distance <= maxDistance) {
|
|
2335
|
+
inRange = true;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
return { range: properties.distance, in_range: totalTracks == 0 ? true : inRange };
|
|
2078
2339
|
}
|
|
2079
|
-
return
|
|
2340
|
+
return { in_range: false };
|
|
2080
2341
|
}
|
|
2081
2342
|
/**
|
|
2082
|
-
*
|
|
2343
|
+
* @function buildDefaultSignature
|
|
2344
|
+
* @description
|
|
2345
|
+
* Populates default properties for an event object, including action type flags,
|
|
2346
|
+
* tags, and status updates. Determines if the event is issued, updated, or cancelled
|
|
2347
|
+
* based on correlations, description content, VTEC codes, and expiration time.
|
|
2083
2348
|
*
|
|
2084
2349
|
* @private
|
|
2085
2350
|
* @static
|
|
2086
|
-
* @param {
|
|
2087
|
-
* @
|
|
2088
|
-
* @returns {*}
|
|
2351
|
+
* @param {any} event
|
|
2352
|
+
* @returns {any}
|
|
2089
2353
|
*/
|
|
2090
|
-
static
|
|
2091
|
-
|
|
2092
|
-
const
|
|
2093
|
-
const
|
|
2094
|
-
const
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2097
|
-
const
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2354
|
+
static buildDefaultSignature(event) {
|
|
2355
|
+
var _a, _b;
|
|
2356
|
+
const props = (_a = event.properties) != null ? _a : {};
|
|
2357
|
+
const statusCorrelation = definitions.correlations.find((c) => c.type === props.action_type);
|
|
2358
|
+
const defEventTags = definitions.tags;
|
|
2359
|
+
const tags = Object.entries(defEventTags).filter(([key]) => props == null ? void 0 : props.description.toLowerCase().includes(key.toLowerCase())).map(([, value]) => value);
|
|
2360
|
+
props.tags = tags.length > 0 ? tags : [`N/A`];
|
|
2361
|
+
const setAction = (type) => {
|
|
2362
|
+
props.is_cancelled = type === `C`;
|
|
2363
|
+
props.is_updated = type === `U`;
|
|
2364
|
+
props.is_issued = type === `I`;
|
|
2365
|
+
};
|
|
2366
|
+
if (statusCorrelation) {
|
|
2367
|
+
props.action_type = (_b = statusCorrelation.forward) != null ? _b : props.action_type;
|
|
2368
|
+
props.is_updated = !!statusCorrelation.update;
|
|
2369
|
+
props.is_issued = !!statusCorrelation.new;
|
|
2370
|
+
props.is_cancelled = !!statusCorrelation.cancel;
|
|
2371
|
+
} else {
|
|
2372
|
+
setAction(`I`);
|
|
2105
2373
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
x[i] = yLP;
|
|
2374
|
+
if (props.description) {
|
|
2375
|
+
const detectedPhrase = definitions.cancelSignatures.find((sig) => props.description.toLowerCase().includes(sig.toLowerCase()));
|
|
2376
|
+
if (detectedPhrase) {
|
|
2377
|
+
setAction(`C`);
|
|
2378
|
+
}
|
|
2112
2379
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2380
|
+
if (event.vtec) {
|
|
2381
|
+
const getType = event.vtec.split(`.`)[0];
|
|
2382
|
+
const isTestProduct = definitions.productTypes[getType] == `Test Product`;
|
|
2383
|
+
if (isTestProduct) {
|
|
2384
|
+
setAction(`C`);
|
|
2385
|
+
props.is_test = true;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
if (new Date(props == null ? void 0 : props.expires).getTime() < (/* @__PURE__ */ new Date()).getTime()) {
|
|
2389
|
+
setAction(`C`);
|
|
2390
|
+
}
|
|
2391
|
+
return event;
|
|
2118
2392
|
}
|
|
2393
|
+
};
|
|
2394
|
+
var events_default = EventParser;
|
|
2395
|
+
|
|
2396
|
+
// src/database.ts
|
|
2397
|
+
var Database = class {
|
|
2119
2398
|
/**
|
|
2120
|
-
*
|
|
2399
|
+
* @function stanzaCacheImport
|
|
2400
|
+
* @description
|
|
2401
|
+
* Inserts a single NWWS stanza into the database cache. If the total number
|
|
2402
|
+
* of stanzas exceeds the configured maximum history, it deletes the oldest
|
|
2403
|
+
* entries to maintain the limit. Duplicate stanzas are ignored.
|
|
2121
2404
|
*
|
|
2122
|
-
* @private
|
|
2123
2405
|
* @static
|
|
2124
|
-
* @
|
|
2125
|
-
* @param {
|
|
2126
|
-
*
|
|
2406
|
+
* @async
|
|
2407
|
+
* @param {string} stanza
|
|
2408
|
+
* The raw stanza XML or text to store in the database.
|
|
2409
|
+
*
|
|
2410
|
+
* @returns {Promise<void>}
|
|
2411
|
+
* Resolves when the stanza has been inserted and any necessary pruning
|
|
2412
|
+
* of old stanzas has been performed.
|
|
2413
|
+
*
|
|
2414
|
+
* @example
|
|
2415
|
+
* await Database.stanzaCacheImport("<alert>...</alert>");
|
|
2127
2416
|
*/
|
|
2128
|
-
static
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2417
|
+
static stanzaCacheImport(stanza) {
|
|
2418
|
+
return __async(this, null, function* () {
|
|
2419
|
+
const settings2 = settings;
|
|
2420
|
+
try {
|
|
2421
|
+
const db = cache.db;
|
|
2422
|
+
if (!db) return;
|
|
2423
|
+
db.prepare(`INSERT OR IGNORE INTO stanzas (stanza) VALUES (?)`).run(stanza);
|
|
2424
|
+
const countRow = db.prepare(`SELECT COUNT(*) AS total FROM stanzas`).get();
|
|
2425
|
+
const totalRows = countRow.total;
|
|
2426
|
+
const maxHistory = settings2.noaa_weather_wire_service_settings.cache.max_db_history;
|
|
2427
|
+
if (totalRows > maxHistory) {
|
|
2428
|
+
const rowsToDelete = Math.floor((totalRows - maxHistory) / 2);
|
|
2429
|
+
if (rowsToDelete > 0) {
|
|
2430
|
+
db.prepare(`
|
|
2431
|
+
DELETE FROM stanzas
|
|
2432
|
+
WHERE rowid IN (
|
|
2433
|
+
SELECT rowid
|
|
2434
|
+
FROM stanzas
|
|
2435
|
+
ORDER BY rowid ASC
|
|
2436
|
+
LIMIT ?
|
|
2437
|
+
)
|
|
2438
|
+
`).run(rowsToDelete);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2443
|
+
utils_default.warn(`Failed to import stanza into cache: ${msg}`);
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
2135
2446
|
}
|
|
2136
2447
|
/**
|
|
2137
|
-
*
|
|
2448
|
+
* @function loadDatabase
|
|
2449
|
+
* @description
|
|
2450
|
+
* Initializes the application's SQLite database, creating necessary tables
|
|
2451
|
+
* for storing stanzas and shapefiles. If the shapefiles table is empty,
|
|
2452
|
+
* it imports predefined shapefiles from disk, processes their features,
|
|
2453
|
+
* and populates the database. Emits warnings during the import process.
|
|
2138
2454
|
*
|
|
2139
|
-
* @private
|
|
2140
2455
|
* @static
|
|
2141
|
-
* @
|
|
2142
|
-
* @returns {
|
|
2456
|
+
* @async
|
|
2457
|
+
* @returns {Promise<void>}
|
|
2458
|
+
* Resolves when the database and shapefiles have been initialized.
|
|
2459
|
+
*
|
|
2460
|
+
* @example
|
|
2461
|
+
* await Database.loadDatabase();
|
|
2462
|
+
* console.log('Database initialized and shapefiles imported.');
|
|
2143
2463
|
*/
|
|
2144
|
-
static
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2464
|
+
static loadDatabase() {
|
|
2465
|
+
return __async(this, null, function* () {
|
|
2466
|
+
const settings2 = settings;
|
|
2467
|
+
try {
|
|
2468
|
+
const { fs: fs2, path: path2, sqlite3: sqlite32, shapefile: shapefile2 } = packages;
|
|
2469
|
+
if (!fs2.existsSync(settings2.database)) fs2.writeFileSync(settings2.database, "");
|
|
2470
|
+
cache.db = new sqlite32(settings2.database);
|
|
2471
|
+
cache.db.prepare(`
|
|
2472
|
+
CREATE TABLE IF NOT EXISTS stanzas (
|
|
2473
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2474
|
+
stanza TEXT
|
|
2475
|
+
)
|
|
2476
|
+
`).run();
|
|
2477
|
+
cache.db.prepare(`
|
|
2478
|
+
CREATE TABLE IF NOT EXISTS shapefiles (
|
|
2479
|
+
id TEXT PRIMARY KEY,
|
|
2480
|
+
location TEXT,
|
|
2481
|
+
geometry TEXT
|
|
2482
|
+
)
|
|
2483
|
+
`).run();
|
|
2484
|
+
const shapefileCount = cache.db.prepare(`SELECT COUNT(*) AS count FROM shapefiles`).get().count;
|
|
2485
|
+
if (shapefileCount === 0) {
|
|
2486
|
+
utils_default.warn(definitions.messages.shapefile_creation);
|
|
2487
|
+
for (const shape of definitions.shapefiles) {
|
|
2488
|
+
const filepath = path2.resolve(__dirname, "../../shapefiles", shape.file);
|
|
2489
|
+
const { features } = yield shapefile2.read(filepath, filepath);
|
|
2490
|
+
utils_default.warn(`Importing ${features.length} entries from ${shape.file}...`);
|
|
2491
|
+
const insertStmt = cache.db.prepare(`
|
|
2492
|
+
INSERT OR REPLACE INTO shapefiles (id, location, geometry) VALUES (?, ?, ?)
|
|
2493
|
+
`);
|
|
2494
|
+
const insertTransaction = cache.db.transaction((entries) => {
|
|
2495
|
+
for (const feature of entries) {
|
|
2496
|
+
const { properties, geometry } = feature;
|
|
2497
|
+
let final, location;
|
|
2498
|
+
if (properties.FIPS) {
|
|
2499
|
+
final = `${properties.STATE}${shape.id}${properties.FIPS.substring(2)}`;
|
|
2500
|
+
location = `${properties.COUNTYNAME}, ${properties.STATE}`;
|
|
2501
|
+
} else if (properties.FULLSTAID) {
|
|
2502
|
+
final = `${properties.ST}${shape.id}${properties.WFO}`;
|
|
2503
|
+
location = `${properties.CITY}, ${properties.STATE}`;
|
|
2504
|
+
} else if (properties.STATE) {
|
|
2505
|
+
final = `${properties.STATE}${shape.id}${properties.ZONE}`;
|
|
2506
|
+
location = `${properties.NAME}, ${properties.STATE}`;
|
|
2507
|
+
} else {
|
|
2508
|
+
final = properties.ID;
|
|
2509
|
+
location = properties.NAME;
|
|
2510
|
+
}
|
|
2511
|
+
insertStmt.run(final, location, JSON.stringify(geometry));
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
insertTransaction(features);
|
|
2515
|
+
}
|
|
2516
|
+
utils_default.warn(definitions.messages.shapefile_creation_finished);
|
|
2517
|
+
}
|
|
2518
|
+
} catch (error) {
|
|
2519
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2520
|
+
utils_default.warn(`Failed to load database: ${msg}`);
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
2153
2523
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2524
|
+
};
|
|
2525
|
+
var database_default = Database;
|
|
2526
|
+
|
|
2527
|
+
// src/xmpp.ts
|
|
2528
|
+
var Xmpp = class {
|
|
2529
|
+
/**
|
|
2530
|
+
* @function isSessionReconnectionEligible
|
|
2531
|
+
* @description
|
|
2532
|
+
* Checks if the XMPP session has been inactive longer than the given interval
|
|
2533
|
+
* and, if so, attempts a controlled reconnection.
|
|
2156
2534
|
*
|
|
2157
|
-
* @
|
|
2535
|
+
* @async
|
|
2158
2536
|
* @static
|
|
2159
|
-
* @param {number
|
|
2160
|
-
* @
|
|
2161
|
-
* @returns {*}
|
|
2537
|
+
* @param {number} currentInterval
|
|
2538
|
+
* @returns {Promise<void>}
|
|
2162
2539
|
*/
|
|
2163
|
-
static
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
let phase = 0;
|
|
2171
|
-
let frac = 0;
|
|
2172
|
-
for (let b = 0; b < bits.length; b++) {
|
|
2173
|
-
const bit = bits[b];
|
|
2174
|
-
const freq = bit ? markFreq : spaceFreq;
|
|
2175
|
-
const samplesPerBit = sampleRate / baud + frac;
|
|
2176
|
-
const n = Math.round(samplesPerBit);
|
|
2177
|
-
frac = samplesPerBit - n;
|
|
2178
|
-
const inc = twoPi * freq / sampleRate;
|
|
2179
|
-
for (let i = 0; i < n; i++) {
|
|
2180
|
-
result.push(Math.round(Math.sin(phase) * amplitude * 32767));
|
|
2181
|
-
phase += inc;
|
|
2182
|
-
if (phase > twoPi) phase -= twoPi;
|
|
2540
|
+
static isSessionReconnectionEligible(currentInterval) {
|
|
2541
|
+
return __async(this, null, function* () {
|
|
2542
|
+
const settings2 = settings;
|
|
2543
|
+
const lastStanzaElapsed = Date.now() - cache.lastStanza;
|
|
2544
|
+
const threshold = currentInterval * 1e3;
|
|
2545
|
+
if (!cache.isConnected && !cache.sigHalt || !cache.session) {
|
|
2546
|
+
return;
|
|
2183
2547
|
}
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2548
|
+
if (lastStanzaElapsed < threshold) {
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
if (cache.attemptingReconnect) {
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
cache.attemptingReconnect = true;
|
|
2555
|
+
cache.isConnected = false;
|
|
2556
|
+
cache.totalReconnects += 1;
|
|
2557
|
+
try {
|
|
2558
|
+
cache.events.emit("onReconnection", {
|
|
2559
|
+
reconnects: cache.totalReconnects,
|
|
2560
|
+
lastStanza: lastStanzaElapsed,
|
|
2561
|
+
lastName: settings2.noaa_weather_wire_service_settings.credentials.nickname
|
|
2562
|
+
});
|
|
2563
|
+
yield cache.session.stop().catch(() => {
|
|
2564
|
+
});
|
|
2565
|
+
yield cache.session.start().catch(() => {
|
|
2566
|
+
});
|
|
2567
|
+
} catch (err) {
|
|
2568
|
+
utils_default.warn(`XMPP reconnection failed: ${err.message}`);
|
|
2569
|
+
} finally {
|
|
2570
|
+
cache.attemptingReconnect = false;
|
|
2571
|
+
}
|
|
2572
|
+
});
|
|
2192
2573
|
}
|
|
2193
2574
|
/**
|
|
2194
|
-
*
|
|
2575
|
+
* @function deploySession
|
|
2576
|
+
* @description
|
|
2577
|
+
* Initializes the NOAA Weather Wire Service (NWWS-OI) XMPP client session and
|
|
2578
|
+
* manages its lifecycle events including connection, disconnection, errors,
|
|
2579
|
+
* and message handling.
|
|
2195
2580
|
*
|
|
2196
|
-
* @
|
|
2581
|
+
* @async
|
|
2197
2582
|
* @static
|
|
2198
|
-
* @
|
|
2199
|
-
* @param {number} repeats
|
|
2200
|
-
* @param {number} [sampleRate=8000]
|
|
2201
|
-
* @param {{preMarkSec?: number, gapSec?: number}} [options={}]
|
|
2202
|
-
* @returns {*}
|
|
2583
|
+
* @returns {Promise<void>}
|
|
2203
2584
|
*/
|
|
2204
|
-
static
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2585
|
+
static deploySession() {
|
|
2586
|
+
return __async(this, null, function* () {
|
|
2587
|
+
var _a, _b;
|
|
2588
|
+
const settings2 = settings;
|
|
2589
|
+
(_b = (_a = settings2.noaa_weather_wire_service_settings.credentials).nickname) != null ? _b : _a.nickname = settings2.noaa_weather_wire_service_settings.credentials.username;
|
|
2590
|
+
cache.session = packages.xmpp.client({
|
|
2591
|
+
service: "xmpp://nwws-oi.weather.gov",
|
|
2592
|
+
domain: "nwws-oi.weather.gov",
|
|
2593
|
+
username: settings2.noaa_weather_wire_service_settings.credentials.username,
|
|
2594
|
+
password: settings2.noaa_weather_wire_service_settings.credentials.password
|
|
2595
|
+
});
|
|
2596
|
+
cache.session.on("online", (address) => __async(null, null, function* () {
|
|
2597
|
+
const now = Date.now();
|
|
2598
|
+
if (cache.lastConnect && now - cache.lastConnect < 1e4) {
|
|
2599
|
+
cache.sigHalt = true;
|
|
2600
|
+
utils_default.warn(definitions.messages.reconnect_too_fast);
|
|
2601
|
+
yield utils_default.sleep(2e3);
|
|
2602
|
+
yield cache.session.stop().catch(() => {
|
|
2603
|
+
});
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
cache.isConnected = true;
|
|
2607
|
+
cache.sigHalt = false;
|
|
2608
|
+
cache.lastConnect = now;
|
|
2609
|
+
cache.session.send(packages.xmpp.xml("presence", {
|
|
2610
|
+
to: `nwws@conference.nwws-oi.weather.gov/${settings2.noaa_weather_wire_service_settings.credentials.nickname}`,
|
|
2611
|
+
xmlns: "http://jabber.org/protocol/muc"
|
|
2612
|
+
}));
|
|
2613
|
+
cache.events.emit("onConnection", settings2.noaa_weather_wire_service_settings.credentials.nickname);
|
|
2614
|
+
if (cache.attemptingReconnect) return;
|
|
2615
|
+
cache.attemptingReconnect = true;
|
|
2616
|
+
yield utils_default.sleep(15e3);
|
|
2617
|
+
cache.attemptingReconnect = false;
|
|
2618
|
+
}));
|
|
2619
|
+
cache.session.on("offline", () => {
|
|
2620
|
+
cache.isConnected = false;
|
|
2621
|
+
cache.sigHalt = true;
|
|
2622
|
+
utils_default.warn("XMPP connection went offline");
|
|
2623
|
+
});
|
|
2624
|
+
cache.session.on("error", (error) => {
|
|
2625
|
+
cache.isConnected = false;
|
|
2626
|
+
cache.sigHalt = true;
|
|
2627
|
+
utils_default.warn(`XMPP connection error: ${error.message}`);
|
|
2628
|
+
});
|
|
2629
|
+
cache.session.on("stanza", (stanza) => __async(null, null, function* () {
|
|
2630
|
+
var _a2;
|
|
2631
|
+
try {
|
|
2632
|
+
cache.lastStanza = Date.now();
|
|
2633
|
+
if (stanza.is("message")) {
|
|
2634
|
+
const validate = stanza_default.validate(stanza);
|
|
2635
|
+
const skipMessage = validate.ignore || validate.isCap && !settings2.noaa_weather_wire_service_settings.preferences.cap_only || !validate.isCap && settings2.noaa_weather_wire_service_settings.preferences.cap_only || validate.isCap && !validate.isCapDescription;
|
|
2636
|
+
if (skipMessage) return;
|
|
2637
|
+
events_default.eventHandler(validate);
|
|
2638
|
+
database_default.stanzaCacheImport(JSON.stringify(validate));
|
|
2639
|
+
cache.events.emit("onMessage", validate);
|
|
2640
|
+
}
|
|
2641
|
+
if (stanza.is("presence") && ((_a2 = stanza.attrs.from) == null ? void 0 : _a2.startsWith("nwws@conference.nwws-oi.weather.gov/"))) {
|
|
2642
|
+
const occupant = stanza.attrs.from.split("/").slice(1).join("/");
|
|
2643
|
+
cache.events.emit("onOccupant", {
|
|
2644
|
+
occupant,
|
|
2645
|
+
type: stanza.attrs.type === "unavailable" ? "unavailable" : "available"
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
} catch (err) {
|
|
2649
|
+
utils_default.warn(`Error processing stanza: ${err.message}`);
|
|
2650
|
+
}
|
|
2651
|
+
}));
|
|
2652
|
+
try {
|
|
2653
|
+
yield cache.session.start();
|
|
2654
|
+
} catch (err) {
|
|
2655
|
+
utils_default.warn(`Failed to start XMPP session: ${err.message}`);
|
|
2217
2656
|
}
|
|
2218
|
-
|
|
2219
|
-
bursts.push(extendedBody);
|
|
2220
|
-
if (i !== repeats - 1) bursts.push(gap);
|
|
2221
|
-
}
|
|
2222
|
-
return this.concatPCM16(bursts);
|
|
2657
|
+
});
|
|
2223
2658
|
}
|
|
2224
2659
|
};
|
|
2225
|
-
var
|
|
2660
|
+
var xmpp_default = Xmpp;
|
|
2226
2661
|
|
|
2227
|
-
// src/
|
|
2228
|
-
var
|
|
2662
|
+
// src/utils.ts
|
|
2663
|
+
var Utils = class _Utils {
|
|
2229
2664
|
/**
|
|
2230
|
-
*
|
|
2665
|
+
* @function sleep
|
|
2666
|
+
* @description
|
|
2667
|
+
* Pauses execution for a specified number of milliseconds.
|
|
2231
2668
|
*
|
|
2232
|
-
* @public
|
|
2233
2669
|
* @static
|
|
2234
2670
|
* @async
|
|
2235
|
-
* @param {
|
|
2236
|
-
* @
|
|
2237
|
-
* @param {types.UGCParsed} [ugc=null]
|
|
2238
|
-
* @param {types.VTECParsed} [vtec=null]
|
|
2239
|
-
* @returns {Promise<types.BaseProperties>}
|
|
2671
|
+
* @param {number} ms
|
|
2672
|
+
* @returns {Promise<void>}
|
|
2240
2673
|
*/
|
|
2241
|
-
static
|
|
2674
|
+
static sleep(ms) {
|
|
2242
2675
|
return __async(this, null, function* () {
|
|
2243
|
-
|
|
2244
|
-
const settings2 = settings;
|
|
2245
|
-
const definitions2 = {
|
|
2246
|
-
tornado: text_default.textProductToString(message, `TORNADO...`) || text_default.textProductToString(message, `WATERSPOUT...`) || `N/A`,
|
|
2247
|
-
hail: text_default.textProductToString(message, `MAX HAIL SIZE...`, [`IN`]) || text_default.textProductToString(message, `HAIL...`, [`IN`]) || `N/A`,
|
|
2248
|
-
gusts: text_default.textProductToString(message, `MAX WIND GUST...`) || text_default.textProductToString(message, `WIND...`) || `N/A`,
|
|
2249
|
-
flood: text_default.textProductToString(message, `FLASH FLOOD...`) || `N/A`,
|
|
2250
|
-
damage: text_default.textProductToString(message, `DAMAGE THREAT...`) || `N/A`,
|
|
2251
|
-
source: text_default.textProductToString(message, `SOURCE...`, [`.`]) || `N/A`,
|
|
2252
|
-
attributes: text_default.textProductToString(message, `STANZA ATTRIBUTES...`) ? JSON.parse(text_default.textProductToString(message, `STANZA ATTRIBUTES...`)) : null,
|
|
2253
|
-
polygon: text_default.textProductToPolygon(message),
|
|
2254
|
-
description: text_default.textProductToDescription(message, (_a = vtec == null ? void 0 : vtec.raw) != null ? _a : null),
|
|
2255
|
-
wmo: message.match(new RegExp(definitions.expressions.wmo, "imu")),
|
|
2256
|
-
mdTorIntensity: text_default.textProductToString(message, `MOST PROBABLE PEAK TORNADO INTENSITY...`) || `N/A`,
|
|
2257
|
-
mdWindGusts: text_default.textProductToString(message, `MOST PROBABLE PEAK WIND GUST...`) || `N/A`,
|
|
2258
|
-
mdHailSize: text_default.textProductToString(message, `MOST PROBABLE PEAK HAIL SIZE...`) || `N/A`
|
|
2259
|
-
};
|
|
2260
|
-
const getOffice = this.getICAO(vtec, (_c = (_b = validated.attributes) != null ? _b : definitions2.attributes) != null ? _c : {}, definitions2.wmo);
|
|
2261
|
-
const getCorrectIssued = this.getCorrectIssuedDate((_e = (_d = definitions2.attributes) != null ? _d : validated.attributes) != null ? _e : {});
|
|
2262
|
-
const getCorrectExpiry = this.getCorrectExpiryDate(vtec, ugc);
|
|
2263
|
-
const getAwip = text_default.awipTextToEvent((_g = (_f = definitions2.attributes) == null ? void 0 : _f.awipsid) != null ? _g : validated.awipsType.prefix);
|
|
2264
|
-
const base = {
|
|
2265
|
-
locations: (ugc == null ? void 0 : ugc.locations.join(`; `)) || `No Location Specified (UGC Missing)`,
|
|
2266
|
-
issued: getCorrectIssued,
|
|
2267
|
-
expires: getCorrectExpiry,
|
|
2268
|
-
geocode: { UGC: (ugc == null ? void 0 : ugc.zones) || [`XX000`] },
|
|
2269
|
-
description: definitions2.description,
|
|
2270
|
-
sender_name: getOffice.name,
|
|
2271
|
-
sender_icao: getOffice.icao,
|
|
2272
|
-
attributes: __spreadProps(__spreadValues(__spreadValues({}, validated.attributes), definitions2.attributes), {
|
|
2273
|
-
getAwip
|
|
2274
|
-
}),
|
|
2275
|
-
parameters: {
|
|
2276
|
-
wmo: Array.isArray(definitions2.wmo) ? definitions2.wmo[0] : (_h = definitions2.wmo) != null ? _h : `N/A`,
|
|
2277
|
-
source: definitions2.source,
|
|
2278
|
-
max_hail_size: definitions2.hail,
|
|
2279
|
-
max_wind_gust: definitions2.gusts,
|
|
2280
|
-
damage_threat: definitions2.damage,
|
|
2281
|
-
tornado_detection: definitions2.tornado,
|
|
2282
|
-
flood_detection: definitions2.flood,
|
|
2283
|
-
discussion_tornado_intensity: definitions2.mdTorIntensity,
|
|
2284
|
-
discussion_wind_intensity: definitions2.mdWindGusts,
|
|
2285
|
-
discussion_hail_intensity: definitions2.mdHailSize
|
|
2286
|
-
},
|
|
2287
|
-
geometry: definitions2.polygon.length > 0 ? { type: "Polygon", coordinates: definitions2.polygon } : null
|
|
2288
|
-
};
|
|
2289
|
-
if (settings2.NoaaWeatherWireService.alertPreferences.isShapefileUGC && base.geometry == null && ugc != null) {
|
|
2290
|
-
const coordinates = yield ugc_default.getCoordinates(ugc.zones);
|
|
2291
|
-
base.geometry = { type: "Polygon", coordinates };
|
|
2292
|
-
}
|
|
2293
|
-
return base;
|
|
2676
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2294
2677
|
});
|
|
2295
2678
|
}
|
|
2296
2679
|
/**
|
|
2297
|
-
*
|
|
2680
|
+
* @function warn
|
|
2681
|
+
* @description
|
|
2682
|
+
* Emits a log event and prints a warning to the console. Throttles repeated
|
|
2683
|
+
* warnings within a short interval unless `force` is `true`.
|
|
2298
2684
|
*
|
|
2299
|
-
* @public
|
|
2300
2685
|
* @static
|
|
2301
|
-
* @param {
|
|
2302
|
-
* @
|
|
2686
|
+
* @param {string} message
|
|
2687
|
+
* @param {boolean} [force=false]
|
|
2303
2688
|
*/
|
|
2304
|
-
static
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2689
|
+
static warn(message, force = false) {
|
|
2690
|
+
cache.events.emit("log", message);
|
|
2691
|
+
if (!settings.journal) return;
|
|
2692
|
+
if (cache.lastWarn != null && Date.now() - cache.lastWarn < 500 && !force) return;
|
|
2693
|
+
cache.lastWarn = Date.now();
|
|
2694
|
+
console.warn(`\x1B[33m[ATMOSX-PARSER]\x1B[0m [${(/* @__PURE__ */ new Date()).toLocaleString()}] ${message}`);
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* @function loadCollectionCache
|
|
2698
|
+
* @description
|
|
2699
|
+
* Loads cached NWWS messages from disk, validates them, and passes them
|
|
2700
|
+
* to the event parser. Honors CAP preferences and ignores empty or
|
|
2701
|
+
* incompatible files.
|
|
2702
|
+
*
|
|
2703
|
+
* @static
|
|
2704
|
+
* @async
|
|
2705
|
+
*/
|
|
2706
|
+
static loadCollectionCache() {
|
|
2707
|
+
return __async(this, null, function* () {
|
|
2708
|
+
try {
|
|
2709
|
+
const settings2 = settings;
|
|
2710
|
+
if (settings2.noaa_weather_wire_service_settings.cache.enabled && settings2.noaa_weather_wire_service_settings.cache.directory) {
|
|
2711
|
+
if (!packages.fs.existsSync(settings2.noaa_weather_wire_service_settings.cache.directory)) return;
|
|
2712
|
+
const cacheDir = settings2.noaa_weather_wire_service_settings.cache.directory;
|
|
2713
|
+
const getAllFiles = packages.fs.readdirSync(cacheDir).filter((file) => file.endsWith(".bin") && file.startsWith("cache-"));
|
|
2714
|
+
this.warn(definitions.messages.dump_cache.replace(`{count}`, getAllFiles.length.toString()), true);
|
|
2715
|
+
yield this.sleep(2e3);
|
|
2716
|
+
for (const file of getAllFiles) {
|
|
2717
|
+
const filepath = packages.path.join(cacheDir, file);
|
|
2718
|
+
const readFile = packages.fs.readFileSync(filepath, { encoding: "utf-8" });
|
|
2719
|
+
const readSize = packages.fs.statSync(filepath).size;
|
|
2720
|
+
if (readSize == 0) {
|
|
2721
|
+
continue;
|
|
2722
|
+
}
|
|
2723
|
+
const isCap = readFile.includes(`<?xml`);
|
|
2724
|
+
if (isCap && !settings2.noaa_weather_wire_service_settings.preferences.cap_only) continue;
|
|
2725
|
+
if (!isCap && settings2.noaa_weather_wire_service_settings.preferences.cap_only) continue;
|
|
2726
|
+
const validate = stanza_default.validate(readFile, { isCap, raw: true });
|
|
2727
|
+
yield events_default.eventHandler(validate);
|
|
2322
2728
|
}
|
|
2729
|
+
this.warn(definitions.messages.dump_cache_complete, true);
|
|
2323
2730
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2731
|
+
} catch (error) {
|
|
2732
|
+
_Utils.warn(`Failed to load cache: ${error.stack}`);
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* @function loadGeoJsonData
|
|
2738
|
+
* @description
|
|
2739
|
+
* Fetches GeoJSON data from the National Weather Service endpoint and
|
|
2740
|
+
* passes it to the event parser for processing.
|
|
2741
|
+
*
|
|
2742
|
+
* @static
|
|
2743
|
+
* @async
|
|
2744
|
+
*/
|
|
2745
|
+
static loadGeoJsonData() {
|
|
2746
|
+
return __async(this, null, function* () {
|
|
2747
|
+
try {
|
|
2748
|
+
const settings2 = settings;
|
|
2749
|
+
const response = yield this.createHttpRequest(
|
|
2750
|
+
settings2.national_weather_service_settings.endpoint
|
|
2751
|
+
);
|
|
2752
|
+
if (response.error) return;
|
|
2753
|
+
events_default.eventHandler({
|
|
2754
|
+
message: JSON.stringify(response.message),
|
|
2755
|
+
attributes: {},
|
|
2756
|
+
isCap: true,
|
|
2757
|
+
isApi: true,
|
|
2758
|
+
isVtec: false,
|
|
2759
|
+
isUGC: false,
|
|
2760
|
+
isCapDescription: false,
|
|
2761
|
+
awipsType: { type: "api", prefix: "AP" },
|
|
2762
|
+
ignore: false
|
|
2763
|
+
});
|
|
2764
|
+
} catch (error) {
|
|
2765
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2766
|
+
_Utils.warn(`Failed to load National Weather Service GeoJSON Data: ${msg}`);
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* @function createHttpRequest
|
|
2772
|
+
* @description
|
|
2773
|
+
* Performs an HTTP GET request with default headers and timeout, returning
|
|
2774
|
+
* either the response data or an error message.
|
|
2775
|
+
*
|
|
2776
|
+
* @static
|
|
2777
|
+
* @template T
|
|
2778
|
+
* @param {string} url
|
|
2779
|
+
* @param {types.HTTPSettings} [options]
|
|
2780
|
+
* @returns {Promise<{ error: boolean; message: T | string }>}
|
|
2781
|
+
*/
|
|
2782
|
+
static createHttpRequest(url, options) {
|
|
2783
|
+
return __async(this, null, function* () {
|
|
2784
|
+
var _a;
|
|
2785
|
+
const defaultOptions = {
|
|
2786
|
+
timeout: 1e4,
|
|
2787
|
+
headers: {
|
|
2788
|
+
"User-Agent": "AtmosphericX",
|
|
2789
|
+
"Accept": "application/geo+json, text/plain, */*; q=0.9",
|
|
2790
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
2791
|
+
}
|
|
2792
|
+
};
|
|
2793
|
+
const requestOptions = __spreadProps(__spreadValues(__spreadValues({}, defaultOptions), options), {
|
|
2794
|
+
headers: __spreadValues(__spreadValues({}, defaultOptions.headers), (_a = options == null ? void 0 : options.headers) != null ? _a : {})
|
|
2795
|
+
});
|
|
2796
|
+
try {
|
|
2797
|
+
const resp = yield packages.axios.get(url, {
|
|
2798
|
+
headers: requestOptions.headers,
|
|
2799
|
+
timeout: requestOptions.timeout,
|
|
2800
|
+
maxRedirects: 0,
|
|
2801
|
+
validateStatus: (status) => status === 200 || status === 500
|
|
2802
|
+
});
|
|
2803
|
+
return { error: false, message: resp.data };
|
|
2804
|
+
} catch (err) {
|
|
2805
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2806
|
+
return { error: true, message: msg };
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
/**
|
|
2811
|
+
* @function garbageCollectionCache
|
|
2812
|
+
* @description
|
|
2813
|
+
* Deletes cache files exceeding the specified size limit to free disk space.
|
|
2814
|
+
* Recursively traverses the cache directory and removes files larger than
|
|
2815
|
+
* the given maximum.
|
|
2816
|
+
*
|
|
2817
|
+
* @static
|
|
2818
|
+
* @param {number} maxFileMegabytes
|
|
2819
|
+
*/
|
|
2820
|
+
static garbageCollectionCache(maxFileMegabytes) {
|
|
2821
|
+
try {
|
|
2822
|
+
const settings2 = settings;
|
|
2823
|
+
const cacheDir = settings2.noaa_weather_wire_service_settings.cache.directory;
|
|
2824
|
+
if (!cacheDir) return;
|
|
2825
|
+
const { fs: fs2, path: path2 } = packages;
|
|
2826
|
+
if (!fs2.existsSync(cacheDir)) return;
|
|
2827
|
+
const maxBytes = maxFileMegabytes * 1024 * 1024;
|
|
2828
|
+
const stackDirs = [cacheDir];
|
|
2829
|
+
const files = [];
|
|
2830
|
+
while (stackDirs.length) {
|
|
2831
|
+
const currentDir = stackDirs.pop();
|
|
2832
|
+
fs2.readdirSync(currentDir).forEach((file) => {
|
|
2833
|
+
const fullPath = path2.join(currentDir, file);
|
|
2834
|
+
const stat = fs2.statSync(fullPath);
|
|
2835
|
+
if (stat.isDirectory()) stackDirs.push(fullPath);
|
|
2836
|
+
else files.push({ file: fullPath, size: stat.size });
|
|
2837
|
+
});
|
|
2326
2838
|
}
|
|
2839
|
+
files.forEach((f) => {
|
|
2840
|
+
if (f.size > maxBytes) fs2.unlinkSync(f.file);
|
|
2841
|
+
});
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2844
|
+
_Utils.warn(`Failed to perform garbage collection: ${msg}`);
|
|
2327
2845
|
}
|
|
2328
|
-
const tags = Object.entries(defEventTags).filter(([key]) => description.includes(key.toLowerCase())).map(([, value]) => value);
|
|
2329
|
-
return { eventName, tags: tags.length > 0 ? tags : [`N/A`] };
|
|
2330
2846
|
}
|
|
2331
2847
|
/**
|
|
2332
|
-
*
|
|
2848
|
+
* @function handleCronJob
|
|
2849
|
+
* @description
|
|
2850
|
+
* Performs scheduled tasks for NWWS XMPP session maintenance or GeoJSON data
|
|
2851
|
+
* updates depending on the job type.
|
|
2333
2852
|
*
|
|
2334
|
-
* @public
|
|
2335
2853
|
* @static
|
|
2336
|
-
* @param {
|
|
2854
|
+
* @param {boolean} isWire
|
|
2337
2855
|
*/
|
|
2338
|
-
static
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2856
|
+
static handleCronJob(isWire) {
|
|
2857
|
+
try {
|
|
2858
|
+
const settings2 = settings;
|
|
2859
|
+
const cache2 = settings2.noaa_weather_wire_service_settings.cache;
|
|
2860
|
+
const reconnections = settings2.noaa_weather_wire_service_settings.reconnection_settings;
|
|
2861
|
+
if (isWire) {
|
|
2862
|
+
if (cache2.enabled) {
|
|
2863
|
+
void this.garbageCollectionCache(cache2.max_file_size);
|
|
2864
|
+
}
|
|
2865
|
+
if (reconnections.enabled) {
|
|
2866
|
+
void xmpp_default.isSessionReconnectionEligible(reconnections.interval);
|
|
2867
|
+
}
|
|
2868
|
+
} else {
|
|
2869
|
+
void this.loadGeoJsonData();
|
|
2352
2870
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2871
|
+
} catch (error) {
|
|
2872
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2873
|
+
_Utils.warn(`Failed to perform scheduled tasks (${isWire ? "NWWS" : "GeoJSON"}): ${msg}`);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* @function mergeClientSettings
|
|
2878
|
+
* @description
|
|
2879
|
+
* Recursively merges a ClientSettings object into a target object,
|
|
2880
|
+
* preserving nested structures and overriding existing values.
|
|
2881
|
+
*
|
|
2882
|
+
* @static
|
|
2883
|
+
* @param {Record<string, unknown>} target
|
|
2884
|
+
* @param {types.ClientSettingsTypes} settings
|
|
2885
|
+
* @returns {Record<string, unknown>}
|
|
2886
|
+
*/
|
|
2887
|
+
static mergeClientSettings(target, settings2) {
|
|
2888
|
+
for (const key in settings2) {
|
|
2889
|
+
if (!Object.prototype.hasOwnProperty.call(settings2, key)) continue;
|
|
2890
|
+
const value = settings2[key];
|
|
2891
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2892
|
+
if (!target[key] || typeof target[key] !== "object" || Array.isArray(target[key])) {
|
|
2893
|
+
target[key] = {};
|
|
2894
|
+
}
|
|
2895
|
+
this.mergeClientSettings(target[key], value);
|
|
2896
|
+
} else {
|
|
2897
|
+
target[key] = value;
|
|
2355
2898
|
}
|
|
2356
2899
|
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2900
|
+
return target;
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* @function calculateDistance
|
|
2904
|
+
* @description
|
|
2905
|
+
* Calculates the great-circle distance between two geographic coordinates
|
|
2906
|
+
* using the haversine formula.
|
|
2907
|
+
*
|
|
2908
|
+
* @static
|
|
2909
|
+
* @param {types.Coordinates} coord1
|
|
2910
|
+
* @param {types.Coordinates} coord2
|
|
2911
|
+
* @param {'miles' | 'kilometers'} [unit='miles']
|
|
2912
|
+
* @returns {number}
|
|
2913
|
+
*/
|
|
2914
|
+
static calculateDistance(coord1, coord2, unit = "miles") {
|
|
2915
|
+
if (!coord1 || !coord2) return 0;
|
|
2916
|
+
const { lat: lat1, lon: lon1 } = coord1;
|
|
2917
|
+
const { lat: lat2, lon: lon2 } = coord2;
|
|
2918
|
+
if ([lat1, lon1, lat2, lon2].some((v) => typeof v !== "number")) return 0;
|
|
2919
|
+
const toRad = (deg) => deg * Math.PI / 180;
|
|
2920
|
+
const R = unit === "miles" ? 3958.8 : 6371;
|
|
2921
|
+
const dLat = toRad(lat2 - lat1);
|
|
2922
|
+
const dLon = toRad(lon2 - lon1);
|
|
2923
|
+
const a = __pow(Math.sin(dLat / 2), 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * __pow(Math.sin(dLon / 2), 2);
|
|
2924
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
2925
|
+
return Math.round(R * c * 100) / 100;
|
|
2926
|
+
}
|
|
2927
|
+
/**
|
|
2928
|
+
* @function isReadyToProcess
|
|
2929
|
+
* @description
|
|
2930
|
+
* Determines whether processing can continue based on the current
|
|
2931
|
+
* tracked locations and filter state. Emits limited warnings if no
|
|
2932
|
+
* locations are available.
|
|
2933
|
+
*
|
|
2934
|
+
* @static
|
|
2935
|
+
* @param {boolean} isFiltering
|
|
2936
|
+
* @returns {boolean}
|
|
2937
|
+
*/
|
|
2938
|
+
static isReadyToProcess(isFiltering) {
|
|
2939
|
+
const totalTracks = Object.keys(cache.currentLocations).length;
|
|
2940
|
+
if (totalTracks > 0) {
|
|
2941
|
+
cache.totalLocationWarns = 0;
|
|
2942
|
+
return true;
|
|
2943
|
+
}
|
|
2944
|
+
if (!isFiltering) return true;
|
|
2945
|
+
if (cache.totalLocationWarns < 3) {
|
|
2946
|
+
_Utils.warn(definitions.messages.no_current_locations);
|
|
2947
|
+
cache.totalLocationWarns++;
|
|
2948
|
+
return false;
|
|
2949
|
+
}
|
|
2950
|
+
_Utils.warn(definitions.messages.disabled_location_warning, true);
|
|
2951
|
+
return true;
|
|
2952
|
+
}
|
|
2953
|
+
};
|
|
2954
|
+
var utils_default = Utils;
|
|
2955
|
+
|
|
2956
|
+
// src/eas.ts
|
|
2957
|
+
var EAS = class {
|
|
2958
|
+
/**
|
|
2959
|
+
* @function generateEASAudio
|
|
2960
|
+
* @description
|
|
2961
|
+
* Generates an EAS (Emergency Alert System) audio file for a given message
|
|
2962
|
+
* and SAME/VTEC code. The audio is composed of optional intro tones, SAME
|
|
2963
|
+
* headers, attention tones, TTS narration of the message, and repeated
|
|
2964
|
+
* SAME headers. The resulting audio is processed for NWR-style broadcast
|
|
2965
|
+
* quality and saved as a WAV file.
|
|
2966
|
+
*
|
|
2967
|
+
* @static
|
|
2968
|
+
* @async
|
|
2969
|
+
* @param {string} message
|
|
2970
|
+
* @param {string} vtec
|
|
2971
|
+
* @returns {Promise<string | null>}
|
|
2972
|
+
*/
|
|
2973
|
+
static generateEASAudio(message, vtec) {
|
|
2974
|
+
return new Promise((resolve) => __async(this, null, function* () {
|
|
2975
|
+
const settings2 = settings;
|
|
2976
|
+
const assetsDir = settings2.global_settings.eas_settings.directory;
|
|
2977
|
+
const rngFile = `${vtec.replace(/[^a-zA-Z0-9]/g, `_`)}`.substring(0, 32).replace(/^_+|_+$/g, "");
|
|
2978
|
+
const os2 = packages.os.platform();
|
|
2979
|
+
for (const { regex, replacement } of definitions.messageSignatures) {
|
|
2980
|
+
message = message.replace(regex, replacement);
|
|
2367
2981
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2982
|
+
if (!assetsDir) {
|
|
2983
|
+
utils_default.warn(definitions.messages.eas_no_directory);
|
|
2984
|
+
return resolve(null);
|
|
2985
|
+
}
|
|
2986
|
+
if (!packages.fs.existsSync(assetsDir)) {
|
|
2987
|
+
packages.fs.mkdirSync(assetsDir);
|
|
2988
|
+
}
|
|
2989
|
+
const tmpTTS = packages.path.join(assetsDir, `/tmp/${rngFile}.wav`);
|
|
2990
|
+
const outTTS = packages.path.join(assetsDir, `/output/${rngFile}.wav`);
|
|
2991
|
+
const voice = process.platform === "win32" ? "Microsoft David Desktop" : "en-US-GuyNeural";
|
|
2992
|
+
if (!packages.fs.existsSync(packages.path.join(assetsDir, `/tmp`))) {
|
|
2993
|
+
packages.fs.mkdirSync(packages.path.join(assetsDir, `/tmp`), { recursive: true });
|
|
2994
|
+
}
|
|
2995
|
+
if (!packages.fs.existsSync(packages.path.join(assetsDir, `/output`))) {
|
|
2996
|
+
packages.fs.mkdirSync(packages.path.join(assetsDir, `/output`), { recursive: true });
|
|
2997
|
+
}
|
|
2998
|
+
if (os2 == "win32") {
|
|
2999
|
+
packages.say.export(message, voice, 1, tmpTTS);
|
|
3000
|
+
}
|
|
3001
|
+
if (os2 == "linux") {
|
|
3002
|
+
message = message.replace(/[\r\n]+/g, " ");
|
|
3003
|
+
const festivalCommand = `echo "${message.replace(/"/g, '\\"')}" | text2wave -o "${tmpTTS}"`;
|
|
3004
|
+
packages.child.execSync(festivalCommand);
|
|
3005
|
+
}
|
|
3006
|
+
yield utils_default.sleep(3500);
|
|
3007
|
+
let ttsBuffer = null;
|
|
3008
|
+
while (!packages.fs.existsSync(tmpTTS) || (ttsBuffer = packages.fs.readFileSync(tmpTTS)).length === 0) {
|
|
3009
|
+
yield utils_default.sleep(25);
|
|
3010
|
+
}
|
|
3011
|
+
const ttsWav = this.parseWavPCM16(ttsBuffer);
|
|
3012
|
+
const ttsSamples = this.resamplePCM16(ttsWav.samples, ttsWav.sampleRate, 8e3);
|
|
3013
|
+
const ttsRadio = this.applyNWREffect(ttsSamples, 8e3);
|
|
3014
|
+
let toneRadio = null;
|
|
3015
|
+
if (packages.fs.existsSync(settings2.global_settings.eas_settings.intro_wav)) {
|
|
3016
|
+
const toneBuffer = packages.fs.readFileSync(settings2.global_settings.eas_settings.intro_wav);
|
|
3017
|
+
const toneWav = this.parseWavPCM16(toneBuffer);
|
|
3018
|
+
if (toneWav == null) {
|
|
3019
|
+
console.log(`[EAS] Intro tone WAV file is not valid PCM 16-bit format.`);
|
|
3020
|
+
return resolve(null);
|
|
2380
3021
|
}
|
|
3022
|
+
const toneSamples = toneWav.sampleRate !== 8e3 ? this.resamplePCM16(toneWav.samples, toneWav.sampleRate, 8e3) : toneWav.samples;
|
|
3023
|
+
toneRadio = this.applyNWREffect(toneSamples, 8e3);
|
|
2381
3024
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
originalEvent.properties.is_updated = statusCorrelation ? statusCorrelation.update == true : false;
|
|
2388
|
-
originalEvent.properties.is_issued = statusCorrelation ? statusCorrelation.new == true : false;
|
|
2389
|
-
originalEvent.properties.is_cancelled = statusCorrelation ? statusCorrelation.cancel == true : false;
|
|
2390
|
-
originalEvent.hash = packages.crypto.createHash("md5").update(JSON.stringify(eventWithoutPerformance)).digest("hex");
|
|
2391
|
-
if (props.description) {
|
|
2392
|
-
const detectedPhrase = definitions.cancelSignatures.find((sig) => props.description.toLowerCase().includes(sig.toLowerCase()));
|
|
2393
|
-
if (detectedPhrase) {
|
|
2394
|
-
originalEvent.properties.action_type = "Cancel";
|
|
2395
|
-
originalEvent.properties.is_cancelled = true;
|
|
2396
|
-
}
|
|
3025
|
+
let build = toneRadio != null ? [toneRadio, this.generateSilence(0.5, 8e3)] : [];
|
|
3026
|
+
build.push(this.generateSAMEHeader(vtec, 3, 8e3, { preMarkSec: 1.1, gapSec: 0.5 }), this.generateSilence(0.5, 8e3), this.generateAttentionTone(8, 8e3), this.generateSilence(0.5, 8e3), ttsRadio);
|
|
3027
|
+
for (let i = 0; i < 3; i++) {
|
|
3028
|
+
build.push(this.generateSAMEHeader(vtec, 1, 8e3, { preMarkSec: 0.5, gapSec: 0.1 }));
|
|
3029
|
+
build.push(this.generateSilence(0.5, 8e3));
|
|
2397
3030
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
3031
|
+
const allSamples = this.concatPCM16(build);
|
|
3032
|
+
const finalSamples = this.addNoise(allSamples, 2e-3);
|
|
3033
|
+
const outBuffer = this.encodeWavPCM16(Array.from(finalSamples).map((v) => ({ value: v })), 8e3);
|
|
3034
|
+
packages.fs.writeFileSync(outTTS, outBuffer);
|
|
3035
|
+
try {
|
|
3036
|
+
packages.fs.unlinkSync(tmpTTS);
|
|
3037
|
+
} catch (error) {
|
|
3038
|
+
if (error.code !== "EBUSY") {
|
|
3039
|
+
throw error;
|
|
2403
3040
|
}
|
|
2404
3041
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
return true;
|
|
2408
|
-
});
|
|
2409
|
-
if (filtered.length > 0) {
|
|
2410
|
-
cache.events.emit(`onAlerts`, filtered);
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
/**
|
|
2414
|
-
* getHeader constructs a standardized alert header string based on provided attributes, properties, and VTEC information.
|
|
2415
|
-
*
|
|
2416
|
-
* @public
|
|
2417
|
-
* @static
|
|
2418
|
-
* @param {types.TypeAttributes} attributes
|
|
2419
|
-
* @param {?types.BaseProperties} [properties]
|
|
2420
|
-
* @param {?types.VTECParsed} [vtec]
|
|
2421
|
-
* @returns {string}
|
|
2422
|
-
*/
|
|
2423
|
-
static getHeader(attributes, properties, vtec) {
|
|
2424
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
2425
|
-
const parent = `ATSX`;
|
|
2426
|
-
const alertType = (_d = (_c = (_a = attributes == null ? void 0 : attributes.awipsType) == null ? void 0 : _a.type) != null ? _c : (_b = attributes == null ? void 0 : attributes.getAwip) == null ? void 0 : _b.prefix) != null ? _d : `XX`;
|
|
2427
|
-
const ugc = ((_e = properties == null ? void 0 : properties.geocode) == null ? void 0 : _e.UGC) != null ? (_f = properties == null ? void 0 : properties.geocode) == null ? void 0 : _f.UGC.join(`-`) : `000000`;
|
|
2428
|
-
const status = (vtec == null ? void 0 : vtec.status) || "Issued";
|
|
2429
|
-
const issued = (properties == null ? void 0 : properties.issued) != null ? (_g = new Date(properties == null ? void 0 : properties.issued)) == null ? void 0 : _g.toISOString().replace(/[-:]/g, "").split(".")[0] : (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").split(".")[0];
|
|
2430
|
-
const sender = (properties == null ? void 0 : properties.sender_icao) || `XXXX`;
|
|
2431
|
-
const header = `ZCZC-${parent}-${alertType}-${ugc}-${status}-${issued}-${sender}-`;
|
|
2432
|
-
return header;
|
|
3042
|
+
return resolve(outTTS);
|
|
3043
|
+
}));
|
|
2433
3044
|
}
|
|
2434
3045
|
/**
|
|
2435
|
-
*
|
|
3046
|
+
* @function encodeWavPCM16
|
|
3047
|
+
* @description
|
|
3048
|
+
* Encodes an array of 16-bit PCM samples into a standard WAV file buffer.
|
|
3049
|
+
* Produces mono audio with 16 bits per sample and a specified sample rate.
|
|
2436
3050
|
*
|
|
2437
|
-
*
|
|
2438
|
-
*
|
|
2439
|
-
* @param {types.TypeCompiled} validated
|
|
2440
|
-
* @returns {*}
|
|
2441
|
-
*/
|
|
2442
|
-
static eventHandler(validated) {
|
|
2443
|
-
if (validated.isApi) return api_default.event(validated);
|
|
2444
|
-
if (validated.isCap) return cap_default.event(validated);
|
|
2445
|
-
if (!validated.isCap && validated.isVtec && validated.isUGC) return vtec_default2.event(validated);
|
|
2446
|
-
if (!validated.isCap && !validated.isVtec && validated.isUGC) return ugc_default2.event(validated);
|
|
2447
|
-
if (!validated.isCap && !validated.isVtec && !validated.isUGC) return text_default2.event(validated);
|
|
2448
|
-
}
|
|
2449
|
-
/**
|
|
2450
|
-
* getICAO retrieves the ICAO code and corresponding office name based on VTEC tracking information, message attributes, or WMO code.
|
|
3051
|
+
* The input `samples` array should be an array of objects containing a
|
|
3052
|
+
* numeric `value` property representing the PCM sample.
|
|
2451
3053
|
*
|
|
2452
3054
|
* @private
|
|
2453
3055
|
* @static
|
|
2454
|
-
* @param {
|
|
2455
|
-
* @param {
|
|
2456
|
-
* @
|
|
2457
|
-
* @returns {{ icao: any; name: any; }}
|
|
3056
|
+
* @param {Record<string, number>[]} samples
|
|
3057
|
+
* @param {number} [sampleRate=8000]
|
|
3058
|
+
* @returns {Buffer}
|
|
2458
3059
|
*/
|
|
2459
|
-
static
|
|
2460
|
-
|
|
2461
|
-
const
|
|
2462
|
-
const
|
|
2463
|
-
|
|
3060
|
+
static encodeWavPCM16(samples, sampleRate = 8e3) {
|
|
3061
|
+
const bytesPerSample = 2;
|
|
3062
|
+
const blockAlign = 1 * bytesPerSample;
|
|
3063
|
+
const byteRate = sampleRate * blockAlign;
|
|
3064
|
+
const subchunk2Size = samples.length * bytesPerSample;
|
|
3065
|
+
const chunkSize = 36 + subchunk2Size;
|
|
3066
|
+
const buffer = Buffer.alloc(44 + subchunk2Size);
|
|
3067
|
+
let o = 0;
|
|
3068
|
+
buffer.write("RIFF", o);
|
|
3069
|
+
o += 4;
|
|
3070
|
+
buffer.writeUInt32LE(chunkSize, o);
|
|
3071
|
+
o += 4;
|
|
3072
|
+
buffer.write("WAVE", o);
|
|
3073
|
+
o += 4;
|
|
3074
|
+
buffer.write("fmt ", o);
|
|
3075
|
+
o += 4;
|
|
3076
|
+
buffer.writeUInt32LE(16, o);
|
|
3077
|
+
o += 4;
|
|
3078
|
+
buffer.writeUInt16LE(1, o);
|
|
3079
|
+
o += 2;
|
|
3080
|
+
buffer.writeUInt16LE(1, o);
|
|
3081
|
+
o += 2;
|
|
3082
|
+
buffer.writeUInt32LE(sampleRate, o);
|
|
3083
|
+
o += 4;
|
|
3084
|
+
buffer.writeUInt32LE(byteRate, o);
|
|
3085
|
+
o += 4;
|
|
3086
|
+
buffer.writeUInt16LE(blockAlign, o);
|
|
3087
|
+
o += 2;
|
|
3088
|
+
buffer.writeUInt16LE(16, o);
|
|
3089
|
+
o += 2;
|
|
3090
|
+
buffer.write("data", o);
|
|
3091
|
+
o += 4;
|
|
3092
|
+
buffer.writeUInt32LE(subchunk2Size, o);
|
|
3093
|
+
o += 4;
|
|
3094
|
+
for (let i = 0; i < samples.length; i++, o += 2) {
|
|
3095
|
+
buffer.writeInt16LE(samples[i].value, o);
|
|
3096
|
+
}
|
|
3097
|
+
return buffer;
|
|
2464
3098
|
}
|
|
2465
3099
|
/**
|
|
2466
|
-
*
|
|
3100
|
+
* @function parseWavPCM16
|
|
3101
|
+
* @description
|
|
3102
|
+
* Parses a WAV buffer containing 16-bit PCM mono audio and extracts
|
|
3103
|
+
* the sample data along with format information.
|
|
2467
3104
|
*
|
|
2468
|
-
*
|
|
2469
|
-
*
|
|
2470
|
-
*
|
|
2471
|
-
* @returns {*}
|
|
2472
|
-
*/
|
|
2473
|
-
static getCorrectIssuedDate(attributes) {
|
|
2474
|
-
const time = attributes.issue != null ? new Date(attributes.issue).toLocaleString() : (attributes == null ? void 0 : attributes.issue) != null ? new Date(attributes.issue).toLocaleString() : (/* @__PURE__ */ new Date()).toLocaleString();
|
|
2475
|
-
if (time == `Invalid Date`) return (/* @__PURE__ */ new Date()).toLocaleString();
|
|
2476
|
-
return time;
|
|
2477
|
-
}
|
|
2478
|
-
/**
|
|
2479
|
-
* getCorrectExpiryDate determines the correct expiration date for an alert based on VTEC information or UGC zones.
|
|
3105
|
+
* Only supports PCM format (audioFormat = 1), 16 bits per sample,
|
|
3106
|
+
* and single-channel (mono) audio. Returns `null` if the buffer
|
|
3107
|
+
* is invalid or does not meet these requirements.
|
|
2480
3108
|
*
|
|
2481
3109
|
* @private
|
|
2482
3110
|
* @static
|
|
2483
|
-
* @param {
|
|
2484
|
-
* @
|
|
2485
|
-
* @returns {*}
|
|
2486
|
-
*/
|
|
2487
|
-
static getCorrectExpiryDate(vtec, ugc) {
|
|
2488
|
-
const time = (vtec == null ? void 0 : vtec.expires) && !isNaN(new Date(vtec.expires).getTime()) ? new Date(vtec.expires).toLocaleString() : (ugc == null ? void 0 : ugc.expiry) != null ? new Date(ugc.expiry).toLocaleString() : new Date((/* @__PURE__ */ new Date()).getTime() + 1 * 60 * 60 * 1e3).toLocaleString();
|
|
2489
|
-
if (time == `Invalid Date`) return new Date((/* @__PURE__ */ new Date()).getTime() + 1 * 60 * 60 * 1e3).toLocaleString();
|
|
2490
|
-
return time;
|
|
2491
|
-
}
|
|
2492
|
-
};
|
|
2493
|
-
var events_default = EventParser;
|
|
2494
|
-
|
|
2495
|
-
// src/database.ts
|
|
2496
|
-
var Database = class {
|
|
2497
|
-
/**
|
|
2498
|
-
* handleAlertCache stores a unique alert in the SQLite database and ensures the total number of alerts does not exceed 5000.
|
|
2499
|
-
*
|
|
2500
|
-
* @public
|
|
2501
|
-
* @static
|
|
2502
|
-
* @async
|
|
2503
|
-
* @param {*} alert
|
|
2504
|
-
* @returns {*}
|
|
3111
|
+
* @param {Buffer} buffer
|
|
3112
|
+
* @returns { { samples: Int16Array; sampleRate: number; channels: number; bitsPerSample: number } | null }
|
|
2505
3113
|
*/
|
|
2506
|
-
static
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
3114
|
+
static parseWavPCM16(buffer) {
|
|
3115
|
+
if (buffer.toString("ascii", 0, 4) !== "RIFF" || buffer.toString("ascii", 8, 12) !== "WAVE") {
|
|
3116
|
+
return null;
|
|
3117
|
+
}
|
|
3118
|
+
let fmt = null;
|
|
3119
|
+
let data = null;
|
|
3120
|
+
let i = 12;
|
|
3121
|
+
while (i + 8 <= buffer.length) {
|
|
3122
|
+
const id = buffer.toString("ascii", i, i + 4);
|
|
3123
|
+
const size = buffer.readUInt32LE(i + 4);
|
|
3124
|
+
const start = i + 8;
|
|
3125
|
+
const end = start + size;
|
|
3126
|
+
if (id === "fmt ") fmt = buffer.slice(start, end);
|
|
3127
|
+
if (id === "data") data = buffer.slice(start, end);
|
|
3128
|
+
i = end + size % 2;
|
|
3129
|
+
}
|
|
3130
|
+
if (!fmt || !data) return null;
|
|
3131
|
+
const audioFormat = fmt.readUInt16LE(0);
|
|
3132
|
+
const channels = fmt.readUInt16LE(2);
|
|
3133
|
+
const sampleRate = fmt.readUInt32LE(4);
|
|
3134
|
+
const bitsPerSample = fmt.readUInt16LE(14);
|
|
3135
|
+
if (audioFormat !== 1 || bitsPerSample !== 16 || channels !== 1) {
|
|
3136
|
+
return null;
|
|
3137
|
+
}
|
|
3138
|
+
const samples = new Int16Array(data.buffer, data.byteOffset, data.length / 2);
|
|
3139
|
+
return { samples: new Int16Array(samples), sampleRate, channels, bitsPerSample };
|
|
2515
3140
|
}
|
|
2516
3141
|
/**
|
|
2517
|
-
*
|
|
3142
|
+
* @function concatPCM16
|
|
3143
|
+
* @description
|
|
3144
|
+
* Concatenates multiple Int16Array PCM audio buffers into a single
|
|
3145
|
+
* contiguous Int16Array.
|
|
2518
3146
|
*
|
|
2519
|
-
* @
|
|
3147
|
+
* @private
|
|
2520
3148
|
* @static
|
|
2521
|
-
* @
|
|
2522
|
-
* @returns {
|
|
3149
|
+
* @param {Int16Array[]} arrays
|
|
3150
|
+
* @returns {Int16Array}
|
|
2523
3151
|
*/
|
|
2524
|
-
static
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
if (!stanzaTable) {
|
|
2535
|
-
cache.db.prepare(`CREATE TABLE stanzas (id INTEGER PRIMARY KEY AUTOINCREMENT, stanza TEXT)`).run();
|
|
2536
|
-
}
|
|
2537
|
-
if (!shapfileTable) {
|
|
2538
|
-
cache.db.prepare(`CREATE TABLE shapefiles (id TEXT PRIMARY KEY, location TEXT, geometry TEXT)`).run();
|
|
2539
|
-
console.log(definitions.messages.shapefile_creation);
|
|
2540
|
-
for (const shape of definitions.shapefiles) {
|
|
2541
|
-
const { id, file } = shape;
|
|
2542
|
-
const filepath = packages.path.join(__dirname, `../../shapefiles`, file);
|
|
2543
|
-
const { features } = yield packages.shapefile.read(filepath, filepath);
|
|
2544
|
-
console.log(`Importing ${features.length} entries from ${file}...`);
|
|
2545
|
-
const insertStmt = cache.db.prepare(`INSERT OR REPLACE INTO shapefiles (id, location, geometry)VALUES (?, ?, ?)`);
|
|
2546
|
-
const insertTransaction = cache.db.transaction((entries) => {
|
|
2547
|
-
for (const feature of entries) {
|
|
2548
|
-
const { properties, geometry } = feature;
|
|
2549
|
-
let final, location;
|
|
2550
|
-
switch (true) {
|
|
2551
|
-
case !!properties.FIPS:
|
|
2552
|
-
final = `${properties.STATE}${id}${properties.FIPS.substring(2)}`;
|
|
2553
|
-
location = `${properties.COUNTYNAME}, ${properties.STATE}`;
|
|
2554
|
-
break;
|
|
2555
|
-
case !!properties.FULLSTAID:
|
|
2556
|
-
final = `${properties.ST}${id}${properties.WFO}`;
|
|
2557
|
-
location = `${properties.CITY}, ${properties.STATE}`;
|
|
2558
|
-
break;
|
|
2559
|
-
case !!properties.STATE:
|
|
2560
|
-
final = `${properties.STATE}${id}${properties.ZONE}`;
|
|
2561
|
-
location = `${properties.NAME}, ${properties.STATE}`;
|
|
2562
|
-
break;
|
|
2563
|
-
default:
|
|
2564
|
-
final = properties.ID;
|
|
2565
|
-
location = properties.NAME;
|
|
2566
|
-
break;
|
|
2567
|
-
}
|
|
2568
|
-
insertStmt.run(final, location, JSON.stringify(geometry));
|
|
2569
|
-
}
|
|
2570
|
-
});
|
|
2571
|
-
yield insertTransaction(features);
|
|
2572
|
-
}
|
|
2573
|
-
console.log(definitions.messages.shapefile_creation_finished);
|
|
2574
|
-
}
|
|
2575
|
-
} catch (error) {
|
|
2576
|
-
cache.events.emit("onError", { code: "error-load-database", message: `Failed to load database: ${error.message}` });
|
|
2577
|
-
}
|
|
2578
|
-
});
|
|
3152
|
+
static concatPCM16(arrays) {
|
|
3153
|
+
let total = 0;
|
|
3154
|
+
for (const a of arrays) total += a.length;
|
|
3155
|
+
const out = new Int16Array(total);
|
|
3156
|
+
let o = 0;
|
|
3157
|
+
for (const a of arrays) {
|
|
3158
|
+
out.set(a, o);
|
|
3159
|
+
o += a.length;
|
|
3160
|
+
}
|
|
3161
|
+
return out;
|
|
2579
3162
|
}
|
|
2580
|
-
};
|
|
2581
|
-
var database_default = Database;
|
|
2582
|
-
|
|
2583
|
-
// src/xmpp.ts
|
|
2584
|
-
var Xmpp = class {
|
|
2585
3163
|
/**
|
|
2586
|
-
*
|
|
2587
|
-
*
|
|
3164
|
+
* @function pcm16toFloat
|
|
3165
|
+
* @description
|
|
3166
|
+
* Converts a PCM16 Int16Array audio buffer to a Float32Array
|
|
3167
|
+
* with normalized values in the range [-1, 1).
|
|
2588
3168
|
*
|
|
2589
|
-
* @
|
|
3169
|
+
* @private
|
|
2590
3170
|
* @static
|
|
2591
|
-
* @
|
|
2592
|
-
* @
|
|
2593
|
-
* @returns {Promise<void>}
|
|
3171
|
+
* @param {Int16Array} int16
|
|
3172
|
+
* @returns {Float32Array}
|
|
2594
3173
|
*/
|
|
2595
|
-
static
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
const lastStanza = Date.now() - cache.lastStanza;
|
|
2600
|
-
if (lastStanza >= currentInterval * 1e3) {
|
|
2601
|
-
if (!cache.attemptingReconnect) {
|
|
2602
|
-
cache.attemptingReconnect = true;
|
|
2603
|
-
cache.isConnected = false;
|
|
2604
|
-
cache.totalReconnects += 1;
|
|
2605
|
-
cache.events.emit(`onReconnect`, { reconnects: cache.totalReconnects, lastStanza, lastName: settings2.NoaaWeatherWireService.clientCredentials.nickname });
|
|
2606
|
-
yield cache.session.stop().catch(() => {
|
|
2607
|
-
});
|
|
2608
|
-
yield cache.session.start().catch(() => {
|
|
2609
|
-
});
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
}
|
|
2613
|
-
});
|
|
3174
|
+
static pcm16toFloat(int16) {
|
|
3175
|
+
const out = new Float32Array(int16.length);
|
|
3176
|
+
for (let i = 0; i < int16.length; i++) out[i] = int16[i] / 32768;
|
|
3177
|
+
return out;
|
|
2614
3178
|
}
|
|
2615
3179
|
/**
|
|
2616
|
-
*
|
|
2617
|
-
*
|
|
2618
|
-
*
|
|
3180
|
+
* @function floatToPcm16
|
|
3181
|
+
* @description
|
|
3182
|
+
* Converts a Float32Array of audio samples in the range [-1, 1]
|
|
3183
|
+
* to a PCM16 Int16Array.
|
|
2619
3184
|
*
|
|
2620
|
-
* @
|
|
3185
|
+
* @private
|
|
2621
3186
|
* @static
|
|
2622
|
-
* @
|
|
2623
|
-
* @returns {
|
|
3187
|
+
* @param {Float32Array} float32
|
|
3188
|
+
* @returns {Int16Array}
|
|
2624
3189
|
*/
|
|
2625
|
-
static
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
username: settings2.NoaaWeatherWireService.clientCredentials.username,
|
|
2633
|
-
password: settings2.NoaaWeatherWireService.clientCredentials.password
|
|
2634
|
-
});
|
|
2635
|
-
(_b = (_a = settings2.NoaaWeatherWireService.clientCredentials).nickname) != null ? _b : _a.nickname = settings2.NoaaWeatherWireService.clientCredentials.username;
|
|
2636
|
-
cache.session.on(`online`, (address) => __async(null, null, function* () {
|
|
2637
|
-
if (cache.lastConnect && Date.now() - cache.lastConnect < 10 * 1e3) {
|
|
2638
|
-
cache.sigHalt = true;
|
|
2639
|
-
utils_default.sleep(2 * 1e3).then(() => __async(null, null, function* () {
|
|
2640
|
-
yield cache.session.stop();
|
|
2641
|
-
}));
|
|
2642
|
-
cache.events.emit(`onError`, { code: `error-reconnecting-too-fast`, message: `The client is attempting to reconnect too fast. Please wait a few seconds before trying again.` });
|
|
2643
|
-
return;
|
|
2644
|
-
}
|
|
2645
|
-
cache.isConnected = true;
|
|
2646
|
-
cache.sigHalt = false;
|
|
2647
|
-
cache.lastConnect = Date.now();
|
|
2648
|
-
cache.session.send(packages.xmpp.xml("presence", { to: `nwws@conference.nwws-oi.weather.gov/${settings2.NoaaWeatherWireService.clientCredentials.nickname}`, xmlns: "http://jabber.org/protocol/muc" }));
|
|
2649
|
-
cache.session.send(packages.xmpp.xml("presence", { to: `nwws@conference.nwws-oi.weather.gov`, type: "available" }));
|
|
2650
|
-
cache.events.emit(`onConnection`, settings2.NoaaWeatherWireService.clientCredentials.nickname);
|
|
2651
|
-
if (cache.attemptingReconnect) {
|
|
2652
|
-
utils_default.sleep(15 * 1e3).then(() => {
|
|
2653
|
-
cache.attemptingReconnect = false;
|
|
2654
|
-
});
|
|
2655
|
-
}
|
|
2656
|
-
}));
|
|
2657
|
-
cache.session.on(`offline`, () => __async(null, null, function* () {
|
|
2658
|
-
cache.isConnected = false;
|
|
2659
|
-
cache.sigHalt = true;
|
|
2660
|
-
cache.events.emit(`onError`, { code: `connection-lost`, message: `XMPP connection went offline` });
|
|
2661
|
-
}));
|
|
2662
|
-
cache.session.on(`error`, (error) => __async(null, null, function* () {
|
|
2663
|
-
cache.isConnected = false;
|
|
2664
|
-
cache.sigHalt = true;
|
|
2665
|
-
cache.events.emit(`onError`, { code: `connection-error`, message: error.message });
|
|
2666
|
-
}));
|
|
2667
|
-
cache.session.on(`stanza`, (stanza) => __async(null, null, function* () {
|
|
2668
|
-
try {
|
|
2669
|
-
cache.lastStanza = Date.now();
|
|
2670
|
-
if (stanza.is(`message`)) {
|
|
2671
|
-
const validate = stanza_default.validate(stanza);
|
|
2672
|
-
if (validate.ignore || validate.isCap && !settings2.NoaaWeatherWireService.alertPreferences.isCapOnly || !validate.isCap && settings2.NoaaWeatherWireService.alertPreferences.isCapOnly || validate.isCap && !validate.isCapDescription) return;
|
|
2673
|
-
events_default.eventHandler(validate);
|
|
2674
|
-
cache.events.emit(`onMessage`, validate);
|
|
2675
|
-
database_default.stanzaCacheImport(JSON.stringify(validate));
|
|
2676
|
-
}
|
|
2677
|
-
if (stanza.is(`presence`) && stanza.attrs.from && stanza.attrs.from.startsWith("nwws@conference.nwws-oi.weather.gov/")) {
|
|
2678
|
-
const occupant = stanza.attrs.from.split("/").slice(1).join("/");
|
|
2679
|
-
cache.events.emit("onOccupant", { occupant, type: stanza.attrs.type === "unavailable" ? "unavailable" : "available" });
|
|
2680
|
-
}
|
|
2681
|
-
} catch (e) {
|
|
2682
|
-
cache.events.emit(`onError`, { code: `error-processing-stanza`, message: e.message });
|
|
2683
|
-
}
|
|
2684
|
-
}));
|
|
2685
|
-
yield cache.session.start();
|
|
2686
|
-
});
|
|
3190
|
+
static floatToPcm16(float32) {
|
|
3191
|
+
const out = new Int16Array(float32.length);
|
|
3192
|
+
for (let i = 0; i < float32.length; i++) {
|
|
3193
|
+
let v = Math.max(-1, Math.min(1, float32[i]));
|
|
3194
|
+
out[i] = Math.round(v * 32767);
|
|
3195
|
+
}
|
|
3196
|
+
return out;
|
|
2687
3197
|
}
|
|
2688
|
-
};
|
|
2689
|
-
var xmpp_default = Xmpp;
|
|
2690
|
-
|
|
2691
|
-
// src/utils.ts
|
|
2692
|
-
var Utils = class {
|
|
2693
3198
|
/**
|
|
2694
|
-
*
|
|
3199
|
+
* @function resamplePCM16
|
|
3200
|
+
* @description
|
|
3201
|
+
* Resamples a PCM16 audio buffer from an original sample rate to a
|
|
3202
|
+
* target sample rate using linear interpolation.
|
|
2695
3203
|
*
|
|
2696
|
-
* @
|
|
3204
|
+
* @private
|
|
2697
3205
|
* @static
|
|
2698
|
-
* @
|
|
2699
|
-
* @param {number}
|
|
2700
|
-
* @
|
|
3206
|
+
* @param {Int16Array} int16
|
|
3207
|
+
* @param {number} originalRate
|
|
3208
|
+
* @param {number} targetRate
|
|
3209
|
+
* @returns {Int16Array}
|
|
2701
3210
|
*/
|
|
2702
|
-
static
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
3211
|
+
static resamplePCM16(int16, originalRate, targetRate) {
|
|
3212
|
+
if (originalRate === targetRate) return int16;
|
|
3213
|
+
const ratio = targetRate / originalRate;
|
|
3214
|
+
const outLen = Math.max(1, Math.round(int16.length * ratio));
|
|
3215
|
+
const out = new Int16Array(outLen);
|
|
3216
|
+
for (let i = 0; i < outLen; i++) {
|
|
3217
|
+
const pos = i / ratio;
|
|
3218
|
+
const i0 = Math.floor(pos);
|
|
3219
|
+
const i1 = Math.min(i0 + 1, int16.length - 1);
|
|
3220
|
+
const frac = pos - i0;
|
|
3221
|
+
const v = int16[i0] * (1 - frac) + int16[i1] * frac;
|
|
3222
|
+
out[i] = Math.round(v);
|
|
3223
|
+
}
|
|
3224
|
+
return out;
|
|
2706
3225
|
}
|
|
2707
3226
|
/**
|
|
2708
|
-
*
|
|
3227
|
+
* @function generateSilence
|
|
3228
|
+
* @description
|
|
3229
|
+
* Generates a PCM16 audio buffer containing silence for a specified
|
|
3230
|
+
* duration.
|
|
2709
3231
|
*
|
|
2710
|
-
* @
|
|
3232
|
+
* @private
|
|
2711
3233
|
* @static
|
|
2712
|
-
* @
|
|
2713
|
-
* @
|
|
3234
|
+
* @param {number} ms
|
|
3235
|
+
* @param {number} [sampleRate=8000]
|
|
3236
|
+
* @returns {Int16Array}
|
|
2714
3237
|
*/
|
|
2715
|
-
static
|
|
2716
|
-
return
|
|
2717
|
-
try {
|
|
2718
|
-
const settings2 = settings;
|
|
2719
|
-
if (settings2.NoaaWeatherWireService.cache.read && settings2.NoaaWeatherWireService.cache.directory) {
|
|
2720
|
-
if (!packages.fs.existsSync(settings2.NoaaWeatherWireService.cache.directory)) return;
|
|
2721
|
-
const cacheDir = settings2.NoaaWeatherWireService.cache.directory;
|
|
2722
|
-
const getAllFiles = packages.fs.readdirSync(cacheDir).filter((file) => file.endsWith(".bin") && file.startsWith("cache-"));
|
|
2723
|
-
for (const file of getAllFiles) {
|
|
2724
|
-
const filepath = packages.path.join(cacheDir, file);
|
|
2725
|
-
const readFile = packages.fs.readFileSync(filepath, { encoding: "utf-8" });
|
|
2726
|
-
const readSize = packages.fs.statSync(filepath).size;
|
|
2727
|
-
if (readSize == 0) {
|
|
2728
|
-
continue;
|
|
2729
|
-
}
|
|
2730
|
-
const isCap = readFile.includes(`<?xml`);
|
|
2731
|
-
if (isCap && !settings2.NoaaWeatherWireService.alertPreferences.isCapOnly) continue;
|
|
2732
|
-
if (!isCap && settings2.NoaaWeatherWireService.alertPreferences.isCapOnly) continue;
|
|
2733
|
-
const validate = stanza_default.validate(readFile, { awipsid: file, isCap, raw: true, issue: void 0 });
|
|
2734
|
-
yield events_default.eventHandler(validate);
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
} catch (error) {
|
|
2738
|
-
cache.events.emit("onError", { code: "error-load-cache", message: `Failed to load cache: ${error.message}` });
|
|
2739
|
-
}
|
|
2740
|
-
});
|
|
3238
|
+
static generateSilence(ms, sampleRate = 8e3) {
|
|
3239
|
+
return new Int16Array(Math.floor(ms * sampleRate));
|
|
2741
3240
|
}
|
|
2742
3241
|
/**
|
|
2743
|
-
*
|
|
3242
|
+
* @function generateAttentionTone
|
|
3243
|
+
* @description
|
|
3244
|
+
* Generates a dual-frequency Attention Tone (853 Hz and 960 Hz) used in
|
|
3245
|
+
* EAS/SAME alerts. Produces a PCM16 buffer of the specified duration.
|
|
2744
3246
|
*
|
|
2745
|
-
* @
|
|
3247
|
+
* @private
|
|
2746
3248
|
* @static
|
|
2747
|
-
* @
|
|
2748
|
-
* @
|
|
3249
|
+
* @param {number} ms
|
|
3250
|
+
* @param {number} [sampleRate=8000]
|
|
3251
|
+
* @returns {Int16Array}
|
|
2749
3252
|
*/
|
|
2750
|
-
static
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
3253
|
+
static generateAttentionTone(ms, sampleRate = 8e3) {
|
|
3254
|
+
const len = Math.floor(ms * sampleRate);
|
|
3255
|
+
const out = new Int16Array(len);
|
|
3256
|
+
const f1 = 853;
|
|
3257
|
+
const f2 = 960;
|
|
3258
|
+
const twoPi = Math.PI * 2;
|
|
3259
|
+
const amp = 0.1;
|
|
3260
|
+
const fadeLen = Math.floor(sampleRate * 0);
|
|
3261
|
+
for (let i = 0; i < len; i++) {
|
|
3262
|
+
const t = i / sampleRate;
|
|
3263
|
+
const s = Math.sin(twoPi * f1 * t) + Math.sin(twoPi * f2 * t);
|
|
3264
|
+
let gain = 1;
|
|
3265
|
+
if (i < fadeLen) gain = i / fadeLen;
|
|
3266
|
+
else if (i > len - fadeLen) gain = (len - i) / fadeLen;
|
|
3267
|
+
const v = Math.max(-1, Math.min(1, s / 2 * amp * gain));
|
|
3268
|
+
out[i] = Math.round(v * 32767);
|
|
3269
|
+
}
|
|
3270
|
+
return out;
|
|
2762
3271
|
}
|
|
2763
3272
|
/**
|
|
2764
|
-
*
|
|
3273
|
+
* @function applyNWREffect
|
|
3274
|
+
* @description
|
|
3275
|
+
* Applies a National Weather Radio (NWR)-style audio effect to a PCM16
|
|
3276
|
+
* buffer, including high-pass and low-pass filtering, soft clipping
|
|
3277
|
+
* compression, and optional bit reduction to simulate vintage broadcast
|
|
3278
|
+
* characteristics.
|
|
2765
3279
|
*
|
|
2766
|
-
* @
|
|
3280
|
+
* @private
|
|
2767
3281
|
* @static
|
|
3282
|
+
* @param {Int16Array} int16
|
|
3283
|
+
* @param {number} [sampleRate=8000]
|
|
3284
|
+
* @returns {Int16Array}
|
|
2768
3285
|
*/
|
|
2769
|
-
static
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
3286
|
+
static applyNWREffect(int16, sampleRate = 8e3) {
|
|
3287
|
+
const hpCut = 3555;
|
|
3288
|
+
const lpCut = 1600;
|
|
3289
|
+
const noiseLevel = 0;
|
|
3290
|
+
const crushBits = 8;
|
|
3291
|
+
const x = this.pcm16toFloat(int16);
|
|
3292
|
+
const dt = 1 / sampleRate;
|
|
3293
|
+
const rcHP = 1 / (2 * Math.PI * hpCut);
|
|
3294
|
+
const aHP = rcHP / (rcHP + dt);
|
|
3295
|
+
let yHP = 0, xPrev = 0;
|
|
3296
|
+
for (let i = 0; i < x.length; i++) {
|
|
3297
|
+
const xi = x[i];
|
|
3298
|
+
yHP = aHP * (yHP + xi - xPrev);
|
|
3299
|
+
xPrev = xi;
|
|
3300
|
+
x[i] = yHP;
|
|
3301
|
+
}
|
|
3302
|
+
const rcLP = 1 / (2 * Math.PI * lpCut);
|
|
3303
|
+
const aLP = dt / (rcLP + dt);
|
|
3304
|
+
let yLP = 0;
|
|
3305
|
+
for (let i = 0; i < x.length; i++) {
|
|
3306
|
+
yLP = yLP + aLP * (x[i] - yLP);
|
|
3307
|
+
x[i] = yLP;
|
|
3308
|
+
}
|
|
3309
|
+
const compGain = 2;
|
|
3310
|
+
const norm = Math.tanh(compGain);
|
|
3311
|
+
for (let i = 0; i < x.length; i++) x[i] = Math.tanh(x[i] * compGain) / norm;
|
|
3312
|
+
const levels = Math.pow(2, crushBits) - 1;
|
|
3313
|
+
return this.floatToPcm16(x);
|
|
2774
3314
|
}
|
|
2775
3315
|
/**
|
|
2776
|
-
*
|
|
3316
|
+
* @function addNoise
|
|
3317
|
+
* @description
|
|
3318
|
+
* Adds random noise to a PCM16 audio buffer and normalizes the signal
|
|
3319
|
+
* to prevent clipping. Useful for simulating real-world signal conditions
|
|
3320
|
+
* or reducing digital artifacts.
|
|
2777
3321
|
*
|
|
2778
|
-
* @
|
|
3322
|
+
* @private
|
|
2779
3323
|
* @static
|
|
2780
|
-
* @
|
|
2781
|
-
* @param {
|
|
2782
|
-
* @
|
|
2783
|
-
* @returns {unknown}
|
|
3324
|
+
* @param {Int16Array} int16
|
|
3325
|
+
* @param {number} [noiseLevel=0.02]
|
|
3326
|
+
* @returns {Int16Array}
|
|
2784
3327
|
*/
|
|
2785
|
-
static
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
"Accept": "application/geo+json, text/plain, */*; q=0.9",
|
|
2793
|
-
"Accept-Language": "en-US,en;q=0.9"
|
|
2794
|
-
}
|
|
2795
|
-
};
|
|
2796
|
-
const requestOptions = __spreadProps(__spreadValues(__spreadValues({}, defaultOptions), options), {
|
|
2797
|
-
headers: __spreadValues(__spreadValues({}, defaultOptions.headers), (_a = options == null ? void 0 : options.headers) != null ? _a : {})
|
|
2798
|
-
});
|
|
2799
|
-
try {
|
|
2800
|
-
const resp = yield packages.axios.get(url, {
|
|
2801
|
-
headers: requestOptions.headers,
|
|
2802
|
-
timeout: requestOptions.timeout,
|
|
2803
|
-
maxRedirects: 0,
|
|
2804
|
-
validateStatus: (status) => status === 200 || status === 500
|
|
2805
|
-
});
|
|
2806
|
-
return { error: false, message: resp.data };
|
|
2807
|
-
} catch (err) {
|
|
2808
|
-
return { error: true, message: (_b = err == null ? void 0 : err.message) != null ? _b : String(err) };
|
|
2809
|
-
}
|
|
2810
|
-
});
|
|
3328
|
+
static addNoise(int16, noiseLevel = 0.02) {
|
|
3329
|
+
const x = this.pcm16toFloat(int16);
|
|
3330
|
+
for (let i = 0; i < x.length; i++) x[i] += (Math.random() * 2 - 1) * noiseLevel;
|
|
3331
|
+
let peak = 0;
|
|
3332
|
+
for (let i = 0; i < x.length; i++) peak = Math.max(peak, Math.abs(x[i]));
|
|
3333
|
+
if (peak > 1) for (let i = 0; i < x.length; i++) x[i] *= 0.98 / peak;
|
|
3334
|
+
return this.floatToPcm16(x);
|
|
2811
3335
|
}
|
|
2812
3336
|
/**
|
|
2813
|
-
*
|
|
3337
|
+
* @function asciiTo8N1Bits
|
|
3338
|
+
* @description
|
|
3339
|
+
* Converts an ASCII string into a sequence of bits using the 8N1 framing
|
|
3340
|
+
* convention (1 start bit, 8 data bits, 2 stop bits) commonly used in
|
|
3341
|
+
* serial and EAS transmissions.
|
|
2814
3342
|
*
|
|
2815
|
-
* @
|
|
3343
|
+
* @private
|
|
2816
3344
|
* @static
|
|
2817
|
-
* @param {
|
|
3345
|
+
* @param {string} str
|
|
3346
|
+
* @returns {number[]}
|
|
2818
3347
|
*/
|
|
2819
|
-
static
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
const stackFiles = [cacheDirectory], files = [];
|
|
2827
|
-
while (stackFiles.length) {
|
|
2828
|
-
const currentDirectory = stackFiles.pop();
|
|
2829
|
-
packages.fs.readdirSync(currentDirectory).forEach((file) => {
|
|
2830
|
-
const fullPath = packages.path.join(currentDirectory, file);
|
|
2831
|
-
const stat = packages.fs.statSync(fullPath);
|
|
2832
|
-
if (stat.isDirectory()) stackFiles.push(fullPath);
|
|
2833
|
-
else files.push({ file: fullPath, size: stat.size });
|
|
2834
|
-
});
|
|
2835
|
-
}
|
|
2836
|
-
if (!files.length) return;
|
|
2837
|
-
files.forEach((f) => {
|
|
2838
|
-
if (f.size > maxBytes) packages.fs.unlinkSync(f.file);
|
|
2839
|
-
});
|
|
2840
|
-
} catch (error) {
|
|
2841
|
-
cache.events.emit("onError", { code: "error-garbage-collection", message: `Failed to perform garbage collection: ${error.message}` });
|
|
3348
|
+
static asciiTo8N1Bits(str) {
|
|
3349
|
+
const bits = [];
|
|
3350
|
+
for (let i = 0; i < str.length; i++) {
|
|
3351
|
+
const c = str.charCodeAt(i) & 255;
|
|
3352
|
+
bits.push(0);
|
|
3353
|
+
for (let b = 0; b < 8; b++) bits.push(c >> b & 1);
|
|
3354
|
+
bits.push(1, 1);
|
|
2842
3355
|
}
|
|
3356
|
+
return bits;
|
|
2843
3357
|
}
|
|
2844
3358
|
/**
|
|
2845
|
-
*
|
|
3359
|
+
* @function generateAFSK
|
|
3360
|
+
* @description
|
|
3361
|
+
* Converts a sequence of bits into AFSK-modulated PCM16 audio data for EAS
|
|
3362
|
+
* alerts. Applies a fade-in and fade-out to reduce clicks and generates
|
|
3363
|
+
* the audio at the specified sample rate.
|
|
2846
3364
|
*
|
|
2847
|
-
* @
|
|
3365
|
+
* @private
|
|
2848
3366
|
* @static
|
|
2849
|
-
* @param {
|
|
3367
|
+
* @param {number[]} bits
|
|
3368
|
+
* @param {number} [sampleRate=8000]
|
|
3369
|
+
* @returns {Int16Array}
|
|
2850
3370
|
*/
|
|
2851
|
-
static
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
3371
|
+
static generateAFSK(bits, sampleRate = 8e3) {
|
|
3372
|
+
const baud = 520.83;
|
|
3373
|
+
const markFreq = 2083.3;
|
|
3374
|
+
const spaceFreq = 1562.5;
|
|
3375
|
+
const amplitude = 0.6;
|
|
3376
|
+
const twoPi = Math.PI * 2;
|
|
3377
|
+
const result = [];
|
|
3378
|
+
let phase = 0;
|
|
3379
|
+
let frac = 0;
|
|
3380
|
+
for (let b = 0; b < bits.length; b++) {
|
|
3381
|
+
const bit = bits[b];
|
|
3382
|
+
const freq = bit ? markFreq : spaceFreq;
|
|
3383
|
+
const samplesPerBit = sampleRate / baud + frac;
|
|
3384
|
+
const n = Math.round(samplesPerBit);
|
|
3385
|
+
frac = samplesPerBit - n;
|
|
3386
|
+
const inc = twoPi * freq / sampleRate;
|
|
3387
|
+
for (let i = 0; i < n; i++) {
|
|
3388
|
+
result.push(Math.round(Math.sin(phase) * amplitude * 32767));
|
|
3389
|
+
phase += inc;
|
|
3390
|
+
if (phase > twoPi) phase -= twoPi;
|
|
2859
3391
|
}
|
|
2860
|
-
} catch (error) {
|
|
2861
|
-
cache.events.emit("onError", { code: "error-cron-job", message: `Failed to perform scheduled tasks: ${error.message}` });
|
|
2862
3392
|
}
|
|
3393
|
+
const fadeSamples = Math.floor(sampleRate * 2e-3);
|
|
3394
|
+
for (let i = 0; i < fadeSamples; i++) {
|
|
3395
|
+
const gain = i / fadeSamples;
|
|
3396
|
+
result[i] = Math.round(result[i] * gain);
|
|
3397
|
+
result[result.length - 1 - i] = Math.round(result[result.length - 1 - i] * gain);
|
|
3398
|
+
}
|
|
3399
|
+
return Int16Array.from(result);
|
|
2863
3400
|
}
|
|
2864
3401
|
/**
|
|
2865
|
-
*
|
|
3402
|
+
* @function generateSAMEHeader
|
|
3403
|
+
* @description
|
|
3404
|
+
* Generates a SAME (Specific Area Message Encoding) audio header for
|
|
3405
|
+
* EAS alerts. Converts a VTEC string into AFSK-modulated PCM16 audio,
|
|
3406
|
+
* optionally repeating the signal with pre-mark and gap intervals.
|
|
2866
3407
|
*
|
|
2867
|
-
* @
|
|
3408
|
+
* @private
|
|
2868
3409
|
* @static
|
|
2869
|
-
* @param {
|
|
2870
|
-
* @param {
|
|
3410
|
+
* @param {string} vtec
|
|
3411
|
+
* @param {number} repeats
|
|
3412
|
+
* @param {number} [sampleRate=8000]
|
|
3413
|
+
* @param {{preMarkSec?: number, gapSec?: number}} [options={}]
|
|
3414
|
+
* @returns {Int16Array}
|
|
2871
3415
|
*/
|
|
2872
|
-
static
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
3416
|
+
static generateSAMEHeader(vtec, repeats, sampleRate = 8e3, options = {}) {
|
|
3417
|
+
var _a, _b;
|
|
3418
|
+
const preMarkSec = (_a = options.preMarkSec) != null ? _a : 0.3;
|
|
3419
|
+
const gapSec = (_b = options.gapSec) != null ? _b : 0.1;
|
|
3420
|
+
const bursts = [];
|
|
3421
|
+
const gap = this.generateSilence(gapSec, sampleRate);
|
|
3422
|
+
for (let i = 0; i < repeats; i++) {
|
|
3423
|
+
const bodyBits = this.asciiTo8N1Bits(vtec);
|
|
3424
|
+
const body = this.generateAFSK(bodyBits, sampleRate);
|
|
3425
|
+
const extendedBodyDuration = Math.round(preMarkSec * sampleRate);
|
|
3426
|
+
const extendedBody = new Int16Array(extendedBodyDuration + gap.length);
|
|
3427
|
+
for (let j = 0; j < extendedBodyDuration; j++) {
|
|
3428
|
+
extendedBody[j] = Math.round(body[j % body.length] * 0.2);
|
|
2883
3429
|
}
|
|
3430
|
+
extendedBody.set(gap, extendedBodyDuration);
|
|
3431
|
+
bursts.push(extendedBody);
|
|
3432
|
+
if (i !== repeats - 1) bursts.push(gap);
|
|
2884
3433
|
}
|
|
3434
|
+
return this.concatPCM16(bursts);
|
|
2885
3435
|
}
|
|
2886
3436
|
};
|
|
2887
|
-
var
|
|
3437
|
+
var eas_default = EAS;
|
|
2888
3438
|
|
|
2889
3439
|
// src/index.ts
|
|
2890
3440
|
var AlertManager = class {
|
|
@@ -2892,63 +3442,103 @@ var AlertManager = class {
|
|
|
2892
3442
|
this.start(metadata);
|
|
2893
3443
|
}
|
|
2894
3444
|
/**
|
|
2895
|
-
* setDisplayName
|
|
2896
|
-
*
|
|
2897
|
-
*
|
|
2898
|
-
*
|
|
2899
|
-
*
|
|
2900
|
-
* @param {
|
|
3445
|
+
* @function setDisplayName
|
|
3446
|
+
* @description
|
|
3447
|
+
* Sets the display nickname for the NWWS XMPP session. Trims the provided
|
|
3448
|
+
* name and validates it, emitting a warning if the name is empty or invalid.
|
|
3449
|
+
*
|
|
3450
|
+
* @param {string} [name]
|
|
2901
3451
|
*/
|
|
2902
3452
|
setDisplayName(name) {
|
|
2903
3453
|
const settings2 = settings;
|
|
2904
3454
|
const trimmed = name == null ? void 0 : name.trim();
|
|
2905
3455
|
if (!trimmed) {
|
|
2906
|
-
|
|
3456
|
+
utils_default.warn(definitions.messages.invalid_nickname);
|
|
2907
3457
|
return;
|
|
2908
3458
|
}
|
|
2909
|
-
settings2.
|
|
3459
|
+
settings2.noaa_weather_wire_service_settings.credentials.nickname = trimmed;
|
|
2910
3460
|
}
|
|
2911
3461
|
/**
|
|
2912
|
-
*
|
|
3462
|
+
* @function setCurrentLocation
|
|
3463
|
+
* @description
|
|
3464
|
+
* Sets the current location with a name and geographic coordinates.
|
|
3465
|
+
* Validates the coordinates before updating the cache, emitting warnings
|
|
3466
|
+
* if values are missing or invalid.
|
|
2913
3467
|
*
|
|
2914
|
-
* @
|
|
2915
|
-
* @
|
|
3468
|
+
* @param {string} locationName
|
|
3469
|
+
* @param {types.Coordinates} [coordinates]
|
|
2916
3470
|
*/
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3471
|
+
setCurrentLocation(locationName, coordinates) {
|
|
3472
|
+
if (!coordinates) {
|
|
3473
|
+
utils_default.warn(`Coordinates not provided for location: ${locationName}`);
|
|
3474
|
+
return;
|
|
3475
|
+
}
|
|
3476
|
+
const { lat, lon } = coordinates;
|
|
3477
|
+
if (typeof lat !== "number" || typeof lon !== "number" || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
|
|
3478
|
+
utils_default.warn(definitions.messages.invalid_coordinates.replace("{lat}", String(lat)).replace("{lon}", String(lon)));
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
cache.currentLocations[locationName] = coordinates;
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3484
|
+
* @function createEasAudio
|
|
3485
|
+
* @description
|
|
3486
|
+
* Generates an EAS (Emergency Alert System) audio file using the provided
|
|
3487
|
+
* description and header.
|
|
3488
|
+
*
|
|
3489
|
+
* @async
|
|
3490
|
+
* @param {string} description
|
|
3491
|
+
* @param {string} header
|
|
3492
|
+
* @returns {Promise<Buffer>}
|
|
3493
|
+
*/
|
|
3494
|
+
createEasAudio(description, header) {
|
|
3495
|
+
return __async(this, null, function* () {
|
|
3496
|
+
return yield eas_default.generateEASAudio(description, header);
|
|
2927
3497
|
});
|
|
2928
|
-
return combinations;
|
|
2929
3498
|
}
|
|
2930
3499
|
/**
|
|
2931
|
-
*
|
|
3500
|
+
* @function getAllAlertTypes
|
|
3501
|
+
* @description
|
|
3502
|
+
* Generates a list of all possible alert types by combining defined
|
|
3503
|
+
* event names with action names.
|
|
3504
|
+
*
|
|
3505
|
+
* @returns {string[]}
|
|
3506
|
+
*/
|
|
3507
|
+
getAllAlertTypes() {
|
|
3508
|
+
const events2 = new Set(Object.values(definitions.events));
|
|
3509
|
+
const actions = new Set(Object.values(definitions.actions));
|
|
3510
|
+
return Array.from(events2).flatMap(
|
|
3511
|
+
(event) => Array.from(actions).map((action) => `${event} ${action}`)
|
|
3512
|
+
);
|
|
3513
|
+
}
|
|
3514
|
+
/**
|
|
3515
|
+
* @function searchStanzaDatabase
|
|
3516
|
+
* @description
|
|
3517
|
+
* Searches the stanza database for entries containing the specified query.
|
|
3518
|
+
* Escapes SQL wildcard characters and returns results in descending order
|
|
3519
|
+
* by ID, up to the specified limit.
|
|
2932
3520
|
*
|
|
2933
|
-
* @public
|
|
2934
3521
|
* @async
|
|
2935
|
-
* @param {string} query
|
|
2936
|
-
* @
|
|
3522
|
+
* @param {string} query
|
|
3523
|
+
* @param {number} [limit=250]
|
|
3524
|
+
* @returns {Promise<any[]>}
|
|
2937
3525
|
*/
|
|
2938
|
-
searchStanzaDatabase(query) {
|
|
3526
|
+
searchStanzaDatabase(query, limit = 250) {
|
|
2939
3527
|
return __async(this, null, function* () {
|
|
2940
|
-
|
|
3528
|
+
const escapeLike = (s) => s.replace(/[%_]/g, "\\$&");
|
|
3529
|
+
const rows = yield cache.db.prepare(`SELECT * FROM stanzas WHERE stanza LIKE ? ESCAPE '\\' ORDER BY id DESC LIMIT ${limit}`).all(`%${escapeLike(query)}%`);
|
|
3530
|
+
return rows;
|
|
2941
3531
|
});
|
|
2942
3532
|
}
|
|
2943
3533
|
/**
|
|
2944
|
-
* setSettings
|
|
2945
|
-
*
|
|
2946
|
-
*
|
|
3534
|
+
* @function setSettings
|
|
3535
|
+
* @description
|
|
3536
|
+
* Merges the provided client settings into the current configuration,
|
|
3537
|
+
* preserving nested structures.
|
|
2947
3538
|
*
|
|
2948
|
-
* @public
|
|
2949
3539
|
* @async
|
|
2950
|
-
* @param {types.
|
|
2951
|
-
* @returns {Promise<void>}
|
|
3540
|
+
* @param {types.ClientSettingsTypes} settings
|
|
3541
|
+
* @returns {Promise<void>}
|
|
2952
3542
|
*/
|
|
2953
3543
|
setSettings(settings2) {
|
|
2954
3544
|
return __async(this, null, function* () {
|
|
@@ -2956,71 +3546,97 @@ var AlertManager = class {
|
|
|
2956
3546
|
});
|
|
2957
3547
|
}
|
|
2958
3548
|
/**
|
|
2959
|
-
*
|
|
2960
|
-
*
|
|
2961
|
-
*
|
|
2962
|
-
*
|
|
2963
|
-
*
|
|
2964
|
-
*
|
|
2965
|
-
*
|
|
2966
|
-
*
|
|
2967
|
-
* - onAnyEventType (Ex. onTornadoWarning) Emitted when a specific alert event type is received
|
|
2968
|
-
*
|
|
2969
|
-
* @public
|
|
2970
|
-
* @param {string} event
|
|
2971
|
-
* @param {(...args: any[]) => void} callback
|
|
2972
|
-
* @returns {() => void}
|
|
3549
|
+
* @function on
|
|
3550
|
+
* @description
|
|
3551
|
+
* Registers a callback for a specific event and returns a function
|
|
3552
|
+
* to unregister the listener.
|
|
3553
|
+
*
|
|
3554
|
+
* @param {string} event
|
|
3555
|
+
* @param {(...args: any[]) => void} callback
|
|
3556
|
+
* @returns {() => void}
|
|
2973
3557
|
*/
|
|
2974
|
-
|
|
3558
|
+
on(event, callback) {
|
|
2975
3559
|
cache.events.on(event, callback);
|
|
2976
3560
|
return () => cache.events.off(event, callback);
|
|
2977
3561
|
}
|
|
2978
3562
|
/**
|
|
2979
|
-
* start
|
|
2980
|
-
*
|
|
3563
|
+
* @function start
|
|
3564
|
+
* @description
|
|
3565
|
+
* Initializes the client with the provided settings, starts the NWWS XMPP
|
|
3566
|
+
* session if applicable, loads cached messages, and sets up scheduled
|
|
3567
|
+
* tasks (cron jobs) for ongoing processing.
|
|
2981
3568
|
*
|
|
2982
|
-
* @public
|
|
2983
3569
|
* @async
|
|
2984
|
-
* @param {
|
|
2985
|
-
* @returns {Promise<void>}
|
|
3570
|
+
* @param {types.ClientSettingsTypes} metadata
|
|
3571
|
+
* @returns {Promise<void>}
|
|
2986
3572
|
*/
|
|
2987
3573
|
start(metadata) {
|
|
2988
3574
|
return __async(this, null, function* () {
|
|
3575
|
+
var _a, _b;
|
|
2989
3576
|
if (!cache.isReady) {
|
|
2990
|
-
|
|
2991
|
-
return
|
|
3577
|
+
utils_default.warn(definitions.messages.not_ready);
|
|
3578
|
+
return;
|
|
2992
3579
|
}
|
|
2993
3580
|
this.setSettings(metadata);
|
|
2994
|
-
if (settings.catchUnhandledExceptions) {
|
|
2995
|
-
utils_default.detectUncaughtExceptions();
|
|
2996
|
-
}
|
|
2997
3581
|
const settings2 = settings;
|
|
2998
|
-
this.isNoaaWeatherWireService =
|
|
3582
|
+
this.isNoaaWeatherWireService = settings2.is_wire;
|
|
2999
3583
|
cache.isReady = false;
|
|
3584
|
+
while (!utils_default.isReadyToProcess((_b = (_a = settings2.global_settings.filtering.location) == null ? void 0 : _a.filter) != null ? _b : false)) {
|
|
3585
|
+
yield utils_default.sleep(2e3);
|
|
3586
|
+
}
|
|
3000
3587
|
if (this.isNoaaWeatherWireService) {
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3588
|
+
try {
|
|
3589
|
+
yield database_default.loadDatabase();
|
|
3590
|
+
yield xmpp_default.deploySession();
|
|
3591
|
+
yield utils_default.loadCollectionCache();
|
|
3592
|
+
} catch (err) {
|
|
3593
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3594
|
+
utils_default.warn(`Failed to initialize NWWS services: ${msg}`);
|
|
3595
|
+
}
|
|
3004
3596
|
}
|
|
3005
3597
|
utils_default.handleCronJob(this.isNoaaWeatherWireService);
|
|
3006
|
-
|
|
3598
|
+
if (this.job) {
|
|
3599
|
+
try {
|
|
3600
|
+
this.job.stop();
|
|
3601
|
+
} catch (e) {
|
|
3602
|
+
utils_default.warn(`Failed to stop existing cron job.`);
|
|
3603
|
+
}
|
|
3604
|
+
this.job = null;
|
|
3605
|
+
}
|
|
3606
|
+
const interval = !this.isNoaaWeatherWireService ? settings2.national_weather_service_settings.interval : 5;
|
|
3607
|
+
this.job = new packages.jobs.Cron(`*/${interval} * * * * *`, () => {
|
|
3007
3608
|
utils_default.handleCronJob(this.isNoaaWeatherWireService);
|
|
3008
3609
|
});
|
|
3009
3610
|
});
|
|
3010
3611
|
}
|
|
3011
3612
|
/**
|
|
3012
|
-
* stop
|
|
3613
|
+
* @function stop
|
|
3614
|
+
* @description
|
|
3615
|
+
* Stops active scheduled tasks (cron job) and, if connected, the NWWS
|
|
3616
|
+
* XMPP session. Updates relevant cache flags to indicate the session
|
|
3617
|
+
* is no longer active.
|
|
3013
3618
|
*
|
|
3014
|
-
* @public
|
|
3015
3619
|
* @async
|
|
3016
|
-
* @returns {Promise<void>}
|
|
3620
|
+
* @returns {Promise<void>}
|
|
3017
3621
|
*/
|
|
3018
3622
|
stop() {
|
|
3019
3623
|
return __async(this, null, function* () {
|
|
3020
3624
|
cache.isReady = true;
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3625
|
+
if (this.job) {
|
|
3626
|
+
try {
|
|
3627
|
+
this.job.stop();
|
|
3628
|
+
} catch (e) {
|
|
3629
|
+
utils_default.warn(`Failed to stop cron job.`);
|
|
3630
|
+
}
|
|
3631
|
+
this.job = null;
|
|
3632
|
+
}
|
|
3633
|
+
const session = cache.session;
|
|
3634
|
+
if (session && this.isNoaaWeatherWireService) {
|
|
3635
|
+
try {
|
|
3636
|
+
yield session.stop();
|
|
3637
|
+
} catch (e) {
|
|
3638
|
+
utils_default.warn(`Failed to stop XMPP session.`);
|
|
3639
|
+
}
|
|
3024
3640
|
cache.sigHalt = true;
|
|
3025
3641
|
cache.isConnected = false;
|
|
3026
3642
|
cache.session = null;
|
|
@@ -3039,6 +3655,6 @@ var index_default = AlertManager;
|
|
|
3039
3655
|
StanzaParser,
|
|
3040
3656
|
TextParser,
|
|
3041
3657
|
UGCParser,
|
|
3042
|
-
|
|
3043
|
-
|
|
3658
|
+
Utils,
|
|
3659
|
+
VtecParser
|
|
3044
3660
|
});
|