@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,121 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.runMonitor = runMonitor;
40
+ exports.resetTerminalTitle = resetTerminalTitle;
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const rest_client_1 = require("../../dmdata/rest-client");
43
+ const monitor_1 = require("../monitor/monitor");
44
+ const formatter_1 = require("../../ui/formatter");
45
+ const theme_1 = require("../../ui/theme");
46
+ const config_resolver_1 = require("../startup/config-resolver");
47
+ const updateChecker = __importStar(require("../startup/update-checker"));
48
+ const log = __importStar(require("../../logger"));
49
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
50
+ const { version: VERSION } = require("../../../package.json");
51
+ async function runMonitor(opts) {
52
+ // ログレベル設定
53
+ if (opts.debug) {
54
+ log.setLogLevel(log.LogLevel.DEBUG);
55
+ }
56
+ // 設定解決 (CLI引数 → 環境変数 → Configファイル → デフォルト)
57
+ const config = (0, config_resolver_1.resolveConfig)(opts);
58
+ // Banner title (契約チェック前に表示)
59
+ console.log();
60
+ console.log(chalk_1.default.cyan.bold.inverse(`${config.appName} v${VERSION} — Project DM-D.S.S リアルタイム地震・津波情報モニター`));
61
+ console.log();
62
+ // ターミナルタイトル設定
63
+ setTerminalTitle(`${config.appName} v${VERSION}`);
64
+ // 契約状況チェック
65
+ try {
66
+ const contractedClassifications = await (0, rest_client_1.listContracts)(config.apiKey);
67
+ const skipped = config.classifications.filter((c) => !contractedClassifications.includes(c));
68
+ const active = config.classifications.filter((c) => contractedClassifications.includes(c));
69
+ for (const s of skipped) {
70
+ log.warn(`${s} は未契約のためスキップします`);
71
+ }
72
+ if (active.length === 0) {
73
+ log.error("有効な契約区分がありません。dmdata.jp で区分を契約してください。");
74
+ process.exit(1);
75
+ }
76
+ config.classifications = active;
77
+ }
78
+ catch (err) {
79
+ log.warn(`契約状況の確認に失敗しました: ${err instanceof Error ? err.message : err}`);
80
+ log.warn("指定された区分のまま接続を試みます");
81
+ }
82
+ // テーマ読込
83
+ const themeWarnings = (0, theme_1.loadTheme)();
84
+ for (const w of themeWarnings) {
85
+ log.warn(w);
86
+ }
87
+ // formatter キャッシュ初期化
88
+ if (config.tableWidth != null) {
89
+ (0, formatter_1.setFrameWidth)(config.tableWidth);
90
+ }
91
+ (0, formatter_1.setInfoFullText)(config.infoFullText ?? false);
92
+ (0, formatter_1.setDisplayMode)(config.displayMode);
93
+ (0, formatter_1.setMaxObservations)(config.maxObservations);
94
+ (0, formatter_1.setTruncation)(config.truncation);
95
+ printBanner(config);
96
+ updateChecker.checkForUpdates("fleq", VERSION);
97
+ await (0, monitor_1.startMonitor)(config);
98
+ }
99
+ /** ターミナルタイトルを設定する (ANSI OSC sequence) */
100
+ function setTerminalTitle(title) {
101
+ if (process.stdout.isTTY) {
102
+ process.stdout.write(`\x1b]2;${title}\x07`);
103
+ }
104
+ }
105
+ /** ターミナルタイトルをリセットする */
106
+ function resetTerminalTitle() {
107
+ if (process.stdout.isTTY) {
108
+ // 空文字を設定するとターミナルがデフォルトタイトルに戻る
109
+ process.stdout.write(`\x1b]2;\x07`);
110
+ }
111
+ }
112
+ /** 起動バナー表示 */
113
+ function printBanner(config) {
114
+ log.info(`受信区分: ${config.classifications.join(", ")}`);
115
+ log.info(`テストモード: ${config.testMode}`);
116
+ if (config.displayMode !== "normal") {
117
+ log.info(`表示モード: ${config.displayMode}`);
118
+ }
119
+ log.info("接続を開始します...");
120
+ console.log();
121
+ }
@@ -0,0 +1,121 @@
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.buildProgram = buildProgram;
37
+ const commander_1 = require("commander");
38
+ const config_1 = require("../../config");
39
+ const log = __importStar(require("../../logger"));
40
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
41
+ const { version: VERSION } = require("../../../package.json");
42
+ function buildProgram() {
43
+ const program = new commander_1.Command();
44
+ program
45
+ .name("fleq")
46
+ .description("Project DM-D.S.S (dmdata.jp) の地震・津波・EEW情報をリアルタイムで受信・表示するCLIツールです")
47
+ .version(VERSION)
48
+ .option("-k, --api-key <key>", "dmdata.jp APIキーを指定します(環境変数 DMDATA_API_KEY でも指定できます)")
49
+ .option("-c, --classifications <items>", "受信区分を指定します(カンマ区切り: telegram.earthquake,eew.forecast,eew.warning,telegram.volcano)")
50
+ .option("--test <mode>", 'テスト電文の扱いを指定します: "no" | "including" | "only"')
51
+ .option("--keep-existing", "既存のWebSocket接続を維持します(互換オプション。現在はこちらがデフォルトです)")
52
+ .option("--close-others", "同一APIキーの既存 open socket を閉じてから接続します")
53
+ .option("--mode <mode>", '表示モードを指定します: "normal" | "compact"')
54
+ .option("--debug", "デバッグログを表示します", false)
55
+ .action(async (opts) => {
56
+ const { runMonitor } = await Promise.resolve().then(() => __importStar(require("./cli-run")));
57
+ return runMonitor(opts);
58
+ });
59
+ // init コマンド
60
+ program
61
+ .command("init")
62
+ .description("インタラクティブに初期設定を行います")
63
+ .action(async () => {
64
+ const { runInit } = await Promise.resolve().then(() => __importStar(require("./cli-init")));
65
+ return runInit();
66
+ });
67
+ const configCmd = program
68
+ .command("config")
69
+ .description("Configファイルの設定を管理します");
70
+ configCmd
71
+ .command("show")
72
+ .description("現在の設定を表示します")
73
+ .action(() => {
74
+ (0, config_1.printConfig)();
75
+ });
76
+ configCmd
77
+ .command("set <key> <value>")
78
+ .description("設定値を保存します")
79
+ .action((key, value) => {
80
+ try {
81
+ (0, config_1.setConfigValue)(key, value);
82
+ log.info(`設定しました: ${key}`);
83
+ }
84
+ catch (err) {
85
+ if (err instanceof config_1.ConfigError) {
86
+ log.error(err.message);
87
+ process.exit(1);
88
+ }
89
+ throw err;
90
+ }
91
+ });
92
+ configCmd
93
+ .command("unset <key>")
94
+ .description("設定値を削除します")
95
+ .action((key) => {
96
+ try {
97
+ (0, config_1.unsetConfigValue)(key);
98
+ log.info(`削除しました: ${key}`);
99
+ }
100
+ catch (err) {
101
+ if (err instanceof config_1.ConfigError) {
102
+ log.error(err.message);
103
+ process.exit(1);
104
+ }
105
+ throw err;
106
+ }
107
+ });
108
+ configCmd
109
+ .command("path")
110
+ .description("Configファイルのパスを表示します")
111
+ .action(() => {
112
+ console.log((0, config_1.getConfigPath)());
113
+ });
114
+ configCmd
115
+ .command("keys")
116
+ .description("設定可能なキー一覧を表示します")
117
+ .action(() => {
118
+ (0, config_1.printConfigKeys)();
119
+ });
120
+ return program;
121
+ }
@@ -0,0 +1,355 @@
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.EewEventLogger = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const log = __importStar(require("../../logger"));
40
+ /** ログ出力のデフォルトディレクトリ */
41
+ const DEFAULT_LOG_DIR = path.join(process.cwd(), "eew-logs");
42
+ /** MaxIntChangeReason コードの表示ラベル */
43
+ const MAX_INT_CHANGE_REASON_LABELS = {
44
+ 0: "不明",
45
+ 1: "M変化",
46
+ 2: "震源変化",
47
+ 3: "M+震源",
48
+ 4: "深さ変化",
49
+ 9: "PLUM法",
50
+ };
51
+ /** eventId をファイル名に安全な文字列へサニタイズ (パストラバーサル防止) */
52
+ function sanitizeEventId(eventId) {
53
+ // 英数字・ハイフン・アンダースコアのみ残す
54
+ return eventId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
55
+ }
56
+ /** 数値を2桁ゼロ埋め */
57
+ const pad2 = (n) => String(n).padStart(2, "0");
58
+ /** 日時をローカルタイムの読みやすい形式にフォーマット */
59
+ function formatLocalTime(isoStr) {
60
+ const d = new Date(isoStr);
61
+ if (isNaN(d.getTime()))
62
+ return isoStr;
63
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
64
+ }
65
+ /** 現在時刻を HH:mm:ss 形式で返す */
66
+ function nowTimeStr() {
67
+ const d = new Date();
68
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
69
+ }
70
+ /** 現在時刻を YYYYMMDD_HHmmss 形式で返す */
71
+ function nowFileTimestamp() {
72
+ const d = new Date();
73
+ return `${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}_${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
74
+ }
75
+ /** 差分情報をテキスト化 */
76
+ function formatDiff(diff, info) {
77
+ const parts = [];
78
+ if (diff.previousMagnitude && info.earthquake?.magnitude) {
79
+ parts.push(`M${diff.previousMagnitude}→M${info.earthquake.magnitude}`);
80
+ }
81
+ if (diff.previousDepth && info.earthquake?.depth) {
82
+ parts.push(`${diff.previousDepth}→${info.earthquake.depth}`);
83
+ }
84
+ if (diff.previousMaxInt && info.forecastIntensity?.areas.length) {
85
+ const topInt = info.forecastIntensity.areas[0].intensity;
86
+ parts.push(`震度${diff.previousMaxInt}→${topInt}`);
87
+ }
88
+ if (diff.hypocenterChange)
89
+ parts.push("震源変更");
90
+ return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
91
+ }
92
+ /** 非同期ファイル書き込み (エラーはログ出力のみ) */
93
+ async function appendFileAsync(filePath, data) {
94
+ try {
95
+ await fs.promises.appendFile(filePath, data, "utf-8");
96
+ }
97
+ catch (err) {
98
+ if (err instanceof Error) {
99
+ log.error(`EEW ログ書き込み失敗: ${err.message}`);
100
+ }
101
+ }
102
+ }
103
+ /**
104
+ * EEW イベントごとにログファイルへ逐次追記するロガー。
105
+ * 各報の受信時に非同期でディスクへ書き込む。
106
+ */
107
+ class EewEventLogger {
108
+ logDir;
109
+ /** eventId → ファイルパス */
110
+ activeFiles = new Map();
111
+ /** 書き込みの順序保証用チェーン (eventId → Promise) */
112
+ writeChains = new Map();
113
+ /** ログ記録が有効かどうか */
114
+ enabled = true;
115
+ /** 記録対象のフィールド */
116
+ fields = {
117
+ hypocenter: true,
118
+ originTime: true,
119
+ coordinates: true,
120
+ magnitude: true,
121
+ forecastIntensity: true,
122
+ maxLgInt: true,
123
+ forecastAreas: true,
124
+ lgIntensity: true,
125
+ isPlum: true,
126
+ hasArrived: true,
127
+ diff: true,
128
+ maxIntChangeReason: true,
129
+ };
130
+ constructor(logDir) {
131
+ this.logDir = logDir ?? DEFAULT_LOG_DIR;
132
+ }
133
+ /** ログ記録の有効/無効を設定 */
134
+ setEnabled(enabled) {
135
+ this.enabled = enabled;
136
+ }
137
+ /** ログ記録が有効かどうかを返す */
138
+ isEnabled() {
139
+ return this.enabled;
140
+ }
141
+ /** 記録対象フィールドを設定 */
142
+ setFields(fields) {
143
+ this.fields = { ...fields };
144
+ }
145
+ /** 記録対象フィールドを返す */
146
+ getFields() {
147
+ return { ...this.fields };
148
+ }
149
+ /** 特定フィールドの有効/無効を切り替え */
150
+ toggleField(field) {
151
+ this.fields[field] = !this.fields[field];
152
+ return this.fields[field];
153
+ }
154
+ /** EEW 報を受信した際に呼び出す。第1報でファイル作成、続報は追記。 */
155
+ logReport(info, result) {
156
+ if (!this.enabled)
157
+ return;
158
+ const eventId = sanitizeEventId(info.eventId || "unknown");
159
+ if (result.isNew) {
160
+ this.startNewEvent(eventId, info);
161
+ }
162
+ else {
163
+ this.appendReport(eventId, info, result.diff);
164
+ }
165
+ }
166
+ /** イベント終了を記録し、追跡から除去する */
167
+ closeEvent(eventId, reason) {
168
+ const filePath = this.activeFiles.get(eventId);
169
+ if (filePath == null)
170
+ return;
171
+ const line = `\n--- 記録終了 (${reason}) ${nowTimeStr()} ---\n`;
172
+ this.enqueueWrite(eventId, filePath, line);
173
+ log.debug(`EEW ログ記録終了: ${filePath}`);
174
+ this.activeFiles.delete(eventId);
175
+ }
176
+ /** 全アクティブイベントのログを閉じる (シャットダウン時) */
177
+ closeAll() {
178
+ const eventIds = [...this.activeFiles.keys()];
179
+ for (const eventId of eventIds) {
180
+ this.closeEvent(eventId, "シャットダウン");
181
+ }
182
+ }
183
+ /** ログディレクトリを返す (テスト用) */
184
+ getLogDir() {
185
+ return this.logDir;
186
+ }
187
+ /** 全書き込みの完了を待つ (テスト用) */
188
+ async flush() {
189
+ await Promise.all([...this.writeChains.values()]);
190
+ }
191
+ /** 新規イベントのログファイルを作成 */
192
+ startNewEvent(eventId, info) {
193
+ this.ensureLogDir();
194
+ const fileName = `eew_${eventId}_${nowFileTimestamp()}.log`;
195
+ const filePath = path.join(this.logDir, fileName);
196
+ this.activeFiles.set(eventId, filePath);
197
+ const header = this.buildHeader(eventId, info);
198
+ const report = this.buildReportBlock(info, undefined);
199
+ this.enqueueWrite(eventId, filePath, header + report);
200
+ log.info(`EEW ログ記録開始: ${filePath}`);
201
+ }
202
+ /** 既存イベントに報を追記 */
203
+ appendReport(eventId, info, diff) {
204
+ // ファイルが未作成の場合(eventId なしで始まった等)は新規作成
205
+ if (!this.activeFiles.has(eventId)) {
206
+ this.startNewEvent(eventId, info);
207
+ return;
208
+ }
209
+ const filePath = this.activeFiles.get(eventId);
210
+ const report = this.buildReportBlock(info, diff);
211
+ this.enqueueWrite(eventId, filePath, report);
212
+ }
213
+ /** 書き込みをチェーンに追加して順序を保証する */
214
+ enqueueWrite(eventId, filePath, data) {
215
+ const prev = this.writeChains.get(eventId) ?? Promise.resolve();
216
+ const next = prev.then(() => appendFileAsync(filePath, data)).then(() => {
217
+ // チェーンが完了し、かつアクティブファイルが閉じられていれば Map から除去
218
+ if (!this.activeFiles.has(eventId) && this.writeChains.get(eventId) === next) {
219
+ this.writeChains.delete(eventId);
220
+ }
221
+ });
222
+ this.writeChains.set(eventId, next);
223
+ }
224
+ /** ファイルヘッダを構築 */
225
+ buildHeader(eventId, info) {
226
+ const lines = [];
227
+ lines.push(`=== 緊急地震速報 EventID: ${eventId} ===`);
228
+ lines.push(`記録開始: ${formatLocalTime(info.reportDateTime)}`);
229
+ lines.push("");
230
+ return lines.join("\n");
231
+ }
232
+ /** 地域名に注記 ({Lx,P,A}) を付与 */
233
+ formatAreaName(area) {
234
+ const flags = [];
235
+ if (this.fields.lgIntensity && area.lgIntensity) {
236
+ flags.push(`L${area.lgIntensity}`);
237
+ }
238
+ if (this.fields.isPlum && area.isPlum) {
239
+ flags.push("P");
240
+ }
241
+ if (this.fields.hasArrived && area.hasArrived) {
242
+ flags.push("A");
243
+ }
244
+ return flags.length > 0 ? `${area.name}{${flags.join(",")}}` : area.name;
245
+ }
246
+ /** 地域注記の凡例行が必要かどうか判定 */
247
+ needsAreaLegend(areas) {
248
+ if (this.fields.lgIntensity && areas.some(a => a.lgIntensity))
249
+ return true;
250
+ if (this.fields.isPlum && areas.some(a => a.isPlum))
251
+ return true;
252
+ if (this.fields.hasArrived && areas.some(a => a.hasArrived))
253
+ return true;
254
+ return false;
255
+ }
256
+ /** 1報分のテキストブロックを構築 */
257
+ buildReportBlock(info, diff) {
258
+ const serial = info.serial ?? "?";
259
+ const isCancelled = info.infoType === "取消";
260
+ const typeLabel = isCancelled
261
+ ? "取消"
262
+ : info.isWarning
263
+ ? "警報"
264
+ : "予報";
265
+ const time = nowTimeStr();
266
+ const lines = [];
267
+ lines.push(`--- 第${serial}報 (${typeLabel}) ${time} ---`);
268
+ if (isCancelled) {
269
+ lines.push("この地震についての緊急地震速報は取り消されました。");
270
+ lines.push("");
271
+ return lines.join("\n");
272
+ }
273
+ if (info.earthquake) {
274
+ const eq = info.earthquake;
275
+ if (this.fields.hypocenter) {
276
+ lines.push(`震源: ${eq.hypocenterName}`);
277
+ }
278
+ // originTime (hypocenter が OFF なら非表示)
279
+ if (this.fields.hypocenter && this.fields.originTime && eq.originTime) {
280
+ lines.push(` 発生: ${formatLocalTime(eq.originTime)}`);
281
+ }
282
+ // coordinates (hypocenter が OFF なら非表示)
283
+ if (this.fields.hypocenter && this.fields.coordinates && eq.latitude && eq.longitude) {
284
+ lines.push(` 座標: ${eq.latitude} ${eq.longitude}`);
285
+ }
286
+ if (info.isAssumedHypocenter) {
287
+ if (this.fields.hypocenter) {
288
+ lines.push("仮定震源要素 (震源未確定・PLUM法による推定)");
289
+ }
290
+ }
291
+ else {
292
+ if (this.fields.magnitude) {
293
+ lines.push(`M${eq.magnitude} 深さ${eq.depth}`);
294
+ }
295
+ if (this.fields.diff && diff) {
296
+ const diffStr = formatDiff(diff, info);
297
+ if (diffStr.length > 0) {
298
+ lines.push(`変化:${diffStr}`);
299
+ }
300
+ }
301
+ // maxIntChangeReason (diff の直下)
302
+ if (this.fields.maxIntChangeReason && info.maxIntChangeReason != null) {
303
+ const label = MAX_INT_CHANGE_REASON_LABELS[info.maxIntChangeReason] ?? "不明";
304
+ lines.push(`震度変化理由: ${label} [${info.maxIntChangeReason}]`);
305
+ }
306
+ }
307
+ }
308
+ if (info.forecastIntensity && info.forecastIntensity.areas.length > 0) {
309
+ if (this.fields.forecastIntensity) {
310
+ const topInt = info.forecastIntensity.areas[0].intensity;
311
+ lines.push(`最大予測震度: ${topInt}`);
312
+ }
313
+ // maxLgInt (forecastIntensity が OFF なら非表示)
314
+ if (this.fields.forecastIntensity && this.fields.maxLgInt && info.forecastIntensity.maxLgInt) {
315
+ lines.push(`最大予測長周期階級: ${info.forecastIntensity.maxLgInt}`);
316
+ }
317
+ if (this.fields.forecastAreas) {
318
+ // 注記の凡例行
319
+ if (this.needsAreaLegend(info.forecastIntensity.areas)) {
320
+ const legendParts = [];
321
+ if (this.fields.lgIntensity)
322
+ legendParts.push("Lx=長周期階級");
323
+ if (this.fields.isPlum)
324
+ legendParts.push("P=PLUM");
325
+ if (this.fields.hasArrived)
326
+ legendParts.push("A=主要動到達");
327
+ lines.push(` 注記: {${legendParts.join(", ")}}`);
328
+ }
329
+ // 震度ごとにグループ化して地域名を表示
330
+ const byIntensity = new Map();
331
+ for (const area of info.forecastIntensity.areas) {
332
+ const existing = byIntensity.get(area.intensity) ?? [];
333
+ existing.push(this.formatAreaName(area));
334
+ byIntensity.set(area.intensity, existing);
335
+ }
336
+ for (const [intensity, names] of byIntensity) {
337
+ lines.push(` 震度${intensity}: ${names.join(", ")}`);
338
+ }
339
+ }
340
+ }
341
+ // 最終報
342
+ if (info.nextAdvisory) {
343
+ lines.push(info.nextAdvisory);
344
+ }
345
+ lines.push("");
346
+ return lines.join("\n");
347
+ }
348
+ /** ログディレクトリが存在しなければ作成 */
349
+ ensureLogDir() {
350
+ if (!fs.existsSync(this.logDir)) {
351
+ fs.mkdirSync(this.logDir, { recursive: true });
352
+ }
353
+ }
354
+ }
355
+ exports.EewEventLogger = EewEventLogger;