@atmosx/event-product-parser 2.0.16 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +4 -237
  2. package/dist/cjs/index.cjs +2233 -3105
  3. package/dist/esm/index.mjs +2233 -3108
  4. package/package.json +3 -2
  5. package/src/@building/building.clean.ts +30 -0
  6. package/src/@building/building.create.ts +42 -0
  7. package/src/@building/building.enhance.ts +56 -0
  8. package/src/@building/building.geometry.ts +42 -0
  9. package/src/@building/building.headers.ts +37 -0
  10. package/src/@building/building.office.ts +43 -0
  11. package/src/@building/building.polygon.ts +71 -0
  12. package/src/@building/building.properties.ts +89 -0
  13. package/src/@building/building.signature.ts +78 -0
  14. package/src/@building/building.tags.ts +24 -0
  15. package/src/@building/building.tracking.ts +68 -0
  16. package/src/@building/building.validate.ts +132 -0
  17. package/src/@core/core.callback.ts +39 -0
  18. package/src/@core/core.getEvents.ts +25 -0
  19. package/src/@core/core.getNodes.ts +25 -0
  20. package/src/@core/core.listener.ts +24 -0
  21. package/src/@core/core.setNode.ts +81 -0
  22. package/src/@core/core.start.ts +54 -0
  23. package/src/@core/core.stop.ts +32 -0
  24. package/src/@dictionaries/dictionaries.betterEventNames.ts +85 -0
  25. package/src/@dictionaries/dictionaries.eventActions.ts +28 -0
  26. package/src/@dictionaries/{awips.ts → dictionaries.eventAwipAbreviations.ts} +12 -6
  27. package/src/@dictionaries/dictionaries.eventCancelMessages.ts +29 -0
  28. package/src/@dictionaries/dictionaries.eventCauses.ts +36 -0
  29. package/src/@dictionaries/dictionaries.eventProducts.ts +25 -0
  30. package/src/@dictionaries/dictionaries.eventRecords.ts +25 -0
  31. package/src/@dictionaries/dictionaries.eventSeverity.ts +27 -0
  32. package/src/@dictionaries/dictionaries.eventStatus.ts +31 -0
  33. package/src/@dictionaries/{signatures.ts → dictionaries.eventTags.ts} +13 -68
  34. package/src/@dictionaries/dictionaries.eventTypes.ts +82 -0
  35. package/src/@dictionaries/dictionaries.eventsOffshore.ts +31 -0
  36. package/src/@dictionaries/dictionaries.hailStrings.ts +31 -0
  37. package/src/@dictionaries/{icao.ts → dictionaries.officeICAOs.ts} +13 -6
  38. package/src/@dictionaries/dictionaries.regExp.ts +28 -0
  39. package/src/@dictionaries/dictionaries.shapefileLinks.ts +36 -0
  40. package/src/@dictionaries/dictionaries.statusCorrelationText.ts +40 -0
  41. package/src/@dictionaries/dictionaries.test_signatures.ts +23 -0
  42. package/src/@dictionaries/dictionaries.transcribedMessageReplacements.ts +68 -0
  43. package/src/@events/events.api.ts +113 -0
  44. package/src/@events/events.text.ts +79 -0
  45. package/src/@events/events.ugc.ts +83 -0
  46. package/src/@events/events.vtec.ts +87 -0
  47. package/src/@manager/manager.mkEvent.ts +79 -0
  48. package/src/@manager/manager.rmEvent.ts +44 -0
  49. package/src/@manager/manager.setHash.ts +37 -0
  50. package/src/@manager/manager.updateNodes.ts +51 -0
  51. package/src/@modules/@database/database.cache.ts +48 -0
  52. package/src/@modules/@database/database.init.ts +45 -0
  53. package/src/@modules/@database/database.shapefiles.ts +97 -0
  54. package/src/@modules/@database/database.stanza.ts +47 -0
  55. package/src/@modules/@stanza/stanza.getAwipsType.ts +46 -0
  56. package/src/@modules/@stanza/stanza.validate.ts +50 -0
  57. package/src/@modules/@utilities/utilities.createHttp.ts +75 -0
  58. package/src/@modules/@utilities/utilities.getFormattedTime.ts +43 -0
  59. package/src/@modules/@utilities/utilities.getSettings.ts +25 -0
  60. package/src/@modules/@utilities/utilities.getShapeNearestPoint.ts +114 -0
  61. package/src/@modules/@utilities/utilities.setCronSchedule.ts +38 -0
  62. package/src/@modules/@utilities/utilities.setEventEmit.ts +41 -0
  63. package/src/@modules/@utilities/utilities.setListener.ts +30 -0
  64. package/src/@modules/@utilities/utilities.setSettings.ts +42 -0
  65. package/src/@modules/@utilities/utilities.setSleep.ts +33 -0
  66. package/src/@modules/@utilities/utilities.setTimeoutAction.ts +59 -0
  67. package/src/@modules/@utilities/utilities.setWarning.ts +34 -0
  68. package/src/@modules/@xmpp/xmpp.xDeploy.ts +58 -0
  69. package/src/@modules/@xmpp/xmpp.xError.ts +38 -0
  70. package/src/@modules/@xmpp/xmpp.xOffline.ts +38 -0
  71. package/src/@modules/@xmpp/xmpp.xOnline.ts +61 -0
  72. package/src/@modules/@xmpp/xmpp.xReconnect.ts +59 -0
  73. package/src/@modules/@xmpp/xmpp.xStanza.ts +63 -0
  74. package/src/@parsers/@hvtec/hvtec.extract.ts +40 -0
  75. package/src/@parsers/@pvtec/pvtec.expires.ts +26 -0
  76. package/src/@parsers/@pvtec/pvtec.extract.ts +50 -0
  77. package/src/@parsers/@text/text.getDescriptionFromProduct.ts +53 -0
  78. package/src/@parsers/@text/text.getPolygonFromProduct.ts +32 -0
  79. package/src/@parsers/@text/text.getTextFromProduct.ts +43 -0
  80. package/src/@parsers/@text/text.getXML.ts +61 -0
  81. package/src/@parsers/@ugc/ugc.coordinates.ts +110 -0
  82. package/src/@parsers/@ugc/ugc.expiry.ts +32 -0
  83. package/src/@parsers/@ugc/ugc.extract.ts +37 -0
  84. package/src/@parsers/@ugc/ugc.header.ts +30 -0
  85. package/src/@parsers/@ugc/ugc.locations.ts +29 -0
  86. package/src/@parsers/@ugc/ugc.zones.ts +52 -0
  87. package/src/@types/type.event.ts +67 -0
  88. package/src/@types/type.properties.ts +75 -0
  89. package/src/@types/types.attributes.ts +28 -0
  90. package/src/@types/types.compiled.ts +35 -0
  91. package/src/@types/types.hash.ts +24 -0
  92. package/src/@types/types.hvtec.ts +25 -0
  93. package/src/@types/types.pvtec.ts +29 -0
  94. package/src/@types/types.settings.ts +71 -0
  95. package/src/@types/types.stanza.ts +37 -0
  96. package/src/@types/types.ugc.ts +24 -0
  97. package/src/bootstrap.ts +82 -163
  98. package/src/index.ts +48 -216
  99. package/test.js +65 -49
  100. package/src/@dictionaries/events.ts +0 -168
  101. package/src/@parsers/@events/api.ts +0 -146
  102. package/src/@parsers/@events/cap.ts +0 -123
  103. package/src/@parsers/@events/text.ts +0 -104
  104. package/src/@parsers/@events/ugc.ts +0 -107
  105. package/src/@parsers/@events/vtec.ts +0 -76
  106. package/src/@parsers/events.ts +0 -392
  107. package/src/@parsers/hvtec.ts +0 -46
  108. package/src/@parsers/pvtec.ts +0 -72
  109. package/src/@parsers/stanza.ts +0 -97
  110. package/src/@parsers/text.ts +0 -165
  111. package/src/@parsers/ugc.ts +0 -247
  112. package/src/@submodules/database.ts +0 -201
  113. package/src/@submodules/eas.ts +0 -490
  114. package/src/@submodules/utils.ts +0 -191
  115. package/src/@submodules/xmpp.ts +0 -142
  116. package/src/types.ts +0 -259
@@ -1,165 +0,0 @@
1
- /*
2
- _ _ __ __
3
- /\ | | | | (_) \ \ / /
4
- / \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
5
- / /\ \| __| "_ ` _ \ / _ \/ __| "_ \| "_ \ / _ \ "__| |/ __| > <
6
- / ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
7
- /_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
8
- | |
9
- |_|
10
-
11
- Written by: KiyoWx (k3yomi)
12
- */
13
-
14
- import * as loader from '../bootstrap';
15
-
16
- export class TextParser {
17
-
18
- /**
19
- * @function textProductToString
20
- * @description
21
- * Searches a text product message for a line containing a specific value,
22
- * extracts the substring immediately following that value, and optionally
23
- * removes additional specified strings. Cleans up the extracted string by
24
- * trimming whitespace and removing any remaining occurrences of the search
25
- * value or '<' characters.
26
- *
27
- * @static
28
- * @param {string} message
29
- * @param {string} value
30
- * @param {string[]} [removal=[]]
31
- * @returns {string | null}
32
- */
33
- public static textProductToString(message: string, value: string, removal: string[] = []): string | null {
34
- const lines = message.split('\n');
35
- for (const line of lines) {
36
- if (line.includes(value)) {
37
- let result = line.slice(line.indexOf(value) + value.length).trim();
38
- for (const str of removal) { result = result.split(str).join(''); }
39
- result = result.replace(value, '').replace('<', '').trim();
40
- return result || null;
41
- }
42
- }
43
- return null;
44
- }
45
-
46
- /**
47
- * @function textProductToPolygon
48
- * @description
49
- * Parses a text product message to extract polygon coordinates based on
50
- * LAT...LON data. Coordinates are converted to [latitude, longitude] pairs
51
- * with longitude negated (assumes Western Hemisphere). If the polygon has
52
- * more than two points, the first point is repeated at the end to close it.
53
- *
54
- * @static
55
- * @param {string} message
56
- * @returns {[number, number][]}
57
- */
58
- public static textProductToPolygon(message: string): [number, number][] {
59
- const coordinates: [number, number][] = [];
60
- const latLonMatch = message.match(/LAT\.{3}LON\s+([\d\s]+)/i);
61
- if (!latLonMatch || !latLonMatch[1]) return coordinates;
62
- const coordStrings = latLonMatch[1].replace(/\n/g, ' ').trim().split(/\s+/);
63
- for (let i = 0; i < coordStrings.length - 1; i += 2) {
64
- const lat = parseFloat(coordStrings[i]) / 100;
65
- const lon = -parseFloat(coordStrings[i + 1]) / 100;
66
- if (!isNaN(lat) && !isNaN(lon)) { coordinates.push([lon, lat]); }
67
- }
68
- if (coordinates.length > 2) { coordinates.push(coordinates[0]); }
69
- return coordinates;
70
- }
71
-
72
- /**
73
- * @function textProductToDescription
74
- * @description
75
- * Extracts a clean description portion from a text product message, optionally
76
- * removing a handle and any extra metadata such as "STANZA ATTRIBUTES...".
77
- * Also trims and normalizes whitespace.
78
- *
79
- * @static
80
- * @param {string} message
81
- * @param {string | null} [handle=null]
82
- * @returns {string}
83
- */
84
- public static textProductToDescription(message: string, handle: string = null): string {
85
- const original = message;
86
- const discoveredDates = Array.from(message.matchAll(loader.definitions.regular_expressions.dateline));
87
- if (discoveredDates.length) {
88
- const lastMatch = discoveredDates[discoveredDates.length - 1][0];
89
- const startIdx = message.lastIndexOf(lastMatch);
90
- if (startIdx !== -1) {
91
- const endIdx = message.indexOf('&&', startIdx);
92
- message = message.substring(startIdx + lastMatch.length, endIdx !== -1 ? endIdx : undefined).trimStart();
93
- if (message.startsWith('/')) message = message.slice(1).trimStart();
94
- if (handle && message.includes(handle)) {
95
- const handleIdx = message.indexOf(handle);
96
- message = message.substring(handleIdx + handle.length).trimStart();
97
- if (message.startsWith('/')) message = message.slice(1).trimStart();
98
- }
99
- }
100
- } else if (handle) {
101
- const handleIdx = message.indexOf(handle);
102
- if (handleIdx !== -1) {
103
- let afterHandle = message.substring(handleIdx + handle.length).trimStart();
104
- if (afterHandle.startsWith('/')) afterHandle = afterHandle.slice(1).trimStart();
105
- const latEnd = afterHandle.indexOf('&&')
106
- message = latEnd !== -1 ? afterHandle.substring(0, latEnd).trim() : afterHandle.trim();
107
- }
108
- }
109
- return message.replace(/\s+/g, ' ').trim().startsWith('STANZA ATTRIBUTES...') ? original : message.split('STANZA ATTRIBUTES...')[0].trim();
110
- }
111
-
112
- /**
113
- * @function getXmlValues
114
- * @description
115
- * Recursively extracts specified values from a parsed XML-like object.
116
- * Searches both object keys and array items for matching keys (case-insensitive)
117
- * and returns the corresponding values. If multiple unique values are found for
118
- * a key, an array is returned; if one value is found, it returns that value;
119
- * if none are found, returns `null`.
120
- *
121
- * @static
122
- * @param {any} parsed
123
- * @param {string[]} valuesToExtract
124
- * @returns {Record<string, string | string[] | null>}
125
- */
126
- public static getXmlValues(parsed: any, valuesToExtract: string[]): Record<string, string> {
127
- const extracted: Record<string, any> = {};
128
- const findValueByKey = (obj: any, searchKey: string) => {
129
- const results = [];
130
- if (obj === null || typeof obj !== 'object') {
131
- return results;
132
- }
133
- const searchKeyLower = searchKey.toLowerCase();
134
- for (const key in obj) {
135
- if (obj.hasOwnProperty(key) && key.toLowerCase() === searchKeyLower) {
136
- results.push(obj[key]);
137
- }
138
- }
139
- if (Array.isArray(obj)) {
140
- for (const item of obj) {
141
- if (item.valueName && item.valueName.toLowerCase() === searchKeyLower && item.value !== undefined) {
142
- results.push(item.value);
143
- }
144
- const nestedResults = findValueByKey(item, searchKey);
145
- results.push(...nestedResults);
146
- }
147
- }
148
- for (const key in obj) {
149
- if (obj.hasOwnProperty(key)) {
150
- const nestedResults = findValueByKey(obj[key], searchKey);
151
- results.push(...nestedResults);
152
- }
153
- }
154
- return results;
155
- };
156
- for (const key of valuesToExtract) {
157
- const values = findValueByKey(parsed.alert, key);
158
- const uniqueValues = [...new Set(values)];
159
- extracted[key] = uniqueValues.length === 0 ? null : (uniqueValues.length === 1 ? uniqueValues[0] : uniqueValues);
160
- }
161
- return extracted;
162
- }
163
- }
164
-
165
- export default TextParser;
@@ -1,247 +0,0 @@
1
- /*
2
- _ _ __ __
3
- /\ | | | | (_) \ \ / /
4
- / \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
5
- / /\ \| __| "_ ` _ \ / _ \/ __| "_ \| "_ \ / _ \ "__| |/ __| > <
6
- / ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
7
- /_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
8
- | |
9
- |_|
10
-
11
- Written by: KiyoWx (k3yomi)
12
- */
13
-
14
- import * as loader from '../bootstrap';
15
- import * as types from '../types';
16
-
17
- export class UGCParser {
18
-
19
- /**
20
- * @function ugcExtractor
21
- * @description
22
- * Extracts UGC (Universal Geographic Code) information from a message.
23
- * This includes parsing the header, resolving zones, calculating the expiry
24
- * date, and retrieving associated location names from the database.
25
- *
26
- * @static
27
- * @async
28
- * @param {string} message
29
- * @returns {Promise<types.UGCEntry | null>}
30
- */
31
- public static async ugcExtractor(message: string): Promise<types.UGCEntry | null> {
32
- const header = this.getHeader(message);
33
- if (!header) return null;
34
- const zones = this.getZones(header);
35
- if (zones.length === 0) return null;
36
- const expiry = this.getExpiry(message);
37
- const locations = await this.getLocations(zones);
38
- return {
39
- zones: zones,
40
- locations: locations,
41
- expiry: expiry
42
- };
43
- }
44
-
45
- /**
46
- * @function getHeader
47
- * @description
48
- * Extracts the UGC header from a message by locating patterns defined in
49
- * `ugc1` and `ugc2` regular expressions. Removes all whitespace and the
50
- * trailing character from the matched header.
51
- *
52
- * @static
53
- * @param {string} message
54
- * @returns {string | null}
55
- */
56
- public static getHeader(message: string): string | null {
57
- const start = message.search(loader.definitions.regular_expressions.ugc1);
58
- const subMessage = message.substring(start);
59
- const end = subMessage.search(loader.definitions.regular_expressions.ugc2);
60
- const full = subMessage.substring(0, end).replace(/\s+/g, '').slice(0, -1);
61
- return full || null;
62
- }
63
-
64
- /**
65
- * @function getExpiry
66
- * @description
67
- * Extracts an expiration date from a message using the UGC3 format.
68
- * The function parses day, hour, and minute from the message and constructs
69
- * a Date object in the current month and year. Returns `null` if no valid
70
- * expiration is found.
71
- *
72
- * @static
73
- * @param {string} message
74
- * @returns {Date | null}
75
- */
76
- public static getExpiry(message: string): Date | null {
77
- const match = message.match(/\b(\d{6})-/);
78
- if (!match) return null;
79
- const token = match[1];
80
- const day = parseInt(token.slice(0, 2), 10);
81
- const hour = parseInt(token.slice(2, 4), 10);
82
- const minute = parseInt(token.slice(4, 6), 10);
83
- const now = new Date();
84
- const expires = new Date(Date.UTC(now.getUTCFullYear(),now.getUTCMonth(),day,hour,minute));
85
- return expires;
86
- }
87
-
88
- /**
89
- * @function getLocations
90
- * @description
91
- * Retrieves human-readable location names for an array of zone identifiers
92
- * from the shapefiles database. If a zone is not found, the zone ID itself
93
- * is returned. Duplicate locations are removed and the result is sorted.
94
- *
95
- * @static
96
- * @async
97
- * @param {string[]} zones
98
- * @returns {Promise<string[]>}
99
- */
100
- public static async getLocations(zones: string[]): Promise<string[]> {
101
- const uniqueZones = Array.from(new Set(zones.map(z => z.trim())));
102
- const placeholders = uniqueZones.map(() => '?').join(',');
103
- const rows = await loader.cache.db.prepare(
104
- `SELECT id, location FROM shapefiles WHERE id IN (${placeholders})`
105
- ).all(...uniqueZones);
106
- const locationMap = new Map<string, string>();
107
- for (const row of rows) { locationMap.set(row.id, row.location) }
108
- const locations = uniqueZones.map(id => locationMap.get(id) ?? id);
109
- return locations.sort();
110
- }
111
-
112
- /**
113
- * @function getCoordinates
114
- * @description
115
- * Calculates the outer boundary coordinates for a set of UGC zones by
116
- * querying their geometries from the database, merging them, and extracting
117
- * the largest outer ring. The coordinates are downsampled based on a skip
118
- * setting to reduce complexity. Returns `null` if no valid coordinates are found.
119
- *
120
- * @static
121
- * @param {string[]} zones
122
- * @returns {[number, number][]}
123
- */
124
- public static getCoordinates(zones: string[], isUnion=true): any | null {
125
- const list = [...new Set(zones.map(z => z.trim()))].filter(z => z === 'XX000' ? false : true);
126
- if (list.length === 0) return null;
127
- const placeholders = list.map(() => "?").join(",");
128
- const rows = loader.cache.db
129
- .prepare(`SELECT geometry FROM shapefiles WHERE id IN (${placeholders})`)
130
- .all(...list);
131
- const polygons: any[] = [];
132
- for (const row of rows) {
133
- if (!row?.geometry) continue;
134
- const geom = JSON.parse(row.geometry);
135
- if (geom?.type === "Polygon") {
136
- polygons.push(geom.coordinates);
137
- }
138
- }
139
- if (polygons.length === 0) return null;
140
- if (isUnion) {
141
- const unionFn = loader.packages.polygonClipping.union as (...polys: any[]) => any;
142
- const mergedCoords = unionFn(...polygons);
143
- if (!mergedCoords || mergedCoords.length === 0) return null;
144
- let maxArea = -1;
145
- let bestPoly: any[] = [];
146
- for (const poly of mergedCoords) {
147
- const outerRing = poly[0];
148
- let area = 0;
149
- for (let i = 0; i < outerRing.length - 1; i++) {
150
- const [x1, y1] = outerRing[i];
151
- const [x2, y2] = outerRing[i + 1];
152
- area += x1 * y2 - x2 * y1;
153
- }
154
- area = Math.abs(area / 2);
155
- if (area > maxArea) {
156
- maxArea = area;
157
- bestPoly = poly;
158
- }
159
- }
160
- if (!bestPoly || bestPoly.length === 0) return null;
161
- const outerRing = bestPoly[0];
162
- const skip = Math.max(1, parseInt(String(loader.settings.global_settings.shapefile_skip), 10) || 1);
163
- let skipped = outerRing.filter((_: any, idx: number) => idx % skip === 0);
164
- if (skipped.length < 4) {
165
- skipped = outerRing.slice();
166
- }
167
- const first = skipped[0];
168
- const last = skipped[skipped.length - 1];
169
- if (!first || !last || first[0] !== last[0] || first[1] !== last[1]) {
170
- skipped.push([first[0], first[1]]);
171
- }
172
- return {type: "Polygon", coordinates: [skipped]};
173
- } else {
174
- const multi: any[] = [];
175
- for (const polyCoords of polygons) {
176
- if (Array.isArray(polyCoords) && Array.isArray(polyCoords[0])) {
177
- multi.push(polyCoords);
178
- }
179
- }
180
- if (multi.length === 0) return null;
181
- const skip = Math.max(1, parseInt(String(loader.settings.global_settings.shapefile_skip), 10) || 1);
182
- if (skip > 1) {
183
- for (let p = 0; p < multi.length; p++) {
184
- for (let r = 0; r < multi[p].length; r++) {
185
- const ring = multi[p][r];
186
- let reduced = ring.filter((_: any, i: number) => i % skip === 0);
187
- if (reduced.length < 4) reduced = ring.slice();
188
- const first = reduced[0];
189
- const last = reduced[reduced.length - 1];
190
- if ( first && last && (first[0] !== last[0] || first[1] !== last[1])) {
191
- reduced.push([first[0], first[1]]);
192
- }
193
- multi[p][r] = reduced;
194
- }
195
- }
196
- }
197
- return {type: "MultiPolygon", coordinates: multi};
198
- }
199
- }
200
-
201
- /**
202
- * @function getZones
203
- * @description
204
- * Parses a UGC header string and returns an array of individual zone
205
- * identifiers. Handles ranges indicated with `>` and preserves the
206
- * state and format prefixes.
207
- *
208
- * @static
209
- * @param {string} header
210
- * @returns {string[]}
211
- */
212
- public static getZones(header: string): string[] {
213
- const ugcSplit = header.split('-');
214
- const zones: string[] = [];
215
- let state = ugcSplit[0].substring(0, 2);
216
- const format = ugcSplit[0].substring(2, 3);
217
- for (const part of ugcSplit) {
218
- if (/^[A-Z]/.test(part)) {
219
- state = part.substring(0, 2);
220
- if (part.includes('>')) {
221
- const [start, end] = part.split('>');
222
- const startNum = parseInt(start.substring(3), 10);
223
- const endNum = parseInt(end, 10);
224
- for (let j = startNum; j <= endNum; j++) {
225
- zones.push(`${state}${format}${j.toString().padStart(3, '0')}`);
226
- }
227
- } else {
228
- zones.push(part);
229
- }
230
- continue;
231
- }
232
- if (part.includes('>')) {
233
- const [start, end] = part.split('>');
234
- const startNum = parseInt(start, 10);
235
- const endNum = parseInt(end, 10);
236
- for (let j = startNum; j <= endNum; j++) {
237
- zones.push(`${state}${format}${j.toString().padStart(3, '0')}`);
238
- }
239
- } else {
240
- zones.push(`${state}${format}${part}`);
241
- }
242
- }
243
- return zones.filter(item => item !== '');
244
- }
245
- }
246
-
247
- export default UGCParser;
@@ -1,201 +0,0 @@
1
- /*
2
- _ _ __ __
3
- /\ | | | | (_) \ \ / /
4
- / \ | |_ _ __ ___ ___ ___ _ __ | |__ ___ _ __ _ ___ \ V /
5
- / /\ \| __| "_ ` _ \ / _ \/ __| "_ \| "_ \ / _ \ "__| |/ __| > <
6
- / ____ \ |_| | | | | | (_) \__ \ |_) | | | | __/ | | | (__ / . \
7
- /_/ \_\__|_| |_| |_|\___/|___/ .__/|_| |_|\___|_| |_|\___/_/ \_\
8
- | |
9
- |_|
10
-
11
- Written by: KiyoWx (k3yomi)
12
- */
13
-
14
- import * as loader from '../bootstrap';
15
- import * as types from '../types';
16
- import Utils from './utils';
17
- import EventParser from '../@parsers/events';
18
-
19
- export class Database {
20
-
21
- /**
22
- * @function stanzaCacheImport
23
- * @description
24
- * Inserts a single NWWS stanza into the database cache. If the total number
25
- * of stanzas exceeds the configured maximum history, it deletes the oldest
26
- * entries to maintain the limit. Duplicate stanzas are ignored.
27
- *
28
- * @static
29
- * @async
30
- * @param {string} stanza - The raw stanza XML or text to store in the database.
31
- * @returns {Promise<void>} - Resolves when the stanza has been inserted and any necessary pruning of old stanzas has been performed.
32
- */
33
- public static async stanzaCacheImport(stanza: Record<string, any>): Promise<void> {
34
- const settings = loader.settings as types.ClientSettingsTypes;
35
- try {
36
- const db = loader.cache.db;
37
- if (!db) return;
38
- db.prepare(`INSERT OR IGNORE INTO stanzas (type, stanza, issued) VALUES (?, ?, ?)`).run(stanza?.awipsType?.type, JSON.stringify(stanza), stanza?.attributes?.issue);
39
- const countRow = db.prepare(`SELECT COUNT(*) AS total FROM stanzas`).get() as { total: number };
40
- const totalRows = countRow.total;
41
- const maxHistory = settings.noaa_weather_wire_service_settings.cache.max_db_history;
42
- if (totalRows > maxHistory) {
43
- const rowsToDelete = Math.floor((totalRows - maxHistory) / 2);
44
- if (rowsToDelete > 0) {
45
- db.prepare(`
46
- DELETE FROM stanzas
47
- WHERE rowid IN (
48
- SELECT rowid
49
- FROM stanzas
50
- ORDER BY rowid ASC
51
- LIMIT ?
52
- )
53
- `).run(rowsToDelete);
54
- }
55
- }
56
- } catch (error: unknown) {
57
- const msg = error instanceof Error ? error.message : String(error);
58
- Utils.warn(`Failed to import stanza into cache: ${msg}. Please try to delete ${settings.database} and restart the application.`);
59
- }
60
- }
61
-
62
- /**
63
- * @function loadDatabase
64
- * @description
65
- * Initializes the application's SQLite database, creating necessary tables
66
- * for storing stanzas and shapefiles. If the shapefiles table is empty,
67
- * it imports predefined shapefiles from disk, processes their features,
68
- * and populates the database. Emits warnings during the import process.
69
- *
70
- * @static
71
- * @async
72
- * @returns {Promise<void>} - Resolves when the database has been initialized and shapefiles have been imported if necessary.
73
- */
74
- public static async loadDatabase(): Promise<void> {
75
- const settings = loader.settings as types.ClientSettingsTypes;
76
- try {
77
- const { fs, path, sqlite3, shapefile } = loader.packages;
78
- if (!fs.existsSync(settings.database)) fs.writeFileSync(settings.database, '');
79
- loader.cache.db = new sqlite3(settings.database);
80
- loader.cache.db.prepare(`
81
- CREATE TABLE IF NOT EXISTS stanzas (
82
- id INTEGER PRIMARY KEY AUTOINCREMENT,
83
- type TEXT,
84
- issued TEXT,
85
- stanza TEXT
86
- )
87
- `).run();
88
- loader.cache.db.prepare(`
89
- CREATE TABLE IF NOT EXISTS shapefiles (
90
- id TEXT PRIMARY KEY,
91
- location TEXT,
92
- geometry TEXT
93
- )
94
- `).run();
95
- const shapefileCount = loader.cache.db.prepare(`SELECT COUNT(*) AS count FROM shapefiles`).get().count;
96
- if (shapefileCount === 0) {
97
- await Utils.sleep(1000);
98
- Utils.warn(loader.definitions.messages.shapefile_creation);
99
- for (const shape of loader.definitions.shapefiles_directory) {
100
- const name = shape.name;
101
- const type = shape.id;
102
- const link = shape.link;
103
- const response = await fetch(link);
104
- const arrayBuffer = await response.arrayBuffer();
105
- const zip = new loader.packages.jszip();
106
- const content = await zip.loadAsync(arrayBuffer);
107
- const dirPath = path.resolve(__dirname, '../../shapefiles');
108
- if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath);
109
- for (const fileName of Object.keys(content.files)) {
110
- if (fileName.endsWith('.shp') || fileName.endsWith('.dbf')) {
111
- const fileData = await content.files[fileName].async('nodebuffer');
112
- const outputPath = path.resolve(dirPath, `${name}_${type}${path.extname(fileName)}`);
113
- fs.writeFileSync(outputPath, fileData);
114
- Utils.warn(`Successfully downloaded and extracted ${fileName}`);
115
- }
116
- }
117
- const filepath = path.resolve(__dirname, '../../shapefiles', shape.name + '_' + shape.id);
118
- const { features } = await shapefile.read(
119
- filepath,
120
- filepath,
121
- );
122
- Utils.warn(`Importing ${features.length} entries from ${shape.name}_${shape.id}...`);
123
- const insertStmt = loader.cache.db.prepare(`
124
- INSERT OR REPLACE INTO shapefiles (id, location, geometry) VALUES (?, ?, ?)
125
- `);
126
- const insertTransaction = loader.cache.db.transaction((entries: any[]) => {
127
- for (const feature of entries) {
128
- const { properties, geometry } = feature;
129
- let final: string, location: string;
130
- if (properties.FIPS) {
131
- final = `${properties.STATE}${shape.id}${properties.FIPS.substring(2)}`;
132
- location = `${properties.COUNTYNAME}, ${properties.STATE}`;
133
- }
134
- else if (properties.FULLSTAID) {
135
- final = `${properties.ST}${shape.id}${properties.WFO}`;
136
- location = `${properties.CITY}, ${properties.STATE}`;
137
- }
138
- else if (properties.STATE) {
139
- final = `${properties.STATE}${shape.id}${properties.ZONE}`;
140
- location = `${properties.NAME}, ${properties.STATE}`;
141
- }
142
- else {
143
- final = properties.ID;
144
- location = properties.NAME;
145
- }
146
- insertStmt.run(final, location, JSON.stringify(geometry));
147
- }
148
- });
149
- fs.unlinkSync(filepath + '.shp');
150
- fs.unlinkSync(filepath + '.dbf');
151
- Utils.warn(`Cleaned up temporary files for ${shape.name}_${shape.id}`);
152
- insertTransaction(features);
153
- }
154
- Utils.warn(loader.definitions.messages.shapefile_creation_finished);
155
- fs.rm(path.resolve(__dirname, '../../shapefiles'), { recursive: true, force: true }, () => {});
156
- }
157
- } catch (error: unknown) {
158
- const msg = error instanceof Error ? error.message : String(error);
159
- Utils.warn(`Failed to load database: ${msg}`);
160
- }
161
- }
162
-
163
- /**
164
- * @function loadCollectionCache
165
- * @description
166
- * Loads cached stanzas from the database, validates them, and processes them through the event parser.
167
- * Only processes stanzas that are not marked to be ignored and match the CAP preferences.
168
- *
169
- * @static
170
- * @async
171
- * @returns {Promise<void>}
172
- */
173
- public static async loadCollectionCache(): Promise<void> {
174
- try {
175
- const settings = loader.settings as types.ClientSettingsTypes;
176
- if (settings.noaa_weather_wire_service_settings.cache.enabled) {
177
- const maxRows = settings.noaa_weather_wire_service_settings.cache.max_db_cache_size ?? 5000;
178
- const rows = await loader.cache.db.prepare(`SELECT * FROM stanzas ORDER BY rowid DESC LIMIT ?`)
179
- .all(maxRows) as { rowid: number; stanza: string }[];
180
- Utils.warn(loader.definitions.messages.dump_cache.replace(`{count}`, rows.length.toString()), true);
181
- const eventsToProcess = rows
182
- .map(row => {return JSON.parse(row.stanza)})
183
- .filter(validate => {
184
- if (!validate) return false;
185
- const skip = validate.ignore ||
186
- (validate.isCap && !settings.noaa_weather_wire_service_settings.preferences.cap_only) ||
187
- (!validate.isCap && settings.noaa_weather_wire_service_settings.preferences.cap_only) ||
188
- (validate.isCap && !validate.isCapDescription);
189
- return !skip;
190
- });
191
- await Promise.all(eventsToProcess.map(validate => EventParser.eventHandler(validate)));
192
- Utils.warn(loader.definitions.messages.dump_cache_complete, true);
193
- return;
194
- }
195
- } catch (error: any) {
196
- Utils.warn(`Failed to load cache: ${error.stack}`);
197
- }
198
- }
199
- }
200
-
201
- export default Database;