(() => { const SVG_NS = 'http://www.w3.org/2000/svg'; const DEFAULT_VIEWBOX = { x: 0, y: 0, width: 2000, height: 1000 }; const SNAP_TOLERANCE = 16; function initRoomPolygonEditor() { const floorSelect = document.getElementById('room-floor-id'); const canvas = document.getElementById('room-polygon-canvas'); const polygonInput = document.getElementById('room-polygon-points'); const snapWalls = document.getElementById('room-snap-walls'); const undoButton = document.getElementById('room-undo-point'); const clearButton = document.getElementById('room-clear-polygon'); const mapHint = document.getElementById('room-map-hint'); if (!floorSelect || !canvas || !polygonInput) { return; } const state = { viewBox: { ...DEFAULT_VIEWBOX }, wallPoints: [], floorLayerNodes: [], points: parsePoints(polygonInput.value), draggingIndex: null }; const updateInput = () => { if (state.points.length >= 3) { polygonInput.value = JSON.stringify(state.points.map((point) => ({ x: Math.round(point.x), y: Math.round(point.y) }))); } else { polygonInput.value = ''; } }; const render = () => { canvas.innerHTML = ''; canvas.setAttribute( 'viewBox', `${state.viewBox.x} ${state.viewBox.y} ${state.viewBox.width} ${state.viewBox.height}` ); const bg = createSvgElement('rect'); bg.setAttribute('x', String(state.viewBox.x)); bg.setAttribute('y', String(state.viewBox.y)); bg.setAttribute('width', String(state.viewBox.width)); bg.setAttribute('height', String(state.viewBox.height)); bg.setAttribute('fill', '#f7f7f7'); bg.setAttribute('stroke', '#e1e1e1'); canvas.appendChild(bg); if (state.floorLayerNodes.length > 0) { const floorLayer = createSvgElement('g'); floorLayer.setAttribute('opacity', '0.55'); floorLayer.setAttribute('pointer-events', 'none'); state.floorLayerNodes.forEach((node) => { floorLayer.appendChild(node.cloneNode(true)); }); canvas.appendChild(floorLayer); } if (state.points.length >= 2) { const polyline = createSvgElement('polyline'); polyline.setAttribute('fill', state.points.length >= 3 ? 'rgba(13, 110, 253, 0.16)' : 'none'); polyline.setAttribute('stroke', '#0d6efd'); polyline.setAttribute('stroke-width', '3'); polyline.setAttribute('points', buildPointString(state.points)); if (state.points.length >= 3) { polyline.setAttribute('stroke-linejoin', 'round'); polyline.setAttribute('stroke-linecap', 'round'); polyline.setAttribute('points', `${buildPointString(state.points)} ${state.points[0].x},${state.points[0].y}`); } canvas.appendChild(polyline); } state.points.forEach((point, index) => { const vertex = createSvgElement('circle'); vertex.setAttribute('cx', String(point.x)); vertex.setAttribute('cy', String(point.y)); vertex.setAttribute('r', '9'); vertex.setAttribute('fill', '#ffffff'); vertex.setAttribute('stroke', '#dc3545'); vertex.setAttribute('stroke-width', '3'); vertex.setAttribute('data-vertex-index', String(index)); canvas.appendChild(vertex); }); updateInput(); }; const snapPoint = (point) => { if (!snapWalls || !snapWalls.checked || state.wallPoints.length === 0) { return point; } let nearest = null; let nearestDist = Number.POSITIVE_INFINITY; state.wallPoints.forEach((wallPoint) => { const dx = wallPoint.x - point.x; const dy = wallPoint.y - point.y; const dist = Math.sqrt((dx * dx) + (dy * dy)); if (dist < nearestDist) { nearestDist = dist; nearest = wallPoint; } }); if (nearest && nearestDist <= SNAP_TOLERANCE) { return { x: nearest.x, y: nearest.y }; } return point; }; const addPoint = (event) => { const point = toSvgPoint(canvas, event); if (!point) { return; } state.points.push(snapPoint(point)); render(); }; const onPointerDown = (event) => { const target = event.target; if (!(target instanceof SVGElement)) { return; } const vertex = target.closest('[data-vertex-index]'); if (vertex) { const vertexIndex = Number(vertex.getAttribute('data-vertex-index')); if (Number.isInteger(vertexIndex)) { state.draggingIndex = vertexIndex; vertex.setPointerCapture(event.pointerId); } return; } addPoint(event); }; const onPointerMove = (event) => { if (state.draggingIndex === null) { return; } const point = toSvgPoint(canvas, event); if (!point) { return; } state.points[state.draggingIndex] = snapPoint(point); render(); }; const stopDragging = () => { state.draggingIndex = null; }; const parseFloorSvg = (rawSvg) => { const parser = new DOMParser(); const doc = parser.parseFromString(rawSvg, 'image/svg+xml'); const root = doc.documentElement; if (!root || root.nodeName.toLowerCase() === 'parsererror') { return null; } return root; }; const readViewBox = (svgRoot) => { const vb = (svgRoot.getAttribute('viewBox') || '').trim(); if (vb) { const parts = vb.split(/\s+/).map((value) => Number(value)); if (parts.length === 4 && parts.every((value) => Number.isFinite(value))) { return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] }; } } const width = Number(svgRoot.getAttribute('width')); const height = Number(svgRoot.getAttribute('height')); if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) { return { x: 0, y: 0, width, height }; } return { ...DEFAULT_VIEWBOX }; }; const collectSnapPoints = (svgRoot) => { const points = []; const addPointValue = (x, y) => { if (!Number.isFinite(x) || !Number.isFinite(y)) { return; } points.push({ x: Math.round(x), y: Math.round(y) }); }; svgRoot.querySelectorAll('line').forEach((line) => { addPointValue(Number(line.getAttribute('x1')), Number(line.getAttribute('y1'))); addPointValue(Number(line.getAttribute('x2')), Number(line.getAttribute('y2'))); }); svgRoot.querySelectorAll('polyline, polygon').forEach((shape) => { parsePointString(shape.getAttribute('points') || '').forEach((point) => addPointValue(point.x, point.y)); }); svgRoot.querySelectorAll('rect').forEach((rect) => { const x = Number(rect.getAttribute('x')); const y = Number(rect.getAttribute('y')); const width = Number(rect.getAttribute('width')); const height = Number(rect.getAttribute('height')); addPointValue(x, y); addPointValue(x + width, y); addPointValue(x + width, y + height); addPointValue(x, y + height); }); svgRoot.querySelectorAll('path').forEach((path) => { const d = path.getAttribute('d') || ''; const numbers = (d.match(/-?\d+(\.\d+)?/g) || []).map((value) => Number(value)); for (let i = 0; i < numbers.length - 1; i += 2) { addPointValue(numbers[i], numbers[i + 1]); } }); return dedupePoints(points); }; const loadFloor = async () => { const selected = floorSelect.selectedOptions[0]; const svgUrl = selected ? (selected.dataset.svgUrl || '') : ''; state.viewBox = { ...DEFAULT_VIEWBOX }; state.wallPoints = []; state.floorLayerNodes = []; if (!svgUrl) { if (mapHint) { mapHint.textContent = 'Fuer dieses Stockwerk ist keine Karte hinterlegt. Polygon kann trotzdem frei gezeichnet werden.'; } render(); 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 root = parseFloorSvg(raw); if (!root) { throw new Error('Invalid SVG'); } state.viewBox = readViewBox(root); state.wallPoints = collectSnapPoints(root); state.floorLayerNodes = Array.from(root.childNodes) .filter((node) => node.nodeType === Node.ELEMENT_NODE) .map((node) => node.cloneNode(true)); if (mapHint) { mapHint.textContent = state.wallPoints.length > 0 ? 'Klick setzt Punkte. Punkte sind per Drag verschiebbar. Snap nutzt Wandpunkte aus der Stockwerkskarte.' : 'Klick setzt Punkte. Punkte sind per Drag verschiebbar.'; } } catch (error) { if (mapHint) { mapHint.textContent = 'Stockwerkskarte konnte nicht geladen werden. Polygon kann frei gezeichnet werden.'; } } render(); }; canvas.addEventListener('pointerdown', onPointerDown); canvas.addEventListener('pointermove', onPointerMove); canvas.addEventListener('pointerup', stopDragging); canvas.addEventListener('pointercancel', stopDragging); canvas.addEventListener('pointerleave', stopDragging); floorSelect.addEventListener('change', () => { loadFloor(); }); if (undoButton) { undoButton.addEventListener('click', () => { if (state.points.length === 0) { return; } state.points.pop(); render(); }); } if (clearButton) { clearButton.addEventListener('click', () => { state.points = []; render(); }); } render(); loadFloor(); } function parsePoints(raw) { if (!raw) { return []; } try { const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) { return []; } return parsed .map((point) => ({ x: Number(point && point.x), y: Number(point && point.y) })) .filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y)); } catch (error) { return []; } } function parsePointString(pointsAttr) { return pointsAttr .trim() .split(/\s+/) .map((pair) => { const values = pair.split(','); if (values.length !== 2) { return null; } const x = Number(values[0]); const y = Number(values[1]); if (!Number.isFinite(x) || !Number.isFinite(y)) { return null; } return { x, y }; }) .filter(Boolean); } function dedupePoints(points) { const map = new Map(); points.forEach((point) => { const key = `${Math.round(point.x)}:${Math.round(point.y)}`; if (!map.has(key)) { map.set(key, { x: Math.round(point.x), y: Math.round(point.y) }); } }); return Array.from(map.values()); } function buildPointString(points) { return points.map((point) => `${Math.round(point.x)},${Math.round(point.y)}`).join(' '); } function createSvgElement(name) { return document.createElementNS(SVG_NS, name); } function toSvgPoint(svg, event) { const pt = svg.createSVGPoint(); pt.x = event.clientX; pt.y = event.clientY; const ctm = svg.getScreenCTM(); if (!ctm) { return null; } const transformed = pt.matrixTransform(ctm.inverse()); return { x: transformed.x, y: transformed.y }; } document.addEventListener('DOMContentLoaded', initRoomPolygonEditor); })();