feat(viewer): auto-detect API URLs + 9 UX improvements (no-data overlay, tabs, labels, layer toggles map)
This commit is contained in:
@@ -12,8 +12,9 @@
|
||||
height: 100vh; overflow: hidden;
|
||||
font-family: monospace; background: #1a1a2e; color: #e0e0e0;
|
||||
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 */
|
||||
#datebar {
|
||||
background: #0d0d20; border-bottom: 1px solid #0f3460;
|
||||
@@ -45,7 +46,7 @@
|
||||
}
|
||||
#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; }
|
||||
#layer-toggles { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
|
||||
#layer-toggles { display: none; }
|
||||
.layer-btn {
|
||||
font-family: monospace; font-size: 10px; padding: 2px 7px; cursor: pointer;
|
||||
border-radius: 2px; border: 1px solid; background: transparent;
|
||||
@@ -60,6 +61,20 @@
|
||||
/* Row 2: map */
|
||||
#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 {
|
||||
position: absolute; top: 10px; left: 50px; z-index: 1000;
|
||||
@@ -73,6 +88,11 @@
|
||||
display: inline-block; background: #ff8800; color: #1a1a2e;
|
||||
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 {
|
||||
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 > 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 {
|
||||
display: none;
|
||||
@@ -218,8 +253,8 @@
|
||||
<div id="datebar">
|
||||
<input type="date" id="date-picker">
|
||||
<button id="btn-today" onclick="datePickerToday()">Aujourd'hui</button>
|
||||
<span id="mission-label" class="no-data">Chargement...</span>
|
||||
<span id="load-status"></span>
|
||||
<span id="mission-label" class="no-data">—</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>
|
||||
</div>
|
||||
|
||||
@@ -272,20 +307,24 @@ flowchart LR
|
||||
<!-- Row 1: Header -->
|
||||
<div id="header">
|
||||
<span id="title">COSMA NAV v6</span>
|
||||
<span id="stats">Chargement...</span>
|
||||
<div id="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>
|
||||
<select id="sortie-select"><option value="">— Sortie —</option></select>
|
||||
<span id="stats" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:#a0c4ff;"></span>
|
||||
<select id="sortie-select"><option value="">Chargement Gdrive (~30s)...</option></select>
|
||||
<span id="mission-label-header" style="font-size:11px;font-family:monospace;padding:2px 8px;color:#555;white-space:nowrap;"></span>
|
||||
<button id="btn-sync" disabled>Sync & Process</button>
|
||||
<span id="sync-progress"></span>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: 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="usbl-panel">
|
||||
<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 -->
|
||||
<div id="controls">
|
||||
<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>
|
||||
<span id="cursor-time">—</span>
|
||||
<label for="trail-select">trail</label>
|
||||
<span id="cursor-time" style="font-size:11px;color:#e0e0e0;white-space:nowrap;min-width:70px;">—</span>
|
||||
<label for="trail-select" style="font-size:10px;color:#666;white-space:nowrap;">Trail:</label>
|
||||
<select id="trail-select">
|
||||
<option value="10000">10s</option>
|
||||
<option value="30000">30s</option>
|
||||
@@ -314,7 +353,7 @@ flowchart LR
|
||||
</select>
|
||||
<button id="btn-viewall" onclick="viewAll()">View all</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;">
|
||||
<option value="10000">10s</option>
|
||||
<option value="30000">30s</option>
|
||||
@@ -329,8 +368,15 @@ flowchart LR
|
||||
</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">
|
||||
<!-- Tab: Charts globaux -->
|
||||
<div id="panel-charts" class="panel-section active">
|
||||
<div id="charts-4grid">
|
||||
<div class="chart-wrap">
|
||||
<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>
|
||||
</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="graph-cell" id="usv-yaw"></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-status"></div>
|
||||
</div>
|
||||
<div class="panel-header" id="auv-panel-header">
|
||||
AUV
|
||||
<span id="auv-tabs"></span>
|
||||
</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="graph-cell" id="auv-pry"></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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@@ -383,8 +433,12 @@ flowchart LR
|
||||
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
|
||||
<script>
|
||||
// == Constants ==
|
||||
const API = 'http://192.168.0.83:8766';
|
||||
const API2 = 'http://192.168.0.83:8767';
|
||||
const API = (location.hostname === 'laboratoire.freeboxos.fr')
|
||||
? '/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 AUV_COLOR = '#ff8800';
|
||||
const PLOTLY_LAYOUT = {
|
||||
@@ -629,7 +683,10 @@ function initCursorSlider() {
|
||||
});
|
||||
cursorSlider.on('update', (values) => {
|
||||
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();
|
||||
});
|
||||
document.getElementById('trail-select').addEventListener('change', () => applyTrailAndCursor());
|
||||
@@ -670,7 +727,8 @@ function clearMapLayers() {
|
||||
|
||||
// == Load data for a given date ==
|
||||
async function loadDate(date) {
|
||||
setStatus('Chargement...');
|
||||
setStatus('chargement...');
|
||||
showNoDataOverlay(true);
|
||||
clearMapLayers();
|
||||
allPoints = [];
|
||||
usblPoints = [];
|
||||
@@ -760,6 +818,7 @@ async function loadDate(date) {
|
||||
populatePlotlyCharts();
|
||||
initCursorSlider();
|
||||
applyTrailAndCursor();
|
||||
showNoDataOverlay(false);
|
||||
|
||||
document.getElementById('title').textContent = `COSMA v6 — ${date}`;
|
||||
setStatus(`${totalShip} USV sess. | ${totalSub} AUV sess. | ${allPoints.length} pts USV | ${usblPoints.length} pts USBL`);
|
||||
@@ -940,6 +999,28 @@ function datePickerToday() {
|
||||
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 ==
|
||||
let _pipelineRendered = false;
|
||||
function togglePipeline() {
|
||||
@@ -1138,6 +1219,7 @@ function _wireSortieSelect() {
|
||||
sel.addEventListener('change', () => {
|
||||
const btn = document.getElementById('btn-sync');
|
||||
btn.disabled = !sel.value;
|
||||
_updateMissionLabelHeader(sel.value);
|
||||
if (sel.value) {
|
||||
const opt = sel.options[sel.selectedIndex];
|
||||
if (opt.textContent.includes('✓')) {
|
||||
@@ -1155,7 +1237,7 @@ function _populateSortieSelect(sorties) {
|
||||
sel.options[0].textContent = '— Aucune sortie —';
|
||||
return;
|
||||
}
|
||||
sel.options[0].textContent = '— Sortie —';
|
||||
sel.options[0].textContent = `${sorties.length} sorties dispo — sélectionnez`;
|
||||
sorties.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id;
|
||||
@@ -1165,6 +1247,13 @@ function _populateSortieSelect(sorties) {
|
||||
_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() {
|
||||
const sel = document.getElementById('sortie-select');
|
||||
sel.options[0].textContent = 'Sorties: chargement…';
|
||||
|
||||
Reference in New Issue
Block a user