@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.
- package/README.md +4 -237
- package/dist/cjs/index.cjs +2233 -3105
- package/dist/esm/index.mjs +2233 -3108
- package/package.json +3 -2
- package/src/@building/building.clean.ts +30 -0
- package/src/@building/building.create.ts +42 -0
- package/src/@building/building.enhance.ts +56 -0
- package/src/@building/building.geometry.ts +42 -0
- package/src/@building/building.headers.ts +37 -0
- package/src/@building/building.office.ts +43 -0
- package/src/@building/building.polygon.ts +71 -0
- package/src/@building/building.properties.ts +89 -0
- package/src/@building/building.signature.ts +78 -0
- package/src/@building/building.tags.ts +24 -0
- package/src/@building/building.tracking.ts +68 -0
- package/src/@building/building.validate.ts +132 -0
- package/src/@core/core.callback.ts +39 -0
- package/src/@core/core.getEvents.ts +25 -0
- package/src/@core/core.getNodes.ts +25 -0
- package/src/@core/core.listener.ts +24 -0
- package/src/@core/core.setNode.ts +81 -0
- package/src/@core/core.start.ts +54 -0
- package/src/@core/core.stop.ts +32 -0
- package/src/@dictionaries/dictionaries.betterEventNames.ts +85 -0
- package/src/@dictionaries/dictionaries.eventActions.ts +28 -0
- package/src/@dictionaries/{awips.ts → dictionaries.eventAwipAbreviations.ts} +12 -6
- package/src/@dictionaries/dictionaries.eventCancelMessages.ts +29 -0
- package/src/@dictionaries/dictionaries.eventCauses.ts +36 -0
- package/src/@dictionaries/dictionaries.eventProducts.ts +25 -0
- package/src/@dictionaries/dictionaries.eventRecords.ts +25 -0
- package/src/@dictionaries/dictionaries.eventSeverity.ts +27 -0
- package/src/@dictionaries/dictionaries.eventStatus.ts +31 -0
- package/src/@dictionaries/{signatures.ts → dictionaries.eventTags.ts} +13 -68
- package/src/@dictionaries/dictionaries.eventTypes.ts +82 -0
- package/src/@dictionaries/dictionaries.eventsOffshore.ts +31 -0
- package/src/@dictionaries/dictionaries.hailStrings.ts +31 -0
- package/src/@dictionaries/{icao.ts → dictionaries.officeICAOs.ts} +13 -6
- package/src/@dictionaries/dictionaries.regExp.ts +28 -0
- package/src/@dictionaries/dictionaries.shapefileLinks.ts +36 -0
- package/src/@dictionaries/dictionaries.statusCorrelationText.ts +40 -0
- package/src/@dictionaries/dictionaries.test_signatures.ts +23 -0
- package/src/@dictionaries/dictionaries.transcribedMessageReplacements.ts +68 -0
- package/src/@events/events.api.ts +113 -0
- package/src/@events/events.text.ts +79 -0
- package/src/@events/events.ugc.ts +83 -0
- package/src/@events/events.vtec.ts +87 -0
- package/src/@manager/manager.mkEvent.ts +79 -0
- package/src/@manager/manager.rmEvent.ts +44 -0
- package/src/@manager/manager.setHash.ts +37 -0
- package/src/@manager/manager.updateNodes.ts +51 -0
- package/src/@modules/@database/database.cache.ts +48 -0
- package/src/@modules/@database/database.init.ts +45 -0
- package/src/@modules/@database/database.shapefiles.ts +97 -0
- package/src/@modules/@database/database.stanza.ts +47 -0
- package/src/@modules/@stanza/stanza.getAwipsType.ts +46 -0
- package/src/@modules/@stanza/stanza.validate.ts +50 -0
- package/src/@modules/@utilities/utilities.createHttp.ts +75 -0
- package/src/@modules/@utilities/utilities.getFormattedTime.ts +43 -0
- package/src/@modules/@utilities/utilities.getSettings.ts +25 -0
- package/src/@modules/@utilities/utilities.getShapeNearestPoint.ts +114 -0
- package/src/@modules/@utilities/utilities.setCronSchedule.ts +38 -0
- package/src/@modules/@utilities/utilities.setEventEmit.ts +41 -0
- package/src/@modules/@utilities/utilities.setListener.ts +30 -0
- package/src/@modules/@utilities/utilities.setSettings.ts +42 -0
- package/src/@modules/@utilities/utilities.setSleep.ts +33 -0
- package/src/@modules/@utilities/utilities.setTimeoutAction.ts +59 -0
- package/src/@modules/@utilities/utilities.setWarning.ts +34 -0
- package/src/@modules/@xmpp/xmpp.xDeploy.ts +58 -0
- package/src/@modules/@xmpp/xmpp.xError.ts +38 -0
- package/src/@modules/@xmpp/xmpp.xOffline.ts +38 -0
- package/src/@modules/@xmpp/xmpp.xOnline.ts +61 -0
- package/src/@modules/@xmpp/xmpp.xReconnect.ts +59 -0
- package/src/@modules/@xmpp/xmpp.xStanza.ts +63 -0
- package/src/@parsers/@hvtec/hvtec.extract.ts +40 -0
- package/src/@parsers/@pvtec/pvtec.expires.ts +26 -0
- package/src/@parsers/@pvtec/pvtec.extract.ts +50 -0
- package/src/@parsers/@text/text.getDescriptionFromProduct.ts +53 -0
- package/src/@parsers/@text/text.getPolygonFromProduct.ts +32 -0
- package/src/@parsers/@text/text.getTextFromProduct.ts +43 -0
- package/src/@parsers/@text/text.getXML.ts +61 -0
- package/src/@parsers/@ugc/ugc.coordinates.ts +110 -0
- package/src/@parsers/@ugc/ugc.expiry.ts +32 -0
- package/src/@parsers/@ugc/ugc.extract.ts +37 -0
- package/src/@parsers/@ugc/ugc.header.ts +30 -0
- package/src/@parsers/@ugc/ugc.locations.ts +29 -0
- package/src/@parsers/@ugc/ugc.zones.ts +52 -0
- package/src/@types/type.event.ts +67 -0
- package/src/@types/type.properties.ts +75 -0
- package/src/@types/types.attributes.ts +28 -0
- package/src/@types/types.compiled.ts +35 -0
- package/src/@types/types.hash.ts +24 -0
- package/src/@types/types.hvtec.ts +25 -0
- package/src/@types/types.pvtec.ts +29 -0
- package/src/@types/types.settings.ts +71 -0
- package/src/@types/types.stanza.ts +37 -0
- package/src/@types/types.ugc.ts +24 -0
- package/src/bootstrap.ts +82 -163
- package/src/index.ts +48 -216
- package/test.js +65 -49
- package/src/@dictionaries/events.ts +0 -168
- package/src/@parsers/@events/api.ts +0 -146
- package/src/@parsers/@events/cap.ts +0 -123
- package/src/@parsers/@events/text.ts +0 -104
- package/src/@parsers/@events/ugc.ts +0 -107
- package/src/@parsers/@events/vtec.ts +0 -76
- package/src/@parsers/events.ts +0 -392
- package/src/@parsers/hvtec.ts +0 -46
- package/src/@parsers/pvtec.ts +0 -72
- package/src/@parsers/stanza.ts +0 -97
- package/src/@parsers/text.ts +0 -165
- package/src/@parsers/ugc.ts +0 -247
- package/src/@submodules/database.ts +0 -201
- package/src/@submodules/eas.ts +0 -490
- package/src/@submodules/utils.ts +0 -191
- package/src/@submodules/xmpp.ts +0 -142
- package/src/types.ts +0 -259
package/src/@parsers/text.ts
DELETED
|
@@ -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;
|
package/src/@parsers/ugc.ts
DELETED
|
@@ -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;
|