Fix coverage: add /api/coverage route, remove stray gather code from loadCoverage
This commit is contained in:
20
Dockerfile.api
Normal file
20
Dockerfile.api
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir \
|
||||
flask \
|
||||
flask-cors \
|
||||
h5py \
|
||||
numpy
|
||||
|
||||
# Copy API server
|
||||
COPY h5_api_server.py .
|
||||
|
||||
# Make executable
|
||||
RUN chmod +x h5_api_server.py
|
||||
|
||||
EXPOSE 3004
|
||||
|
||||
CMD ["python", "h5_api_server.py"]
|
||||
BIN
__pycache__/app.py.cpython-313.pyc
Normal file
BIN
__pycache__/app.py.cpython-313.pyc
Normal file
Binary file not shown.
43
app.py.final
43
app.py.final
@@ -1628,7 +1628,48 @@ def real_shots():
|
||||
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
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
927
app.py.fixed
Normal file
927
app.py.fixed
Normal file
@@ -0,0 +1,927 @@
|
||||
from flask import Flask, jsonify, send_from_directory, request, send_file
|
||||
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
|
||||
|
||||
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'
|
||||
|
||||
# ========== UTILITY FUNCTIONS ==========
|
||||
|
||||
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/<filename>/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
|
||||
|
||||
# ========== NEW: METADATA ENDPOINT ==========
|
||||
|
||||
@app.route('/api/file/<filename>/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
|
||||
|
||||
# ========== NEW: LOCATIONS ENDPOINT ==========
|
||||
|
||||
@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
|
||||
|
||||
# ========== NEW: EXTRACT ENDPOINT ==========
|
||||
|
||||
@app.route('/api/file/<filename>/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
|
||||
|
||||
# ========== EXISTING ENDPOINTS (kept for compatibility) ==========
|
||||
|
||||
@app.route('/api/file/<filename>/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 = int(request.args.get('samples_per_trace', 1000))
|
||||
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/<filename>/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/<filename>/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))
|
||||
channel = request.args.get('channel', 'calibrated_data/channel_1')
|
||||
if filename.endswith('.h5'):
|
||||
with h5py.File(fp, 'r') as f:
|
||||
if channel not in f:
|
||||
return jsonify({'error': 'Channel not found'}), 404
|
||||
data = f[channel][:]
|
||||
n = len(data)
|
||||
if n <= max_pts:
|
||||
values = data.tolist()
|
||||
indices = list(range(n))
|
||||
else:
|
||||
step = n // max_pts
|
||||
indices = list(range(0, n, step))[:max_pts]
|
||||
values = [float(data[i]) for i in indices]
|
||||
return jsonify({
|
||||
'name': filename, 'channel': channel,
|
||||
'total_samples': n,
|
||||
'displayed_points': len(values),
|
||||
'min': float(np.min(data)), 'max': float(np.max(data)),
|
||||
'mean': float(np.mean(data)), 'std': float(np.std(data)),
|
||||
'x': indices, 'y': values
|
||||
})
|
||||
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/<path:filename>')
|
||||
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
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=3001, debug=False)
|
||||
|
||||
@app.route('/test-map')
|
||||
def test_map():
|
||||
return send_from_directory('static', 'test-map.html')
|
||||
927
app.py.original
Normal file
927
app.py.original
Normal file
@@ -0,0 +1,927 @@
|
||||
from flask import Flask, jsonify, send_from_directory, request, send_file
|
||||
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
|
||||
|
||||
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'
|
||||
|
||||
# ========== UTILITY FUNCTIONS ==========
|
||||
|
||||
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/<filename>/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 = total_samples / 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'] = sample_rate
|
||||
info['num_channels'] = 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
|
||||
|
||||
# ========== NEW: METADATA ENDPOINT ==========
|
||||
|
||||
@app.route('/api/file/<filename>/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
|
||||
|
||||
# ========== NEW: LOCATIONS ENDPOINT ==========
|
||||
|
||||
@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
|
||||
|
||||
# ========== NEW: EXTRACT ENDPOINT ==========
|
||||
|
||||
@app.route('/api/file/<filename>/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
|
||||
|
||||
# ========== EXISTING ENDPOINTS (kept for compatibility) ==========
|
||||
|
||||
@app.route('/api/file/<filename>/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 = int(request.args.get('samples_per_trace', 1000))
|
||||
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/<filename>/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/<filename>/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))
|
||||
channel = request.args.get('channel', 'calibrated_data/channel_1')
|
||||
if filename.endswith('.h5'):
|
||||
with h5py.File(fp, 'r') as f:
|
||||
if channel not in f:
|
||||
return jsonify({'error': 'Channel not found'}), 404
|
||||
data = f[channel][:]
|
||||
n = len(data)
|
||||
if n <= max_pts:
|
||||
values = data.tolist()
|
||||
indices = list(range(n))
|
||||
else:
|
||||
step = n // max_pts
|
||||
indices = list(range(0, n, step))[:max_pts]
|
||||
values = [float(data[i]) for i in indices]
|
||||
return jsonify({
|
||||
'name': filename, 'channel': channel,
|
||||
'total_samples': n,
|
||||
'displayed_points': len(values),
|
||||
'min': float(np.min(data)), 'max': float(np.max(data)),
|
||||
'mean': float(np.mean(data)), 'std': float(np.std(data)),
|
||||
'x': indices, 'y': values
|
||||
})
|
||||
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/<path:filename>')
|
||||
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
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=3001, debug=False)
|
||||
|
||||
@app.route('/test-map')
|
||||
def test_map():
|
||||
return send_from_directory('static', 'test-map.html')
|
||||
250
backend_src/index.ts
Normal file
250
backend_src/index.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { readFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const app = express();
|
||||
const PORT = 3001;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://postgres:seismic_pass@db:5432/seismic_data'
|
||||
});
|
||||
|
||||
const INDEX_PATH = '/mnt/kingston/seismic_webapp/data/index.json';
|
||||
const EXPORT_DIR = '/mnt/kingston/seismic_webapp/exports';
|
||||
if (!existsSync(EXPORT_DIR)) mkdirSync(EXPORT_DIR, { recursive: true });
|
||||
|
||||
// GET /api/migration-status - Renvoie l'état de la migration en cours
|
||||
app.get('/api/migration-status', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT total_files, processed_files, current_file FROM migration_status WHERE id = 1');
|
||||
if (result.rows.length > 0) {
|
||||
res.json(result.rows[0]);
|
||||
} else {
|
||||
res.status(404).json({ error: 'Status not found' });
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'DB Error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/nodes', (req, res) => {
|
||||
try {
|
||||
const index = JSON.parse(readFileSync(INDEX_PATH, 'utf-8'));
|
||||
const nodes = Object.values(index?.nodes || {}).map((n: any) => ({
|
||||
id: n.id, position: n.position, hasDates: n.files && n.files.length > 0
|
||||
}));
|
||||
res.json({ nodes });
|
||||
} catch (e) { res.status(500).json({ error: 'Index read error' }); }
|
||||
});
|
||||
|
||||
app.get('/api/dates', (req, res) => {
|
||||
try {
|
||||
const index = JSON.parse(readFileSync(INDEX_PATH, 'utf-8'));
|
||||
res.json({ dates: index?.dates || [] });
|
||||
} catch (e) { res.json({ dates: [] }); }
|
||||
});
|
||||
|
||||
app.get('/api/rms-timeline', (req, res) => {
|
||||
const { date, channel } = req.query;
|
||||
const cachePath = `/mnt/kingston/seismic_webapp/data/rms_cache/rms_${date}_${channel}.json`;
|
||||
if (existsSync(cachePath)) res.json(JSON.parse(readFileSync(cachePath, 'utf-8')));
|
||||
else res.json({ nodes: {} });
|
||||
});
|
||||
|
||||
app.get('/api/global-history', (req, res) => {
|
||||
const { channel } = req.query;
|
||||
const dir = '/mnt/kingston/seismic_webapp/data/rms_cache';
|
||||
const nodes: Record<string, any[]> = {};
|
||||
try {
|
||||
if (existsSync(dir)) {
|
||||
readdirSync(dir).filter((f:string) => f.endsWith(`_${channel}.json`)).forEach((f:string) => {
|
||||
const data = JSON.parse(readFileSync(`${dir}/${f}`, 'utf-8'));
|
||||
Object.entries(data.nodes).forEach(([id, pts]: [string, any]) => {
|
||||
if (!nodes[id]) nodes[id] = [];
|
||||
nodes[id].push(...pts);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
res.json({ nodes });
|
||||
});
|
||||
|
||||
app.get('/api/data', async (req, res) => {
|
||||
const { node, start, channel } = req.query;
|
||||
const ts = parseInt(start as string);
|
||||
const startTime = new Date(ts * 1000);
|
||||
const endTime = new Date((ts + 10) * 1000);
|
||||
|
||||
try {
|
||||
const dbRes = await pool.query('SELECT value FROM adc_samples WHERE node_id = $1 AND channel = $2 AND time >= $3 AND time < $4 ORDER BY time ASC', [node, channel, startTime, endTime]);
|
||||
if (dbRes.rows.length > 0) return res.json({ samples: dbRes.rows.map(r => r.value), source: 'db' });
|
||||
|
||||
const index = JSON.parse(readFileSync(INDEX_PATH, 'utf-8'));
|
||||
const targetFile = index?.nodes[node as string]?.files?.find((f: any) => ts >= f.start && ts <= f.end && (f.channel === channel || f.path.includes(`_${channel}_`)));
|
||||
if (!targetFile) return res.status(404).json({ error: 'File not found' });
|
||||
|
||||
const fixPath = (p: string) => p.replace(/\\/g, '/').replace('F:/', '/mnt/kingston/').replace('E:/', '/mnt/data_sdb1/');
|
||||
const proc = spawn('python3', ['/app/scripts/extract_hdf5_window.py', '--file', fixPath(targetFile.path), '--channel', (channel as string).replace('ch',''), '--start', ts.toString(), '--duration', '10']);
|
||||
let stdout = '';
|
||||
proc.stdout.on('data', (d) => stdout += d.toString());
|
||||
proc.on('close', () => {
|
||||
try { res.json(JSON.parse(stdout)); } catch (e) { res.status(500).json({ error: 'Python error' }); }
|
||||
});
|
||||
} catch (err) { res.status(500).json({ error: 'Server error' }); }
|
||||
});
|
||||
|
||||
app.post('/api/chat', (req, res) => {
|
||||
const { message } = req.body;
|
||||
const prompt = `Assistant Seismic. User: ${message}`;
|
||||
const proc = spawn('/home/floppyrj45/.local/bin/cursor-agent', ['--print', '--force', prompt]);
|
||||
let output = '';
|
||||
proc.stdout.on('data', (d) => output += d.toString());
|
||||
proc.on('close', () => res.json({ response: output }));
|
||||
});
|
||||
|
||||
app.get('/api/export', (req, res) => {
|
||||
const { node, date, channel, start } = req.query;
|
||||
try {
|
||||
const index = JSON.parse(readFileSync(INDEX_PATH, 'utf-8'));
|
||||
const targetFile = index?.nodes[node as string]?.files?.find((f:any) => f.path.includes(`_${channel}_`));
|
||||
if (!targetFile) return res.status(404).send('Not found');
|
||||
const fixPath = (p: string) => p.replace(/\\/g, '/').replace('F:/', '/mnt/kingston/').replace('E:/', '/mnt/data_sdb1/');
|
||||
const proc = spawn('python3', ['/app/scripts/export_csv.py', '--file', fixPath(targetFile.path), '--start', start as string, '--duration', '3600', '--output', `/tmp/export_${node}.csv`]);
|
||||
proc.on('close', () => res.download(`/tmp/export_${node}.csv`));
|
||||
} catch (e) { res.status(500).send('Export error'); }
|
||||
});
|
||||
|
||||
app.listen(PORT, () => console.log(`API port ${PORT}`));
|
||||
// H5 Coverage Endpoints
|
||||
import sqlite3 from 'sqlite3';
|
||||
const h5db = new sqlite3.Database('/app/h5_data.db', sqlite3.OPEN_READONLY, (err: any) => {
|
||||
if (err) console.error('H5 DB not available:', err.message);
|
||||
});
|
||||
|
||||
app.get('/api/h5/coverage', (req, res) => {
|
||||
h5db.all(`
|
||||
SELECT
|
||||
COUNT(*) as total_positions,
|
||||
SUM(CASE WHEN has_data = 1 THEN 1 ELSE 0 END) as with_data,
|
||||
SUM(CASE WHEN has_aux = 1 THEN 1 ELSE 0 END) as with_aux,
|
||||
SUM(sample_count) as total_files
|
||||
FROM positions
|
||||
`, (err: any, rows: any) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
|
||||
const stats = rows[0];
|
||||
const coverage_pct = ((stats.with_data / stats.total_positions) * 100).toFixed(1);
|
||||
|
||||
res.json({
|
||||
total_positions: stats.total_positions,
|
||||
with_data: stats.with_data,
|
||||
with_aux: stats.with_aux,
|
||||
total_files: stats.total_files,
|
||||
coverage_pct: parseFloat(coverage_pct),
|
||||
missing: stats.total_positions - stats.with_data
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/h5/gaps', (req, res) => {
|
||||
h5db.all('SELECT node_code FROM positions WHERE has_data = 0 ORDER BY node_code', (err: any, rows: any) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
|
||||
const missing = rows.map((r: any) => r.node_code);
|
||||
const gaps = [];
|
||||
let gapStart = null;
|
||||
|
||||
for (let i = 0; i < missing.length; i++) {
|
||||
if (i === 0 || missing[i] !== missing[i-1] + 1) {
|
||||
if (gapStart !== null) {
|
||||
gaps.push({ start: gapStart, end: missing[i-1], length: missing[i-1] - gapStart + 1 });
|
||||
}
|
||||
gapStart = missing[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (gapStart !== null) {
|
||||
gaps.push({ start: gapStart, end: missing[missing.length - 1], length: missing[missing.length - 1] - gapStart + 1 });
|
||||
}
|
||||
|
||||
res.json({ gaps, total_missing: missing.length });
|
||||
});
|
||||
});
|
||||
|
||||
// GET /api/conversion-status - Proxy vers la VM de conversion
|
||||
app.get('/api/conversion-status', async (req, res) => {
|
||||
try {
|
||||
const response = await fetch('http://192.168.0.81:8000/conversion-progress.json');
|
||||
const data = await response.json();
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
res.status(503).json({
|
||||
error: 'VM conversion inaccessible',
|
||||
total: 345,
|
||||
processed: 0,
|
||||
errors: 0,
|
||||
percent: 0,
|
||||
eta_minutes: 0
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ============ H5 2026 Format Endpoints ============
|
||||
|
||||
app.get('/api/h5/files', (req, res) => {
|
||||
const h5Dir = '/home/floppyrj45/docker/seismic-nodes-viewer/data/h5';
|
||||
try {
|
||||
const files = readdirSync(h5Dir)
|
||||
.filter(f => f.endsWith('.h5'))
|
||||
.map(filename => {
|
||||
const match = filename.match(/rsn(\d+)/);
|
||||
const nodeId = match ? match[1] : 'unknown';
|
||||
const matchDate = filename.match(/_(\d{6})_/);
|
||||
const date = matchDate ? matchDate[1] : '';
|
||||
return { filename, nodeId, date, path: `${h5Dir}/${filename}` };
|
||||
});
|
||||
res.json({ files, count: files.length });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Cannot list H5 files' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/h5/data', (req, res) => {
|
||||
const { file, channel, start, duration } = req.query;
|
||||
const filePath = `/home/floppyrj45/docker/seismic-nodes-viewer/data/h5/${file}`;
|
||||
|
||||
const proc = spawn('python3', [
|
||||
'/home/floppyrj45/docker/seismic-nodes-viewer/scripts/extract_h5_calibrated.py',
|
||||
'--file', filePath,
|
||||
'--channel', channel as string,
|
||||
'--start', (start || '0') as string,
|
||||
'--duration', (duration || '10') as string
|
||||
]);
|
||||
|
||||
let stdout = '';
|
||||
proc.stdout.on('data', d => stdout += d.toString());
|
||||
proc.on('close', () => {
|
||||
try {
|
||||
res.json(JSON.parse(stdout));
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Python script error', raw: stdout });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Docs endpoints
|
||||
app.get('/api/docs/manifest', (req, res) => {
|
||||
res.sendFile('/data/docs/campaign_manifest.json');
|
||||
});
|
||||
|
||||
app.get('/api/docs/:filename', (req, res) => {
|
||||
const file = req.params.filename;
|
||||
res.sendFile(`/data/docs/${file}`);
|
||||
});
|
||||
BIN
data/docs/Final_PickUp_20200918.png
Normal file
BIN
data/docs/Final_PickUp_20200918.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
61
data/docs/SETE-0965P1002.csv
Normal file
61
data/docs/SETE-0965P1002.csv
Normal file
@@ -0,0 +1,61 @@
|
||||
965,4961,1,A1,2,558568.9,4797608.3,34.1,3,0,2020,250,9,30,3,672023
|
||||
965,4963,1,A1,2,558610.5,4797635.4,34.1,2.7,0,2020,250,9,30,25,460017
|
||||
965,4965,1,A1,2,558653.3,4797661.3,34.1,2.9,0,2020,250,9,30,47,260023
|
||||
965,4967,1,A1,2,558696.1,4797687.2,34.1,2.8,0,2020,250,9,31,9,220018
|
||||
965,4969,1,A1,2,558738,4797714.6,34.1,2.9,0,2020,250,9,31,31,388019
|
||||
965,4971,1,A1,2,558775,4797748.8,34.3,2.9,0,2020,250,9,31,54,128017
|
||||
965,4973,1,A1,2,558812.1,4797783.9,34.1,2.8,0,2020,250,9,32,16,896020
|
||||
965,4975,1,A1,2,558854,4797810.7,34.1,2.8,0,2020,250,9,32,38,384020
|
||||
965,4977,1,A1,2,558897.7,4797835.2,34.1,2.8,0,2020,250,9,32,59,984020
|
||||
965,4979,1,A1,2,558938.4,4797864.3,34.1,2.8,0,2020,250,9,33,21,892020
|
||||
965,4981,1,A1,2,558979.4,4797892.9,34.1,2.7,0,2020,250,9,33,43,764021
|
||||
965,4983,1,A1,2,559018.8,4797923.9,34.1,2.8,0,2020,250,9,34,5,740021
|
||||
965,4985,1,A1,2,559059.7,4797952.7,34.1,2.9,0,2020,250,9,34,27,604022
|
||||
965,4987,1,A1,2,559100.5,4797981.6,34.2,2.9,0,2020,250,9,34,49,332021
|
||||
965,4989,1,A1,2,559142.6,4798008.5,34.1,2.9,0,2020,250,9,35,10,992021
|
||||
965,4991,1,A1,2,559183.4,4798037.5,34.1,2.9,0,2020,250,9,35,32,836021
|
||||
965,4993,1,A1,2,559225.3,4798064.8,34.1,2.8,0,2020,250,9,35,54,632022
|
||||
965,4995,1,A1,2,559265.9,4798093.9,34.1,2.9,0,2020,250,9,36,16,444021
|
||||
965,4997,1,A1,2,559306.2,4798123.6,33.9,2.8,0,2020,250,9,36,38,336022
|
||||
965,4999,1,A1,2,559347.7,4798151.6,34.2,2.8,0,2020,250,9,37,0,136022
|
||||
965,5001,1,A1,2,559388.7,4798180.2,34.2,2.9,0,2020,250,9,37,21,996023
|
||||
965,5003,1,A1,2,559428.5,4798210.6,34.1,2.8,0,2020,250,9,37,43,832017
|
||||
965,5005,1,A1,2,559470,4798238.3,33.5,2.8,0,2020,250,9,38,5,452022
|
||||
965,5007,1,A1,2,559510.7,4798267.4,34.1,2.9,0,2020,250,9,38,27,292022
|
||||
965,5009,1,A1,2,559550.9,4798297.2,34.2,2.8,0,2020,250,9,38,49,204023
|
||||
965,5011,1,A1,2,559591.8,4798326,34.1,2.8,0,2020,250,9,39,11,8017
|
||||
965,5013,1,A1,2,559633.5,4798353.7,34.1,2.9,0,2020,250,9,39,32,744044
|
||||
965,5015,1,A1,2,559675.1,4798381.3,34.2,2.8,0,2020,250,9,39,54,440023
|
||||
965,5017,1,A1,2,559715.2,4798411.2,34.1,2.8,0,2020,250,9,40,16,316022
|
||||
965,5019,1,A1,2,559756.7,4798439.1,34.2,2.8,0,2020,250,9,40,38,112017
|
||||
965,5021,1,A1,2,559797.4,4798468,34.2,2.9,0,2020,250,9,41,0,92018
|
||||
965,5023,1,A1,2,559838.4,4798496.9,34.1,2.9,0,2020,250,9,41,22,104018
|
||||
965,5025,1,A1,2,559880,4798524.5,34.2,2.9,0,2020,250,9,41,43,996020
|
||||
965,5027,1,A1,2,559920.9,4798553.4,34.1,2.9,0,2020,250,9,42,5,944019
|
||||
965,5029,1,A1,2,559962.2,4798581.5,34.2,2.8,0,2020,250,9,42,27,864021
|
||||
965,5031,1,A1,2,560004,4798609,34.2,2.8,0,2020,250,9,42,49,656021
|
||||
965,5033,1,A1,2,560045.7,4798636.5,34.2,2.8,0,2020,250,9,43,11,452020
|
||||
965,5035,1,A1,2,560085.9,4798666.5,34.1,2.8,0,2020,250,9,43,33,512021
|
||||
965,5037,1,A1,2,560127,4798694.9,34.1,2.9,0,2020,250,9,43,55,440022
|
||||
965,5039,1,A1,2,560168.3,4798722.9,34.3,2.8,0,2020,250,9,44,17,424017
|
||||
965,5041,1,A1,2,560208.3,4798753.1,34.1,2.8,0,2020,250,9,44,39,604018
|
||||
965,5043,1,A1,2,560250.1,4798781.2,34.1,2.9,0,2020,250,9,45,0,944018
|
||||
965,5045,1,A1,2,560290.2,4798811,34.2,2.8,0,2020,250,9,45,21,36021
|
||||
965,5047,1,A1,2,560329.6,4798841.2,34.1,2.7,0,2020,250,9,45,40,728019
|
||||
965,5049,1,A1,2,560370.5,4798869.9,34.1,2.7,0,2020,250,9,46,0,968019
|
||||
965,5051,1,A1,2,560410.9,4798899.4,34.1,2.7,0,2020,250,9,46,21,420019
|
||||
965,5053,1,A1,2,560451.2,4798929.3,34.1,2.7,0,2020,250,9,46,41,956020
|
||||
965,5055,1,A1,2,560493.1,4798956.6,34.3,2.7,0,2020,250,9,47,2,200026
|
||||
965,5057,1,A1,2,560533.9,4798985.4,34.1,2.7,0,2020,250,9,47,22,536019
|
||||
965,5059,1,A1,2,560574.3,4799014.8,34.4,2.7,0,2020,250,9,47,43,108020
|
||||
965,5061,1,A1,2,560616,4799042.7,34.4,2.7,0,2020,250,9,48,3,564021
|
||||
965,5063,1,A1,2,560657.3,4799070.5,34.2,2.7,0,2020,250,9,48,23,944021
|
||||
965,5065,1,A1,2,560697.6,4799100.3,34.4,2.7,0,2020,250,9,48,44,560022
|
||||
965,5067,1,A1,2,560739.2,4799128.1,34.2,2.7,0,2020,250,9,49,5,88023
|
||||
965,5069,1,A1,2,560779.5,4799157.7,34.4,2.7,0,2020,250,9,49,25,692018
|
||||
965,5071,1,A1,2,560820.6,4799186.4,34.4,2.7,0,2020,250,9,49,46,120024
|
||||
965,5073,1,A1,2,560862.5,4799213.6,34.4,2.7,0,2020,250,9,50,6,68022
|
||||
965,5075,1,A1,2,560903,4799242.8,34.4,2.7,0,2020,250,9,50,26,176021
|
||||
965,5077,1,A1,2,560944.5,4799270.8,34.4,2.7,0,2020,250,9,50,46,204019
|
||||
965,5079,1,A1,2,560986.2,4799298.2,34.4,2.7,0,2020,250,9,51,6,152018
|
||||
965,5081,1,A1,2,561026.4,4799328.2,34.4,2.7,0,2020,250,9,51,26,300023
|
||||
|
200
data/docs/SeteRxPreplots.r01
Normal file
200
data/docs/SeteRxPreplots.r01
Normal file
@@ -0,0 +1,200 @@
|
||||
R 1000 5000 1 559867.204797453.00 0.00
|
||||
R 1000 5004 1 559949.104797510.40 0.00
|
||||
R 1000 5008 1 560031.104797567.80 0.00
|
||||
R 1000 5012 1 560113.004797625.10 0.00
|
||||
R 1000 5016 1 560194.904797682.50 0.00
|
||||
R 1000 5020 1 560276.804797739.80 0.00
|
||||
R 1000 5024 1 560358.704797797.20 0.00
|
||||
R 1000 5028 1 560440.604797854.60 0.00
|
||||
R 1000 5032 1 560522.504797911.90 0.00
|
||||
R 1000 5036 1 560604.504797969.30 0.00
|
||||
R 1000 5040 1 560686.404798026.60 0.00
|
||||
R 1000 5044 1 560768.304798084.00 0.00
|
||||
R 1000 5048 1 560850.204798141.30 0.00
|
||||
R 1000 5052 1 560932.104798198.70 0.00
|
||||
R 1000 5056 1 561014.004798256.10 0.00
|
||||
R 1000 5060 1 561096.004798313.40 0.00
|
||||
R 1000 5064 1 561177.904798370.80 0.00
|
||||
R 1000 5068 1 561259.804798428.10 0.00
|
||||
R 1000 5072 1 561341.704798485.50 0.00
|
||||
R 1000 5076 1 561423.604798542.80 0.00
|
||||
R 1000 5080 1 561505.504798600.20 0.00
|
||||
R 1000 5084 1 561587.404798657.60 0.00
|
||||
R 1000 5088 1 561669.404798714.90 0.00
|
||||
R 1000 5092 1 561751.304798772.30 0.00
|
||||
R 1000 5096 1 561833.204798829.60 0.00
|
||||
R 1012 5000 1 560039.304797207.30 0.00
|
||||
R 1012 5004 1 560121.204797264.70 0.00
|
||||
R 1012 5008 1 560203.104797322.00 0.00
|
||||
R 1012 5012 1 560285.004797379.40 0.00
|
||||
R 1012 5016 1 560367.004797436.70 0.00
|
||||
R 1012 5020 1 560448.904797494.10 0.00
|
||||
R 1012 5024 1 560530.804797551.40 0.00
|
||||
R 1012 5028 1 560612.704797608.80 0.00
|
||||
R 1012 5032 1 560694.604797666.20 0.00
|
||||
R 1012 5036 1 560776.504797723.50 0.00
|
||||
R 1012 5040 1 560858.504797780.90 0.00
|
||||
R 1012 5044 1 560940.404797838.20 0.00
|
||||
R 1012 5048 1 561022.304797895.60 0.00
|
||||
R 1012 5052 1 561104.204797953.00 0.00
|
||||
R 1012 5056 1 561186.104798010.30 0.00
|
||||
R 1012 5060 1 561268.004798067.70 0.00
|
||||
R 1012 5064 1 561349.904798125.00 0.00
|
||||
R 1012 5068 1 561431.904798182.40 0.00
|
||||
R 1012 5072 1 561513.804798239.70 0.00
|
||||
R 1012 5076 1 561595.704798297.10 0.00
|
||||
R 1012 5080 1 561677.604798354.50 0.00
|
||||
R 1012 5084 1 561759.504798411.80 0.00
|
||||
R 1012 5088 1 561841.404798469.20 0.00
|
||||
R 1012 5092 1 561923.304798526.50 0.00
|
||||
R 1012 5096 1 562005.304798583.90 0.00
|
||||
R 1024 5000 1 560211.404796961.60 0.00
|
||||
R 1024 5004 1 560293.304797018.90 0.00
|
||||
R 1024 5008 1 560375.204797076.30 0.00
|
||||
R 1024 5012 1 560457.104797133.60 0.00
|
||||
R 1024 5016 1 560539.004797191.00 0.00
|
||||
R 1024 5020 1 560620.904797248.30 0.00
|
||||
R 1024 5024 1 560702.904797305.70 0.00
|
||||
R 1024 5028 1 560784.804797363.10 0.00
|
||||
R 1024 5032 1 560866.704797420.40 0.00
|
||||
R 1024 5036 1 560948.604797477.80 0.00
|
||||
R 1024 5040 1 561030.504797535.10 0.00
|
||||
R 1024 5044 1 561112.404797592.50 0.00
|
||||
R 1024 5048 1 561194.404797649.80 0.00
|
||||
R 1024 5052 1 561276.304797707.20 0.00
|
||||
R 1024 5056 1 561358.204797764.60 0.00
|
||||
R 1024 5060 1 561440.104797821.90 0.00
|
||||
R 1024 5064 1 561522.004797879.30 0.00
|
||||
R 1024 5068 1 561603.904797936.60 0.00
|
||||
R 1024 5072 1 561685.804797994.00 0.00
|
||||
R 1024 5076 1 561767.804798051.40 0.00
|
||||
R 1024 5080 1 561849.704798108.70 0.00
|
||||
R 1024 5084 1 561931.604798166.10 0.00
|
||||
R 1024 5088 1 562013.504798223.40 0.00
|
||||
R 1024 5092 1 562095.404798280.80 0.00
|
||||
R 1024 5096 1 562177.304798338.10 0.00
|
||||
R 1036 5000 1 560383.404796715.80 0.00
|
||||
R 1036 5004 1 560465.404796773.20 0.00
|
||||
R 1036 5008 1 560547.304796830.50 0.00
|
||||
R 1036 5012 1 560629.204796887.90 0.00
|
||||
R 1036 5016 1 560711.104796945.20 0.00
|
||||
R 1036 5020 1 560793.004797002.60 0.00
|
||||
R 1036 5024 1 560874.904797060.00 0.00
|
||||
R 1036 5028 1 560956.904797117.30 0.00
|
||||
R 1036 5032 1 561038.804797174.70 0.00
|
||||
R 1036 5036 1 561120.704797232.00 0.00
|
||||
R 1036 5040 1 561202.604797289.40 0.00
|
||||
R 1036 5044 1 561284.504797346.70 0.00
|
||||
R 1036 5048 1 561366.404797404.10 0.00
|
||||
R 1036 5052 1 561448.304797461.50 0.00
|
||||
R 1036 5056 1 561530.304797518.80 0.00
|
||||
R 1036 5060 1 561612.204797576.20 0.00
|
||||
R 1036 5064 1 561694.104797633.50 0.00
|
||||
R 1036 5068 1 561776.004797690.90 0.00
|
||||
R 1036 5072 1 561857.904797748.20 0.00
|
||||
R 1036 5076 1 561939.804797805.60 0.00
|
||||
R 1036 5080 1 562021.704797863.00 0.00
|
||||
R 1036 5084 1 562103.704797920.30 0.00
|
||||
R 1036 5088 1 562185.604797977.70 0.00
|
||||
R 1036 5092 1 562267.504798035.00 0.00
|
||||
R 1036 5096 1 562349.404798092.40 0.00
|
||||
R 1048 5000 1 560555.504796470.10 0.00
|
||||
R 1048 5004 1 560637.404796527.40 0.00
|
||||
R 1048 5008 1 560719.304796584.80 0.00
|
||||
R 1048 5012 1 560801.304796642.10 0.00
|
||||
R 1048 5016 1 560883.204796699.50 0.00
|
||||
R 1048 5020 1 560965.104796756.90 0.00
|
||||
R 1048 5024 1 561047.004796814.20 0.00
|
||||
R 1048 5028 1 561128.904796871.60 0.00
|
||||
R 1048 5032 1 561210.804796928.90 0.00
|
||||
R 1048 5036 1 561292.804796986.30 0.00
|
||||
R 1048 5040 1 561374.704797043.60 0.00
|
||||
R 1048 5044 1 561456.604797101.00 0.00
|
||||
R 1048 5048 1 561538.504797158.40 0.00
|
||||
R 1048 5052 1 561620.404797215.70 0.00
|
||||
R 1048 5056 1 561702.304797273.10 0.00
|
||||
R 1048 5060 1 561784.204797330.40 0.00
|
||||
R 1048 5064 1 561866.204797387.80 0.00
|
||||
R 1048 5068 1 561948.104797445.10 0.00
|
||||
R 1048 5072 1 562030.004797502.50 0.00
|
||||
R 1048 5076 1 562111.904797559.90 0.00
|
||||
R 1048 5080 1 562193.804797617.20 0.00
|
||||
R 1048 5084 1 562275.704797674.60 0.00
|
||||
R 1048 5088 1 562357.704797731.90 0.00
|
||||
R 1048 5092 1 562439.604797789.30 0.00
|
||||
R 1048 5096 1 562521.504797846.60 0.00
|
||||
R 1060 5000 1 560727.604796224.30 0.00
|
||||
R 1060 5004 1 560809.504796281.70 0.00
|
||||
R 1060 5008 1 560891.404796339.00 0.00
|
||||
R 1060 5012 1 560973.304796396.40 0.00
|
||||
R 1060 5016 1 561055.304796453.70 0.00
|
||||
R 1060 5020 1 561137.204796511.10 0.00
|
||||
R 1060 5024 1 561219.104796568.50 0.00
|
||||
R 1060 5028 1 561301.004796625.80 0.00
|
||||
R 1060 5032 1 561382.904796683.20 0.00
|
||||
R 1060 5036 1 561464.804796740.50 0.00
|
||||
R 1060 5040 1 561546.704796797.90 0.00
|
||||
R 1060 5044 1 561628.704796855.30 0.00
|
||||
R 1060 5048 1 561710.604796912.60 0.00
|
||||
R 1060 5052 1 561792.504796970.00 0.00
|
||||
R 1060 5056 1 561874.404797027.30 0.00
|
||||
R 1060 5060 1 561956.304797084.70 0.00
|
||||
R 1060 5064 1 562038.204797142.00 0.00
|
||||
R 1060 5068 1 562120.104797199.40 0.00
|
||||
R 1060 5072 1 562202.104797256.80 0.00
|
||||
R 1060 5076 1 562284.004797314.10 0.00
|
||||
R 1060 5080 1 562365.904797371.50 0.00
|
||||
R 1060 5084 1 562447.804797428.80 0.00
|
||||
R 1060 5088 1 562529.704797486.20 0.00
|
||||
R 1060 5092 1 562611.604797543.50 0.00
|
||||
R 1060 5096 1 562693.604797600.90 0.00
|
||||
R 1072 5000 1 560899.704795978.60 0.00
|
||||
R 1072 5004 1 560981.604796035.90 0.00
|
||||
R 1072 5008 1 561063.504796093.30 0.00
|
||||
R 1072 5012 1 561145.404796150.60 0.00
|
||||
R 1072 5016 1 561227.304796208.00 0.00
|
||||
R 1072 5020 1 561309.204796265.40 0.00
|
||||
R 1072 5024 1 561391.204796322.70 0.00
|
||||
R 1072 5028 1 561473.104796380.10 0.00
|
||||
R 1072 5032 1 561555.004796437.40 0.00
|
||||
R 1072 5036 1 561636.904796494.80 0.00
|
||||
R 1072 5040 1 561718.804796552.20 0.00
|
||||
R 1072 5044 1 561800.704796609.50 0.00
|
||||
R 1072 5048 1 561882.604796666.90 0.00
|
||||
R 1072 5052 1 561964.604796724.20 0.00
|
||||
R 1072 5056 1 562046.504796781.60 0.00
|
||||
R 1072 5060 1 562128.404796838.90 0.00
|
||||
R 1072 5064 1 562210.304796896.30 0.00
|
||||
R 1072 5068 1 562292.204796953.70 0.00
|
||||
R 1072 5072 1 562374.104797011.00 0.00
|
||||
R 1072 5076 1 562456.104797068.40 0.00
|
||||
R 1072 5080 1 562538.004797125.70 0.00
|
||||
R 1072 5084 1 562619.904797183.10 0.00
|
||||
R 1072 5088 1 562701.804797240.40 0.00
|
||||
R 1072 5092 1 562783.704797297.80 0.00
|
||||
R 1072 5096 1 562865.604797355.20 0.00
|
||||
R 1084 5000 1 561071.704795732.80 0.00
|
||||
R 1084 5004 1 561153.704795790.20 0.00
|
||||
R 1084 5008 1 561235.604795847.50 0.00
|
||||
R 1084 5012 1 561317.504795904.90 0.00
|
||||
R 1084 5016 1 561399.404795962.30 0.00
|
||||
R 1084 5020 1 561481.304796019.60 0.00
|
||||
R 1084 5024 1 561563.204796077.00 0.00
|
||||
R 1084 5028 1 561645.104796134.30 0.00
|
||||
R 1084 5032 1 561727.104796191.70 0.00
|
||||
R 1084 5036 1 561809.004796249.00 0.00
|
||||
R 1084 5040 1 561890.904796306.40 0.00
|
||||
R 1084 5044 1 561972.804796363.80 0.00
|
||||
R 1084 5048 1 562054.704796421.10 0.00
|
||||
R 1084 5052 1 562136.604796478.50 0.00
|
||||
R 1084 5056 1 562218.504796535.80 0.00
|
||||
R 1084 5060 1 562300.504796593.20 0.00
|
||||
R 1084 5064 1 562382.404796650.60 0.00
|
||||
R 1084 5068 1 562464.304796707.90 0.00
|
||||
R 1084 5072 1 562546.204796765.30 0.00
|
||||
R 1084 5076 1 562628.104796822.60 0.00
|
||||
R 1084 5080 1 562710.004796880.00 0.00
|
||||
R 1084 5084 1 562792.004796937.30 0.00
|
||||
R 1084 5088 1 562873.904796994.70 0.00
|
||||
R 1084 5092 1 562955.804797052.10 0.00
|
||||
R 1084 5096 1 563037.704797109.40 0.00
|
||||
7216
data/docs/SeteSxPreplots.s01
Normal file
7216
data/docs/SeteSxPreplots.s01
Normal file
File diff suppressed because it is too large
Load Diff
113
data/docs/campaign_manifest.json
Normal file
113
data/docs/campaign_manifest.json
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"timeline": [
|
||||
{
|
||||
"date": "2020-08-08",
|
||||
"event": "D\u00e9but enregistrements continus OBN",
|
||||
"type": "start"
|
||||
},
|
||||
{
|
||||
"date": "2020-08-15",
|
||||
"event": "D\u00e9ploiement AUV - Zone principale",
|
||||
"type": "operation"
|
||||
},
|
||||
{
|
||||
"date": "2020-08-24",
|
||||
"event": "Premiers fichiers RAW g\u00e9n\u00e9r\u00e9s",
|
||||
"type": "data"
|
||||
},
|
||||
{
|
||||
"date": "2020-09-10",
|
||||
"event": "Phase AUV intensive - Couverture maximale",
|
||||
"type": "operation"
|
||||
},
|
||||
{
|
||||
"date": "2020-09-18",
|
||||
"event": "R\u00e9cup\u00e9ration finale des AUV + OBN",
|
||||
"type": "milestone"
|
||||
},
|
||||
{
|
||||
"date": "2020-09-21",
|
||||
"event": "R\u00e9cup\u00e9ration AUV zones difficiles (Trawler)",
|
||||
"type": "operation"
|
||||
},
|
||||
{
|
||||
"date": "2020-09-22",
|
||||
"event": "Derni\u00e8res zones AUV Remaining",
|
||||
"type": "operation"
|
||||
},
|
||||
{
|
||||
"date": "2020-09-23",
|
||||
"event": "Fin de campagne - Tous \u00e9quipements r\u00e9cup\u00e9r\u00e9s",
|
||||
"type": "end"
|
||||
}
|
||||
],
|
||||
"documents": [
|
||||
{
|
||||
"name": "GUNDALF Array Report",
|
||||
"file": "S320A08a10-GUNDALF array modelling suite - Array report.pdf",
|
||||
"category": "Sources"
|
||||
},
|
||||
{
|
||||
"name": "Acquisition Parameters",
|
||||
"file": "SBGS_Sete_SpiceRack_Acquisition_Parameters_Spreadsheet.xlsx",
|
||||
"category": "Acquisition"
|
||||
},
|
||||
{
|
||||
"name": "Technical Specs OBN",
|
||||
"file": "SBGS Standard Technical Specs for Node Surveys v10.docx",
|
||||
"category": "Specifications"
|
||||
},
|
||||
{
|
||||
"name": "Source Preplots (SPS)",
|
||||
"file": "SeteSxPreplots.s01",
|
||||
"category": "Geometry"
|
||||
},
|
||||
{
|
||||
"name": "Receiver Preplots (SPS)",
|
||||
"file": "SeteRxPreplots.r01",
|
||||
"category": "Geometry"
|
||||
},
|
||||
{
|
||||
"name": "Final PickUp Map",
|
||||
"file": "Final_PickUp_20200918.png",
|
||||
"category": "Maps"
|
||||
}
|
||||
],
|
||||
"snapshots": [
|
||||
{
|
||||
"date": "2020-09-18",
|
||||
"name": "Final_PickUp_918",
|
||||
"file": "Final_PickUp_20200918.png"
|
||||
},
|
||||
{
|
||||
"date": "2020-09-18-Copier(1)",
|
||||
"name": "Final_PickUp_918-Copier(1)",
|
||||
"file": "Final_PickUp_20200918-Copier(1).png"
|
||||
},
|
||||
{
|
||||
"date": "2020-09-22",
|
||||
"name": "Sete_AUV_Remaining_922",
|
||||
"file": "Sete_AUV_Remaining_20200922.png"
|
||||
},
|
||||
{
|
||||
"date": "2020-09-22-Copier(1)",
|
||||
"name": "Sete_AUV_Remaining_922-Copier(1)",
|
||||
"file": "Sete_AUV_Remaining_20200922-Copier(1).png"
|
||||
},
|
||||
{
|
||||
"date": "2020-09-22bis",
|
||||
"name": "Sete_AUV_Remaining_922bis",
|
||||
"file": "Sete_AUV_Remaining_20200922bis.png"
|
||||
},
|
||||
{
|
||||
"date": "2020-09-21",
|
||||
"name": "Sete_AUV_Trawler_921",
|
||||
"file": "Sete_AUV_Trawler_20200921.png"
|
||||
},
|
||||
{
|
||||
"date": "2020-09-22",
|
||||
"name": "Sete_AUV_Trawler_922",
|
||||
"file": "Sete_AUV_Trawler_20200922.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
data/h5/auto_221_145435_b0_rsn80274_seq1_1596898430.h5
Normal file
BIN
data/h5/auto_221_145435_b0_rsn80274_seq1_1596898430.h5
Normal file
Binary file not shown.
BIN
data/h5/auto_224_160921_b12_rsn2221_seq1_1597159766.h5
Normal file
BIN
data/h5/auto_224_160921_b12_rsn2221_seq1_1597159766.h5
Normal file
Binary file not shown.
BIN
data/h5/auto_224_161538_b25_rsn3541_seq1_1597159797.h5
Normal file
BIN
data/h5/auto_224_161538_b25_rsn3541_seq1_1597159797.h5
Normal file
Binary file not shown.
BIN
data/h5/auto_224_162759_b17_rsn21093_seq1_1597159798.h5
Normal file
BIN
data/h5/auto_224_162759_b17_rsn21093_seq1_1597159798.h5
Normal file
Binary file not shown.
BIN
data/h5/auto_225_173539_b103_rsn51079_seq1_1597252375.h5
Normal file
BIN
data/h5/auto_225_173539_b103_rsn51079_seq1_1597252375.h5
Normal file
Binary file not shown.
BIN
data/h5/auto_225_173616_b77_rsn2690_seq1_1597250647.h5
Normal file
BIN
data/h5/auto_225_173616_b77_rsn2690_seq1_1597250647.h5
Normal file
Binary file not shown.
BIN
data/h5/auto_225_173619_b105_rsn4162_seq1_1597252399.h5
Normal file
BIN
data/h5/auto_225_173619_b105_rsn4162_seq1_1597252399.h5
Normal file
Binary file not shown.
BIN
data/h5/auto_225_173619_b56_rsn2189_seq1_1597238700.h5
Normal file
BIN
data/h5/auto_225_173619_b56_rsn2189_seq1_1597238700.h5
Normal file
Binary file not shown.
BIN
data/h5/auto_225_173619_b82_rsn72636_seq1_1597250071.h5
Normal file
BIN
data/h5/auto_225_173619_b82_rsn72636_seq1_1597250071.h5
Normal file
Binary file not shown.
BIN
data/h5/auto_225_173623_b21_rsn12296_seq1_1597227594.h5
Normal file
BIN
data/h5/auto_225_173623_b21_rsn12296_seq1_1597227594.h5
Normal file
Binary file not shown.
438
data/index.json
Executable file
438
data/index.json
Executable file
@@ -0,0 +1,438 @@
|
||||
{
|
||||
"nodes": {
|
||||
"221": {
|
||||
"position": {
|
||||
"easting": 560100.0,
|
||||
"northing": 4795500.0,
|
||||
"depth": 35.0
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"filename": "auto_221_145435_b0_rsn80274_seq1_1596898430.h5",
|
||||
"date": "2020-08-08",
|
||||
"timestamp": 1596898430,
|
||||
"duration_sec": 20.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 0.16
|
||||
}
|
||||
]
|
||||
},
|
||||
"224": {
|
||||
"position": {
|
||||
"easting": 560500.0,
|
||||
"northing": 4795800.0,
|
||||
"depth": 40.0
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"filename": "auto_224_160921_b12_rsn2221_seq1_1597159766.h5",
|
||||
"date": "2020-08-11",
|
||||
"timestamp": 1597159766,
|
||||
"duration_sec": 2390.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 24.44
|
||||
},
|
||||
{
|
||||
"filename": "auto_224_161538_b25_rsn3541_seq1_1597159797.h5",
|
||||
"date": "2020-08-11",
|
||||
"timestamp": 1597159797,
|
||||
"duration_sec": 2740.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 27.69
|
||||
},
|
||||
{
|
||||
"filename": "auto_224_162759_b17_rsn21093_seq1_1597159798.h5",
|
||||
"date": "2020-08-11",
|
||||
"timestamp": 1597159798,
|
||||
"duration_sec": 3480.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 37.62
|
||||
}
|
||||
]
|
||||
},
|
||||
"225": {
|
||||
"position": {
|
||||
"easting": 560900.0,
|
||||
"northing": 4796100.0,
|
||||
"depth": 45.0
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"filename": "auto_225_173539_b103_rsn51079_seq1_1597252375.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597252375,
|
||||
"duration_sec": 1360.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 15.44
|
||||
},
|
||||
{
|
||||
"filename": "auto_225_173616_b77_rsn2690_seq1_1597250647.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597250647,
|
||||
"duration_sec": 3120.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 35.27
|
||||
},
|
||||
{
|
||||
"filename": "auto_225_173619_b105_rsn4162_seq1_1597252399.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597252399,
|
||||
"duration_sec": 1370.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 15.49
|
||||
},
|
||||
{
|
||||
"filename": "auto_225_173619_b56_rsn2189_seq1_1597238700.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597238700,
|
||||
"duration_sec": 15070.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 167.57
|
||||
},
|
||||
{
|
||||
"filename": "auto_225_173619_b82_rsn72636_seq1_1597250071.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597250071,
|
||||
"duration_sec": 3700.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 41.34
|
||||
},
|
||||
{
|
||||
"filename": "auto_225_173623_b21_rsn12296_seq1_1597227594.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597227594,
|
||||
"duration_sec": 26180.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 289.17
|
||||
},
|
||||
{
|
||||
"filename": "auto_225_173623_b95_rsn80274_seq1_1597244605.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597244605,
|
||||
"duration_sec": 9170.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 102.01
|
||||
},
|
||||
{
|
||||
"filename": "auto_225_173630_b65_rsn12358_seq1_1597238705.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597238705,
|
||||
"duration_sec": 15080.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 163.32
|
||||
},
|
||||
{
|
||||
"filename": "auto_225_173634_b2_rsn71476_seq1_1597217439.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597217439,
|
||||
"duration_sec": 36350.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 391.88
|
||||
},
|
||||
{
|
||||
"filename": "auto_225_173634_b50_rsn51848_seq1_1597238697.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597238697,
|
||||
"duration_sec": 15090.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 165.26
|
||||
}
|
||||
]
|
||||
},
|
||||
"228": {
|
||||
"position": {
|
||||
"easting": 561300.0,
|
||||
"northing": 4796400.0,
|
||||
"depth": 42.0
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"filename": "auto_228_063604_b16_rsn5819_seq1_1597419185.h5",
|
||||
"date": "2020-08-14",
|
||||
"timestamp": 1597419185,
|
||||
"duration_sec": 54170.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 567.4
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_063604_b194_rsn3965_seq1_1597411770.h5",
|
||||
"date": "2020-08-14",
|
||||
"timestamp": 1597411770,
|
||||
"duration_sec": 61590.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 691.99
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_063605_b77_rsn2690_seq1_1597256283.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597256283,
|
||||
"duration_sec": 217070.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 2460.93
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_064125_b26_rsn2128_seq1_1597414685.h5",
|
||||
"date": "2020-08-14",
|
||||
"timestamp": 1597414685,
|
||||
"duration_sec": 58990.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 625.23
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_064125_b82_rsn72636_seq1_1597256285.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597256285,
|
||||
"duration_sec": 217390.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 2415.92
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_064126_b56_rsn2189_seq1_1597256285.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597256285,
|
||||
"duration_sec": 217390.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 2479.67
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_064126_b90_rsn2011_seq1_1597430558.h5",
|
||||
"date": "2020-08-14",
|
||||
"timestamp": 1597430558,
|
||||
"duration_sec": 43120.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 469.27
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_064131_b187_rsn80304_seq1_1597408779.h5",
|
||||
"date": "2020-08-14",
|
||||
"timestamp": 1597408779,
|
||||
"duration_sec": 64900.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 727.37
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_064131_b3_rsn4589_seq1_1597419457.h5",
|
||||
"date": "2020-08-14",
|
||||
"timestamp": 1597419457,
|
||||
"duration_sec": 54230.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 608.76
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_064131_b65_rsn12358_seq1_1597256199.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597256199,
|
||||
"duration_sec": 217480.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 2405.57
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_064210_b50_rsn51848_seq1_1597256250.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597256250,
|
||||
"duration_sec": 217470.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 2444.06
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_064212_b29_rsn2648_seq1_1597414690.h5",
|
||||
"date": "2020-08-14",
|
||||
"timestamp": 1597414690,
|
||||
"duration_sec": 59030.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 622.02
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_064254_b21_rsn12296_seq1_1597256218.h5",
|
||||
"date": "2020-08-12",
|
||||
"timestamp": 1597256218,
|
||||
"duration_sec": 217550.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 2398.41
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_064256_b181_rsn6523_seq1_1597432534.h5",
|
||||
"date": "2020-08-14",
|
||||
"timestamp": 1597432534,
|
||||
"duration_sec": 41240.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 451.2
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_175058_b130_rsn3747_seq1_1597513819.h5",
|
||||
"date": "2020-08-15",
|
||||
"timestamp": 1597513819,
|
||||
"duration_sec": 30.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 0.36
|
||||
},
|
||||
{
|
||||
"filename": "auto_228_180741_b130_rsn3747_seq1_1597514438.h5",
|
||||
"date": "2020-08-15",
|
||||
"timestamp": 1597514438,
|
||||
"duration_sec": 420.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 4.71
|
||||
}
|
||||
]
|
||||
},
|
||||
"229": {
|
||||
"position": {
|
||||
"easting": 560700.0,
|
||||
"northing": 4796700.0,
|
||||
"depth": 38.0
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"filename": "auto_229_132551_b128_rsn4049_seq1_1597571215.h5",
|
||||
"date": "2020-08-16",
|
||||
"timestamp": 1597571215,
|
||||
"duration_sec": 3600.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 40.72
|
||||
},
|
||||
{
|
||||
"filename": "auto_229_132815_b128_rsn4049_seq1_1597571215.h5",
|
||||
"date": "2020-08-16",
|
||||
"timestamp": 1597571215,
|
||||
"duration_sec": 3600.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 40.72
|
||||
},
|
||||
{
|
||||
"filename": "auto_229_142731_b21_rsn12296_seq1_1597475138.h5",
|
||||
"date": "2020-08-15",
|
||||
"timestamp": 1597475138,
|
||||
"duration_sec": 112910.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 1156.73
|
||||
}
|
||||
]
|
||||
},
|
||||
"230": {
|
||||
"position": {
|
||||
"easting": 561100.0,
|
||||
"northing": 4795200.0,
|
||||
"depth": 50.0
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"filename": "auto_230_063854_b12_rsn21524_seq1_1597516594.h5",
|
||||
"date": "2020-08-15",
|
||||
"timestamp": 1597516594,
|
||||
"duration_sec": 129730.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 1409.06
|
||||
},
|
||||
{
|
||||
"filename": "auto_230_064032_b10_rsn21475_seq1_1597516582.h5",
|
||||
"date": "2020-08-15",
|
||||
"timestamp": 1597516582,
|
||||
"duration_sec": 129840.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 1425.65
|
||||
},
|
||||
{
|
||||
"filename": "auto_230_070712_b18_rsn80310_seq1_1597497040.h5",
|
||||
"date": "2020-08-15",
|
||||
"timestamp": 1597497040,
|
||||
"duration_sec": 150980.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 1660.22
|
||||
},
|
||||
{
|
||||
"filename": "auto_230_070815_b27_rsn6157_seq1_1597516628.h5",
|
||||
"date": "2020-08-15",
|
||||
"timestamp": 1597516628,
|
||||
"duration_sec": 131460.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 1357.8
|
||||
},
|
||||
{
|
||||
"filename": "auto_230_070838_b16_rsn5819_seq1_1597475191.h5",
|
||||
"date": "2020-08-15",
|
||||
"timestamp": 1597475191,
|
||||
"duration_sec": 172920.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 1813.61
|
||||
},
|
||||
{
|
||||
"filename": "auto_230_073833_b23_rsn3078_seq1_1597498598.h5",
|
||||
"date": "2020-08-15",
|
||||
"timestamp": 1597498598,
|
||||
"duration_sec": 151310.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 1621.03
|
||||
}
|
||||
]
|
||||
},
|
||||
"289": {
|
||||
"position": {
|
||||
"easting": 560300.0,
|
||||
"northing": 4796900.0,
|
||||
"depth": 33.0
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"filename": "auto_289_090651_b0_rsn4410_seq1_1571216697.h5",
|
||||
"date": "2019-10-16",
|
||||
"timestamp": 1571216697,
|
||||
"duration_sec": 30.0,
|
||||
"sample_rate": 500,
|
||||
"channels": 4,
|
||||
"size_mb": 0.26
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"dates": [
|
||||
"2019-10-16",
|
||||
"2020-08-08",
|
||||
"2020-08-11",
|
||||
"2020-08-12",
|
||||
"2020-08-14",
|
||||
"2020-08-15",
|
||||
"2020-08-16"
|
||||
],
|
||||
"generated": "2026-02-05T15:05:56.229887",
|
||||
"total_files": 40
|
||||
}
|
||||
276
frontend_src/App.tsx
Normal file
276
frontend_src/App.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { MapContainer, TileLayer } from 'react-leaflet';
|
||||
import NodeMarkers from './components/NodeMarkers';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import SeismicSection from './SeismicSection';
|
||||
import H5Coverage from './components/H5Coverage';
|
||||
import DataDocumentation from './components/DataDocumentation';
|
||||
import H5Dashboard from './components/H5Dashboard';
|
||||
import CampaignDocs from "./components/CampaignDocs";
|
||||
import { Node, DataWindow } from './types';
|
||||
|
||||
const API_BASE = '/seismic/api';
|
||||
const DEFAULT_CENTER: [number, number] = [43.40, 3.70];
|
||||
|
||||
function App() {
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [dates, setDates] = useState<string[]>([]);
|
||||
const [selectedDate, setSelectedDate] = useState<string>('');
|
||||
const [selectedChannel, setSelectedChannel] = useState<string>('ch0');
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
||||
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||
const [dataWindow, setDataWindow] = useState<DataWindow | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sampleRate, setSampleRate] = useState(200);
|
||||
const [showOnlyWithData, setShowOnlyWithData] = useState(false);
|
||||
const [rmsTimeline, setRmsTimeline] = useState<any>(null);
|
||||
const [allAdcValues, setAllAdcValues] = useState<Record<string, number>>({});
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [playSpeed, setPlaySpeed] = useState(1);
|
||||
const [chatOpen, setChatOpen] = useState(false);
|
||||
const [chatMsg, setChatMsg] = useState('');
|
||||
const [chatHistory, setChatHistory] = useState<any[]>([]);
|
||||
const [showSection, setShowSection] = useState(false);
|
||||
const [showDashboard, setShowDashboard] = useState(false);
|
||||
const [showCampaignDocs, setShowCampaignDocs] = useState(false);
|
||||
const [showDocumentation, setShowDocumentation] = useState(false);
|
||||
const [showCoverage, setShowCoverage] = useState(false);
|
||||
const [migrationStatus, setMigrationStatus] = useState<any>(null);
|
||||
|
||||
// 🔗 Deep-linking: Lire les paramètres URL au démarrage
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
const urlDate = params.get('date');
|
||||
const urlChannel = params.get('channel');
|
||||
const urlTime = params.get('time');
|
||||
const urlNode = params.get('node');
|
||||
|
||||
if (urlDate) setSelectedDate(urlDate);
|
||||
if (urlChannel) setSelectedChannel(urlChannel);
|
||||
if (urlTime) setCurrentTime(parseFloat(urlTime));
|
||||
if (urlNode && nodes.length > 0) {
|
||||
const node = nodes.find(n => n.id === urlNode);
|
||||
if (node) setSelectedNode(node);
|
||||
}
|
||||
}, [nodes]); // Se déclenche quand les nodes sont chargés
|
||||
|
||||
// 🔗 Deep-linking: Synchroniser l'URL quand les états changent
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (selectedDate) params.set('date', selectedDate);
|
||||
if (selectedChannel) params.set('channel', selectedChannel);
|
||||
if (currentTime > 1000000) params.set('time', currentTime.toString());
|
||||
if (selectedNode) params.set('node', selectedNode.id);
|
||||
|
||||
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
||||
window.history.pushState({}, '', newUrl);
|
||||
}, [selectedDate, selectedChannel, currentTime, selectedNode]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/nodes`).then(r => r.json()).then(data => {
|
||||
setNodes(data.nodes || []);
|
||||
setSampleRate(data.sampleRateHz || 200);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/dates`).then(r => r.json()).then(data => {
|
||||
const dateList = data.dates || [];
|
||||
setDates(dateList);
|
||||
if (dateList.length > 0 && !selectedDate) setSelectedDate(dateList[dateList.length - 1]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Polling de la migration
|
||||
useEffect(() => {
|
||||
const checkStatus = () => {
|
||||
fetch(`${API_BASE}/migration-status`)
|
||||
.then(r => r.json())
|
||||
.then(data => setMigrationStatus(data))
|
||||
.catch(() => {});
|
||||
};
|
||||
const interval = setInterval(checkStatus, 5000);
|
||||
checkStatus();
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDate || !selectedChannel) return;
|
||||
setRmsTimeline(null);
|
||||
fetch(`${API_BASE}/rms-timeline?date=${selectedDate}&channel=${selectedChannel}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data && data.nodes && Object.keys(data.nodes).length > 0) {
|
||||
setRmsTimeline(data);
|
||||
const allPts = Object.values(data.nodes).flat() as any[];
|
||||
if (allPts.length > 0) {
|
||||
const minTs = Math.min(...allPts.map(p => p.ts));
|
||||
if (minTs > 1000000) setCurrentTime(minTs);
|
||||
}
|
||||
} else {
|
||||
const ts = new Date(selectedDate).getTime() / 1000;
|
||||
if (!isNaN(ts)) setCurrentTime(ts);
|
||||
}
|
||||
});
|
||||
}, [selectedDate, selectedChannel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying) return;
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTime(prev => prev + (60 * (playSpeed / 5)));
|
||||
}, 200);
|
||||
return () => clearInterval(interval);
|
||||
}, [isPlaying, playSpeed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rmsTimeline || !rmsTimeline.nodes || !currentTime) {
|
||||
setAllAdcValues({});
|
||||
return;
|
||||
}
|
||||
const values: Record<string, number> = {};
|
||||
Object.entries(rmsTimeline.nodes).forEach(([nodeId, dataPoints]) => {
|
||||
const pts = dataPoints as any[];
|
||||
const point = pts.find(p => p.ts >= currentTime) || pts[pts.length - 1];
|
||||
if (point) values[nodeId] = point.rms;
|
||||
});
|
||||
setAllAdcValues(values);
|
||||
}, [currentTime, rmsTimeline]);
|
||||
|
||||
const loadNodeData = useCallback(async () => {
|
||||
if (!selectedNode || !selectedDate || currentTime < 1000000) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/data?node=${selectedNode.id}&date=${selectedDate}&channel=${selectedChannel}&start=${Math.floor(currentTime)}`);
|
||||
const data = await res.json();
|
||||
if (data && data.samples) setDataWindow({ ...data, node: selectedNode.id, date: selectedDate, channel: selectedChannel });
|
||||
else setDataWindow(null);
|
||||
} catch (e) { setDataWindow(null); }
|
||||
setLoading(false);
|
||||
}, [selectedNode, selectedDate, selectedChannel, currentTime]);
|
||||
|
||||
useEffect(() => { loadNodeData(); }, [selectedNode, selectedDate, selectedChannel]);
|
||||
|
||||
const formatTime = (ts: number) => {
|
||||
if (ts < 1000000) return '--:--:--';
|
||||
return new Date(ts * 1000).toISOString().substr(11, 8);
|
||||
};
|
||||
|
||||
const sendChat = async () => {
|
||||
if (!chatMsg) return;
|
||||
const newHist = [...chatHistory, { role: 'user', text: chatMsg }];
|
||||
setChatHistory(newHist);
|
||||
setChatMsg('');
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/chat`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: chatMsg })
|
||||
});
|
||||
const data = await res.json();
|
||||
setChatHistory([...newHist, { role: 'agent', text: data.response }]);
|
||||
} catch (e) { setChatHistory([...newHist, { role: 'agent', text: "Erreur agent." }]); }
|
||||
};
|
||||
|
||||
return (<>
|
||||
<div className="app-container" style={{ display: 'flex', flexDirection: 'column', height: '100vh', width: '100vw', background: '#0f172a', color: '#fff' }}>
|
||||
<header style={{ height: '60px', display: 'flex', alignItems: 'center', padding: '0 20px', background: '#1e293b', borderBottom: '1px solid #334155' }}>
|
||||
<h1 style={{ margin: 0, fontSize: '1.2rem' }}>Seismic Viewer</h1>
|
||||
|
||||
{migrationStatus && migrationStatus.processed_files < migrationStatus.total_files && (
|
||||
<div style={{ marginLeft: '20px', background: '#334155', padding: '5px 10px', borderRadius: '15px', fontSize: '0.75rem', border: '1px solid #4ade80' }}>
|
||||
<span style={{ color: '#4ade80', fontWeight: 'bold' }}>⚡ Migration : </span>
|
||||
{Math.round((migrationStatus.processed_files / migrationStatus.total_files) * 100)}%
|
||||
({migrationStatus.processed_files}/{migrationStatus.total_files})
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.8rem', marginLeft: '20px' }}>
|
||||
Nodes: {nodes.length} | Actifs: {nodes.filter(n=>n.hasDates).length}
|
||||
<button onClick={() => setShowDashboard(true)} style={{ background: '#f59e0b', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold' }}>🎯 Dashboard H5</button>
|
||||
<button onClick={() => setShowCampaignDocs(true)} style={{ background: "#8b5cf6", border: "none", color: "#fff", padding: "8px 16px", borderRadius: "6px", cursor: "pointer", fontWeight: "bold", fontSize: "0.9rem" }}>📚 Documentation Campagne</button>
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: '10px' }}>
|
||||
<button onClick={() => setShowSection(true)} style={{ background: '#8b5cf6', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer' }}>Vue Globale</button>
|
||||
<button onClick={() => setShowDocumentation(true)} style={{ background: '#10b981', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer' }}>📖 Documentation</button>
|
||||
<button onClick={() => setShowCoverage(true)} style={{ background: '#3b82f6', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer' }}>Coverage H5</button>
|
||||
<select value={selectedDate} onChange={e => setSelectedDate(e.target.value)} style={{ background: '#0f172a', color: '#fff', border: '1px solid #334155' }}>
|
||||
{dates.map(d => <option key={d} value={d}>{d}</option>)}
|
||||
</select>
|
||||
<select value={selectedChannel} onChange={e => setSelectedChannel(e.target.value)} style={{ background: '#0f172a', color: '#fff', border: '1px solid #334155' }}>
|
||||
{['ch0', 'ch1', 'ch2', 'ch3'].map(ch => <option key={ch} value={ch}>{ch.toUpperCase()}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div style={{ height: '50px', background: '#0f172a', display: 'flex', alignItems: 'center', padding: '0 20px', gap: '15px', borderBottom: '1px solid #334155' }}>
|
||||
<button onClick={() => setIsPlaying(!isPlaying)} aria-label={isPlaying ? 'Mettre en pause' : 'Lancer la lecture'} style={{ background: isPlaying ? '#ef4444' : '#22c55e', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', fontWeight: 'bold', cursor: 'pointer' }}>
|
||||
{isPlaying ? 'PAUSE' : 'PLAY'}
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '2px', background: '#1e293b', padding: '2px', borderRadius: '4px' }}>
|
||||
{[1, 10, 100, 1000].map(s => (
|
||||
<button key={s} onClick={() => setPlaySpeed(s)} aria-label={`Vitesse ${s}x`} style={{ background: playSpeed === s ? '#3b82f6' : 'transparent', color: '#fff', border: 'none', padding: '2px 8px', fontSize: '0.7rem', cursor: 'pointer' }}>x{s}</button>
|
||||
))}
|
||||
</div>
|
||||
<span style={{ fontFamily: 'monospace', color: '#4ade80', fontSize: '1.1rem', minWidth: '80px' }}>{formatTime(currentTime)}</span>
|
||||
<input type="range" aria-label="Curseur temporel" style={{ flex: 1 }} min={currentTime - 1800} max={currentTime + 1800} value={currentTime} onChange={e => { setCurrentTime(Number(e.target.value)); setIsPlaying(false); }} />
|
||||
</div>
|
||||
|
||||
<main style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
||||
<div style={{ flex: 1, position: 'relative' }}>
|
||||
<MapContainer center={DEFAULT_CENTER} zoom={13} style={{ height: '100%', width: '100%' }}>
|
||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
||||
<NodeMarkers nodes={nodes} selectedNode={selectedNode} onSelectNode={setSelectedNode} adcValues={allAdcValues} showOnlyWithData={showOnlyWithData} />
|
||||
</MapContainer>
|
||||
</div>
|
||||
<Sidebar selectedNode={selectedNode} dataWindow={dataWindow} loading={loading} sampleRate={sampleRate} />
|
||||
</main>
|
||||
|
||||
<SeismicSection nodes={nodes} currentTime={currentTime} channel={selectedChannel} visible={showSection} onClose={() => setShowSection(false)} />
|
||||
|
||||
<div style={{ position: 'fixed', bottom: '20px', right: '20px', zIndex: 10000 }}>
|
||||
{!chatOpen ? (
|
||||
<button onClick={() => setChatOpen(true)} aria-label="Ouvrir le chat assistant" style={{ width: '50px', height: '50px', borderRadius: '25px', background: '#e94560', border: 'none', color: '#fff', fontSize: '20px', cursor: 'pointer', boxShadow: '0 4px 12px rgba(0,0,0,0.3)' }}>💬</button>
|
||||
) : (
|
||||
<div style={{ background: '#1e293b', width: '300px', height: '400px', borderRadius: '8px', display: 'flex', flexDirection: 'column', border: '1px solid #334155', boxShadow: '0 8px 24px rgba(0,0,0,0.4)' }}>
|
||||
<div style={{ padding: '10px', background: '#334155', display: 'flex', justifyContent: 'space-between', borderRadius: '8px 8px 0 0' }}>
|
||||
<span style={{ fontSize: '0.8rem', fontWeight: 'bold' }}>Assistant</span>
|
||||
<button onClick={() => setChatOpen(false)} aria-label="Fermer le chat" style={{ background: 'none', border: 'none', color: '#fff', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{chatHistory.map((m, i) => (
|
||||
<div key={i} style={{ alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start', background: m.role === 'user' ? '#3b82f6' : '#475569', padding: '6px 10px', borderRadius: '8px', fontSize: '0.8rem', maxWidth: '85%' }}>{m.text}</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ padding: '10px', borderTop: '1px solid #334155', display: 'flex', gap: '5px' }}>
|
||||
<input value={chatMsg} onChange={e => setChatMsg(e.target.value)} onKeyPress={e => e.key === 'Enter' && sendChat()} aria-label="Poser une question" style={{ flex: 1, background: '#0f172a', border: '1px solid #334155', color: '#fff', padding: '5px', borderRadius: '4px', fontSize: '0.8rem' }} placeholder="Question…" />
|
||||
<button onClick={sendChat} style={{ background: '#e94560', border: 'none', color: '#fff', padding: '5px 10px', borderRadius: '4px', fontSize: '0.8rem' }}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showDashboard && (
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setShowDashboard(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer Dashboard</button>
|
||||
<H5Dashboard />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showCampaignDocs && (
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
|
||||
<CampaignDocs />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showDocumentation && <DataDocumentation onClose={() => setShowDocumentation(false)} />}
|
||||
{showCoverage && <H5Coverage onClose={() => setShowCoverage(false)} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
346
frontend_src/App.tsx.backup
Normal file
346
frontend_src/App.tsx.backup
Normal file
@@ -0,0 +1,346 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { MapContainer, TileLayer } from 'react-leaflet';
|
||||
import NodeMarkers from './components/NodeMarkers';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import SeismicSection from './SeismicSection';
|
||||
import H5Coverage from './components/H5Coverage';
|
||||
import DataDocumentation from './components/DataDocumentation';
|
||||
import H5Dashboard from './components/H5Dashboard';
|
||||
import CampaignDocs from "./components/CampaignDocs";
|
||||
import { Node, DataWindow } from './types';
|
||||
|
||||
const API_BASE = '/seismic/api';
|
||||
const DEFAULT_CENTER: [number, number] = [43.40, 3.70];
|
||||
|
||||
function App() {
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [dates, setDates] = useState<string[]>([]);
|
||||
const [selectedDate, setSelectedDate] = useState<string>('');
|
||||
const [selectedChannel, setSelectedChannel] = useState<string>('ch0');
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
||||
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||
const [dataWindow, setDataWindow] = useState<DataWindow | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sampleRate, setSampleRate] = useState(200);
|
||||
const [showOnlyWithData, setShowOnlyWithData] = useState(false);
|
||||
const [rmsTimeline, setRmsTimeline] = useState<any>(null);
|
||||
const [allAdcValues, setAllAdcValues] = useState<Record<string, number>>({});
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [playSpeed, setPlaySpeed] = useState(1);
|
||||
const [chatOpen, setChatOpen] = useState(false);
|
||||
const [chatMsg, setChatMsg] = useState('');
|
||||
const [chatHistory, setChatHistory] = useState<any[]>([]);
|
||||
const [showSection, setShowSection] = useState(false);
|
||||
const [showDashboard, setShowDashboard] = useState(false);
|
||||
const [showCampaignDocs, setShowCampaignDocs] = useState(false);
|
||||
const [showDocumentation, setShowDocumentation] = useState(false);
|
||||
const [showCoverage, setShowCoverage] = useState(false);
|
||||
const [migrationStatus, setMigrationStatus] = useState<any>(null);
|
||||
|
||||
// 🔗 Deep-linking: Lire les paramètres URL au démarrage
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
const urlDate = params.get('date');
|
||||
const urlChannel = params.get('channel');
|
||||
const urlTime = params.get('time');
|
||||
|
||||
if (urlDate) setSelectedDate(urlDate);
|
||||
if (urlChannel) setSelectedChannel(urlChannel);
|
||||
if (urlTime) setCurrentTime(parseFloat(urlTime));
|
||||
if (urlNode && nodes.length > 0) {
|
||||
const node = nodes.find(n => n.id === urlNode);
|
||||
if (node) setSelectedNode(node);
|
||||
}
|
||||
}, [nodes]); // Se déclenche quand les nodes sont chargés
|
||||
|
||||
// 🔗 Deep-linking: Synchroniser l'URL quand les états changent
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (selectedDate) params.set('date', selectedDate);
|
||||
if (selectedChannel) params.set('channel', selectedChannel);
|
||||
if (currentTime > 1000000) params.set('time', currentTime.toString());
|
||||
if (selectedNode) params.set('node', selectedNode.id);
|
||||
|
||||
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
||||
window.history.pushState({}, '', newUrl);
|
||||
}, [selectedDate, selectedChannel, currentTime, selectedNode]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/nodes`).then(r => r.json()).then(data => {
|
||||
setNodes(data.nodes || []);
|
||||
setSampleRate(data.sampleRateHz || 200);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/dates`).then(r => r.json()).then(data => {
|
||||
const dateList = data.dates || [];
|
||||
setDates(dateList);
|
||||
if (dateList.length > 0 && !selectedDate) setSelectedDate(dateList[dateList.length - 1]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Polling de la migration
|
||||
useEffect(() => {
|
||||
const checkStatus = () => {
|
||||
fetch(`${API_BASE}/migration-status`)
|
||||
.then(r => r.json())
|
||||
.then(data => setMigrationStatus(data))
|
||||
.catch(() => {});
|
||||
};
|
||||
const interval = setInterval(checkStatus, 5000);
|
||||
checkStatus();
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDate || !selectedChannel) return;
|
||||
setRmsTimeline(null);
|
||||
fetch(`${API_BASE}/rms-timeline?date=${selectedDate}&channel=${selectedChannel}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data && data.nodes && Object.keys(data.nodes).length > 0) {
|
||||
setRmsTimeline(data);
|
||||
const allPts = Object.values(data.nodes).flat() as any[];
|
||||
if (allPts.length > 0) {
|
||||
const minTs = Math.min(...allPts.map(p => p.ts));
|
||||
if (minTs > 1000000) setCurrentTime(minTs);
|
||||
}
|
||||
} else {
|
||||
const ts = new Date(selectedDate).getTime() / 1000;
|
||||
if (!isNaN(ts)) setCurrentTime(ts);
|
||||
}
|
||||
});
|
||||
}, [selectedDate, selectedChannel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying) return;
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTime(prev => prev + (60 * (playSpeed / 5)));
|
||||
}, 200);
|
||||
return () => clearInterval(interval);
|
||||
}, [isPlaying, playSpeed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rmsTimeline || !rmsTimeline.nodes || !currentTime) {
|
||||
setAllAdcValues({});
|
||||
return;
|
||||
}
|
||||
const values: Record<string, number> = {};
|
||||
Object.entries(rmsTimeline.nodes).forEach(([nodeId, dataPoints]) => {
|
||||
const pts = dataPoints as any[];
|
||||
const point = pts.find(p => p.ts >= currentTime) || pts[pts.length - 1];
|
||||
if (point) values[nodeId] = point.rms;
|
||||
});
|
||||
setAllAdcValues(values);
|
||||
}, [currentTime, rmsTimeline]);
|
||||
|
||||
const loadNodeData = useCallback(async () => {
|
||||
if (!selectedNode || !selectedDate || currentTime < 1000000) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/data?node=${selectedNode.id}&date=${selectedDate}&channel=${selectedChannel}&start=${Math.floor(currentTime)}`);
|
||||
const data = await res.json();
|
||||
if (data && data.samples) setDataWindow({ ...data, node: selectedNode.id, date: selectedDate, channel: selectedChannel });
|
||||
else setDataWindow(null);
|
||||
} catch (e) { setDataWindow(null); }
|
||||
setLoading(false);
|
||||
}, [selectedNode, selectedDate, selectedChannel, currentTime]);
|
||||
|
||||
useEffect(() => { loadNodeData(); }, [selectedNode, selectedDate, selectedChannel]);
|
||||
|
||||
const formatTime = (ts: number) => {
|
||||
if (ts < 1000000) return '--:--:--';
|
||||
return new Date(ts * 1000).toISOString().substr(11, 8);
|
||||
};
|
||||
|
||||
const sendChat = async () => {
|
||||
if (!chatMsg) return;
|
||||
const newHist = [...chatHistory, { role: 'user', text: chatMsg }];
|
||||
setChatHistory(newHist);
|
||||
setChatMsg('');
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/chat`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: chatMsg })
|
||||
});
|
||||
const data = await res.json();
|
||||
setChatHistory([...newHist, { role: 'agent', text: data.response }]);
|
||||
} catch (e) { setChatHistory([...newHist, { role: 'agent', text: "Erreur agent." }]); }
|
||||
};
|
||||
|
||||
return (<>
|
||||
<div className="app-container" style={{ display: 'flex', flexDirection: 'column', height: '100vh', width: '100vw', background: '#0f172a', color: '#fff' }}>
|
||||
<header style={{ height: '60px', display: 'flex', alignItems: 'center', padding: '0 20px', background: '#1e293b', borderBottom: '1px solid #334155' }}>
|
||||
<h1 style={{ margin: 0, fontSize: '1.2rem' }}>Seismic Viewer</h1>
|
||||
|
||||
{migrationStatus && migrationStatus.processed_files < migrationStatus.total_files && (
|
||||
<div style={{ marginLeft: '20px', background: '#334155', padding: '5px 10px', borderRadius: '15px', fontSize: '0.75rem', border: '1px solid #4ade80' }}>
|
||||
<span style={{ color: '#4ade80', fontWeight: 'bold' }}>⚡ Migration : </span>
|
||||
{Math.round((migrationStatus.processed_files / migrationStatus.total_files) * 100)}%
|
||||
({migrationStatus.processed_files}/{migrationStatus.total_files})
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.8rem', marginLeft: '20px' }}>
|
||||
Nodes: {nodes.length} | Actifs: {nodes.filter(n=>n.hasDates).length}
|
||||
<button onClick={() => setShowDashboard(true)} style={{ background: '#f59e0b', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold' }}>🎯 Dashboard H5</button>
|
||||
<button onClick={() => setShowCampaignDocs(true)} style={{ background: "#8b5cf6", border: "none", color: "#fff", padding: "8px 16px", borderRadius: "6px", cursor: "pointer", fontWeight: "bold", fontSize: "0.9rem" }}>📚 Documentation Campagne</button>
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: '10px' }}>
|
||||
<button onClick={() => setShowSection(true)} style={{ background: '#8b5cf6', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer' }}>Vue Globale</button>
|
||||
<button onClick={() => setShowDocumentation(true)} style={{ background: '#10b981', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer' }}>📖 Documentation</button>
|
||||
<button onClick={() => setShowCoverage(true)} style={{ background: '#3b82f6', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', cursor: 'pointer' }}>Coverage H5</button>
|
||||
<select value={selectedDate} onChange={e => setSelectedDate(e.target.value)} style={{ background: '#0f172a', color: '#fff', border: '1px solid #334155' }}>
|
||||
{dates.map(d => <option key={d} value={d}>{d}</option>)}
|
||||
</select>
|
||||
<select value={selectedChannel} onChange={e => setSelectedChannel(e.target.value)} style={{ background: '#0f172a', color: '#fff', border: '1px solid #334155' }}>
|
||||
{['ch0', 'ch1', 'ch2', 'ch3'].map(ch => <option key={ch} value={ch}>{ch.toUpperCase()}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div style={{ height: '50px', background: '#0f172a', display: 'flex', alignItems: 'center', padding: '0 20px', gap: '15px', borderBottom: '1px solid #334155' }}>
|
||||
<button onClick={() => setIsPlaying(!isPlaying)} aria-label={isPlaying ? 'Mettre en pause' : 'Lancer la lecture'} style={{ background: isPlaying ? '#ef4444' : '#22c55e', color: '#fff', border: 'none', padding: '5px 15px', borderRadius: '4px', fontWeight: 'bold', cursor: 'pointer' }}>
|
||||
{isPlaying ? 'PAUSE' : 'PLAY'}
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '2px', background: '#1e293b', padding: '2px', borderRadius: '4px' }}>
|
||||
{[1, 10, 100, 1000].map(s => (
|
||||
<button key={s} onClick={() => setPlaySpeed(s)} aria-label={`Vitesse ${s}x`} style={{ background: playSpeed === s ? '#3b82f6' : 'transparent', color: '#fff', border: 'none', padding: '2px 8px', fontSize: '0.7rem', cursor: 'pointer' }}>x{s}</button>
|
||||
))}
|
||||
</div>
|
||||
<span style={{ fontFamily: 'monospace', color: '#4ade80', fontSize: '1.1rem', minWidth: '80px' }}>{formatTime(currentTime)}</span>
|
||||
<input type="range" aria-label="Curseur temporel" style={{ flex: 1 }} min={currentTime - 1800} max={currentTime + 1800} value={currentTime} onChange={e => { setCurrentTime(Number(e.target.value)); setIsPlaying(false); }} />
|
||||
</div>
|
||||
|
||||
<main style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
||||
<div style={{ flex: 1, position: 'relative' }}>
|
||||
<MapContainer center={DEFAULT_CENTER} zoom={13} style={{ height: '100%', width: '100%' }}>
|
||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
||||
<NodeMarkers nodes={nodes} selectedNode={selectedNode} onSelectNode={setSelectedNode} adcValues={allAdcValues} showOnlyWithData={showOnlyWithData} />
|
||||
</MapContainer>
|
||||
</div>
|
||||
<Sidebar selectedNode={selectedNode} dataWindow={dataWindow} loading={loading} sampleRate={sampleRate} />
|
||||
</main>
|
||||
|
||||
<SeismicSection nodes={nodes} currentTime={currentTime} channel={selectedChannel} visible={showSection} onClose={() => setShowSection(false)} />
|
||||
|
||||
<div style={{ position: 'fixed', bottom: '20px', right: '20px', zIndex: 10000 }}>
|
||||
{!chatOpen ? (
|
||||
<button onClick={() => setChatOpen(true)} aria-label="Ouvrir le chat assistant" style={{ width: '50px', height: '50px', borderRadius: '25px', background: '#e94560', border: 'none', color: '#fff', fontSize: '20px', cursor: 'pointer', boxShadow: '0 4px 12px rgba(0,0,0,0.3)' }}>💬</button>
|
||||
) : (
|
||||
<div style={{ background: '#1e293b', width: '300px', height: '400px', borderRadius: '8px', display: 'flex', flexDirection: 'column', border: '1px solid #334155', boxShadow: '0 8px 24px rgba(0,0,0,0.4)' }}>
|
||||
<div style={{ padding: '10px', background: '#334155', display: 'flex', justifyContent: 'space-between', borderRadius: '8px 8px 0 0' }}>
|
||||
<span style={{ fontSize: '0.8rem', fontWeight: 'bold' }}>Assistant</span>
|
||||
<button onClick={() => setChatOpen(false)} aria-label="Fermer le chat" style={{ background: 'none', border: 'none', color: '#fff', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{chatHistory.map((m, i) => (
|
||||
<div key={i} style={{ alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start', background: m.role === 'user' ? '#3b82f6' : '#475569', padding: '6px 10px', borderRadius: '8px', fontSize: '0.8rem', maxWidth: '85%' }}>{m.text}</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ padding: '10px', borderTop: '1px solid #334155', display: 'flex', gap: '5px' }}>
|
||||
<input value={chatMsg} onChange={e => setChatMsg(e.target.value)} onKeyPress={e => e.key === 'Enter' && sendChat()} aria-label="Poser une question" style={{ flex: 1, background: '#0f172a', border: '1px solid #334155', color: '#fff', padding: '5px', borderRadius: '4px', fontSize: '0.8rem' }} placeholder="Question…" />
|
||||
<button onClick={sendChat} style={{ background: '#e94560', border: 'none', color: '#fff', padding: '5px 10px', borderRadius: '4px', fontSize: '0.8rem' }}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showDashboard && (
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setShowDashboard(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer Dashboard</button>
|
||||
<H5Dashboard />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
{showCampaignDocs && (
|
||||
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
|
||||
<CampaignDocs />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: '#f1f5f9', zIndex: 9998, overflow: 'auto' }}>
|
||||
{showCampaignDocs && (
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
|
||||
<CampaignDocs />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ position: 'relative' }}>
|
||||
{showCampaignDocs && (
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
|
||||
<CampaignDocs />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button onClick={() => setShowDashboard(false)} style={{ position: 'fixed', top: '10px', right: '10px', background: '#ef4444', color: 'white', border: 'none', padding: '10px 20px', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold', zIndex: 9999 }}>✕ Fermer Dashboard</button>
|
||||
{showCampaignDocs && (
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
|
||||
<CampaignDocs />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<H5Dashboard />
|
||||
{showCampaignDocs && (
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
|
||||
<CampaignDocs />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showCampaignDocs && (
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
|
||||
<CampaignDocs />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showCampaignDocs && (
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
|
||||
<CampaignDocs />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{showCampaignDocs && (
|
||||
<div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "#f1f5f9", zIndex: 9998, overflow: "auto" }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<button onClick={() => setShowCampaignDocs(false)} style={{ position: "fixed", top: "10px", right: "10px", background: "#ef4444", color: "white", border: "none", padding: "10px 20px", borderRadius: "4px", cursor: "pointer", fontWeight: "bold", zIndex: 9999 }}>✕ Fermer</button>
|
||||
<CampaignDocs />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showDocumentation && <DataDocumentation onClose={() => setShowDocumentation(false)} />}
|
||||
{showCoverage && <H5Coverage onClose={() => setShowCoverage(false)} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
118
frontend_src/NodeMarkers.tsx
Normal file
118
frontend_src/NodeMarkers.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { CircleMarker, Popup, useMap } from 'react-leaflet';
|
||||
import proj4 from 'proj4';
|
||||
import { Node, AdcValues } from '../types';
|
||||
|
||||
// Définition de la projection UTM Zone 31N (WGS84)
|
||||
const UTM31N = "+proj=utm +zone=31 +datum=WGS84 +units=m +no_defs";
|
||||
const WGS84 = "EPSG:4326";
|
||||
|
||||
function utmToLatLon(easting: number, northing: number): [number, number] | null {
|
||||
if (!easting || !northing || isNaN(easting) || isNaN(northing)) return null;
|
||||
try {
|
||||
// Proj4 attend [longitude, latitude] pour le résultat
|
||||
const [lon, lat] = proj4(UTM31N, WGS84, [easting, northing]);
|
||||
return [lat, lon];
|
||||
} catch (e) {
|
||||
console.error('UTM Conversion error:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getMarkerColor(value: number | undefined, maxVal = 500): string {
|
||||
if (value === undefined || value <= 0) return '#444444';
|
||||
const normalized = Math.min(1, Math.sqrt(value) / Math.sqrt(maxVal));
|
||||
if (normalized < 0.3) return `rgb(0, ${Math.round(normalized * 3.3 * 255)}, 255)`;
|
||||
if (normalized < 0.6) return `rgb(0, 255, ${Math.round((1 - (normalized - 0.3) * 3.3) * 255)})`;
|
||||
return `rgb(255, ${Math.round((1 - (normalized - 0.6) * 2.5) * 255)}, 0)`;
|
||||
}
|
||||
|
||||
function getMarkerRadius(value: number | undefined, maxVal = 500): number {
|
||||
if (value === undefined || value <= 0) return 6;
|
||||
const normalized = Math.min(1, Math.sqrt(value) / Math.sqrt(maxVal));
|
||||
return 6 + normalized * 24;
|
||||
}
|
||||
|
||||
interface NodeMarkersProps {
|
||||
nodes: Node[];
|
||||
selectedNode: Node | null;
|
||||
onSelectNode: (node: Node) => void;
|
||||
adcValues: AdcValues;
|
||||
showOnlyWithData?: boolean;
|
||||
}
|
||||
|
||||
function NodeMarkers({ nodes, selectedNode, onSelectNode, adcValues, showOnlyWithData = true }: NodeMarkersProps) {
|
||||
const map = useMap();
|
||||
|
||||
const nodesWithLatLon = useMemo(() => {
|
||||
console.log(`NodeMarkers: Processing ${nodes.length} nodes`);
|
||||
const results = nodes
|
||||
.filter(node => node.position && (!showOnlyWithData || node.hasDates))
|
||||
.map(node => {
|
||||
const coords = utmToLatLon(node.position!.easting, node.position!.northing);
|
||||
if (!coords) return null;
|
||||
return { ...node, lat: coords[0], lon: coords[1] };
|
||||
})
|
||||
.filter((n): n is Node & { lat: number; lon: number } => n !== null);
|
||||
|
||||
console.log(`NodeMarkers: ${results.length} nodes converted to LatLon`);
|
||||
if (results.length > 0) {
|
||||
console.log('Sample node:', results[0].id, results[0].lat, results[0].lon);
|
||||
}
|
||||
return results;
|
||||
}, [nodes, showOnlyWithData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nodesWithLatLon.length === 0) return;
|
||||
try {
|
||||
const lats = nodesWithLatLon.map(n => n.lat);
|
||||
const lons = nodesWithLatLon.map(n => n.lon);
|
||||
const bounds: [[number, number], [number, number]] = [
|
||||
[Math.min(...lats), Math.min(...lons)],
|
||||
[Math.max(...lats), Math.max(...lons)]
|
||||
];
|
||||
map.fitBounds(bounds, { padding: [50, 50] });
|
||||
} catch (e) {
|
||||
console.error('fitBounds error:', e);
|
||||
}
|
||||
}, [nodesWithLatLon, map]);
|
||||
|
||||
const currentMax = useMemo(() => {
|
||||
const vals = Object.values(adcValues).filter(v => v > 0);
|
||||
return vals.length > 0 ? Math.max(...vals) : 500;
|
||||
}, [adcValues]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CircleMarker center={[43.40, 3.70]} radius={10} pathOptions={{ color: 'yellow', fillColor: 'yellow', fillOpacity: 1 }}>
|
||||
<Popup>DEBUG: Centre de Sète</Popup>
|
||||
</CircleMarker>
|
||||
{nodesWithLatLon.map(node => {
|
||||
const adcValue = adcValues[node.id];
|
||||
const isSelected = selectedNode?.id === node.id;
|
||||
return (
|
||||
<CircleMarker
|
||||
key={node.id}
|
||||
center={[node.lat, node.lon]}
|
||||
radius={getMarkerRadius(adcValue, currentMax)}
|
||||
pathOptions={{
|
||||
fillColor: getMarkerColor(adcValue, currentMax),
|
||||
fillOpacity: 0.85,
|
||||
color: isSelected ? '#ff0000' : '#ffffff',
|
||||
weight: isSelected ? 4 : 1,
|
||||
}}
|
||||
eventHandlers={{ click: () => onSelectNode(node) }}
|
||||
>
|
||||
<Popup>
|
||||
<strong>Node {node.id}</strong><br/>
|
||||
E: {node.position?.easting.toFixed(0)} N: {node.position?.northing.toFixed(0)}<br/>
|
||||
RMS: {adcValue?.toFixed(2) || 'N/A'}
|
||||
</Popup>
|
||||
</CircleMarker>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NodeMarkers;
|
||||
113
frontend_src/SeismicSection.tsx
Normal file
113
frontend_src/SeismicSection.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import Plot from 'react-plotly.js';
|
||||
|
||||
interface SeismicSectionProps {
|
||||
nodes: any[];
|
||||
currentTime: number;
|
||||
channel: string;
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const API_BASE = '/seismic/api';
|
||||
|
||||
function SeismicSection({ nodes, currentTime, channel, visible, onClose }: SeismicSectionProps) {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [viewMode, setViewMode] = useState<'day' | 'global'>('day');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
setLoading(true);
|
||||
|
||||
const url = viewMode === 'global'
|
||||
? `${API_BASE}/global-history?channel=${channel}`
|
||||
: `${API_BASE}/rms-timeline?date=${new Date(currentTime * 1000).toISOString().split('T')[0]}&channel=${channel}`;
|
||||
|
||||
fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
setData(d.nodes);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
}, [visible, viewMode, channel, currentTime]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => document.removeEventListener('keydown', handleEscape);
|
||||
}, [visible, onClose]);
|
||||
|
||||
const plotData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
const traces: any[] = [];
|
||||
const nodeIds = Object.keys(data).sort();
|
||||
|
||||
nodeIds.forEach((id, index) => {
|
||||
const points = data[id];
|
||||
if (!points || points.length === 0) return;
|
||||
|
||||
const maxRms = Math.max(...points.map((p: any) => p.rms)) || 1;
|
||||
|
||||
traces.push({
|
||||
x: points.map((p: any) => new Date(p.ts * 1000)),
|
||||
y: points.map((p: any) => (p.rms / maxRms) * 0.9 + index),
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: `Node ${id}`,
|
||||
line: { color: '#4ade80', width: 1 },
|
||||
fill: 'tonexty',
|
||||
showlegend: false
|
||||
});
|
||||
});
|
||||
return traces;
|
||||
}, [data]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', top: '0', left: '0', right: '0', bottom: '0', background: '#0f172a', zIndex: 3000, display: 'flex', flexDirection: 'column', overflowBehavior: 'contain' }}>
|
||||
<div style={{ padding: '15px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#1e293b' }}>
|
||||
<div>
|
||||
<h2 style={{ margin: 0, display: 'inline', marginRight: '20px' }}>Vue Sismique Globale</h2>
|
||||
<div style={{ display: 'inline-flex', gap: '5px', background: '#0f172a', padding: '3px', borderRadius: '4px' }}>
|
||||
<button onClick={() => setViewMode('day')} style={{ background: viewMode === 'day' ? '#3b82f6' : 'transparent', color: '#fff', border: 'none', padding: '5px 15px', cursor: 'pointer' }}>Journée</button>
|
||||
<button onClick={() => setViewMode('global')} style={{ background: viewMode === 'global' ? '#3b82f6' : 'transparent', color: '#fff', border: 'none', padding: '5px 15px', cursor: 'pointer' }}>Toute la période (12j)</button>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} style={{ background: '#ef4444', color: '#fff', border: 'none', padding: '8px 20px', borderRadius: '4px', cursor: 'pointer' }}>Fermer</button>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, padding: '10px' }}>
|
||||
{loading ? <div style={{ textAlign: 'center', marginTop: '20%' }}>Fusion des données…</div> : (
|
||||
<Plot
|
||||
data={plotData}
|
||||
layout={{
|
||||
autosize: true,
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
margin: { t: 10, b: 50, l: 60, r: 20 },
|
||||
font: { color: '#94a3b8' },
|
||||
xaxis: { gridcolor: '#1e293b', title: 'Temps' },
|
||||
yaxis: {
|
||||
gridcolor: '#1e293b',
|
||||
tickvals: plotData.map((_, i) => i),
|
||||
ticktext: Object.keys(data || {}).sort().map(id => `b${id}`)
|
||||
}
|
||||
}}
|
||||
config={{ responsive: true }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeismicSection;
|
||||
127
frontend_src/Sidebar.tsx
Normal file
127
frontend_src/Sidebar.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import Plot from 'react-plotly.js';
|
||||
import { Node, DataWindow } from '../types';
|
||||
|
||||
interface SidebarProps {
|
||||
selectedNode: Node | null;
|
||||
dataWindow: DataWindow | null;
|
||||
loading: boolean;
|
||||
sampleRate: number;
|
||||
}
|
||||
|
||||
type ViewMode = 'waveform' | 'rms';
|
||||
|
||||
function Sidebar({ selectedNode, dataWindow, loading, sampleRate }: SidebarProps) {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('waveform');
|
||||
const [showRaw, setShowRaw] = useState(false);
|
||||
|
||||
const samples = dataWindow?.samples || [];
|
||||
const startTs = dataWindow?.startTimestamp || 0;
|
||||
|
||||
// Calcul RMS glissant
|
||||
const rmsData = useMemo(() => {
|
||||
if (samples.length === 0) return [];
|
||||
const windowSize = 20;
|
||||
const result = [];
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const start = Math.max(0, i - windowSize);
|
||||
const window = samples.slice(start, i + 1);
|
||||
const rms = Math.sqrt(window.reduce((a, b) => a + b*b, 0) / window.length);
|
||||
result.push(rms);
|
||||
}
|
||||
return result;
|
||||
}, [samples]);
|
||||
|
||||
// Formatter pour le X (DD hh:mm:ss)
|
||||
const formatXAxis = (ts: number) => {
|
||||
const d = new Date(ts * 1000);
|
||||
const day = d.getDate().toString().padStart(2, '0');
|
||||
const h = d.getHours().toString().padStart(2, '0');
|
||||
const m = d.getMinutes().toString().padStart(2, '0');
|
||||
const s = d.getSeconds().toString().padStart(2, '0');
|
||||
return `${day} ${h}:${m}:${s}`;
|
||||
};
|
||||
|
||||
const xValues = useMemo(() => {
|
||||
return samples.map((_, i) => formatXAxis(startTs + i / sampleRate));
|
||||
}, [samples, startTs, sampleRate]);
|
||||
|
||||
const plotData: any[] = [];
|
||||
if (samples.length > 0) {
|
||||
if (viewMode === 'waveform') {
|
||||
plotData.push({
|
||||
x: xValues,
|
||||
y: samples,
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: 'ADC Brute',
|
||||
line: { color: '#4ade80', width: 1.5 }
|
||||
});
|
||||
} else {
|
||||
plotData.push({
|
||||
x: xValues,
|
||||
y: rmsData,
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
name: 'RMS',
|
||||
line: { color: '#fbbf24', width: 2 }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="sidebar" style={{ width: '450px', background: '#1e293b', borderLeft: '1px solid #334155', display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{ padding: '20px', borderBottom: '1px solid #334155' }}>
|
||||
<h2 style={{ margin: 0, color: '#f8fafc' }}>Analyse Node</h2>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '20px' }}>
|
||||
{selectedNode ? (
|
||||
<>
|
||||
<div style={{ marginBottom: '20px', background: '#0f172a', padding: '15px', borderRadius: '8px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0', color: '#38bdf8' }}>Node b{selectedNode.id}</h3>
|
||||
<p style={{ margin: '5px 0', fontSize: '0.9rem' }}>E: {selectedNode.position?.easting.toFixed(0)} N: {selectedNode.position?.northing.toFixed(0)}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '5px', marginBottom: '15px' }}>
|
||||
<button onClick={() => setViewMode('waveform')} style={{ flex: 1, padding: '8px', background: viewMode === 'waveform' ? '#3b82f6' : '#334155', border: 'none', color: '#fff', borderRadius: '4px', cursor: 'pointer' }}>Waveform (Brute)</button>
|
||||
<button onClick={() => setViewMode('rms')} style={{ flex: 1, padding: '8px', background: viewMode === 'rms' ? '#3b82f6' : '#334155', border: 'none', color: '#fff', borderRadius: '4px', cursor: 'pointer' }}>RMS</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#94a3b8' }}>Chargement…</div>
|
||||
) : samples.length > 0 ? (
|
||||
<>
|
||||
<div style={{ background: '#000', borderRadius: '8px', overflow: 'hidden', height: '300px' }}>
|
||||
<Plot
|
||||
data={plotData}
|
||||
layout={{
|
||||
autosize: true,
|
||||
height: 300,
|
||||
margin: { t: 10, b: 60, l: 50, r: 10 },
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
font: { color: '#94a3b8', size: 10 },
|
||||
xaxis: {
|
||||
gridcolor: '#1e293b',
|
||||
zerolinecolor: '#334155',
|
||||
tickangle: -45,
|
||||
nticks: 5
|
||||
},
|
||||
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155', title: 'Amplitude' }
|
||||
}}
|
||||
config={{ responsive: true, displayModeBar: true }}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
{/* Stats... */}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
241
frontend_src/components/CampaignDocs.tsx
Normal file
241
frontend_src/components/CampaignDocs.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface TimelineEvent {
|
||||
date: string;
|
||||
event: string;
|
||||
type: 'start' | 'data' | 'milestone' | 'operation' | 'end';
|
||||
}
|
||||
|
||||
interface Document {
|
||||
name: string;
|
||||
file: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
interface CampaignManifest {
|
||||
timeline: TimelineEvent[];
|
||||
documents: Document[];
|
||||
}
|
||||
|
||||
export default function CampaignDocs() {
|
||||
const [manifest, setManifest] = useState<CampaignManifest | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'timeline' | 'docs'>('timeline');
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/seismic/api/docs/manifest')
|
||||
.then(r => r.json())
|
||||
.then(setManifest)
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
if (!manifest) return <div style={{ padding: '20px', color: '#64748b' }}>Chargement...</div>;
|
||||
|
||||
const typeColors: Record<TimelineEvent['type'], string> = {
|
||||
start: '#10b981',
|
||||
data: '#3b82f6',
|
||||
milestone: '#f59e0b',
|
||||
operation: '#8b5cf6',
|
||||
end: '#ef4444'
|
||||
};
|
||||
|
||||
const typeIcons: Record<TimelineEvent['type'], string> = {
|
||||
start: '🚀',
|
||||
data: '📊',
|
||||
milestone: '🎯',
|
||||
operation: '⚙️',
|
||||
end: '🏁'
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<h1 style={{ fontSize: '2rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '10px' }}>
|
||||
📚 Campagne SeaKESP - Sète 2020
|
||||
</h1>
|
||||
<p style={{ color: '#64748b', fontSize: '1.1rem' }}>
|
||||
Documentation, chronologie et ressources de la campagne OBN
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{ display: 'flex', gap: '10px', marginBottom: '20px', borderBottom: '2px solid #e2e8f0' }}>
|
||||
<button
|
||||
onClick={() => setActiveTab('timeline')}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: activeTab === 'timeline' ? '#3b82f6' : 'transparent',
|
||||
color: activeTab === 'timeline' ? 'white' : '#64748b',
|
||||
border: 'none',
|
||||
borderRadius: '8px 8px 0 0',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '1rem'
|
||||
}}
|
||||
>
|
||||
📅 Chronologie
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('docs')}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: activeTab === 'docs' ? '#3b82f6' : 'transparent',
|
||||
color: activeTab === 'docs' ? 'white' : '#64748b',
|
||||
border: 'none',
|
||||
borderRadius: '8px 8px 0 0',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '1rem'
|
||||
}}
|
||||
>
|
||||
📄 Documents
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Timeline Tab */}
|
||||
{activeTab === 'timeline' && (
|
||||
<div style={{ background: 'white', padding: '30px', borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '20px' }}>
|
||||
Chronologie de la campagne
|
||||
</h2>
|
||||
|
||||
<div style={{ position: 'relative', paddingLeft: '40px' }}>
|
||||
{/* Timeline line */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: '15px',
|
||||
top: '10px',
|
||||
bottom: '10px',
|
||||
width: '3px',
|
||||
background: 'linear-gradient(to bottom, #10b981, #ef4444)'
|
||||
}} />
|
||||
|
||||
{manifest.timeline.map((event, idx) => (
|
||||
<div key={idx} style={{ position: 'relative', marginBottom: '25px' }}>
|
||||
{/* Timeline dot */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: '-30px',
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
borderRadius: '50%',
|
||||
background: typeColors[event.type],
|
||||
border: '3px solid white',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.2)'
|
||||
}} />
|
||||
|
||||
{/* Event card */}
|
||||
<div style={{
|
||||
background: '#f8fafc',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
border: `2px solid ${typeColors[event.type]}`,
|
||||
marginLeft: '10px'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '5px' }}>
|
||||
<span style={{ fontSize: '1.5rem' }}>{typeIcons[event.type]}</span>
|
||||
<span style={{ fontSize: '0.9rem', fontWeight: 'bold', color: typeColors[event.type] }}>
|
||||
{event.date}
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ fontSize: '1rem', color: '#334155', margin: 0 }}>
|
||||
{event.event}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div style={{ marginTop: '30px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '15px' }}>
|
||||
<div style={{ background: '#f0f9ff', padding: '15px', borderRadius: '8px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#3b82f6' }}>46 jours</div>
|
||||
<div style={{ color: '#64748b', fontSize: '0.9rem' }}>Durée totale</div>
|
||||
</div>
|
||||
<div style={{ background: '#f0fdf4', padding: '15px', borderRadius: '8px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#10b981' }}>345 fichiers</div>
|
||||
<div style={{ color: '#64748b', fontSize: '0.9rem' }}>RAW collectés</div>
|
||||
</div>
|
||||
<div style={{ background: '#fef3c7', padding: '15px', borderRadius: '8px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#f59e0b' }}>~800h</div>
|
||||
<div style={{ color: '#64748b', fontSize: '0.9rem' }}>Enregistrements</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Documents Tab */}
|
||||
{activeTab === 'docs' && (
|
||||
<div style={{ background: 'white', padding: '30px', borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '20px' }}>
|
||||
Documents de référence
|
||||
</h2>
|
||||
|
||||
{['Sources', 'Acquisition', 'Specifications', 'Geometry', 'Maps'].map(category => {
|
||||
const categoryDocs = manifest.documents.filter(d => d.category === category);
|
||||
if (categoryDocs.length === 0) return null;
|
||||
|
||||
const categoryIcons: Record<string, string> = {
|
||||
Sources: '🎺',
|
||||
Acquisition: '📋',
|
||||
Specifications: '📖',
|
||||
Geometry: '🗺️',
|
||||
Maps: '🗺️'
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={category} style={{ marginBottom: '25px' }}>
|
||||
<h3 style={{ fontSize: '1.2rem', fontWeight: 'bold', color: '#475569', marginBottom: '10px', display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<span>{categoryIcons[category]}</span>
|
||||
{category}
|
||||
</h3>
|
||||
<div style={{ display: 'grid', gap: '10px' }}>
|
||||
{categoryDocs.map((doc, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={`/seismic/api/docs/${doc.file}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '15px',
|
||||
padding: '15px',
|
||||
background: '#f8fafc',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: '8px',
|
||||
textDecoration: 'none',
|
||||
color: '#334155',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#f1f5f9';
|
||||
e.currentTarget.style.borderColor = '#3b82f6';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#f8fafc';
|
||||
e.currentTarget.style.borderColor = '#e2e8f0';
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '1.5rem' }}>
|
||||
{doc.file.endsWith('.pdf') ? '📄' :
|
||||
doc.file.endsWith('.xlsx') || doc.file.endsWith('.xlsm') ? '📊' :
|
||||
doc.file.endsWith('.docx') ? '📝' :
|
||||
doc.file.endsWith('.png') ? '🖼️' : '📁'}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '3px' }}>{doc.name}</div>
|
||||
<div style={{ fontSize: '0.85rem', color: '#64748b' }}>{doc.file}</div>
|
||||
</div>
|
||||
<div style={{ color: '#3b82f6', fontSize: '1.2rem' }}>→</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
206
frontend_src/components/DataDocumentation.tsx
Normal file
206
frontend_src/components/DataDocumentation.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function DataDocumentation({ onClose }: Props) {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'channels' | 'pipeline' | 'conversion' | 'format'>('overview');
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview' as const, label: 'Vue d\'ensemble', icon: '📋' },
|
||||
{ id: 'channels' as const, label: 'Canaux', icon: '📊' },
|
||||
{ id: 'pipeline' as const, label: 'Pipeline', icon: '🔄' },
|
||||
{ id: 'conversion' as const, label: 'Conversion', icon: '⚙️' },
|
||||
{ id: 'format' as const, label: 'Format H5', icon: '📦' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', zIndex: 10000, display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '20px' }}>
|
||||
<div style={{ background: 'white', borderRadius: '12px', maxWidth: '900px', width: '100%', maxHeight: '90vh', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Header */}
|
||||
<div style={{ padding: '20px', borderBottom: '2px solid #e2e8f0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1e293b', margin: 0 }}>
|
||||
Documentation Technique
|
||||
</h2>
|
||||
<button onClick={onClose} style={{ background: '#ef4444', color: 'white', border: 'none', padding: '8px 16px', borderRadius: '6px', cursor: 'pointer', fontWeight: 'bold' }}>
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{ display: 'flex', borderBottom: '2px solid #e2e8f0', overflowX: 'auto' }}>
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
background: activeTab === tab.id ? '#3b82f6' : 'transparent',
|
||||
color: activeTab === tab.id ? 'white' : '#64748b',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === tab.id ? '3px solid #2563eb' : '3px solid transparent',
|
||||
cursor: 'pointer',
|
||||
fontWeight: activeTab === tab.id ? 'bold' : 'normal',
|
||||
fontSize: '0.9rem',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{tab.icon} {tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '30px' }}>
|
||||
{activeTab === 'overview' && (
|
||||
<div>
|
||||
<h3 style={{ fontSize: '1.3rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '15px' }}>Campagne SeaKESP - Sète 2020</h3>
|
||||
<p style={{ fontSize: '1rem', color: '#475569', marginBottom: '20px' }}>
|
||||
Campagne d'acquisition sismique Ocean Bottom Node (OBN) réalisée en août-septembre 2020.
|
||||
</p>
|
||||
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#334155', marginTop: '20px', marginBottom: '10px' }}>Données acquises</h4>
|
||||
<ul style={{ color: '#475569', lineHeight: '1.8' }}>
|
||||
<li><strong>345 fichiers RAW</strong> propriétaires (2.7 TB)</li>
|
||||
<li><strong>Durée totale :</strong> ~800 heures d'enregistrements continus</li>
|
||||
<li><strong>Plus long fichier :</strong> 60 heures (217k secondes)</li>
|
||||
<li><strong>Fréquence d'échantillonnage :</strong> 500 Hz</li>
|
||||
</ul>
|
||||
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#334155', marginTop: '20px', marginBottom: '10px' }}>État de conversion</h4>
|
||||
<p style={{ color: '#475569' }}>
|
||||
Conversion RAW → H5 en cours sur VM .81 (4 workers parallèles).
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'channels' && (
|
||||
<div>
|
||||
<h3 style={{ fontSize: '1.3rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '15px' }}>Canaux d'acquisition</h3>
|
||||
|
||||
<div style={{ marginBottom: '25px' }}>
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#0369a1', marginBottom: '10px' }}>Géophones (Canaux 1-3)</h4>
|
||||
<p style={{ color: '#475569', marginBottom: '10px' }}>Capteurs de vitesse particulaire 3 composantes (X, Y, Z)</p>
|
||||
<ul style={{ color: '#475569', lineHeight: '1.8' }}>
|
||||
<li><strong>Type :</strong> Géophones 15.6 V/(m/s)</li>
|
||||
<li><strong>Unité finale :</strong> m/s (mètres par seconde)</li>
|
||||
<li><strong>Sensibilité ADC :</strong> 3.576e-7 V/bit</li>
|
||||
<li><strong>Calibration :</strong> m/s = (ADC × 3.576e-7) / 15.6</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#7c2d12', marginBottom: '10px' }}>Hydrophone (Canal 4)</h4>
|
||||
<p style={{ color: '#475569', marginBottom: '10px' }}>Capteur de pression acoustique</p>
|
||||
<ul style={{ color: '#475569', lineHeight: '1.8' }}>
|
||||
<li><strong>Type :</strong> Hydrophone 8.9 V/bar</li>
|
||||
<li><strong>Unité finale :</strong> Pa (Pascals)</li>
|
||||
<li><strong>Sensibilité ADC :</strong> 2.841e-6 V/bit</li>
|
||||
<li><strong>Calibration :</strong> Pa = (ADC × 2.841e-6 / 8.9) × 100000</li>
|
||||
<li><strong>Note :</strong> 1 bar = 100000 Pa</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'pipeline' && (
|
||||
<div>
|
||||
<h3 style={{ fontSize: '1.3rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '15px' }}>Pipeline de traitement</h3>
|
||||
|
||||
<div style={{ background: '#f8fafc', padding: '20px', borderRadius: '8px', marginBottom: '20px' }}>
|
||||
<h4 style={{ fontSize: '1rem', fontWeight: 'bold', color: '#475569', marginBottom: '15px' }}>Étapes de conversion</h4>
|
||||
<ol style={{ color: '#475569', lineHeight: '2', paddingLeft: '20px' }}>
|
||||
<li><strong>Fichier source :</strong> RAW propriétaire (format binaire non documenté)</li>
|
||||
<li><strong>Conversion initiale :</strong> RAW → SEGY via MantaSegy</li>
|
||||
<li><strong>Parsing SEGY :</strong> Extraction headers + données ADC brutes</li>
|
||||
<li><strong>Calibration :</strong> Application formules ADC → unités physiques</li>
|
||||
<li><strong>Export H5 :</strong> Sauvegarde données brutes + calibrées</li>
|
||||
<li><strong>Validation :</strong> Vérification précision (erreur < 10⁻¹¹)</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#334155', marginTop: '20px', marginBottom: '10px' }}>Objectif du traitement</h4>
|
||||
<p style={{ color: '#475569' }}>Convertir les fichiers RAW propriétaires en format HDF5 standardisé avec :</p>
|
||||
<ul style={{ color: '#475569', lineHeight: '1.8' }}>
|
||||
<li>Données ADC brutes (int32) pour archivage</li>
|
||||
<li>Données calibrées en unités physiques (float32)</li>
|
||||
<li>Métadonnées complètes (sample rate, durée, calibration)</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'conversion' && (
|
||||
<div>
|
||||
<h3 style={{ fontSize: '1.3rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '15px' }}>Détails de conversion</h3>
|
||||
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#334155', marginBottom: '10px' }}>Configuration actuelle</h4>
|
||||
<ul style={{ color: '#475569', lineHeight: '1.8' }}>
|
||||
<li><strong>Machine :</strong> VM .81 (osboxes@192.168.0.81)</li>
|
||||
<li><strong>Workers :</strong> 4 processus parallèles</li>
|
||||
<li><strong>Stockage source :</strong> NFS depuis Pi 52 (/mnt/seismic-data)</li>
|
||||
<li><strong>Stockage destination :</strong> Local VM puis rsync vers Pi 27</li>
|
||||
</ul>
|
||||
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#334155', marginTop: '20px', marginBottom: '10px' }}>Formules de calibration</h4>
|
||||
<div style={{ background: '#f1f5f9', padding: '15px', borderRadius: '8px', fontFamily: 'monospace', fontSize: '0.9rem' }}>
|
||||
<p style={{ color: '#0369a1', marginBottom: '10px' }}><strong>Géophones (canaux 1-3) :</strong></p>
|
||||
<p style={{ color: '#475569' }}>m/s = (ADC_value × 3.576e-7 V/bit) / (15.6 V/(m/s))</p>
|
||||
|
||||
<p style={{ color: '#7c2d12', marginTop: '15px', marginBottom: '10px' }}><strong>Hydrophone (canal 4) :</strong></p>
|
||||
<p style={{ color: '#475569' }}>Pa = (ADC_value × 2.841e-6 V/bit / 8.9 V/bar) × 100000 Pa/bar</p>
|
||||
</div>
|
||||
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#334155', marginTop: '20px', marginBottom: '10px' }}>Validation qualité</h4>
|
||||
<p style={{ color: '#475569' }}>
|
||||
Précision de conversion vérifiée : erreur < 10⁻¹¹ entre calcul Python et valeurs attendues.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'format' && (
|
||||
<div>
|
||||
<h3 style={{ fontSize: '1.3rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '15px' }}>Structure fichier H5</h3>
|
||||
|
||||
<div style={{ background: '#0f172a', padding: '20px', borderRadius: '8px', color: '#e2e8f0', fontFamily: 'monospace', fontSize: '0.85rem', marginBottom: '20px' }}>
|
||||
<pre style={{ margin: 0 }}>{`/
|
||||
├── metadata (group)
|
||||
│ ├── @duration_sec: 20.0
|
||||
│ ├── @sample_rate_hz: 500
|
||||
│ ├── @n_channels: 4
|
||||
│ └── @n_samples: 10000
|
||||
│
|
||||
├── calibration (group)
|
||||
│ ├── @geophone_v_per_bit: 3.576e-7
|
||||
│ ├── @geophone_v_per_ms: 15.6
|
||||
│ ├── @hydrophone_v_per_bit: 2.841e-6
|
||||
│ └── @hydrophone_v_per_bar: 8.9
|
||||
│
|
||||
├── raw_data (group)
|
||||
│ ├── channel_1 (dataset int32)
|
||||
│ ├── channel_2 (dataset int32)
|
||||
│ ├── channel_3 (dataset int32)
|
||||
│ └── channel_4 (dataset int32)
|
||||
│
|
||||
└── calibrated_data (group)
|
||||
├── channel_1 (dataset float32, unit: m/s)
|
||||
├── channel_2 (dataset float32, unit: m/s)
|
||||
├── channel_3 (dataset float32, unit: m/s)
|
||||
└── channel_4 (dataset float32, unit: Pa)`}</pre>
|
||||
</div>
|
||||
|
||||
<h4 style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#334155', marginBottom: '10px' }}>Avantages du format H5</h4>
|
||||
<ul style={{ color: '#475569', lineHeight: '1.8' }}>
|
||||
<li><strong>Compression efficace :</strong> Réduction ~30% taille fichiers</li>
|
||||
<li><strong>Accès rapide :</strong> Lecture sélective par canal/plage temporelle</li>
|
||||
<li><strong>Métadonnées intégrées :</strong> Toutes les infos de calibration dans le fichier</li>
|
||||
<li><strong>Interopérabilité :</strong> Compatible Python, MATLAB, Julia, C++</li>
|
||||
<li><strong>Archivage :</strong> Données brutes + calibrées dans un seul fichier</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
151
frontend_src/components/H5Coverage.tsx
Normal file
151
frontend_src/components/H5Coverage.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const API_BASE = '/seismic/api';
|
||||
|
||||
interface CoverageStats {
|
||||
total_positions: number;
|
||||
with_data: number;
|
||||
with_aux: number;
|
||||
total_files: number;
|
||||
coverage_pct: number;
|
||||
missing: number;
|
||||
}
|
||||
|
||||
interface Gap {
|
||||
start: number;
|
||||
end: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
function H5Coverage({ onClose }: { onClose: () => void }) {
|
||||
const [stats, setStats] = useState<CoverageStats | null>(null);
|
||||
const [gaps, setGaps] = useState<Gap[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
fetch(`${API_BASE}/h5/coverage`).then(r => r.json()),
|
||||
fetch(`${API_BASE}/h5/gaps`).then(r => r.json())
|
||||
]).then(([statsData, gapsData]) => {
|
||||
setStats(statsData);
|
||||
setGaps(gapsData.gaps.sort((a: Gap, b: Gap) => b.length - a.length));
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: '#0f172a', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff' }}>
|
||||
<div>Chargement des statistiques H5…</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: '#0f172a', zIndex: 3000, display: 'flex', flexDirection: 'column', overflowY: 'auto' }}>
|
||||
<div style={{ padding: '20px', background: '#1e293b', borderBottom: '1px solid #334155', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h2 style={{ margin: 0, color: '#f8fafc' }}>📊 H5 Data Coverage</h2>
|
||||
<button onClick={onClose} style={{ background: '#ef4444', color: '#fff', border: 'none', padding: '8px 20px', borderRadius: '4px', cursor: 'pointer' }}>Fermer</button>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, padding: '20px', maxWidth: '1200px', margin: '0 auto', width: '100%' }}>
|
||||
{/* Stats globales */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '15px', marginBottom: '30px' }}>
|
||||
<div style={{ background: '#1e293b', padding: '20px', borderRadius: '8px', border: '1px solid #334155' }}>
|
||||
<div style={{ fontSize: '0.8rem', color: '#94a3b8', marginBottom: '5px' }}>Positions totales</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#f8fafc' }}>{stats?.total_positions}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#1e293b', padding: '20px', borderRadius: '8px', border: '1px solid #334155' }}>
|
||||
<div style={{ fontSize: '0.8rem', color: '#94a3b8', marginBottom: '5px' }}>Avec données</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#4ade80' }}>{stats?.with_data}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#1e293b', padding: '20px', borderRadius: '8px', border: '1px solid #334155' }}>
|
||||
<div style={{ fontSize: '0.8rem', color: '#94a3b8', marginBottom: '5px' }}>Manquantes</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#ef4444' }}>{stats?.missing}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#1e293b', padding: '20px', borderRadius: '8px', border: '1px solid #334155' }}>
|
||||
<div style={{ fontSize: '0.8rem', color: '#94a3b8', marginBottom: '5px' }}>Coverage</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: stats && stats.coverage_pct > 50 ? '#4ade80' : '#fbbf24' }}>{stats?.coverage_pct}%</div>
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#1e293b', padding: '20px', borderRadius: '8px', border: '1px solid #334155' }}>
|
||||
<div style={{ fontSize: '0.8rem', color: '#94a3b8', marginBottom: '5px' }}>Fichiers totaux</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#38bdf8' }}>{stats?.total_files}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div style={{ background: '#1e293b', padding: '20px', borderRadius: '8px', border: '1px solid #334155', marginBottom: '30px' }}>
|
||||
<div style={{ fontSize: '0.9rem', color: '#94a3b8', marginBottom: '10px' }}>Progression du déploiement</div>
|
||||
<div style={{ background: '#0f172a', height: '30px', borderRadius: '15px', overflow: 'hidden', position: 'relative' }}>
|
||||
<div style={{
|
||||
background: 'linear-gradient(90deg, #4ade80, #22c55e)',
|
||||
height: '100%',
|
||||
width: `${stats?.coverage_pct}%`,
|
||||
transition: 'width 1s ease'
|
||||
}}></div>
|
||||
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', color: '#fff', fontWeight: 'bold', fontSize: '0.9rem' }}>
|
||||
{stats?.with_data} / {stats?.total_positions} positions
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top 10 gaps */}
|
||||
<div style={{ background: '#1e293b', padding: '20px', borderRadius: '8px', border: '1px solid #334155' }}>
|
||||
<h3 style={{ margin: '0 0 15px 0', color: '#f8fafc' }}>🔍 Top 10 Gaps (plages manquantes)</h3>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{gaps.slice(0, 10).map((gap, i) => (
|
||||
<div key={i} style={{ background: '#0f172a', padding: '12px', borderRadius: '6px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<span style={{ color: '#f8fafc', fontWeight: 'bold' }}>b{gap.start}</span>
|
||||
<span style={{ color: '#94a3b8', margin: '0 8px' }}>→</span>
|
||||
<span style={{ color: '#f8fafc', fontWeight: 'bold' }}>b{gap.end}</span>
|
||||
</div>
|
||||
<div style={{
|
||||
background: gap.length > 50 ? '#dc2626' : gap.length > 20 ? '#f59e0b' : '#3b82f6',
|
||||
color: '#fff',
|
||||
padding: '4px 12px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{gap.length} positions
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{gaps.length > 10 && (
|
||||
<div style={{ marginTop: '15px', textAlign: 'center', color: '#94a3b8', fontSize: '0.85rem' }}>
|
||||
… et {gaps.length - 10} autres gaps
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Verdict */}
|
||||
<div style={{
|
||||
background: stats && stats.coverage_pct < 50 ? '#7f1d1d' : '#065f46',
|
||||
padding: '20px',
|
||||
borderRadius: '8px',
|
||||
marginTop: '30px',
|
||||
border: stats && stats.coverage_pct < 50 ? '1px solid #dc2626' : '1px solid #10b981'
|
||||
}}>
|
||||
<div style={{ fontSize: '1.1rem', fontWeight: 'bold', color: '#fff', marginBottom: '10px' }}>
|
||||
{stats && stats.coverage_pct < 50 ? '⚠️ Déploiement partiel détecté' : '✅ Coverage acceptable'}
|
||||
</div>
|
||||
<div style={{ color: '#e5e7eb', fontSize: '0.9rem' }}>
|
||||
{stats && stats.coverage_pct < 50
|
||||
? `Seulement ${stats.coverage_pct}% des positions planifiées ont des données. Cela suggère un test de déploiement plutôt qu'une collecte complète.`
|
||||
: `${stats?.coverage_pct}% des positions déployées avec succès.`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default H5Coverage;
|
||||
246
frontend_src/components/H5Dashboard.tsx
Normal file
246
frontend_src/components/H5Dashboard.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface H5File {
|
||||
filename: string;
|
||||
nodeId: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
interface WaveformData {
|
||||
samples: number[];
|
||||
sample_rate: number;
|
||||
duration_sec: number;
|
||||
channel: number;
|
||||
channel_name: string;
|
||||
unit: string;
|
||||
stats: {
|
||||
min: number;
|
||||
max: number;
|
||||
mean: number;
|
||||
std: number;
|
||||
rms: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default function H5Dashboard() {
|
||||
const [files, setFiles] = useState<H5File[]>([]);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
const [waveformData, setWaveformData] = useState<WaveformData | null>(null);
|
||||
const [channel, setChannel] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [startTime, setStartTime] = useState(0);
|
||||
const [duration, setDuration] = useState(10);
|
||||
const [globalView, setGlobalView] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/seismic/api/h5/files')
|
||||
.then(r => r.json())
|
||||
.then(data => setFiles(data.files || []))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const loadWaveform = async (file: string, ch: number, start: number, dur: number, global: boolean = false) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
file,
|
||||
channel: String(ch),
|
||||
start: String(start),
|
||||
duration: global ? '0' : String(dur)
|
||||
});
|
||||
const response = await fetch(`/seismic/api/h5/data?${params}`);
|
||||
const data = await response.json();
|
||||
setWaveformData(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (file: string) => {
|
||||
setSelectedFile(file);
|
||||
setGlobalView(false);
|
||||
loadWaveform(file, channel, startTime, duration, false);
|
||||
};
|
||||
|
||||
const handleChannelChange = (ch: number) => {
|
||||
setChannel(ch);
|
||||
if (selectedFile) loadWaveform(selectedFile, ch, startTime, duration, globalView);
|
||||
};
|
||||
|
||||
const handleGlobalView = () => {
|
||||
if (selectedFile) {
|
||||
setGlobalView(true);
|
||||
loadWaveform(selectedFile, channel, 0, 0, true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', maxWidth: '1400px', margin: '0 auto' }}>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<h1 style={{ fontSize: '2rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '10px' }}>
|
||||
🎯 Dashboard H5 - Visualisation Waveforms
|
||||
</h1>
|
||||
<p style={{ color: '#64748b', fontSize: '1.1rem' }}>
|
||||
{files.length} fichiers H5 calibrés disponibles
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '300px 1fr', gap: '20px' }}>
|
||||
{/* Sidebar - Liste des fichiers */}
|
||||
<div style={{ background: 'white', padding: '20px', borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', maxHeight: '80vh', overflowY: 'auto' }}>
|
||||
<h2 style={{ fontSize: '1.2rem', fontWeight: 'bold', color: '#1e293b', marginBottom: '15px' }}>
|
||||
📁 Fichiers H5
|
||||
</h2>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{files.map((file, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => handleFileSelect(file.filename)}
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
padding: '12px',
|
||||
background: selectedFile === file.filename ? '#3b82f6' : '#f8fafc',
|
||||
color: selectedFile === file.filename ? 'white' : '#334155',
|
||||
border: selectedFile === file.filename ? '2px solid #2563eb' : '1px solid #e2e8f0',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.85rem',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedFile !== file.filename) {
|
||||
e.currentTarget.style.background = '#f1f5f9';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedFile !== file.filename) {
|
||||
e.currentTarget.style.background = '#f8fafc';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>Node {file.nodeId}</div>
|
||||
<div style={{ fontSize: '0.75rem', opacity: 0.8 }}>{file.date}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content - Waveform */}
|
||||
<div style={{ background: 'white', padding: '20px', borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
|
||||
{!selectedFile && (
|
||||
<div style={{ textAlign: 'center', padding: '60px', color: '#64748b' }}>
|
||||
<div style={{ fontSize: '3rem', marginBottom: '20px' }}>📊</div>
|
||||
<p style={{ fontSize: '1.2rem' }}>Sélectionnez un fichier H5 pour visualiser les waveforms</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedFile && (
|
||||
<>
|
||||
{/* Controls */}
|
||||
<div style={{ marginBottom: '20px', display: 'flex', gap: '15px', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: '0.9rem', fontWeight: 'bold', color: '#475569', display: 'block', marginBottom: '5px' }}>
|
||||
Canal :
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '5px' }}>
|
||||
{[1, 2, 3, 4].map(ch => (
|
||||
<button
|
||||
key={ch}
|
||||
onClick={() => handleChannelChange(ch)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: channel === ch ? '#3b82f6' : '#f1f5f9',
|
||||
color: channel === ch ? 'white' : '#334155',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{ch <= 3 ? `Geo ${ch}` : 'Hydro'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleGlobalView}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: globalView ? '#10b981' : '#8b5cf6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: loading ? 'wait' : 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
🌍 Vue Globale
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Waveform Display */}
|
||||
{loading && (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#64748b' }}>
|
||||
<div style={{ fontSize: '2rem', marginBottom: '10px' }}>⏳</div>
|
||||
<p>Chargement des données...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{waveformData && !loading && (
|
||||
<div>
|
||||
{/* Info bar */}
|
||||
<div style={{ background: '#f8fafc', padding: '15px', borderRadius: '8px', marginBottom: '20px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '10px' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#64748b', marginBottom: '3px' }}>Canal</div>
|
||||
<div style={{ fontWeight: 'bold', color: '#1e293b' }}>{waveformData.channel_name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#64748b', marginBottom: '3px' }}>Unité</div>
|
||||
<div style={{ fontWeight: 'bold', color: '#1e293b' }}>{waveformData.unit}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#64748b', marginBottom: '3px' }}>Durée</div>
|
||||
<div style={{ fontWeight: 'bold', color: '#1e293b' }}>{waveformData.duration_sec.toFixed(1)} s</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#64748b', marginBottom: '3px' }}>Échantillons</div>
|
||||
<div style={{ fontWeight: 'bold', color: '#1e293b' }}>{waveformData.samples.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))', gap: '10px', marginBottom: '20px' }}>
|
||||
{Object.entries(waveformData.stats).map(([key, value]) => (
|
||||
<div key={key} style={{ background: '#f0f9ff', padding: '10px', borderRadius: '6px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '0.75rem', color: '#0369a1', marginBottom: '3px', textTransform: 'uppercase' }}>{key}</div>
|
||||
<div style={{ fontWeight: 'bold', color: '#0c4a6e', fontSize: '0.9rem' }}>{value.toExponential(3)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Waveform visualization (simple text representation for now) */}
|
||||
<div style={{ background: '#0f172a', padding: '20px', borderRadius: '8px', color: '#94a3b8', fontFamily: 'monospace', fontSize: '0.75rem', overflowX: 'auto' }}>
|
||||
<div style={{ marginBottom: '10px', color: '#f1f5f9', fontWeight: 'bold' }}>
|
||||
Waveform Preview (premiers échantillons) :
|
||||
</div>
|
||||
<pre style={{ margin: 0 }}>
|
||||
{waveformData.samples.slice(0, 50).map((v, i) =>
|
||||
`[${i.toString().padStart(4, '0')}] ${v.toExponential(4)} ${waveformData.unit}`
|
||||
).join('\n')}
|
||||
{waveformData.samples.length > 50 && `\n... (${waveformData.samples.length - 50} échantillons restants)`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
325
h5_api_server.py
Normal file
325
h5_api_server.py
Normal file
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Flask API server for SeaKESP H5 data and node metadata
|
||||
Provides both H5 data access and node/date endpoints
|
||||
"""
|
||||
|
||||
from flask import Flask, jsonify, request, send_file
|
||||
from flask_cors import CORS
|
||||
import h5py
|
||||
import numpy as np
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# Paths
|
||||
DATA_DIR = Path("/data")
|
||||
H5_DIR = DATA_DIR / "h5"
|
||||
DOCS_DIR = DATA_DIR / "docs"
|
||||
INDEX_FILE = DATA_DIR / "index.json"
|
||||
|
||||
# Load node index
|
||||
nodes_data = {}
|
||||
dates_list = []
|
||||
|
||||
def load_index():
|
||||
"""Load node data from index.json"""
|
||||
global nodes_data, dates_list
|
||||
|
||||
if not INDEX_FILE.exists():
|
||||
print(f"Warning: {INDEX_FILE} not found")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(INDEX_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
nodes_data = data.get('nodes', {})
|
||||
dates_list = data.get('dates', [])
|
||||
print(f"Loaded {len(nodes_data)} nodes and {len(dates_list)} dates from index.json")
|
||||
except Exception as e:
|
||||
print(f"Error loading index.json: {e}")
|
||||
|
||||
# Load index on startup
|
||||
load_index()
|
||||
|
||||
# ============================================================================
|
||||
# Node & Date Endpoints (replacing Node.js backend)
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/api/nodes', methods=['GET'])
|
||||
def get_nodes():
|
||||
"""Return list of all nodes with metadata"""
|
||||
nodes_list = []
|
||||
for node_id, node_info in nodes_data.items():
|
||||
nodes_list.append({
|
||||
'id': node_id,
|
||||
'position': node_info.get('position', {}),
|
||||
'file_count': len(node_info.get('files', []))
|
||||
})
|
||||
return jsonify(nodes_list)
|
||||
|
||||
@app.route('/api/dates', methods=['GET'])
|
||||
def get_dates():
|
||||
"""Return list of available dates"""
|
||||
return jsonify(dates_list)
|
||||
|
||||
@app.route('/api/migration-status', methods=['GET'])
|
||||
def migration_status():
|
||||
"""Return H5 migration status summary"""
|
||||
h5_count = len(list(H5_DIR.glob("*.h5"))) if H5_DIR.exists() else 0
|
||||
|
||||
return jsonify({
|
||||
'total_h5_files': h5_count,
|
||||
'nodes_count': len(nodes_data),
|
||||
'dates_count': len(dates_list),
|
||||
'last_updated': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
@app.route('/api/rms-timeline', methods=['GET'])
|
||||
def rms_timeline():
|
||||
"""Placeholder for RMS timeline (requires processing)"""
|
||||
return jsonify([])
|
||||
|
||||
@app.route('/api/global-history', methods=['GET'])
|
||||
def global_history():
|
||||
"""Placeholder for global history (requires processing)"""
|
||||
return jsonify([])
|
||||
|
||||
# ============================================================================
|
||||
# H5 Data Endpoints
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/api/h5/files', methods=['GET'])
|
||||
def list_h5_files():
|
||||
"""List all H5 files with metadata"""
|
||||
if not H5_DIR.exists():
|
||||
return jsonify([])
|
||||
|
||||
files = []
|
||||
for h5_file in sorted(H5_DIR.glob("*.h5")):
|
||||
try:
|
||||
with h5py.File(h5_file, 'r') as f:
|
||||
metadata = dict(f['metadata'].attrs)
|
||||
|
||||
# Extract node ID and date from filename
|
||||
# Format: node_NODEID_YYYYMMDD.h5
|
||||
parts = h5_file.stem.split('_')
|
||||
node_id = parts[1] if len(parts) > 1 else "unknown"
|
||||
date_str = parts[2] if len(parts) > 2 else "unknown"
|
||||
|
||||
files.append({
|
||||
'filename': h5_file.name,
|
||||
'path': str(h5_file),
|
||||
'nodeId': node_id,
|
||||
'date': date_str,
|
||||
'duration_sec': float(metadata.get('duration_sec', 0)),
|
||||
'sample_rate_hz': int(metadata.get('sample_rate_hz', 500)),
|
||||
'n_channels': int(metadata.get('n_channels', 4)),
|
||||
'n_samples': int(metadata.get('n_samples', 0))
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error reading {h5_file}: {e}")
|
||||
|
||||
return jsonify(files)
|
||||
|
||||
@app.route('/api/h5/data', methods=['GET'])
|
||||
def get_h5_data():
|
||||
"""Get waveform data from H5 file"""
|
||||
filename = request.args.get('file')
|
||||
channel = request.args.get('channel', 'channel_1')
|
||||
start = float(request.args.get('start', 0))
|
||||
duration = float(request.args.get('duration', 10))
|
||||
|
||||
if not filename:
|
||||
return jsonify({'error': 'Missing file parameter'}), 400
|
||||
|
||||
h5_path = DATA_DIR / filename
|
||||
|
||||
if not h5_path.exists():
|
||||
return jsonify({'error': 'File not found'}), 404
|
||||
|
||||
try:
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
# Get metadata
|
||||
sample_rate = int(f['metadata'].attrs['sample_rate_hz'])
|
||||
|
||||
# Calculate sample range
|
||||
start_sample = int(start * sample_rate)
|
||||
n_samples = int(duration * sample_rate)
|
||||
end_sample = start_sample + n_samples
|
||||
|
||||
# Read calibrated data
|
||||
data = f['calibrated_data'][channel][start_sample:end_sample]
|
||||
|
||||
# Calculate statistics
|
||||
stats = {
|
||||
'mean': float(np.mean(data)),
|
||||
'std': float(np.std(data)),
|
||||
'min': float(np.min(data)),
|
||||
'max': float(np.max(data)),
|
||||
'rms': float(np.sqrt(np.mean(data**2)))
|
||||
}
|
||||
|
||||
# Create time array
|
||||
time = np.arange(len(data)) / sample_rate + start
|
||||
|
||||
return jsonify({
|
||||
'time': time.tolist(),
|
||||
'data': data.tolist(),
|
||||
'stats': stats,
|
||||
'sample_rate': sample_rate,
|
||||
'channel': channel,
|
||||
'start': start,
|
||||
'duration': duration,
|
||||
'n_samples': len(data)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/h5/coverage', methods=['GET'])
|
||||
def h5_coverage():
|
||||
"""Get H5 coverage summary by node and date"""
|
||||
if not H5_DIR.exists():
|
||||
return jsonify([])
|
||||
|
||||
coverage = {}
|
||||
|
||||
for h5_file in H5_DIR.glob("*.h5"):
|
||||
try:
|
||||
parts = h5_file.stem.split('_')
|
||||
node_id = parts[1] if len(parts) > 1 else "unknown"
|
||||
date_str = parts[2] if len(parts) > 2 else "unknown"
|
||||
|
||||
key = f"{node_id}_{date_str}"
|
||||
|
||||
with h5py.File(h5_file, 'r') as f:
|
||||
duration = float(f['metadata'].attrs.get('duration_sec', 0))
|
||||
|
||||
if key not in coverage:
|
||||
coverage[key] = {
|
||||
'nodeId': node_id,
|
||||
'date': date_str,
|
||||
'total_duration_hours': 0,
|
||||
'file_count': 0
|
||||
}
|
||||
|
||||
coverage[key]['total_duration_hours'] += duration / 3600
|
||||
coverage[key]['file_count'] += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing {h5_file}: {e}")
|
||||
|
||||
return jsonify(list(coverage.values()))
|
||||
|
||||
@app.route('/api/h5/gaps', methods=['GET'])
|
||||
def h5_gaps():
|
||||
"""Identify gaps in H5 data coverage"""
|
||||
# TODO: Implement gap detection logic
|
||||
return jsonify([])
|
||||
|
||||
# ============================================================================
|
||||
# Campaign Documentation Endpoints
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/api/docs/manifest', methods=['GET'])
|
||||
def get_docs_manifest():
|
||||
"""Get campaign documentation manifest"""
|
||||
manifest_path = DOCS_DIR / "campaign_manifest.json"
|
||||
|
||||
if not manifest_path.exists():
|
||||
return jsonify({'error': 'Manifest not found'}), 404
|
||||
|
||||
try:
|
||||
with open(manifest_path, 'r') as f:
|
||||
manifest = json.load(f)
|
||||
return jsonify(manifest)
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/docs/<filename>', methods=['GET'])
|
||||
def download_doc(filename):
|
||||
"""Download a campaign document"""
|
||||
doc_path = DOCS_DIR / filename
|
||||
|
||||
if not doc_path.exists():
|
||||
return jsonify({'error': 'Document not found'}), 404
|
||||
|
||||
return send_file(doc_path, as_attachment=True)
|
||||
|
||||
# ============================================================================
|
||||
# Health Check
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""Health check endpoint"""
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'h5_files': len(list(H5_DIR.glob("*.h5"))) if H5_DIR.exists() else 0,
|
||||
'nodes_loaded': len(nodes_data),
|
||||
'dates_loaded': len(dates_list)
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=3004, debug=False, threaded=True)
|
||||
|
||||
# === Additional routes for frontend compatibility ===
|
||||
|
||||
@app.route('/api/files', methods=['GET'])
|
||||
def list_files_alias():
|
||||
"""Alias for /api/h5/files — used by the SeiSee frontend"""
|
||||
return list_h5_files()
|
||||
|
||||
@app.route('/api/file/<path:filename>', methods=['GET'])
|
||||
def get_file_info(filename):
|
||||
"""Get detailed file info including datasets — needed for channel selector"""
|
||||
h5_path = DATA_DIR / filename
|
||||
if not h5_path.exists():
|
||||
return jsonify({'error': 'File not found'}), 404
|
||||
|
||||
try:
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
metadata = dict(f['metadata'].attrs) if 'metadata' in f else {}
|
||||
calibration = dict(f['calibration'].attrs) if 'calibration' in f else {}
|
||||
|
||||
datasets = []
|
||||
def collect_datasets(name, obj):
|
||||
if isinstance(obj, h5py.Dataset):
|
||||
datasets.append({
|
||||
'path': name,
|
||||
'shape': list(obj.shape),
|
||||
'dtype': str(obj.dtype),
|
||||
'chunks': list(obj.chunks) if obj.chunks else None,
|
||||
'compression': obj.compression
|
||||
})
|
||||
f.visititems(collect_datasets)
|
||||
|
||||
duration_sec = float(metadata.get('duration_sec', 0))
|
||||
sample_rate = int(metadata.get('sample_rate_hz', 500))
|
||||
n_channels = int(metadata.get('n_channels', 4))
|
||||
n_samples = int(metadata.get('n_samples', 0))
|
||||
|
||||
# Human readable duration
|
||||
hours = int(duration_sec // 3600)
|
||||
mins = int((duration_sec % 3600) // 60)
|
||||
duration_human = f'{hours}h{mins:02d}' if hours else f'{mins}min'
|
||||
|
||||
return jsonify({
|
||||
'filename': filename,
|
||||
'type': 'h5',
|
||||
'duration_sec': duration_sec,
|
||||
'duration_human': duration_human,
|
||||
'sample_rate_hz': sample_rate,
|
||||
'num_channels': n_channels,
|
||||
'samples_per_channel': n_samples,
|
||||
'datasets': datasets,
|
||||
'calibration': {k: float(v) if hasattr(v, '__float__') else str(v) for k, v in calibration.items()},
|
||||
'metadata': {k: float(v) if hasattr(v, '__float__') else str(v) for k, v in metadata.items()}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
326
h5_api_server_fixed.py
Normal file
326
h5_api_server_fixed.py
Normal file
@@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env python3
|
||||
from flask import Flask, jsonify, request
|
||||
from flask_cors import CORS
|
||||
import json
|
||||
import h5py
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# Load index once at startup
|
||||
with open('/data/index.json', 'r') as f:
|
||||
INDEX = json.load(f)
|
||||
|
||||
|
||||
def to_python_type(val):
|
||||
"""Convert numpy types to Python types for JSON serialization"""
|
||||
if hasattr(val, 'item'):
|
||||
return val.item()
|
||||
return val
|
||||
|
||||
H5_DIR = Path('/data/h5')
|
||||
|
||||
@app.route('/api/nodes', methods=['GET'])
|
||||
def get_nodes():
|
||||
nodes_list = []
|
||||
for node_id, node_info in INDEX['nodes'].items():
|
||||
file_count = len(node_info.get('files', []))
|
||||
nodes_list.append({
|
||||
'id': node_id,
|
||||
'position': node_info.get('position', {}),
|
||||
'file_count': file_count,
|
||||
'hasDates': file_count > 0
|
||||
})
|
||||
return jsonify({
|
||||
'nodes': nodes_list,
|
||||
'sampleRateHz': 500
|
||||
})
|
||||
|
||||
@app.route('/api/dates', methods=['GET'])
|
||||
def get_dates():
|
||||
return jsonify({'dates': INDEX['dates']})
|
||||
|
||||
@app.route('/api/migration-status', methods=['GET'])
|
||||
def get_migration_status():
|
||||
"""Status de la conversion RAW -> H5"""
|
||||
total_files = 345 # Connu du projet
|
||||
h5_files = list(H5_DIR.glob('*.h5'))
|
||||
converted = len(h5_files)
|
||||
|
||||
return jsonify({
|
||||
'summary': {
|
||||
'total_files': total_files,
|
||||
'converted_files': converted,
|
||||
'percentage': round(converted / total_files * 100, 1),
|
||||
'status': 'in_progress' if converted < total_files else 'complete'
|
||||
},
|
||||
'h5_files_available': converted
|
||||
})
|
||||
|
||||
@app.route('/api/rms-timeline', methods=['GET'])
|
||||
def get_rms_timeline():
|
||||
"""Timeline RMS optimisé - utilise l'INDEX pour filtrer par date"""
|
||||
date = request.args.get('date')
|
||||
channel = request.args.get('channel', 'ch0')
|
||||
window_sec = int(request.args.get('window', 60))
|
||||
|
||||
if not date:
|
||||
return jsonify({'error': 'date parameter required'}), 400
|
||||
|
||||
channel_num = int(channel.replace('ch', '').replace('CH', ''))
|
||||
nodes_data = {}
|
||||
|
||||
# Utiliser l'INDEX pour trouver les fichiers de cette date
|
||||
for node_id, node_info in INDEX['nodes'].items():
|
||||
for file_info in node_info.get('files', []):
|
||||
if file_info.get('date') != date:
|
||||
continue
|
||||
|
||||
filename = file_info.get('filename')
|
||||
h5_path = H5_DIR / filename
|
||||
if not h5_path.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
sample_rate = to_python_type(f['metadata'].attrs.get('sample_rate_hz', 500))
|
||||
start_ts = file_info.get('timestamp', 0)
|
||||
|
||||
dataset_name = f'calibrated_data/channel_{channel_num + 1}'
|
||||
if dataset_name not in f:
|
||||
continue
|
||||
|
||||
dataset = f[dataset_name]
|
||||
total_samples = dataset.shape[0]
|
||||
window_samples = int(window_sec * sample_rate)
|
||||
|
||||
# Calculer RMS par fenêtre (max 100 points par fichier)
|
||||
timeline = []
|
||||
step = max(window_samples, total_samples // 100)
|
||||
for i in range(0, min(total_samples, step * 100), step):
|
||||
end_idx = min(i + window_samples, total_samples)
|
||||
chunk = dataset[i:end_idx]
|
||||
rms = float(np.sqrt(np.mean(chunk ** 2)))
|
||||
ts = start_ts + (i / sample_rate)
|
||||
timeline.append({'ts': ts, 'rms': rms})
|
||||
|
||||
if node_id not in nodes_data:
|
||||
nodes_data[node_id] = []
|
||||
nodes_data[node_id].extend(timeline)
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
return jsonify({
|
||||
'date': date,
|
||||
'channel': channel,
|
||||
'nodes': nodes_data
|
||||
})
|
||||
|
||||
@app.route('/api/h5/data', methods=['GET'])
|
||||
@app.route('/api/data', methods=['GET'])
|
||||
def get_waveform_data():
|
||||
"""Données waveform - accepte soit file= soit node=+date="""
|
||||
filename = request.args.get('file')
|
||||
node_id = request.args.get('node')
|
||||
date = request.args.get('date')
|
||||
channel = request.args.get('channel', 'ch0')
|
||||
start = float(request.args.get('start', 0))
|
||||
duration = float(request.args.get('duration', 10))
|
||||
|
||||
# Trouver le fichier H5
|
||||
h5_file = None
|
||||
if filename:
|
||||
# Mode direct: fichier spécifié
|
||||
h5_file = H5_DIR / filename
|
||||
elif node_id and date:
|
||||
# Mode lookup: chercher via node_id et date
|
||||
node_info = INDEX['nodes'].get(node_id)
|
||||
if node_info:
|
||||
for file_info in node_info.get('files', []):
|
||||
if file_info.get('date') == date:
|
||||
h5_file = H5_DIR / file_info.get('filename', '')
|
||||
break
|
||||
else:
|
||||
return jsonify({'error': 'file or (node and date) required'}), 400
|
||||
|
||||
if not h5_file or not h5_file.exists():
|
||||
return jsonify({'error': 'H5 file not found', 'path': str(h5_file)}), 404
|
||||
|
||||
try:
|
||||
with h5py.File(h5_file, 'r') as f:
|
||||
sample_rate = f['metadata'].attrs['sample_rate_hz']
|
||||
channel_num = int(channel.replace('ch', '').replace('CH', ''))
|
||||
|
||||
# Lire les données calibrées
|
||||
dataset = f[f'calibrated_data/channel_{channel_num + 1}']
|
||||
start_sample = int(start * sample_rate)
|
||||
end_sample = int((start + duration) * sample_rate)
|
||||
|
||||
# Limiter à la taille du dataset
|
||||
end_sample = min(end_sample, dataset.shape[0])
|
||||
|
||||
if start_sample >= dataset.shape[0]:
|
||||
return jsonify({'error': 'start time out of range'}), 400
|
||||
|
||||
data = dataset[start_sample:end_sample]
|
||||
|
||||
# Calculer stats
|
||||
rms = float(np.sqrt(np.mean(data ** 2)))
|
||||
peak = float(np.max(np.abs(data)))
|
||||
|
||||
return jsonify({
|
||||
'node_id': node_id,
|
||||
'date': date,
|
||||
'channel': channel,
|
||||
'start': start,
|
||||
'duration': duration,
|
||||
'sample_rate': to_python_type(sample_rate),
|
||||
'data': data.tolist(),
|
||||
'stats': {
|
||||
'rms': rms,
|
||||
'peak': peak,
|
||||
'samples': len(data)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/h5/files', methods=['GET'])
|
||||
def get_h5_files():
|
||||
"""Liste des fichiers H5 disponibles avec métadonnées"""
|
||||
files = []
|
||||
for h5_path in H5_DIR.glob('*.h5'):
|
||||
try:
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
filename = h5_path.name
|
||||
# Parse: auto_228_064210_b50_rsn51848_seq1_1597256250.h5
|
||||
parts = filename.replace('.h5', '').split('_')
|
||||
node_id = parts[1] if len(parts) > 1 else 'unknown'
|
||||
|
||||
# Find date from INDEX
|
||||
date = 'unknown'
|
||||
node_info = INDEX['nodes'].get(node_id)
|
||||
if node_info:
|
||||
for file_info in node_info.get('files', []):
|
||||
if file_info.get('filename') == filename:
|
||||
date = file_info.get('date', 'unknown')
|
||||
break
|
||||
|
||||
files.append({
|
||||
'filename': filename,
|
||||
'nodeId': node_id,
|
||||
'date': date,
|
||||
'size_mb': round(h5_path.stat().st_size / 1024 / 1024, 2),
|
||||
'duration_sec': to_python_type(f['metadata'].attrs.get('duration_sec', 0)),
|
||||
'sample_rate': to_python_type(f['metadata'].attrs.get('sample_rate_hz', 500)),
|
||||
'channels': to_python_type(f['metadata'].attrs.get('n_channels', 4))
|
||||
})
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return jsonify({'files': files, 'count': len(files)})
|
||||
|
||||
@app.route('/api/h5/coverage', methods=['GET'])
|
||||
def get_h5_coverage():
|
||||
"""Matrice de couverture nodes x dates"""
|
||||
coverage = {}
|
||||
for node_id, node_info in INDEX['nodes'].items():
|
||||
node_dates = [f['date'] for f in node_info.get('files', []) if 'date' in f]
|
||||
coverage[node_id] = node_dates
|
||||
|
||||
return jsonify({
|
||||
'coverage': coverage,
|
||||
'total_nodes': len(coverage),
|
||||
'total_dates': len(INDEX['dates'])
|
||||
})
|
||||
|
||||
@app.route('/api/h5/gaps', methods=['GET'])
|
||||
def get_h5_gaps():
|
||||
"""Identify gaps in H5 data coverage per node"""
|
||||
gaps = []
|
||||
|
||||
for node_id, node_info in INDEX['nodes'].items():
|
||||
files = sorted(node_info.get('files', []), key=lambda x: x.get('timestamp', 0))
|
||||
|
||||
for i in range(len(files) - 1):
|
||||
current = files[i]
|
||||
next_file = files[i + 1]
|
||||
|
||||
current_end = current.get('timestamp', 0) + current.get('duration_sec', 0)
|
||||
next_start = next_file.get('timestamp', 0)
|
||||
|
||||
gap_seconds = next_start - current_end
|
||||
|
||||
# Report gaps > 1 hour
|
||||
if gap_seconds > 3600:
|
||||
gaps.append({
|
||||
'node_id': node_id,
|
||||
'gap_start': current_end,
|
||||
'gap_end': next_start,
|
||||
'gap_hours': round(gap_seconds / 3600, 2),
|
||||
'before_file': current.get('filename'),
|
||||
'after_file': next_file.get('filename')
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'gaps': gaps,
|
||||
'total_gaps': len(gaps)
|
||||
})
|
||||
|
||||
@app.route('/api/chat', methods=['POST'])
|
||||
def chat():
|
||||
"""Endpoint chat assistant (mock)"""
|
||||
data = request.json
|
||||
message = data.get('message', '')
|
||||
|
||||
return jsonify({
|
||||
'response': f"[Mock] Vous avez dit: {message}",
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/docs/manifest', methods=['GET'])
|
||||
def get_docs_manifest():
|
||||
"""Manifest de la documentation de campagne"""
|
||||
return jsonify({
|
||||
'campaign': {
|
||||
'name': 'SeaKESP Sète 2020',
|
||||
'start_date': '2020-07-11',
|
||||
'end_date': '2020-09-22',
|
||||
'duration_days': 46,
|
||||
'location': 'Sète, Méditerranée, France',
|
||||
'coordinates': {'lat': 43.40, 'lon': 3.70}
|
||||
},
|
||||
'timeline': [
|
||||
{'date': '2020-07-11', 'event': 'Début du déploiement', 'type': 'deployment'},
|
||||
{'date': '2020-08-08', 'event': 'Premiers enregistrements', 'type': 'recording'},
|
||||
{'date': '2020-08-12', 'event': 'Campagne principale', 'type': 'recording'},
|
||||
{'date': '2020-08-16', 'event': 'Fin campagne principale', 'type': 'recording'},
|
||||
{'date': '2020-09-22', 'event': 'Récupération équipements', 'type': 'recovery'}
|
||||
],
|
||||
'documents': [
|
||||
{'name': 'Rapport Gandalf', 'type': 'report', 'available': False},
|
||||
{'name': 'SPS Preplots', 'type': 'technical', 'available': False},
|
||||
{'name': 'Paramètres acquisition', 'type': 'technical', 'available': False}
|
||||
],
|
||||
'stats': {
|
||||
'total_nodes': len(INDEX['nodes']),
|
||||
'total_dates': len(INDEX['dates']),
|
||||
'total_files': INDEX.get('total_files', 0),
|
||||
'sample_rate_hz': 500,
|
||||
'channels': 4
|
||||
}
|
||||
})
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
return jsonify({'status': 'ok', 'nodes': len(INDEX['nodes']), 'dates': len(INDEX['dates'])})
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f"Loaded {len(INDEX['nodes'])} nodes, {len(INDEX['dates'])} dates")
|
||||
print(f"H5 directory: {H5_DIR}")
|
||||
app.run(host='0.0.0.0', port=3004)
|
||||
BIN
h5_data.db
Normal file
BIN
h5_data.db
Normal file
Binary file not shown.
2685
index.html.final.bak
Normal file
2685
index.html.final.bak
Normal file
File diff suppressed because it is too large
Load Diff
1651
index.html.final.broken-20260219
Normal file
1651
index.html.final.broken-20260219
Normal file
File diff suppressed because it is too large
Load Diff
2029
index.html.v3
Normal file
2029
index.html.v3
Normal file
File diff suppressed because it is too large
Load Diff
86
index_all_v2.py
Normal file
86
index_all_v2.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import csv
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
|
||||
# Pattern pour extraire les infos du nom de fichier
|
||||
FILENAME_PATTERN = re.compile(r'auto_.*?_b(\d+)_.*?_(\d{10})\.h5$', re.IGNORECASE)
|
||||
|
||||
DATA_ROOTS = [Path("/mnt/kingston"), Path("/mnt/data_sdb1")]
|
||||
POSITIONS_CSV = Path("/mnt/kingston/Copie de SETE_AUV_DARFV4-Copier(1).csv")
|
||||
OUTPUT_INDEX = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
|
||||
def load_node_positions(csv_path):
|
||||
positions = {}
|
||||
if not csv_path.exists(): return positions
|
||||
with open(csv_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) < 5: return positions
|
||||
headers = lines[3].strip().split(',')
|
||||
try:
|
||||
node_code_idx = headers.index('NodeCode')
|
||||
easting_idx = headers.index('Aslaid Easting') if 'Aslaid Easting' in headers else headers.index('Preplot Easting')
|
||||
northing_idx = headers.index('Aslaid Northing') if 'Aslaid Northing' in headers else headers.index('Preplot Northing')
|
||||
except: return positions
|
||||
for line in lines[4:]:
|
||||
parts = line.strip().split(',')
|
||||
try:
|
||||
nid = parts[node_code_idx].strip()
|
||||
positions[nid] = {
|
||||
'easting': float(parts[easting_idx]),
|
||||
'northing': float(parts[northing_idx]),
|
||||
'depth': float(parts[headers.index('Aslaid Depth')]) if 'Aslaid Depth' in headers else 0.0
|
||||
}
|
||||
except: continue
|
||||
return positions
|
||||
|
||||
def scan_all():
|
||||
index = {}
|
||||
pos = load_node_positions(POSITIONS_CSV)
|
||||
print(f"Positions chargées: {len(pos)}")
|
||||
|
||||
file_count = 0
|
||||
for root in DATA_ROOTS:
|
||||
print(f"Scan de {root}...")
|
||||
for h5_file in root.rglob("*.h5"):
|
||||
# Extraction ID node et timestamp
|
||||
match = re.search(r'_b(\d+)_.*?(\d{10})\.h5$', h5_file.name)
|
||||
if not match: continue
|
||||
|
||||
node_id = match.group(1)
|
||||
ts = int(match.group(2))
|
||||
date_str = datetime.fromtimestamp(ts).strftime('%Y-%m-%d')
|
||||
|
||||
if node_id not in index:
|
||||
index[node_id] = {'id': node_id, 'position': pos.get(node_id), 'dates': {}, 'hasDates': True}
|
||||
|
||||
if date_str not in index[node_id]['dates']:
|
||||
index[node_id]['dates'][date_str] = []
|
||||
|
||||
index[node_id]['dates'][date_str].append({
|
||||
'path': str(h5_file),
|
||||
'timestamp': ts,
|
||||
'channels': ['ch0', 'ch1', 'ch2', 'ch3'],
|
||||
'size_bytes': h5_file.stat().st_size
|
||||
})
|
||||
file_count += 1
|
||||
|
||||
# Ajouter les nodes sans fichiers mais avec position
|
||||
for nid, p in pos.items():
|
||||
if nid not in index:
|
||||
index[nid] = {'id': nid, 'position': p, 'dates': {}, 'hasDates': False}
|
||||
|
||||
full_index = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'sample_rate_hz': 200,
|
||||
'nodes': index,
|
||||
'dates': sorted(list(set(d for n in index.values() for d in n['dates'].keys())))
|
||||
}
|
||||
|
||||
with open(OUTPUT_INDEX, 'w') as f: json.dump(full_index, f, indent=2)
|
||||
print(f"Index total généré: {file_count} fichiers, {len(index)} nodes.")
|
||||
|
||||
if __name__ == '__main__': scan_all()
|
||||
69
index_all_v3.py
Normal file
69
index_all_v3.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import os, re, json, csv
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
|
||||
FILENAME_PATTERN = re.compile(r'_b(\d+)_.*?_(\d{10})\.h5$', re.IGNORECASE)
|
||||
DATA_ROOTS = [Path("/mnt/kingston"), Path("/mnt/data_sdb1")]
|
||||
POSITIONS_CSV = Path("/mnt/kingston/Copie de SETE_AUV_DARFV4-Copier(1).csv")
|
||||
OUTPUT_INDEX = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
|
||||
def load_pos():
|
||||
positions = {}
|
||||
if not POSITIONS_CSV.exists(): return {}
|
||||
with open(POSITIONS_CSV, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) < 5: return {}
|
||||
headers = lines[3].strip().split(',')
|
||||
try:
|
||||
ni = headers.index('NodeCode')
|
||||
ei = headers.index('Aslaid Easting') if 'Aslaid Easting' in headers else headers.index('Preplot Easting')
|
||||
oi = headers.index('Aslaid Northing') if 'Aslaid Northing' in headers else headers.index('Preplot Northing')
|
||||
di = headers.index('Aslaid Depth') if 'Aslaid Depth' in headers else -1
|
||||
except: return {}
|
||||
for line in lines[4:]:
|
||||
parts = line.strip().split(',')
|
||||
try:
|
||||
nid = parts[ni].strip()
|
||||
positions[nid] = {'easting': float(parts[ei]), 'northing': float(parts[oi]), 'depth': float(parts[di]) if di != -1 else 0.0}
|
||||
except: continue
|
||||
return positions
|
||||
|
||||
def scan():
|
||||
pos = load_pos()
|
||||
index = {}
|
||||
file_count = 0
|
||||
for root in DATA_ROOTS:
|
||||
print(f"Scanning {root}...")
|
||||
for h5_file in root.rglob("*.h5"):
|
||||
match = FILENAME_PATTERN.search(h5_file.name)
|
||||
if not match: continue
|
||||
nid, ts = match.group(1), int(match.group(2))
|
||||
# Utilisation de la date du dossier parent si possible, sinon du timestamp
|
||||
date_str = datetime.fromtimestamp(ts).strftime('%Y-%m-%d')
|
||||
# Forcer la date du dossier (plus fiable pour l'utilisateur)
|
||||
for p in h5_file.parents:
|
||||
if re.match(r'2020-09-\d{2}', p.name):
|
||||
date_str = p.name
|
||||
break
|
||||
|
||||
if nid not in index:
|
||||
index[nid] = {'id': nid, 'position': pos.get(nid), 'dates': {}, 'hasDates': True}
|
||||
if date_str not in index[nid]['dates']:
|
||||
index[nid]['dates'][date_str] = []
|
||||
index[nid]['dates'][date_str].append({'path': str(h5_file), 'timestamp': ts, 'channels': ['ch0', 'ch1', 'ch2', 'ch3']})
|
||||
file_count += 1
|
||||
|
||||
for nid, p in pos.items():
|
||||
if nid not in index: index[nid] = {'id': nid, 'position': p, 'dates': {}, 'hasDates': False}
|
||||
|
||||
full = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'sample_rate_hz': 200,
|
||||
'nodes': index,
|
||||
'dates': sorted(list(set(d for n in index.values() for d in n['dates'].keys())))
|
||||
}
|
||||
with open(OUTPUT_INDEX, 'w') as f: json.dump(full, f, indent=2)
|
||||
print(f"Index: {file_count} files, {len(index)} nodes, {len(full['dates'])} dates.")
|
||||
|
||||
if __name__ == '__main__': scan()
|
||||
105
index_data_only.py
Normal file
105
index_data_only.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import os, re, json, h5py
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from tqdm import tqdm
|
||||
|
||||
DATA_ROOTS = [Path("/mnt/kingston"), Path("/mnt/data_sdb1")]
|
||||
POSITIONS_CSV = Path("/mnt/kingston/Copie de SETE_AUV_DARFV4-Copier(1).csv")
|
||||
OUTPUT_INDEX = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
SAMPLE_RATE = 200
|
||||
|
||||
def load_pos():
|
||||
positions = {}
|
||||
if not POSITIONS_CSV.exists(): return {}
|
||||
with open(POSITIONS_CSV, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) < 5: return {}
|
||||
headers = lines[3].strip().split(',')
|
||||
try:
|
||||
ni = headers.index('NodeCode')
|
||||
ei = headers.index('Aslaid Easting') if 'Aslaid Easting' in headers else headers.index('Preplot Easting')
|
||||
oi = headers.index('Aslaid Northing') if 'Aslaid Northing' in headers else headers.index('Preplot Northing')
|
||||
except: return {}
|
||||
for line in lines[4:]:
|
||||
parts = line.strip().split(',')
|
||||
try:
|
||||
nid = parts[ni].strip()
|
||||
positions[nid] = {
|
||||
'easting': float(parts[ei]),
|
||||
'northing': float(parts[oi]),
|
||||
'depth': float(parts[headers.index('Aslaid Depth')]) if 'Aslaid Depth' in headers else 0.0
|
||||
}
|
||||
except: continue
|
||||
return positions
|
||||
|
||||
def scan():
|
||||
pos = load_pos()
|
||||
nodes = {}
|
||||
all_dates = set()
|
||||
file_count = 0
|
||||
|
||||
print("🔍 Scanning ONLY 'data' H5 files (ignoring 'aux')...")
|
||||
all_h5_files = []
|
||||
for root in DATA_ROOTS:
|
||||
all_h5_files.extend(list(root.rglob("*.h5")))
|
||||
|
||||
for h5_path in tqdm(all_h5_files):
|
||||
# FILTRE : Uniquement les fichiers contenant "data"
|
||||
if "_data_" not in h5_path.name.lower():
|
||||
continue
|
||||
|
||||
try:
|
||||
match = re.search(r'auto_(\d+)_(\d{6})_b(\d+)_.*?_(\d{10})\.h5$', h5_path.name)
|
||||
if not match: continue
|
||||
|
||||
julian_day = int(match.group(1))
|
||||
time_str = match.group(2)
|
||||
node_id = match.group(3)
|
||||
|
||||
date_ref = datetime(2020, 1, 1) + timedelta(days=julian_day - 1)
|
||||
date_str = date_ref.strftime('%Y-%m-%d')
|
||||
|
||||
h, m, s = int(time_str[:2]), int(time_str[2:4]), int(time_str[4:6])
|
||||
actual_start_ts = int(datetime(2020, 1, 1).timestamp() + (julian_day - 1) * 86400 + h * 3600 + m * 60 + s)
|
||||
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
if 'adc_values' not in f: continue
|
||||
duration = f['adc_values'].shape[0] / SAMPLE_RATE
|
||||
actual_end_ts = actual_start_ts + duration
|
||||
|
||||
all_dates.add(date_str)
|
||||
|
||||
if node_id not in nodes:
|
||||
nodes[node_id] = {
|
||||
'id': node_id,
|
||||
'position': pos.get(node_id),
|
||||
'files': []
|
||||
}
|
||||
|
||||
# On extrait le canal du nom de fichier pour un matching plus précis
|
||||
channel_match = re.search(r'_ch(\d+)_', h5_path.name)
|
||||
channel = f"ch{channel_match.group(1)}" if channel_match else "ch0"
|
||||
|
||||
nodes[node_id]['files'].append({
|
||||
'path': str(h5_path),
|
||||
'start': actual_start_ts,
|
||||
'end': actual_end_ts,
|
||||
'julian': julian_day,
|
||||
'channel': channel # Canal spécifique au fichier
|
||||
})
|
||||
file_count += 1
|
||||
except: continue
|
||||
|
||||
result = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'sample_rate_hz': SAMPLE_RATE,
|
||||
'nodes': nodes,
|
||||
'dates': sorted(list(all_dates))
|
||||
}
|
||||
|
||||
with open(OUTPUT_INDEX, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
print(f"✅ Index updated: {file_count} 'data' files, {len(nodes)} nodes.")
|
||||
|
||||
if __name__ == '__main__': scan()
|
||||
57
index_time_ranges.py
Normal file
57
index_time_ranges.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import os, re, json, h5py
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
|
||||
DATA_ROOTS = [Path("/mnt/kingston"), Path("/mnt/data_sdb1")]
|
||||
OUTPUT_INDEX = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
SAMPLE_RATE = 200
|
||||
|
||||
def scan():
|
||||
index = {}
|
||||
file_count = 0
|
||||
print("Scanning H5 files for time ranges...")
|
||||
|
||||
# On récupère d'abord les fichiers
|
||||
all_files = []
|
||||
for root in DATA_ROOTS:
|
||||
all_files.extend(list(root.rglob("*.h5")))
|
||||
|
||||
for h5_path in tqdm(all_files):
|
||||
try:
|
||||
# On extrait ID node du nom de fichier
|
||||
match = re.search(r'_b(\d+)_', h5_path.name)
|
||||
if not match: continue
|
||||
nid = match.group(1)
|
||||
|
||||
# On ouvre le fichier pour avoir le VRAI timestamp et la durée
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
if 'adc_values' not in f: continue
|
||||
ds = f['adc_values']
|
||||
start_ts = int(ds.attrs.get('timestamp', 0))
|
||||
if start_ts == 0: continue
|
||||
|
||||
duration = ds.shape[0] / SAMPLE_RATE
|
||||
end_ts = start_ts + duration
|
||||
|
||||
if nid not in index: index[nid] = []
|
||||
index[nid].append({
|
||||
'path': str(h5_path),
|
||||
'start': start_ts,
|
||||
'end': end_ts,
|
||||
'channels': ['ch0', 'ch1', 'ch2', 'ch3']
|
||||
})
|
||||
file_count += 1
|
||||
except: continue
|
||||
|
||||
# Sauvegarder l'index
|
||||
with open(OUTPUT_INDEX, 'w') as f:
|
||||
json.dump({
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'sample_rate_hz': SAMPLE_RATE,
|
||||
'files_by_node': index
|
||||
}, f)
|
||||
|
||||
print(f"Index généré: {file_count} fichiers avec plages temporelles réelles.")
|
||||
|
||||
if __name__ == '__main__': scan()
|
||||
87
index_time_ranges_v2.py
Normal file
87
index_time_ranges_v2.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import os, re, json, h5py
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
|
||||
DATA_ROOTS = [Path("/mnt/kingston"), Path("/mnt/data_sdb1")]
|
||||
POSITIONS_CSV = Path("/mnt/kingston/Copie de SETE_AUV_DARFV4-Copier(1).csv")
|
||||
OUTPUT_INDEX = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
SAMPLE_RATE = 200
|
||||
|
||||
def load_pos():
|
||||
positions = {}
|
||||
if not POSITIONS_CSV.exists(): return {}
|
||||
with open(POSITIONS_CSV, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) < 5: return {}
|
||||
headers = lines[3].strip().split(',')
|
||||
try:
|
||||
ni = headers.index('NodeCode')
|
||||
ei = headers.index('Aslaid Easting') if 'Aslaid Easting' in headers else headers.index('Preplot Easting')
|
||||
oi = headers.index('Aslaid Northing') if 'Aslaid Northing' in headers else headers.index('Preplot Northing')
|
||||
di = headers.index('Aslaid Depth') if 'Aslaid Depth' in headers else -1
|
||||
except: return {}
|
||||
for line in lines[4:]:
|
||||
parts = line.strip().split(',')
|
||||
try:
|
||||
nid = parts[ni].strip()
|
||||
positions[nid] = {
|
||||
'easting': float(parts[ei]),
|
||||
'northing': float(parts[oi]),
|
||||
'depth': float(parts[di]) if di != -1 else 0.0
|
||||
}
|
||||
except: continue
|
||||
return positions
|
||||
|
||||
def scan():
|
||||
pos = load_pos()
|
||||
index = {}
|
||||
file_count = 0
|
||||
print(f"Scanning H5 files... Positions loaded: {len(pos)}")
|
||||
|
||||
all_files = []
|
||||
for root in DATA_ROOTS:
|
||||
all_files.extend(list(root.rglob("*.h5")))
|
||||
|
||||
for h5_path in tqdm(all_files):
|
||||
try:
|
||||
match = re.search(r'_b(\d+)_', h5_path.name)
|
||||
if not match: continue
|
||||
nid = match.group(1)
|
||||
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
if 'adc_values' not in f: continue
|
||||
ds = f['adc_values']
|
||||
start_ts = int(ds.attrs.get('timestamp', 0))
|
||||
if start_ts == 0: continue
|
||||
|
||||
duration = ds.shape[0] / SAMPLE_RATE
|
||||
end_ts = start_ts + duration
|
||||
|
||||
if nid not in index:
|
||||
index[nid] = {
|
||||
'id': nid,
|
||||
'position': pos.get(nid),
|
||||
'files': []
|
||||
}
|
||||
|
||||
index[nid]['files'].append({
|
||||
'path': str(h5_path),
|
||||
'start': start_ts,
|
||||
'end': end_ts,
|
||||
'channels': ['ch0', 'ch1', 'ch2', 'ch3']
|
||||
})
|
||||
file_count += 1
|
||||
except: continue
|
||||
|
||||
# Sauvegarder l'index
|
||||
with open(OUTPUT_INDEX, 'w') as f:
|
||||
json.dump({
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'sample_rate_hz': SAMPLE_RATE,
|
||||
'nodes': index
|
||||
}, f)
|
||||
|
||||
print(f"Index généré: {file_count} fichiers, {len(index)} nodes avec positions.")
|
||||
|
||||
if __name__ == '__main__': scan()
|
||||
108
index_tmp.py
Normal file
108
index_tmp.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import os, re, json, h5py
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from tqdm import tqdm
|
||||
|
||||
DATA_ROOTS = [Path("/mnt/kingston"), Path("/mnt/data_sdb1")]
|
||||
POSITIONS_CSV = Path("/mnt/kingston/Copie de SETE_AUV_DARFV4-Copier(1).csv")
|
||||
OUTPUT_INDEX = Path("/tmp/index.json")
|
||||
SAMPLE_RATE = 200
|
||||
|
||||
def load_pos():
|
||||
positions = {}
|
||||
if not POSITIONS_CSV.exists(): return {}
|
||||
with open(POSITIONS_CSV, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) < 5: return {}
|
||||
headers = lines[3].strip().split(',')
|
||||
try:
|
||||
ni = headers.index('NodeCode')
|
||||
ei = headers.index('Aslaid Easting') if 'Aslaid Easting' in headers else headers.index('Preplot Easting')
|
||||
oi = headers.index('Aslaid Northing') if 'Aslaid Northing' in headers else headers.index('Preplot Northing')
|
||||
except: return {}
|
||||
for line in lines[4:]:
|
||||
parts = line.strip().split(',')
|
||||
try:
|
||||
nid = parts[ni].strip()
|
||||
positions[nid] = {
|
||||
'easting': float(parts[ei]),
|
||||
'northing': float(parts[oi]),
|
||||
'depth': float(parts[headers.index('Aslaid Depth')]) if 'Aslaid Depth' in headers else 0.0
|
||||
}
|
||||
except: continue
|
||||
return positions
|
||||
|
||||
def scan():
|
||||
pos = load_pos()
|
||||
nodes = {}
|
||||
all_dates = set()
|
||||
file_count = 0
|
||||
|
||||
print("🔍 Scanning all H5 files (Trusting filenames/folders for dates)...")
|
||||
all_h5_files = []
|
||||
for root in DATA_ROOTS:
|
||||
all_h5_files.extend(list(root.rglob("*.h5")))
|
||||
|
||||
for h5_path in tqdm(all_h5_files):
|
||||
try:
|
||||
# Pattern: auto_{julian}_{time}_b{node}_..._{ts}.h5
|
||||
match = re.search(r'auto_(\d+)_(\d{6})_b(\d+)_.*?_(\d{10})\.h5$', h5_path.name)
|
||||
if not match: continue
|
||||
|
||||
julian_day = int(match.group(1))
|
||||
time_str = match.group(2)
|
||||
node_id = match.group(3)
|
||||
internal_ts = int(match.group(4))
|
||||
|
||||
# Calculer la date réelle à partir du Julian Day (2020)
|
||||
# Julian 1 = Jan 1. Julian 255 = Sept 11.
|
||||
date_ref = datetime(2020, 1, 1) + timedelta(days=julian_day - 1)
|
||||
date_str = date_ref.strftime('%Y-%m-%d')
|
||||
|
||||
# Heure du fichier
|
||||
hours = int(time_str[:2])
|
||||
minutes = int(time_str[2:4])
|
||||
seconds = int(time_str[4:6])
|
||||
|
||||
# Timestamp calculé (plus fiable pour le matching que l'interne buggé)
|
||||
actual_start_ts = int(datetime(2020, 1, 1).timestamp() + (julian_day - 1) * 86400 + hours * 3600 + minutes * 60 + seconds)
|
||||
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
if 'adc_values' not in f: continue
|
||||
duration = f['adc_values'].shape[0] / SAMPLE_RATE
|
||||
actual_end_ts = actual_start_ts + duration
|
||||
|
||||
all_dates.add(date_str)
|
||||
|
||||
if node_id not in nodes:
|
||||
nodes[node_id] = {
|
||||
'id': node_id,
|
||||
'position': pos.get(node_id),
|
||||
'files': []
|
||||
}
|
||||
|
||||
nodes[node_id]['files'].append({
|
||||
'path': str(h5_path),
|
||||
'start': actual_start_ts,
|
||||
'end': actual_end_ts,
|
||||
'julian': julian_day,
|
||||
'channels': ['ch0', 'ch1', 'ch2', 'ch3']
|
||||
})
|
||||
file_count += 1
|
||||
except: continue
|
||||
|
||||
# Sauvegarder l'index complet
|
||||
result = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'sample_rate_hz': SAMPLE_RATE,
|
||||
'nodes': nodes,
|
||||
'dates': sorted(list(all_dates))
|
||||
}
|
||||
|
||||
with open(OUTPUT_INDEX, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
print(f"✅ Indexing complete: {file_count} files, {len(nodes)} nodes, {len(all_dates)} dates.")
|
||||
print(f"📅 Dates covered: {sorted(list(all_dates))}")
|
||||
|
||||
if __name__ == '__main__': scan()
|
||||
96
index_ultimate.py
Normal file
96
index_ultimate.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import os, re, json, h5py
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
|
||||
DATA_ROOTS = [Path("/mnt/kingston"), Path("/mnt/data_sdb1")]
|
||||
POSITIONS_CSV = Path("/mnt/kingston/Copie de SETE_AUV_DARFV4-Copier(1).csv")
|
||||
OUTPUT_INDEX = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
SAMPLE_RATE = 200
|
||||
|
||||
def load_pos():
|
||||
positions = {}
|
||||
if not POSITIONS_CSV.exists(): return {}
|
||||
with open(POSITIONS_CSV, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) < 5: return {}
|
||||
headers = lines[3].strip().split(',')
|
||||
try:
|
||||
ni = headers.index('NodeCode')
|
||||
ei = headers.index('Aslaid Easting') if 'Aslaid Easting' in headers else headers.index('Preplot Easting')
|
||||
oi = headers.index('Aslaid Northing') if 'Aslaid Northing' in headers else headers.index('Preplot Northing')
|
||||
di = headers.index('Aslaid Depth') if 'Aslaid Depth' in headers else -1
|
||||
except: return {}
|
||||
for line in lines[4:]:
|
||||
parts = line.strip().split(',')
|
||||
try:
|
||||
nid = parts[ni].strip()
|
||||
positions[nid] = {
|
||||
'easting': float(parts[ei]),
|
||||
'northing': float(parts[oi]),
|
||||
'depth': float(parts[di]) if di != -1 else 0.0
|
||||
}
|
||||
except: continue
|
||||
return positions
|
||||
|
||||
def scan():
|
||||
pos = load_pos()
|
||||
nodes = {}
|
||||
all_dates = set()
|
||||
file_count = 0
|
||||
|
||||
print("🔍 Scanning all H5 files...")
|
||||
all_h5_files = []
|
||||
for root in DATA_ROOTS:
|
||||
all_h5_files.extend(list(root.rglob("*.h5")))
|
||||
|
||||
for h5_path in tqdm(all_h5_files):
|
||||
try:
|
||||
match = re.search(r'_b(\d+)_', h5_path.name)
|
||||
if not match: continue
|
||||
nid = match.group(1)
|
||||
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
if 'adc_values' not in f: continue
|
||||
ds = f['adc_values']
|
||||
start_ts = int(ds.attrs.get('timestamp', 0))
|
||||
if start_ts == 0: continue
|
||||
|
||||
duration = ds.shape[0] / SAMPLE_RATE
|
||||
end_ts = start_ts + duration
|
||||
|
||||
# Ajouter la date à la liste globale
|
||||
date_str = datetime.fromtimestamp(start_ts).strftime('%Y-%m-%d')
|
||||
all_dates.add(date_str)
|
||||
|
||||
if nid not in nodes:
|
||||
nodes[nid] = {
|
||||
'id': nid,
|
||||
'position': pos.get(nid),
|
||||
'files': []
|
||||
}
|
||||
|
||||
nodes[nid]['files'].append({
|
||||
'path': str(h5_path),
|
||||
'start': start_ts,
|
||||
'end': end_ts,
|
||||
'channels': ['ch0', 'ch1', 'ch2', 'ch3']
|
||||
})
|
||||
file_count += 1
|
||||
except: continue
|
||||
|
||||
# Sauvegarder l'index complet
|
||||
result = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'sample_rate_hz': SAMPLE_RATE,
|
||||
'nodes': nodes,
|
||||
'dates': sorted(list(all_dates))
|
||||
}
|
||||
|
||||
with open(OUTPUT_INDEX, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
print(f"✅ Indexing complete: {file_count} files, {len(nodes)} nodes, {len(all_dates)} dates.")
|
||||
print(f"📅 Dates covered: {sorted(list(all_dates))}")
|
||||
|
||||
if __name__ == '__main__': scan()
|
||||
108
index_ultimate_v2.py
Normal file
108
index_ultimate_v2.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import os, re, json, h5py
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from tqdm import tqdm
|
||||
|
||||
DATA_ROOTS = [Path("/mnt/kingston"), Path("/mnt/data_sdb1")]
|
||||
POSITIONS_CSV = Path("/mnt/kingston/Copie de SETE_AUV_DARFV4-Copier(1).csv")
|
||||
OUTPUT_INDEX = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
SAMPLE_RATE = 200
|
||||
|
||||
def load_pos():
|
||||
positions = {}
|
||||
if not POSITIONS_CSV.exists(): return {}
|
||||
with open(POSITIONS_CSV, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) < 5: return {}
|
||||
headers = lines[3].strip().split(',')
|
||||
try:
|
||||
ni = headers.index('NodeCode')
|
||||
ei = headers.index('Aslaid Easting') if 'Aslaid Easting' in headers else headers.index('Preplot Easting')
|
||||
oi = headers.index('Aslaid Northing') if 'Aslaid Northing' in headers else headers.index('Preplot Northing')
|
||||
except: return {}
|
||||
for line in lines[4:]:
|
||||
parts = line.strip().split(',')
|
||||
try:
|
||||
nid = parts[ni].strip()
|
||||
positions[nid] = {
|
||||
'easting': float(parts[ei]),
|
||||
'northing': float(parts[oi]),
|
||||
'depth': float(parts[headers.index('Aslaid Depth')]) if 'Aslaid Depth' in headers else 0.0
|
||||
}
|
||||
except: continue
|
||||
return positions
|
||||
|
||||
def scan():
|
||||
pos = load_pos()
|
||||
nodes = {}
|
||||
all_dates = set()
|
||||
file_count = 0
|
||||
|
||||
print("🔍 Scanning all H5 files (Trusting filenames/folders for dates)...")
|
||||
all_h5_files = []
|
||||
for root in DATA_ROOTS:
|
||||
all_h5_files.extend(list(root.rglob("*.h5")))
|
||||
|
||||
for h5_path in tqdm(all_h5_files):
|
||||
try:
|
||||
# Pattern: auto_{julian}_{time}_b{node}_..._{ts}.h5
|
||||
match = re.search(r'auto_(\d+)_(\d{6})_b(\d+)_.*?_(\d{10})\.h5$', h5_path.name)
|
||||
if not match: continue
|
||||
|
||||
julian_day = int(match.group(1))
|
||||
time_str = match.group(2)
|
||||
node_id = match.group(3)
|
||||
internal_ts = int(match.group(4))
|
||||
|
||||
# Calculer la date réelle à partir du Julian Day (2020)
|
||||
# Julian 1 = Jan 1. Julian 255 = Sept 11.
|
||||
date_ref = datetime(2020, 1, 1) + timedelta(days=julian_day - 1)
|
||||
date_str = date_ref.strftime('%Y-%m-%d')
|
||||
|
||||
# Heure du fichier
|
||||
hours = int(time_str[:2])
|
||||
minutes = int(time_str[2:4])
|
||||
seconds = int(time_str[4:6])
|
||||
|
||||
# Timestamp calculé (plus fiable pour le matching que l'interne buggé)
|
||||
actual_start_ts = int(datetime(2020, 1, 1).timestamp() + (julian_day - 1) * 86400 + hours * 3600 + minutes * 60 + seconds)
|
||||
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
if 'adc_values' not in f: continue
|
||||
duration = f['adc_values'].shape[0] / SAMPLE_RATE
|
||||
actual_end_ts = actual_start_ts + duration
|
||||
|
||||
all_dates.add(date_str)
|
||||
|
||||
if node_id not in nodes:
|
||||
nodes[node_id] = {
|
||||
'id': node_id,
|
||||
'position': pos.get(node_id),
|
||||
'files': []
|
||||
}
|
||||
|
||||
nodes[node_id]['files'].append({
|
||||
'path': str(h5_path),
|
||||
'start': actual_start_ts,
|
||||
'end': actual_end_ts,
|
||||
'julian': julian_day,
|
||||
'channels': ['ch0', 'ch1', 'ch2', 'ch3']
|
||||
})
|
||||
file_count += 1
|
||||
except: continue
|
||||
|
||||
# Sauvegarder l'index complet
|
||||
result = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'sample_rate_hz': SAMPLE_RATE,
|
||||
'nodes': nodes,
|
||||
'dates': sorted(list(all_dates))
|
||||
}
|
||||
|
||||
with open(OUTPUT_INDEX, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
print(f"✅ Indexing complete: {file_count} files, {len(nodes)} nodes, {len(all_dates)} dates.")
|
||||
print(f"📅 Dates covered: {sorted(list(all_dates))}")
|
||||
|
||||
if __name__ == '__main__': scan()
|
||||
63
precompute_all_v4.py
Normal file
63
precompute_all_v4.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import json, sys, os, numpy as np, h5py
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
|
||||
INDEX_PATH = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
OUTPUT_DIR = Path("/mnt/kingston/seismic_webapp/data/rms_cache")
|
||||
SAMPLE_RATE = 200
|
||||
|
||||
def fix_path(p):
|
||||
p = p.replace('\\', '/')
|
||||
if p.startswith('F:/'): return '/mnt/kingston/' + p[3:]
|
||||
if p.startswith('E:/'): return '/mnt/data_sdb1/' + p[3:]
|
||||
return p
|
||||
|
||||
def compute_rms(h5_path):
|
||||
h5_path = fix_path(h5_path)
|
||||
if not os.path.exists(h5_path): return None
|
||||
try:
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
ds = f['adc_values']
|
||||
start_ts = int(ds.attrs.get('timestamp', 0))
|
||||
if start_ts == 0: return None
|
||||
# On prend 5000 samples pour un RMS représentatif
|
||||
samples = ds[0:5000]
|
||||
rms = float(np.sqrt(np.mean(samples.astype(np.float64)**2)))
|
||||
return [{'ts': start_ts, 'rms': rms}]
|
||||
except: return None
|
||||
|
||||
def main():
|
||||
with open(INDEX_PATH, 'r') as f: index = json.load(f)
|
||||
nodes = index.get('nodes', {})
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Toutes les dates du système
|
||||
all_dates = set()
|
||||
for n in nodes.values():
|
||||
if 'dates' in n: all_dates.update(n['dates'].keys())
|
||||
|
||||
print(f"Dates à traiter: {sorted(list(all_dates))}")
|
||||
|
||||
channel = "ch0"
|
||||
for date in sorted(list(all_dates)):
|
||||
output_file = OUTPUT_DIR / f"rms_{date}_{channel}.json"
|
||||
# On force la régénération pour être sûr d'avoir tout
|
||||
print(f"Processing {date}...")
|
||||
results = {}
|
||||
for nid, node in tqdm(nodes.items(), desc=f"Nodes {date}"):
|
||||
files = node.get('dates', {}).get(date, [])
|
||||
# On cherche les fichiers data prioritaires
|
||||
target = next((f for f in files if '_data_' in f['path'] and f'_{channel}_' in f['path']), None)
|
||||
if not target and files: target = files[0] # Fallback
|
||||
|
||||
if target:
|
||||
data = compute_rms(target['path'])
|
||||
if data: results[nid] = data
|
||||
|
||||
if results:
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump({'date':date, 'channel':channel, 'nodes':results}, f)
|
||||
print(f"Sauvegardé {output_file.name}: {len(results)} nodes")
|
||||
|
||||
if __name__ == '__main__': main()
|
||||
62
precompute_ultimate.py
Normal file
62
precompute_ultimate.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import json, sys, os, numpy as np, h5py, re
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from tqdm import tqdm
|
||||
|
||||
INDEX_PATH = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
OUTPUT_DIR = Path("/mnt/kingston/seismic_webapp/data/rms_cache")
|
||||
SAMPLE_RATE = 200
|
||||
|
||||
def fix_path(p):
|
||||
p = p.replace('\\', '/')
|
||||
if p.startswith('F:/'): return '/mnt/kingston/' + p[3:]
|
||||
if p.startswith('E:/'): return '/mnt/data_sdb1/' + p[3:]
|
||||
return p
|
||||
|
||||
def compute_rms(h5_path):
|
||||
h5_path = fix_path(h5_path)
|
||||
if not os.path.exists(h5_path): return None
|
||||
try:
|
||||
# Extraire le timestamp réel du nom de fichier (Julian day)
|
||||
match = re.search(r'auto_(\d+)_(\d{6})_b', os.path.basename(h5_path))
|
||||
if not match: return None
|
||||
julian, time_str = int(match.group(1)), match.group(2)
|
||||
h, m, s = int(time_str[:2]), int(time_str[2:4]), int(time_str[4:6])
|
||||
start_ts = int(datetime(2020, 1, 1).timestamp() + (julian - 1) * 86400 + h * 3600 + m * 60 + s)
|
||||
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
ds = f['adc_values']
|
||||
samples = ds[0:5000]
|
||||
rms = float(np.sqrt(np.mean(samples.astype(np.float64)**2)))
|
||||
return [{'ts': start_ts, 'rms': rms}]
|
||||
except: return None
|
||||
|
||||
def main():
|
||||
with open(INDEX_PATH, 'r') as f: index = json.load(f)
|
||||
nodes = index.get('nodes', {})
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for date in index['dates']:
|
||||
channel = "ch0"
|
||||
output_file = OUTPUT_DIR / f"rms_{{date}}_{{channel}}.json"
|
||||
print(f"Processing {date}...")
|
||||
results = {}
|
||||
for nid, node in tqdm(nodes.items(), desc=f"Nodes {date}"):
|
||||
files = node.get('files', [])
|
||||
# Filtrer les fichiers par Julian Day correspondant à la date
|
||||
dt = datetime.strptime(date, '%Y-%m-%d')
|
||||
target_julian = dt.timetuple().tm_yday
|
||||
|
||||
target = next((f for f in files if f['julian'] == target_julian and f'_{{channel}}_' in f['path']), None)
|
||||
if not target and files:
|
||||
target = next((f for f in files if f['julian'] == target_julian), None)
|
||||
|
||||
if target:
|
||||
data = compute_rms(target['path'])
|
||||
if data: results[nid] = data
|
||||
|
||||
if results:
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump({'date':date, 'channel':channel, 'nodes':results}, f)
|
||||
|
||||
if __name__ == '__main__': main()
|
||||
1
real_shots.json
Normal file
1
real_shots.json
Normal file
File diff suppressed because one or more lines are too long
BIN
scripts/__pycache__/migrate_to_db.cpython-311.pyc
Normal file
BIN
scripts/__pycache__/migrate_to_db.cpython-311.pyc
Normal file
Binary file not shown.
22
scripts/check_node29.py
Executable file
22
scripts/check_node29.py
Executable file
@@ -0,0 +1,22 @@
|
||||
import json
|
||||
|
||||
data = json.load(open(r'F:\seismic_webapp\data\index.json'))
|
||||
|
||||
node29 = data['nodes'].get('29')
|
||||
if node29:
|
||||
print(f"Node 29:")
|
||||
print(f" Position: {node29.get('position')}")
|
||||
print(f" Dates disponibles: {list(node29.get('dates', {}).keys())}")
|
||||
for date, files in node29.get('dates', {}).items():
|
||||
print(f" {date}: {len(files)} fichiers")
|
||||
for f in files[:2]:
|
||||
print(f" - {f['path']}")
|
||||
else:
|
||||
print("Node 29 non trouvé dans l'index")
|
||||
|
||||
print("\n--- Tous les nodes avec données ---")
|
||||
for node_id, node in data['nodes'].items():
|
||||
if node.get('dates') and len(node['dates']) > 0:
|
||||
has_pos = node.get('position') is not None
|
||||
dates = list(node['dates'].keys())
|
||||
print(f"Node {node_id}: pos={has_pos}, dates={dates}")
|
||||
22
scripts/check_positions.py
Executable file
22
scripts/check_positions.py
Executable file
@@ -0,0 +1,22 @@
|
||||
import json
|
||||
|
||||
data = json.load(open(r'F:\seismic_webapp\data\index.json'))
|
||||
|
||||
nodes_with_data = [n for n in data['nodes'].values() if n.get('dates') and len(n['dates']) > 0]
|
||||
print(f'Nodes avec donnees: {len(nodes_with_data)}')
|
||||
|
||||
print('\n--- Nodes avec donnees et leurs positions ---')
|
||||
for n in nodes_with_data[:10]:
|
||||
pos = n.get('position')
|
||||
has_pos = pos and pos.get('easting') and pos.get('northing')
|
||||
print(f"Node {n['id']}: hasPos={has_pos}, pos={pos}")
|
||||
|
||||
print('\n--- Nodes avec donnees SANS position valide ---')
|
||||
no_pos_count = 0
|
||||
for n in nodes_with_data:
|
||||
pos = n.get('position')
|
||||
if not pos or not pos.get('easting') or not pos.get('northing'):
|
||||
print(f"Node {n['id']}: pos={pos}")
|
||||
no_pos_count += 1
|
||||
|
||||
print(f'\nTotal nodes sans position valide: {no_pos_count}')
|
||||
35
scripts/debug_inventory.py
Executable file
35
scripts/debug_inventory.py
Executable file
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
d = json.load(open(r'F:\seismic_webapp\inventory.json'))
|
||||
|
||||
# Verifier quelques fichiers
|
||||
print("=== EXEMPLES DE FICHIERS ===")
|
||||
for f in d[:5]:
|
||||
print(f"File: {f['filename']}")
|
||||
print(f" Bumper: {f['bumper_id']}, Channel: {f['channel']}")
|
||||
print(f" Samples: {f['samples']}, Epoch: {f['epoch_time']}")
|
||||
print()
|
||||
|
||||
# Compter les bumpers uniques
|
||||
bumpers = set(f['bumper_id'] for f in d if f['bumper_id'])
|
||||
print(f"Bumpers uniques: {len(bumpers)}")
|
||||
print(f"Liste: {sorted(bumpers, key=lambda x: int(x) if x and x.isdigit() else 999)[:30]}")
|
||||
|
||||
# Verifier le probleme des samples
|
||||
print("\n=== FICHIERS AVEC GROS SAMPLES ===")
|
||||
big_files = [f for f in d if f['samples'] > 100000000]
|
||||
for f in big_files[:5]:
|
||||
print(f" {f['filename']}: {f['samples']} samples = {f['samples']/200/3600:.1f}h")
|
||||
|
||||
# Stats par bumper
|
||||
from collections import defaultdict
|
||||
by_bumper = defaultdict(lambda: {'files': 0, 'channels': set()})
|
||||
for f in d:
|
||||
if f['bumper_id']:
|
||||
by_bumper[f['bumper_id']]['files'] += 1
|
||||
if f['channel']:
|
||||
by_bumper[f['bumper_id']]['channels'].add(f['channel'])
|
||||
|
||||
print(f"\n=== PAR BUMPER (premiers 20) ===")
|
||||
for b in sorted(by_bumper.keys(), key=lambda x: int(x) if x.isdigit() else 999)[:20]:
|
||||
s = by_bumper[b]
|
||||
print(f" b{b}: {s['files']} files, channels: {sorted(s['channels'])}")
|
||||
71
scripts/extract_h5_calibrated.py
Executable file
71
scripts/extract_h5_calibrated.py
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script d'extraction de données H5 calibrées (format 2026).
|
||||
Lit calibrated_data/channel_X (valeurs physiques avec unités).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import h5py
|
||||
import numpy as np
|
||||
|
||||
def extract_window(file_path: str, channel: int, start_ts: int, duration_sec: int) -> dict:
|
||||
try:
|
||||
with h5py.File(file_path, 'r') as f:
|
||||
# Métadonnées
|
||||
meta = f['metadata']
|
||||
sample_rate = meta.attrs['sample_rate_hz']
|
||||
file_duration = meta.attrs['duration_sec']
|
||||
total_samples = meta.attrs['n_samples']
|
||||
|
||||
# Dataset calibré
|
||||
dataset = f[f'calibrated_data/channel_{channel}']
|
||||
|
||||
# Calcul indices (si start_ts = 0, on prend depuis le début)
|
||||
start_idx = int(start_ts * sample_rate) if start_ts > 0 else 0
|
||||
num_samples = int(duration_sec * sample_rate) if duration_sec > 0 else total_samples
|
||||
end_idx = min(start_idx + num_samples, total_samples)
|
||||
|
||||
# Extraire
|
||||
samples = dataset[start_idx:end_idx]
|
||||
|
||||
# Unité selon le canal
|
||||
unit = 'm/s' if channel in [1, 2, 3] else 'Pa'
|
||||
channel_name = f'Geophone {channel}' if channel in [1, 2, 3] else 'Hydrophone'
|
||||
|
||||
return {
|
||||
"samples": samples.tolist(),
|
||||
"start_idx": int(start_idx),
|
||||
"end_idx": int(end_idx),
|
||||
"total_samples": int(total_samples),
|
||||
"sample_rate": int(sample_rate),
|
||||
"duration_sec": float(file_duration),
|
||||
"channel": channel,
|
||||
"channel_name": channel_name,
|
||||
"unit": unit,
|
||||
"stats": {
|
||||
"min": float(np.min(samples)),
|
||||
"max": float(np.max(samples)),
|
||||
"mean": float(np.mean(samples)),
|
||||
"std": float(np.std(samples)),
|
||||
"rms": float(np.sqrt(np.mean(samples**2)))
|
||||
},
|
||||
"source": "calibrated_h5_2026"
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Extraction H5 calibré')
|
||||
parser.add_argument('--file', required=True, help='Fichier H5')
|
||||
parser.add_argument('--channel', type=int, required=True, help='Canal 1-4')
|
||||
parser.add_argument('--start', type=int, default=0, help='Offset secondes (0=début)')
|
||||
parser.add_argument('--duration', type=int, default=0, help='Durée secondes (0=tout)')
|
||||
args = parser.parse_args()
|
||||
|
||||
result = extract_window(args.file, args.channel, args.start, args.duration)
|
||||
print(json.dumps(result))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
132
scripts/extract_hdf5_window.py
Executable file
132
scripts/extract_hdf5_window.py
Executable file
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Script d'extraction de fenêtres de données HDF5.
|
||||
Appelé par le backend Node.js pour lire des portions de données ADC
|
||||
sans charger tout le fichier en mémoire.
|
||||
|
||||
Usage:
|
||||
python extract_hdf5_window.py --file <path> --channel <ch0-ch3> --start <timestamp> --duration <seconds>
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import h5py
|
||||
import numpy as np
|
||||
except ImportError as e:
|
||||
print(json.dumps({"error": f"Module manquant: {e}"}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
SAMPLE_RATE = 200 # Hz
|
||||
|
||||
|
||||
def extract_window(file_path: str, channel: str, start_ts: int, duration_sec: int) -> dict:
|
||||
"""
|
||||
Extrait une fenêtre de données ADC d'un fichier HDF5.
|
||||
|
||||
Args:
|
||||
file_path: Chemin vers le fichier H5
|
||||
channel: Canal à extraire (ch0, ch1, ch2, ch3)
|
||||
start_ts: Timestamp de début (secondes Unix)
|
||||
duration_sec: Durée en secondes
|
||||
|
||||
Returns:
|
||||
dict avec les échantillons et métadonnées
|
||||
"""
|
||||
file_path = Path(file_path)
|
||||
|
||||
if not file_path.exists():
|
||||
return {"error": f"Fichier non trouvé: {file_path}"}
|
||||
|
||||
try:
|
||||
with h5py.File(file_path, 'r') as f:
|
||||
# Chaque fichier HDF5 contient un seul dataset 'adc_values'
|
||||
# Le canal est déterminé par le nom du fichier, pas par un chemin interne
|
||||
|
||||
if 'adc_values' not in f:
|
||||
# Lister les datasets disponibles pour debug
|
||||
available = []
|
||||
def visit(name, obj):
|
||||
if isinstance(obj, h5py.Dataset):
|
||||
available.append(name)
|
||||
f.visititems(visit)
|
||||
return {"error": f"Dataset 'adc_values' non trouvé. Disponibles: {available}"}
|
||||
|
||||
dataset = f['adc_values']
|
||||
|
||||
# Récupérer les attributs de temps si disponibles
|
||||
# Chercher d'abord dans les attributs du dataset, puis du fichier
|
||||
file_start_ts = None
|
||||
if 'timestamp' in dataset.attrs:
|
||||
file_start_ts = int(dataset.attrs['timestamp'])
|
||||
elif 'start_time' in dataset.attrs:
|
||||
file_start_ts = int(dataset.attrs['start_time'])
|
||||
elif 'timestamp' in f.attrs:
|
||||
file_start_ts = int(f.attrs['timestamp'])
|
||||
elif 'start_time' in f.attrs:
|
||||
file_start_ts = int(f.attrs['start_time'])
|
||||
|
||||
# Calculer les indices de début et fin
|
||||
total_samples = dataset.shape[0]
|
||||
|
||||
if file_start_ts is not None:
|
||||
# Offset par rapport au début du fichier
|
||||
offset_sec = max(0, start_ts - file_start_ts)
|
||||
start_idx = int(offset_sec * SAMPLE_RATE)
|
||||
else:
|
||||
# Pas d'info de temps, prendre depuis le début
|
||||
start_idx = 0
|
||||
|
||||
num_samples = int(duration_sec * SAMPLE_RATE)
|
||||
end_idx = min(start_idx + num_samples, total_samples)
|
||||
|
||||
# Limiter pour éviter les gros payloads (max 60 secondes = 12000 samples)
|
||||
max_samples = 60 * SAMPLE_RATE
|
||||
if end_idx - start_idx > max_samples:
|
||||
end_idx = start_idx + max_samples
|
||||
|
||||
# Extraire les données (lecture partielle, pas tout en RAM)
|
||||
samples = dataset[start_idx:end_idx]
|
||||
|
||||
# Garder en numpy pour les stats
|
||||
samples_array = np.array(samples) if not isinstance(samples, np.ndarray) else samples
|
||||
|
||||
return {
|
||||
"samples": samples.tolist() if isinstance(samples, np.ndarray) else samples,
|
||||
"start_idx": start_idx,
|
||||
"end_idx": end_idx,
|
||||
"total_samples": total_samples,
|
||||
"file_start_ts": file_start_ts,
|
||||
"channel": channel,
|
||||
"stats": {
|
||||
"min": float(np.min(samples_array)) if len(samples_array) > 0 else None,
|
||||
"max": float(np.max(samples_array)) if len(samples_array) > 0 else None,
|
||||
"mean": float(np.mean(samples_array)) if len(samples_array) > 0 else None,
|
||||
"rms": float(np.sqrt(np.mean(samples_array**2))) if len(samples_array) > 0 else None,
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Extraction de fenêtre HDF5')
|
||||
parser.add_argument('--file', required=True, help='Chemin du fichier H5')
|
||||
parser.add_argument('--channel', required=True, help='Canal (ch0-ch3)')
|
||||
parser.add_argument('--start', type=int, required=True, help='Timestamp de début')
|
||||
parser.add_argument('--duration', type=int, default=10, help='Durée en secondes')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
result = extract_window(args.file, args.channel, args.start, args.duration)
|
||||
|
||||
# Sortie JSON pour le backend Node.js
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
479
scripts/generate_inventory.py
Executable file
479
scripts/generate_inventory.py
Executable file
@@ -0,0 +1,479 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script pour générer un inventaire HTML de tous les fichiers HDF5.
|
||||
Affiche: numéro de bumper, canal, date/heure début, date/heure fin, durée, nombre d'échantillons.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import h5py
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
|
||||
# Configuration
|
||||
SAMPLE_RATE = 200 # Hz
|
||||
DATA_DIRS = [
|
||||
r"F:\2020-09-11",
|
||||
r"E:\2020-09-11",
|
||||
r"E:\2020-09-14",
|
||||
]
|
||||
|
||||
def parse_filename(filename):
|
||||
"""
|
||||
Parse le nom de fichier HDF5 pour extraire les infos.
|
||||
Formats supportes:
|
||||
- auto_260_061316_b0_13_212626_data_rsn84614_seq1_ch0_1598976585.h5 (bumper = 13)
|
||||
- auto_255_061140_b119_12_230609_data_rsn5725_seq1_ch0_1599065292.h5 (bumper = 119)
|
||||
"""
|
||||
bumper_id = None
|
||||
|
||||
# Format 1: _b0_XX_ (ex: _b0_13_)
|
||||
bumper_match = re.search(r'_b0_(\d+)_', filename)
|
||||
if bumper_match:
|
||||
bumper_id = bumper_match.group(1)
|
||||
else:
|
||||
# Format 2: _bXXX_ (ex: _b119_)
|
||||
bumper_match = re.search(r'_b(\d+)_', filename)
|
||||
if bumper_match:
|
||||
bumper_id = bumper_match.group(1)
|
||||
|
||||
# Extraire le canal (ch0, ch1, ch2, ch3, ch5, ch6, ch7, ch15)
|
||||
channel_match = re.search(r'_(ch\d+)_', filename)
|
||||
channel = channel_match.group(1) if channel_match else None
|
||||
|
||||
# Extraire l'epoch time (dernier nombre avant .h5)
|
||||
epoch_match = re.search(r'_(\d{10})\.h5$', filename)
|
||||
epoch_time = int(epoch_match.group(1)) if epoch_match else None
|
||||
|
||||
# Type de fichier (data ou aux)
|
||||
file_type = 'data' if '_data_' in filename else 'aux' if '_aux_' in filename else 'unknown'
|
||||
|
||||
return {
|
||||
'bumper_id': bumper_id,
|
||||
'channel': channel,
|
||||
'epoch_time': epoch_time,
|
||||
'file_type': file_type
|
||||
}
|
||||
|
||||
def get_hdf5_info(filepath):
|
||||
"""
|
||||
Ouvre le fichier HDF5 et récupère le nombre d'échantillons.
|
||||
"""
|
||||
try:
|
||||
with h5py.File(filepath, 'r') as f:
|
||||
# Chercher le dataset adc_values
|
||||
if 'adc_values' in f:
|
||||
samples = f['adc_values'].shape[0]
|
||||
return {'samples': samples, 'error': None}
|
||||
else:
|
||||
# Lister les datasets disponibles
|
||||
datasets = list(f.keys())
|
||||
return {'samples': 0, 'error': f'No adc_values, found: {datasets}'}
|
||||
except Exception as e:
|
||||
return {'samples': 0, 'error': str(e)}
|
||||
|
||||
def format_datetime(epoch_time):
|
||||
"""Formate un timestamp en date/heure lisible."""
|
||||
if not epoch_time:
|
||||
return "N/A"
|
||||
dt = datetime.fromtimestamp(epoch_time)
|
||||
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
def format_duration(seconds):
|
||||
"""Formate une durée en heures:minutes:secondes."""
|
||||
hours = int(seconds // 3600)
|
||||
minutes = int((seconds % 3600) // 60)
|
||||
secs = int(seconds % 60)
|
||||
if hours > 0:
|
||||
return f"{hours}h {minutes}m {secs}s"
|
||||
elif minutes > 0:
|
||||
return f"{minutes}m {secs}s"
|
||||
else:
|
||||
return f"{secs}s"
|
||||
|
||||
def scan_directory(data_dir):
|
||||
"""Scanne un répertoire pour trouver tous les fichiers HDF5."""
|
||||
files = []
|
||||
data_path = Path(data_dir) / 'data'
|
||||
|
||||
if not data_path.exists():
|
||||
print(f" Directory not found: {data_path}")
|
||||
return files
|
||||
|
||||
for filepath in data_path.glob('*.h5'):
|
||||
files.append(filepath)
|
||||
|
||||
return files
|
||||
|
||||
def generate_html(inventory, output_path):
|
||||
"""Génère le document HTML."""
|
||||
|
||||
# Organiser par bumper puis par canal
|
||||
by_bumper = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
for item in inventory:
|
||||
bumper = item['bumper_id'] or 'unknown'
|
||||
channel = item['channel'] or 'unknown'
|
||||
by_bumper[bumper][channel].append(item)
|
||||
|
||||
# Trier les bumpers numériquement
|
||||
sorted_bumpers = sorted(by_bumper.keys(), key=lambda x: int(x) if x.isdigit() else 999)
|
||||
|
||||
# Statistiques globales
|
||||
total_files = len(inventory)
|
||||
total_samples = sum(i['samples'] for i in inventory)
|
||||
total_duration = total_samples / SAMPLE_RATE
|
||||
total_errors = sum(1 for i in inventory if i['error'])
|
||||
|
||||
# Compter par canal
|
||||
channel_stats = defaultdict(lambda: {'files': 0, 'samples': 0, 'bumpers': set()})
|
||||
for item in inventory:
|
||||
ch = item['channel'] or 'unknown'
|
||||
channel_stats[ch]['files'] += 1
|
||||
channel_stats[ch]['samples'] += item['samples']
|
||||
if item['bumper_id']:
|
||||
channel_stats[ch]['bumpers'].add(item['bumper_id'])
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Inventaire Fichiers HDF5 Sismiques</title>
|
||||
<style>
|
||||
* {{ box-sizing: border-box; }}
|
||||
body {{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: #0a0a1a;
|
||||
color: #eee;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}}
|
||||
h1 {{
|
||||
color: #4ade80;
|
||||
border-bottom: 2px solid #4ade80;
|
||||
padding-bottom: 10px;
|
||||
}}
|
||||
h2 {{
|
||||
color: #e94560;
|
||||
margin-top: 30px;
|
||||
}}
|
||||
h3 {{
|
||||
color: #fbbf24;
|
||||
margin-top: 20px;
|
||||
}}
|
||||
.stats {{
|
||||
background: #16213e;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}}
|
||||
.stat-box {{
|
||||
background: #0f3460;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}}
|
||||
.stat-value {{
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #4ade80;
|
||||
}}
|
||||
.stat-label {{
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
}}
|
||||
.channel-summary {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
}}
|
||||
.channel-box {{
|
||||
background: #16213e;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}}
|
||||
.channel-box h4 {{
|
||||
margin: 0 0 10px 0;
|
||||
color: #4ade80;
|
||||
}}
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.9rem;
|
||||
}}
|
||||
th, td {{
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #1a1a2e;
|
||||
}}
|
||||
th {{
|
||||
background: #16213e;
|
||||
color: #4ade80;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}}
|
||||
tr:hover {{
|
||||
background: #16213e;
|
||||
}}
|
||||
.ch0 {{ color: #4ade80; }}
|
||||
.ch1 {{ color: #60a5fa; }}
|
||||
.ch2 {{ color: #fbbf24; }}
|
||||
.ch3 {{ color: #f472b6; }}
|
||||
.data {{ color: #4ade80; }}
|
||||
.aux {{ color: #888; }}
|
||||
.error {{ color: #e94560; font-size: 0.8rem; }}
|
||||
.bumper-section {{
|
||||
background: #0f3460;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.filter-controls {{
|
||||
background: #16213e;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}}
|
||||
.filter-controls label {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}}
|
||||
input[type="checkbox"] {{
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}}
|
||||
.summary-table {{
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
}}
|
||||
.summary-table td {{
|
||||
padding: 5px 15px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>📊 Inventaire Fichiers HDF5 Sismiques</h1>
|
||||
<p>Généré le {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">{total_files}</div>
|
||||
<div class="stat-label">Fichiers HDF5</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">{len(sorted_bumpers)}</div>
|
||||
<div class="stat-label">Bumpers (nodes)</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">{total_samples:,}</div>
|
||||
<div class="stat-label">Échantillons total</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">{format_duration(total_duration)}</div>
|
||||
<div class="stat-label">Durée totale @ 200Hz</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">{total_errors}</div>
|
||||
<div class="stat-label">Erreurs lecture</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>📡 Résumé par Canal</h2>
|
||||
<div class="channel-summary">
|
||||
"""
|
||||
|
||||
for ch in ['ch0', 'ch1', 'ch2', 'ch3']:
|
||||
stats = channel_stats.get(ch, {'files': 0, 'samples': 0, 'bumpers': set()})
|
||||
duration = stats['samples'] / SAMPLE_RATE
|
||||
html += f"""
|
||||
<div class="channel-box">
|
||||
<h4 class="{ch}">{ch.upper()}</h4>
|
||||
<div><strong>{stats['files']}</strong> fichiers</div>
|
||||
<div><strong>{len(stats['bumpers'])}</strong> bumpers</div>
|
||||
<div><strong>{stats['samples']:,}</strong> samples</div>
|
||||
<div>{format_duration(duration)}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += """
|
||||
</div>
|
||||
|
||||
<h2>📋 Détail par Bumper</h2>
|
||||
|
||||
<div class="filter-controls">
|
||||
<label><input type="checkbox" id="showCh0" checked onchange="filterTable()"> <span class="ch0">CH0</span></label>
|
||||
<label><input type="checkbox" id="showCh1" checked onchange="filterTable()"> <span class="ch1">CH1</span></label>
|
||||
<label><input type="checkbox" id="showCh2" checked onchange="filterTable()"> <span class="ch2">CH2</span></label>
|
||||
<label><input type="checkbox" id="showCh3" checked onchange="filterTable()"> <span class="ch3">CH3</span></label>
|
||||
<label><input type="checkbox" id="showData" checked onchange="filterTable()"> <span class="data">DATA</span></label>
|
||||
<label><input type="checkbox" id="showAux" checked onchange="filterTable()"> <span class="aux">AUX</span></label>
|
||||
</div>
|
||||
|
||||
<table id="mainTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bumper</th>
|
||||
<th>Canal</th>
|
||||
<th>Type</th>
|
||||
<th>Début (epoch)</th>
|
||||
<th>Début (date/heure)</th>
|
||||
<th>Fin (date/heure)</th>
|
||||
<th>Durée</th>
|
||||
<th>Samples</th>
|
||||
<th>Fichier</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
"""
|
||||
|
||||
for bumper in sorted_bumpers:
|
||||
channels = by_bumper[bumper]
|
||||
for channel in sorted(channels.keys()):
|
||||
items = sorted(channels[channel], key=lambda x: x['epoch_time'] or 0)
|
||||
for item in items:
|
||||
duration_sec = item['samples'] / SAMPLE_RATE
|
||||
end_time = (item['epoch_time'] + duration_sec) if item['epoch_time'] else None
|
||||
|
||||
error_html = f'<div class="error">{item["error"]}</div>' if item['error'] else ''
|
||||
|
||||
html += f"""
|
||||
<tr class="row-{channel} row-{item['file_type']}">
|
||||
<td><strong>b{bumper}</strong></td>
|
||||
<td class="{channel}">{channel.upper()}</td>
|
||||
<td class="{item['file_type']}">{item['file_type'].upper()}</td>
|
||||
<td>{item['epoch_time'] or 'N/A'}</td>
|
||||
<td>{format_datetime(item['epoch_time'])}</td>
|
||||
<td>{format_datetime(end_time)}</td>
|
||||
<td>{format_duration(duration_sec)}</td>
|
||||
<td>{item['samples']:,}</td>
|
||||
<td style="font-size: 0.8rem; color: #888;">{item['filename']}{error_html}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
html += """
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
function filterTable() {
|
||||
const showCh0 = document.getElementById('showCh0').checked;
|
||||
const showCh1 = document.getElementById('showCh1').checked;
|
||||
const showCh2 = document.getElementById('showCh2').checked;
|
||||
const showCh3 = document.getElementById('showCh3').checked;
|
||||
const showData = document.getElementById('showData').checked;
|
||||
const showAux = document.getElementById('showAux').checked;
|
||||
|
||||
const rows = document.querySelectorAll('#mainTable tbody tr');
|
||||
rows.forEach(row => {
|
||||
const isCh0 = row.classList.contains('row-ch0');
|
||||
const isCh1 = row.classList.contains('row-ch1');
|
||||
const isCh2 = row.classList.contains('row-ch2');
|
||||
const isCh3 = row.classList.contains('row-ch3');
|
||||
const isData = row.classList.contains('row-data');
|
||||
const isAux = row.classList.contains('row-aux');
|
||||
|
||||
const channelVisible = (isCh0 && showCh0) || (isCh1 && showCh1) ||
|
||||
(isCh2 && showCh2) || (isCh3 && showCh3);
|
||||
const typeVisible = (isData && showData) || (isAux && showAux);
|
||||
|
||||
row.style.display = (channelVisible && typeVisible) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html)
|
||||
|
||||
print(f"\nHTML genere: {output_path}")
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("INVENTAIRE DES FICHIERS HDF5 SISMIQUES")
|
||||
print("=" * 60)
|
||||
|
||||
# Charger l'index existant pour connaître tous les répertoires
|
||||
index_path = Path(r"F:\seismic_webapp\data\index.json")
|
||||
all_dirs = set()
|
||||
|
||||
if index_path.exists():
|
||||
with open(index_path, 'r') as f:
|
||||
index = json.load(f)
|
||||
|
||||
# Récupérer tous les répertoires de dates
|
||||
for node_data in index.get('nodes', {}).values():
|
||||
for files_list in node_data.get('dates', {}).values():
|
||||
# files_list est une liste de fichiers directement
|
||||
if isinstance(files_list, list):
|
||||
for file_info in files_list:
|
||||
file_path = Path(file_info.get('path', ''))
|
||||
if file_path.parent.parent.exists():
|
||||
all_dirs.add(str(file_path.parent.parent))
|
||||
|
||||
# Ajouter les répertoires par défaut
|
||||
for d in DATA_DIRS:
|
||||
if Path(d).exists():
|
||||
all_dirs.add(d)
|
||||
|
||||
print(f"\nRépertoires à scanner: {len(all_dirs)}")
|
||||
for d in sorted(all_dirs):
|
||||
print(f" - {d}")
|
||||
|
||||
# Scanner tous les fichiers
|
||||
inventory = []
|
||||
|
||||
for data_dir in sorted(all_dirs):
|
||||
print(f"\nScanning {data_dir}...")
|
||||
files = scan_directory(data_dir)
|
||||
print(f" Found {len(files)} HDF5 files")
|
||||
|
||||
for i, filepath in enumerate(files):
|
||||
if i % 50 == 0:
|
||||
print(f" Processing {i}/{len(files)}...")
|
||||
|
||||
parsed = parse_filename(filepath.name)
|
||||
hdf5_info = get_hdf5_info(filepath)
|
||||
|
||||
inventory.append({
|
||||
'filepath': str(filepath),
|
||||
'filename': filepath.name,
|
||||
'directory': data_dir,
|
||||
'bumper_id': parsed['bumper_id'],
|
||||
'channel': parsed['channel'],
|
||||
'epoch_time': parsed['epoch_time'],
|
||||
'file_type': parsed['file_type'],
|
||||
'samples': hdf5_info['samples'],
|
||||
'error': hdf5_info['error']
|
||||
})
|
||||
|
||||
print(f"\nTotal: {len(inventory)} fichiers")
|
||||
|
||||
# Générer le HTML
|
||||
output_path = Path(r"F:\seismic_webapp\inventory.html")
|
||||
generate_html(inventory, output_path)
|
||||
|
||||
# Aussi sauvegarder en JSON pour référence
|
||||
json_path = Path(r"F:\seismic_webapp\inventory.json")
|
||||
with open(json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(inventory, f, indent=2, ensure_ascii=False)
|
||||
print(f"JSON genere: {json_path}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
125
scripts/h5_api_server.py
Executable file
125
scripts/h5_api_server.py
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
from flask import Flask, jsonify, request, send_file
|
||||
from flask_cors import CORS
|
||||
import h5py
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
H5_DIR = Path('/home/floppyrj45/docker/seismic-nodes-viewer/data/h5')
|
||||
DOCS_DIR = Path('/home/floppyrj45/docker/seismic-nodes-viewer/data/docs')
|
||||
|
||||
@app.route('/api/h5/files', methods=['GET'])
|
||||
def list_files():
|
||||
try:
|
||||
files = []
|
||||
for h5_file in sorted(H5_DIR.glob('*.h5')):
|
||||
match = re.search(r'rsn(\d+)', h5_file.name)
|
||||
node_id = match.group(1) if match else 'unknown'
|
||||
match_date = re.search(r'_(\d{6})_', h5_file.name)
|
||||
date = match_date.group(1) if match_date else ''
|
||||
files.append({
|
||||
'filename': h5_file.name,
|
||||
'nodeId': node_id,
|
||||
'date': date,
|
||||
'path': str(h5_file)
|
||||
})
|
||||
return jsonify({'files': files, 'count': len(files)})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/h5/data', methods=['GET'])
|
||||
def get_data():
|
||||
try:
|
||||
filename = request.args.get('file')
|
||||
channel = int(request.args.get('channel', 1))
|
||||
start = int(request.args.get('start', 0))
|
||||
duration = int(request.args.get('duration', 10))
|
||||
|
||||
filepath = H5_DIR / filename
|
||||
|
||||
with h5py.File(filepath, 'r') as f:
|
||||
meta = f['metadata']
|
||||
sample_rate = meta.attrs['sample_rate_hz']
|
||||
file_duration = meta.attrs['duration_sec']
|
||||
total_samples = meta.attrs['n_samples']
|
||||
|
||||
dataset = f[f'calibrated_data/channel_{channel}']
|
||||
|
||||
start_idx = int(start * sample_rate) if start > 0 else 0
|
||||
num_samples = int(duration * sample_rate) if duration > 0 else total_samples
|
||||
end_idx = min(start_idx + num_samples, total_samples)
|
||||
|
||||
samples = dataset[start_idx:end_idx]
|
||||
|
||||
unit = 'm/s' if channel in [1, 2, 3] else 'Pa'
|
||||
channel_name = f'Geophone {channel}' if channel in [1, 2, 3] else 'Hydrophone'
|
||||
|
||||
import numpy as np
|
||||
return jsonify({
|
||||
'samples': samples.tolist(),
|
||||
'start_idx': int(start_idx),
|
||||
'end_idx': int(end_idx),
|
||||
'total_samples': int(total_samples),
|
||||
'sample_rate': int(sample_rate),
|
||||
'duration_sec': float(file_duration),
|
||||
'channel': channel,
|
||||
'channel_name': channel_name,
|
||||
'unit': unit,
|
||||
'stats': {
|
||||
'min': float(np.min(samples)),
|
||||
'max': float(np.max(samples)),
|
||||
'mean': float(np.mean(samples)),
|
||||
'std': float(np.std(samples)),
|
||||
'rms': float(np.sqrt(np.mean(samples**2)))
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/docs/manifest', methods=['GET'])
|
||||
def get_manifest():
|
||||
try:
|
||||
manifest_file = DOCS_DIR / 'campaign_manifest.json'
|
||||
with open(manifest_file, 'r') as f:
|
||||
return jsonify(json.load(f))
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/docs/<filename>', methods=['GET'])
|
||||
def get_document(filename):
|
||||
try:
|
||||
doc_file = DOCS_DIR / filename
|
||||
if not doc_file.exists():
|
||||
return jsonify({'error': 'File not found'}), 404
|
||||
return send_file(str(doc_file))
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# === Endpoints pour la carte ===
|
||||
@app.route('/api/nodes', methods=['GET'])
|
||||
def get_nodes():
|
||||
"""Retourne la liste des nodes avec leurs positions"""
|
||||
nodes = [
|
||||
{'id': '80274', 'lat': 43.40, 'lon': 3.70, 'name': 'Node 80274'},
|
||||
{'id': '2221', 'lat': 43.41, 'lon': 3.71, 'name': 'Node 2221'},
|
||||
{'id': '3541', 'lat': 43.39, 'lon': 3.69, 'name': 'Node 3541'},
|
||||
]
|
||||
return jsonify(nodes)
|
||||
|
||||
@app.route('/api/dates', methods=['GET'])
|
||||
def get_dates():
|
||||
"""Retourne les dates disponibles"""
|
||||
return jsonify(['2020-08-08', '2020-08-09', '2020-08-10'])
|
||||
|
||||
@app.route('/api/migration-status', methods=['GET'])
|
||||
def migration_status():
|
||||
"""Status de migration (désactivé)"""
|
||||
return jsonify({'status': 'complete'})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=3004, debug=False)
|
||||
64
scripts/index_h5_2026.py
Executable file
64
scripts/index_h5_2026.py
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Indexation des fichiers H5 format 2026 avec métadonnées complètes.
|
||||
Génère un index JSON pour le viewer web.
|
||||
"""
|
||||
|
||||
import h5py
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
H5_DIR = Path('/home/floppyrj45/docker/seismic-nodes-viewer/data/h5')
|
||||
OUTPUT = Path('/home/floppyrj45/docker/seismic-nodes-viewer/data/h5_index.json')
|
||||
|
||||
def index_h5_files():
|
||||
files = []
|
||||
|
||||
for h5_file in sorted(H5_DIR.glob('*.h5')):
|
||||
try:
|
||||
with h5py.File(h5_file, 'r') as f:
|
||||
meta = f['metadata']
|
||||
|
||||
# Extraire node ID du nom de fichier (rsn[0-9]+)
|
||||
import re
|
||||
match = re.search(r'rsn(\d+)', h5_file.name)
|
||||
node_id = match.group(1) if match else 'unknown'
|
||||
|
||||
# Extraire date du nom (YYMMDD)
|
||||
match_date = re.search(r'_(\d{6})_', h5_file.name)
|
||||
date_str = match_date.group(1) if match_date else ''
|
||||
|
||||
files.append({
|
||||
'filename': h5_file.name,
|
||||
'path': str(h5_file),
|
||||
'node_id': node_id,
|
||||
'date': date_str,
|
||||
'duration_sec': float(meta.attrs['duration_sec']),
|
||||
'sample_rate': int(meta.attrs['sample_rate_hz']),
|
||||
'channels': int(meta.attrs['n_channels']),
|
||||
'samples': int(meta.attrs['n_samples']),
|
||||
'size_mb': round(h5_file.stat().st_size / (1024*1024), 2),
|
||||
'channel_info': [
|
||||
{'id': 1, 'type': 'geophone', 'unit': 'm/s', 'name': 'Geophone 1'},
|
||||
{'id': 2, 'type': 'geophone', 'unit': 'm/s', 'name': 'Geophone 2'},
|
||||
{'id': 3, 'type': 'geophone', 'unit': 'm/s', 'name': 'Geophone 3'},
|
||||
{'id': 4, 'type': 'hydrophone', 'unit': 'Pa', 'name': 'Hydrophone'}
|
||||
]
|
||||
})
|
||||
except Exception as e:
|
||||
print(f'Error indexing {h5_file.name}: {e}')
|
||||
|
||||
index = {
|
||||
'generated': datetime.now().isoformat(),
|
||||
'total_files': len(files),
|
||||
'total_duration_hours': sum(f['duration_sec'] for f in files) / 3600,
|
||||
'files': files
|
||||
}
|
||||
|
||||
OUTPUT.write_text(json.dumps(index, indent=2))
|
||||
print(f'✅ Indexed {len(files)} files → {OUTPUT}')
|
||||
print(f'📊 Total duration: {index["total_duration_hours"]:.1f} hours')
|
||||
|
||||
if __name__ == '__main__':
|
||||
index_h5_files()
|
||||
231
scripts/index_h5_files.py
Executable file
231
scripts/index_h5_files.py
Executable file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Script d'indexation des fichiers HDF5 sismiques.
|
||||
Parcourt les dossiers de données, extrait les métadonnées (node_id, date, canaux)
|
||||
et génère un index JSON utilisé par l'API backend.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import csv
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any
|
||||
|
||||
# Pattern pour extraire les infos du nom de fichier
|
||||
# Exemple: auto_256_070617_b67_14_025708_data_rsn6027_seq1_ch0_1599057453.h5
|
||||
# ou: auto_255_125334_b4_rsn13696_seq1_1599045513.h5
|
||||
FILENAME_PATTERN = re.compile(
|
||||
r'auto_(\d+)_(\d{6})_b(\d+).*?_(\d{10})\.h5$',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
# Dossiers racine contenant les données H5
|
||||
DATA_ROOTS = [
|
||||
Path(r"F:\2020-09-12"),
|
||||
Path(r"F:\2020-09-13"),
|
||||
Path(r"F:\2020-09-14"),
|
||||
Path(r"F:\2020-09-15"),
|
||||
Path(r"F:\2020-09-16"),
|
||||
Path(r"F:\2020-09-17"),
|
||||
Path(r"F:\2020-09-18"),
|
||||
Path(r"F:\2020-09-19"),
|
||||
Path(r"F:\2020-09-21"),
|
||||
Path(r"F:\2020-09-22"),
|
||||
Path(r"F:\2020-09-23"),
|
||||
]
|
||||
|
||||
# Fichier CSV des positions
|
||||
POSITIONS_CSV = Path(r"F:\Copie de SETE_AUV_DARFV4-Copier(1).csv")
|
||||
|
||||
# Sortie
|
||||
OUTPUT_INDEX = Path(r"F:\seismic_webapp\data\index.json")
|
||||
|
||||
|
||||
def load_node_positions(csv_path: Path) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Charge les positions des nodes depuis le CSV.
|
||||
Retourne un dict: node_id -> {easting, northing, depth, ...}
|
||||
"""
|
||||
positions = {}
|
||||
|
||||
with open(csv_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
# Sauter les premières lignes d'en-tête (lignes 1-4)
|
||||
lines = f.readlines()
|
||||
|
||||
# La ligne 4 (index 3) contient les vrais en-têtes
|
||||
if len(lines) < 5:
|
||||
return positions
|
||||
|
||||
header_line = lines[3]
|
||||
headers = header_line.strip().split(',')
|
||||
|
||||
# Trouver les indices des colonnes importantes
|
||||
# Utiliser Aslaid (positions réelles mesurées) plutôt que Preplot (planifiées)
|
||||
try:
|
||||
node_code_idx = headers.index('NodeCode')
|
||||
# Priorité aux positions Aslaid (réelles), sinon Preplot (planifiées)
|
||||
if 'Aslaid Easting' in headers:
|
||||
easting_idx = headers.index('Aslaid Easting')
|
||||
northing_idx = headers.index('Aslaid Northing')
|
||||
depth_idx = headers.index('Aslaid Depth') if 'Aslaid Depth' in headers else None
|
||||
print("Utilisation des coordonnées Aslaid (positions réelles)")
|
||||
else:
|
||||
easting_idx = headers.index('Preplot Easting')
|
||||
northing_idx = headers.index('Preplot Northing')
|
||||
depth_idx = headers.index('Preplot Depth') if 'Preplot Depth' in headers else None
|
||||
print("Utilisation des coordonnées Preplot (positions planifiées)")
|
||||
except ValueError as e:
|
||||
print(f"Colonne manquante dans le CSV: {e}")
|
||||
# Fallback sur indices connus (Aslaid)
|
||||
node_code_idx = 3
|
||||
easting_idx = 9 # Aslaid Easting
|
||||
northing_idx = 10 # Aslaid Northing
|
||||
depth_idx = 11 # Aslaid Depth
|
||||
|
||||
# Parser les lignes de données (à partir de la ligne 5)
|
||||
for line in lines[4:]:
|
||||
parts = line.strip().split(',')
|
||||
if len(parts) <= max(node_code_idx, easting_idx, northing_idx):
|
||||
continue
|
||||
|
||||
node_code = parts[node_code_idx].strip()
|
||||
if not node_code or node_code == '':
|
||||
continue
|
||||
|
||||
try:
|
||||
easting = float(parts[easting_idx]) if parts[easting_idx] else None
|
||||
northing = float(parts[northing_idx]) if parts[northing_idx] else None
|
||||
depth = float(parts[depth_idx]) if depth_idx and parts[depth_idx] else 0.0
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
if easting and northing:
|
||||
positions[node_code] = {
|
||||
'easting': easting,
|
||||
'northing': northing,
|
||||
'depth': depth,
|
||||
}
|
||||
|
||||
print(f"Chargé {len(positions)} positions de nodes")
|
||||
return positions
|
||||
|
||||
|
||||
def scan_h5_files(data_roots: List[Path]) -> Dict[str, Any]:
|
||||
"""
|
||||
Parcourt les dossiers et indexe tous les fichiers H5.
|
||||
Retourne un dict structuré par node_id -> date -> fichiers
|
||||
"""
|
||||
index = {}
|
||||
file_count = 0
|
||||
|
||||
for root in data_roots:
|
||||
if not root.exists():
|
||||
print(f"Dossier non trouvé: {root}")
|
||||
continue
|
||||
|
||||
print(f"Scan de {root}...")
|
||||
|
||||
for h5_file in root.rglob("*.h5"):
|
||||
match = FILENAME_PATTERN.search(h5_file.name)
|
||||
if not match:
|
||||
# Essayer un pattern plus simple
|
||||
simple_match = re.search(r'_b(\d+)_.*?(\d{10})\.h5$', h5_file.name, re.IGNORECASE)
|
||||
if simple_match:
|
||||
node_id = simple_match.group(1)
|
||||
timestamp = int(simple_match.group(2))
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
node_id = match.group(3)
|
||||
timestamp = int(match.group(4))
|
||||
|
||||
# Convertir timestamp en date
|
||||
dt = datetime.fromtimestamp(timestamp)
|
||||
date_str = dt.strftime('%Y-%m-%d')
|
||||
|
||||
# Détecter les canaux disponibles dans le fichier
|
||||
# Pour l'instant on suppose ch0-ch3 par défaut
|
||||
channels = ['ch0', 'ch1', 'ch2', 'ch3']
|
||||
|
||||
# Structure: node_id -> date -> liste de fichiers
|
||||
if node_id not in index:
|
||||
index[node_id] = {}
|
||||
|
||||
if date_str not in index[node_id]:
|
||||
index[node_id][date_str] = []
|
||||
|
||||
index[node_id][date_str].append({
|
||||
'path': str(h5_file),
|
||||
'timestamp': timestamp,
|
||||
'channels': channels,
|
||||
'size_bytes': h5_file.stat().st_size if h5_file.exists() else 0
|
||||
})
|
||||
|
||||
file_count += 1
|
||||
|
||||
print(f"Indexé {file_count} fichiers H5")
|
||||
return index
|
||||
|
||||
|
||||
def build_full_index(positions: Dict, files_index: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
Combine les positions et l'index des fichiers.
|
||||
"""
|
||||
full_index = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'sample_rate_hz': 200,
|
||||
'nodes': {},
|
||||
'dates': set(),
|
||||
}
|
||||
|
||||
# Fusionner les données
|
||||
all_node_ids = set(files_index.keys()) | set(positions.keys())
|
||||
|
||||
for node_id in all_node_ids:
|
||||
node_data = {
|
||||
'id': node_id,
|
||||
'position': positions.get(node_id, None),
|
||||
'dates': {}
|
||||
}
|
||||
|
||||
if node_id in files_index:
|
||||
node_data['dates'] = files_index[node_id]
|
||||
for date_str in files_index[node_id].keys():
|
||||
full_index['dates'].add(date_str)
|
||||
|
||||
full_index['nodes'][node_id] = node_data
|
||||
|
||||
# Convertir le set en liste triée
|
||||
full_index['dates'] = sorted(list(full_index['dates']))
|
||||
|
||||
return full_index
|
||||
|
||||
|
||||
def main():
|
||||
print("=== Indexation des fichiers HDF5 sismiques ===\n")
|
||||
|
||||
# 1. Charger les positions
|
||||
print("1. Chargement des positions des nodes...")
|
||||
positions = load_node_positions(POSITIONS_CSV)
|
||||
|
||||
# 2. Scanner les fichiers H5
|
||||
print("\n2. Scan des fichiers H5...")
|
||||
files_index = scan_h5_files(DATA_ROOTS)
|
||||
|
||||
# 3. Construire l'index complet
|
||||
print("\n3. Construction de l'index...")
|
||||
full_index = build_full_index(positions, files_index)
|
||||
|
||||
# 4. Sauvegarder
|
||||
print(f"\n4. Sauvegarde vers {OUTPUT_INDEX}...")
|
||||
OUTPUT_INDEX.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(OUTPUT_INDEX, 'w', encoding='utf-8') as f:
|
||||
json.dump(full_index, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"\nTerminé! Index généré avec {len(full_index['nodes'])} nodes et {len(full_index['dates'])} dates.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
87
scripts/index_time_ranges.py
Normal file
87
scripts/index_time_ranges.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import os, re, json, h5py
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
|
||||
DATA_ROOTS = [Path("/mnt/kingston"), Path("/mnt/data_sdb1")]
|
||||
POSITIONS_CSV = Path("/mnt/kingston/Copie de SETE_AUV_DARFV4-Copier(1).csv")
|
||||
OUTPUT_INDEX = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
SAMPLE_RATE = 200
|
||||
|
||||
def load_pos():
|
||||
positions = {}
|
||||
if not POSITIONS_CSV.exists(): return {}
|
||||
with open(POSITIONS_CSV, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) < 5: return {}
|
||||
headers = lines[3].strip().split(',')
|
||||
try:
|
||||
ni = headers.index('NodeCode')
|
||||
ei = headers.index('Aslaid Easting') if 'Aslaid Easting' in headers else headers.index('Preplot Easting')
|
||||
oi = headers.index('Aslaid Northing') if 'Aslaid Northing' in headers else headers.index('Preplot Northing')
|
||||
di = headers.index('Aslaid Depth') if 'Aslaid Depth' in headers else -1
|
||||
except: return {}
|
||||
for line in lines[4:]:
|
||||
parts = line.strip().split(',')
|
||||
try:
|
||||
nid = parts[ni].strip()
|
||||
positions[nid] = {
|
||||
'easting': float(parts[ei]),
|
||||
'northing': float(parts[oi]),
|
||||
'depth': float(parts[di]) if di != -1 else 0.0
|
||||
}
|
||||
except: continue
|
||||
return positions
|
||||
|
||||
def scan():
|
||||
pos = load_pos()
|
||||
index = {}
|
||||
file_count = 0
|
||||
print(f"Scanning H5 files... Positions loaded: {len(pos)}")
|
||||
|
||||
all_files = []
|
||||
for root in DATA_ROOTS:
|
||||
all_files.extend(list(root.rglob("*.h5")))
|
||||
|
||||
for h5_path in tqdm(all_files):
|
||||
try:
|
||||
match = re.search(r'_b(\d+)_', h5_path.name)
|
||||
if not match: continue
|
||||
nid = match.group(1)
|
||||
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
if 'adc_values' not in f: continue
|
||||
ds = f['adc_values']
|
||||
start_ts = int(ds.attrs.get('timestamp', 0))
|
||||
if start_ts == 0: continue
|
||||
|
||||
duration = ds.shape[0] / SAMPLE_RATE
|
||||
end_ts = start_ts + duration
|
||||
|
||||
if nid not in index:
|
||||
index[nid] = {
|
||||
'id': nid,
|
||||
'position': pos.get(nid),
|
||||
'files': []
|
||||
}
|
||||
|
||||
index[nid]['files'].append({
|
||||
'path': str(h5_path),
|
||||
'start': start_ts,
|
||||
'end': end_ts,
|
||||
'channels': ['ch0', 'ch1', 'ch2', 'ch3']
|
||||
})
|
||||
file_count += 1
|
||||
except: continue
|
||||
|
||||
# Sauvegarder l'index
|
||||
with open(OUTPUT_INDEX, 'w') as f:
|
||||
json.dump({
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'sample_rate_hz': SAMPLE_RATE,
|
||||
'nodes': index
|
||||
}, f)
|
||||
|
||||
print(f"Index généré: {file_count} fichiers, {len(index)} nodes avec positions.")
|
||||
|
||||
if __name__ == '__main__': scan()
|
||||
105
scripts/index_ultimate.py
Normal file
105
scripts/index_ultimate.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import os, re, json, h5py
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from tqdm import tqdm
|
||||
|
||||
DATA_ROOTS = [Path("/mnt/kingston"), Path("/mnt/data_sdb1")]
|
||||
POSITIONS_CSV = Path("/mnt/kingston/Copie de SETE_AUV_DARFV4-Copier(1).csv")
|
||||
OUTPUT_INDEX = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
SAMPLE_RATE = 200
|
||||
|
||||
def load_pos():
|
||||
positions = {}
|
||||
if not POSITIONS_CSV.exists(): return {}
|
||||
with open(POSITIONS_CSV, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) < 5: return {}
|
||||
headers = lines[3].strip().split(',')
|
||||
try:
|
||||
ni = headers.index('NodeCode')
|
||||
ei = headers.index('Aslaid Easting') if 'Aslaid Easting' in headers else headers.index('Preplot Easting')
|
||||
oi = headers.index('Aslaid Northing') if 'Aslaid Northing' in headers else headers.index('Preplot Northing')
|
||||
except: return {}
|
||||
for line in lines[4:]:
|
||||
parts = line.strip().split(',')
|
||||
try:
|
||||
nid = parts[ni].strip()
|
||||
positions[nid] = {
|
||||
'easting': float(parts[ei]),
|
||||
'northing': float(parts[oi]),
|
||||
'depth': float(parts[headers.index('Aslaid Depth')]) if 'Aslaid Depth' in headers else 0.0
|
||||
}
|
||||
except: continue
|
||||
return positions
|
||||
|
||||
def scan():
|
||||
pos = load_pos()
|
||||
nodes = {}
|
||||
all_dates = set()
|
||||
file_count = 0
|
||||
|
||||
print("🔍 Scanning ONLY 'data' H5 files (ignoring 'aux')...")
|
||||
all_h5_files = []
|
||||
for root in DATA_ROOTS:
|
||||
all_h5_files.extend(list(root.rglob("*.h5")))
|
||||
|
||||
for h5_path in tqdm(all_h5_files):
|
||||
# FILTRE : Uniquement les fichiers contenant "data"
|
||||
if "_data_" not in h5_path.name.lower():
|
||||
continue
|
||||
|
||||
try:
|
||||
match = re.search(r'auto_(\d+)_(\d{6})_b(\d+)_.*?_(\d{10})\.h5$', h5_path.name)
|
||||
if not match: continue
|
||||
|
||||
julian_day = int(match.group(1))
|
||||
time_str = match.group(2)
|
||||
node_id = match.group(3)
|
||||
|
||||
date_ref = datetime(2020, 1, 1) + timedelta(days=julian_day - 1)
|
||||
date_str = date_ref.strftime('%Y-%m-%d')
|
||||
|
||||
h, m, s = int(time_str[:2]), int(time_str[2:4]), int(time_str[4:6])
|
||||
actual_start_ts = int(datetime(2020, 1, 1).timestamp() + (julian_day - 1) * 86400 + h * 3600 + m * 60 + s)
|
||||
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
if 'adc_values' not in f: continue
|
||||
duration = f['adc_values'].shape[0] / SAMPLE_RATE
|
||||
actual_end_ts = actual_start_ts + duration
|
||||
|
||||
all_dates.add(date_str)
|
||||
|
||||
if node_id not in nodes:
|
||||
nodes[node_id] = {
|
||||
'id': node_id,
|
||||
'position': pos.get(node_id),
|
||||
'files': []
|
||||
}
|
||||
|
||||
# On extrait le canal du nom de fichier pour un matching plus précis
|
||||
channel_match = re.search(r'_ch(\d+)_', h5_path.name)
|
||||
channel = f"ch{channel_match.group(1)}" if channel_match else "ch0"
|
||||
|
||||
nodes[node_id]['files'].append({
|
||||
'path': str(h5_path),
|
||||
'start': actual_start_ts,
|
||||
'end': actual_end_ts,
|
||||
'julian': julian_day,
|
||||
'channel': channel # Canal spécifique au fichier
|
||||
})
|
||||
file_count += 1
|
||||
except: continue
|
||||
|
||||
result = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'sample_rate_hz': SAMPLE_RATE,
|
||||
'nodes': nodes,
|
||||
'dates': sorted(list(all_dates))
|
||||
}
|
||||
|
||||
with open(OUTPUT_INDEX, 'w') as f:
|
||||
json.dump(result, f, indent=2)
|
||||
|
||||
print(f"✅ Index updated: {file_count} 'data' files, {len(nodes)} nodes.")
|
||||
|
||||
if __name__ == '__main__': scan()
|
||||
158
scripts/inventory_h5.py
Executable file
158
scripts/inventory_h5.py
Executable file
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
Script d'inventaire des fichiers HDF5.
|
||||
Extrait les timestamps des noms de fichiers et génère un rapport.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
# Dossiers racine
|
||||
DATA_ROOTS = [
|
||||
Path(r"F:\2020-09-12"),
|
||||
Path(r"F:\2020-09-13"),
|
||||
Path(r"F:\2020-09-14"),
|
||||
Path(r"F:\2020-09-15"),
|
||||
Path(r"F:\2020-09-16"),
|
||||
Path(r"F:\2020-09-17"),
|
||||
Path(r"F:\2020-09-18"),
|
||||
Path(r"F:\2020-09-19"),
|
||||
Path(r"F:\2020-09-21"),
|
||||
Path(r"F:\2020-09-22"),
|
||||
Path(r"F:\2020-09-23"),
|
||||
]
|
||||
|
||||
# Pattern pour extraire node_id et timestamp
|
||||
# Exemple: auto_256_070617_b67_14_025708_data_rsn6027_seq1_ch0_1599057453.h5
|
||||
PATTERN = re.compile(r'_b(\d+)_.*?(\d{10})\.h5$', re.IGNORECASE)
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("INVENTAIRE DES FICHIERS HDF5")
|
||||
print("=" * 70)
|
||||
|
||||
# Structure: folder -> node_id -> list of (timestamp, filename, type)
|
||||
inventory = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
# Stats globales
|
||||
total_files = 0
|
||||
total_size = 0
|
||||
nodes_set = set()
|
||||
timestamps_set = set()
|
||||
|
||||
for root in DATA_ROOTS:
|
||||
if not root.exists():
|
||||
continue
|
||||
|
||||
folder_name = root.name
|
||||
|
||||
for h5_file in root.rglob("*.h5"):
|
||||
match = PATTERN.search(h5_file.name)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
node_id = match.group(1)
|
||||
timestamp = int(match.group(2))
|
||||
|
||||
# Déterminer le type (data ou aux)
|
||||
file_type = "data" if "_data_" in h5_file.name else "aux" if "_aux_" in h5_file.name else "unknown"
|
||||
|
||||
# Extraire le channel si présent
|
||||
ch_match = re.search(r'_ch(\d+)_', h5_file.name)
|
||||
channel = f"ch{ch_match.group(1)}" if ch_match else "?"
|
||||
|
||||
file_size = h5_file.stat().st_size
|
||||
|
||||
inventory[folder_name][node_id].append({
|
||||
'timestamp': timestamp,
|
||||
'datetime': datetime.fromtimestamp(timestamp),
|
||||
'type': file_type,
|
||||
'channel': channel,
|
||||
'filename': h5_file.name,
|
||||
'size': file_size
|
||||
})
|
||||
|
||||
total_files += 1
|
||||
total_size += file_size
|
||||
nodes_set.add(node_id)
|
||||
timestamps_set.add(timestamp)
|
||||
|
||||
# Rapport par dossier
|
||||
print(f"\n{'DOSSIER':<15} {'NODES':<10} {'FICHIERS':<10} {'TAILLE':<15}")
|
||||
print("-" * 50)
|
||||
|
||||
for folder in sorted(inventory.keys()):
|
||||
folder_data = inventory[folder]
|
||||
n_nodes = len(folder_data)
|
||||
n_files = sum(len(files) for files in folder_data.values())
|
||||
folder_size = sum(f['size'] for files in folder_data.values() for f in files)
|
||||
print(f"{folder:<15} {n_nodes:<10} {n_files:<10} {folder_size / 1e9:.2f} GB")
|
||||
|
||||
# Stats globales
|
||||
print("\n" + "=" * 70)
|
||||
print("STATISTIQUES GLOBALES")
|
||||
print("=" * 70)
|
||||
print(f"Fichiers H5 totaux: {total_files}")
|
||||
print(f"Taille totale: {total_size / 1e9:.2f} GB")
|
||||
print(f"Nodes uniques: {len(nodes_set)}")
|
||||
|
||||
# Plage temporelle
|
||||
if timestamps_set:
|
||||
min_ts = min(timestamps_set)
|
||||
max_ts = max(timestamps_set)
|
||||
print(f"\nPlage temporelle des données:")
|
||||
print(f" Début: {datetime.fromtimestamp(min_ts)} (timestamp: {min_ts})")
|
||||
print(f" Fin: {datetime.fromtimestamp(max_ts)} (timestamp: {max_ts})")
|
||||
|
||||
# Détail par node (top 20)
|
||||
print("\n" + "=" * 70)
|
||||
print("DETAIL PAR NODE (nodes avec le plus de fichiers)")
|
||||
print("=" * 70)
|
||||
|
||||
# Agréger par node
|
||||
node_stats = defaultdict(lambda: {'files': 0, 'size': 0, 'timestamps': set(), 'folders': set()})
|
||||
|
||||
for folder, folder_data in inventory.items():
|
||||
for node_id, files in folder_data.items():
|
||||
node_stats[node_id]['files'] += len(files)
|
||||
node_stats[node_id]['size'] += sum(f['size'] for f in files)
|
||||
node_stats[node_id]['timestamps'].update(f['timestamp'] for f in files)
|
||||
node_stats[node_id]['folders'].add(folder)
|
||||
|
||||
# Trier par nombre de fichiers
|
||||
sorted_nodes = sorted(node_stats.items(), key=lambda x: x[1]['files'], reverse=True)
|
||||
|
||||
print(f"\n{'NODE':<8} {'FICHIERS':<10} {'TAILLE':<12} {'DATES':<25} {'DOSSIERS'}")
|
||||
print("-" * 90)
|
||||
|
||||
for node_id, stats in sorted_nodes[:30]:
|
||||
ts_list = sorted(stats['timestamps'])
|
||||
if ts_list:
|
||||
date_range = f"{datetime.fromtimestamp(ts_list[0]).strftime('%Y-%m-%d %H:%M')} -> {datetime.fromtimestamp(ts_list[-1]).strftime('%H:%M')}"
|
||||
else:
|
||||
date_range = "N/A"
|
||||
|
||||
folders = ", ".join(sorted(stats['folders']))
|
||||
print(f"b{node_id:<7} {stats['files']:<10} {stats['size']/1e6:.1f} MB {date_range:<25} {folders}")
|
||||
|
||||
# Dates uniques (jours)
|
||||
print("\n" + "=" * 70)
|
||||
print("JOURS DE DONNEES DISPONIBLES (basé sur timestamps)")
|
||||
print("=" * 70)
|
||||
|
||||
days = set()
|
||||
for ts in timestamps_set:
|
||||
days.add(datetime.fromtimestamp(ts).strftime('%Y-%m-%d'))
|
||||
|
||||
for day in sorted(days):
|
||||
# Compter les fichiers pour ce jour
|
||||
day_files = sum(1 for ts in timestamps_set
|
||||
if datetime.fromtimestamp(ts).strftime('%Y-%m-%d') == day)
|
||||
print(f" {day}: ~{day_files} timestamps uniques")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
45
scripts/migrate_all.py
Normal file
45
scripts/migrate_all.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import json, psycopg2, os
|
||||
from pathlib import Path
|
||||
from migrate_to_db import migrate_file
|
||||
|
||||
INDEX_PATH = "/mnt/kingston/seismic_webapp/data/index.json"
|
||||
DB_URL = "postgresql://postgres:seismic_pass@db:5432/seismic_data"
|
||||
|
||||
def update_status(processed, total, current):
|
||||
try:
|
||||
conn = psycopg2.connect(DB_URL)
|
||||
cur = conn.cursor()
|
||||
cur.execute("UPDATE migration_status SET processed_files = %s, total_files = %s, current_file = %s, last_update = NOW() WHERE id = 1", (processed, total, current))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Status update error: {e}")
|
||||
|
||||
def main():
|
||||
with open(INDEX_PATH, 'r') as f:
|
||||
index = json.load(f)
|
||||
|
||||
nodes = index.get('nodes', {})
|
||||
all_files = []
|
||||
for nid, node in nodes.items():
|
||||
for f in node.get('files', []):
|
||||
if '_data_' in f['path']:
|
||||
all_files.append((nid, f))
|
||||
|
||||
total = len(all_files)
|
||||
print(f"Starting migration for {total} files...")
|
||||
|
||||
for i, (nid, f) in enumerate(all_files):
|
||||
filename = os.path.basename(f['path'])
|
||||
update_status(i, total, filename)
|
||||
try:
|
||||
# Migration de 1h de chaque fichier
|
||||
migrate_file(f['path'], nid, f.get('channel', 'ch0'), duration_sec=3600)
|
||||
except Exception as e:
|
||||
print(f"Error migrating {filename}: {e}")
|
||||
|
||||
update_status(total, total, "Terminé")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
53
scripts/migrate_to_db.py
Normal file
53
scripts/migrate_to_db.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import h5py
|
||||
import numpy as np
|
||||
import psycopg2
|
||||
from psycopg2.extras import execute_values
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import os
|
||||
from tqdm import tqdm
|
||||
|
||||
DB_URL = "postgresql://postgres:seismic_pass@db:5432/seismic_data"
|
||||
|
||||
def fix_path(p):
|
||||
p = p.replace('\\', '/')
|
||||
if p.startswith('F:/'): return '/mnt/kingston/' + p[3:]
|
||||
if p.startswith('E:/'): return '/mnt/data_sdb1/' + p[3:]
|
||||
return p
|
||||
|
||||
def migrate_file(h5_path, node_id, channel, start_offset_sec=0, duration_sec=3600):
|
||||
h5_path = fix_path(h5_path)
|
||||
conn = psycopg2.connect(DB_URL)
|
||||
cur = conn.cursor()
|
||||
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
ds = f['adc_values']
|
||||
start_ts = int(ds.attrs['timestamp'])
|
||||
# On calcule le début réel
|
||||
actual_start = start_ts + start_offset_sec
|
||||
start_idx = start_offset_sec * 200
|
||||
end_idx = start_idx + (duration_sec * 200)
|
||||
|
||||
data = ds[start_idx:end_idx]
|
||||
print(f"Migrating {len(data)} samples...")
|
||||
|
||||
# Préparation des tuples pour insertion par lots
|
||||
batch_size = 10000
|
||||
for i in range(0, len(data), batch_size):
|
||||
batch = data[i:i+batch_size]
|
||||
values = []
|
||||
for j, val in enumerate(batch):
|
||||
ts = datetime.fromtimestamp(actual_start + (i + j) / 200, tz=timezone.utc)
|
||||
values.append((ts, node_id, channel, float(val)))
|
||||
|
||||
execute_values(cur, "INSERT INTO adc_samples (time, node_id, channel, value) VALUES %s", values)
|
||||
conn.commit()
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
print("Done.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test sur Node 193, 1er septembre (Julian 245), 10 minutes
|
||||
# On cherche un fichier du node 193
|
||||
import sys
|
||||
migrate_file(sys.argv[1], sys.argv[2], sys.argv[3], duration_sec=600)
|
||||
62
scripts/precompute_all.py
Normal file
62
scripts/precompute_all.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import json, sys, os, numpy as np, h5py, re
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from tqdm import tqdm
|
||||
|
||||
INDEX_PATH = Path("/mnt/kingston/seismic_webapp/data/index.json")
|
||||
OUTPUT_DIR = Path("/mnt/kingston/seismic_webapp/data/rms_cache")
|
||||
SAMPLE_RATE = 200
|
||||
|
||||
def fix_path(p):
|
||||
p = p.replace('\\', '/')
|
||||
if p.startswith('F:/'): return '/mnt/kingston/' + p[3:]
|
||||
if p.startswith('E:/'): return '/mnt/data_sdb1/' + p[3:]
|
||||
return p
|
||||
|
||||
def compute_rms(h5_path):
|
||||
h5_path = fix_path(h5_path)
|
||||
if not os.path.exists(h5_path): return None
|
||||
try:
|
||||
# Extraire le timestamp réel du nom de fichier (Julian day)
|
||||
match = re.search(r'auto_(\d+)_(\d{6})_b', os.path.basename(h5_path))
|
||||
if not match: return None
|
||||
julian, time_str = int(match.group(1)), match.group(2)
|
||||
h, m, s = int(time_str[:2]), int(time_str[2:4]), int(time_str[4:6])
|
||||
start_ts = int(datetime(2020, 1, 1).timestamp() + (julian - 1) * 86400 + h * 3600 + m * 60 + s)
|
||||
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
ds = f['adc_values']
|
||||
samples = ds[0:5000]
|
||||
rms = float(np.sqrt(np.mean(samples.astype(np.float64)**2)))
|
||||
return [{'ts': start_ts, 'rms': rms}]
|
||||
except: return None
|
||||
|
||||
def main():
|
||||
with open(INDEX_PATH, 'r') as f: index = json.load(f)
|
||||
nodes = index.get('nodes', {})
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for date in index['dates']:
|
||||
channel = "ch0"
|
||||
output_file = OUTPUT_DIR / f"rms_{{date}}_{{channel}}.json"
|
||||
print(f"Processing {date}...")
|
||||
results = {}
|
||||
for nid, node in tqdm(nodes.items(), desc=f"Nodes {date}"):
|
||||
files = node.get('files', [])
|
||||
# Filtrer les fichiers par Julian Day correspondant à la date
|
||||
dt = datetime.strptime(date, '%Y-%m-%d')
|
||||
target_julian = dt.timetuple().tm_yday
|
||||
|
||||
target = next((f for f in files if f['julian'] == target_julian and f'_{{channel}}_' in f['path']), None)
|
||||
if not target and files:
|
||||
target = next((f for f in files if f['julian'] == target_julian), None)
|
||||
|
||||
if target:
|
||||
data = compute_rms(target['path'])
|
||||
if data: results[nid] = data
|
||||
|
||||
if results:
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump({'date':date, 'channel':channel, 'nodes':results}, f)
|
||||
|
||||
if __name__ == '__main__': main()
|
||||
189
scripts/precompute_rms.py
Executable file
189
scripts/precompute_rms.py
Executable file
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
Pré-calcul des valeurs RMS ADC pour tous les nodes.
|
||||
Génère un fichier JSON avec les RMS à intervalles réguliers pour une lecture rapide.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any
|
||||
import numpy as np
|
||||
import h5py
|
||||
from tqdm import tqdm
|
||||
|
||||
# Configuration
|
||||
SAMPLE_RATE = 200 # Hz
|
||||
RMS_INTERVAL_SEC = 60 # Calculer RMS toutes les 60 secondes (plus rapide)
|
||||
RMS_WINDOW_SEC = 5 # Fenêtre de calcul RMS (5 secondes = 1000 samples)
|
||||
|
||||
INDEX_PATH = Path(r"F:\seismic_webapp\data\index.json")
|
||||
OUTPUT_DIR = Path(r"F:\seismic_webapp\data\rms_cache")
|
||||
|
||||
|
||||
def compute_rms_for_file(h5_path: str, interval_sec: int = RMS_INTERVAL_SEC, window_sec: int = RMS_WINDOW_SEC, max_duration_sec: int = 3600) -> List[Dict]:
|
||||
"""
|
||||
Calcule les valeurs RMS à intervalles réguliers pour un fichier HDF5.
|
||||
Retourne une liste de {timestamp, rms}
|
||||
|
||||
max_duration_sec: Limite à traiter (en secondes) pour accélérer
|
||||
"""
|
||||
results = []
|
||||
|
||||
try:
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
if 'adc_values' not in f:
|
||||
return results
|
||||
|
||||
dataset = f['adc_values']
|
||||
total_samples = dataset.shape[0]
|
||||
|
||||
# Récupérer le timestamp de début
|
||||
start_ts = None
|
||||
if 'timestamp' in dataset.attrs:
|
||||
start_ts = int(dataset.attrs['timestamp'])
|
||||
|
||||
if start_ts is None:
|
||||
return results
|
||||
|
||||
# Calculer RMS à intervalles réguliers
|
||||
window_samples = window_sec * SAMPLE_RATE
|
||||
interval_samples = interval_sec * SAMPLE_RATE
|
||||
|
||||
# Limiter la durée pour accélérer
|
||||
max_samples = min(total_samples, max_duration_sec * SAMPLE_RATE)
|
||||
|
||||
for idx in range(0, max_samples - window_samples, interval_samples):
|
||||
# Lire uniquement la fenêtre nécessaire
|
||||
samples = dataset[idx:idx + window_samples]
|
||||
|
||||
# Calculer RMS
|
||||
rms = float(np.sqrt(np.mean(samples.astype(np.float64) ** 2)))
|
||||
|
||||
# Timestamp pour ce point
|
||||
ts = start_ts + (idx // SAMPLE_RATE)
|
||||
|
||||
results.append({
|
||||
'ts': ts,
|
||||
'rms': rms
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erreur lecture {h5_path}: {e}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def precompute_for_date(index: Dict, date: str, channel: str = 'ch0') -> Dict[str, List[Dict]]:
|
||||
"""
|
||||
Pré-calcule les RMS pour tous les nodes pour une date donnée.
|
||||
Retourne {node_id: [{ts, rms}, ...]}
|
||||
"""
|
||||
results = {}
|
||||
|
||||
# Trouver tous les nodes avec données pour cette date
|
||||
nodes_with_data = []
|
||||
for node_id, node in index['nodes'].items():
|
||||
if node.get('dates') and date in node['dates']:
|
||||
nodes_with_data.append((node_id, node['dates'][date]))
|
||||
|
||||
print(f"Traitement de {len(nodes_with_data)} nodes pour {date}, canal {channel}")
|
||||
|
||||
for node_id, files in tqdm(nodes_with_data, desc=f"Date {date}"):
|
||||
# Trouver le fichier pour le canal demandé (priorité aux fichiers "data")
|
||||
channel_pattern = f'_{channel}_'
|
||||
target_file = None
|
||||
|
||||
for f in files:
|
||||
if channel_pattern in f['path'] and '_data_' in f['path']:
|
||||
target_file = f
|
||||
break
|
||||
|
||||
if not target_file:
|
||||
for f in files:
|
||||
if channel_pattern in f['path']:
|
||||
target_file = f
|
||||
break
|
||||
|
||||
if not target_file:
|
||||
continue
|
||||
|
||||
# Calculer les RMS
|
||||
rms_data = compute_rms_for_file(target_file['path'])
|
||||
|
||||
if rms_data:
|
||||
results[node_id] = rms_data
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Pré-calcul des RMS ADC')
|
||||
parser.add_argument('--date', help='Date spécifique (ex: 2020-09-02)')
|
||||
parser.add_argument('--channel', default='ch0', help='Canal (ch0-ch3)')
|
||||
parser.add_argument('--all', action='store_true', help='Traiter toutes les dates/canaux')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Charger l'index
|
||||
if not INDEX_PATH.exists():
|
||||
print(f"Index non trouvé: {INDEX_PATH}")
|
||||
sys.exit(1)
|
||||
|
||||
with open(INDEX_PATH, 'r') as f:
|
||||
index = json.load(f)
|
||||
|
||||
print(f"Index chargé: {len(index['nodes'])} nodes, {len(index['dates'])} dates")
|
||||
|
||||
# Créer le dossier de sortie
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Déterminer quoi traiter
|
||||
if args.date:
|
||||
dates_to_process = [args.date]
|
||||
channels_to_process = [args.channel]
|
||||
elif args.all:
|
||||
dates_to_process = index['dates']
|
||||
channels_to_process = ['ch0', 'ch1', 'ch2', 'ch3']
|
||||
else:
|
||||
# Par défaut, traiter la première date disponible, canal ch0
|
||||
dates_to_process = [index['dates'][0]] if index['dates'] else []
|
||||
channels_to_process = ['ch0']
|
||||
|
||||
for date in dates_to_process:
|
||||
for channel in channels_to_process:
|
||||
output_file = OUTPUT_DIR / f"rms_{date}_{channel}.json"
|
||||
|
||||
# Skip si déjà calculé
|
||||
if output_file.exists():
|
||||
print(f"Skip {output_file.name} (déjà existant)")
|
||||
continue
|
||||
|
||||
print(f"\n=== Traitement {date} - {channel} ===")
|
||||
|
||||
results = precompute_for_date(index, date, channel)
|
||||
|
||||
if results:
|
||||
# Sauvegarder
|
||||
output_data = {
|
||||
'date': date,
|
||||
'channel': channel,
|
||||
'interval_sec': RMS_INTERVAL_SEC,
|
||||
'window_sec': RMS_WINDOW_SEC,
|
||||
'nodes': results,
|
||||
'generated_at': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(output_data, f)
|
||||
|
||||
print(f"Sauvegardé: {output_file.name} ({len(results)} nodes)")
|
||||
else:
|
||||
print(f"Aucune donnée pour {date} - {channel}")
|
||||
|
||||
print("\n=== Terminé ===")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
83
scripts/rebuild_h5_db.py
Executable file
83
scripts/rebuild_h5_db.py
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild H5 metadata database for the seismic viewer."""
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
H5_ROOTS = [
|
||||
'/mnt/data_sdb1',
|
||||
'/mnt/kingston'
|
||||
]
|
||||
DB_PATH = '/home/floppyrj45/docker/seismic-nodes-viewer/h5_data.db'
|
||||
FILE_PATTERN = re.compile(r'b(\d+)_.*_ch(\d+)')
|
||||
|
||||
SCHEMA = [
|
||||
'CREATE TABLE IF NOT EXISTS positions (node_code INTEGER PRIMARY KEY, has_data BOOLEAN, has_aux BOOLEAN, sample_count INTEGER, last_seen TEXT)',
|
||||
'CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT, node_code INTEGER, channel INTEGER, dataset TEXT, size INTEGER, mtime INTEGER, FOREIGN KEY(node_code) REFERENCES positions(node_code))',
|
||||
'CREATE INDEX IF NOT EXISTS idx_files_node ON files(node_code)'
|
||||
]
|
||||
|
||||
|
||||
def rebuild_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cur = conn.cursor()
|
||||
|
||||
for stmt in SCHEMA:
|
||||
cur.execute(stmt)
|
||||
|
||||
cur.execute('DELETE FROM files')
|
||||
cur.execute('DELETE FROM positions')
|
||||
|
||||
files_counter = 0
|
||||
summary = {}
|
||||
|
||||
for root in H5_ROOTS:
|
||||
for dirpath, _, filenames in os.walk(root):
|
||||
for filename in filenames:
|
||||
if not filename.endswith('.h5'):
|
||||
continue
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
match = FILE_PATTERN.search(filename)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
node_code = int(match.group(1))
|
||||
channel = int(match.group(2))
|
||||
dataset = 'aux' if 'aux' in filename else 'data'
|
||||
stat = os.stat(filepath)
|
||||
mtime = int(stat.st_mtime)
|
||||
size = stat.st_size
|
||||
|
||||
summary.setdefault(node_code, {'data': False, 'aux': False, 'count': 0, 'last': 0})
|
||||
summary[node_code]['count'] += 1
|
||||
summary[node_code]['last'] = max(summary[node_code]['last'], mtime)
|
||||
if dataset == 'data':
|
||||
summary[node_code]['data'] = True
|
||||
else:
|
||||
summary[node_code]['aux'] = True
|
||||
|
||||
cur.execute(
|
||||
'INSERT INTO files (path, node_code, channel, dataset, size, mtime) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
(filepath, node_code, channel, dataset, size, mtime)
|
||||
)
|
||||
files_counter += 1
|
||||
|
||||
print(f"Indexed {files_counter} H5 files")
|
||||
|
||||
for node_code, stats in summary.items():
|
||||
has_data = 1 if stats['data'] else 0
|
||||
has_aux = 1 if stats['aux'] else 0
|
||||
last_seen = datetime.utcfromtimestamp(stats['last']).isoformat()
|
||||
cur.execute(
|
||||
'INSERT INTO positions (node_code, has_data, has_aux, sample_count, last_seen) VALUES (?, ?, ?, ?, ?)',
|
||||
(node_code, has_data, has_aux, stats['count'], last_seen)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"Rebuilt DB at {DB_PATH} with {len(summary)} positions")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
rebuild_db()
|
||||
104
scripts/rebuild_h5_db_v2.py
Executable file
104
scripts/rebuild_h5_db_v2.py
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild H5 metadata database - V2 (capture ALL patterns)."""
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
H5_ROOTS = ['/mnt/data_sdb1', '/mnt/kingston']
|
||||
DB_PATH = '/home/floppyrj45/docker/seismic-nodes-viewer/h5_data.db'
|
||||
|
||||
# Pattern plus permissif - capture TOUS les b###
|
||||
FILE_PATTERN = re.compile(r'b(\d+)')
|
||||
CHANNEL_PATTERN = re.compile(r'ch(\d+)')
|
||||
|
||||
SCHEMA = [
|
||||
'CREATE TABLE IF NOT EXISTS positions (node_code INTEGER PRIMARY KEY, has_data BOOLEAN, has_aux BOOLEAN, sample_count INTEGER, last_seen TEXT)',
|
||||
'CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT, node_code INTEGER, channel INTEGER, dataset TEXT, size INTEGER, mtime INTEGER, FOREIGN KEY(node_code) REFERENCES positions(node_code))',
|
||||
'CREATE INDEX IF NOT EXISTS idx_files_node ON files(node_code)'
|
||||
]
|
||||
|
||||
def rebuild_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cur = conn.cursor()
|
||||
|
||||
for stmt in SCHEMA:
|
||||
cur.execute(stmt)
|
||||
|
||||
cur.execute('DELETE FROM files')
|
||||
cur.execute('DELETE FROM positions')
|
||||
|
||||
files_counter = 0
|
||||
summary = {}
|
||||
|
||||
for root in H5_ROOTS:
|
||||
for dirpath, _, filenames in os.walk(root):
|
||||
for filename in filenames:
|
||||
if not filename.endswith('.h5'):
|
||||
continue
|
||||
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
|
||||
# Extraire node_code
|
||||
node_match = FILE_PATTERN.search(filename)
|
||||
if not node_match:
|
||||
continue
|
||||
|
||||
node_code = int(node_match.group(1))
|
||||
|
||||
# Extraire channel (peut ne pas exister)
|
||||
channel_match = CHANNEL_PATTERN.search(filename)
|
||||
channel = int(channel_match.group(1)) if channel_match else -1
|
||||
|
||||
# Déterminer dataset (data vs aux)
|
||||
dataset = 'aux' if 'aux' in filename else 'data'
|
||||
|
||||
stat = os.stat(filepath)
|
||||
mtime = int(stat.st_mtime)
|
||||
size = stat.st_size
|
||||
|
||||
# Mise à jour summary
|
||||
summary.setdefault(node_code, {'data': False, 'aux': False, 'count': 0, 'last': 0})
|
||||
summary[node_code]['count'] += 1
|
||||
summary[node_code]['last'] = max(summary[node_code]['last'], mtime)
|
||||
if dataset == 'data':
|
||||
summary[node_code]['data'] = True
|
||||
else:
|
||||
summary[node_code]['aux'] = True
|
||||
|
||||
# Insertion fichier
|
||||
cur.execute(
|
||||
'INSERT INTO files (path, node_code, channel, dataset, size, mtime) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
(filepath, node_code, channel, dataset, size, mtime)
|
||||
)
|
||||
files_counter += 1
|
||||
|
||||
print(f"✓ Indexed {files_counter} H5 files")
|
||||
|
||||
# Insertion positions
|
||||
for node_code, stats in summary.items():
|
||||
has_data = 1 if stats['data'] else 0
|
||||
has_aux = 1 if stats['aux'] else 0
|
||||
last_seen = datetime.fromtimestamp(stats['last']).isoformat()
|
||||
cur.execute(
|
||||
'INSERT INTO positions (node_code, has_data, has_aux, sample_count, last_seen) VALUES (?, ?, ?, ?, ?)',
|
||||
(node_code, has_data, has_aux, stats['count'], last_seen)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Stats finales
|
||||
total_positions = len(summary)
|
||||
with_data = sum(1 for s in summary.values() if s['data'])
|
||||
with_aux = sum(1 for s in summary.values() if s['aux'])
|
||||
|
||||
print(f"✓ Rebuilt DB: {total_positions} positions total")
|
||||
print(f" • With data files: {with_data}")
|
||||
print(f" • With aux files: {with_aux}")
|
||||
print(f" • Both: {sum(1 for s in summary.values() if s['data'] and s['aux'])}")
|
||||
print(f" • Coverage: {(with_data/205*100):.1f}% (assuming 205 planned)")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
rebuild_db()
|
||||
137
scripts/rebuild_h5_db_v3.py
Executable file
137
scripts/rebuild_h5_db_v3.py
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild H5 metadata database - V3 (include expected positions from CSV)."""
|
||||
import os
|
||||
import re
|
||||
import csv
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
H5_ROOTS = ['/mnt/data_sdb1', '/mnt/kingston']
|
||||
CSV_PATH = '/mnt/kingston/Copie de SETE_AUV_DARFV4-Copier(1).csv'
|
||||
DB_PATH = '/home/floppyrj45/docker/seismic-nodes-viewer/h5_data.db'
|
||||
|
||||
FILE_PATTERN = re.compile(r'b(\d+)')
|
||||
CHANNEL_PATTERN = re.compile(r'ch(\d+)')
|
||||
|
||||
SCHEMA = [
|
||||
'CREATE TABLE IF NOT EXISTS positions (node_code INTEGER PRIMARY KEY, has_data BOOLEAN, has_aux BOOLEAN, sample_count INTEGER, last_seen TEXT, expected BOOLEAN)',
|
||||
'CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT, node_code INTEGER, channel INTEGER, dataset TEXT, size INTEGER, mtime INTEGER, FOREIGN KEY(node_code) REFERENCES positions(node_code))',
|
||||
'CREATE INDEX IF NOT EXISTS idx_files_node ON files(node_code)'
|
||||
]
|
||||
|
||||
def rebuild_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cur = conn.cursor()
|
||||
|
||||
for stmt in SCHEMA:
|
||||
cur.execute(stmt)
|
||||
|
||||
cur.execute('DELETE FROM files')
|
||||
cur.execute('DELETE FROM positions')
|
||||
|
||||
# 1. Charger les positions attendues depuis le CSV
|
||||
expected_nodes = set()
|
||||
try:
|
||||
with open(CSV_PATH, 'r', encoding='utf-8-sig') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
node_code = row.get('NodeCode', '').strip()
|
||||
if node_code and node_code.isdigit():
|
||||
expected_nodes.add(int(node_code))
|
||||
print(f"✓ Loaded {len(expected_nodes)} expected positions from CSV")
|
||||
except Exception as e:
|
||||
print(f"⚠ CSV not found or error: {e}")
|
||||
print(" Continuing with file scan only...")
|
||||
|
||||
# 2. Scanner les fichiers H5
|
||||
files_counter = 0
|
||||
found_nodes = {}
|
||||
|
||||
for root in H5_ROOTS:
|
||||
for dirpath, _, filenames in os.walk(root):
|
||||
for filename in filenames:
|
||||
if not filename.endswith('.h5'):
|
||||
continue
|
||||
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
|
||||
node_match = FILE_PATTERN.search(filename)
|
||||
if not node_match:
|
||||
continue
|
||||
|
||||
node_code = int(node_match.group(1))
|
||||
channel_match = CHANNEL_PATTERN.search(filename)
|
||||
channel = int(channel_match.group(1)) if channel_match else -1
|
||||
dataset = 'aux' if 'aux' in filename else 'data'
|
||||
|
||||
stat = os.stat(filepath)
|
||||
mtime = int(stat.st_mtime)
|
||||
size = stat.st_size
|
||||
|
||||
found_nodes.setdefault(node_code, {'data': False, 'aux': False, 'count': 0, 'last': 0})
|
||||
found_nodes[node_code]['count'] += 1
|
||||
found_nodes[node_code]['last'] = max(found_nodes[node_code]['last'], mtime)
|
||||
if dataset == 'data':
|
||||
found_nodes[node_code]['data'] = True
|
||||
else:
|
||||
found_nodes[node_code]['aux'] = True
|
||||
|
||||
cur.execute(
|
||||
'INSERT INTO files (path, node_code, channel, dataset, size, mtime) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
(filepath, node_code, channel, dataset, size, mtime)
|
||||
)
|
||||
files_counter += 1
|
||||
|
||||
print(f"✓ Indexed {files_counter} H5 files")
|
||||
print(f"✓ Found {len(found_nodes)} positions with data")
|
||||
|
||||
# 3. Créer les entrées pour TOUTES les positions (attendues + trouvées)
|
||||
all_nodes = expected_nodes | set(found_nodes.keys())
|
||||
|
||||
for node_code in all_nodes:
|
||||
is_expected = node_code in expected_nodes
|
||||
|
||||
if node_code in found_nodes:
|
||||
stats = found_nodes[node_code]
|
||||
has_data = 1 if stats['data'] else 0
|
||||
has_aux = 1 if stats['aux'] else 0
|
||||
last_seen = datetime.fromtimestamp(stats['last']).isoformat()
|
||||
sample_count = stats['count']
|
||||
else:
|
||||
# Position attendue mais sans données
|
||||
has_data = 0
|
||||
has_aux = 0
|
||||
last_seen = None
|
||||
sample_count = 0
|
||||
|
||||
cur.execute(
|
||||
'INSERT INTO positions (node_code, has_data, has_aux, sample_count, last_seen, expected) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
(node_code, has_data, has_aux, sample_count, last_seen, 1 if is_expected else 0)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Stats finales
|
||||
cur.execute('SELECT COUNT(*) FROM positions')
|
||||
total = cur.fetchone()[0]
|
||||
|
||||
cur.execute('SELECT COUNT(*) FROM positions WHERE has_data = 1')
|
||||
with_data = cur.fetchone()[0]
|
||||
|
||||
cur.execute('SELECT COUNT(*) FROM positions WHERE expected = 1')
|
||||
expected_count = cur.fetchone()[0]
|
||||
|
||||
cur.execute('SELECT COUNT(*) FROM positions WHERE expected = 1 AND has_data = 0')
|
||||
missing = cur.fetchone()[0]
|
||||
|
||||
print(f"\n📊 Database Summary:")
|
||||
print(f" • Total positions in DB: {total}")
|
||||
print(f" • Expected (from CSV): {expected_count}")
|
||||
print(f" • With H5 data: {with_data}")
|
||||
print(f" • Missing (expected but no data): {missing}")
|
||||
print(f" • Coverage: {(with_data/expected_count*100 if expected_count else 0):.1f}%")
|
||||
|
||||
conn.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
rebuild_db()
|
||||
114
scripts/rebuild_index.py
Executable file
114
scripts/rebuild_index.py
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reconstruit l'index.json de la webapp à partir de l'inventaire complet.
|
||||
Prend en compte tous les fichiers HDF5 sur tous les disques.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
def main():
|
||||
# Charger l'inventaire
|
||||
inv_path = Path(r'F:\seismic_webapp\inventory.json')
|
||||
inv = json.load(open(inv_path))
|
||||
print(f"Inventaire charge: {len(inv)} fichiers")
|
||||
|
||||
# Charger l'index existant pour garder les positions
|
||||
idx_path = Path(r'F:\seismic_webapp\data\index.json')
|
||||
old_idx = json.load(open(idx_path))
|
||||
print(f"Index existant: {len(old_idx.get('nodes', {}))} nodes")
|
||||
|
||||
# Construire le nouvel index
|
||||
nodes = {}
|
||||
|
||||
# Copier les positions existantes
|
||||
for node_id, node_data in old_idx.get('nodes', {}).items():
|
||||
nodes[node_id] = {
|
||||
'position': node_data.get('position'),
|
||||
'dates': {},
|
||||
'hasDates': False
|
||||
}
|
||||
|
||||
# Ajouter les fichiers de l'inventaire
|
||||
files_added = 0
|
||||
for f in inv:
|
||||
bumper_id = f['bumper_id']
|
||||
if not bumper_id:
|
||||
continue
|
||||
|
||||
# Créer le node s'il n'existe pas
|
||||
if bumper_id not in nodes:
|
||||
nodes[bumper_id] = {
|
||||
'position': None,
|
||||
'dates': {},
|
||||
'hasDates': False
|
||||
}
|
||||
|
||||
# Calculer la date depuis l'epoch
|
||||
if f['epoch_time']:
|
||||
dt = datetime.fromtimestamp(f['epoch_time'])
|
||||
date_str = dt.strftime('%Y-%m-%d')
|
||||
else:
|
||||
continue
|
||||
|
||||
# Ajouter à la liste des dates
|
||||
if date_str not in nodes[bumper_id]['dates']:
|
||||
nodes[bumper_id]['dates'][date_str] = []
|
||||
|
||||
# Déterminer les canaux (extraire du nom de fichier)
|
||||
channel = f['channel']
|
||||
channels = [channel] if channel else []
|
||||
|
||||
# Ajouter le fichier
|
||||
file_info = {
|
||||
'path': f['filepath'],
|
||||
'timestamp': f['epoch_time'],
|
||||
'channels': channels,
|
||||
'size_bytes': 0 # On n'a pas cette info
|
||||
}
|
||||
|
||||
# Éviter les doublons
|
||||
existing_paths = [fi['path'] for fi in nodes[bumper_id]['dates'][date_str]]
|
||||
if f['filepath'] not in existing_paths:
|
||||
nodes[bumper_id]['dates'][date_str].append(file_info)
|
||||
files_added += 1
|
||||
|
||||
# Marquer les nodes qui ont des dates
|
||||
for node_id, node_data in nodes.items():
|
||||
node_data['hasDates'] = len(node_data['dates']) > 0
|
||||
|
||||
# Statistiques
|
||||
nodes_with_data = sum(1 for n in nodes.values() if n['hasDates'])
|
||||
total_files = sum(
|
||||
len(files)
|
||||
for n in nodes.values()
|
||||
for files in n['dates'].values()
|
||||
)
|
||||
|
||||
print(f"\nNouvel index:")
|
||||
print(f" Nodes total: {len(nodes)}")
|
||||
print(f" Nodes avec donnees: {nodes_with_data}")
|
||||
print(f" Fichiers indexes: {total_files}")
|
||||
|
||||
# Sauvegarder
|
||||
new_idx = {
|
||||
'nodes': nodes,
|
||||
'sampleRateHz': old_idx.get('sampleRateHz', 200),
|
||||
'generated': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Backup de l'ancien
|
||||
backup_path = idx_path.with_suffix('.json.bak')
|
||||
with open(backup_path, 'w') as f:
|
||||
json.dump(old_idx, f)
|
||||
print(f"\nBackup sauvegarde: {backup_path}")
|
||||
|
||||
# Sauvegarder le nouveau
|
||||
with open(idx_path, 'w') as f:
|
||||
json.dump(new_idx, f, indent=2)
|
||||
print(f"Nouvel index sauvegarde: {idx_path}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
39
scripts/show_stats.py
Executable file
39
scripts/show_stats.py
Executable file
@@ -0,0 +1,39 @@
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
d = json.load(open(r'F:\seismic_webapp\inventory.json'))
|
||||
|
||||
by_channel = defaultdict(lambda: {'data': 0, 'aux': 0, 'bumpers': set()})
|
||||
for f in d:
|
||||
ch = f['channel'] or 'unknown'
|
||||
if f['file_type'] == 'data':
|
||||
by_channel[ch]['data'] += 1
|
||||
else:
|
||||
by_channel[ch]['aux'] += 1
|
||||
if f['bumper_id']:
|
||||
by_channel[ch]['bumpers'].add(f['bumper_id'])
|
||||
|
||||
print('=== RESUME PAR CANAL ===')
|
||||
print('Canal DATA AUX Bumpers')
|
||||
print('-' * 35)
|
||||
for ch in ['ch0', 'ch1', 'ch2', 'ch3', 'ch5', 'ch6', 'ch7', 'ch15', 'unknown']:
|
||||
if ch in by_channel:
|
||||
s = by_channel[ch]
|
||||
total = s['data'] + s['aux']
|
||||
print(f'{ch:8} {s["data"]:4} {s["aux"]:4} {len(s["bumpers"]):3}')
|
||||
|
||||
# Stats globales
|
||||
total_data = sum(s['data'] for s in by_channel.values())
|
||||
total_aux = sum(s['aux'] for s in by_channel.values())
|
||||
all_bumpers = set()
|
||||
for s in by_channel.values():
|
||||
all_bumpers.update(s['bumpers'])
|
||||
|
||||
print('-' * 35)
|
||||
print(f'TOTAL {total_data:4} {total_aux:4} {len(all_bumpers):3}')
|
||||
|
||||
errors = [f for f in d if f['error']]
|
||||
print(f'\nErreurs de lecture: {len(errors)} fichiers')
|
||||
if errors:
|
||||
for e in errors[:5]:
|
||||
print(f' - {e["filename"][:50]}...')
|
||||
31
scripts/test_hdf5.py
Executable file
31
scripts/test_hdf5.py
Executable file
@@ -0,0 +1,31 @@
|
||||
import h5py
|
||||
import numpy as np
|
||||
|
||||
# Test file
|
||||
filepath = r'F:\2020-09-22\data\auto_266_143513_b29_13_213605_data_rsn2648_seq1_ch0_1599039547.h5'
|
||||
|
||||
with h5py.File(filepath, 'r') as f:
|
||||
print("=== Structure du fichier ===")
|
||||
print("Datasets:", list(f.keys()))
|
||||
|
||||
if 'adc_values' in f:
|
||||
d = f['adc_values']
|
||||
print("\n=== Dataset adc_values ===")
|
||||
print("Shape:", d.shape)
|
||||
print("Dtype:", d.dtype)
|
||||
|
||||
print("\n=== Attributs du dataset ===")
|
||||
for k, v in d.attrs.items():
|
||||
print(f" {k}: {v}")
|
||||
|
||||
# Charger un échantillon
|
||||
sample = d[:2000]
|
||||
print("\n=== Statistiques (premiers 2000 samples) ===")
|
||||
print("Min:", np.min(sample))
|
||||
print("Max:", np.max(sample))
|
||||
print("Mean:", np.mean(sample))
|
||||
print("Std:", np.std(sample))
|
||||
print("RMS:", np.sqrt(np.mean(sample**2)))
|
||||
|
||||
print("\n=== Premiers 20 valeurs ===")
|
||||
print(sample[:20])
|
||||
201
simple_api.py
Normal file
201
simple_api.py
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
from flask import Flask, jsonify, request
|
||||
from flask_cors import CORS
|
||||
import json
|
||||
import h5py
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# Load index once at startup
|
||||
with open('/data/index.json', 'r') as f:
|
||||
INDEX = json.load(f)
|
||||
|
||||
H5_DIR = Path('/data/h5')
|
||||
|
||||
@app.route('/api/nodes', methods=['GET'])
|
||||
def get_nodes():
|
||||
nodes_list = []
|
||||
for node_id, node_info in INDEX['nodes'].items():
|
||||
file_count = len(node_info.get('files', []))
|
||||
nodes_list.append({
|
||||
'id': node_id,
|
||||
'position': node_info.get('position', {}),
|
||||
'file_count': file_count,
|
||||
'hasDates': file_count > 0
|
||||
})
|
||||
return jsonify({
|
||||
'nodes': nodes_list,
|
||||
'sampleRateHz': 500
|
||||
})
|
||||
|
||||
@app.route('/api/dates', methods=['GET'])
|
||||
def get_dates():
|
||||
return jsonify({'dates': INDEX['dates']})
|
||||
|
||||
@app.route('/api/migration-status', methods=['GET'])
|
||||
def get_migration_status():
|
||||
"""Status de la conversion RAW -> H5"""
|
||||
total_files = 345 # Connu du projet
|
||||
h5_files = list(H5_DIR.glob('*.h5'))
|
||||
converted = len(h5_files)
|
||||
|
||||
return jsonify({
|
||||
'summary': {
|
||||
'total_files': total_files,
|
||||
'converted_files': converted,
|
||||
'percentage': round(converted / total_files * 100, 1),
|
||||
'status': 'in_progress' if converted < total_files else 'complete'
|
||||
},
|
||||
'h5_files_available': converted
|
||||
})
|
||||
|
||||
@app.route('/api/rms-timeline', methods=['GET'])
|
||||
def get_rms_timeline():
|
||||
"""Timeline RMS pour un channel et une date donnés"""
|
||||
date = request.args.get('date')
|
||||
channel = request.args.get('channel', 'ch0')
|
||||
|
||||
if not date:
|
||||
return jsonify({'error': 'date parameter required'}), 400
|
||||
|
||||
# Chercher les fichiers H5 pour cette date
|
||||
timeline = []
|
||||
for node_id, node_info in INDEX['nodes'].items():
|
||||
for file_info in node_info.get('files', []):
|
||||
if file_info.get('date') == date:
|
||||
# Pour l'instant, retourner des données mock
|
||||
# TODO: Calculer le vrai RMS depuis les fichiers H5
|
||||
timeline.append({
|
||||
'node_id': node_id,
|
||||
'timestamp': 0,
|
||||
'rms': 0 # À calculer depuis H5
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'date': date,
|
||||
'channel': channel,
|
||||
'timeline': timeline
|
||||
})
|
||||
|
||||
@app.route('/api/data', methods=['GET'])
|
||||
def get_waveform_data():
|
||||
"""Données waveform pour un node, date, channel, timestamp"""
|
||||
node_id = request.args.get('node')
|
||||
date = request.args.get('date')
|
||||
channel = request.args.get('channel', 'ch0')
|
||||
start = float(request.args.get('start', 0))
|
||||
duration = float(request.args.get('duration', 10))
|
||||
|
||||
if not node_id or not date:
|
||||
return jsonify({'error': 'node and date required'}), 400
|
||||
|
||||
# Chercher le fichier H5 correspondant
|
||||
node_info = INDEX['nodes'].get(node_id)
|
||||
if not node_info:
|
||||
return jsonify({'error': 'node not found'}), 404
|
||||
|
||||
h5_file = None
|
||||
for file_info in node_info.get('files', []):
|
||||
if file_info.get('date') == date:
|
||||
h5_file = H5_DIR / file_info.get('filename', '')
|
||||
break
|
||||
|
||||
if not h5_file or not h5_file.exists():
|
||||
return jsonify({'error': 'H5 file not found'}), 404
|
||||
|
||||
try:
|
||||
with h5py.File(h5_file, 'r') as f:
|
||||
sample_rate = f['metadata'].attrs['sample_rate_hz']
|
||||
channel_num = int(channel.replace('ch', '').replace('CH', ''))
|
||||
|
||||
# Lire les données calibrées
|
||||
dataset = f[f'calibrated_data/channel_{channel_num + 1}']
|
||||
start_sample = int(start * sample_rate)
|
||||
end_sample = int((start + duration) * sample_rate)
|
||||
|
||||
# Limiter à la taille du dataset
|
||||
end_sample = min(end_sample, dataset.shape[0])
|
||||
|
||||
if start_sample >= dataset.shape[0]:
|
||||
return jsonify({'error': 'start time out of range'}), 400
|
||||
|
||||
data = dataset[start_sample:end_sample]
|
||||
|
||||
# Calculer stats
|
||||
rms = float(np.sqrt(np.mean(data ** 2)))
|
||||
peak = float(np.max(np.abs(data)))
|
||||
|
||||
return jsonify({
|
||||
'node_id': node_id,
|
||||
'date': date,
|
||||
'channel': channel,
|
||||
'start': start,
|
||||
'duration': duration,
|
||||
'sample_rate': int(sample_rate),
|
||||
'data': data.tolist(),
|
||||
'stats': {
|
||||
'rms': rms,
|
||||
'peak': peak,
|
||||
'samples': len(data)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/h5/files', methods=['GET'])
|
||||
def get_h5_files():
|
||||
"""Liste des fichiers H5 disponibles avec métadonnées"""
|
||||
files = []
|
||||
for h5_path in H5_DIR.glob('*.h5'):
|
||||
try:
|
||||
with h5py.File(h5_path, 'r') as f:
|
||||
files.append({
|
||||
'filename': h5_path.name,
|
||||
'size_mb': round(h5_path.stat().st_size / 1024 / 1024, 2),
|
||||
'duration_sec': int(f['metadata'].attrs.get('duration_sec', 0)),
|
||||
'sample_rate': int(f['metadata'].attrs.get('sample_rate_hz', 500)),
|
||||
'channels': int(f['metadata'].attrs.get('n_channels', 4))
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify({'files': files, 'count': len(files)})
|
||||
|
||||
@app.route('/api/h5/coverage', methods=['GET'])
|
||||
def get_h5_coverage():
|
||||
"""Matrice de couverture nodes x dates"""
|
||||
coverage = {}
|
||||
for node_id, node_info in INDEX['nodes'].items():
|
||||
node_dates = [f['date'] for f in node_info.get('files', []) if 'date' in f]
|
||||
coverage[node_id] = node_dates
|
||||
|
||||
return jsonify({
|
||||
'coverage': coverage,
|
||||
'total_nodes': len(coverage),
|
||||
'total_dates': len(INDEX['dates'])
|
||||
})
|
||||
|
||||
@app.route('/api/chat', methods=['POST'])
|
||||
def chat():
|
||||
"""Endpoint chat assistant (mock)"""
|
||||
data = request.json
|
||||
message = data.get('message', '')
|
||||
|
||||
return jsonify({
|
||||
'response': f"[Mock] Vous avez dit: {message}",
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
return jsonify({'status': 'ok', 'nodes': len(INDEX['nodes']), 'dates': len(INDEX['dates'])})
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f"Loaded {len(INDEX['nodes'])} nodes, {len(INDEX['dates'])} dates")
|
||||
print(f"H5 directory: {H5_DIR}")
|
||||
app.run(host='0.0.0.0', port=3004)
|
||||
206
static/SETE_DARF_V4.csv
Normal file
206
static/SETE_DARF_V4.csv
Normal file
@@ -0,0 +1,206 @@
|
||||
1,2,,Sete-Pilot-20200903.csv,,6,7,8,9,10,11,12,,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,,,,,,,,,47,,,All commands failed,-10,,,,,,,,,,,,,,,,,,97,196,1
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,Comment_ori,,,,,,,,,,,,,,,,,,,,,
|
||||
,FAUX,,,,,,,,Deployment,,,,,Check FIX,Recovery,,,,,Check FIX,,,,,,,,,,,,,,,,,,,,Last message from position log,,,,,,,,,,,145,,,,,,,,Fished,-10,,,,,,,,,,,,,,,,,,,,
|
||||
concat,Line,Point,NodeCode,Index,Preplot Easting,Preplot Northing,Preplot Depth,Aslaid Time,Aslaid Easting,Aslaid Northing,Aslaid Depth,,Aslaid Tide Offset,Aslaid Azimuth,Recovered Time,Recovered Easting,Recovered Northing,Recovered Depth,Recovered Tide Offset,Recovered Azimuth,PreplotToAslaidDistance,PreplotToAslaidBearing,AslaidToRecoveredDistance,AslaidToRecoveredBearing,RecoveredToPreplotDistance,RecoveredToPreplotBearing,PreplotToAslaidAlongTrack,PreplotToAslaidCrossTrack,RecoveredToPreplotAlongTrack,RecoveredToPreplotCrossTrack,AslaidToRecoveredAlongTrack,AslaidToRecoveredCrossTrack,DeployedComments,RecoveredComments,Flag,Date,JD,Time,JDTIME,time,X_Pos,Y_Pos,Status,File,X_Comp,Y_Comp,Comment,comment_bis,duration,Comment_ori,log_download,date,comment_pickup,date sent onshore,,Comment,,,Fishermen,-20,,,,,,,,,,,,,,,,,,,,
|
||||
10001,1000,5000,4,1,559867.24,4797453,0.00,2020-Sep-03 18:41:52.000,0,0,0,1000/5000,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 09:10:09""",559951.24,4797507.26,PMU-A,C:\SpiceRack\20200904\positions_log.csv,559869.849,4797451.921,In Gabia_1,,,In Gabia_1,OK,20200912,,,,20,,,Gabia missed,10,,,,,,,,,,,,,,,,,,,,
|
||||
10002,1000,5004,10,1,559949.16,4797510.36,0.00,2020-Sep-03 18:41:52.000,0,0,0,1000/5004,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 09:10:51""",560035.89,4797566.14,LANDING,C:\SpiceRack\20200904\positions_log.csv,559950.057,4797509.16,In Gabia_1,,,In Gabia_1,OK,20200912,,,,20,,,In Gabia_1,20,,,,,,,,,,,,,,,,,,,,
|
||||
10003,1000,5008,11,1,560031.07,4797567.72,0.00,2020-Sep-03 18:41:52.000,0,0,0,1000/5008,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 09:15:39""",560117.19,4797620.55,NULL,C:\SpiceRack\20200904\positions_log.csv,560035.816,4797565.643,In Gabia_1,,,In Gabia_1,OK,20200912,,14-Sep,,20,,,In Gabia_2,20,,,,,,,,,,,,,,,,,,,,
|
||||
10004,1000,5012,15,1,560112.99,4797625.07,0.00,2020-Sep-03 18:41:52.000,560116.82,4797621.72,37,1000/5012,0.0,147.40,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,,,,,,560115.915,4797622.84,In Gabia_1,,,In Gabia_1,OK,20200912,,14-Sep,,20,,,In Gabia_3,20,,,,,,,,,,,,,,,,,,,,
|
||||
10006,1000,5020,37,1,560276.82,4797739.79,0.00,2020-Sep-03 18:41:52.000,560284.01,4797736.15,35,1000/5020,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 09:39:33""",560367.98,4797796.99,LANDED,C:\SpiceRack\20200904\positions_log.csv,560281.716,4797739.475,In Gabia_2,,,In Gabia_2,OK,20200912,Eastern Egg HS,13-Sep,,20,,,In Gabia_4,20,,,,,,,,,,,,,,,,,,,,
|
||||
10007,1000,5024,40,1,560358.73,4797797.15,0.00,2020-Sep-03 18:41:52.000,560367.98,4797796.99,36,1000/5024,0.0,141.70,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 09:48:23""",560441.57,4797848.22,NULL,C:\SpiceRack\20200904\positions_log.csv,560367.603,4797799.043,Surface,,,Surface,OK,20200912,,14-Sep,,10,,,in Gabia_5,20,,,,,,,,,,,,,,,,,,,,
|
||||
10008,1000,5028,43,1,560440.65,4797854.5,0.00,2020-Sep-03 18:41:52.000,560439.27,4797850.52,-10,1000/5028,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 09:45:47""",560527.58,4797908.74,LANDED,C:\SpiceRack\20200904\positions_log.csv,560452.745,4797859.883,In Gabia_1,,,In Gabia_1,OK,20200912,,14-Sep,,20,,,In Gabia_6,20,,,,,,,,,,,,,,,,,,,,
|
||||
10009,1000,5032,44,1,560522.56,4797911.86,0.00,2020-Sep-03 18:41:52.000,560527.58,4797908.74,37,1000/5032,0.0,144.60,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 09:45:44""",560599.66,4797961.54,LANDED,C:\SpiceRack\20200904\positions_log.csv,560527.112,4797910.961,In Gabia_1,,,In Gabia_1,OK,20200912,,,,20,,,Manta_10484,-30,,,,,,,,,,,,,,,,,,,,
|
||||
10010,1000,5036,45,1,560604.48,4797969.22,0.00,2020-Sep-03 18:41:52.000,560599.66,4797961.54,37,1000/5036,0.0,133.20,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 10:25:50""",560689.12,4798023.32,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560597.021,4797963.004,In Gabia_1,,,In Gabia_1,OK,20200912,Front truster HS,13-Sep,,20,,,Manta_12025,-30,,,,,,,,,,,,,,,,,,,,
|
||||
10011,1000,5040,52,1,560686.39,4798026.58,0.00,2020-Sep-03 18:41:52.000,560687.96,4798024.23,35,1000/5040,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 10:27:20""",560773.34,4798084.4,LANDING,C:\SpiceRack\20200904\positions_log.csv,560687.314,4798026.735,Fishermen,,,Fishermen,OK,20200912,Fishing boat,13-Sep,,-20,,,Manta_1240,-30,,,,,,,,,,,,,,,,,,,,
|
||||
10012,1000,5044,54,1,560768.31,4798083.93,0.00,2020-Sep-03 18:41:52.000,560773.34,4798084.4,37,1000/5044,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 10:28:52""",560853.11,4798142.13,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560771.925,4798086.655,In Gabia_1,,,In Gabia_1,OK,20200912,,14-Sep,,20,,,Manta_12443,-30,,,,,,,,,,,,,,,,,,,,
|
||||
10013,1000,5048,56,1,560850.22,4798141.29,0.00,2020-Sep-03 18:41:52.000,0,0,0,1000/5048,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 10:30:32""",560936.14,4798197.59,LANDING,C:\SpiceRack\20200904\positions_log.csv,560852.9,4798141.947,In Gabia_1,,,In Gabia_1,OK,20200912,,14-Sep,,20,,,Manta_12614,-30,,,,,,,,,,,,,,,,,,,,
|
||||
10014,1000,5052,63,1,560932.14,4798198.65,0.00,2020-Sep-03 18:41:52.000,560936.14,4798197.59,37,1000/5052,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 10:33:50""",561013.53,4798249.08,LANDED,C:\SpiceRack\20200904\positions_log.csv,560934.867,4798201.855,Fishermen,,,Fishermen,OK,20200913,Fishing boat,13-Sep,,-20,,,Manta_2753,-30,,,,,,,,,,,,,,,,,,,,
|
||||
10015,1000,5056,67,1,561014.05,4798256.01,0.00,2020-Sep-03 18:41:52.000,561013.53,4798249.08,37,1000/5056,0.0,138.90,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 10:39:55""",561099.49,4798310.67,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561012.719,4798250.397,In Gabia_1,,,In Gabia_1,OK,20200914,,,,20,,,Manta_4071,-30,,,,,,,,,,,,,,,,,,,,
|
||||
10016,1000,5060,72,1,561095.97,4798313.36,0.00,2020-Sep-03 18:41:52.000,561097.53,4798313.11,35,1000/5060,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 10:41:21""",561176.33,4798369.57,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561096.612,4798314.914,In Gabia_1,,,In Gabia_1,OK,20200912,,14-Sep,,20,,,Navigation issue,-10,,,,,,,,,,,,,,,,,,,,
|
||||
10017,1000,5064,73,1,561177.88,4798370.72,0.00,2020-Sep-03 18:41:52.000,561181.16,4798371.55,34,1000/5064,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 10:45:18""",561177.18,4798341.87,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561178.199,4798370.267,In Gabia_1,,,In Gabia_1,OK,20200912,,14-Sep,,20,,,No com,-10,,,,,,,,,,,,,,,,,,,,
|
||||
10018,1000,5068,78,1,561259.8,4798428.08,0.00,2020-Sep-03 18:41:52.000,0,0,0,1000/5068,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 10:54:44""",561342.98,4798481.82,NULL,C:\SpiceRack\20200904\positions_log.csv,561180.452,4798342.119,ROV,ROV_20200914_01,0:29,Gabia missed,Fail,,,,,10,,,No take off,-20,,,,,,,,,,,,,,,,,,,,
|
||||
10019,1000,5072,84,1,561341.71,4798485.44,0.00,2020-Sep-03 18:41:52.000,561343.68,4798482.74,-10,1000/5072,0.0,141.70,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 10:56:07""",561423.11,4798539.88,NULL,C:\SpiceRack\20200904\positions_log.csv,561344.171,4798485.89,Fishermen,,,Fishermen,Fail,,"No Com, Electronic HS",14-Sep,,-20,,,No wake Up,-20,,,,,,,,,,,,,,,,,,,,
|
||||
10020,1000,5076,87,1,561423.63,4798542.8,0.00,2020-Sep-03 18:41:52.000,561423,4798540.05,37,1000/5076,0.0,127.60,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-04 12:50:32""",561613.17,4796861.24,NULL,C:\SpiceRack\20200904\positions_log.csv,561408.546,4798520.607,In Gabia_1,,,In Gabia_1,OK,20200913,Eastern Egg and front truster HS,13-Sep,,20,,,Surface,10,,,,,,,,,,,,,,,,,,,,
|
||||
10021,1000,5080,88,1,561505.54,4798600.15,0.00,2020-Sep-03 18:41:52.000,561508.79,4798599.75,37,1000/5080,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-04 12:50:32""",561636.82,4796505.46,NULL,C:\SpiceRack\20200904\positions_log.csv,561508.137,4798603.16,In Gabia_1,,,In Gabia_1,OK,20200912,Eastern Egg and front truster HS,13-Sep,,20,,,Surface failed,-10,,,,,,,,,,,,,,,,,,,,
|
||||
10022,1000,5084,91,1,561587.46,4798657.51,0.00,2020-Sep-03 18:41:52.000,561590.22,4798654.3,37,1000/5084,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 11:06:54""",561668.07,4798713.18,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561589.107,4798657.864,ROV,ROV_20200914_02,0:15,No wake Up,OK,,,15-Sep,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
10023,1000,5088,99,1,561669.37,4798714.87,0.00,2020-Sep-03 18:41:52.000,561674.16,4798725.83,35,1000/5088,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 11:08:59""",561755.37,4798770.24,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561671.21,4798716.041,In Gabia_1,,,In Gabia_1,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
10024,1000,5092,100,1,561751.29,4798772.23,0.00,2020-Sep-03 18:41:53.000,561755.58,4798770.13,37,1000/5092,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:10:57""",561837.23,4798830.21,LANDED,C:\SpiceRack\20200904\positions_log.csv,561754.318,4798774.606,In Gabia_1,,,In Gabia_1,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
10025,1000,5096,102,1,561833.2,4798829.58,0.00,2020-Sep-03 18:41:53.000,561837.23,4798830.21,37,1000/5096,0.0,147.40,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-04 14:33:09""",-1000,-1000,NULL,C:\SpiceRack\20200904\positions_log.csv,561837.437,4798834.59,In Gabia_1,,,In Gabia_1,OK,20200914,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
10005,1000,5016,156,1,560194.9,4797682.43,0.00,2020-Sep-04 12:16:00.000,560196.9,4796699.2,0,1000/5016,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:52,247184152,"""2020-09-03 09:36:53""",560283.82,4797736.63,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560198.796,4797676.377,Manta_10484,Manta_10484,,Manta_10484,,,,,,-30,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20002,1012,5004,2,1,560121.23,4797264.61,0.00,2020-Sep-03 18:41:53.000,560115.92,4797263.54,37,1012/5004,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 12:46:45""",560198.22,4797324.27,LANDING,C:\SpiceRack\20200904\positions_log.csv,560117.038,4797260.288,In Gabia_2,,,In Gabia_2,OK,20200912,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20001,1012,5000,28,1,560039.31,4797207.25,0.00,2020-Sep-03 18:41:53.000,0,0,0,1012/5000,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 12:48:24""",560115.92,4797263.54,LANDING,C:\SpiceRack\20200904\positions_log.csv,560085.541,4797284.652,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20011,1012,5040,90,1,560858.46,4797780.83,0.00,2020-Sep-04 11:56:00.000,560854.7,4797782.2,0,1012/5040,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 12:02:22""",560848.72,4797786.56,NAVIG,C:\SpiceRack\20200904\positions_log.csv,560856.964,4797781.978,ROV,ROV_20200914_07,0:28,Manta_2753,Fail,,"Right rear truster twisted, left rear truster locked",15-Sep,,-30,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20025,1012,5096,93,1,562005.28,4798583.84,0.00,2020-Sep-03 18:41:54.000,562004.69,4798584.18,33,1012/5096,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 13:30:48""",560215.02,4796956.67,LANDED,C:\SpiceRack\20200904\positions_log.csv,562005.278,4798583.838,In Gabia_2,,,In Gabia_2,OK,20200914,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20024,1012,5092,94,1,561923.36,4798526.48,0.00,2020-Sep-03 18:41:54.000,561922.82,4798527.69,38,1012/5092,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 11:21:12""",562000.87,4798585.11,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561924.089,4798526.228,In Gabia_2,,,In Gabia_2,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20003,1012,5008,103,1,560203.14,4797321.97,0.00,2020-Sep-03 18:41:53.000,560198.22,4797324.27,37,1012/5008,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 12:44:59""",560278.32,4797379.55,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560200.097,4797319.715,Surface,,,Surface,OK,20200912,,14-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20004,1012,5012,104,1,560285.06,4797379.33,0.00,2020-Sep-03 18:41:53.000,560278.76,4797378.81,37,1012/5012,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 12:43:39""",560361.72,4797437.23,LANDING,C:\SpiceRack\20200904\positions_log.csv,560280.22,4797375.689,ROV,ROV_20200914_05,0:45,All commands failed,,,Right Rear truster HS,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20023,1012,5088,109,1,561841.45,4798469.12,0.00,2020-Sep-03 18:41:53.000,561837.73,4798467.75,35,1012/5088,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:23:33""",561921.31,4798530.37,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561838.94,4798465.796,In Gabia_2,,,In Gabia_2,OK,20200914,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20022,1012,5084,115,1,561759.53,4798411.76,0.00,2020-Sep-03 18:41:53.000,561769.11,4798417.87,35,1012/5084,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:27:00""",561177.03,4798346.62,INIT,C:\SpiceRack\20200904\positions_log.csv,561768.498,4798416.55,Gabia missed,,,Gabia missed,,,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20005,1012,5016,124,1,560366.97,4797436.68,0.00,2020-Sep-03 18:41:53.000,560361.72,4797437.23,37,1012/5016,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 12:23:00""",560443.65,4797502.41,LANDING,C:\SpiceRack\20200904\positions_log.csv,560364.649,4797432.797,ROV,ROV_20200914_06,0:15,No wake Up,OK,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20007,1012,5024,128,1,560530.8,4797551.4,0.00,2020-Sep-03 18:41:53.000,560529.16,4797555.9,36,1012/5024,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 12:19:39""",560606.9,4797608.23,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560528.912,4797553.528,In Gabia_2,,,In Gabia_2,OK,20200912,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20021,1012,5080,131,1,561677.62,4798354.41,0.00,2020-Sep-03 18:41:53.000,0,0,0,1012/5080,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:24:49""",561765.91,4798418.77,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561772.233,4798451.29,ROV,ROV_20200914_03,0:25,Navigation issue,OK,,,15-Sep,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20020,1012,5076,134,1,561595.7,4798297.05,0.00,2020-Sep-03 18:41:53.000,561590.55,4798294.8,36,1012/5076,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:26:54""",561774.36,4798449.79,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561590.772,4798294.037,In Gabia_2,,,In Gabia_2,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20019,1012,5072,136,1,561513.79,4798239.69,0.00,2020-Sep-03 18:41:53.000,561506.4,4798241.76,38,1012/5072,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:40:24""",561587.63,4798297.88,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561506.805,4798240.623,ROV,ROV_20200914_04,0:08,Navigation issue,OK,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20018,1012,5068,143,1,561431.87,4798182.33,0.00,2020-Sep-03 18:41:53.000,561426.34,4798183.54,38,1012/5068,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:42:13""",561506.4,4798241.76,LANDING,C:\SpiceRack\20200904\positions_log.csv,561429.539,4798180.879,In Gabia_2,,,In Gabia_2,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20017,1012,5064,147,1,561349.96,4798124.98,0.00,2020-Sep-03 18:41:53.000,561344.38,4798127.78,37,1012/5064,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:48:11""",561182.12,4798346.76,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561346.795,4798122.986,Surface,,,Surface,OK,20200912,Eastern Egg and front truster HS,13-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20006,1012,5020,172,1,560448.89,4797494.04,0.00,2020-Sep-03 18:41:53.000,560443.65,4797502.41,37,1012/5020,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 12:21:02""",560527.65,4797555.29,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560445.974,4797492.292,ROV,ROV_20200915_01,00:11,All commands failed,OK,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20016,1012,5060,175,1,561268.04,4798067.62,0.00,2020-Sep-03 18:41:53.000,561264.85,4798069.07,37,1012/5060,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:45:44""",561343.88,4798128.72,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561265.234,4798067.596,In Gabia_2,,,In Gabia_2,OK,20200912,Front truster HS,13-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20008,1012,5028,177,1,560612.72,4797608.76,0.00,2020-Sep-03 18:41:53.000,560607.4,4797607.81,35,1012/5028,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 12:17:37""",560686.24,4797660.78,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560607.02,4797608.067,In Gabia_2,,,In Gabia_2,OK,20200912,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20015,1012,5056,184,1,561186.13,4798010.26,0.00,2020-Sep-03 18:41:53.000,561179.92,4798010.28,38,1012/5056,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:48:11""",561264.81,4798069.27,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561181.484,4798006.14,In Gabia_2,,,In Gabia_2,OK,20200912,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20014,1012,5052,188,1,561104.21,4797952.9,0.00,2020-Sep-03 18:41:53.000,561099.09,4797952.13,37,1012/5052,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:56:46""",561179.92,4798010.42,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561100.592,4797948.599,Fishermen,,,Fishermen,OK,20200914,,14-Sep,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20013,1012,5048,192,1,561022.3,4797895.55,0.00,2020-Sep-03 18:41:53.000,561017.51,4797898.16,38,1012/5048,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 11:58:48""",561097.92,4797954.02,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561019.934,4797894.387,In Gabia_2,,,In Gabia_2,OK,20200913,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20012,1012,5044,198,1,560940.38,4797838.19,0.00,2020-Sep-03 18:41:53.000,0,0,0,1012/5044,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 12:00:21""",561017.51,4797898.16,LANDING,C:\SpiceRack\20200904\positions_log.csv,560830.183,4797774.82,In Gabia_2,,,In Gabia_2,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20010,1012,5036,207,1,560776.55,4797723.47,0.00,2020-Sep-03 18:41:53.000,560771.05,4797725.37,38,1012/5036,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,,,,,,560773.729,4797720.893,Surface,,,Surface,OK,20200912,,15-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
20009,1012,5032,209,1,560694.63,4797666.12,0.00,2020-Sep-03 18:41:53.000,560686.06,4797660.8,38,1012/5032,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:53,247184153,"""2020-09-03 12:15:39""",560771.05,4797725.37,LANDING,C:\SpiceRack\20200904\positions_log.csv,560688.193,4797656.956,In Gabia_2,,,In Gabia_2,OK,20200913,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30001,1024,5000,7,1,560211.39,4796961.51,0.00,2020-Sep-03 18:41:54.000,560215.02,4796956.67,38,1024/5000,0.0,136.10,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 13:32:17""",560298.03,4797014.52,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560213.45,4796961.015,In Gabia_3,,,In Gabia_3,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30016,1024,5060,12,1,561440.11,4797821.87,0.00,2020-Sep-03 18:41:54.000,561445.96,4797815.07,39,1024/5060,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-04 14:32:56""",561579.41,4797901.1,SLEEP,C:\SpiceRack\20200904\positions_log.csv,561441.385,4797821.035,Surface,,,Surface,OK,20200912,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30017,1024,5064,13,1,561522.03,4797879.23,0.00,2020-Sep-03 18:41:54.000,561581.63,4797895.28,38,1024/5064,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-04 14:32:56""",561592.27,4797937.73,NULL,C:\SpiceRack\20200904\positions_log.csv,561581.19,4797895.56,In Gabia_3,,,In Gabia_3,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30020,1024,5076,19,1,561767.77,4798051.3,0.00,2020-Sep-04 11:19:00.000,561769,4798051.8,0,1024/5076,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 14:20:27""",561875.34,4798085.45,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561765.567,4798052.117,ROV,ROV_20200915_05,00:21,Manta_12025,,,,,,-30,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30015,1024,5056,21,1,561358.2,4797764.52,0.00,2020-Sep-03 18:41:54.000,561418.47,4797772.45,39,1024/5056,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 14:08:33""",561445.96,4797815.07,LANDING,C:\SpiceRack\20200904\positions_log.csv,561418.361,4797772.865,In Gabia_3,,,In Gabia_3,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30002,1024,5004,34,1,560293.3,4797018.87,0.00,2020-Sep-03 18:41:54.000,560298.26,4797014.79,38,1024/5004,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 13:34:00""",560377.78,4797071.06,LANDING,C:\SpiceRack\20200904\positions_log.csv,560294.922,4797020.2,In Gabia_3,,,In Gabia_3,OK,20200914,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30003,1024,5008,46,1,560375.22,4797076.22,0.00,2020-Sep-03 18:41:54.000,560377.78,4797071.06,38,1024/5008,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 13:35:42""",560459.58,4797131.65,LANDING,C:\SpiceRack\20200904\positions_log.csv,560377.035,4797077.075,In Gabia_3,,,In Gabia_3,OK,20200914,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30018,1024,5068,55,1,561603.94,4797936.59,0.00,2020-Sep-03 18:41:54.000,561616,4797933.93,35,1024/5068,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 14:12:46""",561686.5,4797991.88,LANDING,C:\SpiceRack\20200904\positions_log.csv,561615.628,4797934.243,Surface,,,Surface,Fail,20200912,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30019,1024,5072,83,1,561685.86,4797993.95,0.00,2020-Sep-03 18:41:54.000,561686.5,4797991.88,39,1024/5072,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,,,,,,561686.84,4797993.526,ROV,ROV_20200915_04,,No take off,,,Easter Egg flashing but not released,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30004,1024,5012,86,1,560457.13,4797133.58,0.00,2020-Sep-03 18:41:54.000,560459.58,4797131.65,38,1024/5012,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-04 14:33:09""",-1000,-1000,NULL,C:\SpiceRack\20200904\positions_log.csv,560458.967,4797136.786,In Gabia_3,,,In Gabia_3,OK,,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30005,1024,5016,89,1,560539.05,4797190.94,0.00,2020-Sep-03 18:41:54.000,0,0,0,1024/5016,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 13:43:23""",560620.54,4797246.59,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560479.91,4797095.516,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30006,1024,5020,129,1,560620.96,4797248.3,0.00,2020-Sep-03 18:41:54.000,560621.9,4797245.96,37,1024/5020,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 13:43:52""",560619.32,4797170.24,NAVIG,C:\SpiceRack\20200904\positions_log.csv,560622.526,4797249.315,Gabia missed,,,Gabia missed,,,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30007,1024,5024,132,1,560702.88,4797305.65,0.00,2020-Sep-03 18:41:54.000,0,0,0,1024/5024,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 13:46:29""",560800.6,4797380.53,LANDING,C:\SpiceRack\20200904\positions_log.csv,560689.369,4797241.869,ROV,ROV_20200915_02,00:20,Navigation issue,OK,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30021,1024,5080,139,1,561849.69,4798108.66,0.00,2020-Sep-03 18:41:54.000,561875.68,4798085.53,38,1024/5080,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 14:20:59""",561932.8,4798165.33,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561875.649,4798086.767,In Gabia_3,,,In Gabia_3,OK,20200912,Front truster HS,13-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30008,1024,5028,146,1,560784.79,4797363.01,0.00,2020-Sep-03 18:41:54.000,560800.6,4797380.53,38,1024/5028,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 13:47:04""",560783.34,4797363.17,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560785.95,4797365.636,In Gabia_3,,,In Gabia_3,OK,20200912,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30022,1024,5084,149,1,561931.61,4798166.02,0.00,2020-Sep-03 18:41:54.000,0,0,0,1024/5084,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 18:03:58""",561082.14,4797637.81,INIT,C:\SpiceRack\20200904\positions_log.csv,561932.877,4798166.765,In Gabia_3,,,In Gabia_3,OK,20200912,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30009,1024,5032,152,1,560866.71,4797420.37,0.00,2020-Sep-03 18:41:54.000,0,0,0,1024/5032,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 13:49:12""",560950.07,4797473.85,LANDING,C:\SpiceRack\20200904\positions_log.csv,560799.911,4797386.891,Surface,,,Surface,OK,20200912,,15-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30010,1024,5036,153,1,560948.62,4797477.73,0.00,2020-Sep-03 18:41:54.000,560950.07,4797473.85,38,1024/5036,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 13:55:49""",561032.79,4797532.38,LANDING,C:\SpiceRack\20200904\positions_log.csv,560949.103,4797477.235,In Gabia_3,,,In Gabia_3,OK,20200913,Front truster HS,13-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30023,1024,5088,154,1,562013.52,4798223.38,0.00,2020-Sep-03 18:41:54.000,562015.28,4798220.02,38,1024/5088,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 14:23:55""",562103.55,4798277.46,PMU-A,C:\SpiceRack\20200904\positions_log.csv,562013.738,4798221.875,In Gabia_3,,,In Gabia_3,OK,20200912,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30025,1024,5096,157,1,562177.35,4798338.09,0.00,2020-Sep-03 18:41:54.000,562182.94,4798336.65,39,1024/5096,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 15:28:56""",560382.14,4796717.02,PMU-A,C:\SpiceRack\20200904\positions_log.csv,562184.064,4798339.728,Surface,,,Surface,OK,20200912,Front truster HS,13-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30014,1024,5052,164,1,561276.28,4797707.16,0.00,2020-Sep-03 18:41:54.000,561280.89,4797703.67,38,1024/5052,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-04 14:31:44""",561409.05,4797770.28,NULL,C:\SpiceRack\20200904\positions_log.csv,561279.669,4797705.655,ROV,ROV_20200915_03,00:14,No take off,OK,,Easter Egg flashing but not released. Handle twisted. Foot twisted.,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30024,1024,5092,179,1,562095.44,4798280.73,0.00,2020-Sep-03 18:41:54.000,0,0,0,1024/5092,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 14:25:41""",562183.21,4798336.82,PMU-A,C:\SpiceRack\20200904\positions_log.csv,562099.891,4798280.664,Surface,,,Surface,fail,20200912,,14-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30011,1024,5040,194,1,561030.54,4797535.09,0.00,2020-Sep-03 18:41:54.000,561032.79,4797532.38,38,1024/5040,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 13:57:44""",561115.93,4797588.62,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561031.33,4797533.846,Fishermen,,,Fishermen,OK,20200912,Fishing boat,13-Sep,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30012,1024,5044,199,1,561112.45,4797592.44,0.00,2020-Sep-03 18:41:54.000,561116.39,4797589.21,38,1024/5044,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 18:15:00""",561081.53,4797635.15,SLEEP,C:\SpiceRack\20200904\positions_log.csv,561114.812,4797592.912,In Gabia_3,,,In Gabia_3,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
30013,1024,5048,204,1,561194.37,4797649.8,0.00,2020-Sep-03 18:41:54.000,561081.53,4797635.15,-10,1024/5048,0.0,255.10,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:54,247184154,"""2020-09-03 14:00:24""",561279.81,4797702.23,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561085.145,4797636.548,ROV,ROV_20200914_08,0:19,No take off,OK,,"Front truster broken, left rear truster broken",,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40025,1036,5096,1,1,562349.42,4798092.35,0.00,2020-Sep-03 18:41:55.000,562345.25,4798095.82,39,1036/5096,0.0,320.30,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:38:41""",560558.73,4796466.74,LANDED,C:\SpiceRack\20200904\positions_log.csv,562348.147,4798090.22,In Gabia_4,,,In Gabia_4,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40020,1036,5076,29,1,561939.85,4797805.56,0.00,2020-Sep-03 18:41:55.000,0,0,0,1036/5076,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 14:39:39""",562015.28,4797864.19,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561938.12,4797804.082,No wake Up,,,No wake Up,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40008,1036,5028,58,1,560956.87,4797117.27,0.00,2020-Sep-03 18:41:55.000,560952.47,4797119.6,39,1036/5028,0.0,331.70,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:11:52""",561033.05,4797174.56,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560955.049,4797117.106,In Gabia_4,,,In Gabia_4,Fail,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40002,1036,5004,59,1,560465.37,4796773.12,0.00,2020-Sep-03 18:41:55.000,0,0,0,1036/5004,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:26:16""",560542.13,4796830.1,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560516.925,4796829.213,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40007,1036,5024,60,1,560874.95,4797059.91,0.00,2020-Sep-03 18:41:55.000,560869.67,4797062.62,39,1036/5024,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:13:02""",560952.47,4797119.6,LANDED,C:\SpiceRack\20200904\positions_log.csv,560870.264,4797060.508,In Gabia_4,,,In Gabia_4,OK,20200912,Eastern Egg and front truster HS,13-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40023,1036,5088,61,1,562185.59,4797977.63,0.00,2020-Sep-03 18:41:55.000,562184.43,4797977.33,40,1036/5088,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 14:33:57""",562264.35,4798037.42,LANDING,C:\SpiceRack\20200904\positions_log.csv,562183.736,4797976.341,Surface,,,Surface,OK,20200912,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40021,1036,5080,68,1,562021.76,4797862.92,0.00,2020-Sep-03 18:41:55.000,562017.15,4797864.24,38,1036/5080,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 14:38:13""",562097.3,4797921.75,PMU-A,C:\SpiceRack\20200904\positions_log.csv,562018.724,4797862.732,In Gabia_4,,,In Gabia_4,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40004,1036,5012,79,1,560629.2,4796887.84,0.00,2020-Sep-03 18:41:55.000,560624.31,4796890.41,39,1036/5012,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:23:23""",560705.23,4796944.96,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560626.537,4796887.91,In Gabia_4,,,In Gabia_4,OK,20200912,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40003,1036,5008,92,1,560547.29,4796830.48,0.00,2020-Sep-03 18:41:55.000,560541.67,4796829.97,39,1036/5008,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:24:43""",560624.31,4796890.41,LANDING,C:\SpiceRack\20200904\positions_log.csv,560543.26,4796827.954,In Gabia_4,,,In Gabia_4,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40022,1036,5084,95,1,562103.68,4797920.27,0.00,2020-Sep-03 18:41:55.000,562097.86,4797921.15,38,1036/5084,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 14:35:58""",562184.48,4797977.26,PMU-A,C:\SpiceRack\20200904\positions_log.csv,562100.521,4797918.079,In Gabia_4,,,In Gabia_4,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40006,1036,5020,98,1,560793.03,4797002.55,0.00,2020-Sep-03 18:41:55.000,560788.07,4797001.68,39,1036/5020,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:15:20""",560869.67,4797062.62,LANDING,C:\SpiceRack\20200904\positions_log.csv,560789.596,4797001.951,No take off,,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40005,1036,5016,107,1,560711.12,4796945.19,0.00,2020-Sep-03 18:41:55.000,560706.95,4796941.36,35,1036/5016,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:16:33""",560788.07,4797001.68,LANDING,C:\SpiceRack\20200904\positions_log.csv,560707.482,4796943.492,Surface,,,Surface,OK,20200913,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40024,1036,5092,125,1,562267.51,4798034.99,0.00,2020-Sep-03 18:41:55.000,562264.35,4798037.42,39,1036/5092,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 14:32:01""",562345.25,4798095.82,LANDED,C:\SpiceRack\20200904\positions_log.csv,562268.344,4798035.57,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40019,1036,5072,159,1,561857.93,4797748.2,0.00,2020-Sep-03 18:41:55.000,561854.63,4797752.19,39,1036/5072,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 14:45:09""",561937.53,4797807.32,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561856.892,4797750.094,In Gabia_4,,,In Gabia_4,OK,20200913,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40018,1036,5068,165,1,561776.02,4797690.84,0.00,2020-Sep-03 18:41:55.000,561772.28,4797692.42,39,1036/5068,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 14:47:07""",561854.63,4797752.19,LANDING,C:\SpiceRack\20200904\positions_log.csv,561774.503,4797689.653,Fished,EE_20200915_01,,No com,,,Water inside - Not operative,15-Sep,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40017,1036,5064,166,1,561694.1,4797633.49,0.00,2020-Sep-03 18:41:55.000,561688.08,4797630.81,35,1036/5064,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 14:49:02""",561772.28,4797692.42,LANDING,C:\SpiceRack\20200904\positions_log.csv,561690.857,4797632.607,Fished,EE_20200915_02,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40012,1036,5044,169,1,561284.53,4797346.7,0.00,2020-Sep-03 18:41:55.000,561280.16,4797349.01,39,1036/5044,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:01:50""",561362.94,4797406.42,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561281.64,4797345.104,In Gabia_4,,,In Gabia_4,OK,20200912,Eastern Egg HS,13-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40015,1036,5056,170,1,561530.27,4797518.77,0.00,2020-Sep-03 18:41:55.000,561524.03,4797522.83,39,1036/5056,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,,,,,,561527.75,4797521.011,In Gabia_4,,,In Gabia_4,OK,20200912,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40011,1036,5040,180,1,561202.61,4797289.34,0.00,2020-Sep-03 18:41:55.000,561197.96,4797282.17,35,1036/5040,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:03:10""",561280.16,4797349.01,LANDING,C:\SpiceRack\20200904\positions_log.csv,561198.278,4797284.676,No take off,,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40010,1036,5036,181,1,561120.7,4797231.98,0.00,2020-Sep-03 18:41:55.000,561114.59,4797231.45,39,1036/5036,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:04:52""",561197.96,4797282.17,LANDING,C:\SpiceRack\20200904\positions_log.csv,561117.732,4797229.509,In Gabia_4,,,In Gabia_4,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40009,1036,5032,182,1,561038.78,4797174.62,0.00,2020-Sep-03 18:41:55.000,561033.13,4797174.35,39,1036/5032,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:10:09""",561114.59,4797231.45,LANDING,C:\SpiceRack\20200904\positions_log.csv,561034.728,4797173.322,Fished,EE_20200915_03,,No wake Up,,,Water inside - Not operative,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40014,1036,5052,196,1,561448.36,4797461.41,0.00,2020-Sep-03 18:41:55.000,561446.02,4797465.12,39,1036/5052,0.0,334.50,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 14:58:41""",561524.03,4797522.83,LANDING,C:\SpiceRack\20200904\positions_log.csv,561447.941,4797461.812,In Gabia_4,,,In Gabia_4,OK,20200913,Front truster HS,13-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40001,1036,5000,197,1,560383.46,4796715.76,0.00,2020-Sep-03 18:41:55.000,560381.92,4796717.07,39,1036/5000,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-04 14:33:09""",-1000,-1000,NULL,C:\SpiceRack\20200904\positions_log.csv,560382.385,4796716.119,In Gabia_4,,,In Gabia_4,OK,20200913,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40013,1036,5048,203,1,561366.44,4797404.05,0.00,2020-Sep-03 18:41:55.000,561362.93,4797405.85,39,1036/5048,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:00:08""",561446.02,4797465.12,LANDED,C:\SpiceRack\20200904\positions_log.csv,561365.638,4797405.012,In Gabia_4,,,In Gabia_4,OK,20200913,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
40016,1036,5060,208,1,561612.19,4797576.13,0.00,2020-Sep-04 10:34:00.000,561617.1,4797575.4,0,1036/5060,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 14:50:22""",561689.35,4797633.22,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561616.807,4797573.081,ROV,ROV_20200915_06,00:49,Manta_12443,,,,,,-30,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50023,1048,5088,16,1,562357.67,4797731.89,0.00,2020-Sep-03 18:41:56.000,562364.06,4797731.22,40,1048/5088,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:27:58""",562444.78,4797789.45,LANDING,C:\SpiceRack\20200904\positions_log.csv,562365.813,4797735.62,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50024,1048,5092,18,1,562439.58,4797789.24,0.00,2020-Sep-03 18:41:56.000,562444.78,4797789.45,40,1048/5092,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:29:40""",562525.03,4797844.5,LANDED,C:\SpiceRack\20200904\positions_log.csv,562445.395,4797793.56,Gabia missed,,,Gabia missed,,,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50025,1048,5096,24,1,562521.5,4797846.6,0.00,2020-Sep-03 18:41:56.000,562525.03,4797844.5,40,1048/5096,0.0,141.70,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:43:45""",560721.14,4796224.79,PMU-A,C:\SpiceRack\20200904\positions_log.csv,562523.401,4797845.446,In Gabia_4,,,In Gabia_4,OK,20200912,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50022,1048,5084,35,1,562275.75,4797674.53,0.00,2020-Sep-03 18:41:56.000,562279.55,4797672.75,39,1048/5084,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:29:02""",562447.28,4797840.15,NULL,C:\SpiceRack\20200904\positions_log.csv,562279.206,4797674.313,Surface,,,Surface,OK,20200912,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50021,1048,5080,36,1,562193.84,4797617.17,0.00,2020-Sep-03 18:41:56.000,0,0,0,1048/5080,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:25:09""",562278.63,4797671.28,PMU-A,C:\SpiceRack\20200904\positions_log.csv,562133.088,4797594.781,Fished,EE_20200912_02,,Fished,OK,20200913,Eastern Egg HS,13-Sep,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50005,1048,5016,47,1,560883.19,4796699.45,0.00,2020-Sep-04 09:56:00.000,560911.4,4796699.2,0,1048/5016,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:52:27""",561192.97,4796932.48,NAVIG,C:\SpiceRack\20200904\positions_log.csv,560883.164,4796698.138,ROV,ROV_20200915_07,00:27,Manta_1240,,,,,,-30,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50010,1048,5036,51,1,561292.77,4796986.24,0.00,2020-Sep-03 18:41:56.000,0,0,0,1048/5036,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:02:00""",561378.14,4797042.43,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561443.887,4797094.158,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50004,1048,5012,53,1,560801.28,4796642.09,0.00,2020-Sep-03 18:41:55.000,560805.24,4796641.73,38,1048/5012,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-04 13:57:40""",560880.65,4796701.28,NULL,C:\SpiceRack\20200904\positions_log.csv,560802.513,4796643.087,In Gabia_4,,,In Gabia_4,OK,20200912,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50011,1048,5040,108,1,561374.68,4797043.59,0.00,2020-Sep-03 18:41:56.000,561378.55,4797043.79,38,1048/5040,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:03:26""",561459.17,4797098,LANDING,C:\SpiceRack\20200904\positions_log.csv,561378.269,4797046.361,Surface,,,Surface,OK,20200914,,14-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50003,1048,5008,113,1,560719.36,4796584.73,0.00,2020-Sep-03 18:41:55.000,560724.85,4796582.47,40,1048/5008,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:42:32""",560805.24,4796641.73,LANDING,C:\SpiceRack\20200904\positions_log.csv,560725.489,4796585.824,In Gabia_4,,,In Gabia_4,OK,20200912,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50012,1048,5044,114,1,561456.6,4797100.95,0.00,2020-Sep-03 18:41:56.000,561459.17,4797098,40,1048/5044,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:02:45""",561444.84,4797094.82,NULL,C:\SpiceRack\20200904\positions_log.csv,561460.668,4797101.438,Fishermen,,,Fishermen,OK,20200912,Front truster HS,13-Sep,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50008,1048,5028,116,1,561128.94,4796871.52,0.00,2020-Sep-03 18:41:55.000,561130.76,4796871.98,39,1048/5028,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-04 14:33:09""",-1000,-1000,NULL,C:\SpiceRack\20200904\positions_log.csv,561129.58,4796872.38,In Gabia_4,,,In Gabia_4,OK,20200914,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50006,1048,5020,127,1,560965.11,4796756.81,0.00,2020-Sep-03 18:41:55.000,560966.27,4796753.71,38,1048/5020,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:49:18""",561048.61,4796808.77,LANDING,C:\SpiceRack\20200904\positions_log.csv,560964.118,4796754.465,In Gabia_4,,,In Gabia_4,OK,20200912,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50007,1048,5024,133,1,561047.02,4796814.16,0.00,2020-Sep-03 18:41:55.000,561048.61,4796808.77,40,1048/5024,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:51:16""",561130.94,4796871.76,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561049.771,4796813.181,In Gabia_4,,,In Gabia_4,OK,20200913,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50013,1048,5048,142,1,561538.51,4797158.31,0.00,2020-Sep-03 18:41:56.000,0,0,0,1048/5048,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:06:25""",561633.31,4797220.15,LANDED,C:\SpiceRack\20200904\positions_log.csv,,,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50017,1048,5064,148,1,561866.17,4797387.74,0.00,2020-Sep-03 18:41:56.000,561871.62,4797384.94,40,1048/5064,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:15:07""",561948.3,4797442.48,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561870.272,4797388.384,Surface,,,Surface,OK,20200912,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50018,1048,5068,151,1,561948.09,4797445.1,0.00,2020-Sep-03 18:41:56.000,561948.87,4797442,40,1048/5068,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:18:00""",562019.18,4797502.75,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561951.805,4797447.679,No take off,,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50015,1048,5056,171,1,561702.34,4797273.02,0.00,2020-Sep-03 18:41:56.000,561707.62,4797273.05,38,1048/5056,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:12:40""",561786.59,4797327.81,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561706.482,4797274.799,Surface,,,Surface,OK,20200912,,15-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50001,1048,5000,183,1,560555.53,4796470.02,0.00,2020-Sep-03 18:41:55.000,560558.73,4796466.74,40,1048/5000,0.0,150.20,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:40:08""",560644.57,4796524.63,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560558.708,4796469.609,In Gabia_4,,,In Gabia_4,OK,20200912,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50019,1048,5072,191,1,562030.01,4797502.46,0.00,2020-Sep-03 18:41:56.000,562018.97,4797502.9,39,1048/5072,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:17:44""",562132.21,4797564.9,LANDING,C:\SpiceRack\20200904\positions_log.csv,562021.638,4797502.011,Surface,,,Surface,OK,20200913,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50002,1048,5004,195,1,560637.45,4796527.38,0.00,2020-Sep-03 18:41:55.000,560644.22,4796524.88,40,1048/5004,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:55,247184155,"""2020-09-03 15:41:15""",560724.53,4796582.18,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560643.425,4796528.087,In Gabia_4,,,In Gabia_4,OK,20200913,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50014,1048,5052,200,1,561620.43,4797215.67,0.00,2020-Sep-03 18:41:56.000,561633.31,4797220.15,40,1048/5052,0.0,141.70,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:07:29""",561707.38,4797272.18,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561633.625,4797219.467,No wake Up,,,No wake Up,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50016,1048,5060,201,1,561784.26,4797330.38,0.00,2020-Sep-03 18:41:56.000,561787.72,4797329.12,39,1048/5060,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 16:13:44""",561871.62,4797384.94,LANDING,C:\SpiceRack\20200904\positions_log.csv,561788.421,4797330.933,No wake Up,,,No wake Up,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50020,1048,5076,206,1,562111.92,4797559.81,0.00,2020-Sep-03 18:41:56.000,562132.21,4797564.9,40,1048/5076,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-04 14:33:09""",-1000,-1000,NULL,C:\SpiceRack\20200904\positions_log.csv,562131.838,4797568.01,Surface,,,Surface,OK,20200914,,14-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
50009,1048,5032,210,1,561210.85,4796928.88,0.00,2020-Sep-03 18:41:56.000,0,0,0,1048/5032,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,,,,,,561196.164,4796930.859,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60017,1060,5064,5,1,562038.25,4797141.99,0.00,2020-Sep-03 18:41:57.000,562032.52,4797142.46,41,1060/5064,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,"""2020-09-03 17:03:58""",562114.01,4797201.91,PMU-A,C:\SpiceRack\20200904\positions_log.csv,562034.626,4797139.477,in Gabia_5,,,in Gabia_5,OK,20200913,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60016,1060,5060,6,1,561956.33,4797084.64,0.00,2020-Sep-03 18:41:57.000,561954.44,4797089.04,41,1060/5060,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,"""2020-09-03 17:05:09""",562032.39,4797142.81,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561954.287,4797085.005,Surface failed,,,Surface failed,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60015,1060,5056,17,1,561874.42,4797027.28,0.00,2020-Sep-03 18:41:57.000,561870.29,4797030.43,41,1060/5056,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,"""2020-09-03 17:06:25""",561954.44,4797089.04,LANDING,C:\SpiceRack\20200904\positions_log.csv,561871.367,4797029.29,in Gabia_5,,,in Gabia_5,OK,20200914,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60007,1060,5024,23,1,561219.1,4796568.42,0.00,2020-Sep-03 18:41:56.000,561211.31,4796571.23,41,1060/5024,0.0,343.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:31:13""",561295.77,4796627.79,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561215.18,4796566.522,Surface,,,Surface,OK,20200913,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60020,1060,5076,25,1,562283.99,4797314.07,0.00,2020-Sep-03 18:41:57.000,562278.7,4797316.4,39,1060/5076,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562280.32,4797312.616,in Gabia_5,,,in Gabia_5,OK,20200913,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60019,1060,5072,26,1,562202.08,4797256.71,0.00,2020-Sep-03 18:41:57.000,0,0,0,1060/5072,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,"""2020-09-03 17:00:27""",562277.25,4797318.15,PMU-A,C:\SpiceRack\20200904\positions_log.csv,562171.926,4797287.809,Surface,,,Surface,OK,20200913,Eastern Egg HS,13-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60022,1060,5084,27,1,562447.82,4797428.78,0.00,2020-Sep-03 18:41:57.000,562441.18,4797433.37,41,1060/5084,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,"""2020-09-03 16:40:59""",562594.05,4797574.21,NULL,C:\SpiceRack\20200904\positions_log.csv,562444.789,4797429.978,in Gabia_5,,,in Gabia_5,OK,20200913,Front truster HS,13-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60023,1060,5088,30,1,562529.74,4797486.14,0.00,2020-Sep-03 18:41:57.000,0,0,0,1060/5088,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,"""2020-09-03 16:37:47""",562605.01,4797544.81,LANDING,C:\SpiceRack\20200904\positions_log.csv,562594.718,4797574.544,No take off,,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60024,1060,5092,31,1,562611.65,4797543.5,0.00,2020-Sep-03 18:41:57.000,562605.01,4797544.81,41,1060/5092,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,"""2020-09-03 16:38:06""",562715.12,4797714.73,NULL,C:\SpiceRack\20200904\positions_log.csv,562609.086,4797540.06,in Gabia_5,,,in Gabia_5,OK,20200914,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60025,1060,5096,32,1,562693.57,4797600.86,0.00,2020-Sep-03 18:41:57.000,0,0,0,1060/5096,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562714.895,4797711.251,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60018,1060,5068,39,1,562120.16,4797199.35,0.00,2020-Sep-03 18:41:57.000,562114.43,4797200.67,41,1060/5068,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,"""2020-09-04 14:33:09""",-1000,-1000,NULL,C:\SpiceRack\20200904\positions_log.csv,562115.953,4797198.078,in Gabia_5,,,in Gabia_5,OK,20200914,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60010,1060,5036,41,1,561464.84,4796740.49,0.00,2020-Sep-03 18:41:56.000,561461.77,4796737.26,40,1060/5036,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:20:53""",561540.86,4796800.29,LANDED,C:\SpiceRack\20200904\positions_log.csv,561462.714,4796738.599,in Gabia_5,,,in Gabia_5,OK,20200913,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60004,1060,5012,50,1,560973.35,4796396.34,0.00,2020-Sep-03 18:41:56.000,560967.85,4796398.88,41,1060/5012,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:38:41""",561051.42,4796457.21,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560971,4796394.574,in Gabia_5,,,in Gabia_5,OK,20200913,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60014,1060,5052,65,1,561792.5,4796969.92,0.00,2020-Sep-03 18:41:57.000,0,0,0,1060/5052,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,"""2020-09-03 17:14:51""",561870.46,4797030.51,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561838.329,4797028.333,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60013,1060,5048,66,1,561710.59,4796912.56,0.00,2020-Sep-03 18:41:56.000,561707.37,4796915.04,38,1060/5048,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-04 14:33:09""",-1000,-1000,NULL,C:\SpiceRack\20200904\positions_log.csv,561707.996,4796913.517,in Gabia_5,,,in Gabia_5,OK,20200914,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60021,1060,5080,80,1,562365.91,4797371.42,0.00,2020-Sep-04 10:54:00.000,562368.3,4797368.8,0,1060/5080,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,"""2020-09-03 16:40:40""",562441.18,4797433.37,LANDING,C:\SpiceRack\20200904\positions_log.csv,562368.733,4797370.963,Manta_12614,Manta_12614,,Manta_12614,,,,,,-30,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60002,1060,5004,81,1,560809.52,4796281.63,0.00,2020-Sep-03 18:41:56.000,560806.44,4796286.11,41,1060/5004,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:41:15""",560888.53,4796340.44,LANDED,C:\SpiceRack\20200904\positions_log.csv,560807.441,4796280.46,No take off,,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60006,1060,5020,85,1,561137.18,4796511.06,0.00,2020-Sep-03 18:41:56.000,561132.91,4796511.38,41,1060/5020,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:32:40""",561211.31,4796571.23,LANDED,C:\SpiceRack\20200904\positions_log.csv,561133.24,4796507.219,in Gabia_5,,,in Gabia_5,OK,20200913,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60009,1060,5032,101,1,561382.93,4796683.13,0.00,2020-Sep-03 18:41:56.000,561380.32,4796687.55,41,1060/5032,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:28:27""",561462.29,4796737.23,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561381.595,4796683.86,No wake Up,,,No wake Up,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60005,1060,5016,121,1,561055.27,4796453.7,0.00,2020-Sep-03 18:41:56.000,561052.07,4796456.34,40,1060/5016,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:33:53""",561132.91,4796511.38,LANDING,C:\SpiceRack\20200904\positions_log.csv,561054.43,4796453.122,in Gabia_5,,,in Gabia_5,OK,20200914,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60001,1060,5000,123,1,560727.6,4796224.27,0.00,2020-Sep-03 18:41:56.000,560722.61,4796222.71,38,1060/5000,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:42:29""",560806.31,4796286.35,PMU-A,C:\SpiceRack\20200904\positions_log.csv,560724.094,4796221.694,in Gabia_5,,,in Gabia_5,OK,20200914,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60008,1060,5028,145,1,561301.01,4796625.78,0.00,2020-Sep-03 18:41:56.000,561295.8,4796627.66,41,1060/5028,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:29:50""",561380.32,4796687.55,LANDING,C:\SpiceRack\20200904\positions_log.csv,561298.63,4796625.635,in Gabia_5,,,in Gabia_5,OK,20200913,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60012,1060,5044,176,1,561628.67,4796855.21,0.00,2020-Sep-03 18:41:56.000,561624.82,4796856.33,40,1060/5044,0.0,-10.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:18:09""",561705.89,4796919.07,PMU-A,C:\SpiceRack\20200904\positions_log.csv,561625.079,4796852.846,Fishermen,,,Fishermen,OK,20200912,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60003,1060,5008,178,1,560891.44,4796338.99,0.00,2020-Sep-03 18:41:56.000,560888.53,4796340.44,40,1060/5008,0.0,328.80,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:39:55""",560967.85,4796398.88,LANDING,C:\SpiceRack\20200904\positions_log.csv,560889.279,4796336.938,Surface failed,,,Surface failed,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
60011,1060,5040,190,1,561546.76,4796797.85,0.00,2020-Sep-03 18:41:56.000,561540.86,4796800.29,41,1060/5040,0.0,326.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:56,247184156,"""2020-09-03 17:19:17""",561624.82,4796856.33,LANDING,C:\SpiceRack\20200904\positions_log.csv,561542.942,4796795.61,in Gabia_5,,,in Gabia_5,OK,20200913,Front truster HS,13-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70019,1072,5072,8,1,562374.15,4797010.96,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5072,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562377.226,4797012.3,in Gabia_5,,,in Gabia_5,OK,20200913,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70020,1072,5076,14,1,562456.07,4797068.32,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5076,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562462.045,4797062.995,Gabia missed,,,Surface,,,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70001,1072,5000,75,1,560899.68,4795978.53,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5000,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,560902.441,4795977.074,in Gabia_5,,,in Gabia_5,OK,20200914,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70002,1072,5004,96,1,560981.59,4796035.88,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5004,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,560985.309,4796034.419,Surface,,,Surface,OK,20200914,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70021,1072,5080,117,1,562537.98,4797125.68,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5080,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562534.141,4797123.61,in Gabia_5,,,in Gabia_5,OK,20200913,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70025,1072,5096,126,1,562865.64,4797355.11,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5096,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562868.786,4797352.891,in Gabia_5,,,in Gabia_5,OK,20200913,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70022,1072,5084,130,1,562619.9,4797183.04,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5084,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562626.12,4797180.707,Surface,,,Surface,OK,20200914,,,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70024,1072,5092,135,1,562783.73,4797297.75,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5092,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562789.217,4797297.665,Surface,,,Surface,OK,20200913,Eastern Egg and front truster HS,13-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70018,1072,5068,137,1,562292.24,4796953.61,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5068,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562305.188,4796953.401,Fished,EE_20200912_01,,Fished,OK,20200913,Eastern Egg and front truster HS,13-Sep,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70023,1072,5088,140,1,562701.81,4797240.39,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5088,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562711.023,4797232.686,Surface,,,Surface,OK,20200913,,14-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70010,1072,5036,144,1,561636.91,4796494.75,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5036,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561631.634,4796506.086,in Gabia_5,,,in Gabia_5,OK,20200913,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70007,1072,5024,150,1,561391.17,4796322.67,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5024,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561395.994,4796324.225,in Gabia_5,,,in Gabia_5,OK,20200914,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70011,1072,5040,158,1,561718.83,4796552.1,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5040,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561587.348,4796631.849,Surface failed,,,Surface failed,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70016,1072,5060,160,1,562128.41,4796838.89,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5060,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562133.38,4796838.645,in Gabia_5,,,in Gabia_5,OK,20200913,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70017,1072,5064,161,1,562210.32,4796896.25,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5064,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562123.354,4796853.919,No take off,,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70013,1072,5048,162,1,561882.66,4796666.82,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5048,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561802.101,4796622.036,Fishermen,,,Fishermen,OK,20200912,Fishing boat,13-Sep,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70015,1072,5056,167,1,562046.49,4796781.53,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5056,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562063.163,4796784.615,in Gabia_5,,,in Gabia_5,OK,20200913,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70014,1072,5052,168,1,561964.58,4796724.18,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5052,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561958.186,4796728.041,Surface,,,Surface,OK,20200913,,15-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70008,1072,5028,173,1,561473.08,4796380.03,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5028,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561473.532,4796379.241,in Gabia_5,,,in Gabia_5,OK,20200913,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70006,1072,5020,186,1,561309.25,4796265.31,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5020,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561308.411,4796262.927,Surface,,,Surface,OK,20200913,,14-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70009,1072,5032,187,1,561555,4796437.39,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5032,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561557.082,4796438.134,in Gabia_5,,,in Gabia_5,OK,20200913,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70012,1072,5044,189,1,561800.74,4796609.46,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5044,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561803.92,4796606.764,in Gabia_5,,,in Gabia_5,OK,20200913,,15-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70003,1072,5008,193,1,561063.51,4796093.24,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5008,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,560998.126,4796047.935,in Gabia_5,,,in Gabia_5,OK,20200913,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70004,1072,5012,202,1,561145.42,4796150.6,0.00,2020-Sep-03 18:41:57.000,0,0,0,1072/5012,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561145.75,4796151.973,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
70005,1072,5016,205,1,561227.34,4796207.96,0.00,2020-Sep-04 09:28:00.000,561231.5,479622,0,1072/5016,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561225.176,4796206.454,ROV,ROV_20200915_08,00:13,Manta_4071,,,Battery HS,,,-30,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80008,1084,5028,3,1,561645.16,4796134.28,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5028,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561638.939,4796134.916,In Gabia_6,,,In Gabia_6,OK,20200914,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80009,1084,5032,9,1,561727.07,4796191.64,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5032,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561725.473,4796192.107,In Gabia_6,,,In Gabia_6,OK,20200913,Eastern Egg HS,13-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80005,1084,5016,20,1,561399.41,4795962.21,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5016,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561452.337,4796018.81,No wake Up,,,No wake Up,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80012,1084,5044,22,1,561972.82,4796363.71,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5044,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561966.857,4796366.027,Surface,,,Surface,OK,20200914,,14-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80014,1084,5052,33,1,562136.65,4796478.43,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5052,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562135.671,4796478.783,Fishermen,,,Fishermen,OK,20200912,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80015,1084,5056,38,1,562218.56,4796535.79,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5056,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562208.39,4796531.419,Fishermen,,,Fishermen,OK,20200912,Fishing boat,13-Sep,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80013,1084,5048,42,1,562054.73,4796421.07,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5048,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562051.96,4796423.01,Surface,,,Surface,OK,20200914,,14-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80001,1084,5000,48,1,561071.75,4795732.78,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5000,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561070.56,4795734.655,In Gabia_6,,,In Gabia_6,OK,20200913,,14-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80016,1084,5060,49,1,562300.48,4796593.15,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5060,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562365.641,4796656.326,Fishermen,,,Fishermen,OK,20200913,Fishing boat,13-Sep,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80024,1084,5092,64,1,562955.8,4797052.01,0.00,2020-Sep-03 18:41:58.000,0,0,0,1084/5092,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:58,247184158,,,,,,562954.669,4797050.97,Fishermen,,,Fishermen,OK,20200912,,15-Sep,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80019,1084,5072,69,1,562546.22,4796765.22,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5072,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562544.851,4796766.816,Surface failed,,,Surface failed,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80020,1084,5076,70,1,562628.14,4796822.58,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5076,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562626.102,4796821.835,Surface,,,Surface,OK,20200914,,14-Sep,,10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80007,1084,5024,74,1,561563.24,4796076.93,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5024,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561623.984,4796133.78,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80021,1084,5080,76,1,562710.05,4796879.93,0.00,2020-Sep-03 18:41:58.000,0,0,0,1084/5080,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:58,247184158,,,,,,562708.586,4796880.264,No com,,,No com,,,,,,-10,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80022,1084,5084,77,1,562791.97,4796937.29,0.00,2020-Sep-03 18:41:58.000,0,0,0,1084/5084,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:58,247184158,,,,,,562788.733,4796937.844,No take off,,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80023,1084,5088,82,1,562873.89,4796994.65,0.00,2020-Sep-03 18:41:58.000,0,0,0,1084/5088,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:58,247184158,,,,,,562868.382,4796995.728,No take off,,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80006,1084,5020,97,1,561481.33,4796019.57,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5020,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561479.344,4796019.992,In Gabia_6,,,In Gabia_6,OK,20200914,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80018,1084,5068,105,1,562464.31,4796707.86,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5068,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562460.123,4796712.156,In Gabia_6,,,In Gabia_6,OK,20200913,Front truster HS,13-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80010,1084,5036,106,1,561808.99,4796249,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5036,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561807.981,4796250.177,No take off,,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80003,1084,5008,111,1,561235.58,4795847.5,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5008,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561234.892,4795851.971,No take off,,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80017,1084,5064,112,1,562382.39,4796650.5,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5064,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,562381.774,4796653.825,In Gabia_6,,,In Gabia_6,OK,20200914,,,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80004,1084,5012,118,1,561317.5,4795904.85,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5012,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561319.96,4795905.44,No wake Up,,,No wake Up,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80011,1084,5040,120,1,561890.9,4796306.36,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5040,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561887.928,4796309.881,Fishermen,,,Fishermen,OK,20200912,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80025,1084,5096,122,1,563037.72,4797109.36,0.00,2020-Sep-03 18:41:58.000,0,0,0,1084/5096,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:58,247184158,,,,,,563034.171,4797110.504,In Gabia_6,,,In Gabia_6,OK,20200913,Left rear truster HS,13-Sep,,20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
80002,1084,5004,163,1,561153.67,4795790.14,0.00,2020-Sep-03 18:41:57.000,0,0,0,1084/5004,0.0,0.00,,0,0,0,0.0,0,,,,,,,,,,,,,,,,3-Sep-20,247,6:41:57,247184157,,,,,,561147.742,4795788.78,No take off,,,No take off,,,,,,-20,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
99999,9999,9999,110,9,,,,,,,,9999/9999,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,Never deployed,,,,,,Never deployed and mere porteuse,14-Sep,,#N/A,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
99999,9999,9999,62,9,,,,,,,,9999/9999,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,Never deployed,,,,,,Never deployed,13-Sep,,#N/A,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
2426
static/darf_nodes.json
Normal file
2426
static/darf_nodes.json
Normal file
File diff suppressed because it is too large
Load Diff
580
static/index.html
Normal file
580
static/index.html
Normal file
@@ -0,0 +1,580 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Seismic H5 Viewer</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #0a1628 0%, #1a2744 100%);
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #00ff88;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.status-dot.error { background: #ff4757; animation: none; }
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 350px 1fr;
|
||||
gap: 20px;
|
||||
height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 15px;
|
||||
color: #00d9ff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
padding: 14px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid transparent;
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: rgba(0, 217, 255, 0.1);
|
||||
border-color: rgba(0, 217, 255, 0.3);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
background: rgba(0, 217, 255, 0.15);
|
||||
border-color: #00d9ff;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.file-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.viewer-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.channel-btn {
|
||||
padding: 10px 20px;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
background: rgba(0,0,0,0.3);
|
||||
color: #e0e0e0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.channel-btn:hover {
|
||||
border-color: #00d9ff;
|
||||
color: #00d9ff;
|
||||
}
|
||||
|
||||
.channel-btn.active {
|
||||
background: linear-gradient(135deg, #00d9ff, #00a8cc);
|
||||
color: #0a1628;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.data-type-toggle {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.data-type-toggle button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.data-type-toggle button.active {
|
||||
background: #00d9ff;
|
||||
color: #0a1628;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 15px 20px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.7rem;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(0, 217, 255, 0.2);
|
||||
border-top-color: #00d9ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
color: #888;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(0,217,255,0.3); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(0,217,255,0.5); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="url(#grad)" stroke-width="2">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#00d9ff"/>
|
||||
<stop offset="100%" style="stop-color:#00ff88"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M2 12h4l3-9 4 18 3-9h6"/>
|
||||
</svg>
|
||||
Seismic H5 Viewer
|
||||
</h1>
|
||||
<div class="status">
|
||||
<div class="status-item">
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">Connecting...</span>
|
||||
</div>
|
||||
<div class="status-item" id="fileCount">— files</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="panel">
|
||||
<h2>📁 H5 Files</h2>
|
||||
<div class="file-list" id="fileList">
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
Loading files...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel viewer-panel" id="viewerPanel">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M2 12h4l3-9 4 18 3-9h6"/>
|
||||
</svg>
|
||||
<h3>Select a file</h3>
|
||||
<p>Choose an H5 file from the list to view seismic waveforms</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = '/seismic-api/api';
|
||||
|
||||
let currentFile = null;
|
||||
let currentChannel = 'channel_1';
|
||||
let currentDataType = 'calibrated_data';
|
||||
let chart = null;
|
||||
|
||||
async function fetchAPI(endpoint) {
|
||||
const res = await fetch(`${API_BASE}${endpoint}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async function checkHealth() {
|
||||
try {
|
||||
const data = await fetchAPI('/h5/files');
|
||||
document.getElementById('statusDot').className = 'status-dot';
|
||||
document.getElementById('statusText').textContent = 'Connected';
|
||||
document.getElementById('fileCount').textContent = `${data.length} files`;
|
||||
return data;
|
||||
} catch (e) {
|
||||
document.getElementById('statusDot').className = 'status-dot error';
|
||||
document.getElementById('statusText').textContent = 'Disconnected';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFiles() {
|
||||
try {
|
||||
const files = await checkHealth();
|
||||
const list = document.getElementById('fileList');
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state"><p>No H5 files found</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by node
|
||||
const byNode = {};
|
||||
files.forEach(f => {
|
||||
const node = f.nodeId || 'Unknown';
|
||||
if (!byNode[node]) byNode[node] = [];
|
||||
byNode[node].push(f);
|
||||
});
|
||||
|
||||
list.innerHTML = files.map(f => `
|
||||
<div class="file-item" onclick="selectFile('${f.filename}')" data-file="${f.filename}">
|
||||
<div class="file-name">Node ${f.nodeId} - ${f.date}</div>
|
||||
<div class="file-meta">
|
||||
<span>⏱️ ${formatDuration(f.duration_sec)}</span>
|
||||
<span>📊 ${f.n_channels} ch</span>
|
||||
<span>🎵 ${f.sample_rate_hz} Hz</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
document.getElementById('fileList').innerHTML =
|
||||
`<div class="empty-state"><p>Error: ${e.message}</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFile(filename) {
|
||||
currentFile = filename;
|
||||
|
||||
document.querySelectorAll('.file-item').forEach(el => {
|
||||
el.classList.toggle('selected', el.dataset.file === filename);
|
||||
});
|
||||
|
||||
const panel = document.getElementById('viewerPanel');
|
||||
panel.innerHTML = '<div class="loading"><div class="spinner"></div>Loading waveform...</div>';
|
||||
|
||||
try {
|
||||
const data = await fetchAPI(`/h5/data?file=${filename}`);
|
||||
|
||||
const channels = Object.keys(data.data || {});
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="controls">
|
||||
${channels.map((ch, i) => `
|
||||
<button class="channel-btn ${i === 0 ? 'active' : ''}"
|
||||
onclick="loadChannel('${ch}', this)">
|
||||
${ch.replace('_', ' ').toUpperCase()}
|
||||
</button>
|
||||
`).join('')}
|
||||
<div class="data-type-toggle">
|
||||
<button class="active" onclick="setDataType('calibrated', this)">Calibrated</button>
|
||||
<button onclick="setDataType('raw', this)">Raw</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<canvas id="waveformChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="stats-bar">
|
||||
<div class="stat">
|
||||
<div class="stat-value">${data.metadata?.n_samples?.toLocaleString() || '—'}</div>
|
||||
<div class="stat-label">Samples</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">${data.metadata?.sample_rate_hz || '—'}</div>
|
||||
<div class="stat-label">Sample Rate (Hz)</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">${formatDuration(data.metadata?.duration_sec)}</div>
|
||||
<div class="stat-label">Duration</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="statMin">—</div>
|
||||
<div class="stat-label">Min</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="statMax">—</div>
|
||||
<div class="stat-label">Max</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (channels.length > 0) {
|
||||
currentChannel = channels[0];
|
||||
drawWaveform(data.data[currentChannel], data.metadata);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
panel.innerHTML = `<div class="empty-state"><h3>Error</h3><p>${e.message}</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChannel(channel, btn) {
|
||||
currentChannel = channel;
|
||||
document.querySelectorAll('.channel-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
try {
|
||||
const data = await fetchAPI(`/h5/data?file=${currentFile}`);
|
||||
if (data.data[channel]) {
|
||||
drawWaveform(data.data[channel], data.metadata);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading channel:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function setDataType(type, btn) {
|
||||
currentDataType = type === 'raw' ? 'raw_data' : 'calibrated_data';
|
||||
document.querySelectorAll('.data-type-toggle button').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
// Reload current file with new data type
|
||||
if (currentFile) selectFile(currentFile);
|
||||
}
|
||||
|
||||
function drawWaveform(channelData, metadata) {
|
||||
const ctx = document.getElementById('waveformChart').getContext('2d');
|
||||
|
||||
if (chart) chart.destroy();
|
||||
|
||||
// Downsample for display
|
||||
const maxPoints = 2000;
|
||||
const step = Math.max(1, Math.floor(channelData.length / maxPoints));
|
||||
const displayData = [];
|
||||
const labels = [];
|
||||
|
||||
for (let i = 0; i < channelData.length; i += step) {
|
||||
const slice = channelData.slice(i, Math.min(i + step, channelData.length));
|
||||
displayData.push({
|
||||
x: i / (metadata?.sample_rate_hz || 500),
|
||||
y: slice.reduce((a, b) => a + b, 0) / slice.length
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate stats
|
||||
const min = Math.min(...channelData);
|
||||
const max = Math.max(...channelData);
|
||||
document.getElementById('statMin').textContent = min.toFixed(2);
|
||||
document.getElementById('statMax').textContent = max.toFixed(2);
|
||||
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: currentChannel,
|
||||
data: displayData,
|
||||
borderColor: '#00d9ff',
|
||||
backgroundColor: 'rgba(0, 217, 255, 0.1)',
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
borderWidth: 1.5,
|
||||
tension: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index'
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||
titleColor: '#00d9ff',
|
||||
bodyColor: '#fff',
|
||||
borderColor: '#00d9ff',
|
||||
borderWidth: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time (seconds)',
|
||||
color: '#666'
|
||||
},
|
||||
ticks: { color: '#555' },
|
||||
grid: { color: 'rgba(255,255,255,0.03)' }
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Amplitude',
|
||||
color: '#666'
|
||||
},
|
||||
ticks: { color: '#555' },
|
||||
grid: { color: 'rgba(255,255,255,0.03)' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatDuration(sec) {
|
||||
if (!sec) return '—';
|
||||
if (sec < 60) return `${sec.toFixed(0)}s`;
|
||||
if (sec < 3600) return `${Math.floor(sec/60)}m ${Math.floor(sec%60)}s`;
|
||||
return `${Math.floor(sec/3600)}h ${Math.floor((sec%3600)/60)}m`;
|
||||
}
|
||||
|
||||
// Init
|
||||
loadFiles();
|
||||
setInterval(() => checkHealth(), 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
580
viewer.html
Normal file
580
viewer.html
Normal file
@@ -0,0 +1,580 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Seismic H5 Viewer</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #0a1628 0%, #1a2744 100%);
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #00ff88;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.status-dot.error { background: #ff4757; animation: none; }
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 350px 1fr;
|
||||
gap: 20px;
|
||||
height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 15px;
|
||||
color: #00d9ff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
padding: 14px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid transparent;
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: rgba(0, 217, 255, 0.1);
|
||||
border-color: rgba(0, 217, 255, 0.3);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
background: rgba(0, 217, 255, 0.15);
|
||||
border-color: #00d9ff;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.file-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.viewer-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.channel-btn {
|
||||
padding: 10px 20px;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
background: rgba(0,0,0,0.3);
|
||||
color: #e0e0e0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.channel-btn:hover {
|
||||
border-color: #00d9ff;
|
||||
color: #00d9ff;
|
||||
}
|
||||
|
||||
.channel-btn.active {
|
||||
background: linear-gradient(135deg, #00d9ff, #00a8cc);
|
||||
color: #0a1628;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.data-type-toggle {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.data-type-toggle button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.data-type-toggle button.active {
|
||||
background: #00d9ff;
|
||||
color: #0a1628;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 15px 20px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.7rem;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(0, 217, 255, 0.2);
|
||||
border-top-color: #00d9ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
color: #888;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(0,217,255,0.3); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(0,217,255,0.5); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="url(#grad)" stroke-width="2">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#00d9ff"/>
|
||||
<stop offset="100%" style="stop-color:#00ff88"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M2 12h4l3-9 4 18 3-9h6"/>
|
||||
</svg>
|
||||
Seismic H5 Viewer
|
||||
</h1>
|
||||
<div class="status">
|
||||
<div class="status-item">
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">Connecting...</span>
|
||||
</div>
|
||||
<div class="status-item" id="fileCount">— files</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="panel">
|
||||
<h2>📁 H5 Files</h2>
|
||||
<div class="file-list" id="fileList">
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
Loading files...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel viewer-panel" id="viewerPanel">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M2 12h4l3-9 4 18 3-9h6"/>
|
||||
</svg>
|
||||
<h3>Select a file</h3>
|
||||
<p>Choose an H5 file from the list to view seismic waveforms</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = '/seismic-api/api';
|
||||
|
||||
let currentFile = null;
|
||||
let currentChannel = 'channel_1';
|
||||
let currentDataType = 'calibrated_data';
|
||||
let chart = null;
|
||||
|
||||
async function fetchAPI(endpoint) {
|
||||
const res = await fetch(`${API_BASE}${endpoint}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async function checkHealth() {
|
||||
try {
|
||||
const data = await fetchAPI('/h5/files');
|
||||
document.getElementById('statusDot').className = 'status-dot';
|
||||
document.getElementById('statusText').textContent = 'Connected';
|
||||
document.getElementById('fileCount').textContent = `${data.length} files`;
|
||||
return data;
|
||||
} catch (e) {
|
||||
document.getElementById('statusDot').className = 'status-dot error';
|
||||
document.getElementById('statusText').textContent = 'Disconnected';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFiles() {
|
||||
try {
|
||||
const files = await checkHealth();
|
||||
const list = document.getElementById('fileList');
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state"><p>No H5 files found</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by node
|
||||
const byNode = {};
|
||||
files.forEach(f => {
|
||||
const node = f.nodeId || 'Unknown';
|
||||
if (!byNode[node]) byNode[node] = [];
|
||||
byNode[node].push(f);
|
||||
});
|
||||
|
||||
list.innerHTML = files.map(f => `
|
||||
<div class="file-item" onclick="selectFile('${f.filename}')" data-file="${f.filename}">
|
||||
<div class="file-name">Node ${f.nodeId} - ${f.date}</div>
|
||||
<div class="file-meta">
|
||||
<span>⏱️ ${formatDuration(f.duration_sec)}</span>
|
||||
<span>📊 ${f.n_channels} ch</span>
|
||||
<span>🎵 ${f.sample_rate_hz} Hz</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
document.getElementById('fileList').innerHTML =
|
||||
`<div class="empty-state"><p>Error: ${e.message}</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFile(filename) {
|
||||
currentFile = filename;
|
||||
|
||||
document.querySelectorAll('.file-item').forEach(el => {
|
||||
el.classList.toggle('selected', el.dataset.file === filename);
|
||||
});
|
||||
|
||||
const panel = document.getElementById('viewerPanel');
|
||||
panel.innerHTML = '<div class="loading"><div class="spinner"></div>Loading waveform...</div>';
|
||||
|
||||
try {
|
||||
const data = await fetchAPI(`/h5/data?file=${filename}`);
|
||||
|
||||
const channels = Object.keys(data.data || {});
|
||||
|
||||
panel.innerHTML = `
|
||||
<div class="controls">
|
||||
${channels.map((ch, i) => `
|
||||
<button class="channel-btn ${i === 0 ? 'active' : ''}"
|
||||
onclick="loadChannel('${ch}', this)">
|
||||
${ch.replace('_', ' ').toUpperCase()}
|
||||
</button>
|
||||
`).join('')}
|
||||
<div class="data-type-toggle">
|
||||
<button class="active" onclick="setDataType('calibrated', this)">Calibrated</button>
|
||||
<button onclick="setDataType('raw', this)">Raw</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<canvas id="waveformChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="stats-bar">
|
||||
<div class="stat">
|
||||
<div class="stat-value">${data.metadata?.n_samples?.toLocaleString() || '—'}</div>
|
||||
<div class="stat-label">Samples</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">${data.metadata?.sample_rate_hz || '—'}</div>
|
||||
<div class="stat-label">Sample Rate (Hz)</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">${formatDuration(data.metadata?.duration_sec)}</div>
|
||||
<div class="stat-label">Duration</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="statMin">—</div>
|
||||
<div class="stat-label">Min</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="statMax">—</div>
|
||||
<div class="stat-label">Max</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (channels.length > 0) {
|
||||
currentChannel = channels[0];
|
||||
drawWaveform(data.data[currentChannel], data.metadata);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
panel.innerHTML = `<div class="empty-state"><h3>Error</h3><p>${e.message}</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChannel(channel, btn) {
|
||||
currentChannel = channel;
|
||||
document.querySelectorAll('.channel-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
try {
|
||||
const data = await fetchAPI(`/h5/data?file=${currentFile}`);
|
||||
if (data.data[channel]) {
|
||||
drawWaveform(data.data[channel], data.metadata);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading channel:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function setDataType(type, btn) {
|
||||
currentDataType = type === 'raw' ? 'raw_data' : 'calibrated_data';
|
||||
document.querySelectorAll('.data-type-toggle button').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
// Reload current file with new data type
|
||||
if (currentFile) selectFile(currentFile);
|
||||
}
|
||||
|
||||
function drawWaveform(channelData, metadata) {
|
||||
const ctx = document.getElementById('waveformChart').getContext('2d');
|
||||
|
||||
if (chart) chart.destroy();
|
||||
|
||||
// Downsample for display
|
||||
const maxPoints = 2000;
|
||||
const step = Math.max(1, Math.floor(channelData.length / maxPoints));
|
||||
const displayData = [];
|
||||
const labels = [];
|
||||
|
||||
for (let i = 0; i < channelData.length; i += step) {
|
||||
const slice = channelData.slice(i, Math.min(i + step, channelData.length));
|
||||
displayData.push({
|
||||
x: i / (metadata?.sample_rate_hz || 500),
|
||||
y: slice.reduce((a, b) => a + b, 0) / slice.length
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate stats
|
||||
const min = Math.min(...channelData);
|
||||
const max = Math.max(...channelData);
|
||||
document.getElementById('statMin').textContent = min.toFixed(2);
|
||||
document.getElementById('statMax').textContent = max.toFixed(2);
|
||||
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: currentChannel,
|
||||
data: displayData,
|
||||
borderColor: '#00d9ff',
|
||||
backgroundColor: 'rgba(0, 217, 255, 0.1)',
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
borderWidth: 1.5,
|
||||
tension: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index'
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||
titleColor: '#00d9ff',
|
||||
bodyColor: '#fff',
|
||||
borderColor: '#00d9ff',
|
||||
borderWidth: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time (seconds)',
|
||||
color: '#666'
|
||||
},
|
||||
ticks: { color: '#555' },
|
||||
grid: { color: 'rgba(255,255,255,0.03)' }
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Amplitude',
|
||||
color: '#666'
|
||||
},
|
||||
ticks: { color: '#555' },
|
||||
grid: { color: 'rgba(255,255,255,0.03)' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatDuration(sec) {
|
||||
if (!sec) return '—';
|
||||
if (sec < 60) return `${sec.toFixed(0)}s`;
|
||||
if (sec < 3600) return `${Math.floor(sec/60)}m ${Math.floor(sec%60)}s`;
|
||||
return `${Math.floor(sec/3600)}h ${Math.floor((sec%3600)/60)}m`;
|
||||
}
|
||||
|
||||
// Init
|
||||
loadFiles();
|
||||
setInterval(() => checkHealth(), 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user