@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.
Files changed (99) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +42 -6
  3. package/dist/config.js +37 -4
  4. package/dist/dmdata/rest-client.js +58 -3
  5. package/dist/dmdata/telegram-parser.js +115 -64
  6. package/dist/dmdata/ws-client.js +49 -18
  7. package/dist/engine/cli/cli-run.js +88 -3
  8. package/dist/engine/cli/cli.js +12 -0
  9. package/dist/engine/eew/eew-tracker.js +41 -15
  10. package/dist/engine/filter/compile-filter.js +21 -0
  11. package/dist/engine/filter/compiler.js +188 -0
  12. package/dist/engine/filter/errors.js +41 -0
  13. package/dist/engine/filter/field-registry.js +78 -0
  14. package/dist/engine/filter/index.js +15 -0
  15. package/dist/engine/filter/parser.js +137 -0
  16. package/dist/engine/filter/rank-maps.js +34 -0
  17. package/dist/engine/filter/tokenizer.js +121 -0
  18. package/dist/engine/filter/type-checker.js +104 -0
  19. package/dist/engine/filter/types.js +2 -0
  20. package/dist/engine/filter-template/pipeline-controller.js +73 -0
  21. package/dist/engine/filter-template/pipeline.js +16 -0
  22. package/dist/engine/messages/display-callbacks.js +7 -0
  23. package/dist/engine/messages/message-router.js +114 -182
  24. package/dist/engine/messages/summary-tracker.js +106 -0
  25. package/dist/engine/messages/telegram-stats.js +103 -0
  26. package/dist/engine/messages/volcano-route-handler.js +122 -0
  27. package/dist/engine/monitor/monitor.js +52 -4
  28. package/dist/engine/monitor/shutdown.js +1 -0
  29. package/dist/engine/notification/notifier.js +21 -4
  30. package/dist/engine/notification/sound-player.js +398 -36
  31. package/dist/engine/presentation/diff-store.js +158 -0
  32. package/dist/engine/presentation/diff-types.js +2 -0
  33. package/dist/engine/presentation/events/from-earthquake.js +53 -0
  34. package/dist/engine/presentation/events/from-eew.js +72 -0
  35. package/dist/engine/presentation/events/from-lg-observation.js +58 -0
  36. package/dist/engine/presentation/events/from-nankai-trough.js +39 -0
  37. package/dist/engine/presentation/events/from-raw.js +35 -0
  38. package/dist/engine/presentation/events/from-seismic-text.js +37 -0
  39. package/dist/engine/presentation/events/from-tsunami.js +51 -0
  40. package/dist/engine/presentation/events/from-volcano.js +88 -0
  41. package/dist/engine/presentation/events/to-presentation-event.js +32 -0
  42. package/dist/engine/presentation/level-helpers.js +118 -0
  43. package/dist/engine/presentation/processors/process-earthquake.js +36 -0
  44. package/dist/engine/presentation/processors/process-eew.js +105 -0
  45. package/dist/engine/presentation/processors/process-lg-observation.js +30 -0
  46. package/dist/engine/presentation/processors/process-message.js +53 -0
  47. package/dist/engine/presentation/processors/process-nankai-trough.js +30 -0
  48. package/dist/engine/presentation/processors/process-raw.js +22 -0
  49. package/dist/engine/presentation/processors/process-seismic-text.js +30 -0
  50. package/dist/engine/presentation/processors/process-tsunami.js +42 -0
  51. package/dist/engine/presentation/processors/process-volcano.js +41 -0
  52. package/dist/engine/presentation/types.js +2 -0
  53. package/dist/engine/startup/config-resolver.js +2 -0
  54. package/dist/engine/template/compile-template.js +18 -0
  55. package/dist/engine/template/compiler.js +105 -0
  56. package/dist/engine/template/field-accessor.js +31 -0
  57. package/dist/engine/template/filters.js +100 -0
  58. package/dist/engine/template/index.js +5 -0
  59. package/dist/engine/template/parser.js +185 -0
  60. package/dist/engine/template/tokenizer.js +96 -0
  61. package/dist/engine/template/types.js +2 -0
  62. package/dist/types.js +3 -2
  63. package/dist/ui/display-adapter.js +60 -0
  64. package/dist/ui/earthquake-formatter.js +22 -5
  65. package/dist/ui/eew-formatter.js +25 -10
  66. package/dist/ui/formatter.js +116 -32
  67. package/dist/ui/minimap/grid-layout.js +91 -0
  68. package/dist/ui/minimap/index.js +16 -0
  69. package/dist/ui/minimap/minimap-renderer.js +277 -0
  70. package/dist/ui/minimap/pref-mapping.js +82 -0
  71. package/dist/ui/minimap/types.js +2 -0
  72. package/dist/ui/night-overlay.js +56 -0
  73. package/dist/ui/repl-handlers/command-definitions.js +327 -0
  74. package/dist/ui/repl-handlers/index.js +11 -0
  75. package/dist/ui/repl-handlers/info-handlers.js +633 -0
  76. package/dist/ui/repl-handlers/operation-handlers.js +233 -0
  77. package/dist/ui/repl-handlers/settings-handlers.js +927 -0
  78. package/dist/ui/repl-handlers/types.js +10 -0
  79. package/dist/ui/repl.js +81 -1752
  80. package/dist/ui/statistics-formatter.js +258 -0
  81. package/dist/ui/status-line.js +80 -0
  82. package/dist/ui/summary/index.js +5 -0
  83. package/dist/ui/summary/summary-line.js +18 -0
  84. package/dist/ui/summary/summary-model.js +31 -0
  85. package/dist/ui/summary/token-builders.js +317 -0
  86. package/dist/ui/summary/types.js +2 -0
  87. package/dist/ui/summary/width-fit.js +41 -0
  88. package/dist/ui/summary-interval-formatter.js +72 -0
  89. package/dist/ui/test-samples.js +6 -0
  90. package/dist/ui/theme.js +43 -5
  91. package/dist/ui/tip-shuffler.js +81 -0
  92. package/dist/ui/volcano-formatter.js +15 -13
  93. package/dist/ui/waiting-tips-eew.js +63 -0
  94. package/dist/ui/waiting-tips-info-systems.js +81 -0
  95. package/dist/ui/waiting-tips-seismology.js +97 -0
  96. package/dist/ui/waiting-tips-tsunami.js +72 -0
  97. package/dist/ui/waiting-tips-weather.js +189 -0
  98. package/dist/ui/waiting-tips.js +420 -249
  99. 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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
+ }
@@ -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 currentTheme = buildDefaultResolvedTheme();
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
- currentTheme = buildDefaultResolvedTheme();
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
- currentTheme = buildDefaultResolvedTheme();
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
- currentTheme = deepFreezeTheme(theme);
421
+ baseTheme = deepFreezeTheme(theme);
422
+ rebuildActiveTheme();
386
423
  return [...sanitizeWarnings, ...warnings];
387
424
  }
388
425
  catch (err) {
389
- currentTheme = buildDefaultResolvedTheme();
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;