show_eew_rep / app.py
chen
Update app.py
8219d66 verified
import folium
import gradio as gr
from folium.plugins import MarkerCluster
import re
from datetime import datetime
import os
def parse_rep_file(file_content):
"""
Parse EEW REP file content and extract earthquake and station data
"""
lines = file_content.split('\n')
# Initialize data structures
earthquake_data = {}
stations = []
for i, line in enumerate(lines):
line = line.strip()
if not line:
continue
# Parse header line
if line.startswith('Reporting time'):
parts = line.split()
earthquake_data['reporting_time'] = f"{parts[1]} {parts[2]}"
# Extract quality metrics
metrics_match = re.search(r'averr=([\d.]+).*?Q=([\d-]+).*?Gap=(\d+).*?Avg_wei=([\d.]+).*?n=(\d+).*?n_c=(\d+).*?n_m=(\d+).*?Padj=([\d.]+).*?no_eq=(\d+)', line)
if metrics_match:
earthquake_data.update({
'averr': float(metrics_match.group(1)),
'Q': int(metrics_match.group(2)),
'gap': int(metrics_match.group(3)),
'avg_wei': float(metrics_match.group(4)),
'n_total': int(metrics_match.group(5)),
'n_location': int(metrics_match.group(6)),
'n_magnitude': int(metrics_match.group(7)),
'padj': float(metrics_match.group(8)),
'event_num': int(metrics_match.group(9))
})
# Parse event summary line (contains origin time and location)
elif re.match(r'^\d{4}\s+\d{1,2}\s+\d{1,2}', line):
parts = line.split()
if len(parts) >= 12:
try:
earthquake_data.update({
'year': int(parts[0]),
'month': int(parts[1]),
'day': int(parts[2]),
'hour': int(parts[3]),
'minute': int(parts[4]),
'second': float(parts[5]),
'latitude': float(parts[6]),
'longitude': float(parts[7]),
'depth': float(parts[8]),
'magnitude_ml': float(parts[9]),
'magnitude_pd_s': float(parts[10]),
'magnitude_pv': float(parts[11]),
'magnitude_pd': float(parts[12]) if len(parts) > 12 else 0.0,
'magnitude_tc': float(parts[13]) if len(parts) > 13 else 0.0,
'process_time': float(parts[14]) if len(parts) > 14 else 0.0
})
except (ValueError, IndexError):
pass
# Parse station data (starts after the header lines)
elif re.match(r'^[A-Z]\d{3}', line) or re.match(r'^\s+[A-Z]', line):
parts = line.split()
if len(parts) >= 20:
try:
station = {
'code': parts[0].strip(),
'component': parts[1],
'network': parts[2],
'location': parts[3],
'latitude': float(parts[4]),
'longitude': float(parts[5]),
'pga': float(parts[6]), # cm/s²
'pgv': float(parts[7]), # cm/s
'pgd': float(parts[8]), # cm
'tc': float(parts[9]), # S-P time residual
'mtc': float(parts[10]), # Tau-c magnitude
'mpv': float(parts[11]), # Velocity magnitude
'mpd': float(parts[12]), # Displacement magnitude
'perror': float(parts[13]), # Travel time residual
'distance': float(parts[14]), # km
'weight': float(parts[15]), # Station weight
'p_arrival': f"{parts[16]} {parts[17]}",
'pick_weight': int(parts[18]),
'update_sec': int(parts[19]),
'ps_ratio': float(parts[20]) if len(parts) > 20 else 0.0,
'used_sec': int(parts[21]) if len(parts) > 21 else 0
}
stations.append(station)
except (ValueError, IndexError):
pass
return earthquake_data, stations
def create_earthquake_map(rep_content, filename=""):
"""
Create an interactive map showing earthquake epicenter and stations
"""
earthquake_data, stations = parse_rep_file(rep_content)
if not earthquake_data or 'latitude' not in earthquake_data:
return None, "Error: Could not parse earthquake data from REP file"
# Create map centered on earthquake epicenter
m = folium.Map(
location=[earthquake_data['latitude'], earthquake_data['longitude']],
zoom_start=8,
tiles='OpenStreetMap'
)
# Add earthquake epicenter marker
magnitude = earthquake_data.get('magnitude_ml', 0)
mag_color = 'red' if magnitude >= 7.0 else 'orange' if magnitude >= 6.0 else 'yellow'
popup_content = f"""
<b>Earthquake Event #{earthquake_data.get('event_num', 'N/A')}</b><br>
<b>Magnitude:</b> {magnitude:.1f} ML<br>
<b>Location:</b> {earthquake_data['latitude']:.4f}°, {earthquake_data['longitude']:.4f}°<br>
<b>Depth:</b> {earthquake_data.get('depth', 'N/A')} km<br>
<b>Origin Time:</b> {earthquake_data.get('year', '')}-{earthquake_data.get('month', ''):02d}-{earthquake_data.get('day', ''):02d} {earthquake_data.get('hour', ''):02d}:{earthquake_data.get('minute', ''):02d}:{earthquake_data.get('second', ''):.1f}<br>
<b>Processing Time:</b> {earthquake_data.get('process_time', 0):.1f} seconds<br>
<b>Stations:</b> {len(stations)} total<br>
<b>Quality:</b> Gap={earthquake_data.get('gap', 'N/A')}°, RMS={earthquake_data.get('averr', 'N/A'):.1f}s
"""
folium.CircleMarker(
location=[earthquake_data['latitude'], earthquake_data['longitude']],
radius=max(5, magnitude * 2),
color=mag_color,
fill=True,
fill_color=mag_color,
fill_opacity=0.7,
popup=folium.Popup(popup_content, max_width=300)
).add_to(m)
# Add station markers
station_cluster = MarkerCluster(name="Stations").add_to(m)
for station in stations:
# Color code stations by PGA
pga = station.get('pga', 0)
if pga > 50:
color = 'darkred'
elif pga > 20:
color = 'red'
elif pga > 10:
color = 'orange'
elif pga > 5:
color = 'yellow'
else:
color = 'green'
station_popup = f"""
<b>Station: {station['code']}</b><br>
<b>Network:</b> {station['network']}<br>
<b>Component:</b> {station['component']}<br>
<b>Location:</b> {station['latitude']:.4f}°, {station['longitude']:.4f}°<br>
<b>Distance:</b> {station.get('distance', 'N/A')} km<br>
<b>PGA:</b> {station.get('pga', 0):.2f} cm/s²<br>
<b>PGV:</b> {station.get('pgv', 0):.3f} cm/s<br>
<b>PGD:</b> {station.get('pgd', 0):.3f} cm<br>
<b>P-arrival:</b> {station.get('p_arrival', 'N/A')}<br>
<b>Pick Weight:</b> {station.get('pick_weight', 'N/A')}<br>
<b>Travel Time Residual:</b> {station.get('perror', 0):.2f}s
"""
folium.CircleMarker(
location=[station['latitude'], station['longitude']],
radius=3,
color=color,
fill=True,
fill_color=color,
fill_opacity=0.8,
popup=folium.Popup(station_popup, max_width=250)
).add_to(station_cluster)
# Add layer control
folium.LayerControl().add_to(m)
# Add title
title_html = f'''
<h3 align="center" style="font-size:20px"><b>Earthquake EEW Report</b></h3>
<p align="center">Event #{earthquake_data.get('event_num', 'N/A')} - Magnitude {magnitude:.1f} ML</p>
<p align="center">File: {filename}</p>
'''
m.get_root().html.add_child(folium.Element(title_html))
return m, f"Successfully processed {len(stations)} stations"
def process_rep_file(file):
"""
Process uploaded REP file and return map HTML
"""
if file is None:
return None, "Please upload a REP file"
try:
content = file.decode('utf-8')
filename = getattr(file, 'name', 'uploaded_file.rep')
m, message = create_earthquake_map(content, filename)
if m is None:
return None, message
# Save map to HTML string
import io
import base64
map_html = m.get_root().render()
return map_html, message
except Exception as e:
return None, f"Error processing file: {str(e)}"
# Gradio Interface
def create_gradio_interface():
with gr.Blocks(title="EEW REP File Map Viewer") as interface:
gr.Markdown("""
# 🌍 Earthquake EEW Report Map Viewer
Upload an EEW REP file to visualize the earthquake epicenter and seismic stations on an interactive map.
## Features:
- **Epicenter**: Red/orange/yellow circle based on magnitude
- **Stations**: Colored by PGA intensity (green=low, red=high)
- **Interactive**: Click markers for detailed information
- **Clustering**: Stations grouped for better visibility at low zoom
## REP File Format:
Files should be in the standard EEW report format with `.rep` extension.
""")
with gr.Row():
with gr.Column(scale=1):
file_input = gr.File(
label="Upload REP File",
file_types=[".rep"],
type="binary"
)
process_btn = gr.Button("Generate Map", variant="primary")
with gr.Column(scale=2):
map_output = gr.HTML(label="Interactive Map")
status_output = gr.Textbox(label="Status", interactive=False)
# Sample files section
gr.Markdown("### Sample REP Files")
gr.Markdown("Try these sample files to see the visualization:")
# Event handlers
process_btn.click(
fn=process_rep_file,
inputs=[file_input],
outputs=[map_output, status_output]
)
# Auto-process on file upload
file_input.change(
fn=process_rep_file,
inputs=[file_input],
outputs=[map_output, status_output]
)
return interface
# Main execution
if __name__ == "__main__":
interface = create_gradio_interface()
interface.launch(
server_name="0.0.0.0",
server_port=7860,
show_error=True,
theme=gr.themes.Soft()
)