238 lines
7.7 KiB
JavaScript
238 lines
7.7 KiB
JavaScript
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;
|
|
}
|