diff --git a/viz/static/js/scene.js b/viz/static/js/scene.js
index 9275ec3..365b7e7 100644
--- a/viz/static/js/scene.js
+++ b/viz/static/js/scene.js
@@ -3,29 +3,33 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ── Renderer ──────────────────────────────────────────────────────────────────
const viewerEl = document.getElementById('viewer');
-const renderer = new THREE.WebGLRenderer({ antialias: true });
-renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
-renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
-viewerEl.appendChild(renderer.domElement);
+let renderer, scene, camera, controls, webglOk = false;
+try {
+ renderer = new THREE.WebGLRenderer({ antialias: true });
+ renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
+ renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
+ viewerEl.appendChild(renderer.domElement);
+ webglOk = true;
+} catch(e) {
+ viewerEl.innerHTML = '
WebGL indisponible — rechargez la page ou fermez les autres onglets.
';
+}
// ── Scene ─────────────────────────────────────────────────────────────────────
-const scene = new THREE.Scene();
-scene.background = new THREE.Color(0x06060f);
-scene.fog = new THREE.Fog(0x06060f, 500, 2000);
-scene.add(new THREE.GridHelper(1000, 100, 0x111133, 0x0a0a22));
-scene.add(new THREE.AxesHelper(5));
-
-// ── Camera + Controls ─────────────────────────────────────────────────────────
-const camera = new THREE.PerspectiveCamera(60, viewerEl.clientWidth / viewerEl.clientHeight, 0.1, 5000);
-camera.position.set(0, 150, 250);
-const controls = new OrbitControls(camera, renderer.domElement);
-controls.enableDamping = true;
-controls.dampingFactor = 0.08;
-controls.minDistance = 1;
-controls.maxDistance = 3000;
-
-// ── Layer registry ────────────────────────────────────────────────────────────
const layers = {};
+if (webglOk) {
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x06060f);
+ scene.fog = new THREE.Fog(0x06060f, 500, 2000);
+ scene.add(new THREE.GridHelper(1000, 100, 0x111133, 0x0a0a22));
+ scene.add(new THREE.AxesHelper(5));
+ camera = new THREE.PerspectiveCamera(60, viewerEl.clientWidth / viewerEl.clientHeight, 0.1, 5000);
+ camera.position.set(0, 150, 250);
+ controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ controls.dampingFactor = 0.08;
+ controls.minDistance = 1;
+ controls.maxDistance = 3000;
+}
function addLayer(name, obj) {
layers[name] = layers[name] || [];
@@ -96,79 +100,88 @@ fetch('/api/trajectory')
document.getElementById('status').textContent =
`traj: ${d.traj_status || 'n/a'}`;
- if (d.usv_gps) {
- const { x, y, z, rtk } = d.usv_gps;
- addLayer('usv_gps', makeLine(x, y, z, 0x3a9fff));
- addLayer('usv_gps', makePoints(x, y, z, i => RTK_COLORS[Math.min(rtk[i], 2)]));
- }
+ if (webglOk) {
+ if (d.usv_gps) {
+ const { x, y, z, rtk } = d.usv_gps;
+ addLayer('usv_gps', makeLine(x, y, z, 0x3a9fff));
+ addLayer('usv_gps', makePoints(x, y, z, i => RTK_COLORS[Math.min(rtk[i], 2)]));
+ }
- if (d.auv_usbl) {
- const { x, y, z } = d.auv_usbl;
- addLayer('auv_usbl', makePoints(x, y, z, () => [0.27, 1.0, 0.67]));
- }
+ if (d.auv_usbl) {
+ const { x, y, z } = d.auv_usbl;
+ addLayer('auv_usbl', makePoints(x, y, z, () => [0.27, 1.0, 0.67]));
+ }
- const trajData = d.fused || d.lingbot_local;
- const trajLayer = d.fused ? 'fused' : 'lingbot_local';
- if (trajData) {
- const { x, y, z, T_4x4 } = trajData;
- addLayer(trajLayer, makeLine(x, y, z, d.fused ? 0xffffff : 0xffd700));
+ const trajData = d.fused || d.lingbot_local;
+ const trajLayer = d.fused ? 'fused' : 'lingbot_local';
+ if (trajData) {
+ const { x, y, z, T_4x4 } = trajData;
+ addLayer(trajLayer, makeLine(x, y, z, d.fused ? 0xffffff : 0xffd700));
- if (T_4x4 && T_4x4.length > 0) {
- const step = Math.max(1, Math.floor(T_4x4.length / 100)); // max 100 frustums
- for (let i = 0; i < T_4x4.length; i += step) {
- addLayer('frustums', makeFrustum(T_4x4[i]));
+ if (T_4x4 && T_4x4.length > 0) {
+ const step = Math.max(1, Math.floor(T_4x4.length / 100));
+ for (let i = 0; i < T_4x4.length; i += step) {
+ addLayer('frustums', makeFrustum(T_4x4[i]));
+ }
+ }
+
+ if (x.length > 0) {
+ controls.target.set(x[0], z[0], -y[0]);
+ camera.position.set(x[0], z[0] + 50, -y[0] + 100);
+ controls.update();
}
}
- // Center camera
- if (x.length > 0) {
- controls.target.set(x[0], z[0], -y[0]);
- camera.position.set(x[0], z[0] + 50, -y[0] + 100);
+ if (d.ply) {
+ const { x, y, z } = d.ply;
+ addLayer('ply', makePoints(x, y, z, (i) => {
+ const t = Math.min(1, Math.max(0, (-z[i]) / 20));
+ return [0.2+0.3*t, 0.35+0.2*t, 0.5-0.2*t];
+ }));
+ }
+
+ if (!trajData && d.usv_gps) {
+ const { x, y, z } = d.usv_gps;
+ const cx = x.reduce((a,b)=>a+b,0)/x.length;
+ const cy = y.reduce((a,b)=>a+b,0)/y.length;
+ controls.target.set(cx, 0, -cy);
+ camera.position.set(cx, 50, -cy + 80);
controls.update();
}
}
- if (d.ply) {
- const { x, y, z } = d.ply;
- addLayer('ply', makePoints(x, y, z, (i) => {
- const t = Math.min(1, Math.max(0, (-z[i]) / 20));
- return [0.2+0.3*t, 0.35+0.2*t, 0.5-0.2*t];
- }));
- }
-
- // If only USV GPS, center on that
- if (!trajData && d.usv_gps) {
- const { x, y, z } = d.usv_gps;
- const cx = x.reduce((a,b)=>a+b,0)/x.length;
- const cy = y.reduce((a,b)=>a+b,0)/y.length;
- controls.target.set(cx, 0, -cy);
- camera.position.set(cx, 50, -cy + 80);
- controls.update();
- }
-
- // 2D depth/altitude chart
+ // 2D cross-section chart: surface=0, AUV below (negative), seafloor even lower
if (d.auv_profile) {
const { t_s, depth_m, altitude_m } = d.auv_profile;
- const hasAlt = altitude_m && altitude_m.some(v => !isNaN(v) && v !== null);
+ // depth_m from HDF5 is already negative (below surface)
+ // altitude_m is positive (above seafloor), filter zeros (Kogger not yet reading)
+ const ALT_MIN = 0.3;
const datasets = [
{
- label: 'Depth (m)',
- data: t_s.map((t, i) => ({ x: t, y: -depth_m[i] })),
+ label: 'AUV (m sous surface)',
+ data: t_s.map((t, i) => ({ x: t, y: depth_m[i] })),
borderColor: '#3a9fff',
- backgroundColor: 'rgba(58,159,255,0.08)',
- borderWidth: 1.5,
+ backgroundColor: 'rgba(58,159,255,0.10)',
+ borderWidth: 2,
pointRadius: 0,
- fill: true,
+ fill: { target: 'origin', above: 'rgba(58,159,255,0.05)' },
},
];
+ const hasAlt = altitude_m && altitude_m.some(v => v > ALT_MIN);
if (hasAlt) {
datasets.push({
- label: 'Altitude above seafloor (m)',
- data: t_s.map((t, i) => ({ x: t, y: altitude_m[i] })),
+ label: 'Fond marin (m sous surface)',
+ // seafloor = AUV depth - altitude above seafloor; null where altitude unreliable
+ data: t_s.map((t, i) => ({
+ x: t,
+ y: (altitude_m[i] > ALT_MIN) ? depth_m[i] - altitude_m[i] : null,
+ })),
borderColor: '#44ffaa',
- backgroundColor: 'transparent',
+ backgroundColor: 'rgba(68,255,170,0.06)',
borderWidth: 1.5,
pointRadius: 0,
+ spanGaps: false,
+ fill: 'end',
});
}
Chart.defaults.color = '#556';
@@ -182,19 +195,20 @@ fetch('/api/trajectory')
maintainAspectRatio: false,
parsing: false,
plugins: {
- legend: { labels: { color: '#889', font: { size: 10, family: 'Courier New' } } },
+ legend: { labels: { color: '#889', font: { size: 10, family: 'Courier New' }, boxWidth: 20 } },
tooltip: { enabled: false },
},
scales: {
x: {
type: 'linear',
- title: { display: true, text: 'Time (s)', color: '#445' },
+ title: { display: true, text: 'Temps (s)', color: '#445' },
ticks: { color: '#445', font: { size: 9 } },
grid: { color: '#111' },
},
y: {
- suggestedMin: 0,
- title: { display: true, text: 'm', color: '#445' },
+ reverse: false,
+ suggestedMax: 0,
+ title: { display: true, text: 'profondeur (m)', color: '#445' },
ticks: { color: '#445', font: { size: 9 } },
grid: { color: '#111' },
},
@@ -216,15 +230,17 @@ document.querySelectorAll('[data-layer]').forEach(cb => {
});
// ── Resize ────────────────────────────────────────────────────────────────────
-window.addEventListener('resize', () => {
- camera.aspect = viewerEl.clientWidth / viewerEl.clientHeight;
- camera.updateProjectionMatrix();
- renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
-});
+if (webglOk) {
+ window.addEventListener('resize', () => {
+ camera.aspect = viewerEl.clientWidth / viewerEl.clientHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
+ });
-// ── Render loop ───────────────────────────────────────────────────────────────
-(function animate() {
- requestAnimationFrame(animate);
- controls.update();
- renderer.render(scene, camera);
-})();
+ // ── Render loop ─────────────────────────────────────────────────────────────
+ (function animate() {
+ requestAnimationFrame(animate);
+ controls.update();
+ renderer.render(scene, camera);
+ })();
+}