@sayue_ltr/fleq 1.50.0 → 1.51.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/CHANGELOG.md +103 -0
- package/README.md +47 -5
- package/dist/config.js +35 -2
- package/dist/dmdata/rest-client.js +58 -3
- package/dist/dmdata/telegram-parser.js +37 -59
- package/dist/dmdata/ws-client.js +49 -18
- package/dist/engine/cli/cli-run.js +71 -1
- package/dist/engine/cli/cli.js +12 -0
- package/dist/engine/filter/compile-filter.js +21 -0
- package/dist/engine/filter/compiler.js +188 -0
- package/dist/engine/filter/errors.js +41 -0
- package/dist/engine/filter/field-registry.js +78 -0
- package/dist/engine/filter/index.js +15 -0
- package/dist/engine/filter/parser.js +137 -0
- package/dist/engine/filter/rank-maps.js +34 -0
- package/dist/engine/filter/tokenizer.js +121 -0
- package/dist/engine/filter/type-checker.js +104 -0
- package/dist/engine/filter/types.js +2 -0
- package/dist/engine/filter-template/pipeline-controller.js +73 -0
- package/dist/engine/filter-template/pipeline.js +16 -0
- package/dist/engine/messages/display-callbacks.js +7 -0
- package/dist/engine/messages/message-router.js +114 -182
- package/dist/engine/messages/summary-tracker.js +106 -0
- package/dist/engine/messages/telegram-stats.js +103 -0
- package/dist/engine/messages/volcano-route-handler.js +122 -0
- package/dist/engine/monitor/monitor.js +51 -3
- package/dist/engine/monitor/shutdown.js +1 -0
- package/dist/engine/notification/notifier.js +16 -1
- package/dist/engine/notification/sound-player.js +193 -28
- package/dist/engine/presentation/diff-store.js +158 -0
- package/dist/engine/presentation/diff-types.js +2 -0
- package/dist/engine/presentation/events/from-earthquake.js +53 -0
- package/dist/engine/presentation/events/from-eew.js +72 -0
- package/dist/engine/presentation/events/from-lg-observation.js +58 -0
- package/dist/engine/presentation/events/from-nankai-trough.js +39 -0
- package/dist/engine/presentation/events/from-raw.js +35 -0
- package/dist/engine/presentation/events/from-seismic-text.js +37 -0
- package/dist/engine/presentation/events/from-tsunami.js +51 -0
- package/dist/engine/presentation/events/from-volcano.js +88 -0
- package/dist/engine/presentation/events/to-presentation-event.js +32 -0
- package/dist/engine/presentation/level-helpers.js +118 -0
- package/dist/engine/presentation/processors/process-earthquake.js +36 -0
- package/dist/engine/presentation/processors/process-eew.js +90 -0
- package/dist/engine/presentation/processors/process-lg-observation.js +30 -0
- package/dist/engine/presentation/processors/process-message.js +53 -0
- package/dist/engine/presentation/processors/process-nankai-trough.js +30 -0
- package/dist/engine/presentation/processors/process-raw.js +22 -0
- package/dist/engine/presentation/processors/process-seismic-text.js +30 -0
- package/dist/engine/presentation/processors/process-tsunami.js +42 -0
- package/dist/engine/presentation/processors/process-volcano.js +41 -0
- package/dist/engine/presentation/types.js +2 -0
- package/dist/engine/startup/config-resolver.js +2 -0
- package/dist/engine/template/compile-template.js +18 -0
- package/dist/engine/template/compiler.js +102 -0
- package/dist/engine/template/field-accessor.js +25 -0
- package/dist/engine/template/filters.js +94 -0
- package/dist/engine/template/index.js +5 -0
- package/dist/engine/template/parser.js +190 -0
- package/dist/engine/template/tokenizer.js +96 -0
- package/dist/engine/template/types.js +2 -0
- package/dist/types.js +2 -1
- package/dist/ui/display-adapter.js +60 -0
- package/dist/ui/earthquake-formatter.js +17 -5
- package/dist/ui/eew-formatter.js +25 -10
- package/dist/ui/formatter.js +67 -32
- package/dist/ui/minimap/grid-layout.js +91 -0
- package/dist/ui/minimap/index.js +16 -0
- package/dist/ui/minimap/minimap-renderer.js +277 -0
- package/dist/ui/minimap/pref-mapping.js +82 -0
- package/dist/ui/minimap/types.js +2 -0
- package/dist/ui/night-overlay.js +56 -0
- package/dist/ui/repl-handlers/command-definitions.js +320 -0
- package/dist/ui/repl-handlers/index.js +11 -0
- package/dist/ui/repl-handlers/info-handlers.js +577 -0
- package/dist/ui/repl-handlers/operation-handlers.js +233 -0
- package/dist/ui/repl-handlers/settings-handlers.js +923 -0
- package/dist/ui/repl-handlers/types.js +10 -0
- package/dist/ui/repl.js +81 -1752
- package/dist/ui/statistics-formatter.js +208 -0
- package/dist/ui/status-line.js +69 -0
- package/dist/ui/summary/index.js +5 -0
- package/dist/ui/summary/summary-line.js +18 -0
- package/dist/ui/summary/summary-model.js +31 -0
- package/dist/ui/summary/token-builders.js +317 -0
- package/dist/ui/summary/types.js +2 -0
- package/dist/ui/summary/width-fit.js +41 -0
- package/dist/ui/summary-interval-formatter.js +72 -0
- package/dist/ui/theme.js +34 -5
- package/dist/ui/tip-shuffler.js +81 -0
- package/dist/ui/volcano-formatter.js +15 -13
- package/dist/ui/waiting-tips.js +289 -249
- package/package.json +1 -1
|
@@ -1,53 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
35
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
6
|
exports.createMessageHandler = createMessageHandler;
|
|
37
|
-
const
|
|
38
|
-
const volcano_parser_1 = require("../../dmdata/volcano-parser");
|
|
39
|
-
const formatter_1 = require("../../ui/formatter");
|
|
40
|
-
const eew_formatter_1 = require("../../ui/eew-formatter");
|
|
41
|
-
const earthquake_formatter_1 = require("../../ui/earthquake-formatter");
|
|
42
|
-
const volcano_formatter_1 = require("../../ui/volcano-formatter");
|
|
43
|
-
const volcano_vfvo53_aggregator_1 = require("./volcano-vfvo53-aggregator");
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
44
8
|
const eew_tracker_1 = require("../eew/eew-tracker");
|
|
45
9
|
const eew_logger_1 = require("../eew/eew-logger");
|
|
46
10
|
const notifier_1 = require("../notification/notifier");
|
|
47
11
|
const tsunami_state_1 = require("./tsunami-state");
|
|
48
12
|
const volcano_state_1 = require("./volcano-state");
|
|
49
|
-
const
|
|
50
|
-
const
|
|
13
|
+
const telegram_stats_1 = require("./telegram-stats");
|
|
14
|
+
const summary_tracker_1 = require("./summary-tracker");
|
|
15
|
+
const process_message_1 = require("../presentation/processors/process-message");
|
|
16
|
+
const to_presentation_event_1 = require("../presentation/events/to-presentation-event");
|
|
17
|
+
const pipeline_1 = require("../filter-template/pipeline");
|
|
18
|
+
const diff_store_1 = require("../presentation/diff-store");
|
|
19
|
+
const volcano_route_handler_1 = require("./volcano-route-handler");
|
|
51
20
|
/**
|
|
52
21
|
* classification と head.type から処理ルートを判定する。
|
|
53
22
|
* ルーティング優先順位:
|
|
@@ -86,169 +55,130 @@ function classifyMessage(classification, headType) {
|
|
|
86
55
|
}
|
|
87
56
|
return "raw";
|
|
88
57
|
}
|
|
89
|
-
// ──
|
|
90
|
-
/**
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
(0, eew_formatter_1.displayEewInfo)(eewInfo, {
|
|
114
|
-
activeCount: result.activeCount,
|
|
115
|
-
diff: result.diff,
|
|
116
|
-
colorIndex: result.colorIndex,
|
|
117
|
-
});
|
|
118
|
-
notifier.notifyEew(eewInfo, result);
|
|
119
|
-
}
|
|
120
|
-
/** テキスト系 (VXSE56/VXSE60/VZSE40) パス */
|
|
121
|
-
function handleSeismicText(msg, notifier) {
|
|
122
|
-
const textInfo = (0, telegram_parser_1.parseSeismicTextTelegram)(msg);
|
|
123
|
-
if (textInfo) {
|
|
124
|
-
(0, earthquake_formatter_1.displaySeismicTextInfo)(textInfo);
|
|
125
|
-
notifier.notifySeismicText(textInfo);
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
(0, formatter_1.displayRawHeader)(msg);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/** 長周期地震動観測情報 (VXSE62) パス */
|
|
132
|
-
function handleLgObservation(msg, notifier) {
|
|
133
|
-
const lgInfo = (0, telegram_parser_1.parseLgObservationTelegram)(msg);
|
|
134
|
-
if (lgInfo) {
|
|
135
|
-
(0, earthquake_formatter_1.displayLgObservationInfo)(lgInfo);
|
|
136
|
-
notifier.notifyLgObservation(lgInfo);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
(0, formatter_1.displayRawHeader)(msg);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/** 地震情報 (VXSE51/52/53/61 等) パス */
|
|
143
|
-
function handleEarthquake(msg, notifier) {
|
|
144
|
-
const eqInfo = (0, telegram_parser_1.parseEarthquakeTelegram)(msg);
|
|
145
|
-
if (eqInfo) {
|
|
146
|
-
(0, earthquake_formatter_1.displayEarthquakeInfo)(eqInfo);
|
|
147
|
-
notifier.notifyEarthquake(eqInfo);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
(0, formatter_1.displayRawHeader)(msg);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
/** 津波情報 (VTSE41/51/52) パス */
|
|
154
|
-
function handleTsunami(msg, notifier, tsunamiState) {
|
|
155
|
-
const tsunamiInfo = (0, telegram_parser_1.parseTsunamiTelegram)(msg);
|
|
156
|
-
if (tsunamiInfo) {
|
|
157
|
-
// VTSE41 (津波警報・注意報) の場合のみ状態を更新
|
|
158
|
-
if (msg.head.type === "VTSE41") {
|
|
159
|
-
tsunamiState.update(tsunamiInfo);
|
|
160
|
-
}
|
|
161
|
-
(0, earthquake_formatter_1.displayTsunamiInfo)(tsunamiInfo);
|
|
162
|
-
notifier.notifyTsunami(tsunamiInfo);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
(0, formatter_1.displayRawHeader)(msg);
|
|
58
|
+
// ── dispatch helpers ──
|
|
59
|
+
/** 通知のみ実行 (filter 非適用) */
|
|
60
|
+
function dispatchNotify(outcome, notifier) {
|
|
61
|
+
switch (outcome.domain) {
|
|
62
|
+
case "eew":
|
|
63
|
+
notifier.notifyEew(outcome.parsed, outcome.eewResult);
|
|
64
|
+
break;
|
|
65
|
+
case "earthquake":
|
|
66
|
+
notifier.notifyEarthquake(outcome.parsed);
|
|
67
|
+
break;
|
|
68
|
+
case "seismicText":
|
|
69
|
+
notifier.notifySeismicText(outcome.parsed);
|
|
70
|
+
break;
|
|
71
|
+
case "lgObservation":
|
|
72
|
+
notifier.notifyLgObservation(outcome.parsed);
|
|
73
|
+
break;
|
|
74
|
+
case "tsunami":
|
|
75
|
+
notifier.notifyTsunami(outcome.parsed);
|
|
76
|
+
break;
|
|
77
|
+
case "nankaiTrough":
|
|
78
|
+
notifier.notifyNankaiTrough(outcome.parsed);
|
|
79
|
+
break;
|
|
80
|
+
// raw: 通知なし
|
|
81
|
+
// volcano: VolcanoRouteHandler が通知を担当
|
|
166
82
|
}
|
|
167
83
|
}
|
|
168
|
-
/**
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
function handleVolcano(msg, vfvo53Aggregator) {
|
|
181
|
-
const volcanoInfo = (0, volcano_parser_1.parseVolcanoTelegram)(msg);
|
|
182
|
-
if (volcanoInfo) {
|
|
183
|
-
vfvo53Aggregator.handle(volcanoInfo);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
(0, formatter_1.displayRawHeader)(msg);
|
|
84
|
+
/** outcome.stats に基づいて統計を記録する */
|
|
85
|
+
function recordStats(outcome, stats) {
|
|
86
|
+
if (outcome.stats.shouldRecord) {
|
|
87
|
+
stats.record({
|
|
88
|
+
headType: outcome.headType,
|
|
89
|
+
category: outcome.statsCategory,
|
|
90
|
+
eventId: outcome.stats.eventId,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (outcome.stats.maxIntUpdate) {
|
|
94
|
+
const u = outcome.stats.maxIntUpdate;
|
|
95
|
+
stats.updateMaxInt(u.eventId, u.maxInt, u.headType);
|
|
187
96
|
}
|
|
188
97
|
}
|
|
189
98
|
/** 受信データのハンドリング */
|
|
190
|
-
function createMessageHandler() {
|
|
99
|
+
function createMessageHandler(options) {
|
|
100
|
+
const pipeline = options?.pipeline ?? { filter: null, template: null, focus: null };
|
|
101
|
+
const display = options?.display;
|
|
191
102
|
const eewLogger = new eew_logger_1.EewEventLogger();
|
|
192
103
|
const notifier = new notifier_1.Notifier();
|
|
193
104
|
const tsunamiState = new tsunami_state_1.TsunamiStateHolder();
|
|
194
105
|
const volcanoState = new volcano_state_1.VolcanoStateHolder();
|
|
106
|
+
const stats = new telegram_stats_1.TelegramStats();
|
|
107
|
+
const summaryTracker = new summary_tracker_1.SummaryWindowTracker();
|
|
108
|
+
const diffStore = new diff_store_1.PresentationDiffStore();
|
|
195
109
|
const eewTracker = new eew_tracker_1.EewTracker({
|
|
196
110
|
onCleanup: (eventId) => {
|
|
197
111
|
eewLogger.closeEvent(eventId, "タイムアウト");
|
|
198
112
|
},
|
|
199
113
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
114
|
+
const processDeps = {
|
|
115
|
+
eewTracker,
|
|
116
|
+
eewLogger,
|
|
117
|
+
tsunamiState,
|
|
118
|
+
volcanoState,
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* 共通の表示パイプライン処理。
|
|
122
|
+
* filter/diffStore/summaryTracker/focus/template/compact の6ステップを一元的に実行する。
|
|
123
|
+
* @returns true なら表示済み。false ならフィルタで非表示。
|
|
124
|
+
*/
|
|
125
|
+
function runDisplayPipeline(outcome, displayFn) {
|
|
126
|
+
const rawEvent = (0, to_presentation_event_1.toPresentationEvent)(outcome);
|
|
127
|
+
const event = diffStore.apply(rawEvent);
|
|
128
|
+
const displayed = (0, pipeline_1.shouldDisplay)(event, pipeline);
|
|
129
|
+
summaryTracker.record(event, displayed);
|
|
130
|
+
if (!displayed) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const isFocused = pipeline.focus == null || pipeline.focus(event);
|
|
134
|
+
if (!isFocused && display) {
|
|
135
|
+
console.log(chalk_1.default.dim(display.renderSummaryLine(event)));
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
const templateOutput = (0, pipeline_1.renderTemplate)(event, pipeline);
|
|
139
|
+
if (templateOutput != null) {
|
|
140
|
+
console.log(templateOutput);
|
|
141
|
+
return true;
|
|
209
142
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const presentation = (0, volcano_presentation_1.resolveVolcanoBatchPresentation)(batch);
|
|
214
|
-
(0, volcano_formatter_1.displayVolcanoAshfallBatch)(batch, presentation);
|
|
215
|
-
if (opts.notify) {
|
|
216
|
-
notifier.notifyVolcanoBatch(batch, presentation);
|
|
143
|
+
if (display && display.getDisplayMode() === "compact") {
|
|
144
|
+
console.log(display.renderSummaryLine(event));
|
|
145
|
+
return true;
|
|
217
146
|
}
|
|
147
|
+
displayFn();
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
// 火山ルートハンドラ
|
|
151
|
+
const volcanoHandler = new volcano_route_handler_1.VolcanoRouteHandler({
|
|
152
|
+
volcanoState,
|
|
153
|
+
notifier,
|
|
154
|
+
runDisplayPipeline,
|
|
155
|
+
display,
|
|
218
156
|
});
|
|
219
157
|
const handler = (msg) => {
|
|
220
158
|
// XML電文でない場合はヘッダ情報のみ表示
|
|
221
159
|
if (msg.format !== "xml" || !msg.head.xml) {
|
|
222
|
-
|
|
160
|
+
display?.displayRawHeader(msg);
|
|
223
161
|
return;
|
|
224
162
|
}
|
|
225
163
|
const route = classifyMessage(msg.classification, msg.head.type);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
handleTsunami(msg, notifier, tsunamiState);
|
|
241
|
-
break;
|
|
242
|
-
case "nankaiTrough":
|
|
243
|
-
handleNankaiTrough(msg, notifier);
|
|
244
|
-
break;
|
|
245
|
-
case "volcano":
|
|
246
|
-
handleVolcano(msg, vfvo53Aggregator);
|
|
247
|
-
break;
|
|
248
|
-
case "raw":
|
|
249
|
-
(0, formatter_1.displayRawHeader)(msg);
|
|
250
|
-
break;
|
|
164
|
+
// 火山は VolcanoRouteHandler に委譲
|
|
165
|
+
if (route === "volcano") {
|
|
166
|
+
volcanoHandler.handle(msg);
|
|
167
|
+
stats.record({
|
|
168
|
+
headType: msg.head.type,
|
|
169
|
+
category: (0, telegram_stats_1.routeToCategory)(route),
|
|
170
|
+
eventId: msg.xmlReport?.head.eventId ?? null,
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// 火山以外: processMessage → recordStats → dispatchNotify → runDisplayPipeline
|
|
175
|
+
const outcome = (0, process_message_1.processMessage)(msg, route, processDeps);
|
|
176
|
+
if (outcome == null) {
|
|
177
|
+
return;
|
|
251
178
|
}
|
|
179
|
+
recordStats(outcome, stats);
|
|
180
|
+
dispatchNotify(outcome, notifier);
|
|
181
|
+
runDisplayPipeline(outcome, () => display?.displayOutcome(outcome));
|
|
252
182
|
};
|
|
253
183
|
return {
|
|
254
184
|
handler,
|
|
@@ -256,6 +186,8 @@ function createMessageHandler() {
|
|
|
256
186
|
notifier,
|
|
257
187
|
tsunamiState,
|
|
258
188
|
volcanoState,
|
|
259
|
-
|
|
189
|
+
stats,
|
|
190
|
+
summaryTracker,
|
|
191
|
+
flushAndDisposeVolcanoBuffer: () => volcanoHandler.flushAndDispose(),
|
|
260
192
|
};
|
|
261
193
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SummaryWindowTracker = exports.WINDOW_MINUTES = void 0;
|
|
4
|
+
const intensity_1 = require("../../utils/intensity");
|
|
5
|
+
/** リングバッファの窓幅(分) */
|
|
6
|
+
exports.WINDOW_MINUTES = 30;
|
|
7
|
+
const MINUTE_MS = 60_000;
|
|
8
|
+
/** 直近30分のリングバッファで受信統計を追跡する */
|
|
9
|
+
class SummaryWindowTracker {
|
|
10
|
+
buckets = [];
|
|
11
|
+
/** イベントを記録する */
|
|
12
|
+
record(event, matched, now) {
|
|
13
|
+
const ts = now ?? Date.now();
|
|
14
|
+
this.pruneOld(ts);
|
|
15
|
+
const bucket = this.getOrCreateBucket(ts);
|
|
16
|
+
bucket.received++;
|
|
17
|
+
if (matched) {
|
|
18
|
+
bucket.matched++;
|
|
19
|
+
}
|
|
20
|
+
bucket.byDomain[event.domain] = (bucket.byDomain[event.domain] ?? 0) + 1;
|
|
21
|
+
// maxInt 追跡 (バケット単位で記録)
|
|
22
|
+
if (event.maxInt != null) {
|
|
23
|
+
const rank = (0, intensity_1.intensityToRank)(event.maxInt);
|
|
24
|
+
if (rank > bucket.maxIntRank) {
|
|
25
|
+
bucket.maxIntRank = rank;
|
|
26
|
+
bucket.maxIntStr = event.maxInt;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** 現在のスナップショットを取得する */
|
|
31
|
+
getSnapshot(now) {
|
|
32
|
+
const ts = now ?? Date.now();
|
|
33
|
+
this.pruneOld(ts);
|
|
34
|
+
let totalReceived = 0;
|
|
35
|
+
let totalMatched = 0;
|
|
36
|
+
const byDomain = {};
|
|
37
|
+
for (const bucket of this.buckets) {
|
|
38
|
+
totalReceived += bucket.received;
|
|
39
|
+
totalMatched += bucket.matched;
|
|
40
|
+
for (const [domain, count] of Object.entries(bucket.byDomain)) {
|
|
41
|
+
byDomain[domain] = (byDomain[domain] ?? 0) + (count ?? 0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// maxInt を残存バケットから再計算 (30分窓で減衰)
|
|
45
|
+
let maxIntRank = 0;
|
|
46
|
+
let maxIntStr = null;
|
|
47
|
+
for (const bucket of this.buckets) {
|
|
48
|
+
if (bucket.maxIntRank > maxIntRank) {
|
|
49
|
+
maxIntRank = bucket.maxIntRank;
|
|
50
|
+
maxIntStr = bucket.maxIntStr;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// sparklineData: 30スロット (古い順 → 新しい順)
|
|
54
|
+
const sparklineData = this.buildSparklineData(ts);
|
|
55
|
+
return {
|
|
56
|
+
totalReceived,
|
|
57
|
+
totalMatched,
|
|
58
|
+
byDomain,
|
|
59
|
+
maxIntSeen: maxIntStr,
|
|
60
|
+
sparklineData,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** 統計をクリアする */
|
|
64
|
+
clear() {
|
|
65
|
+
this.buckets = [];
|
|
66
|
+
}
|
|
67
|
+
/** 30分超の古いバケットを除去する */
|
|
68
|
+
pruneOld(now) {
|
|
69
|
+
const cutoff = this.minuteStart(now) - (exports.WINDOW_MINUTES - 1) * MINUTE_MS;
|
|
70
|
+
this.buckets = this.buckets.filter((b) => b.minuteStartMs >= cutoff);
|
|
71
|
+
}
|
|
72
|
+
/** 指定時刻のバケットを取得、なければ作成 */
|
|
73
|
+
getOrCreateBucket(now) {
|
|
74
|
+
const ms = this.minuteStart(now);
|
|
75
|
+
const existing = this.buckets.find((b) => b.minuteStartMs === ms);
|
|
76
|
+
if (existing)
|
|
77
|
+
return existing;
|
|
78
|
+
const bucket = {
|
|
79
|
+
minuteStartMs: ms,
|
|
80
|
+
received: 0,
|
|
81
|
+
matched: 0,
|
|
82
|
+
byDomain: {},
|
|
83
|
+
maxIntRank: 0,
|
|
84
|
+
maxIntStr: null,
|
|
85
|
+
};
|
|
86
|
+
this.buckets.push(bucket);
|
|
87
|
+
return bucket;
|
|
88
|
+
}
|
|
89
|
+
/** 30スロットの sparkline データを生成する (古い順) */
|
|
90
|
+
buildSparklineData(now) {
|
|
91
|
+
const currentMinuteStart = this.minuteStart(now);
|
|
92
|
+
const data = new Array(exports.WINDOW_MINUTES).fill(0);
|
|
93
|
+
for (const bucket of this.buckets) {
|
|
94
|
+
const slotIndex = Math.round((bucket.minuteStartMs - (currentMinuteStart - (exports.WINDOW_MINUTES - 1) * MINUTE_MS)) / MINUTE_MS);
|
|
95
|
+
if (slotIndex >= 0 && slotIndex < exports.WINDOW_MINUTES) {
|
|
96
|
+
data[slotIndex] = bucket.received;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return data;
|
|
100
|
+
}
|
|
101
|
+
/** タイムスタンプを分の開始に丸める */
|
|
102
|
+
minuteStart(ts) {
|
|
103
|
+
return Math.floor(ts / MINUTE_MS) * MINUTE_MS;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.SummaryWindowTracker = SummaryWindowTracker;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TelegramStats = void 0;
|
|
4
|
+
exports.routeToCategory = routeToCategory;
|
|
5
|
+
/** Route → StatsCategory 変換 */
|
|
6
|
+
function routeToCategory(route) {
|
|
7
|
+
switch (route) {
|
|
8
|
+
case "eew": return "eew";
|
|
9
|
+
case "earthquake":
|
|
10
|
+
case "seismicText":
|
|
11
|
+
case "lgObservation": return "earthquake";
|
|
12
|
+
case "tsunami": return "tsunami";
|
|
13
|
+
case "volcano": return "volcano";
|
|
14
|
+
case "nankaiTrough": return "nankaiTrough";
|
|
15
|
+
default: return "other";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** 最大震度 headType → priority マッピング */
|
|
19
|
+
const MAX_INT_PRIORITY = {
|
|
20
|
+
VXSE53: 3,
|
|
21
|
+
VXSE61: 2,
|
|
22
|
+
VXSE51: 1,
|
|
23
|
+
};
|
|
24
|
+
/** Set/Map のサイズ上限 */
|
|
25
|
+
const MAX_EVENT_ENTRIES = 1000;
|
|
26
|
+
/** 上限超過時に削除するエントリ数 (バッチ削除で頻繁な削除を回避) */
|
|
27
|
+
const EVICT_BATCH_SIZE = 100;
|
|
28
|
+
/** Set のサイズ上限を適用する。超過時は挿入順で古い方から削除する。 */
|
|
29
|
+
function evictOldestFromSet(set, maxSize) {
|
|
30
|
+
if (set.size <= maxSize)
|
|
31
|
+
return;
|
|
32
|
+
let toRemove = set.size - maxSize + EVICT_BATCH_SIZE;
|
|
33
|
+
for (const item of set) {
|
|
34
|
+
if (toRemove <= 0)
|
|
35
|
+
break;
|
|
36
|
+
set.delete(item);
|
|
37
|
+
toRemove--;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Map のサイズ上限を適用する。超過時は挿入順で古い方から削除する。 */
|
|
41
|
+
function evictOldestFromMap(map, maxSize) {
|
|
42
|
+
if (map.size <= maxSize)
|
|
43
|
+
return;
|
|
44
|
+
let toRemove = map.size - maxSize + EVICT_BATCH_SIZE;
|
|
45
|
+
for (const key of map.keys()) {
|
|
46
|
+
if (toRemove <= 0)
|
|
47
|
+
break;
|
|
48
|
+
map.delete(key);
|
|
49
|
+
toRemove--;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** セッション中の電文受信統計を管理する */
|
|
53
|
+
class TelegramStats {
|
|
54
|
+
startTime;
|
|
55
|
+
countByType = new Map();
|
|
56
|
+
categoryByType = new Map();
|
|
57
|
+
eewEventIds = new Set();
|
|
58
|
+
earthquakeMaxIntByEvent = new Map();
|
|
59
|
+
constructor(startTime) {
|
|
60
|
+
this.startTime = startTime ?? new Date();
|
|
61
|
+
}
|
|
62
|
+
/** headType カウント加算。EEW の場合は eventId を Set に追加 */
|
|
63
|
+
record(rec) {
|
|
64
|
+
this.countByType.set(rec.headType, (this.countByType.get(rec.headType) ?? 0) + 1);
|
|
65
|
+
// headType → category の対応は固定なので初回のみ登録する
|
|
66
|
+
if (!this.categoryByType.has(rec.headType)) {
|
|
67
|
+
this.categoryByType.set(rec.headType, rec.category);
|
|
68
|
+
}
|
|
69
|
+
if (rec.category === "eew" && rec.eventId != null) {
|
|
70
|
+
this.eewEventIds.add(rec.eventId);
|
|
71
|
+
evictOldestFromSet(this.eewEventIds, MAX_EVENT_ENTRIES);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 地震イベントの代表最大震度を更新する。
|
|
76
|
+
* 認識する headType: VXSE53 (priority 3), VXSE61 (priority 2), VXSE51 (priority 1)。
|
|
77
|
+
* 未知の headType は priority 0 として扱う。
|
|
78
|
+
*/
|
|
79
|
+
updateMaxInt(eventId, maxInt, headType) {
|
|
80
|
+
const priority = MAX_INT_PRIORITY[headType] ?? 0;
|
|
81
|
+
const existing = this.earthquakeMaxIntByEvent.get(eventId);
|
|
82
|
+
if (existing == null || priority >= existing.priority) {
|
|
83
|
+
this.earthquakeMaxIntByEvent.set(eventId, { maxInt, priority });
|
|
84
|
+
evictOldestFromMap(this.earthquakeMaxIntByEvent, MAX_EVENT_ENTRIES);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** 表示用の読み取り専用スナップショットを返す */
|
|
88
|
+
getSnapshot() {
|
|
89
|
+
let totalCount = 0;
|
|
90
|
+
for (const count of this.countByType.values()) {
|
|
91
|
+
totalCount += count;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
startTime: new Date(this.startTime),
|
|
95
|
+
countByType: new Map(this.countByType),
|
|
96
|
+
categoryByType: new Map(this.categoryByType),
|
|
97
|
+
eewEventCount: this.eewEventIds.size,
|
|
98
|
+
earthquakeMaxIntByEvent: new Map([...this.earthquakeMaxIntByEvent.entries()].map(([k, v]) => [k, v.maxInt])),
|
|
99
|
+
totalCount,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.TelegramStats = TelegramStats;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 火山電文のルーティング処理を一元管理するハンドラ。
|
|
4
|
+
*
|
|
5
|
+
* 火山は VFVO53 アグリゲータによるバッチ集約があるため、
|
|
6
|
+
* 他ドメインの processMessage() → outcome → display の線形フローとは異なる。
|
|
7
|
+
* このハンドラが火山の パース → キャッシュ → 集約 → 通知 → 表示 を担当する。
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.VolcanoRouteHandler = void 0;
|
|
11
|
+
const volcano_parser_1 = require("../../dmdata/volcano-parser");
|
|
12
|
+
const volcano_vfvo53_aggregator_1 = require("./volcano-vfvo53-aggregator");
|
|
13
|
+
const volcano_presentation_1 = require("../notification/volcano-presentation");
|
|
14
|
+
const process_volcano_1 = require("../presentation/processors/process-volcano");
|
|
15
|
+
// ── 定数 ──
|
|
16
|
+
const VOLCANO_CACHE_TTL_MS = 10 * 60 * 1000; // 10分
|
|
17
|
+
// ── 本体 ──
|
|
18
|
+
class VolcanoRouteHandler {
|
|
19
|
+
volcanoState;
|
|
20
|
+
notifier;
|
|
21
|
+
runDisplayPipeline;
|
|
22
|
+
display;
|
|
23
|
+
aggregator;
|
|
24
|
+
msgCache = new Map();
|
|
25
|
+
constructor(deps) {
|
|
26
|
+
this.volcanoState = deps.volcanoState;
|
|
27
|
+
this.notifier = deps.notifier;
|
|
28
|
+
this.runDisplayPipeline = deps.runDisplayPipeline;
|
|
29
|
+
this.display = deps.display;
|
|
30
|
+
this.aggregator = new volcano_vfvo53_aggregator_1.VolcanoVfvo53Aggregator((info, opts) => this.emitSingle(info, opts), (batch, opts) => this.emitBatch(batch, opts));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 火山電文を処理する。
|
|
34
|
+
* @returns パース成功なら ParsedVolcanoInfo (統計記録用)、失敗なら null。
|
|
35
|
+
*/
|
|
36
|
+
handle(msg) {
|
|
37
|
+
this.pruneMsgCache();
|
|
38
|
+
const volcanoInfo = (0, volcano_parser_1.parseVolcanoTelegram)(msg);
|
|
39
|
+
if (!volcanoInfo)
|
|
40
|
+
return null;
|
|
41
|
+
this.msgCache.set(volcanoInfo.volcanoCode, { msg, cachedAt: Date.now() });
|
|
42
|
+
this.aggregator.handle(volcanoInfo);
|
|
43
|
+
return volcanoInfo;
|
|
44
|
+
}
|
|
45
|
+
/** 保留中の火山バッファを flush してリソースを破棄する */
|
|
46
|
+
flushAndDispose() {
|
|
47
|
+
this.aggregator.flushAndDispose();
|
|
48
|
+
}
|
|
49
|
+
// ── private: emit callbacks ──
|
|
50
|
+
emitSingle(info, opts) {
|
|
51
|
+
const cacheEntry = this.msgCache.get(info.volcanoCode);
|
|
52
|
+
const cachedMsg = cacheEntry?.msg;
|
|
53
|
+
const outcome = cachedMsg
|
|
54
|
+
? (0, process_volcano_1.buildVolcanoOutcome)(cachedMsg, info, this.volcanoState)
|
|
55
|
+
: null;
|
|
56
|
+
const presentation = (0, volcano_presentation_1.resolveVolcanoPresentation)(info, this.volcanoState);
|
|
57
|
+
this.volcanoState.update(info);
|
|
58
|
+
// 通知は filter 非適用
|
|
59
|
+
if (opts?.notify !== false) {
|
|
60
|
+
this.notifier.notifyVolcano(info, presentation);
|
|
61
|
+
}
|
|
62
|
+
// PresentationEvent パイプライン
|
|
63
|
+
if (outcome) {
|
|
64
|
+
this.runDisplayPipeline(outcome, () => this.display?.displayVolcano(info, presentation));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// msg キャッシュがない場合はフォールバック表示
|
|
68
|
+
this.display?.displayVolcano(info, presentation);
|
|
69
|
+
}
|
|
70
|
+
this.msgCache.delete(info.volcanoCode);
|
|
71
|
+
}
|
|
72
|
+
emitBatch(batch, opts) {
|
|
73
|
+
const presentation = (0, volcano_presentation_1.resolveVolcanoBatchPresentation)(batch);
|
|
74
|
+
if (opts.notify) {
|
|
75
|
+
this.notifier.notifyVolcanoBatch(batch, presentation);
|
|
76
|
+
}
|
|
77
|
+
const firstItem = batch.items[0];
|
|
78
|
+
const cacheEntry = firstItem ? this.msgCache.get(firstItem.volcanoCode) : undefined;
|
|
79
|
+
const cachedMsg = cacheEntry?.msg;
|
|
80
|
+
if (cachedMsg) {
|
|
81
|
+
const batchOutcome = {
|
|
82
|
+
domain: "volcano",
|
|
83
|
+
msg: cachedMsg,
|
|
84
|
+
headType: cachedMsg.head.type,
|
|
85
|
+
statsCategory: "volcano",
|
|
86
|
+
parsed: batch.items,
|
|
87
|
+
isBatch: true,
|
|
88
|
+
volcanoPresentation: presentation,
|
|
89
|
+
batchReportDateTime: batch.reportDateTime,
|
|
90
|
+
batchIsTest: batch.isTest,
|
|
91
|
+
stats: {
|
|
92
|
+
shouldRecord: false,
|
|
93
|
+
},
|
|
94
|
+
presentation: {
|
|
95
|
+
frameLevel: presentation.frameLevel,
|
|
96
|
+
soundLevel: presentation.soundLevel,
|
|
97
|
+
notifyCategory: "volcano",
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
this.runDisplayPipeline(batchOutcome, () => this.display?.displayVolcanoBatch(batch, presentation));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.display?.displayVolcanoBatch(batch, presentation);
|
|
104
|
+
}
|
|
105
|
+
this.cleanupBatchCache(batch);
|
|
106
|
+
}
|
|
107
|
+
// ── private: cache management ──
|
|
108
|
+
pruneMsgCache() {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
for (const [key, entry] of this.msgCache) {
|
|
111
|
+
if (now - entry.cachedAt > VOLCANO_CACHE_TTL_MS) {
|
|
112
|
+
this.msgCache.delete(key);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
cleanupBatchCache(batch) {
|
|
117
|
+
for (const item of batch.items) {
|
|
118
|
+
this.msgCache.delete(item.volcanoCode);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
exports.VolcanoRouteHandler = VolcanoRouteHandler;
|