@sayue_ltr/fleq 1.49.2

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 (52) hide show
  1. package/CHANGELOG.md +898 -0
  2. package/LICENSE +21 -0
  3. package/README.md +535 -0
  4. package/assets/icons/.gitkeep +0 -0
  5. package/assets/sounds/.gitkeep +0 -0
  6. package/assets/sounds/cancel.mp3 +0 -0
  7. package/assets/sounds/critical.mp3 +0 -0
  8. package/assets/sounds/info.mp3 +0 -0
  9. package/assets/sounds/normal.mp3 +0 -0
  10. package/assets/sounds/warning.mp3 +0 -0
  11. package/dist/config.js +638 -0
  12. package/dist/dmdata/connection-manager.js +2 -0
  13. package/dist/dmdata/endpoint-selector.js +185 -0
  14. package/dist/dmdata/multi-connection-manager.js +158 -0
  15. package/dist/dmdata/rest-client.js +281 -0
  16. package/dist/dmdata/telegram-parser.js +704 -0
  17. package/dist/dmdata/volcano-parser.js +647 -0
  18. package/dist/dmdata/ws-client.js +336 -0
  19. package/dist/engine/cli/cli-init.js +266 -0
  20. package/dist/engine/cli/cli-run.js +121 -0
  21. package/dist/engine/cli/cli.js +121 -0
  22. package/dist/engine/eew/eew-logger.js +355 -0
  23. package/dist/engine/eew/eew-tracker.js +229 -0
  24. package/dist/engine/messages/message-router.js +261 -0
  25. package/dist/engine/messages/tsunami-state.js +96 -0
  26. package/dist/engine/messages/volcano-state.js +131 -0
  27. package/dist/engine/messages/volcano-vfvo53-aggregator.js +173 -0
  28. package/dist/engine/monitor/monitor.js +118 -0
  29. package/dist/engine/monitor/repl-coordinator.js +63 -0
  30. package/dist/engine/monitor/shutdown.js +114 -0
  31. package/dist/engine/notification/node-notifier-loader.js +19 -0
  32. package/dist/engine/notification/notifier.js +338 -0
  33. package/dist/engine/notification/sound-player.js +230 -0
  34. package/dist/engine/notification/volcano-presentation.js +166 -0
  35. package/dist/engine/startup/config-resolver.js +139 -0
  36. package/dist/engine/startup/tsunami-initializer.js +91 -0
  37. package/dist/engine/startup/update-checker.js +229 -0
  38. package/dist/engine/startup/volcano-initializer.js +89 -0
  39. package/dist/index.js +24 -0
  40. package/dist/logger.js +95 -0
  41. package/dist/types.js +61 -0
  42. package/dist/ui/earthquake-formatter.js +871 -0
  43. package/dist/ui/eew-formatter.js +335 -0
  44. package/dist/ui/formatter.js +689 -0
  45. package/dist/ui/repl.js +2059 -0
  46. package/dist/ui/test-samples.js +880 -0
  47. package/dist/ui/theme.js +516 -0
  48. package/dist/ui/volcano-formatter.js +667 -0
  49. package/dist/ui/waiting-tips.js +227 -0
  50. package/dist/utils/intensity.js +13 -0
  51. package/dist/utils/secrets.js +14 -0
  52. package/package.json +69 -0
@@ -0,0 +1,229 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.EewTracker = void 0;
37
+ const intensityUtils = __importStar(require("../../utils/intensity"));
38
+ /** 古いイベントを自動削除するまでの時間 (ミリ秒) */
39
+ const CLEANUP_THRESHOLD_MS = 10 * 60 * 1000; // 10分
40
+ /** 深さ文字列から数値(km)を抽出 */
41
+ function parseDepthKm(depth) {
42
+ const m = depth.match(/(\d+)\s*km/);
43
+ return m ? parseInt(m[1], 10) : null;
44
+ }
45
+ /** 予測震度リストから最大震度を取得 */
46
+ function getMaxForecastIntensity(areas) {
47
+ if (areas.length === 0)
48
+ return null;
49
+ let maxInt = areas[0].intensity;
50
+ let maxRank = intensityUtils.intensityToRank(maxInt);
51
+ for (let i = 1; i < areas.length; i++) {
52
+ const rank = intensityUtils.intensityToRank(areas[i].intensity);
53
+ if (rank > maxRank) {
54
+ maxRank = rank;
55
+ maxInt = areas[i].intensity;
56
+ }
57
+ }
58
+ return maxInt;
59
+ }
60
+ /** 2つの EEW 情報から差分を計算 */
61
+ function computeDiff(prev, curr) {
62
+ const diff = {};
63
+ let hasDiff = false;
64
+ // マグニチュード変化
65
+ if (prev.earthquake?.magnitude && curr.earthquake?.magnitude) {
66
+ const prevMag = parseFloat(prev.earthquake.magnitude);
67
+ const currMag = parseFloat(curr.earthquake.magnitude);
68
+ if (!isNaN(prevMag) && !isNaN(currMag) && prevMag !== currMag) {
69
+ diff.previousMagnitude = prev.earthquake.magnitude;
70
+ hasDiff = true;
71
+ }
72
+ }
73
+ // 深さ変化
74
+ if (prev.earthquake?.depth && curr.earthquake?.depth) {
75
+ const prevD = parseDepthKm(prev.earthquake.depth);
76
+ const currD = parseDepthKm(curr.earthquake.depth);
77
+ if (prevD != null && currD != null && prevD !== currD) {
78
+ diff.previousDepth = prev.earthquake.depth;
79
+ hasDiff = true;
80
+ }
81
+ }
82
+ // 最大予測震度変化 (配列順に依存せず最大値を正規化して比較)
83
+ if (prev.forecastIntensity?.areas.length && curr.forecastIntensity?.areas.length) {
84
+ const prevMax = getMaxForecastIntensity(prev.forecastIntensity.areas);
85
+ const currMax = getMaxForecastIntensity(curr.forecastIntensity.areas);
86
+ if (prevMax && currMax && prevMax !== currMax) {
87
+ diff.previousMaxInt = prevMax;
88
+ hasDiff = true;
89
+ }
90
+ }
91
+ // 震源地名変化
92
+ if (prev.earthquake?.hypocenterName && curr.earthquake?.hypocenterName) {
93
+ if (prev.earthquake.hypocenterName !== curr.earthquake.hypocenterName) {
94
+ diff.hypocenterChange = true;
95
+ hasDiff = true;
96
+ }
97
+ }
98
+ return hasDiff ? diff : undefined;
99
+ }
100
+ /**
101
+ * 複数の EEW イベントを EventID ごとに追跡し、
102
+ * 重複報の検出・キャンセル状態の管理を行う。
103
+ */
104
+ class EewTracker {
105
+ events = new Map();
106
+ onCleanup;
107
+ constructor(options) {
108
+ this.onCleanup = options?.onCleanup;
109
+ }
110
+ /** EEW 情報を受け取り、状態を更新して結果を返す */
111
+ update(info) {
112
+ // 古いイベントをクリーンアップ
113
+ this.cleanup();
114
+ const eventId = info.eventId || "";
115
+ if (!eventId) {
116
+ // EventID がない場合は常に新規扱い
117
+ return {
118
+ isNew: true,
119
+ isDuplicate: false,
120
+ isCancelled: info.infoType === "取消",
121
+ activeCount: this.getActiveCount(),
122
+ colorIndex: 0,
123
+ };
124
+ }
125
+ const serialRaw = parseInt(info.serial || "", 10);
126
+ const serial = Number.isFinite(serialRaw) ? serialRaw : null;
127
+ const isCancelled = info.infoType === "取消";
128
+ const existing = this.events.get(eventId);
129
+ if (existing) {
130
+ // 既知のイベント — 報数チェック
131
+ if (!isCancelled && serial != null && serial > 0 && serial <= existing.lastSerial) {
132
+ // 同じか古い報数 → 重複
133
+ return {
134
+ isNew: false,
135
+ isDuplicate: true,
136
+ isCancelled: false,
137
+ activeCount: this.getActiveCount(),
138
+ colorIndex: existing.colorIndex,
139
+ };
140
+ }
141
+ // 差分計算
142
+ const diff = existing.previousInfo ? computeDiff(existing.previousInfo, info) : undefined;
143
+ const previousInfo = existing.previousInfo;
144
+ // 状態更新
145
+ if (serial != null) {
146
+ existing.lastSerial = Math.max(existing.lastSerial, serial);
147
+ }
148
+ existing.isWarning = existing.isWarning || info.isWarning;
149
+ existing.isCancelled = isCancelled;
150
+ existing.lastUpdate = new Date();
151
+ existing.previousInfo = info;
152
+ return {
153
+ isNew: false,
154
+ isDuplicate: false,
155
+ isCancelled,
156
+ activeCount: this.getActiveCount(),
157
+ diff,
158
+ previousInfo,
159
+ colorIndex: existing.colorIndex,
160
+ };
161
+ }
162
+ // 新規イベント
163
+ const colorIndex = this.nextColorIndex();
164
+ this.events.set(eventId, {
165
+ eventId,
166
+ lastSerial: serial ?? 0,
167
+ isWarning: info.isWarning,
168
+ isCancelled,
169
+ isFinalized: false,
170
+ lastUpdate: new Date(),
171
+ previousInfo: info,
172
+ colorIndex,
173
+ });
174
+ return {
175
+ isNew: true,
176
+ isDuplicate: false,
177
+ isCancelled,
178
+ activeCount: this.getActiveCount(),
179
+ colorIndex,
180
+ };
181
+ }
182
+ /**
183
+ * イベントを終了扱いにする (最終報受信時)。
184
+ * 遅延到着した重複報の検出のためエントリは保持し、
185
+ * アクティブカウントからは除外する。
186
+ */
187
+ finalizeEvent(eventId) {
188
+ const ev = this.events.get(eventId);
189
+ if (ev) {
190
+ ev.isFinalized = true;
191
+ }
192
+ }
193
+ /** 未使用の最小カラーインデックスを返す */
194
+ nextColorIndex() {
195
+ const used = new Set();
196
+ for (const ev of this.events.values()) {
197
+ if (!ev.isCancelled && !ev.isFinalized)
198
+ used.add(ev.colorIndex);
199
+ }
200
+ let idx = 0;
201
+ while (used.has(idx))
202
+ idx++;
203
+ return idx;
204
+ }
205
+ /** 現在アクティブ(キャンセル・最終報済みでない)イベント数を返す */
206
+ getActiveCount() {
207
+ let count = 0;
208
+ for (const ev of this.events.values()) {
209
+ if (!ev.isCancelled && !ev.isFinalized)
210
+ count++;
211
+ }
212
+ return count;
213
+ }
214
+ /** 最終更新から一定時間経過したイベントを削除 */
215
+ cleanup() {
216
+ const now = Date.now();
217
+ const expired = [];
218
+ for (const [id, ev] of this.events) {
219
+ if (now - ev.lastUpdate.getTime() > CLEANUP_THRESHOLD_MS) {
220
+ expired.push(id);
221
+ }
222
+ }
223
+ for (const id of expired) {
224
+ this.events.delete(id);
225
+ this.onCleanup?.(id);
226
+ }
227
+ }
228
+ }
229
+ exports.EewTracker = EewTracker;
@@ -0,0 +1,261 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ 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");
44
+ const eew_tracker_1 = require("../eew/eew-tracker");
45
+ const eew_logger_1 = require("../eew/eew-logger");
46
+ const notifier_1 = require("../notification/notifier");
47
+ const tsunami_state_1 = require("./tsunami-state");
48
+ const volcano_state_1 = require("./volcano-state");
49
+ const volcano_presentation_1 = require("../notification/volcano-presentation");
50
+ const log = __importStar(require("../../logger"));
51
+ /**
52
+ * classification と head.type から処理ルートを判定する。
53
+ * ルーティング優先順位:
54
+ * 1. eew.forecast / eew.warning → EEW
55
+ * 2. telegram.earthquake + VXSE56/VXSE60/VZSE40 → テキスト系
56
+ * 3. telegram.earthquake + VXSE62 → 長周期地震動観測
57
+ * 4. telegram.earthquake + VXSE* → 地震情報
58
+ * 5. telegram.earthquake + VTSE* → 津波情報
59
+ * 6. telegram.earthquake + VYSE* → 南海トラフ
60
+ * 7. telegram.volcano → 火山情報
61
+ * 8. その他 → raw
62
+ */
63
+ function classifyMessage(classification, headType) {
64
+ if (classification === "eew.forecast" || classification === "eew.warning") {
65
+ return "eew";
66
+ }
67
+ if (classification === "telegram.volcano") {
68
+ return "volcano";
69
+ }
70
+ if (classification === "telegram.earthquake") {
71
+ if (headType === "VXSE56" || headType === "VXSE60" || headType === "VZSE40") {
72
+ return "seismicText";
73
+ }
74
+ if (headType === "VXSE62") {
75
+ return "lgObservation";
76
+ }
77
+ if (headType.startsWith("VXSE")) {
78
+ return "earthquake";
79
+ }
80
+ if (headType.startsWith("VTSE")) {
81
+ return "tsunami";
82
+ }
83
+ if (headType.startsWith("VYSE")) {
84
+ return "nankaiTrough";
85
+ }
86
+ }
87
+ return "raw";
88
+ }
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);
166
+ }
167
+ }
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);
187
+ }
188
+ }
189
+ /** 受信データのハンドリング */
190
+ function createMessageHandler() {
191
+ const eewLogger = new eew_logger_1.EewEventLogger();
192
+ const notifier = new notifier_1.Notifier();
193
+ const tsunamiState = new tsunami_state_1.TsunamiStateHolder();
194
+ const volcanoState = new volcano_state_1.VolcanoStateHolder();
195
+ const eewTracker = new eew_tracker_1.EewTracker({
196
+ onCleanup: (eventId) => {
197
+ eewLogger.closeEvent(eventId, "タイムアウト");
198
+ },
199
+ });
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);
209
+ }
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);
217
+ }
218
+ });
219
+ const handler = (msg) => {
220
+ // XML電文でない場合はヘッダ情報のみ表示
221
+ if (msg.format !== "xml" || !msg.head.xml) {
222
+ (0, formatter_1.displayRawHeader)(msg);
223
+ return;
224
+ }
225
+ 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;
251
+ }
252
+ };
253
+ return {
254
+ handler,
255
+ eewLogger,
256
+ notifier,
257
+ tsunamiState,
258
+ volcanoState,
259
+ flushAndDisposeVolcanoBuffer: () => vfvo53Aggregator.flushAndDispose(),
260
+ };
261
+ }
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TsunamiStateHolder = void 0;
4
+ exports.detectTsunamiAlertLevel = detectTsunamiAlertLevel;
5
+ const theme_1 = require("../../ui/theme");
6
+ const earthquake_formatter_1 = require("../../ui/earthquake-formatter");
7
+ /** レベルの優先度 (大きいほど深刻) */
8
+ const LEVEL_PRIORITY = {
9
+ "大津波警報": 3,
10
+ "津波警報": 2,
11
+ "津波注意報": 1,
12
+ };
13
+ /** レベルに対応するテーマロール */
14
+ const LEVEL_ROLE = {
15
+ "大津波警報": "tsunamiMajor",
16
+ "津波警報": "tsunamiWarning",
17
+ "津波注意報": "tsunamiAdvisory",
18
+ };
19
+ /** 判定対象の kind 一覧 */
20
+ const ALERT_KINDS = new Set(["大津波警報", "津波警報", "津波注意報"]);
21
+ /**
22
+ * forecast の kind 一覧から最大警報レベルを判定する。
23
+ * 津波予報 (0.2m 以下) や kind なしの場合は null を返す。
24
+ */
25
+ function detectTsunamiAlertLevel(kinds) {
26
+ let maxLevel = null;
27
+ let maxPriority = 0;
28
+ for (const kind of kinds) {
29
+ if (ALERT_KINDS.has(kind)) {
30
+ const level = kind;
31
+ const priority = LEVEL_PRIORITY[level];
32
+ if (priority > maxPriority) {
33
+ maxPriority = priority;
34
+ maxLevel = level;
35
+ }
36
+ }
37
+ }
38
+ return maxLevel;
39
+ }
40
+ /**
41
+ * 津波情報の状態を保持し、プロンプト表示と detail コマンドを提供する。
42
+ */
43
+ class TsunamiStateHolder {
44
+ category = "tsunami";
45
+ emptyMessage = "現在、継続中の津波情報はありません。";
46
+ currentLevel = null;
47
+ lastInfo = null;
48
+ /** 現在の警報レベルを返す (テスト用) */
49
+ getLevel() {
50
+ return this.currentLevel;
51
+ }
52
+ /** VTSE41 受信時に状態を更新する */
53
+ update(info) {
54
+ // 取消報 → クリア
55
+ if (info.infoType === "取消") {
56
+ this.clear();
57
+ return;
58
+ }
59
+ // forecast から警報レベルを検出
60
+ const kinds = (info.forecast ?? []).map((f) => f.kind);
61
+ const level = detectTsunamiAlertLevel(kinds);
62
+ if (level == null) {
63
+ // 警報レベルなし (津波予報のみ等) → クリア
64
+ this.clear();
65
+ return;
66
+ }
67
+ this.currentLevel = level;
68
+ this.lastInfo = info;
69
+ }
70
+ /** 状態をクリアする */
71
+ clear() {
72
+ this.currentLevel = null;
73
+ this.lastInfo = null;
74
+ }
75
+ // ── PromptStatusProvider ──
76
+ getPromptStatus() {
77
+ if (this.currentLevel == null)
78
+ return null;
79
+ const role = LEVEL_ROLE[this.currentLevel];
80
+ const colorFn = (0, theme_1.getRoleChalk)(role);
81
+ return {
82
+ text: colorFn(this.currentLevel),
83
+ priority: 10,
84
+ };
85
+ }
86
+ // ── DetailProvider ──
87
+ hasDetail() {
88
+ return this.lastInfo != null;
89
+ }
90
+ showDetail() {
91
+ if (this.lastInfo != null) {
92
+ (0, earthquake_formatter_1.displayTsunamiInfo)(this.lastInfo);
93
+ }
94
+ }
95
+ }
96
+ exports.TsunamiStateHolder = TsunamiStateHolder;