cwadayi commited on
Commit
8b354bc
·
verified ·
1 Parent(s): 91e1a52

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -96
app.py CHANGED
@@ -1,14 +1,15 @@
1
  import os
2
  import re
3
- import tempfile
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
  try:
13
  import tabulate as _tabulate # noqa: F401
14
  HAS_TABULATE = True
@@ -49,15 +50,10 @@ def fetch_reports(time_from, time_to):
49
  return r.json()
50
 
51
  # -----------------------------
52
- # JSON 解析(讀 EarthquakeInfo)+ 強化數字解析
53
  # -----------------------------
54
  def _to_float(x):
55
- """
56
- 將各種數字表達轉成 float:
57
- - 純數字:23.5
58
- - 含單位/文字:'23.5°N'、'121.6 E'、'25.3 公里' -> 擷取第一個浮點數
59
- - 其他不可解析 -> None
60
- """
61
  if x is None:
62
  return None
63
  if isinstance(x, (int, float)):
@@ -70,9 +66,8 @@ def _to_float(x):
70
 
71
  def parse_ea0015(obj):
72
  """
73
- 解析 CWA E-A0015-001
74
- 主要欄位在 records.earthquake[].EarthquakeInfo.*
75
- 取出:OriginTime, Lat, Lon, Depth_km, Magnitude, Location, ReportURL
76
  """
77
  records = obj.get("records") or obj.get("Records") or {}
78
  quakes = records.get("earthquake") or records.get("Earthquake") or []
@@ -83,8 +78,6 @@ def parse_ea0015(obj):
83
  for q in quakes:
84
  ei = q.get("EarthquakeInfo") or q.get("earthquakeInfo") or {}
85
  epic = ei.get("Epicenter") or ei.get("epicenter") or {}
86
-
87
- # Magnitude 可能在 Magnitude 或 EarthquakeMagnitude
88
  mago = (
89
  ei.get("Magnitude") or ei.get("magnitude")
90
  or ei.get("EarthquakeMagnitude") or ei.get("earthquakeMagnitude")
@@ -96,7 +89,6 @@ def parse_ea0015(obj):
96
  or q.get("OriginTime") or q.get("originTime")
97
  )
98
 
99
- # 經緯度多種鍵名
100
  lat_raw = (
101
  epic.get("EpicenterLat") or epic.get("epicenterLat")
102
  or epic.get("EpicenterLatitude") or epic.get("epicenterLatitude")
@@ -108,14 +100,12 @@ def parse_ea0015(obj):
108
  or epic.get("Lon") or epic.get("lon")
109
  )
110
 
111
- # 深度:Depth / FocalDepth / FocalDepthKm / depth / focalDepth...
112
  depth_raw = (
113
  ei.get("Depth") or ei.get("depth")
114
  or ei.get("FocalDepth") or ei.get("focalDepth")
115
  or ei.get("FocalDepthKm") or ei.get("focalDepthKm")
116
  )
117
 
118
- # 規模:MagnitudeValue / value / Magnitude / magnitude
119
  mag_raw = (
120
  mago.get("MagnitudeValue") or mago.get("magnitudeValue")
121
  or mago.get("Value") or mago.get("value")
@@ -143,71 +133,7 @@ def parse_ea0015(obj):
143
  return df
144
 
145
  # -----------------------------
146
- # 視覺化(回傳檔案路徑;相容舊/新 Gradio)
147
- # -----------------------------
148
- def _save_fig_to_tmp(fig, suffix=".png", dpi=180):
149
- outpath = tempfile.NamedTemporaryFile(delete=False, suffix=suffix).name
150
- fig.savefig(outpath, format="png", dpi=dpi, bbox_inches="tight")
151
- plt.close(fig)
152
- return outpath
153
-
154
- def plot_trend_path(df):
155
- if df.empty:
156
- return None
157
- mag = pd.to_numeric(df["Magnitude"], errors="coerce")
158
- fig, ax = plt.subplots(figsize=(6, 4))
159
- ax.scatter(df["OriginTime"], mag)
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
- return _save_fig_to_tmp(fig)
165
-
166
- def plot_map_path(df):
167
- """
168
- 最簡版地圖:純 Matplotlib 散點圖
169
- - 自動依據資料決定範圍(加一點 padding)
170
- - 點大小 = 規模函數;點顏色 = 深度(km)
171
- """
172
- if df.empty:
173
- return None
174
-
175
- d = df.dropna(subset=["Lon", "Lat"]).copy()
176
- if d.empty:
177
- return None
178
-
179
- # 數值化
180
- d["Magnitude"] = pd.to_numeric(d["Magnitude"], errors="coerce").fillna(0).clip(lower=0)
181
- d["Depth_km"] = pd.to_numeric(d["Depth_km"], errors="coerce")
182
-
183
- # 自動範圍 + padding
184
- pad = 0.5
185
- lon_min, lon_max = d["Lon"].min() - pad, d["Lon"].max() + pad
186
- lat_min, lat_max = d["Lat"].min() - pad, d["Lat"].max() + pad
187
-
188
- # 點大小(可依喜好調係數)
189
- size = (d["Magnitude"] + 2) ** 3
190
-
191
- # 繪圖
192
- fig, ax = plt.subplots(figsize=(6, 6))
193
- ax.set_xlim(lon_min, lon_max)
194
- ax.set_ylim(lat_min, lat_max)
195
-
196
- sc = ax.scatter(d["Lon"], d["Lat"], s=size, c=d["Depth_km"],
197
- edgecolor="black", alpha=0.9)
198
-
199
- cb = plt.colorbar(sc, ax=ax, fraction=0.046, pad=0.04)
200
- cb.set_label("Depth (km)")
201
-
202
- ax.set_xlabel("Longitude (°E)")
203
- ax.set_ylabel("Latitude (°N)")
204
- ax.set_title("Epicenters (auto region, simple map)")
205
- ax.grid(True, linestyle="--", alpha=0.3)
206
-
207
- return _save_fig_to_tmp(fig)
208
-
209
- # -----------------------------
210
- # 表格輸出(tabulate 可選)
211
  # -----------------------------
212
  def _format_taipei(series):
213
  try:
@@ -247,6 +173,68 @@ def df_to_markdown(df, top_n=100):
247
  table = _to_simple_md_table(slim.reset_index(drop=True))
248
  return header + table
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  # -----------------------------
251
  # 主流程
252
  # -----------------------------
@@ -255,23 +243,19 @@ def query_and_render(time_from, time_to, sort_order):
255
  raw = fetch_reports(time_from, time_to)
256
  df = parse_ea0015(raw)
257
  if df.empty:
258
- return "(查無資料)", None, None, None
259
 
260
  if sort_order == "OriginTime (舊→新)":
261
  df = df.sort_values("OriginTime", ascending=True, na_position="last").reset_index(drop=True)
262
 
263
  md = df_to_markdown(df)
264
- trend_path = plot_trend_path(df)
265
- map_path = plot_map_path(df)
266
 
267
  csv_bytes = df.to_csv(index=False).encode("utf-8-sig")
268
- csv_path = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="CWA_E-A0015-001_").name
269
- with open(csv_path, "wb") as f:
270
- f.write(csv_bytes)
271
-
272
- return md, trend_path, map_path, csv_path
273
  except Exception as e:
274
- return f"錯誤:{e}", None, None, None
275
 
276
  # -----------------------------
277
  # 介面
@@ -300,9 +284,8 @@ with gr.Blocks(fill_height=True) as demo:
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="台灣範圍圖(簡易)", type="filepath")
305
- dl_btn = gr.DownloadButton(label="下載 CSV") # 回傳路徑即可
306
 
307
  # 快速鍵
308
  btn_12h.click(lambda: set_time_range(hours=12), outputs=[time_from, time_to])
@@ -314,7 +297,7 @@ with gr.Blocks(fill_height=True) as demo:
314
  run_btn.click(
315
  query_and_render,
316
  inputs=[time_from, time_to, sort_dd],
317
- outputs=[table_out, trend_out, map_out, dl_btn],
318
  )
319
 
320
  if __name__ == "__main__":
 
1
  import os
2
  import re
 
3
  from datetime import datetime, timedelta, timezone
4
 
5
  import requests
6
  import pandas as pd
 
7
  import gradio as gr
8
+ import folium
9
+ from folium.plugins import MarkerCluster
10
+ from branca.colormap import linear
11
 
12
+ # ---------- 可選依賴偵測(表格美化用,沒裝也能跑) ----------
13
  try:
14
  import tabulate as _tabulate # noqa: F401
15
  HAS_TABULATE = True
 
50
  return r.json()
51
 
52
  # -----------------------------
53
+ # 解析 JSON
54
  # -----------------------------
55
  def _to_float(x):
56
+ """將字串(含單位)抽出第一個數字成 float;失敗回 None。"""
 
 
 
 
 
57
  if x is None:
58
  return None
59
  if isinstance(x, (int, float)):
 
66
 
67
  def parse_ea0015(obj):
68
  """
69
+ 解析 CWA E-A0015-001 -> DataFrame 欄位:
70
+ OriginTime, Magnitude, Depth_km, Lat, Lon, Location, ReportURL
 
71
  """
72
  records = obj.get("records") or obj.get("Records") or {}
73
  quakes = records.get("earthquake") or records.get("Earthquake") or []
 
78
  for q in quakes:
79
  ei = q.get("EarthquakeInfo") or q.get("earthquakeInfo") or {}
80
  epic = ei.get("Epicenter") or ei.get("epicenter") or {}
 
 
81
  mago = (
82
  ei.get("Magnitude") or ei.get("magnitude")
83
  or ei.get("EarthquakeMagnitude") or ei.get("earthquakeMagnitude")
 
89
  or q.get("OriginTime") or q.get("originTime")
90
  )
91
 
 
92
  lat_raw = (
93
  epic.get("EpicenterLat") or epic.get("epicenterLat")
94
  or epic.get("EpicenterLatitude") or epic.get("epicenterLatitude")
 
100
  or epic.get("Lon") or epic.get("lon")
101
  )
102
 
 
103
  depth_raw = (
104
  ei.get("Depth") or ei.get("depth")
105
  or ei.get("FocalDepth") or ei.get("focalDepth")
106
  or ei.get("FocalDepthKm") or ei.get("focalDepthKm")
107
  )
108
 
 
109
  mag_raw = (
110
  mago.get("MagnitudeValue") or mago.get("magnitudeValue")
111
  or mago.get("Value") or mago.get("value")
 
133
  return df
134
 
135
  # -----------------------------
136
+ # 表格輸出
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  # -----------------------------
138
  def _format_taipei(series):
139
  try:
 
173
  table = _to_simple_md_table(slim.reset_index(drop=True))
174
  return header + table
175
 
176
+ # -----------------------------
177
+ # OSM 地圖(Folium)輸出 HTML
178
+ # -----------------------------
179
+ def map_osm_html(df: pd.DataFrame):
180
+ if df.empty:
181
+ return "<div style='padding:8px'>(查無資料)</div>"
182
+
183
+ d = df.dropna(subset=["Lat", "Lon"]).copy()
184
+ if d.empty:
185
+ return "<div style='padding:8px'>(無經緯度可繪製)</div>"
186
+
187
+ # 數值化
188
+ d["Magnitude"] = pd.to_numeric(d["Magnitude"], errors="coerce").fillna(0).clip(lower=0)
189
+ d["Depth_km"] = pd.to_numeric(d["Depth_km"], errors="coerce")
190
+
191
+ # 地圖中心 / 範圍
192
+ center = [d["Lat"].mean(), d["Lon"].mean()]
193
+ m = folium.Map(location=center, zoom_start=6, tiles="OpenStreetMap", control_scale=True)
194
+
195
+ # 顏色條(深度)
196
+ depth_min, depth_max = float(d["Depth_km"].min()), float(d["Depth_km"].max())
197
+ if depth_min == depth_max:
198
+ depth_min, depth_max = max(0.0, depth_min - 1), depth_max + 1
199
+ cmap = linear.Viridis_08.scale(depth_min, depth_max)
200
+ cmap.caption = "Depth (km)"
201
+ cmap.add_to(m)
202
+
203
+ cluster = MarkerCluster().add_to(m)
204
+
205
+ # 逐筆加入圓標
206
+ for _, r in d.iterrows():
207
+ lat, lon = float(r["Lat"]), float(r["Lon"])
208
+ mag = float(r["Magnitude"]) if pd.notna(r["Magnitude"]) else 0.0
209
+ depth = float(r["Depth_km"]) if pd.notna(r["Depth_km"]) else 0.0
210
+
211
+ size = 4 + 2.5 * max(0.0, mag) # 依規模調整像素半徑
212
+ color = cmap(depth)
213
+
214
+ popup_html = f"""
215
+ <b>OriginTime</b>: {r['OriginTime']}<br>
216
+ <b>Magnitude</b>: {mag:.1f}<br>
217
+ <b>Depth</b>: {depth:.1f} km<br>
218
+ <b>Location</b>: {r.get('Location','') or ''}<br>
219
+ <a href="{r.get('ReportURL','') or '#'}" target="_blank">CWA 報告</a>
220
+ """
221
+
222
+ folium.CircleMarker(
223
+ location=[lat, lon],
224
+ radius=size,
225
+ color="#000000",
226
+ weight=1,
227
+ fill=True,
228
+ fill_color=color,
229
+ fill_opacity=0.85,
230
+ popup=folium.Popup(popup_html, max_width=320),
231
+ ).add_to(cluster)
232
+
233
+ # fit bounds
234
+ m.fit_bounds([[d["Lat"].min(), d["Lon"].min()], [d["Lat"].max(), d["Lon"].max()]], padding=(20, 20))
235
+
236
+ return m.get_root().render()
237
+
238
  # -----------------------------
239
  # 主流程
240
  # -----------------------------
 
243
  raw = fetch_reports(time_from, time_to)
244
  df = parse_ea0015(raw)
245
  if df.empty:
246
+ return "(查無資料)", "<div style='padding:8px'>(查無資料)</div>", None
247
 
248
  if sort_order == "OriginTime (舊→新)":
249
  df = df.sort_values("OriginTime", ascending=True, na_position="last").reset_index(drop=True)
250
 
251
  md = df_to_markdown(df)
252
+ map_html = map_osm_html(df)
 
253
 
254
  csv_bytes = df.to_csv(index=False).encode("utf-8-sig")
255
+ # Gradio DownloadButton 接受 bytes 或路徑;這裡直接給 bytes
256
+ return md, map_html, csv_bytes
 
 
 
257
  except Exception as e:
258
+ return f"錯誤:{e}", "<div style='padding:8px'>(無法繪圖)</div>", None
259
 
260
  # -----------------------------
261
  # 介面
 
284
  run_btn = gr.Button("查詢", variant="primary")
285
 
286
  table_out = gr.Markdown("(尚未查詢)")
287
+ map_out = gr.HTML() # 嵌入 OSM 互動地圖
288
+ dl_btn = gr.DownloadButton(label="下載 CSV", value=None, file_name="CWA_E-A0015-001.csv")
 
289
 
290
  # 快速鍵
291
  btn_12h.click(lambda: set_time_range(hours=12), outputs=[time_from, time_to])
 
297
  run_btn.click(
298
  query_and_render,
299
  inputs=[time_from, time_to, sort_dd],
300
+ outputs=[table_out, map_out, dl_btn],
301
  )
302
 
303
  if __name__ == "__main__":