cwadayi commited on
Commit
8f56933
·
verified ·
1 Parent(s): b3cbfc1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -131
app.py CHANGED
@@ -3,82 +3,115 @@ import gradio as gr
3
  import pandas as pd
4
  import plotly.express as px
5
  from datetime import datetime, timedelta
 
6
 
7
- # USGS API 的基礎 URL
8
  USGS_API_BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query"
9
 
10
- def create_earthquake_map(df):
11
- """
12
- 使用 Plotly Express 根據 DataFrame 建立地震分佈圖。
13
- """
14
- fig = px.scatter_geo(
15
- df,
16
- lat='latitude',
17
- lon='longitude',
18
- color='magnitude', # 顏色深淺代表規模
19
- size='magnitude', # 點的大小也代表規模
20
- hover_name='place', # 滑鼠懸停時顯示地點
21
- hover_data=['time_str', 'depth'],
22
- projection="natural earth",
23
- title="地震分佈圖",
24
- color_continuous_scale=px.colors.sequential.Plasma # 設定顏色主題
25
- )
26
-
27
- # 調整地圖外觀
28
- fig.update_layout(
29
- margin={"r":0,"t":40,"l":0,"b":0}
30
- )
31
- return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  def process_earthquake_data(data):
34
- """
35
- 將從 API 獲取的原始資料轉換為 Markdown 和 DataFrame。
36
- """
37
  if not data or 'features' not in data or not data['features']:
38
- return "在指定的條件下,找不到任何地震資料。", None
39
 
40
- count = data['metadata']['count']
41
- title = data['metadata']['title']
42
-
43
- # 準備 Markdown 輸出
44
- md_output = f"## {title}\n\n"
45
- md_output += f"**查詢成功,共找到 {count} 筆地震資料。**\n\n"
46
- md_output += "| 時間 (UTC) | 地點 | 芮氏規模 | 深度 (公里) | 經緯度 |\n"
47
- md_output += "|---|---|---|---|---|\n"
48
-
49
- # 準備 DataFrame 的資料列表
50
  earthquake_list = []
51
-
52
  for feature in data['features']:
53
  properties = feature['properties']
54
  geometry = feature['geometry']
55
-
56
  event_time_utc = datetime.fromtimestamp(properties['time'] / 1000)
57
- time_str = event_time_utc.strftime('%Y-%m-%d %H:%M:%S')
58
 
59
- magnitude = properties['mag']
60
- place = properties['place']
61
- longitude, latitude, depth = geometry['coordinates']
62
-
63
- # 加入到 Markdown 字串
64
- place_md = place.replace('|', '\|') # 避免 Markdown 表格語法衝突
65
- md_output += f"| {time_str} | {place_md} | {magnitude:.2f} | {depth:.2f} | ({latitude:.4f}, {longitude:.4f}) |\n"
66
-
67
- # 加入到 list 中,用於建立 DataFrame
68
  earthquake_list.append({
69
- 'latitude': latitude,
70
- 'longitude': longitude,
71
- 'magnitude': magnitude,
72
- 'depth': depth,
73
- 'place': place,
74
- 'time_str': time_str
 
75
  })
76
 
77
  df = pd.DataFrame(earthquake_list)
78
- return md_output, df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
 
 
 
 
80
 
81
- def query_earthquakes(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  starttime_str: str,
83
  endtime_str: str,
84
  min_magnitude: float,
@@ -86,107 +119,142 @@ def query_earthquakes(
86
  min_lat: float, max_lat: float,
87
  min_lon: float, max_lon: float
88
  ):
89
- """
90
- 接收來自 Gradio UI 的輸入,查詢並回傳格式化的地震資料及地圖。
91
- """
 
 
 
 
 
 
 
92
  params = {
93
- "format": "geojson",
94
- "starttime": starttime_str,
95
- "endtime": endtime_str,
96
- "minmagnitude": min_magnitude,
97
- "limit": 1000, # 可以適度增加 limit
98
- "orderby": "time"
99
  }
100
 
101
  if use_geo:
102
  if not all([min_lat is not None, max_lat is not None, min_lon is not None, max_lon is not None]):
103
- return "錯誤:若要指定地理範圍,則經緯度四個欄位皆須填寫。", None
104
- if min_lat >= max_lat or min_lon >= max_lon:
105
- return "錯誤:最小經緯度必須小於最大經緯度。", None
106
- params.update({
107
- "minlatitude": min_lat, "maxlatitude": max_lat,
108
- "minlongitude": min_lon, "maxlongitude": max_lon
109
- })
110
 
111
  try:
112
- response = requests.get(USGS_API_BASE_URL, params=params, timeout=20)
113
  response.raise_for_status()
114
- data = response.json()
115
-
116
- # 處理資料
117
- markdown_result, df_result = process_earthquake_data(data)
118
 
119
- # 如果有資料,就建立地圖
120
- if df_result is not None and not df_result.empty:
121
- map_figure = create_earthquake_map(df_result)
122
- return markdown_result, map_figure
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  else:
124
- return markdown_result, None # 沒有資料時,回傳空的 map
125
-
126
- except requests.exceptions.RequestException as e:
127
- return f"錯誤:網路請求失敗: {e}", None
128
  except Exception as e:
129
- return f"發生未知錯誤: {e}", None
 
 
 
 
130
 
131
- # --- Gradio 介面設計 ---
132
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
133
- gr.Markdown("# 🌎 USGS 地震資料查詢與視覺化工具")
134
- gr.Markdown("透過此工具,您可以查詢並在地圖上視覺化美國地質調查局 (USGS) 的全球地震紀錄。")
 
135
 
136
  with gr.Row():
137
- with gr.Column(scale=2):
138
- gr.Markdown("### 1. 設定查詢條件")
139
- today = datetime.now()
140
- seven_days_ago = today - timedelta(days=7)
141
 
142
- starttime_input = gr.Textbox(label="查詢開始日期", value=seven_days_ago.strftime('%Y-%m-%d'))
143
- endtime_input = gr.Textbox(label="查詢結束日期", value=today.strftime('%Y-%m-%d'))
 
 
 
 
 
144
 
145
- magnitude_slider = gr.Slider(minimum=0, maximum=10, step=0.1, label="最小芮氏規模", value=4.5)
146
-
147
- with gr.Accordion("可選:指定地理範圍", open=False): # 使用 Accordion 讓介面更簡潔
148
  use_geo_checkbox = gr.Checkbox(label="啟用地理範圍篩選", value=False)
149
  with gr.Row():
150
- min_lat_input = gr.Number(label="最小緯度 (南)", value=21.5)
151
- max_lat_input = gr.Number(label="最大緯度 (北)", value=25.5)
152
  with gr.Row():
153
- min_lon_input = gr.Number(label="最小經度 (西)", value=120.0)
154
- max_lon_input = gr.Number(label="最大經度 (東)", value=122.5)
155
-
156
- submit_btn = gr.Button("開始查詢", variant="primary")
157
 
 
 
 
158
  with gr.Column(scale=5):
159
- gr.Markdown("### 查詢結果")
160
- # 建立 Tabs 來切換地圖和詳細資料
161
  with gr.Tabs():
162
- with gr.TabItem("地震分佈圖"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  map_plot = gr.Plot()
164
- with gr.TabItem("詳細資料列表"):
165
- output_markdown = gr.Markdown("點擊「開始查詢」後,結果將會顯示於此。")
 
166
 
167
- # 綁定按鈕點擊事件
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  submit_btn.click(
169
- fn=query_earthquakes,
170
- inputs=[
171
- starttime_input, endtime_input, magnitude_slider,
172
- use_geo_checkbox, min_lat_input, max_lat_input, min_lon_input, max_lon_input
173
- ],
174
- outputs=[output_markdown, map_plot] # 同時更新 Markdown 和 Plot
175
  )
176
 
177
- gr.Examples(
178
- examples=[
179
- ["2025-08-09", today.strftime('%Y-%m-%d'), 6.0, False, 21.5, 25.5, 120.0, 122.5],
180
- ["2024-04-02", "2024-04-04", 5.5, True, 23.0, 25.0, 121.0, 122.5],
181
- ["2024-01-01", "2024-01-02", 7.0, True, 35.0, 38.0, 136.0, 140.0],
182
- ],
183
- inputs=[
184
- starttime_input, endtime_input, magnitude_slider,
185
- use_geo_checkbox, min_lat_input, max_lat_input, min_lon_input, max_lon_input
186
- ],
187
- outputs=[output_markdown, map_plot],
188
- fn=query_earthquakes,
189
- cache_examples=True
190
  )
191
 
192
  if __name__ == "__main__":
 
3
  import pandas as pd
4
  import plotly.express as px
5
  from datetime import datetime, timedelta
6
+ import time
7
 
8
+ # --- 全局設定與 CSS 樣式 ---
9
  USGS_API_BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query"
10
 
11
+ # 自訂 CSS 來美化介面
12
+ CUSTOM_CSS = """
13
+ /* 美化主標題 */
14
+ #main-title {
15
+ text-align: center;
16
+ font-family: 'Arial Black', sans-serif;
17
+ color: #2c3e50;
18
+ font-size: 2.5em;
19
+ padding-top: 10px;
20
+ }
21
+ /* 讓控制面板的卡片有陰影和圓角 */
22
+ #control-panel {
23
+ border-radius: 15px;
24
+ background-color: #f8f9fa;
25
+ padding: 20px;
26
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
27
+ }
28
+ /* 結果統計卡片的樣式 */
29
+ .stats-card {
30
+ border-radius: 10px;
31
+ padding: 15px;
32
+ text-align: center;
33
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
34
+ }
35
+ .stats-card h3 { font-size: 1.2em; color: #34495e; }
36
+ .stats-card p { font-size: 1.5em; font-weight: bold; color: #e74c3c; }
37
+ /* 頁腳樣式 */
38
+ #footer {
39
+ text-align: center;
40
+ color: #7f8c8d;
41
+ font-size: 0.9em;
42
+ padding: 15px;
43
+ }
44
+ """
45
+
46
+ # --- 核心資料處理函式 ---
47
 
48
  def process_earthquake_data(data):
49
+ """將 API 的 JSON 資料轉換為 DataFrame, Markdown 和統計摘要"""
 
 
50
  if not data or 'features' not in data or not data['features']:
51
+ return None, "在指定的條件下,找不到任何地震資料。", None
52
 
 
 
 
 
 
 
 
 
 
 
53
  earthquake_list = []
 
54
  for feature in data['features']:
55
  properties = feature['properties']
56
  geometry = feature['geometry']
 
57
  event_time_utc = datetime.fromtimestamp(properties['time'] / 1000)
 
58
 
 
 
 
 
 
 
 
 
 
59
  earthquake_list.append({
60
+ 'latitude': geometry['coordinates'][1],
61
+ 'longitude': geometry['coordinates'][0],
62
+ 'magnitude': properties['mag'],
63
+ 'depth': geometry['coordinates'][2],
64
+ 'place': properties['place'],
65
+ 'time_utc': event_time_utc,
66
+ 'url': properties['url']
67
  })
68
 
69
  df = pd.DataFrame(earthquake_list)
70
+ if df.empty:
71
+ return None, "在指定的條件下,找不到任何地震資料。", None
72
+
73
+ # --- 製作統計摘要 ---
74
+ strongest_quake = df.loc[df['magnitude'].idxmax()]
75
+ deepest_quake = df.loc[df['depth'].idxmax()]
76
+ stats = {
77
+ "count": len(df),
78
+ "avg_mag": f"{df['magnitude'].mean():.2f}",
79
+ "strongest_mag": f"{strongest_quake['magnitude']:.2f}",
80
+ "strongest_loc": strongest_quake['place'],
81
+ "deepest_depth": f"{deepest_quake['depth']:.2f} km",
82
+ "deepest_loc": deepest_quake['place']
83
+ }
84
+
85
+ # --- 製作 Markdown 表格 ---
86
+ df_display = df.copy()
87
+ df_display['time_utc'] = df_display['time_utc'].dt.strftime('%Y-%m-%d %H:%M:%S')
88
+ md_table = df_display[['time_utc', 'place', 'magnitude', 'depth']].to_markdown(index=False)
89
+
90
+ return df, md_table, stats
91
 
92
+ def create_earthquake_map(df):
93
+ """使用 Plotly 繪製互動式地震分佈圖"""
94
+ if df is None or df.empty:
95
+ return None
96
 
97
+ fig = px.scatter_geo(
98
+ df,
99
+ lat='latitude',
100
+ lon='longitude',
101
+ color='magnitude',
102
+ size=df['magnitude'].apply(lambda x: x**2), # 讓規模大小差異更明顯
103
+ hover_name='place',
104
+ hover_data={'magnitude': ':.2f', 'depth': ':.2f km', 'time_utc': True, 'latitude':False, 'longitude':False},
105
+ projection="natural earth",
106
+ title="地震分佈圖",
107
+ color_continuous_scale=px.colors.sequential.Inferno
108
+ )
109
+ fig.update_layout(margin={"r":0, "t":40, "l":0, "b":0})
110
+ return fig
111
+
112
+ # --- Gradio 主要執行函式 ---
113
+
114
+ def query_and_visualize(
115
  starttime_str: str,
116
  endtime_str: str,
117
  min_magnitude: float,
 
119
  min_lat: float, max_lat: float,
120
  min_lon: float, max_lon: float
121
  ):
122
+ """主函式,接收 UI 輸入,回傳所有 UI 更新"""
123
+ # 1. 顯示載入狀態
124
+ yield {
125
+ submit_btn: gr.update(value="⏳ 查詢中...", interactive=False),
126
+ map_plot: None,
127
+ stats_group: gr.update(visible=False),
128
+ download_group: gr.update(visible=False),
129
+ md_output: "正在從 USGS 獲取資料,請稍候..."
130
+ }
131
+
132
  params = {
133
+ "format": "geojson", "starttime": starttime_str, "endtime": endtime_str,
134
+ "minmagnitude": min_magnitude, "limit": 2000, "orderby": "time"
 
 
 
 
135
  }
136
 
137
  if use_geo:
138
  if not all([min_lat is not None, max_lat is not None, min_lon is not None, max_lon is not None]):
139
+ yield { submit_btn: gr.update(value="開始查詢", interactive=True), md_output: "❌ 錯誤:地理範圍四個欄位皆須填寫。" }
140
+ return
141
+ params.update({"minlatitude": min_lat, "maxlatitude": max_lat, "minlongitude": min_lon, "maxlongitude": max_lon})
 
 
 
 
142
 
143
  try:
144
+ response = requests.get(USGS_API_BASE_URL, params=params, timeout=25)
145
  response.raise_for_status()
146
+ df, md_table, stats = process_earthquake_data(response.json())
 
 
 
147
 
148
+ if df is not None:
149
+ map_figure = create_earthquake_map(df)
150
+ # DataFrame 存為 CSV 格式的暫存檔
151
+ csv_path = "earthquake_data.csv"
152
+ df.to_csv(csv_path, index=False)
153
+
154
+ yield {
155
+ submit_btn: gr.update(value="查詢完成", interactive=True),
156
+ map_plot: map_figure,
157
+ md_output: md_table,
158
+ stats_group: gr.update(visible=True),
159
+ download_group: gr.update(visible=True),
160
+ file_download: gr.update(value=csv_path, visible=True),
161
+ # 更新統計數據
162
+ stats_count_val: str(stats['count']),
163
+ stats_avg_mag_val: stats['avg_mag'],
164
+ stats_strongest_val: f"{stats['strongest_mag']} @ {stats['strongest_loc']}",
165
+ stats_deepest_val: f"{stats['deepest_depth']} @ {stats['deepest_loc']}"
166
+ }
167
  else:
168
+ yield {
169
+ submit_btn: gr.update(value="開始查詢", interactive=True),
170
+ md_output: "在指定的條件下,找不到任何地震資料。"
171
+ }
172
  except Exception as e:
173
+ yield { submit_btn: gr.update(value="查詢失敗", interactive=True), md_output: f" 發生錯誤: {e}" }
174
+
175
+ # 短暫延遲後恢復按鈕文字
176
+ time.sleep(2)
177
+ yield { submit_btn: gr.update(value="開始查詢", interactive=True) }
178
 
179
+ # --- Gradio UI 介面設計 ---
180
+
181
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="sky", secondary_hue="light"), css=CUSTOM_CSS) as demo:
182
+ # 標題
183
+ gr.Markdown("# 🌋 地震視覺化儀 (Quake Visualizer)", elem_id="main-title")
184
 
185
  with gr.Row():
186
+ # 左側控制面板
187
+ with gr.Column(scale=2, elem_id="control-panel"):
188
+ gr.Markdown("### ⚙️ 控制面板")
 
189
 
190
+ with gr.Accordion("時間與規模", open=True):
191
+ # 預設查詢時間為過去 24 小時
192
+ now = datetime.now()
193
+ yesterday = now - timedelta(days=1)
194
+ starttime_input = gr.Textbox(label="開始日期 (YYYY-MM-DD)", value=yesterday.strftime('%Y-%m-%d'))
195
+ endtime_input = gr.Textbox(label="結束日期 (YYYY-MM-DD)", value=now.strftime('%Y-%m-%d'))
196
+ magnitude_slider = gr.Slider(minimum=0, maximum=10, step=0.1, label="最小芮氏規模", value=2.5)
197
 
198
+ with gr.Accordion("地理範圍 (可選)", open=False):
 
 
199
  use_geo_checkbox = gr.Checkbox(label="啟用地理範圍篩選", value=False)
200
  with gr.Row():
201
+ min_lat_input = gr.Number(label="最小緯度", value=21.5, interactive=False)
202
+ max_lat_input = gr.Number(label="最大緯度", value=25.5, interactive=False)
203
  with gr.Row():
204
+ min_lon_input = gr.Number(label="最小經度", value=120.0, interactive=False)
205
+ max_lon_input = gr.Number(label="最大經度", value=122.5, interactive=False)
 
 
206
 
207
+ submit_btn = gr.Button("開始查詢", variant="primary")
208
+
209
+ # 右側結果顯示區
210
  with gr.Column(scale=5):
 
 
211
  with gr.Tabs():
212
+ with gr.TabItem("📊 視覺化儀表板"):
213
+ with gr.Row(visible=False) as stats_group:
214
+ with gr.Column(elem_classes="stats-card"):
215
+ gr.Markdown("<h3>總計地震數</h3>")
216
+ stats_count_val = gr.Markdown("<p>0</p>")
217
+ with gr.Column(elem_classes="stats-card"):
218
+ gr.Markdown("<h3>平均規模</h3>")
219
+ stats_avg_mag_val = gr.Markdown("<p>0</p>")
220
+ with gr.Column(elem_classes="stats-card"):
221
+ gr.Markdown("<h3>最強地震</h3>")
222
+ stats_strongest_val = gr.Markdown("<p>-</p>")
223
+ with gr.Column(elem_classes="stats-card"):
224
+ gr.Markdown("<h3>最深地震</h3>")
225
+ stats_deepest_val = gr.Markdown("<p>-</p>")
226
+
227
  map_plot = gr.Plot()
228
+
229
+ with gr.Row(visible=False) as download_group:
230
+ file_download = gr.File(label="下載 CSV 資料", visible=True)
231
 
232
+ with gr.TabItem("📄 詳細數據"):
233
+ md_output = gr.Markdown("點擊「開始查詢」後,結果將顯示於此。")
234
+
235
+ gr.Markdown("---", elem_id="footer")
236
+ gr.Markdown("由 Gradio 與 Plotly 強力驅動 | 資料來源:美國地質調查局 (USGS)", elem_id="footer")
237
+
238
+ # --- 事件監聽與互動邏輯 ---
239
+ def toggle_geo_inputs(enabled):
240
+ return {
241
+ min_lat_input: gr.update(interactive=enabled), max_lat_input: gr.update(interactive=enabled),
242
+ min_lon_input: gr.update(interactive=enabled), max_lon_input: gr.update(interactive=enabled)
243
+ }
244
+ use_geo_checkbox.change(fn=toggle_geo_inputs, inputs=use_geo_checkbox, outputs=[min_lat_input, max_lat_input, min_lon_input, max_lon_input])
245
+
246
+ # 綁定主查詢函式
247
  submit_btn.click(
248
+ fn=query_and_visualize,
249
+ inputs=[starttime_input, endtime_input, magnitude_slider, use_geo_checkbox, min_lat_input, max_lat_input, min_lon_input, max_lon_input],
250
+ outputs=[submit_btn, map_plot, md_output, stats_group, download_group, file_download, stats_count_val, stats_avg_mag_val, stats_strongest_val, stats_deepest_val]
 
 
 
251
  )
252
 
253
+ # 頁面載入時自動觸發一次查詢
254
+ demo.load(
255
+ fn=query_and_visualize,
256
+ inputs=[starttime_input, endtime_input, magnitude_slider, use_geo_checkbox, min_lat_input, max_lat_input, min_lon_input, max_lon_input],
257
+ outputs=[submit_btn, map_plot, md_output, stats_group, download_group, file_download, stats_count_val, stats_avg_mag_val, stats_strongest_val, stats_deepest_val]
 
 
 
 
 
 
 
 
258
  )
259
 
260
  if __name__ == "__main__":