cwadayi commited on
Commit
4fcf8ef
Β·
verified Β·
1 Parent(s): 1fe0746

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +155 -149
src/streamlit_app.py CHANGED
@@ -1,164 +1,170 @@
1
  import streamlit as st
2
  import pandas as pd
3
- import requests
4
- import plotly.express as px
5
- import time
6
 
7
- # --- Page Configuration ---
8
  st.set_page_config(
9
- page_title="Taiwan CWA Earthquake Alarms",
10
- page_icon="🚨",
11
- layout="wide",
12
  )
13
 
14
- # --- Data Fetching and Processing ---
15
- @st.cache_data(ttl=60)
16
- def fetch_cwa_alarms():
 
 
 
17
  """
18
- Fetches earthquake alarm data from the CWA API.
 
19
  """
20
- API_URL = "https://app-2.cwa.gov.tw/api/v1/earthquake/alarm/list"
21
  try:
22
- response = requests.get(API_URL, timeout=10)
23
- response.raise_for_status()
24
- raw_data = response.json().get('data', [])
25
-
26
- if not raw_data:
27
- return pd.DataFrame()
28
-
29
- return pd.DataFrame(raw_data)
30
-
31
- except requests.exceptions.RequestException as e:
32
- st.error(f"Failed to fetch data from CWA API: {e}")
33
- return pd.DataFrame()
34
-
35
- # --- Main Application UI ---
36
- st.title("🚨 Taiwan CWA Earthquake Alarm Viewer")
37
- st.markdown(f"This app displays the latest earthquake early-warning alarms. Last updated: **{pd.Timestamp.now(tz='Asia/Taipei').strftime('%Y-%m-%d %H:%M:%S')}** (https://cwadayi-streamlit-alarm-taiwan.hf.space)")
38
-
39
- # Manual refresh button is still useful
40
- if st.button("Refresh Data Now"):
41
- st.cache_data.clear()
42
- st.rerun()
43
-
44
- df = fetch_cwa_alarms()
45
-
46
- # --- Sidebar for Filters and Controls ---
47
- with st.sidebar:
48
- st.header("βš™οΈ Controls & Filters")
49
-
50
- # --- Auto-Refresh Controls ---
51
- st.subheader("Auto-Refresh")
52
- auto_refresh = st.checkbox("Enable Auto-Refresh", value=False)
53
- refresh_interval = st.number_input(
54
- "Refresh Interval (seconds)",
55
- min_value=10,
56
- max_value=300,
57
- value=60,
58
- step=5,
59
- disabled=not auto_refresh,
60
- help="Set how often the app should automatically refresh data. Must be between 10 and 300 seconds."
61
- )
62
-
63
- st.divider()
64
-
65
- # --- Data Filters ---
66
- st.subheader("Data Filters")
67
- if not df.empty:
68
- # Ensure 'msgType' and 'msgNo' are strings for consistent sorting and filtering
69
- df['msgType'] = df['msgType'].astype(str)
70
- df['msgNo'] = df['msgNo'].astype(str)
71
-
72
- msg_types = sorted(df['msgType'].unique())
73
- msg_numbers = sorted(df['msgNo'].unique())
74
-
75
- selected_types = st.multiselect('Message Type(s)', options=msg_types, default=msg_types)
76
- selected_numbers = st.multiselect('Message Number(s)', options=msg_numbers, default=msg_numbers)
77
- else:
78
- st.info("Waiting for data to load filters...")
79
- # Define empty lists to prevent errors when df is empty
80
- selected_types = []
81
- selected_numbers = []
82
-
83
-
84
- # --- Main Content Area ---
85
- if df.empty:
86
- st.warning("No alarm data is currently available or the API could not be reached.")
87
- else:
88
- # Apply filters from the sidebar
89
- filtered_df = df[
90
- df['msgType'].isin(selected_types) &
91
- df['msgNo'].isin(selected_numbers)
92
- ].copy() # Use .copy() to avoid SettingWithCopyWarning
93
-
94
- if filtered_df.empty:
95
- st.warning("No data matches your current filter settings. Please adjust the filters in the sidebar.")
96
- else:
97
- # Prepare data for display
98
- filtered_df['originTime'] = pd.to_datetime(filtered_df['originTime'])
99
- filtered_df['magnitudeValue'] = pd.to_numeric(filtered_df['magnitudeValue'])
100
- filtered_df['depth'] = pd.to_numeric(filtered_df['depth'])
101
-
102
- # --- Interactive Map Display ---
103
- st.header("Earthquake Epicenter Map")
104
- map_df = filtered_df.sort_values('magnitudeValue', ascending=False).drop_duplicates(subset='originTime').copy()
105
-
106
- # Check if 'locationDesc' column exists and is not empty
107
- if 'locationDesc' in filtered_df.columns and not filtered_df['locationDesc'].apply(lambda x: isinstance(x, list) and len(x) == 0).all():
108
- table_df_for_hover = filtered_df.explode('locationDesc')
109
- hover_areas = table_df_for_hover.groupby('originTime')['locationDesc'].apply(lambda x: ', '.join(set(x))).reset_index(name="Affected Areas")
110
- map_df = pd.merge(map_df, hover_areas, on='originTime', how='left')
111
- map_df['Affected Areas'] = map_df['Affected Areas'].fillna('N/A')
112
- hover_data_config = {"Affected Areas": True, "magnitudeValue": ':.1f', "depth": True, "epicenterLat": False, "epicenterLon": False}
113
- else:
114
- map_df['Affected Areas'] = 'N/A'
115
- hover_data_config = {"magnitudeValue": ':.1f', "depth": True, "epicenterLat": False, "epicenterLon": False}
116
 
 
 
 
117
 
118
- fig = px.scatter_map(
119
- map_df, lat="epicenterLat", lon="epicenterLon", size="magnitudeValue", color="depth",
120
- color_continuous_scale=px.colors.sequential.OrRd, size_max=25, zoom=6.5,
121
- center={"lat": 23.9, "lon": 121.5},
122
- hover_name="originTime",
123
- hover_data=hover_data_config,
124
- labels={"depth": "Depth (km)", "magnitudeValue": "Magnitude"}
 
 
 
125
  )
126
-
127
- fig.update_layout(margin={"r": 0, "t": 40, "l": 0, "b": 0}, legend_title_text='Magnitude')
128
- st.plotly_chart(fig, use_container_width=True)
129
-
130
- # --- FIX: Add vertical space to prevent overlap ---
131
- st.markdown("<br>", unsafe_allow_html=True)
132
-
133
- # --- Detailed Table Display ---
134
- st.header("Detailed Alarm Reports")
135
-
136
- if 'locationDesc' in filtered_df.columns:
137
- filtered_df['Alarm Areas'] = filtered_df['locationDesc'].apply(lambda areas: ', '.join(areas) if isinstance(areas, list) else 'N/A')
138
  else:
139
- filtered_df['Alarm Areas'] = 'N/A'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- display_df = filtered_df[[
142
- "originTime", "identifier", "msgType", "msgNo", "magnitudeValue",
143
- "depth", "epicenterLat", "epicenterLon", "Alarm Areas"
144
- ]].rename(columns={
145
- "originTime": "Origin Time", "identifier": "Report ID", "msgType": "Message Type",
146
- "msgNo": "Message No.", "magnitudeValue": "Magnitude", "depth": "Depth (km)",
147
- "epicenterLat": "Latitude", "epicenterLon": "Longitude"
148
- })
149
-
150
- st.dataframe(
151
- display_df.sort_values(by=["Origin Time", "Message No."], ascending=[False, True]),
152
- use_container_width=True, hide_index=True,
153
- column_config={"Origin Time": st.column_config.DatetimeColumn(format="YYYY-MM-DD HH:mm:ss")}
154
- )
 
 
 
 
 
 
155
 
156
- # --- Auto-Refresh Logic ---
157
- if auto_refresh:
158
- with st.sidebar:
159
- placeholder = st.empty()
160
- for i in range(refresh_interval, 0, -1):
161
- placeholder.info(f"⏳ Refreshing in {i} seconds...")
162
- time.sleep(1)
163
- placeholder.empty()
164
- st.rerun()
 
1
  import streamlit as st
2
  import pandas as pd
3
+ from obspy import UTCDateTime
4
+ from obspy.clients.fdsn import Client
5
+ import matplotlib.pyplot as plt
6
 
7
+ # --- App Configuration ---
8
  st.set_page_config(
9
+ page_title="Taiwan Earthquake Explorer πŸ‡ΉπŸ‡Ό",
10
+ page_icon="πŸŒ‹",
11
+ layout="wide"
12
  )
13
 
14
+ # --- FDSN Client & Caching ---
15
+ # Initialize the IRIS FDSN client
16
+ client = Client("IRIS")
17
+
18
+ @st.cache_data
19
+ def search_earthquakes(start_time, end_time, min_mag, min_lat, max_lat, min_lon, max_lon):
20
  """
21
+ Searches for earthquake events using the ObsPy client.
22
+ Results are cached to prevent re-running the same query.
23
  """
 
24
  try:
25
+ catalog = client.get_events(
26
+ starttime=start_time,
27
+ endtime=end_time,
28
+ minlatitude=min_lat,
29
+ maxlatitude=max_lat,
30
+ minlongitude=min_lon,
31
+ maxlongitude=max_lon,
32
+ minmagnitude=min_mag,
33
+ orderby="time-asc" # Order by time, ascending
34
+ )
35
+ return catalog
36
+ except Exception as e:
37
+ st.error(f"Could not retrieve event data: {e}")
38
+ return None
39
+
40
+ @st.cache_data
41
+ def get_waveforms_for_event(_event): # Caching requires hashable arguments
42
+ """
43
+ Retrieves and processes seismic waveforms for a given event.
44
+ _event is a tuple of event properties to make it hashable for caching.
45
+ """
46
+ event_time_str, event_lat, event_lon, event_depth = _event
47
+ event_time = UTCDateTime(event_time_str)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ # Define a time window around the event origin time
50
+ t_start = event_time - 30 # 30 seconds before
51
+ t_end = event_time + 5 * 60 # 5 minutes after
52
 
53
+ try:
54
+ # Fetch waveforms from the TW network for broadband channels
55
+ stream = client.get_waveforms(
56
+ network="TW",
57
+ station="*",
58
+ location="*",
59
+ channel="BH*", # Broadband High Gain channels (BHZ, BHN, BHE)
60
+ starttime=t_start,
61
+ endtime=t_end,
62
+ attach_response=True # Attach instrument response
63
  )
64
+ if len(stream) > 0:
65
+ # Pre-processing: Detrend, taper, and remove instrument response
66
+ stream.detrend("linear")
67
+ stream.taper(max_percentage=0.05, type="cosine")
68
+ stream.remove_response(output="VEL") # Convert to velocity (m/s)
69
+ return stream
 
 
 
 
 
 
70
  else:
71
+ return None
72
+ except Exception:
73
+ # Return None if no data is available or an error occurs
74
+ return None
75
+
76
+
77
+ # --- Streamlit User Interface ---
78
+ st.title("πŸŒ‹ Taiwan Earthquake Explorer")
79
+ st.markdown("Search, map, and visualize seismic data from the TW network via IRIS FDSN.")
80
+
81
+ # --- Sidebar for Search Controls ---
82
+ st.sidebar.header("πŸ” Search Parameters")
83
+
84
+ # Date and Time Inputs
85
+ default_start = UTCDateTime("2024-04-02T23:00:00")
86
+ default_end = UTCDateTime("2024-04-03T01:00:00")
87
+ start_date = st.sidebar.date_input("Start Date", value=default_start.date)
88
+ start_time_str = st.sidebar.text_input("Start Time (UTC)", value=default_start.strftime("%H:%M:%S"))
89
+ end_date = st.sidebar.date_input("End Date", value=default_end.date)
90
+ end_time_str = st.sidebar.text_input("End Time (UTC)", value=default_end.strftime("%H:%M:%S"))
91
+
92
+ # Combine date and time
93
+ start_utc = UTCDateTime(f"{start_date}T{start_time_str}")
94
+ end_utc = UTCDateTime(f"{end_date}T{end_time_str}")
95
+
96
+ # Magnitude and Location Inputs
97
+ st.sidebar.markdown("---")
98
+ min_mag = st.sidebar.slider("Minimum Magnitude", 2.0, 8.0, 5.5)
99
+
100
+ st.sidebar.markdown("---")
101
+ st.sidebar.subheader("Geographical Region (Taiwan)")
102
+ min_lat = st.sidebar.number_input("Min Latitude", value=21.5, format="%.2f")
103
+ max_lat = st.sidebar.number_input("Max Latitude", value=25.5, format="%.2f")
104
+ min_lon = st.sidebar.number_input("Min Longitude", value=120.0, format="%.2f")
105
+ max_lon = st.sidebar.number_input("Max Longitude", value=122.5, format="%.2f")
106
+
107
+ # --- Main App Logic ---
108
+ if st.sidebar.button("Search for Earthquakes", type="primary"):
109
+ catalog = search_earthquakes(start_utc, end_utc, min_mag, min_lat, max_lat, min_lon, max_lon)
110
+
111
+ if catalog and len(catalog) > 0:
112
+ st.success(f"βœ… Found {len(catalog)} earthquake(s).")
113
+
114
+ # --- Process and Display Data ---
115
+ event_data = []
116
+ for event in catalog:
117
+ origin = event.preferred_origin() or event.origins[0]
118
+ magnitude = event.preferred_magnitude() or event.magnitudes[0]
119
+ event_data.append({
120
+ "Time (UTC)": origin.time.strftime('%Y-%m-%d %H:%M:%S'),
121
+ "Latitude": origin.latitude,
122
+ "Longitude": origin.longitude,
123
+ "Depth (km)": origin.depth / 1000.0,
124
+ "Magnitude": magnitude.mag,
125
+ "Mag Type": magnitude.magnitude_type
126
+ })
127
+
128
+ event_df = pd.DataFrame(event_data)
129
+
130
+ st.subheader("πŸ—ΊοΈ Earthquake Map")
131
+ st.map(event_df, latitude='Latitude', longitude='Longitude', size='Magnitude', zoom=6)
132
+
133
+ st.subheader("πŸ“Š Earthquake Catalog Table")
134
+ st.dataframe(event_df)
135
+
136
+ st.markdown("---")
137
+ st.subheader(" seismograph Seismic Waveform Viewer")
138
+
139
+ # --- Waveform Selection and Display ---
140
+ event_options = {f"{row['Time (UTC)']} - Mag: {row['Magnitude']:.1f}": index for index, row in event_df.iterrows()}
141
+ selected_event_str = st.selectbox("Select an event to view waveforms:", options=event_options.keys())
142
+
143
+ if selected_event_str:
144
+ selected_index = event_options[selected_event_str]
145
+ selected_event = catalog[selected_index]
146
+ origin = selected_event.preferred_origin() or selected_event.origins[0]
147
 
148
+ # Create a hashable tuple for caching
149
+ event_tuple = (
150
+ str(origin.time),
151
+ origin.latitude,
152
+ origin.longitude,
153
+ origin.depth
154
+ )
155
+
156
+ with st.spinner("Fetching waveforms from TW network... This may take a moment."):
157
+ waveforms = get_waveforms_for_event(event_tuple)
158
+
159
+ if waveforms and len(waveforms) > 0:
160
+ st.success(f"πŸ“Š Found and processed {len(waveforms)} waveform traces.")
161
+ # Plotting the waveforms
162
+ fig, ax = plt.subplots(figsize=(12, len(waveforms) * 1.5))
163
+ waveforms.plot(fig=fig, handle=True)
164
+ ax.set_title(f"Seismic Waveforms for Event: {selected_event_str}")
165
+ st.pyplot(fig)
166
+ else:
167
+ st.warning("Could not retrieve any waveform data for the selected event from the TW network.")
168
 
169
+ else:
170
+ st.warning("No earthquakes found matching your criteria. Try expanding the search window or lowering the magnitude.")