/* calibration.js – Kalibrierungs-Frontend */ // ── Panel-Loading (lazy) ─────────────────────────────────────────────────────── const _panelLoaded = new Set(); async function loadPanel(tab, src) { if (_panelLoaded.has(tab)) return; try { const r = await fetch(src); if (!r.ok) throw new Error(`HTTP ${r.status}`); document.getElementById('tab-' + tab).innerHTML = await r.text(); _panelLoaded.add(tab); // h2-Klick → Section ein-/ausklappen document.getElementById('tab-' + tab).querySelectorAll('.section h2').forEach(h2 => { h2.addEventListener('click', () => h2.closest('.section').classList.toggle('collapsed')); }); // Tab-spezifische Initialisierung if (tab === 'camera-npz') initCameraNpz(); else if (tab === 'board') initBoard(); else if (tab === 'robot-x-axis') initXAxis(); else if (tab === 'arm1') initArm('arm1'); else if (tab === 'marker') initMarker(); } catch (err) { document.getElementById('tab-' + tab).innerHTML = `
Panel konnte nicht geladen werden: ${err}
`; } } // ── Tab-Switching ───────────────────────────────────────────────────────────── document.getElementById('tabSidebar').addEventListener('click', e => { const btn = e.target.closest('.tab-btn'); if (!btn) return; document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); btn.classList.add('active'); document.getElementById('tab-' + btn.dataset.tab).classList.add('active'); loadPanel(btn.dataset.tab, btn.dataset.src); }); // Erstes Tab sofort laden (function () { const first = document.querySelector('.tab-btn.active'); if (first) loadPanel(first.dataset.tab, first.dataset.src); })(); // ── Shared: SSE-Stream lesen ────────────────────────────────────────────────── async function readSseStream(response, logFn, onDone) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split('\n\n'); buffer = parts.pop(); // unvollständiges letztes Fragment behalten for (const part of parts) { for (const line of part.split('\n')) { if (!line.startsWith('data: ')) continue; try { const evt = JSON.parse(line.slice(6)); if (evt.type === 'log') { if (evt.text !== '') logFn(evt.text); } else if (evt.type === 'done') { onDone(evt); } } catch { /* ungültiges JSON überspringen */ } } } } } // ── Camera NPZ ──────────────────────────────────────────────────────────────── function initCameraNpz() { const logCamera = document.getElementById('log-camera'); function logC(msg) { const ts = new Date().toLocaleTimeString('de-CH'); logCamera.value += `[${ts}] ${msg}\n`; logCamera.scrollTop = logCamera.scrollHeight; } function formatDate(isoString) { if (!isoString) return '–'; return new Date(isoString).toLocaleString('de-CH'); } function updateCalibInfo(meta) { const sel = document.getElementById('cam-select-calib'); if (!meta) { document.getElementById('info-timestamp').textContent = '(keine Session vorhanden)'; document.getElementById('info-created').textContent = '–'; document.getElementById('info-images').textContent = '–'; sel.innerHTML = ''; return; } document.getElementById('info-timestamp').textContent = meta.timestamp ?? '–'; document.getElementById('info-created').textContent = formatDate(meta.createdAt); const cameras = meta.cameras ?? []; const imgTxt = meta.imageCount != null ? `${meta.imageCount} Bilder total. ${cameras.length} Kamera(s) verwendet.` : '–'; document.getElementById('info-images').textContent = imgTxt; const prev = sel.value; sel.innerHTML = ''; for (const cam of cameras) { const opt = document.createElement('option'); opt.value = cam; opt.textContent = cam; if (cam === prev) opt.selected = true; sel.appendChild(opt); } if (cameras.length === 1) sel.value = cameras[0]; } async function loadCalibCurrent() { try { const r = await fetch('/api/calibration/current'); const d = await r.json(); updateCalibInfo(d.meta); if (d.session) logC(`Session geladen: ${d.session}`); else logC('Noch keine Kalibrierungs-Session vorhanden.'); } catch (err) { logC(`Fehler beim Laden: ${err}`); } } async function safeJson(r) { const raw = await r.text().catch(() => ''); try { return { ok: r.ok, status: r.status, data: JSON.parse(raw) }; } catch { return { ok: r.ok, status: r.status, data: null, raw: raw.slice(0, 300) }; } } // Aktuelle Session laden loadCalibCurrent(); // "Neue Kalibrierung anlegen" document.getElementById('btn-new-calib').addEventListener('click', async () => { logC('Neue Kalibrierung wird angelegt …'); try { const r = await fetch('/api/calibration/new', { method: 'POST' }); const d = await r.json(); if (d.error) { logC(`Fehler: ${d.error}`); return; } updateCalibInfo(d.meta); if (d.warning) logC(`Warnung: ${d.warning}`); else logC(`Session angelegt: ${d.session} | Fotos: ${(d.savedFiles ?? []).join(', ')}`); } catch (err) { logC(`Fehler: ${err}`); } }); // "Foto aufnehmen" document.getElementById('btn-foto-calib').addEventListener('click', async () => { logC('Fotos werden aufgenommen …'); try { const r = await fetch('/api/calibration/foto', { method: 'POST' }); const d = await r.json(); if (d.error) { logC(`Fehler: ${d.error}`); return; } updateCalibInfo(d.meta); logC(`Gespeichert: ${(d.savedFiles ?? []).join(', ')}`); } catch (err) { logC(`Fehler: ${err}`); } }); // "Kalibrierung berechnen" – SSE document.getElementById('btn-compute-calib').addEventListener('click', async () => { const camera = document.getElementById('cam-select-calib').value; if (!camera) { logC('⚠ Bitte zuerst eine Kamera auswählen.'); return; } logC(`Starte Kalibrierung für ${camera} …`); const btn = document.getElementById('btn-compute-calib'); btn.disabled = true; try { const response = await fetch('/api/calibration/compute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ camera }), }); if (!response.ok) { const raw = await response.text().catch(() => ''); let msg; try { msg = JSON.parse(raw).error || raw; } catch { msg = raw.slice(0, 300) || response.statusText; } logC(`❌ HTTP ${response.status}: ${msg || '(kein Fehlertext – evtl. Server neu starten?)'}`); return; } await readSseStream(response, logC, (evt) => { if (evt.exitCode === 0) logC('✅ Kalibrierung abgeschlossen.'); else logC(`❌ Script beendet mit Exit-Code ${evt.exitCode}`); }); } catch (err) { logC(`Fehler: ${err}`); } finally { btn.disabled = false; } }); // "NPZ speichern" → an Webcam-Service übertragen document.getElementById('btn-upload-npz').addEventListener('click', async () => { const camera = document.getElementById('cam-select-calib').value; if (!camera) { logC('⚠ Bitte zuerst eine Kamera auswählen.'); return; } logC(`NPZ wird an Webcam-Service übertragen (${camera}) …`); try { const { ok, status, data, raw } = await safeJson(await fetch('/api/calibration/upload-npz', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ camera }), })); if (!ok || data?.error) { logC(`❌ HTTP ${status}: ${data?.error ?? raw ?? '(kein Fehlertext)'}`); return; } logC(`✅ Gespeichert: ${data.webcam?.saved} (${data.size} Bytes)`); logC(` calibrationUrl: ${data.webcam?.calibrationUrl}`); } catch (err) { logC(`❌ Fehler: ${err}`); } }); } // ── Board: Marker-Tabelle ───────────────────────────────────────────────────── async function loadBoardTable() { const wrap = document.getElementById('board-marker-table-wrap'); if (!wrap) return; const th = (a) => `style="text-align:${a};padding:3px 8px;border-bottom:1px solid #2a2d35;white-space:nowrap;background:#1e293b;color:#555b6e;font-weight:normal"`; const td = (a, x = '') => `style="padding:2px 8px;border-bottom:1px solid #111418;text-align:${a};white-space:nowrap;${x}"`; try { const r = await fetch('/api/board/latest'); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await r.json(); if (!data.runDir) { wrap.innerHTML = 'Noch kein Board-Run vorhanden.
'; return; } const meas = data.measuredMarkers; if (!meas?.markers?.length) { wrap.innerHTML = `Run: ${data.runDir} – kein 3b-Output (≥2 Kameras erforderlich).
`; return; } // Modell-Info aus robot.json const boardMarkers = data.robot?.links?.Board?.markers ?? []; const modelMap = {}; for (const m of boardMarkers) modelMap[m.id] = m; const f1 = v => (v == null ? '–' : Number(v).toFixed(1)); const f2 = v => (v == null ? '–' : Number(v).toFixed(2)); const f4 = v => (v == null ? '–' : Number(v).toFixed(4)); const markers = [...meas.markers].sort((a, b) => { if (a.link !== b.link) return a.link === 'Board' ? -1 : 1; return a.marker_id - b.marker_id; }); let html = `Run: ${data.runDir} · ${markers.length} Marker trianguliert
| ID | Set | Link | Kam. | x mm | y mm | z mm | nx | ny | nz | dist mm | Δz mm | Kante mm |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ${m.marker_id} | ${set} | ${m.link} | ${m.num_cameras} | ${f1(px)} | ${f1(py)} | ${f1(pz)} | ${f4(mnx)} | ${f4(mny)} | ${f4(mnz)} | ${dist} | ${dz} | ${f1(m.edge_length_mm)} |
KAMERAS
| ID | x mm | y mm | z mm | dir_x | dir_y | dir_z |
|---|---|---|---|---|---|---|
| ${c.camera_id} | ${f1(cx)} | ${f1(cy)} | ${f1(cz)} | ${f4(dx)} | ${f4(dy)} | ${f4(dz)} |
Fehler: ${err}
`; } } // ── Board ───────────────────────────────────────────────────────────────────── // ── Tab: Robot X Axis ───────────────────────────────────────────────────────── async function populateXAxisSetDropdowns() { let sets = []; try { const r = await fetch('/api/robot/board-sets'); if (r.ok) sets = (await r.json()).sets ?? []; } catch {} const sel = document.getElementById('xaxis-ref-set'); if (sel) { sel.innerHTML = '' + sets.map(s => ``).join(''); } } function initXAxis() { const logEl = document.getElementById('log-xaxis'); function logX(msg) { const ts = new Date().toLocaleTimeString('de-CH'); logEl.value += `[${ts}] ${msg}\n`; logEl.scrollTop = logEl.scrollHeight; } populateXAxisSetDropdowns(); document.getElementById('btn-xaxis-run').addEventListener('click', async () => { const refSet = document.getElementById('xaxis-ref-set')?.value ?? ''; logX(`Board-Erkennung … Referenz: ${refSet || 'alle'}`); const btn = document.getElementById('btn-xaxis-run'); btn.disabled = true; try { const response = await fetch('/api/board/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refSet: refSet || undefined }), }); if (!response.ok) { const raw = await response.text().catch(() => ''); let msg; try { msg = JSON.parse(raw).error || raw; } catch { msg = raw.slice(0, 300) || `HTTP ${response.status}`; } logX(`❌ HTTP ${response.status}: ${msg}`); return; } await readSseStream(response, logX, (evt) => { if (evt.exitCode === 0) { logX('✅ Board-Run abgeschlossen.'); if (evt.runDir) { document.getElementById('xaxis-last-run').textContent = evt.runDir; const frame = document.getElementById('xaxis-viewer-frame'); if (frame?.contentWindow) { frame.contentWindow.postMessage({ type: 'reload' }, '*'); } } } else { logX(`❌ Beendet mit Exit-Code ${evt.exitCode}`); } }); } catch (err) { logX(`❌ Fehler: ${err}`); } finally { btn.disabled = false; } }); // ── X-Achse übernehmen ──────────────────────────────────────────────────── // Empfängt postMessage aus dem eingebetteten boardViewer-iframe. let _xaxisDirection = null; // zuletzt gemessene Richtung [vx,vy,vz] const adoptBtn = document.getElementById('btn-xaxis-adopt'); function onXaxisMessage(e) { // Nachricht muss vom boardViewer-iframe stammen const frame = document.getElementById('xaxis-viewer-frame'); if (!frame || e.source !== frame.contentWindow) return; const msg = e.data; if (!msg || msg.type !== 'xaxis-measurement') return; if (Array.isArray(msg.direction)) { _xaxisDirection = msg.direction; const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(3) + '°'; logX(`📐 Messung empfangen: dir=[${msg.direction.map(v => v.toFixed(4)).join(', ')}]` + ` XY=${fmt(msg.angleXY)} XZ=${fmt(msg.angleXZ)}` + ` (${msg.numMarkers} Marker, Ø${msg.distMm.toFixed(1)} mm)`); if (adoptBtn) { adoptBtn.disabled = false; adoptBtn.style.opacity = '1'; adoptBtn.style.cursor = 'pointer'; adoptBtn.title = `X-Achse übernehmen (dir=[${_xaxisDirection.map(v => v.toFixed(4)).join(', ')}])`; } } else { // Ungültige / zu kleine Bewegung → Button sperren _xaxisDirection = null; if (adoptBtn) { adoptBtn.disabled = true; adoptBtn.style.opacity = '.45'; adoptBtn.style.cursor = 'not-allowed'; adoptBtn.title = 'Noch keine gültige Messung verfügbar'; } } } window.addEventListener('message', onXaxisMessage); if (adoptBtn) { adoptBtn.addEventListener('click', async () => { if (!_xaxisDirection) return; const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(4); logX(`🔄 Übernehme X-Achse: dir=[${_xaxisDirection.map(fmt).join(', ')}] …`); adoptBtn.disabled = true; adoptBtn.style.opacity = '.45'; try { const r = await fetch('/api/robot/adopt-x-axis', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ direction: _xaxisDirection }), }); const data = await r.json(); if (!r.ok) { logX(`❌ Fehler: ${data.error ?? r.status}`); return; } logX(`✅ X-Achse gespeichert — ${data.numChanged} Marker rotiert`); logX(` Ursprung (A0-Schwerpunkt): [${data.origin.join(', ')}] mm`); logX(` Neue X-Achse: [${data.newXAxis.join(', ')}]` + ` Korr. XY=${(data.angleXYdeg >= 0 ? '+' : '') + data.angleXYdeg}°` + ` XZ=${(data.angleXZdeg >= 0 ? '+' : '') + data.angleXZdeg}°`); // Viewer neu laden damit die aktualisierten Positionen sichtbar werden const frame = document.getElementById('xaxis-viewer-frame'); if (frame?.contentWindow) frame.contentWindow.postMessage({ type: 'reload' }, '*'); } catch (err) { logX(`❌ Netzwerkfehler: ${err}`); } finally { adoptBtn.disabled = false; adoptBtn.style.opacity = '1'; } }); } } // ── Tabs: Arm1 / Arm2 / Elbow / Hand (generisch) ──────────────────────────── // Alle Gelenk-Tabs teilen dieselbe Init-Logik – der Tab-Name (arm1, arm2, …) // wird als Prefix für Element-IDs und Viewer-Frame-ID genutzt. function initArm(tab) { const logEl = document.getElementById(`log-${tab}`); const frameEl = document.getElementById(`${tab}-viewer-frame`); const runBtn = document.getElementById(`btn-${tab}-run`); const lastRunEl = document.getElementById(`${tab}-last-run`); if (!logEl) return; // Panel noch nicht geladen function log(msg) { const ts = new Date().toLocaleTimeString('de-CH'); logEl.value += `[${ts}] ${msg}\n`; logEl.scrollTop = logEl.scrollHeight; } // "Board erkennen"-Button if (runBtn) { runBtn.addEventListener('click', async () => { log('Board-Erkennung …'); runBtn.disabled = true; try { const response = await fetch('/api/board/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); if (!response.ok) { const raw = await response.text().catch(() => ''); let msg; try { msg = JSON.parse(raw).error || raw; } catch { msg = raw.slice(0, 300) || `HTTP ${response.status}`; } log(`❌ HTTP ${response.status}: ${msg}`); return; } await readSseStream(response, log, (evt) => { if (evt.exitCode === 0) { log('✅ Board-Run abgeschlossen.'); if (evt.runDir) { if (lastRunEl) lastRunEl.textContent = evt.runDir; if (frameEl?.contentWindow) frameEl.contentWindow.postMessage({ type: 'reload' }, '*'); } } else { log(`❌ Beendet mit Exit-Code ${evt.exitCode}`); } }); } catch (err) { log(`❌ Fehler: ${err}`); } finally { runBtn.disabled = false; } }); } // ── Kalibrierungs-Aktionen (werden nach Rotation-Messung aktiv) ────────── // Tab-Name → Link-Name in robot.json const TAB_TO_LINK = { arm1: 'Arm1', arm2: 'Arm2', elbow: 'Ellbow', hand: 'Hand' }; const robotLink = TAB_TO_LINK[tab] ?? tab; const calibActionsEl = document.getElementById(`${tab}-calib-actions`); const assignFixedBtn = document.getElementById(`btn-${tab}-assign-fixed`); const assignFixedInfo = document.getElementById(`${tab}-assign-fixed-info`); const setOriginBtn = document.getElementById(`btn-${tab}-set-origin`); const setOriginInfo = document.getElementById(`${tab}-set-origin-info`); let _lastYAxisMsg = null; // letztes gültiges yaxis-measurement function enableCalibActions(msg) { if (!calibActionsEl) return; calibActionsEl.style.display = 'block'; // ── Button 1: Fixe Marker → Base ──────────────────────────────────────── if (assignFixedBtn) { const skipped = msg.skipped ?? []; if (skipped.length > 0) { const ids = skipped.map(s => s.id).join(', '); assignFixedBtn.disabled = false; assignFixedBtn.style.opacity = '1'; assignFixedBtn.style.cursor = 'pointer'; assignFixedBtn.title = `Marker ${ids} in robot.json dem Link 'Base' zuordnen`; if (assignFixedInfo) { assignFixedInfo.textContent = `Kaum-bewegende Marker: ${ids} ` + `(Bewegung < ${msg.skipped.map(s => s.maxMoveMm + ' mm').join(', ')}) ` + `→ Link 'Base' in robot.json eintragen.`; } } else { assignFixedBtn.disabled = true; if (assignFixedInfo) assignFixedInfo.textContent = 'Alle erkannten Marker rotieren – kein fixer Marker gefunden.'; } } // ── Button 2: Joint-Origin Y/Z ─────────────────────────────────────────── if (setOriginBtn) { const [, ay, az] = msg.axisPoint; setOriginBtn.disabled = false; setOriginBtn.style.opacity = '1'; setOriginBtn.style.cursor = 'pointer'; setOriginBtn.title = `Joint '${robotLink}': origin[Y]=${ay.toFixed(1)} mm, origin[Z]=${az.toFixed(1)} mm`; if (setOriginInfo) { setOriginInfo.textContent = `Berechnete Achse: Y = ${ay.toFixed(1)} mm · Z = ${az.toFixed(1)} mm ` + `→ in robot.json links.${robotLink}.jointToParent.origin setzen.`; } } } function disableCalibActions() { if (assignFixedBtn) { assignFixedBtn.disabled = true; assignFixedBtn.style.opacity = '.45'; assignFixedBtn.style.cursor = 'not-allowed'; } if (setOriginBtn) { setOriginBtn.disabled = true; setOriginBtn.style.opacity = '.45'; setOriginBtn.style.cursor = 'not-allowed'; } } if (assignFixedBtn) { assignFixedBtn.addEventListener('click', async () => { if (!_lastYAxisMsg) return; const skipped = _lastYAxisMsg.skipped ?? []; const markerIds = skipped.map(s => s.id); const measuredPositions = skipped .filter(s => Array.isArray(s.posA)) .map(s => ({ id: s.id, position_mm: s.posA })); log(`🔄 Ordne Marker [${markerIds.join(', ')}] dem Link 'Base' zu …`); assignFixedBtn.disabled = true; try { const r = await fetch('/api/robot/assign-fixed-markers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ markerIds, targetLink: 'Base', measuredPositions }), }); const data = await r.json(); if (!r.ok) { log(`❌ Fehler: ${data.error ?? r.status}`); return; } log(`✅ Zugeordnet: ${data.numAdded} neu, ${data.numAlreadyPresent} bereits vorhanden`); data.changes.forEach(c => { if (c.action === 'added') log(` + Marker ${c.markerId} → ${c.targetLink}`); else if (c.action === 'already-present') log(` ○ Marker ${c.markerId} bereits in '${c.existingLink}'`); else if (c.action === 'skipped-no-position') log(` ⚠ Marker ${c.markerId}: keine Positions-Daten`); }); } catch (err) { log(`❌ Netzwerkfehler: ${err}`); } finally { assignFixedBtn.disabled = false; } }); } if (setOriginBtn) { setOriginBtn.addEventListener('click', async () => { if (!_lastYAxisMsg) return; const [, ay, az] = _lastYAxisMsg.axisPoint; log(`🔄 Setze ${robotLink}.jointToParent.origin: Y=${ay.toFixed(1)} Z=${az.toFixed(1)} …`); setOriginBtn.disabled = true; try { const r = await fetch('/api/robot/set-joint-origin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ linkName: robotLink, y: ay, z: az }), }); const data = await r.json(); if (!r.ok) { log(`❌ Fehler: ${data.error ?? r.status}`); return; } log(`✅ Joint-Origin gesetzt: [${data.oldOrigin.join(', ')}] → [${data.newOrigin.join(', ')}]`); } catch (err) { log(`❌ Netzwerkfehler: ${err}`); } finally { setOriginBtn.disabled = false; } }); } // ── Achsen-Messung vom Viewer empfangen ─────────────────────────────────── window.addEventListener('message', (e) => { if (!frameEl || e.source !== frameEl.contentWindow) return; const msg = e.data; if (msg?.type !== 'yaxis-measurement') return; if (Array.isArray(msg.axisDir)) { _lastYAxisMsg = msg; const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(3) + '°'; log(`📐 Achse (${msg.numMarkers}/${msg.numMarkersCommon} Marker): dir=[${msg.axisDir.map(v => v.toFixed(4)).join(', ')}]` + ` XY=${fmt(msg.tiltXY)} YZ=${fmt(msg.tiltYZ)}`); log(` Referenzpunkt: [${msg.axisPoint.map(v => v.toFixed(1)).join(', ')}] mm`); if ((msg.skipped ?? []).length) { log(` Gefiltert (zu geringe Bewegung): ${msg.skipped.map(s => `${s.id} (${s.maxMoveMm} mm)`).join(', ')}`); } enableCalibActions(msg); // In rotation_detection.json speichern (anhängen) fetch('/api/xaxis/save-rotation-detection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ axis: { dir: msg.axisDir, referencePoint: msg.axisPoint, tiltXY_deg: msg.tiltXY, tiltYZ_deg: msg.tiltYZ }, runs: { A: msg.runA ?? null, B: msg.runB ?? null, C: msg.runC ?? null }, numMarkers: msg.numMarkers, markers: msg.markerData ?? [], }), }).then(r => r.json()) .then(d => log(`💾 Gespeichert: ${d.file} (${d.total} Messungen)`)) .catch(e => log(`⚠ Speichern fehlgeschlagen: ${e.message}`)); } else { // Kein gültiges Ergebnis → Buttons deaktivieren _lastYAxisMsg = null; disableCalibActions(); } }); } // ── Tab: Board (shared helpers) ─────────────────────────────────────────────── /** Befüllt alle Set-Dropdowns aus /api/robot/board-sets */ async function populateBoardSetDropdowns() { let sets = []; try { const r = await fetch('/api/robot/board-sets'); if (r.ok) sets = (await r.json()).sets ?? []; } catch { /* kein Server / noch keine Sets → leere Dropdowns */ } // Hilfsfunktion: