Usgs_api_openai / app.py
cwadayi's picture
Update app.py
769f92b verified
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()