Spaces:
Sleeping
Sleeping
File size: 12,637 Bytes
d470cce 38b76c5 b3cbfc1 38b76c5 8f56933 d470cce 8f56933 38b76c5 d470cce 8f56933 769f92b 8f56933 769f92b 8f56933 b3cbfc1 8f56933 38b76c5 8f56933 38b76c5 b3cbfc1 38b76c5 b3cbfc1 8f56933 b3cbfc1 8f56933 b3cbfc1 769f92b 8f56933 38b76c5 769f92b 8f56933 769f92b 8f56933 769f92b 8f56933 38b76c5 769f92b 38b76c5 8f56933 38b76c5 8f56933 d470cce 38b76c5 769f92b 8f56933 d470cce 8f56933 38b76c5 8f56933 b3cbfc1 8f56933 769f92b 8f56933 769f92b 8f56933 b3cbfc1 769f92b d470cce 769f92b 8f56933 d470cce 8f56933 316b3ca 8f56933 38b76c5 d470cce 8f56933 38b76c5 8f56933 769f92b 8f56933 769f92b 8f56933 769f92b b3cbfc1 8f56933 b3cbfc1 8f56933 38b76c5 769f92b 8f56933 b3cbfc1 8f56933 769f92b 8f56933 769f92b b3cbfc1 8f56933 38b76c5 8f56933 769f92b 8f56933 769f92b 8f56933 769f92b 8f56933 769f92b 8f56933 769f92b 38b76c5 8f56933 769f92b d470cce 38b76c5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
import requests
import gradio as gr
import pandas as pd
import plotly.express as px
from datetime import datetime, timedelta
import time
# --- 全局設定與 CSS 樣式 ---
USGS_API_BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query"
# 自訂 CSS 來美化介面
CUSTOM_CSS = """
/* ... (CSS 樣式與之前相同,為節省空間此處省略) ... */
#main-title { text-align: center; font-family: 'Arial Black', sans-serif; color: #2c3e50; font-size: 2.5em; padding-top: 10px; }
#control-panel { border-radius: 15px; background-color: #f8f9fa; padding: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
.stats-card { border-radius: 10px; padding: 15px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.stats-card h3 { font-size: 1.2em; color: #34495e; }
.stats-card p { font-size: 1.5em; font-weight: bold; color: #e74c3c; }
#footer { text-align: center; color: #7f8c8d; font-size: 0.9em; padding: 15px; }
"""
# --- 核心資料處理函式 ---
def process_earthquake_data(data):
"""將 API 的 JSON 資料轉換為 DataFrame, Markdown 和統計摘要"""
if not data or 'features' not in data or not data['features']:
return None, "在指定的條件下,找不到任何地震資料。", None
earthquake_list = []
for feature in data['features']:
properties = feature['properties']
geometry = feature['geometry']
event_time_utc = datetime.fromtimestamp(properties['time'] / 1000)
earthquake_list.append({
'latitude': geometry['coordinates'][1],
'longitude': geometry['coordinates'][0],
'magnitude': properties['mag'],
'depth': geometry['coordinates'][2],
'place': properties['place'],
'time_utc': event_time_utc,
'url': properties['url']
})
df = pd.DataFrame(earthquake_list)
if df.empty:
return None, "在指定的條件下,找不到任何地震資料。", None
strongest_quake = df.loc[df['magnitude'].idxmax()]
deepest_quake = df.loc[df['depth'].idxmax()]
stats = {
"count": len(df),
"avg_mag": f"{df['magnitude'].mean():.2f}",
"strongest_mag": f"{strongest_quake['magnitude']:.2f}",
"strongest_loc": strongest_quake['place'],
"deepest_depth": f"{deepest_quake['depth']:.2f} km",
"deepest_loc": deepest_quake['place']
}
df_display = df.copy()
df_display['time_utc'] = df_display['time_utc'].dt.strftime('%Y-%m-%d %H:%M:%S')
md_table = df_display[['time_utc', 'place', 'magnitude', 'depth']].to_markdown(index=False)
return df, md_table, stats
# --- 🎨 全新升級的地圖函式 ---
def create_earthquake_map(df, map_style):
"""使用 Plotly 繪製更 Fancy 的互動式地震分佈圖"""
if df is None or df.empty:
return None
# 建立一個包含豐富 HTML 格式的懸停資訊欄位
df['hover_text'] = df.apply(
lambda row: (
f"<b>📍 {row['place']}</b><br><br>"
f"<b>時間:</b> {row['time_utc'].strftime('%Y-%m-%d %H:%M:%S')} UTC<br>"
f"<b>芮氏規模:</b> {row['magnitude']:.2f}<br>"
f"<b>深度:</b> {row['depth']:.2f} km<br>"
f"<a href='{row['url']}' target='_blank'>查看 USGS 詳細報告</a>"
),
axis=1
)
# 設置地圖中心點
center_lat = df['latitude'].mean()
center_lon = df['longitude'].mean()
# 使用 px.scatter_mapbox 來獲得更豐富的底圖
fig = px.scatter_mapbox(
df,
lat="latitude",
lon="longitude",
size="magnitude", # 點的大小與規模相關
color="magnitude", # 點的顏色也與規模相關
hover_name="hover_text", # 使用我們自訂的 HTML 懸停文字
hover_data={"hover_text": False, "latitude": False, "longitude": False, "magnitude": False}, # 隱藏預設的懸停資料
color_continuous_scale=px.colors.sequential.YlOrRd, # 使用黃-橙-紅的火焰色階
size_max=50, # 最大的點的尺寸
zoom=3, # 初始縮放等級
center={"lat": center_lat, "lon": center_lon},
mapbox_style=map_style # 從 UI 傳入的地圖樣式!
)
fig.update_layout(
title={
'text': "<b>地震分佈視覺化儀表板</b>",
'y':0.95,
'x':0.5,
'xanchor': 'center',
'yanchor': 'top'
},
margin={"r": 0, "t": 40, "l": 0, "b": 0},
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
mapbox=dict(
bearing=0,
pitch=0
),
# 移除 colorbar 的標題,讓介面更乾淨
coloraxis_colorbar=dict(
title=""
)
)
return fig
# --- Gradio 主要執行函式 ---
def query_and_visualize(
starttime_str: str,
endtime_str: str,
min_magnitude: float,
use_geo: bool,
min_lat: float, max_lat: float,
min_lon: float, max_lon: float,
map_style: str # 新增地圖樣式參數
):
"""主函式,接收 UI 輸入,回傳所有 UI 更新"""
yield {
submit_btn: gr.update(value="⏳ 查詢中...", interactive=False),
map_plot: None,
stats_group: gr.update(visible=False),
download_group: gr.update(visible=False),
md_output: "正在從 USGS 獲取資料,請稍候..."
}
params = {
"format": "geojson", "starttime": starttime_str, "endtime": endtime_str,
"minmagnitude": min_magnitude, "limit": 2000, "orderby": "time"
}
if use_geo:
# ... (驗證邏輯與之前相同) ...
params.update({"minlatitude": min_lat, "maxlatitude": max_lat, "minlongitude": min_lon, "maxlongitude": max_lon})
try:
response = requests.get(USGS_API_BASE_URL, params=params, timeout=25)
response.raise_for_status()
df, md_table, stats = process_earthquake_data(response.json())
if df is not None:
# 傳入 map_style
map_figure = create_earthquake_map(df, map_style)
csv_path = "earthquake_data.csv"
df.to_csv(csv_path, index=False)
yield {
# ... (其他元件更新與之前相同) ...
submit_btn: gr.update(value="查詢完成", interactive=True),
map_plot: map_figure,
md_output: md_table,
stats_group: gr.update(visible=True),
download_group: gr.update(visible=True),
file_download: gr.update(value=csv_path, visible=True),
stats_count_val: str(stats['count']),
stats_avg_mag_val: stats['avg_mag'],
stats_strongest_val: f"{stats['strongest_mag']} @ {stats['strongest_loc']}",
stats_deepest_val: f"{stats['deepest_depth']} @ {stats['deepest_loc']}"
}
else:
# ... (錯誤處理與之前相同) ...
yield { submit_btn: gr.update(value="開始查詢", interactive=True), md_output: "在指定的條件下,找不到任何地震資料。" }
except Exception as e:
# ... (錯誤處理與之前相同) ...
yield { submit_btn: gr.update(value="查詢失敗", interactive=True), md_output: f"❌ 發生錯誤: {e}" }
time.sleep(2)
yield { submit_btn: gr.update(value="開始查詢", interactive=True) }
# --- Gradio UI 介面設計 ---
with gr.Blocks(theme=gr.themes.Soft(primary_hue="sky", secondary_hue="neutral"), css=CUSTOM_CSS) as demo:
gr.Markdown("# 🌋 地震視覺化儀 (Quake Visualizer)", elem_id="main-title")
with gr.Row():
with gr.Column(scale=2, elem_id="control-panel"):
gr.Markdown("### ⚙️ 控制面板")
with gr.Accordion("時間與規模", open=True):
# ... (時間與規模元件與之前相同) ...
now = datetime.now()
yesterday = now - timedelta(days=1)
starttime_input = gr.Textbox(label="開始日期 (YYYY-MM-DD)", value=yesterday.strftime('%Y-%m-%d'))
endtime_input = gr.Textbox(label="結束日期 (YYYY-MM-DD)", value=now.strftime('%Y-%m-%d'))
magnitude_slider = gr.Slider(minimum=0, maximum=10, step=0.1, label="最小芮氏規模", value=2.5)
with gr.Accordion("地理範圍 (可選)", open=False):
# ... (地理範圍元件與之前相同) ...
use_geo_checkbox = gr.Checkbox(label="啟用地理範圍篩選", value=False)
with gr.Row():
min_lat_input = gr.Number(label="最小緯度", value=21.5, interactive=False)
max_lat_input = gr.Number(label="最大緯度", value=25.5, interactive=False)
with gr.Row():
min_lon_input = gr.Number(label="最小經度", value=120.0, interactive=False)
max_lon_input = gr.Number(label="最大經度", value=122.5, interactive=False)
# --- ✨ 新增地圖樣式選擇器 ---
with gr.Accordion("地圖樣式", open=True):
map_style_dropdown = gr.Dropdown(
label="選擇地圖底圖",
choices=[
"open-street-map",
"carto-darkmatter",
"carto-positron",
"stamen-terrain",
"stamen-toner",
"stamen-watercolor"
],
value="carto-darkmatter" # 預設使用深色模式
)
submit_btn = gr.Button("開始查詢", variant="primary")
with gr.Column(scale=5):
with gr.Tabs():
with gr.TabItem("📊 視覺化儀表板"):
# ... (統計卡片與下載元件與之前相同) ...
with gr.Row(visible=False) as stats_group:
with gr.Column(elem_classes="stats-card"):
gr.Markdown("<h3>總計地震數</h3>")
stats_count_val = gr.Markdown("<p>0</p>")
with gr.Column(elem_classes="stats-card"):
gr.Markdown("<h3>平均規模</h3>")
stats_avg_mag_val = gr.Markdown("<p>0</p>")
with gr.Column(elem_classes="stats-card"):
gr.Markdown("<h3>最強地震</h3>")
stats_strongest_val = gr.Markdown("<p>-</p>")
with gr.Column(elem_classes="stats-card"):
gr.Markdown("<h3>最深地震</h3>")
stats_deepest_val = gr.Markdown("<p>-</p>")
map_plot = gr.Plot()
with gr.Row(visible=False) as download_group:
file_download = gr.File(label="下載 CSV 資料", visible=True)
with gr.TabItem("📄 詳細數據"):
md_output = gr.Markdown("點擊「開始查詢」後,結果將顯示於此。")
# ... (頁腳與之前相同) ...
gr.Markdown("---", elem_id="footer")
gr.Markdown("由 Gradio 與 Plotly 強力驅動 | 資料來源:美國地制調查局 (USGS)", elem_id="footer")
# --- 事件監聽與互動邏輯 ---
# ... (地理範圍鎖定邏輯與之前相同) ...
def toggle_geo_inputs(enabled):
return {
min_lat_input: gr.update(interactive=enabled), max_lat_input: gr.update(interactive=enabled),
min_lon_input: gr.update(interactive=enabled), max_lon_input: gr.update(interactive=enabled)
}
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])
# 組合所有輸入元件
all_inputs = [starttime_input, endtime_input, magnitude_slider, use_geo_checkbox, min_lat_input, max_lat_input, min_lon_input, max_lon_input, map_style_dropdown]
all_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]
# 綁定主查詢函式
submit_btn.click(fn=query_and_visualize, inputs=all_inputs, outputs=all_outputs)
# 頁面載入時自動觸發一次查詢
demo.load(fn=query_and_visualize, inputs=all_inputs, outputs=all_outputs)
if __name__ == "__main__":
demo.launch()
|