const map = L.map('map').setView([51.1657, 10.4515], 6); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19 }).addTo(map); const markers = {}; const nodeData = {}; const statusDot = document.getElementById('statusDot'); const statusText = document.getElementById('statusText'); const nodeCount = document.getElementById('nodeCount'); let ws; const hopColors = { 0: '#2196F3', // Direkt - Blau 1: '#4CAF50', // 1 Hop - Grün 2: '#FF9800', // 2 Hops - Orange 3: '#F44336', // 3 Hops - Rot 4: '#9C27B0', // 4 Hops - Lila 5: '#795548', // 5 Hops - Braun }; const hopColorDefault = '#9E9E9E'; // Unbekannt - Grau function getHopColor(hopCount) { if (hopCount == null) return hopColorDefault; if (hopCount >= 6) return hopColorDefault; return hopColors[hopCount] || hopColorDefault; } function createIcon(color) { return L.divIcon({ className: '', html: ` `, iconSize: [24, 24], iconAnchor: [12, 12], popupAnchor: [0, -14], tooltipAnchor: [14, 0] }); } function nodeTooltip(node) { const name = node.long_name || node.short_name || node.node_id; const hops = node.hop_count != null ? node.hop_count : '?'; const bat = node.battery != null ? `${node.battery}%` : '-'; const snr = node.snr != null ? `${node.snr.toFixed(1)} dB` : '-'; const hw = node.hw_model || '-'; const alt = node.alt != null ? `${Math.round(node.alt)} m` : '-'; const lastSeen = node.last_seen ? new Date(node.last_seen * 1000).toLocaleString('de-DE') : '-'; return `
${escapeHtml(name)}
Hardware: ${escapeHtml(hw)}
Hops: ${hops}
SNR: ${snr}
Batterie: ${bat}
Höhe: ${alt}
Zuletzt: ${lastSeen}
`; } function updateMarker(node) { if (!node.lat || !node.lon) return; const id = node.node_id; nodeData[id] = node; const icon = createIcon(getHopColor(node.hop_count)); if (markers[id]) { markers[id].setLatLng([node.lat, node.lon]); markers[id].setIcon(icon); markers[id].setTooltipContent(nodeTooltip(node)); } else { markers[id] = L.marker([node.lat, node.lon], { icon }) .addTo(map) .bindTooltip(nodeTooltip(node), { direction: 'right', className: 'node-tooltip-wrap' }); } } function fitBounds() { const coords = Object.values(markers).map(m => m.getLatLng()); if (coords.length > 0) { map.fitBounds(L.latLngBounds(coords).pad(0.1)); } } function connectWebSocket() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(`${proto}//${location.host}/ws`); ws.onopen = () => { statusDot.classList.add('connected'); statusText.textContent = 'Verbunden'; }; ws.onclose = () => { statusDot.classList.remove('connected'); statusText.textContent = 'Reconnect...'; setTimeout(connectWebSocket, 3000); }; ws.onerror = () => { ws.close(); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); switch (msg.type) { case 'initial': msg.data.forEach(node => updateMarker(node)); nodeCount.textContent = `${Object.keys(markers).length} Nodes`; fitBounds(); break; case 'node_update': updateMarker(msg.data); nodeCount.textContent = `${Object.keys(markers).length} Nodes`; break; } }; } function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // Legend const legend = L.control({ position: 'bottomright' }); legend.onAdd = function () { const div = L.DomUtil.create('div', 'legend'); div.innerHTML = 'Hops'; const entries = [ [0, 'Direkt'], [1, '1 Hop'], [2, '2 Hops'], [3, '3 Hops'], [4, '4 Hops'], [5, '5+ Hops'], [null, 'Unbekannt'], ]; entries.forEach(([hop, label]) => { const color = hop != null ? (hopColors[hop] || hopColorDefault) : hopColorDefault; div.innerHTML += `
${label}
`; }); return div; }; legend.addTo(map); connectWebSocket();