feat(viewer): auto-detect API URLs + 9 UX improvements (no-data overlay, tabs, labels, layer toggles map)

This commit is contained in:
Poulpe
2026-04-27 22:36:08 +00:00
parent b21e306a86
commit 7a5d442fbd

View File

@@ -12,8 +12,9 @@
height: 100vh; overflow: hidden; height: 100vh; overflow: hidden;
font-family: monospace; background: #1a1a2e; color: #e0e0e0; font-family: monospace; background: #1a1a2e; color: #e0e0e0;
display: grid; display: grid;
grid-template-rows: 36px 40px minmax(200px, 1fr) 54px 1fr; grid-template-rows: 36px 40px minmax(200px, 1fr) 54px 28px 1fr;
} }
.hidden { display: none !important; }
/* Row 0: datebar */ /* Row 0: datebar */
#datebar { #datebar {
background: #0d0d20; border-bottom: 1px solid #0f3460; background: #0d0d20; border-bottom: 1px solid #0f3460;
@@ -45,7 +46,7 @@
} }
#title { font-size: 13px; font-weight: bold; color: #e94560; white-space: nowrap; } #title { font-size: 13px; font-weight: bold; color: #e94560; white-space: nowrap; }
#stats { font-size: 11px; color: #a0c4ff; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #stats { font-size: 11px; color: #a0c4ff; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#layer-toggles { display: flex; gap: 6px; align-items: center; flex-shrink: 0; } #layer-toggles { display: none; }
.layer-btn { .layer-btn {
font-family: monospace; font-size: 10px; padding: 2px 7px; cursor: pointer; font-family: monospace; font-size: 10px; padding: 2px 7px; cursor: pointer;
border-radius: 2px; border: 1px solid; background: transparent; border-radius: 2px; border: 1px solid; background: transparent;
@@ -60,6 +61,20 @@
/* Row 2: map */ /* Row 2: map */
#map { position: relative; min-height: 0; } #map { position: relative; min-height: 0; }
/* No-data overlay */
#no-data-overlay {
position: absolute; inset: 0; z-index: 2000;
background: rgba(10,10,26,0.82);
display: flex; align-items: center; justify-content: center;
pointer-events: none;
}
#no-data-overlay.hidden { display: none; }
#no-data-overlay .nodata-msg {
font-size: 18px; color: #555; font-family: monospace; text-align: center;
border: 1px solid #1a1a3e; padding: 20px 32px; background: #0a0a1a;
pointer-events: none;
}
/* USBL panel over map */ /* USBL panel over map */
#usbl-panel { #usbl-panel {
position: absolute; top: 10px; left: 50px; z-index: 1000; position: absolute; top: 10px; left: 50px; z-index: 1000;
@@ -73,6 +88,11 @@
display: inline-block; background: #ff8800; color: #1a1a2e; display: inline-block; background: #ff8800; color: #1a1a2e;
font-size: 9px; font-weight: bold; padding: 1px 4px; border-radius: 2px; margin-top: 4px; font-size: 9px; font-weight: bold; padding: 1px 4px; border-radius: 2px; margin-top: 4px;
} }
/* layer toggles bottom-right on map */
#map-layer-toggles {
position: absolute; bottom: 10px; right: 10px; z-index: 1000;
display: flex; flex-direction: column; gap: 4px; align-items: flex-end;
}
/* legend over map */ /* legend over map */
#legend { #legend {
position: absolute; bottom: 10px; left: 10px; z-index: 1000; position: absolute; bottom: 10px; left: 10px; z-index: 1000;
@@ -146,6 +166,21 @@
.chart-wrap .plotly-wrap { flex: 1; min-height: 0; position: relative; } .chart-wrap .plotly-wrap { flex: 1; min-height: 0; position: relative; }
.chart-wrap .plotly-wrap > div { width: 100% !important; height: 100% !important; } .chart-wrap .plotly-wrap > div { width: 100% !important; height: 100% !important; }
/* Tab navigation */
#panels-tabs {
background: #0d0d20; border-top: 1px solid #0f3460; border-bottom: 1px solid #0f3460;
display: flex; gap: 0; flex-shrink: 0;
}
.panel-tab {
font-family: monospace; font-size: 11px; padding: 6px 16px; cursor: pointer;
border: none; border-right: 1px solid #0f3460; background: transparent;
color: #666;
}
.panel-tab.active { background: #16213e; color: #e0e0e0; }
.panel-tab:hover:not(.active) { background: #0f3460; color: #a0c4ff; }
.panel-section { display: none; }
.panel-section.active { display: block; }
/* Pipeline overlay */ /* Pipeline overlay */
#pipeline-overlay { #pipeline-overlay {
display: none; display: none;
@@ -218,8 +253,8 @@
<div id="datebar"> <div id="datebar">
<input type="date" id="date-picker"> <input type="date" id="date-picker">
<button id="btn-today" onclick="datePickerToday()">Aujourd'hui</button> <button id="btn-today" onclick="datePickerToday()">Aujourd'hui</button>
<span id="mission-label" class="no-data">Chargement...</span> <span id="mission-label" class="no-data"></span>
<span id="load-status"></span> <span id="load-status" style="font-size:10px;color:#888;font-style:italic;flex:1;"></span>
<button id="btn-pipeline" onclick="togglePipeline()">Pipeline</button> <button id="btn-pipeline" onclick="togglePipeline()">Pipeline</button>
</div> </div>
@@ -272,20 +307,24 @@ flowchart LR
<!-- Row 1: Header --> <!-- Row 1: Header -->
<div id="header"> <div id="header">
<span id="title">COSMA NAV v6</span> <span id="title">COSMA NAV v6</span>
<span id="stats">Chargement...</span> <span id="stats" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:#a0c4ff;"></span>
<div id="layer-toggles"> <select id="sortie-select"><option value="">Chargement Gdrive (~30s)...</option></select>
<button class="layer-btn active" id="btn-usv" onclick="toggleLayer('usv')">USV</button> <span id="mission-label-header" style="font-size:11px;font-family:monospace;padding:2px 8px;color:#555;white-space:nowrap;"></span>
<button class="layer-btn active" id="btn-auv" onclick="toggleLayer('auv')">AUV</button>
<button class="layer-btn active" id="btn-vec" onclick="toggleLayer('vec')">USBL vec</button>
<button class="layer-btn active" id="btn-usbl-panel" onclick="toggleLayer('panel')">Stats</button>
</div>
<select id="sortie-select"><option value="">— Sortie —</option></select>
<button id="btn-sync" disabled>Sync &amp; Process</button> <button id="btn-sync" disabled>Sync &amp; Process</button>
<span id="sync-progress"></span> <span id="sync-progress"></span>
</div> </div>
<!-- Row 2: Map --> <!-- Row 2: Map -->
<div id="map"> <div id="map">
<div id="no-data-overlay">
<div class="nodata-msg">⬇ Sélectionnez une sortie pour charger les données</div>
</div>
<div id="map-layer-toggles">
<button class="layer-btn active" id="btn-usv" onclick="toggleLayer('usv')">⛵ USV</button>
<button class="layer-btn active" id="btn-auv" onclick="toggleLayer('auv')">🛥 AUV</button>
<button class="layer-btn active" id="btn-vec" onclick="toggleLayer('vec')">USBL vec</button>
<button class="layer-btn active" id="btn-usbl-panel" onclick="toggleLayer('panel')">Stats</button>
</div>
<div id="legend"></div> <div id="legend"></div>
<div id="usbl-panel"> <div id="usbl-panel">
<div class="uprow"><span class="uplabel">Dist</span><span class="upval" id="up-dist"></span></div> <div class="uprow"><span class="uplabel">Dist</span><span class="upval" id="up-dist"></span></div>
@@ -299,10 +338,10 @@ flowchart LR
<!-- Row 3: Controls --> <!-- Row 3: Controls -->
<div id="controls"> <div id="controls">
<div id="ctrl-row1"> <div id="ctrl-row1">
<span id="ctrl-label">t</span> <span id="ctrl-label" style="font-size:10px;color:#666;white-space:nowrap;">Heure:</span>
<div id="cursor-slider-wrap"><div id="cursor-slider"></div></div> <div id="cursor-slider-wrap"><div id="cursor-slider"></div></div>
<span id="cursor-time"></span> <span id="cursor-time" style="font-size:11px;color:#e0e0e0;white-space:nowrap;min-width:70px;"></span>
<label for="trail-select">trail</label> <label for="trail-select" style="font-size:10px;color:#666;white-space:nowrap;">Trail:</label>
<select id="trail-select"> <select id="trail-select">
<option value="10000">10s</option> <option value="10000">10s</option>
<option value="30000">30s</option> <option value="30000">30s</option>
@@ -314,7 +353,7 @@ flowchart LR
</select> </select>
<button id="btn-viewall" onclick="viewAll()">View all</button> <button id="btn-viewall" onclick="viewAll()">View all</button>
<button id="btn-play"></button> <button id="btn-play"></button>
<label for="window-select" style="font-size:10px;color:#666;white-space:nowrap;">win</label> <label for="window-select" style="font-size:10px;color:#666;white-space:nowrap;">Window:</label>
<select id="window-select" style="background:#0f3460;border:1px solid #a855f7;color:#a855f7;font-family:monospace;font-size:11px;padding:2px 6px;border-radius:2px;cursor:pointer;"> <select id="window-select" style="background:#0f3460;border:1px solid #a855f7;color:#a855f7;font-family:monospace;font-size:11px;padding:2px 6px;border-radius:2px;cursor:pointer;">
<option value="10000">10s</option> <option value="10000">10s</option>
<option value="30000">30s</option> <option value="30000">30s</option>
@@ -329,8 +368,15 @@ flowchart LR
</div> </div>
</div> </div>
<!-- Row 4: 2×2 Plotly charts --> <!-- Row 4: tabs + panels -->
<div id="panels-tabs">
<button class="panel-tab active" onclick="switchTab('charts')">🗺 Charts globaux</button>
<button class="panel-tab" onclick="switchTab('usv')">⛵ USV</button>
<button class="panel-tab" onclick="switchTab('auv')">🛥 AUV <span id="auv-tabs"></span></button>
</div>
<div id="graphs-section"> <div id="graphs-section">
<!-- Tab: Charts globaux -->
<div id="panel-charts" class="panel-section active">
<div id="charts-4grid"> <div id="charts-4grid">
<div class="chart-wrap"> <div class="chart-wrap">
<div class="chart-title">Depth AUV (m)</div> <div class="chart-title">Depth AUV (m)</div>
@@ -349,7 +395,10 @@ flowchart LR
<div class="plotly-wrap"><div id="chart-usbl"></div></div> <div class="plotly-wrap"><div id="chart-usbl"></div></div>
</div> </div>
</div> </div>
<div class="panel-header">USV</div> </div>
<!-- Tab: USV -->
<div id="panel-usv" class="panel-section hidden">
<div class="panel-header">⛵ USV</div>
<div class="graphs-grid" id="usv-graphs"> <div class="graphs-grid" id="usv-graphs">
<div class="graph-cell" id="usv-yaw"></div> <div class="graph-cell" id="usv-yaw"></div>
<div class="graph-cell" id="usv-heading"></div> <div class="graph-cell" id="usv-heading"></div>
@@ -360,10 +409,10 @@ flowchart LR
<div class="graph-cell wide" id="usv-motors"></div> <div class="graph-cell wide" id="usv-motors"></div>
<div class="graph-cell wide" id="usv-status"></div> <div class="graph-cell wide" id="usv-status"></div>
</div> </div>
<div class="panel-header" id="auv-panel-header">
AUV
<span id="auv-tabs"></span>
</div> </div>
<!-- Tab: AUV -->
<div id="panel-auv" class="panel-section hidden">
<div class="panel-header" id="auv-panel-header">🛥 AUV</div>
<div class="graphs-grid" id="auv-graphs"> <div class="graphs-grid" id="auv-graphs">
<div class="graph-cell" id="auv-pry"></div> <div class="graph-cell" id="auv-pry"></div>
<div class="graph-cell" id="auv-depth"></div> <div class="graph-cell" id="auv-depth"></div>
@@ -376,6 +425,7 @@ flowchart LR
<div class="graph-cell wide" id="auv-motors"></div> <div class="graph-cell wide" id="auv-motors"></div>
</div> </div>
</div> </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
@@ -383,8 +433,12 @@ flowchart LR
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script> <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
<script> <script>
// == Constants == // == Constants ==
const API = 'http://192.168.0.83:8766'; const API = (location.hostname === 'laboratoire.freeboxos.fr')
const API2 = 'http://192.168.0.83:8767'; ? '/cosma-pK8j876lkj-api'
: 'http://192.168.0.83:8766';
const API2 = (location.hostname === 'laboratoire.freeboxos.fr')
? '/cosma-pK8j876lkj-pipe'
: 'http://192.168.0.83:8767';
const COLORS = ['#00b4d8','#06d6a0','#ffd166','#e94560','#a855f7','#ff8800','#7dc8e0','#b8f0d4']; const COLORS = ['#00b4d8','#06d6a0','#ffd166','#e94560','#a855f7','#ff8800','#7dc8e0','#b8f0d4'];
const AUV_COLOR = '#ff8800'; const AUV_COLOR = '#ff8800';
const PLOTLY_LAYOUT = { const PLOTLY_LAYOUT = {
@@ -629,7 +683,10 @@ function initCursorSlider() {
}); });
cursorSlider.on('update', (values) => { cursorSlider.on('update', (values) => {
tNow = Math.round(+values[0]); tNow = Math.round(+values[0]);
document.getElementById('cursor-time').textContent = fmtMs(tNow); // Show HH:MM:SS for slider label
const d = new Date(tNow);
const hms = d.toISOString().slice(11,19);
document.getElementById('cursor-time').textContent = `Heure: ${hms}`;
applyTrailAndCursor(); applyTrailAndCursor();
}); });
document.getElementById('trail-select').addEventListener('change', () => applyTrailAndCursor()); document.getElementById('trail-select').addEventListener('change', () => applyTrailAndCursor());
@@ -670,7 +727,8 @@ function clearMapLayers() {
// == Load data for a given date == // == Load data for a given date ==
async function loadDate(date) { async function loadDate(date) {
setStatus('Chargement...'); setStatus('chargement...');
showNoDataOverlay(true);
clearMapLayers(); clearMapLayers();
allPoints = []; allPoints = [];
usblPoints = []; usblPoints = [];
@@ -760,6 +818,7 @@ async function loadDate(date) {
populatePlotlyCharts(); populatePlotlyCharts();
initCursorSlider(); initCursorSlider();
applyTrailAndCursor(); applyTrailAndCursor();
showNoDataOverlay(false);
document.getElementById('title').textContent = `COSMA v6 — ${date}`; document.getElementById('title').textContent = `COSMA v6 — ${date}`;
setStatus(`${totalShip} USV sess. | ${totalSub} AUV sess. | ${allPoints.length} pts USV | ${usblPoints.length} pts USBL`); setStatus(`${totalShip} USV sess. | ${totalSub} AUV sess. | ${allPoints.length} pts USV | ${usblPoints.length} pts USBL`);
@@ -940,6 +999,28 @@ function datePickerToday() {
loadDate(today); loadDate(today);
} }
// == Tab switching ==
function switchTab(name) {
['charts','usv','auv'].forEach(t => {
const p = document.getElementById('panel-'+t);
if (p) p.classList.toggle('active', t === name);
if (p) p.classList.toggle('hidden', t !== name);
});
document.querySelectorAll('.panel-tab').forEach((btn, i) => {
const names = ['charts','usv','auv'];
btn.classList.toggle('active', names[i] === name);
});
}
// == No-data overlay ==
function showNoDataOverlay(show) {
const el = document.getElementById('no-data-overlay');
if (el) el.classList.toggle('hidden', !show);
// Show/hide layer toggles
const lt = document.getElementById('map-layer-toggles');
if (lt) lt.style.display = show ? 'none' : 'flex';
}
// == Pipeline overlay == // == Pipeline overlay ==
let _pipelineRendered = false; let _pipelineRendered = false;
function togglePipeline() { function togglePipeline() {
@@ -1138,6 +1219,7 @@ function _wireSortieSelect() {
sel.addEventListener('change', () => { sel.addEventListener('change', () => {
const btn = document.getElementById('btn-sync'); const btn = document.getElementById('btn-sync');
btn.disabled = !sel.value; btn.disabled = !sel.value;
_updateMissionLabelHeader(sel.value);
if (sel.value) { if (sel.value) {
const opt = sel.options[sel.selectedIndex]; const opt = sel.options[sel.selectedIndex];
if (opt.textContent.includes('✓')) { if (opt.textContent.includes('✓')) {
@@ -1155,7 +1237,7 @@ function _populateSortieSelect(sorties) {
sel.options[0].textContent = '— Aucune sortie —'; sel.options[0].textContent = '— Aucune sortie —';
return; return;
} }
sel.options[0].textContent = '— Sortie —'; sel.options[0].textContent = `${sorties.length} sorties dispo — sélectionnez`;
sorties.forEach(s => { sorties.forEach(s => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = s.id; opt.value = s.id;
@@ -1165,6 +1247,13 @@ function _populateSortieSelect(sorties) {
_wireSortieSelect(); _wireSortieSelect();
} }
function _updateMissionLabelHeader(sortieId) {
const el = document.getElementById('mission-label-header');
if (!el) return;
el.textContent = sortieId ? `Mission: ${sortieId}` : '';
el.style.color = sortieId ? '#06d6a0' : '#555';
}
function loadSorties() { function loadSorties() {
const sel = document.getElementById('sortie-select'); const sel = document.getElementById('sortie-select');
sel.options[0].textContent = 'Sorties: chargement…'; sel.options[0].textContent = 'Sorties: chargement…';