@atmosx/event-product-parser 3.0.61 → 3.0.63
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/.github/docs/docs-functions.md +1 -1
- package/.github/docs/docs-general-configurations.md +1 -1
- package/.github/docs/docs-integrations.md +1 -1
- package/.github/docs/docs-listeners.md +1 -1
- package/.github/docs/docs-source-configuration.md +1 -1
- package/.github/docs/docs-technical.md +1 -1
- package/README.md +7 -5
- package/dist/cjs/index.cjs +21216 -6908
- package/dist/esm/index.mjs +21207 -6900
- package/package.json +49 -47
- package/pnpm-workspace.yaml +4 -0
- package/src/@building/CreateEvent.ts +20 -22
- package/src/@building/GetEventAttachments.ts +8 -8
- package/src/@building/GetEventEnhancedName.ts +2 -4
- package/src/@building/GetEventGeometry.ts +5 -5
- package/src/@building/GetEventHeader.ts +11 -11
- package/src/@building/GetEventNodes.ts +12 -12
- package/src/@building/GetEventOffice.ts +11 -11
- package/src/@building/GetEventProperties.ts +60 -55
- package/src/@building/GetEventSignature.ts +7 -7
- package/src/@building/GetEventTags.ts +1 -1
- package/src/@building/GetEventTracking.ts +28 -32
- package/src/@core/ClearEvents.ts +7 -7
- package/src/@core/CreateListener.ts +2 -2
- package/src/@core/GetEvents.ts +2 -2
- package/src/@core/GetNodes.ts +2 -2
- package/src/@core/GetRandomEvent.ts +2 -2
- package/src/@core/GetVersion.ts +2 -2
- package/src/@core/ManualEvent.ts +20 -21
- package/src/@core/QueryStanza.ts +7 -7
- package/src/@core/SetNode.ts +32 -26
- package/src/@core/StartService.ts +18 -18
- package/src/@core/StopService.ts +8 -8
- package/src/@enums/Expressions.ts +1 -1
- package/src/@enums/Tags.ts +1 -1
- package/src/@events/ParseAPI.ts +44 -39
- package/src/@events/ParseText.ts +19 -19
- package/src/@events/ParseUGC.ts +21 -21
- package/src/@events/ParseVTEC.ts +32 -33
- package/src/@manager/MakeEvents.ts +33 -30
- package/src/@manager/RemoveEvent.ts +38 -34
- package/src/@manager/SetHash.ts +16 -11
- package/src/@manager/UpdateEvents.ts +10 -10
- package/src/@manager/UpdateNode.ts +13 -13
- package/src/@manager/ValidateEvents.ts +36 -37
- package/src/@modules/@database/GetCachedEvents.ts +10 -12
- package/src/@modules/@database/ImportBroadcastify.ts +12 -13
- package/src/@modules/@database/ImportShapefiles.ts +17 -18
- package/src/@modules/@database/ImportStanza.ts +11 -11
- package/src/@modules/@database/InitializeDatabase.ts +16 -16
- package/src/@modules/@eas/GenerateEASMessage.ts +53 -47
- package/src/@modules/@eas/GetPCM16.ts +11 -6
- package/src/@modules/@eas/GetSampledPCM16.ts +12 -6
- package/src/@modules/@eas/GetTTS.ts +12 -7
- package/src/@modules/@eas/GetWavPCM16.ts +5 -5
- package/src/@modules/@eas/SetAFSK.ts +11 -6
- package/src/@modules/@eas/SetAttentionTone.ts +9 -4
- package/src/@modules/@eas/SetNoise.ts +10 -5
- package/src/@modules/@eas/SetRadioEffect.ts +10 -5
- package/src/@modules/@eas/SetSameHeader.ts +20 -12
- package/src/@modules/@stanza/GetAwipsType.ts +14 -15
- package/src/@modules/@stanza/ValidateStanza.ts +25 -13
- package/src/@modules/@utilities/CreateHttp.ts +24 -24
- package/src/@modules/@utilities/GetMatched.ts +8 -3
- package/src/@modules/@utilities/GetSettings.ts +3 -3
- package/src/@modules/@utilities/GetShapeNearestPoint.ts +22 -17
- package/src/@modules/@utilities/QueueManager.ts +70 -0
- package/src/@modules/@utilities/SetCronSchedule.ts +20 -20
- package/src/@modules/@utilities/SetDebug.ts +10 -10
- package/src/@modules/@utilities/SetEventEmit.ts +15 -15
- package/src/@modules/@utilities/SetListener.ts +6 -6
- package/src/@modules/@utilities/SetSettings.ts +5 -5
- package/src/@modules/@utilities/SetSleep.ts +3 -3
- package/src/@modules/@utilities/SetTimeoutAction.ts +25 -25
- package/src/@modules/@utilities/SetWarning.ts +8 -8
- package/src/@modules/@xmpp/DeployXMPP.ts +16 -16
- package/src/@modules/@xmpp/ErrorXMPP.ts +4 -4
- package/src/@modules/@xmpp/OfflineXMPP.ts +11 -11
- package/src/@modules/@xmpp/OnlineXMPP.ts +16 -16
- package/src/@modules/@xmpp/ReconnectXMPP.ts +27 -27
- package/src/@modules/@xmpp/StanzaXMPP.ts +23 -23
- package/src/@parsers/@hvtec/HVExtract.ts +9 -9
- package/src/@parsers/@text/GetDescriptionFromProduct.ts +18 -19
- package/src/@parsers/@text/GetEmbededText.ts +3 -3
- package/src/@parsers/@text/GetStringText.ts +6 -6
- package/src/@parsers/@text/GetTextFromProduct.ts +10 -10
- package/src/@parsers/@text/GetXML.ts +5 -5
- package/src/@parsers/@ugc/GetCache.ts +2 -2
- package/src/@parsers/@ugc/{GetUGCHeader.ts → GetHeader.ts} +2 -2
- package/src/@parsers/@ugc/GetLocations.ts +5 -5
- package/src/@parsers/@ugc/GetZonePolygon.ts +10 -10
- package/src/@parsers/@ugc/GetZones.ts +1 -1
- package/src/@parsers/@ugc/SetCache.ts +11 -6
- package/src/@parsers/@ugc/UGCExtract.ts +10 -10
- package/src/@parsers/{@pvtec → @vtec}/VTECExtract.ts +19 -19
- package/src/@tasks/CreateTasks.ts +110 -0
- package/src/@tasks/TaskGenerateAudio.ts +34 -0
- package/src/@tasks/TaskGenerateJSON.ts +34 -0
- package/src/@tasks/TaskGenerateText.ts +34 -0
- package/src/@tasks/TaskSendNTFY.ts +91 -0
- package/src/@tasks/TaskSendWebhook.ts +100 -0
- package/src/@types/HVTEC.ts +4 -4
- package/src/@types/Hash.ts +3 -3
- package/src/@types/Settings.ts +1 -1
- package/src/@types/StanzaCompiled.ts +13 -13
- package/src/@types/UGC.ts +3 -3
- package/src/@types/VTEC.ts +10 -10
- package/src/{@types → @types-lower}/Event.ts +4 -4
- package/src/{@types → @types-lower}/Properties.ts +2 -1
- package/src/{@types → @types-lower}/Stanza.ts +1 -1
- package/src/bootstrap.ts +26 -26
- package/src/index.ts +30 -27
- package/tools/build.js +45 -0
- package/tsconfig.json +21 -1
- package/src/@manager/Tasks.ts +0 -246
- package/tsup.config.ts +0 -16
- /package/src/@parsers/{@pvtec → @vtec}/GetExpiry.ts +0 -0
- /package/src/{@types → @types-lower}/Attributes.ts +0 -0
|
@@ -17,24 +17,24 @@
|
|
|
17
17
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { TypeEvent } from "
|
|
21
|
-
import { TypeSettings } from "
|
|
22
|
-
import {
|
|
23
|
-
import { GetEventEnhancedName } from "
|
|
24
|
-
import { GetEventSignature } from "
|
|
25
|
-
import { MakeEvents } from "
|
|
26
|
-
import { RemoveEvent } from "
|
|
27
|
-
import { GetEventAttachments } from "
|
|
28
|
-
import { SetEventEmit } from "
|
|
29
|
-
import { SetDebug } from "
|
|
30
|
-
import { GetMatched } from "
|
|
31
|
-
import { GetEventGeometry } from "
|
|
20
|
+
import { TypeEvent } from "types-lower/Event"
|
|
21
|
+
import { TypeSettings } from "types/Settings"
|
|
22
|
+
import { Bootstrap } from "@bootstrap"
|
|
23
|
+
import { GetEventEnhancedName } from "@building/GetEventEnhancedName"
|
|
24
|
+
import { GetEventSignature } from "@building/GetEventSignature"
|
|
25
|
+
import { MakeEvents } from "@manager/MakeEvents"
|
|
26
|
+
import { RemoveEvent } from "@manager/RemoveEvent"
|
|
27
|
+
import { GetEventAttachments } from "@building/GetEventAttachments"
|
|
28
|
+
import { SetEventEmit } from "@utilities/SetEventEmit"
|
|
29
|
+
import { SetDebug } from "@utilities/SetDebug"
|
|
30
|
+
import { GetMatched } from "@utilities/GetMatched"
|
|
31
|
+
import { GetEventGeometry } from "@building/GetEventGeometry"
|
|
32
32
|
import { createHash } from "crypto"
|
|
33
33
|
|
|
34
34
|
export const ValidateEvents = async (events: TypeEvent[]): Promise<void> => {
|
|
35
35
|
const tick = performance.now();
|
|
36
36
|
if (events.length === 0) return
|
|
37
|
-
const configurations =
|
|
37
|
+
const configurations = Bootstrap.Settings as TypeSettings
|
|
38
38
|
const sets = {} as Record<string, Set<string>>;
|
|
39
39
|
const bools = {} as Record<string, boolean>;
|
|
40
40
|
const megered = {...configurations.GlobalSettings, ...configurations.GlobalSettings.EventFiltering}
|
|
@@ -50,19 +50,19 @@ export const ValidateEvents = async (events: TypeEvent[]): Promise<void> => {
|
|
|
50
50
|
const icao = properties.geocode.office.office
|
|
51
51
|
|
|
52
52
|
if (properties.status_metadata.is_test) {
|
|
53
|
-
SetEventEmit({
|
|
53
|
+
SetEventEmit({ Event: `onTestProduct`, Metadata: define })
|
|
54
54
|
if (bools?.IgnoreTestProducts) return true;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
if (properties.status_metadata.is_expired) {
|
|
58
|
-
SetEventEmit({
|
|
59
|
-
RemoveEvent(define, false)
|
|
58
|
+
SetEventEmit({ Event: `onExpiredProduct`, Metadata: define })
|
|
59
|
+
RemoveEvent({ Event: define, IsTimeBasedExpiration: false })
|
|
60
60
|
return true;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
if (properties.metadata?.vtec?.
|
|
64
|
-
const isSPC = properties.metadata?.vtec?.
|
|
65
|
-
SetEventEmit({
|
|
63
|
+
if (properties.metadata?.vtec?.Watch && properties.metadata.source != `events.api`) {
|
|
64
|
+
const isSPC = properties.metadata?.vtec?.PredictionCenter;
|
|
65
|
+
SetEventEmit({ Event: isSPC ? `onStormPredictionWatch` : `onNonStormPredictionWatch`, Metadata: define })
|
|
66
66
|
if (bools?.SPCWatchesOnly && !isSPC) {
|
|
67
67
|
return true;
|
|
68
68
|
}
|
|
@@ -74,45 +74,45 @@ export const ValidateEvents = async (events: TypeEvent[]): Promise<void> => {
|
|
|
74
74
|
for (const key in sets) {
|
|
75
75
|
const setting = sets[key]
|
|
76
76
|
const values = [...setting];
|
|
77
|
-
if (key === 'ListeningEvents' && setting.size > 0 && !GetMatched(values, define.properties.event)) {
|
|
77
|
+
if (key === 'ListeningEvents' && setting.size > 0 && !GetMatched({ Strings: values, String: define.properties.event })) {
|
|
78
78
|
SetEventEmit({
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
Event: `onFilteredEvent`,
|
|
80
|
+
Metadata: define
|
|
81
81
|
});
|
|
82
82
|
return true
|
|
83
83
|
}
|
|
84
|
-
if (key === 'IgnoredEvents' && setting.size > 0 && GetMatched(values, define.properties.event)) {
|
|
84
|
+
if (key === 'IgnoredEvents' && setting.size > 0 && GetMatched({ Strings: values, String: define.properties.event })) {
|
|
85
85
|
SetEventEmit({
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
Event: `onIgnoredEvent`,
|
|
87
|
+
Metadata: define
|
|
88
88
|
});
|
|
89
89
|
return true
|
|
90
90
|
}
|
|
91
91
|
if (key === 'ListeningICAO' && setting.size > 0 && icao != null && !setting.has(icao.toLowerCase())) {
|
|
92
92
|
SetEventEmit({
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
Event: `onFilteredICAO`,
|
|
94
|
+
Metadata: define
|
|
95
95
|
});
|
|
96
96
|
return true
|
|
97
97
|
}
|
|
98
98
|
if (key === 'IgnoredICAO' && setting.size > 0 && icao != null && setting.has(icao.toLowerCase())) {
|
|
99
99
|
SetEventEmit({
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
Event: `onIgnoredICAO`,
|
|
101
|
+
Metadata: define
|
|
102
102
|
});
|
|
103
103
|
return true
|
|
104
104
|
}
|
|
105
105
|
if (key === 'ListeningUGC' && setting.size > 0 && zones.length > 0 && !zones.some((ugc: string) => setting.has(ugc.toLowerCase()))) {
|
|
106
106
|
SetEventEmit({
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
Event: `onFilteredUGC`,
|
|
108
|
+
Metadata: define
|
|
109
109
|
});
|
|
110
110
|
return true
|
|
111
111
|
}
|
|
112
112
|
if (key === 'ListeningStates' && setting.size > 0 && zones.length > 0 && !zones.some((ugc: string) => setting.has(ugc.substring(0, 2).toLowerCase()))) {
|
|
113
113
|
SetEventEmit({
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
Event: `onFilteredState`,
|
|
115
|
+
Metadata: define
|
|
116
116
|
});
|
|
117
117
|
return true
|
|
118
118
|
}
|
|
@@ -121,7 +121,7 @@ export const ValidateEvents = async (events: TypeEvent[]): Promise<void> => {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
const filtering = events.filter((event: TypeEvent) => {
|
|
124
|
-
|
|
124
|
+
Bootstrap.Cache.Parsed = Bootstrap.Cache.Parsed.filter((e) => e !== event);
|
|
125
125
|
const define = GetEventSignature(event) as TypeEvent;
|
|
126
126
|
const pre = {...define,properties: {...define.properties, metadata: {...define.properties.metadata }}};
|
|
127
127
|
const properties = define.properties; delete pre.properties.metadata.ms; delete pre.properties.metadata.header;
|
|
@@ -132,11 +132,10 @@ export const ValidateEvents = async (events: TypeEvent[]): Promise<void> => {
|
|
|
132
132
|
properties.metadata.attachments = GetEventAttachments(event)
|
|
133
133
|
}
|
|
134
134
|
properties.metadata.hash = createHash("sha256").update(JSON.stringify(pre)).digest("hex")
|
|
135
|
-
SetEventEmit({
|
|
135
|
+
SetEventEmit({ Event: `onProductType${enhanced.replace(/\s+/g, '')}`, Metadata: define });
|
|
136
136
|
return !filtered
|
|
137
137
|
})
|
|
138
138
|
|
|
139
|
-
SetDebug({
|
|
139
|
+
SetDebug({ Title: `ValidateEvents (${filtering.length}/${events.length})`, Message: `${Math.round(performance.now() - tick)}ms` })
|
|
140
140
|
await MakeEvents(filtering)
|
|
141
|
-
SetDebug({ title: `MakeEvents (${filtering.length}/${events.length})`, message: `${Math.round(performance.now() - tick)}ms` })
|
|
142
141
|
}
|
|
@@ -17,20 +17,19 @@
|
|
|
17
17
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { TypeSettings } from "
|
|
21
|
-
import {
|
|
22
|
-
import { SetWarning } from
|
|
23
|
-
import { CreateEvent } from
|
|
24
|
-
import { ValidateEvents } from "../../@manager/ValidateEvents";
|
|
20
|
+
import { TypeSettings } from "types/Settings"
|
|
21
|
+
import { Bootstrap } from "@bootstrap"
|
|
22
|
+
import { SetWarning } from "@utilities/SetWarning"
|
|
23
|
+
import { CreateEvent } from "@building/CreateEvent"
|
|
25
24
|
|
|
26
25
|
export const GetCachedEvents = async (): Promise<void> => {
|
|
27
26
|
try {
|
|
28
|
-
const settings =
|
|
27
|
+
const settings = Bootstrap.Settings as TypeSettings;
|
|
29
28
|
const tick = performance.now();
|
|
30
29
|
if (settings.NOAAWeatherWireServiceSettings.CacheSettings.Enabled) {
|
|
31
30
|
const max = settings.NOAAWeatherWireServiceSettings.CacheSettings.MaxRetentionHistory ?? 500;
|
|
32
|
-
const get = await
|
|
33
|
-
SetWarning({
|
|
31
|
+
const get = await Bootstrap.Database.prepare(`SELECT * FROM stanzas ORDER BY rowid DESC LIMIT ?`).all(max) as { rowid: number; stanza: string }[];
|
|
32
|
+
SetWarning({ Message: `Fetched ${get.length} cached stanzas from the database in ${Math.floor(performance.now() - tick)} ms` })
|
|
34
33
|
let events = get.map((row) => JSON.parse(row.stanza))
|
|
35
34
|
.filter(stanza => {
|
|
36
35
|
if (!stanza) { return }
|
|
@@ -40,11 +39,10 @@ export const GetCachedEvents = async (): Promise<void> => {
|
|
|
40
39
|
return !isSkippable
|
|
41
40
|
});
|
|
42
41
|
events = events.sort((a, b) => b.issued - a.issued)
|
|
43
|
-
await Promise.all(events.map(event => CreateEvent(event
|
|
44
|
-
SetWarning({
|
|
45
|
-
await ValidateEvents(bootstrap.cache.processed)
|
|
42
|
+
await Promise.all(events.map(event => CreateEvent(event)))
|
|
43
|
+
SetWarning({ Message: `Processed ${events.length} cached stanzas in ${Math.floor(performance.now() - tick)} ms` })
|
|
46
44
|
}
|
|
47
45
|
} catch (error) {
|
|
48
|
-
SetWarning({
|
|
46
|
+
SetWarning({ Message: `An error occurred while fetching cached stanzas: ${error.message} -> ${error.stack}` })
|
|
49
47
|
}
|
|
50
48
|
}
|
|
@@ -17,38 +17,37 @@
|
|
|
17
17
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
21
|
-
import { CreateHttp } from
|
|
22
|
-
import { SetWarning } from "
|
|
20
|
+
import { Bootstrap } from "@bootstrap"
|
|
21
|
+
import { CreateHttp } from "@utilities/CreateHttp"
|
|
22
|
+
import { SetWarning } from "@utilities/SetWarning"
|
|
23
23
|
|
|
24
24
|
export const ImportBroadcastify = async (): Promise<void> => {
|
|
25
|
-
const settings =
|
|
25
|
+
const settings = Bootstrap.Settings;
|
|
26
26
|
|
|
27
|
-
const isBroadcastifyImported =
|
|
27
|
+
const isBroadcastifyImported = Bootstrap.Database
|
|
28
28
|
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='broadcastify';`)
|
|
29
29
|
.get();
|
|
30
30
|
|
|
31
|
-
|
|
32
31
|
if (!settings.BroadcastifySettings.BroadcastifyAttachments) { return }
|
|
33
32
|
if (isBroadcastifyImported) { return }
|
|
34
33
|
|
|
35
34
|
const broadcastify = await CreateHttp({
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
URL: settings.BroadcastifySettings.BroadcastifyDatabase,
|
|
36
|
+
Timeout: 5000,
|
|
38
37
|
});
|
|
39
38
|
if (!broadcastify.error) {
|
|
40
|
-
|
|
39
|
+
Bootstrap.Database.prepare(`CREATE TABLE broadcastify ( state TEXT, county TEXT, feed TEXT, type TEXT, link TEXT);`)
|
|
41
40
|
.run();
|
|
42
41
|
const states = JSON.parse(broadcastify.message);
|
|
43
|
-
const insert =
|
|
44
|
-
const transaction =
|
|
42
|
+
const insert = Bootstrap.Database.prepare(`INSERT INTO broadcastify (state, county, feed, type, link) VALUES (?, ?, ?, ?, ?)`);
|
|
43
|
+
const transaction = Bootstrap.Database.transaction((rows: any[]) => {
|
|
45
44
|
for (const row of rows) {
|
|
46
45
|
insert.run(row);
|
|
47
46
|
}
|
|
48
47
|
});
|
|
49
48
|
const batch: any[] = [];
|
|
50
49
|
for (const state of states) {
|
|
51
|
-
SetWarning({
|
|
50
|
+
SetWarning({ Message: `Importing ${state.counties.length} counties from ${state.state} for Broadcastify` });
|
|
52
51
|
for (const county of state.counties) {
|
|
53
52
|
const countyName = county.county;
|
|
54
53
|
for (const feed of county.feeds) {
|
|
@@ -65,6 +64,6 @@ export const ImportBroadcastify = async (): Promise<void> => {
|
|
|
65
64
|
transaction(batch);
|
|
66
65
|
}
|
|
67
66
|
if (broadcastify.error) {
|
|
68
|
-
SetWarning({
|
|
67
|
+
SetWarning({ Message: `Error importing Broadcastify data: ${broadcastify.message}` });
|
|
69
68
|
}
|
|
70
69
|
};
|
|
@@ -17,29 +17,28 @@
|
|
|
17
17
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { TypeSettings } from "
|
|
21
|
-
import { EnumShapefiles } from
|
|
22
|
-
import {
|
|
23
|
-
import { SetSleep } from
|
|
24
|
-
import { SetWarning } from
|
|
20
|
+
import { TypeSettings } from "types/Settings"
|
|
21
|
+
import { EnumShapefiles } from "@enums/Shapefiles"
|
|
22
|
+
import { Bootstrap } from "@bootstrap"
|
|
23
|
+
import { SetSleep } from "@utilities/SetSleep"
|
|
24
|
+
import { SetWarning } from "@utilities/SetWarning"
|
|
25
25
|
import { existsSync, mkdirSync, writeFileSync, unlinkSync, rm } from "fs"
|
|
26
|
-
import { resolve, extname} from
|
|
27
|
-
import { loadAsync } from
|
|
28
|
-
import { read } from
|
|
26
|
+
import { resolve, extname} from "path"
|
|
27
|
+
import { loadAsync } from "jszip"
|
|
28
|
+
import { read } from "shapefile"
|
|
29
29
|
|
|
30
30
|
export const ImportShapefiles = async (): Promise<void> => {
|
|
31
|
-
const settings = bootstrap.settings as TypeSettings;
|
|
32
31
|
try {
|
|
33
|
-
const tShapefiles =
|
|
32
|
+
const tShapefiles = Bootstrap.Database
|
|
34
33
|
.prepare(`SELECT COUNT(*) AS count FROM shapefiles`)
|
|
35
34
|
.get().count;
|
|
36
35
|
if (tShapefiles === 0) {
|
|
37
|
-
await SetSleep({
|
|
36
|
+
await SetSleep({Timeout: 1e3});
|
|
38
37
|
for (const shapefile of EnumShapefiles) {
|
|
39
38
|
const response = await fetch(shapefile.link);
|
|
40
39
|
const arrayBuff = await response.arrayBuffer();
|
|
41
40
|
const content = await loadAsync(arrayBuff);
|
|
42
|
-
const directory = resolve(__dirname,
|
|
41
|
+
const directory = resolve(__dirname, `shapefiles`);
|
|
43
42
|
if (!existsSync(directory)) {
|
|
44
43
|
mkdirSync(directory, { recursive: true });
|
|
45
44
|
}
|
|
@@ -50,15 +49,15 @@ export const ImportShapefiles = async (): Promise<void> => {
|
|
|
50
49
|
writeFileSync(output, data)
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
|
-
const filepath = resolve(__dirname, '
|
|
52
|
+
const filepath = resolve(__dirname, 'shapefiles', shapefile.name + '_' + shapefile.id)
|
|
54
53
|
const { features } = await read(
|
|
55
54
|
filepath,
|
|
56
55
|
filepath,
|
|
57
56
|
);
|
|
58
|
-
SetWarning({
|
|
59
|
-
const insert =
|
|
57
|
+
SetWarning({ Message: `Importing ${features.length} features from ${shapefile.name}_${shapefile.id} for Shapefiles` })
|
|
58
|
+
const insert = Bootstrap.Database
|
|
60
59
|
.prepare(`INSERT OR REPLACE INTO shapefiles (id, location, geometry) VALUES (?, ?, ?)`)
|
|
61
|
-
const transaction =
|
|
60
|
+
const transaction = Bootstrap.Database.transaction((entries: any[]) => {
|
|
62
61
|
for (const entry of entries) {
|
|
63
62
|
const { properties, geometry } = entry;
|
|
64
63
|
let final: string, location: string;
|
|
@@ -85,9 +84,9 @@ export const ImportShapefiles = async (): Promise<void> => {
|
|
|
85
84
|
unlinkSync(`${filepath}.dbf`)
|
|
86
85
|
transaction(features)
|
|
87
86
|
}
|
|
88
|
-
rm(resolve(__dirname, '
|
|
87
|
+
rm(resolve(__dirname, 'shapefiles'), { recursive: true, force: true }, () => {});
|
|
89
88
|
}
|
|
90
89
|
} catch (error) {
|
|
91
|
-
SetWarning( {
|
|
90
|
+
SetWarning( {Message: `An error occurred while compiling shapefiles: ${error.message}` })
|
|
92
91
|
}
|
|
93
92
|
}
|
|
@@ -17,31 +17,31 @@
|
|
|
17
17
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { TypeSettings } from "
|
|
21
|
-
import { TypeStanzaCompiled } from
|
|
22
|
-
import {
|
|
23
|
-
import { SetWarning } from
|
|
20
|
+
import { TypeSettings } from "types/Settings"
|
|
21
|
+
import { TypeStanzaCompiled } from "types/StanzaCompiled"
|
|
22
|
+
import { Bootstrap } from "@bootstrap"
|
|
23
|
+
import { SetWarning } from "@utilities/SetWarning"
|
|
24
24
|
|
|
25
|
-
export const ImportStanza = async (
|
|
26
|
-
const settings =
|
|
25
|
+
export const ImportStanza = async (Stanza: TypeStanzaCompiled): Promise<void> => {
|
|
26
|
+
const settings = Bootstrap.Settings as TypeSettings;
|
|
27
27
|
try {
|
|
28
28
|
if (!settings.NOAAWeatherWireServiceSettings.CacheSettings.Enabled) { return }
|
|
29
|
-
|
|
29
|
+
Bootstrap.Database
|
|
30
30
|
.prepare(`INSERT OR IGNORE INTO stanzas (type, stanza, issued) VALUES (?, ?, ?)`)
|
|
31
|
-
.run(
|
|
32
|
-
const count =
|
|
31
|
+
.run(Stanza.Type.Type, JSON.stringify(Stanza), Stanza.Attributes.issue)
|
|
32
|
+
const count = Bootstrap.Database
|
|
33
33
|
.prepare(`SELECT COUNT(*) as total FROM stanzas`)
|
|
34
34
|
.get() as { total: number }
|
|
35
35
|
const max = settings.NOAAWeatherWireServiceSettings.CacheSettings.MaxDatabaseHistory;
|
|
36
36
|
if (count.total > max) {
|
|
37
37
|
const toDelete = count.total - max;
|
|
38
38
|
if (toDelete > 0) {
|
|
39
|
-
|
|
39
|
+
Bootstrap.Database
|
|
40
40
|
.prepare(`DELETE FROM stanzas WHERE id IN (SELECT id FROM stanzas ORDER BY issued ASC LIMIT ?)`)
|
|
41
41
|
.run(toDelete);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
} catch (error) {
|
|
45
|
-
SetWarning({
|
|
45
|
+
SetWarning({ Message: `An error occurred while importing stanza: ${error.message}` })
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -18,42 +18,42 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
import { TypeSettings } from "
|
|
22
|
-
import {
|
|
23
|
-
import { SetWarning } from
|
|
24
|
-
import { ImportShapefiles } from
|
|
25
|
-
import { ImportBroadcastify } from "
|
|
21
|
+
import { TypeSettings } from "types/Settings"
|
|
22
|
+
import { Bootstrap } from "@bootstrap"
|
|
23
|
+
import { SetWarning } from "@utilities/SetWarning"
|
|
24
|
+
import { ImportShapefiles } from "@database/ImportShapefiles"
|
|
25
|
+
import { ImportBroadcastify } from "@database/ImportBroadcastify"
|
|
26
26
|
import { existsSync, writeFileSync } from "fs"
|
|
27
|
-
import sqlite3 from
|
|
27
|
+
import sqlite3 from "better-sqlite3"
|
|
28
28
|
|
|
29
29
|
export const InitializeDatabase = async (): Promise<void> => {
|
|
30
|
-
const settings =
|
|
30
|
+
const settings = Bootstrap.Settings as TypeSettings;
|
|
31
31
|
try {
|
|
32
32
|
if (!existsSync(settings.Database)) {
|
|
33
33
|
writeFileSync(settings.Database, '')
|
|
34
|
-
SetWarning({
|
|
34
|
+
SetWarning({ Message: `Creating new database at ${settings.Database}` })
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
Bootstrap.Database = new sqlite3(settings.Database);
|
|
37
|
+
Bootstrap.Database
|
|
38
38
|
.prepare(`CREATE TABLE IF NOT EXISTS stanzas ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT, issued TEXT, stanza TEXT )`)
|
|
39
39
|
.run();
|
|
40
|
-
|
|
40
|
+
Bootstrap.Database
|
|
41
41
|
.prepare(`CREATE TABLE IF NOT EXISTS shapefiles (id TEXT PRIMARY KEY, location TEXT, geometry TEXT)`)
|
|
42
42
|
.run();
|
|
43
|
-
const isNeedingShapefiles =
|
|
43
|
+
const isNeedingShapefiles = Bootstrap.Database
|
|
44
44
|
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='shapefiles';`)
|
|
45
45
|
.get();
|
|
46
|
-
const isNeedingBroadcastify =
|
|
46
|
+
const isNeedingBroadcastify = Bootstrap.Database
|
|
47
47
|
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='broadcastify';`)
|
|
48
48
|
.get();
|
|
49
49
|
|
|
50
50
|
if (!isNeedingShapefiles || !isNeedingBroadcastify) {
|
|
51
|
-
SetWarning({
|
|
51
|
+
SetWarning({Message: `Required database tables are currently building, please ${Bootstrap.Colors.Red}DO NOT${Bootstrap.Colors.Reset} close your terminal. The building will not finish and will remain incomplete. If you do mess up, you will need to delete ${settings.Database} and restart the application.` })
|
|
52
52
|
await ImportBroadcastify();
|
|
53
53
|
await ImportShapefiles();
|
|
54
|
-
SetWarning({
|
|
54
|
+
SetWarning({ Message: `Building has completed, you can now continue or close the terminal`})
|
|
55
55
|
}
|
|
56
56
|
} catch (error) {
|
|
57
|
-
SetWarning({
|
|
57
|
+
SetWarning({ Message: `An error occurred while initializing the database: ${error.message}` })
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -17,44 +17,42 @@
|
|
|
17
17
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { TypeSettings } from "
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { SetDebug } from "
|
|
34
|
-
import { join } from
|
|
35
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from
|
|
36
|
-
import { execSync } from
|
|
37
|
-
import { platform } from
|
|
20
|
+
import { TypeSettings } from "types/Settings"
|
|
21
|
+
import { Bootstrap } from "@bootstrap"
|
|
22
|
+
import { GetWavPCM16 } from "@eas/GetWavPCM16"
|
|
23
|
+
import { GetSampledPCM16 } from "@eas/GetSampledPCM16"
|
|
24
|
+
import { SetRadioEffect } from "@eas/SetRadioEffect"
|
|
25
|
+
import { SetSameHeader } from "@eas/SetSameHeader"
|
|
26
|
+
import { SetAttentionTone } from "@eas/SetAttentionTone"
|
|
27
|
+
import { GetMergedPCM16 } from "@eas/GetMergedPCM16"
|
|
28
|
+
import { SetNoise } from "@eas/SetNoise"
|
|
29
|
+
import { GetPCM16 } from "@eas/GetPCM16"
|
|
30
|
+
import { GetTTS } from "@eas/GetTTS"
|
|
31
|
+
import { GetCleanDescription } from "@eas/GetCleanDescription"
|
|
32
|
+
import { SetWarning } from "@utilities/SetWarning"
|
|
33
|
+
import { SetDebug } from "@utilities/SetDebug"
|
|
34
|
+
import { join } from "path"
|
|
35
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs"
|
|
36
|
+
import { execSync } from "child_process"
|
|
37
|
+
import { platform } from "os"
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
interface GenerateEASMessageOptions {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
Message: string
|
|
42
|
+
Header: string
|
|
43
|
+
Title: string
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export const GenerateEASMessage = async (
|
|
46
|
+
export const GenerateEASMessage = async ({ Message, Header, Title }: GenerateEASMessageOptions): Promise<string> => {
|
|
47
47
|
const tick = performance.now()
|
|
48
|
-
const settings =
|
|
48
|
+
const settings = Bootstrap.Settings as TypeSettings;
|
|
49
49
|
const directory = settings.GlobalSettings.ArchiveSettings.EasDirectory;
|
|
50
50
|
const prefix = settings.GlobalSettings.ArchiveSettings.EasToneout;
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
let title = (options.title ?? `${Math.random().toString(36).substring(2, 15)}-${header.replace(/[^a-zA-Z0-9]/g, '')}`).replace(/[\\:*?"<>|]/g, "_").replace(/\s+/g, " ").trim();
|
|
54
|
-
if (!message || !header) {
|
|
51
|
+
let title = (Title ?? `${Math.random().toString(36).substring(2, 15)}-${Header.replace(/[^a-zA-Z0-9]/g, '')}`).replace(/[\\:*?"<>|]/g, "_").replace(/\s+/g, " ").trim();
|
|
52
|
+
if (!Message || !Header) {
|
|
55
53
|
SetWarning({
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
Title: `EAS`,
|
|
55
|
+
Message: `Message and header are required to generate an EAS tone.`
|
|
58
56
|
})
|
|
59
57
|
return null;
|
|
60
58
|
}
|
|
@@ -62,6 +60,14 @@ export const GenerateEASMessage = async (options: GenerateEASMessageOptions): Pr
|
|
|
62
60
|
let buffRadio: any;
|
|
63
61
|
let buffFull: any[] = [];
|
|
64
62
|
|
|
63
|
+
if (!directory) {
|
|
64
|
+
SetWarning({
|
|
65
|
+
Title: `EAS`,
|
|
66
|
+
Message: `EAS directory is not set in the settings. Please set it to generate EAS tones.`
|
|
67
|
+
});
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
if (!existsSync(directory)) {
|
|
66
72
|
mkdirSync(directory, { recursive: true });
|
|
67
73
|
}
|
|
@@ -71,16 +77,16 @@ export const GenerateEASMessage = async (options: GenerateEASMessageOptions): Pr
|
|
|
71
77
|
|
|
72
78
|
if (vPlatform === 'darwin') {
|
|
73
79
|
SetWarning({
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
Title: `EAS`,
|
|
81
|
+
Message: `EAS tone generation is not supported on macOS.`
|
|
76
82
|
});
|
|
77
83
|
return null
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
|
|
81
87
|
await new Promise<void>((resolve, reject) => {
|
|
82
|
-
const tMsg = GetCleanDescription(
|
|
83
|
-
GetTTS(tMsg, tmpTTS).then(() => {
|
|
88
|
+
const tMsg = GetCleanDescription(Message)
|
|
89
|
+
GetTTS({Text: tMsg, OutputPath: tmpTTS}).then(() => {
|
|
84
90
|
resolve();
|
|
85
91
|
});
|
|
86
92
|
});
|
|
@@ -89,8 +95,8 @@ export const GenerateEASMessage = async (options: GenerateEASMessageOptions): Pr
|
|
|
89
95
|
}
|
|
90
96
|
buffTTS = readFileSync(tmpTTS);
|
|
91
97
|
const vWav = GetWavPCM16(buffTTS);
|
|
92
|
-
const vSamples = GetSampledPCM16(vWav.
|
|
93
|
-
const vRadio = SetRadioEffect(vSamples, 8000)
|
|
98
|
+
const vSamples = GetSampledPCM16({Int16: vWav.Samples, OriginalRate: vWav.SampleRate, TargetRate: 8000})
|
|
99
|
+
const vRadio = SetRadioEffect({Int16: vSamples, SampleRate: 8000});
|
|
94
100
|
|
|
95
101
|
if (existsSync(prefix)) {
|
|
96
102
|
let tBuffer = readFileSync(prefix);
|
|
@@ -109,35 +115,35 @@ export const GenerateEASMessage = async (options: GenerateEASMessageOptions): Pr
|
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
if (tWav == null) {
|
|
112
|
-
SetWarning({
|
|
118
|
+
SetWarning({
|
|
119
|
+
Title: `EAS`,
|
|
120
|
+
Message: `Intro tone isn't a valid .WAV file or isn't in PCM 16-bit format. Converted attempt failed; please convert it then try again.`
|
|
121
|
+
})
|
|
113
122
|
return null;
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
const tSamples = (tWav.
|
|
117
|
-
buffRadio = SetRadioEffect(tSamples, 8000)
|
|
125
|
+
const tSamples = (tWav.SampleRate != 8000 ? GetSampledPCM16({Int16: tWav.Samples, OriginalRate: tWav.SampleRate, TargetRate: 8000}) : tWav.Samples)
|
|
126
|
+
buffRadio = SetRadioEffect({Int16: tSamples, SampleRate: 8000})
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
buffFull = buffRadio != null ? [buffRadio, new Int16Array(Math.floor(0.5 * 8000))] : [];
|
|
121
130
|
buffFull.push(
|
|
122
|
-
SetSameHeader(
|
|
123
|
-
preMarkSec: 1.1,
|
|
124
|
-
gapSec: 0.5,
|
|
125
|
-
}),
|
|
131
|
+
SetSameHeader({ VTEC: Header, Repeats: 3, SampleRate: 8000, PreMarkSec: 1.1, GapSec: 0.5 }),
|
|
126
132
|
new Int16Array(Math.floor(0.5 * 8000)),
|
|
127
|
-
SetAttentionTone(8, 8000),
|
|
133
|
+
SetAttentionTone({ MS: 8, SampleRate: 8000 }),
|
|
128
134
|
new Int16Array(Math.floor(0.5 * 8000)),
|
|
129
135
|
vRadio
|
|
130
136
|
)
|
|
131
137
|
for (let i = 0; i < 3; i++) {
|
|
132
|
-
buffFull.push(SetSameHeader(
|
|
138
|
+
buffFull.push(SetSameHeader({ VTEC: Header, Repeats: 1, SampleRate: 8000, PreMarkSec: 0.5, GapSec: 0.1 }))
|
|
133
139
|
buffFull.push(new Int16Array(Math.floor(0.5 * 8000)));
|
|
134
140
|
}
|
|
135
141
|
const aSamples = GetMergedPCM16(buffFull);
|
|
136
|
-
const aFinal = SetNoise(aSamples, 0.002);
|
|
137
|
-
const aBuffer = GetPCM16(Array.from(aFinal).map(v => ({ value: v })), 8000);
|
|
142
|
+
const aFinal = SetNoise({ Int16: aSamples, NoiseLevel: 0.002 });
|
|
143
|
+
const aBuffer = GetPCM16({ Samples: Array.from(aFinal).map(v => ({ value: v })), SampleRate: 8000 });
|
|
138
144
|
writeFileSync(outTTS, aBuffer)
|
|
139
145
|
try{ unlinkSync(tmpTTS) } catch {}
|
|
140
|
-
SetDebug({
|
|
146
|
+
SetDebug({ Title: `GenerateEASMessage`, Message: `${Math.round(performance.now() - tick)}ms` })
|
|
141
147
|
return outTTS;
|
|
142
148
|
}
|
|
143
149
|
|
|
@@ -18,12 +18,17 @@
|
|
|
18
18
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
interface GetPCM16Options {
|
|
22
|
+
Samples: Record<string, number>[]
|
|
23
|
+
SampleRate: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const GetPCM16 = ({ Samples, SampleRate }: GetPCM16Options): Buffer => {
|
|
22
27
|
let o = 0;
|
|
23
28
|
const bytesPerSample = 2;
|
|
24
29
|
const blockAlign = 1 * bytesPerSample;
|
|
25
|
-
const byteRate =
|
|
26
|
-
const subchunk2Size =
|
|
30
|
+
const byteRate = SampleRate * blockAlign;
|
|
31
|
+
const subchunk2Size = Samples.length * bytesPerSample;
|
|
27
32
|
const chunkSize = 36 + subchunk2Size;
|
|
28
33
|
const buffer = Buffer.alloc(44 + subchunk2Size);
|
|
29
34
|
|
|
@@ -35,7 +40,7 @@ export const GetPCM16 = (samples: Record<string, number>[], sampleRate: number):
|
|
|
35
40
|
buffer.writeUInt32LE(16, o); o += 4;
|
|
36
41
|
buffer.writeUInt16LE(1, o); o += 2;
|
|
37
42
|
buffer.writeUInt16LE(1, o); o += 2;
|
|
38
|
-
buffer.writeUInt32LE(
|
|
43
|
+
buffer.writeUInt32LE(SampleRate, o); o += 4;
|
|
39
44
|
buffer.writeUInt32LE(byteRate, o); o += 4;
|
|
40
45
|
buffer.writeUInt16LE(blockAlign, o); o += 2;
|
|
41
46
|
buffer.writeUInt16LE(16, o); o += 2;
|
|
@@ -43,8 +48,8 @@ export const GetPCM16 = (samples: Record<string, number>[], sampleRate: number):
|
|
|
43
48
|
buffer.write("data", o); o += 4;
|
|
44
49
|
buffer.writeUInt32LE(subchunk2Size, o); o += 4;
|
|
45
50
|
|
|
46
|
-
for (let i = 0; i <
|
|
47
|
-
buffer.writeInt16LE(
|
|
51
|
+
for (let i = 0; i < Samples.length; i++, o += 2) {
|
|
52
|
+
buffer.writeInt16LE(Samples[i].value, o);
|
|
48
53
|
}
|
|
49
54
|
return buffer;
|
|
50
55
|
}
|