Spaces:
Running
Running
Commit
·
ea06d35
1
Parent(s):
75416a6
add function to create input station map and update UI for waveform loading
Browse files
app.py
CHANGED
|
@@ -754,6 +754,129 @@ def load_ground_truth_image(event_name):
|
|
| 754 |
return None
|
| 755 |
|
| 756 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 757 |
def load_and_display_waveform(event_name, start_time, end_time, epicenter_lon, epicenter_lat):
|
| 758 |
"""載入並顯示波形,讓使用者確認範圍"""
|
| 759 |
try:
|
|
@@ -772,6 +895,10 @@ def load_and_display_waveform(event_name, start_time, end_time, epicenter_lon, e
|
|
| 772 |
# 3. 繪製波形
|
| 773 |
waveform_plot = plot_waveform(st, selected_stations, start_time, end_time)
|
| 774 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
info_text = f"✅ 已載入波形資料\n"
|
| 776 |
info_text += f"選取時間範圍: {start_time:.1f} - {end_time:.1f} 秒\n"
|
| 777 |
info_text += f"震央位置: ({epicenter_lon:.4f}, {epicenter_lat:.4f})\n"
|
|
@@ -779,16 +906,16 @@ def load_and_display_waveform(event_name, start_time, end_time, epicenter_lon, e
|
|
| 779 |
info_text += f"請確認波形範圍後,點擊「執行預測」按鈕"
|
| 780 |
|
| 781 |
logger.info("波形載入完成")
|
| 782 |
-
return waveform_plot, info_text, gr.update(interactive=True)
|
| 783 |
|
| 784 |
except Exception as e:
|
| 785 |
logger.error(f"波形載入發生錯誤: {e}")
|
| 786 |
import traceback
|
| 787 |
traceback.print_exc()
|
| 788 |
-
return None, f"錯誤: {str(e)}", gr.update(interactive=False)
|
| 789 |
|
| 790 |
|
| 791 |
-
def predict_intensity(event_name, start_time, end_time, epicenter_lon, epicenter_lat
|
| 792 |
"""執行震度預測"""
|
| 793 |
try:
|
| 794 |
# 1. 載入完整的 mseed 檔案
|
|
@@ -801,12 +928,12 @@ def predict_intensity(event_name, start_time, end_time, epicenter_lon, epicenter
|
|
| 801 |
selected_stations = select_nearest_stations(st, epicenter_lat, epicenter_lon, n_stations=25)
|
| 802 |
|
| 803 |
if len(selected_stations) == 0:
|
| 804 |
-
return None, "錯誤:找不到有效的測站資料"
|
| 805 |
|
| 806 |
-
# 3.
|
| 807 |
logger.info(f"提取波形資料(時間範圍: {start_time}-{end_time} 秒)...")
|
| 808 |
waveforms, station_info_list, valid_stations = extract_waveforms_from_stream(
|
| 809 |
-
st, selected_stations, start_time, end_time, vs30_input
|
| 810 |
)
|
| 811 |
|
| 812 |
if len(waveforms) == 0:
|
|
@@ -847,7 +974,7 @@ def predict_intensity(event_name, start_time, end_time, epicenter_lon, epicenter
|
|
| 847 |
target["latitude"],
|
| 848 |
target["longitude"],
|
| 849 |
target["elevation"],
|
| 850 |
-
get_vs30(target["latitude"], target["longitude"],
|
| 851 |
])
|
| 852 |
target_names.append(target["station"])
|
| 853 |
|
|
@@ -906,10 +1033,27 @@ def predict_intensity(event_name, start_time, end_time, epicenter_lon, epicenter
|
|
| 906 |
with gr.Blocks(title="TTSAM 震度預測系統") as demo:
|
| 907 |
gr.Markdown("# 🌏 TTSAM 震度預測系統")
|
| 908 |
|
|
|
|
| 909 |
with gr.Row():
|
| 910 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
with gr.Column(scale=1):
|
| 912 |
-
gr.Markdown("##
|
|
|
|
| 913 |
event_dropdown = gr.Dropdown(
|
| 914 |
choices=list(EARTHQUAKE_EVENTS.keys()),
|
| 915 |
value=list(EARTHQUAKE_EVENTS.keys())[0],
|
|
@@ -925,43 +1069,30 @@ with gr.Blocks(title="TTSAM 震度預測系統") as demo:
|
|
| 925 |
epicenter_lon_input = gr.Number(value=121.57, label="震央經度")
|
| 926 |
epicenter_lat_input = gr.Number(value=23.88, label="震央緯度")
|
| 927 |
|
| 928 |
-
gr.Markdown("### 場址參數")
|
| 929 |
with gr.Row():
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
label="Vs30 (m/s)",
|
| 933 |
-
info="剪力波速度(預設 600 m/s,若有 Vs30 資料會自動覆蓋)"
|
| 934 |
-
)
|
| 935 |
-
|
| 936 |
-
load_waveform_btn = gr.Button("📊 載入波形", variant="secondary")
|
| 937 |
-
predict_btn = gr.Button("🔮 執行預測", variant="primary", interactive=False)
|
| 938 |
-
|
| 939 |
-
gr.Markdown("""
|
| 940 |
-
### 使用步驟
|
| 941 |
-
1. 選擇地震事件和時間範圍
|
| 942 |
-
2. 輸入震央位置
|
| 943 |
-
3. 點擊「載入波形」確認波形範圍
|
| 944 |
-
4. 確認無誤後,點擊「執行預測」
|
| 945 |
-
|
| 946 |
-
ℹ️ 系統會自動選擇距離震央最近的 25 個測站
|
| 947 |
-
""")
|
| 948 |
|
| 949 |
-
|
| 950 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 951 |
|
| 952 |
-
#
|
| 953 |
with gr.Column(scale=1):
|
| 954 |
gr.Markdown("## 輸入波形")
|
| 955 |
-
waveform_plot = gr.Plot(label="
|
| 956 |
|
| 957 |
-
#
|
| 958 |
with gr.Row():
|
| 959 |
-
#
|
| 960 |
with gr.Column(scale=1):
|
| 961 |
gr.Markdown("## Ground Truth 震度分布")
|
| 962 |
-
ground_truth_image = gr.Image(label="實際觀測震度", type="filepath")
|
| 963 |
|
| 964 |
-
#
|
| 965 |
with gr.Column(scale=1):
|
| 966 |
gr.Markdown("## 預測震度分布")
|
| 967 |
intensity_map = gr.HTML(label="互動式震度地圖", elem_id="intensity_map")
|
|
@@ -971,13 +1102,13 @@ with gr.Blocks(title="TTSAM 震度預測系統") as demo:
|
|
| 971 |
load_waveform_btn.click(
|
| 972 |
fn=load_and_display_waveform,
|
| 973 |
inputs=[event_dropdown, start_slider, end_slider, epicenter_lon_input, epicenter_lat_input],
|
| 974 |
-
outputs=[waveform_plot, info_output, predict_btn]
|
| 975 |
)
|
| 976 |
|
| 977 |
# 第二步:執行預測
|
| 978 |
predict_btn.click(
|
| 979 |
fn=predict_intensity,
|
| 980 |
-
inputs=[event_dropdown, start_slider, end_slider, epicenter_lon_input, epicenter_lat_input
|
| 981 |
outputs=[ground_truth_image, intensity_map, stats_output]
|
| 982 |
)
|
| 983 |
|
|
|
|
| 754 |
return None
|
| 755 |
|
| 756 |
|
| 757 |
+
def create_input_station_map(selected_stations, epicenter_lat, epicenter_lon):
|
| 758 |
+
"""創建輸入測站分布地圖:顯示所有測站 + 突顯被選中的 25 個"""
|
| 759 |
+
import folium
|
| 760 |
+
from folium import plugins
|
| 761 |
+
|
| 762 |
+
# 創建地圖,中心點設在震央
|
| 763 |
+
m = folium.Map(
|
| 764 |
+
location=[epicenter_lat, epicenter_lon],
|
| 765 |
+
zoom_start=8,
|
| 766 |
+
tiles='OpenStreetMap',
|
| 767 |
+
width='100%',
|
| 768 |
+
height='500px'
|
| 769 |
+
)
|
| 770 |
+
|
| 771 |
+
# 建立被選中測站的 set(用於快速查詢)
|
| 772 |
+
selected_station_codes = {s["station"] for s in selected_stations}
|
| 773 |
+
|
| 774 |
+
# 1. 先繪製所有測站(灰色小點)
|
| 775 |
+
logger.info(f"繪製所有測站 ({len(site_info)} 個)...")
|
| 776 |
+
for idx, row in site_info.iterrows():
|
| 777 |
+
station_code = row["Station"]
|
| 778 |
+
lat = row["Latitude"]
|
| 779 |
+
lon = row["Longitude"]
|
| 780 |
+
|
| 781 |
+
# 跳過被選中的測站(稍後用不同樣式繪製)
|
| 782 |
+
if station_code in selected_station_codes:
|
| 783 |
+
continue
|
| 784 |
+
|
| 785 |
+
folium.CircleMarker(
|
| 786 |
+
location=[lat, lon],
|
| 787 |
+
radius=2,
|
| 788 |
+
popup=f'{station_code}',
|
| 789 |
+
tooltip=station_code,
|
| 790 |
+
color='gray',
|
| 791 |
+
fillColor='lightgray',
|
| 792 |
+
fillOpacity=0.4,
|
| 793 |
+
weight=1
|
| 794 |
+
).add_to(m)
|
| 795 |
+
|
| 796 |
+
# 2. 標記震央(紅色星星)
|
| 797 |
+
folium.Marker(
|
| 798 |
+
[epicenter_lat, epicenter_lon],
|
| 799 |
+
popup=f'<b>震央</b><br>({epicenter_lat:.3f}, {epicenter_lon:.3f})',
|
| 800 |
+
icon=folium.Icon(color='red', icon='star', prefix='fa'),
|
| 801 |
+
tooltip='震央位置',
|
| 802 |
+
zIndexOffset=1000
|
| 803 |
+
).add_to(m)
|
| 804 |
+
|
| 805 |
+
# 3. 標記被選中的 25 個測站(彩色大點)
|
| 806 |
+
for i, station_data in enumerate(selected_stations):
|
| 807 |
+
station_code = station_data["station"]
|
| 808 |
+
lat = station_data["latitude"]
|
| 809 |
+
lon = station_data["longitude"]
|
| 810 |
+
distance = station_data["distance"]
|
| 811 |
+
|
| 812 |
+
# 創建 popup 內容
|
| 813 |
+
popup_html = f"""
|
| 814 |
+
<div style="font-family: Arial; min-width: 150px;">
|
| 815 |
+
<h4 style="margin: 0 0 10px 0; color: #d63031;">{station_code}</h4>
|
| 816 |
+
<table style="width:100%;">
|
| 817 |
+
<tr><td><b>狀態:</b></td><td><span style="color: #00b894;">✓ 已選中</span></td></tr>
|
| 818 |
+
<tr><td><b>順序:</b></td><td>第 {i+1} 近</td></tr>
|
| 819 |
+
<tr><td><b>距離:</b></td><td>{distance:.2f}°</td></tr>
|
| 820 |
+
<tr><td><b>位置:</b></td><td>({lat:.3f}, {lon:.3f})</td></tr>
|
| 821 |
+
</table>
|
| 822 |
+
</div>
|
| 823 |
+
"""
|
| 824 |
+
|
| 825 |
+
# 根據距離設定顏色
|
| 826 |
+
if i < 5:
|
| 827 |
+
color = 'green'
|
| 828 |
+
elif i < 15:
|
| 829 |
+
color = 'blue'
|
| 830 |
+
else:
|
| 831 |
+
color = 'orange'
|
| 832 |
+
|
| 833 |
+
folium.CircleMarker(
|
| 834 |
+
location=[lat, lon],
|
| 835 |
+
radius=10,
|
| 836 |
+
popup=folium.Popup(popup_html, max_width=250),
|
| 837 |
+
tooltip=f'✓ {station_code} (第{i+1}近)',
|
| 838 |
+
color='black',
|
| 839 |
+
fillColor=color,
|
| 840 |
+
fillOpacity=0.8,
|
| 841 |
+
weight=2,
|
| 842 |
+
zIndexOffset=500
|
| 843 |
+
).add_to(m)
|
| 844 |
+
|
| 845 |
+
# 4. 添加圖例
|
| 846 |
+
total_stations = len(site_info)
|
| 847 |
+
legend_html = f'''
|
| 848 |
+
<div style="
|
| 849 |
+
position: fixed;
|
| 850 |
+
top: 10px; left: 10px;
|
| 851 |
+
width: 220px;
|
| 852 |
+
background-color: white;
|
| 853 |
+
border: 2px solid grey;
|
| 854 |
+
z-index: 9999;
|
| 855 |
+
font-size: 13px;
|
| 856 |
+
padding: 10px;
|
| 857 |
+
border-radius: 5px;
|
| 858 |
+
box-shadow: 2px 2px 6px rgba(0,0,0,0.3);
|
| 859 |
+
">
|
| 860 |
+
<h4 style="margin: 0 0 10px 0;">測站分布</h4>
|
| 861 |
+
<p style="margin: 5px 0;"><span style="color: red; font-size: 18px;">★</span> 震央</p>
|
| 862 |
+
<p style="margin: 5px 0;"><span style="color: lightgray;">●</span> 所有測站 ({total_stations} 個)</p>
|
| 863 |
+
<hr style="margin: 8px 0; border: none; border-top: 1px solid #ddd;">
|
| 864 |
+
<p style="margin: 5px 0; font-weight: bold;">被選中的測站:</p>
|
| 865 |
+
<p style="margin: 5px 0;"><span style="color: green; font-size: 16px;">●</span> 前 5 近</p>
|
| 866 |
+
<p style="margin: 5px 0;"><span style="color: blue; font-size: 16px;">●</span> 6-15 近</p>
|
| 867 |
+
<p style="margin: 5px 0;"><span style="color: orange; font-size: 16px;">●</span> 16-25 近</p>
|
| 868 |
+
<p style="margin: 5px 0; font-size: 11px; color: #666;">共選擇 {len(selected_stations)} 個測站</p>
|
| 869 |
+
</div>
|
| 870 |
+
'''
|
| 871 |
+
|
| 872 |
+
m.get_root().html.add_child(folium.Element(legend_html))
|
| 873 |
+
|
| 874 |
+
# 5. 添加全屏按鈕
|
| 875 |
+
plugins.Fullscreen().add_to(m)
|
| 876 |
+
|
| 877 |
+
return m
|
| 878 |
+
|
| 879 |
+
|
| 880 |
def load_and_display_waveform(event_name, start_time, end_time, epicenter_lon, epicenter_lat):
|
| 881 |
"""載入並顯示波形,讓使用者確認範圍"""
|
| 882 |
try:
|
|
|
|
| 895 |
# 3. 繪製波形
|
| 896 |
waveform_plot = plot_waveform(st, selected_stations, start_time, end_time)
|
| 897 |
|
| 898 |
+
# 4. 創建輸入測站地圖
|
| 899 |
+
station_map = create_input_station_map(selected_stations, epicenter_lat, epicenter_lon)
|
| 900 |
+
station_map_html = station_map._repr_html_()
|
| 901 |
+
|
| 902 |
info_text = f"✅ 已載入波形資料\n"
|
| 903 |
info_text += f"選取時間範圍: {start_time:.1f} - {end_time:.1f} 秒\n"
|
| 904 |
info_text += f"震央位置: ({epicenter_lon:.4f}, {epicenter_lat:.4f})\n"
|
|
|
|
| 906 |
info_text += f"請確認波形範圍後,點擊「執行預測」按鈕"
|
| 907 |
|
| 908 |
logger.info("波形載入完成")
|
| 909 |
+
return station_map_html, waveform_plot, info_text, gr.update(interactive=True)
|
| 910 |
|
| 911 |
except Exception as e:
|
| 912 |
logger.error(f"波形載入發生錯誤: {e}")
|
| 913 |
import traceback
|
| 914 |
traceback.print_exc()
|
| 915 |
+
return None, None, f"錯誤: {str(e)}", gr.update(interactive=False)
|
| 916 |
|
| 917 |
|
| 918 |
+
def predict_intensity(event_name, start_time, end_time, epicenter_lon, epicenter_lat):
|
| 919 |
"""執行震度預測"""
|
| 920 |
try:
|
| 921 |
# 1. 載入完整的 mseed 檔案
|
|
|
|
| 928 |
selected_stations = select_nearest_stations(st, epicenter_lat, epicenter_lon, n_stations=25)
|
| 929 |
|
| 930 |
if len(selected_stations) == 0:
|
| 931 |
+
return None, None, "錯誤:找不到有效的測站資料"
|
| 932 |
|
| 933 |
+
# 3. 從選定的測站提取波形(vs30_input 使用預設值 600,會被資料庫值覆蓋)
|
| 934 |
logger.info(f"提取波形資料(時間範圍: {start_time}-{end_time} 秒)...")
|
| 935 |
waveforms, station_info_list, valid_stations = extract_waveforms_from_stream(
|
| 936 |
+
st, selected_stations, start_time, end_time, vs30_input=600
|
| 937 |
)
|
| 938 |
|
| 939 |
if len(waveforms) == 0:
|
|
|
|
| 974 |
target["latitude"],
|
| 975 |
target["longitude"],
|
| 976 |
target["elevation"],
|
| 977 |
+
get_vs30(target["latitude"], target["longitude"], user_vs30=600)
|
| 978 |
])
|
| 979 |
target_names.append(target["station"])
|
| 980 |
|
|
|
|
| 1033 |
with gr.Blocks(title="TTSAM 震度預測系統") as demo:
|
| 1034 |
gr.Markdown("# 🌏 TTSAM 震度預測系統")
|
| 1035 |
|
| 1036 |
+
# ========== 上層:使用說明與參數設定 ==========
|
| 1037 |
with gr.Row():
|
| 1038 |
+
# 左上:使用步驟與狀態顯示
|
| 1039 |
+
with gr.Column(scale=1):
|
| 1040 |
+
gr.Markdown("## 使用步驟")
|
| 1041 |
+
gr.Markdown("""
|
| 1042 |
+
1. 選擇地震事件和時間範圍
|
| 1043 |
+
2. 輸入震央位置和場址參數
|
| 1044 |
+
3. 點擊「載入波形」確認波形範圍
|
| 1045 |
+
4. 確認無誤後,點擊「執行預測」
|
| 1046 |
+
|
| 1047 |
+
ℹ️ 系統會自動選擇距離震央最近的 25 個測站
|
| 1048 |
+
""")
|
| 1049 |
+
|
| 1050 |
+
info_output = gr.Textbox(label="狀態資訊", lines=6, interactive=False)
|
| 1051 |
+
stats_output = gr.Textbox(label="預測統計", lines=4, interactive=False)
|
| 1052 |
+
|
| 1053 |
+
# 右上:輸入參數
|
| 1054 |
with gr.Column(scale=1):
|
| 1055 |
+
gr.Markdown("## 輸入參數")
|
| 1056 |
+
|
| 1057 |
event_dropdown = gr.Dropdown(
|
| 1058 |
choices=list(EARTHQUAKE_EVENTS.keys()),
|
| 1059 |
value=list(EARTHQUAKE_EVENTS.keys())[0],
|
|
|
|
| 1069 |
epicenter_lon_input = gr.Number(value=121.57, label="震央經度")
|
| 1070 |
epicenter_lat_input = gr.Number(value=23.88, label="震央緯度")
|
| 1071 |
|
|
|
|
| 1072 |
with gr.Row():
|
| 1073 |
+
load_waveform_btn = gr.Button("📊 載入波形", variant="secondary", scale=1)
|
| 1074 |
+
predict_btn = gr.Button("🔮 執行預測", variant="primary", scale=1, interactive=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1075 |
|
| 1076 |
+
# ========== 中層:輸入測站地圖與波形圖 ==========
|
| 1077 |
+
with gr.Row():
|
| 1078 |
+
# 中左:輸入測站地圖
|
| 1079 |
+
with gr.Column(scale=1):
|
| 1080 |
+
gr.Markdown("## 輸入測站分布")
|
| 1081 |
+
input_station_map = gr.HTML(label="輸入測站地圖")
|
| 1082 |
|
| 1083 |
+
# 中右:輸入波形
|
| 1084 |
with gr.Column(scale=1):
|
| 1085 |
gr.Markdown("## 輸入波形")
|
| 1086 |
+
waveform_plot = gr.Plot(label="地震波形(選定的 25 個測站)")
|
| 1087 |
|
| 1088 |
+
# ========== 下層:Ground Truth vs 預測結果 ==========
|
| 1089 |
with gr.Row():
|
| 1090 |
+
# 左下:Ground Truth
|
| 1091 |
with gr.Column(scale=1):
|
| 1092 |
gr.Markdown("## Ground Truth 震度分布")
|
| 1093 |
+
ground_truth_image = gr.Image(label="實際觀測震度", type="filepath", height=600)
|
| 1094 |
|
| 1095 |
+
# 右下:預測震度地圖
|
| 1096 |
with gr.Column(scale=1):
|
| 1097 |
gr.Markdown("## 預測震度分布")
|
| 1098 |
intensity_map = gr.HTML(label="互動式震度地圖", elem_id="intensity_map")
|
|
|
|
| 1102 |
load_waveform_btn.click(
|
| 1103 |
fn=load_and_display_waveform,
|
| 1104 |
inputs=[event_dropdown, start_slider, end_slider, epicenter_lon_input, epicenter_lat_input],
|
| 1105 |
+
outputs=[input_station_map, waveform_plot, info_output, predict_btn]
|
| 1106 |
)
|
| 1107 |
|
| 1108 |
# 第二步:執行預測
|
| 1109 |
predict_btn.click(
|
| 1110 |
fn=predict_intensity,
|
| 1111 |
+
inputs=[event_dropdown, start_slider, end_slider, epicenter_lon_input, epicenter_lat_input],
|
| 1112 |
outputs=[ground_truth_image, intensity_map, stats_output]
|
| 1113 |
)
|
| 1114 |
|