From 279e70a5e0a6e84f26f0582ad43e0ab9d4face8b Mon Sep 17 00:00:00 2001 From: Floppyrj45 Date: Fri, 24 Apr 2026 20:56:17 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20viewer=20split=203D+2D=20=E2=80=94=20AU?= =?UTF-8?q?V=20depth=20chart=20+=20USBL=20XY=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- viz/server.py | 56 ++++++++++++++++------- viz/static/js/scene.js | 76 +++++++++++++++++++++++++------ viz/static/trajectory.html | 93 +++++++++++++++++++++++--------------- 3 files changed, 159 insertions(+), 66 deletions(-) diff --git a/viz/server.py b/viz/server.py index 7899c3a..faa292a 100644 --- a/viz/server.py +++ b/viz/server.py @@ -37,26 +37,48 @@ def _load_data() -> dict: if "auv_mcap" in f: dep = f["auv_mcap/depth_m"][:] - t_auv = f["auv_mcap/t_ns"][:].tolist() - # AUV lat/lon zeros — plot depth over time as Z only - n_pts = len(dep) - out["auv_depth"] = { - "x": list(range(n_pts)), # index as proxy for time - "y": [0.0] * n_pts, - "z": (-dep).tolist(), # positive = deeper - "t": t_auv, + t_ns_auv = f["auv_mcap/t_ns"][:] + alt = f["auv_mcap/altitude_m"][:] if "auv_mcap/altitude_m" in f \ + else np.full(len(dep), np.nan) + step = max(1, len(dep) // 600) + t_s = ((t_ns_auv[::step].astype(float) - float(t_ns_auv[0])) / 1e9).tolist() + out["auv_profile"] = { + "t_s": t_s, + "depth_m": dep[::step].tolist(), + "altitude_m": alt[::step].tolist(), } - if "usbl_fixes" in f: - north = f["usbl_fixes/north_m"][:] - east = f["usbl_fixes/east_m"][:] - depth = f["usbl_fixes/depth_m"][:] - valid = ~(np.isnan(north) | (north == 0) & (east == 0)) + if "usbl_fixes" in f and "usv_gps" in f: + bearing = f["usbl_fixes/bearing_deg"][:] + range_m = f["usbl_fixes/range_m"][:] + usbl_dep = f["usbl_fixes/depth_m"][:] + t_usbl = f["usbl_fixes/t_ns"][:] + usv_e_raw = f["usv_gps/easting"][:] + usv_n_raw = f["usv_gps/northing"][:] + usv_t_raw = f["usv_gps/t_ns"][:] + hdg_raw = f["usv_gps/heading_deg"][:] \ + if "usv_gps/heading_deg" in f \ + else np.full(len(usv_t_raw), np.nan) + + valid = ~(np.isnan(bearing) | np.isnan(range_m) | np.isnan(usbl_dep)) if valid.any(): - out["usbl"] = { - "x": east[valid].tolist(), - "y": north[valid].tolist(), - "z": (-depth[valid]).tolist(), + t_f = t_usbl[valid].astype(float) + t_usv = usv_t_raw.astype(float) + usv_ei = np.interp(t_f, t_usv, usv_e_raw) + usv_ni = np.interp(t_f, t_usv, usv_n_raw) + hdg_i = np.interp(t_f, t_usv, hdg_raw) \ + if not np.all(np.isnan(hdg_raw)) \ + else np.zeros(valid.sum()) + range_h = np.sqrt(np.maximum(0.0, range_m[valid]**2 - usbl_dep[valid]**2)) + angle = np.deg2rad(hdg_i + bearing[valid]) + auv_e = usv_ei + range_h * np.sin(angle) + auv_n = usv_ni + range_h * np.cos(angle) + ce = out.get("origin", {}).get("easting", 0.0) + cn = out.get("origin", {}).get("northing", 0.0) + out["auv_usbl"] = { + "x": (auv_e - ce).tolist(), + "y": (auv_n - cn).tolist(), + "z": (-usbl_dep[valid]).tolist(), } except Exception as e: out["error_fixes"] = str(e) diff --git a/viz/static/js/scene.js b/viz/static/js/scene.js index 69bbe5f..000bb5e 100644 --- a/viz/static/js/scene.js +++ b/viz/static/js/scene.js @@ -2,10 +2,11 @@ import * as THREE from 'three'; 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(innerWidth, innerHeight); -document.body.appendChild(renderer.domElement); +renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight); +viewerEl.appendChild(renderer.domElement); // ── Scene ───────────────────────────────────────────────────────────────────── const scene = new THREE.Scene(); @@ -15,7 +16,7 @@ scene.add(new THREE.GridHelper(1000, 100, 0x111133, 0x0a0a22)); scene.add(new THREE.AxesHelper(5)); // ── Camera + Controls ───────────────────────────────────────────────────────── -const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 5000); +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; @@ -101,14 +102,9 @@ fetch('/api/trajectory') addLayer('usv_gps', makePoints(x, y, z, i => RTK_COLORS[Math.min(rtk[i], 2)])); } - if (d.auv_depth) { - const { x, y, z } = d.auv_depth; - addLayer('auv_depth', makeLine(x, y, z, 0x44ffaa)); - } - - if (d.usbl) { - const { x, y, z } = d.usbl; - addLayer('usbl', makePoints(x, y, z, () => [1, 0.42, 0.21])); + 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; @@ -149,6 +145,60 @@ fetch('/api/trajectory') camera.position.set(cx, 50, -cy + 80); controls.update(); } + + // 2D depth/altitude chart + 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); + const datasets = [ + { + label: 'Depth (m)', + data: t_s.map((t, i) => ({ x: t, y: -depth_m[i] })), + borderColor: '#3a9fff', + backgroundColor: 'rgba(58,159,255,0.08)', + borderWidth: 1.5, + pointRadius: 0, + fill: true, + }, + ]; + if (hasAlt) { + datasets.push({ + label: 'Altitude above seafloor (m)', + data: t_s.map((t, i) => ({ x: t, y: altitude_m[i] })), + borderColor: '#44ffaa', + backgroundColor: 'transparent', + borderWidth: 1.5, + pointRadius: 0, + }); + } + new Chart(document.getElementById('depth-chart'), { + type: 'line', + data: { datasets }, + options: { + animation: false, + responsive: true, + maintainAspectRatio: false, + parsing: false, + plugins: { + legend: { labels: { color: '#889', font: { size: 10, family: 'Courier New' } } }, + tooltip: { enabled: false }, + }, + scales: { + x: { + type: 'linear', + title: { display: true, text: 'Time (s)', color: '#445' }, + ticks: { color: '#445', font: { size: 9 } }, + grid: { color: '#111' }, + }, + y: { + title: { display: true, text: 'm', color: '#445' }, + ticks: { color: '#445', font: { size: 9 } }, + grid: { color: '#111' }, + }, + }, + }, + }); + } }) .catch(err => { document.getElementById('status').textContent = `Error: ${err}`; @@ -164,9 +214,9 @@ document.querySelectorAll('[data-layer]').forEach(cb => { // ── Resize ──────────────────────────────────────────────────────────────────── window.addEventListener('resize', () => { - camera.aspect = innerWidth / innerHeight; + camera.aspect = viewerEl.clientWidth / viewerEl.clientHeight; camera.updateProjectionMatrix(); - renderer.setSize(innerWidth, innerHeight); + renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight); }); // ── Render loop ─────────────────────────────────────────────────────────────── diff --git a/viz/static/trajectory.html b/viz/static/trajectory.html index 6a2ed75..aa400ab 100644 --- a/viz/static/trajectory.html +++ b/viz/static/trajectory.html @@ -5,51 +5,71 @@ COSMA NAV — Trajectory Viewer -
-

COSMA NAV

- - - - - - - + +
+
+

COSMA NAV

+ + + + + + +
+
Loading…
+
Orbit: drag · Zoom: scroll · Pan: right-drag
+
+ +
+
AUV — Depth & Altitude Profile
+
-
Loading…
-
Orbit: drag · Zoom: scroll · Pan: right-drag
+