所在地預估
-
-
+
-
-
-
diff --git a/app/src/renderer.ts b/app/src/renderer.ts
index 50e35b4..0d80204 100644
--- a/app/src/renderer.ts
+++ b/app/src/renderer.ts
@@ -1,6 +1,8 @@
import { createApp } from "vue";
+import { createPinia } from "pinia";
import App from "./pages/index.vue";
import "./css/index.css";
-createApp(App).mount("#app");
+const pinia = createPinia();
+createApp(App).use(pinia).mount("#app");
diff --git a/app/src/ts/constant.ts b/app/src/ts/constant.ts
new file mode 100644
index 0000000..f51bdaf
--- /dev/null
+++ b/app/src/ts/constant.ts
@@ -0,0 +1,245 @@
+export const constant = {
+ WS_CONFIG: {
+ type: "start",
+ key: "K0Q9Z4BJ23YVGNM7Q0G6D10V5QLFX4",
+ service: [
+ "trem.rts",
+ "websocket.eew",
+ "websocket.report",
+ "websocket.tsunami",
+ "trem.intensity",
+ "cwa.intensity",
+ ],
+ // config : {
+ // "eew.cwa": {
+ // "loc-to-int": false,
+ // },
+ // },
+ },
+
+ MAP_LIST: ["TW", "JP", "CN", "KR", "KP"],
+ COLOR_PRIORITY: { "#28FF28": 2, "#F9F900": 1, "#FF0000": 0 },
+
+ REGION_CODE: {
+ 1001: "臺灣東北部海域",
+ 1002: "臺灣東部海域",
+ 1003: "臺灣東南部海域",
+ 1004: "臺灣西北部海域",
+ 1005: "臺灣西部海域",
+ 1006: "臺灣西南部海域",
+ 1007: "臺灣北部海域",
+ 1008: "臺灣南部海域",
+
+ 1101: "石垣島附近海域",
+ 1102: "宮古島附近海域",
+ 1111: "東沙群島附近海域",
+ 1121: "呂宋島北部海域",
+
+ 2001: "宜蘭縣近海",
+ 2002: "花蓮縣近海",
+ 2003: "臺東縣近海",
+ 2004: "新北市近海",
+ 2005: "基隆市近海",
+ 2006: "桃園市近海",
+ 2007: "新竹縣近海",
+ 2008: "新竹市近海",
+ 2009: "苗栗縣近海",
+ 2010: "臺中市近海",
+ 2011: "彰化縣近海",
+ 2012: "雲林縣近海",
+ 2013: "嘉義縣近海",
+ 2014: "臺南市近海",
+ 2015: "高雄市近海",
+ 2016: "屏東縣近海",
+ 2017: "連江縣近海",
+ 2018: "金門縣近海",
+ 2019: "澎湖縣近海",
+
+ 9999: "未知區域",
+ },
+ BOX_GEOJSON: {},
+ REGION: {},
+ TIME_TABLE: {},
+ TIME_TABLE_OBJECT: [] as unknown[],
+
+ LANG: {},
+ CONFIG_AUTO_SAVE_TIME: 10_000,
+ STATION_INFO_FETCH_TIME: 300_000,
+ API_HTTP_TIMEOUT: 2_500,
+ API_HTTP_RETRY: 5_000,
+ API_WEBSOCKET_RETRY: 5_000,
+ API_WEBSOCKET_VERIFY: 3_000,
+ TAIWAN_BOUNDS: [
+ [25.33, 119.31],
+ [21.88, 122.18],
+ ],
+ AUDIO: {
+ ALERT: new Audio("../audio/ALERT.wav"),
+ EEW: new Audio("../audio/EEW.wav"),
+ INTENSITY: new Audio("../audio/INTENSITY.wav"),
+ PGA1: new Audio("../audio/PGA1.wav"),
+ PGA2: new Audio("../audio/PGA2.wav"),
+ REPORT: new Audio("../audio/REPORT.wav"),
+ SHINDO0: new Audio("../audio/SHINDO0.wav"),
+ SHINDO1: new Audio("../audio/SHINDO1.wav"),
+ SHINDO2: new Audio("../audio/SHINDO2.wav"),
+ TSUNAMI: new Audio("../audio/TSUNAMI.wav"),
+ UPDATE: new Audio("../audio/UPDATE.wav"),
+ },
+ SETTING: {
+ MAP_DISPLAY: {
+ "實測震度 + 預估震度": 1,
+ "即時加速度 + 預估震度": 2,
+ 預估震度: 3,
+ 即時加速度: 4,
+ 實測震度: 5,
+ },
+ LOCAL_ARRAY: {
+ 北部: [
+ "臺北市",
+ "新北市",
+ "基隆市",
+ "新竹市",
+ "桃園市",
+ "新竹縣",
+ "宜蘭縣",
+ ],
+ 中部: ["臺中市", "苗栗縣", "彰化縣", "南投縣", "雲林縣"],
+ 南部: ["高雄市", "臺南市", "嘉義市", "嘉義縣", "屏東縣", "澎湖縣"],
+ 東部: ["花蓮縣", "臺東縣"],
+ 外島: ["金門縣", "連江縣"],
+ 南韓: ["南陽州市"],
+ 中國: ["重慶市"],
+ },
+ SPECIAL_LOCAL: {
+ 南陽州市: ["和道邑"],
+ 重慶市: ["北碚區"],
+ },
+ CHECKBOX_DEF: {
+ "early-warning-CWA": 1,
+ "early-warning-JMA": 1,
+ "early-warning-KMA": 1,
+ "early-warning-NIED": 1,
+ "early-warning-SCDZJ": 1,
+ "graphics-block-auto-zoom": 1,
+ "graphics-show-plates": 1,
+ "other-auto-start": 1,
+ "other-voice": 1,
+ "show-window-detect": 1,
+ "show-window-eew": 1,
+ "show-window-realtime-int": 1,
+ "show-window-report": 1,
+ "sound-effects-EEW": 1,
+ "sound-effects-EEW2": 1,
+ "sound-effects-PAlert": 1,
+ "sound-effects-PGA1": 1,
+ "sound-effects-PGA2": 1,
+ "sound-effects-Report": 1,
+ "sound-effects-Shindo0": 1,
+ "sound-effects-Shindo1": 1,
+ "sound-effects-Shindo2": 1,
+ "sound-effects-Update": 1,
+ "sound-effects-dong": 1,
+ },
+ LOCALSTORAGE_DEF: {
+ location: {
+ city: "臺南市",
+ town: "歸仁區",
+ lat: 22.967286,
+ lon: 120.2940045,
+ },
+ warning: { "realtime-station": "0級", "estimate-int": "0級" },
+ "bg-filter": 20,
+ "bg-percentage": 100,
+ "map-display-effect": 1,
+ },
+ LOCALFALLBACK: {
+ "13379360": "重慶市北碚區",
+ "7735548": "南陽州市和道邑",
+ },
+ STATION_REGION: [] as unknown[],
+ INTENSITY: [
+ "0級",
+ "1級",
+ "2級",
+ "3級",
+ "4級",
+ "5弱",
+ "5強",
+ "6弱",
+ "6強",
+ "7級",
+ ],
+ },
+};
+
+export const variable = {
+ last_map_update: 0,
+ time_cache_list: [] as unknown[],
+ map: null as string | null,
+ map_layer: {
+ eew: {},
+ },
+ subscripted_list: [] as unknown[],
+ station_info: null as string | null,
+ station_icon: null as string | null,
+ time_offset: 0,
+ config: {},
+ _config: "",
+ replay: 0,
+ replay_timestamp: 0,
+ replay_list: [] as unknown[],
+ ws_connected: false,
+ ws_reconnect: true,
+ last_get_data_time: 0,
+ eew_list: {} as string | null,
+ icon_size: 0,
+ intensity_list: {},
+ intensity_geojson: null as string | null,
+ tsunami_geojson: null as string | null,
+ intensity_time: 0,
+ audio: {
+ shindo: -1,
+ pga: -1,
+ status: {
+ shindo: 0,
+ pga: 0,
+ },
+ count: {
+ pga_1: 0,
+ pga_2: 0,
+ shindo_1: 0,
+ shindo_2: 0,
+ },
+ },
+ focus: {
+ bounds: {
+ report: null as string | null,
+ intensity: null as string | null,
+ tsunami: null as string | null,
+ eew: null as LatLngBounds | null,
+ rts: null as string | null,
+ },
+ status: {
+ report: 0,
+ intensity: 0,
+ tsunami: 0,
+ eew: 0,
+ rts: 0,
+ },
+ },
+ speech_status: 0,
+ last_map_hash: "",
+ setting: {
+ station: [] as unknown[],
+ },
+ report: {
+ last: {},
+ more: {},
+ data: [] as unknown[],
+ check_: 1,
+ list_retry: 3,
+ survey: null as string | null,
+ withoutNo: "",
+ },
+};
diff --git a/app/src/ts/data.ts b/app/src/ts/data.ts
new file mode 100644
index 0000000..3bd4a49
--- /dev/null
+++ b/app/src/ts/data.ts
@@ -0,0 +1,116 @@
+import { ref, onMounted, onUnmounted } from "vue";
+import utils from "../ts/utils";
+import { useMapStore } from "../ts/store";
+import path from "path";
+import fs from "fs";
+import { app } from "electron";
+
+const MapStore = useMapStore();
+
+let replay_timer: any | undefined;
+
+function startReplay() {
+ if (MapStore.replay_list.length) {
+ replay_timer = setInterval(() => readReplayFile(), 1000);
+ }
+}
+
+async function ntp() {
+ const res = await fetch(
+ `https://lb-${Math.ceil(Math.random() * 4)}.exptech.com.tw/ntp`,
+ );
+ const data = await res.text();
+ MapStore.time_offset = Number(data) - Date.now();
+}
+
+async function realtimeRts() {
+ const res = await fetch(`${utils.url("lb")}v1/trem/rts`);
+ const data = await res.json();
+ const alert = Object.keys(data.box).length;
+ showRtsDot(data, alert);
+ if (alert) showRtsBox(data.box);
+ MapStore.last_get_data_time = utils.now();
+ document.getElementById("connect").style.color = "goldenrod";
+}
+
+async function realtimeEew() {
+ const res = await fetch(`${utils.url("lb")}v1/eq/eew`);
+ const data = await res.json();
+ for (const eew of data) {
+ eew.timestamp = utils.now();
+ showEew(eew);
+ }
+}
+
+function readReplayFile() {
+ if (!MapStore.replay_list.length) {
+ MapStore.replay = 0;
+ if (replay_timer) clearInterval(replay_timer);
+ return;
+ }
+
+ const name = MapStore.replay_list.shift();
+ const data = JSON.parse(
+ fs
+ .readFileSync(path.join(app.getPath("userData"), `replay/${name}`))
+ .toString(),
+ );
+ const alert = Object.keys(data.rts.box).length;
+ showRtsDot(data.rts, alert);
+ if (alert) showRtsBox(data.rts.box);
+
+ for (const eew of data.eew) {
+ eew.time = data.rts.time;
+ eew.timestamp = utils.now();
+ showEew(eew);
+ }
+
+ for (const intensity of data.intensity) {
+ showIntensity(intensity);
+ }
+
+ MapStore.replay = data.rts.time;
+}
+
+function startIntervals() {
+ const rtsInterval = setInterval(() => {
+ showRtsList();
+ if (!MapStore.replay_list.length) {
+ realtimeRts();
+ realtimeEew();
+ }
+ }, 1000);
+
+ const reportInterval = setInterval(() => {
+ if (Object.keys(MapStore.eew_list).length !== 0) return;
+ report();
+ }, 10000);
+
+ const ntpInterval = setInterval(() => {
+ ntp();
+ }, 60000);
+
+ return { rtsInterval, reportInterval, ntpInterval };
+}
+
+function clearIntervals(intervals: Array
) {
+ // clearInterval(intervals.rtsInterval);
+ // clearInterval(intervals.reportInterval);
+ // clearInterval(intervals.ntpInterval);
+}
+
+export function useData() {
+ const intervals = ref(null);
+
+ onMounted(() => {
+ startReplay();
+ ntp();
+ intervals.value = startIntervals();
+ });
+
+ onUnmounted(() => {
+ if (intervals.value) {
+ clearIntervals(intervals.value);
+ }
+ });
+}
diff --git a/app/src/ts/eew.ts b/app/src/ts/eew.ts
new file mode 100644
index 0000000..1a486b1
--- /dev/null
+++ b/app/src/ts/eew.ts
@@ -0,0 +1,260 @@
+import { ref } from "vue";
+import L from "leaflet";
+import utils from "../ts/utils";
+import { useMapStore } from "../ts/store";
+import crypto from "crypto";
+import townJson from "../../resource/map/town.json";
+
+export function startEewInterval() {
+ const MapStore = useMapStore();
+
+ let draw_lock = false;
+ let last_show_epicenter_time = 0;
+ const last_map_count = 0;
+
+ setInterval(() => {
+ const _eew_list = Object.keys(MapStore.eew_list);
+
+ if (!_eew_list.length) return;
+
+ if (draw_lock) return;
+ draw_lock = true;
+ MapStore.focus.bounds.eew = L.latLngBounds(null, null);
+ MapStore.focus.status.eew = 1;
+ for (const id of _eew_list) {
+ const data = MapStore.eew_list[id].data;
+ const now_time = data.time + (utils.now() - data.timestamp);
+ if (now_time - data.eq.time > 240_000) {
+ if (MapStore.eew_list[data.id].layer.s)
+ MapStore.eew_list[data.id].layer.s.remove();
+ if (MapStore.eew_list[data.id].layer.s_fill)
+ MapStore.eew_list[data.id].layer.s_fill.remove();
+ if (MapStore.eew_list[data.id].layer.p)
+ MapStore.eew_list[data.id].layer.p.remove();
+ MapStore.eew_list[data.id].layer.epicenterIcon.remove();
+ delete MapStore.eew_list[data.id];
+ MapStore.lastMapUpdate = 0;
+ continue;
+ }
+ const dist = ps_wave_dist(data.eq.depth, data.eq.time, now_time);
+ const p_dist = dist.p_dist < 0 ? 0 : dist.p_dist;
+ const s_dist = dist.s_dist < 0 ? 0 : dist.s_dist;
+ MapStore.eew_list[data.id].dist = s_dist;
+ const s_t = dist.s_t;
+ if (MapStore.eew_list[data.id].layer.p)
+ MapStore.eew_list[data.id].layer.p.setRadius(p_dist);
+ if (MapStore.eew_list[data.id].layer.s)
+ MapStore.eew_list[data.id].layer.s.setRadius(s_dist);
+ if (MapStore.eew_list[data.id].layer.s_fill)
+ MapStore.eew_list[data.id].layer.s_fill.setRadius(s_dist);
+ if (_eew_list[last_map_count] == id) {
+ MapStore.focus.bounds.eew.extend([data.eq.lat, data.eq.lon]);
+ if (data.detail == 0) {
+ console.log(true);
+ } else if (!data.eq.max)
+ MapStore.focus.bounds.eew.extend(
+ MapStore.eew_list[data.id].layer.s.getBounds(),
+ );
+ else {
+ const intensity_list =
+ MapStore.eew_list[_eew_list[last_map_count]].eew_intensity_list;
+ for (const name of Object.keys(intensity_list)) {
+ const intensity = utils.intensity_float_to_int(
+ intensity_list[name].i,
+ );
+ if (intensity > 1 && s_dist / 1000 > intensity_list[name].dist)
+ MapStore.focus.bounds.eew.extend([
+ intensity_list[name].lat,
+ intensity_list[name].lon,
+ ]);
+ }
+ MapStore.focus.bounds.eew.extend(MapStore.focus.bounds.rts);
+ }
+ }
+ if (s_t) {
+ const progress = Math.floor(
+ ((now_time - data.eq.time) / 1000 / s_t) * 100,
+ );
+ const progress_bar = ``;
+ MapStore.eew_list[data.id].layer.epicenterTooltip = true;
+ MapStore.eew_list[data.id].layer.epicenterIcon.bindTooltip(
+ progress_bar,
+ {
+ opacity: 1,
+ permanent: true,
+ direction: "right",
+ offset: [10, 0],
+ className: "progress-tooltip",
+ },
+ );
+ } else if (MapStore.eew_list[data.id].layer.epicenterTooltip) {
+ MapStore.eew_list[data.id].layer.epicenterIcon.unbindTooltip();
+ delete MapStore.eew_list[data.id].layer.epicenterTooltip;
+ }
+ }
+
+ const time_now = utils.now();
+ if (time_now - last_show_epicenter_time > 1000) {
+ last_show_epicenter_time = time_now;
+ const flashElements = document.getElementsByClassName("flash");
+ for (const item of flashElements) item.classList.remove("hidden");
+ setTimeout(() => {
+ for (const item of flashElements) item.classList.add("hidden");
+ }, 500);
+ }
+
+ draw_lock = false;
+ }, 0);
+
+ const lastMapUpdate = ref(0);
+
+ setInterval(() => {
+ const _eew_list = Object.keys(MapStore.eew_list);
+
+ if (!_eew_list.length) return;
+
+ const nowLocalTime = Date.now();
+ if (nowLocalTime - lastMapUpdate.value < 10000) return;
+ lastMapUpdate.value = nowLocalTime;
+
+ let lastMapCount = 0;
+ lastMapCount++;
+ if (lastMapCount >= _eew_list.length) lastMapCount = 0;
+
+ const data = MapStore.eew_list[_eew_list[lastMapCount]].data;
+
+ if (
+ !MapStore.focus.status.intensity &&
+ MapStore.eew_list[_eew_list[lastMapCount]].eew_intensity_list
+ ) {
+ const hash = crypto
+ .createHash("sha256")
+ .update(
+ JSON.stringify(
+ MapStore.eew_list[_eew_list[lastMapCount]].eew_intensity_list,
+ ),
+ );
+ const digest = hash.digest("hex");
+ if (MapStore.lastMapHash !== digest) {
+ MapStore.lastMapHash = digest;
+ if (MapStore.intensity_geojson) MapStore.intensity_geojson.remove();
+ if (data.status !== 3) {
+ MapStore.intensity_geojson = L.geoJson(
+ townJson as GeoJSON.FeatureCollection,
+ {
+ style: (args) => {
+ const name =
+ args.properties.COUNTYNAME + " " + args.properties.TOWNNAME;
+ const intensity = utils.intensity_float_to_int(
+ MapStore.eew_list[_eew_list[lastMapCount]].eew_intensity_list[
+ name
+ ].i,
+ );
+ let color = !intensity
+ ? "#3F4045"
+ : utils.int_to_color(intensity);
+ let nsspe = 0;
+
+ if (data.eq.area) {
+ for (const i of Object.keys(data.eq.area)) {
+ if (
+ data.eq.area[i].includes(
+ utils
+ .region_string_to_code(
+ MapStore.REGION,
+ args.properties.COUNTYNAME,
+ args.properties.TOWNNAME,
+ )
+ .toString(),
+ )
+ ) {
+ nsspe = Number(i);
+ break;
+ }
+ }
+ }
+
+ if (nsspe) color = utils.int_to_color(nsspe);
+
+ return {
+ color:
+ intensity == 4 || intensity == 5 || intensity == 6
+ ? "grey"
+ : "white",
+ weight: nsspe ? 1.5 : 0.4,
+ fillColor: color,
+ fillOpacity: 1,
+ };
+ },
+ },
+ ).addTo(MapStore.map as L.Map);
+ }
+ }
+ }
+
+ console.log(data);
+ // $("#info-depth").text(data.eq.depth);
+ // $("#info-no").text(`第${data.serial}報${data.final ? "(最終)" : ""}`);
+ // $("#info-loc").text(data.eq.loc);
+ // $("#info-mag").text(data.eq.mag.toFixed(1));
+ // $("#info-time").text(utils.formatTime(data.eq.time));
+ // $("#info-title-box-type").text(
+ // _eew_list.length > 1 ? `${lastMapCount + 1}/${_eew_list.length} ` : "",
+ // ) +
+ // (!data.status
+ // ? `地震速報|${data.author.toUpperCase()}`
+ // : data.status == 1
+ // ? `緊急地震速報|${data.author.toUpperCase()}`
+ // : `地震速報(取消)|${data.author.toUpperCase()}`);
+ // $("#info-box").css(
+ // "backgroundColor",
+ // !data.status ? "#FF9900" : data.status == 1 ? "#C00000" : "#505050",
+ // );
+ // const infoIntensity = $("#info-intensity");
+ // infoIntensity.text(utils.intensity_list[data.eq.max]);
+ // infoIntensity.attr(
+ // "class",
+ // `info-body-title-title-box intensity-${data.eq.max}`,
+ // );
+ }, 1000);
+
+ function ps_wave_dist(depth: number, time: number, now: number) {
+ let p_dist = 0;
+ let s_dist = 0;
+ let s_t = 0;
+
+ const t = (now - time) / 1000;
+
+ const _time_table =
+ MapStore.TIME_TABLE[utils.findClosest(MapStore.TIME_TABLE_OBJECT, depth)];
+ let prev_table = null;
+ for (const table of _time_table) {
+ if (!p_dist && table.P > t)
+ if (prev_table) {
+ const t_diff = table.P - prev_table.P;
+ const r_diff = table.R - prev_table.R;
+ const t_offset = t - prev_table.P;
+ const r_offset = (t_offset / t_diff) * r_diff;
+ p_dist = prev_table.R + r_offset;
+ } else p_dist = table.R;
+
+ if (!s_dist && table.S > t)
+ if (prev_table) {
+ const t_diff = table.S - prev_table.S;
+ const r_diff = table.R - prev_table.R;
+ const t_offset = t - prev_table.S;
+ const r_offset = (t_offset / t_diff) * r_diff;
+ s_dist = prev_table.R + r_offset;
+ } else {
+ s_dist = table.R;
+ s_t = table.S;
+ }
+ if (p_dist && s_dist) break;
+ prev_table = table;
+ }
+
+ p_dist *= 1000;
+ s_dist *= 1000;
+ return { p_dist, s_dist, s_t };
+ }
+}
diff --git a/app/src/ts/map.ts b/app/src/ts/map.ts
deleted file mode 100644
index 4f60033..0000000
--- a/app/src/ts/map.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import L from "leaflet";
-import "leaflet/dist/leaflet.css";
-
-const MAP_LIST = ["TW", "JP", "CN", "KR", "KP"];
-
-export function initializeMap(mapElement: HTMLElement): void {
- const map = L.map(mapElement, {
- maxBounds: [
- [60, 50],
- [10, 180],
- ],
- preferCanvas: true,
- attributionControl: false,
- zoomSnap: 0.25,
- zoomDelta: 0.25,
- doubleClickZoom: false,
- zoomControl: false,
- minZoom: 5.5,
- maxZoom: 10,
- });
-
- map.createPane("circlePane");
- map.getPane("circlePane").style.zIndex = 10;
-
- map.createPane("detection");
- map.getPane("detection").style.zIndex = 2000;
-
- for (const map_name of MAP_LIST) {
- fetch(`../resource/map/${map_name}.json`)
- .then((response) => response.json())
- .then((data) => {
- L.geoJSON(data, {
- edgeBufferTiles: 2,
- minZoom: 5.5,
- maxZoom: 10,
- style: {
- weight: 0.6,
- color: map_name === "TW" ? "white" : "gray",
- fillColor: "#3F4045",
- fillOpacity: 0.5,
- },
- }).addTo(map);
- })
- .catch((error) => {
- console.error("Error loading GeoJSON:", error);
- });
- }
-
- map.setView([23.6, 120.4], 7.8);
-
- map.on("zoomend", updateIconSize);
-
- const variable = { eew_list: [] };
- function updateIconSize(): void {
- const icon_size = (Number(map.getZoom().toFixed(1)) - 7.8) * 2;
-
- for (const key in variable.eew_list) {
- const oldMarker = variable.eew_list[key].layer.epicenterIcon;
- const newIconSize = [40 + icon_size * 3, 40 + icon_size * 3];
-
- const icon = variable.eew_list[key].layer.epicenterIcon.options.icon;
- icon.options.iconSize = newIconSize;
- oldMarker.setIcon(icon);
-
- if (oldMarker.getTooltip()) {
- oldMarker.bindTooltip(oldMarker.getTooltip()._content, {
- opacity: 1,
- permanent: true,
- direction: "right",
- offset: [newIconSize[0] / 2, 0],
- className: "progress-tooltip",
- });
- }
-
- if (variable.eew_list[key].cancel) {
- const iconElement = oldMarker.getElement();
- if (iconElement) {
- iconElement.style.opacity = "0.5";
- iconElement.className = "cancel";
- iconElement.style.visibility = "visible";
- }
- }
- }
- }
-}
diff --git a/app/src/ts/rts.ts b/app/src/ts/rts.ts
index 3ff6679..4934c39 100644
--- a/app/src/ts/rts.ts
+++ b/app/src/ts/rts.ts
@@ -1,5 +1,6 @@
-import { ref, onMounted, onUnmounted } from "vue";
import utils from "../ts/utils";
+import { useMapStore } from "../ts/store";
+import L from "leaflet";
const MAX_RETRY_ATTEMPTS = 3;
@@ -34,87 +35,179 @@ export async function getStationInfo(): Promise {
});
}
-const variable = {
- last_map_update: 0,
- map_layer: {
- eew: {},
- },
- station_icon: {},
- time_offset: 0,
- config: {},
- _config: "",
- replay: 0,
- replay_timestamp: 0,
- ws_connected: false,
- ws_reconnect: true,
- last_get_data_time: 0,
- eew_list: {},
- icon_size: 0,
- intensity_list: {},
- intensity_time: 0,
- audio: {
- shindo: -1,
- pga: -1,
- status: {
- shindo: 0,
- pga: 0,
- },
- count: {
- pga_1: 0,
- pga_2: 0,
- shindo_1: 0,
- shindo_2: 0,
- },
- },
- focus: {
- status: {
- report: 0,
- intensity: 0,
- tsunami: 0,
- eew: 0,
- rts: 0,
- },
- },
- speech_status: 0,
- last_map_hash: "",
- report: {
- last: {},
- more: {},
- check_: 1,
- list_retry: 3,
- withoutNo: "",
- },
-};
-
-export const useTimeUpdate = () => {
- const docTime = ref("");
- const docColor = ref("white");
-
- const updateTime = () => {
- const _now = utils.now();
-
- if (variable.replay) {
- docTime.value = utils.formatTime(variable.replay);
- docColor.value = "text-yellow-300";
- } else if (_now - variable.last_get_data_time > 5000) {
- docTime.value = utils.formatTime(_now);
- docColor.value = "text-red-600";
- } else {
- docTime.value = utils.formatTime(_now);
- docColor.value = "text-white";
+export default function showRtsDot(data: any, alert: boolean) {
+ const MapStore = useMapStore();
+ if (!MapStore.station_info) return;
+
+ if (!alert) {
+ MapStore.audio = {
+ shindo: -1,
+ pga: -1,
+ status: {
+ shindo: 0,
+ pga: 0,
+ },
+ count: {
+ pga_1: 0,
+ pga_2: 0,
+ shindo_1: 0,
+ shindo_2: 0,
+ },
+ };
+ } else {
+ for (const area of data.int) {
+ const box = document.createElement("div");
+ box.className = "realtime-item";
+
+ const int = document.createElement("div");
+ int.className = `realtime-intensity intensity-${area.i}`;
+ int.textContent = utils.int_to_intensity(area.i);
+
+ const loc = document.createElement("div");
+ loc.className = "realtime-location";
+ const locStr = utils.region_code_to_string(MapStore.REGION, area.code);
+ loc.textContent = `${locStr.city}${locStr.town}`;
+
+ box.appendChild(int);
+ box.appendChild(loc);
+ // realtime_list.value.appendChild(box);
}
- };
+ }
- onMounted(() => {
- updateTime();
- const timer = setInterval(updateTime, 1000);
+ for (const id of Object.keys(MapStore.station_icon)) {
+ MapStore.station_icon[id].remove();
+ delete MapStore.station_icon[id];
+ }
- onUnmounted(() => {
- clearInterval(timer);
- });
- });
- return {
- docTime,
- docColor,
- };
-};
+ let maxPga = -1;
+ let maxShindo = -1;
+ let trigger = 0;
+ let level = 0;
+
+ for (const id of Object.keys(data.station)) {
+ if (!MapStore.station_info[id]) continue;
+ const i = utils.intensity_float_to_int(data.station[id].i);
+ const intensityClass = `pga_dot pga_${data.station[id].i.toString().replace(".", "_")}`;
+ const I = utils.intensity_float_to_int(data.station[id].I);
+ const icon = !data.station[id].alert
+ ? L.divIcon({
+ className: intensityClass,
+ html: "",
+ iconSize: [10 + MapStore.icon_size, 10 + MapStore.icon_size],
+ })
+ : L.divIcon({
+ className: I == 0 ? "pga_dot pga-intensity-0" : `dot intensity-${I}`,
+ html: `${I == 0 ? "" : utils.int_to_intensity(I)}`,
+ iconSize: [20 + MapStore.icon_size, 20 + MapStore.icon_size],
+ });
+
+ const pga = data.station[id].pga;
+ const info = MapStore.station_info[id].info.at(-1);
+ if (data.station[id].alert) {
+ trigger++;
+ level += pga;
+ }
+
+ let loc = utils.region_code_to_string(MapStore.REGION, info.code);
+ if (!loc) loc = "未知區域";
+ else loc = `${loc.city}${loc.town}`;
+
+ if (maxPga < pga) maxPga = pga;
+ if (maxShindo < i) maxShindo = i;
+
+ const stationText = `${loc}${id} | ${MapStore.station_info[id].net}
`;
+
+ if (alert) {
+ if (pga > MapStore.audio.pga) {
+ if (pga > 200 && MapStore.audio.status.pga != 2) {
+ if (checkbox("sound-effects-PGA2") == 1) MapStore.AUDIO.PGA2.play();
+ MapStore.audio.status.pga = 2;
+ } else if (pga > 8 && !MapStore.audio.status.pga) {
+ if (checkbox("sound-effects-PGA1") == 1) MapStore.AUDIO.PGA1.play();
+ MapStore.audio.status.pga = 1;
+ }
+ MapStore.audio.pga = pga;
+ if (pga > 8) MapStore.audio.count.pga_1 = 0;
+ if (pga > 200) MapStore.audio.count.pga_2 = 0;
+ }
+ if (i > MapStore.audio.shindo) {
+ if (i > 3 && MapStore.audio.status.shindo != 3) {
+ if (checkbox("sound-effects-Shindo2") == 1)
+ MapStore.AUDIO.SHINDO2.play();
+ MapStore.audio.status.shindo = 3;
+ } else if (i > 1 && MapStore.audio.status.shindo < 2) {
+ if (checkbox("sound-effects-Shindo1") == 1)
+ MapStore.AUDIO.SHINDO1.play();
+ MapStore.audio.status.shindo = 2;
+ } else if (!MapStore.audio.status.shindo) {
+ if (checkbox("sound-effects-Shindo0") == 1)
+ MapStore.AUDIO.SHINDO0.play();
+ MapStore.audio.status.shindo = 1;
+ }
+ if (i > 3) MapStore.audio.count.shindo_2 = 0;
+ if (i > 1) MapStore.audio.count.shindo_1 = 0;
+ MapStore.audio.shindo = i;
+ }
+ }
+ if (MapStore.audio.pga && maxPga < MapStore.audio.pga) {
+ if (MapStore.audio.status.pga == 2) {
+ if (maxPga < 200) {
+ MapStore.audio.count.pga_2++;
+ if (MapStore.audio.count.pga_2 >= 30) {
+ MapStore.audio.count.pga_2 = 0;
+ MapStore.audio.status.pga = 1;
+ }
+ } else MapStore.audio.count.pga_2 = 0;
+ } else if (MapStore.audio.status.pga == 1) {
+ if (maxPga < 8) {
+ MapStore.audio.count.pga_1++;
+ if (MapStore.audio.count.pga_1 >= 30) {
+ MapStore.audio.count.pga_1 = 0;
+ MapStore.audio.status.pga = 0;
+ }
+ } else MapStore.audio.count.pga_1 = 0;
+ }
+ MapStore.audio.pga = maxPga;
+ }
+ if (MapStore.audio.shindo && maxShindo < MapStore.audio.shindo) {
+ if (MapStore.audio.status.shindo == 3) {
+ if (maxShindo < 4) {
+ MapStore.audio.count.shindo_2++;
+ if (MapStore.audio.count.shindo_2 >= 15) {
+ MapStore.audio.count.shindo_2 = 0;
+ MapStore.audio.status.shindo = 2;
+ }
+ } else MapStore.audio.count.shindo_2 = 0;
+ } else if (MapStore.audio.status.shindo == 2) {
+ if (maxShindo < 2) {
+ MapStore.audio.count.shindo_1++;
+ if (MapStore.audio.count.shindo_1 >= 15) {
+ MapStore.audio.count.shindo_1 = 0;
+ MapStore.audio.status.shindo = 1;
+ }
+ } else MapStore.audio.count.shindo_1 = 0;
+ }
+ MapStore.audio.shindo = maxShindo;
+ }
+
+ if (
+ (!Object.keys(data.box).length &&
+ !Object.keys(MapStore.eew_list).length) ||
+ data.station[id].alert
+ ) {
+ if (!MapStore.focus.status.intensity) {
+ MapStore.station_icon[id] = L.marker([info.lat, info.lon], {
+ icon: icon,
+ zIndexOffset: I * 1000,
+ })
+ .bindTooltip(stationText, { opacity: 1 })
+ .addTo(MapStore.map);
+ }
+ }
+ }
+
+ // max_pga_text.value.textContent = `${maxPga > 999 ? "999+" : maxPga.toFixed(2)} gal`;
+ // max_pga_text.value.className = `intensity-${alert ? maxShindo : 0}`;
+ // document.getElementById("trigger").textContent = trigger.toString();
+ // document.getElementById("level").textContent = Math.round(level).toString();
+}
diff --git a/app/src/ts/store.ts b/app/src/ts/store.ts
new file mode 100644
index 0000000..43aa563
--- /dev/null
+++ b/app/src/ts/store.ts
@@ -0,0 +1,203 @@
+import { defineStore } from "pinia";
+import L from "leaflet";
+import "leaflet/dist/leaflet.css";
+
+const MAP_LIST = ["TW", "JP", "CN", "KR", "KP"];
+
+export const useMapStore = defineStore("map", {
+ actions: {
+ init_map(mapElement: HTMLElement) {
+ const map = L.map(mapElement, {
+ maxBounds: [
+ [60, 50],
+ [10, 180],
+ ],
+ preferCanvas: true,
+ attributionControl: false,
+ zoomSnap: 0.25,
+ zoomDelta: 0.25,
+ doubleClickZoom: false,
+ zoomControl: false,
+ minZoom: 5.5,
+ maxZoom: 10,
+ });
+
+ map.createPane("circlePane");
+ map.getPane("circlePane").style.zIndex = "10";
+
+ map.createPane("detection");
+ map.getPane("detection").style.zIndex = "2000";
+
+ for (const map_name of MAP_LIST) {
+ fetch(`../resource/map/${map_name}.json`)
+ .then((response) => response.json())
+ .then((data) => {
+ const options = {
+ edgeBufferTiles: 2,
+ minZoom: 5.5,
+ maxZoom: 10,
+ style: {
+ weight: 0.6,
+ color: map_name === "TW" ? "white" : "gray",
+ fillColor: "#3F4045",
+ fillOpacity: 0.5,
+ },
+ };
+
+ L.geoJSON(data, options).addTo(map);
+ })
+ .catch((error) => {
+ console.error("Error loading GeoJSON:", error);
+ });
+ }
+
+ map.setView([23.6, 120.4], 7.8);
+
+ map.on("zoomend", updateIconSize);
+
+ interface EewItem {
+ data: {
+ eq: {
+ lat: number;
+ lon: number;
+ };
+ dist: number;
+ };
+ layer: any;
+ cancel: any;
+ }
+
+ const variable: { eew_list: EewItem[] } = { eew_list: [] };
+ function updateIconSize(): void {
+ const icon_size = (Number(map.getZoom().toFixed(1)) - 7.8) * 2;
+
+ for (const key in variable.eew_list) {
+ const oldMarker = variable.eew_list[key].layer.epicenterIcon;
+ const newIconSize = [40 + icon_size * 3, 40 + icon_size * 3];
+
+ const icon = variable.eew_list[key].layer.epicenterIcon.options.icon;
+ icon.options.iconSize = newIconSize;
+ oldMarker.setIcon(icon);
+
+ if (oldMarker.getTooltip()) {
+ oldMarker.bindTooltip(oldMarker.getTooltip()._content, {
+ opacity: 1,
+ permanent: true,
+ direction: "right",
+ offset: [newIconSize[0] / 2, 0],
+ className: "progress-tooltip",
+ });
+ }
+
+ if (variable.eew_list[key].cancel) {
+ const iconElement = oldMarker.getElement();
+ if (iconElement) {
+ iconElement.style.opacity = "0.5";
+ iconElement.className = "cancel";
+ iconElement.style.visibility = "visible";
+ }
+ }
+ }
+ }
+ this.map = map;
+ },
+ },
+ state: (): MapState => {
+ return {
+ lastMapUpdate: 0,
+ lastMapHash: "",
+ map: null,
+ eew_list: {},
+ layer: "",
+ focus: {
+ bounds: {
+ eew: null,
+ rts: null,
+ },
+ status: {
+ eew: 0,
+ intensity: 0,
+ rts: 0,
+ },
+ },
+ intensity_geojson: null,
+ replay_list: null,
+ station_icon: null,
+ BOX_GEOJSON: {
+ features: {
+ geometry: null,
+ },
+ },
+ TIME_TABLE: {},
+ TIME_TABLE_OBJECT: [],
+ intensity_list: ["0", "1", "2", "3", "4", "5⁻", "5⁺", "6⁻", "6⁺", "7"],
+ last_get_data_time: 0,
+ time_offset: 0,
+ replay: 0,
+ station_info: null,
+ audio: {
+ shindo: -1,
+ pga: -1,
+ status: {
+ shindo: 0,
+ pga: 0,
+ },
+ count: {
+ pga_1: 0,
+ pga_2: 0,
+ shindo_1: 0,
+ shindo_2: 0,
+ },
+ },
+ };
+ },
+});
+
+interface MapState {
+ lastMapUpdate: number;
+ lastMapHash: string;
+ map: L.Map;
+ eew_list?: Record;
+ layer: string;
+ focus: {
+ bounds: {
+ eew: L.LatLngBounds;
+ rts: L.LatLngBounds;
+ };
+ status: {
+ eew: number;
+ intensity: number;
+ rts: number;
+ };
+ };
+ REGION: Record;
+ intensity_geojson: L.GeoJSON | null;
+ TIME_TABLE: Record;
+ TIME_TABLE_OBJECT: Array;
+ intensity_list: Array;
+ replay_list: Array;
+ station_icon: Array;
+ BOX_GEOJSON: {
+ features: {
+ geometry: Array;
+ };
+ };
+ last_get_data_time: number;
+ time_offset: number;
+ replay: number;
+ station_info: string;
+ audio: {
+ shindo: number;
+ pga: number;
+ status: {
+ shindo: number;
+ pga: number;
+ };
+ count: {
+ pga_1: number;
+ pga_2: number;
+ shindo_1: number;
+ shindo_2: number;
+ };
+ };
+}
diff --git a/app/src/ts/tsunami.ts b/app/src/ts/tsunami.ts
index e69de29..b11c287 100644
--- a/app/src/ts/tsunami.ts
+++ b/app/src/ts/tsunami.ts
@@ -0,0 +1,50 @@
+// import { onMounted, onUnmounted } from "vue";
+// import { variable } from "../ts/constant";
+// import L from "leaflet";
+
+// export const useTsunamiUpdate = () => {
+// let tsunamiLayer: L.Layer | null = null;
+
+// const showTsunami = () => {
+// fetch("../resource/map/tsunami.json")
+// .then((response) => response.json())
+// .then((data) => {
+// if (tsunamiLayer) {
+// (variable.map as any).removeLayer(tsunamiLayer);
+// }
+
+// tsunamiLayer = L.vectorGrid
+// .slicer(data, {
+// rendererFactory: L.svg.tile,
+// vectorTileLayerStyles: {
+// sliced: (properties) => ({
+// fillColor: "red",
+// fillOpacity: 1,
+// stroke: true,
+// color: "red",
+// weight: 5,
+// }),
+// },
+// })
+// .addTo(variable.map);
+
+// setTimeout(() => {
+// if (tsunamiLayer) {
+// (variable.map as any).removeLayer(tsunamiLayer);
+// }
+// }, 2000);
+// })
+// .catch((error) => {
+// console.error("Error loading GeoJSON:", error);
+// });
+// };
+
+// onMounted(() => {
+// setInterval(showTsunami, 3000);
+
+// // 清理定時器
+// onUnmounted(() => {
+// clearInterval(showTsunami);
+// });
+// });
+// };
diff --git a/app/src/ts/utils.ts b/app/src/ts/utils.ts
index 86d33b4..7b6590b 100644
--- a/app/src/ts/utils.ts
+++ b/app/src/ts/utils.ts
@@ -1,11 +1,20 @@
-const intensity_list = ["0", "1", "2", "3", "4", "5⁻", "5⁺", "6⁻", "6⁺", "7"];
+import { ref, onMounted, onUnmounted } from "vue";
+import crypto from "crypto";
+import { useMapStore } from "../ts/store";
const utils = {
url: (t: string) => {
return `https://${t}-${Math.ceil(Math.random() * 2)}.exptech.dev/api/`;
},
+ ntp: async () => {
+ const res = await fetch(
+ `https://lb-${Math.ceil(Math.random() * 4)}.exptech.com.tw/ntp`,
+ );
+ const data = await res.text();
+ useMapStore().time_offset = Number(data) - Date.now();
+ },
int_to_intensity: (int: string | number) => {
- return typeof int === "number" ? intensity_list[int] : "0";
+ return typeof int === "number" ? useMapStore().intensity_list[int] : "0";
},
parseJSON: (jsonString: string) => {
try {
@@ -16,6 +25,25 @@ const utils = {
},
now: () => Date.now() + 0,
formatTwoDigits: (n: number) => (n < 10 ? "0" + n : n),
+ generateMD5: (input: string) => {
+ return crypto.createHash("md5").update(input).digest("hex");
+ },
+ region_code_to_string: (region: any, code: string) => {
+ for (const city of Object.keys(region))
+ for (const town of Object.keys(region[city]))
+ if (region[city][town].code == code)
+ return {
+ city,
+ town,
+ lat: region[city][town].lat,
+ lon: region[city][town].lon,
+ };
+ return null;
+ },
+ region_string_to_code: (region: any, city: string, town: string) => {
+ if (region[city][town]) return region[city][town].code;
+ return null;
+ },
formatTime: (timestamp: string | number | Date) => {
const date = new Date(timestamp);
const year = date.getFullYear();
@@ -24,9 +52,172 @@ const utils = {
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
+ findClosest: (arr: number[], target: number) => {
+ return arr.reduce((prev, curr) =>
+ Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev,
+ );
+ },
+ pow: (num: number) => {
+ return Math.pow(num, 2);
+ },
+ eew_area_pga: (lat: number, lon: number, depth: number, mag: number) => {
+ const json: any = {};
+ let eew_max_i = 0;
+
+ for (const city of Object.keys(useMapStore().REGION)) {
+ for (const town of Object.keys(useMapStore().REGION[city])) {
+ const info = useMapStore().REGION[city][town];
+ const dist_surface = utils.distance(lat, lon)(info.lat, info.lon);
+ const dist = Math.sqrt(utils.pow(dist_surface) + utils.pow(depth));
+ const pga =
+ 1.657 * Math.pow(Math.E, 1.533 * mag) * Math.pow(dist, -1.607);
+ let i = utils.pga_to_float(pga);
+
+ if (i >= 4.5) {
+ i = utils.eew_area_pgv([lat, lon], [info.lat, info.lon], depth, mag);
+ }
+
+ if (i > eew_max_i) {
+ eew_max_i = i;
+ }
+
+ json[`${city} ${town}`] = { dist, i, lat: info.lat, lon: info.lon };
+ }
+ }
+
+ json.max_i = eew_max_i;
+ return json;
+ },
+ // eew_location_info: (data: any) => {
+ // const dist_surface = distance(data.lat, data.lon)(
+ // TREM.user.lat,
+ // TREM.user.lon,
+ // );
+ // const dist = Math.sqrt(pow(dist_surface) + pow(data.depth));
+ // const pga =
+ // 1.657 *
+ // Math.pow(Math.E, 1.533 * data.scale) *
+ // Math.pow(dist, -1.607) *
+ // (storage.getItem("site") ?? 1.751);
+ // let i = pga_to_float(pga);
+ // if (i > 3)
+ // i = eew_i(
+ // [data.lat, data.lon],
+ // [TREM.user.lat, TREM.user.lon],
+ // data.depth,
+ // data.scale,
+ // );
+ // return { dist, i };
+ // },
+ eew_area_pgv: (
+ epicenterLocaltion: any,
+ pointLocaltion: any,
+ depth: number,
+ magW: number,
+ ) => {
+ const long = 10 ** (0.5 * magW - 1.85) / 2;
+ const epicenterDistance = utils.distance(
+ epicenterLocaltion[0],
+ epicenterLocaltion[1],
+ )(pointLocaltion[0], pointLocaltion[1]);
+ const hypocenterDistance =
+ (depth ** 2 + epicenterDistance ** 2) ** 0.5 - long;
+ const x = Math.max(hypocenterDistance, 3);
+ const gpv600 =
+ 10 **
+ (0.58 * magW +
+ 0.0038 * depth -
+ 1.29 -
+ Math.log10(x + 0.0028 * 10 ** (0.5 * magW)) -
+ 0.002 * x);
+ const pgv400 = gpv600 * 1.31;
+ const pgv = pgv400 * 1.0;
+ return 2.68 + 1.72 * Math.log10(pgv);
+ },
+ distance: (latA: number, lngA: number) => {
+ return (latB: number, lngB: number) => {
+ latA = (latA * Math.PI) / 180;
+ lngA = (lngA * Math.PI) / 180;
+ latB = (latB * Math.PI) / 180;
+ lngB = (lngB * Math.PI) / 180;
+ const sin_latA = Math.sin(Math.atan(Math.tan(latA)));
+ const sin_latB = Math.sin(Math.atan(Math.tan(latB)));
+ const cos_latA = Math.cos(Math.atan(Math.tan(latA)));
+ const cos_latB = Math.cos(Math.atan(Math.tan(latB)));
+ return (
+ Math.acos(
+ sin_latA * sin_latB + cos_latA * cos_latB * Math.cos(lngA - lngB),
+ ) * 6371.008
+ );
+ };
+ },
+ pga_to_float: (pga: number) => {
+ return 2 * Math.log10(pga) + 0.7;
+ },
+ pga_to_intensity: (pga: number) => {
+ return utils.intensity_float_to_int(utils.pga_to_float(pga));
+ },
+ intensity_float_to_int: (float: number) => {
+ return float < 0
+ ? 0
+ : float < 4.5
+ ? Math.round(float)
+ : float < 5
+ ? 5
+ : float < 5.5
+ ? 6
+ : float < 6
+ ? 7
+ : float < 6.5
+ ? 8
+ : 9;
+ },
+ int_to_color: (int: number) => {
+ const list = [
+ "#202020",
+ "#003264",
+ "#0064C8",
+ "#1E9632",
+ "#FFC800",
+ "#FF9600",
+ "#FF6400",
+ "#FF0000",
+ "#C00000",
+ "#9600C8",
+ ];
+ return list[int];
+ },
+ useTimeUpdate: () => {
+ const docTime = ref("");
+ const docColor = ref("");
+ const updateTime = () => {
+ const _now = utils.now();
+ let colorClass = "text-white";
+ if (useMapStore().replay) {
+ docTime.value = utils.formatTime(useMapStore().replay);
+ colorClass = "text-yellow-300";
+ } else if (_now - useMapStore().last_get_data_time > 5000) {
+ docTime.value = utils.formatTime(_now);
+ colorClass = "text-red-600";
+ } else {
+ docTime.value = utils.formatTime(_now);
+ }
+ docColor.value = colorClass;
+ };
+ onMounted(() => {
+ updateTime();
+ const timer = setInterval(updateTime, 1000);
+ onUnmounted(() => {
+ clearInterval(timer);
+ });
+ });
+ return {
+ docTime,
+ docColor,
+ };
+ },
};
export default utils;