@sayue_ltr/fleq 1.50.0 → 1.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/README.md +47 -5
- package/dist/config.js +35 -2
- package/dist/dmdata/rest-client.js +58 -3
- package/dist/dmdata/telegram-parser.js +37 -59
- package/dist/dmdata/ws-client.js +49 -18
- package/dist/engine/cli/cli-run.js +71 -1
- package/dist/engine/cli/cli.js +12 -0
- package/dist/engine/filter/compile-filter.js +21 -0
- package/dist/engine/filter/compiler.js +188 -0
- package/dist/engine/filter/errors.js +41 -0
- package/dist/engine/filter/field-registry.js +78 -0
- package/dist/engine/filter/index.js +15 -0
- package/dist/engine/filter/parser.js +137 -0
- package/dist/engine/filter/rank-maps.js +34 -0
- package/dist/engine/filter/tokenizer.js +121 -0
- package/dist/engine/filter/type-checker.js +104 -0
- package/dist/engine/filter/types.js +2 -0
- package/dist/engine/filter-template/pipeline-controller.js +73 -0
- package/dist/engine/filter-template/pipeline.js +16 -0
- package/dist/engine/messages/display-callbacks.js +7 -0
- package/dist/engine/messages/message-router.js +114 -182
- package/dist/engine/messages/summary-tracker.js +106 -0
- package/dist/engine/messages/telegram-stats.js +103 -0
- package/dist/engine/messages/volcano-route-handler.js +122 -0
- package/dist/engine/monitor/monitor.js +51 -3
- package/dist/engine/monitor/shutdown.js +1 -0
- package/dist/engine/notification/notifier.js +16 -1
- package/dist/engine/notification/sound-player.js +193 -28
- package/dist/engine/presentation/diff-store.js +158 -0
- package/dist/engine/presentation/diff-types.js +2 -0
- package/dist/engine/presentation/events/from-earthquake.js +53 -0
- package/dist/engine/presentation/events/from-eew.js +72 -0
- package/dist/engine/presentation/events/from-lg-observation.js +58 -0
- package/dist/engine/presentation/events/from-nankai-trough.js +39 -0
- package/dist/engine/presentation/events/from-raw.js +35 -0
- package/dist/engine/presentation/events/from-seismic-text.js +37 -0
- package/dist/engine/presentation/events/from-tsunami.js +51 -0
- package/dist/engine/presentation/events/from-volcano.js +88 -0
- package/dist/engine/presentation/events/to-presentation-event.js +32 -0
- package/dist/engine/presentation/level-helpers.js +118 -0
- package/dist/engine/presentation/processors/process-earthquake.js +36 -0
- package/dist/engine/presentation/processors/process-eew.js +90 -0
- package/dist/engine/presentation/processors/process-lg-observation.js +30 -0
- package/dist/engine/presentation/processors/process-message.js +53 -0
- package/dist/engine/presentation/processors/process-nankai-trough.js +30 -0
- package/dist/engine/presentation/processors/process-raw.js +22 -0
- package/dist/engine/presentation/processors/process-seismic-text.js +30 -0
- package/dist/engine/presentation/processors/process-tsunami.js +42 -0
- package/dist/engine/presentation/processors/process-volcano.js +41 -0
- package/dist/engine/presentation/types.js +2 -0
- package/dist/engine/startup/config-resolver.js +2 -0
- package/dist/engine/template/compile-template.js +18 -0
- package/dist/engine/template/compiler.js +102 -0
- package/dist/engine/template/field-accessor.js +25 -0
- package/dist/engine/template/filters.js +94 -0
- package/dist/engine/template/index.js +5 -0
- package/dist/engine/template/parser.js +190 -0
- package/dist/engine/template/tokenizer.js +96 -0
- package/dist/engine/template/types.js +2 -0
- package/dist/types.js +2 -1
- package/dist/ui/display-adapter.js +60 -0
- package/dist/ui/earthquake-formatter.js +17 -5
- package/dist/ui/eew-formatter.js +25 -10
- package/dist/ui/formatter.js +67 -32
- package/dist/ui/minimap/grid-layout.js +91 -0
- package/dist/ui/minimap/index.js +16 -0
- package/dist/ui/minimap/minimap-renderer.js +277 -0
- package/dist/ui/minimap/pref-mapping.js +82 -0
- package/dist/ui/minimap/types.js +2 -0
- package/dist/ui/night-overlay.js +56 -0
- package/dist/ui/repl-handlers/command-definitions.js +320 -0
- package/dist/ui/repl-handlers/index.js +11 -0
- package/dist/ui/repl-handlers/info-handlers.js +577 -0
- package/dist/ui/repl-handlers/operation-handlers.js +233 -0
- package/dist/ui/repl-handlers/settings-handlers.js +923 -0
- package/dist/ui/repl-handlers/types.js +10 -0
- package/dist/ui/repl.js +81 -1752
- package/dist/ui/statistics-formatter.js +208 -0
- package/dist/ui/status-line.js +69 -0
- package/dist/ui/summary/index.js +5 -0
- package/dist/ui/summary/summary-line.js +18 -0
- package/dist/ui/summary/summary-model.js +31 -0
- package/dist/ui/summary/token-builders.js +317 -0
- package/dist/ui/summary/types.js +2 -0
- package/dist/ui/summary/width-fit.js +41 -0
- package/dist/ui/summary-interval-formatter.js +72 -0
- package/dist/ui/theme.js +34 -5
- package/dist/ui/tip-shuffler.js +81 -0
- package/dist/ui/volcano-formatter.js +15 -13
- package/dist/ui/waiting-tips.js +289 -249
- package/package.json +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatStatsDuration = formatStatsDuration;
|
|
4
|
+
exports.displayStatistics = displayStatistics;
|
|
5
|
+
const formatter_1 = require("./formatter");
|
|
6
|
+
// ── 定数 ──
|
|
7
|
+
const TYPE_LABELS = {
|
|
8
|
+
VXSE43: "緊急地震速報(警報)",
|
|
9
|
+
VXSE44: "緊急地震速報(予報)",
|
|
10
|
+
VXSE45: "緊急地震速報(地震動予報)",
|
|
11
|
+
VXSE51: "震度速報",
|
|
12
|
+
VXSE52: "震源に関する情報",
|
|
13
|
+
VXSE53: "震源・震度に関する情報",
|
|
14
|
+
VXSE56: "地震の活動状況等に関する情報",
|
|
15
|
+
VXSE60: "地震解説",
|
|
16
|
+
VXSE61: "顕著な地震の震度速報",
|
|
17
|
+
VXSE62: "長周期地震動に関する観測情報",
|
|
18
|
+
VZSE40: "地震回数に関する情報",
|
|
19
|
+
VTSE41: "津波警報・注意報・予報",
|
|
20
|
+
VTSE51: "津波情報",
|
|
21
|
+
VTSE52: "沖合の津波観測に関する情報",
|
|
22
|
+
VYSE50: "南海トラフ地震臨時情報",
|
|
23
|
+
VYSE51: "南海トラフ地震関連解説情報(臨時)",
|
|
24
|
+
VYSE52: "南海トラフ地震関連解説情報(定例)",
|
|
25
|
+
VYSE60: "南海トラフ地震関連解説情報(経過)",
|
|
26
|
+
VFVO50: "噴火警報・予報",
|
|
27
|
+
VFVO51: "火山の状況に関する解説情報",
|
|
28
|
+
VFVO52: "噴火に関する火山観測報",
|
|
29
|
+
VFVO53: "降灰予報(定時)",
|
|
30
|
+
VFVO54: "降灰予報(速報)",
|
|
31
|
+
VFVO55: "降灰予報(詳細)",
|
|
32
|
+
VFVO56: "噴火速報",
|
|
33
|
+
VFVO60: "推定噴煙流向報",
|
|
34
|
+
VFSVii: "火山現象に関する海上警報",
|
|
35
|
+
VZVO40: "火山に関するお知らせ",
|
|
36
|
+
};
|
|
37
|
+
const CATEGORY_LABELS = {
|
|
38
|
+
eew: "EEW",
|
|
39
|
+
earthquake: "地震",
|
|
40
|
+
tsunami: "津波",
|
|
41
|
+
volcano: "火山",
|
|
42
|
+
nankaiTrough: "南海トラフ",
|
|
43
|
+
other: "その他",
|
|
44
|
+
};
|
|
45
|
+
const CATEGORY_ORDER = [
|
|
46
|
+
"eew",
|
|
47
|
+
"earthquake",
|
|
48
|
+
"tsunami",
|
|
49
|
+
"volcano",
|
|
50
|
+
"nankaiTrough",
|
|
51
|
+
"other",
|
|
52
|
+
];
|
|
53
|
+
const INTENSITY_ORDER = ["1", "2", "3", "4", "5-", "5+", "6-", "6+", "7"];
|
|
54
|
+
const FRAME_LEVEL = "info";
|
|
55
|
+
// ── 公開関数 ──
|
|
56
|
+
/** 経過時間をミリ秒から日本語の文字列に変換する */
|
|
57
|
+
function formatStatsDuration(ms) {
|
|
58
|
+
const totalMinutes = Math.floor(ms / (60 * 1000));
|
|
59
|
+
const totalHours = Math.floor(totalMinutes / 60);
|
|
60
|
+
const totalDays = Math.floor(totalHours / 24);
|
|
61
|
+
if (totalDays >= 1) {
|
|
62
|
+
const remainHours = totalHours - totalDays * 24;
|
|
63
|
+
if (remainHours === 0)
|
|
64
|
+
return `${totalDays}日`;
|
|
65
|
+
return `${totalDays}日${remainHours}時間`;
|
|
66
|
+
}
|
|
67
|
+
if (totalHours >= 1) {
|
|
68
|
+
const remainMinutes = totalMinutes - totalHours * 60;
|
|
69
|
+
if (remainMinutes === 0)
|
|
70
|
+
return `${totalHours}時間`;
|
|
71
|
+
return `${totalHours}時間${remainMinutes}分`;
|
|
72
|
+
}
|
|
73
|
+
return `${totalMinutes}分`;
|
|
74
|
+
}
|
|
75
|
+
/** 電文受信統計をフレームボックス形式で標準出力に表示する */
|
|
76
|
+
function displayStatistics(snapshot, now) {
|
|
77
|
+
const effectiveNow = now ?? new Date();
|
|
78
|
+
const elapsedMs = effectiveNow.getTime() - snapshot.startTime.getTime();
|
|
79
|
+
if (snapshot.totalCount === 0) {
|
|
80
|
+
const title = "統計";
|
|
81
|
+
const msg = "まだ電文を受信していません";
|
|
82
|
+
const width = calcWidth([title, msg]);
|
|
83
|
+
console.log((0, formatter_1.frameTop)(FRAME_LEVEL, width));
|
|
84
|
+
console.log((0, formatter_1.frameLine)(FRAME_LEVEL, title, width));
|
|
85
|
+
console.log((0, formatter_1.frameLine)(FRAME_LEVEL, msg, width));
|
|
86
|
+
console.log((0, formatter_1.frameBottom)(FRAME_LEVEL, width));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// カテゴリ別に headType を分類
|
|
90
|
+
const typesByCategory = new Map();
|
|
91
|
+
for (const [headType, category] of snapshot.categoryByType) {
|
|
92
|
+
if (!typesByCategory.has(category)) {
|
|
93
|
+
typesByCategory.set(category, []);
|
|
94
|
+
}
|
|
95
|
+
typesByCategory.get(category).push(headType);
|
|
96
|
+
}
|
|
97
|
+
// 表示するカテゴリ(件数 > 0 のもの)
|
|
98
|
+
const activeCategories = CATEGORY_ORDER.filter((cat) => (typesByCategory.get(cat)?.some((t) => (snapshot.countByType.get(t) ?? 0) > 0)) ?? false);
|
|
99
|
+
// 最大カウント値を取得してカウント列の幅を決定
|
|
100
|
+
let maxCount = 0;
|
|
101
|
+
for (const count of snapshot.countByType.values()) {
|
|
102
|
+
if (count > maxCount)
|
|
103
|
+
maxCount = count;
|
|
104
|
+
}
|
|
105
|
+
const countWidth = Math.max(4, String(maxCount).length);
|
|
106
|
+
// 全コンテンツ行を収集してフレーム幅を動的計算
|
|
107
|
+
const allContentLines = buildAllContentLines(snapshot, activeCategories, typesByCategory, elapsedMs, countWidth);
|
|
108
|
+
const width = calcWidth(["統計", ...allContentLines]);
|
|
109
|
+
// 出力
|
|
110
|
+
console.log((0, formatter_1.frameTop)(FRAME_LEVEL, width));
|
|
111
|
+
console.log((0, formatter_1.frameLine)(FRAME_LEVEL, "統計", width));
|
|
112
|
+
for (const line of allContentLines) {
|
|
113
|
+
if (line === "__DIVIDER__") {
|
|
114
|
+
console.log((0, formatter_1.frameDivider)(FRAME_LEVEL, width));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.log((0, formatter_1.frameLine)(FRAME_LEVEL, line, width));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
console.log((0, formatter_1.frameBottom)(FRAME_LEVEL, width));
|
|
121
|
+
}
|
|
122
|
+
// ── 内部ヘルパー ──
|
|
123
|
+
/** フレーム幅をコンテンツ行の最大幅から計算する (最小40、最大200) */
|
|
124
|
+
function calcWidth(contentLines) {
|
|
125
|
+
let maxContentWidth = 0;
|
|
126
|
+
for (const line of contentLines) {
|
|
127
|
+
if (line === "__DIVIDER__")
|
|
128
|
+
continue;
|
|
129
|
+
const w = (0, formatter_1.visualWidth)(line);
|
|
130
|
+
if (w > maxContentWidth)
|
|
131
|
+
maxContentWidth = w;
|
|
132
|
+
}
|
|
133
|
+
// frameLine adds 4 chars overhead (│ + space + space + │)
|
|
134
|
+
return Math.max(40, Math.min(200, maxContentWidth + 4));
|
|
135
|
+
}
|
|
136
|
+
/** 最大震度内訳行を構築する */
|
|
137
|
+
function buildIntBreakdownLine(earthquakeMaxIntByEvent) {
|
|
138
|
+
const intCounts = new Map();
|
|
139
|
+
for (const maxInt of earthquakeMaxIntByEvent.values()) {
|
|
140
|
+
intCounts.set(maxInt, (intCounts.get(maxInt) ?? 0) + 1);
|
|
141
|
+
}
|
|
142
|
+
const parts = [];
|
|
143
|
+
for (const intensity of INTENSITY_ORDER) {
|
|
144
|
+
const count = intCounts.get(intensity);
|
|
145
|
+
if (count != null && count > 0) {
|
|
146
|
+
parts.push(`${intensity}:${count}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return ` 最大震度内訳 ${parts.join(" ")}`;
|
|
150
|
+
}
|
|
151
|
+
/** 全コンテンツ行を構築する (__DIVIDER__ はフレーム区切り線のセンチネル) */
|
|
152
|
+
function buildAllContentLines(snapshot, activeCategories, typesByCategory, elapsedMs, countWidth) {
|
|
153
|
+
const lines = [];
|
|
154
|
+
// ヘッダー行
|
|
155
|
+
const startStr = snapshot.startTime.toLocaleString("ja-JP", {
|
|
156
|
+
year: "numeric",
|
|
157
|
+
month: "2-digit",
|
|
158
|
+
day: "2-digit",
|
|
159
|
+
hour: "2-digit",
|
|
160
|
+
minute: "2-digit",
|
|
161
|
+
});
|
|
162
|
+
lines.push(`開始: ${startStr} 経過: ${formatStatsDuration(elapsedMs)} 合計: ${snapshot.totalCount}件`);
|
|
163
|
+
// カテゴリセクション
|
|
164
|
+
for (let i = 0; i < activeCategories.length; i++) {
|
|
165
|
+
const category = activeCategories[i];
|
|
166
|
+
lines.push("__DIVIDER__");
|
|
167
|
+
// カテゴリヘッダー
|
|
168
|
+
const types = typesByCategory.get(category) ?? [];
|
|
169
|
+
const categoryCount = types.reduce((sum, t) => sum + (snapshot.countByType.get(t) ?? 0), 0);
|
|
170
|
+
const catLabel = CATEGORY_LABELS[category];
|
|
171
|
+
let catHeader;
|
|
172
|
+
if (category === "eew") {
|
|
173
|
+
catHeader = `[${catLabel}] ${categoryCount}件 / ${snapshot.eewEventCount}イベント`;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
catHeader = `[${catLabel}] ${categoryCount}件`;
|
|
177
|
+
}
|
|
178
|
+
lines.push(catHeader);
|
|
179
|
+
// タイプ行
|
|
180
|
+
// タイプ列の幅を揃えるため最長タイプ名を求める
|
|
181
|
+
let maxTypeWidth = 0;
|
|
182
|
+
for (const headType of types) {
|
|
183
|
+
if ((0, formatter_1.visualWidth)(headType) > maxTypeWidth)
|
|
184
|
+
maxTypeWidth = (0, formatter_1.visualWidth)(headType);
|
|
185
|
+
}
|
|
186
|
+
let maxLabelWidth = 0;
|
|
187
|
+
for (const headType of types) {
|
|
188
|
+
const label = TYPE_LABELS[headType] ?? headType;
|
|
189
|
+
if ((0, formatter_1.visualWidth)(label) > maxLabelWidth)
|
|
190
|
+
maxLabelWidth = (0, formatter_1.visualWidth)(label);
|
|
191
|
+
}
|
|
192
|
+
for (const headType of types) {
|
|
193
|
+
const count = snapshot.countByType.get(headType) ?? 0;
|
|
194
|
+
if (count === 0)
|
|
195
|
+
continue;
|
|
196
|
+
const label = TYPE_LABELS[headType] ?? headType;
|
|
197
|
+
const typePad = " ".repeat(Math.max(0, maxTypeWidth - (0, formatter_1.visualWidth)(headType)));
|
|
198
|
+
const labelPad = " ".repeat(Math.max(0, maxLabelWidth - (0, formatter_1.visualWidth)(label)));
|
|
199
|
+
const countStr = String(count).padStart(countWidth);
|
|
200
|
+
lines.push(` ${headType}${typePad} ${label}${labelPad} : ${countStr}`);
|
|
201
|
+
}
|
|
202
|
+
// 地震カテゴリの場合は最大震度内訳を追加
|
|
203
|
+
if (category === "earthquake" && snapshot.earthquakeMaxIntByEvent.size > 0) {
|
|
204
|
+
lines.push(buildIntBreakdownLine(snapshot.earthquakeMaxIntByEvent));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return lines;
|
|
208
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.StatusLine = void 0;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const formatter_1 = require("../ui/formatter");
|
|
9
|
+
/** 現在時刻を HH:mm:ss 形式で返す */
|
|
10
|
+
function formatCurrentTime() {
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
13
|
+
return `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
14
|
+
}
|
|
15
|
+
class StatusLine {
|
|
16
|
+
pulseOn = true;
|
|
17
|
+
connectedAt = null;
|
|
18
|
+
lastMessageTime = null;
|
|
19
|
+
clockMode = "elapsed";
|
|
20
|
+
tick() {
|
|
21
|
+
this.pulseOn = !this.pulseOn;
|
|
22
|
+
}
|
|
23
|
+
setConnected(connected) {
|
|
24
|
+
if (connected) {
|
|
25
|
+
this.connectedAt = Date.now();
|
|
26
|
+
this.lastMessageTime = null;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
this.connectedAt = null;
|
|
30
|
+
this.lastMessageTime = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
markMessageReceived() {
|
|
34
|
+
this.lastMessageTime = Date.now();
|
|
35
|
+
}
|
|
36
|
+
setClockMode(mode) {
|
|
37
|
+
this.clockMode = mode;
|
|
38
|
+
}
|
|
39
|
+
getClockMode() {
|
|
40
|
+
return this.clockMode;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* プロンプト用プレフィックスを生成する。
|
|
44
|
+
* @param options.noSuffix true のときは末尾の "]> " を付与しない
|
|
45
|
+
*/
|
|
46
|
+
buildPrefix(options) {
|
|
47
|
+
const suffix = options?.noSuffix ? "" : chalk_1.default.gray("]> ");
|
|
48
|
+
if (this.connectedAt == null) {
|
|
49
|
+
return (chalk_1.default.gray("FlEq [") + chalk_1.default.gray("○ --:--:--") + suffix);
|
|
50
|
+
}
|
|
51
|
+
const dot = this.pulseOn ? chalk_1.default.cyan("●") : chalk_1.default.gray("○");
|
|
52
|
+
const timeStr = this.clockMode === "clock"
|
|
53
|
+
? formatCurrentTime()
|
|
54
|
+
: (0, formatter_1.formatElapsedTime)(Date.now() - (this.lastMessageTime ?? this.connectedAt));
|
|
55
|
+
return (chalk_1.default.gray("FlEq [") +
|
|
56
|
+
dot +
|
|
57
|
+
chalk_1.default.gray(" ") +
|
|
58
|
+
chalk_1.default.white(timeStr) +
|
|
59
|
+
suffix);
|
|
60
|
+
}
|
|
61
|
+
getLastMessageTime() {
|
|
62
|
+
return this.lastMessageTime;
|
|
63
|
+
}
|
|
64
|
+
/** プロンプト経過時間の基準時刻を返す (lastMessageTime ?? connectedAt) */
|
|
65
|
+
getElapsedBase() {
|
|
66
|
+
return this.lastMessageTime ?? this.connectedAt;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.StatusLine = StatusLine;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderSummaryLine = void 0;
|
|
4
|
+
var summary_line_1 = require("./summary-line");
|
|
5
|
+
Object.defineProperty(exports, "renderSummaryLine", { enumerable: true, get: function () { return summary_line_1.renderSummaryLine; } });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderSummaryLine = renderSummaryLine;
|
|
4
|
+
const summary_model_1 = require("./summary-model");
|
|
5
|
+
const token_builders_1 = require("./token-builders");
|
|
6
|
+
const width_fit_1 = require("./width-fit");
|
|
7
|
+
/**
|
|
8
|
+
* PresentationEvent → 幅適応1行文字列を生成する。
|
|
9
|
+
* @param event PresentationEvent
|
|
10
|
+
* @param maxWidth 最大幅(デフォルト: ターミナル幅)
|
|
11
|
+
* @returns 着色済み1行文字列
|
|
12
|
+
*/
|
|
13
|
+
function renderSummaryLine(event, maxWidth) {
|
|
14
|
+
const width = maxWidth ?? (process.stdout.columns || 80);
|
|
15
|
+
const model = (0, summary_model_1.buildSummaryModel)(event);
|
|
16
|
+
const tokens = (0, token_builders_1.buildSummaryTokens)(event, model);
|
|
17
|
+
return (0, width_fit_1.fitTokensToWidth)(tokens, width);
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSummaryModel = buildSummaryModel;
|
|
4
|
+
const SEVERITY_MAP = {
|
|
5
|
+
critical: "[緊急]",
|
|
6
|
+
warning: "[警告]",
|
|
7
|
+
normal: "[情報]",
|
|
8
|
+
info: "[通知]",
|
|
9
|
+
cancel: "[取消]",
|
|
10
|
+
};
|
|
11
|
+
function buildSummaryModel(event) {
|
|
12
|
+
const severity = SEVERITY_MAP[event.frameLevel];
|
|
13
|
+
const areaNames = event.areaNames.length > 0
|
|
14
|
+
? event.areaNames
|
|
15
|
+
: event.forecastAreaNames.length > 0
|
|
16
|
+
? event.forecastAreaNames
|
|
17
|
+
: undefined;
|
|
18
|
+
return {
|
|
19
|
+
domain: event.domain,
|
|
20
|
+
severity,
|
|
21
|
+
title: event.title,
|
|
22
|
+
location: event.hypocenterName ?? event.volcanoName ?? undefined,
|
|
23
|
+
magnitude: event.magnitude ? `M${event.magnitude}` : undefined,
|
|
24
|
+
maxInt: event.maxInt ? `震度${event.maxInt}` : undefined,
|
|
25
|
+
maxLgInt: event.maxLgInt ? `長周期${event.maxLgInt}` : undefined,
|
|
26
|
+
headline: event.headline ?? undefined,
|
|
27
|
+
volcanoName: event.volcanoName ?? undefined,
|
|
28
|
+
serial: event.serial ? `#${event.serial}` : undefined,
|
|
29
|
+
areaNames,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSummaryTokens = buildSummaryTokens;
|
|
4
|
+
const formatter_1 = require("../formatter");
|
|
5
|
+
// ── Helper ──
|
|
6
|
+
function token(id, text, priority, dropMode, shortText) {
|
|
7
|
+
const minW = shortText != null ? (0, formatter_1.visualWidth)(shortText) : (0, formatter_1.visualWidth)(text);
|
|
8
|
+
const prefW = (0, formatter_1.visualWidth)(text);
|
|
9
|
+
return { id, text, shortText, priority, minWidth: minW, preferredWidth: prefW, dropMode };
|
|
10
|
+
}
|
|
11
|
+
/** 地方・県名の末尾パターンを除去する簡易短縮 */
|
|
12
|
+
function shortenHypocenter(name) {
|
|
13
|
+
return name
|
|
14
|
+
.replace(/地方$/, "")
|
|
15
|
+
.replace(/^.+県/, "");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* areaNames を先頭 n 件で結合し、残りがあれば「ほかN」の shortText を返す。
|
|
19
|
+
*/
|
|
20
|
+
function topAreaTokenParts(names, limit) {
|
|
21
|
+
if (names.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
const top = names.slice(0, limit);
|
|
24
|
+
const text = top.join(",");
|
|
25
|
+
if (names.length > limit) {
|
|
26
|
+
const short = `${top[0]}ほか${names.length - 1}`;
|
|
27
|
+
return { text, shortText: short };
|
|
28
|
+
}
|
|
29
|
+
return { text };
|
|
30
|
+
}
|
|
31
|
+
// ── Domain builders ──
|
|
32
|
+
function buildEewTokens(event, model) {
|
|
33
|
+
const tokens = [];
|
|
34
|
+
tokens.push(token("severity", model.severity, 0, "never"));
|
|
35
|
+
// kind
|
|
36
|
+
if (event.isCancellation) {
|
|
37
|
+
tokens.push(token("kind", "EEW取消", 0, "never"));
|
|
38
|
+
}
|
|
39
|
+
else if (event.isWarning) {
|
|
40
|
+
tokens.push(token("kind", "EEW警報", 0, "never"));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
tokens.push(token("kind", "EEW予報", 0, "never"));
|
|
44
|
+
}
|
|
45
|
+
// serial
|
|
46
|
+
if (model.serial) {
|
|
47
|
+
tokens.push(token("serial", model.serial, 1, "drop"));
|
|
48
|
+
}
|
|
49
|
+
// hypocenter
|
|
50
|
+
if (event.hypocenterName) {
|
|
51
|
+
const short = shortenHypocenter(event.hypocenterName);
|
|
52
|
+
tokens.push(token("hypocenter", event.hypocenterName, 1, "shorten", short !== event.hypocenterName ? short : undefined));
|
|
53
|
+
}
|
|
54
|
+
// maxInt
|
|
55
|
+
const eewMaxInt = event.forecastMaxInt ? `震度${event.forecastMaxInt}` : (model.maxInt ?? "震度-");
|
|
56
|
+
tokens.push(token("maxInt", eewMaxInt, 0, "never"));
|
|
57
|
+
// maxLgInt
|
|
58
|
+
if (model.maxLgInt) {
|
|
59
|
+
tokens.push(token("maxLgInt", model.maxLgInt, 2, "drop"));
|
|
60
|
+
}
|
|
61
|
+
// magnitude
|
|
62
|
+
if (model.magnitude) {
|
|
63
|
+
tokens.push(token("magnitude", model.magnitude, 2, "shorten", model.magnitude));
|
|
64
|
+
}
|
|
65
|
+
// depth
|
|
66
|
+
if (event.depth) {
|
|
67
|
+
tokens.push(token("depth", `深さ${event.depth}`, 3, "drop"));
|
|
68
|
+
}
|
|
69
|
+
// forecastAreaTop
|
|
70
|
+
if (event.forecastAreaNames.length > 0) {
|
|
71
|
+
const parts = topAreaTokenParts(event.forecastAreaNames, 3);
|
|
72
|
+
if (parts) {
|
|
73
|
+
tokens.push(token("forecastAreaTop", parts.text, 3, "drop"));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return tokens;
|
|
77
|
+
}
|
|
78
|
+
function buildEarthquakeTokens(event, model) {
|
|
79
|
+
const tokens = [];
|
|
80
|
+
const headType = event.type;
|
|
81
|
+
tokens.push(token("severity", model.severity, 0, "never"));
|
|
82
|
+
if (headType === "VXSE51") {
|
|
83
|
+
// 震度速報
|
|
84
|
+
tokens.push(token("type", "震度速報", 0, "never"));
|
|
85
|
+
if (model.maxInt)
|
|
86
|
+
tokens.push(token("maxInt", model.maxInt, 0, "never"));
|
|
87
|
+
const parts = topAreaTokenParts(event.areaNames, 2);
|
|
88
|
+
if (parts)
|
|
89
|
+
tokens.push(token("topAreas", parts.text, 1, "shorten", parts.shortText));
|
|
90
|
+
if (event.headline && event.headline.includes("津波")) {
|
|
91
|
+
tokens.push(token("tsunami", event.headline, 2, "drop"));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (headType === "VXSE52") {
|
|
95
|
+
// 震源情報
|
|
96
|
+
tokens.push(token("type", "震源情報", 0, "never"));
|
|
97
|
+
if (event.hypocenterName) {
|
|
98
|
+
const short = shortenHypocenter(event.hypocenterName);
|
|
99
|
+
tokens.push(token("hypocenter", event.hypocenterName, 1, "shorten", short !== event.hypocenterName ? short : undefined));
|
|
100
|
+
}
|
|
101
|
+
if (model.magnitude)
|
|
102
|
+
tokens.push(token("magnitude", model.magnitude, 1, "shorten", model.magnitude));
|
|
103
|
+
if (event.depth)
|
|
104
|
+
tokens.push(token("depth", `深さ${event.depth}`, 2, "drop"));
|
|
105
|
+
if (event.headline && event.headline.includes("津波")) {
|
|
106
|
+
tokens.push(token("tsunami", event.headline, 2, "drop"));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (headType === "VXSE53") {
|
|
110
|
+
// 震源・震度情報
|
|
111
|
+
tokens.push(token("type", "震源・震度情報", 0, "shorten", "震源震度"));
|
|
112
|
+
if (event.hypocenterName) {
|
|
113
|
+
const short = shortenHypocenter(event.hypocenterName);
|
|
114
|
+
tokens.push(token("hypocenter", event.hypocenterName, 1, "shorten", short !== event.hypocenterName ? short : undefined));
|
|
115
|
+
}
|
|
116
|
+
if (model.magnitude)
|
|
117
|
+
tokens.push(token("magnitude", model.magnitude, 1, "shorten", model.magnitude));
|
|
118
|
+
if (model.maxInt)
|
|
119
|
+
tokens.push(token("maxInt", model.maxInt, 0, "never"));
|
|
120
|
+
if (model.maxLgInt)
|
|
121
|
+
tokens.push(token("maxLgInt", model.maxLgInt, 2, "drop"));
|
|
122
|
+
if (event.headline && event.headline.includes("津波")) {
|
|
123
|
+
tokens.push(token("tsunami", event.headline, 2, "drop"));
|
|
124
|
+
}
|
|
125
|
+
const parts = topAreaTokenParts(event.areaNames, 2);
|
|
126
|
+
if (parts)
|
|
127
|
+
tokens.push(token("topAreas", parts.text, 2, "drop"));
|
|
128
|
+
}
|
|
129
|
+
else if (headType === "VXSE61") {
|
|
130
|
+
// 遠地地震
|
|
131
|
+
tokens.push(token("type", "遠地地震情報", 0, "shorten", "遠地地震"));
|
|
132
|
+
if (event.hypocenterName) {
|
|
133
|
+
const short = shortenHypocenter(event.hypocenterName);
|
|
134
|
+
tokens.push(token("hypocenter", event.hypocenterName, 1, "shorten", short !== event.hypocenterName ? short : undefined));
|
|
135
|
+
}
|
|
136
|
+
if (model.magnitude)
|
|
137
|
+
tokens.push(token("magnitude", model.magnitude, 1, "shorten", model.magnitude));
|
|
138
|
+
if (model.maxInt)
|
|
139
|
+
tokens.push(token("maxInt", model.maxInt, 0, "never"));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// その他の地震電文
|
|
143
|
+
tokens.push(token("type", event.title, 0, "shorten"));
|
|
144
|
+
if (event.hypocenterName) {
|
|
145
|
+
const short = shortenHypocenter(event.hypocenterName);
|
|
146
|
+
tokens.push(token("hypocenter", event.hypocenterName, 1, "shorten", short !== event.hypocenterName ? short : undefined));
|
|
147
|
+
}
|
|
148
|
+
if (model.maxInt)
|
|
149
|
+
tokens.push(token("maxInt", model.maxInt, 0, "never"));
|
|
150
|
+
}
|
|
151
|
+
return tokens;
|
|
152
|
+
}
|
|
153
|
+
function buildTsunamiTokens(event, model) {
|
|
154
|
+
const tokens = [];
|
|
155
|
+
tokens.push(token("severity", model.severity, 0, "never"));
|
|
156
|
+
// bannerKind: headline から抽出、なければ title
|
|
157
|
+
const bannerKind = event.headline ?? event.title;
|
|
158
|
+
tokens.push(token("bannerKind", bannerKind, 0, "never"));
|
|
159
|
+
// topAreas
|
|
160
|
+
const parts = topAreaTokenParts(event.forecastAreaNames, 2);
|
|
161
|
+
if (parts)
|
|
162
|
+
tokens.push(token("topAreas", parts.text, 1, "shorten", parts.shortText));
|
|
163
|
+
// areaCount
|
|
164
|
+
if (event.forecastAreaCount > 0) {
|
|
165
|
+
tokens.push(token("areaCount", `(${event.forecastAreaCount}地域)`, 1, "drop"));
|
|
166
|
+
}
|
|
167
|
+
// hypocenter
|
|
168
|
+
if (event.hypocenterName) {
|
|
169
|
+
tokens.push(token("hypocenter", event.hypocenterName, 3, "drop"));
|
|
170
|
+
}
|
|
171
|
+
// magnitude
|
|
172
|
+
if (model.magnitude) {
|
|
173
|
+
tokens.push(token("magnitude", model.magnitude, 3, "drop"));
|
|
174
|
+
}
|
|
175
|
+
return tokens;
|
|
176
|
+
}
|
|
177
|
+
function buildVolcanoTokens(event, model) {
|
|
178
|
+
const tokens = [];
|
|
179
|
+
const headType = event.type;
|
|
180
|
+
tokens.push(token("severity", model.severity, 0, "never"));
|
|
181
|
+
if (headType === "VFVO50" || headType.startsWith("VFSV")) {
|
|
182
|
+
// 火山警報
|
|
183
|
+
tokens.push(token("type", event.title, 0, "shorten"));
|
|
184
|
+
if (event.volcanoName)
|
|
185
|
+
tokens.push(token("volcanoName", event.volcanoName, 0, "never"));
|
|
186
|
+
if (event.alertLevel != null) {
|
|
187
|
+
tokens.push(token("alertLevel", `Lv${event.alertLevel}`, 0, "shorten"));
|
|
188
|
+
}
|
|
189
|
+
if (event.areaCount > 0) {
|
|
190
|
+
tokens.push(token("areaCount", `対象${event.areaCount}市町村`, 2, "drop"));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (headType === "VFVO52" || headType === "VFVO56") {
|
|
194
|
+
// 噴火速報 / 噴火情報
|
|
195
|
+
tokens.push(token("type", event.title, 0, "never"));
|
|
196
|
+
if (event.volcanoName)
|
|
197
|
+
tokens.push(token("volcanoName", event.volcanoName, 0, "never"));
|
|
198
|
+
// phenomenon/plumeHeight: try to extract from raw if available
|
|
199
|
+
// Phase 3 - use available info only
|
|
200
|
+
}
|
|
201
|
+
else if (headType === "VFVO53" || headType === "VFVO54" || headType === "VFVO55") {
|
|
202
|
+
// 降灰
|
|
203
|
+
tokens.push(token("type", event.title, 0, "shorten"));
|
|
204
|
+
if (event.volcanoName)
|
|
205
|
+
tokens.push(token("volcanoName", event.volcanoName, 0, "never"));
|
|
206
|
+
if (event.areaCount > 0) {
|
|
207
|
+
tokens.push(token("areaCount", `対象${event.areaCount}地域`, 1, "drop"));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else if (headType === "VFVO51" || headType === "VZVO40") {
|
|
211
|
+
// 火山テキスト
|
|
212
|
+
tokens.push(token("type", event.title, 0, "shorten"));
|
|
213
|
+
if (event.volcanoName)
|
|
214
|
+
tokens.push(token("volcanoName", event.volcanoName, 0, "never"));
|
|
215
|
+
if (event.headline) {
|
|
216
|
+
tokens.push(token("headline", event.headline, 1, "shorten"));
|
|
217
|
+
}
|
|
218
|
+
if (event.alertLevel != null) {
|
|
219
|
+
tokens.push(token("alertLevel", `Lv${event.alertLevel}`, 2, "drop"));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (headType === "VFVO60") {
|
|
223
|
+
// 噴煙流向
|
|
224
|
+
tokens.push(token("type", event.title, 0, "shorten"));
|
|
225
|
+
if (event.volcanoName)
|
|
226
|
+
tokens.push(token("volcanoName", event.volcanoName, 0, "never"));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// fallback
|
|
230
|
+
tokens.push(token("type", event.title, 0, "shorten"));
|
|
231
|
+
if (event.volcanoName)
|
|
232
|
+
tokens.push(token("volcanoName", event.volcanoName, 0, "never"));
|
|
233
|
+
}
|
|
234
|
+
return tokens;
|
|
235
|
+
}
|
|
236
|
+
function buildSeismicTextTokens(event, model) {
|
|
237
|
+
const tokens = [];
|
|
238
|
+
tokens.push(token("severity", model.severity, 0, "never"));
|
|
239
|
+
tokens.push(token("type", event.title, 0, "shorten"));
|
|
240
|
+
if (event.headline) {
|
|
241
|
+
tokens.push(token("headline", event.headline, 1, "shorten"));
|
|
242
|
+
}
|
|
243
|
+
return tokens;
|
|
244
|
+
}
|
|
245
|
+
function buildLgObservationTokens(event, model) {
|
|
246
|
+
const tokens = [];
|
|
247
|
+
tokens.push(token("severity", model.severity, 0, "never"));
|
|
248
|
+
tokens.push(token("type", "長周期地震動観測情報", 0, "shorten", "長周期観測"));
|
|
249
|
+
if (event.hypocenterName) {
|
|
250
|
+
const short = shortenHypocenter(event.hypocenterName);
|
|
251
|
+
tokens.push(token("hypocenter", event.hypocenterName, 1, "shorten", short !== event.hypocenterName ? short : undefined));
|
|
252
|
+
}
|
|
253
|
+
if (model.maxLgInt) {
|
|
254
|
+
// "長周期4" → shortText "L4"
|
|
255
|
+
const lgNum = model.maxLgInt.replace("長周期", "");
|
|
256
|
+
tokens.push(token("maxLgInt", model.maxLgInt, 0, "shorten", `L${lgNum}`));
|
|
257
|
+
}
|
|
258
|
+
if (model.maxInt) {
|
|
259
|
+
tokens.push(token("maxInt", model.maxInt, 1, "shorten"));
|
|
260
|
+
}
|
|
261
|
+
const parts = topAreaTokenParts(event.observationNames, 2);
|
|
262
|
+
if (parts)
|
|
263
|
+
tokens.push(token("topAreas", parts.text, 2, "drop"));
|
|
264
|
+
if (model.magnitude) {
|
|
265
|
+
tokens.push(token("magnitude", model.magnitude, 2, "drop"));
|
|
266
|
+
}
|
|
267
|
+
if (event.depth) {
|
|
268
|
+
tokens.push(token("depth", `深さ${event.depth}`, 3, "drop"));
|
|
269
|
+
}
|
|
270
|
+
return tokens;
|
|
271
|
+
}
|
|
272
|
+
function buildNankaiTroughTokens(event, model) {
|
|
273
|
+
const tokens = [];
|
|
274
|
+
tokens.push(token("severity", model.severity, 0, "never"));
|
|
275
|
+
tokens.push(token("type", "南海トラフ臨時情報", 0, "shorten", "南海トラフ"));
|
|
276
|
+
if (event.headline) {
|
|
277
|
+
tokens.push(token("headline", event.headline, 1, "shorten"));
|
|
278
|
+
}
|
|
279
|
+
return tokens;
|
|
280
|
+
}
|
|
281
|
+
function buildRawTokens(event, model) {
|
|
282
|
+
const tokens = [];
|
|
283
|
+
tokens.push(token("severity", model.severity, 0, "never"));
|
|
284
|
+
tokens.push(token("RAW", "RAW", 0, "never"));
|
|
285
|
+
tokens.push(token("type", event.type, 0, "never"));
|
|
286
|
+
if (event.title) {
|
|
287
|
+
tokens.push(token("title", event.title, 1, "shorten"));
|
|
288
|
+
}
|
|
289
|
+
if (event.headline) {
|
|
290
|
+
tokens.push(token("headline", event.headline, 2, "drop"));
|
|
291
|
+
}
|
|
292
|
+
if (event.publishingOffice) {
|
|
293
|
+
tokens.push(token("office", event.publishingOffice, 3, "drop"));
|
|
294
|
+
}
|
|
295
|
+
return tokens;
|
|
296
|
+
}
|
|
297
|
+
// ── Public API ──
|
|
298
|
+
function buildSummaryTokens(event, model) {
|
|
299
|
+
switch (model.domain) {
|
|
300
|
+
case "eew":
|
|
301
|
+
return buildEewTokens(event, model);
|
|
302
|
+
case "earthquake":
|
|
303
|
+
return buildEarthquakeTokens(event, model);
|
|
304
|
+
case "tsunami":
|
|
305
|
+
return buildTsunamiTokens(event, model);
|
|
306
|
+
case "volcano":
|
|
307
|
+
return buildVolcanoTokens(event, model);
|
|
308
|
+
case "seismicText":
|
|
309
|
+
return buildSeismicTextTokens(event, model);
|
|
310
|
+
case "lgObservation":
|
|
311
|
+
return buildLgObservationTokens(event, model);
|
|
312
|
+
case "nankaiTrough":
|
|
313
|
+
return buildNankaiTroughTokens(event, model);
|
|
314
|
+
case "raw":
|
|
315
|
+
return buildRawTokens(event, model);
|
|
316
|
+
}
|
|
317
|
+
}
|