Loading noctua/web/static/img/synoptic.svg +356 −278 File changed.Preview size limit exceeded, changes collapsed. Show changes noctua/web/static/js/synoptic.js +342 −220 Original line number Diff line number Diff line // Use the global variable defined in the HTML template const svgUrl = window.SYNOPTIC_SVG_URL; const container = document.getElementById('svg-container'); let refGeom = null; let telGeom = null; let stageRefGeom = null; let stageMirrorGeom = null; // --- Drawing Calibration --- // If in Inkscape you drew the dome slit facing NORTH (Bottom), leave this at 0. // If you drew it facing SOUTH (Top), set it to 180. const DOME_BASE_ROTATION = 180; // Maximum shutter opening distance in pixels for each half const SHUTTER_MAX_OPEN_PX = 8; // Maximum opening distance in pixels for each telescope petal const PETALS_MAX_OPEN_PX = 2; /** * Parse SVG bounding boxes to establish the coordinate system. * Parse SVG bounding boxes to establish the coordinate systems. * Sets transform-origins for CSS manipulation. * * Returns Loading @@ -19,16 +31,12 @@ function initializeGeometry() { // --- Dome & Telescope Geometry --- const refEl = document.getElementById('structure-reference'); const telEl = document.getElementById('structure-telescope'); const domeEl = document.getElementById('structure-dome'); if (!refEl || !telEl) { console.warn("Synoptic: Missing #structure-reference or #structure-telescope in SVG."); return; } // getBBox() restituisce le coordinate non-trasformate locali dell'SVG if (refEl && telEl) { const refBBox = refEl.getBBox(); refGeom = { cx: refBBox.x + refBBox.width / 2, Loading @@ -42,12 +50,92 @@ cy: telBBox.y + telBBox.height / 2 }; // Fissa il perno di rotazione della cupola esattamente al centro del cerchio di riferimento if (domeEl) { domeEl.style.transformOrigin = `${refGeom.cx}px ${refGeom.cy}px`; } } // --- Linear Stage Geometry --- const stageRefEl = document.getElementById('stage-reference'); const stageMirrorEl = document.getElementById('structure-stage-mirror'); if (stageRefEl && stageMirrorEl) { const sRefBBox = stageRefEl.getBBox(); // For the stage, we map the real-world 0-150mm range to the width of the reference box stageRefGeom = { startX: sRefBBox.x, // 0 mm endX: sRefBBox.x + sRefBBox.width, // 150 mm widthPx: sRefBBox.width }; const mBBox = stageMirrorEl.getBBox(); stageMirrorGeom = { cx: mBBox.x + mBBox.width / 2 }; } } /** * Rotate the dome graphic using CSS to preserve Inkscape base transforms. * * Parameters * ---------- * az : number * The azimuth of the dome in degrees. * * Returns * ------- * void */ function animateDome(az) { const domeEl = document.getElementById('structure-dome'); if (!domeEl || !refGeom) return; // Direction: North (Bottom) -> East (Right) = Counter-Clockwise = Negative Rotation in SVG. const svgRotation = -az + DOME_BASE_ROTATION; domeEl.style.transform = `rotate(${svgRotation}deg)`; } /** * Translate the telescope graphic using CSS relative to its original drawn position. * * Parameters * ---------- * alt : number * Telescope altitude in degrees. * az : number * Telescope azimuth in degrees. * * Returns * ------- * void */ function animateTelescope(alt, az) { const telEl = document.getElementById('structure-telescope'); if (!telEl || !refGeom || !telGeom) return; // Radius: Alt 90 = center, Alt 0 = outer edge const radius = refGeom.rMax * (1 - (alt / 90.0)); const azRad = az * (Math.PI / 180.0); // North at bottom (+Y), East at right (+X) const targetX = refGeom.cx + radius * Math.sin(azRad); const targetY = refGeom.cy + radius * Math.cos(azRad); const deltaX = targetX - telGeom.cx; const deltaY = targetY - telGeom.cy; telEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`; } /** * Animate the dome shutter halves based on the open state. Loading @@ -69,17 +157,15 @@ if (!dome1El || !dome2El) return; // Ensure state is clamped between 0 and 1 const clampedState = Math.max(0, Math.min(1, state)); // Calculate translation in pixels (state 0 = 0px, state 1 = 8px) const offsetPx = clampedState * SHUTTER_MAX_OPEN_PX; // dome1 moves Left (negative X), dome2 moves Right (positive X) // dome1 moves Left (-X), dome2 moves Right (+X) dome1El.style.transform = `translate(-${offsetPx}px, 0px)`; dome2El.style.transform = `translate(${offsetPx}px, 0px)`; } /** * Animate the telescope cover petals based on the open state. * Moves four petals diagonally outward. Loading @@ -103,10 +189,7 @@ if (!cover1El || !cover2El || !cover3El || !cover4El) return; // Ensure state is clamped between 0 and 1 const clampedState = Math.max(0, Math.min(1, state)); // Calculate translation in pixels (state 0 = 0px, state 1 = 2px) const offsetPx = clampedState * PETALS_MAX_OPEN_PX; // Cover 1: Left and Down (-X, +Y) Loading @@ -122,67 +205,103 @@ cover4El.style.transform = `translate(${offsetPx}px, -${offsetPx}px)`; } /** * Rotate the dome graphic using CSS to preserve Inkscape base transforms. * Translate the mirror graphic along the linear stage axis. * Maps 0-150mm to the physical pixel width of the reference box. * * Parameters * ---------- * az : number * The azimuth of the dome in degrees. * posMm : number * Stage position in millimeters (0 to 150). * * Returns * ------- * void */ function animateDome(az) { function animateStage(posMm) { const domeEl = document.getElementById('structure-dome'); if (!domeEl || !refGeom) return; const mirrorEl = document.getElementById('structure-stage-mirror'); if (!mirrorEl || !stageRefGeom || !stageMirrorGeom) return; // Direzione: Nord (Basso) -> Est (Destra) = Antiorario = Rotazione Negativa in SVG. const svgRotation = -az + DOME_BASE_ROTATION; // Clamp value to physical limits const clampedPos = Math.max(0, Math.min(150, posMm)); // Usiamo style.transform invece di setAttribute per non rompere il posizionamento di Inkscape domeEl.style.transform = `rotate(${svgRotation}deg)`; } // Calculate percentage along the rail (0.0 to 1.0) const percentage = clampedPos / 150.0; // Map percentage to target X pixel on screen const targetX = stageRefGeom.startX + (percentage * stageRefGeom.widthPx); // Calculate CSS translation relative to where the mirror was originally drawn const deltaX = targetX - stageMirrorGeom.cx; // Translate only on X axis mirrorEl.style.transform = `translate(${deltaX}px, 0px)`; } /** * Translate the telescope graphic using CSS relative to its original drawn position. * Forcefully apply fill color to an element and its inline styles. */ function applyFillForcefully(el, color) { el.setAttribute('fill', color); let style = el.getAttribute('style') || ''; if (style.match(/fill\s*:/i)) { // Replace existing fill with the new color and force !important style = style.replace(/fill\s*:[^;]+;?/gi, `fill: ${color} !important;`); } else { // Append fill if it doesn't exist style += `; fill: ${color} !important;`; } el.setAttribute('style', style); } /** * Update the fill color of a PDU indicator SVG element. * Properly handles string anomalies and Inkscape Grouping <g>. * * Parameters * ---------- * alt : number * Telescope altitude in degrees. * az : number * Telescope azimuth in degrees. * el : SVGElement * The DOM element to colorize. * state : string or boolean or number * 'On', 'Off', True, False, 1, 0. * * Returns * ------- * void */ function updateSwitchColor(el, state) { if (!el) return; function animateTelescope(alt, az) { const telEl = document.getElementById('structure-telescope'); if (!telEl || !refGeom || !telGeom) return; const COLOR_OFF = "rgba(0, 255, 255, 0.3)"; const COLOR_ON = "rgba(0, 255, 0, 0.9)"; const COLOR_UNKNOWN = "rgba(128, 128, 128, 0.5)"; // Raggio: Alt 90 = centro, Alt 0 = bordo esterno const radius = refGeom.rMax * (1 - (alt / 90.0)); let targetColor = COLOR_UNKNOWN; const azRad = az * (Math.PI / 180.0); // 1. Normalize the state to avoid case-sensitivity or hidden spaces let normState = state; if (typeof state === 'string') { normState = state.trim().toLowerCase(); } // Nord in basso (+Y), Est a destra (+X) const targetX = refGeom.cx + radius * Math.sin(azRad); const targetY = refGeom.cy + radius * Math.cos(azRad); // 2. Evaluate normalized state if (normState === "on" || normState === "yes" || normState === "true" || normState === 1 || normState === true) { targetColor = COLOR_ON; } else if (normState === "off" || normState === "no" || normState === "false" || normState === 0 || normState === false) { targetColor = COLOR_OFF; } // Calcola di quanti pixel deve spostarsi il telescopio rispetto a dove lo hai disegnato const deltaX = targetX - telGeom.cx; const deltaY = targetY - telGeom.cy; // 3. Apply color to the parent element applyFillForcefully(el, targetColor); // Applica lo spostamento relativo telEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`; // 4. Apply color to all graphical children (Crucial if the SVG ID is on a <g> Group) const children = el.querySelectorAll('path, rect, circle, ellipse, polygon'); children.forEach(child => { applyFillForcefully(child, targetColor); }); } // 1. Fetch and inject the SVG Loading @@ -190,7 +309,6 @@ .then(response => response.text()) .then(svgText => { container.innerHTML = svgText; // Allow the browser time to render the SVG before calculating Bounding Boxes setTimeout(initializeGeometry, 100); }) .catch(err => console.error("Error loading SVG:", err)); Loading @@ -198,10 +316,11 @@ // 2. Listen to Telemetry and update UI document.addEventListener('noctua-telemetry', (e) => { const { name, data } = e.detail; const subsystem = name.replace('all-', ''); // ---- A. Update Text Elements ---- // ---- A. Update Text and Color Elements ---- const svgElements = document.querySelectorAll(`[id^="telemetry:${subsystem}:"]`); svgElements.forEach(el => { Loading @@ -212,6 +331,14 @@ const key = parts[3]; if (data[device] && data[device].response !== undefined) { // Handle Color Indicators (Switches/PDUs) if (key === "indicator") { updateSwitchColor(el, data[device].response); return; // Stop here, do not change textContent } // Handle Text Values let value = "???"; if (typeof data[device].response === 'object' && data[device].response !== null) { Loading @@ -232,31 +359,22 @@ } }); // ---- B. Animate Graphics ---- if (subsystem === 'dome') { // Dome rotation if (subsystem === 'dome') { if (data.position && data.position.response) { const domeAz = data.position.response.azimuth; if (typeof domeAz === 'number') { animateDome(domeAz); } if (typeof domeAz === 'number') animateDome(domeAz); } // Dome shutter opening if (data.shutter && data.shutter.response !== undefined) { const shutterState = data.shutter.response; // Assuming telemetry sends a float between 0 and 1 if (typeof shutterState === 'number') { animateShutter(shutterState); } if (typeof shutterState === 'number') animateShutter(shutterState); } } // Telescope movement if (subsystem === 'telescope') { // Telescope translation (Alt/Az mapping) if (data.coordinates && data.coordinates.response) { const altaz = data.coordinates.response.altaz; if (Array.isArray(altaz) && altaz.length === 2) { Loading @@ -264,13 +382,17 @@ } } // Telescope cover petals opening if (data.cover && data.cover.response !== undefined) { const coverState = data.cover.response; // Assuming telemetry sends a float between 0 and 1 if (typeof coverState === 'number') { animatePetals(coverState); if (typeof coverState === 'number') animatePetals(coverState); } } if (subsystem === 'stage') { if (data.position && data.position.response !== undefined) { const stageMm = data.position.response; if (typeof stageMm === 'number') animateStage(stageMm); } } }); Loading
noctua/web/static/img/synoptic.svg +356 −278 File changed.Preview size limit exceeded, changes collapsed. Show changes
noctua/web/static/js/synoptic.js +342 −220 Original line number Diff line number Diff line // Use the global variable defined in the HTML template const svgUrl = window.SYNOPTIC_SVG_URL; const container = document.getElementById('svg-container'); let refGeom = null; let telGeom = null; let stageRefGeom = null; let stageMirrorGeom = null; // --- Drawing Calibration --- // If in Inkscape you drew the dome slit facing NORTH (Bottom), leave this at 0. // If you drew it facing SOUTH (Top), set it to 180. const DOME_BASE_ROTATION = 180; // Maximum shutter opening distance in pixels for each half const SHUTTER_MAX_OPEN_PX = 8; // Maximum opening distance in pixels for each telescope petal const PETALS_MAX_OPEN_PX = 2; /** * Parse SVG bounding boxes to establish the coordinate system. * Parse SVG bounding boxes to establish the coordinate systems. * Sets transform-origins for CSS manipulation. * * Returns Loading @@ -19,16 +31,12 @@ function initializeGeometry() { // --- Dome & Telescope Geometry --- const refEl = document.getElementById('structure-reference'); const telEl = document.getElementById('structure-telescope'); const domeEl = document.getElementById('structure-dome'); if (!refEl || !telEl) { console.warn("Synoptic: Missing #structure-reference or #structure-telescope in SVG."); return; } // getBBox() restituisce le coordinate non-trasformate locali dell'SVG if (refEl && telEl) { const refBBox = refEl.getBBox(); refGeom = { cx: refBBox.x + refBBox.width / 2, Loading @@ -42,12 +50,92 @@ cy: telBBox.y + telBBox.height / 2 }; // Fissa il perno di rotazione della cupola esattamente al centro del cerchio di riferimento if (domeEl) { domeEl.style.transformOrigin = `${refGeom.cx}px ${refGeom.cy}px`; } } // --- Linear Stage Geometry --- const stageRefEl = document.getElementById('stage-reference'); const stageMirrorEl = document.getElementById('structure-stage-mirror'); if (stageRefEl && stageMirrorEl) { const sRefBBox = stageRefEl.getBBox(); // For the stage, we map the real-world 0-150mm range to the width of the reference box stageRefGeom = { startX: sRefBBox.x, // 0 mm endX: sRefBBox.x + sRefBBox.width, // 150 mm widthPx: sRefBBox.width }; const mBBox = stageMirrorEl.getBBox(); stageMirrorGeom = { cx: mBBox.x + mBBox.width / 2 }; } } /** * Rotate the dome graphic using CSS to preserve Inkscape base transforms. * * Parameters * ---------- * az : number * The azimuth of the dome in degrees. * * Returns * ------- * void */ function animateDome(az) { const domeEl = document.getElementById('structure-dome'); if (!domeEl || !refGeom) return; // Direction: North (Bottom) -> East (Right) = Counter-Clockwise = Negative Rotation in SVG. const svgRotation = -az + DOME_BASE_ROTATION; domeEl.style.transform = `rotate(${svgRotation}deg)`; } /** * Translate the telescope graphic using CSS relative to its original drawn position. * * Parameters * ---------- * alt : number * Telescope altitude in degrees. * az : number * Telescope azimuth in degrees. * * Returns * ------- * void */ function animateTelescope(alt, az) { const telEl = document.getElementById('structure-telescope'); if (!telEl || !refGeom || !telGeom) return; // Radius: Alt 90 = center, Alt 0 = outer edge const radius = refGeom.rMax * (1 - (alt / 90.0)); const azRad = az * (Math.PI / 180.0); // North at bottom (+Y), East at right (+X) const targetX = refGeom.cx + radius * Math.sin(azRad); const targetY = refGeom.cy + radius * Math.cos(azRad); const deltaX = targetX - telGeom.cx; const deltaY = targetY - telGeom.cy; telEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`; } /** * Animate the dome shutter halves based on the open state. Loading @@ -69,17 +157,15 @@ if (!dome1El || !dome2El) return; // Ensure state is clamped between 0 and 1 const clampedState = Math.max(0, Math.min(1, state)); // Calculate translation in pixels (state 0 = 0px, state 1 = 8px) const offsetPx = clampedState * SHUTTER_MAX_OPEN_PX; // dome1 moves Left (negative X), dome2 moves Right (positive X) // dome1 moves Left (-X), dome2 moves Right (+X) dome1El.style.transform = `translate(-${offsetPx}px, 0px)`; dome2El.style.transform = `translate(${offsetPx}px, 0px)`; } /** * Animate the telescope cover petals based on the open state. * Moves four petals diagonally outward. Loading @@ -103,10 +189,7 @@ if (!cover1El || !cover2El || !cover3El || !cover4El) return; // Ensure state is clamped between 0 and 1 const clampedState = Math.max(0, Math.min(1, state)); // Calculate translation in pixels (state 0 = 0px, state 1 = 2px) const offsetPx = clampedState * PETALS_MAX_OPEN_PX; // Cover 1: Left and Down (-X, +Y) Loading @@ -122,67 +205,103 @@ cover4El.style.transform = `translate(${offsetPx}px, -${offsetPx}px)`; } /** * Rotate the dome graphic using CSS to preserve Inkscape base transforms. * Translate the mirror graphic along the linear stage axis. * Maps 0-150mm to the physical pixel width of the reference box. * * Parameters * ---------- * az : number * The azimuth of the dome in degrees. * posMm : number * Stage position in millimeters (0 to 150). * * Returns * ------- * void */ function animateDome(az) { function animateStage(posMm) { const domeEl = document.getElementById('structure-dome'); if (!domeEl || !refGeom) return; const mirrorEl = document.getElementById('structure-stage-mirror'); if (!mirrorEl || !stageRefGeom || !stageMirrorGeom) return; // Direzione: Nord (Basso) -> Est (Destra) = Antiorario = Rotazione Negativa in SVG. const svgRotation = -az + DOME_BASE_ROTATION; // Clamp value to physical limits const clampedPos = Math.max(0, Math.min(150, posMm)); // Usiamo style.transform invece di setAttribute per non rompere il posizionamento di Inkscape domeEl.style.transform = `rotate(${svgRotation}deg)`; } // Calculate percentage along the rail (0.0 to 1.0) const percentage = clampedPos / 150.0; // Map percentage to target X pixel on screen const targetX = stageRefGeom.startX + (percentage * stageRefGeom.widthPx); // Calculate CSS translation relative to where the mirror was originally drawn const deltaX = targetX - stageMirrorGeom.cx; // Translate only on X axis mirrorEl.style.transform = `translate(${deltaX}px, 0px)`; } /** * Translate the telescope graphic using CSS relative to its original drawn position. * Forcefully apply fill color to an element and its inline styles. */ function applyFillForcefully(el, color) { el.setAttribute('fill', color); let style = el.getAttribute('style') || ''; if (style.match(/fill\s*:/i)) { // Replace existing fill with the new color and force !important style = style.replace(/fill\s*:[^;]+;?/gi, `fill: ${color} !important;`); } else { // Append fill if it doesn't exist style += `; fill: ${color} !important;`; } el.setAttribute('style', style); } /** * Update the fill color of a PDU indicator SVG element. * Properly handles string anomalies and Inkscape Grouping <g>. * * Parameters * ---------- * alt : number * Telescope altitude in degrees. * az : number * Telescope azimuth in degrees. * el : SVGElement * The DOM element to colorize. * state : string or boolean or number * 'On', 'Off', True, False, 1, 0. * * Returns * ------- * void */ function updateSwitchColor(el, state) { if (!el) return; function animateTelescope(alt, az) { const telEl = document.getElementById('structure-telescope'); if (!telEl || !refGeom || !telGeom) return; const COLOR_OFF = "rgba(0, 255, 255, 0.3)"; const COLOR_ON = "rgba(0, 255, 0, 0.9)"; const COLOR_UNKNOWN = "rgba(128, 128, 128, 0.5)"; // Raggio: Alt 90 = centro, Alt 0 = bordo esterno const radius = refGeom.rMax * (1 - (alt / 90.0)); let targetColor = COLOR_UNKNOWN; const azRad = az * (Math.PI / 180.0); // 1. Normalize the state to avoid case-sensitivity or hidden spaces let normState = state; if (typeof state === 'string') { normState = state.trim().toLowerCase(); } // Nord in basso (+Y), Est a destra (+X) const targetX = refGeom.cx + radius * Math.sin(azRad); const targetY = refGeom.cy + radius * Math.cos(azRad); // 2. Evaluate normalized state if (normState === "on" || normState === "yes" || normState === "true" || normState === 1 || normState === true) { targetColor = COLOR_ON; } else if (normState === "off" || normState === "no" || normState === "false" || normState === 0 || normState === false) { targetColor = COLOR_OFF; } // Calcola di quanti pixel deve spostarsi il telescopio rispetto a dove lo hai disegnato const deltaX = targetX - telGeom.cx; const deltaY = targetY - telGeom.cy; // 3. Apply color to the parent element applyFillForcefully(el, targetColor); // Applica lo spostamento relativo telEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`; // 4. Apply color to all graphical children (Crucial if the SVG ID is on a <g> Group) const children = el.querySelectorAll('path, rect, circle, ellipse, polygon'); children.forEach(child => { applyFillForcefully(child, targetColor); }); } // 1. Fetch and inject the SVG Loading @@ -190,7 +309,6 @@ .then(response => response.text()) .then(svgText => { container.innerHTML = svgText; // Allow the browser time to render the SVG before calculating Bounding Boxes setTimeout(initializeGeometry, 100); }) .catch(err => console.error("Error loading SVG:", err)); Loading @@ -198,10 +316,11 @@ // 2. Listen to Telemetry and update UI document.addEventListener('noctua-telemetry', (e) => { const { name, data } = e.detail; const subsystem = name.replace('all-', ''); // ---- A. Update Text Elements ---- // ---- A. Update Text and Color Elements ---- const svgElements = document.querySelectorAll(`[id^="telemetry:${subsystem}:"]`); svgElements.forEach(el => { Loading @@ -212,6 +331,14 @@ const key = parts[3]; if (data[device] && data[device].response !== undefined) { // Handle Color Indicators (Switches/PDUs) if (key === "indicator") { updateSwitchColor(el, data[device].response); return; // Stop here, do not change textContent } // Handle Text Values let value = "???"; if (typeof data[device].response === 'object' && data[device].response !== null) { Loading @@ -232,31 +359,22 @@ } }); // ---- B. Animate Graphics ---- if (subsystem === 'dome') { // Dome rotation if (subsystem === 'dome') { if (data.position && data.position.response) { const domeAz = data.position.response.azimuth; if (typeof domeAz === 'number') { animateDome(domeAz); } if (typeof domeAz === 'number') animateDome(domeAz); } // Dome shutter opening if (data.shutter && data.shutter.response !== undefined) { const shutterState = data.shutter.response; // Assuming telemetry sends a float between 0 and 1 if (typeof shutterState === 'number') { animateShutter(shutterState); } if (typeof shutterState === 'number') animateShutter(shutterState); } } // Telescope movement if (subsystem === 'telescope') { // Telescope translation (Alt/Az mapping) if (data.coordinates && data.coordinates.response) { const altaz = data.coordinates.response.altaz; if (Array.isArray(altaz) && altaz.length === 2) { Loading @@ -264,13 +382,17 @@ } } // Telescope cover petals opening if (data.cover && data.cover.response !== undefined) { const coverState = data.cover.response; // Assuming telemetry sends a float between 0 and 1 if (typeof coverState === 'number') { animatePetals(coverState); if (typeof coverState === 'number') animatePetals(coverState); } } if (subsystem === 'stage') { if (data.position && data.position.response !== undefined) { const stageMm = data.position.response; if (typeof stageMm === 'number') animateStage(stageMm); } } });