Merge pull request #23 from Roslund/traceroutes
Ability to show traceroutes on map.
This commit is contained in:
commit
07c3cc2c0d
2 changed files with 315 additions and 0 deletions
110
src/index.js
110
src/index.js
|
|
@ -146,6 +146,14 @@ app.get('/api', async (req, res) => {
|
||||||
"path": "/api/v1/nodes/:nodeId/traceroutes",
|
"path": "/api/v1/nodes/:nodeId/traceroutes",
|
||||||
"description": "Trace routes for a meshtastic node",
|
"description": "Trace routes for a meshtastic node",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "/api/v1/traceroutes",
|
||||||
|
"description": "Recent traceroute edges across all nodes",
|
||||||
|
"params": {
|
||||||
|
"time_from": "Only include traceroutes updated after this unix timestamp (milliseconds)",
|
||||||
|
"time_to": "Only include traceroutes updated 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",
|
||||||
|
|
@ -566,6 +574,108 @@ app.get('/api/v1/nodes/:nodeId/traceroutes', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Aggregated recent traceroute edges (global), filtered by updated_at
|
||||||
|
// Returns deduplicated edges with the latest SNR and timestamp.
|
||||||
|
// GET /api/v1/nodes/traceroutes?time_from=...&time_to=...
|
||||||
|
app.get('/api/v1/traceroutes', 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;
|
||||||
|
|
||||||
|
// Pull recent traceroutes within the time window. We only want replies (want_response=false)
|
||||||
|
// and those that were actually gated to MQTT (gateway_id not null)
|
||||||
|
const traces = await prisma.traceRoute.findMany({
|
||||||
|
where: {
|
||||||
|
want_response: false,
|
||||||
|
gateway_id: { not: null },
|
||||||
|
updated_at: {
|
||||||
|
gte: timeFrom ? new Date(timeFrom) : undefined,
|
||||||
|
lte: timeTo ? new Date(timeTo) : undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
take: 5000, // cap to keep response bounded; UI can page/adjust time window if needed
|
||||||
|
});
|
||||||
|
|
||||||
|
// Normalize JSON fields that may be strings (depending on driver)
|
||||||
|
const normalized = traces.map((t) => {
|
||||||
|
const trace = { ...t };
|
||||||
|
if (typeof trace.route === 'string') {
|
||||||
|
try { trace.route = JSON.parse(trace.route); } catch(_) {}
|
||||||
|
}
|
||||||
|
if (typeof trace.route_back === 'string') {
|
||||||
|
try { trace.route_back = JSON.parse(trace.route_back); } catch(_) {}
|
||||||
|
}
|
||||||
|
if (typeof trace.snr_towards === 'string') {
|
||||||
|
try { trace.snr_towards = JSON.parse(trace.snr_towards); } catch(_) {}
|
||||||
|
}
|
||||||
|
if (typeof trace.snr_back === 'string') {
|
||||||
|
try { trace.snr_back = JSON.parse(trace.snr_back); } catch(_) {}
|
||||||
|
}
|
||||||
|
return trace;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build edges from the forward (towards) path using snr_towards
|
||||||
|
// The forward path is: to (initiator) → route[0] → route[1] → ... → from (responder?)
|
||||||
|
// snr_towards holds SNR per hop along that path.
|
||||||
|
// We only care about neighbor-like edges between consecutive hops with their SNR and updated_at.
|
||||||
|
const edgeKey = (a, b) => `${String(a)}->${String(b)}`; // directional; map layer can choose how to render
|
||||||
|
const edges = new Map();
|
||||||
|
|
||||||
|
for (const tr of normalized) {
|
||||||
|
const path = [];
|
||||||
|
if (tr.to != null) path.push(Number(tr.to));
|
||||||
|
if (Array.isArray(tr.route)) {
|
||||||
|
for (const hop of tr.route) {
|
||||||
|
if (hop != null) path.push(Number(hop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tr.from != null) path.push(Number(tr.from));
|
||||||
|
|
||||||
|
const snrs = Array.isArray(tr.snr_towards) ? tr.snr_towards : [];
|
||||||
|
|
||||||
|
for (let i = 0; i < path.length - 1; i++) {
|
||||||
|
const fromNode = path[i];
|
||||||
|
const toNode = path[i + 1];
|
||||||
|
// snr_towards aligns to hops; guard if missing
|
||||||
|
const snr = typeof snrs[i] === 'number' ? snrs[i] : null;
|
||||||
|
|
||||||
|
const key = edgeKey(fromNode, toNode);
|
||||||
|
const existing = edges.get(key);
|
||||||
|
if (!existing) {
|
||||||
|
edges.set(key, {
|
||||||
|
from: fromNode,
|
||||||
|
to: toNode,
|
||||||
|
snr: snr,
|
||||||
|
updated_at: tr.updated_at,
|
||||||
|
channel_id: tr.channel_id ?? null,
|
||||||
|
gateway_id: tr.gateway_id ?? null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Deduplicate by keeping the most recent updated_at
|
||||||
|
if (new Date(tr.updated_at) > new Date(existing.updated_at)) {
|
||||||
|
existing.snr = snr;
|
||||||
|
existing.updated_at = tr.updated_at;
|
||||||
|
existing.channel_id = tr.channel_id ?? existing.channel_id;
|
||||||
|
existing.gateway_id = tr.gateway_id ?? existing.gateway_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
traceroute_edges: Array.from(edges.values()),
|
||||||
|
});
|
||||||
|
|
||||||
|
} 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 {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1315,6 +1315,28 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- configTraceroutesMaxAgeInSeconds -->
|
||||||
|
<div class="p-2">
|
||||||
|
<label class="block text-sm font-medium text-gray-900">Traceroutes Max Age</label>
|
||||||
|
<div class="text-xs text-gray-600 mb-2">Traceroute edges older than this time are hidden. Reload to update map.</div>
|
||||||
|
<select v-model="configTraceroutesMaxAgeInSeconds" 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="null">Show All</option>
|
||||||
|
<option value="900">15 minutes</option>
|
||||||
|
<option value="1800">30 minutes</option>
|
||||||
|
<option value="3600">1 hour</option>
|
||||||
|
<option value="10800">3 hours</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="345600">4 days</option>
|
||||||
|
<option value="432000">5 days</option>
|
||||||
|
<option value="518400">6 days</option>
|
||||||
|
<option value="604800">7 days</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- configNeighboursMaxDistanceInMeters -->
|
<!-- configNeighboursMaxDistanceInMeters -->
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<label class="block text-sm font-medium text-gray-900">Neighbours Max Distance (meters)</label>
|
<label class="block text-sm font-medium text-gray-900">Neighbours Max Distance (meters)</label>
|
||||||
|
|
@ -1613,6 +1635,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getConfigTraceroutesMaxAgeInSeconds() {
|
||||||
|
const value = localStorage.getItem("config_traceroutes_max_age_in_seconds");
|
||||||
|
// default to 3 days if unset, to limit payloads
|
||||||
|
return value != null ? parseInt(value) : 259200;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfigTraceroutesMaxAgeInSeconds(value) {
|
||||||
|
if(value != null){
|
||||||
|
return localStorage.setItem("config_traceroutes_max_age_in_seconds", value);
|
||||||
|
} else {
|
||||||
|
return localStorage.removeItem("config_traceroutes_max_age_in_seconds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getConfigNeighboursMaxDistanceInMeters() {
|
function getConfigNeighboursMaxDistanceInMeters() {
|
||||||
const value = localStorage.getItem("config_neighbours_max_distance_in_meters");
|
const value = localStorage.getItem("config_neighbours_max_distance_in_meters");
|
||||||
return value != null ? parseInt(value) : null;
|
return value != null ? parseInt(value) : null;
|
||||||
|
|
@ -1649,6 +1685,7 @@
|
||||||
configNodesDisconnectedAgeInSeconds: window.getConfigNodesDisconnectedAgeInSeconds(),
|
configNodesDisconnectedAgeInSeconds: window.getConfigNodesDisconnectedAgeInSeconds(),
|
||||||
configNodesOfflineAgeInSeconds: window.getConfigNodesOfflineAgeInSeconds(),
|
configNodesOfflineAgeInSeconds: window.getConfigNodesOfflineAgeInSeconds(),
|
||||||
configWaypointsMaxAgeInSeconds: window.getConfigWaypointsMaxAgeInSeconds(),
|
configWaypointsMaxAgeInSeconds: window.getConfigWaypointsMaxAgeInSeconds(),
|
||||||
|
configTraceroutesMaxAgeInSeconds: window.getConfigTraceroutesMaxAgeInSeconds(),
|
||||||
configNeighboursMaxDistanceInMeters: window.getConfigNeighboursMaxDistanceInMeters(),
|
configNeighboursMaxDistanceInMeters: window.getConfigNeighboursMaxDistanceInMeters(),
|
||||||
configZoomLevelGoToNode: window.getConfigZoomLevelGoToNode(),
|
configZoomLevelGoToNode: window.getConfigZoomLevelGoToNode(),
|
||||||
configAutoUpdatePositionInUrl: window.getConfigAutoUpdatePositionInUrl(),
|
configAutoUpdatePositionInUrl: window.getConfigAutoUpdatePositionInUrl(),
|
||||||
|
|
@ -1685,6 +1722,7 @@
|
||||||
selectedNodePositionHistoryPolyLines: [],
|
selectedNodePositionHistoryPolyLines: [],
|
||||||
|
|
||||||
selectedTraceRoute: null,
|
selectedTraceRoute: null,
|
||||||
|
tracerouteEdges: [],
|
||||||
|
|
||||||
selectedNodeToShowNeighbours: null,
|
selectedNodeToShowNeighbours: null,
|
||||||
selectedNodeToShowNeighboursType: null,
|
selectedNodeToShowNeighboursType: null,
|
||||||
|
|
@ -2560,6 +2598,9 @@
|
||||||
configWaypointsMaxAgeInSeconds() {
|
configWaypointsMaxAgeInSeconds() {
|
||||||
window.setConfigWaypointsMaxAgeInSeconds(this.configWaypointsMaxAgeInSeconds);
|
window.setConfigWaypointsMaxAgeInSeconds(this.configWaypointsMaxAgeInSeconds);
|
||||||
},
|
},
|
||||||
|
configTraceroutesMaxAgeInSeconds() {
|
||||||
|
window.setConfigTraceroutesMaxAgeInSeconds(this.configTraceroutesMaxAgeInSeconds);
|
||||||
|
},
|
||||||
configNeighboursMaxDistanceInMeters() {
|
configNeighboursMaxDistanceInMeters() {
|
||||||
window.setConfigNeighboursMaxDistanceInMeters(this.configNeighboursMaxDistanceInMeters);
|
window.setConfigNeighboursMaxDistanceInMeters(this.configNeighboursMaxDistanceInMeters);
|
||||||
},
|
},
|
||||||
|
|
@ -2595,6 +2636,7 @@
|
||||||
var nodeMarkers = {};
|
var nodeMarkers = {};
|
||||||
var selectedNodeOutlineCircle = null;
|
var selectedNodeOutlineCircle = null;
|
||||||
var waypoints = [];
|
var waypoints = [];
|
||||||
|
var tracerouteEdgesCache = [];
|
||||||
|
|
||||||
// set map bounds to be a little more than full size to prevent panning off screen
|
// set map bounds to be a little more than full size to prevent panning off screen
|
||||||
var bounds = [
|
var bounds = [
|
||||||
|
|
@ -2669,6 +2711,7 @@
|
||||||
var nodesBackboneLayerGroup = new L.LayerGroup();
|
var nodesBackboneLayerGroup = new L.LayerGroup();
|
||||||
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();
|
||||||
|
|
||||||
// create icons
|
// create icons
|
||||||
var iconMqttConnected = L.divIcon({
|
var iconMqttConnected = L.divIcon({
|
||||||
|
|
@ -2736,6 +2779,7 @@
|
||||||
"Backbone Connection": backboneNeighboursLayerGroup,
|
"Backbone Connection": backboneNeighboursLayerGroup,
|
||||||
"Waypoints": waypointsLayerGroup,
|
"Waypoints": waypointsLayerGroup,
|
||||||
"Position History": nodePositionHistoryLayerGroup,
|
"Position History": nodePositionHistoryLayerGroup,
|
||||||
|
"Traceroutes": traceroutesLayerGroup,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
// make the "Nodes" group exclusive (use radio inputs instead of checkbox)
|
// make the "Nodes" group exclusive (use radio inputs instead of checkbox)
|
||||||
|
|
@ -2762,6 +2806,9 @@
|
||||||
if(enabledOverlayLayers.includes("Position History")){
|
if(enabledOverlayLayers.includes("Position History")){
|
||||||
nodePositionHistoryLayerGroup.addTo(map);
|
nodePositionHistoryLayerGroup.addTo(map);
|
||||||
}
|
}
|
||||||
|
if(enabledOverlayLayers.includes("Traceroutes")){
|
||||||
|
traceroutesLayerGroup.addTo(map);
|
||||||
|
}
|
||||||
|
|
||||||
// update config when map overlay is added
|
// update config when map overlay is added
|
||||||
map.on('overlayadd', function(event) {
|
map.on('overlayadd', function(event) {
|
||||||
|
|
@ -2913,6 +2960,10 @@
|
||||||
waypointsLayerGroup.clearLayers();
|
waypointsLayerGroup.clearLayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearAllTraceroutes() {
|
||||||
|
traceroutesLayerGroup.clearLayers();
|
||||||
|
}
|
||||||
|
|
||||||
function closeAllPopups() {
|
function closeAllPopups() {
|
||||||
map.eachLayer(function(layer) {
|
map.eachLayer(function(layer) {
|
||||||
if(layer.options.pane === "popupPane"){
|
if(layer.options.pane === "popupPane"){
|
||||||
|
|
@ -3048,6 +3099,47 @@
|
||||||
// show overlay for node neighbours
|
// show overlay for node neighbours
|
||||||
window._onShowNodeNeighboursWeHeardClick(node);
|
window._onShowNodeNeighboursWeHeardClick(node);
|
||||||
|
|
||||||
|
// Overlay ALL traceroute edges that terminate at this node (edge.to == node.node_id)
|
||||||
|
for (const edge of tracerouteEdgesCache) {
|
||||||
|
if (String(edge.to) !== String(node.node_id)) continue;
|
||||||
|
|
||||||
|
const fromMarker = findNodeMarkerById(edge.from);
|
||||||
|
if (!fromMarker) continue;
|
||||||
|
|
||||||
|
const snrDb = (typeof edge.snr === 'number') ? (edge.snr === -128 ? null : (Number(edge.snr) / 4)) : null;
|
||||||
|
const trColour = snrDb != null ? getColourForSnr(snrDb) : '#6b7280';
|
||||||
|
|
||||||
|
const trTooltip = (() => {
|
||||||
|
const fromNode = findNodeById(edge.from);
|
||||||
|
const toNode = findNodeById(node.node_id);
|
||||||
|
const distanceInMeters = fromMarker.getLatLng().distanceTo(nodeMarker.getLatLng()).toFixed(2);
|
||||||
|
let distance = `${distanceInMeters} meters`;
|
||||||
|
if (distanceInMeters >= 1000) {
|
||||||
|
const km = (distanceInMeters / 1000).toFixed(2);
|
||||||
|
distance = `${km} kilometers`;
|
||||||
|
}
|
||||||
|
const terrainImageUrl = getTerrainProfileImage(fromNode, toNode);
|
||||||
|
return `<b>Traceroute hop</b>`
|
||||||
|
+ `<br/>from <b>[${escapeString(fromNode.short_name)}] ${escapeString(fromNode.long_name)}</b>`
|
||||||
|
+ ` to <b>[${escapeString(toNode.short_name)}] ${escapeString(toNode.long_name)}</b>`
|
||||||
|
+ `<br/>SNR: ${snrDb != null ? snrDb + 'dB' : '?'}`
|
||||||
|
+ `<br/>Distance: ${distance}`
|
||||||
|
+ `<br/><br/>Terrain images from <a href=\"http://www.heywhatsthat.com\" target=\"_blank\">HeyWhatsThat.com</a>`
|
||||||
|
+ `<br/><a href=\"${terrainImageUrl}\" target=\"_blank\"><img src=\"${terrainImageUrl}\" width=\"100%\"></a>`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
L.polyline([
|
||||||
|
fromMarker.getLatLng(),
|
||||||
|
nodeMarker.getLatLng(),
|
||||||
|
], {
|
||||||
|
color: trColour,
|
||||||
|
opacity: 0.9,
|
||||||
|
}).arrowheads({ size: '10px', fill: true, offsets: { start: '25px', end: '25px' } })
|
||||||
|
.addTo(nodeNeighboursLayerGroup)
|
||||||
|
.bindTooltip(trTooltip, { sticky: true, opacity: 1, interactive: true })
|
||||||
|
.bindPopup(trTooltip);
|
||||||
|
}
|
||||||
|
|
||||||
// ensure we have neighbours to show
|
// ensure we have neighbours to show
|
||||||
const neighbours = node.neighbours ?? [];
|
const neighbours = node.neighbours ?? [];
|
||||||
if(neighbours.length === 0){
|
if(neighbours.length === 0){
|
||||||
|
|
@ -3171,6 +3263,47 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overlay ALL traceroute edges that originate from this node (edge.from == node.node_id)
|
||||||
|
for (const edge of tracerouteEdgesCache) {
|
||||||
|
if (String(edge.from) !== String(node.node_id)) continue;
|
||||||
|
|
||||||
|
const toMarker = findNodeMarkerById(edge.to);
|
||||||
|
if (!toMarker) continue;
|
||||||
|
|
||||||
|
const snrDb = (typeof edge.snr === 'number') ? (edge.snr === -128 ? null : (Number(edge.snr) / 4)) : null;
|
||||||
|
const trColour = snrDb != null ? getColourForSnr(snrDb) : '#6b7280';
|
||||||
|
|
||||||
|
const trTooltip2 = (() => {
|
||||||
|
const fromNode = findNodeById(node.node_id);
|
||||||
|
const toNode = findNodeById(edge.to);
|
||||||
|
const distanceInMeters = nodeMarker.getLatLng().distanceTo(toMarker.getLatLng()).toFixed(2);
|
||||||
|
let distance = `${distanceInMeters} meters`;
|
||||||
|
if (distanceInMeters >= 1000) {
|
||||||
|
const km = (distanceInMeters / 1000).toFixed(2);
|
||||||
|
distance = `${km} kilometers`;
|
||||||
|
}
|
||||||
|
const terrainImageUrl = getTerrainProfileImage(fromNode, toNode);
|
||||||
|
return `<b>Traceroute hop</b>`
|
||||||
|
+ `<br/>from <b>[${escapeString(fromNode.short_name)}] ${escapeString(fromNode.long_name)}</b>`
|
||||||
|
+ ` to <b>[${escapeString(toNode.short_name)}] ${escapeString(toNode.long_name)}</b>`
|
||||||
|
+ `<br/>SNR: ${snrDb != null ? snrDb + 'dB' : '?'}`
|
||||||
|
+ `<br/>Distance: ${distance}`
|
||||||
|
+ `<br/><br/>Terrain images from <a href=\"http://www.heywhatsthat.com\" target=\"_blank\">HeyWhatsThat.com</a>`
|
||||||
|
+ `<br/><a href=\"${terrainImageUrl}\" target=\"_blank\"><img src=\"${terrainImageUrl}\" width=\"100%\"></a>`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
L.polyline([
|
||||||
|
nodeMarker.getLatLng(),
|
||||||
|
toMarker.getLatLng(),
|
||||||
|
], {
|
||||||
|
color: trColour,
|
||||||
|
opacity: 0.9,
|
||||||
|
}).arrowheads({ size: '10px', fill: true, offsets: { start: '25px', end: '25px' } })
|
||||||
|
.addTo(nodeNeighboursLayerGroup)
|
||||||
|
.bindTooltip(trTooltip2, { sticky: true, opacity: 1, interactive: true })
|
||||||
|
.bindPopup(trTooltip2);
|
||||||
|
}
|
||||||
|
|
||||||
// ensure we have neighbours to show
|
// ensure we have neighbours to show
|
||||||
if(neighbourNodeInfos.length === 0){
|
if(neighbourNodeInfos.length === 0){
|
||||||
return;
|
return;
|
||||||
|
|
@ -3257,6 +3390,7 @@
|
||||||
clearAllNodes();
|
clearAllNodes();
|
||||||
clearAllNeighbours();
|
clearAllNeighbours();
|
||||||
clearAllWaypoints();
|
clearAllWaypoints();
|
||||||
|
clearAllTraceroutes();
|
||||||
clearNodeOutline();
|
clearNodeOutline();
|
||||||
cleanUpNodeNeighbours();
|
cleanUpNodeNeighbours();
|
||||||
}
|
}
|
||||||
|
|
@ -3624,6 +3758,66 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onTracerouteEdgesUpdated(edges) {
|
||||||
|
|
||||||
|
traceroutesLayerGroup.clearLayers();
|
||||||
|
|
||||||
|
tracerouteEdgesCache = edges;
|
||||||
|
|
||||||
|
for (const edge of edges) {
|
||||||
|
// Convert SNR for traceroutes: snr/4 dB; -128 means unknown
|
||||||
|
const snrDb = (typeof edge.snr === 'number')
|
||||||
|
? (edge.snr === -128 ? null : (Number(edge.snr) / 4))
|
||||||
|
: null;
|
||||||
|
const fromNode = findNodeById(edge.from);
|
||||||
|
const toNode = findNodeById(edge.to);
|
||||||
|
if (!fromNode || !toNode) continue;
|
||||||
|
|
||||||
|
const fromMarker = findNodeMarkerById(edge.from);
|
||||||
|
const toMarker = findNodeMarkerById(edge.to);
|
||||||
|
if (!fromMarker || !toMarker) continue;
|
||||||
|
|
||||||
|
const distanceInMeters = fromMarker.getLatLng().distanceTo(toMarker.getLatLng()).toFixed(2);
|
||||||
|
|
||||||
|
let distance = `${distanceInMeters} meters`;
|
||||||
|
if (distanceInMeters >= 1000) {
|
||||||
|
const km = (distanceInMeters / 1000).toFixed(2);
|
||||||
|
distance = `${km} kilometers`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colour = '#f97316';
|
||||||
|
|
||||||
|
const terrainImageUrl = getTerrainProfileImage(fromNode, toNode);
|
||||||
|
|
||||||
|
const tooltip = `Traceroute hop`
|
||||||
|
+ `<br/>from <b>[${escapeString(fromNode.short_name)}] ${escapeString(fromNode.long_name)}</b>`
|
||||||
|
+ ` to <b>[${escapeString(toNode.short_name)}] ${escapeString(toNode.long_name)}</b>`
|
||||||
|
+ `<br/>SNR: ${snrDb != null ? snrDb + 'dB' : '?'}`
|
||||||
|
+ `<br/>Distance: ${distance}`
|
||||||
|
+ (edge.updated_at ? `<br/>Updated: ${moment(new Date(edge.updated_at)).fromNow()}` : '')
|
||||||
|
+ (edge.channel_id ? `<br/>Channel: ${edge.channel_id}` : '')
|
||||||
|
+ `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`
|
||||||
|
+ `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`;
|
||||||
|
|
||||||
|
const line = L.polyline([
|
||||||
|
fromMarker.getLatLng(),
|
||||||
|
toMarker.getLatLng(),
|
||||||
|
], {
|
||||||
|
color: colour,
|
||||||
|
opacity: 0.9,
|
||||||
|
}).addTo(traceroutesLayerGroup);
|
||||||
|
|
||||||
|
line.bindTooltip(tooltip, {
|
||||||
|
sticky: true,
|
||||||
|
opacity: 1,
|
||||||
|
interactive: true,
|
||||||
|
}).bindPopup(tooltip)
|
||||||
|
.on('click', function(event) {
|
||||||
|
event.target.closeTooltip();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onPositionHistoryUpdated(updatedPositionHistories) {
|
function onPositionHistoryUpdated(updatedPositionHistories) {
|
||||||
|
|
||||||
let positionHistoryLinesCords = [];
|
let positionHistoryLinesCords = [];
|
||||||
|
|
@ -3756,6 +3950,17 @@
|
||||||
onWaypointsUpdated(response.data.waypoints);
|
onWaypointsUpdated(response.data.waypoints);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// fetch traceroute edges
|
||||||
|
const traceroutesMaxAgeSec = getConfigTraceroutesMaxAgeInSeconds();
|
||||||
|
const timeFrom = traceroutesMaxAgeSec ? (Date.now() - traceroutesMaxAgeSec * 1000) : undefined;
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (timeFrom) params.set('time_from', timeFrom);
|
||||||
|
await window.axios.get(`/api/v1/traceroutes?${params.toString()}`).then(async (response) => {
|
||||||
|
onTracerouteEdgesUpdated(response.data.traceroute_edges ?? []);
|
||||||
|
}).catch(() => {
|
||||||
|
onTracerouteEdgesUpdated([]);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRegionFrequencyRange(regionName) {
|
function getRegionFrequencyRange(regionName) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue