jimmy60504 commited on
Commit
ea06d35
·
1 Parent(s): 75416a6

add function to create input station map and update UI for waveform loading

Browse files
Files changed (1) hide show
  1. app.py +169 -38
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, vs30_input):
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"], vs30_input)
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
- vs30_input = gr.Number(
931
- value=600,
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
- info_output = gr.Textbox(label="狀態資訊", lines=5, interactive=False)
950
- stats_output = gr.Textbox(label="預測統計", lines=4)
 
 
 
 
951
 
952
- # 右側:波形圖
953
  with gr.Column(scale=1):
954
  gr.Markdown("## 輸入波形")
955
- waveform_plot = gr.Plot(label="地震波形(選定的最近測站)")
956
 
957
- # 下方:Ground Truth vs 預測結果
958
  with gr.Row():
959
- # 左側:Ground Truth
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, vs30_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