617 lines
22 KiB
JavaScript
617 lines
22 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
const DEFAULT_PLAN_SIZE = { width: 2000, height: 1000 };
|
|
|
|
const canvas = document.getElementById('floor-plan-canvas');
|
|
const overlay = document.getElementById('floor-plan-overlay');
|
|
const positionLabel = document.getElementById('floor-plan-position');
|
|
if (!canvas || !overlay) {
|
|
return;
|
|
}
|
|
|
|
const xFieldName = canvas.dataset.xField;
|
|
const yFieldName = canvas.dataset.yField;
|
|
const xField = xFieldName ? document.querySelector(`input[name="${xFieldName}"]`) : null;
|
|
const yField = yFieldName ? document.querySelector(`input[name="${yFieldName}"]`) : null;
|
|
if (!xField || !yField) {
|
|
return;
|
|
}
|
|
|
|
const markerWidth = Math.max(1, Number(canvas.dataset.markerWidth) || 10);
|
|
const markerHeight = Math.max(1, Number(canvas.dataset.markerHeight) || 10);
|
|
const markerType = canvas.dataset.markerType || '';
|
|
const activeId = Number(canvas.dataset.activeId || 0);
|
|
const panelReferences = JSON.parse(canvas.dataset.referencePanels || '[]');
|
|
const outletReferences = JSON.parse(canvas.dataset.referenceOutlets || '[]');
|
|
|
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
|
|
let markerX = 0;
|
|
let markerY = 0;
|
|
let dragging = false;
|
|
let panning = false;
|
|
let panStart = null;
|
|
let dragOffsetX = 0;
|
|
let dragOffsetY = 0;
|
|
let viewX = 0;
|
|
let viewY = 0;
|
|
let viewWidth = DEFAULT_PLAN_SIZE.width;
|
|
let viewHeight = DEFAULT_PLAN_SIZE.height;
|
|
|
|
const activeMarker = document.createElementNS(SVG_NS, 'rect');
|
|
activeMarker.classList.add('active-marker');
|
|
if (markerType === 'patchpanel') {
|
|
activeMarker.classList.add('panel-marker');
|
|
} else {
|
|
activeMarker.classList.add('outlet-marker');
|
|
}
|
|
activeMarker.setAttribute('width', String(markerWidth));
|
|
activeMarker.setAttribute('height', String(markerHeight));
|
|
overlay.appendChild(activeMarker);
|
|
|
|
const planSize = { ...DEFAULT_PLAN_SIZE };
|
|
const updateOverlayViewBox = () => {
|
|
overlay.setAttribute('viewBox', `${viewX} ${viewY} ${viewWidth} ${viewHeight}`);
|
|
};
|
|
|
|
const updatePositionLabel = (x, y) => {
|
|
if (positionLabel) {
|
|
positionLabel.textContent = `${Math.round(x)} x ${Math.round(y)}`;
|
|
}
|
|
};
|
|
|
|
const paintActiveMarker = () => {
|
|
activeMarker.setAttribute('x', String(Math.round(markerX)));
|
|
activeMarker.setAttribute('y', String(Math.round(markerY)));
|
|
};
|
|
|
|
const setMarkerPosition = (rawX, rawY) => {
|
|
const maxX = Math.max(0, planSize.width - markerWidth);
|
|
const maxY = Math.max(0, planSize.height - markerHeight);
|
|
markerX = clamp(rawX, 0, maxX);
|
|
markerY = clamp(rawY, 0, maxY);
|
|
|
|
paintActiveMarker();
|
|
xField.value = Math.round(markerX);
|
|
yField.value = Math.round(markerY);
|
|
updatePositionLabel(markerX, markerY);
|
|
};
|
|
|
|
const toOverlayPoint = (clientX, clientY) => {
|
|
const rect = overlay.getBoundingClientRect();
|
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
return null;
|
|
}
|
|
const ratioX = (clientX - rect.left) / rect.width;
|
|
const ratioY = (clientY - rect.top) / rect.height;
|
|
const transformed = {
|
|
x: viewX + (ratioX * viewWidth),
|
|
y: viewY + (ratioY * viewHeight)
|
|
};
|
|
return { x: transformed.x, y: transformed.y };
|
|
};
|
|
|
|
const clampView = () => {
|
|
const minWidth = Math.max(30, planSize.width * 0.1);
|
|
const minHeight = Math.max(30, planSize.height * 0.1);
|
|
viewWidth = clamp(viewWidth, minWidth, planSize.width);
|
|
viewHeight = clamp(viewHeight, minHeight, planSize.height);
|
|
viewX = clamp(viewX, 0, Math.max(0, planSize.width - viewWidth));
|
|
viewY = clamp(viewY, 0, Math.max(0, planSize.height - viewHeight));
|
|
};
|
|
|
|
const applyView = () => {
|
|
clampView();
|
|
updateOverlayViewBox();
|
|
};
|
|
|
|
const zoomAt = (clientX, clientY, factor) => {
|
|
const point = toOverlayPoint(clientX, clientY);
|
|
if (!point) {
|
|
return;
|
|
}
|
|
const ratioX = (point.x - viewX) / viewWidth;
|
|
const ratioY = (point.y - viewY) / viewHeight;
|
|
const nextWidth = viewWidth * factor;
|
|
const nextHeight = viewHeight * factor;
|
|
viewX = point.x - (ratioX * nextWidth);
|
|
viewY = point.y - (ratioY * nextHeight);
|
|
viewWidth = nextWidth;
|
|
viewHeight = nextHeight;
|
|
applyView();
|
|
};
|
|
|
|
const resetView = () => {
|
|
viewX = 0;
|
|
viewY = 0;
|
|
viewWidth = planSize.width;
|
|
viewHeight = planSize.height;
|
|
applyView();
|
|
};
|
|
|
|
const updateFromInputs = () => {
|
|
setMarkerPosition(Number(xField.value) || 0, Number(yField.value) || 0);
|
|
};
|
|
|
|
const clearReferenceMarkers = () => {
|
|
overlay.querySelectorAll('.reference-marker').forEach((node) => node.remove());
|
|
};
|
|
|
|
const clearRoomHighlight = () => {
|
|
overlay.querySelectorAll('.room-highlight').forEach((node) => node.remove());
|
|
};
|
|
|
|
const getNumericCoord = (value) => {
|
|
if (value === null || value === undefined || value === '') {
|
|
return null;
|
|
}
|
|
const num = Number(value);
|
|
return Number.isFinite(num) ? num : null;
|
|
};
|
|
|
|
const appendReference = (entry, cssClass, width, height) => {
|
|
const rawX = entry.x ?? entry.pos_x;
|
|
const rawY = entry.y ?? entry.pos_y;
|
|
const x = getNumericCoord(rawX);
|
|
const y = getNumericCoord(rawY);
|
|
if (x === null || y === null) {
|
|
return;
|
|
}
|
|
|
|
const ref = document.createElementNS(SVG_NS, 'rect');
|
|
ref.classList.add('reference-marker', cssClass);
|
|
ref.setAttribute('x', String(Math.round(x)));
|
|
ref.setAttribute('y', String(Math.round(y)));
|
|
ref.setAttribute('width', String(Math.max(1, Math.round(width))));
|
|
ref.setAttribute('height', String(Math.max(1, Math.round(height))));
|
|
if (entry.name) {
|
|
ref.setAttribute('aria-label', String(entry.name));
|
|
}
|
|
overlay.insertBefore(ref, activeMarker);
|
|
};
|
|
|
|
const appendRoomHighlight = () => {
|
|
if (!outletRoomSelect) {
|
|
return;
|
|
}
|
|
const selectedRoomOption = outletRoomSelect.selectedOptions?.[0];
|
|
if (!selectedRoomOption || !selectedRoomOption.value) {
|
|
return;
|
|
}
|
|
|
|
const currentFloorId = getCurrentFloorId();
|
|
const roomFloorId = Number(selectedRoomOption.dataset.floorId || 0);
|
|
if (!currentFloorId || roomFloorId !== currentFloorId) {
|
|
return;
|
|
}
|
|
|
|
const polygonRaw = String(selectedRoomOption.dataset.roomPolygon || '').trim();
|
|
if (polygonRaw) {
|
|
try {
|
|
const parsed = JSON.parse(polygonRaw);
|
|
if (Array.isArray(parsed)) {
|
|
const points = parsed
|
|
.map((point) => ({
|
|
x: Number(point && point.x),
|
|
y: Number(point && point.y)
|
|
}))
|
|
.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
|
|
|
|
if (points.length >= 3) {
|
|
const polygon = document.createElementNS(SVG_NS, 'polygon');
|
|
polygon.classList.add('room-highlight');
|
|
polygon.setAttribute(
|
|
'points',
|
|
points.map((point) => `${Math.round(point.x)},${Math.round(point.y)}`).join(' ')
|
|
);
|
|
overlay.insertBefore(polygon, activeMarker);
|
|
return;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// ignore invalid room polygon json
|
|
}
|
|
}
|
|
|
|
const x = Number(selectedRoomOption.dataset.roomX || 0);
|
|
const y = Number(selectedRoomOption.dataset.roomY || 0);
|
|
const width = Number(selectedRoomOption.dataset.roomWidth || 0);
|
|
const height = Number(selectedRoomOption.dataset.roomHeight || 0);
|
|
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) {
|
|
return;
|
|
}
|
|
if (width <= 0 || height <= 0) {
|
|
return;
|
|
}
|
|
|
|
const rect = document.createElementNS(SVG_NS, 'rect');
|
|
rect.classList.add('room-highlight');
|
|
rect.setAttribute('x', String(Math.round(x)));
|
|
rect.setAttribute('y', String(Math.round(y)));
|
|
rect.setAttribute('width', String(Math.round(width)));
|
|
rect.setAttribute('height', String(Math.round(height)));
|
|
overlay.insertBefore(rect, activeMarker);
|
|
};
|
|
|
|
const panelLocationSelect = document.getElementById('panel-location-select');
|
|
const panelBuildingSelect = document.getElementById('panel-building-select');
|
|
const panelFloorSelect = document.getElementById('panel-floor-select');
|
|
const outletRoomSelect = document.getElementById('outlet-room-select');
|
|
const floorPlanSvg = document.getElementById('floor-plan-svg');
|
|
const panelPlacementFields = document.getElementById('panel-placement-fields');
|
|
const panelFloorPlanGroup = document.getElementById('panel-floor-plan-group');
|
|
const panelFloorMissingHint = document.getElementById('panel-floor-missing-hint');
|
|
const outletBindPatchpanelSelect = document.getElementById('outlet-bind-patchpanel-port-id');
|
|
|
|
const buildingOptions = panelBuildingSelect ? Array.from(panelBuildingSelect.options).filter((option) => option.value !== '') : [];
|
|
const floorOptions = panelFloorSelect ? Array.from(panelFloorSelect.options).filter((option) => option.value !== '') : [];
|
|
|
|
const getCurrentFloorId = () => {
|
|
const floorOption = panelFloorSelect?.selectedOptions?.[0];
|
|
if (floorOption?.value) {
|
|
return Number(floorOption.value);
|
|
}
|
|
const roomOption = outletRoomSelect?.selectedOptions?.[0];
|
|
return Number(roomOption?.dataset?.floorId || 0);
|
|
};
|
|
|
|
const filterPatchpanelBindOptions = () => {
|
|
if (!outletBindPatchpanelSelect) {
|
|
return;
|
|
}
|
|
const currentFloorId = getCurrentFloorId();
|
|
const options = Array.from(outletBindPatchpanelSelect.options).filter((option) => option.value !== '');
|
|
let firstMatch = '';
|
|
let selectedStillVisible = false;
|
|
|
|
options.forEach((option) => {
|
|
const optionFloorId = Number(option.dataset.floorId || 0);
|
|
const matchesFloor = !currentFloorId || optionFloorId === currentFloorId;
|
|
option.hidden = !matchesFloor;
|
|
if (matchesFloor && !option.disabled && !firstMatch) {
|
|
firstMatch = option.value;
|
|
}
|
|
if (matchesFloor && option.selected) {
|
|
selectedStillVisible = true;
|
|
}
|
|
});
|
|
|
|
if (!selectedStillVisible && firstMatch && !outletBindPatchpanelSelect.value) {
|
|
outletBindPatchpanelSelect.value = firstMatch;
|
|
}
|
|
};
|
|
|
|
const renderReferenceMarkers = () => {
|
|
clearRoomHighlight();
|
|
clearReferenceMarkers();
|
|
const currentFloorId = getCurrentFloorId();
|
|
if (!currentFloorId) {
|
|
return;
|
|
}
|
|
appendRoomHighlight();
|
|
|
|
panelReferences.forEach((entry) => {
|
|
if (Number(entry.floor_id) !== currentFloorId) {
|
|
return;
|
|
}
|
|
if (markerType === 'patchpanel' && Number(entry.id) === activeId) {
|
|
return;
|
|
}
|
|
appendReference(entry, 'panel-marker', Math.max(1, Number(entry.width) || 20), Math.max(1, Number(entry.height) || 5));
|
|
});
|
|
|
|
outletReferences.forEach((entry) => {
|
|
if (Number(entry.floor_id) !== currentFloorId) {
|
|
return;
|
|
}
|
|
if (markerType === 'outlet' && Number(entry.id) === activeId) {
|
|
return;
|
|
}
|
|
appendReference(entry, 'outlet-marker', 10, 10);
|
|
});
|
|
};
|
|
|
|
const updateFloorPlanImage = () => {
|
|
if (!floorPlanSvg) {
|
|
return;
|
|
}
|
|
|
|
const floorOption = panelFloorSelect?.selectedOptions?.[0];
|
|
const roomOption = outletRoomSelect?.selectedOptions?.[0];
|
|
const svgUrl = floorOption?.dataset?.svgUrl || roomOption?.dataset?.floorSvgUrl || '';
|
|
|
|
if (svgUrl) {
|
|
floorPlanSvg.src = svgUrl;
|
|
floorPlanSvg.hidden = false;
|
|
loadPlanDimensions(svgUrl);
|
|
} else {
|
|
floorPlanSvg.removeAttribute('src');
|
|
floorPlanSvg.hidden = true;
|
|
planSize.width = DEFAULT_PLAN_SIZE.width;
|
|
planSize.height = DEFAULT_PLAN_SIZE.height;
|
|
resetView();
|
|
}
|
|
renderReferenceMarkers();
|
|
filterPatchpanelBindOptions();
|
|
};
|
|
|
|
if (floorPlanSvg) {
|
|
floorPlanSvg.addEventListener('error', () => {
|
|
floorPlanSvg.removeAttribute('src');
|
|
floorPlanSvg.hidden = true;
|
|
});
|
|
}
|
|
|
|
const loadPlanDimensions = async (svgUrl) => {
|
|
if (!svgUrl) {
|
|
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]);
|
|
resetView();
|
|
renderReferenceMarkers();
|
|
updateFromInputs();
|
|
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;
|
|
}
|
|
resetView();
|
|
renderReferenceMarkers();
|
|
updateFromInputs();
|
|
} catch (error) {
|
|
planSize.width = DEFAULT_PLAN_SIZE.width;
|
|
planSize.height = DEFAULT_PLAN_SIZE.height;
|
|
resetView();
|
|
renderReferenceMarkers();
|
|
updateFromInputs();
|
|
}
|
|
};
|
|
|
|
const updatePanelPlacementVisibility = () => {
|
|
if (!panelFloorSelect || !panelPlacementFields || !panelFloorPlanGroup) {
|
|
return;
|
|
}
|
|
|
|
const hasFloor = !!panelFloorSelect.value;
|
|
panelPlacementFields.hidden = !hasFloor;
|
|
panelFloorPlanGroup.hidden = !hasFloor;
|
|
if (panelFloorMissingHint) {
|
|
panelFloorMissingHint.hidden = hasFloor;
|
|
}
|
|
};
|
|
|
|
const filterFloorOptions = () => {
|
|
if (!panelFloorSelect) {
|
|
return;
|
|
}
|
|
const buildingValue = panelBuildingSelect?.value || '';
|
|
let firstMatch = '';
|
|
floorOptions.forEach((option) => {
|
|
const matches = !buildingValue || option.dataset.buildingId === buildingValue;
|
|
option.hidden = !matches;
|
|
option.disabled = !matches;
|
|
if (matches && !firstMatch) {
|
|
firstMatch = option.value;
|
|
}
|
|
});
|
|
|
|
const selectedOption = panelFloorSelect.querySelector(`option[value="${panelFloorSelect.value}"]`);
|
|
const isSelectedHidden = selectedOption ? selectedOption.hidden : true;
|
|
if (buildingValue && (!panelFloorSelect.value || isSelectedHidden) && firstMatch) {
|
|
panelFloorSelect.value = firstMatch;
|
|
}
|
|
|
|
updatePanelPlacementVisibility();
|
|
updateFloorPlanImage();
|
|
};
|
|
|
|
const filterBuildingOptions = () => {
|
|
if (!panelBuildingSelect) {
|
|
return;
|
|
}
|
|
const locationValue = panelLocationSelect?.value || '';
|
|
let firstMatch = '';
|
|
buildingOptions.forEach((option) => {
|
|
const matches = !locationValue || option.dataset.locationId === locationValue;
|
|
option.hidden = !matches;
|
|
option.disabled = !matches;
|
|
if (matches && !firstMatch) {
|
|
firstMatch = option.value;
|
|
}
|
|
});
|
|
|
|
const selectedOption = panelBuildingSelect.querySelector(`option[value="${panelBuildingSelect.value}"]`);
|
|
const isSelectedHidden = selectedOption ? selectedOption.hidden : true;
|
|
if (locationValue && (!panelBuildingSelect.value || isSelectedHidden) && firstMatch) {
|
|
panelBuildingSelect.value = firstMatch;
|
|
}
|
|
};
|
|
|
|
activeMarker.addEventListener('pointerdown', (event) => {
|
|
event.preventDefault();
|
|
dragging = true;
|
|
panning = false;
|
|
const point = toOverlayPoint(event.clientX, event.clientY);
|
|
if (!point) {
|
|
return;
|
|
}
|
|
dragOffsetX = point.x - markerX;
|
|
dragOffsetY = point.y - markerY;
|
|
activeMarker.setPointerCapture(event.pointerId);
|
|
});
|
|
|
|
activeMarker.addEventListener('pointermove', (event) => {
|
|
if (!dragging) {
|
|
return;
|
|
}
|
|
const point = toOverlayPoint(event.clientX, event.clientY);
|
|
if (!point) {
|
|
return;
|
|
}
|
|
setMarkerPosition(point.x - dragOffsetX, point.y - dragOffsetY);
|
|
});
|
|
|
|
const stopDrag = (event) => {
|
|
if (dragging) {
|
|
dragging = false;
|
|
if (activeMarker.hasPointerCapture(event.pointerId)) {
|
|
activeMarker.releasePointerCapture(event.pointerId);
|
|
}
|
|
}
|
|
if (panning) {
|
|
panning = false;
|
|
panStart = null;
|
|
if (overlay.hasPointerCapture(event.pointerId)) {
|
|
overlay.releasePointerCapture(event.pointerId);
|
|
}
|
|
}
|
|
};
|
|
|
|
['pointerup', 'pointercancel', 'pointerleave'].forEach((evt) => {
|
|
activeMarker.addEventListener(evt, stopDrag);
|
|
});
|
|
|
|
overlay.addEventListener('pointerdown', (event) => {
|
|
if (event.shiftKey || event.button === 1) {
|
|
event.preventDefault();
|
|
panning = true;
|
|
dragging = false;
|
|
panStart = {
|
|
clientX: event.clientX,
|
|
clientY: event.clientY,
|
|
viewX,
|
|
viewY
|
|
};
|
|
overlay.setPointerCapture(event.pointerId);
|
|
return;
|
|
}
|
|
if (event.target !== overlay) {
|
|
return;
|
|
}
|
|
const point = toOverlayPoint(event.clientX, event.clientY);
|
|
if (!point) {
|
|
return;
|
|
}
|
|
setMarkerPosition(point.x - markerWidth / 2, point.y - markerHeight / 2);
|
|
});
|
|
|
|
overlay.addEventListener('pointermove', (event) => {
|
|
if (!panning || !panStart) {
|
|
return;
|
|
}
|
|
const rect = overlay.getBoundingClientRect();
|
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
return;
|
|
}
|
|
const scaleX = viewWidth / rect.width;
|
|
const scaleY = viewHeight / rect.height;
|
|
const dx = (event.clientX - panStart.clientX) * scaleX;
|
|
const dy = (event.clientY - panStart.clientY) * scaleY;
|
|
viewX = panStart.viewX - dx;
|
|
viewY = panStart.viewY - dy;
|
|
applyView();
|
|
});
|
|
|
|
overlay.addEventListener('pointerup', stopDrag);
|
|
overlay.addEventListener('pointercancel', stopDrag);
|
|
|
|
overlay.addEventListener('wheel', (event) => {
|
|
event.preventDefault();
|
|
const factor = event.deltaY < 0 ? 0.9 : 1.1;
|
|
zoomAt(event.clientX, event.clientY, factor);
|
|
}, { passive: false });
|
|
|
|
[xField, yField].forEach((input) => {
|
|
input.addEventListener('input', () => {
|
|
updateFromInputs();
|
|
});
|
|
});
|
|
|
|
window.addEventListener('resize', () => {
|
|
updateFromInputs();
|
|
renderReferenceMarkers();
|
|
});
|
|
|
|
if (panelLocationSelect) {
|
|
panelLocationSelect.addEventListener('change', () => {
|
|
filterBuildingOptions();
|
|
filterFloorOptions();
|
|
});
|
|
}
|
|
|
|
if (panelBuildingSelect) {
|
|
panelBuildingSelect.addEventListener('change', () => {
|
|
filterFloorOptions();
|
|
});
|
|
}
|
|
|
|
if (panelFloorSelect) {
|
|
panelFloorSelect.addEventListener('change', () => {
|
|
updatePanelPlacementVisibility();
|
|
updateFloorPlanImage();
|
|
});
|
|
}
|
|
|
|
if (outletRoomSelect) {
|
|
outletRoomSelect.addEventListener('change', () => {
|
|
updateFloorPlanImage();
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('[data-floor-plan-zoom]').forEach((button) => {
|
|
button.addEventListener('click', () => {
|
|
const action = button.getAttribute('data-floor-plan-zoom');
|
|
if (action === 'in') {
|
|
const rect = overlay.getBoundingClientRect();
|
|
zoomAt(rect.left + (rect.width / 2), rect.top + (rect.height / 2), 0.85);
|
|
return;
|
|
}
|
|
if (action === 'out') {
|
|
const rect = overlay.getBoundingClientRect();
|
|
zoomAt(rect.left + (rect.width / 2), rect.top + (rect.height / 2), 1.15);
|
|
return;
|
|
}
|
|
resetView();
|
|
});
|
|
});
|
|
|
|
updateOverlayViewBox();
|
|
updateFromInputs();
|
|
filterPatchpanelBindOptions();
|
|
|
|
if (panelLocationSelect) {
|
|
filterBuildingOptions();
|
|
filterFloorOptions();
|
|
} else {
|
|
updateFloorPlanImage();
|
|
}
|
|
|
|
updatePanelPlacementVisibility();
|
|
});
|