Spaces:
Sleeping
Sleeping
| 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() | |