jimmy60504 commited on
Commit
4ee8fb3
·
1 Parent(s): 10ceedb

add interactive intensity map feature using Folium and update related functions

Browse files
Files changed (2) hide show
  1. app.py +187 -65
  2. requirements.txt +1 -0
app.py CHANGED
@@ -2,7 +2,6 @@ import gradio as gr
2
  import numpy as np
3
  import matplotlib.pyplot as plt
4
  from obspy import read
5
- from datasets import load_dataset
6
  import xarray as xr
7
  import torch
8
  import torch.nn as nn
@@ -382,12 +381,6 @@ def get_vs30(lat, lon, user_vs30=600):
382
  return float(vs30)
383
 
384
 
385
- def get_station_position(station):
386
- latitude, longitude, elevation = site_info.loc[
387
- (site_info["Station"] == station), ["Latitude", "Longitude", "Elevation"]
388
- ].values[0]
389
- return latitude, longitude, elevation
390
-
391
 
392
  def calculate_intensity(pga, label=False):
393
  intensity_label = ["0", "1", "2", "3", "4", "5-", "5+", "6-", "6+", "7"]
@@ -501,17 +494,18 @@ def extract_waveforms_from_stream(st, selected_stations, start_time, end_time, v
501
  n_data = signal_processing(n_data)
502
  e_data = signal_processing(e_data)
503
 
504
- # 調整長度到 3000
505
- for data in [z_data, n_data, e_data]:
506
- if len(data) > target_length:
507
- data = data[:target_length]
508
- elif len(data) < target_length:
509
- data = np.pad(data, (0, target_length - len(data)))
510
-
511
- # 組合三分量 (3000, 3)
512
- waveform_3c = np.stack([z_data[:target_length],
513
- n_data[:target_length],
514
- e_data[:target_length]], axis=1)
 
515
  waveforms.append(waveform_3c)
516
 
517
  # 準備測站資訊
@@ -533,14 +527,19 @@ def extract_waveforms_from_stream(st, selected_stations, start_time, end_time, v
533
 
534
 
535
  def plot_waveform(st, selected_stations, start_time, end_time):
536
- """繪製選定測站的波形圖(堆疊顯示前 10 個測站)"""
537
- fig, axes = plt.subplots(min(10, len(selected_stations)), 1, figsize=(12, 8), sharex=True)
 
 
 
538
 
539
- if len(selected_stations) == 1:
540
- axes = [axes]
 
541
 
542
- for i, station_data in enumerate(selected_stations[:10]):
543
  station_code = station_data["station"]
 
544
 
545
  try:
546
  st_station = st.select(station=station_code)
@@ -549,56 +548,178 @@ def plot_waveform(st, selected_stations, start_time, end_time):
549
  times = tr.times()
550
  data = tr.data
551
 
552
- axes[i].plot(times, data, 'black', linewidth=0.5)
 
 
 
 
 
553
 
554
- # 標記選取範圍
555
- axes[i].axvline(start_time, color='red', linestyle='--', linewidth=1, alpha=0.7)
556
- axes[i].axvline(end_time, color='red', linestyle='--', linewidth=1, alpha=0.7)
557
- axes[i].axvspan(start_time, end_time, alpha=0.2, color='blue')
558
 
559
- axes[i].set_ylabel(f'{station_code}\n({station_data["distance"]:.2f}°)', fontsize=8)
560
- axes[i].grid(True, alpha=0.3)
561
- axes[i].tick_params(labelsize=8)
562
  except Exception as e:
563
  logger.warning(f"無法繪製測站 {station_code}: {e}")
564
 
565
- axes[-1].set_xlabel('Time (s)')
566
- fig.suptitle(f'波形記錄(前 10 個最近測站,共選擇 {len(selected_stations)} 個)', fontsize=12)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
  plt.tight_layout()
568
 
569
  return fig
570
 
571
 
572
- def plot_intensity_map(pga_list, target_names):
573
- fig, ax = plt.subplots(figsize=(6, 8))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
 
575
- # 繪製台灣地圖底圖
576
- taiwan_lon = [120, 122]
577
- taiwan_lat = [22, 25]
 
 
 
 
 
578
 
579
- # 根據 target_names 取得座標
580
- lats, lons, intensities = [], [], []
581
  for i, target_name in enumerate(target_names):
582
  target = next((t for t in target_dict if t["station"] == target_name), None)
583
  if target:
584
- lats.append(target["latitude"])
585
- lons.append(target["longitude"])
586
- intensities.append(calculate_intensity(pga_list[i]))
587
-
588
- # 繪製散點圖
589
- scatter = ax.scatter(lons, lats, c=intensities, cmap='YlOrRd', s=100,
590
- vmin=0, vmax=7, edgecolors='black', linewidth=0.5)
591
-
592
- ax.set_xlabel('Longitude')
593
- ax.set_ylabel('Latitude')
594
- ax.set_title('Predicted Intensity Distribution')
595
- ax.set_xlim(taiwan_lon)
596
- ax.set_ylim(taiwan_lat)
597
-
598
- cbar = plt.colorbar(scatter, ax=ax)
599
- cbar.set_label('Intensity')
600
-
601
- return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
 
603
 
604
  def load_and_display_waveform(event_name, start_time, end_time, epicenter_lon, epicenter_lat):
@@ -693,8 +814,9 @@ def predict_intensity(event_name, start_time, end_time, epicenter_lon, epicenter
693
  weight, sigma, mu = model(tensor_data)
694
  pga_list = torch.sum(weight * mu, dim=2).cpu().detach().numpy().flatten().tolist()
695
 
696
- # 8. 繪製結果
697
- intensity_plot = plot_intensity_map(pga_list, target_names)
 
698
 
699
  # 9. 統計資訊
700
  max_intensity = max([calculate_intensity(pga, label=True) for pga in pga_list])
@@ -705,7 +827,7 @@ def predict_intensity(event_name, start_time, end_time, epicenter_lon, epicenter
705
  stats += f"預測最大震度: {max_intensity}"
706
 
707
  logger.info("預測完成!")
708
- return intensity_plot, stats
709
 
710
  except Exception as e:
711
  logger.error(f"預測過程發生錯誤: {e}")
@@ -735,8 +857,8 @@ with gr.Blocks(title="TTSAM 震度預測系統") as demo:
735
 
736
  gr.Markdown("### 震央位置")
737
  with gr.Row():
738
- epicenter_lon_input = gr.Number(value=121.5, label="震央經度")
739
- epicenter_lat_input = gr.Number(value=24.0, label="震央緯度")
740
 
741
  gr.Markdown("### 場址參數")
742
  with gr.Row():
@@ -764,7 +886,7 @@ with gr.Blocks(title="TTSAM 震度預測系統") as demo:
764
  # 右側:震度分布圖
765
  with gr.Column(scale=1):
766
  gr.Markdown("## 預測震度分布")
767
- intensity_plot = gr.Plot(label="震度分布圖")
768
  stats_output = gr.Textbox(label="預測統計", lines=4)
769
 
770
  # 下方:波形圖
@@ -785,7 +907,7 @@ with gr.Blocks(title="TTSAM 震度預測系統") as demo:
785
  predict_btn.click(
786
  fn=predict_intensity,
787
  inputs=[event_dropdown, start_slider, end_slider, epicenter_lon_input, epicenter_lat_input, vs30_input],
788
- outputs=[intensity_plot, stats_output]
789
  )
790
 
791
  demo.launch()
 
2
  import numpy as np
3
  import matplotlib.pyplot as plt
4
  from obspy import read
 
5
  import xarray as xr
6
  import torch
7
  import torch.nn as nn
 
381
  return float(vs30)
382
 
383
 
 
 
 
 
 
 
384
 
385
  def calculate_intensity(pga, label=False):
386
  intensity_label = ["0", "1", "2", "3", "4", "5-", "5+", "6-", "6+", "7"]
 
494
  n_data = signal_processing(n_data)
495
  e_data = signal_processing(e_data)
496
 
497
+ # 先創建全零陣列 (3000, 3)
498
+ waveform_3c = np.zeros((target_length, 3))
499
+
500
+ # 填入實際資料(自動處理長度不足或過長的情況)
501
+ z_len = min(len(z_data), target_length)
502
+ n_len = min(len(n_data), target_length)
503
+ e_len = min(len(e_data), target_length)
504
+
505
+ waveform_3c[:z_len, 0] = z_data[:z_len]
506
+ waveform_3c[:n_len, 1] = n_data[:n_len]
507
+ waveform_3c[:e_len, 2] = e_data[:e_len]
508
+
509
  waveforms.append(waveform_3c)
510
 
511
  # 準備測站資訊
 
527
 
528
 
529
  def plot_waveform(st, selected_stations, start_time, end_time):
530
+ """繪製選定測站的波形圖(距離-時間圖,可顯示全部 25 個測站)"""
531
+ fig, ax = plt.subplots(figsize=(14, 10))
532
+
533
+ # 設定振幅縮放比例(避免波形重疊)
534
+ amplitude_scale = 0.03 # 可調整此值來控制波形大小
535
 
536
+ plotted_count = 0
537
+ distances = []
538
+ station_names = []
539
 
540
+ for i, station_data in enumerate(selected_stations):
541
  station_code = station_data["station"]
542
+ distance = station_data["distance"]
543
 
544
  try:
545
  st_station = st.select(station=station_code)
 
548
  times = tr.times()
549
  data = tr.data
550
 
551
+ # 正規化波形振幅
552
+ data_normalized = data / (np.max(np.abs(data)) + 1e-10)
553
+
554
+ # 繪製波形,Y軸位置為距離
555
+ ax.plot(times, distance + data_normalized * amplitude_scale,
556
+ 'black', linewidth=0.3, alpha=0.8)
557
 
558
+ distances.append(distance)
559
+ station_names.append(station_code)
560
+ plotted_count += 1
 
561
 
 
 
 
562
  except Exception as e:
563
  logger.warning(f"無法繪製測站 {station_code}: {e}")
564
 
565
+ # 標記選取時間範圍
566
+ ax.axvline(start_time, color='red', linestyle='--', linewidth=2,
567
+ alpha=0.7, label='選取範圍')
568
+ ax.axvline(end_time, color='red', linestyle='--', linewidth=2, alpha=0.7)
569
+ ax.axvspan(start_time, end_time, alpha=0.15, color='blue')
570
+
571
+ # 設定軸標籤和標題
572
+ ax.set_xlabel('Time (s)', fontsize=12)
573
+ ax.set_ylabel('Distance from Epicenter (°)', fontsize=12)
574
+ ax.set_title(f'Record Section - {plotted_count} Stations Sorted by Distance',
575
+ fontsize=14, fontweight='bold')
576
+
577
+ # 在右側標註測站名稱
578
+ if distances:
579
+ ax2 = ax.twinx()
580
+ ax2.set_ylim(ax.get_ylim())
581
+ ax2.set_ylabel('Station Code', fontsize=12)
582
+
583
+ # 每隔幾個測站標註一次(避免過於擁擠)
584
+ step = max(1, len(distances) // 10)
585
+ tick_positions = distances[::step]
586
+ tick_labels = station_names[::step]
587
+ ax2.set_yticks(tick_positions)
588
+ ax2.set_yticklabels(tick_labels, fontsize=8)
589
+
590
+ ax.grid(True, alpha=0.3, axis='x')
591
+ ax.legend(loc='upper right')
592
  plt.tight_layout()
593
 
594
  return fig
595
 
596
 
597
+ def get_intensity_color(intensity):
598
+ """根據震度等級返回對應顏色(參考 intensityMap.html)"""
599
+ color_map = {
600
+ 0: "#ffffff", # 白色
601
+ 1: "#33FFDD", # 青色
602
+ 2: "#34ff32", # 綠色
603
+ 3: "#fefd32", # 黃色
604
+ 4: "#fe8532", # 橘色
605
+ 5: "#fd5233", # 紅橘色 (5-)
606
+ 6: "#c43f3b", # 深紅色 (5+)
607
+ 7: "#9d4646", # 暗紅色 (6-)
608
+ 8: "#9a4c86", # 紫紅色 (6+)
609
+ 9: "#b51fea", # 紫色 (7)
610
+ }
611
+ return color_map.get(intensity, "#ffffff")
612
+
613
+
614
+ def create_intensity_map(pga_list, target_names, epicenter_lat=None, epicenter_lon=None):
615
+ """使用 Folium 創建互動式震度分布地圖"""
616
+ import folium
617
+ from folium import plugins
618
+
619
+ # 創建地圖,中心點設在台灣中心
620
+ m = folium.Map(
621
+ location=[23.5, 121],
622
+ zoom_start=7,
623
+ tiles='OpenStreetMap'
624
+ )
625
 
626
+ # 如果有震央位置,標記震央
627
+ if epicenter_lat and epicenter_lon:
628
+ folium.Marker(
629
+ [epicenter_lat, epicenter_lon],
630
+ popup=f'震央<br>({epicenter_lat:.3f}, {epicenter_lon:.3f})',
631
+ icon=folium.Icon(color='red', icon='star', prefix='fa'),
632
+ tooltip='震央位置'
633
+ ).add_to(m)
634
 
635
+ # 添加震度測站標記
 
636
  for i, target_name in enumerate(target_names):
637
  target = next((t for t in target_dict if t["station"] == target_name), None)
638
  if target:
639
+ lat = target["latitude"]
640
+ lon = target["longitude"]
641
+ intensity = calculate_intensity(pga_list[i])
642
+ intensity_label = calculate_intensity(pga_list[i], label=True)
643
+ color = get_intensity_color(intensity)
644
+ pga = pga_list[i]
645
+
646
+ # 創建 HTML popup 內容
647
+ popup_html = f"""
648
+ <div style="font-family: Arial; min-width: 150px;">
649
+ <h4 style="margin: 0 0 10px 0;">{target_name}</h4>
650
+ <table style="width:100%;">
651
+ <tr><td><b>震度:</b></td><td style="color: {color}; font-weight: bold; font-size: 16px;">{intensity_label}</td></tr>
652
+ <tr><td><b>PGA:</b></td><td>{pga:.4f} m/s²</td></tr>
653
+ <tr><td><b>位置:</b></td><td>({lat:.3f}, {lon:.3f})</td></tr>
654
+ </table>
655
+ </div>
656
+ """
657
+
658
+ # 創建圓形標記
659
+ folium.CircleMarker(
660
+ location=[lat, lon],
661
+ radius=12,
662
+ popup=folium.Popup(popup_html, max_width=250),
663
+ tooltip=f'{target_name}: 震度 {intensity_label}',
664
+ color='black',
665
+ fillColor=color,
666
+ fillOpacity=0.8,
667
+ weight=2
668
+ ).add_to(m)
669
+
670
+ # 在圓圈中心添加震度文字
671
+ folium.Marker(
672
+ [lat, lon],
673
+ icon=folium.DivIcon(html=f'''
674
+ <div style="
675
+ font-size: 10px;
676
+ font-weight: bold;
677
+ color: black;
678
+ text-align: center;
679
+ text-shadow: 1px 1px 2px white, -1px -1px 2px white;
680
+ ">{intensity_label}</div>
681
+ ''')
682
+ ).add_to(m)
683
+
684
+ # 添加圖例
685
+ legend_html = '''
686
+ <div style="
687
+ position: fixed;
688
+ top: 10px; left: 10px;
689
+ width: 180px;
690
+ background-color: white;
691
+ border: 2px solid grey;
692
+ z-index: 9999;
693
+ font-size: 14px;
694
+ padding: 10px;
695
+ border-radius: 5px;
696
+ box-shadow: 2px 2px 6px rgba(0,0,0,0.3);
697
+ ">
698
+ <h4 style="margin: 0 0 10px 0;">震度等級 Intensity</h4>
699
+ <table style="width: 100%;">
700
+ '''
701
+
702
+ intensity_levels = ["0", "1", "2", "3", "4", "5-", "5+", "6-", "6+", "7"]
703
+ for idx, level in enumerate(intensity_levels):
704
+ color = get_intensity_color(idx)
705
+ legend_html += f'''
706
+ <tr>
707
+ <td style="width: 30px; height: 20px; background-color: {color}; border: 1px solid black;"></td>
708
+ <td style="padding-left: 5px;">{level}</td>
709
+ </tr>
710
+ '''
711
+
712
+ legend_html += '''
713
+ </table>
714
+ </div>
715
+ '''
716
+
717
+ m.get_root().html.add_child(folium.Element(legend_html))
718
+
719
+ # 添加全屏按鈕
720
+ plugins.Fullscreen().add_to(m)
721
+
722
+ return m
723
 
724
 
725
  def load_and_display_waveform(event_name, start_time, end_time, epicenter_lon, epicenter_lat):
 
814
  weight, sigma, mu = model(tensor_data)
815
  pga_list = torch.sum(weight * mu, dim=2).cpu().detach().numpy().flatten().tolist()
816
 
817
+ # 8. 繪製互動式地圖
818
+ intensity_map = create_intensity_map(pga_list, target_names, epicenter_lat, epicenter_lon)
819
+ map_html = intensity_map._repr_html_()
820
 
821
  # 9. 統計資訊
822
  max_intensity = max([calculate_intensity(pga, label=True) for pga in pga_list])
 
827
  stats += f"預測最大震度: {max_intensity}"
828
 
829
  logger.info("預測完成!")
830
+ return map_html, stats
831
 
832
  except Exception as e:
833
  logger.error(f"預測過程發生錯誤: {e}")
 
857
 
858
  gr.Markdown("### 震央位置")
859
  with gr.Row():
860
+ epicenter_lon_input = gr.Number(value=121.57, label="震央經度")
861
+ epicenter_lat_input = gr.Number(value=23.88, label="震央緯度")
862
 
863
  gr.Markdown("### 場址參數")
864
  with gr.Row():
 
886
  # 右側:震度分布圖
887
  with gr.Column(scale=1):
888
  gr.Markdown("## 預測震度分布")
889
+ intensity_map = gr.HTML(label="互動式震度地圖")
890
  stats_output = gr.Textbox(label="預測統計", lines=4)
891
 
892
  # 下方:波形圖
 
907
  predict_btn.click(
908
  fn=predict_intensity,
909
  inputs=[event_dropdown, start_slider, end_slider, epicenter_lon_input, epicenter_lat_input, vs30_input],
910
+ outputs=[intensity_map, stats_output]
911
  )
912
 
913
  demo.launch()
requirements.txt CHANGED
@@ -11,3 +11,4 @@ scipy
11
  pandas
12
  loguru
13
  huggingface_hub
 
 
11
  pandas
12
  loguru
13
  huggingface_hub
14
+ folium