#13 räume definieren
This commit is contained in:
377
app/assets/js/room-polygon-editor.js
Normal file
377
app/assets/js/room-polygon-editor.js
Normal file
@@ -0,0 +1,377 @@
|
||||
(() => {
|
||||
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);
|
||||
})();
|
||||
Reference in New Issue
Block a user