cwadayi commited on
Commit
917008f
·
verified ·
1 Parent(s): 2063ba3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +67 -29
app.py CHANGED
@@ -8,23 +8,28 @@ import pandas as pd
8
  import matplotlib.pyplot as plt
9
  import gradio as gr
10
 
11
- # -------- tabulate(可選)偵測,沒裝也不會壞 --------
12
  try:
13
  import tabulate as _tabulate # noqa: F401
14
  HAS_TABULATE = True
15
  except Exception:
16
  HAS_TABULATE = False
17
 
 
 
 
 
 
 
 
18
  # -----------------------------
19
  # 台北時區 (UTC+8)
20
  # -----------------------------
21
  TAIPEI_TZ = timezone(timedelta(hours=8))
22
 
23
-
24
  def _fmt(dt: datetime) -> str:
25
  return dt.strftime("%Y-%m-%dT%H:%M:%S")
26
 
27
-
28
  def set_time_range(hours=None, days=None):
29
  """依台北時間回傳 (timeFrom, timeTo) 字串"""
30
  now = datetime.now(TAIPEI_TZ)
@@ -36,13 +41,11 @@ def set_time_range(hours=None, days=None):
36
  t_from = now - timedelta(days=3)
37
  return _fmt(t_from), _fmt(now)
38
 
39
-
40
  # -----------------------------
41
  # 取 API 資料
42
  # -----------------------------
43
  API_URL = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001"
44
 
45
-
46
  def fetch_reports(time_from, time_to):
47
  api_key = os.getenv("CWA_API_KEY", "").strip()
48
  if not api_key:
@@ -52,7 +55,6 @@ def fetch_reports(time_from, time_to):
52
  r.raise_for_status()
53
  return r.json()
54
 
55
-
56
  # -----------------------------
57
  # 解析 JSON → DataFrame(彈性容錯)
58
  # -----------------------------
@@ -65,7 +67,6 @@ def _safe_get(d, *keys, default=None):
65
  return default
66
  return cur
67
 
68
-
69
  def _to_float(x):
70
  try:
71
  if x is None or str(x).strip() == "":
@@ -74,7 +75,6 @@ def _to_float(x):
74
  except Exception:
75
  return None
76
 
77
-
78
  def parse_ea0015(obj):
79
  """
80
  支援 records/Records、earthquake/Earthquake 等大小寫差異。
@@ -123,16 +123,14 @@ def parse_ea0015(obj):
123
  df = df.sort_values("OriginTime", ascending=False, na_position="last").reset_index(drop=True)
124
  return df
125
 
126
-
127
  # -----------------------------
128
- # 視覺化(回傳「檔案路徑」以相容舊/新 Gradio)
129
  # -----------------------------
130
- def _save_fig_to_tmp(fig, suffix=".png"):
131
- buf_path = tempfile.NamedTemporaryFile(delete=False, suffix=suffix).name
132
- fig.savefig(buf_path, format="png", dpi=160, bbox_inches="tight")
133
  plt.close(fig)
134
- return buf_path
135
-
136
 
137
  def plot_trend_path(df):
138
  if df.empty:
@@ -145,24 +143,69 @@ def plot_trend_path(df):
145
  fig.autofmt_xdate()
146
  return _save_fig_to_tmp(fig)
147
 
148
-
149
  def plot_map_path(df):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  if df.empty:
151
  return None
152
- lon_min, lon_max, lat_min, lat_max = 119, 123, 21, 26
153
  fig, ax = plt.subplots(figsize=(6, 6))
154
  ax.set_xlim(lon_min, lon_max)
155
  ax.set_ylim(lat_min, lat_max)
156
  mags = df["Magnitude"].fillna(0)
157
  sizes = (mags.clip(lower=0) + 2) ** 3
158
- ax.scatter(df["Lon"], df["Lat"], s=sizes, alpha=0.6, edgecolor="black")
159
- for m in [3, 4, 5, 6]:
160
- ax.scatter([], [], s=((m + 2) ** 3), alpha=0.6, edgecolor="black", label=f"M {m}")
161
- ax.legend(title="Magnitude")
 
 
162
  ax.grid(True, linestyle="--", alpha=0.3)
163
  return _save_fig_to_tmp(fig)
164
 
165
-
166
  # -----------------------------
167
  # 表格輸出(tabulate 可選)
168
  # -----------------------------
@@ -176,7 +219,6 @@ def _format_taipei(series):
176
  except Exception:
177
  return series.astype(str)
178
 
179
-
180
  def _to_simple_md_table(df: pd.DataFrame) -> str:
181
  cols = list(df.columns)
182
  header = "|" + "|".join(cols) + "|\n"
@@ -190,7 +232,6 @@ def _to_simple_md_table(df: pd.DataFrame) -> str:
190
  rows.append("|" + "|".join(cells) + "|")
191
  return header + sep + "\n".join(rows)
192
 
193
-
194
  def df_to_markdown(df, top_n=100):
195
  if df.empty:
196
  return "(查無資料)"
@@ -206,7 +247,6 @@ def df_to_markdown(df, top_n=100):
206
  table = _to_simple_md_table(slim.reset_index(drop=True))
207
  return header + table
208
 
209
-
210
  # -----------------------------
211
  # 主流程
212
  # -----------------------------
@@ -233,7 +273,6 @@ def query_and_render(time_from, time_to, sort_order):
233
  except Exception as e:
234
  return f"錯誤:{e}", None, None, None
235
 
236
-
237
  # -----------------------------
238
  # 介面
239
  # -----------------------------
@@ -261,10 +300,9 @@ with gr.Blocks(fill_height=True) as demo:
261
  run_btn = gr.Button("查詢", variant="primary")
262
 
263
  table_out = gr.Markdown("(尚未查詢)")
264
- # 回傳檔案路徑 → Image 用 filepath 模式最穩
265
  trend_out = gr.Image(label="趨勢圖", type="filepath")
266
- map_out = gr.Image(label="台灣範圍圖", type="filepath")
267
- dl_btn = gr.DownloadButton(label="下載 CSV") # 不使用 file_name 參數,傳路徑即可
268
 
269
  # 快速鍵
270
  btn_12h.click(lambda: set_time_range(hours=12), outputs=[time_from, time_to])
 
8
  import matplotlib.pyplot as plt
9
  import gradio as gr
10
 
11
+ # ---- 可選依賴偵測 ----
12
  try:
13
  import tabulate as _tabulate # noqa: F401
14
  HAS_TABULATE = True
15
  except Exception:
16
  HAS_TABULATE = False
17
 
18
+ try:
19
+ import numpy as np
20
+ import pygmt
21
+ HAS_PYGMT = True
22
+ except Exception:
23
+ HAS_PYGMT = False
24
+
25
  # -----------------------------
26
  # 台北時區 (UTC+8)
27
  # -----------------------------
28
  TAIPEI_TZ = timezone(timedelta(hours=8))
29
 
 
30
  def _fmt(dt: datetime) -> str:
31
  return dt.strftime("%Y-%m-%dT%H:%M:%S")
32
 
 
33
  def set_time_range(hours=None, days=None):
34
  """依台北時間回傳 (timeFrom, timeTo) 字串"""
35
  now = datetime.now(TAIPEI_TZ)
 
41
  t_from = now - timedelta(days=3)
42
  return _fmt(t_from), _fmt(now)
43
 
 
44
  # -----------------------------
45
  # 取 API 資料
46
  # -----------------------------
47
  API_URL = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001"
48
 
 
49
  def fetch_reports(time_from, time_to):
50
  api_key = os.getenv("CWA_API_KEY", "").strip()
51
  if not api_key:
 
55
  r.raise_for_status()
56
  return r.json()
57
 
 
58
  # -----------------------------
59
  # 解析 JSON → DataFrame(彈性容錯)
60
  # -----------------------------
 
67
  return default
68
  return cur
69
 
 
70
  def _to_float(x):
71
  try:
72
  if x is None or str(x).strip() == "":
 
75
  except Exception:
76
  return None
77
 
 
78
  def parse_ea0015(obj):
79
  """
80
  支援 records/Records、earthquake/Earthquake 等大小寫差異。
 
123
  df = df.sort_values("OriginTime", ascending=False, na_position="last").reset_index(drop=True)
124
  return df
125
 
 
126
  # -----------------------------
127
+ # 視覺化(回傳檔案路徑;相容舊/新 Gradio)
128
  # -----------------------------
129
+ def _save_fig_to_tmp(fig, suffix=".png", dpi=180):
130
+ outpath = tempfile.NamedTemporaryFile(delete=False, suffix=suffix).name
131
+ fig.savefig(outpath, format="png", dpi=dpi, bbox_inches="tight")
132
  plt.close(fig)
133
+ return outpath
 
134
 
135
  def plot_trend_path(df):
136
  if df.empty:
 
143
  fig.autofmt_xdate()
144
  return _save_fig_to_tmp(fig)
145
 
 
146
  def plot_map_path(df):
147
+ """
148
+ 優先使用 PyGMT 畫台灣區域地圖;若 PyGMT 不可用,退回 Matplotlib 版。
149
+ 範圍:119/123, 21/26;顏色=深度(km),大小=規模
150
+ """
151
+ lon_min, lon_max, lat_min, lat_max = 119, 123, 21, 26
152
+
153
+ # ---- PyGMT 版本 ----
154
+ if HAS_PYGMT and not df.empty:
155
+ d = df.dropna(subset=["Lon", "Lat"]).copy()
156
+ if d.empty:
157
+ return None
158
+
159
+ mag = d["Magnitude"].fillna(0).clip(lower=0)
160
+ size_cm = 0.06 * (mag + 1.5) # 各點大小(cm)
161
+ depth = d["Depth_km"].fillna(0)
162
+
163
+ vmin = float(max(0, np.nanmin(depth)))
164
+ vmax = float(max(100, np.nanmax(depth)))
165
+ cmap = "roma" # 或試 "turbo", "viridis"
166
+
167
+ fig = pygmt.Figure()
168
+ region = [lon_min, lon_max, lat_min, lat_max]
169
+
170
+ fig.coast(
171
+ region=region, projection="M12c",
172
+ land="lightgray", water="white",
173
+ shorelines="0.5p,black", borders="1/0.6p,black",
174
+ frame=["WSen", "xaf", "yaf"]
175
+ )
176
+
177
+ # 畫震央
178
+ # 這裡用 per-point size:把每個大小轉字串傳入 style
179
+ fig.plot(
180
+ x=d["Lon"].to_list(), y=d["Lat"].to_list(),
181
+ style=["c{}c".format(s) for s in size_cm.to_list()],
182
+ color=depth.to_list(), cmap=cmap, pen="0.25p,black"
183
+ )
184
+
185
+ fig.colorbar(frame=["x+lDepth (km)"], cmap=True, position="JMR+w7c/0.4c+o0.6c/0c")
186
+ fig.basemap(map_scale="jBL+w50k+o0.6c/0.6c+f+lkm")
187
+
188
+ outpath = tempfile.NamedTemporaryFile(delete=False, suffix=".png").name
189
+ fig.savefig(outpath, dpi=220)
190
+ return outpath
191
+
192
+ # ---- 備援:Matplotlib 版本 ----
193
  if df.empty:
194
  return None
 
195
  fig, ax = plt.subplots(figsize=(6, 6))
196
  ax.set_xlim(lon_min, lon_max)
197
  ax.set_ylim(lat_min, lat_max)
198
  mags = df["Magnitude"].fillna(0)
199
  sizes = (mags.clip(lower=0) + 2) ** 3
200
+ sc = ax.scatter(df["Lon"], df["Lat"], s=sizes, c=df["Depth_km"], alpha=0.8, edgecolor="black")
201
+ cb = plt.colorbar(sc, ax=ax, fraction=0.046, pad=0.04)
202
+ cb.set_label("Depth (km)")
203
+ ax.set_xlabel("Longitude (°E)")
204
+ ax.set_ylabel("Latitude (°N)")
205
+ ax.set_title("Epicenters in Taiwan Region (119–123E, 21–26N)")
206
  ax.grid(True, linestyle="--", alpha=0.3)
207
  return _save_fig_to_tmp(fig)
208
 
 
209
  # -----------------------------
210
  # 表格輸出(tabulate 可選)
211
  # -----------------------------
 
219
  except Exception:
220
  return series.astype(str)
221
 
 
222
  def _to_simple_md_table(df: pd.DataFrame) -> str:
223
  cols = list(df.columns)
224
  header = "|" + "|".join(cols) + "|\n"
 
232
  rows.append("|" + "|".join(cells) + "|")
233
  return header + sep + "\n".join(rows)
234
 
 
235
  def df_to_markdown(df, top_n=100):
236
  if df.empty:
237
  return "(查無資料)"
 
247
  table = _to_simple_md_table(slim.reset_index(drop=True))
248
  return header + table
249
 
 
250
  # -----------------------------
251
  # 主流程
252
  # -----------------------------
 
273
  except Exception as e:
274
  return f"錯誤:{e}", None, None, None
275
 
 
276
  # -----------------------------
277
  # 介面
278
  # -----------------------------
 
300
  run_btn = gr.Button("查詢", variant="primary")
301
 
302
  table_out = gr.Markdown("(尚未查詢)")
 
303
  trend_out = gr.Image(label="趨勢圖", type="filepath")
304
+ map_out = gr.Image(label="台灣範圍圖(PyGMT)", type="filepath")
305
+ dl_btn = gr.DownloadButton(label="下載 CSV") # 傳路徑,不用 file_name 參數
306
 
307
  # 快速鍵
308
  btn_12h.click(lambda: set_time_range(hours=12), outputs=[time_from, time_to])