feat: viewer split 3D+2D — AUV depth chart + USBL XY fix

This commit is contained in:
Floppyrj45
2026-04-24 20:56:17 +02:00
parent 36c7889db1
commit 279e70a5e0
3 changed files with 159 additions and 66 deletions

View File

@@ -37,26 +37,48 @@ def _load_data() -> dict:
if "auv_mcap" in f: if "auv_mcap" in f:
dep = f["auv_mcap/depth_m"][:] dep = f["auv_mcap/depth_m"][:]
t_auv = f["auv_mcap/t_ns"][:].tolist() t_ns_auv = f["auv_mcap/t_ns"][:]
# AUV lat/lon zeros — plot depth over time as Z only alt = f["auv_mcap/altitude_m"][:] if "auv_mcap/altitude_m" in f \
n_pts = len(dep) else np.full(len(dep), np.nan)
out["auv_depth"] = { step = max(1, len(dep) // 600)
"x": list(range(n_pts)), # index as proxy for time t_s = ((t_ns_auv[::step].astype(float) - float(t_ns_auv[0])) / 1e9).tolist()
"y": [0.0] * n_pts, out["auv_profile"] = {
"z": (-dep).tolist(), # positive = deeper "t_s": t_s,
"t": t_auv, "depth_m": dep[::step].tolist(),
"altitude_m": alt[::step].tolist(),
} }
if "usbl_fixes" in f: if "usbl_fixes" in f and "usv_gps" in f:
north = f["usbl_fixes/north_m"][:] bearing = f["usbl_fixes/bearing_deg"][:]
east = f["usbl_fixes/east_m"][:] range_m = f["usbl_fixes/range_m"][:]
depth = f["usbl_fixes/depth_m"][:] usbl_dep = f["usbl_fixes/depth_m"][:]
valid = ~(np.isnan(north) | (north == 0) & (east == 0)) 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(): if valid.any():
out["usbl"] = { t_f = t_usbl[valid].astype(float)
"x": east[valid].tolist(), t_usv = usv_t_raw.astype(float)
"y": north[valid].tolist(), usv_ei = np.interp(t_f, t_usv, usv_e_raw)
"z": (-depth[valid]).tolist(), 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: except Exception as e:
out["error_fixes"] = str(e) out["error_fixes"] = str(e)

View File

@@ -2,10 +2,11 @@ import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ── Renderer ────────────────────────────────────────────────────────────────── // ── Renderer ──────────────────────────────────────────────────────────────────
const viewerEl = document.getElementById('viewer');
const renderer = new THREE.WebGLRenderer({ antialias: true }); const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight); renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
document.body.appendChild(renderer.domElement); viewerEl.appendChild(renderer.domElement);
// ── Scene ───────────────────────────────────────────────────────────────────── // ── Scene ─────────────────────────────────────────────────────────────────────
const scene = new THREE.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)); scene.add(new THREE.AxesHelper(5));
// ── Camera + Controls ───────────────────────────────────────────────────────── // ── 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); camera.position.set(0, 150, 250);
const controls = new OrbitControls(camera, renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; 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)])); addLayer('usv_gps', makePoints(x, y, z, i => RTK_COLORS[Math.min(rtk[i], 2)]));
} }
if (d.auv_depth) { if (d.auv_usbl) {
const { x, y, z } = d.auv_depth; const { x, y, z } = d.auv_usbl;
addLayer('auv_depth', makeLine(x, y, z, 0x44ffaa)); addLayer('auv_usbl', makePoints(x, y, z, () => [0.27, 1.0, 0.67]));
}
if (d.usbl) {
const { x, y, z } = d.usbl;
addLayer('usbl', makePoints(x, y, z, () => [1, 0.42, 0.21]));
} }
const trajData = d.fused || d.lingbot_local; const trajData = d.fused || d.lingbot_local;
@@ -149,6 +145,60 @@ fetch('/api/trajectory')
camera.position.set(cx, 50, -cy + 80); camera.position.set(cx, 50, -cy + 80);
controls.update(); 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 => { .catch(err => {
document.getElementById('status').textContent = `Error: ${err}`; document.getElementById('status').textContent = `Error: ${err}`;
@@ -164,9 +214,9 @@ document.querySelectorAll('[data-layer]').forEach(cb => {
// ── Resize ──────────────────────────────────────────────────────────────────── // ── Resize ────────────────────────────────────────────────────────────────────
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight; camera.aspect = viewerEl.clientWidth / viewerEl.clientHeight;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight); renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight);
}); });
// ── Render loop ─────────────────────────────────────────────────────────────── // ── Render loop ───────────────────────────────────────────────────────────────

View File

@@ -5,51 +5,71 @@
<title>COSMA NAV — Trajectory Viewer</title> <title>COSMA NAV — Trajectory Viewer</title>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #06060f; color: #ccc; font-family: 'Courier New', monospace; overflow: hidden; } body { background: #06060f; color: #ccc; font-family: 'Courier New', monospace;
canvas { display: block; } display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
#viewer { position: relative; flex: 0 0 62vh; }
#viewer canvas { display: block; width: 100% !important; height: 100% !important; }
#legend { #legend {
position: fixed; top: 16px; left: 16px; position: absolute; top: 14px; left: 14px;
background: rgba(4,4,16,0.85); padding: 14px 18px; background: rgba(4,4,16,0.88); padding: 12px 16px;
border-radius: 8px; border: 1px solid #223; border-radius: 8px; border: 1px solid #223;
font-size: 12px; min-width: 180px; backdrop-filter: blur(4px); font-size: 11px; min-width: 170px; backdrop-filter: blur(4px); z-index: 10;
}
#legend h3 { color: #8af; margin-bottom: 10px; font-size: 13px; letter-spacing: 1px; }
#legend label { display: flex; align-items: center; gap: 8px; cursor: pointer; margin: 5px 0; user-select: none; }
.swatch { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
#info {
position: fixed; bottom: 16px; left: 16px;
background: rgba(4,4,16,0.85); padding: 8px 14px;
border-radius: 6px; font-size: 11px; color: #89a;
backdrop-filter: blur(4px); border: 1px solid #223;
} }
#legend h3 { color: #8af; margin-bottom: 8px; font-size: 12px; letter-spacing: 1px; }
#legend label { display: flex; align-items: center; gap: 7px; cursor: pointer; margin: 4px 0; user-select: none; }
.swatch { width: 9px; height: 9px; border-radius: 2px; flex-shrink: 0; }
#status { #status {
position: fixed; top: 16px; right: 16px; position: absolute; top: 14px; right: 14px;
background: rgba(4,4,16,0.85); padding: 8px 14px; background: rgba(4,4,16,0.88); padding: 6px 12px;
border-radius: 6px; font-size: 11px; color: #67d; border-radius: 6px; font-size: 10px; color: #67d;
backdrop-filter: blur(4px); border: 1px solid #223; backdrop-filter: blur(4px); border: 1px solid #223; z-index: 10;
} }
#info {
position: absolute; bottom: 8px; left: 14px;
background: rgba(4,4,16,0.75); padding: 5px 10px;
border-radius: 5px; font-size: 10px; color: #89a; z-index: 10;
}
#chart-panel {
flex: 1; background: #08080f; border-top: 1px solid #1a1a2e;
padding: 10px 16px 8px; display: flex; flex-direction: column;
}
#chart-title {
font-size: 10px; color: #556; letter-spacing: 1px; text-transform: uppercase;
margin-bottom: 6px;
}
#depth-chart { flex: 1; width: 100%; }
</style> </style>
</head> </head>
<body> <body>
<div id="legend">
<h3>COSMA NAV</h3> <div id="viewer">
<label><input type="checkbox" checked data-layer="usv_gps"> <div id="legend">
<span class="swatch" style="background:#3a9fff"></span>USV GPS</label> <h3>COSMA NAV</h3>
<label><input type="checkbox" checked data-layer="auv_depth"> <label><input type="checkbox" checked data-layer="usv_gps">
<span class="swatch" style="background:#44ffaa"></span>AUV depth</label> <span class="swatch" style="background:#3a9fff"></span>USV GPS</label>
<label><input type="checkbox" checked data-layer="usbl"> <label><input type="checkbox" checked data-layer="auv_usbl">
<span class="swatch" style="background:#ff6b35"></span>USBL fixes</label> <span class="swatch" style="background:#44ffaa"></span>AUV (USBL)</label>
<label><input type="checkbox" checked data-layer="fused"> <label><input type="checkbox" checked data-layer="fused">
<span class="swatch" style="background:#ffffff"></span>Fused world</label> <span class="swatch" style="background:#ffffff"></span>Fused world</label>
<label><input type="checkbox" checked data-layer="lingbot_local"> <label><input type="checkbox" checked data-layer="lingbot_local">
<span class="swatch" style="background:#ffd700"></span>lingbot local</label> <span class="swatch" style="background:#ffd700"></span>lingbot local</label>
<label><input type="checkbox" checked data-layer="frustums"> <label><input type="checkbox" checked data-layer="frustums">
<span class="swatch" style="background:#ffd70066"></span>Frustums</label> <span class="swatch" style="background:#ffd70066"></span>Frustums</label>
<label><input type="checkbox" checked data-layer="ply"> <label><input type="checkbox" checked data-layer="ply">
<span class="swatch" style="background:#556"></span>Point cloud</label> <span class="swatch" style="background:#556"></span>Point cloud</label>
</div>
<div id="status">Loading&hellip;</div>
<div id="info">Orbit: drag &middot; Zoom: scroll &middot; Pan: right-drag</div>
</div>
<div id="chart-panel">
<div id="chart-title">AUV — Depth &amp; Altitude Profile</div>
<canvas id="depth-chart"></canvas>
</div> </div>
<div id="status">Loading&hellip;</div>
<div id="info">Orbit: drag &middot; Zoom: scroll &middot; Pan: right-drag</div>
<script type="importmap"> <script type="importmap">
{"imports":{ {"imports":{
@@ -57,6 +77,7 @@
"three/addons/":"https://cdn.jsdelivr.net/npm/three@0.160/examples/jsm/" "three/addons/":"https://cdn.jsdelivr.net/npm/three@0.160/examples/jsm/"
}} }}
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4/dist/chart.umd.min.js"></script>
<script type="module" src="/static/js/scene.js"></script> <script type="module" src="/static/js/scene.js"></script>
</body> </body>
</html> </html>