Add connections endpoint and UI configuration for connections time period and colored lines
This commit is contained in:
parent
1333447398
commit
556dde517b
2 changed files with 407 additions and 3 deletions
188
src/index.js
188
src/index.js
|
|
@ -155,6 +155,14 @@ app.get('/api', async (req, res) => {
|
||||||
"time_to": "Only include traceroutes updated before this unix timestamp (milliseconds)"
|
"time_to": "Only include traceroutes updated before this unix timestamp (milliseconds)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "/api/v1/connections",
|
||||||
|
"description": "Aggregated edges between nodes from traceroutes",
|
||||||
|
"params": {
|
||||||
|
"time_from": "Only include edges created after this unix timestamp (milliseconds)",
|
||||||
|
"time_to": "Only include edges created before this unix timestamp (milliseconds)"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "/api/v1/nodes/:nodeId/position-history",
|
"path": "/api/v1/nodes/:nodeId/position-history",
|
||||||
"description": "Position history for a meshtastic node",
|
"description": "Position history for a meshtastic node",
|
||||||
|
|
@ -698,6 +706,186 @@ app.get('/api/v1/traceroutes', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Aggregated edges endpoint
|
||||||
|
// GET /api/v1/connections?time_from=...&time_to=...
|
||||||
|
app.get('/api/v1/connections', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const timeFrom = req.query.time_from ? parseInt(req.query.time_from) : undefined;
|
||||||
|
const timeTo = req.query.time_to ? parseInt(req.query.time_to) : undefined;
|
||||||
|
|
||||||
|
// Query edges from database
|
||||||
|
const edges = await prisma.edge.findMany({
|
||||||
|
where: {
|
||||||
|
created_at: {
|
||||||
|
gte: timeFrom ? new Date(timeFrom) : undefined,
|
||||||
|
lte: timeTo ? new Date(timeTo) : undefined,
|
||||||
|
},
|
||||||
|
// Only include edges where both nodes have positions
|
||||||
|
from_latitude: { not: null },
|
||||||
|
from_longitude: { not: null },
|
||||||
|
to_latitude: { not: null },
|
||||||
|
to_longitude: { not: null },
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{ created_at: 'desc' },
|
||||||
|
{ packet_id: 'desc' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collect all unique node IDs from edges
|
||||||
|
const nodeIds = new Set();
|
||||||
|
for (const edge of edges) {
|
||||||
|
nodeIds.add(edge.from_node_id);
|
||||||
|
nodeIds.add(edge.to_node_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch current positions for all nodes
|
||||||
|
const nodes = await prisma.node.findMany({
|
||||||
|
where: {
|
||||||
|
node_id: { in: Array.from(nodeIds) },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
node_id: true,
|
||||||
|
latitude: true,
|
||||||
|
longitude: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a map of current node positions
|
||||||
|
const nodePositions = new Map();
|
||||||
|
for (const node of nodes) {
|
||||||
|
nodePositions.set(node.node_id, {
|
||||||
|
latitude: node.latitude,
|
||||||
|
longitude: node.longitude,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter edges: only include edges where both nodes are still at the same location
|
||||||
|
const validEdges = edges.filter(edge => {
|
||||||
|
const fromCurrentPos = nodePositions.get(edge.from_node_id);
|
||||||
|
const toCurrentPos = nodePositions.get(edge.to_node_id);
|
||||||
|
|
||||||
|
// Skip if either node doesn't exist or doesn't have a current position
|
||||||
|
if (!fromCurrentPos || !toCurrentPos ||
|
||||||
|
fromCurrentPos.latitude === null || fromCurrentPos.longitude === null ||
|
||||||
|
toCurrentPos.latitude === null || toCurrentPos.longitude === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if stored positions match current positions
|
||||||
|
const fromMatches = fromCurrentPos.latitude === edge.from_latitude &&
|
||||||
|
fromCurrentPos.longitude === edge.from_longitude;
|
||||||
|
const toMatches = toCurrentPos.latitude === edge.to_latitude &&
|
||||||
|
toCurrentPos.longitude === edge.to_longitude;
|
||||||
|
|
||||||
|
return fromMatches && toMatches;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Normalize node pairs: always use min/max to treat A->B and B->A as same connection
|
||||||
|
const connectionsMap = new Map();
|
||||||
|
|
||||||
|
for (const edge of validEdges) {
|
||||||
|
const nodeA = edge.from_node_id < edge.to_node_id ? edge.from_node_id : edge.to_node_id;
|
||||||
|
const nodeB = edge.from_node_id < edge.to_node_id ? edge.to_node_id : edge.from_node_id;
|
||||||
|
const key = `${nodeA}-${nodeB}`;
|
||||||
|
|
||||||
|
if (!connectionsMap.has(key)) {
|
||||||
|
connectionsMap.set(key, {
|
||||||
|
node_a: nodeA,
|
||||||
|
node_b: nodeB,
|
||||||
|
direction_ab: [], // A -> B edges
|
||||||
|
direction_ba: [], // B -> A edges
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = connectionsMap.get(key);
|
||||||
|
const isAB = edge.from_node_id === nodeA;
|
||||||
|
|
||||||
|
// Add edge to appropriate direction
|
||||||
|
if (isAB) {
|
||||||
|
connection.direction_ab.push({
|
||||||
|
snr: edge.snr,
|
||||||
|
snr_db: edge.snr / 4, // Convert to dB
|
||||||
|
created_at: edge.created_at,
|
||||||
|
packet_id: edge.packet_id,
|
||||||
|
source: edge.source,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
connection.direction_ba.push({
|
||||||
|
snr: edge.snr,
|
||||||
|
snr_db: edge.snr / 4,
|
||||||
|
created_at: edge.created_at,
|
||||||
|
packet_id: edge.packet_id,
|
||||||
|
source: edge.source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate each connection
|
||||||
|
const connections = Array.from(connectionsMap.values()).map(conn => {
|
||||||
|
// Deduplicate edges by packet_id for each direction (keep first occurrence, which is most recent)
|
||||||
|
const dedupeByPacketId = (edges) => {
|
||||||
|
const seen = new Set();
|
||||||
|
return edges.filter(edge => {
|
||||||
|
if (seen.has(edge.packet_id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen.add(edge.packet_id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deduplicatedAB = dedupeByPacketId(conn.direction_ab);
|
||||||
|
const deduplicatedBA = dedupeByPacketId(conn.direction_ba);
|
||||||
|
|
||||||
|
// Calculate average SNR for A->B (using deduplicated edges)
|
||||||
|
const avgSnrAB = deduplicatedAB.length > 0
|
||||||
|
? deduplicatedAB.reduce((sum, e) => sum + e.snr_db, 0) / deduplicatedAB.length
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Calculate average SNR for B->A (using deduplicated edges)
|
||||||
|
const avgSnrBA = deduplicatedBA.length > 0
|
||||||
|
? deduplicatedBA.reduce((sum, e) => sum + e.snr_db, 0) / deduplicatedBA.length
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Get last 5 edges for each direction (already sorted by created_at DESC, packet_id DESC, now deduplicated)
|
||||||
|
const last5AB = deduplicatedAB.slice(0, 5);
|
||||||
|
const last5BA = deduplicatedBA.slice(0, 5);
|
||||||
|
|
||||||
|
// Determine worst average SNR
|
||||||
|
const worstAvgSnrDb = [avgSnrAB, avgSnrBA]
|
||||||
|
.filter(v => v !== null)
|
||||||
|
.reduce((min, val) => val < min ? val : min, Infinity);
|
||||||
|
|
||||||
|
return {
|
||||||
|
node_a: conn.node_a,
|
||||||
|
node_b: conn.node_b,
|
||||||
|
direction_ab: {
|
||||||
|
avg_snr_db: avgSnrAB,
|
||||||
|
last_5_edges: last5AB,
|
||||||
|
total_count: deduplicatedAB.length, // Use deduplicated count
|
||||||
|
},
|
||||||
|
direction_ba: {
|
||||||
|
avg_snr_db: avgSnrBA,
|
||||||
|
last_5_edges: last5BA,
|
||||||
|
total_count: deduplicatedBA.length, // Use deduplicated count
|
||||||
|
},
|
||||||
|
worst_avg_snr_db: worstAvgSnrDb !== Infinity ? worstAvgSnrDb : null,
|
||||||
|
};
|
||||||
|
}).filter(conn => conn.worst_avg_snr_db !== null); // Only return connections with at least one direction
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
connections: connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({
|
||||||
|
message: "Something went wrong, try again later.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/v1/nodes/:nodeId/position-history', async (req, res) => {
|
app.get('/api/v1/nodes/:nodeId/position-history', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1440,6 +1440,36 @@
|
||||||
<div class="text-xs text-gray-600">Map will animate flying to nodes.</div>
|
<div class="text-xs text-gray-600">Map will animate flying to nodes.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- configConnectionsTimePeriodInSeconds -->
|
||||||
|
<div class="p-2">
|
||||||
|
<label class="block text-sm font-medium text-gray-900">Connections Time Period</label>
|
||||||
|
<div class="text-xs text-gray-600 mb-2">Edges within this time period are shown in the Connections layer. Reload to update map.</div>
|
||||||
|
<select v-model="configConnectionsTimePeriodInSeconds" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
<option value="300">5 minutes</option>
|
||||||
|
<option value="900">15 minutes</option>
|
||||||
|
<option value="3600">1 hour</option>
|
||||||
|
<option value="21600">6 hours</option>
|
||||||
|
<option value="43200">12 hours</option>
|
||||||
|
<option value="86400">24 hours</option>
|
||||||
|
<option value="172800">2 days</option>
|
||||||
|
<option value="259200">3 days</option>
|
||||||
|
<option value="604800">7 days</option>
|
||||||
|
<option value="1209600">14 days</option>
|
||||||
|
<option value="2592000">30 days</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- configConnectionsColoredLines -->
|
||||||
|
<div class="p-2">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex items-center h-5">
|
||||||
|
<input type="checkbox" v-model="configConnectionsColoredLines" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300" required>
|
||||||
|
</div>
|
||||||
|
<label class="ml-2 text-sm font-medium text-gray-900">Colored Connection Lines</label>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600">Colors the connection lines by the average SNR in the worst direction. Reload to update map.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1725,6 +1755,29 @@
|
||||||
return localStorage.setItem("config_zoom_level_go_to_node", value);
|
return localStorage.setItem("config_zoom_level_go_to_node", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getConfigConnectionsTimePeriodInSeconds() {
|
||||||
|
const value = localStorage.getItem("config_connections_time_period_in_seconds");
|
||||||
|
// default to 24 hours if unset
|
||||||
|
return value != null ? parseInt(value) : 86400;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfigConnectionsTimePeriodInSeconds(value) {
|
||||||
|
return localStorage.setItem("config_connections_time_period_in_seconds", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfigConnectionsColoredLines() {
|
||||||
|
const value = localStorage.getItem("config_connections_colored_lines");
|
||||||
|
// disable colored lines by default
|
||||||
|
if(value === null){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return value === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfigConnectionsColoredLines(value) {
|
||||||
|
return localStorage.setItem("config_connections_colored_lines", value);
|
||||||
|
}
|
||||||
|
|
||||||
function isMobile() {
|
function isMobile() {
|
||||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
@ -1748,6 +1801,8 @@
|
||||||
configAutoUpdatePositionInUrl: window.getConfigAutoUpdatePositionInUrl(),
|
configAutoUpdatePositionInUrl: window.getConfigAutoUpdatePositionInUrl(),
|
||||||
configEnableMapAnimations: window.getConfigEnableMapAnimations(),
|
configEnableMapAnimations: window.getConfigEnableMapAnimations(),
|
||||||
configTemperatureFormat: window.getConfigTemperatureFormat(),
|
configTemperatureFormat: window.getConfigTemperatureFormat(),
|
||||||
|
configConnectionsTimePeriodInSeconds: window.getConfigConnectionsTimePeriodInSeconds(),
|
||||||
|
configConnectionsColoredLines: window.getConfigConnectionsColoredLines(),
|
||||||
|
|
||||||
isShowingHardwareModels: false,
|
isShowingHardwareModels: false,
|
||||||
hardwareModelStats: null,
|
hardwareModelStats: null,
|
||||||
|
|
@ -2673,6 +2728,12 @@
|
||||||
configTemperatureFormat() {
|
configTemperatureFormat() {
|
||||||
window.setConfigTemperatureFormat(this.configTemperatureFormat);
|
window.setConfigTemperatureFormat(this.configTemperatureFormat);
|
||||||
},
|
},
|
||||||
|
configConnectionsTimePeriodInSeconds() {
|
||||||
|
window.setConfigConnectionsTimePeriodInSeconds(this.configConnectionsTimePeriodInSeconds);
|
||||||
|
},
|
||||||
|
configConnectionsColoredLines() {
|
||||||
|
window.setConfigConnectionsColoredLines(this.configConnectionsColoredLines);
|
||||||
|
},
|
||||||
deviceMetricsTimeRange() {
|
deviceMetricsTimeRange() {
|
||||||
this.loadNodeDeviceMetrics(this.selectedNode.node_id);
|
this.loadNodeDeviceMetrics(this.selectedNode.node_id);
|
||||||
},
|
},
|
||||||
|
|
@ -2771,6 +2832,7 @@
|
||||||
var waypointsLayerGroup = new L.LayerGroup();
|
var waypointsLayerGroup = new L.LayerGroup();
|
||||||
var nodePositionHistoryLayerGroup = new L.LayerGroup();
|
var nodePositionHistoryLayerGroup = new L.LayerGroup();
|
||||||
var traceroutesLayerGroup = new L.LayerGroup();
|
var traceroutesLayerGroup = new L.LayerGroup();
|
||||||
|
var connectionsLayerGroup = new L.LayerGroup();
|
||||||
|
|
||||||
// create icons
|
// create icons
|
||||||
var iconMqttConnected = L.divIcon({
|
var iconMqttConnected = L.divIcon({
|
||||||
|
|
@ -2859,6 +2921,7 @@
|
||||||
"Legend": legendLayerGroup,
|
"Legend": legendLayerGroup,
|
||||||
"Neighbours": neighboursLayerGroup,
|
"Neighbours": neighboursLayerGroup,
|
||||||
"Backbone Connection": backboneNeighboursLayerGroup,
|
"Backbone Connection": backboneNeighboursLayerGroup,
|
||||||
|
"Connections": connectionsLayerGroup,
|
||||||
"Waypoints": waypointsLayerGroup,
|
"Waypoints": waypointsLayerGroup,
|
||||||
"Position History": nodePositionHistoryLayerGroup,
|
"Position History": nodePositionHistoryLayerGroup,
|
||||||
"Traceroutes": traceroutesLayerGroup,
|
"Traceroutes": traceroutesLayerGroup,
|
||||||
|
|
@ -2882,6 +2945,9 @@
|
||||||
if(enabledOverlayLayers.includes("Backbone Connection")){
|
if(enabledOverlayLayers.includes("Backbone Connection")){
|
||||||
backboneNeighboursLayerGroup.addTo(map);
|
backboneNeighboursLayerGroup.addTo(map);
|
||||||
}
|
}
|
||||||
|
if(enabledOverlayLayers.includes("Connections")){
|
||||||
|
connectionsLayerGroup.addTo(map);
|
||||||
|
}
|
||||||
if(enabledOverlayLayers.includes("Waypoints")){
|
if(enabledOverlayLayers.includes("Waypoints")){
|
||||||
waypointsLayerGroup.addTo(map);
|
waypointsLayerGroup.addTo(map);
|
||||||
}
|
}
|
||||||
|
|
@ -3053,6 +3119,10 @@
|
||||||
traceroutesLayerGroup.clearLayers();
|
traceroutesLayerGroup.clearLayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearAllConnections() {
|
||||||
|
connectionsLayerGroup.clearLayers();
|
||||||
|
}
|
||||||
|
|
||||||
function closeAllPopups() {
|
function closeAllPopups() {
|
||||||
map.eachLayer(function(layer) {
|
map.eachLayer(function(layer) {
|
||||||
if(layer.options.pane === "popupPane"){
|
if(layer.options.pane === "popupPane"){
|
||||||
|
|
@ -3120,9 +3190,40 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColourForSnr(snr) {
|
function getColourForSnr(snr) {
|
||||||
if(snr >= -5) return "#16a34a"; // good
|
if(snr >= -4) return "#16a34a"; // good
|
||||||
if(snr > -15) return "#fff200"; // meh
|
if(snr > -8) return "#fff200"; // medium-good
|
||||||
if(snr <= -15) return "#dc2626"; // bad
|
if(snr > -12) return "#ff9f1c"; // medium
|
||||||
|
return "#dc2626"; // bad
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSignalBarsIndicator(snrDb) {
|
||||||
|
if(snrDb == null) return '';
|
||||||
|
|
||||||
|
// Determine number of bars based on SNR
|
||||||
|
let bars = 0;
|
||||||
|
if(snrDb >= -4) bars = 4; // good
|
||||||
|
else if(snrDb > -8) bars = 3; // medium-good
|
||||||
|
else if(snrDb > -12) bars = 2; // medium
|
||||||
|
else bars = 1; // bad
|
||||||
|
|
||||||
|
const color = getColourForSnr(snrDb);
|
||||||
|
|
||||||
|
// Create 4 bars with increasing height
|
||||||
|
let indicator = '<span style="display: inline-flex; align-items: flex-end; gap: 2px; height: 12px; vertical-align: middle; margin-left: 4px;">';
|
||||||
|
|
||||||
|
// Bar heights: 4px, 6px, 8px, 10px
|
||||||
|
const barHeights = [4, 6, 8, 10];
|
||||||
|
const barWidth = 2;
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
const height = barHeights[i];
|
||||||
|
const isActive = i < bars;
|
||||||
|
const barColor = isActive ? color : '#d1d5db'; // gray for inactive bars
|
||||||
|
indicator += `<span style="width: ${barWidth}px; height: ${height}px; background-color: ${barColor}; border-radius: 1px; display: inline-block;"></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator += '</span>';
|
||||||
|
return indicator;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanUpNodeNeighbours() {
|
function cleanUpNodeNeighbours() {
|
||||||
|
|
@ -3492,6 +3593,7 @@
|
||||||
clearAllNeighbours();
|
clearAllNeighbours();
|
||||||
clearAllWaypoints();
|
clearAllWaypoints();
|
||||||
clearAllTraceroutes();
|
clearAllTraceroutes();
|
||||||
|
clearAllConnections();
|
||||||
clearNodeOutline();
|
clearNodeOutline();
|
||||||
cleanUpNodeNeighbours();
|
cleanUpNodeNeighbours();
|
||||||
}
|
}
|
||||||
|
|
@ -3968,6 +4070,109 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onConnectionsUpdated(connections) {
|
||||||
|
// Clear existing connections
|
||||||
|
clearAllConnections();
|
||||||
|
|
||||||
|
for (const connection of connections) {
|
||||||
|
// Find both node markers
|
||||||
|
const nodeAMarker = findNodeMarkerById(connection.node_a);
|
||||||
|
const nodeBMarker = findNodeMarkerById(connection.node_b);
|
||||||
|
|
||||||
|
// Skip if either node marker doesn't exist
|
||||||
|
if (!nodeAMarker || !nodeBMarker) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find node objects for names and terrain profile
|
||||||
|
const nodeA = findNodeById(connection.node_a);
|
||||||
|
const nodeB = findNodeById(connection.node_b);
|
||||||
|
|
||||||
|
if (!nodeA || !nodeB) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate distance between nodes
|
||||||
|
const distanceInMeters = nodeAMarker.getLatLng().distanceTo(nodeBMarker.getLatLng()).toFixed(2);
|
||||||
|
let distance = `${distanceInMeters} meters`;
|
||||||
|
if (distanceInMeters >= 1000) {
|
||||||
|
const distanceInKilometers = (distanceInMeters / 1000).toFixed(2);
|
||||||
|
distance = `${distanceInKilometers} kilometers`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine line color based on worst average SNR (if colored lines enabled)
|
||||||
|
const configConnectionsColoredLines = getConfigConnectionsColoredLines();
|
||||||
|
const worstSnrDb = connection.worst_avg_snr_db;
|
||||||
|
const lineColor = configConnectionsColoredLines && worstSnrDb != null ? getColourForSnr(worstSnrDb) : '#2563eb';
|
||||||
|
|
||||||
|
// Create polyline (bidirectional, no arrows)
|
||||||
|
const line = L.polyline([
|
||||||
|
nodeAMarker.getLatLng(),
|
||||||
|
nodeBMarker.getLatLng(),
|
||||||
|
], {
|
||||||
|
color: lineColor,
|
||||||
|
opacity: 0.75,
|
||||||
|
weight: 3,
|
||||||
|
}).addTo(connectionsLayerGroup);
|
||||||
|
|
||||||
|
// Generate tooltip
|
||||||
|
let tooltip = `<b>Connection</b>`;
|
||||||
|
tooltip += `<br/>[${escapeString(nodeA.short_name)}] ${escapeString(nodeA.long_name)} <-> [${escapeString(nodeB.short_name)}] ${escapeString(nodeB.long_name)}`;
|
||||||
|
tooltip += `<br/>Distance: ${distance}`;
|
||||||
|
tooltip += `<br/>`;
|
||||||
|
|
||||||
|
// Direction A -> B
|
||||||
|
if (connection.direction_ab.avg_snr_db != null) {
|
||||||
|
tooltip += `<br/><b>${escapeString(nodeA.short_name)} -> ${escapeString(nodeB.short_name)}:</b>`;
|
||||||
|
tooltip += `<br/>SNR: ${connection.direction_ab.avg_snr_db.toFixed(1)}dB ${getSignalBarsIndicator(connection.direction_ab.avg_snr_db)} (Average of ${connection.direction_ab.total_count} edges)`;
|
||||||
|
if (connection.direction_ab.last_5_edges.length > 0) {
|
||||||
|
tooltip += `<br/>Last 5 edges:`;
|
||||||
|
for (const edge of connection.direction_ab.last_5_edges) {
|
||||||
|
const timeAgo = moment(new Date(edge.created_at)).fromNow();
|
||||||
|
console.log(edge.source);
|
||||||
|
const sourceIcon = edge.source === "TRACEROUTE_APP" ? "⇵" : (edge.source === "NEIGHBORINFO_APP" ? "✳" : "?");
|
||||||
|
tooltip += `<br/> ${edge.snr_db.toFixed(1)}dB ${getSignalBarsIndicator(edge.snr_db)} (${timeAgo} by:${sourceIcon})`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tooltip += `<br/>No recent edges`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direction B -> A
|
||||||
|
if (connection.direction_ba.avg_snr_db != null) {
|
||||||
|
tooltip += `<br/><br/><b>${escapeString(nodeB.short_name)} -> ${escapeString(nodeA.short_name)}:</b>`;
|
||||||
|
tooltip += `<br/>SNR: ${connection.direction_ba.avg_snr_db.toFixed(1)}dB ${getSignalBarsIndicator(connection.direction_ba.avg_snr_db)} (Average of ${connection.direction_ba.total_count} edges)`;
|
||||||
|
if (connection.direction_ba.last_5_edges.length > 0) {
|
||||||
|
tooltip += `<br/>Last 5 edges:`;
|
||||||
|
for (const edge of connection.direction_ba.last_5_edges) {
|
||||||
|
const timeAgo = moment(new Date(edge.created_at)).fromNow();
|
||||||
|
const sourceIcon = edge.source === "TRACEROUTE_APP" ? "⇵" : (edge.source === "NEIGHBORINFO_APP" ? "✳" : "?");
|
||||||
|
tooltip += `<br/> ${edge.snr_db.toFixed(1)}dB ${getSignalBarsIndicator(edge.snr_db)} (${timeAgo} by:${sourceIcon})`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tooltip += `<br/>No recent edges`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add terrain profile image
|
||||||
|
const terrainImageUrl = getTerrainProfileImage(nodeA, nodeB);
|
||||||
|
tooltip += `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`;
|
||||||
|
tooltip += `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`;
|
||||||
|
|
||||||
|
// Bind tooltip and popup
|
||||||
|
line.bindTooltip(tooltip, {
|
||||||
|
sticky: true,
|
||||||
|
opacity: 1,
|
||||||
|
interactive: true,
|
||||||
|
})
|
||||||
|
.bindPopup(tooltip)
|
||||||
|
.on('click', function(event) {
|
||||||
|
// close tooltip on click to prevent tooltip and popup showing at same time
|
||||||
|
event.target.closeTooltip();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onPositionHistoryUpdated(updatedPositionHistories) {
|
function onPositionHistoryUpdated(updatedPositionHistories) {
|
||||||
|
|
||||||
let positionHistoryLinesCords = [];
|
let positionHistoryLinesCords = [];
|
||||||
|
|
@ -4111,6 +4316,17 @@
|
||||||
onTracerouteEdgesUpdated([]);
|
onTracerouteEdgesUpdated([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// fetch connections (edges)
|
||||||
|
const connectionsTimePeriodSec = getConfigConnectionsTimePeriodInSeconds();
|
||||||
|
const connectionsTimeFrom = connectionsTimePeriodSec ? (Date.now() - connectionsTimePeriodSec * 1000) : undefined;
|
||||||
|
const connectionsParams = new URLSearchParams();
|
||||||
|
if (connectionsTimeFrom) connectionsParams.set('time_from', connectionsTimeFrom);
|
||||||
|
await window.axios.get(`/api/v1/connections?${connectionsParams.toString()}`).then(async (response) => {
|
||||||
|
onConnectionsUpdated(response.data.connections ?? []);
|
||||||
|
}).catch(() => {
|
||||||
|
onConnectionsUpdated([]);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRegionFrequencyRange(regionName) {
|
function getRegionFrequencyRange(regionName) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue