jimmy60504 commited on
Commit
2c0009f
·
1 Parent(s): 2bd4899

refactor: streamline event handling and improve input station visualization

Browse files
Files changed (1) hide show
  1. app.py +122 -245
app.py CHANGED
@@ -91,11 +91,10 @@ except FileNotFoundError:
91
  except Exception as e:
92
  logger.error(f"{target_file} 載入失敗: {e}")
93
 
94
-
95
  # ============ 震央資訊管理 ============
96
 
97
  earthquake_metadata = {}
98
- event_json_path="waveform/event.json"
99
 
100
  try:
101
  import json
@@ -135,7 +134,6 @@ except FileNotFoundError:
135
  except Exception as e:
136
  logger.error(f"讀取事件元資料時發生錯誤: {e}")
137
 
138
-
139
  # 載入模型
140
  model_path = hf_hub_download(
141
  repo_id="SeisBlue/TTSAM", filename="ttsam_trained_model_11.pt"
@@ -263,8 +261,8 @@ def select_nearest_stations(st, epicenter_lat, epicenter_lon, n_stations=25):
263
 
264
 
265
  def extract_waveforms_from_stream(event_name,
266
- st, selected_stations, duration, vs30_input
267
- ):
268
  """
269
  從 Stream 中提取選定測站的波形資料
270
 
@@ -403,170 +401,6 @@ def extract_waveforms_from_stream(event_name,
403
  return waveforms, station_info_list, valid_stations, missing_components_count
404
 
405
 
406
- def create_input_station_map(selected_stations, epicenter_lat, epicenter_lon):
407
- """創建輸入測站分布地圖:顯示所有測站 + 突顯被選中的 25 個(使用 Plotly)"""
408
-
409
- selected_station_codes = {s["station"] for s in selected_stations}
410
-
411
- # 準備所有測站資料(未選中的測站)
412
- all_stations_lat = []
413
- all_stations_lon = []
414
- all_stations_text = []
415
-
416
- logger.info(f"繪製所有測站 ({len(site_info)} 個)...")
417
- for idx, row in site_info.iterrows():
418
- station_code = row["Station"]
419
- if station_code not in selected_station_codes:
420
- all_stations_lat.append(row["Latitude"])
421
- all_stations_lon.append(row["Longitude"])
422
- all_stations_text.append(station_code)
423
-
424
- # 準備選中測站資料(按距離分組)
425
- selected_group1_lat, selected_group1_lon, selected_group1_text = (
426
- [],
427
- [],
428
- [],
429
- ) # 前 5 近
430
- selected_group2_lat, selected_group2_lon, selected_group2_text = (
431
- [],
432
- [],
433
- [],
434
- ) # 6-15 近
435
- selected_group3_lat, selected_group3_lon, selected_group3_text = (
436
- [],
437
- [],
438
- [],
439
- ) # 16-25 近
440
-
441
- for i, station_data in enumerate(selected_stations):
442
- station_code = station_data["station"]
443
- lat = station_data["latitude"]
444
- lon = station_data["longitude"]
445
- distance = station_data["distance"]
446
-
447
- hover_text = f"{station_code}<br>✓ 已選中<br>第 {i+1} 近<br>距離: {distance:.2f}°<br>({lat:.3f}, {lon:.3f})"
448
-
449
- if i < 5:
450
- selected_group1_lat.append(lat)
451
- selected_group1_lon.append(lon)
452
- selected_group1_text.append(hover_text)
453
- elif i < 15:
454
- selected_group2_lat.append(lat)
455
- selected_group2_lon.append(lon)
456
- selected_group2_text.append(hover_text)
457
- else:
458
- selected_group3_lat.append(lat)
459
- selected_group3_lon.append(lon)
460
- selected_group3_text.append(hover_text)
461
-
462
- # 創建 Plotly 地圖
463
- fig = go.Figure()
464
-
465
- # 添加所有測站(灰色小點)
466
- fig.add_trace(
467
- go.Scattermap(
468
- lat=all_stations_lat,
469
- lon=all_stations_lon,
470
- mode="markers",
471
- marker=dict(size=6, color="gray", opacity=0.6),
472
- text=all_stations_text,
473
- hovertemplate="%{text}<extra></extra>",
474
- name=f"所有測站 ({len(all_stations_lat)} 個)",
475
- showlegend=True,
476
- )
477
- )
478
-
479
- # 添加選中測站 - 前 5 近(綠色)
480
- if selected_group1_lat:
481
- fig.add_trace(
482
- go.Scattermap(
483
- lat=selected_group1_lat,
484
- lon=selected_group1_lon,
485
- mode="markers",
486
- marker=dict(size=12, color="green", opacity=0.8),
487
- text=selected_group1_text,
488
- hovertemplate="%{text}<extra></extra>",
489
- name="前 5 近",
490
- showlegend=True,
491
- )
492
- )
493
-
494
- # 添加選中測站 - 6-15 近(藍色)
495
- if selected_group2_lat:
496
- fig.add_trace(
497
- go.Scattermap(
498
- lat=selected_group2_lat,
499
- lon=selected_group2_lon,
500
- mode="markers",
501
- marker=dict(size=12, color="blue", opacity=0.8),
502
- text=selected_group2_text,
503
- hovertemplate="%{text}<extra></extra>",
504
- name="6-15 近",
505
- showlegend=True,
506
- )
507
- )
508
-
509
- # 添加選中測站 - 16-25 近(橘色)
510
- if selected_group3_lat:
511
- fig.add_trace(
512
- go.Scattermap(
513
- lat=selected_group3_lat,
514
- lon=selected_group3_lon,
515
- mode="markers",
516
- marker=dict(size=12, color="orange", opacity=0.8),
517
- text=selected_group3_text,
518
- hovertemplate="%{text}<extra></extra>",
519
- name="16-25 近",
520
- showlegend=True,
521
- )
522
- )
523
-
524
- # 添加震央(紅色大點)
525
- fig.add_trace(
526
- go.Scattermap(
527
- lat=[epicenter_lat],
528
- lon=[epicenter_lon],
529
- mode="markers",
530
- marker=dict(size=25, color="red"),
531
- text=[f"震央<br>({epicenter_lat:.3f}, {epicenter_lon:.3f})"],
532
- hovertemplate="%{text}<extra></extra>",
533
- name="震央",
534
- showlegend=True,
535
- )
536
- )
537
-
538
- fig.add_trace(
539
- go.Scattermap(
540
- lat=[epicenter_lat],
541
- lon=[epicenter_lon],
542
- mode="markers",
543
- marker=dict(size=10, color="white"),
544
- showlegend=False,
545
- )
546
- )
547
-
548
- # 設置地圖佈局
549
- fig.update_layout(
550
- map=dict(
551
- style="open-street-map",
552
- center=dict(lat=epicenter_lat, lon=epicenter_lon),
553
- zoom=7,
554
- ),
555
- height=500, # 設置固定高度以適應 Gradio 容器
556
- margin=dict(l=0, r=0, t=0, b=0),
557
- showlegend=True,
558
- legend=dict(
559
- yanchor="top",
560
- y=0.95,
561
- xanchor="left",
562
- x=0.01,
563
- bgcolor="rgba(255, 255, 255, 0.8)",
564
- ),
565
- )
566
-
567
- return fig
568
-
569
-
570
  def plot_waveform(st, selected_stations, first_pick, duration):
571
  """
572
  繪製選定測站的波形圖(距離-時間圖,可顯示全部 25 個測站)
@@ -580,7 +414,7 @@ def plot_waveform(st, selected_stations, first_pick, duration):
580
  # 計算結束時間
581
  end_time = first_pick + duration
582
 
583
- fig, ax = plt.subplots(figsize=(14, 10))
584
 
585
  # 設定振幅縮放比例(避免波形重疊)
586
  amplitude_scale = 0.03 # 可調整此值來控制波形大小
@@ -624,7 +458,8 @@ def plot_waveform(st, selected_stations, first_pick, duration):
624
  except Exception as e:
625
  logger.warning(f"無法繪製測站 {station_code}: {e}")
626
 
627
- ax.axvline(first_pick, color="blue", linestyle="--", linewidth=2, alpha=0.7, label="初動時間")
 
628
 
629
  # 標記選取時間範圍
630
  ax.axvline(
@@ -633,7 +468,7 @@ def plot_waveform(st, selected_stations, first_pick, duration):
633
  linestyle="--",
634
  linewidth=2,
635
  alpha=0.7,
636
- label="選取範圍",
637
  )
638
  ax.axvline(end_time, color="red", linestyle="--", linewidth=2, alpha=0.7)
639
  ax.axvspan(0, end_time, alpha=0.15, color="blue")
@@ -685,9 +520,10 @@ def get_intensity_color(intensity):
685
 
686
 
687
  def create_intensity_map(
688
- pga_list, target_names, epicenter_lat=None, epicenter_lon=None
 
689
  ):
690
- """使用 Plotly 創建互動式震度分布地圖"""
691
 
692
  # 按震度等級分組資料
693
  intensity_groups = {
@@ -720,23 +556,45 @@ def create_intensity_map(
720
  intensity_groups[intensity]["lon"].append(lon)
721
  intensity_groups[intensity]["text"].append(hover_text)
722
 
723
- # 決定地圖中心(優先使用 epicenter,否則計算平均值)
724
- if epicenter_lat is not None and epicenter_lon is not None:
725
- map_center_lat = epicenter_lat
726
- map_center_lon = epicenter_lon
727
- elif all_lats and all_lons:
728
- map_center_lat = np.mean(all_lats)
729
- map_center_lon = np.mean(all_lons)
730
- else:
731
- # 如果沒有任何測站資料,使用台灣的中心
732
- map_center_lat = 23.5
733
- map_center_lon = 121.0
734
- logger.warning("無法決定地圖中心,使用台灣預設中心")
735
 
736
  # 創建 Plotly 地圖
737
  fig = go.Figure()
738
 
739
- # 添加各震度等級的測站
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740
  intensity_labels = ["0", "1", "2", "3", "4", "5-", "5+", "6-", "6+", "7"]
741
  for intensity_level in range(10):
742
  group = intensity_groups[intensity_level]
@@ -746,7 +604,7 @@ def create_intensity_map(
746
  lat=group["lat"],
747
  lon=group["lon"],
748
  mode="markers",
749
- marker=dict(size=14, color=group["color"], opacity=0.8),
750
  text=group["text"],
751
  hovertemplate="%{text}<extra></extra>",
752
  name=f"震度 {intensity_labels[intensity_level]}",
@@ -760,21 +618,47 @@ def create_intensity_map(
760
  lat=[None],
761
  lon=[None],
762
  mode="markers",
763
- marker=dict(size=14, color=group["color"], opacity=0.8),
764
  name=f"震度 {intensity_labels[intensity_level]}",
765
  showlegend=True,
766
  hoverinfo="skip",
767
  )
768
  )
769
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
  # 設置地圖佈局
771
  fig.update_layout(
772
  map=dict(
773
  style="open-street-map",
774
  center=dict(lat=map_center_lat, lon=map_center_lon),
775
- zoom=7,
776
  ),
777
- height=800, # 設置固定高度以適應 Gradio 容器
778
  margin=dict(l=0, r=0, t=0, b=0),
779
  showlegend=True,
780
  legend=dict(
@@ -830,25 +714,21 @@ def step1_load_mseed_and_select_stations(event_name):
830
 
831
  if len(selected_stations) == 0:
832
  logger.error("找不到有效的測站資料")
833
- return None, None, None, None, gr.update(interactive=False)
834
-
835
- # 創建輸入測站地圖
836
- station_map = create_input_station_map(
837
- selected_stations, epicenter_lat, epicenter_lon
838
- )
839
 
840
  logger.info("[步驟 1] 完成 - mseed 已載入,測站已選擇")
841
- return st, selected_stations, station_map, None
842
 
843
  except Exception as e:
844
  logger.error(f"[步驟 1] 發生錯誤: {e}")
845
  import traceback
846
  traceback.print_exc()
847
- return None, None, None, None
848
 
849
 
850
  # ============ 步驟 2:提取波形(使用快取的 stream + stations)============
851
- def step2_extract_and_plot_waveforms(cached_stream, cached_stations, event_name, duration):
 
852
  """
853
  步驟 2:根據時間範圍提取波形並繪圖
854
 
@@ -876,7 +756,8 @@ def step2_extract_and_plot_waveforms(cached_stream, cached_stations, event_name,
876
  return None, None, None, gr.update(interactive=False)
877
 
878
  # 繪製波形圖
879
- waveform_plot = plot_waveform(cached_stream, cached_stations, first_pick, duration)
 
880
 
881
  logger.info(f"[步驟 2] 完成 - 已提取 {len(waveforms)} 個測站的波形")
882
  return waveforms, station_info_list, waveform_plot
@@ -889,7 +770,8 @@ def step2_extract_and_plot_waveforms(cached_stream, cached_stations, event_name,
889
 
890
 
891
  # ============ 步驟 3:執行模型推論(使用快取的波形)============
892
- def step3_predict_intensity(cached_waveforms, cached_station_info, event_name):
 
893
  """
894
  步驟 3:執行震度預測
895
 
@@ -897,11 +779,13 @@ def step3_predict_intensity(cached_waveforms, cached_station_info, event_name):
897
 
898
  spec #2:測站選擇上限 (25 站)、波形取樣率 (100 Hz)、時間窗長度 (30 秒)
899
  spec #3:推論流���、PGA → 震度轉換
 
 
900
  """
901
  try:
902
  if cached_waveforms is None or cached_station_info is None:
903
  logger.warning("[步驟 3] 快取資料不存在,請先載入並提取波形")
904
- return None, None
905
 
906
  epicenter_lat = earthquake_metadata[event_name]["epicenter_lat"]
907
  epicenter_lon = earthquake_metadata[event_name]["epicenter_lon"]
@@ -987,24 +871,21 @@ def step3_predict_intensity(cached_waveforms, cached_station_info, event_name):
987
  pga_list = all_pga_list
988
  target_names = all_target_names
989
 
990
- # 繪製互動式地圖(固定高度 800
991
  intensity_map = create_intensity_map(
992
- pga_list, target_names, epicenter_lat, epicenter_lon
 
993
  )
994
 
995
- # 載入實際觀測震度圖
996
- observed_intensity_path = load_observed_intensity_image(event_name)
997
-
998
  logger.info("[步驟 3] 預測完成!")
999
- return observed_intensity_path, intensity_map
1000
 
1001
  except Exception as e:
1002
  logger.error(f"[步驟 3] 發生錯誤: {e}")
1003
  import traceback
1004
 
1005
  traceback.print_exc()
1006
- return None, None
1007
-
1008
 
1009
 
1010
  # ============ Gradio 介面 ============
@@ -1033,85 +914,81 @@ with gr.Blocks(title="TTSAM 震度預測系統", fill_height=True) as demo:
1033
  duration_slider = gr.Slider(
1034
  2, 15, value=15, step=1, label="P 波後時間 (秒)"
1035
  )
1036
- # ========== 中層:輸入測站地圖與波形圖 ==========
1037
- with gr.Row():
1038
- # 中左:輸入測站地圖
1039
- with gr.Column(scale=1):
1040
- gr.Markdown("## 輸入測站分布")
1041
- input_station_map = gr.Plot(label="輸入測站地圖")
1042
-
1043
- # 中右:輸入波形
1044
- with gr.Column(scale=1):
1045
- gr.Markdown("## 輸入波形")
1046
  waveform_plot = gr.Plot(
1047
  label="地震波形(選定的 25 個測站)",
1048
  )
1049
 
1050
-
1051
- # ========== 下層:實際觀測 vs 預測結果 ==========
1052
  with gr.Row():
1053
- # 左下:預測震度地圖
 
 
1054
  with gr.Column(scale=1):
1055
- gr.Markdown("## 預測震度分布")
1056
- predicted_intensity_map = gr.Plot(label="互動式震度地圖")
1057
 
1058
- # 右下:實際觀測震度圖
1059
  with gr.Column(scale=1):
1060
- gr.Markdown("## 實際觀測震度分布")
1061
  observed_intensity_image = gr.Image(
1062
  label="實際觀測震度",
1063
  type="filepath",
1064
- height=800,
1065
- value=load_observed_intensity_image(list(earthquake_metadata.keys())[0]),
1066
  )
1067
 
1068
  # ========== 隱藏的 State 變數(用於快取中間結果)==========
1069
- cached_stream = gr.State(None) # ObsPy Stream object
1070
- cached_stations = gr.State(None) # 選中的 25 個測站列表
1071
- cached_waveforms = gr.State(None) # 提取的波形資料
1072
- cached_station_info = gr.State(None) # 測站資訊列表
1073
 
1074
  # ========== 事件綁定(使用鏈式觸發 + gr.State 快取)==========
1075
 
1076
- # 【觸發點 1】事件切換:自動執行完整流程 步驟 1 → 步驟 2 → 步驟 3
1077
  event_dropdown.change(
1078
  fn=step1_load_mseed_and_select_stations,
1079
  inputs=[event_dropdown],
1080
- outputs=[cached_stream, cached_stations, input_station_map, waveform_plot]
 
 
 
 
1081
  ).then( # 鏈式觸發步驟 2
1082
  fn=step2_extract_and_plot_waveforms,
1083
  inputs=[cached_stream, cached_stations, event_dropdown, duration_slider],
1084
  outputs=[cached_waveforms, cached_station_info, waveform_plot]
1085
  ).then( # 鏈式觸發步驟 3
1086
  fn=step3_predict_intensity,
1087
- inputs=[cached_waveforms, cached_station_info, event_dropdown],
1088
- outputs=[observed_intensity_image, predicted_intensity_map]
1089
  )
1090
 
1091
- # 【觸發點 2】時間範圍調整:自動執行步驟 2 → 步驟 3(不重新讀檔)
1092
  duration_slider.change(
1093
  fn=step2_extract_and_plot_waveforms,
1094
  inputs=[cached_stream, cached_stations, event_dropdown, duration_slider],
1095
  outputs=[cached_waveforms, cached_station_info, waveform_plot]
1096
  ).then( # 鏈式觸發步驟 3
1097
  fn=step3_predict_intensity,
1098
- inputs=[cached_waveforms, cached_station_info, event_dropdown],
1099
- outputs=[observed_intensity_image, predicted_intensity_map]
1100
  )
1101
 
1102
- # 【冷啟動】應用載入時自動執行完整流程 步驟 1 → 步驟 2 → 步驟 3
1103
  demo.load(
1104
  fn=step1_load_mseed_and_select_stations,
1105
  inputs=[event_dropdown],
1106
- outputs=[cached_stream, cached_stations, input_station_map, waveform_plot]
 
 
 
 
1107
  ).then(
1108
  fn=step2_extract_and_plot_waveforms,
1109
  inputs=[cached_stream, cached_stations, event_dropdown, duration_slider],
1110
  outputs=[cached_waveforms, cached_station_info, waveform_plot]
1111
  ).then(
1112
  fn=step3_predict_intensity,
1113
- inputs=[cached_waveforms, cached_station_info, event_dropdown],
1114
- outputs=[observed_intensity_image, predicted_intensity_map]
1115
  )
1116
 
1117
  demo.launch()
 
91
  except Exception as e:
92
  logger.error(f"{target_file} 載入失敗: {e}")
93
 
 
94
  # ============ 震央資訊管理 ============
95
 
96
  earthquake_metadata = {}
97
+ event_json_path = "waveform/event.json"
98
 
99
  try:
100
  import json
 
134
  except Exception as e:
135
  logger.error(f"讀取事件元資料時發生錯誤: {e}")
136
 
 
137
  # 載入模型
138
  model_path = hf_hub_download(
139
  repo_id="SeisBlue/TTSAM", filename="ttsam_trained_model_11.pt"
 
261
 
262
 
263
  def extract_waveforms_from_stream(event_name,
264
+ st, selected_stations, duration, vs30_input
265
+ ):
266
  """
267
  從 Stream 中提取選定測站的波形資料
268
 
 
401
  return waveforms, station_info_list, valid_stations, missing_components_count
402
 
403
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  def plot_waveform(st, selected_stations, first_pick, duration):
405
  """
406
  繪製選定測站的波形圖(距離-時間圖,可顯示全部 25 個測站)
 
414
  # 計算結束時間
415
  end_time = first_pick + duration
416
 
417
+ fig, ax = plt.subplots(figsize=(14, 4))
418
 
419
  # 設定振幅縮放比例(避免波形重疊)
420
  amplitude_scale = 0.03 # 可調整此值來控制波形大小
 
458
  except Exception as e:
459
  logger.warning(f"無法繪製測站 {station_code}: {e}")
460
 
461
+ ax.axvline(first_pick, color="blue", linestyle="--", linewidth=2, alpha=0.7,
462
+ label="First Motion")
463
 
464
  # 標記選取時間範圍
465
  ax.axvline(
 
468
  linestyle="--",
469
  linewidth=2,
470
  alpha=0.7,
471
+ label="Input Waveform",
472
  )
473
  ax.axvline(end_time, color="red", linestyle="--", linewidth=2, alpha=0.7)
474
  ax.axvspan(0, end_time, alpha=0.15, color="blue")
 
520
 
521
 
522
  def create_intensity_map(
523
+ pga_list, target_names, epicenter_lat=None, epicenter_lon=None,
524
+ selected_stations=None
525
  ):
526
+ """使用 Plotly 創建互動式震度分布地圖(合併輸入測站與預測震度)"""
527
 
528
  # 按震度等級分組資料
529
  intensity_groups = {
 
556
  intensity_groups[intensity]["lon"].append(lon)
557
  intensity_groups[intensity]["text"].append(hover_text)
558
 
559
+ # 地圖中心固定為台灣中心
560
+ map_center_lat = 23.6
561
+ map_center_lon = 121.0
 
 
 
 
 
 
 
 
 
562
 
563
  # 創建 Plotly 地圖
564
  fig = go.Figure()
565
 
566
+ # 【底層】添加輸入測站(灰色無填充圓圈,不搶視覺焦點)
567
+ if selected_stations:
568
+ input_station_lats = []
569
+ input_station_lons = []
570
+ input_station_texts = []
571
+
572
+ for station_data in selected_stations:
573
+ input_station_lats.append(station_data["latitude"])
574
+ input_station_lons.append(station_data["longitude"])
575
+ input_station_texts.append(
576
+ f"{station_data['station']}<br>"
577
+ f"輸入測站<br>"
578
+ f"位置: ({station_data['latitude']:.3f}, {station_data['longitude']:.3f})"
579
+ )
580
+
581
+ fig.add_trace(
582
+ go.Scattermap(
583
+ lat=input_station_lats,
584
+ lon=input_station_lons,
585
+ mode="markers",
586
+ marker=dict(
587
+ size=8,
588
+ color="rgba(128, 128, 128, 0.3)", # 半透明灰色
589
+ ),
590
+ text=input_station_texts,
591
+ hovertemplate="%{text}<extra></extra>",
592
+ name="輸入測站",
593
+ showlegend=True,
594
+ )
595
+ )
596
+
597
+ # 【頂層】添加各震度等級的測站(預測結果)
598
  intensity_labels = ["0", "1", "2", "3", "4", "5-", "5+", "6-", "6+", "7"]
599
  for intensity_level in range(10):
600
  group = intensity_groups[intensity_level]
 
604
  lat=group["lat"],
605
  lon=group["lon"],
606
  mode="markers",
607
+ marker=dict(size=14, color=group["color"], opacity=0.9),
608
  text=group["text"],
609
  hovertemplate="%{text}<extra></extra>",
610
  name=f"震度 {intensity_labels[intensity_level]}",
 
618
  lat=[None],
619
  lon=[None],
620
  mode="markers",
621
+ marker=dict(size=14, color=group["color"], opacity=0.9),
622
  name=f"震度 {intensity_labels[intensity_level]}",
623
  showlegend=True,
624
  hoverinfo="skip",
625
  )
626
  )
627
 
628
+ # 【中層】添加震央(紅色標記)
629
+ if epicenter_lat is not None and epicenter_lon is not None:
630
+ fig.add_trace(
631
+ go.Scattermap(
632
+ lat=[epicenter_lat],
633
+ lon=[epicenter_lon],
634
+ mode="markers",
635
+ marker=dict(size=25, color="red"),
636
+ text=[f"震央<br>({epicenter_lat:.3f}, {epicenter_lon:.3f})"],
637
+ hovertemplate="%{text}<extra></extra>",
638
+ name="震央",
639
+ showlegend=True,
640
+ )
641
+ )
642
+
643
+ fig.add_trace(
644
+ go.Scattermap(
645
+ lat=[epicenter_lat],
646
+ lon=[epicenter_lon],
647
+ mode="markers",
648
+ marker=dict(size=10, color="white"),
649
+ showlegend=False,
650
+ hoverinfo="skip",
651
+ )
652
+ )
653
+
654
  # 設置地圖佈局
655
  fig.update_layout(
656
  map=dict(
657
  style="open-street-map",
658
  center=dict(lat=map_center_lat, lon=map_center_lon),
659
+ zoom=6.5,
660
  ),
661
+ height=550, # 設置固定高度以適應 Gradio 容器
662
  margin=dict(l=0, r=0, t=0, b=0),
663
  showlegend=True,
664
  legend=dict(
 
714
 
715
  if len(selected_stations) == 0:
716
  logger.error("找不到有效的測站資料")
717
+ return None, None
 
 
 
 
 
718
 
719
  logger.info("[步驟 1] 完成 - mseed 已載入,測站已選擇")
720
+ return st, selected_stations
721
 
722
  except Exception as e:
723
  logger.error(f"[步驟 1] 發生錯誤: {e}")
724
  import traceback
725
  traceback.print_exc()
726
+ return None, None
727
 
728
 
729
  # ============ 步驟 2:提取波形(使用快取的 stream + stations)============
730
+ def step2_extract_and_plot_waveforms(cached_stream, cached_stations, event_name,
731
+ duration):
732
  """
733
  步驟 2:根據時間範圍提取波形並繪圖
734
 
 
756
  return None, None, None, gr.update(interactive=False)
757
 
758
  # 繪製波形圖
759
+ waveform_plot = plot_waveform(cached_stream, cached_stations, first_pick,
760
+ duration)
761
 
762
  logger.info(f"[步驟 2] 完成 - 已提取 {len(waveforms)} 個測站的波形")
763
  return waveforms, station_info_list, waveform_plot
 
770
 
771
 
772
  # ============ 步驟 3:執行模型推論(使用快取的波形)============
773
+ def step3_predict_intensity(cached_waveforms, cached_station_info, cached_stations,
774
+ event_name):
775
  """
776
  步驟 3:執行震度預測
777
 
 
779
 
780
  spec #2:測站選擇上限 (25 站)、波形取樣率 (100 Hz)、時間窗長度 (30 秒)
781
  spec #3:推論流���、PGA → 震度轉換
782
+
783
+ 注意:此函數只返回預測地圖,觀測圖片由 step1 單獨處理
784
  """
785
  try:
786
  if cached_waveforms is None or cached_station_info is None:
787
  logger.warning("[步驟 3] 快取資料不存在,請先載入並提取波形")
788
+ return None
789
 
790
  epicenter_lat = earthquake_metadata[event_name]["epicenter_lat"]
791
  epicenter_lon = earthquake_metadata[event_name]["epicenter_lon"]
 
871
  pga_list = all_pga_list
872
  target_names = all_target_names
873
 
874
+ # 繪製互動式地圖(固定高度 800)- 合併輸入測站與預測震度
875
  intensity_map = create_intensity_map(
876
+ pga_list, target_names, epicenter_lat, epicenter_lon,
877
+ selected_stations=cached_stations
878
  )
879
 
 
 
 
880
  logger.info("[步驟 3] 預測完成!")
881
+ return intensity_map
882
 
883
  except Exception as e:
884
  logger.error(f"[步驟 3] 發生錯誤: {e}")
885
  import traceback
886
 
887
  traceback.print_exc()
888
+ return None
 
889
 
890
 
891
  # ============ Gradio 介面 ============
 
914
  duration_slider = gr.Slider(
915
  2, 15, value=15, step=1, label="P 波後時間 (秒)"
916
  )
 
 
 
 
 
 
 
 
 
 
917
  waveform_plot = gr.Plot(
918
  label="地震波形(選定的 25 個測站)",
919
  )
920
 
921
+ # ========== 下層:合併地圖 vs 實際觀測 ==========
 
922
  with gr.Row():
923
+ # 實際觀測震度圖
924
+
925
+ # 合併後的地圖(輸入測站 + 預測震度)
926
  with gr.Column(scale=1):
927
+ predicted_intensity_map = gr.Plot(label="合併地圖")
 
928
 
 
929
  with gr.Column(scale=1):
 
930
  observed_intensity_image = gr.Image(
931
  label="實際觀測震度",
932
  type="filepath",
933
+ value=load_observed_intensity_image(
934
+ list(earthquake_metadata.keys())[0]),
935
  )
936
 
937
  # ========== 隱藏的 State 變數(用於快取中間結果)==========
938
+ cached_stream = gr.State(None) # ObsPy Stream object
939
+ cached_stations = gr.State(None) # 選中的 25 個測站列表
940
+ cached_waveforms = gr.State(None) # 提取的波形資料
941
+ cached_station_info = gr.State(None) # 測站資訊列表
942
 
943
  # ========== 事件綁定(使用鏈式觸發 + gr.State 快取)==========
944
 
945
+ # 【觸發點 1】事件切換:自動執行完整流程 步驟 1 → 步驟 2 → 步驟 3 + 載入觀測圖片
946
  event_dropdown.change(
947
  fn=step1_load_mseed_and_select_stations,
948
  inputs=[event_dropdown],
949
+ outputs=[cached_stream, cached_stations]
950
+ ).then( # 載入觀測圖片(只在事件切換時執行)
951
+ fn=load_observed_intensity_image,
952
+ inputs=[event_dropdown],
953
+ outputs=[observed_intensity_image]
954
  ).then( # 鏈式觸發步驟 2
955
  fn=step2_extract_and_plot_waveforms,
956
  inputs=[cached_stream, cached_stations, event_dropdown, duration_slider],
957
  outputs=[cached_waveforms, cached_station_info, waveform_plot]
958
  ).then( # 鏈式觸發步驟 3
959
  fn=step3_predict_intensity,
960
+ inputs=[cached_waveforms, cached_station_info, cached_stations, event_dropdown],
961
+ outputs=[predicted_intensity_map]
962
  )
963
 
964
+ # 【觸發點 2】時間範圍調整:自動執行步驟 2 → 步驟 3(不重新讀檔,不更新觀測圖片)
965
  duration_slider.change(
966
  fn=step2_extract_and_plot_waveforms,
967
  inputs=[cached_stream, cached_stations, event_dropdown, duration_slider],
968
  outputs=[cached_waveforms, cached_station_info, waveform_plot]
969
  ).then( # 鏈式觸發步驟 3
970
  fn=step3_predict_intensity,
971
+ inputs=[cached_waveforms, cached_station_info, cached_stations, event_dropdown],
972
+ outputs=[predicted_intensity_map]
973
  )
974
 
975
+ # 【冷啟動】應用載入時自動執行完整流程 步驟 1 → 載入觀測圖片 → 步驟 2 → 步驟 3
976
  demo.load(
977
  fn=step1_load_mseed_and_select_stations,
978
  inputs=[event_dropdown],
979
+ outputs=[cached_stream, cached_stations]
980
+ ).then(
981
+ fn=load_observed_intensity_image,
982
+ inputs=[event_dropdown],
983
+ outputs=[observed_intensity_image]
984
  ).then(
985
  fn=step2_extract_and_plot_waveforms,
986
  inputs=[cached_stream, cached_stations, event_dropdown, duration_slider],
987
  outputs=[cached_waveforms, cached_station_info, waveform_plot]
988
  ).then(
989
  fn=step3_predict_intensity,
990
+ inputs=[cached_waveforms, cached_station_info, cached_stations, event_dropdown],
991
+ outputs=[predicted_intensity_map]
992
  )
993
 
994
  demo.launch()