Spaces:
Sleeping
Sleeping
| 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() | |
| ) |