@sayue_ltr/fleq 1.51.0 → 2.0.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 +78 -0
- package/README.md +1 -1
- package/dist/config.js +2 -2
- package/dist/dmdata/telegram-parser.js +78 -5
- package/dist/engine/cli/cli-run.js +17 -2
- package/dist/engine/eew/eew-tracker.js +41 -15
- package/dist/engine/monitor/monitor.js +1 -1
- package/dist/engine/notification/notifier.js +5 -3
- package/dist/engine/notification/sound-player.js +232 -35
- package/dist/engine/presentation/processors/process-eew.js +15 -0
- package/dist/engine/presentation/processors/process-message.js +2 -2
- package/dist/engine/template/compiler.js +4 -1
- package/dist/engine/template/field-accessor.js +10 -4
- package/dist/engine/template/filters.js +14 -8
- package/dist/engine/template/parser.js +10 -15
- package/dist/types.js +1 -1
- package/dist/ui/earthquake-formatter.js +5 -0
- package/dist/ui/formatter.js +49 -0
- package/dist/ui/repl-handlers/command-definitions.js +11 -4
- package/dist/ui/repl-handlers/info-handlers.js +97 -41
- package/dist/ui/repl-handlers/settings-handlers.js +19 -15
- package/dist/ui/statistics-formatter.js +65 -15
- package/dist/ui/status-line.js +11 -0
- package/dist/ui/test-samples.js +6 -0
- package/dist/ui/theme.js +9 -0
- package/dist/ui/waiting-tips-eew.js +63 -0
- package/dist/ui/waiting-tips-info-systems.js +81 -0
- package/dist/ui/waiting-tips-seismology.js +97 -0
- package/dist/ui/waiting-tips-tsunami.js +72 -0
- package/dist/ui/waiting-tips-weather.js +189 -0
- package/dist/ui/waiting-tips.js +137 -6
- package/package.json +1 -1
package/dist/ui/formatter.js
CHANGED
|
@@ -67,6 +67,7 @@ exports.collectHighlightSpans = collectHighlightSpans;
|
|
|
67
67
|
exports.highlightAndWrap = highlightAndWrap;
|
|
68
68
|
exports.formatTimestamp = formatTimestamp;
|
|
69
69
|
exports.formatElapsedTime = formatElapsedTime;
|
|
70
|
+
exports.formatUptime = formatUptime;
|
|
70
71
|
exports.intensityColor = intensityColor;
|
|
71
72
|
exports.lgIntensityColor = lgIntensityColor;
|
|
72
73
|
exports.lgIntToNumeric = lgIntToNumeric;
|
|
@@ -619,6 +620,54 @@ function formatElapsedTime(ms) {
|
|
|
619
620
|
const ss = totalSec % 60;
|
|
620
621
|
return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}:${String(ss).padStart(2, "0")}`;
|
|
621
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* 稼働時間を "DDD:HH:MM:SS" 形式に整形する。
|
|
625
|
+
* 未使用の上位ゼロ桁は dim (gray) 表示にして画面ヤケを軽減する。
|
|
626
|
+
* 日数が 3 桁を超える場合は桁数が増える。
|
|
627
|
+
*/
|
|
628
|
+
function formatUptime(ms) {
|
|
629
|
+
const safeMs = Math.max(0, ms);
|
|
630
|
+
const totalSec = Math.floor(safeMs / 1000);
|
|
631
|
+
const dd = Math.floor(totalSec / 86400);
|
|
632
|
+
const hh = Math.floor((totalSec % 86400) / 3600);
|
|
633
|
+
const mm = Math.floor((totalSec % 3600) / 60);
|
|
634
|
+
const ss = totalSec % 60;
|
|
635
|
+
const ddStr = String(dd).padStart(3, "0");
|
|
636
|
+
const hhStr = String(hh).padStart(2, "0");
|
|
637
|
+
const mmStr = String(mm).padStart(2, "0");
|
|
638
|
+
const ssStr = String(ss).padStart(2, "0");
|
|
639
|
+
const raw = `${ddStr}:${hhStr}:${mmStr}:${ssStr}`;
|
|
640
|
+
// セグメント境界: DDD(0-2) : HH(4-5) : MM(7-8) : SS(10-11)
|
|
641
|
+
// segStarts は dd < 1000 (3桁) の通常ケース用。
|
|
642
|
+
// dd >= 1000 では firstNonZero=0 → activeSegStart=0 で全体 white になるため問題なし。
|
|
643
|
+
const segStarts = [0, 4, 7, 10];
|
|
644
|
+
// 先頭から連続するゼロと区切り文字をスキャン
|
|
645
|
+
let firstNonZero = -1;
|
|
646
|
+
for (let i = 0; i < raw.length; i++) {
|
|
647
|
+
if (raw[i] !== "0" && raw[i] !== ":") {
|
|
648
|
+
firstNonZero = i;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// dim 境界を決定
|
|
653
|
+
// - 日部分 (index 0-2): 文字レベルで先頭ゼロをグレーアウト
|
|
654
|
+
// - 時刻部分 (HH/MM/SS): セグメント境界でグレーアウト
|
|
655
|
+
// - 全桁ゼロ: SS セグメントから通常表示
|
|
656
|
+
let dimEnd;
|
|
657
|
+
if (firstNonZero === -1) {
|
|
658
|
+
dimEnd = segStarts[segStarts.length - 1];
|
|
659
|
+
}
|
|
660
|
+
else if (firstNonZero <= 2) {
|
|
661
|
+
dimEnd = firstNonZero;
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
dimEnd = segStarts.filter((s) => s <= firstNonZero).pop();
|
|
665
|
+
}
|
|
666
|
+
if (dimEnd === 0) {
|
|
667
|
+
return chalk_1.default.white(raw);
|
|
668
|
+
}
|
|
669
|
+
return chalk_1.default.gray(raw.slice(0, dimEnd)) + chalk_1.default.white(raw.slice(dimEnd));
|
|
670
|
+
}
|
|
622
671
|
/** 区切り線 (後方互換用、新コードではフレーム関数を使用) */
|
|
623
672
|
function separator(char = "─", len = 60) {
|
|
624
673
|
return chalk_1.default.gray(char.repeat(len));
|
|
@@ -41,11 +41,17 @@ const ops = __importStar(require("./operation-handlers"));
|
|
|
41
41
|
function buildCommandMap(getCtx) {
|
|
42
42
|
return {
|
|
43
43
|
help: {
|
|
44
|
-
description: "
|
|
45
|
-
detail: "
|
|
44
|
+
description: "コマンドの詳細を表示 (例: help notify)",
|
|
45
|
+
detail: "help <command>: コマンドの詳細を表示\n help <command> <sub>: サブコマンドの詳細を表示\n 一覧は commands コマンドで表示できます。",
|
|
46
46
|
category: "info",
|
|
47
47
|
handler: (args) => info.handleHelp(getCtx(), args),
|
|
48
48
|
},
|
|
49
|
+
commands: {
|
|
50
|
+
description: "コマンド一覧を表示 (例: commands settings)",
|
|
51
|
+
detail: "引数なし: 全コマンドをカテゴリ別に一覧表示\n commands <category>: カテゴリで絞り込み (info / status / settings / operation)\n commands <query>: 名前・説明で検索",
|
|
52
|
+
category: "info",
|
|
53
|
+
handler: (args) => info.handleCommands(getCtx(), args),
|
|
54
|
+
},
|
|
49
55
|
"?": {
|
|
50
56
|
description: "help のエイリアス",
|
|
51
57
|
category: "info",
|
|
@@ -185,12 +191,13 @@ function buildCommandMap(getCtx) {
|
|
|
185
191
|
handler: (args) => settings.handleFocus(getCtx(), args),
|
|
186
192
|
},
|
|
187
193
|
clock: {
|
|
188
|
-
description: "プロンプト時計の切替 (例: clock / clock
|
|
189
|
-
detail: "clock:
|
|
194
|
+
description: "プロンプト時計の切替 (例: clock / clock uptime)",
|
|
195
|
+
detail: "clock: 経過時間→現在時刻→稼働時間をトグル切替\n clock elapsed: 経過時間表示 (デフォルト)\n clock now: 現在時刻表示\n clock uptime: 稼働時間表示 (DDD:HH:MM:SS)",
|
|
190
196
|
category: "settings",
|
|
191
197
|
subcommands: {
|
|
192
198
|
elapsed: { description: "経過時間表示 (デフォルト)" },
|
|
193
199
|
now: { description: "現在時刻表示" },
|
|
200
|
+
uptime: { description: "稼働時間表示 (DDD:HH:MM:SS)" },
|
|
194
201
|
},
|
|
195
202
|
handler: (args) => settings.handleClock(getCtx(), args),
|
|
196
203
|
},
|
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.CATEGORY_ALIASES = exports.COMMAND_ALIASES = void 0;
|
|
40
40
|
exports.getCurrentSettingValues = getCurrentSettingValues;
|
|
41
41
|
exports.handleDetail = handleDetail;
|
|
42
|
+
exports.handleCommands = handleCommands;
|
|
42
43
|
exports.handleHelp = handleHelp;
|
|
43
44
|
exports.handleStats = handleStats;
|
|
44
45
|
exports.handleHistory = handleHistory;
|
|
@@ -161,6 +162,7 @@ function printColorGrid(termWidth, items, renderFn) {
|
|
|
161
162
|
}
|
|
162
163
|
/** コマンドのエイリアス (逆引き用) */
|
|
163
164
|
exports.COMMAND_ALIASES = {
|
|
165
|
+
cmds: "commands",
|
|
164
166
|
hist: "history",
|
|
165
167
|
cols: "colors",
|
|
166
168
|
det: "detail",
|
|
@@ -224,8 +226,11 @@ function getCurrentSettingValues(ctx) {
|
|
|
224
226
|
options: "normal / compact",
|
|
225
227
|
},
|
|
226
228
|
clock: {
|
|
227
|
-
current:
|
|
228
|
-
|
|
229
|
+
current: (() => {
|
|
230
|
+
const m = statusLine.getClockMode();
|
|
231
|
+
return m === "clock" ? "現在時刻" : m === "uptime" ? "稼働時間" : "経過時間";
|
|
232
|
+
})(),
|
|
233
|
+
options: "elapsed / now / uptime",
|
|
229
234
|
},
|
|
230
235
|
notify: {
|
|
231
236
|
current: `${onCount}/${totalCount} ON${muteInfo}`,
|
|
@@ -274,6 +279,88 @@ function handleDetail(ctx, args) {
|
|
|
274
279
|
}
|
|
275
280
|
console.log(chalk_1.default.yellow(` 不明なサブコマンド: ${sub}`) + chalk_1.default.gray(" (利用可能: tsunami)"));
|
|
276
281
|
}
|
|
282
|
+
/** カテゴリ名を解決する (日本語ラベルにも対応) */
|
|
283
|
+
function resolveCategory(input) {
|
|
284
|
+
const lower = input.toLowerCase();
|
|
285
|
+
const categories = Object.keys(types_2.CATEGORY_LABELS);
|
|
286
|
+
const exact = categories.find((c) => c === lower);
|
|
287
|
+
if (exact != null)
|
|
288
|
+
return exact;
|
|
289
|
+
for (const cat of categories) {
|
|
290
|
+
if (types_2.CATEGORY_LABELS[cat] === input)
|
|
291
|
+
return cat;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
function handleCommands(ctx, args) {
|
|
296
|
+
const trimmed = args.trim();
|
|
297
|
+
let filterCategory = null;
|
|
298
|
+
let searchQuery = null;
|
|
299
|
+
if (trimmed.length > 0) {
|
|
300
|
+
filterCategory = resolveCategory(trimmed);
|
|
301
|
+
if (filterCategory == null) {
|
|
302
|
+
searchQuery = trimmed.toLowerCase();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// 検索モード
|
|
306
|
+
if (searchQuery != null) {
|
|
307
|
+
const query = searchQuery;
|
|
308
|
+
const matches = Object.entries(ctx.commands)
|
|
309
|
+
.filter(([name]) => name !== "?" && name !== "exit" && name !== "cmds")
|
|
310
|
+
.filter(([name, entry]) => name.toLowerCase().includes(query) ||
|
|
311
|
+
entry.description.toLowerCase().includes(query))
|
|
312
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
313
|
+
if (matches.length === 0) {
|
|
314
|
+
console.log(chalk_1.default.yellow(` "${trimmed}" に一致するコマンドはありません`));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
console.log();
|
|
318
|
+
console.log(chalk_1.default.cyan.bold(` 検索結果: "${trimmed}"`));
|
|
319
|
+
console.log(chalk_1.default.gray(` help <command> で各コマンドの詳細を表示`));
|
|
320
|
+
console.log();
|
|
321
|
+
for (const [name, entry] of matches) {
|
|
322
|
+
const sub = entry.subcommands != null ? chalk_1.default.cyan("+ ") : " ";
|
|
323
|
+
const alias = Object.entries(exports.COMMAND_ALIASES).find(([, v]) => v === name)?.[0];
|
|
324
|
+
const aliasSuffix = alias != null ? chalk_1.default.gray(` (${alias})`) : "";
|
|
325
|
+
console.log(chalk_1.default.white(` ${sub}${name.padEnd(14)}`) + chalk_1.default.gray(entry.description) + aliasSuffix);
|
|
326
|
+
}
|
|
327
|
+
console.log();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// 一覧モード
|
|
331
|
+
const currentValues = getCurrentSettingValues(ctx);
|
|
332
|
+
const displayed = new Set();
|
|
333
|
+
const categoryOrder = ["info", "status", "settings", "operation"];
|
|
334
|
+
const categories = filterCategory != null ? [filterCategory] : categoryOrder;
|
|
335
|
+
console.log();
|
|
336
|
+
console.log(chalk_1.default.cyan.bold(" 利用可能なコマンド:"));
|
|
337
|
+
console.log(chalk_1.default.gray(` help <command> で各コマンドの詳細を表示`));
|
|
338
|
+
for (const category of categories) {
|
|
339
|
+
console.log();
|
|
340
|
+
console.log(chalk_1.default.cyan(` [${types_2.CATEGORY_LABELS[category]}]`));
|
|
341
|
+
const commandNames = Object.keys(ctx.commands)
|
|
342
|
+
.filter((name) => name !== "exit" && name !== "?" && name !== "cmds" && ctx.commands[name].category === category)
|
|
343
|
+
.sort();
|
|
344
|
+
for (const name of commandNames) {
|
|
345
|
+
if (displayed.has(name))
|
|
346
|
+
continue;
|
|
347
|
+
displayed.add(name);
|
|
348
|
+
const entry = ctx.commands[name];
|
|
349
|
+
const sub = entry.subcommands != null ? chalk_1.default.cyan("+ ") : " ";
|
|
350
|
+
const setting = currentValues[name];
|
|
351
|
+
const valueSuffix = setting != null
|
|
352
|
+
? chalk_1.default.gray(" [") + chalk_1.default.yellow(setting.current) + chalk_1.default.gray("]")
|
|
353
|
+
: "";
|
|
354
|
+
const alias = Object.entries(exports.COMMAND_ALIASES).find(([, v]) => v === name)?.[0];
|
|
355
|
+
const aliasSuffix = alias != null ? chalk_1.default.gray(` (${alias})`) : "";
|
|
356
|
+
console.log(chalk_1.default.white(` ${sub}${name.padEnd(14)}`) + chalk_1.default.gray(entry.description) + aliasSuffix + valueSuffix);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
console.log();
|
|
360
|
+
console.log(chalk_1.default.cyan(" +") + chalk_1.default.gray(" = サブコマンドあり (help <command> で詳細表示)"));
|
|
361
|
+
console.log(chalk_1.default.gray(" カテゴリ絞り込み: commands <category> / 検索: commands <query>"));
|
|
362
|
+
console.log();
|
|
363
|
+
}
|
|
277
364
|
function handleHelp(ctx, args) {
|
|
278
365
|
const trimmed = args.trim();
|
|
279
366
|
if (trimmed.length > 0) {
|
|
@@ -284,13 +371,15 @@ function handleHelp(ctx, args) {
|
|
|
284
371
|
return;
|
|
285
372
|
}
|
|
286
373
|
if (parts.length > 1 && entry.subcommands) {
|
|
287
|
-
const
|
|
374
|
+
const subInput = parts[1].toLowerCase();
|
|
375
|
+
const subKey = Object.keys(entry.subcommands).find((k) => k.toLowerCase() === subInput);
|
|
376
|
+
const sub = subKey != null ? entry.subcommands[subKey] : undefined;
|
|
288
377
|
if (sub == null) {
|
|
289
378
|
console.log(chalk_1.default.yellow(` 不明なサブコマンド: ${parts[0]} ${parts[1]}`));
|
|
290
379
|
return;
|
|
291
380
|
}
|
|
292
381
|
console.log();
|
|
293
|
-
console.log(chalk_1.default.cyan.bold(` ${parts[0]} ${
|
|
382
|
+
console.log(chalk_1.default.cyan.bold(` ${parts[0]} ${subKey}`) + chalk_1.default.gray(` — ${sub.description}`));
|
|
294
383
|
if (sub.detail) {
|
|
295
384
|
console.log();
|
|
296
385
|
for (const line of sub.detail.split("\n")) {
|
|
@@ -321,45 +410,12 @@ function handleHelp(ctx, args) {
|
|
|
321
410
|
console.log();
|
|
322
411
|
return;
|
|
323
412
|
}
|
|
324
|
-
// help —
|
|
413
|
+
// help — 引数なしはガイド表示
|
|
325
414
|
console.log();
|
|
326
|
-
console.log(chalk_1.default.cyan.bold("
|
|
327
|
-
|
|
328
|
-
const displayed = new Set();
|
|
329
|
-
const categoryOrder = ["info", "status", "settings", "operation"];
|
|
330
|
-
for (const category of categoryOrder) {
|
|
331
|
-
console.log();
|
|
332
|
-
console.log(chalk_1.default.cyan(` [${types_2.CATEGORY_LABELS[category]}]`));
|
|
333
|
-
const commandNames = Object.keys(ctx.commands)
|
|
334
|
-
.filter((name) => name !== "exit" && name !== "?" && ctx.commands[name].category === category)
|
|
335
|
-
.sort();
|
|
336
|
-
for (const name of commandNames) {
|
|
337
|
-
const entry = ctx.commands[name];
|
|
338
|
-
if (displayed.has(entry.description))
|
|
339
|
-
continue;
|
|
340
|
-
displayed.add(entry.description);
|
|
341
|
-
const setting = currentValues[name];
|
|
342
|
-
const valueSuffix = setting != null
|
|
343
|
-
? chalk_1.default.gray(" [") + chalk_1.default.yellow(setting.current) + chalk_1.default.gray("]") +
|
|
344
|
-
(setting.options ? chalk_1.default.gray(` (${setting.options})`) : "")
|
|
345
|
-
: "";
|
|
346
|
-
const alias = Object.entries(exports.COMMAND_ALIASES)
|
|
347
|
-
.find(([, v]) => v === name)?.[0];
|
|
348
|
-
const aliasSuffix = alias != null ? chalk_1.default.gray(` (${alias})`) : "";
|
|
349
|
-
console.log(chalk_1.default.white(` ${name.padEnd(14)}`) + chalk_1.default.gray(entry.description) + aliasSuffix + valueSuffix);
|
|
350
|
-
if (entry.subcommands) {
|
|
351
|
-
const subNames = Object.keys(entry.subcommands).sort();
|
|
352
|
-
for (let i = 0; i < subNames.length; i++) {
|
|
353
|
-
const subName = subNames[i];
|
|
354
|
-
const sub = entry.subcommands[subName];
|
|
355
|
-
const prefix = i < subNames.length - 1 ? "├─" : "└─";
|
|
356
|
-
console.log(chalk_1.default.gray(` ${prefix} `) + chalk_1.default.white(subName.padEnd(10)) + chalk_1.default.gray(sub.description));
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
415
|
+
console.log(chalk_1.default.cyan.bold(" help <command>") + chalk_1.default.gray(" — コマンドの詳細を表示"));
|
|
416
|
+
console.log(chalk_1.default.cyan.bold(" commands") + chalk_1.default.gray(" — コマンド一覧を表示"));
|
|
361
417
|
console.log();
|
|
362
|
-
console.log(chalk_1.default.gray("
|
|
418
|
+
console.log(chalk_1.default.gray(" 例: help notify, help test table, commands settings"));
|
|
363
419
|
console.log();
|
|
364
420
|
}
|
|
365
421
|
function handleStats(ctx) {
|
|
@@ -408,31 +408,35 @@ function handleFocus(ctx, args) {
|
|
|
408
408
|
}
|
|
409
409
|
function handleClock(ctx, args) {
|
|
410
410
|
const trimmed = args.trim();
|
|
411
|
+
const labels = {
|
|
412
|
+
clock: "現在時刻",
|
|
413
|
+
elapsed: "経過時間",
|
|
414
|
+
uptime: "稼働時間",
|
|
415
|
+
};
|
|
411
416
|
if (trimmed.length === 0) {
|
|
412
417
|
const current = ctx.statusLine.getClockMode();
|
|
413
|
-
const next = current === "elapsed" ? "clock" : "elapsed";
|
|
418
|
+
const next = current === "elapsed" ? "clock" : current === "clock" ? "uptime" : "elapsed";
|
|
414
419
|
ctx.statusLine.setClockMode(next);
|
|
415
420
|
ctx.config.promptClock = next;
|
|
416
421
|
ctx.updateConfig((c) => { c.promptClock = next; });
|
|
417
|
-
|
|
418
|
-
console.log(` プロンプト時計を ${label} に切り替えました。`);
|
|
422
|
+
console.log(` プロンプト時計を ${labels[next]} に切り替えました。`);
|
|
419
423
|
return;
|
|
420
424
|
}
|
|
421
425
|
const clockLower = trimmed.toLowerCase();
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
ctx.statusLine.setClockMode(
|
|
430
|
-
ctx.config.promptClock =
|
|
431
|
-
ctx.updateConfig((c) => { c.promptClock =
|
|
432
|
-
console.log(
|
|
426
|
+
const modeMap = {
|
|
427
|
+
elapsed: "elapsed",
|
|
428
|
+
now: "clock",
|
|
429
|
+
uptime: "uptime",
|
|
430
|
+
};
|
|
431
|
+
const mode = modeMap[clockLower];
|
|
432
|
+
if (mode) {
|
|
433
|
+
ctx.statusLine.setClockMode(mode);
|
|
434
|
+
ctx.config.promptClock = mode;
|
|
435
|
+
ctx.updateConfig((c) => { c.promptClock = mode; });
|
|
436
|
+
console.log(` プロンプト時計を ${labels[mode]} に変更しました。`);
|
|
433
437
|
}
|
|
434
438
|
else {
|
|
435
|
-
console.log(chalk_1.default.yellow(" elapsed
|
|
439
|
+
console.log(chalk_1.default.yellow(" elapsed, now, uptime のいずれかを指定してください。"));
|
|
436
440
|
}
|
|
437
441
|
}
|
|
438
442
|
function handleTipInterval(ctx, args) {
|
|
@@ -1,8 +1,42 @@
|
|
|
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
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.formatStatsDuration = formatStatsDuration;
|
|
4
37
|
exports.displayStatistics = displayStatistics;
|
|
5
38
|
const formatter_1 = require("./formatter");
|
|
39
|
+
const theme = __importStar(require("./theme"));
|
|
6
40
|
// ── 定数 ──
|
|
7
41
|
const TYPE_LABELS = {
|
|
8
42
|
VXSE43: "緊急地震速報(警報)",
|
|
@@ -42,6 +76,14 @@ const CATEGORY_LABELS = {
|
|
|
42
76
|
nankaiTrough: "南海トラフ",
|
|
43
77
|
other: "その他",
|
|
44
78
|
};
|
|
79
|
+
const CATEGORY_ROLE = {
|
|
80
|
+
eew: "statsCategoryEew",
|
|
81
|
+
earthquake: "statsCategoryEarthquake",
|
|
82
|
+
tsunami: "statsCategoryTsunami",
|
|
83
|
+
volcano: "statsCategoryVolcano",
|
|
84
|
+
nankaiTrough: "statsCategoryNankaiTrough",
|
|
85
|
+
other: "statsCategoryOther",
|
|
86
|
+
};
|
|
45
87
|
const CATEGORY_ORDER = [
|
|
46
88
|
"eew",
|
|
47
89
|
"earthquake",
|
|
@@ -52,6 +94,13 @@ const CATEGORY_ORDER = [
|
|
|
52
94
|
];
|
|
53
95
|
const INTENSITY_ORDER = ["1", "2", "3", "4", "5-", "5+", "6-", "6+", "7"];
|
|
54
96
|
const FRAME_LEVEL = "info";
|
|
97
|
+
// ── chalk ショートカット ──
|
|
98
|
+
function muted(s) {
|
|
99
|
+
return theme.getRoleChalk("statsMuted")(s);
|
|
100
|
+
}
|
|
101
|
+
function count(s) {
|
|
102
|
+
return theme.getRoleChalk("statsCount")(s);
|
|
103
|
+
}
|
|
55
104
|
// ── 公開関数 ──
|
|
56
105
|
/** 経過時間をミリ秒から日本語の文字列に変換する */
|
|
57
106
|
function formatStatsDuration(ms) {
|
|
@@ -78,7 +127,7 @@ function displayStatistics(snapshot, now) {
|
|
|
78
127
|
const elapsedMs = effectiveNow.getTime() - snapshot.startTime.getTime();
|
|
79
128
|
if (snapshot.totalCount === 0) {
|
|
80
129
|
const title = "統計";
|
|
81
|
-
const msg = "まだ電文を受信していません";
|
|
130
|
+
const msg = muted("まだ電文を受信していません");
|
|
82
131
|
const width = calcWidth([title, msg]);
|
|
83
132
|
console.log((0, formatter_1.frameTop)(FRAME_LEVEL, width));
|
|
84
133
|
console.log((0, formatter_1.frameLine)(FRAME_LEVEL, title, width));
|
|
@@ -133,7 +182,7 @@ function calcWidth(contentLines) {
|
|
|
133
182
|
// frameLine adds 4 chars overhead (│ + space + space + │)
|
|
134
183
|
return Math.max(40, Math.min(200, maxContentWidth + 4));
|
|
135
184
|
}
|
|
136
|
-
/**
|
|
185
|
+
/** 最大震度内訳行を構築する(色付き) */
|
|
137
186
|
function buildIntBreakdownLine(earthquakeMaxIntByEvent) {
|
|
138
187
|
const intCounts = new Map();
|
|
139
188
|
for (const maxInt of earthquakeMaxIntByEvent.values()) {
|
|
@@ -141,12 +190,13 @@ function buildIntBreakdownLine(earthquakeMaxIntByEvent) {
|
|
|
141
190
|
}
|
|
142
191
|
const parts = [];
|
|
143
192
|
for (const intensity of INTENSITY_ORDER) {
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
|
|
193
|
+
const cnt = intCounts.get(intensity);
|
|
194
|
+
if (cnt != null && cnt > 0) {
|
|
195
|
+
const intStyle = (0, formatter_1.intensityColor)(intensity);
|
|
196
|
+
parts.push(`${intStyle(intensity)}${muted(":")}${cnt}`);
|
|
147
197
|
}
|
|
148
198
|
}
|
|
149
|
-
return ` 最大震度内訳 ${parts.join(" ")}`;
|
|
199
|
+
return ` ${muted("最大震度内訳")} ${parts.join(" ")}`;
|
|
150
200
|
}
|
|
151
201
|
/** 全コンテンツ行を構築する (__DIVIDER__ はフレーム区切り線のセンチネル) */
|
|
152
202
|
function buildAllContentLines(snapshot, activeCategories, typesByCategory, elapsedMs, countWidth) {
|
|
@@ -159,21 +209,21 @@ function buildAllContentLines(snapshot, activeCategories, typesByCategory, elaps
|
|
|
159
209
|
hour: "2-digit",
|
|
160
210
|
minute: "2-digit",
|
|
161
211
|
});
|
|
162
|
-
lines.push(
|
|
212
|
+
lines.push(`${muted("開始:")} ${startStr} ${muted("経過:")} ${formatStatsDuration(elapsedMs)} ${muted("合計:")} ${count(String(snapshot.totalCount))}件`);
|
|
163
213
|
// カテゴリセクション
|
|
164
|
-
for (
|
|
165
|
-
const category = activeCategories[i];
|
|
214
|
+
for (const category of activeCategories) {
|
|
166
215
|
lines.push("__DIVIDER__");
|
|
167
216
|
// カテゴリヘッダー
|
|
168
217
|
const types = typesByCategory.get(category) ?? [];
|
|
169
218
|
const categoryCount = types.reduce((sum, t) => sum + (snapshot.countByType.get(t) ?? 0), 0);
|
|
170
219
|
const catLabel = CATEGORY_LABELS[category];
|
|
220
|
+
const catStyle = theme.getRoleChalk(CATEGORY_ROLE[category]);
|
|
171
221
|
let catHeader;
|
|
172
222
|
if (category === "eew") {
|
|
173
|
-
catHeader = `[${catLabel}] ${categoryCount}件 / ${snapshot.eewEventCount}イベント`;
|
|
223
|
+
catHeader = `${catStyle(`[${catLabel}]`)} ${count(String(categoryCount))}件 / ${count(String(snapshot.eewEventCount))}イベント`;
|
|
174
224
|
}
|
|
175
225
|
else {
|
|
176
|
-
catHeader = `[${catLabel}] ${categoryCount}件`;
|
|
226
|
+
catHeader = `${catStyle(`[${catLabel}]`)} ${count(String(categoryCount))}件`;
|
|
177
227
|
}
|
|
178
228
|
lines.push(catHeader);
|
|
179
229
|
// タイプ行
|
|
@@ -190,14 +240,14 @@ function buildAllContentLines(snapshot, activeCategories, typesByCategory, elaps
|
|
|
190
240
|
maxLabelWidth = (0, formatter_1.visualWidth)(label);
|
|
191
241
|
}
|
|
192
242
|
for (const headType of types) {
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
243
|
+
const cnt = snapshot.countByType.get(headType) ?? 0;
|
|
244
|
+
if (cnt === 0)
|
|
195
245
|
continue;
|
|
196
246
|
const label = TYPE_LABELS[headType] ?? headType;
|
|
197
247
|
const typePad = " ".repeat(Math.max(0, maxTypeWidth - (0, formatter_1.visualWidth)(headType)));
|
|
198
248
|
const labelPad = " ".repeat(Math.max(0, maxLabelWidth - (0, formatter_1.visualWidth)(label)));
|
|
199
|
-
const countStr = String(
|
|
200
|
-
lines.push(` ${headType}${typePad} ${label}${labelPad} : ${countStr}`);
|
|
249
|
+
const countStr = String(cnt).padStart(countWidth);
|
|
250
|
+
lines.push(` ${muted(headType)}${typePad} ${label}${labelPad} ${muted(":")} ${count(countStr)}`);
|
|
201
251
|
}
|
|
202
252
|
// 地震カテゴリの場合は最大震度内訳を追加
|
|
203
253
|
if (category === "earthquake" && snapshot.earthquakeMaxIntByEvent.size > 0) {
|
package/dist/ui/status-line.js
CHANGED
|
@@ -45,6 +45,17 @@ class StatusLine {
|
|
|
45
45
|
*/
|
|
46
46
|
buildPrefix(options) {
|
|
47
47
|
const suffix = options?.noSuffix ? "" : chalk_1.default.gray("]> ");
|
|
48
|
+
// uptime モードは接続状態に依存しない
|
|
49
|
+
if (this.clockMode === "uptime") {
|
|
50
|
+
const dot = this.connectedAt == null
|
|
51
|
+
? chalk_1.default.gray("○")
|
|
52
|
+
: this.pulseOn ? chalk_1.default.cyan("●") : chalk_1.default.gray("○");
|
|
53
|
+
return (chalk_1.default.gray("FlEq [") +
|
|
54
|
+
dot +
|
|
55
|
+
chalk_1.default.gray(" ") +
|
|
56
|
+
(0, formatter_1.formatUptime)(process.uptime() * 1000) +
|
|
57
|
+
suffix);
|
|
58
|
+
}
|
|
48
59
|
if (this.connectedAt == null) {
|
|
49
60
|
return (chalk_1.default.gray("FlEq [") + chalk_1.default.gray("○ --:--:--") + suffix);
|
|
50
61
|
}
|
package/dist/ui/test-samples.js
CHANGED
|
@@ -116,6 +116,7 @@ exports.SAMPLE_EARTHQUAKE = {
|
|
|
116
116
|
reportDateTime: "2024/01/01 00:00:00",
|
|
117
117
|
headline: "1日00時00分ころ、地震がありました。",
|
|
118
118
|
publishingOffice: "気象庁",
|
|
119
|
+
eventId: "20240101000000",
|
|
119
120
|
earthquake: {
|
|
120
121
|
originTime: "2024/01/01 00:00:00",
|
|
121
122
|
hypocenterName: "石川県能登地方",
|
|
@@ -280,6 +281,7 @@ const FALLBACK_EARTHQUAKE_WARNING = {
|
|
|
280
281
|
reportDateTime: "2024/01/02 10:00:00",
|
|
281
282
|
headline: "長野県北部で震度4を観測しました。",
|
|
282
283
|
publishingOffice: "気象庁",
|
|
284
|
+
eventId: null,
|
|
283
285
|
earthquake: {
|
|
284
286
|
originTime: "2024/01/02 09:58:00",
|
|
285
287
|
hypocenterName: "長野県北部",
|
|
@@ -305,6 +307,7 @@ const FALLBACK_EARTHQUAKE_CANCEL = {
|
|
|
305
307
|
reportDateTime: "2024/01/02 10:05:00",
|
|
306
308
|
headline: "先ほどの地震情報を取り消します。",
|
|
307
309
|
publishingOffice: "気象庁",
|
|
310
|
+
eventId: null,
|
|
308
311
|
isTest: true,
|
|
309
312
|
};
|
|
310
313
|
const FALLBACK_EARTHQUAKE_ENCHI = {
|
|
@@ -314,6 +317,7 @@ const FALLBACK_EARTHQUAKE_ENCHI = {
|
|
|
314
317
|
reportDateTime: "2024/01/03 08:20:00",
|
|
315
318
|
headline: "日本への津波の影響はありません。",
|
|
316
319
|
publishingOffice: "気象庁",
|
|
320
|
+
eventId: null,
|
|
317
321
|
earthquake: {
|
|
318
322
|
originTime: "2024/01/03 08:10:00",
|
|
319
323
|
hypocenterName: "台湾付近",
|
|
@@ -332,6 +336,7 @@ const FALLBACK_EARTHQUAKE_SHINDO = {
|
|
|
332
336
|
reportDateTime: "2024/01/04 14:00:00",
|
|
333
337
|
headline: "各地の震度に関する情報です。",
|
|
334
338
|
publishingOffice: "気象庁",
|
|
339
|
+
eventId: null,
|
|
335
340
|
intensity: {
|
|
336
341
|
maxInt: "5弱",
|
|
337
342
|
areas: [
|
|
@@ -349,6 +354,7 @@ const FALLBACK_EARTHQUAKE_LG = {
|
|
|
349
354
|
reportDateTime: "2024/01/05 19:30:00",
|
|
350
355
|
headline: "関東地方で長周期地震動階級4を観測しました。",
|
|
351
356
|
publishingOffice: "気象庁",
|
|
357
|
+
eventId: null,
|
|
352
358
|
earthquake: {
|
|
353
359
|
originTime: "2024/01/05 19:27:00",
|
|
354
360
|
hypocenterName: "千葉県北西部",
|
package/dist/ui/theme.js
CHANGED
|
@@ -189,6 +189,15 @@ exports.DEFAULT_ROLES = {
|
|
|
189
189
|
// volcano: バナー
|
|
190
190
|
volcanoAlertBanner: { bg: "vermillion", fg: "#FFFFFF", bold: true },
|
|
191
191
|
volcanoFlashBanner: { bg: "darkRed", fg: "#FFFFFF", bold: true },
|
|
192
|
+
// stats: 統計表示
|
|
193
|
+
statsMuted: "gray",
|
|
194
|
+
statsCount: { fg: "sky", bold: true },
|
|
195
|
+
statsCategoryEew: { fg: "sky", bold: true },
|
|
196
|
+
statsCategoryEarthquake: { fg: "blue", bold: true },
|
|
197
|
+
statsCategoryTsunami: { fg: "blueGreen", bold: true },
|
|
198
|
+
statsCategoryVolcano: { fg: "orange", bold: true },
|
|
199
|
+
statsCategoryNankaiTrough: { fg: "vermillion", bold: true },
|
|
200
|
+
statsCategoryOther: { fg: "gray", bold: true },
|
|
192
201
|
};
|
|
193
202
|
/** ロール名の一覧 */
|
|
194
203
|
const ROLE_NAMES = Object.keys(exports.DEFAULT_ROLES);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EEW_TIP_CATEGORIES = void 0;
|
|
4
|
+
// ── 緊急地震速報の技術 ──────────────────────────────────────
|
|
5
|
+
// ── 世界の地震早期警報 ──────────────────────────────────────
|
|
6
|
+
exports.EEW_TIP_CATEGORIES = [
|
|
7
|
+
// ── 緊急地震速報の技術 ──
|
|
8
|
+
{
|
|
9
|
+
id: "eew-technology",
|
|
10
|
+
tips: [
|
|
11
|
+
// --- 基本原理 ---
|
|
12
|
+
"Tip: 緊急地震速報のシステムは、気象庁と防災科研の合計約1,700か所の地震計のデータをサーバで集約し、地震検知から数秒で震源・マグニチュード・予測震度を自動計算します。",
|
|
13
|
+
"Tip: EEWの処理は地震波の検知から最短約2秒で第1報を出すことを目指しています。P波のわずか数秒のデータから瞬時に震源を推定する超高速処理です。",
|
|
14
|
+
"Tip: EEWのマグニチュード推定はP波初期の最大振幅から行われ、続報では全相の最大振幅も使って更新されます。観測点が増えるほど推定精度が上がります。",
|
|
15
|
+
"Tip: 予測震度の計算には震源からの距離と地盤増幅率を考慮した距離減衰式が使われます。各地の地盤データは事前に組み込まれています。",
|
|
16
|
+
"Tip: EEWの猶予時間は「震源からの距離 ÷ S波速度 − 情報伝達の遅延」で決まります。震源から離れるほど猶予は長くなりますが、震源直上ではほぼゼロです。",
|
|
17
|
+
// --- IPF法とPLUM法 ---
|
|
18
|
+
"Tip: IPF法(Integrated Particle Filter)は複数の観測点のデータを統合して震源とマグニチュードを逐次更新する手法で、2016年からEEWに導入されています。",
|
|
19
|
+
"Tip: PLUM法(Propagation of Local Undamped Motion)は観測された実際の揺れの強さからその周辺の揺れを直接予測する手法で、震源推定のミスに強い利点があります。",
|
|
20
|
+
"Tip: 現在のEEWはIPF法とPLUM法のハイブリッド方式で、同一イベントについて両手法の予測震度を比較し大きい方を採用します。",
|
|
21
|
+
// --- 技術的課題 ---
|
|
22
|
+
"Tip: 同時に複数の地震が発生すると、異なる地震の波形が混ざり合いEEWの震源・マグニチュード推定が狂うことがあります。この「同時発生問題」は重要な技術課題です。",
|
|
23
|
+
"Tip: EEWの精度は震源からの距離と深さに影響されます。内陸浅発の直下型地震ではS波の到達が速く、速報が間に合わない構造的限界があります。",
|
|
24
|
+
"Tip: 地震の破壊伝播は断層面上を秒速約2〜3kmで広がります。M9級の巨大地震では断層の破壊に3分以上かかり、その間もEEWは更新を続けます。",
|
|
25
|
+
"Tip: 深発地震ではEEWの震源深さの仮定が外れて震度推定が大きくずれたり、異常震域で予想外の地域が揺れたりする課題があります。",
|
|
26
|
+
"Tip: EEWの取消報は主に雷・振動ノイズなどの誤検知時に発表されますが、観測点が疎な地域では実際の地震でも取消となる場合があります。",
|
|
27
|
+
// --- 精度と活用 ---
|
|
28
|
+
"Tip: EEWの予測震度には±1階級程度の誤差があり得ます。続報が出るたびに精度は上がりますが、初報は速さ重視のため振れ幅が大きくなります。",
|
|
29
|
+
"Tip: EEWは「予測」が外れても出すことに意味があります。たとえ精度が完璧でなくても、数秒の警告で身を守る行動を取れる可能性が生まれます。",
|
|
30
|
+
"Tip: EEW予報の第1報は速さ重視で不確実性が高いですが、第2報・第3報と続報が出るたびに観測データが増えて精度が向上していきます。",
|
|
31
|
+
"Tip: 「高度利用者向け」のEEW予報は気象業務支援センターを通じてリアルタイムに配信され、鉄道の自動制動やエレベーターの自動停止に活用されています。",
|
|
32
|
+
"Tip: 気象庁のEEWシステムには長周期地震動の予測機能が2023年2月から組み込まれ、高層ビル向けの早期警報に活用されています。",
|
|
33
|
+
"Tip: EEWの「予報」は1点の観測でも発表されますが、「警報」は2点以上で地震波が確認された場合に限られます。これは雷などの誤検知を防ぐためです。",
|
|
34
|
+
"Tip: EEWの警報は最大震度5弱以上または最大長周期地震動階級3以上が予想される場合に発表されます。発表対象地域は震度4以上または長周期3以上の地域を含みます。",
|
|
35
|
+
// --- 海底観測の効果 ---
|
|
36
|
+
"Tip: 海底観測網(S-net・DONET・N-net)のデータがEEWに統合されたことで、海溝型地震のP波をより早く捉えられるようになり、検知や警報発表が数秒〜十数秒以上早まる場合があります。",
|
|
37
|
+
// --- 歴史 ---
|
|
38
|
+
"Tip: 新幹線の地震検知システムUrEDASは1992年に実用化され、EEWの技術的先駆けと言えるシステムです。後に簡略化版のコンパクトUrEDASも開発されました。",
|
|
39
|
+
"Tip: 一般向けEEWの提供は2007年10月から始まりました。それ以前は高度利用者向けの限定的な配信のみでした。",
|
|
40
|
+
"Tip: 2007年の新潟県中越沖地震(M6.8)は一般向けEEW開始前の試験運用中に発生し、速報の有効性を実証するとともに改善点も浮き彫りにしました。",
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
// ── 世界の地震早期警報 ──
|
|
44
|
+
{
|
|
45
|
+
id: "eew-global",
|
|
46
|
+
tips: [
|
|
47
|
+
"Tip: メキシコのSASMEXは1991年に運用を開始した世界初の公共向け地震早期警報システムで、太平洋沿岸の地震からメキシコシティまでの距離を猶予時間に活かしています。",
|
|
48
|
+
"Tip: アメリカのShakeAlertは2019年にカリフォルニアで一般向け配信が始まり、2021年にオレゴン・ワシントン州にも拡大した地震早期警報システムです。",
|
|
49
|
+
"Tip: 台湾の交通部中央気象署はEEWシステムを運用しており、内陸の中規模地震では約10秒で解が得られ、PWS等を通じて携帯端末に配信されます。",
|
|
50
|
+
"Tip: 韓国気象庁は2015年からEEWの運用を開始し、2016年の慶州地震(M5.8)での課題を受けてシステムの高速化が進められました。",
|
|
51
|
+
"Tip: 中国地震局のEEWシステムは世界最大の人口をカバーする規模で整備が進んでおり、四川省成都の成都高新減災研究所(ICL)による民間EEWも有名です。",
|
|
52
|
+
"Tip: トルコでは2023年の大地震災害を機にEEWの研究・地域実装が加速しており、AFADを中心にイズミルなどで早期警報プロジェクトが進んでいます。",
|
|
53
|
+
"Tip: イタリアのINGVは地中海地域のEEW研究の中心で、PRESTo/ElarmSなど複数の手法を組み合わせたシステムの開発を行っています。",
|
|
54
|
+
"Tip: ルーマニアは中深発地震帯(ヴランチャ地震帯)からの距離を利用して首都ブカレストに数十秒の猶予時間を確保するEEWを運用しています。",
|
|
55
|
+
"Tip: 世界のEEWシステムは、日本の気象庁方式のような「地震パラメータ推定型」と、観測された揺れを直接伝える「ウェーブフロント型」に大別されます。",
|
|
56
|
+
"Tip: EEWの国際的な研究コミュニティでは、機械学習(深層学習)によるP波の自動検出やマグニチュード推定の高速化が活発に研究されています。",
|
|
57
|
+
"Tip: スマートフォンの加速度センサーをクラウドソーシングで利用するMyShake(カリフォルニア大学バークレー校)は、低コストで地震を検知する新しいアプローチです。",
|
|
58
|
+
"Tip: Googleは2020年からAndroid端末の加速度センサーで地震を検知し、世界各国にEEW的な警報を配信するAndroid Earthquake Alerts Systemを展開しています。",
|
|
59
|
+
"Tip: チリはM8超の巨大地震が頻発する地震大国で、CSN(国立地震センター)がEEWの研究・導入を進めています。",
|
|
60
|
+
"Tip: カナダのNRCanはブリティッシュコロンビア州で2024年にEEWの運用を開始し、ケベック・東オンタリオにも2025年に拡大しました。",
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
];
|