@sayue_ltr/fleq 1.50.1 → 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 +174 -0
- package/README.md +42 -6
- package/dist/config.js +37 -4
- package/dist/dmdata/rest-client.js +58 -3
- package/dist/dmdata/telegram-parser.js +115 -64
- package/dist/dmdata/ws-client.js +49 -18
- package/dist/engine/cli/cli-run.js +88 -3
- package/dist/engine/cli/cli.js +12 -0
- package/dist/engine/eew/eew-tracker.js +41 -15
- 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 +52 -4
- package/dist/engine/monitor/shutdown.js +1 -0
- package/dist/engine/notification/notifier.js +21 -4
- package/dist/engine/notification/sound-player.js +398 -36
- 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 +105 -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 +105 -0
- package/dist/engine/template/field-accessor.js +31 -0
- package/dist/engine/template/filters.js +100 -0
- package/dist/engine/template/index.js +5 -0
- package/dist/engine/template/parser.js +185 -0
- package/dist/engine/template/tokenizer.js +96 -0
- package/dist/engine/template/types.js +2 -0
- package/dist/types.js +3 -2
- package/dist/ui/display-adapter.js +60 -0
- package/dist/ui/earthquake-formatter.js +22 -5
- package/dist/ui/eew-formatter.js +25 -10
- package/dist/ui/formatter.js +116 -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 +327 -0
- package/dist/ui/repl-handlers/index.js +11 -0
- package/dist/ui/repl-handlers/info-handlers.js +633 -0
- package/dist/ui/repl-handlers/operation-handlers.js +233 -0
- package/dist/ui/repl-handlers/settings-handlers.js +927 -0
- package/dist/ui/repl-handlers/types.js +10 -0
- package/dist/ui/repl.js +81 -1752
- package/dist/ui/statistics-formatter.js +258 -0
- package/dist/ui/status-line.js +80 -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/test-samples.js +6 -0
- package/dist/ui/theme.js +43 -5
- package/dist/ui/tip-shuffler.js +81 -0
- package/dist/ui/volcano-formatter.js +15 -13
- 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 +420 -249
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fitTokensToWidth = fitTokensToWidth;
|
|
4
|
+
const formatter_1 = require("../formatter");
|
|
5
|
+
const SEPARATOR = " ";
|
|
6
|
+
const SEPARATOR_WIDTH = 2;
|
|
7
|
+
function fitTokensToWidth(tokens, maxWidth) {
|
|
8
|
+
if (tokens.length === 0)
|
|
9
|
+
return "";
|
|
10
|
+
// Step 1: Check if all tokens fit at preferred width
|
|
11
|
+
const totalPreferred = tokens.reduce((sum, t) => sum + t.preferredWidth, 0);
|
|
12
|
+
const separatorTotal = SEPARATOR_WIDTH * Math.max(0, tokens.length - 1);
|
|
13
|
+
if (totalPreferred + separatorTotal <= maxWidth) {
|
|
14
|
+
return tokens.map((t) => t.text).join(SEPARATOR);
|
|
15
|
+
}
|
|
16
|
+
// Step 2: Drop tokens by priority (4 → 3 → 2)
|
|
17
|
+
let remaining = [...tokens];
|
|
18
|
+
for (const pri of [4, 3, 2]) {
|
|
19
|
+
const currentWidth = calcTotalWidth(remaining);
|
|
20
|
+
if (currentWidth <= maxWidth)
|
|
21
|
+
break;
|
|
22
|
+
remaining = remaining.filter((t) => !(t.priority === pri && t.dropMode === "drop"));
|
|
23
|
+
}
|
|
24
|
+
// Step 3: Shorten tokens if still too wide
|
|
25
|
+
if (calcTotalWidth(remaining) > maxWidth) {
|
|
26
|
+
remaining = remaining.map((t) => {
|
|
27
|
+
if (t.dropMode === "shorten" && t.shortText != null) {
|
|
28
|
+
return { ...t, text: t.shortText };
|
|
29
|
+
}
|
|
30
|
+
return t;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return remaining.map((t) => t.text).join(SEPARATOR);
|
|
34
|
+
}
|
|
35
|
+
function calcTotalWidth(tokens) {
|
|
36
|
+
if (tokens.length === 0)
|
|
37
|
+
return 0;
|
|
38
|
+
const textWidth = tokens.reduce((sum, t) => sum + (0, formatter_1.visualWidth)(t.text), 0);
|
|
39
|
+
const sepWidth = SEPARATOR_WIDTH * (tokens.length - 1);
|
|
40
|
+
return textWidth + sepWidth;
|
|
41
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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.buildSparkline = buildSparkline;
|
|
7
|
+
exports.formatSummaryInterval = formatSummaryInterval;
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const summary_tracker_1 = require("../engine/messages/summary-tracker");
|
|
10
|
+
/** sparkline で使う8段階の文字 */
|
|
11
|
+
const SPARK_CHARS = "▁▂▃▄▅▆▇█";
|
|
12
|
+
/** ドメインの表示ラベル */
|
|
13
|
+
const DOMAIN_LABELS = {
|
|
14
|
+
eew: "EEW",
|
|
15
|
+
earthquake: "地震",
|
|
16
|
+
tsunami: "津波",
|
|
17
|
+
seismicText: "テキスト",
|
|
18
|
+
lgObservation: "長周期",
|
|
19
|
+
volcano: "火山",
|
|
20
|
+
nankaiTrough: "南海トラフ",
|
|
21
|
+
raw: "その他",
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* sparklineData (数値配列) から sparkline 文字列を生成する。
|
|
25
|
+
* 最大値に対する比率で8段階の文字を選択する。全部0なら ▁ の繰り返し。
|
|
26
|
+
*/
|
|
27
|
+
function buildSparkline(data) {
|
|
28
|
+
const max = Math.max(...data);
|
|
29
|
+
if (max === 0) {
|
|
30
|
+
return SPARK_CHARS[0].repeat(data.length);
|
|
31
|
+
}
|
|
32
|
+
return data
|
|
33
|
+
.map((v) => {
|
|
34
|
+
const ratio = v / max;
|
|
35
|
+
const idx = Math.min(Math.round(ratio * (SPARK_CHARS.length - 1)), SPARK_CHARS.length - 1);
|
|
36
|
+
return SPARK_CHARS[idx];
|
|
37
|
+
})
|
|
38
|
+
.join("");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 要約行をフォーマットする。
|
|
42
|
+
* @param snapshot SummaryWindowTracker のスナップショット
|
|
43
|
+
* @param intervalMinutes 要約間隔(分)
|
|
44
|
+
* @param sparkline sparkline を含めるか
|
|
45
|
+
*/
|
|
46
|
+
function formatSummaryInterval(snapshot, intervalMinutes, sparkline) {
|
|
47
|
+
const parts = [];
|
|
48
|
+
// ドメイン別件数
|
|
49
|
+
const domainParts = [];
|
|
50
|
+
for (const [domain, count] of Object.entries(snapshot.byDomain)) {
|
|
51
|
+
if (count > 0) {
|
|
52
|
+
const label = DOMAIN_LABELS[domain] ?? domain;
|
|
53
|
+
domainParts.push(`${label} ${count}件`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ヘッダ
|
|
57
|
+
const header = chalk_1.default.gray(`── ${intervalMinutes}分要約 ──`);
|
|
58
|
+
const domainStr = domainParts.length > 0
|
|
59
|
+
? domainParts.join(chalk_1.default.gray(" | "))
|
|
60
|
+
: chalk_1.default.gray("受信なし");
|
|
61
|
+
// maxInt
|
|
62
|
+
const maxIntStr = snapshot.maxIntSeen != null
|
|
63
|
+
? chalk_1.default.gray(` (最大${snapshot.maxIntSeen})`)
|
|
64
|
+
: "";
|
|
65
|
+
parts.push(`${header} ${domainStr}${maxIntStr}`);
|
|
66
|
+
// sparkline 行
|
|
67
|
+
if (sparkline) {
|
|
68
|
+
const sparkStr = buildSparkline(snapshot.sparklineData);
|
|
69
|
+
parts.push(chalk_1.default.gray("受信 ") + sparkStr + chalk_1.default.gray(` (${summary_tracker_1.WINDOW_MINUTES}分)`));
|
|
70
|
+
}
|
|
71
|
+
return parts.join("\n");
|
|
72
|
+
}
|
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
|
@@ -40,6 +40,8 @@ exports.DEFAULT_ROLES = exports.DEFAULT_PALETTE = void 0;
|
|
|
40
40
|
exports.hexToRgb = hexToRgb;
|
|
41
41
|
exports.rgbToHex = rgbToHex;
|
|
42
42
|
exports.resolveTheme = resolveTheme;
|
|
43
|
+
exports.setNightMode = setNightMode;
|
|
44
|
+
exports.isNightMode = isNightMode;
|
|
43
45
|
exports.getThemePath = getThemePath;
|
|
44
46
|
exports.loadTheme = loadTheme;
|
|
45
47
|
exports.loadThemeFromPath = loadThemeFromPath;
|
|
@@ -58,6 +60,7 @@ const fs = __importStar(require("fs"));
|
|
|
58
60
|
const path = __importStar(require("path"));
|
|
59
61
|
const chalk_1 = __importDefault(require("chalk"));
|
|
60
62
|
const config_1 = require("../config");
|
|
63
|
+
const night_overlay_1 = require("./night-overlay");
|
|
61
64
|
/** RoleStyleDef の型ガード */
|
|
62
65
|
function isRoleStyleDef(value) {
|
|
63
66
|
if (typeof value === "string")
|
|
@@ -186,6 +189,15 @@ exports.DEFAULT_ROLES = {
|
|
|
186
189
|
// volcano: バナー
|
|
187
190
|
volcanoAlertBanner: { bg: "vermillion", fg: "#FFFFFF", bold: true },
|
|
188
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 },
|
|
189
201
|
};
|
|
190
202
|
/** ロール名の一覧 */
|
|
191
203
|
const ROLE_NAMES = Object.keys(exports.DEFAULT_ROLES);
|
|
@@ -352,7 +364,29 @@ function buildDefaultResolvedTheme() {
|
|
|
352
364
|
return deepFreezeTheme(theme);
|
|
353
365
|
}
|
|
354
366
|
// ── モジュール状態 ──
|
|
355
|
-
let
|
|
367
|
+
let baseTheme = buildDefaultResolvedTheme();
|
|
368
|
+
let nightModeEnabled = false;
|
|
369
|
+
let currentTheme = baseTheme;
|
|
370
|
+
// ── night mode ──
|
|
371
|
+
/** baseTheme に night overlay を適用して currentTheme を更新する */
|
|
372
|
+
function rebuildActiveTheme() {
|
|
373
|
+
if (nightModeEnabled) {
|
|
374
|
+
currentTheme = deepFreezeTheme((0, night_overlay_1.applyNightOverlay)(baseTheme));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
currentTheme = baseTheme;
|
|
378
|
+
}
|
|
379
|
+
chalkCache.clear();
|
|
380
|
+
}
|
|
381
|
+
/** ナイトモードの ON/OFF を設定し、テーマを再構築する */
|
|
382
|
+
function setNightMode(enabled) {
|
|
383
|
+
nightModeEnabled = enabled;
|
|
384
|
+
rebuildActiveTheme();
|
|
385
|
+
}
|
|
386
|
+
/** ナイトモードが有効かどうかを返す */
|
|
387
|
+
function isNightMode() {
|
|
388
|
+
return nightModeEnabled;
|
|
389
|
+
}
|
|
356
390
|
// ── パス解決 ──
|
|
357
391
|
/** theme.json のパスを返す */
|
|
358
392
|
function getThemePath() {
|
|
@@ -367,14 +401,16 @@ function loadTheme() {
|
|
|
367
401
|
function loadThemeFromPath(themePath) {
|
|
368
402
|
chalkCache.clear();
|
|
369
403
|
if (!fs.existsSync(themePath)) {
|
|
370
|
-
|
|
404
|
+
baseTheme = buildDefaultResolvedTheme();
|
|
405
|
+
rebuildActiveTheme();
|
|
371
406
|
return [];
|
|
372
407
|
}
|
|
373
408
|
try {
|
|
374
409
|
const raw = fs.readFileSync(themePath, "utf-8");
|
|
375
410
|
const parsed = JSON.parse(raw);
|
|
376
411
|
if (typeof parsed !== "object" || parsed == null || Array.isArray(parsed)) {
|
|
377
|
-
|
|
412
|
+
baseTheme = buildDefaultResolvedTheme();
|
|
413
|
+
rebuildActiveTheme();
|
|
378
414
|
return ["theme.json の形式が不正です。デフォルトテーマを使用します。"];
|
|
379
415
|
}
|
|
380
416
|
const { themeFile, warnings: sanitizeWarnings } = sanitizeThemeInput(parsed);
|
|
@@ -382,11 +418,13 @@ function loadThemeFromPath(themePath) {
|
|
|
382
418
|
palette: exports.DEFAULT_PALETTE,
|
|
383
419
|
roles: exports.DEFAULT_ROLES,
|
|
384
420
|
});
|
|
385
|
-
|
|
421
|
+
baseTheme = deepFreezeTheme(theme);
|
|
422
|
+
rebuildActiveTheme();
|
|
386
423
|
return [...sanitizeWarnings, ...warnings];
|
|
387
424
|
}
|
|
388
425
|
catch (err) {
|
|
389
|
-
|
|
426
|
+
baseTheme = buildDefaultResolvedTheme();
|
|
427
|
+
rebuildActiveTheme();
|
|
390
428
|
if (err instanceof SyntaxError) {
|
|
391
429
|
return ["theme.json のJSONパースに失敗しました。デフォルトテーマを使用します。"];
|
|
392
430
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TipShuffler = void 0;
|
|
4
|
+
const waiting_tips_1 = require("./waiting-tips");
|
|
5
|
+
/**
|
|
6
|
+
* 待機中Tipのエポックデッキ生成シャッフラ。
|
|
7
|
+
*
|
|
8
|
+
* - カテゴリごとにシャッフルした後、同カテゴリ連続を避けつつ
|
|
9
|
+
* 全Tipを1エポック分のデッキにインターリーブする。
|
|
10
|
+
* - デッキを使い切ったら自動で再構築する。
|
|
11
|
+
* - タイミング制御は持たず、`next()` で次のTipを返すだけの純粋な順序供給器。
|
|
12
|
+
*/
|
|
13
|
+
class TipShuffler {
|
|
14
|
+
deck = [];
|
|
15
|
+
rng;
|
|
16
|
+
constructor(rng = Math.random) {
|
|
17
|
+
this.rng = rng;
|
|
18
|
+
this.rebuildDeck();
|
|
19
|
+
}
|
|
20
|
+
/** 次のTipを返す。デッキが空なら自動再構築。 */
|
|
21
|
+
next() {
|
|
22
|
+
if (this.deck.length === 0) {
|
|
23
|
+
this.rebuildDeck();
|
|
24
|
+
}
|
|
25
|
+
return this.deck.shift();
|
|
26
|
+
}
|
|
27
|
+
rebuildDeck() {
|
|
28
|
+
// 1. カテゴリごとにシャッフル
|
|
29
|
+
const buckets = [];
|
|
30
|
+
for (let ci = 0; ci < waiting_tips_1.TIP_CATEGORIES.length; ci++) {
|
|
31
|
+
const shuffled = this.shuffle([...waiting_tips_1.TIP_CATEGORIES[ci].tips]);
|
|
32
|
+
for (const tip of shuffled) {
|
|
33
|
+
buckets.push({ categoryIndex: ci, tip });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// 2. インターリーブ: 同カテゴリ連続を避けつつデッキ構築
|
|
37
|
+
this.deck = this.interleave(buckets);
|
|
38
|
+
}
|
|
39
|
+
/** 同カテゴリ連続を避けつつ全アイテムをインターリーブする */
|
|
40
|
+
interleave(items) {
|
|
41
|
+
// カテゴリごとのキューに分割
|
|
42
|
+
const queues = new Map();
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
if (!queues.has(item.categoryIndex)) {
|
|
45
|
+
queues.set(item.categoryIndex, []);
|
|
46
|
+
}
|
|
47
|
+
queues.get(item.categoryIndex).push(item.tip);
|
|
48
|
+
}
|
|
49
|
+
const result = [];
|
|
50
|
+
let lastCategory = -1;
|
|
51
|
+
while (queues.size > 0) {
|
|
52
|
+
// 直前カテゴリ以外で残りがあるカテゴリから選択
|
|
53
|
+
const candidates = [...queues.keys()].filter((k) => k !== lastCategory);
|
|
54
|
+
if (candidates.length === 0) {
|
|
55
|
+
// 1カテゴリしか残っていない場合はそのまま流し込む
|
|
56
|
+
const remaining = [...queues.keys()][0];
|
|
57
|
+
result.push(...queues.get(remaining));
|
|
58
|
+
queues.delete(remaining);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
// ランダムに1カテゴリ選択
|
|
62
|
+
const chosen = candidates[Math.floor(this.rng() * candidates.length)];
|
|
63
|
+
const queue = queues.get(chosen);
|
|
64
|
+
result.push(queue.shift());
|
|
65
|
+
lastCategory = chosen;
|
|
66
|
+
if (queue.length === 0) {
|
|
67
|
+
queues.delete(chosen);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
/** Fisher-Yates シャッフル */
|
|
73
|
+
shuffle(arr) {
|
|
74
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
75
|
+
const j = Math.floor(this.rng() * (i + 1));
|
|
76
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
77
|
+
}
|
|
78
|
+
return arr;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.TipShuffler = TipShuffler;
|