// client.js // UI: Buttons, Anzeige von Result als JSON + Baum, Fallback für Commands function appendLog(line) { const el = document.getElementById("log"); if (!el) return; const now = new Date().toISOString(); el.value += `[${now}] ${line}\n`; el.scrollTop = el.scrollHeight; } function clearTextarea(id) { const el = document.getElementById(id); if (el) el.value = ""; } function clearElement(id) { const el = document.getElementById(id); if (el) el.innerHTML = ""; } function formatScalar(value) { if (value === null) return "null"; if (value === undefined) return "undefined"; if (typeof value === "string") return JSON.stringify(value); if (typeof value === "number" && Number.isFinite(value)) return String(value); if (typeof value === "boolean") return String(value); return String(value); } function renderTree(container, value, key = "result", open = true) { if (!container) return; container.innerHTML = ""; container.appendChild(renderNode(key, value, open)); } function renderNode(key, value, open = false) { const isObject = value !== null && typeof value === "object"; if (!isObject) { const leaf = document.createElement("div"); leaf.className = "tree-leaf"; leaf.textContent = `${key}: ${formatScalar(value)}`; return leaf; } const details = document.createElement("details"); details.open = open; const summary = document.createElement("summary"); summary.textContent = Array.isArray(value) ? `${key} [${value.length}]` : key; details.appendChild(summary); const body = document.createElement("div"); body.style.marginLeft = "16px"; if (Array.isArray(value)) { value.forEach((item, idx) => { body.appendChild(renderNode(String(idx), item, false)); }); } else { Object.entries(value).forEach(([childKey, childVal]) => { body.appendChild(renderNode(childKey, childVal, false)); }); } details.appendChild(body); return details; } function renderResult(result) { const jsonEl = document.getElementById("result-json"); const treeEl = document.getElementById("result-tree"); if (jsonEl) { jsonEl.value = JSON.stringify(result, null, 2); } renderTree(treeEl, result, "result", true); } dataCache = null headersCache = null rowsCache = null timeCache = null jsonCache = null async function fetchCSV(){ if(dataCache == null || headersCache == null || rowsCache == null || timeCache == null){ ({ data: dataCache, headers: headersCache, rows: rowsCache } = await fetchCSV_fromServer()); timeCache = Date.now(); } if (Date.now() - timeCache > 1000){ ({ data: dataCache, headers: headersCache, rows: rowsCache } = await fetchCSV_fromServer()); timeCache = Date.now(); } return {data: dataCache, headers: headersCache, rows: rowsCache} } async function fetchCSV_fromServer() { const res = await fetch("/api/latest-snapshot"); if (!res.ok) throw new Error("Fehler beim Laden des Snapshots"); let data; if (res.headers.get("content-type")?.includes("application/json")) { data = await res.json(); } else { const csvData = await res.text(); data = { filename: "latest.csv", mtime: new Date().toISOString(), content: csvData }; } if (!jsonCache && data.jsonFile) { jsonCache = JSON.parse(data.jsonFile.content); } const lines = data.content.trim().split(/\r?\n/).filter(Boolean); if (lines.length < 2) { throw new Error("Keine oder unvollständige Daten"); } const headers = lines[0].split(",").map(h => h.trim()); const rows = lines.slice(1).map(line => { const cells = line.split(","); const obj = {}; headers.forEach((h, i) => { const raw = (cells[i] ?? "").trim(); const numeric = Number(raw); obj[h] = raw !== "" && Number.isFinite(numeric) ? numeric : raw; }); return obj; }); return { data, headers, rows }; } async function fetchWebcamSnapshotData() { const { data, headers, rows } = await fetchCSV(); return { filename: data.filename, mtime: data.mtime, headers, rows, robotIntrinsics: jsonCache, imageFile: data.imageFile, image2: data.image2 }; } async function sendToBodyTracker({imageFile, image2, robotIntrinsics}) { const response = await fetch('/api/estimate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageFile, image2, robotIntrinsics }) }); if (!response.ok) { const message = await response.text(); throw new Error(`BodyTracker fehlgeschlagen (${response.status}): ${message}`); } return await response.json(); } async function renderSnapshot() { const table = document.getElementById("snapshot-table"); const pictureEl = document.getElementById("snapshot-info-picture"); if (!table) return; try { const { data, headers, rows } = await fetchCSV(); // Info anzeigen const infoEl = document.getElementById("snapshot-info"); if (infoEl) { infoEl.textContent = `Datei: ${data.filename}, Geändert: ${new Date(data.mtime).toLocaleString()}, Zeilen: ${rows.length}`; } // Bild anzeigen, falls vorhanden if (pictureEl) { let imagesHtml = ''; if (data.imageFile) { imagesHtml += `${data.imageFile.filename}`; } if (data.image2) { imagesHtml += `${data.image2.filename}`; } if (imagesHtml) { pictureEl.innerHTML = `
${imagesHtml}
`; } else { pictureEl.innerHTML = ""; } } // Tabelle leeren table.innerHTML = ""; // Header const thead = document.createElement("thead"); const headerRow = document.createElement("tr"); headers.forEach(h => { const th = document.createElement("th"); th.textContent = h; headerRow.appendChild(th); }); thead.appendChild(headerRow); table.appendChild(thead); // Body const tbody = document.createElement("tbody"); rows.forEach(row => { const tr = document.createElement("tr"); headers.forEach(h => { const td = document.createElement("td"); let value = row[h]; if (typeof value === 'number') { if (h === 'id' || h === 'seen_by') { value = Math.round(value); } else { value = value.toFixed(1); } } td.textContent = value; tr.appendChild(td); }); tbody.appendChild(tr); }); table.appendChild(tbody); } catch (err) { console.error("Fehler beim Rendern des Snapshots:", err); } } async function onCalculateClick() { clearTextarea("analysis-log"); clearTextarea("result-json"); clearElement("result-tree"); clearElement("snapshot-table"); appendLog("Starte Berechnung..."); try { const response = await fetch("robot.json"); const robot = await response.json(); const snapshot = await fetchWebcamSnapshotData(); appendLog(`Snapshot geladen: ${snapshot.filename} (${snapshot.rows.length} Zeilen)`); let result = null; try { result = await sendToBodyTracker(snapshot); appendLog("BodyTracker wurde aufgerufen."); } catch (err) { appendLog(`BodyTracker nicht aufgerufen: ${err.message}`); } if (!result) { appendLog("Fallback: lokale Pose-Berechnung mit calculateAngles.js"); result = await window.calculate(snapshot.robotIntrinsics, robot); } renderResult(result); await renderSnapshot(); appendLog("Result angezeigt."); } catch (err) { appendLog(`Fehler: ${err.message}`); } } // ── Foto ───────────────────────────────────────────────────────────────────── /** * Foto-Button: holt HD-JPEGs aller 3 Kameras parallel * und zeigt sie nebeneinander im Snapshot-Bereich an. */ async function onFotoClick() { const display = document.getElementById('snapshot-info-picture'); if (!display) return; const camIds = ['cam0', 'cam1', 'cam2']; const labels = { cam0: 'front', cam1: 'left', cam2: 'right' }; appendLog('Foto: alle Kameras …'); const t = Date.now(); // Wrapper mit 3 Spalten const wrapper = document.createElement('div'); wrapper.style.cssText = 'display:flex; gap:6px; align-items:flex-start;'; camIds.forEach(id => { const col = document.createElement('div'); col.style.cssText = 'flex:1 1 0; min-width:0;'; const caption = document.createElement('div'); caption.style.cssText = 'font-size:0.8em; margin-bottom:3px; color:#aaa;'; caption.textContent = `${id} (${labels[id]})`; const img = document.createElement('img'); img.style.cssText = 'width:100%; height:auto; display:block;'; img.alt = id; img.onload = () => appendLog(` ${id}: ${img.naturalWidth}×${img.naturalHeight}px`); img.onerror = () => appendLog(` ${id}: Fehler`); img.src = `/api/webcam/snapshot/${id}?t=${t}`; col.appendChild(caption); col.appendChild(img); wrapper.appendChild(col); }); display.innerHTML = ''; display.appendChild(wrapper); } async function onCommandClick(btn) { const cmd = btn.dataset.cmd; const payloadSelector = btn.dataset.payload; const payload = payloadSelector ? document.querySelector(payloadSelector)?.value ?? "" : ""; if (typeof window.sendCommand === "function") { try { await window.sendCommand(cmd, payload); appendLog(`Command gesendet: ${cmd}${payload ? " " + payload : ""}`); } catch (err) { appendLog(`Command-Fehler: ${err.message}`); } return; } appendLog(`Command (kein Transport definiert): ${cmd}${payload ? " " + payload : ""}`); } function setupUi() { const calculateBtn = document.getElementById("btn-calculate"); if (calculateBtn) { calculateBtn.addEventListener("click", onCalculateClick); } const fotoBtn = document.getElementById("btn-foto"); if (fotoBtn) { fotoBtn.addEventListener("click", onFotoClick); } document.querySelectorAll("button[data-cmd]").forEach(btn => { if (btn.id === "btn-calculate") return; btn.addEventListener("click", () => onCommandClick(btn)); }); // ===== SECTION COLLAPSE/EXPAND ===== document.querySelectorAll(".section h2").forEach(heading => { heading.addEventListener("click", () => { const section = heading.closest(".section"); if (section) { section.classList.toggle("collapsed"); } }); heading.style.cursor = "pointer"; }); // Auto-expand when content is written to a collapsed section const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === "childList" || mutation.type === "characterData") { let el = mutation.target; while (el && el !== document.body) { const section = el.closest(".section"); if (section && section.classList.contains("collapsed")) { section.classList.remove("collapsed"); break; } el = el.parentElement; } } }); }); // Observer auf alle Elemente in Sections anwenden document.querySelectorAll(".section").forEach(section => { observer.observe(section, { childList: true, subtree: true, characterData: true, characterDataOldValue: false }); }); } window.addEventListener("DOMContentLoaded", setupUi);