@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +47 -5
  3. package/dist/config.js +35 -2
  4. package/dist/dmdata/rest-client.js +58 -3
  5. package/dist/dmdata/telegram-parser.js +37 -59
  6. package/dist/dmdata/ws-client.js +49 -18
  7. package/dist/engine/cli/cli-run.js +71 -1
  8. package/dist/engine/cli/cli.js +12 -0
  9. package/dist/engine/filter/compile-filter.js +21 -0
  10. package/dist/engine/filter/compiler.js +188 -0
  11. package/dist/engine/filter/errors.js +41 -0
  12. package/dist/engine/filter/field-registry.js +78 -0
  13. package/dist/engine/filter/index.js +15 -0
  14. package/dist/engine/filter/parser.js +137 -0
  15. package/dist/engine/filter/rank-maps.js +34 -0
  16. package/dist/engine/filter/tokenizer.js +121 -0
  17. package/dist/engine/filter/type-checker.js +104 -0
  18. package/dist/engine/filter/types.js +2 -0
  19. package/dist/engine/filter-template/pipeline-controller.js +73 -0
  20. package/dist/engine/filter-template/pipeline.js +16 -0
  21. package/dist/engine/messages/display-callbacks.js +7 -0
  22. package/dist/engine/messages/message-router.js +114 -182
  23. package/dist/engine/messages/summary-tracker.js +106 -0
  24. package/dist/engine/messages/telegram-stats.js +103 -0
  25. package/dist/engine/messages/volcano-route-handler.js +122 -0
  26. package/dist/engine/monitor/monitor.js +51 -3
  27. package/dist/engine/monitor/shutdown.js +1 -0
  28. package/dist/engine/notification/notifier.js +16 -1
  29. package/dist/engine/notification/sound-player.js +193 -28
  30. package/dist/engine/presentation/diff-store.js +158 -0
  31. package/dist/engine/presentation/diff-types.js +2 -0
  32. package/dist/engine/presentation/events/from-earthquake.js +53 -0
  33. package/dist/engine/presentation/events/from-eew.js +72 -0
  34. package/dist/engine/presentation/events/from-lg-observation.js +58 -0
  35. package/dist/engine/presentation/events/from-nankai-trough.js +39 -0
  36. package/dist/engine/presentation/events/from-raw.js +35 -0
  37. package/dist/engine/presentation/events/from-seismic-text.js +37 -0
  38. package/dist/engine/presentation/events/from-tsunami.js +51 -0
  39. package/dist/engine/presentation/events/from-volcano.js +88 -0
  40. package/dist/engine/presentation/events/to-presentation-event.js +32 -0
  41. package/dist/engine/presentation/level-helpers.js +118 -0
  42. package/dist/engine/presentation/processors/process-earthquake.js +36 -0
  43. package/dist/engine/presentation/processors/process-eew.js +90 -0
  44. package/dist/engine/presentation/processors/process-lg-observation.js +30 -0
  45. package/dist/engine/presentation/processors/process-message.js +53 -0
  46. package/dist/engine/presentation/processors/process-nankai-trough.js +30 -0
  47. package/dist/engine/presentation/processors/process-raw.js +22 -0
  48. package/dist/engine/presentation/processors/process-seismic-text.js +30 -0
  49. package/dist/engine/presentation/processors/process-tsunami.js +42 -0
  50. package/dist/engine/presentation/processors/process-volcano.js +41 -0
  51. package/dist/engine/presentation/types.js +2 -0
  52. package/dist/engine/startup/config-resolver.js +2 -0
  53. package/dist/engine/template/compile-template.js +18 -0
  54. package/dist/engine/template/compiler.js +102 -0
  55. package/dist/engine/template/field-accessor.js +25 -0
  56. package/dist/engine/template/filters.js +94 -0
  57. package/dist/engine/template/index.js +5 -0
  58. package/dist/engine/template/parser.js +190 -0
  59. package/dist/engine/template/tokenizer.js +96 -0
  60. package/dist/engine/template/types.js +2 -0
  61. package/dist/types.js +2 -1
  62. package/dist/ui/display-adapter.js +60 -0
  63. package/dist/ui/earthquake-formatter.js +17 -5
  64. package/dist/ui/eew-formatter.js +25 -10
  65. package/dist/ui/formatter.js +67 -32
  66. package/dist/ui/minimap/grid-layout.js +91 -0
  67. package/dist/ui/minimap/index.js +16 -0
  68. package/dist/ui/minimap/minimap-renderer.js +277 -0
  69. package/dist/ui/minimap/pref-mapping.js +82 -0
  70. package/dist/ui/minimap/types.js +2 -0
  71. package/dist/ui/night-overlay.js +56 -0
  72. package/dist/ui/repl-handlers/command-definitions.js +320 -0
  73. package/dist/ui/repl-handlers/index.js +11 -0
  74. package/dist/ui/repl-handlers/info-handlers.js +577 -0
  75. package/dist/ui/repl-handlers/operation-handlers.js +233 -0
  76. package/dist/ui/repl-handlers/settings-handlers.js +923 -0
  77. package/dist/ui/repl-handlers/types.js +10 -0
  78. package/dist/ui/repl.js +81 -1752
  79. package/dist/ui/statistics-formatter.js +208 -0
  80. package/dist/ui/status-line.js +69 -0
  81. package/dist/ui/summary/index.js +5 -0
  82. package/dist/ui/summary/summary-line.js +18 -0
  83. package/dist/ui/summary/summary-model.js +31 -0
  84. package/dist/ui/summary/token-builders.js +317 -0
  85. package/dist/ui/summary/types.js +2 -0
  86. package/dist/ui/summary/width-fit.js +41 -0
  87. package/dist/ui/summary-interval-formatter.js +72 -0
  88. package/dist/ui/theme.js +34 -5
  89. package/dist/ui/tip-shuffler.js +81 -0
  90. package/dist/ui/volcano-formatter.js +15 -13
  91. package/dist/ui/waiting-tips.js +289 -249
  92. package/package.json +1 -1
@@ -1,53 +1,22 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
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 telegram_parser_1 = require("../../dmdata/telegram-parser");
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 volcano_presentation_1 = require("../notification/volcano-presentation");
50
- const log = __importStar(require("../../logger"));
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
- /** EEW パス: パース → 重複判定 → ログ → 表示 → 通知 */
91
- function handleEew(msg, eewTracker, eewLogger, notifier) {
92
- const eewInfo = (0, telegram_parser_1.parseEewTelegram)(msg);
93
- if (!eewInfo) {
94
- (0, formatter_1.displayRawHeader)(msg);
95
- return;
96
- }
97
- const result = eewTracker.update(eewInfo);
98
- if (result.isDuplicate) {
99
- log.debug(`EEW 重複報スキップ: EventID=${eewInfo.eventId} 第${eewInfo.serial}報`);
100
- return;
101
- }
102
- // ログ記録 (非重複報のみ)
103
- eewLogger.logReport(eewInfo, result);
104
- // 取消報の場合はログを閉じる
105
- if (result.isCancelled && eewInfo.eventId) {
106
- eewLogger.closeEvent(eewInfo.eventId, "取消");
107
- }
108
- // 最終報の場合はログを閉じ、トラッカーのイベントを終了扱いにする
109
- if (eewInfo.nextAdvisory && eewInfo.eventId && !result.isCancelled) {
110
- eewLogger.closeEvent(eewInfo.eventId, "最終報");
111
- eewTracker.finalizeEvent(eewInfo.eventId);
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
- /** 南海トラフ関連 (VYSE50/51/52/60) パス */
169
- function handleNankaiTrough(msg, notifier) {
170
- const nankaiInfo = (0, telegram_parser_1.parseNankaiTroughTelegram)(msg);
171
- if (nankaiInfo) {
172
- (0, earthquake_formatter_1.displayNankaiTroughInfo)(nankaiInfo);
173
- notifier.notifyNankaiTrough(nankaiInfo);
174
- }
175
- else {
176
- (0, formatter_1.displayRawHeader)(msg);
177
- }
178
- }
179
- /** 火山情報パス (aggregator 経由) */
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
- // VFVO53 バッチ集約器
201
- const vfvo53Aggregator = new volcano_vfvo53_aggregator_1.VolcanoVfvo53Aggregator(
202
- // emitSingle: 従来の単発処理パイプライン (opts?.notify === false なら通知スキップ)
203
- (info, opts) => {
204
- const presentation = (0, volcano_presentation_1.resolveVolcanoPresentation)(info, volcanoState);
205
- (0, volcano_formatter_1.displayVolcanoInfo)(info, presentation);
206
- volcanoState.update(info);
207
- if (opts?.notify !== false) {
208
- notifier.notifyVolcano(info, presentation);
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
- // emitBatch: バッチ専用処理
212
- (batch, opts) => {
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
- (0, formatter_1.displayRawHeader)(msg);
160
+ display?.displayRawHeader(msg);
223
161
  return;
224
162
  }
225
163
  const route = classifyMessage(msg.classification, msg.head.type);
226
- switch (route) {
227
- case "eew":
228
- handleEew(msg, eewTracker, eewLogger, notifier);
229
- break;
230
- case "seismicText":
231
- handleSeismicText(msg, notifier);
232
- break;
233
- case "lgObservation":
234
- handleLgObservation(msg, notifier);
235
- break;
236
- case "earthquake":
237
- handleEarthquake(msg, notifier);
238
- break;
239
- case "tsunami":
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
- flushAndDisposeVolcanoBuffer: () => vfvo53Aggregator.flushAndDispose(),
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;