1652 lines
76 KiB
Plaintext
1652 lines
76 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<link rel="icon" href="/favicon.ico">
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27%3E%3Ctext y=%27.9em%27 font-size=%2790%27%3E🌊%3C/text%3E%3C/svg%3E">
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27><text y=%27.9em%27 font-size=%2790%27>🌊</text></svg>">
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27><text y=%27.9em%27 font-size=%2790%27>🌊</text></svg>">
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27><text y=%27.9em%27 font-size=%2790%27>🌊</text></svg>">
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>🌊 SeiSee Web — Seismic Viewer</title>
|
||
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<style>
|
||
:root{--bg:#1a1a2e;--panel:#16213e;--header:#0f3460;--accent:#00d4ff;--accent2:#e94560;--text:#eee;--muted:#8899aa;--border:#2a2a4a;--green:#4ade80;--yellow:#fde047;--hover:#1a3a6a}
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);height:100vh;overflow:hidden;display:flex;flex-direction:column}
|
||
.hdr{background:var(--header);padding:6px 16px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--border);flex-shrink:0}
|
||
.hdr h1{font-size:1rem;color:var(--accent);white-space:nowrap}
|
||
.hdr .sep{width:1px;height:24px;background:var(--border)}
|
||
.toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;flex:1}
|
||
.toolbar label{font-size:.7rem;color:var(--muted);text-transform:uppercase}
|
||
.toolbar select,.toolbar input[type=number]{background:var(--bg);border:1px solid var(--border);color:var(--text);padding:3px 6px;border-radius:3px;font-size:.8rem}
|
||
.toolbar input[type=number]{width:65px}
|
||
.toolbar select{min-width:80px}
|
||
.tbtn{background:var(--accent);color:#000;border:none;padding:4px 10px;border-radius:3px;cursor:pointer;font-weight:600;font-size:.75rem}
|
||
.tbtn:hover{opacity:.85}
|
||
.tbtn.active{background:var(--accent2);color:#fff}
|
||
.tgroup{display:flex;align-items:center;gap:4px;padding:0 6px;border-left:1px solid var(--border)}
|
||
.main{display:flex;flex:1;overflow:hidden}
|
||
.sidebar{width:260px;min-width:200px;max-width:400px;background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
|
||
.sidebar .stitle{padding:8px 12px;font-size:.75rem;color:var(--muted);text-transform:uppercase;border-bottom:1px solid var(--border);display:flex;justify-content:space-between}
|
||
.sidebar .search{width:100%;padding:6px 10px;background:var(--bg);border:none;border-bottom:1px solid var(--border);color:var(--text);font-size:.8rem}
|
||
.sidebar .search:focus{outline:none;background:#0d1117}
|
||
.flist{flex:1;overflow-y:auto}
|
||
.fitem{padding:5px 12px;cursor:pointer;border-bottom:1px solid var(--border);font-size:.8rem}
|
||
.fitem:hover{background:var(--hover)}
|
||
.fitem.active{background:var(--hover);border-left:3px solid var(--accent)}
|
||
.fitem .fhead{display:flex;justify-content:space-between;align-items:center;margin-bottom:2px}
|
||
.fitem .fn{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
|
||
.fitem .ft{font-size:.65rem;padding:1px 5px;border-radius:3px;font-weight:700;flex-shrink:0;margin-left:6px}
|
||
.fitem .finfo{font-size:.65rem;color:var(--muted)}
|
||
.ft-h5{background:#1e3a8a;color:#60a5fa}
|
||
.ft-segy{background:#166534;color:var(--green)}
|
||
.content{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
||
.tabs{display:flex;background:var(--panel);border-bottom:1px solid var(--border);flex-shrink:0}
|
||
.tab{padding:6px 16px;cursor:pointer;font-size:.8rem;color:var(--muted);border-bottom:2px solid transparent}
|
||
.tab:hover{color:var(--text)}
|
||
.tab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||
.view{flex:1;overflow:hidden;display:none;position:relative}
|
||
.view.active{display:flex;flex-direction:column}
|
||
#sectionView{position:relative}
|
||
#heatmapContainer{flex:1;min-height:0}
|
||
#wiggleCanvas{position:absolute;top:0;left:0;pointer-events:none}
|
||
.htable-wrap{flex:1;overflow:auto;padding:8px}
|
||
.htable{width:100%;border-collapse:collapse;font-size:.75rem}
|
||
.htable th{background:var(--header);padding:4px 8px;text-align:left;position:sticky;top:0;z-index:1;color:var(--accent)}
|
||
.htable td{padding:3px 8px;border-bottom:1px solid var(--border)}
|
||
.htable tr:hover td{background:var(--hover)}
|
||
.infopanel{background:var(--panel);border-top:1px solid var(--border);padding:4px 12px;font-size:.7rem;color:var(--muted);display:flex;gap:16px;flex-shrink:0}
|
||
.infopanel span{white-space:nowrap}
|
||
.empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted);font-size:1rem}
|
||
.resize-handle{width:4px;cursor:col-resize;background:transparent;flex-shrink:0}
|
||
.resize-handle:hover{background:var(--accent)}
|
||
::-webkit-scrollbar{width:6px;height:6px}
|
||
::-webkit-scrollbar-track{background:var(--bg)}
|
||
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
||
::-webkit-scrollbar-thumb:hover{background:var(--muted)}
|
||
.ch-section{border-top:1px solid var(--border);padding:8px 12px;flex-shrink:0;max-height:40vh;overflow-y:auto}
|
||
.ch-section .stitle{padding:0 0 6px 0;border:none}
|
||
.ch-btn{display:block;width:100%;text-align:left;padding:3px 8px;background:none;border:1px solid var(--border);color:var(--text);cursor:pointer;font-size:.75rem;border-radius:2px;margin-bottom:2px}
|
||
.ch-btn:hover{border-color:var(--accent)}
|
||
.ch-btn.active{background:var(--hover);border-color:var(--accent);color:var(--accent)}
|
||
.meta-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:12px;padding:16px;overflow:auto}
|
||
.meta-card{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:12px}
|
||
.meta-card h3{color:var(--accent);font-size:.9rem;margin-bottom:8px;border-bottom:1px solid var(--border);padding-bottom:4px}
|
||
.meta-row{display:flex;justify-content:space-between;padding:4px 0;font-size:.75rem;border-bottom:1px solid rgba(255,255,255,0.05)}
|
||
.meta-row .label{color:var(--muted);font-weight:600}
|
||
.meta-row .value{color:var(--text);text-align:right;max-width:60%;overflow:hidden;text-overflow:ellipsis}
|
||
#mapContainer{flex:1;position:relative}
|
||
#map{width:100%;height:100%}
|
||
.map-controls{position:absolute;top:10px;right:10px;z-index:1000;background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px}
|
||
.map-controls button{background:var(--accent);color:#000;border:none;padding:4px 8px;border-radius:3px;cursor:pointer;font-size:.75rem;margin:2px}
|
||
.map-controls button:hover{opacity:.85}
|
||
.extract-panel{padding:16px;overflow:auto}
|
||
.extract-form{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:16px;max-width:600px}
|
||
.extract-form .form-row{margin-bottom:12px}
|
||
.extract-form label{display:block;font-size:.75rem;color:var(--muted);margin-bottom:4px;text-transform:uppercase}
|
||
.extract-form input,.extract-form select{width:100%;padding:6px 10px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:3px;font-size:.8rem}
|
||
.extract-form button{background:var(--green);color:#000;border:none;padding:8px 16px;border-radius:3px;cursor:pointer;font-weight:600;font-size:.85rem;width:100%}
|
||
.extract-form button:hover{opacity:.85}
|
||
.extract-info{margin-top:12px;padding:8px;background:rgba(0,212,255,0.1);border-left:3px solid var(--accent);font-size:.75rem;color:var(--text)}
|
||
.pipeline-panel{padding:20px;overflow:auto;display:flex;flex-direction:column;gap:20px}
|
||
.pipeline-diagram{display:flex;align-items:stretch;justify-content:space-between;background:var(--panel);padding:20px;border-radius:8px;border:1px solid var(--border);flex-wrap:wrap;gap:16px}
|
||
.p-step{flex:1;min-width:240px;background:var(--bg);padding:16px;border-radius:6px;border:1px solid var(--border);display:flex;flex-direction:column;position:relative}
|
||
.p-step h4{color:var(--accent);font-size:1.1rem;margin-bottom:6px}
|
||
.p-step p{font-size:.8rem;color:var(--muted);margin-bottom:8px;line-height:1.4}
|
||
.p-step .count{font-size:1.5rem;font-weight:bold;color:var(--text);margin:8px 0}
|
||
.p-step .tech{font-size:.7rem;background:var(--hover);padding:4px 8px;border-radius:3px;display:inline-block;margin-top:auto;margin-bottom:8px}
|
||
.p-step .formulas{margin-top:8px;padding-top:8px;border-top:1px solid var(--border);font-size:.7rem;line-height:1.6}
|
||
.p-step .formulas div{margin:3px 0;color:var(--muted)}
|
||
.p-step .formulas code{background:rgba(0,212,255,0.1);padding:2px 5px;border-radius:2px;color:var(--accent);font-family:monospace;font-size:.68rem}
|
||
.p-arrow{color:var(--muted);font-size:2rem;display:flex;align-items:center}
|
||
.p-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px}
|
||
.stat-card{background:var(--panel);padding:16px;border-radius:8px;border:1px solid var(--border)}
|
||
.stat-card h5{color:var(--muted);font-size:.75rem;text-transform:uppercase;margin-bottom:8px}
|
||
.stat-card .val{font-size:1.5rem;font-weight:bold;color:var(--accent)}
|
||
.progress-bar{height:6px;background:var(--bg);border-radius:3px;margin-top:8px;overflow:hidden}
|
||
.progress-fill{height:100%;background:var(--green);width:0%}
|
||
.geosup-panel{padding:20px;overflow:auto;display:flex;flex-direction:column;gap:20px}
|
||
.geosup-section{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:16px}
|
||
.geosup-section h3{color:var(--accent);font-size:1rem;margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
||
.doc-list{display:flex;flex-direction:column;gap:8px}
|
||
.doc-item{display:flex;align-items:center;gap:12px;padding:10px;background:var(--bg);border-radius:4px;cursor:pointer;transition:background 0.2s}
|
||
.doc-item:hover{background:var(--hover)}
|
||
.doc-item .icon{font-size:1.5rem}
|
||
.doc-item .info{flex:1}
|
||
.doc-item .name{color:var(--text);font-weight:500;font-size:.85rem}
|
||
.doc-item .meta{color:var(--muted);font-size:.7rem;margin-top:2px}
|
||
.doc-item .badge{background:var(--accent);color:#000;padding:2px 6px;border-radius:3px;font-size:.65rem;font-weight:700}
|
||
.doc-item .badge.gundalf{background:var(--accent2);color:#fff}
|
||
.geosup-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
|
||
.geosup-stat{background:var(--bg);padding:12px;border-radius:4px;text-align:center}
|
||
.geosup-stat .val{font-size:1.5rem;font-weight:bold;color:var(--accent)}
|
||
.geosup-stat .label{font-size:.7rem;color:var(--muted);margin-top:4px}
|
||
.map-layers{position:absolute;top:10px;left:10px;z-index:1000;background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px;max-width:200px}
|
||
.map-layers h5{font-size:.7rem;color:var(--muted);margin-bottom:6px;text-transform:uppercase}
|
||
.layer-toggle{display:flex;align-items:center;gap:6px;padding:3px 0;font-size:.75rem;cursor:pointer}
|
||
.layer-toggle input{cursor:pointer}
|
||
.layer-toggle .dot{width:10px;height:10px;border-radius:50%;display:inline-block}
|
||
.dot-source{background:#3b82f6}
|
||
.dot-receiver{background:#ef4444}
|
||
.dot-deployment{background:#22c55e}
|
||
.dot-file{background:#fbbf24}
|
||
.load-info{background:var(--panel);border:1px solid var(--accent);border-radius:4px;padding:8px 12px;margin-bottom:8px;font-size:.75rem;line-height:1.6}
|
||
.load-info .label{color:var(--muted);font-weight:600;margin-right:4px}
|
||
.load-info .value{color:var(--accent)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="hdr">
|
||
<h1>🌊 SeiSee Web</h1>
|
||
<div class="sep"></div>
|
||
<div class="toolbar">
|
||
<div class="tgroup" style="border:none">
|
||
<label>View:</label>
|
||
<button class="tbtn active" id="btnDensity" onclick="setView('density')">Density</button>
|
||
<button class="tbtn" id="btnWiggle" onclick="setView('wiggle')">Wiggle</button>
|
||
<button class="tbtn" id="btnBoth" onclick="setView('both')">Both</button>
|
||
</div>
|
||
<div class="tgroup">
|
||
<label>Colormap:</label>
|
||
<select id="selCmap" onchange="renderSection()">
|
||
<option value="RdBu_r">Seismic (RdBu)</option>
|
||
<option value="Greys">Grayscale</option>
|
||
<option value="Viridis">Viridis</option>
|
||
<option value="Plasma">Plasma</option>
|
||
<option value="Hot">Hot</option>
|
||
<option value="Picnic">Picnic</option>
|
||
</select>
|
||
</div>
|
||
<div class="tgroup">
|
||
<label>Gain:</label>
|
||
<input type="number" id="inGain" value="1" min="0.1" max="100" step="0.5" onchange="renderSection()">
|
||
</div>
|
||
<div class="tgroup">
|
||
<label>Clip %:</label>
|
||
<input type="number" id="inClip" value="99" min="50" max="100" step="1" onchange="renderSection()">
|
||
</div>
|
||
<div class="tgroup">
|
||
<label>Traces:</label>
|
||
<input type="number" id="inTraceStart" value="0" min="0" step="10">
|
||
<span style="color:var(--muted)">–</span>
|
||
<input type="number" id="inTraceCount" value="200" min="10" max="1000" step="10">
|
||
</div>
|
||
<div class="tgroup">
|
||
<label>Samp/Tr:</label>
|
||
<input type="number" id="inSPT" value="1000" min="100" max="50000" step="100">
|
||
</div>
|
||
<div class="tgroup">
|
||
<label>Norm:</label>
|
||
<select id="selNorm" onchange="renderSection()">
|
||
<option value="global">Global</option>
|
||
<option value="trace">Per Trace</option>
|
||
</select>
|
||
</div>
|
||
<div class="tgroup">
|
||
<button class="tbtn" onclick="loadSection()" style="background:var(--green);color:#000">▶ Load</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="main">
|
||
<div class="sidebar" id="sidebar">
|
||
<div class="stitle"><span>Files</span><span id="fileCount">0</span></div>
|
||
<input type="text" class="search" id="searchInput" placeholder="Search..." oninput="filterFiles()">
|
||
<div class="flist" id="fileList"></div>
|
||
<div class="ch-section" id="channelSection" style="display:none">
|
||
<div class="stitle">Channel</div>
|
||
<div id="channelList"></div>
|
||
</div>
|
||
</div>
|
||
<div class="resize-handle" id="resizeHandle"></div>
|
||
|
||
<div class="content">
|
||
<div class="tabs">
|
||
<div class="tab active" onclick="switchTab('section')">Section</div>
|
||
<div class="tab" onclick="switchTab('metadata')">Metadata</div>
|
||
<div class="tab" onclick="switchTab('map')">Map</div>
|
||
<div class="tab" onclick="switchTab('extract')">Extract</div>
|
||
<div class="tab" onclick="switchTab('pipeline')">Pipeline</div>
|
||
<div class="tab" onclick="switchTab('geosup')">Geosup</div>
|
||
<div class="tab" onclick="switchTab('headers')">Headers</div>
|
||
<div class="tab" onclick="switchTab('waveform')">Waveform</div>
|
||
<div class="tab" onclick="switchTab('gather')">Gather</div>
|
||
<div class="tab" onclick="switchTab('coverage')">Coverage</div>
|
||
</div>
|
||
|
||
<div class="view active" id="sectionView">
|
||
<div id="loadInfoPanel" style="display:none" class="load-info">
|
||
<span class="label">Loaded:</span>
|
||
<span class="value" id="loadedTracesInfo">—</span>
|
||
</div>
|
||
<div id="heatmapContainer"></div>
|
||
<canvas id="wiggleCanvas"></canvas>
|
||
</div>
|
||
|
||
<div class="view" id="metadataView">
|
||
<div class="meta-grid" id="metadataContent">
|
||
<div class="empty">Select a file to view metadata</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="mapView">
|
||
<div id="mapContainer">
|
||
<div id="map"></div>
|
||
<div class="map-layers" id="mapLayers">
|
||
<h5>Geosup Layers</h5>
|
||
<label class="layer-toggle"><input type="checkbox" id="layerSources" onchange="toggleLayer('sources')"><span class="dot dot-source"></span> Sources</label>
|
||
<label class="layer-toggle"><input type="checkbox" id="layerReceivers" onchange="toggleLayer('receivers')"><span class="dot dot-receiver"></span> Receivers</label>
|
||
<label class="layer-toggle"><input type="checkbox" id="layerDeployments" onchange="toggleLayer('deployments')"><span class="dot dot-deployment"></span> Deployments</label>
|
||
<label class="layer-toggle"><input type="checkbox" id="layerFiles" checked onchange="toggleLayer('files')"><span class="dot dot-file"></span> Files</label>
|
||
</div>
|
||
<div class="map-controls">
|
||
<div style="margin-bottom:4px;font-size:.7rem;color:var(--muted)">Edit Location</div>
|
||
<button onclick="enableLocationEdit()">📍 Set Location</button>
|
||
<button onclick="loadGeosupLayers()" style="margin-top:4px">🔄 Load Geosup</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="extractView">
|
||
<div class="extract-panel">
|
||
<div class="extract-form">
|
||
<h3 style="color:var(--accent);margin-bottom:12px">Extract Segment</h3>
|
||
<div class="form-row">
|
||
<label>Channel</label>
|
||
<select id="extractChannel"></select>
|
||
</div>
|
||
<div class="form-row">
|
||
<label>Start Time (seconds)</label>
|
||
<input type="number" id="extractStart" value="0" min="0" step="60">
|
||
</div>
|
||
<div class="form-row">
|
||
<label>Duration (seconds)</label>
|
||
<input type="number" id="extractDuration" value="1200" min="1" step="60">
|
||
<div style="font-size:.65rem;color:var(--muted);margin-top:2px">Default: 1200s (20 minutes)</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<label>Format</label>
|
||
<select id="extractFormat">
|
||
<option value="csv">CSV</option>
|
||
<option value="h5">HDF5 (.h5)</option>
|
||
</select>
|
||
</div>
|
||
<button onclick="downloadExtract()">⬇️ Download Extract</button>
|
||
<div class="extract-info" id="extractInfo" style="display:none"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="pipelineView">
|
||
<div class="pipeline-panel">
|
||
<h2 style="color:var(--accent);font-size:1.2rem;border-bottom:1px solid var(--border);padding-bottom:10px">Processing Chain — RAW → H5</h2>
|
||
|
||
<div class="pipeline-diagram">
|
||
<div class="p-step">
|
||
<h4>1. RAW</h4>
|
||
<p>Données brutes Manta (format propriétaire)</p>
|
||
<div class="count" id="countRaw">--</div>
|
||
<div class="tech">Format Propriétaire</div>
|
||
<div class="formulas">
|
||
<div><strong>Source:</strong> Hydrophones + Géophones Manta</div>
|
||
<div><strong>Résolution ADC:</strong> 24-bit, ±2.5V</div>
|
||
</div>
|
||
</div>
|
||
<div class="p-arrow">➔</div>
|
||
<div class="p-step">
|
||
<h4>2. SEGY</h4>
|
||
<p>Conversion intermédiaire via mantasegy (Box64/ARM)</p>
|
||
<div class="count" id="countSegy">--</div>
|
||
<div class="tech">mantasegy (Box64/ARM)</div>
|
||
<div class="formulas">
|
||
<div><strong>Format:</strong> SEG-Y Rev.1</div>
|
||
<div><strong>Conversion:</strong> Binaire Manta → SEG-Y</div>
|
||
</div>
|
||
</div>
|
||
<div class="p-arrow">➔</div>
|
||
<div class="p-step">
|
||
<h4>3. Normalisation</h4>
|
||
<p>Conversion ADC → valeurs physiques calibrées</p>
|
||
<div class="tech">Python + Calibration</div>
|
||
<div class="formulas">
|
||
<div><strong>Géophones (SM-24):</strong></div>
|
||
<div><code>v(m/s) = (ADC / gain) × 28.8 V/(m/s)</code></div>
|
||
<div style="margin-top:6px"><strong>Hydrophones:</strong></div>
|
||
<div><code>P(µPa) = (ADC / gain) × sens(µPa/V)</code></div>
|
||
<div style="margin-top:6px"><strong>ADC → Voltage:</strong></div>
|
||
<div><code>V = ADC × (5.0 / 2²⁴)</code></div>
|
||
<div style="margin-top:6px"><strong>Normalisation trace:</strong></div>
|
||
<div><code>(trace - mean) / std</code></div>
|
||
</div>
|
||
</div>
|
||
<div class="p-arrow">➔</div>
|
||
<div class="p-step">
|
||
<h4>4. HDF5</h4>
|
||
<p>Export HDF5 avec Python + calibration</p>
|
||
<div class="count" id="countH5">--</div>
|
||
<div class="tech">Python + h5py</div>
|
||
<div class="formulas">
|
||
<div><strong>Format:</strong> HDF5 (.h5)</div>
|
||
<div><strong>Groupes:</strong> calibrated_data, raw_data, metadata</div>
|
||
<div><strong>Compression:</strong> gzip</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="p-stats">
|
||
<div class="stat-card">
|
||
<h5>Espace Disque (USB)</h5>
|
||
<div class="val" id="diskUsage">-- %</div>
|
||
<div class="progress-bar"><div class="progress-fill" id="diskBar" style="width:0%"></div></div>
|
||
<div style="font-size:.75rem;color:var(--muted);margin-top:4px" id="diskDetail">-- GB utilisés sur -- GB</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<h5>Status Conversion</h5>
|
||
<div class="val" id="convertStatus" style="font-size:1rem">Inactif</div>
|
||
<div style="font-size:.75rem;color:var(--muted);margin-top:4px" id="convertDetail">Aucune tâche en cours</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<h5>Complétion H5</h5>
|
||
<div class="val" id="completionPct">-- %</div>
|
||
<div class="progress-bar"><div class="progress-fill" id="completionBar" style="width:0%"></div></div>
|
||
<div style="font-size:.75rem;color:var(--muted);margin-top:4px" id="completionDetail">-- / 345 fichiers traités</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button onclick="loadPipelineStatus()" style="width:140px;padding:8px;background:var(--panel);border:1px solid var(--border);color:var(--text);border-radius:4px;cursor:pointer">↻ Actualiser</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="geosupView">
|
||
<div class="geosup-panel">
|
||
<h2 style="color:var(--accent);font-size:1.2rem;border-bottom:1px solid var(--border);padding-bottom:10px">📍 Geosup — Positions & Documentation</h2>
|
||
|
||
<div class="geosup-stats" id="geosupStats">
|
||
<div class="geosup-stat"><div class="val" id="statSources">--</div><div class="label">Sources</div></div>
|
||
<div class="geosup-stat"><div class="val" id="statReceivers">--</div><div class="label">Receivers</div></div>
|
||
<div class="geosup-stat"><div class="val" id="statDeployments">--</div><div class="label">Deployments</div></div>
|
||
<div class="geosup-stat"><div class="val" id="statDocs">--</div><div class="label">Documents</div></div>
|
||
</div>
|
||
|
||
<div class="geosup-section">
|
||
<h3>📚 Documentation</h3>
|
||
<div class="doc-list" id="docList">
|
||
<div class="empty">Chargement...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="geosup-section">
|
||
<h3>🗺️ Position Coverage</h3>
|
||
<p style="font-size:.8rem;color:var(--muted);margin-bottom:8px">Use the <strong>Map</strong> tab to visualize source, receiver, and deployment positions. Toggle layers using the controls.</p>
|
||
<button onclick="switchTab('map');setTimeout(loadGeosupLayers,500)" class="tbtn" style="background:var(--green)">Open Map with Positions →</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="headersView">
|
||
<div class="htable-wrap" id="headersContent">
|
||
<div class="empty">Select a file to view headers</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="waveformView">
|
||
<div id="waveformChart" style="flex:1"></div>
|
||
<div class="view" id="coverageView">
|
||
<div class="coverage-panel">
|
||
<div class="coverage-stats" id="coverageStats">
|
||
<div class="coverage-stat"><div class="val" id="covNodes">--</div><div class="label">Nodes</div></div>
|
||
<div class="coverage-stat"><div class="val" id="covFiles">--</div><div class="label">Files</div></div>
|
||
<div class="coverage-stat"><div class="val" id="covHours">--</div><div class="label">Total Hours</div></div>
|
||
<div class="coverage-stat"><div class="val" id="covLines">--</div><div class="label">Lines</div></div>
|
||
<div class="coverage-legend" id="coverageLegend"></div>
|
||
<button class="tbtn" onclick="loadCoverage()" style="background:var(--green);color:#000">Refresh</button>
|
||
</div>
|
||
<div class="coverage-chart-wrap" id="coverageChartWrap">
|
||
<div class="empty" id="coverageEmpty">Loading coverage data...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="view" id="gatherView">
|
||
<div class="gather-panel">
|
||
<div class="gather-line-select" id="gatherLineSelect">
|
||
<label>Select Line:</label>
|
||
<select id="gatherLineDropdown" onchange="selectGatherLine(this.value)">
|
||
<option value="">-- Choose --</option>
|
||
</select>
|
||
<button onclick="selectAllGatherFiles()">Select All</button>
|
||
<button onclick="clearGatherFiles()" style="background:var(--accent2);color:#fff">Clear</button>
|
||
<span id="gatherSelCount" style="font-size:.75rem;color:var(--accent);margin-left:8px">0 selected</span>
|
||
</div>
|
||
<div class="gather-controls">
|
||
<label>Sort:</label>
|
||
<select id="gatherSort" onchange="renderGather()">
|
||
<option value="board_id">Board ID</option>
|
||
<option value="line_point">Line + Point</option>
|
||
<option value="distance">Distance</option>
|
||
</select>
|
||
<label>Norm:</label>
|
||
<select id="gatherNorm" onchange="renderGather()">
|
||
<option value="global">Global</option>
|
||
<option value="trace" selected>Per-trace</option>
|
||
</select>
|
||
<label>Gain:</label>
|
||
<input type="number" id="gatherGain" value="5" min="0.1" max="100" step="0.5" onchange="renderGather()">
|
||
<label>Spacing:</label>
|
||
<input type="range" id="gatherSpacing" min="20" max="200" value="60" oninput="renderGather()">
|
||
<label>Mode:</label>
|
||
<select id="gatherMode" onchange="renderGather()">
|
||
<option value="wiggle">Wiggle</option>
|
||
<option value="density">Density</option>
|
||
</select>
|
||
<label>Channel:</label>
|
||
<select id="gatherChannel">
|
||
<option value="calibrated_data/channel_1">channel_1</option>
|
||
<option value="calibrated_data/channel_2">channel_2</option>
|
||
<option value="calibrated_data/channel_3">channel_3</option>
|
||
<option value="calibrated_data/channel_4">channel_4</option>
|
||
</select>
|
||
<label>START:</label>
|
||
<input type="number" id="gatherStart" value="0" min="0" step="10" style="width:70px" onchange="onGatherParamChange()">
|
||
<label>DURATION:</label>
|
||
<input type="number" id="gatherDuration" value="60" min="1" step="10" style="width:70px" onchange="onGatherParamChange()">
|
||
<div style="display:flex;align-items:center;gap:6px;flex:1;min-width:300px">
|
||
<span style="font-size:.7rem;color:var(--accent);font-weight:600;min-width:55px" id="gatherSliderLabel">0:00</span>
|
||
<input type="range" id="gatherTimeSlider" min="0" max="8000" value="0" step="1"
|
||
style="flex:1;height:24px;accent-color:var(--accent);cursor:pointer"
|
||
oninput="onGatherSliderInput(this.value)">
|
||
<span style="font-size:.7rem;color:var(--muted);min-width:55px;text-align:right" id="gatherSliderMax">2:13:20</span>
|
||
</div>
|
||
<button class="tbtn" onclick="loadGather()" style="background:var(--green);color:#000">Load Gather</button>
|
||
<button class="tbtn" onclick="extractGather()" style="background:#f59e0b;color:#000;margin-left:4px" title="Export full resolution data (all channels, no decimation)">📥 Extract</button>
|
||
</div>
|
||
<div class="gather-canvas-wrap" id="gatherCanvasWrap" style="flex:1;overflow:auto;position:relative;min-height:0;border:1px solid var(--border);border-radius:4px;margin:4px 8px">
|
||
<canvas id="gatherCanvas" style="display:block;width:100%"></canvas>
|
||
<div class="empty" id="gatherEmpty">Select files using checkboxes in the file list, then click "Load Gather"</div>
|
||
</div>
|
||
<div class="gather-stats" id="gatherStats" style="padding:4px 12px;font-size:.7rem;color:var(--muted)"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="infopanel" id="infopanel">
|
||
<span>Ready</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const API = '/seismic/api';
|
||
let files = [], selectedFile = null, selectedChannel = 'calibrated_data/channel_1';
|
||
let sectionData = null;
|
||
let currentView = 'density';
|
||
let currentTab = 'section';
|
||
let map = null;
|
||
let locations = {};
|
||
let currentMetadata = null;
|
||
let fileInfoCache = {};
|
||
|
||
async function init() {
|
||
const r = await fetch(API + '/files');
|
||
const d = await r.json();
|
||
files = d.files || [];
|
||
document.getElementById('fileCount').textContent = files.length;
|
||
renderFiles(files);
|
||
setupResize();
|
||
|
||
try {
|
||
const lr = await fetch(API + '/locations');
|
||
locations = await lr.json();
|
||
} catch (e) {
|
||
console.error('Failed to load locations:', e);
|
||
}
|
||
}
|
||
|
||
function renderFiles(list) {
|
||
document.getElementById('fileList').innerHTML = list.map(f => {
|
||
const info = fileInfoCache[f.name] || {};
|
||
let infoText = '';
|
||
if (info.duration_human && info.num_channels && info.sample_rate_hz) {
|
||
infoText = `<div class="finfo">${info.duration_human} · ${info.num_channels} ch · ${info.sample_rate_hz} Hz</div>`;
|
||
}
|
||
return `<div class="fitem${selectedFile===f.name?' active':''}" onclick="selectFile('${f.name}')">
|
||
<div class="fhead">
|
||
<span class="fn" title="${f.name}">${f.name}</span>
|
||
<span class="ft ft-${f.type}">${f.type.toUpperCase()}</span>
|
||
</div>
|
||
${infoText}
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function filterFiles() {
|
||
const q = document.getElementById('searchInput').value.toLowerCase();
|
||
renderFiles(files.filter(f => f.name.toLowerCase().includes(q)));
|
||
}
|
||
|
||
async function selectFile(name) {
|
||
selectedFile = name;
|
||
filterFiles();
|
||
setInfo(`Loading ${name}...`);
|
||
|
||
const r = await fetch(`${API}/file/${encodeURIComponent(name)}/info`);
|
||
const info = await r.json();
|
||
fileInfoCache[name] = info;
|
||
|
||
const chSec = document.getElementById('channelSection');
|
||
if (info.type === 'h5' && info.datasets) {
|
||
chSec.style.display = 'block';
|
||
const cals = info.datasets.filter(d => d.path.includes('calibrated'));
|
||
const raws = info.datasets.filter(d => d.path.includes('raw'));
|
||
let html = '<div style="font-size:.65rem;color:var(--muted);margin-bottom:4px">Calibrated:</div>';
|
||
cals.forEach(d => {
|
||
html += `<button class="ch-btn${d.path===selectedChannel?' active':''}" onclick="selectChannel('${d.path}')">${d.path.split('/').pop()} <span style="color:var(--muted)">(${d.dtype})</span></button>`;
|
||
});
|
||
if (raws.length) {
|
||
html += '<div style="font-size:.65rem;color:var(--muted);margin:4px 0">Raw:</div>';
|
||
raws.forEach(d => {
|
||
html += `<button class="ch-btn${d.path===selectedChannel?' active':''}" onclick="selectChannel('${d.path}')">${d.path.split('/').pop()} <span style="color:var(--muted)">(${d.dtype})</span></button>`;
|
||
});
|
||
}
|
||
document.getElementById('channelList').innerHTML = html;
|
||
|
||
const extractCh = document.getElementById('extractChannel');
|
||
extractCh.innerHTML = '';
|
||
[...cals, ...raws].forEach(d => {
|
||
const opt = document.createElement('option');
|
||
opt.value = d.path;
|
||
opt.textContent = d.path;
|
||
extractCh.appendChild(opt);
|
||
});
|
||
|
||
if (info.duration_human) {
|
||
setInfo(`${name} — ${info.duration_human}, ${info.num_channels} channels @ ${info.sample_rate_hz} Hz`);
|
||
} else if (info.samples_per_channel) {
|
||
const spt = parseInt(document.getElementById('inSPT').value);
|
||
const totalTr = Math.floor(info.samples_per_channel / spt);
|
||
setInfo(`${name} — ${info.samples_per_channel.toLocaleString()} samples, ${totalTr} virtual traces @ ${spt} spt`);
|
||
}
|
||
} else {
|
||
chSec.style.display = 'none';
|
||
if (info.num_traces) {
|
||
setInfo(`${name} — ${info.num_traces} traces, ${info.samples_per_trace} samples/trace`);
|
||
document.getElementById('inSPT').value = info.samples_per_trace || 1000;
|
||
}
|
||
}
|
||
|
||
renderFiles(files.filter(f => f.name.toLowerCase().includes(document.getElementById('searchInput').value.toLowerCase())));
|
||
loadMetadata();
|
||
if (currentTab === 'section') loadSection();
|
||
if (currentTab === 'headers') loadHeaders();
|
||
if (currentTab === 'map') initMap();
|
||
}
|
||
|
||
function selectChannel(ch) {
|
||
selectedChannel = ch;
|
||
document.querySelectorAll('.ch-btn').forEach(el => {
|
||
el.classList.toggle('active', el.onclick.toString().includes(ch));
|
||
});
|
||
if (currentTab === 'section') loadSection();
|
||
}
|
||
|
||
async function loadMetadata() {
|
||
if (!selectedFile) return;
|
||
const mc = document.getElementById('metadataContent');
|
||
mc.innerHTML = '<div class="empty">Loading metadata...</div>';
|
||
|
||
try {
|
||
const r = await fetch(`${API}/file/${encodeURIComponent(selectedFile)}/metadata`);
|
||
const data = await r.json();
|
||
currentMetadata = data;
|
||
|
||
if (data.error) {
|
||
mc.innerHTML = `<div class="empty" style="color:var(--accent2)">${data.error}</div>`;
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
html += '<div class="meta-card"><h3>📄 File Information</h3>';
|
||
html += `<div class="meta-row"><span class="label">Filename</span><span class="value">${data.filename || '—'}</span></div>`;
|
||
html += `<div class="meta-row"><span class="label">Size</span><span class="value">${data.file_size_mb || '—'} MB</span></div>`;
|
||
html += `<div class="meta-row"><span class="label">Modified</span><span class="value">${data.modified ? new Date(data.modified).toLocaleString('fr') : '—'}</span></div>`;
|
||
html += '</div>';
|
||
|
||
html += '<div class="meta-card"><h3>⏱️ Recording Information</h3>';
|
||
html += `<div class="meta-row"><span class="label">Start Time</span><span class="value">${data.start_timestamp ? new Date(data.start_timestamp).toLocaleString('fr') : '—'}</span></div>`;
|
||
html += `<div class="meta-row"><span class="label">End Time</span><span class="value">${data.end_timestamp ? new Date(data.end_timestamp).toLocaleString('fr') : '—'}</span></div>`;
|
||
html += `<div class="meta-row"><span class="label">Duration</span><span class="value">${data.duration_human || '—'}</span></div>`;
|
||
html += `<div class="meta-row"><span class="label">Sample Rate</span><span class="value">${data.sample_rate_hz || data.sample_rate || '—'} Hz</span></div>`;
|
||
html += '</div>';
|
||
|
||
html += '<div class="meta-card"><h3>📊 Data Information</h3>';
|
||
html += `<div class="meta-row"><span class="label">Channels</span><span class="value">${data.n_channels || data.num_traces || '—'}</span></div>`;
|
||
html += `<div class="meta-row"><span class="label">Total Samples</span><span class="value">${(data.n_samples || data.total_samples || 0).toLocaleString()}</span></div>`;
|
||
html += `<div class="meta-row"><span class="label">Samples/Channel</span><span class="value">${(data.n_samples || 0).toLocaleString()}</span></div>`;
|
||
html += `<div class="meta-row"><span class="label">Source File</span><span class="value" title="${data.source_file || '—'}">${data.source_file || '—'}</span></div>`;
|
||
html += '</div>';
|
||
|
||
if (data.calibration) {
|
||
html += '<div class="meta-card"><h3>⚙️ Calibration</h3>';
|
||
for (const [k, v] of Object.entries(data.calibration)) {
|
||
html += `<div class="meta-row"><span class="label">${k.replace(/_/g, ' ')}</span><span class="value">${v}</span></div>`;
|
||
}
|
||
html += '</div>';
|
||
}
|
||
|
||
if (data.channels && data.channels.length) {
|
||
html += '<div class="meta-card" style="grid-column:1/-1"><h3>📡 Channels</h3>';
|
||
html += '<table class="htable"><tr><th>Channel</th><th>Sensor</th><th>Unit</th><th>Samples</th><th>Type</th></tr>';
|
||
data.channels.forEach(ch => {
|
||
html += `<tr>
|
||
<td>${ch.name}</td>
|
||
<td>${ch.sensor || '—'}</td>
|
||
<td>${ch.unit || '—'}</td>
|
||
<td>${(ch.samples || 0).toLocaleString()}</td>
|
||
<td>${ch.dtype}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</table></div>';
|
||
}
|
||
|
||
mc.innerHTML = html;
|
||
} catch (e) {
|
||
mc.innerHTML = `<div class="empty" style="color:var(--accent2)">Error: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function initMap() {
|
||
if (!map) {
|
||
setTimeout(() => {
|
||
map = L.map('map').setView([43.2965, 5.3698], 10);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© OpenStreetMap'
|
||
}).addTo(map);
|
||
map.on('click', onMapClick);
|
||
}, 100);
|
||
}
|
||
updateMapMarkers();
|
||
}
|
||
|
||
function updateMapMarkers() {
|
||
if (!map) return;
|
||
map.eachLayer(layer => {
|
||
if (layer instanceof L.Marker) {
|
||
map.removeLayer(layer);
|
||
}
|
||
});
|
||
for (const [filename, loc] of Object.entries(locations)) {
|
||
const marker = L.marker([loc.lat, loc.lon]).addTo(map);
|
||
marker.bindPopup(`<b>${filename}</b><br>Lat: ${loc.lat.toFixed(6)}<br>Lon: ${loc.lon.toFixed(6)}`);
|
||
if (filename === selectedFile) {
|
||
marker.openPopup();
|
||
map.setView([loc.lat, loc.lon], 12);
|
||
}
|
||
}
|
||
}
|
||
|
||
let editingLocation = false;
|
||
function enableLocationEdit() {
|
||
if (!selectedFile) {
|
||
alert('Please select a file first');
|
||
return;
|
||
}
|
||
editingLocation = true;
|
||
alert(`Click on the map to set location for ${selectedFile}`);
|
||
}
|
||
|
||
async function onMapClick(e) {
|
||
if (!editingLocation || !selectedFile) return;
|
||
const lat = e.latlng.lat;
|
||
const lon = e.latlng.lng;
|
||
try {
|
||
const r = await fetch(API + '/locations', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({filename: selectedFile, lat, lon})
|
||
});
|
||
const data = await r.json();
|
||
if (data.success) {
|
||
locations = data.locations;
|
||
updateMapMarkers();
|
||
setInfo(`Location set for ${selectedFile}: ${lat.toFixed(6)}, ${lon.toFixed(6)}`);
|
||
}
|
||
} catch (e) {
|
||
setInfo(`Error setting location: ${e.message}`);
|
||
}
|
||
editingLocation = false;
|
||
}
|
||
|
||
async function downloadExtract() {
|
||
if (!selectedFile) {
|
||
alert('Please select a file first');
|
||
return;
|
||
}
|
||
const channel = document.getElementById('extractChannel').value;
|
||
const start = parseFloat(document.getElementById('extractStart').value);
|
||
const duration = parseFloat(document.getElementById('extractDuration').value);
|
||
const format = document.getElementById('extractFormat').value;
|
||
|
||
const info = document.getElementById('extractInfo');
|
||
info.style.display = 'block';
|
||
info.innerHTML = `⏳ Extracting ${duration}s from ${selectedFile}...`;
|
||
|
||
try {
|
||
const url = `${API}/file/${encodeURIComponent(selectedFile)}/extract?start_sec=${start}&duration_sec=${duration}&channel=${encodeURIComponent(channel)}&format=${format}`;
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
const err = await response.json();
|
||
info.innerHTML = `❌ Error: ${err.error || 'Unknown error'}`;
|
||
return;
|
||
}
|
||
const blob = await response.blob();
|
||
const downloadUrl = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = downloadUrl;
|
||
a.download = `extract_${selectedFile.replace('.h5', '')}_${start}_${duration}s.${format}`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
window.URL.revokeObjectURL(downloadUrl);
|
||
info.innerHTML = `✅ Downloaded ${(blob.size / 1024 / 1024).toFixed(2)} MB`;
|
||
} catch (e) {
|
||
info.innerHTML = `❌ Error: ${e.message}`;
|
||
}
|
||
}
|
||
|
||
async function loadPipelineStatus() {
|
||
try {
|
||
const r = await fetch(API + '/pipeline-status');
|
||
const d = await r.json();
|
||
if (d.error) return;
|
||
|
||
document.getElementById('countRaw').textContent = d.counts.raw;
|
||
document.getElementById('countSegy').textContent = d.counts.segy;
|
||
document.getElementById('countH5').textContent = d.counts.h5;
|
||
|
||
const usedPct = d.disk.percent;
|
||
document.getElementById('diskUsage').textContent = `${usedPct}%`;
|
||
document.getElementById('diskBar').style.width = `${usedPct}%`;
|
||
document.getElementById('diskDetail').textContent = `${d.disk.used_gb} GB utilisés sur ${d.disk.total_gb} GB`;
|
||
|
||
const total = d.counts.total_expected || 345;
|
||
const h5 = d.counts.h5;
|
||
const pct = Math.round((h5 / total) * 100);
|
||
document.getElementById('completionPct').textContent = `${pct}%`;
|
||
document.getElementById('completionBar').style.width = `${pct}%`;
|
||
document.getElementById('completionDetail').textContent = `${h5} / ${total} fichiers H5 prêts`;
|
||
|
||
if (d.progress) {
|
||
document.getElementById('convertStatus').textContent = 'En cours...';
|
||
document.getElementById('convertStatus').style.color = 'var(--yellow)';
|
||
document.getElementById('convertDetail').textContent = `Fichier: ${d.progress.current_file}`;
|
||
} else {
|
||
document.getElementById('convertStatus').textContent = 'Inactif';
|
||
document.getElementById('convertStatus').style.color = 'var(--muted)';
|
||
document.getElementById('convertDetail').textContent = 'Aucune tâche en cours';
|
||
}
|
||
} catch (e) {
|
||
console.error('Pipeline status error:', e);
|
||
}
|
||
}
|
||
|
||
async function loadSection() {
|
||
if (!selectedFile) return;
|
||
setInfo('Loading section...');
|
||
const ts = parseInt(document.getElementById('inTraceStart').value);
|
||
const tc = parseInt(document.getElementById('inTraceCount').value);
|
||
const spt = parseInt(document.getElementById('inSPT').value);
|
||
const params = new URLSearchParams({
|
||
trace_start: ts, trace_count: tc,
|
||
samples_per_trace: spt, channel: selectedChannel,
|
||
max_samples: 500
|
||
});
|
||
|
||
try {
|
||
const r = await fetch(`${API}/file/${encodeURIComponent(selectedFile)}/section?${params}`);
|
||
const d = await r.json();
|
||
if (d.error) { setInfo(`Error: ${d.error}`); return; }
|
||
|
||
const bytes = Uint8Array.from(atob(d.data_b64), c => c.charCodeAt(0));
|
||
const floats = new Float32Array(bytes.buffer);
|
||
|
||
sectionData = {
|
||
num_traces: d.num_traces,
|
||
samples_per_trace: d.samples_per_trace,
|
||
trace_start: d.trace_start,
|
||
total_traces: d.total_traces,
|
||
vmin: d.vmin, vmax: d.vmax,
|
||
data: floats
|
||
};
|
||
|
||
const loadInfo = document.getElementById('loadInfoPanel');
|
||
const loadText = document.getElementById('loadedTracesInfo');
|
||
loadInfo.style.display = 'block';
|
||
loadText.innerHTML = `Traces <strong>${d.trace_start}–${d.trace_start + d.num_traces}</strong> (${d.num_traces} traces × ${d.samples_per_trace} samp) · Channel: <strong>${selectedChannel.split('/').pop()}</strong> · Range: [${d.vmin.toFixed(3)}, ${d.vmax.toFixed(3)}]`;
|
||
|
||
setInfo(`${selectedFile} — ${d.num_traces} traces × ${d.samples_per_trace} samples | range [${d.vmin.toFixed(2)}, ${d.vmax.toFixed(2)}] | total: ${d.total_traces} traces`);
|
||
renderSection();
|
||
} catch (e) {
|
||
setInfo(`Error: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
function renderSection() {
|
||
if (!sectionData) return;
|
||
const gain = parseFloat(document.getElementById('inGain').value);
|
||
const clipPct = parseFloat(document.getElementById('inClip').value) / 100;
|
||
const cmap = document.getElementById('selCmap').value;
|
||
const norm = document.getElementById('selNorm').value;
|
||
const { num_traces, samples_per_trace, data, trace_start } = sectionData;
|
||
|
||
const z = [];
|
||
for (let s = 0; s < samples_per_trace; s++) {
|
||
const row = [];
|
||
for (let t = 0; t < num_traces; t++) {
|
||
row.push(data[t * samples_per_trace + s] * gain);
|
||
}
|
||
z.push(row);
|
||
}
|
||
|
||
let clipVal;
|
||
if (norm === 'global') {
|
||
const allVals = Array.from(data).map(v => Math.abs(v * gain));
|
||
allVals.sort((a, b) => a - b);
|
||
clipVal = allVals[Math.floor(allVals.length * clipPct)] || allVals[allVals.length - 1];
|
||
} else {
|
||
for (let s = 0; s < samples_per_trace; s++) {
|
||
for (let t = 0; t < num_traces; t++) {
|
||
const traceVals = [];
|
||
for (let ss = 0; ss < samples_per_trace; ss++) {
|
||
traceVals.push(Math.abs(data[t * samples_per_trace + ss]));
|
||
}
|
||
traceVals.sort((a, b) => a - b);
|
||
const trClip = traceVals[Math.floor(traceVals.length * clipPct)] || 1;
|
||
z[s][t] = (data[t * samples_per_trace + s] * gain) / trClip;
|
||
}
|
||
}
|
||
clipVal = 1;
|
||
}
|
||
|
||
const showDensity = currentView === 'density' || currentView === 'both';
|
||
const showWiggle = currentView === 'wiggle' || currentView === 'both';
|
||
const container = document.getElementById('heatmapContainer');
|
||
|
||
if (showDensity) {
|
||
const traceLabels = Array.from({length: num_traces}, (_, i) => i + trace_start);
|
||
const sampleLabels = Array.from({length: samples_per_trace}, (_, i) => i);
|
||
Plotly.react('heatmapContainer', [{
|
||
z: z,
|
||
x: traceLabels,
|
||
y: sampleLabels,
|
||
type: 'heatmap',
|
||
colorscale: cmap,
|
||
zmin: -clipVal,
|
||
zmax: clipVal,
|
||
colorbar: { title: 'Amplitude', titlefont: { color: '#8899aa', size: 10 }, tickfont: { color: '#8899aa', size: 9 } },
|
||
hovertemplate: 'Trace: %{x}<br>Sample: %{y}<br>Amp: %{z:.4f}<extra></extra>'
|
||
}], {
|
||
paper_bgcolor: '#1a1a2e',
|
||
plot_bgcolor: '#1a1a2e',
|
||
font: { color: '#eee' },
|
||
xaxis: { title: 'Trace', color: '#8899aa', gridcolor: '#2a2a4a' },
|
||
yaxis: { title: 'Sample', color: '#8899aa', gridcolor: '#2a2a4a', autorange: 'reversed' },
|
||
margin: { t: 10, r: 80, b: 40, l: 60 }
|
||
}, { responsive: true });
|
||
} else {
|
||
container.innerHTML = '';
|
||
}
|
||
|
||
const canvas = document.getElementById('wiggleCanvas');
|
||
if (showWiggle) {
|
||
drawWiggle(canvas, container, clipVal);
|
||
} else {
|
||
canvas.width = 0;
|
||
canvas.height = 0;
|
||
}
|
||
}
|
||
|
||
function drawWiggle(canvas, container, clipVal) {
|
||
if (!sectionData) return;
|
||
const { num_traces, samples_per_trace, data, trace_start } = sectionData;
|
||
const gain = parseFloat(document.getElementById('inGain').value);
|
||
const rect = container.getBoundingClientRect();
|
||
canvas.width = rect.width;
|
||
canvas.height = rect.height;
|
||
canvas.style.width = rect.width + 'px';
|
||
canvas.style.height = rect.height + 'px';
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
if (currentView === 'wiggle') {
|
||
ctx.fillStyle = '#1a1a2e';
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
canvas.style.pointerEvents = 'auto';
|
||
} else {
|
||
canvas.style.pointerEvents = 'none';
|
||
}
|
||
|
||
const ml = 60, mr = 80, mt = 10, mb = 40;
|
||
const pw = canvas.width - ml - mr;
|
||
const ph = canvas.height - mt - mb;
|
||
if (pw <= 0 || ph <= 0) return;
|
||
const traceSpacing = pw / num_traces;
|
||
const halfSpacing = traceSpacing * 0.45;
|
||
|
||
for (let t = 0; t < num_traces; t++) {
|
||
const cx = ml + (t + 0.5) * traceSpacing;
|
||
ctx.beginPath();
|
||
const fillPath = new Path2D();
|
||
fillPath.moveTo(cx, mt);
|
||
for (let s = 0; s < samples_per_trace; s++) {
|
||
const val = (data[t * samples_per_trace + s] * gain) / clipVal;
|
||
const x = cx + val * halfSpacing;
|
||
const y = mt + (s / (samples_per_trace - 1)) * ph;
|
||
if (s === 0) ctx.moveTo(x, y);
|
||
else ctx.lineTo(x, y);
|
||
fillPath.lineTo(x, y);
|
||
}
|
||
ctx.strokeStyle = currentView === 'both' ? 'rgba(0,0,0,0.6)' : '#00d4ff';
|
||
ctx.lineWidth = 0.5;
|
||
ctx.stroke();
|
||
fillPath.lineTo(cx, mt + ph);
|
||
fillPath.closePath();
|
||
ctx.save();
|
||
ctx.clip(fillPath);
|
||
ctx.fillStyle = currentView === 'both' ? 'rgba(0,0,0,0.3)' : 'rgba(0,212,255,0.3)';
|
||
ctx.fillRect(cx, mt, halfSpacing + 5, ph);
|
||
ctx.restore();
|
||
}
|
||
|
||
if (currentView === 'wiggle') {
|
||
ctx.strokeStyle = '#2a2a4a';
|
||
ctx.lineWidth = 1;
|
||
ctx.strokeRect(ml, mt, pw, ph);
|
||
ctx.fillStyle = '#8899aa';
|
||
ctx.font = '10px sans-serif';
|
||
ctx.textAlign = 'center';
|
||
const step = Math.max(1, Math.floor(num_traces / 10));
|
||
for (let t = 0; t < num_traces; t += step) {
|
||
const x = ml + (t + 0.5) * traceSpacing;
|
||
ctx.fillText(t + trace_start, x, mt + ph + 15);
|
||
}
|
||
ctx.textAlign = 'right';
|
||
const ystep = Math.max(1, Math.floor(samples_per_trace / 8));
|
||
for (let s = 0; s < samples_per_trace; s += ystep) {
|
||
const y = mt + (s / (samples_per_trace - 1)) * ph;
|
||
ctx.fillText(s, ml - 5, y + 3);
|
||
}
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText('Trace', ml + pw / 2, mt + ph + 32);
|
||
ctx.save();
|
||
ctx.translate(15, mt + ph / 2);
|
||
ctx.rotate(-Math.PI / 2);
|
||
ctx.fillText('Sample', 0, 0);
|
||
ctx.restore();
|
||
}
|
||
}
|
||
|
||
function setView(v) {
|
||
currentView = v;
|
||
document.getElementById('btnDensity').classList.toggle('active', v === 'density');
|
||
document.getElementById('btnWiggle').classList.toggle('active', v === 'wiggle');
|
||
document.getElementById('btnBoth').classList.toggle('active', v === 'both');
|
||
renderSection();
|
||
}
|
||
|
||
function switchTab(tab) {
|
||
currentTab = tab;
|
||
document.querySelectorAll('.tab').forEach((el, i) => {
|
||
el.classList.toggle('active', ['section', 'metadata', 'map', 'extract', 'pipeline', 'geosup', 'headers', 'waveform'][i] === tab);
|
||
});
|
||
document.getElementById('sectionView').classList.toggle('active', tab === 'section');
|
||
document.getElementById('metadataView').classList.toggle('active', tab === 'metadata');
|
||
document.getElementById('mapView').classList.toggle('active', tab === 'map');
|
||
document.getElementById('extractView').classList.toggle('active', tab === 'extract');
|
||
document.getElementById('pipelineView').classList.toggle('active', tab === 'pipeline');
|
||
document.getElementById('geosupView').classList.toggle('active', tab === 'geosup');
|
||
document.getElementById('headersView').classList.toggle('active', tab === 'headers');
|
||
document.getElementById('waveformView').classList.toggle('active', tab === 'waveform');
|
||
|
||
if (tab === 'metadata' && selectedFile) loadMetadata();
|
||
if (tab === 'map') initMap();
|
||
if (tab === 'pipeline') loadPipelineStatus();
|
||
if (tab === 'geosup') loadGeosupData();
|
||
if (tab === 'waveform' && selectedFile) loadWaveform();
|
||
if (tab === 'section') renderSection();
|
||
if (tab === 'headers' && selectedFile) loadHeaders();
|
||
}
|
||
|
||
async function loadHeaders() {
|
||
if (!selectedFile) return;
|
||
const hc = document.getElementById('headersContent');
|
||
hc.innerHTML = '<div class="empty">Loading headers...</div>';
|
||
try {
|
||
const r = await fetch(`${API}/file/${encodeURIComponent(selectedFile)}/headers`);
|
||
const d = await r.json();
|
||
if (d.error) { hc.innerHTML = `<div class="empty" style="color:var(--accent2)">${d.error}</div>`; return; }
|
||
let html = '';
|
||
if (d.type === 'h5') {
|
||
if (d.file_attrs && Object.keys(d.file_attrs).length) {
|
||
html += '<h3 style="color:var(--accent);padding:8px 0;font-size:.85rem">File Attributes</h3>';
|
||
html += '<table class="htable"><tr><th>Key</th><th>Value</th></tr>';
|
||
for (const [k, v] of Object.entries(d.file_attrs)) { html += `<tr><td>${k}</td><td>${v}</td></tr>`; }
|
||
html += '</table>';
|
||
}
|
||
if (d.groups && d.groups.length) {
|
||
html += '<h3 style="color:var(--accent);padding:8px 0;font-size:.85rem">Groups</h3>';
|
||
html += '<table class="htable"><tr><th>Path</th><th>Attributes</th></tr>';
|
||
d.groups.forEach(g => {
|
||
const attrs = Object.entries(g).filter(([k]) => k !== 'path').map(([k, v]) => `${k}: ${v}`).join(', ');
|
||
html += `<tr><td>${g.path}</td><td>${attrs}</td></tr>`;
|
||
});
|
||
html += '</table>';
|
||
}
|
||
html += '<h3 style="color:var(--accent);padding:8px 0;font-size:.85rem">Datasets</h3>';
|
||
html += '<table class="htable"><tr><th>Path</th><th>Shape</th><th>Dtype</th><th>Size</th><th>Attributes</th></tr>';
|
||
(d.datasets || []).forEach(ds => {
|
||
const attrs = Object.entries(ds).filter(([k]) => k.startsWith('attr:')).map(([k, v]) => `${k.slice(5)}: ${v}`).join(', ');
|
||
html += `<tr><td>${ds.path}</td><td>${JSON.stringify(ds.shape)}</td><td>${ds.dtype}</td><td>${ds.size?.toLocaleString()}</td><td>${attrs || '—'}</td></tr>`;
|
||
});
|
||
html += '</table>';
|
||
} else if (d.type === 'segy') {
|
||
if (d.binary_header) {
|
||
html += '<h3 style="color:var(--accent);padding:8px 0;font-size:.85rem">Binary File Header</h3>';
|
||
html += '<table class="htable"><tr><th>Field</th><th>Value</th></tr>';
|
||
for (const [k, v] of Object.entries(d.binary_header)) { html += `<tr><td>${k.replace(/_/g, ' ')}</td><td>${v}</td></tr>`; }
|
||
html += '</table>';
|
||
}
|
||
if (d.trace_headers && d.trace_headers.length) {
|
||
html += `<h3 style="color:var(--accent);padding:8px 0;font-size:.85rem">Trace Headers (${d.trace_headers.length} of ${d.total_traces})</h3>`;
|
||
const cols = Object.keys(d.trace_headers[0]);
|
||
html += '<table class="htable"><tr>' + cols.map(c => `<th>${c}</th>`).join('') + '</tr>';
|
||
d.trace_headers.forEach(th => { html += '<tr>' + cols.map(c => `<td>${th[c] ?? '—'}</td>`).join('') + '</tr>'; });
|
||
html += '</table>';
|
||
}
|
||
}
|
||
hc.innerHTML = html || '<div class="empty">No header data</div>';
|
||
} catch (e) { hc.innerHTML = `<div class="empty" style="color:var(--accent2)">Error: ${e.message}</div>`; }
|
||
}
|
||
|
||
async function loadWaveform() {
|
||
if (!selectedFile || !selectedFile.endsWith('.h5')) {
|
||
document.getElementById('waveformChart').innerHTML = '<div class="empty">Waveform view available for H5 files</div>';
|
||
return;
|
||
}
|
||
document.getElementById('waveformChart').innerHTML = '<div class="empty">Loading...</div>';
|
||
const r = await fetch(`${API}/file/${encodeURIComponent(selectedFile)}/waveform?channel=${encodeURIComponent(selectedChannel)}&points=5000`);
|
||
const d = await r.json();
|
||
if (d.error) { document.getElementById('waveformChart').innerHTML = `<div class="empty">${d.error}</div>`; return; }
|
||
Plotly.newPlot('waveformChart', [{
|
||
x: d.x, y: d.y, type: 'scattergl', mode: 'lines',
|
||
line: { color: '#00d4ff', width: 1 }
|
||
}], {
|
||
paper_bgcolor: '#1a1a2e', plot_bgcolor: '#1a1a2e',
|
||
font: { color: '#eee' },
|
||
xaxis: { title: 'Sample', color: '#8899aa', gridcolor: '#2a2a4a' },
|
||
yaxis: { title: 'Amplitude', color: '#8899aa', gridcolor: '#2a2a4a' },
|
||
margin: { t: 10, r: 20, b: 40, l: 60 },
|
||
annotations: [{
|
||
x: 0.01, y: 0.98, xref: 'paper', yref: 'paper', showarrow: false,
|
||
text: `${d.total_samples.toLocaleString()} samples → ${d.displayed_points} pts | min=${d.min.toFixed(4)} max=${d.max.toFixed(4)}`,
|
||
font: { size: 10, color: '#8899aa' }, bgcolor: '#16213e', borderpad: 4
|
||
}]
|
||
}, { responsive: true });
|
||
}
|
||
|
||
function setupResize() {
|
||
const handle = document.getElementById('resizeHandle');
|
||
const sidebar = document.getElementById('sidebar');
|
||
let dragging = false, startX, startW;
|
||
handle.addEventListener('mousedown', e => {
|
||
dragging = true; startX = e.clientX; startW = sidebar.offsetWidth;
|
||
document.body.style.cursor = 'col-resize';
|
||
e.preventDefault();
|
||
});
|
||
document.addEventListener('mousemove', e => {
|
||
if (!dragging) return;
|
||
sidebar.style.width = Math.max(150, Math.min(500, startW + e.clientX - startX)) + 'px';
|
||
});
|
||
document.addEventListener('mouseup', () => {
|
||
if (dragging) { dragging = false; document.body.style.cursor = ''; }
|
||
});
|
||
}
|
||
|
||
function setInfo(msg) {
|
||
document.getElementById('infopanel').innerHTML = `<span>${msg}</span><span>${new Date().toLocaleTimeString('fr')}</span>`;
|
||
}
|
||
|
||
let geosupData = null;
|
||
let geosupLayers = { sources: null, receivers: null, deployments: null };
|
||
let layerVisibility = { sources: false, receivers: false, deployments: false, files: true };
|
||
|
||
async function loadGeosupData() {
|
||
try {
|
||
const posResp = await fetch(API + '/geosup/positions');
|
||
geosupData = await posResp.json();
|
||
|
||
if (geosupData.counts) {
|
||
document.getElementById('statSources').textContent = geosupData.counts.sources.toLocaleString();
|
||
document.getElementById('statReceivers').textContent = geosupData.counts.receivers.toLocaleString();
|
||
document.getElementById('statDeployments').textContent = geosupData.counts.deployments.toLocaleString();
|
||
}
|
||
|
||
const docResp = await fetch(API + '/geosup/documents');
|
||
const docData = await docResp.json();
|
||
|
||
document.getElementById('statDocs').textContent = docData.count || 0;
|
||
|
||
const docList = document.getElementById('docList');
|
||
if (docData.documents && docData.documents.length) {
|
||
docList.innerHTML = docData.documents.map(doc => `
|
||
<div class="doc-item" onclick="downloadDoc('${doc.name}')">
|
||
<span class="icon">${doc.icon}</span>
|
||
<div class="info">
|
||
<div class="name">${doc.name}</div>
|
||
<div class="meta">${doc.type} • ${doc.size_human}</div>
|
||
</div>
|
||
${doc.is_gundalf ? '<span class="badge gundalf">GUNDALF</span>' : ''}
|
||
${doc.is_params ? '<span class="badge">PARAMS</span>' : ''}
|
||
</div>
|
||
`).join('');
|
||
} else {
|
||
docList.innerHTML = '<div class="empty">No documents found</div>';
|
||
}
|
||
} catch (e) {
|
||
console.error('Geosup load error:', e);
|
||
}
|
||
}
|
||
|
||
function downloadDoc(filename) {
|
||
window.open(API + '/geosup/documents/' + encodeURIComponent(filename), '_blank');
|
||
}
|
||
|
||
async function loadGeosupLayers() {
|
||
if (!map) {
|
||
setTimeout(loadGeosupLayers, 500);
|
||
return;
|
||
}
|
||
|
||
if (!geosupData) {
|
||
const resp = await fetch(API + '/geosup/positions');
|
||
geosupData = await resp.json();
|
||
}
|
||
|
||
if (!geosupData.features) return;
|
||
|
||
Object.values(geosupLayers).forEach(layer => {
|
||
if (layer) map.removeLayer(layer);
|
||
});
|
||
|
||
const sourceMarkers = [];
|
||
const receiverMarkers = [];
|
||
const deploymentMarkers = [];
|
||
|
||
geosupData.features.forEach(feature => {
|
||
const coords = feature.geometry.coordinates;
|
||
const props = feature.properties;
|
||
const popup = `<b>${props.category.toUpperCase()}</b><br>
|
||
Line: ${props.line}, Point: ${props.point}<br>
|
||
E: ${props.easting?.toFixed(1)}, N: ${props.northing?.toFixed(1)}<br>
|
||
Depth: ${props.depth}m${props.timestamp ? '<br>Time: ' + props.timestamp : ''}`;
|
||
|
||
const color = props.category === 'source' ? '#3b82f6' :
|
||
props.category === 'receiver' ? '#ef4444' : '#22c55e';
|
||
|
||
const marker = L.circleMarker([coords[1], coords[0]], {
|
||
radius: 4,
|
||
fillColor: color,
|
||
color: color,
|
||
weight: 1,
|
||
opacity: 0.8,
|
||
fillOpacity: 0.6
|
||
}).bindPopup(popup);
|
||
|
||
if (props.category === 'source') sourceMarkers.push(marker);
|
||
else if (props.category === 'receiver') receiverMarkers.push(marker);
|
||
else deploymentMarkers.push(marker);
|
||
});
|
||
|
||
geosupLayers.sources = L.layerGroup(sourceMarkers);
|
||
geosupLayers.receivers = L.layerGroup(receiverMarkers);
|
||
geosupLayers.deployments = L.layerGroup(deploymentMarkers);
|
||
|
||
document.getElementById('layerSources').checked = true;
|
||
document.getElementById('layerReceivers').checked = true;
|
||
document.getElementById('layerDeployments').checked = true;
|
||
layerVisibility.sources = true;
|
||
layerVisibility.receivers = true;
|
||
layerVisibility.deployments = true;
|
||
|
||
geosupLayers.sources.addTo(map);
|
||
geosupLayers.receivers.addTo(map);
|
||
geosupLayers.deployments.addTo(map);
|
||
|
||
if (geosupData.features.length > 0) {
|
||
const bounds = L.latLngBounds(geosupData.features.map(f => [f.geometry.coordinates[1], f.geometry.coordinates[0]]));
|
||
map.fitBounds(bounds, { padding: [20, 20] });
|
||
}
|
||
|
||
setInfo(`Loaded ${geosupData.counts.total} geosup positions`);
|
||
}
|
||
|
||
function toggleLayer(layerName) {
|
||
const checkbox = document.getElementById('layer' + layerName.charAt(0).toUpperCase() + layerName.slice(1));
|
||
layerVisibility[layerName] = checkbox.checked;
|
||
|
||
if (layerName === 'files') {
|
||
updateMapMarkers();
|
||
} else if (geosupLayers[layerName]) {
|
||
if (layerVisibility[layerName]) {
|
||
geosupLayers[layerName].addTo(map);
|
||
} else {
|
||
map.removeLayer(geosupLayers[layerName]);
|
||
}
|
||
}
|
||
}
|
||
|
||
window.addEventListener('resize', () => {
|
||
if (currentTab === 'section') renderSection();
|
||
if (map) map.invalidateSize();
|
||
});
|
||
|
||
(async function bootInit() {
|
||
for (let attempt = 0; attempt < 5; attempt++) {
|
||
try {
|
||
await init();
|
||
console.log('init OK, files=' + files.length);
|
||
return;
|
||
} catch(e) {
|
||
console.warn('init attempt ' + (attempt+1) + ' failed:', e.message);
|
||
await new Promise(r => setTimeout(r, 2000));
|
||
}
|
||
}
|
||
console.error('init failed after 5 attempts');
|
||
})();
|
||
|
||
function clearGatherFiles() {
|
||
gatherSelectedFiles.clear();
|
||
updateGatherSelCount();
|
||
filterFiles();
|
||
}
|
||
|
||
function closeExtractModal() {
|
||
activeExtractTask = null; // Stop polling
|
||
document.getElementById('extractModal').style.display = 'none';
|
||
}
|
||
|
||
async function doExtract() {
|
||
var channels = document.getElementById('extChannels').value;
|
||
var scope = document.getElementById('extScope').value;
|
||
var start = parseFloat(document.getElementById('extStart').value);
|
||
var duration = parseFloat(document.getElementById('extDuration').value);
|
||
var incNodes = document.getElementById('extIncNodes').checked;
|
||
var incShots = document.getElementById('extIncShots').checked;
|
||
var incMeta = document.getElementById('extIncMeta').checked;
|
||
|
||
var filesParam;
|
||
if (scope === 'all_lines') {
|
||
filesParam = files.filter(f=>f.name.endsWith('.h5')).map(f=>f.name).join(',');
|
||
} else if (scope === 'line') {
|
||
var lineDropdown = document.getElementById('gatherLineDropdown');
|
||
var currentLine = lineDropdown ? lineDropdown.value : '';
|
||
if (currentLine) {
|
||
var lineNum = parseInt(currentLine);
|
||
var lineFiles = [];
|
||
files.forEach(function(f) {
|
||
var ni = getNodeInfo(f.name);
|
||
if (ni && ni.line === lineNum) lineFiles.push(f.name);
|
||
});
|
||
filesParam = lineFiles.join(',');
|
||
} else {
|
||
filesParam = Array.from(gatherSelectedFiles).join(',');
|
||
}
|
||
} else {
|
||
if (gatherOverlapInfo && gatherOverlapInfo.groups && gatherOverlapInfo.groups[gatherActiveGroup]) {
|
||
filesParam = gatherOverlapInfo.groups[gatherActiveGroup].files.join(',');
|
||
} else {
|
||
filesParam = Array.from(gatherSelectedFiles).join(',');
|
||
}
|
||
}
|
||
|
||
// UI update
|
||
document.getElementById('extDownloadBtn').disabled = true;
|
||
document.getElementById('extDownloadBtn').style.opacity = '0.5';
|
||
document.getElementById('extCancelBtn').style.display = 'none';
|
||
document.getElementById('extProgressContainer').style.display = 'block';
|
||
document.getElementById('extEstimate').style.display = 'none';
|
||
|
||
try {
|
||
var r = await fetch(API + '/gather_extract', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
files: filesParam, start: start, duration: duration,
|
||
channels: channels,
|
||
include_nodes: incNodes,
|
||
include_shots: incShots,
|
||
include_metadata: incMeta
|
||
})
|
||
});
|
||
|
||
if (!r.ok) {
|
||
var err = await r.json();
|
||
throw new Error(err.error || r.statusText);
|
||
}
|
||
|
||
var data = await r.json();
|
||
activeExtractTask = data.task_id;
|
||
pollTaskStatus(activeExtractTask);
|
||
|
||
} catch(e) {
|
||
alert('Extract error: ' + e.message);
|
||
resetExtractModal();
|
||
}
|
||
}
|
||
|
||
function extractBoardIdJs(filename) {
|
||
const m = filename.match(/_b(\d+)_/);
|
||
return m ? m[1] : null;
|
||
}
|
||
|
||
function extractGather() {
|
||
if (gatherSelectedFiles.size === 0) {
|
||
setInfo('Extract: select files first');
|
||
return;
|
||
}
|
||
document.getElementById('extStart').value = document.getElementById('gatherStart').value;
|
||
document.getElementById('extDuration').value = document.getElementById('gatherDuration').value;
|
||
|
||
var currentCount = 0;
|
||
if (gatherOverlapInfo && gatherOverlapInfo.groups && gatherOverlapInfo.groups[gatherActiveGroup]) {
|
||
currentCount = gatherOverlapInfo.groups[gatherActiveGroup].files.length;
|
||
} else {
|
||
currentCount = gatherSelectedFiles.size;
|
||
}
|
||
var lineDropdown = document.getElementById('gatherLineDropdown');
|
||
var currentLine = lineDropdown ? lineDropdown.value : '';
|
||
var scopeEl = document.getElementById('extScope');
|
||
scopeEl.options[0].text = 'Current selection (' + currentCount + ' files)';
|
||
if (currentLine) {
|
||
scopeEl.options[1].text = 'All files on ' + currentLine + ' line';
|
||
}
|
||
scopeEl.options[2].text = 'All 8 lines (' + files.filter(f=>f.name.endsWith(".h5")).length + ' files)';
|
||
|
||
updateExtEstimate();
|
||
resetExtractModal(); // Reset UI state
|
||
document.getElementById('extractModal').style.display = 'flex';
|
||
}
|
||
|
||
function fmtFileName(fn) {
|
||
var m = fn.match(/auto_(\d+)_(\d{2})(\d{2})(\d{2})_b(\d+)/);
|
||
if (m) {
|
||
var day = m[1], hh = m[2], mm = m[3], bid = m[5];
|
||
var node = darfNodes && darfNodes[bid];
|
||
var label = 'b' + bid;
|
||
if (node && node.line) label += ' L' + node.line;
|
||
label += ' (D' + day + ' ' + hh + ':' + mm + ')';
|
||
return label;
|
||
}
|
||
return fn.replace('.h5','').substring(0,25);
|
||
}
|
||
|
||
function fmtTime(s) {
|
||
s = Math.round(s);
|
||
const h = Math.floor(s / 3600);
|
||
const m = Math.floor((s % 3600) / 60);
|
||
const sec = s % 60;
|
||
if (h > 0) return h + ':' + String(m).padStart(2,'0') + ':' + String(sec).padStart(2,'0');
|
||
return m + ':' + String(sec).padStart(2,'0');
|
||
}
|
||
|
||
function getNodeInfo(fname) {
|
||
var m = fname.match(/auto_(\d+)_(\d{2})(\d{2})(\d{2})_b(\d+)/);
|
||
if (!m) return null;
|
||
var bid = m[5];
|
||
var node = darfNodes && darfNodes[bid];
|
||
return { day: m[1], hh: m[2], mm: m[3], bid: bid, node: node,
|
||
line: node ? node.line : 0, point: node ? node.point : 0 };
|
||
}
|
||
|
||
function onGatherParamChange() {
|
||
const start = parseInt(document.getElementById('gatherStart').value) || 0;
|
||
document.getElementById('gatherTimeSlider').value = start;
|
||
document.getElementById('gatherSliderLabel').textContent = fmtTime(start);
|
||
if (gatherSelectedFiles.size > 0) loadGather();
|
||
}
|
||
|
||
function onGatherSliderInput(val) {
|
||
val = parseInt(val);
|
||
document.getElementById('gatherStart').value = val;
|
||
document.getElementById('gatherSliderLabel').textContent = fmtTime(val);
|
||
clearTimeout(gatherSliderDebounce);
|
||
gatherSliderDebounce = setTimeout(() => { if (gatherSelectedFiles.size > 0) loadGather(); }, 300);
|
||
}
|
||
|
||
async function pollTaskStatus(taskId) {
|
||
if (activeExtractTask !== taskId) return;
|
||
|
||
try {
|
||
var r = await fetch(API + '/tasks/' + taskId);
|
||
if (!r.ok) throw new Error('Status check failed');
|
||
var status = await r.json();
|
||
|
||
if (status.status === 'processing' || status.status === 'pending') {
|
||
var pct = status.progress || 0;
|
||
var file = status.current_file ? status.current_file.substring(0,20)+'...' : '';
|
||
document.getElementById('extProgressBar').style.width = pct + '%';
|
||
document.getElementById('extPercentText').textContent = pct + '%';
|
||
document.getElementById('extStatusText').textContent = 'Processing ' + status.processed_files + '/' + status.total_files + (file ? ': ' + file : '');
|
||
setTimeout(function() { pollTaskStatus(taskId); }, 1000);
|
||
|
||
} else if (status.status === 'completed') {
|
||
document.getElementById('extProgressBar').style.width = '100%';
|
||
document.getElementById('extPercentText').textContent = '100%';
|
||
document.getElementById('extStatusText').textContent = 'Compressing & Downloading...';
|
||
|
||
window.location.href = API + '/tasks/' + taskId + '/download';
|
||
|
||
setTimeout(function() {
|
||
closeExtractModal();
|
||
resetExtractModal();
|
||
}, 2000);
|
||
|
||
} else if (status.status === 'error') {
|
||
throw new Error(status.error || 'Unknown error');
|
||
}
|
||
|
||
} catch(e) {
|
||
alert('Task error: ' + e.message);
|
||
resetExtractModal();
|
||
}
|
||
}
|
||
|
||
function populateGatherLineDropdown() {
|
||
const lines = new Set();
|
||
Object.values(darfNodes).forEach(n => { if (n.line) lines.add(n.line); });
|
||
const sorted = Array.from(lines).sort((a, b) => a - b);
|
||
const dd = document.getElementById('gatherLineDropdown');
|
||
dd.innerHTML = '<option value="">-- All lines --</option>' + sorted.map(l => `<option value="${l}">Line ${l}</option>`).join('');
|
||
}
|
||
|
||
function populateWaveformChannels(info) { var container = document.getElementById("waveformChannelChecks"); if (!container) return; container.innerHTML = ""; var channels = (info.calibrated_channels || []).concat(info.raw_channels || []); channels.forEach(function(d) { var label = document.createElement("label"); label.style.cssText = "display:flex;align-items:center;gap:4px;color:var(--text);font-size:.8rem;cursor:pointer"; var cb = document.createElement("input"); cb.type = "checkbox"; cb.value = d.path; cb.checked = (d.path === selectedChannel); label.appendChild(cb); label.appendChild(document.createTextNode(d.path.split("/").pop())); container.appendChild(label); });}
|
||
|
||
function renderFileItem(f, showCb, ni) {
|
||
const info = fileInfoCache[f.name] || {};
|
||
let infoText = '';
|
||
if (info.duration_human && info.num_channels && info.sample_rate_hz) {
|
||
infoText = '<div class="finfo">' + info.duration_human + ' · ' + info.num_channels + ' ch · ' + info.sample_rate_hz + ' Hz</div>';
|
||
}
|
||
const escapedName = f.name.replace(/'/g, "\\'");
|
||
const cb = showCb ? '<input type="checkbox" class="gather-cb" ' + (gatherSelectedFiles.has(f.name)?'checked':'') + ' onclick="event.stopPropagation();toggleGatherFile(\'' + escapedName + '\',this.checked)" style="margin-right:6px;cursor:pointer;accent-color:var(--accent)">' : '';
|
||
var label = fmtFileName(f.name);
|
||
if (ni && ni.point) label = 'P' + ni.point + ' — ' + label;
|
||
return '<div class="fitem' + (selectedFile===f.name?' active':'') + '" onclick="selectFile(\'' + escapedName + '\')">' +
|
||
'<div class="fhead">' + cb + '<span class="fn" title="' + f.name + '">' + label + '</span>' +
|
||
'<span class="ft ft-' + f.type + '">' + f.type.toUpperCase() + '</span></div>' +
|
||
infoText + '</div>';
|
||
}
|
||
|
||
function renderFilesFlat(list, showCb) {
|
||
var html = '<div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:6px;padding:0 4px">';
|
||
html += '<button onclick="sidebarGroupBy=\'flat\';filterFiles()" style="font-size:10px;padding:2px 6px;background:var(--accent);border:none;color:#fff;border-radius:3px;cursor:pointer">Flat</button>';
|
||
html += '<button onclick="sidebarGroupBy=\'line\';filterFiles()" style="font-size:10px;padding:2px 6px;background:var(--surface);border:1px solid var(--border);color:var(--text);border-radius:3px;cursor:pointer">By Line</button>';
|
||
html += '</div>';
|
||
list.forEach(function(f) {
|
||
html += renderFileItem(f, showCb, getNodeInfo(f.name));
|
||
});
|
||
document.getElementById('fileList').innerHTML = html;
|
||
}
|
||
|
||
function resetExtractModal() {
|
||
activeExtractTask = null;
|
||
var btn = document.getElementById('extDownloadBtn');
|
||
if(btn) {
|
||
btn.disabled = false;
|
||
btn.style.opacity = '1';
|
||
}
|
||
var cancel = document.getElementById('extCancelBtn');
|
||
if(cancel) cancel.style.display = 'block';
|
||
var prog = document.getElementById('extProgressContainer');
|
||
if(prog) prog.style.display = 'none';
|
||
var est = document.getElementById('extEstimate');
|
||
if(est) est.style.display = 'block';
|
||
var bar = document.getElementById('extProgressBar');
|
||
if(bar) bar.style.width = '0%';
|
||
}
|
||
|
||
function selectAllGatherFiles() {
|
||
files.forEach(f => { if (f.name.endsWith('.h5')) gatherSelectedFiles.add(f.name); });
|
||
updateGatherSelCount();
|
||
filterFiles();
|
||
}
|
||
|
||
function selectGatherLine(lineVal) {
|
||
if (!lineVal) return;
|
||
const line = parseInt(lineVal);
|
||
const boardIds = new Set();
|
||
Object.entries(darfNodes).forEach(([bid, n]) => {
|
||
if (n.line === line) boardIds.add(bid);
|
||
});
|
||
files.forEach(f => {
|
||
const bid = extractBoardIdJs(f.name);
|
||
if (bid && boardIds.has(bid)) gatherSelectedFiles.add(f.name);
|
||
});
|
||
updateGatherSelCount();
|
||
filterFiles();
|
||
}
|
||
|
||
function toggleGatherFile(name, checked) {
|
||
if (checked) gatherSelectedFiles.add(name);
|
||
else gatherSelectedFiles.delete(name);
|
||
updateGatherSelCount();
|
||
}
|
||
|
||
function updateExtEstimate() {
|
||
var channels = document.getElementById('extChannels').value;
|
||
var scope = document.getElementById('extScope').value;
|
||
var duration = parseFloat(document.getElementById('extDuration').value) || 60;
|
||
|
||
var nCh = 8;
|
||
if (channels === 'all_cal' || channels === 'all_raw') nCh = 4;
|
||
else if (channels.startsWith('cal_') || channels.startsWith('raw_')) nCh = 1;
|
||
|
||
var nFiles = gatherSelectedFiles.size;
|
||
if (scope === 'all_lines') nFiles = files.filter(f=>f.name.endsWith('.h5')).length;
|
||
|
||
var samples = duration * 500 * nFiles;
|
||
var estMB = (samples * nCh * 10) / 1e6;
|
||
var estZipMB = estMB * 0.3;
|
||
|
||
document.getElementById('extEstimate').innerHTML =
|
||
'<b>' + nFiles + '</b> files × <b>' + nCh + '</b> ch × <b>' + duration + '</b>s @ 500Hz<br>' +
|
||
'Samples: <b>' + (samples).toLocaleString() + '</b> per channel<br>' +
|
||
'Estimated ZIP: ~<b>' + Math.round(estZipMB) + ' MB</b> (CSV ~' + Math.round(estMB) + ' MB uncompressed)<br>' +
|
||
'<span style="color:var(--green)">⚡ Full resolution — zero decimation</span>';
|
||
}
|
||
|
||
function updateGatherSelCount() {
|
||
document.getElementById('gatherSelCount').textContent = gatherSelectedFiles.size + ' selected';
|
||
}
|
||
|
||
function updateGatherSliderMax(maxDur) {
|
||
gatherMaxDuration = Math.floor(maxDur);
|
||
const slider = document.getElementById('gatherTimeSlider');
|
||
slider.max = gatherMaxDuration;
|
||
document.getElementById('gatherSliderMax').textContent = fmtTime(gatherMaxDuration);
|
||
const durEl = document.getElementById('gatherDuration');
|
||
durEl.max = gatherMaxDuration;
|
||
}
|
||
</script>
|
||
|
||
<div id="extractModal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:9999;align-items:center;justify-content:center">
|
||
<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:24px;max-width:480px;width:90%;color:var(--text);box-shadow:0 8px 32px rgba(0,0,0,0.5)">
|
||
<h3 style="margin:0 0 16px;color:var(--accent)">📥 Gather Extract — Full Resolution</h3>
|
||
|
||
<div style="margin-bottom:12px">
|
||
<label style="font-weight:600;font-size:13px;display:block;margin-bottom:4px">📡 Channels</label>
|
||
<select id="extChannels" style="width:100%;padding:6px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px">
|
||
<option value="all8">All 8 channels (4 calibrated + 4 raw ADC)</option>
|
||
<option value="all_cal">4 calibrated channels</option>
|
||
<option value="all_raw">4 raw ADC channels</option>
|
||
<option value="cal_1">Calibrated channel 1 only</option>
|
||
<option value="cal_2">Calibrated channel 2 only</option>
|
||
<option value="cal_3">Calibrated channel 3 only</option>
|
||
<option value="cal_4">Calibrated channel 4 only</option>
|
||
<option value="raw_1">Raw ADC channel 1 only</option>
|
||
<option value="raw_2">Raw ADC channel 2 only</option>
|
||
<option value="raw_3">Raw ADC channel 3 only</option>
|
||
<option value="raw_4">Raw ADC channel 4 only</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="margin-bottom:12px">
|
||
<label style="font-weight:600;font-size:13px;display:block;margin-bottom:4px">📐 Scope</label>
|
||
<select id="extScope" style="width:100%;padding:6px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px">
|
||
<option value="current">Current selection (23 files)</option>
|
||
<option value="line">All files on current line</option>
|
||
<option value="all_lines">All 8 lines (167 files)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="margin-bottom:12px">
|
||
<label style="font-weight:600;font-size:13px;display:block;margin-bottom:4px">⏱ Time Window</label>
|
||
<div style="display:flex;gap:8px">
|
||
<div style="flex:1"><label style="font-size:11px;color:var(--muted)">Start (s)</label><input type="number" id="extStart" style="width:100%;padding:4px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px"></div>
|
||
<div style="flex:1"><label style="font-size:11px;color:var(--muted)">Duration (s)</label><input type="number" id="extDuration" style="width:100%;padding:4px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-bottom:16px">
|
||
<label style="font-weight:600;font-size:13px;display:block;margin-bottom:6px">📎 Include</label>
|
||
<label style="display:block;font-size:12px;margin-bottom:4px;cursor:pointer"><input type="checkbox" id="extIncNodes" checked> Node positions (DARF deployment data)</label>
|
||
<label style="display:block;font-size:12px;margin-bottom:4px;cursor:pointer"><input type="checkbox" id="extIncShots" checked> SPS shot points (matching time window)</label>
|
||
<label style="display:block;font-size:12px;cursor:pointer"><input type="checkbox" id="extIncMeta" checked> Metadata JSON</label>
|
||
</div>
|
||
|
||
<div id="extEstimate" style="background:var(--bg);padding:8px;border-radius:4px;font-size:12px;margin-bottom:16px;color:var(--muted)">
|
||
Estimating...
|
||
</div>
|
||
|
||
<!-- Progress Bar -->
|
||
<div id="extProgressContainer" style="display:none;margin-bottom:16px">
|
||
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:4px">
|
||
<span id="extStatusText">Preparing...</span>
|
||
<span id="extPercentText">0%</span>
|
||
</div>
|
||
<div style="height:6px;background:var(--bg);border-radius:3px;overflow:hidden">
|
||
<div id="extProgressBar" style="width:0%;height:100%;background:var(--green);transition:width 0.3s"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||
<button id="extCancelBtn" onclick="closeExtractModal()" style="padding:8px 16px;background:var(--surface);border:1px solid var(--border);color:var(--text);border-radius:4px;cursor:pointer">Cancel</button>
|
||
<button id="extDownloadBtn" onclick="doExtract()" style="padding:8px 16px;background:var(--accent);border:none;color:#fff;border-radius:4px;cursor:pointer;font-weight:600">📥 Download ZIP</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|