document.addEventListener('DOMContentLoaded', () => { const SVG_NS = 'http://www.w3.org/2000/svg'; const DEFAULT_PLAN_SIZE = { width: 2000, height: 1000 }; const filterForm = document.getElementById('infra-filter-form'); const floorSelect = document.getElementById('infra-floor-select'); if (filterForm && floorSelect) { floorSelect.addEventListener('change', () => { filterForm.submit(); }); } const canvas = document.getElementById('infra-floor-canvas'); const overlay = document.getElementById('infra-floor-overlay'); const scene = document.getElementById('infra-floor-scene'); const floorSvg = canvas ? canvas.querySelector('.infra-floor-svg') : null; if (!canvas || !overlay || !floorSvg || !scene) { return; } const patchPanels = safeJsonParse(canvas.dataset.patchpanels); const outlets = safeJsonParse(canvas.dataset.outlets); const planSize = { ...DEFAULT_PLAN_SIZE }; const updateOverlayViewBox = () => { overlay.setAttribute('viewBox', `0 0 ${planSize.width} ${planSize.height}`); }; const createMarker = (entry, type) => { const x = Number(entry.x); const y = Number(entry.y); if (!Number.isFinite(x) || !Number.isFinite(y)) { return; } const marker = document.createElementNS(SVG_NS, 'rect'); marker.classList.add('infra-overlay-marker', type); marker.setAttribute('x', String(Math.round(x))); marker.setAttribute('y', String(Math.round(y))); if (type === 'patchpanel') { marker.setAttribute('width', String(Math.max(1, Number(entry.width) || 20))); marker.setAttribute('height', String(Math.max(1, Number(entry.height) || 5))); } else { marker.setAttribute('width', '10'); marker.setAttribute('height', '10'); } const tooltipLines = buildTooltipLines(entry, type); if (tooltipLines.length > 0) { const titleNode = document.createElementNS(SVG_NS, 'title'); titleNode.textContent = tooltipLines.join('\n'); marker.appendChild(titleNode); } overlay.appendChild(marker); }; patchPanels.forEach((entry) => createMarker(entry, 'patchpanel')); outlets.forEach((entry) => createMarker(entry, 'outlet')); const camera = { scale: 1, tx: 0, ty: 0 }; const SCALE_MIN = 0.6; const SCALE_MAX = 3.5; const SCALE_STEP = 0.15; let drag = null; const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); const applyCamera = () => { scene.style.transform = `translate(${camera.tx}px, ${camera.ty}px) scale(${camera.scale})`; }; const zoomAtCenter = (factor) => { const nextScale = clamp(camera.scale * factor, SCALE_MIN, SCALE_MAX); if (Math.abs(nextScale - camera.scale) < 0.0001) { return; } camera.scale = nextScale; applyCamera(); }; const resetCamera = () => { camera.scale = 1; camera.tx = 0; camera.ty = 0; applyCamera(); }; canvas.addEventListener('wheel', (event) => { event.preventDefault(); zoomAtCenter(event.deltaY < 0 ? (1 + SCALE_STEP) : (1 - SCALE_STEP)); }, { passive: false }); canvas.addEventListener('pointerdown', (event) => { drag = { x: event.clientX, y: event.clientY, baseX: camera.tx, baseY: camera.ty }; canvas.classList.add('is-dragging'); canvas.setPointerCapture(event.pointerId); }); canvas.addEventListener('pointermove', (event) => { if (!drag) { return; } camera.tx = drag.baseX + (event.clientX - drag.x); camera.ty = drag.baseY + (event.clientY - drag.y); applyCamera(); }); const stopDrag = (event) => { if (!drag) { return; } drag = null; canvas.classList.remove('is-dragging'); if (event && typeof event.pointerId === 'number') { canvas.releasePointerCapture(event.pointerId); } }; canvas.addEventListener('pointerup', stopDrag); canvas.addEventListener('pointercancel', stopDrag); document.querySelectorAll('[data-infra-zoom]').forEach((button) => { button.addEventListener('click', () => { const action = button.getAttribute('data-infra-zoom'); if (action === 'in') { zoomAtCenter(1 + SCALE_STEP); return; } if (action === 'out') { zoomAtCenter(1 - SCALE_STEP); return; } resetCamera(); }); }); applyCamera(); const loadPlanDimensions = async (svgUrl) => { if (!svgUrl) { updateOverlayViewBox(); return; } try { const response = await fetch(svgUrl, { credentials: 'same-origin' }); if (!response.ok) { throw new Error('SVG not available'); } const raw = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(raw, 'image/svg+xml'); const root = doc.documentElement; if (!root || root.nodeName.toLowerCase() === 'parsererror') { throw new Error('Invalid SVG'); } const vb = String(root.getAttribute('viewBox') || '').trim(); if (vb) { const parts = vb.split(/\s+/).map((value) => Number(value)); if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) { planSize.width = Math.max(1, parts[2]); planSize.height = Math.max(1, parts[3]); updateOverlayViewBox(); return; } } const width = Number(root.getAttribute('width')); const height = Number(root.getAttribute('height')); if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) { planSize.width = width; planSize.height = height; } else { planSize.width = DEFAULT_PLAN_SIZE.width; planSize.height = DEFAULT_PLAN_SIZE.height; } updateOverlayViewBox(); } catch (error) { planSize.width = DEFAULT_PLAN_SIZE.width; planSize.height = DEFAULT_PLAN_SIZE.height; updateOverlayViewBox(); } }; loadPlanDimensions(floorSvg.getAttribute('src') || ''); }); function safeJsonParse(value) { if (!value) { return []; } try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch (error) { return []; } } function buildTooltipLines(entry, type) { if (type === 'patchpanel') { const lines = [ `Patchpanel: ${entry.name || '-'}`, `Ports: ${Number(entry.port_count) || 0}`, `Position: ${Number(entry.x) || 0} x ${Number(entry.y) || 0}` ]; if (entry.comment) { lines.push(`Kommentar: ${entry.comment}`); } return lines; } const roomLabel = `${entry.room_name || '-'}${entry.room_number ? ` (${entry.room_number})` : ''}`; const lines = [ `Wandbuchse: ${entry.name || '-'}`, `Raum: ${roomLabel}`, `Position: ${Number(entry.x) || 0} x ${Number(entry.y) || 0}` ]; if (entry.port_names) { lines.push(`Ports: ${entry.port_names}`); } if (entry.comment) { lines.push(`Kommentar: ${entry.comment}`); } return lines; }