@sayue_ltr/fleq 1.49.2
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 +898 -0
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/assets/icons/.gitkeep +0 -0
- package/assets/sounds/.gitkeep +0 -0
- package/assets/sounds/cancel.mp3 +0 -0
- package/assets/sounds/critical.mp3 +0 -0
- package/assets/sounds/info.mp3 +0 -0
- package/assets/sounds/normal.mp3 +0 -0
- package/assets/sounds/warning.mp3 +0 -0
- package/dist/config.js +638 -0
- package/dist/dmdata/connection-manager.js +2 -0
- package/dist/dmdata/endpoint-selector.js +185 -0
- package/dist/dmdata/multi-connection-manager.js +158 -0
- package/dist/dmdata/rest-client.js +281 -0
- package/dist/dmdata/telegram-parser.js +704 -0
- package/dist/dmdata/volcano-parser.js +647 -0
- package/dist/dmdata/ws-client.js +336 -0
- package/dist/engine/cli/cli-init.js +266 -0
- package/dist/engine/cli/cli-run.js +121 -0
- package/dist/engine/cli/cli.js +121 -0
- package/dist/engine/eew/eew-logger.js +355 -0
- package/dist/engine/eew/eew-tracker.js +229 -0
- package/dist/engine/messages/message-router.js +261 -0
- package/dist/engine/messages/tsunami-state.js +96 -0
- package/dist/engine/messages/volcano-state.js +131 -0
- package/dist/engine/messages/volcano-vfvo53-aggregator.js +173 -0
- package/dist/engine/monitor/monitor.js +118 -0
- package/dist/engine/monitor/repl-coordinator.js +63 -0
- package/dist/engine/monitor/shutdown.js +114 -0
- package/dist/engine/notification/node-notifier-loader.js +19 -0
- package/dist/engine/notification/notifier.js +338 -0
- package/dist/engine/notification/sound-player.js +230 -0
- package/dist/engine/notification/volcano-presentation.js +166 -0
- package/dist/engine/startup/config-resolver.js +139 -0
- package/dist/engine/startup/tsunami-initializer.js +91 -0
- package/dist/engine/startup/update-checker.js +229 -0
- package/dist/engine/startup/volcano-initializer.js +89 -0
- package/dist/index.js +24 -0
- package/dist/logger.js +95 -0
- package/dist/types.js +61 -0
- package/dist/ui/earthquake-formatter.js +871 -0
- package/dist/ui/eew-formatter.js +335 -0
- package/dist/ui/formatter.js +689 -0
- package/dist/ui/repl.js +2059 -0
- package/dist/ui/test-samples.js +880 -0
- package/dist/ui/theme.js +516 -0
- package/dist/ui/volcano-formatter.js +667 -0
- package/dist/ui/waiting-tips.js +227 -0
- package/dist/utils/intensity.js +13 -0
- package/dist/utils/secrets.js +14 -0
- package/package.json +69 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.decodeBody = decodeBody;
|
|
40
|
+
exports.parseXml = parseXml;
|
|
41
|
+
exports.dig = dig;
|
|
42
|
+
exports.str = str;
|
|
43
|
+
exports.first = first;
|
|
44
|
+
exports.parseEarthquakeTelegram = parseEarthquakeTelegram;
|
|
45
|
+
exports.parseEewTelegram = parseEewTelegram;
|
|
46
|
+
exports.parseTsunamiTelegram = parseTsunamiTelegram;
|
|
47
|
+
exports.parseSeismicTextTelegram = parseSeismicTextTelegram;
|
|
48
|
+
exports.parseNankaiTroughTelegram = parseNankaiTroughTelegram;
|
|
49
|
+
exports.parseLgObservationTelegram = parseLgObservationTelegram;
|
|
50
|
+
const zlib_1 = __importDefault(require("zlib"));
|
|
51
|
+
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
52
|
+
const log = __importStar(require("../logger"));
|
|
53
|
+
const xmlParser = new fast_xml_parser_1.XMLParser({
|
|
54
|
+
ignoreAttributes: false,
|
|
55
|
+
attributeNamePrefix: "@_",
|
|
56
|
+
textNodeName: "#text",
|
|
57
|
+
isArray: (name) => {
|
|
58
|
+
// 震度観測地域、市町村等は配列として扱う
|
|
59
|
+
const arrayTags = [
|
|
60
|
+
"Pref",
|
|
61
|
+
"Area",
|
|
62
|
+
"City",
|
|
63
|
+
"IntensityStation",
|
|
64
|
+
"Item",
|
|
65
|
+
"Kind",
|
|
66
|
+
"Category",
|
|
67
|
+
"ForecastInt",
|
|
68
|
+
"Observation",
|
|
69
|
+
"Station",
|
|
70
|
+
"Estimation",
|
|
71
|
+
// 火山電文
|
|
72
|
+
"VolcanoInfo",
|
|
73
|
+
"AshInfo",
|
|
74
|
+
"WindAboveCraterElements",
|
|
75
|
+
];
|
|
76
|
+
return arrayTags.includes(name);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
/** 展開後の最大許容サイズ (10 MB) */
|
|
80
|
+
const MAX_DECOMPRESSED_BYTES = 10 * 1024 * 1024;
|
|
81
|
+
/** body フィールドをデコードしてXML文字列を返す */
|
|
82
|
+
function decodeBody(msg) {
|
|
83
|
+
let buf;
|
|
84
|
+
if (msg.encoding === "base64") {
|
|
85
|
+
buf = Buffer.from(msg.body, "base64");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
buf = Buffer.from(msg.body, "utf-8");
|
|
89
|
+
}
|
|
90
|
+
if (msg.compression === "gzip") {
|
|
91
|
+
buf = zlib_1.default.gunzipSync(buf, { maxOutputLength: MAX_DECOMPRESSED_BYTES });
|
|
92
|
+
}
|
|
93
|
+
else if (msg.compression === "zip") {
|
|
94
|
+
buf = zlib_1.default.unzipSync(buf, { maxOutputLength: MAX_DECOMPRESSED_BYTES });
|
|
95
|
+
}
|
|
96
|
+
if (buf.length > MAX_DECOMPRESSED_BYTES) {
|
|
97
|
+
throw new Error(`展開後のサイズが上限を超えています: ${buf.length} bytes (上限: ${MAX_DECOMPRESSED_BYTES} bytes)`);
|
|
98
|
+
}
|
|
99
|
+
return buf.toString("utf-8");
|
|
100
|
+
}
|
|
101
|
+
/** XML文字列をパースしてJSオブジェクトを返す */
|
|
102
|
+
function parseXml(xmlStr) {
|
|
103
|
+
return xmlParser.parse(xmlStr);
|
|
104
|
+
}
|
|
105
|
+
// ── ヘルパー: 安全なプロパティアクセス ──
|
|
106
|
+
function dig(obj, ...keys) {
|
|
107
|
+
let current = obj;
|
|
108
|
+
for (const key of keys) {
|
|
109
|
+
if (current == null || typeof current !== "object")
|
|
110
|
+
return undefined;
|
|
111
|
+
current = current[key];
|
|
112
|
+
}
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
function str(val) {
|
|
116
|
+
if (val == null)
|
|
117
|
+
return "";
|
|
118
|
+
return String(val);
|
|
119
|
+
}
|
|
120
|
+
function first(val) {
|
|
121
|
+
return Array.isArray(val) ? val[0] : val;
|
|
122
|
+
}
|
|
123
|
+
function normalizeConditionText(condition) {
|
|
124
|
+
if (!condition)
|
|
125
|
+
return "";
|
|
126
|
+
return condition.normalize("NFKC").replace(/\s+/g, "");
|
|
127
|
+
}
|
|
128
|
+
function isAssumedHypocenterCondition(condition) {
|
|
129
|
+
return normalizeConditionText(condition).includes("仮定震源要素");
|
|
130
|
+
}
|
|
131
|
+
function isPlumAreaCondition(condition) {
|
|
132
|
+
return /PLUM法/.test(normalizeConditionText(condition));
|
|
133
|
+
}
|
|
134
|
+
function hasArrivedAreaCondition(condition) {
|
|
135
|
+
return normalizeConditionText(condition).includes("既に主要動到達");
|
|
136
|
+
}
|
|
137
|
+
function isAssumedHypocenterFallbackPattern(earthquake) {
|
|
138
|
+
if (!earthquake)
|
|
139
|
+
return false;
|
|
140
|
+
const mag = parseFloat(earthquake.magnitude);
|
|
141
|
+
const depthMatch = earthquake.depth.match(/^(\d+)km$/);
|
|
142
|
+
const depthKm = depthMatch ? parseInt(depthMatch[1], 10) : -1;
|
|
143
|
+
return mag === 1.0 && depthKm === 10;
|
|
144
|
+
}
|
|
145
|
+
/** 震源関連の情報を抽出 */
|
|
146
|
+
function extractEarthquake(earthquake) {
|
|
147
|
+
if (!earthquake)
|
|
148
|
+
return undefined;
|
|
149
|
+
const originTime = str(dig(earthquake, "OriginTime"));
|
|
150
|
+
const hypo = dig(earthquake, "Hypocenter");
|
|
151
|
+
const area = first(dig(hypo, "Area"));
|
|
152
|
+
const name = str(dig(area, "Name"));
|
|
153
|
+
// 座標パース: "+35.7+139.8-10000/" 形式
|
|
154
|
+
const coordStr = str(dig(area, "jmx_eb:Coordinate", "#text") ||
|
|
155
|
+
dig(area, "Coordinate", "#text") ||
|
|
156
|
+
dig(area, "jmx_eb:Coordinate") ||
|
|
157
|
+
dig(area, "Coordinate"));
|
|
158
|
+
const { lat, lon, depth } = parseCoordinate(coordStr);
|
|
159
|
+
const magRaw = str(dig(earthquake, "jmx_eb:Magnitude", "#text") ||
|
|
160
|
+
dig(earthquake, "Magnitude", "#text") ||
|
|
161
|
+
dig(earthquake, "jmx_eb:Magnitude") ||
|
|
162
|
+
dig(earthquake, "Magnitude") ||
|
|
163
|
+
"");
|
|
164
|
+
// "4" → "4.0" のように小数点第1位を保証する
|
|
165
|
+
const mag = magRaw && !isNaN(parseFloat(magRaw))
|
|
166
|
+
? parseFloat(magRaw).toFixed(1)
|
|
167
|
+
: magRaw;
|
|
168
|
+
return {
|
|
169
|
+
originTime,
|
|
170
|
+
hypocenterName: name,
|
|
171
|
+
latitude: lat,
|
|
172
|
+
longitude: lon,
|
|
173
|
+
depth,
|
|
174
|
+
magnitude: mag,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** 座標文字列をパース: "+35.7+139.8-10000/" → lat, lon, depth */
|
|
178
|
+
function parseCoordinate(coord) {
|
|
179
|
+
if (!coord)
|
|
180
|
+
return { lat: "", lon: "", depth: "" };
|
|
181
|
+
// 形式: "+緯度+経度-深さ/" or "+緯度+経度/"
|
|
182
|
+
const match = coord.match(/([+-][\d.]+)([+-][\d.]+)(?:([+-][\d.]+))?/);
|
|
183
|
+
if (!match)
|
|
184
|
+
return { lat: "", lon: "", depth: "" };
|
|
185
|
+
const latNum = parseFloat(match[1]);
|
|
186
|
+
const lonNum = parseFloat(match[2]);
|
|
187
|
+
const depthNum = match[3] ? Math.abs(parseFloat(match[3])) : 0;
|
|
188
|
+
// 深さはメートル単位で来る場合とキロメートル単位で来る場合がある
|
|
189
|
+
const depthKm = depthNum >= 1000 ? depthNum / 1000 : depthNum;
|
|
190
|
+
return {
|
|
191
|
+
lat: `${latNum >= 0 ? "N" : "S"}${Math.abs(latNum).toFixed(1)}`,
|
|
192
|
+
lon: `${lonNum >= 0 ? "E" : "W"}${Math.abs(lonNum).toFixed(1)}`,
|
|
193
|
+
depth: depthKm > 0 ? `${depthKm}km` : "ごく浅い",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** 震度観測地域を抽出 */
|
|
197
|
+
function extractIntensity(body) {
|
|
198
|
+
const intensity = dig(body, "Intensity");
|
|
199
|
+
if (!intensity)
|
|
200
|
+
return undefined;
|
|
201
|
+
const rawObservation = dig(intensity, "Observation");
|
|
202
|
+
if (!rawObservation)
|
|
203
|
+
return undefined;
|
|
204
|
+
const observation = first(rawObservation);
|
|
205
|
+
const maxInt = str(dig(observation, "MaxInt"));
|
|
206
|
+
const maxLgIntRaw = str(dig(observation, "MaxLgInt"));
|
|
207
|
+
const maxLgInt = maxLgIntRaw || undefined;
|
|
208
|
+
const areas = [];
|
|
209
|
+
const prefs = dig(observation, "Pref");
|
|
210
|
+
if (Array.isArray(prefs)) {
|
|
211
|
+
for (const pref of prefs) {
|
|
212
|
+
const prefAreas = dig(pref, "Area");
|
|
213
|
+
if (Array.isArray(prefAreas)) {
|
|
214
|
+
for (const area of prefAreas) {
|
|
215
|
+
const lgInt = str(dig(area, "MaxLgInt"));
|
|
216
|
+
areas.push({
|
|
217
|
+
name: str(dig(area, "Name")),
|
|
218
|
+
intensity: str(dig(area, "MaxInt")),
|
|
219
|
+
...(lgInt ? { lgIntensity: lgInt } : {}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { maxInt, ...(maxLgInt ? { maxLgInt } : {}), areas };
|
|
226
|
+
}
|
|
227
|
+
/** 津波情報を抽出 */
|
|
228
|
+
function extractTsunami(body) {
|
|
229
|
+
const comments = dig(body, "Comments");
|
|
230
|
+
if (!comments)
|
|
231
|
+
return undefined;
|
|
232
|
+
const forecast = dig(comments, "ForecastComment");
|
|
233
|
+
const text = str(dig(forecast, "Text")) ||
|
|
234
|
+
str(dig(comments, "ForecastComment", "Text"));
|
|
235
|
+
if (!text)
|
|
236
|
+
return undefined;
|
|
237
|
+
return { text };
|
|
238
|
+
}
|
|
239
|
+
// ── EEW ヘルパー ──
|
|
240
|
+
function parseMaxIntChangeReason(body) {
|
|
241
|
+
const raw = str(dig(body, "Intensity", "Forecast", "Appendix", "MaxIntChangeReason"));
|
|
242
|
+
if (!raw)
|
|
243
|
+
return undefined;
|
|
244
|
+
const code = Number.parseInt(raw, 10);
|
|
245
|
+
return Number.isNaN(code) ? undefined : code;
|
|
246
|
+
}
|
|
247
|
+
function extractEewForecastAreas(body) {
|
|
248
|
+
const forecast = dig(body, "Intensity", "Forecast");
|
|
249
|
+
if (!forecast)
|
|
250
|
+
return undefined;
|
|
251
|
+
const overallLgInt = dig(forecast, "ForecastLgInt");
|
|
252
|
+
const overallLgIntFrom = str(Array.isArray(overallLgInt)
|
|
253
|
+
? dig(overallLgInt[0], "From")
|
|
254
|
+
: dig(overallLgInt, "From"));
|
|
255
|
+
const maxLgInt = overallLgIntFrom || undefined;
|
|
256
|
+
const areas = [];
|
|
257
|
+
const prefs = dig(forecast, "Pref");
|
|
258
|
+
if (Array.isArray(prefs)) {
|
|
259
|
+
for (const pref of prefs) {
|
|
260
|
+
const prefAreas = dig(pref, "Area");
|
|
261
|
+
if (Array.isArray(prefAreas)) {
|
|
262
|
+
for (const area of prefAreas) {
|
|
263
|
+
const rawForecastInt = dig(area, "ForecastInt") || dig(area, "ForecastIntFrom");
|
|
264
|
+
const forecastInt = Array.isArray(rawForecastInt) ? rawForecastInt[0] : rawForecastInt;
|
|
265
|
+
const rawLgInt = dig(area, "ForecastLgInt");
|
|
266
|
+
const lgInt = Array.isArray(rawLgInt)
|
|
267
|
+
? str(dig(rawLgInt[0], "From"))
|
|
268
|
+
: str(dig(rawLgInt, "From"));
|
|
269
|
+
const condition = str(dig(area, "Condition"));
|
|
270
|
+
const isPlum = isPlumAreaCondition(condition) || undefined;
|
|
271
|
+
const hasArrived = hasArrivedAreaCondition(condition) || undefined;
|
|
272
|
+
areas.push({
|
|
273
|
+
name: str(dig(area, "Name")),
|
|
274
|
+
intensity: str(dig(forecastInt, "From") || forecastInt || ""),
|
|
275
|
+
...(lgInt ? { lgIntensity: lgInt } : {}),
|
|
276
|
+
...(isPlum ? { isPlum } : {}),
|
|
277
|
+
...(hasArrived ? { hasArrived } : {}),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const hasPlumArea = areas.some((a) => a.isPlum === true);
|
|
284
|
+
return { areas, maxLgInt, hasPlumArea };
|
|
285
|
+
}
|
|
286
|
+
// ── 津波ヘルパー ──
|
|
287
|
+
function extractTsunamiObservations(tsunamiNode) {
|
|
288
|
+
const rawObservation = dig(tsunamiNode, "Observation");
|
|
289
|
+
const observationsNodes = Array.isArray(rawObservation)
|
|
290
|
+
? rawObservation
|
|
291
|
+
: rawObservation
|
|
292
|
+
? [rawObservation]
|
|
293
|
+
: [];
|
|
294
|
+
const observations = [];
|
|
295
|
+
for (const node of observationsNodes) {
|
|
296
|
+
const items = dig(node, "Item");
|
|
297
|
+
if (!Array.isArray(items)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
for (const item of items) {
|
|
301
|
+
const stationsRaw = dig(item, "Station");
|
|
302
|
+
const stations = Array.isArray(stationsRaw)
|
|
303
|
+
? stationsRaw
|
|
304
|
+
: stationsRaw
|
|
305
|
+
? [stationsRaw]
|
|
306
|
+
: [];
|
|
307
|
+
for (const station of stations) {
|
|
308
|
+
observations.push({
|
|
309
|
+
name: str(dig(station, "Name")),
|
|
310
|
+
sensor: str(dig(station, "Sensor")),
|
|
311
|
+
arrivalTime: str(dig(station, "FirstHeight", "ArrivalTime")),
|
|
312
|
+
initial: str(dig(station, "FirstHeight", "Initial")),
|
|
313
|
+
maxHeightCondition: str(dig(station, "MaxHeight", "Condition")),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return observations;
|
|
319
|
+
}
|
|
320
|
+
function extractTsunamiEstimations(tsunamiNode) {
|
|
321
|
+
const rawEstimation = dig(tsunamiNode, "Estimation");
|
|
322
|
+
const estimationNodes = Array.isArray(rawEstimation)
|
|
323
|
+
? rawEstimation
|
|
324
|
+
: rawEstimation
|
|
325
|
+
? [rawEstimation]
|
|
326
|
+
: [];
|
|
327
|
+
const estimations = [];
|
|
328
|
+
for (const node of estimationNodes) {
|
|
329
|
+
const items = dig(node, "Item");
|
|
330
|
+
if (!Array.isArray(items)) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
for (const item of items) {
|
|
334
|
+
const area = first(dig(item, "Area"));
|
|
335
|
+
const areaName = str(dig(area, "Name")).trim();
|
|
336
|
+
if (!areaName) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const maxHeightDescription = str(dig(item, "MaxHeight", "jmx_eb:TsunamiHeight", "@_description")) ||
|
|
340
|
+
str(dig(item, "MaxHeight", "TsunamiHeight", "@_description")) ||
|
|
341
|
+
str(dig(item, "MaxHeight", "Condition"));
|
|
342
|
+
const firstHeight = str(dig(item, "FirstHeight", "ArrivalTime")) ||
|
|
343
|
+
str(dig(item, "FirstHeight", "Condition"));
|
|
344
|
+
estimations.push({
|
|
345
|
+
areaName,
|
|
346
|
+
maxHeightDescription,
|
|
347
|
+
firstHeight,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return estimations;
|
|
352
|
+
}
|
|
353
|
+
// ── 長周期地震動ヘルパー ──
|
|
354
|
+
function extractLgObservationDetails(body) {
|
|
355
|
+
const result = { areas: [] };
|
|
356
|
+
const intensity = dig(body, "Intensity");
|
|
357
|
+
if (!intensity)
|
|
358
|
+
return result;
|
|
359
|
+
const rawObservation = dig(intensity, "Observation");
|
|
360
|
+
if (!rawObservation)
|
|
361
|
+
return result;
|
|
362
|
+
const observation = first(rawObservation);
|
|
363
|
+
result.maxInt = str(dig(observation, "MaxInt")) || undefined;
|
|
364
|
+
result.maxLgInt = str(dig(observation, "MaxLgInt")) || undefined;
|
|
365
|
+
result.lgCategory = str(dig(observation, "LgCategory")) || undefined;
|
|
366
|
+
const prefs = dig(observation, "Pref");
|
|
367
|
+
if (Array.isArray(prefs)) {
|
|
368
|
+
for (const pref of prefs) {
|
|
369
|
+
const prefAreas = dig(pref, "Area");
|
|
370
|
+
if (Array.isArray(prefAreas)) {
|
|
371
|
+
for (const area of prefAreas) {
|
|
372
|
+
const areaMaxInt = str(dig(area, "MaxInt"));
|
|
373
|
+
const areaMaxLgInt = str(dig(area, "MaxLgInt"));
|
|
374
|
+
if (areaMaxLgInt) {
|
|
375
|
+
result.areas.push({
|
|
376
|
+
name: str(dig(area, "Name")),
|
|
377
|
+
maxInt: areaMaxInt,
|
|
378
|
+
maxLgInt: areaMaxLgInt,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
// ── 公開API ──
|
|
388
|
+
/** 地震関連電文(VXSE51/52/53等)をパース */
|
|
389
|
+
function parseEarthquakeTelegram(msg) {
|
|
390
|
+
try {
|
|
391
|
+
const xmlStr = decodeBody(msg);
|
|
392
|
+
const parsed = parseXml(xmlStr);
|
|
393
|
+
// Report > Body を探す
|
|
394
|
+
const report = dig(parsed, "Report") ||
|
|
395
|
+
dig(parsed, "jmx:Report") ||
|
|
396
|
+
dig(parsed, "jmx_seis:Report");
|
|
397
|
+
if (!report) {
|
|
398
|
+
log.debug("Report ノードが見つかりません");
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
const body = dig(report, "Body");
|
|
402
|
+
const head = dig(report, "Head");
|
|
403
|
+
const info = {
|
|
404
|
+
type: msg.head.type,
|
|
405
|
+
infoType: str(dig(head, "InfoType")),
|
|
406
|
+
title: str(dig(head, "Title")),
|
|
407
|
+
reportDateTime: str(dig(head, "ReportDateTime")),
|
|
408
|
+
headline: str(dig(head, "Headline", "Text")) || null,
|
|
409
|
+
publishingOffice: msg.xmlReport?.control?.publishingOffice || "",
|
|
410
|
+
isTest: msg.head.test,
|
|
411
|
+
};
|
|
412
|
+
// 震源
|
|
413
|
+
// Earthquakeノードの取得(配列の場合は先頭を使用)
|
|
414
|
+
let earthquake = dig(body, "Earthquake");
|
|
415
|
+
if (Array.isArray(earthquake)) {
|
|
416
|
+
earthquake = earthquake[0];
|
|
417
|
+
}
|
|
418
|
+
if (earthquake) {
|
|
419
|
+
info.earthquake = extractEarthquake(earthquake);
|
|
420
|
+
}
|
|
421
|
+
// 震度
|
|
422
|
+
info.intensity = extractIntensity(body);
|
|
423
|
+
// 津波
|
|
424
|
+
info.tsunami = extractTsunami(body);
|
|
425
|
+
return info;
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
log.error(`地震電文パースエラー: ${err instanceof Error ? err.message : err}`);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/** EEW電文をパース */
|
|
433
|
+
function parseEewTelegram(msg) {
|
|
434
|
+
try {
|
|
435
|
+
const xmlStr = decodeBody(msg);
|
|
436
|
+
const parsed = parseXml(xmlStr);
|
|
437
|
+
const report = dig(parsed, "Report") ||
|
|
438
|
+
dig(parsed, "jmx:Report") ||
|
|
439
|
+
dig(parsed, "jmx_seis:Report");
|
|
440
|
+
if (!report)
|
|
441
|
+
return null;
|
|
442
|
+
const head = dig(report, "Head");
|
|
443
|
+
const body = dig(report, "Body");
|
|
444
|
+
// 仮定震源要素の検出
|
|
445
|
+
const earthquake = dig(body, "Earthquake");
|
|
446
|
+
const earthquakeCondition = str(dig(earthquake, "Condition"));
|
|
447
|
+
const assumedHypocenterByCondition = isAssumedHypocenterCondition(earthquakeCondition);
|
|
448
|
+
const info = {
|
|
449
|
+
type: msg.head.type,
|
|
450
|
+
infoType: str(dig(head, "InfoType")),
|
|
451
|
+
title: str(dig(head, "Title")),
|
|
452
|
+
reportDateTime: str(dig(head, "ReportDateTime")),
|
|
453
|
+
headline: str(dig(head, "Headline", "Text")) || null,
|
|
454
|
+
publishingOffice: msg.xmlReport?.control?.publishingOffice || "",
|
|
455
|
+
serial: str(dig(head, "Serial")) || null,
|
|
456
|
+
eventId: str(dig(head, "EventID")) || null,
|
|
457
|
+
isTest: msg.head.test,
|
|
458
|
+
isWarning: msg.classification === "eew.warning",
|
|
459
|
+
isAssumedHypocenter: false,
|
|
460
|
+
};
|
|
461
|
+
info.maxIntChangeReason = parseMaxIntChangeReason(body);
|
|
462
|
+
if (earthquake) {
|
|
463
|
+
info.earthquake = extractEarthquake(earthquake);
|
|
464
|
+
}
|
|
465
|
+
const forecastResult = extractEewForecastAreas(body);
|
|
466
|
+
const hasPlumArea = forecastResult?.hasPlumArea ?? false;
|
|
467
|
+
if (forecastResult && forecastResult.areas.length > 0) {
|
|
468
|
+
info.forecastIntensity = {
|
|
469
|
+
...(forecastResult.maxLgInt ? { maxLgInt: forecastResult.maxLgInt } : {}),
|
|
470
|
+
areas: forecastResult.areas,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const assumedHypocenterByFallback = isAssumedHypocenterFallbackPattern(info.earthquake) &&
|
|
474
|
+
(info.maxIntChangeReason === 9 || hasPlumArea);
|
|
475
|
+
info.isAssumedHypocenter =
|
|
476
|
+
assumedHypocenterByCondition || assumedHypocenterByFallback;
|
|
477
|
+
// NextAdvisory (最終報)
|
|
478
|
+
const nextAdvisory = str(dig(body, "NextAdvisory"));
|
|
479
|
+
if (nextAdvisory) {
|
|
480
|
+
info.nextAdvisory = nextAdvisory.trim();
|
|
481
|
+
}
|
|
482
|
+
return info;
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
log.error(`EEW電文パースエラー: ${err instanceof Error ? err.message : err}`);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/** 津波電文(VTSE41/51/52)をパース */
|
|
490
|
+
function parseTsunamiTelegram(msg) {
|
|
491
|
+
try {
|
|
492
|
+
const xmlStr = decodeBody(msg);
|
|
493
|
+
const parsed = parseXml(xmlStr);
|
|
494
|
+
const report = dig(parsed, "Report") ||
|
|
495
|
+
dig(parsed, "jmx:Report") ||
|
|
496
|
+
dig(parsed, "jmx_seis:Report");
|
|
497
|
+
if (!report) {
|
|
498
|
+
log.debug("Report ノードが見つかりません");
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
const head = dig(report, "Head");
|
|
502
|
+
const body = dig(report, "Body");
|
|
503
|
+
const warningComment = dig(body, "Comments", "WarningComment");
|
|
504
|
+
const warningCommentText = Array.isArray(warningComment)
|
|
505
|
+
? str(dig(warningComment[0], "Text"))
|
|
506
|
+
: str(dig(warningComment, "Text"));
|
|
507
|
+
const info = {
|
|
508
|
+
type: msg.head.type,
|
|
509
|
+
infoType: str(dig(head, "InfoType")),
|
|
510
|
+
title: str(dig(head, "Title")),
|
|
511
|
+
reportDateTime: str(dig(head, "ReportDateTime")),
|
|
512
|
+
headline: str(dig(head, "Headline", "Text")) || null,
|
|
513
|
+
publishingOffice: msg.xmlReport?.control?.publishingOffice || "",
|
|
514
|
+
warningComment: warningCommentText,
|
|
515
|
+
isTest: msg.head.test,
|
|
516
|
+
};
|
|
517
|
+
const tsunami = dig(body, "Tsunami");
|
|
518
|
+
const forecastItems = dig(tsunami, "Forecast", "Item");
|
|
519
|
+
if (Array.isArray(forecastItems)) {
|
|
520
|
+
const forecast = [];
|
|
521
|
+
for (const item of forecastItems) {
|
|
522
|
+
const area = first(dig(item, "Area"));
|
|
523
|
+
const category = first(dig(item, "Category"));
|
|
524
|
+
const kind = first(dig(category, "Kind"));
|
|
525
|
+
const areaName = str(dig(area, "Name")).trim();
|
|
526
|
+
if (!areaName) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const maxHeightDescription = str(dig(item, "MaxHeight", "jmx_eb:TsunamiHeight", "@_description")) ||
|
|
530
|
+
str(dig(item, "MaxHeight", "TsunamiHeight", "@_description"));
|
|
531
|
+
const firstHeight = str(dig(item, "FirstHeight", "ArrivalTime")) ||
|
|
532
|
+
str(dig(item, "FirstHeight", "Condition"));
|
|
533
|
+
forecast.push({
|
|
534
|
+
areaName,
|
|
535
|
+
kind: str(dig(kind, "Name")),
|
|
536
|
+
maxHeightDescription,
|
|
537
|
+
firstHeight,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
if (forecast.length > 0) {
|
|
541
|
+
info.forecast = forecast;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const observations = extractTsunamiObservations(tsunami);
|
|
545
|
+
if (observations.length > 0) {
|
|
546
|
+
info.observations = observations;
|
|
547
|
+
}
|
|
548
|
+
const estimations = extractTsunamiEstimations(tsunami);
|
|
549
|
+
if (estimations.length > 0) {
|
|
550
|
+
info.estimations = estimations;
|
|
551
|
+
}
|
|
552
|
+
let earthquake = dig(body, "Earthquake");
|
|
553
|
+
if (Array.isArray(earthquake)) {
|
|
554
|
+
earthquake = earthquake[0];
|
|
555
|
+
}
|
|
556
|
+
if (earthquake) {
|
|
557
|
+
info.earthquake = extractEarthquake(earthquake);
|
|
558
|
+
}
|
|
559
|
+
return info;
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
log.error(`津波電文パースエラー: ${err instanceof Error ? err.message : err}`);
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/** 地震活動テキスト電文(VXSE56/VXSE60/VZSE40)をパース */
|
|
567
|
+
function parseSeismicTextTelegram(msg) {
|
|
568
|
+
try {
|
|
569
|
+
const xmlStr = decodeBody(msg);
|
|
570
|
+
const parsed = parseXml(xmlStr);
|
|
571
|
+
const report = dig(parsed, "Report") ||
|
|
572
|
+
dig(parsed, "jmx:Report") ||
|
|
573
|
+
dig(parsed, "jmx_seis:Report");
|
|
574
|
+
if (!report) {
|
|
575
|
+
log.debug("Report ノードが見つかりません");
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
const head = dig(report, "Head");
|
|
579
|
+
const body = dig(report, "Body");
|
|
580
|
+
const info = {
|
|
581
|
+
type: msg.head.type,
|
|
582
|
+
infoType: str(dig(head, "InfoType")),
|
|
583
|
+
title: str(dig(head, "Title")),
|
|
584
|
+
reportDateTime: str(dig(head, "ReportDateTime")),
|
|
585
|
+
headline: str(dig(head, "Headline", "Text")) || null,
|
|
586
|
+
publishingOffice: msg.xmlReport?.control?.publishingOffice || "",
|
|
587
|
+
bodyText: str(dig(body, "Text")),
|
|
588
|
+
isTest: msg.head.test,
|
|
589
|
+
};
|
|
590
|
+
return info;
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
log.error(`地震活動テキスト電文パースエラー: ${err instanceof Error ? err.message : err}`);
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/** 南海トラフ関連電文(VYSE50/51/52/VYSE60)をパース */
|
|
598
|
+
function parseNankaiTroughTelegram(msg) {
|
|
599
|
+
try {
|
|
600
|
+
const xmlStr = decodeBody(msg);
|
|
601
|
+
const parsed = parseXml(xmlStr);
|
|
602
|
+
const report = dig(parsed, "Report") ||
|
|
603
|
+
dig(parsed, "jmx:Report") ||
|
|
604
|
+
dig(parsed, "jmx_seis:Report");
|
|
605
|
+
if (!report) {
|
|
606
|
+
log.debug("Report ノードが見つかりません");
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
const head = dig(report, "Head");
|
|
610
|
+
const body = dig(report, "Body");
|
|
611
|
+
const info = {
|
|
612
|
+
type: msg.head.type,
|
|
613
|
+
infoType: str(dig(head, "InfoType")),
|
|
614
|
+
title: str(dig(head, "Title")),
|
|
615
|
+
reportDateTime: str(dig(head, "ReportDateTime")),
|
|
616
|
+
headline: str(dig(head, "Headline", "Text")) || null,
|
|
617
|
+
publishingOffice: msg.xmlReport?.control?.publishingOffice || "",
|
|
618
|
+
bodyText: "",
|
|
619
|
+
isTest: msg.head.test,
|
|
620
|
+
};
|
|
621
|
+
// EarthquakeInfo がある場合 (通常の発表電文)
|
|
622
|
+
const eqInfo = dig(body, "EarthquakeInfo");
|
|
623
|
+
if (eqInfo) {
|
|
624
|
+
// InfoSerial (VYSE60 には存在しない場合がある)
|
|
625
|
+
const infoSerial = dig(eqInfo, "InfoSerial");
|
|
626
|
+
if (infoSerial) {
|
|
627
|
+
const name = str(dig(infoSerial, "Name"));
|
|
628
|
+
const code = str(dig(infoSerial, "Code"));
|
|
629
|
+
if (name && code) {
|
|
630
|
+
info.infoSerial = { name, code };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
info.bodyText = str(dig(eqInfo, "Text"));
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
// 取消電文等: Body > Text 直下
|
|
637
|
+
info.bodyText = str(dig(body, "Text"));
|
|
638
|
+
}
|
|
639
|
+
// NextAdvisory
|
|
640
|
+
const nextAdvisory = str(dig(body, "NextAdvisory"));
|
|
641
|
+
if (nextAdvisory) {
|
|
642
|
+
info.nextAdvisory = nextAdvisory.trim();
|
|
643
|
+
}
|
|
644
|
+
return info;
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
log.error(`南海トラフ関連電文パースエラー: ${err instanceof Error ? err.message : err}`);
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/** 長周期地震動観測情報(VXSE62)をパース */
|
|
652
|
+
function parseLgObservationTelegram(msg) {
|
|
653
|
+
try {
|
|
654
|
+
const xmlStr = decodeBody(msg);
|
|
655
|
+
const parsed = parseXml(xmlStr);
|
|
656
|
+
const report = dig(parsed, "Report") ||
|
|
657
|
+
dig(parsed, "jmx:Report") ||
|
|
658
|
+
dig(parsed, "jmx_seis:Report");
|
|
659
|
+
if (!report) {
|
|
660
|
+
log.debug("Report ノードが見つかりません");
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
const head = dig(report, "Head");
|
|
664
|
+
const body = dig(report, "Body");
|
|
665
|
+
const info = {
|
|
666
|
+
type: msg.head.type,
|
|
667
|
+
infoType: str(dig(head, "InfoType")),
|
|
668
|
+
title: str(dig(head, "Title")),
|
|
669
|
+
reportDateTime: str(dig(head, "ReportDateTime")),
|
|
670
|
+
headline: str(dig(head, "Headline", "Text")) || null,
|
|
671
|
+
publishingOffice: msg.xmlReport?.control?.publishingOffice || "",
|
|
672
|
+
areas: [],
|
|
673
|
+
isTest: msg.head.test,
|
|
674
|
+
};
|
|
675
|
+
// 震源
|
|
676
|
+
let earthquake = dig(body, "Earthquake");
|
|
677
|
+
if (Array.isArray(earthquake)) {
|
|
678
|
+
earthquake = earthquake[0];
|
|
679
|
+
}
|
|
680
|
+
if (earthquake) {
|
|
681
|
+
info.earthquake = extractEarthquake(earthquake);
|
|
682
|
+
}
|
|
683
|
+
const lgDetails = extractLgObservationDetails(body);
|
|
684
|
+
info.maxInt = lgDetails.maxInt;
|
|
685
|
+
info.maxLgInt = lgDetails.maxLgInt;
|
|
686
|
+
info.lgCategory = lgDetails.lgCategory;
|
|
687
|
+
info.areas = lgDetails.areas;
|
|
688
|
+
// コメント
|
|
689
|
+
const freeComment = str(dig(body, "Comments", "FreeFormComment"));
|
|
690
|
+
if (freeComment) {
|
|
691
|
+
info.comment = freeComment.trim();
|
|
692
|
+
}
|
|
693
|
+
// 詳細URI
|
|
694
|
+
const uri = str(dig(body, "Comments", "URI"));
|
|
695
|
+
if (uri) {
|
|
696
|
+
info.detailUri = uri.trim();
|
|
697
|
+
}
|
|
698
|
+
return info;
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
log.error(`長周期地震動観測情報パースエラー: ${err instanceof Error ? err.message : err}`);
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
}
|