from flask import Flask, jsonify, send_from_directory, request, send_file, abort from flask_cors import CORS import os import re import h5py import numpy as np from datetime import datetime, timedelta import base64 import struct import traceback import json import io import mimetypes from pyproj import Transformer import shutil import threading import uuid import time import zipfile import csv as csv_mod app = Flask(__name__, static_folder='static', static_url_path='') CORS(app) DATA_DIR = os.environ.get('DATA_DIR', '/data') LOCATIONS_FILE = '/home/floppyrj45/seismic-viewer/locations.json' DARF_FILE = os.path.join(os.path.dirname(__file__), 'static', 'darf_nodes.json') TEMP_DIR = '/tmp/extracts' if not os.path.exists(TEMP_DIR): os.makedirs(TEMP_DIR) # Global task store TASKS = {} # ========== UTILITY FUNCTIONS ========== def cleanup_old_tasks(): """Remove tasks older than 1 hour.""" now = time.time() to_delete = [] for tid, task in TASKS.items(): if now - task['created_at'] > 3600: to_delete.append(tid) # Remove file if exists if 'file_path' in task and os.path.exists(task['file_path']): try: os.remove(task['file_path']) except: pass for tid in to_delete: del TASKS[tid] def load_darf_nodes(): """Load DARF node metadata from JSON file.""" try: with open(DARF_FILE, 'r') as f: return json.load(f) except Exception: return {} def extract_board_id(filename): """Extract board_id from filename pattern: auto_XXX_HHMMSS_b{BOARD_ID}_rsnXXX_...""" m = re.search(r'_b(\d+)_', filename) return m.group(1) if m else None def load_locations(): """Load locations from JSON file.""" if os.path.exists(LOCATIONS_FILE): try: with open(LOCATIONS_FILE, 'r') as f: return json.load(f) except: return {} return {} def save_locations(locations): """Save locations to JSON file.""" with open(LOCATIONS_FILE, 'w') as f: json.dump(locations, f, indent=2) def get_h5_metadata(filepath): """Extract comprehensive metadata from H5 file.""" metadata = {} try: with h5py.File(filepath, 'r') as f: # File-level attributes for k, v in f.attrs.items(): try: metadata[k] = v.item() if hasattr(v, 'item') else str(v) except: metadata[k] = str(v) # Metadata group attributes if 'metadata' in f: for k, v in f['metadata'].attrs.items(): try: metadata[k] = v.item() if hasattr(v, 'item') else str(v) except: metadata[k] = str(v) # Calibration group attributes if 'calibration' in f: calibration = {} for k, v in f['calibration'].attrs.items(): try: calibration[k] = v.item() if hasattr(v, 'item') else str(v) except: calibration[k] = str(v) metadata['calibration'] = calibration # Channel information channels = [] for group_name in ['calibrated_data', 'raw_data']: if group_name in f: for ds_name in f[group_name].keys(): ds = f[group_name][ds_name] ch_info = { 'name': f"{group_name}/{ds_name}", 'shape': list(ds.shape), 'dtype': str(ds.dtype), 'samples': int(ds.shape[0]) if len(ds.shape) > 0 else 0 } for k, v in ds.attrs.items(): try: ch_info[k] = v.item() if hasattr(v, 'item') else str(v) except: ch_info[k] = str(v) channels.append(ch_info) metadata['channels'] = channels # Compute timestamps if 'creation_date' in metadata: try: start_time = datetime.fromisoformat(metadata['creation_date'].replace('Z', '+00:00')) metadata['start_timestamp'] = start_time.isoformat() if 'duration_sec' in metadata: duration_sec = float(metadata['duration_sec']) end_time = start_time + timedelta(seconds=duration_sec) metadata['end_timestamp'] = end_time.isoformat() # Human-readable duration hours = int(duration_sec // 3600) minutes = int((duration_sec % 3600) // 60) seconds = int(duration_sec % 60) metadata['duration_human'] = f"{hours}h {minutes}m {seconds}s" except Exception as e: metadata['timestamp_error'] = str(e) return metadata except Exception as e: return {'error': str(e)} # ========== ROUTES ========== @app.route('/') def index(): return send_from_directory('static', 'index.html') @app.route('/api/files') def list_files(): try: files = [] for fn in os.listdir(DATA_DIR): if fn.endswith(('.segy', '.sgy', '.h5')): fp = os.path.join(DATA_DIR, fn) stat = os.stat(fp) ftype = 'h5' if fn.endswith('.h5') else 'segy' files.append({ 'name': fn, 'size': stat.st_size, 'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(), 'type': ftype }) files.sort(key=lambda x: x['name']) return jsonify({'files': files, 'count': len(files)}) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/file//info') def file_info(filename): try: fp = os.path.join(DATA_DIR, filename) if not os.path.exists(fp): return jsonify({'error': 'Not found'}), 404 stat = os.stat(fp) info = { 'name': filename, 'size': stat.st_size, 'size_mb': round(stat.st_size / 1048576, 2), 'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(), 'type': 'h5' if filename.endswith('.h5') else 'segy', } if filename.endswith('.h5'): with h5py.File(fp, 'r') as f: datasets = [] def visitor(name, obj): if isinstance(obj, h5py.Dataset): datasets.append({ 'path': name, 'shape': list(obj.shape), 'dtype': str(obj.dtype), 'samples': int(obj.shape[0]) if len(obj.shape) > 0 else 0 }) f.visititems(visitor) info['datasets'] = datasets info['num_datasets'] = len(datasets) cal = [d for d in datasets if 'calibrated' in d['path']] raw = [d for d in datasets if 'raw' in d['path']] info['calibrated_channels'] = len(cal) info['raw_channels'] = len(raw) if cal: info['samples_per_channel'] = cal[0]['samples'] # Add sample rate and duration for sidebar display if 'metadata' in f: sample_rate = f['metadata'].attrs.get('sample_rate_hz', None) if sample_rate and cal: total_samples = cal[0]['samples'] duration_sec = float(total_samples) / float(sample_rate) hours = int(duration_sec // 3600) minutes = int((duration_sec % 3600) // 60) info['duration_human'] = f"{hours}h{minutes:02d}m" info['sample_rate_hz'] = int(sample_rate) info['num_channels'] = int(len(cal)) elif filename.endswith(('.segy', '.sgy')): try: from obspy import read as obspy_read st = obspy_read(fp, headonly=True) info['num_traces'] = len(st) if len(st) > 0: info['samples_per_trace'] = st[0].stats.npts info['sample_rate'] = st[0].stats.sampling_rate info['delta'] = st[0].stats.delta except Exception as e: info['segy_error'] = str(e) return jsonify(info) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/file//metadata') def file_metadata(filename): """Return detailed metadata for a file.""" try: fp = os.path.join(DATA_DIR, filename) if not os.path.exists(fp): return jsonify({'error': 'Not found'}), 404 stat = os.stat(fp) result = { 'filename': filename, 'file_size_bytes': stat.st_size, 'file_size_mb': round(stat.st_size / 1048576, 2), 'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(), } if filename.endswith('.h5'): metadata = get_h5_metadata(fp) result.update(metadata) elif filename.endswith(('.segy', '.sgy')): try: from obspy import read as obspy_read st = obspy_read(fp, headonly=True) result['num_traces'] = len(st) if len(st) > 0: result['sample_rate'] = st[0].stats.sampling_rate result['samples_per_trace'] = st[0].stats.npts result['start_timestamp'] = st[0].stats.starttime.isoformat() result['end_timestamp'] = st[0].stats.endtime.isoformat() duration_sec = (st[0].stats.endtime - st[0].stats.starttime) hours = int(duration_sec // 3600) minutes = int((duration_sec % 3600) // 60) seconds = int(duration_sec % 60) result['duration_human'] = f"{hours}h {minutes}m {seconds}s" result['duration_sec'] = duration_sec except Exception as e: result['segy_error'] = str(e) return jsonify(result) except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/locations', methods=['GET']) def get_locations(): """Get all file locations.""" try: locations = load_locations() return jsonify(locations) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/locations', methods=['POST']) def update_location(): """Update location for a file.""" try: data = request.json filename = data.get('filename') lat = data.get('lat') lon = data.get('lon') if not all([filename, lat is not None, lon is not None]): return jsonify({'error': 'Missing filename, lat, or lon'}), 400 locations = load_locations() locations[filename] = { 'lat': float(lat), 'lon': float(lon), 'updated': datetime.now().isoformat() } save_locations(locations) return jsonify({'success': True, 'locations': locations}) except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/file//extract') def extract_segment(filename): """Extract a time segment from a file and return as CSV or H5.""" try: fp = os.path.join(DATA_DIR, filename) if not os.path.exists(fp): return jsonify({'error': 'Not found'}), 404 # Parameters start_sec = float(request.args.get('start_sec', 0)) duration_sec = float(request.args.get('duration_sec', 1200)) # Default 20 min channel = request.args.get('channel', 'calibrated_data/channel_1') format_type = request.args.get('format', 'csv') # csv or h5 if not filename.endswith('.h5'): return jsonify({'error': 'Extract only supported for H5 files'}), 400 with h5py.File(fp, 'r') as f: if channel not in f: return jsonify({'error': f'Channel {channel} not found'}), 404 # Get metadata sample_rate = f['metadata'].attrs.get('sample_rate_hz', 500) total_samples = f[channel].shape[0] # Calculate sample range start_sample = int(start_sec * sample_rate) end_sample = int((start_sec + duration_sec) * sample_rate) # Clamp to valid range start_sample = max(0, min(start_sample, total_samples - 1)) end_sample = max(start_sample + 1, min(end_sample, total_samples)) # Extract data data = f[channel][start_sample:end_sample] if format_type == 'csv': # Generate CSV output = io.StringIO() output.write(f"# Channel: {channel}\n") output.write(f"# Start: {start_sec}s, Duration: {duration_sec}s\n") output.write(f"# Sample rate: {sample_rate} Hz\n") output.write(f"# Samples: {len(data)}\n") output.write("sample,time_sec,value\n") for i, val in enumerate(data): time_s = (start_sample + i) / sample_rate output.write(f"{start_sample + i},{time_s:.6f},{val}\n") output.seek(0) return send_file( io.BytesIO(output.getvalue().encode('utf-8')), mimetype='text/csv', as_attachment=True, download_name=f'{filename.replace(".h5", "")}_extract_{int(start_sec)}_{int(duration_sec)}s.csv' ) elif format_type == 'h5': # Generate H5 output = io.BytesIO() with h5py.File(output, 'w') as out_f: # Copy metadata if 'metadata' in f: meta_group = out_f.create_group('metadata') for k, v in f['metadata'].attrs.items(): meta_group.attrs[k] = v meta_group.attrs['extract_start_sec'] = start_sec meta_group.attrs['extract_duration_sec'] = duration_sec meta_group.attrs['extract_start_sample'] = start_sample meta_group.attrs['extract_end_sample'] = end_sample # Create dataset ds = out_f.create_dataset('data', data=data, compression='gzip') ds.attrs['channel'] = channel ds.attrs['sample_rate_hz'] = sample_rate # Copy channel attributes if channel in f: for k, v in f[channel].attrs.items(): ds.attrs[k] = v output.seek(0) return send_file( output, mimetype='application/x-hdf5', as_attachment=True, download_name=f'{filename.replace(".h5", "")}_extract_{int(start_sec)}_{int(duration_sec)}s.h5' ) else: return jsonify({'error': 'Invalid format. Use csv or h5'}), 400 except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/file//section') def file_section(filename): """Return 2D section data as base64 float32 array.""" try: fp = os.path.join(DATA_DIR, filename) if not os.path.exists(fp): return jsonify({'error': 'Not found'}), 404 trace_start = int(request.args.get('trace_start', 0)) trace_count = int(request.args.get('trace_count', 200)) samples_per_trace = min(int(request.args.get('samples_per_trace', 1000)), 50000) channel = request.args.get('channel', 'calibrated_data/channel_1') max_samples = int(request.args.get('max_samples', 500)) if filename.endswith('.h5'): with h5py.File(fp, 'r') as f: if channel not in f: avail = [] f.visititems(lambda n, o: avail.append(n) if isinstance(o, h5py.Dataset) else None) return jsonify({'error': f'Channel not found', 'available': avail}), 404 data = f[channel] total_samples = data.shape[0] total_traces = total_samples // samples_per_trace trace_start = max(0, min(trace_start, max(0, total_traces - 1))) trace_end = min(trace_start + trace_count, total_traces) actual_count = trace_end - trace_start if actual_count <= 0: return jsonify({'error': 'No traces in range'}), 400 start_sample = trace_start * samples_per_trace end_sample = trace_end * samples_per_trace raw = data[start_sample:end_sample] section = raw.reshape(actual_count, samples_per_trace).astype(np.float32) if samples_per_trace > max_samples: factor = samples_per_trace // max_samples trimmed = section[:, :factor * max_samples] section = trimmed.reshape(actual_count, max_samples, factor).mean(axis=2).astype(np.float32) actual_spt = max_samples else: actual_spt = samples_per_trace vmin = float(np.min(section)) vmax = float(np.max(section)) encoded = base64.b64encode(section.tobytes()).decode('ascii') return jsonify({ 'num_traces': int(actual_count), 'samples_per_trace': actual_spt, 'trace_start': trace_start, 'total_traces': total_traces, 'total_samples': int(total_samples), 'vmin': vmin, 'vmax': vmax, 'data_b64': encoded, 'dtype': 'float32' }) elif filename.endswith(('.segy', '.sgy')): try: from obspy import read as obspy_read st = obspy_read(fp) total_traces = len(st) trace_start = max(0, min(trace_start, total_traces - 1)) trace_end = min(trace_start + trace_count, total_traces) actual_count = trace_end - trace_start if actual_count <= 0: return jsonify({'error': 'No traces'}), 400 spt = st[0].stats.npts actual_spt = spt section = np.zeros((actual_count, spt), dtype=np.float32) for i in range(actual_count): tr = st[trace_start + i] n = min(spt, tr.stats.npts) section[i, :n] = tr.data[:n].astype(np.float32) if spt > max_samples: factor = spt // max_samples trimmed = section[:, :factor * max_samples] section = trimmed.reshape(actual_count, max_samples, factor).mean(axis=2).astype(np.float32) actual_spt = max_samples vmin = float(np.min(section)) vmax = float(np.max(section)) encoded = base64.b64encode(section.tobytes()).decode('ascii') return jsonify({ 'num_traces': int(actual_count), 'samples_per_trace': actual_spt, 'trace_start': trace_start, 'total_traces': total_traces, 'total_samples': int(total_traces * spt), 'vmin': vmin, 'vmax': vmax, 'data_b64': encoded, 'dtype': 'float32' }) except Exception as e: return jsonify({'error': f'SEGY read error: {e}'}), 500 return jsonify({'error': 'Unsupported format'}), 400 except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/file//headers') def file_headers(filename): """Return trace headers for SEGY or dataset attributes for H5.""" try: fp = os.path.join(DATA_DIR, filename) if not os.path.exists(fp): return jsonify({'error': 'Not found'}), 404 if filename.endswith('.h5'): with h5py.File(fp, 'r') as f: attrs = {} for k, v in f.attrs.items(): try: attrs[k] = v.item() if hasattr(v, 'item') else str(v) except: attrs[k] = str(v) datasets = [] def visitor(name, obj): if isinstance(obj, h5py.Dataset): ds_info = { 'path': name, 'shape': list(obj.shape), 'dtype': str(obj.dtype), 'size': int(np.prod(obj.shape)), } for ak, av in obj.attrs.items(): try: ds_info[f'attr:{ak}'] = av.item() if hasattr(av, 'item') else str(av) except: ds_info[f'attr:{ak}'] = str(av) datasets.append(ds_info) f.visititems(visitor) groups = [] def gvisitor(name, obj): if isinstance(obj, h5py.Group): g_info = {'path': name} for ak, av in obj.attrs.items(): try: g_info[f'attr:{ak}'] = av.item() if hasattr(av, 'item') else str(av) except: g_info[f'attr:{ak}'] = str(av) if len(g_info) > 1: groups.append(g_info) f.visititems(gvisitor) return jsonify({ 'type': 'h5', 'file_attrs': attrs, 'datasets': datasets, 'groups': groups }) elif filename.endswith(('.segy', '.sgy')): try: from obspy import read as obspy_read from obspy.io.segy.segy import _read_segy segy = _read_segy(fp) bfh = {} if hasattr(segy, 'binary_file_header'): h = segy.binary_file_header for attr in ['job_identification_number', 'line_number', 'reel_number', 'number_of_data_traces_per_ensemble', 'number_of_auxiliary_traces_per_ensemble', 'sample_interval_in_microseconds', 'number_of_samples_per_data_trace', 'data_sample_format_code']: if hasattr(h, attr): bfh[attr] = getattr(h, attr) trace_headers = [] max_traces = min(50, len(segy.traces)) for i in range(max_traces): th = segy.traces[i].header trace_headers.append({ 'trace': i, 'inline': getattr(th, 'trace_sequence_number_within_line', None), 'crossline': getattr(th, 'trace_sequence_number_within_segy_file', None), 'field_record': getattr(th, 'original_field_record_number', None), 'trace_number': getattr(th, 'trace_number_within_the_original_field_record', None), 'cdp': getattr(th, 'ensemble_number', None), 'trace_in_ensemble': getattr(th, 'trace_number_within_the_ensemble', None), 'offset': getattr(th, 'distance_from_center_of_the_source_point_to_the_center_of_the_receiver_group', None), 'source_x': getattr(th, 'source_coordinate_x', None), 'source_y': getattr(th, 'source_coordinate_y', None), 'group_x': getattr(th, 'group_coordinate_x', None), 'group_y': getattr(th, 'group_coordinate_y', None), 'num_samples': getattr(th, 'number_of_samples_in_this_trace', None), 'sample_interval': getattr(th, 'sample_interval_in_ms_for_this_trace', None), }) return jsonify({ 'type': 'segy', 'binary_header': bfh, 'trace_headers': trace_headers, 'total_traces': len(segy.traces) }) except Exception as e: return jsonify({'error': f'SEGY header read error: {e}'}), 500 return jsonify({'error': 'Unsupported'}), 400 except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/file//waveform') def file_waveform(filename): try: fp = os.path.join(DATA_DIR, filename) if not os.path.exists(fp): return jsonify({'error': 'Not found'}), 404 max_pts = int(request.args.get('points', 5000)) channels_param = request.args.get('channels', '') channel = request.args.get('channel', 'calibrated_data/channel_1') start_sec = request.args.get('start', None) duration_sec = request.args.get('duration', None) if filename.endswith('.h5'): with h5py.File(fp, 'r') as f: # Determine sample rate from file metadata sample_rate = 500 if 'sample_rate' in f.attrs: sample_rate = float(f.attrs['sample_rate']) elif 'acquisition' in f and 'sample_rate' in f['acquisition'].attrs: sample_rate = float(f['acquisition'].attrs['sample_rate']) # Multi-channel mode if channels_param: ch_list = [c.strip() for c in channels_param.split(',') if c.strip()] else: ch_list = [channel] # Validate all channels exist for ch in ch_list: if ch not in f: return jsonify({'error': f'Channel not found: {ch}'}), 404 total_samples = f[ch_list[0]].shape[0] # Compute sample window from start/duration if start_sec is not None: s0 = max(0, int(float(start_sec) * sample_rate)) else: s0 = 0 if duration_sec is not None and duration_sec != 'all': try: s1 = min(total_samples, s0 + int(float(duration_sec) * sample_rate)) except ValueError: s1 = total_samples else: s1 = total_samples result = { 'name': filename, 'sample_rate': int(sample_rate), 'total_samples': total_samples, 'channels': {} } for ch in ch_list: dataset = f[ch] n = s1 - s0 # For large datasets, read only the samples we need (sparse read) if n > max_pts: step = n // max_pts indices = list(range(s0, s1, step))[:max_pts] # Read only needed indices from HDF5 (much faster than loading all) values = [float(dataset[i]) for i in indices] times = [i / sample_rate for i in indices] # Stats from the downsampled values vmin = min(values) vmax = max(values) vmean = sum(values) / len(values) vstd = (sum((v - vmean)**2 for v in values) / len(values)) ** 0.5 else: data = dataset[s0:s1] values = data.tolist() times = [(s0 + i) / sample_rate for i in range(n)] vmin = float(np.min(data)) vmax = float(np.max(data)) vmean = float(np.mean(data)) vstd = float(np.std(data)) result['channels'][ch] = { 'channel': ch, 'displayed_points': len(values), 'min': vmin, 'max': vmax, 'mean': vmean, 'std': vstd, 'x': times, 'y': values } # Backward compat: if single channel, also put x/y at top level if len(ch_list) == 1: ch_data = result['channels'][ch_list[0]] result['channel'] = ch_list[0] result['displayed_points'] = ch_data['displayed_points'] result['min'] = ch_data['min'] result['max'] = ch_data['max'] result['mean'] = ch_data['mean'] result['std'] = ch_data['std'] result['x'] = ch_data['x'] result['y'] = ch_data['y'] return jsonify(result) return jsonify({'error': 'Waveform only for H5'}), 400 except Exception as e: return jsonify({'error': str(e)}), 500 # ========== GEOSUP ENDPOINTS ========== # UTM Zone 31N to WGS84 transformer utm_to_wgs84 = Transformer.from_crs("EPSG:32631", "EPSG:4326", always_xy=True) GEOSUP_DIR = os.path.join(app.static_folder, '') # Static folder def parse_sps_line(line): """Parse SPS S01/R01 format line. Fixed-width format.""" if len(line) < 65: return None try: record_type = line[0].strip() # S or R line_num = line[1:11].strip() point = line[11:21].strip() index = line[21:23].strip() # Easting and Northing are in columns ~26-52 coord_part = line[46:66].strip() # Parse XXXXXX.XNNNNNNN.NN format match = re.match(r'(\d{5,6}\.\d{1,2})(\d{7}\.\d{1,2})', coord_part) if match: easting = float(match.group(1)) northing = float(match.group(2)) else: parts = coord_part.split('.') if len(parts) >= 2: easting_int = parts[0] rest = '.'.join(parts[1:]) if len(rest) > 9: easting = float(easting_int + '.' + rest[:2]) northing = float(rest[2:]) else: return None else: return None depth = line[66:72].strip() if len(line) > 52 else '0' depth = float(depth) if depth else 0 return { 'type': record_type, 'line': line_num, 'point': point, 'index': index, 'easting': easting, 'northing': northing, 'depth': depth } except Exception: return None def parse_deployment_csv(filepath): """Parse deployment CSV file (SETE-0965P1002.csv format).""" records = [] try: with open(filepath, 'r') as f: for line in f: parts = line.strip().split(',') if len(parts) >= 8: try: record = { 'line': parts[0], 'point': parts[1], 'index': parts[2], 'name': parts[3], 'easting': float(parts[5]), 'northing': float(parts[6]), 'depth': float(parts[7]), } if len(parts) >= 16: record['year'] = int(parts[10]) record['day'] = int(parts[11]) record['hour'] = int(parts[12]) record['minute'] = int(parts[13]) record['second'] = int(parts[14]) records.append(record) except (ValueError, IndexError): continue except Exception: pass return records @app.route('/api/geosup/positions') def geosup_positions(): """Return GeoJSON with source, receiver, and deployment positions.""" try: features = [] # Parse sources (S01) s01_path = os.path.join(GEOSUP_DIR, 'SeteSxPreplots.s01') if os.path.exists(s01_path): with open(s01_path, 'r') as f: for line in f: record = parse_sps_line(line) if record and record['type'] == 'S': lon, lat = utm_to_wgs84.transform(record['easting'], record['northing']) features.append({ 'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [lon, lat]}, 'properties': { 'category': 'source', 'line': record['line'], 'point': record['point'], 'easting': record['easting'], 'northing': record['northing'], 'depth': record['depth'] } }) # Parse receivers (R01) r01_path = os.path.join(GEOSUP_DIR, 'SeteRxPreplots.r01') if os.path.exists(r01_path): with open(r01_path, 'r') as f: for line in f: record = parse_sps_line(line) if record and record['type'] == 'R': lon, lat = utm_to_wgs84.transform(record['easting'], record['northing']) features.append({ 'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [lon, lat]}, 'properties': { 'category': 'receiver', 'line': record['line'], 'point': record['point'], 'easting': record['easting'], 'northing': record['northing'], 'depth': record['depth'] } }) # Parse deployments (CSV) csv_path = os.path.join(GEOSUP_DIR, 'SETE-0965P1002.csv') if os.path.exists(csv_path): deployments = parse_deployment_csv(csv_path) for d in deployments: lon, lat = utm_to_wgs84.transform(d['easting'], d['northing']) props = { 'category': 'deployment', 'line': d['line'], 'point': d['point'], 'name': d['name'], 'easting': d['easting'], 'northing': d['northing'], 'depth': d['depth'] } if 'year' in d: props['timestamp'] = f"{d['year']}-{d['day']:03d} {d['hour']:02d}:{d['minute']:02d}:{d['second']:02d}" features.append({ 'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [lon, lat]}, 'properties': props }) sources = len([f for f in features if f['properties']['category'] == 'source']) receivers = len([f for f in features if f['properties']['category'] == 'receiver']) deployments = len([f for f in features if f['properties']['category'] == 'deployment']) return jsonify({ 'type': 'FeatureCollection', 'features': features, 'counts': {'sources': sources, 'receivers': receivers, 'deployments': deployments, 'total': len(features)} }) except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/geosup/documents') def geosup_documents(): """List all geosup documents in static folder.""" try: documents = [] doc_extensions = ('.pdf', '.docx', '.xlsx', '.doc', '.xls') for fn in os.listdir(GEOSUP_DIR): if fn.lower().endswith(doc_extensions): fp = os.path.join(GEOSUP_DIR, fn) stat = os.stat(fp) ext = os.path.splitext(fn)[1].lower() doc_type = {'.pdf': 'PDF', '.docx': 'Word', '.doc': 'Word', '.xlsx': 'Excel', '.xls': 'Excel'}.get(ext, 'Document') icon = {'.pdf': '📕', '.docx': '📘', '.doc': '📘', '.xlsx': '📗', '.xls': '📗'}.get(ext, '📄') is_gundalf = 'GUNDALF' in fn.upper() is_params = 'Acquisition_Parameters' in fn or 'SpiceRack' in fn documents.append({ 'name': fn, 'size': stat.st_size, 'size_human': f"{stat.st_size / 1024:.1f} KB" if stat.st_size < 1048576 else f"{stat.st_size / 1048576:.1f} MB", 'type': doc_type, 'icon': icon, 'is_gundalf': is_gundalf, 'is_params': is_params, 'modified': datetime.fromtimestamp(stat.st_mtime).isoformat() }) documents.sort(key=lambda d: (not d['is_gundalf'], not d['is_params'], d['name'])) return jsonify({'documents': documents, 'count': len(documents)}) except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/geosup/documents/') def geosup_download(filename): """Download a geosup document.""" try: safe_name = os.path.basename(filename) filepath = os.path.join(GEOSUP_DIR, safe_name) if not os.path.exists(filepath): return jsonify({'error': 'File not found'}), 404 mime_type, _ = mimetypes.guess_type(filepath) return send_file(filepath, mimetype=mime_type or 'application/octet-stream', as_attachment=True, download_name=safe_name) except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/geosup/acquisition-params') def geosup_acquisition_params(): """Extract key parameters from the acquisition spreadsheet.""" try: xlsx_path = os.path.join(GEOSUP_DIR, 'SBGS_Sete_SpiceRack_Acquisition_Parameters_Spreadsheet.xlsx') if not os.path.exists(xlsx_path): return jsonify({'error': 'Acquisition parameters file not found'}), 404 from openpyxl import load_workbook wb = load_workbook(xlsx_path, data_only=True) params = {'sheets': [], 'summary': {}} for sheet_name in wb.sheetnames: ws = wb[sheet_name] sheet_data = {'name': sheet_name, 'rows': []} row_count = 0 for row in ws.iter_rows(max_row=30, values_only=True): if any(cell is not None for cell in row): sheet_data['rows'].append([str(cell) if cell is not None else '' for cell in row[:10]]) row_count += 1 if row_count >= 20: break params['sheets'].append(sheet_data) wb.close() return jsonify(params) except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/pipeline-status') def pipeline_status(): """Return pipeline statistics and disk usage.""" try: raw_dir = '/mnt/usb/Recordings' output_dir = '/mnt/usb/H5-Output' # Count files raw_count = 0 if os.path.exists(raw_dir): raw_count = len([n for n in os.listdir(raw_dir) if n.lower().endswith(('.raw', '.manta'))]) segy_count = 0 h5_count = 0 if os.path.exists(output_dir): files = os.listdir(output_dir) segy_count = len([n for n in files if n.lower().endswith(('.segy', '.sgy'))]) h5_count = len([n for n in files if n.lower().endswith('.h5')]) # Disk usage total, used, free = shutil.disk_usage(output_dir) # Conversion progress progress = None if os.path.exists('/tmp/segy2h5_progress.json'): try: with open('/tmp/segy2h5_progress.json', 'r') as f: progress = json.load(f) except: pass return jsonify({ 'counts': { 'raw': raw_count, 'segy': segy_count, 'h5': h5_count, 'total_expected': 345 # Hardcoded target from context }, 'disk': { 'total_gb': round(total / (1024**3), 2), 'used_gb': round(used / (1024**3), 2), 'free_gb': round(free / (1024**3), 2), 'percent': round((used / total) * 100, 1) }, 'progress': progress }) except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 # ========== GATHER ENDPOINT ========== @app.route('/api/gather_info', methods=['POST']) def gather_info(): """Return time window info with overlap clustering for a set of files.""" try: body = request.get_json() or {} files_param = body.get('files', '') filenames = [f.strip() for f in files_param.split(',') if f.strip()] import re as _re from datetime import datetime windows = [] for fn in filenames: fp = os.path.join(DATA_DIR, fn) if not os.path.exists(fp) or not fn.endswith('.h5'): continue m = _re.search(r'_(\d{10})', fn) ts = int(m.group(1)) if m else 0 if ts == 0: continue try: with h5py.File(fp, 'r') as hf: ch = 'calibrated_data/channel_1' if ch not in hf: continue sr = 500 if 'metadata' in hf: sr = int(hf['metadata'].attrs.get('sample_rate_hz', 500)) n = hf[ch].shape[0] dur = n / sr windows.append({'file': fn, 'start_ts': ts, 'end_ts': ts + int(dur), 'duration': round(dur, 1)}) except: continue if not windows: return jsonify({'error': 'No valid files'}), 400 windows.sort(key=lambda w: w['start_ts']) # Cluster into overlapping groups groups = [] for w in windows: placed = False for g in groups: g_start = min(x['start_ts'] for x in g) g_end = max(x['end_ts'] for x in g) if w['start_ts'] < g_end and w['end_ts'] > g_start: g.append(w) placed = True break if not placed: groups.append([w]) groups.sort(key=lambda g: len(g), reverse=True) group_summaries = [] for i, g in enumerate(groups): g_start = min(x['start_ts'] for x in g) g_end = max(x['end_ts'] for x in g) latest_start = max(x['start_ts'] for x in g) earliest_end = min(x['end_ts'] for x in g) union_duration = g_end - g_start group_summaries.append({ 'index': i, 'count': len(g), 'files': [x['file'] for x in g], 'start_ts': g_start, 'end_ts': g_end, 'overlap_start': g_start, 'overlap_duration': union_duration, 'union_duration': union_duration, 'overlap_start_utc': datetime.utcfromtimestamp(g_start).strftime('%Y-%m-%d %H:%M:%S'), 'date': datetime.utcfromtimestamp(g_start).strftime('%Y-%m-%d') }) best = group_summaries[0] if group_summaries else None return jsonify({ 'total_files': len(windows), 'groups': group_summaries, 'best_group': best, 'num_groups': len(groups) }) except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/gather', methods=['GET', 'POST']) def gather(): """Return multi-file trace data for gather view.""" try: if request.method == 'POST' and request.is_json: body = request.get_json() files_param = body.get('files', '') channel = body.get('channel', 'calibrated_data/channel_1') start = float(body.get('start', 0)) duration = float(body.get('duration', 60)) points = int(body.get('points', 2000)) sync = str(body.get('sync', 'true')).lower() == 'true' else: files_param = request.args.get('files', '') channel = request.args.get('channel', 'calibrated_data/channel_1') start = float(request.args.get('start', 0)) duration = float(request.args.get('duration', 60)) points = int(request.args.get('points', 2000)) sync = request.args.get('sync', 'true').lower() == 'true' filenames = [f.strip() for f in files_param.split(',') if f.strip()] if not filenames: return jsonify({'error': 'No files specified'}), 400 import re as _re file_timestamps = {} for fn in filenames: m = _re.search(r'_(\d{10})', fn) file_timestamps[fn] = int(m.group(1)) if m else 0 if sync: valid_ts = [file_timestamps[fn] for fn in filenames if file_timestamps[fn] > 0] min_ts = min(valid_ts) if valid_ts else 0 abs_time_param = None if request.method == 'POST' and request.is_json: abs_time_param = body.get('abs_time', None) else: abs_time_param = request.args.get('abs_time', None) if abs_time_param is not None: abs_start = float(abs_time_param) else: abs_start = min_ts + start traces = [] time_axis = None sample_rate = None max_dur = 0 for fn in filenames: fp = os.path.join(DATA_DIR, fn) if not os.path.exists(fp) or not fn.endswith('.h5'): continue board_id = extract_board_id(fn) try: with h5py.File(fp, 'r') as f: if channel not in f: continue sr = 500 if 'metadata' in f: sr = int(f['metadata'].attrs.get('sample_rate_hz', 500)) if sample_rate is None: sample_rate = sr total_samples = f[channel].shape[0] file_duration = total_samples / sr if sync and file_timestamps[fn] > 0: file_ts = file_timestamps[fn] local_start = abs_start - file_ts local_end = local_start + duration max_dur = max(max_dur, file_duration + (file_ts - min_ts)) else: local_start = start local_end = start + duration max_dur = max(max_dur, file_duration) start_sample = max(0, int(local_start * sr)) end_sample = min(int(local_end * sr), total_samples) if end_sample <= start_sample: trace_info = { 'filename': fn, 'board_id': board_id, 'y': [], 'min': 0, 'max': 0, 'start_ts': file_timestamps.get(fn, 0), 'empty': True } traces.append(trace_info) continue data = f[channel][start_sample:end_sample] n = len(data) if n > points: step = n // points indices = list(range(0, n, step))[:points] values = [float(data[i]) for i in indices] else: values = [float(v) for v in data] indices = list(range(n)) trace_info = { 'filename': fn, 'board_id': board_id, 'y': values, 'min': float(np.min(data)), 'max': float(np.max(data)), 'start_ts': file_timestamps.get(fn, 0) } traces.append(trace_info) if time_axis is None: time_axis = [local_start + i / sr for i in indices] if sync: time_axis = [(i / sr) for i in indices] except Exception as e: traceback.print_exc() continue actual_sr = sample_rate or 500 requested_samples = int(duration * actual_sr) resp = { 'traces': traces, 'sample_rate': actual_sr, 'time_axis': time_axis or [], 'file_duration': round(max_dur, 1), 'sync': sync, 'decimation': { 'display_points': points, 'original_samples': requested_samples, 'ratio': max(1, requested_samples // max(1, points)), 'is_decimated': requested_samples > points } } if sync and file_timestamps: min_ts = min(t for t in file_timestamps.values() if t > 0) if any(t > 0 for t in file_timestamps.values()) else 0 resp['epoch_start'] = min_ts resp['window_start'] = min_ts + start if min_ts else start from datetime import datetime if min_ts: resp['date_start'] = datetime.utcfromtimestamp(min_ts).strftime('%Y-%m-%d %H:%M:%S UTC') return jsonify(resp) except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 # ========== BACKGROUND EXTRACT LOGIC ========== def background_extract(task_id, filenames, start, duration, channels_opt, include_nodes, include_shots, include_metadata): """Worker function for async extraction.""" try: TASKS[task_id]['status'] = 'processing' TASKS[task_id]['total_files'] = len(filenames) # Load resources darf = load_darf_nodes() shot_times = [] if include_shots: try: shot_path = os.path.join(app.static_folder, 'shot_times.json') if os.path.exists(shot_path): with open(shot_path) as sf: shot_times = json.load(sf) except: pass import re as _re file_timestamps = {} for fn in filenames: m = _re.search(r'_(\d{10})', fn) file_timestamps[fn] = int(m.group(1)) if m else 0 valid_ts = [t for t in file_timestamps.values() if t > 0] min_ts = min(valid_ts) if valid_ts else 0 abs_start = min_ts + start abs_end = abs_start + duration export_name = 'gather_extract_s%d_d%d.zip' % (int(start), int(duration)) temp_zip_path = os.path.join(TEMP_DIR, f"{task_id}.zip") with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: metadata = { 'export_time': datetime.utcnow().isoformat() + 'Z', 'start_offset_s': start, 'duration_s': duration, 'abs_start_epoch': abs_start, 'abs_end_epoch': abs_end, 'sample_rate_hz': 500, 'files': [], 'decimation': 'none - full resolution export' } ch_map = { 'all8': ['calibrated_data/channel_1','calibrated_data/channel_2','calibrated_data/channel_3','calibrated_data/channel_4', 'raw_data/channel_1','raw_data/channel_2','raw_data/channel_3','raw_data/channel_4'], 'all_cal': ['calibrated_data/channel_1','calibrated_data/channel_2','calibrated_data/channel_3','calibrated_data/channel_4'], 'all_raw': ['raw_data/channel_1','raw_data/channel_2','raw_data/channel_3','raw_data/channel_4'], 'cal_1': ['calibrated_data/channel_1'], 'cal_2': ['calibrated_data/channel_2'], 'cal_3': ['calibrated_data/channel_3'], 'cal_4': ['calibrated_data/channel_4'], 'raw_1': ['raw_data/channel_1'], 'raw_2': ['raw_data/channel_2'], 'raw_3': ['raw_data/channel_3'], 'raw_4': ['raw_data/channel_4'], } all_channels = ch_map.get(channels_opt, ch_map['all8']) processed_count = 0 for fn in filenames: processed_count += 1 TASKS[task_id]['current_file'] = fn TASKS[task_id]['progress'] = int((processed_count / len(filenames)) * 95) TASKS[task_id]['processed_files'] = processed_count fp = os.path.join(DATA_DIR, fn) if not os.path.exists(fp) or not fn.endswith('.h5'): continue board_id = extract_board_id(fn) node_info = darf.get(str(board_id), {}) if board_id else {} file_ts = file_timestamps.get(fn, 0) if file_ts > 0: local_start = abs_start - file_ts local_end = abs_end - file_ts else: local_start = start local_end = start + duration try: with h5py.File(fp, 'r') as hf: sr = 500 if 'metadata' in hf: sr = int(hf['metadata'].attrs.get('sample_rate_hz', 500)) total_samples = hf['calibrated_data/channel_1'].shape[0] s0 = max(0, int(local_start * sr)) s1 = min(total_samples, int(local_end * sr)) if s1 <= s0: continue n_samples = s1 - s0 channel_data = {} available_channels = [] for ch in all_channels: if ch in hf: channel_data[ch] = hf[ch][s0:s1] available_channels.append(ch) if not channel_data: continue csv_name = fn.replace('.h5', '') + '_extract.csv' csv_buf = io.StringIO() writer = csv_mod.writer(csv_buf) header = ['sample_index', 'time_s', 'time_utc'] for ch in available_channels: short = ch.replace('calibrated_data/channel_', 'cal_ch').replace('raw_data/channel_', 'raw_ch') header.append(short) writer.writerow(header) from datetime import timezone for i in range(n_samples): sample_idx = s0 + i time_s = round(sample_idx / sr, 4) if file_ts > 0: utc_ts = file_ts + time_s time_utc = datetime.fromtimestamp(utc_ts, tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.') + str(int((utc_ts % 1) * 1000)).zfill(3) else: time_utc = '' row = [sample_idx, time_s, time_utc] for ch in available_channels: row.append(channel_data[ch][i]) writer.writerow(row) zf.writestr(csv_name, csv_buf.getvalue()) file_meta = { 'filename': fn, 'board_id': board_id, 'line': node_info.get('line'), 'point': node_info.get('point'), 'start_epoch': file_ts, 'sample_range': [s0, s1], 'n_samples': n_samples, 'duration_s': round(n_samples / sr, 2), 'channels': available_channels } metadata['files'].append(file_meta) except Exception: traceback.print_exc() continue # Matching shots if shot_times: day_start_h = abs_start % 86400 day_end_h = abs_end % 86400 matching_shots = [] for shot in shot_times: shot_tod = shot['time_s'] if day_start_h <= day_end_h: if shot_tod >= day_start_h and shot_tod <= day_end_h: matching_shots.append(shot) else: if shot_tod >= day_start_h or shot_tod <= day_end_h: matching_shots.append(shot) if matching_shots: shot_buf = io.StringIO() writer = csv_mod.writer(shot_buf) writer.writerow(['line', 'time_of_day_s', 'time_str', 'easting', 'northing']) for s in matching_shots: writer.writerow([s.get('line',''), s.get('time_s',''), s.get('time_str',''), s.get('easting',''), s.get('northing','')]) zf.writestr('shots.csv', shot_buf.getvalue()) metadata['matching_shots'] = len(matching_shots) # Node positions if include_nodes and darf: node_buf = io.StringIO() nw = csv_mod.writer(node_buf) nw.writerow(['board_id','line','point','preplot_easting','preplot_northing','preplot_lat','preplot_lon', 'aslaid_easting','aslaid_northing','aslaid_lat','aslaid_lon','depth']) exported_bids = set() for fm in metadata['files']: if fm.get('board_id'): exported_bids.add(str(fm['board_id'])) for bid, node in sorted(darf.items(), key=lambda x: (x[1].get('line',0), x[1].get('point',0))): if bid in exported_bids: nw.writerow([ bid, node.get('line',''), node.get('point',''), node.get('preplot_e',''), node.get('preplot_n',''), node.get('preplot_lat',''), node.get('preplot_lon',''), node.get('aslaid_e',''), node.get('aslaid_n',''), node.get('aslaid_lat',''), node.get('aslaid_lon',''), node.get('depth','') ]) zf.writestr('node_positions.csv', node_buf.getvalue()) if include_metadata: zf.writestr('metadata.json', json.dumps(metadata, indent=2, default=str)) TASKS[task_id]['progress'] = 100 TASKS[task_id]['status'] = 'completed' TASKS[task_id]['file_path'] = temp_zip_path TASKS[task_id]['file_name'] = export_name except Exception as e: TASKS[task_id]['status'] = 'error' TASKS[task_id]['error'] = str(e) traceback.print_exc() @app.route('/api/gather_extract', methods=['POST']) def gather_extract(): """Start async extraction job.""" try: cleanup_old_tasks() body = request.get_json() or {} files_param = body.get('files', '') start = float(body.get('start', 0)) duration = float(body.get('duration', 60)) filenames = [f.strip() for f in files_param.split(',') if f.strip()] if not filenames: return jsonify({'error': 'No files specified'}), 400 task_id = str(uuid.uuid4()) TASKS[task_id] = { 'id': task_id, 'status': 'pending', 'created_at': time.time(), 'progress': 0, 'total_files': len(filenames), 'processed_files': 0, 'current_file': '' } thread = threading.Thread(target=background_extract, args=( task_id, filenames, start, duration, body.get('channels', 'all8'), body.get('include_nodes', True), body.get('include_shots', True), body.get('include_metadata', True) )) thread.daemon = True thread.start() return jsonify({'task_id': task_id, 'status': 'pending'}) except Exception as e: traceback.print_exc() return jsonify({'error': str(e)}), 500 @app.route('/api/tasks/', methods=['GET']) def get_task_status(task_id): """Get status of a background task.""" task = TASKS.get(task_id) if not task: return jsonify({'error': 'Task not found'}), 404 return jsonify(task) @app.route('/api/tasks//download', methods=['GET']) def download_task_result(task_id): """Download the result of a completed task.""" task = TASKS.get(task_id) if not task: return jsonify({'error': 'Task not found'}), 404 if task['status'] != 'completed' or not os.path.exists(task.get('file_path', '')): return jsonify({'error': 'Task not ready or file missing'}), 400 return send_file( task['file_path'], mimetype='application/zip', as_attachment=True, download_name=task.get('file_name', 'extract.zip') ) @app.route('/api/shots') def get_shots(): """Return shot times matching visible time window.""" import re as _re from datetime import datetime as _dt, timedelta as _td, timezone as _tz filename = request.args.get('file', '') start_sec = float(request.args.get('start', 0)) duration_sec = float(request.args.get('duration', 3600)) shot_path = os.path.join(os.path.dirname(__file__), 'static', 'shot_times.json') if not os.path.exists(shot_path): return jsonify({'error': 'shot_times.json not found'}), 404 with open(shot_path) as f: all_shots = json.load(f) m = _re.search(r'_(\d{10,})\.h5', filename) if not m: return jsonify({'error': 'Cannot parse timestamp from filename'}), 400 file_start_unix = int(m.group(1)) file_start_dt = _dt.fromtimestamp(file_start_unix, tz=_tz.utc) win_start_dt = file_start_dt + _td(seconds=start_sec) win_end_dt = file_start_dt + _td(seconds=start_sec + duration_sec) results = [] day = win_start_dt.date() end_day = win_end_dt.date() while day <= end_day: day_start = _dt(day.year, day.month, day.day, tzinfo=_tz.utc) eff_start = max(win_start_dt, day_start) eff_end = min(win_end_dt, day_start + _td(days=1)) tod_start = (eff_start - day_start).total_seconds() tod_end = (eff_end - day_start).total_seconds() for shot in all_shots: if tod_start <= shot['time_s'] <= tod_end: abs_shot_time = day_start + _td(seconds=shot['time_s']) offset = (abs_shot_time - file_start_dt).total_seconds() results.append({ 'offset_s': round(offset, 2), 'time_str': shot['time_str'], 'line': shot['line'], 'date': str(day) }) day += _td(days=1) results.sort(key=lambda x: x['offset_s']) return jsonify({'shots': results, 'count': len(results)}) @app.route('/api/shots_by_time') def shots_by_time(): """Return shots matching an absolute time window (unix timestamps).""" from datetime import datetime as _dt, timedelta as _td, timezone as _tz start_unix = float(request.args.get('start_unix', 0)) duration = float(request.args.get('duration', 60)) shot_path = os.path.join(os.path.dirname(__file__), 'static', 'shot_times.json') if not os.path.exists(shot_path): return jsonify({'error': 'shot_times.json not found'}), 404 with open(shot_path) as f: all_shots = json.load(f) win_start_dt = _dt.fromtimestamp(start_unix, tz=_tz.utc) win_end_dt = _dt.fromtimestamp(start_unix + duration, tz=_tz.utc) results = [] day = win_start_dt.date() end_day = win_end_dt.date() while day <= end_day: day_start = _dt(day.year, day.month, day.day, tzinfo=_tz.utc) eff_start = max(win_start_dt, day_start) eff_end = min(win_end_dt, day_start + _td(days=1)) tod_start = (eff_start - day_start).total_seconds() tod_end = (eff_end - day_start).total_seconds() for shot in all_shots: if tod_start <= shot['time_s'] <= tod_end: abs_shot_time = day_start + _td(seconds=shot['time_s']) offset = (abs_shot_time - win_start_dt).total_seconds() results.append({ 'offset_s': round(offset, 2), 'time_str': shot['time_str'], 'line': shot['line'], 'date': str(day) }) day += _td(days=1) results.sort(key=lambda x: x['offset_s']) return jsonify({'shots': results, 'count': len(results)}) @app.route('/api/real_shots') def real_shots(): """Return real shot positions from SPS S01 files as GeoJSON.""" shot_path = os.path.join(os.path.dirname(__file__), 'static', 'real_shots.json') if not os.path.exists(shot_path): return jsonify({'error': 'real_shots.json not found'}), 404 with open(shot_path) as f: return jsonify(json.load(f)) @app.route("/api/coverage")def get_coverage(): """Get coverage data for all H5 files.""" import re as _re try: darf = {} if os.path.exists(DARF_FILE): with open(DARF_FILE) as _f: darf = json.load(_f) result = [] for f in os.listdir(DATA_DIR): if not f.endswith(".h5"): continue fpath = os.path.join(DATA_DIR, f) info = {"name": f, "size": os.path.getsize(fpath)} m = _re.match(r"auto_(d+)_(d{2})(d{2})(d{2})_b(d+)_rsn(d+)_seq(d+)_(d+)", f) if m: day, hh, mm, ss, board_id, rsn, seq, ts = m.groups() info["board_id"] = int(board_id) info["day"] = int(day) from datetime import datetime as _dt, timedelta as _td base = _dt(2020, 1, 1) + _td(days=int(day)-1, hours=int(hh), minutes=int(mm), seconds=int(ss)) info["date_str"] = base.isoformat() bid_str = str(board_id) if bid_str in darf: info["line"] = darf[bid_str].get("line") info["point"] = darf[bid_str].get("point") info["duration_sec"] = 8000 try: import h5py with h5py.File(fpath, "r") as h5f: attrs = dict(h5f.attrs) info["duration_sec"] = float(attrs.get("duration", attrs.get("record_length", 8000))) except: pass result.append(info) result.sort(key=lambda x: x.get("date_str", "")) return jsonify({"files": result, "count": len(result)}) except Exception as e: return jsonify({"error": str(e)}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=3001, debug=False)