cwadayi commited on
Commit
9eb02f1
·
verified ·
1 Parent(s): 7bad7d7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -203
app.py CHANGED
@@ -1,70 +1,51 @@
1
  import os
2
  import io
3
- import json
4
- from datetime import datetime, timedelta, timezone
5
-
6
  import requests
7
  import pandas as pd
8
  import matplotlib.pyplot as plt
9
  import gradio as gr
10
-
11
 
12
  # -----------------------------
13
- # 時區:台北 (UTC+8)
14
  # -----------------------------
15
  TAIPEI_TZ = timezone(timedelta(hours=8))
16
 
17
- def now_taipei():
18
- return datetime.now(TAIPEI_TZ)
19
-
20
  def fmt_dt(dt: datetime) -> str:
21
  return dt.strftime("%Y-%m-%dT%H:%M:%S")
22
 
23
-
24
- # -----------------------------
25
- # 快速時間範圍
26
- # -----------------------------
27
- def set_time_range(hours: int | None = None, days: int | None = None):
28
- """
29
- 依台北時間回傳 (timeFrom, timeTo) 字串(yyyy-MM-ddTHH:mm:ss)
30
- """
31
- now = now_taipei()
32
  if hours is not None:
33
  t_from = now - timedelta(hours=hours)
34
  elif days is not None:
35
  t_from = now - timedelta(days=days)
36
  else:
37
- t_from = now - timedelta(days=3) # 預設最近 3 天
38
  return fmt_dt(t_from), fmt_dt(now)
39
 
40
-
41
  # -----------------------------
42
- # 呼叫 CWA E-A0015-001 API
43
  # -----------------------------
44
  API_URL = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001"
45
 
46
- def fetch_reports(time_from: str, time_to: str) -> dict:
47
- """
48
- 以環境變數 CWA_API_KEY 做授權參數呼叫 API,回傳 JSON 物件(dict)
49
- """
50
  api_key = os.getenv("CWA_API_KEY", "").strip()
51
  if not api_key:
52
- raise RuntimeError("環境變數 CWA_API_KEY 未設定。請在 Space Secrets 設定。")
53
-
54
  params = {
55
  "Authorization": api_key,
56
  "timeFrom": time_from,
57
- "timeTo": time_to,
58
- # 其餘參數保持預設;排序改由本地處理,以避免介面差異
59
  }
60
- resp = requests.get(API_URL, params=params, timeout=30)
61
- resp.raise_for_status()
62
- return resp.json()
63
-
64
 
65
  # -----------------------------
66
- # 解析 JSON → pandas.DataFrame
67
- # 只保留必要欄位(不含 area / station 等)
68
  # -----------------------------
69
  def safe_get(d, *keys, default=None):
70
  cur = d
@@ -75,49 +56,26 @@ def safe_get(d, *keys, default=None):
75
  return default
76
  return cur
77
 
78
- def parse_ea0015(json_obj: dict) -> pd.DataFrame:
79
- """
80
- 嘗試容錯各種大小寫與路徑差異,輸出欄位:
81
- OriginTime, Lat, Lon, Depth_km, Magnitude, Location, ReportURL
82
- """
83
- records = json_obj.get("records") or json_obj.get("Records") or {}
84
- quakes = (
85
- records.get("earthquake") or
86
- records.get("Earthquake") or
87
- records.get("data") or
88
- []
89
- )
90
 
 
 
 
91
  rows = []
92
  for q in quakes:
93
- origin = safe_get(q, "originTime") or safe_get(q, "OriginTime")
94
- # 經緯度 & 深度
95
- lat = (
96
- safe_get(q, "epicenter", "epicenterLat") or
97
- safe_get(q, "Epicenter", "EpicenterLat")
98
- )
99
- lon = (
100
- safe_get(q, "epicenter", "epicenterLon") or
101
- safe_get(q, "Epicenter", "EpicenterLon")
102
- )
103
- depth = (
104
- safe_get(q, "depth") or
105
- safe_get(q, "Depth")
106
- )
107
- # 規模
108
- mag = (
109
- safe_get(q, "magnitude", "magnitudeValue") or
110
- safe_get(q, "Magnitude", "MagnitudeValue") or
111
- safe_get(q, "magnitude", "magnitude") or
112
- safe_get(q, "Magnitude", "Magnitude")
113
- )
114
- # 位置與連結
115
- loc = (
116
- safe_get(q, "epicenter", "location") or
117
- safe_get(q, "Epicenter", "Location")
118
- )
119
- url = safe_get(q, "reportURL") or safe_get(q, "ReportURL")
120
-
121
  rows.append({
122
  "OriginTime": origin,
123
  "Lat": _to_float(lat),
@@ -125,204 +83,119 @@ def parse_ea0015(json_obj: dict) -> pd.DataFrame:
125
  "Depth_km": _to_float(depth),
126
  "Magnitude": _to_float(mag),
127
  "Location": loc,
128
- "ReportURL": url,
129
  })
130
-
131
  df = pd.DataFrame(rows)
132
-
133
- # 轉時間、排序(預設:OriginTime 由新到舊)
134
  if not df.empty:
135
  df["OriginTime"] = pd.to_datetime(df["OriginTime"], errors="coerce")
136
- df = df.sort_values("OriginTime", ascending=False, na_position="last").reset_index(drop=True)
137
-
138
  return df
139
 
140
- def _to_float(x):
141
- try:
142
- if x is None or x == "":
143
- return None
144
- return float(str(x).strip())
145
- except Exception:
146
- return None
147
-
148
-
149
  # -----------------------------
150
- # 視覺化:趨勢圖、台灣地圖
151
  # -----------------------------
152
- TAIWAN_BBOX = (119, 123, 21, 26) # lon_min, lon_max, lat_min, lat_max
153
-
154
- def plot_trend(df: pd.DataFrame) -> bytes | None:
155
- """時間-規模散點圖,輸出 PNG bytes"""
156
  if df.empty:
157
  return None
158
- fig, ax = plt.subplots(figsize=(6, 3.8))
159
  ax.scatter(df["OriginTime"], df["Magnitude"])
160
  ax.set_xlabel("Origin Time (Taipei)")
161
  ax.set_ylabel("Magnitude")
162
  ax.grid(True, linestyle="--", alpha=0.4)
163
  fig.autofmt_xdate()
164
-
165
  buf = io.BytesIO()
166
- fig.savefig(buf, format="png", dpi=160, bbox_inches="tight")
167
  plt.close(fig)
168
  buf.seek(0)
169
  return buf.getvalue()
170
 
171
- def plot_taiwan_map(df: pd.DataFrame) -> bytes | None:
172
- """
173
- 基礎台灣範圍框圖(非海岸線),用散點展示震央;
174
- 以規模對應 marker 大小,附簡易圖例。
175
- """
176
  if df.empty:
177
  return None
178
-
179
- lon_min, lon_max, lat_min, lat_max = TAIWAN_BBOX
180
  fig, ax = plt.subplots(figsize=(6, 6))
181
-
182
- # 邊框
183
  ax.set_xlim(lon_min, lon_max)
184
  ax.set_ylim(lat_min, lat_max)
185
- ax.set_xlabel("Longitude (°E)")
186
- ax.set_ylabel("Latitude (°N)")
187
- ax.set_title("Epicenters in Taiwan Region (119–123E, 21–26N)")
188
-
189
- # 散點:大小反映規模
190
  mags = df["Magnitude"].fillna(0)
191
- sizes = (mags.clip(lower=0) + 2.0) ** 3 # 調整一下讓差異更明顯
192
  ax.scatter(df["Lon"], df["Lat"], s=sizes, alpha=0.6, edgecolor="black")
193
-
194
- # 簡單圖例:M3/4/5/6 對應大小
195
  for m in [3, 4, 5, 6]:
196
- ax.scatter([], [], s=((m + 2.0) ** 3), alpha=0.6, edgecolor="black", label=f"M {m}")
197
- ax.legend(title="Magnitude", loc="upper right", frameon=True)
198
-
199
  ax.grid(True, linestyle="--", alpha=0.3)
200
-
201
  buf = io.BytesIO()
202
- fig.savefig(buf, format="png", dpi=160, bbox_inches="tight")
203
  plt.close(fig)
204
  buf.seek(0)
205
  return buf.getvalue()
206
 
207
-
208
  # -----------------------------
209
- # 主流程:查詢 + 輸出
210
  # -----------------------------
211
- def query_and_render(time_from: str, time_to: str, sort_order: str):
212
- """
213
- 1) 取資料 → 2) 轉成 DataFrame → 3) 依排序輸出 → 4) 回傳表格、圖、CSV
214
- Gradio 會接收回傳值更新元件。
215
- """
216
- try:
217
- raw = fetch_reports(time_from, time_to)
218
- except Exception as e:
219
- return gr.update(value=f"查詢錯誤:{e}"), None, None, None
220
 
 
221
  try:
222
- df = parse_ea0015(raw)
 
223
  if df.empty:
224
  return "(查無資料)", None, None, None
225
-
226
- # 排序(本地)
227
  if sort_order == "OriginTime (舊→新)":
228
- df = df.sort_values("OriginTime", ascending=True, na_position="last").reset_index(drop=True)
229
- else:
230
- df = df.sort_values("OriginTime", ascending=False, na_position="last").reset_index(drop=True)
231
-
232
- # 表格(轉為 markdown 簡表,手機閱讀較清楚)
233
  md = df_to_markdown(df)
234
-
235
- # 圖
236
  trend_png = plot_trend(df)
237
- map_png = plot_taiwan_map(df)
238
-
239
- # 下載 CSV(以 bytes 形式回傳給 DownloadButton)
240
  csv_bytes = df.to_csv(index=False).encode("utf-8-sig")
241
-
242
- return md, trend_png, map_png, csv_bytes
 
 
 
 
243
  except Exception as e:
244
- return gr.update(value=f"解析錯誤:{e}"), None, None, None
245
-
246
-
247
- def df_to_markdown(df: pd.DataFrame, top_n: int = 100) -> str:
248
- """
249
- 將前 top_n 筆轉為 markdown 表(欄位:OriginTime, Magnitude, Depth_km, Lat, Lon, Location, ReportURL)
250
- """
251
- if df.empty:
252
- return "(查無資料)"
253
-
254
- show_cols = ["OriginTime", "Magnitude", "Depth_km", "Lat", "Lon", "Location", "ReportURL"]
255
- exist_cols = [c for c in show_cols if c in df.columns]
256
- slim = df[exist_cols].head(top_n).copy()
257
-
258
- # 時間顯示(台北時區)
259
- if "OriginTime" in slim.columns:
260
- slim["OriginTime"] = slim["OriginTime"].dt.tz_convert(TAIPEI_TZ).dt.strftime("%Y-%m-%d %H:%M:%S %Z")
261
-
262
- return slim.to_markdown(index=False)
263
-
264
 
265
  # -----------------------------
266
- # Gradio 介面
267
  # -----------------------------
268
  default_from, default_to = set_time_range(days=3)
269
 
270
- with gr.Blocks(fill_height=True, analytics_enabled=False) as demo:
271
- gr.Markdown(
272
- """
273
- # CWA 顯著有感地震報告 (E-A0015-001)
274
- 此 Space **只使用環境變數 `CWA_API_KEY` 作為授權**。
275
- 預設查詢 **最近 3 天(台北時間)**。手機版為單欄顯示。
276
- """
277
- )
278
 
279
- # 單欄(手機友好)
280
  with gr.Column():
281
- with gr.Row():
282
- time_from = gr.Textbox(label="timeFrom yyyy-MM-ddThh:mm:ss", value=default_from, scale=1)
283
- with gr.Row():
284
- time_to = gr.Textbox(label="timeTo yyyy-MM-ddThh:mm:ss", value=default_to, scale=1)
285
 
286
- # 快速時間範圍(已移除「最近6小時」)
287
  with gr.Row():
288
  btn_12h = gr.Button("最近 12 小時")
289
  btn_24h = gr.Button("最近 24 小時")
290
  btn_3d = gr.Button("最近 3 天")
291
  btn_5d = gr.Button("最近 5 天")
292
 
293
- # 排序(在地端)
294
- sort_dd = gr.Dropdown(
295
- choices=["OriginTime (新→舊)", "OriginTime (舊→新)"],
296
- value="OriginTime (新→舊)",
297
- label="排序(本地)"
298
- )
299
 
300
  run_btn = gr.Button("查詢", variant="primary")
301
 
302
- # 輸出
303
  table_out = gr.Markdown("(尚未查詢)")
304
- trend_out = gr.Image(label="趨勢圖(時間-規模)", type="numpy")
305
- map_out = gr.Image(label="台灣範圍圖(119–123E, 21–26N)", type="numpy")
306
- dl_btn = gr.DownloadButton(label="下載 CSV", file_name="CWA_E-A0015-001.csv")
307
 
308
- # 綁定:快速鍵 → 更新時間欄位
309
  btn_12h.click(lambda: set_time_range(hours=12), outputs=[time_from, time_to])
310
  btn_24h.click(lambda: set_time_range(hours=24), outputs=[time_from, time_to])
311
  btn_3d.click(lambda: set_time_range(days=3), outputs=[time_from, time_to])
312
  btn_5d.click(lambda: set_time_range(days=5), outputs=[time_from, time_to])
313
 
314
- # 綁定:查詢 表格 / / CSV
315
- def _on_query(tfrom, tto, sort_sel):
316
- md, trend_png, map_png, csv_bytes = query_and_render(tfrom, tto, sort_sel)
317
- # DownloadButton 需要返回 bytes-like 物件
318
- return md, trend_png, map_png, csv_bytes
319
-
320
- run_btn.click(
321
- _on_query,
322
- inputs=[time_from, time_to, sort_dd],
323
- outputs=[table_out, trend_out, map_out, dl_btn]
324
- )
325
 
326
- # 注意:Hugging Face Spaces 會自動呼叫 demo.launch()
327
  if __name__ == "__main__":
328
  demo.launch()
 
1
  import os
2
  import io
3
+ import tempfile
 
 
4
  import requests
5
  import pandas as pd
6
  import matplotlib.pyplot as plt
7
  import gradio as gr
8
+ from datetime import datetime, timedelta, timezone
9
 
10
  # -----------------------------
11
+ # 台北時區 (UTC+8)
12
  # -----------------------------
13
  TAIPEI_TZ = timezone(timedelta(hours=8))
14
 
 
 
 
15
  def fmt_dt(dt: datetime) -> str:
16
  return dt.strftime("%Y-%m-%dT%H:%M:%S")
17
 
18
+ def set_time_range(hours=None, days=None):
19
+ """依台北時間回傳 (timeFrom, timeTo)"""
20
+ now = datetime.now(TAIPEI_TZ)
 
 
 
 
 
 
21
  if hours is not None:
22
  t_from = now - timedelta(hours=hours)
23
  elif days is not None:
24
  t_from = now - timedelta(days=days)
25
  else:
26
+ t_from = now - timedelta(days=3)
27
  return fmt_dt(t_from), fmt_dt(now)
28
 
 
29
  # -----------------------------
30
+ # API 資料
31
  # -----------------------------
32
  API_URL = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001"
33
 
34
+ def fetch_reports(time_from, time_to):
 
 
 
35
  api_key = os.getenv("CWA_API_KEY", "").strip()
36
  if not api_key:
37
+ raise RuntimeError("請在環境變數設定 CWA_API_KEY")
 
38
  params = {
39
  "Authorization": api_key,
40
  "timeFrom": time_from,
41
+ "timeTo": time_to
 
42
  }
43
+ r = requests.get(API_URL, params=params, timeout=30)
44
+ r.raise_for_status()
45
+ return r.json()
 
46
 
47
  # -----------------------------
48
+ # 解析 JSON → DataFrame
 
49
  # -----------------------------
50
  def safe_get(d, *keys, default=None):
51
  cur = d
 
56
  return default
57
  return cur
58
 
59
+ def _to_float(x):
60
+ try:
61
+ if x is None or x == "":
62
+ return None
63
+ return float(str(x).strip())
64
+ except Exception:
65
+ return None
 
 
 
 
 
66
 
67
+ def parse_reports(data):
68
+ records = data.get("records") or {}
69
+ quakes = records.get("earthquake") or []
70
  rows = []
71
  for q in quakes:
72
+ origin = safe_get(q, "originTime")
73
+ lat = safe_get(q, "epicenter", "epicenterLat")
74
+ lon = safe_get(q, "epicenter", "epicenterLon")
75
+ depth = safe_get(q, "depth")
76
+ mag = safe_get(q, "magnitude", "magnitudeValue")
77
+ loc = safe_get(q, "epicenter", "location")
78
+ url = safe_get(q, "reportURL")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  rows.append({
80
  "OriginTime": origin,
81
  "Lat": _to_float(lat),
 
83
  "Depth_km": _to_float(depth),
84
  "Magnitude": _to_float(mag),
85
  "Location": loc,
86
+ "ReportURL": url
87
  })
 
88
  df = pd.DataFrame(rows)
 
 
89
  if not df.empty:
90
  df["OriginTime"] = pd.to_datetime(df["OriginTime"], errors="coerce")
91
+ df = df.sort_values("OriginTime", ascending=False).reset_index(drop=True)
 
92
  return df
93
 
 
 
 
 
 
 
 
 
 
94
  # -----------------------------
95
+ # 視覺化
96
  # -----------------------------
97
+ def plot_trend(df):
 
 
 
98
  if df.empty:
99
  return None
100
+ fig, ax = plt.subplots(figsize=(6, 4))
101
  ax.scatter(df["OriginTime"], df["Magnitude"])
102
  ax.set_xlabel("Origin Time (Taipei)")
103
  ax.set_ylabel("Magnitude")
104
  ax.grid(True, linestyle="--", alpha=0.4)
105
  fig.autofmt_xdate()
 
106
  buf = io.BytesIO()
107
+ fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
108
  plt.close(fig)
109
  buf.seek(0)
110
  return buf.getvalue()
111
 
112
+ def plot_map(df):
 
 
 
 
113
  if df.empty:
114
  return None
115
+ lon_min, lon_max, lat_min, lat_max = 119, 123, 21, 26
 
116
  fig, ax = plt.subplots(figsize=(6, 6))
 
 
117
  ax.set_xlim(lon_min, lon_max)
118
  ax.set_ylim(lat_min, lat_max)
 
 
 
 
 
119
  mags = df["Magnitude"].fillna(0)
120
+ sizes = (mags + 2) ** 3
121
  ax.scatter(df["Lon"], df["Lat"], s=sizes, alpha=0.6, edgecolor="black")
 
 
122
  for m in [3, 4, 5, 6]:
123
+ ax.scatter([], [], s=((m + 2) ** 3), alpha=0.6, edgecolor="black", label=f"M {m}")
124
+ ax.legend(title="Magnitude")
 
125
  ax.grid(True, linestyle="--", alpha=0.3)
 
126
  buf = io.BytesIO()
127
+ fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
128
  plt.close(fig)
129
  buf.seek(0)
130
  return buf.getvalue()
131
 
 
132
  # -----------------------------
133
+ # 主流程
134
  # -----------------------------
135
+ def df_to_markdown(df, top_n=100):
136
+ if df.empty:
137
+ return "(查無資料)"
138
+ show_cols = ["OriginTime", "Magnitude", "Depth_km", "Lat", "Lon", "Location", "ReportURL"]
139
+ exist_cols = [c for c in show_cols if c in df.columns]
140
+ slim = df[exist_cols].head(top_n).copy()
141
+ if "OriginTime" in slim.columns:
142
+ slim["OriginTime"] = slim["OriginTime"].dt.tz_localize("UTC").dt.tz_convert(TAIPEI_TZ).dt.strftime("%Y-%m-%d %H:%M:%S %Z")
143
+ return slim.to_markdown(index=False)
144
 
145
+ def query_and_render(time_from, time_to, sort_order):
146
  try:
147
+ raw = fetch_reports(time_from, time_to)
148
+ df = parse_reports(raw)
149
  if df.empty:
150
  return "(查無資料)", None, None, None
 
 
151
  if sort_order == "OriginTime (舊→新)":
152
+ df = df.sort_values("OriginTime", ascending=True).reset_index(drop=True)
 
 
 
 
153
  md = df_to_markdown(df)
 
 
154
  trend_png = plot_trend(df)
155
+ map_png = plot_map(df)
 
 
156
  csv_bytes = df.to_csv(index=False).encode("utf-8-sig")
157
+ # 建立���時檔讓 DownloadButton 可以下載
158
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="CWA_E-A0015-001_")
159
+ tmp.write(csv_bytes)
160
+ tmp.flush()
161
+ tmp.close()
162
+ return md, trend_png, map_png, tmp.name
163
  except Exception as e:
164
+ return f"錯誤:{e}", None, None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  # -----------------------------
167
+ # 介面
168
  # -----------------------------
169
  default_from, default_to = set_time_range(days=3)
170
 
171
+ with gr.Blocks(fill_height=True) as demo:
172
+ gr.Markdown("## CWA 顯著有感地震報告 (E-A0015-001)\n預設查詢最近 3 天(台北時間)")
 
 
 
 
 
 
173
 
 
174
  with gr.Column():
175
+ time_from = gr.Textbox(label="timeFrom yyyy-MM-ddTHH:mm:ss", value=default_from)
176
+ time_to = gr.Textbox(label="timeTo yyyy-MM-ddTHH:mm:ss", value=default_to)
 
 
177
 
 
178
  with gr.Row():
179
  btn_12h = gr.Button("最近 12 小時")
180
  btn_24h = gr.Button("最近 24 小時")
181
  btn_3d = gr.Button("最近 3 天")
182
  btn_5d = gr.Button("最近 5 天")
183
 
184
+ sort_dd = gr.Dropdown(choices=["OriginTime (新→舊)", "OriginTime (舊→新)"], value="OriginTime (新→舊)", label="排序")
 
 
 
 
 
185
 
186
  run_btn = gr.Button("查詢", variant="primary")
187
 
 
188
  table_out = gr.Markdown("(尚未查詢)")
189
+ trend_out = gr.Image(label="趨勢圖", type="numpy")
190
+ map_out = gr.Image(label="台灣範圍圖", type="numpy")
191
+ dl_btn = gr.DownloadButton(label="下載 CSV") # 不加 file_name
192
 
 
193
  btn_12h.click(lambda: set_time_range(hours=12), outputs=[time_from, time_to])
194
  btn_24h.click(lambda: set_time_range(hours=24), outputs=[time_from, time_to])
195
  btn_3d.click(lambda: set_time_range(days=3), outputs=[time_from, time_to])
196
  btn_5d.click(lambda: set_time_range(days=5), outputs=[time_from, time_to])
197
 
198
+ run_btn.click(query_and_render, inputs=[time_from, time_to, sort_dd], outputs=[table_out, trend_out, map_out, dl_btn])
 
 
 
 
 
 
 
 
 
 
199
 
 
200
  if __name__ == "__main__":
201
  demo.launch()