feat(viewer): v4 time-series graphs (depth, PWM USV/AUV, status)
This commit is contained in:
@@ -2,13 +2,13 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>COSMA — NAV Viewer v3</title>
|
||||
<title>COSMA — NAV Viewer v4</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/15.7.1/nouislider.min.css"/>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: monospace; background: #1a1a2e; color: #e0e0e0; display: flex; flex-direction: column; height: 100vh; }
|
||||
body { font-family: monospace; background: #1a1a2e; color: #e0e0e0; display: flex; flex-direction: column; min-height: 100vh; }
|
||||
#map { flex: 1; min-height: 0; }
|
||||
#controls {
|
||||
background: #16213e;
|
||||
@@ -89,7 +89,13 @@
|
||||
#btn-auv { color: #ff8800; border-color: #ff8800; }
|
||||
#btn-vec { color: #888; border-color: #888; }
|
||||
#btn-usbl-panel { color: #aaa; border-color: #444; }
|
||||
#graphs-section { background:#12122a; border-top:1px solid #0f3460; overflow-y:auto; max-height:40vh; flex-shrink:0; }
|
||||
.chart-container { padding:6px 12px 4px; border-bottom:1px solid #0f3460; }
|
||||
.chart-container canvas { display:block; }
|
||||
.chart-title { font-size:10px; color:#a0c4ff; margin-bottom:2px; font-family:monospace; }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3/dist/chartjs-plugin-annotation.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
@@ -198,6 +204,77 @@ let trackLayers = [];
|
||||
let auvTrackLayer = null;
|
||||
let windowPoints = [];
|
||||
let usblWindow = [];
|
||||
|
||||
// == Graph state ==
|
||||
let charts = {};
|
||||
let mcapSignals = null;
|
||||
let usvPwm = null;
|
||||
|
||||
const PWM_COLORS_AUV = ['#ff8800','#ffd166','#06d6a0','#00b4d8','#a855f7','#f97316','#e94560','#88c0d0'];
|
||||
const PWM_COLORS_USV = ['#00b4d8','#0096b4','#0077a0','#005f8c','#ffd166','#e6b800','#c49a00','#a07d00'];
|
||||
|
||||
function makeChartOptions() {
|
||||
return {
|
||||
animation: false, parsing: false, responsive: true, maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false, labels: { color:'#a0c4ff', font:{size:9,family:'monospace'}, boxWidth:12 } },
|
||||
annotation: { annotations: {} }
|
||||
},
|
||||
scales: {
|
||||
x: { type:'linear', ticks:{ color:'#666', font:{size:9,family:'monospace'}, maxTicksLimit:8,
|
||||
callback:(v)=>new Date(v).toISOString().substr(11,8) }, grid:{color:'#1a1a3e'} },
|
||||
y: { ticks:{ color:'#666', font:{size:9,family:'monospace'}, maxTicksLimit:5 }, grid:{color:'#1a1a3e'} },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
const mkDs = (lbl, col) => ({ label:lbl, data:[], borderColor:col, borderWidth:1.5, pointRadius:0, tension:0 });
|
||||
charts.depth = new Chart(document.getElementById('chart-depth'), { type:'line', data:{datasets:[mkDs('depth','#06d6a0')]}, options:makeChartOptions() });
|
||||
charts.pwmAuv = new Chart(document.getElementById('chart-pwm-auv'), { type:'line', data:{datasets:[]}, options:makeChartOptions() });
|
||||
charts.pwmUsv = new Chart(document.getElementById('chart-pwm-usv'), { type:'line', data:{datasets:[]}, options:makeChartOptions() });
|
||||
charts.usbl = new Chart(document.getElementById('chart-usbl'), { type:'line', data:{datasets:[mkDs('dist','#a855f7')]}, options:makeChartOptions() });
|
||||
}
|
||||
|
||||
function updateGraphCursor(t_ms) {
|
||||
const ann = { type:'line', xMin:t_ms, xMax:t_ms, borderColor:'#e94560', borderWidth:1.5, borderDash:[4,2] };
|
||||
for (const c of Object.values(charts)) { c.options.plugins.annotation.annotations = {cursor:ann}; c.update('none'); }
|
||||
}
|
||||
|
||||
function updateGraphWindow(t0, t1) {
|
||||
for (const c of Object.values(charts)) { c.options.scales.x.min=t0; c.options.scales.x.max=t1; c.update('none'); }
|
||||
}
|
||||
|
||||
function populateCharts() {
|
||||
if (mcapSignals) {
|
||||
if (mcapSignals.depth) {
|
||||
charts.depth.data.datasets[0].data = mcapSignals.depth.map(p=>({x:p.t,y:p.v}));
|
||||
charts.depth.update('none');
|
||||
}
|
||||
if (mcapSignals.pwm_auv) {
|
||||
const {channels,samples} = mcapSignals.pwm_auv;
|
||||
charts.pwmAuv.data.datasets = channels.map((ch,i)=>({
|
||||
label:'Ch'+ch, data:samples.map(s=>({x:s.t,y:s.v[i]})),
|
||||
borderColor:PWM_COLORS_AUV[i%8], borderWidth:1, pointRadius:0, tension:0
|
||||
}));
|
||||
charts.pwmAuv.options.plugins.legend.display = channels.length > 1;
|
||||
charts.pwmAuv.update('none');
|
||||
}
|
||||
}
|
||||
if (usvPwm && usvPwm.M) {
|
||||
const keys = Object.keys(usvPwm.M).sort();
|
||||
charts.pwmUsv.data.datasets = keys.map((k,i)=>({
|
||||
label:k, data:usvPwm.M[k].map(p=>({x:p.t,y:p.v})),
|
||||
borderColor:PWM_COLORS_USV[i%8], borderWidth:1, pointRadius:0, tension:0
|
||||
}));
|
||||
charts.pwmUsv.options.plugins.legend.display = keys.length > 1;
|
||||
charts.pwmUsv.update('none');
|
||||
}
|
||||
if (usblPoints && usblPoints.length) {
|
||||
charts.usbl.data.datasets[0].data = usblPoints.map(p=>({x:p.t_ms, y:p.dist!==undefined?p.dist:p.distance}));
|
||||
charts.usbl.update('none');
|
||||
}
|
||||
}
|
||||
let cursorMarker = null;
|
||||
let auvMarker = null;
|
||||
let usblVector = null;
|
||||
@@ -469,7 +546,7 @@ async function loadData() {
|
||||
if (tMin === tMax) tMax = tMin + 1000;
|
||||
|
||||
document.getElementById('title').textContent =
|
||||
`COSMA v3 — USV ${manifest.n_sessions} sess. ${manifest.n_points_sampled} pts | AUV ${usblMeta.n_points} pts`;
|
||||
`COSMA v4 — USV ${manifest.n_sessions} sess. ${manifest.n_points_sampled} pts | AUV ${usblMeta.n_points} pts`;
|
||||
|
||||
buildLegend();
|
||||
initSliders();
|
||||
@@ -483,7 +560,31 @@ async function loadData() {
|
||||
}
|
||||
}
|
||||
|
||||
loadData();
|
||||
async function loadGraphData() {
|
||||
initCharts();
|
||||
try {
|
||||
const [mcapResp, usvPwmResp] = await Promise.allSettled([
|
||||
fetch('data/mcap_signals.json'),
|
||||
fetch('data/usv_pwm.json'),
|
||||
]);
|
||||
if (mcapResp.status === 'fulfilled' && mcapResp.value.ok) {
|
||||
mcapSignals = await mcapResp.value.json();
|
||||
}
|
||||
if (usvPwmResp.status === 'fulfilled' && usvPwmResp.value.ok) {
|
||||
usvPwm = await usvPwmResp.value.json();
|
||||
}
|
||||
populateCharts();
|
||||
} catch (e) { console.warn('Graph data load error:', e); }
|
||||
}
|
||||
|
||||
loadData().then(() => { populateCharts(); });
|
||||
loadGraphData();
|
||||
</script>
|
||||
<div id="graphs-section">
|
||||
<div class="chart-container"><div class="chart-title">Profondeur AUV (m)</div><canvas id="chart-depth" height="80"></canvas></div>
|
||||
<div class="chart-container"><div class="chart-title">PWM AUV (canaux)</div><canvas id="chart-pwm-auv" height="80"></canvas></div>
|
||||
<div class="chart-container"><div class="chart-title">PWM USV (M1-M8)</div><canvas id="chart-pwm-usv" height="80"></canvas></div>
|
||||
<div class="chart-container"><div class="chart-title">USBL Distance (m)</div><canvas id="chart-usbl" height="60"></canvas></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user