mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
Added the traceroute and neighbours to the map
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
<style>
|
||||
.legend {
|
||||
background: white;
|
||||
@@ -44,324 +44,321 @@
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
|
||||
<script>
|
||||
// ---- Map Setup ----
|
||||
var map = L.map('map');
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
// ---- Map Setup ----
|
||||
var map = L.map('map');
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
// ---- Node Data ----
|
||||
var markers = {};
|
||||
var markerById = {};
|
||||
var nodes = [
|
||||
{% for node in nodes %}
|
||||
{
|
||||
lat: {{ ((node.last_lat / 10**7) + (range(-9,9) | random) / 30000) | round(7) }},
|
||||
long: {{ ((node.last_long / 10**7) + (range(-9,9) | random) / 30000) |round(7) if node.last_long is not none else "null" }},
|
||||
long_name: {{ (node.long_name or "") | tojson }},
|
||||
short_name: {{ (node.short_name or "") | tojson }},
|
||||
channel: {{ (node.channel or "") | tojson }},
|
||||
hw_model: {{ (node.hw_model or "") | tojson }},
|
||||
role: {{ (node.role or "") | tojson }},
|
||||
last_update: {{ node.last_update | default("", true) | tojson }},
|
||||
firmware: {{ (node.firmware or "") | tojson }},
|
||||
id: {{ (node.node_id or "") | tojson }},
|
||||
isRouter: {{ 'true' if 'router' in (node.role or '').lower() else 'false' }}
|
||||
}{{ "," if not loop.last else "" }}
|
||||
{% endfor %}
|
||||
];
|
||||
// ---- Node Data ----
|
||||
var markers = {};
|
||||
var markerById = {};
|
||||
var nodes = [
|
||||
{% for node in nodes %}
|
||||
{
|
||||
lat: {{ ((node.last_lat / 10**7) + (range(-9,9) | random) / 30000) | round(7) }},
|
||||
long: {{ ((node.last_long / 10**7) + (range(-9,9) | random) / 30000) | round(7) if node.last_long is not none else "null" }},
|
||||
long_name: {{ (node.long_name or "") | tojson }},
|
||||
short_name: {{ (node.short_name or "") | tojson }},
|
||||
channel: {{ (node.channel or "") | tojson }},
|
||||
hw_model: {{ (node.hw_model or "") | tojson }},
|
||||
role: {{ (node.role or "") | tojson }},
|
||||
last_update: {{ node.last_update | default("", true) | tojson }},
|
||||
firmware: {{ (node.firmware or "") | tojson }},
|
||||
id: {{ (node.node_id or "") | tojson }},
|
||||
isRouter: {{ 'true' if 'router' in (node.role or '').lower() else 'false' }}
|
||||
}{{ "," if not loop.last else "" }}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
// ---- Helpers ----
|
||||
const portMap = {
|
||||
1: "Text", 67: "Telemetry", 3: "Position",
|
||||
70: "Traceroute", 4: "Node Info", 71: "Neighbour Info", 73: "Map Report"
|
||||
};
|
||||
function timeAgo(date) {
|
||||
var now = Date.now();
|
||||
var diff = now - new Date(date);
|
||||
var seconds = Math.floor(diff / 1000);
|
||||
var minutes = Math.floor(seconds / 60);
|
||||
var hours = Math.floor(minutes / 60);
|
||||
var days = Math.floor(hours / 24);
|
||||
if (days > 0) return days + "d";
|
||||
if (hours > 0) return hours + "h";
|
||||
if (minutes > 0) return minutes + "m";
|
||||
return seconds + "s";
|
||||
}
|
||||
const palette = [
|
||||
"#e6194b","#4363d8","#f58231","#911eb4","#46f0f0","#f032e6","#bcf60c","#fabebe",
|
||||
"#008080","#e6beff","#9a6324","#fffac8","#800000","#aaffc3","#808000","#ffd8b1","#000075","#808080"
|
||||
];
|
||||
const colorMap = new Map();
|
||||
let nextColorIndex = 0;
|
||||
function hashToColor(str) {
|
||||
if (colorMap.has(str)) return colorMap.get(str);
|
||||
const color = palette[nextColorIndex % palette.length];
|
||||
colorMap.set(str, color);
|
||||
nextColorIndex++;
|
||||
return color;
|
||||
}
|
||||
const nodeMap = new Map();
|
||||
nodes.forEach(n => nodeMap.set(n.id, n));
|
||||
// ---- Helpers ----
|
||||
const portMap = {
|
||||
1: "Text", 67: "Telemetry", 3: "Position",
|
||||
70: "Traceroute", 4: "Node Info", 71: "Neighbour Info", 73: "Map Report"
|
||||
};
|
||||
|
||||
function isInvalidCoord(node) {
|
||||
if (!node) return true;
|
||||
|
||||
let { lat, long } = node;
|
||||
|
||||
// Remove decimal places (round to nearest integer)
|
||||
lat = Math.round(lat);
|
||||
long = Math.round(long);
|
||||
|
||||
return (
|
||||
lat === null || long === null ||
|
||||
lat === undefined || long === undefined ||
|
||||
lat === 0 || long === 0 ||
|
||||
Number.isNaN(lat) || Number.isNaN(long)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ---- Marker Plotting ----
|
||||
var bounds = L.latLngBounds();
|
||||
var channels = new Set();
|
||||
|
||||
nodes.forEach(node => {
|
||||
if (!isInvalidCoord(node)) {
|
||||
let category = node.channel;
|
||||
channels.add(category);
|
||||
let color = hashToColor(category);
|
||||
|
||||
let markerOptions = {
|
||||
radius: node.isRouter ? 9 : 7,
|
||||
color: "white",
|
||||
fillColor: color,
|
||||
fillOpacity: 1,
|
||||
weight: 0.7
|
||||
};
|
||||
|
||||
let popupContent = `<b><a href="/packet_list/${node.id}">${node.long_name}</a> (${node.short_name})</b><br>
|
||||
<b>Channel:</b> ${node.channel}<br>
|
||||
<b>Model:</b> ${node.hw_model}<br>
|
||||
<b>Role:</b> ${node.role}<br>`;
|
||||
if (node.last_update) popupContent += `<b>Last seen:</b> ${timeAgo(node.last_update)}<br>`;
|
||||
if (node.firmware) popupContent += `<b>Firmware:</b> ${node.firmware}<br>`;
|
||||
|
||||
var marker = L.circleMarker([node.lat, node.long], markerOptions).addTo(map);
|
||||
marker.nodeId = node.id;
|
||||
marker.originalColor = color;
|
||||
markerById[node.id] = marker;
|
||||
|
||||
marker.on('click', function(e) {
|
||||
e.originalEvent.stopPropagation();
|
||||
marker.bindPopup(popupContent).openPopup();
|
||||
setTimeout(() => marker.closePopup(), 3000);
|
||||
onNodeClick(node);
|
||||
});
|
||||
|
||||
if (!markers[category]) markers[category] = [];
|
||||
markers[category].push({ marker, isRouter: node.isRouter });
|
||||
bounds.extend(marker.getLatLng());
|
||||
function timeAgo(date) {
|
||||
var now = Date.now();
|
||||
var diff = now - new Date(date);
|
||||
var seconds = Math.floor(diff / 1000);
|
||||
var minutes = Math.floor(seconds / 60);
|
||||
var hours = Math.floor(minutes / 60);
|
||||
var days = Math.floor(hours / 24);
|
||||
if (days > 0) return days + "d";
|
||||
if (hours > 0) return hours + "h";
|
||||
if (minutes > 0) return minutes + "m";
|
||||
return seconds + "s";
|
||||
}
|
||||
});
|
||||
|
||||
// Fit map bounds
|
||||
var bayAreaBounds = [
|
||||
[{{ site_config["site"]["map_top_left_lat"] }}, {{ site_config["site"]["map_top_left_lon"] }}],
|
||||
[{{ site_config["site"]["map_bottom_right_lat"] }}, {{ site_config["site"]["map_bottom_right_lon"] }}]
|
||||
];
|
||||
map.fitBounds(bayAreaBounds);
|
||||
const palette = [
|
||||
"#e6194b","#4363d8","#f58231","#911eb4","#46f0f0","#f032e6","#bcf60c","#fabebe",
|
||||
"#008080","#e6beff","#9a6324","#fffac8","#800000","#aaffc3","#808000","#ffd8b1","#000075","#808080"
|
||||
];
|
||||
const colorMap = new Map();
|
||||
let nextColorIndex = 0;
|
||||
|
||||
function hashToColor(str) {
|
||||
if (colorMap.has(str)) return colorMap.get(str);
|
||||
const color = palette[nextColorIndex % palette.length];
|
||||
colorMap.set(str, color);
|
||||
nextColorIndex++;
|
||||
return color;
|
||||
}
|
||||
|
||||
const nodeMap = new Map();
|
||||
nodes.forEach(n => nodeMap.set(n.id, n));
|
||||
|
||||
function isInvalidCoord(node) {
|
||||
if (!node) return true;
|
||||
let { lat, long } = node;
|
||||
lat = Math.round(lat);
|
||||
long = Math.round(long);
|
||||
return (
|
||||
lat === null || long === null ||
|
||||
lat === undefined || long === undefined ||
|
||||
lat === 0 || long === 0 ||
|
||||
Number.isNaN(lat) || Number.isNaN(long)
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Marker Plotting ----
|
||||
var bounds = L.latLngBounds();
|
||||
var channels = new Set();
|
||||
|
||||
// ---- Filters ----
|
||||
let filterContainer = document.getElementById("filter-container");
|
||||
channels.forEach(channel => {
|
||||
let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
|
||||
let color = hashToColor(channel);
|
||||
let label = document.createElement('label');
|
||||
label.style.color = color;
|
||||
label.innerHTML = `<input type="checkbox" class="filter-checkbox" id="${filterId}" checked> ${channel}`;
|
||||
filterContainer.appendChild(label);
|
||||
});
|
||||
function updateMarkers() {
|
||||
let showRoutersOnly = document.getElementById("filter-routers-only").checked;
|
||||
nodes.forEach(node => {
|
||||
let category = node.channel;
|
||||
let checkbox = document.getElementById(`filter-${category.replace(/\s+/g,'-').toLowerCase()}`);
|
||||
let shouldShow = checkbox.checked && (!showRoutersOnly || node.isRouter);
|
||||
let marker = markerById[node.id];
|
||||
if (shouldShow) map.addLayer(marker);
|
||||
else {
|
||||
map.removeLayer(marker);
|
||||
if (marker.tooltip) { map.removeLayer(marker.tooltip); marker.tooltip = null; }
|
||||
if (!isInvalidCoord(node)) {
|
||||
let category = node.channel;
|
||||
channels.add(category);
|
||||
let color = hashToColor(category);
|
||||
|
||||
let markerOptions = {
|
||||
radius: node.isRouter ? 9 : 7,
|
||||
color: "white",
|
||||
fillColor: color,
|
||||
fillOpacity: 1,
|
||||
weight: 0.7
|
||||
};
|
||||
|
||||
let popupContent = `<b><a href="/packet_list/${node.id}">${node.long_name}</a> (${node.short_name})</b><br>
|
||||
<b>Channel:</b> ${node.channel}<br>
|
||||
<b>Model:</b> ${node.hw_model}<br>
|
||||
<b>Role:</b> ${node.role}<br>`;
|
||||
if (node.last_update) popupContent += `<b>Last seen:</b> ${timeAgo(node.last_update)}<br>`;
|
||||
if (node.firmware) popupContent += `<b>Firmware:</b> ${node.firmware}<br>`;
|
||||
|
||||
var marker = L.circleMarker([node.lat, node.long], markerOptions).addTo(map);
|
||||
marker.nodeId = node.id;
|
||||
marker.originalColor = color;
|
||||
markerById[node.id] = marker;
|
||||
|
||||
marker.on('click', function(e) {
|
||||
e.originalEvent.stopPropagation();
|
||||
marker.bindPopup(popupContent).openPopup();
|
||||
setTimeout(() => marker.closePopup(), 3000);
|
||||
onNodeClick(node);
|
||||
});
|
||||
|
||||
if (!markers[category]) markers[category] = [];
|
||||
markers[category].push({ marker, isRouter: node.isRouter });
|
||||
bounds.extend(marker.getLatLng());
|
||||
}
|
||||
});
|
||||
}
|
||||
document.querySelectorAll(".filter-checkbox").forEach(input => {
|
||||
input.addEventListener("change", updateMarkers);
|
||||
});
|
||||
|
||||
// Fit map bounds
|
||||
var bayAreaBounds = [
|
||||
[{{ site_config["site"]["map_top_left_lat"] }}, {{ site_config["site"]["map_top_left_lon"] }}],
|
||||
[{{ site_config["site"]["map_bottom_right_lat"] }}, {{ site_config["site"]["map_bottom_right_lon"] }}]
|
||||
];
|
||||
map.fitBounds(bayAreaBounds);
|
||||
|
||||
// ---- Edges ----
|
||||
var edgeLayer = L.layerGroup().addTo(map);
|
||||
var edgesData = null;
|
||||
let selectedNodeId = null;
|
||||
// ---- Filters ----
|
||||
let filterContainer = document.getElementById("filter-container");
|
||||
channels.forEach(channel => {
|
||||
let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
|
||||
let color = hashToColor(channel);
|
||||
let label = document.createElement('label');
|
||||
label.style.color = color;
|
||||
label.innerHTML = `<input type="checkbox" class="filter-checkbox" id="${filterId}" checked> ${channel}`;
|
||||
filterContainer.appendChild(label);
|
||||
});
|
||||
|
||||
// Preload edges on page load
|
||||
fetch('/api/edges')
|
||||
.then(res => res.json())
|
||||
.then(data => { edgesData = data.edges; })
|
||||
.catch(err => console.error(err));
|
||||
|
||||
|
||||
|
||||
function onNodeClick(node) {
|
||||
// Toggle off if already selected
|
||||
if (selectedNodeId != node.id) {
|
||||
selectedNodeId = node.id;
|
||||
edgeLayer.clearLayers();
|
||||
console.log(`Clicked node: ${node.long_name}`);
|
||||
if (!edgesData) {
|
||||
console.log("Edges not loaded yet");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure edgeLayer is visible
|
||||
if (!map.hasLayer(edgeLayer)) {
|
||||
edgeLayer.addTo(map);
|
||||
}
|
||||
|
||||
edgesData.forEach(edge => {
|
||||
if (edge.from !== node.id && edge.to !== node.id) return;
|
||||
|
||||
const fromNode = nodeMap.get(edge.from);
|
||||
const toNode = nodeMap.get(edge.to);
|
||||
if (!fromNode || !toNode) return;
|
||||
if (isInvalidCoord(fromNode) || isInvalidCoord(toNode)) return;
|
||||
|
||||
const lineColor = edge.type === "neighbor" ? "red" : "blue";
|
||||
const dash = edge.type === "traceroute" ? "5,5" : null;
|
||||
const weight = edge.type === "neighbor" ? 3 : 2;
|
||||
|
||||
L.polyline(
|
||||
[[fromNode.lat, fromNode.long], [toNode.lat, toNode.long]],
|
||||
{ color: lineColor, weight, opacity: 1, dashArray: dash }
|
||||
)
|
||||
.addTo(edgeLayer)
|
||||
.bringToFront(); // Force this line to be visible
|
||||
|
||||
console.log(`Edge type: To: ${toNode.long_name} (${toNode.lat},${toNode.long})`);
|
||||
});
|
||||
|
||||
// Bring all edges to top
|
||||
edgeLayer.bringToFront();
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Clear edges when clicking map
|
||||
map.on('click', () => {
|
||||
edgeLayer.clearLayers();
|
||||
selectedNodeId = null;
|
||||
});
|
||||
|
||||
// ---- Blinking nodes ----
|
||||
var lastFetchTime = null;
|
||||
const activeBlinks = new Map();
|
||||
|
||||
function fetchLatestPacket() {
|
||||
fetch(`/api/packets?limit=1`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.packets && data.packets.length > 0) lastFetchTime = data.packets[0].import_time;
|
||||
else lastFetchTime = new Date().toISOString();
|
||||
})
|
||||
.catch(err => console.error("Error fetching latest packet:", err));
|
||||
}
|
||||
|
||||
function blinkNode(marker, longName, portnum) {
|
||||
if (!map.hasLayer(marker)) return;
|
||||
|
||||
if (activeBlinks.has(marker)) {
|
||||
clearInterval(activeBlinks.get(marker));
|
||||
marker.setStyle({ fillColor: marker.originalColor });
|
||||
if (marker.tooltip) map.removeLayer(marker.tooltip);
|
||||
}
|
||||
|
||||
let blinkCount = 0;
|
||||
let portName = portMap[portnum] || `Port ${portnum}`;
|
||||
let tooltip = L.tooltip({
|
||||
permanent: true,
|
||||
direction: 'top',
|
||||
offset: [0, -marker.options.radius - 5],
|
||||
className: 'blinking-tooltip'
|
||||
}).setContent(`${longName} (${portName})`).setLatLng(marker.getLatLng());
|
||||
tooltip.addTo(map);
|
||||
marker.tooltip = tooltip;
|
||||
|
||||
let interval = setInterval(() => {
|
||||
if (map.hasLayer(marker)) {
|
||||
marker.setStyle({ fillColor: blinkCount % 2 === 0 ? 'yellow' : marker.originalColor });
|
||||
marker.bringToFront();
|
||||
}
|
||||
blinkCount++;
|
||||
if (blinkCount > 7) {
|
||||
clearInterval(interval);
|
||||
marker.setStyle({ fillColor: marker.originalColor });
|
||||
map.removeLayer(tooltip);
|
||||
activeBlinks.delete(marker);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
activeBlinks.set(marker, interval);
|
||||
}
|
||||
|
||||
function fetchNewPackets() {
|
||||
if (!lastFetchTime) return;
|
||||
fetch(`/api/packets?since=${lastFetchTime}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (!data.packets || data.packets.length === 0) return;
|
||||
data.packets.forEach(packet => {
|
||||
let marker = markerById[packet.from_node_id];
|
||||
if (marker) {
|
||||
let nodeData = nodeMap.get(packet.from_node_id);
|
||||
if (nodeData) blinkNode(marker, nodeData.long_name, packet.portnum);
|
||||
function updateMarkers() {
|
||||
let showRoutersOnly = document.getElementById("filter-routers-only").checked;
|
||||
nodes.forEach(node => {
|
||||
let category = node.channel;
|
||||
let checkbox = document.getElementById(`filter-${category.replace(/\s+/g,'-').toLowerCase()}`);
|
||||
let shouldShow = checkbox.checked && (!showRoutersOnly || node.isRouter);
|
||||
let marker = markerById[node.id];
|
||||
if (shouldShow) map.addLayer(marker);
|
||||
else {
|
||||
map.removeLayer(marker);
|
||||
if (marker.tooltip) {
|
||||
map.removeLayer(marker.tooltip);
|
||||
marker.tooltip = null;
|
||||
}
|
||||
});
|
||||
let latestPacket = data.packets[data.packets.length - 1];
|
||||
if (latestPacket && latestPacket.import_time) lastFetchTime = latestPacket.import_time;
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
// ---- Polling Control ----
|
||||
let packetInterval = null;
|
||||
function startPacketFetcher() {
|
||||
if (!packetInterval) {
|
||||
fetchLatestPacket();
|
||||
packetInterval = setInterval(fetchNewPackets, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
function stopPacketFetcher() {
|
||||
if (packetInterval) { clearInterval(packetInterval); packetInterval = null; }
|
||||
}
|
||||
document.addEventListener("visibilitychange", function() {
|
||||
if (document.hidden) stopPacketFetcher();
|
||||
else startPacketFetcher();
|
||||
});
|
||||
|
||||
// Init
|
||||
fetchLatestPacket();
|
||||
startPacketFetcher();
|
||||
document.querySelectorAll(".filter-checkbox").forEach(input => {
|
||||
input.addEventListener("change", updateMarkers);
|
||||
});
|
||||
|
||||
// ---- Edges ----
|
||||
var edgeLayer = L.layerGroup().addTo(map);
|
||||
var edgesData = null;
|
||||
let selectedNodeId = null;
|
||||
|
||||
// Preload edges on page load
|
||||
fetch('/api/edges')
|
||||
.then(res => res.json())
|
||||
.then(data => { edgesData = data.edges; })
|
||||
.catch(err => console.error(err));
|
||||
|
||||
function onNodeClick(node) {
|
||||
if (selectedNodeId != node.id) {
|
||||
selectedNodeId = node.id;
|
||||
edgeLayer.clearLayers();
|
||||
console.log(`Clicked node: ${node.long_name}`);
|
||||
if (!edgesData) {
|
||||
console.log("Edges not loaded yet");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!map.hasLayer(edgeLayer)) edgeLayer.addTo(map);
|
||||
|
||||
edgesData.forEach(edge => {
|
||||
if (edge.from !== node.id && edge.to !== node.id) return;
|
||||
|
||||
const fromNode = nodeMap.get(edge.from);
|
||||
const toNode = nodeMap.get(edge.to);
|
||||
if (!fromNode || !toNode) return;
|
||||
if (isInvalidCoord(fromNode) || isInvalidCoord(toNode)) return;
|
||||
|
||||
const lineColor = edge.type === "neighbor" ? "red" : "blue";
|
||||
const dash = edge.type === "traceroute" ? "5,5" : null;
|
||||
const weight = edge.type === "neighbor" ? 3 : 2;
|
||||
|
||||
L.polyline(
|
||||
[[fromNode.lat, fromNode.long], [toNode.lat, toNode.long]],
|
||||
{ color: lineColor, weight, opacity: 1, dashArray: dash }
|
||||
)
|
||||
.addTo(edgeLayer)
|
||||
.bringToFront();
|
||||
|
||||
console.log(`Edge type: To: ${toNode.long_name} (${toNode.lat},${toNode.long})`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Clear edges only if the click was not on a marker
|
||||
map.on('click', function(e) {
|
||||
if (!e.originalEvent.target.classList.contains('leaflet-interactive')) {
|
||||
edgeLayer.clearLayers();
|
||||
selectedNodeId = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ---- Blinking Nodes ----
|
||||
var lastFetchTime = null;
|
||||
const activeBlinks = new Map();
|
||||
|
||||
function fetchLatestPacket() {
|
||||
fetch(`/api/packets?limit=1`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.packets && data.packets.length > 0) lastFetchTime = data.packets[0].import_time;
|
||||
else lastFetchTime = new Date().toISOString();
|
||||
})
|
||||
.catch(err => console.error("Error fetching latest packet:", err));
|
||||
}
|
||||
|
||||
function blinkNode(marker, longName, portnum) {
|
||||
if (!map.hasLayer(marker)) return;
|
||||
|
||||
if (activeBlinks.has(marker)) {
|
||||
clearInterval(activeBlinks.get(marker));
|
||||
marker.setStyle({ fillColor: marker.originalColor });
|
||||
if (marker.tooltip) map.removeLayer(marker.tooltip);
|
||||
}
|
||||
|
||||
let blinkCount = 0;
|
||||
let portName = portMap[portnum] || `Port ${portnum}`;
|
||||
let tooltip = L.tooltip({
|
||||
permanent: true,
|
||||
direction: 'top',
|
||||
offset: [0, -marker.options.radius - 5],
|
||||
className: 'blinking-tooltip'
|
||||
}).setContent(`${longName} (${portName})`).setLatLng(marker.getLatLng());
|
||||
tooltip.addTo(map);
|
||||
marker.tooltip = tooltip;
|
||||
|
||||
let interval = setInterval(() => {
|
||||
if (map.hasLayer(marker)) {
|
||||
marker.setStyle({ fillColor: blinkCount % 2 === 0 ? 'yellow' : marker.originalColor });
|
||||
marker.bringToFront();
|
||||
}
|
||||
blinkCount++;
|
||||
if (blinkCount > 7) {
|
||||
clearInterval(interval);
|
||||
marker.setStyle({ fillColor: marker.originalColor });
|
||||
map.removeLayer(tooltip);
|
||||
activeBlinks.delete(marker);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
activeBlinks.set(marker, interval);
|
||||
}
|
||||
|
||||
function fetchNewPackets() {
|
||||
if (!lastFetchTime) return;
|
||||
fetch(`/api/packets?since=${lastFetchTime}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (!data.packets || data.packets.length === 0) return;
|
||||
data.packets.forEach(packet => {
|
||||
let marker = markerById[packet.from_node_id];
|
||||
if (marker) {
|
||||
let nodeData = nodeMap.get(packet.from_node_id);
|
||||
if (nodeData) blinkNode(marker, nodeData.long_name, packet.portnum);
|
||||
}
|
||||
});
|
||||
let latestPacket = data.packets[data.packets.length - 1];
|
||||
if (latestPacket && latestPacket.import_time) lastFetchTime = latestPacket.import_time;
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
// ---- Polling Control ----
|
||||
let packetInterval = null;
|
||||
function startPacketFetcher() {
|
||||
if (!packetInterval) {
|
||||
fetchLatestPacket();
|
||||
packetInterval = setInterval(fetchNewPackets, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function stopPacketFetcher() {
|
||||
if (packetInterval) {
|
||||
clearInterval(packetInterval);
|
||||
packetInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("visibilitychange", function() {
|
||||
if (document.hidden) stopPacketFetcher();
|
||||
else startPacketFetcher();
|
||||
});
|
||||
|
||||
// Init
|
||||
fetchLatestPacket();
|
||||
startPacketFetcher();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user