cwadayi commited on
Commit
595e888
·
verified ·
1 Parent(s): 8d60232

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +196 -1
app.py CHANGED
@@ -36,4 +36,199 @@ def fetch_reports(time_from, time_to):
36
  if not api_key:
37
  raise RuntimeError("請在環境變數設定 CWA_API_KEY")
38
  params = {"Authorization": api_key, "timeFrom": time_from, "timeTo": time_to}
39
- r = requests.get(API_URL, params=params, timeout=30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  if not api_key:
37
  raise RuntimeError("請在環境變數設定 CWA_API_KEY")
38
  params = {"Authorization": api_key, "timeFrom": time_from, "timeTo": time_to}
39
+ r = requests.get(API_URL, params=params, timeout=30)
40
+ r.raise_for_status()
41
+ return r.json()
42
+
43
+ # -----------------------------
44
+ # 解析 JSON → DataFrame(彈性容錯)
45
+ # -----------------------------
46
+ def _safe_get(d, *keys, default=None):
47
+ cur = d
48
+ for k in keys:
49
+ if isinstance(cur, dict) and k in cur:
50
+ cur = cur[k]
51
+ else:
52
+ return default
53
+ return cur
54
+
55
+ def _to_float(x):
56
+ try:
57
+ if x is None or str(x).strip() == "":
58
+ return None
59
+ return float(str(x).strip())
60
+ except Exception:
61
+ return None
62
+
63
+ def parse_ea0015(obj):
64
+ """
65
+ 支援 records/Records、earthquake/Earthquake 等大小寫差異與常見欄位。
66
+ 輸出欄位:OriginTime, Lat, Lon, Depth_km, Magnitude, Location, ReportURL
67
+ """
68
+ records = obj.get("records") or obj.get("Records") or {}
69
+ quakes = records.get("earthquake") or records.get("Earthquake") or records.get("data") or []
70
+
71
+ if not isinstance(quakes, list):
72
+ quakes = []
73
+
74
+ rows = []
75
+ for q in quakes:
76
+ origin = (
77
+ _safe_get(q, "originTime")
78
+ or _safe_get(q, "OriginTime")
79
+ or _safe_get(q, "earthquakeInfo", "originTime")
80
+ or _safe_get(q, "EarthquakeInfo", "OriginTime")
81
+ )
82
+ lat = (
83
+ _safe_get(q, "epicenter", "epicenterLat")
84
+ or _safe_get(q, "Epicenter", "EpicenterLat")
85
+ )
86
+ lon = (
87
+ _safe_get(q, "epicenter", "epicenterLon")
88
+ or _safe_get(q, "Epicenter", "EpicenterLon")
89
+ )
90
+ depth = _safe_get(q, "depth") or _safe_get(q, "Depth")
91
+ mag = (
92
+ _safe_get(q, "magnitude", "magnitudeValue")
93
+ or _safe_get(q, "Magnitude", "MagnitudeValue")
94
+ or _safe_get(q, "magnitude", "magnitude")
95
+ or _safe_get(q, "Magnitude", "Magnitude")
96
+ )
97
+ loc = (
98
+ _safe_get(q, "epicenter", "location")
99
+ or _safe_get(q, "Epicenter", "Location")
100
+ )
101
+ url = _safe_get(q, "reportURL") or _safe_get(q, "ReportURL")
102
+
103
+ rows.append({
104
+ "OriginTime": origin,
105
+ "Lat": _to_float(lat),
106
+ "Lon": _to_float(lon),
107
+ "Depth_km": _to_float(depth),
108
+ "Magnitude": _to_float(mag),
109
+ "Location": loc,
110
+ "ReportURL": url
111
+ })
112
+
113
+ df = pd.DataFrame(rows)
114
+ if not df.empty:
115
+ df["OriginTime"] = pd.to_datetime(df["OriginTime"], errors="coerce")
116
+ df = df.sort_values("OriginTime", ascending=False, na_position="last").reset_index(drop=True)
117
+ return df
118
+
119
+ # -----------------------------
120
+ # 視覺化
121
+ # -----------------------------
122
+ def plot_trend(df):
123
+ if df.empty:
124
+ return None
125
+ fig, ax = plt.subplots(figsize=(6, 4))
126
+ ax.scatter(df["OriginTime"], df["Magnitude"])
127
+ ax.set_xlabel("Origin Time (Taipei)")
128
+ ax.set_ylabel("Magnitude")
129
+ ax.grid(True, linestyle="--", alpha=0.4)
130
+ fig.autofmt_xdate()
131
+ buf = io.BytesIO()
132
+ fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
133
+ plt.close(fig)
134
+ buf.seek(0)
135
+ return buf.getvalue()
136
+
137
+ def plot_map(df):
138
+ if df.empty:
139
+ return None
140
+ lon_min, lon_max, lat_min, lat_max = 119, 123, 21, 26
141
+ fig, ax = plt.subplots(figsize=(6, 6))
142
+ ax.set_xlim(lon_min, lon_max)
143
+ ax.set_ylim(lat_min, lat_max)
144
+ mags = df["Magnitude"].fillna(0)
145
+ sizes = (mags.clip(lower=0) + 2) ** 3
146
+ ax.scatter(df["Lon"], df["Lat"], s=sizes, alpha=0.6, edgecolor="black")
147
+ for m in [3, 4, 5, 6]:
148
+ ax.scatter([], [], s=((m + 2) ** 3), alpha=0.6, edgecolor="black", label=f"M {m}")
149
+ ax.legend(title="Magnitude")
150
+ ax.grid(True, linestyle="--", alpha=0.3)
151
+ buf = io.BytesIO()
152
+ fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
153
+ plt.close(fig)
154
+ buf.seek(0)
155
+ return buf.getvalue()
156
+
157
+ # -----------------------------
158
+ # 主流程
159
+ # -----------------------------
160
+ def _format_taipei(series):
161
+ """把時間 Series 以台北時間顯示;naive 視為台北時間,aware 轉為台北時間。"""
162
+ try:
163
+ if series.dt.tz is None:
164
+ s = series.dt.tz_localize(TAIPEI_TZ)
165
+ else:
166
+ s = series.dt.tz_convert(TAIPEI_TZ)
167
+ return s.dt.strftime("%Y-%m-%d %H:%M:%S %Z")
168
+ except Exception:
169
+ return series.astype(str)
170
+
171
+ def df_to_markdown(df, top_n=100):
172
+ if df.empty:
173
+ return "(查無資料)"
174
+ cols = ["OriginTime", "Magnitude", "Depth_km", "Lat", "Lon", "Location", "ReportURL"]
175
+ cols = [c for c in cols if c in df.columns]
176
+ slim = df[cols].head(top_n).copy()
177
+ if "OriginTime" in slim.columns:
178
+ slim["OriginTime"] = _format_taipei(slim["OriginTime"])
179
+ header = f"共 {len(df)} 筆,顯示前 {min(len(slim), top_n)} 筆\n\n"
180
+ return header + slim.to_markdown(index=False)
181
+
182
+ def query_and_render(time_from, time_to, sort_order):
183
+ try:
184
+ raw = fetch_reports(time_from, time_to)
185
+ df = parse_ea0015(raw)
186
+ if df.empty:
187
+ return "(查無資料)", None, None, None
188
+ if sort_order == "OriginTime (舊→新)":
189
+ df = df.sort_values("OriginTime", ascending=True, na_position="last").reset_index(drop=True)
190
+ md = df_to_markdown(df)
191
+ trend_png = plot_trend(df)
192
+ map_png = plot_map(df)
193
+ csv_bytes = df.to_csv(index=False).encode("utf-8-sig")
194
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="CWA_E-A0015-001_")
195
+ tmp.write(csv_bytes); tmp.flush(); tmp.close()
196
+ return md, trend_png, map_png, tmp.name
197
+ except Exception as e:
198
+ return f"錯誤:{e}", None, None, None
199
+
200
+ # -----------------------------
201
+ # 介面
202
+ # -----------------------------
203
+ default_from, default_to = set_time_range(days=3)
204
+
205
+ with gr.Blocks(fill_height=True) as demo:
206
+ gr.Markdown("## CWA 顯著有感地震報告 (E-A0015-001)\n預設查詢最近 3 天(台北時間)")
207
+
208
+ with gr.Column():
209
+ time_from = gr.Textbox(label="timeFrom yyyy-MM-ddTHH:mm:ss", value=default_from)
210
+ time_to = gr.Textbox(label="timeTo yyyy-MM-ddTHH:mm:ss", value=default_to)
211
+
212
+ with gr.Row():
213
+ btn_12h = gr.Button("最近 12 小時")
214
+ btn_24h = gr.Button("最近 24 小時")
215
+ btn_3d = gr.Button("最近 3 天")
216
+ btn_5d = gr.Button("最近 5 天")
217
+
218
+ sort_dd = gr.Dropdown(choices=["OriginTime (新→舊)", "OriginTime (舊→新)"], value="OriginTime (新→舊)", label="排序")
219
+ run_btn = gr.Button("查詢", variant="primary")
220
+
221
+ table_out = gr.Markdown("(尚未查詢)")
222
+ trend_out = gr.Image(label="趨勢圖", type="numpy")
223
+ map_out = gr.Image(label="台灣範圍圖", type="numpy")
224
+ dl_btn = gr.DownloadButton(label="下載 CSV") # 相容舊/新版
225
+
226
+ btn_12h.click(lambda: set_time_range(hours=12), outputs=[time_from, time_to])
227
+ btn_24h.click(lambda: set_time_range(hours=24), outputs=[time_from, time_to])
228
+ btn_3d.click(lambda: set_time_range(days=3), outputs=[time_from, time_to])
229
+ btn_5d.click(lambda: set_time_range(days=5), outputs=[time_from, time_to])
230
+
231
+ run_btn.click(query_and_render, inputs=[time_from, time_to, sort_dd], outputs=[table_out, trend_out, map_out, dl_btn])
232
+
233
+ if __name__ == "__main__":
234
+ demo.launch()