const MAP_WIDTH = 2982; const MAP_HEIGHT = 1592; document.addEventListener('DOMContentLoaded', () => { loadBeacons(); loadGateways(); loadFingerprints(); setupFloorSelector(); // Setup mouse tracking const wrapper0 = document.getElementById('wrapper-floor-0'); if (wrapper0) wrapper0.addEventListener('click', (e) => handleClick(e, 0)); const wrapper1 = document.getElementById('wrapper-floor-1'); if (wrapper1) wrapper1.addEventListener('click', (e) => handleClick(e, 1)); }); function setupFloorSelector() { const selector = document.getElementById('floor-select'); selector.addEventListener('change', (e) => { const selectedFloor = e.target.value; // Hide all floors document.getElementById('container-floor-0').classList.add('hidden'); document.getElementById('container-floor-1').classList.add('hidden'); // Show selected const target = document.getElementById(`container-floor-${selectedFloor}`); if (target) target.classList.remove('hidden'); }); } async function loadBeacons() { try { const response = await fetch('beacon.csv'); // Adjust path if needed relative to index.html if (!response.ok) throw new Error('Failed to load beacon.csv'); const text = await response.text(); const beacons = parseCSV(text); renderIcons(beacons, 'beacon'); } catch (error) { console.error('Error loading beacons:', error); showError('Failled to load beacon.csv. If you are opening this file locally, you might need to run a local server (e.g., "python3 -m http.server") due to CORS restrictions.'); } } async function loadGateways() { try { const response = await fetch('gateway.csv'); if (!response.ok) throw new Error('Failed to load gateway.csv'); const text = await response.text(); const gateways = parseCSV(text); renderIcons(gateways, 'gateway'); } catch (error) { console.error('Error loading gateways:', error); showError('Failed to load gateway.csv. Check console for details.'); } } function showError(message) { const errorDiv = document.createElement('div'); errorDiv.style.backgroundColor = '#fee'; errorDiv.style.color = 'red'; errorDiv.style.padding = '10px'; errorDiv.style.margin = '10px 0'; errorDiv.style.border = '1px solid red'; errorDiv.textContent = message; document.body.insertBefore(errorDiv, document.querySelector('.floor-container')); } function parseCSV(text) { const lines = text.trim().split('\n'); const headers = lines[0].split(';').map(h => h.trim()); // We expect headers to include: Floor, X, Y, etc. // CSV format based on file view: Position;Floor;RoomName;X;Y;Z;[Name];MAC const data = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const values = line.split(';'); const entry = {}; headers.forEach((header, index) => { entry[header] = values[index] ? values[index].trim() : ''; }); // Parse numbers entry.Floor = parseInt(entry.Floor, 10); entry.X = parseFloat(entry.X); entry.Y = parseFloat(entry.Y); data.push(entry); } return data; } // Fingerprint data storage let fingerprintData = { 0: [], // Floor 0 1: [] // Floor 1 }; async function loadFingerprints() { try { const [response0, response1] = await Promise.all([ fetch('fingerprints-floor0.json'), fetch('fingerprints-floor1.json') ]); if (response0.ok) fingerprintData[0] = await response0.json(); else console.error('Failed to load fingerprints-floor0.json'); if (response1.ok) fingerprintData[1] = await response1.json(); else console.error('Failed to load fingerprints-floor1.json'); console.log('Fingerprints loaded:', fingerprintData); } catch (error) { console.error('Error loading fingerprints:', error); } } function renderIcons(items, type) { items.forEach(item => { // Validation: Ensure valid coordinates and floor if (isNaN(item.X) || isNaN(item.Y) || isNaN(item.Floor)) { console.warn('Invalid item skipped:', item); return; } const overlayId = `overlay-floor-${item.Floor}`; const overlay = document.getElementById(overlayId); if (!overlay) { console.warn(`Overlay not found for floor: ${item.Floor}`); return; } // Container: Absolute at coordinates const iconContainer = document.createElement('div'); // Structure: Flex column, centered horizontally. // Positioning: // left/top put the top-left corner of div at the coordinate. // -translate-x-1/2 centers it horizontally. // -translate-y-[20px] moves it up by 20px (half of 40px icon), so the CENTER of the icon is at the coordinate. // If we used -translate-y-1/2, it would center the whole text+icon group, which varies in height. iconContainer.className = `absolute flex flex-col items-center pointer-events-auto cursor-pointer transform -translate-x-1/2 -translate-y-[20px] z-30 group`; // Add identifiers for gateways if (type === 'gateway') { iconContainer.classList.add('gateway-icon'); iconContainer.setAttribute('data-gateway-name', item.GatewayName); // Set transition for smooth opacity change iconContainer.style.transition = 'opacity 0.2s ease-in-out'; } const leftPercent = (item.X / MAP_WIDTH) * 100; const topPercent = (item.Y / MAP_HEIGHT) * 100; iconContainer.style.left = `${leftPercent}%`; iconContainer.style.top = `${topPercent}%`; // Icon const iconSpan = document.createElement('span'); iconSpan.className = 'iconify text-[#008EED] drop-shadow-sm'; iconSpan.style.width = '40px'; iconSpan.style.height = '40px'; // Explicit size // Label (BeaconName or GatewayName) const labelDiv = document.createElement('div'); // Label styling: small text, centered, semi-transparent bg for readability if overlapping labelDiv.className = 'text-[10px] font-bold text-gray-700 bg-white/70 px-1 rounded mt-[-4px] whitespace-nowrap shadow-sm'; labelDiv.textContent = (type === 'beacon' ? item.BeaconName : item.GatewayName) || item.Position; if (type === 'beacon') { iconSpan.setAttribute('data-icon', 'heroicons:signal'); } else { iconSpan.setAttribute('data-icon', 'lsicon:online-gateway-outline'); iconContainer.style.zIndex = '40'; // Gateways on top } iconContainer.appendChild(iconSpan); iconContainer.appendChild(labelDiv); // Optional Tooltip for details (MAC) const tooltip = document.createElement('div'); tooltip.className = `hidden group-hover:block absolute bottom-full mb-1 px-2 py-1 bg-black text-white text-xs rounded z-50 whitespace-nowrap`; tooltip.textContent = `MAC: ${item.MAC}`; iconContainer.appendChild(tooltip); overlay.appendChild(iconContainer); }); } function handleClick(e, floor) { if (!fingerprintData[floor] || fingerprintData[floor].length === 0) return; // Get wrapper relative coordinates const wrapper = e.currentTarget; const rect = wrapper.getBoundingClientRect(); // Mouse relative to the wrapper const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; // Convert to map coordinates if (rect.width === 0 || rect.height === 0) return; const mapX = (mouseX / rect.width) * MAP_WIDTH; const mapY = (mouseY / rect.height) * MAP_HEIGHT; // Round to nearest 50 let roundedX = Math.round(mapX / 50) * 50; let roundedY = Math.round(mapY / 50) * 50; // If not ending in 50, add 50 if (String(roundedX).slice(-2) !== '50') roundedX += 50; if (String(roundedY).slice(-2) !== '50') roundedY += 50; console.log(`Click at: ${mapX.toFixed(0)}, ${mapY.toFixed(0)} -> Rounded: ${roundedX}, ${roundedY}`); // Find exact match // Note: JSON coords are numbers, strict equality should work if they are exactly 50.0 etc. // Use a tolerance just in case, but user said "same position". const match = fingerprintData[floor].find(p => p.X === roundedX && p.Y === roundedY); if (match) { console.log('Match found:', match); updateGatewayOpacity(floor, match.gateways); } else { console.log('No data at rounded coordinates.'); } } function updateGatewayOpacity(floor, signalMap) { const container = document.getElementById(`overlay-floor-${floor}`); if (!container) return; const gateways = container.querySelectorAll('.gateway-icon'); gateways.forEach(gw => { const name = gw.getAttribute('data-gateway-name'); if (name && signalMap.hasOwnProperty(name)) { const signal = signalMap[name]; // Logic: -60 (best) -> opacity 1, -100 (worst) -> opacity 0 // Range is 40 (-60 to -100) // Normalized: (signal - (-100)) / (-60 - (-100)) = (signal + 100) / 40 let opacity = (signal + 100) / 40; // Clamp if (opacity < 0) opacity = 0; if (opacity > 1) opacity = 1; gw.style.opacity = opacity; } else { // No signal data for this gateway at this point -> 0 opacity gw.style.opacity = 0; } }); }