@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,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getFieldValue = getFieldValue;
4
+ /**
5
+ * PresentationEvent からドットパスで値を取得する。
6
+ *
7
+ * 表示専用ポリシー (dmdata.jp 再配信ポリシー対応) のため、配列インデックス参照
8
+ * (`[N]`) は parser 側で禁止済み。本関数では二重防御として、念のため `raw`
9
+ * フィールドへのアクセスも拒否する。
10
+ *
11
+ * segments 例:
12
+ * - ["title"] → event.title
13
+ * - ["earthquake", "magnitude"] → event.earthquake.magnitude
14
+ */
15
+ function getFieldValue(event, segments) {
16
+ // 二重防御: parser を経由せず直接呼ばれた場合にも raw への参照を拒否
17
+ if (segments[0] === "raw")
18
+ return undefined;
19
+ let current = event;
20
+ for (const seg of segments) {
21
+ if (current == null)
22
+ return undefined;
23
+ if (typeof current === "object") {
24
+ current = current[seg];
25
+ }
26
+ else {
27
+ return undefined;
28
+ }
29
+ }
30
+ return current;
31
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ // ── Template filter functions ──
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.applyFilter = applyFilter;
5
+ /**
6
+ * フィルタ内部で値を文字列化する共通関数。
7
+ *
8
+ * 表示専用ポリシー対応: 配列は改行区切りで文字列化する。
9
+ * `String([...])` が "," 連結するため、`|upper` や `|replace` といった
10
+ * 文字列系フィルタ経由で配列を 1 行機械可読出力に整形できてしまうのを防ぐ。
11
+ */
12
+ function toString(value) {
13
+ if (value == null)
14
+ return "";
15
+ if (Array.isArray(value))
16
+ return value.join("\n");
17
+ return String(value);
18
+ }
19
+ function filterDefault(value, args) {
20
+ if (value == null || value === "") {
21
+ return args[0] ?? "";
22
+ }
23
+ return value;
24
+ }
25
+ function filterTruncate(value, args) {
26
+ const str = toString(value);
27
+ const limit = typeof args[0] === "number" ? args[0] : Number(args[0]);
28
+ if (!Number.isFinite(limit) || str.length <= limit)
29
+ return str;
30
+ return str.slice(0, limit);
31
+ }
32
+ function filterPad(value, args) {
33
+ const str = toString(value);
34
+ const width = typeof args[0] === "number" ? args[0] : Number(args[0]);
35
+ if (!Number.isFinite(width))
36
+ return str;
37
+ return str.padEnd(width);
38
+ }
39
+ function filterDate(value, args) {
40
+ const format = args[0] != null ? String(args[0]) : "HH:mm";
41
+ const date = value instanceof Date ? value : new Date(String(value));
42
+ if (isNaN(date.getTime()))
43
+ return toString(value);
44
+ const HH = String(date.getHours()).padStart(2, "0");
45
+ const mm = String(date.getMinutes()).padStart(2, "0");
46
+ const ss = String(date.getSeconds()).padStart(2, "0");
47
+ const MM = String(date.getMonth() + 1).padStart(2, "0");
48
+ const DD = String(date.getDate()).padStart(2, "0");
49
+ switch (format) {
50
+ case "HH:mm":
51
+ return `${HH}:${mm}`;
52
+ case "HH:mm:ss":
53
+ return `${HH}:${mm}:${ss}`;
54
+ case "MM/DD HH:mm":
55
+ return `${MM}/${DD} ${HH}:${mm}`;
56
+ default:
57
+ return `${HH}:${mm}`;
58
+ }
59
+ }
60
+ function filterReplace(value, args) {
61
+ const str = toString(value);
62
+ const search = args[0] != null ? String(args[0]) : "";
63
+ const replacement = args[1] != null ? String(args[1]) : "";
64
+ // 表示専用ポリシー対応: search/replacement に改行文字を含めると、
65
+ // 配列の改行 join を 1 行化する経路(\n → "," 等の置換)を作れてしまうため禁止。
66
+ if (/[\r\n]/.test(search) || /[\r\n]/.test(replacement)) {
67
+ throw new Error("テンプレート実行エラー: replace フィルタの引数に改行文字 (\\n / \\r) を含めることはできません。表示専用制限です。");
68
+ }
69
+ return str.split(search).join(replacement);
70
+ }
71
+ function filterUpper(value) {
72
+ return toString(value).toUpperCase();
73
+ }
74
+ function filterLower(value) {
75
+ return toString(value).toLowerCase();
76
+ }
77
+ /**
78
+ * テンプレートフィルタを適用する。
79
+ * 未知のフィルタ名の場合は値をそのまま返す。
80
+ */
81
+ function applyFilter(name, value, args) {
82
+ switch (name) {
83
+ case "default":
84
+ return filterDefault(value, args);
85
+ case "truncate":
86
+ return filterTruncate(value, args);
87
+ case "pad":
88
+ return filterPad(value, args);
89
+ case "date":
90
+ return filterDate(value, args);
91
+ case "replace":
92
+ return filterReplace(value, args);
93
+ case "upper":
94
+ return filterUpper(value);
95
+ case "lower":
96
+ return filterLower(value);
97
+ default:
98
+ return value;
99
+ }
100
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compileTemplate = void 0;
4
+ var compile_template_1 = require("./compile-template");
5
+ Object.defineProperty(exports, "compileTemplate", { enumerable: true, get: function () { return compile_template_1.compileTemplate; } });
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTemplate = parseTemplate;
4
+ const tokenizer_1 = require("./tokenizer");
5
+ const MAX_DEPTH = 32;
6
+ function parseTemplate(source) {
7
+ const tokens = (0, tokenizer_1.tokenizeTemplate)(source);
8
+ return new TemplateParser(tokens).parse();
9
+ }
10
+ class TemplateParser {
11
+ tokens;
12
+ pos = 0;
13
+ depth = 0;
14
+ constructor(tokens) {
15
+ this.tokens = tokens;
16
+ }
17
+ parse() {
18
+ return this.parseNodes();
19
+ }
20
+ parseNodes() {
21
+ const nodes = [];
22
+ while (this.pos < this.tokens.length) {
23
+ const token = this.tokens[this.pos];
24
+ if (token.kind === "eof" || token.kind === "else" || token.kind === "endif")
25
+ break;
26
+ if (token.kind === "text" && this.peek(-1)?.kind !== "open" && this.peek(-1)?.kind !== "if_open") {
27
+ // 外側テキスト
28
+ nodes.push({ kind: "text", value: token.value });
29
+ this.pos++;
30
+ continue;
31
+ }
32
+ if (token.kind === "if_open") {
33
+ nodes.push(this.parseIfBlock());
34
+ continue;
35
+ }
36
+ if (token.kind === "open") {
37
+ nodes.push(this.parseInterpolation());
38
+ continue;
39
+ }
40
+ // 未消費トークンはテキストとして扱う
41
+ nodes.push({ kind: "text", value: token.value });
42
+ this.pos++;
43
+ }
44
+ return nodes;
45
+ }
46
+ parseInterpolation() {
47
+ this.expect("open"); // {{
48
+ const exprToken = this.tokens[this.pos];
49
+ this.pos++;
50
+ const expr = this.parseExpr(exprToken.value);
51
+ const filters = [];
52
+ while (this.pos < this.tokens.length && this.tokens[this.pos].kind === "pipe") {
53
+ this.pos++; // |
54
+ const nameToken = this.tokens[this.pos];
55
+ this.pos++;
56
+ const args = [];
57
+ // 複数引数対応: colon が続く限り引数を読む
58
+ while (this.pos < this.tokens.length && this.tokens[this.pos].kind === "colon") {
59
+ this.pos++; // :
60
+ const argToken = this.tokens[this.pos];
61
+ this.pos++;
62
+ args.push(this.parseExpr(argToken.value));
63
+ }
64
+ filters.push({ name: nameToken.value, args });
65
+ }
66
+ this.expect("close"); // }}
67
+ return { kind: "interpolation", expr, filters };
68
+ }
69
+ parseIfBlock() {
70
+ this.expect("if_open"); // {{#if
71
+ const condToken = this.tokens[this.pos];
72
+ this.pos++;
73
+ const test = this.parsePredicate(condToken.value);
74
+ this.expect("close"); // }}
75
+ this.depth++;
76
+ if (this.depth > MAX_DEPTH) {
77
+ throw new Error(`テンプレート構文エラー: #if のネストが深すぎる (最大 ${MAX_DEPTH} 段)`);
78
+ }
79
+ const body = this.parseNodes();
80
+ let elseBody;
81
+ if (this.pos < this.tokens.length && this.tokens[this.pos].kind === "else") {
82
+ this.pos++; // {{else}}
83
+ elseBody = this.parseNodes();
84
+ }
85
+ if (this.pos < this.tokens.length && this.tokens[this.pos].kind === "endif") {
86
+ this.pos++; // {{/if}}
87
+ }
88
+ else {
89
+ this.depth--;
90
+ throw new Error('テンプレート構文エラー: "endif" が必要ですが見つかりません。{{/if}} で閉じてください');
91
+ }
92
+ this.depth--;
93
+ return { kind: "if", test, body, elseBody };
94
+ }
95
+ parseExpr(raw) {
96
+ const trimmed = raw.trim();
97
+ // 文字列リテラル
98
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
99
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
100
+ return { kind: "literal", value: unescapeString(trimmed.slice(1, -1)) };
101
+ }
102
+ // 数値
103
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
104
+ return { kind: "literal", value: Number(trimmed) };
105
+ }
106
+ // boolean / null
107
+ if (trimmed === "true")
108
+ return { kind: "literal", value: true };
109
+ if (trimmed === "false")
110
+ return { kind: "literal", value: false };
111
+ if (trimmed === "null")
112
+ return { kind: "literal", value: null };
113
+ // パス: dot + bracket 記法を (string | number)[] に分割
114
+ return { kind: "path", segments: parsePathSegments(trimmed) };
115
+ }
116
+ parsePredicate(raw) {
117
+ const trimmed = raw.trim();
118
+ // 簡易比較: "field op value" 形式
119
+ const cmpMatch = trimmed.match(/^(\S+)\s+(=|!=|>|>=|<|<=)\s+(.+)$/);
120
+ if (cmpMatch) {
121
+ const opMap = {
122
+ "=": "eq", "!=": "ne", ">": "gt", ">=": "ge", "<": "lt", "<=": "le",
123
+ };
124
+ return {
125
+ kind: "compare",
126
+ op: opMap[cmpMatch[2]],
127
+ left: this.parseExpr(cmpMatch[1]),
128
+ right: this.parseExpr(cmpMatch[3]),
129
+ };
130
+ }
131
+ // truthy
132
+ return { kind: "truthy", expr: this.parseExpr(trimmed) };
133
+ }
134
+ expect(kind) {
135
+ if (this.pos < this.tokens.length && this.tokens[this.pos].kind === kind) {
136
+ this.pos++;
137
+ return;
138
+ }
139
+ const actual = this.pos < this.tokens.length ? this.tokens[this.pos].kind : "eof";
140
+ throw new Error(`テンプレート構文エラー: "${kind}" が必要ですが "${actual}" が見つかりました`);
141
+ }
142
+ peek(offset) {
143
+ return this.tokens[this.pos + offset];
144
+ }
145
+ }
146
+ /**
147
+ * ドットパスを segments 配列に分割する。
148
+ * 例: "foo.bar.baz" → ["foo", "bar", "baz"]
149
+ *
150
+ * 表示専用ポリシー (dmdata.jp 再配信ポリシー対応) のため、以下を禁止する:
151
+ * - ブラケット記法 `[N]` (配列インデックス参照)
152
+ * - 先頭セグメントが `raw` のパス (生 XML データへの直接参照)
153
+ */
154
+ function parsePathSegments(path) {
155
+ const segments = [];
156
+ let i = 0;
157
+ while (i < path.length) {
158
+ if (path[i] === ".") {
159
+ i++; // ドット区切りをスキップ
160
+ continue;
161
+ }
162
+ if (path[i] === "[") {
163
+ throw new Error(`テンプレート構文エラー: 配列インデックス参照 [N] は無効です (path: "${path}")。表示専用制限により、要素を1行に並べる機械可読出力を防いでいます。`);
164
+ }
165
+ // 識別子: 次の . または [ まで
166
+ const start = i;
167
+ while (i < path.length && path[i] !== "." && path[i] !== "[")
168
+ i++;
169
+ segments.push(path.slice(start, i));
170
+ }
171
+ if (segments[0] === "raw") {
172
+ throw new Error(`テンプレート構文エラー: raw フィールド参照は無効です (path: "${path}")。表示専用制限により、生 XML データへの直接アクセスを禁止しています。`);
173
+ }
174
+ return segments;
175
+ }
176
+ /** 文字列リテラル内のエスケープシーケンスを復元する */
177
+ function unescapeString(s) {
178
+ return s.replace(/\\(["'\\nt])/g, (_, ch) => {
179
+ switch (ch) {
180
+ case "n": return "\n";
181
+ case "t": return "\t";
182
+ default: return ch; // \\, \", \'
183
+ }
184
+ });
185
+ }
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tokenizeTemplate = tokenizeTemplate;
4
+ function tokenizeTemplate(source) {
5
+ const tokens = [];
6
+ let i = 0;
7
+ while (i < source.length) {
8
+ // {{#if ...}}
9
+ if (source.startsWith("{{#if", i)) {
10
+ tokens.push({ kind: "if_open", value: "{{#if", pos: i });
11
+ i += 5;
12
+ // 空白スキップ
13
+ while (i < source.length && source[i] === " ")
14
+ i++;
15
+ // 条件式を "}}" まで読む
16
+ const start = i;
17
+ while (i < source.length && !source.startsWith("}}", i))
18
+ i++;
19
+ tokens.push({ kind: "text", value: source.slice(start, i), pos: start });
20
+ if (source.startsWith("}}", i)) {
21
+ tokens.push({ kind: "close", value: "}}", pos: i });
22
+ i += 2;
23
+ }
24
+ continue;
25
+ }
26
+ // {{else}}
27
+ if (source.startsWith("{{else}}", i)) {
28
+ tokens.push({ kind: "else", value: "{{else}}", pos: i });
29
+ i += 8;
30
+ continue;
31
+ }
32
+ // {{/if}}
33
+ if (source.startsWith("{{/if}}", i)) {
34
+ tokens.push({ kind: "endif", value: "{{/if}}", pos: i });
35
+ i += 7;
36
+ continue;
37
+ }
38
+ // {{ interpolation }}
39
+ if (source.startsWith("{{", i)) {
40
+ tokens.push({ kind: "open", value: "{{", pos: i });
41
+ i += 2;
42
+ // interpolation 内容を解析
43
+ while (i < source.length && !source.startsWith("}}", i)) {
44
+ if (source[i] === "|") {
45
+ tokens.push({ kind: "pipe", value: "|", pos: i });
46
+ i++;
47
+ }
48
+ else if (source[i] === ":") {
49
+ tokens.push({ kind: "colon", value: ":", pos: i });
50
+ i++;
51
+ }
52
+ else if (source[i] === " ") {
53
+ i++;
54
+ }
55
+ else if (source[i] === '"' || source[i] === "'") {
56
+ // 文字列リテラル
57
+ const quote = source[i];
58
+ const start = i;
59
+ i++;
60
+ while (i < source.length && source[i] !== quote) {
61
+ if (source[i] === "\\" && i + 1 < source.length)
62
+ i++;
63
+ i++;
64
+ }
65
+ if (i < source.length)
66
+ i++; // 閉じ引用符
67
+ tokens.push({ kind: "text", value: source.slice(start, i), pos: start });
68
+ }
69
+ else {
70
+ // 識別子/数値
71
+ const start = i;
72
+ while (i < source.length &&
73
+ !source.startsWith("}}", i) &&
74
+ source[i] !== "|" &&
75
+ source[i] !== ":" &&
76
+ source[i] !== " ") {
77
+ i++;
78
+ }
79
+ tokens.push({ kind: "text", value: source.slice(start, i), pos: start });
80
+ }
81
+ }
82
+ if (source.startsWith("}}", i)) {
83
+ tokens.push({ kind: "close", value: "}}", pos: i });
84
+ i += 2;
85
+ }
86
+ continue;
87
+ }
88
+ // テキスト
89
+ const start = i;
90
+ while (i < source.length && !source.startsWith("{{", i))
91
+ i++;
92
+ tokens.push({ kind: "text", value: source.slice(start, i), pos: start });
93
+ }
94
+ tokens.push({ kind: "eof", value: "", pos: source.length });
95
+ return tokens;
96
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/types.js CHANGED
@@ -23,7 +23,7 @@ exports.DEFAULT_CONFIG = {
23
23
  volcano: true,
24
24
  },
25
25
  sound: true,
26
- eewLog: true,
26
+ eewLog: false,
27
27
  eewLogFields: {
28
28
  hypocenter: true,
29
29
  originTime: true,
@@ -39,6 +39,8 @@ exports.DEFAULT_CONFIG = {
39
39
  maxIntChangeReason: true,
40
40
  },
41
41
  maxObservations: null,
42
+ nightMode: false,
43
+ summaryInterval: null,
42
44
  backup: false,
43
45
  truncation: {
44
46
  seismicTextLines: 15,
@@ -50,7 +52,6 @@ exports.DEFAULT_CONFIG = {
50
52
  volcanoAshfallDetailLines: 16,
51
53
  volcanoAshfallRegularLines: 10,
52
54
  volcanoPreventionLines: 8,
53
- volcanoMunicipalities: 6,
54
55
  ashfallAreasQuick: 5,
55
56
  ashfallAreasOther: 3,
56
57
  ashfallPeriodsQuick: 1,
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * engine 層の DisplayCallbacks を実装する UI アダプター。
4
+ * 全ての display 関数をここに集約し、engine → ui の逆方向依存を断つ。
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.createDisplayAdapter = createDisplayAdapter;
8
+ const formatter_1 = require("./formatter");
9
+ const summary_1 = require("./summary");
10
+ const eew_formatter_1 = require("./eew-formatter");
11
+ const earthquake_formatter_1 = require("./earthquake-formatter");
12
+ const volcano_formatter_1 = require("./volcano-formatter");
13
+ /** DisplayCallbacks の実装を生成する */
14
+ function createDisplayAdapter() {
15
+ return {
16
+ displayOutcome(outcome) {
17
+ switch (outcome.domain) {
18
+ case "eew":
19
+ (0, eew_formatter_1.displayEewInfo)(outcome.parsed, {
20
+ activeCount: outcome.eewResult.activeCount,
21
+ diff: outcome.eewResult.diff,
22
+ colorIndex: outcome.eewResult.colorIndex,
23
+ });
24
+ break;
25
+ case "earthquake":
26
+ (0, earthquake_formatter_1.displayEarthquakeInfo)(outcome.parsed);
27
+ break;
28
+ case "seismicText":
29
+ (0, earthquake_formatter_1.displaySeismicTextInfo)(outcome.parsed);
30
+ break;
31
+ case "lgObservation":
32
+ (0, earthquake_formatter_1.displayLgObservationInfo)(outcome.parsed);
33
+ break;
34
+ case "tsunami":
35
+ (0, earthquake_formatter_1.displayTsunamiInfo)(outcome.parsed);
36
+ break;
37
+ case "nankaiTrough":
38
+ (0, earthquake_formatter_1.displayNankaiTroughInfo)(outcome.parsed);
39
+ break;
40
+ case "raw":
41
+ (0, formatter_1.displayRawHeader)(outcome.msg);
42
+ break;
43
+ // volcano: VolcanoRouteHandler 経由で displayVolcano/displayVolcanoBatch を直接呼ぶ
44
+ }
45
+ },
46
+ displayRawHeader(msg) {
47
+ (0, formatter_1.displayRawHeader)(msg);
48
+ },
49
+ displayVolcano(info, presentation) {
50
+ (0, volcano_formatter_1.displayVolcanoInfo)(info, presentation);
51
+ },
52
+ displayVolcanoBatch(batch, presentation) {
53
+ (0, volcano_formatter_1.displayVolcanoAshfallBatch)(batch, presentation);
54
+ },
55
+ getDisplayMode() {
56
+ return (0, formatter_1.getDisplayMode)();
57
+ },
58
+ renderSummaryLine: summary_1.renderSummaryLine,
59
+ };
60
+ }
@@ -381,6 +381,11 @@ function displayEarthquakeInfo(info) {
381
381
  buf.push(wl);
382
382
  }
383
383
  }
384
+ // EventID (同一地震の紐付け用、通常モードのみ表示)
385
+ if (info.eventId) {
386
+ buf.push((0, formatter_1.frameDivider)(level, width));
387
+ buf.push((0, formatter_1.frameLine)(level, chalk_1.default.gray(`EventID: ${info.eventId}`), width));
388
+ }
384
389
  // フッター
385
390
  (0, formatter_1.renderFooter)(level, info.type, info.reportDateTime, info.publishingOffice, width, buf);
386
391
  buf.push((0, formatter_1.frameBottom)(level, width));
@@ -825,7 +830,7 @@ function displayLgObservationInfo(info) {
825
830
  buf.push((0, formatter_1.frameLine)(level, chalk_1.default.white("位置: ") + chalk_1.default.white(`${eq.latitude} ${eq.longitude}`), width));
826
831
  }
827
832
  }
828
- // 地域リスト (LgInt 降順)
833
+ // 地域リスト (LgInt 降順 → 階級別グループ化)
829
834
  if (info.areas.length > 0) {
830
835
  buf.push((0, formatter_1.frameDivider)(level, width));
831
836
  const sorted = [...info.areas].sort((a, b) => (0, formatter_1.lgIntToNumeric)(b.maxLgInt) - (0, formatter_1.lgIntToNumeric)(a.maxLgInt));
@@ -833,13 +838,25 @@ function displayLgObservationInfo(info) {
833
838
  const maxObs = (0, formatter_1.getMaxObservations)();
834
839
  const displayAreas = maxObs != null ? sorted.slice(0, maxObs) : sorted;
835
840
  const hiddenCount = sorted.length - displayAreas.length;
841
+ // 長周期階級別にグループ化
842
+ const byLgInt = new Map();
836
843
  for (const area of displayAreas) {
837
- const lc = (0, formatter_1.lgIntensityColor)(area.maxLgInt);
844
+ const key = area.maxLgInt;
845
+ if (!byLgInt.has(key))
846
+ byLgInt.set(key, []);
838
847
  const ic = (0, formatter_1.intensityColor)(area.maxInt);
839
- buf.push((0, formatter_1.frameLine)(level, lc(`長周期${area.maxLgInt}: `) +
840
- chalk_1.default.white(area.name) +
841
- ic(` (震度${area.maxInt})`), width));
848
+ byLgInt.get(key).push({
849
+ primary: chalk_1.default.white(area.name),
850
+ badges: [ic(` (震度${area.maxInt})`)],
851
+ });
842
852
  }
853
+ // LgInt 降順でソート
854
+ const sortedEntries = [...byLgInt.entries()].sort((a, b) => (0, formatter_1.lgIntToNumeric)(b[0]) - (0, formatter_1.lgIntToNumeric)(a[0]));
855
+ const groups = sortedEntries.map(([lgInt, items]) => ({
856
+ prefix: (0, formatter_1.lgIntensityColor)(lgInt)(`長周期${lgInt}: `),
857
+ items,
858
+ }));
859
+ (0, formatter_1.renderGroupedItemList)({ level, width, groups, buf });
843
860
  if (hiddenCount > 0) {
844
861
  buf.push((0, formatter_1.frameLine)(level, chalk_1.default.gray(`... 他 ${hiddenCount} 地点`), width));
845
862
  }
@@ -282,23 +282,38 @@ function displayEewInfo(info, context) {
282
282
  const allAreas = info.forecastIntensity.areas;
283
283
  const displayAreas = maxObs != null ? allAreas.slice(0, maxObs) : allAreas;
284
284
  const hiddenCount = allAreas.length - displayAreas.length;
285
+ // 震度別にグループ化
286
+ const byIntensity = new Map();
285
287
  for (const area of displayAreas) {
286
- const color = (0, formatter_1.intensityColor)(area.intensity);
287
- let areaText = chalk_1.default.white(area.name);
288
+ const key = area.intensity;
289
+ if (!byIntensity.has(key))
290
+ byIntensity.set(key, []);
291
+ const badges = [];
288
292
  if (area.isPlum) {
289
- areaText += theme.getRoleChalk("plumLabel")(" [PLUM]");
290
- }
291
- if (area.hasArrived) {
292
- areaText += theme.getRoleChalk("arrivedLabel")(" [到達]");
293
+ badges.push(theme.getRoleChalk("plumLabel")(" [PLUM]"));
293
294
  }
295
+ // [到達] は下部の専用セクションに集約するため、ここでは付与しない
294
296
  if (area.lgIntensity && (0, formatter_1.lgIntToNumeric)(area.lgIntensity) >= 1) {
295
297
  const lc = (0, formatter_1.lgIntensityColor)(area.lgIntensity);
296
- areaText += lc(` [長周期${area.lgIntensity}]`);
297
- }
298
- for (const wl of (0, formatter_1.wrapFrameLines)(level, color(`震度${area.intensity}: `) + areaText, width)) {
299
- buf.push(wl);
298
+ badges.push(lc(` [長周期${area.lgIntensity}]`));
300
299
  }
300
+ byIntensity.get(key).push({
301
+ primary: chalk_1.default.white(area.name),
302
+ badges: badges.length > 0 ? badges : undefined,
303
+ });
301
304
  }
305
+ // 震度降順でソート
306
+ const order = ["7", "6+", "6強", "6-", "6弱", "5+", "5強", "5-", "5弱", "4", "3", "2", "1"];
307
+ const sortedEntries = [...byIntensity.entries()].sort((a, b) => {
308
+ const ai = order.indexOf(a[0]);
309
+ const bi = order.indexOf(b[0]);
310
+ return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
311
+ });
312
+ const groups = sortedEntries.map(([int, items]) => ({
313
+ prefix: (0, formatter_1.intensityColor)(int)(`震度${int}: `),
314
+ items,
315
+ }));
316
+ (0, formatter_1.renderGroupedItemList)({ level, width, groups, buf });
302
317
  if (hiddenCount > 0) {
303
318
  buf.push((0, formatter_1.frameLine)(level, chalk_1.default.gray(`... 他 ${hiddenCount} 地点`), width));
304
319
  }