diff --git a/lib/forcegraph.ts b/lib/forcegraph.ts index f3b98842..93820ed7 100644 --- a/lib/forcegraph.ts +++ b/lib/forcegraph.ts @@ -7,6 +7,7 @@ import * as d3Timer from "d3-timer"; import * as d3Zoom from "d3-zoom"; import math from "./utils/math.js"; +import * as helper from "./utils/helper.js"; import draw, { MapLink } from "./forcegraph/draw.js"; import { Sidebar } from "./sidebar.js"; import { ClientPointEvent } from "d3-selection"; @@ -158,7 +159,8 @@ export const ForceGraph = function (linkScale: (t: any) => any, sidebar: ReturnT return 0.02; } // @ts-ignore - return Math.max(0.5, node.o.source_tq); + let metric = helper.linkMetric(node.o.source_tq, node.o.source_tp) ?? 0; + return Math.max(0.5, metric); }); let zoom = d3Zoom @@ -244,8 +246,8 @@ export const ForceGraph = function (linkScale: (t: any) => any, sidebar: ReturnT o: link, source: dictNodes[link.source.node_id], target: dictNodes[link.target.node_id], - color: linkScale(link.source_tq), - color_to: linkScale(link.target_tq), + color: linkScale(helper.linkMetric(link.source_tq, link.source_tp) ?? 0), + color_to: linkScale(helper.linkMetric(link.target_tq, link.target_tp) ?? 0), }; }); diff --git a/lib/forcegraph/draw.ts b/lib/forcegraph/draw.ts index 4ff66c2d..8818605d 100644 --- a/lib/forcegraph/draw.ts +++ b/lib/forcegraph/draw.ts @@ -133,7 +133,10 @@ self.drawLink = function drawLink(link: MapLink) { } else if (link.o.type.indexOf("other") === 0) { ctx.globalAlpha = 1; ctx.lineWidth = 3.5; - if (link.o.source_tq >= 0.99 && link.o.target_tq >= 0.99) { + if ( + (helper.linkMetric(link.o.source_tq, link.o.source_tp) ?? 0) >= 0.99 && + (helper.linkMetric(link.o.target_tq, link.o.target_tp) ?? 0) >= 0.99 + ) { link.color = config.forceGraph.otherLinkColor; link.color_to = config.forceGraph.otherLinkColor; } diff --git a/lib/infobox/link.ts b/lib/infobox/link.ts index 8e07da48..c0641103 100644 --- a/lib/infobox/link.ts +++ b/lib/infobox/link.ts @@ -79,11 +79,19 @@ export const Link = function (el: HTMLElement, linkData: LinkData[], linkScale: ); helper.attributeEntry( children, - "node.tq", + (link.source_tp ?? 0) > 0 || (link.target_tp ?? 0) > 0 ? "node.throughput" : "node.tq", h( "span", - { style: { color: linkScale((link.source_tq + link.target_tq) / 2) } }, - helper.showTq(link.source_tq) + " - " + helper.showTq(link.target_tq), + { + style: { + color: linkScale( + ((helper.linkMetric(link.source_tq, link.source_tp) ?? 0) + + (helper.linkMetric(link.target_tq, link.target_tp) ?? 0)) / + 2, + ), + }, + }, + helper.showBiDiLinkMetric(link.source_tq, link.source_tp, link.target_tq, link.target_tp), ), ); diff --git a/lib/infobox/node.ts b/lib/infobox/node.ts index 3e090f14..ca44e438 100644 --- a/lib/infobox/node.ts +++ b/lib/infobox/node.ts @@ -108,7 +108,11 @@ export function Node(el: HTMLElement, node: NodeData, linkScale: (t: any) => any "a", { style: { - color: linkScale((connecting.link.source_tq + connecting.link.target_tq) / 2), + color: linkScale( + ((helper.linkMetric(connecting.link.source_tq, connecting.link.source_tp) ?? 0) + + (helper.linkMetric(connecting.link.target_tq, connecting.link.target_tp) ?? 0)) / + 2, + ), }, props: { title: connecting.link.source.hostname + " - " + connecting.link.target.hostname, @@ -120,7 +124,12 @@ export function Node(el: HTMLElement, node: NodeData, linkScale: (t: any) => any }, }, }, - helper.showTq(connecting.link.source_tq) + " - " + helper.showTq(connecting.link.target_tq), + helper.showBiDiLinkMetric( + connecting.link.source_tq, + connecting.link.source_tp, + connecting.link.target_tq, + connecting.link.target_tp, + ), ), ]), h("td", helper.showDistance(connecting.link)), @@ -158,7 +167,13 @@ export function Node(el: HTMLElement, node: NodeData, linkScale: (t: any) => any name: "node.tq", class: "ion-connection-bars", sort: function (a: Neighbour, b: Neighbour) { - return a.link.source_tq - b.link.source_tq; + let am = helper.linkMetric(a.link.source_tq, a.link.source_tp); + let an = helper.linkMetric(a.link.target_tq, a.link.target_tp); + let bm = helper.linkMetric(b.link.source_tq, b.link.source_tp); + let bn = helper.linkMetric(b.link.target_tq, b.link.target_tp); + let aAvg = am === undefined && an === undefined ? -Infinity : ((am ?? 0) + (an ?? 0)) / 2; + let bAvg = bm === undefined && bn === undefined ? -Infinity : ((bm ?? 0) + (bn ?? 0)) / 2; + return aAvg - bAvg; }, reverse: true, }, diff --git a/lib/linklist.ts b/lib/linklist.ts index fab0c2fc..852490c5 100644 --- a/lib/linklist.ts +++ b/lib/linklist.ts @@ -28,7 +28,13 @@ let headings: Heading[] = [ name: "node.tq", class: "ion-connection-bars", sort: function (a, b) { - return (a.source_tq + a.target_tq) / 2 - (b.source_tq + b.target_tq) / 2; + let am = helper.linkMetric(a.source_tq, a.source_tp); + let bm = helper.linkMetric(b.source_tq, b.source_tp); + let an = helper.linkMetric(a.target_tq, a.target_tp); + let bn = helper.linkMetric(b.target_tq, b.target_tp); + let aAvg = am === undefined && an === undefined ? -Infinity : ((am ?? 0) + (an ?? 0)) / 2; + let bAvg = bm === undefined && bn === undefined ? -Infinity : ((bm ?? 0) + (bn ?? 0)) / 2; + return aAvg - bAvg; }, reverse: true, }, @@ -81,8 +87,16 @@ export const Linklist = function (linkScale: (t: any) => any): CanRender & CanSe h("td", td1Content), h( "td", - { style: { color: linkScale((link.source_tq + link.target_tq) / 2) } }, - helper.showTq(link.source_tq) + " - " + helper.showTq(link.target_tq), + { + style: { + color: linkScale( + ((helper.linkMetric(link.source_tq, link.source_tp) ?? 0) + + (helper.linkMetric(link.target_tq, link.target_tp) ?? 0)) / + 2, + ), + }, + }, + helper.showBiDiLinkMetric(link.source_tq, link.source_tp, link.target_tq, link.target_tp), ), h("td", helper.showDistance(link)), ]); diff --git a/lib/map/labellayer.js b/lib/map/labellayer.js index 6cf950cc..a5d3575c 100644 --- a/lib/map/labellayer.js +++ b/lib/map/labellayer.js @@ -112,10 +112,12 @@ function addLinksToMap(dict, linkScale, graph) { return graph.map(function (link) { let linkColor; - if (link.type.indexOf("other") == 0 && link.source_tq >= 0.99 && link.target_tq >= 0.99) { + let sm = helper.linkMetric(link.source_tq, link.source_tp); + let tm = helper.linkMetric(link.target_tq, link.target_tp); + if (link.type.indexOf("other") == 0 && (sm ?? 0) >= 0.99 && (tm ?? 0) >= 0.99) { linkColor = config.map.otherLinkColor; } else { - linkColor = linkScale((link.source_tq + link.target_tq) / 2); + linkColor = linkScale(((sm ?? 0) + (tm ?? 0)) / 2); } let opts = { @@ -136,9 +138,7 @@ function addLinksToMap(dict, linkScale, graph) { "
" + helper.showDistance(link) + " / " + - helper.showTq(link.source_tq) + - " - " + - helper.showTq(link.target_tq) + + helper.showBiDiLinkMetric(link.source_tq, link.source_tp, link.target_tq, link.target_tp) + "
" + link.type + "
", diff --git a/lib/utils/helper.ts b/lib/utils/helper.ts index 02025be5..b05b68b7 100644 --- a/lib/utils/helper.ts +++ b/lib/utils/helper.ts @@ -123,6 +123,63 @@ export const showTq = function showTq(tq: number) { return (tq * 100).toFixed(0) + "%"; }; +// Format throughput in kbps as an SI-prefixed bit/s string (e.g. "24 Mbit/s"). +export const showThroughput = function showThroughput(kbps: number) { + if (kbps === undefined || kbps === null || isNaN(kbps)) { + return ""; + } + let units = ["kbit/s", "Mbit/s", "Gbit/s", "Tbit/s"]; + let value = kbps; + let unit = 0; + while (value >= 1000 && unit < units.length - 1) { + value /= 1000; + unit++; + } + return (value < 10 ? value.toFixed(1) : value.toFixed(0)) + " " + units[unit]; +}; + +// Returns a Link's link-quality metric for a given direction, normalised to a +// 0..1 scale where higher is better. Batman IV uses tq directly (TQ=0 is +// treated as no metric; call sites coalesce to 0 for colouring). Batman V +// maps throughput logarithmically over [1 Mbps, 1.2 Gbps]. Returns undefined +// when neither metric is set or non-zero. +export const linkMetric = function linkMetric(tq: number | undefined, throughput: number | undefined) { + if (tq !== undefined && tq > 0) { + return tq; + } + if (throughput !== undefined && throughput > 0) { + let lo = Math.log10(1e3); + let hi = Math.log10(1.2e6); + let v = (Math.log10(throughput) - lo) / (hi - lo); + return Math.max(0, Math.min(1, v)); + } + return undefined; +}; + +// Human-readable display string for a Link direction. +export const showLinkMetric = function showLinkMetric(tq: number | undefined, throughput: number | undefined) { + if (tq !== undefined && tq > 0) { + return showTq(tq); + } + if (throughput !== undefined && throughput > 0) { + return showThroughput(throughput); + } + return ""; +}; + +// Human-readable display string for both directions of a Link, omitting the +// separator when one side has no metric (e.g. VPN gateways in Batman V). +export const showBiDiLinkMetric = function showBiDiLinkMetric( + src_tq: number | undefined, + src_throughput: number | undefined, + tgt_tq: number | undefined, + tgt_throughput: number | undefined, +) { + const src = showLinkMetric(src_tq, src_throughput); + const tgt = showLinkMetric(tgt_tq, tgt_throughput); + return [src, tgt].filter(Boolean).join(" - "); +}; + export function attributeEntry(children: VNode[], label: string, value: string | VNode) { if (value !== undefined) { if (typeof value !== "object") { diff --git a/lib/utils/node.ts b/lib/utils/node.ts index 9f354eee..ee53576f 100644 --- a/lib/utils/node.ts +++ b/lib/utils/node.ts @@ -15,8 +15,10 @@ export interface Link { target_addr: string; source_mac?: string; // Same as _addr target_mac?: string; // Same as _addr - source_tq: number; - target_tq: number; + source_tq?: number; // Batman IV link quality (0..1) + target_tq?: number; + source_tp?: number; // Batman V link quality (kbps) + target_tp?: number; } export interface Neighbour { diff --git a/public/locale/cz.json b/public/locale/cz.json index 7a470c29..528570c7 100644 --- a/public/locale/cz.json +++ b/public/locale/cz.json @@ -9,6 +9,7 @@ "distance": "Vzdálenost", "connectionType": "typ připojení", "tq": "tq", + "throughput": "Propustnost", "lastOnline": "poslední on-line %{time} (%{date})", "lastOffline": "lastOffline %{time} (%{date})", "activated": "aktivováno (%{branch})", diff --git a/public/locale/de.json b/public/locale/de.json index c0d18f54..2240039c 100644 --- a/public/locale/de.json +++ b/public/locale/de.json @@ -9,6 +9,7 @@ "distance": "Entfernung", "connectionType": "Verbindungsart", "tq": "Übertragungsqualität", + "throughput": "Durchsatz", "lastOnline": "online, letzte Nachricht %{time} (%{date})", "lastOffline": "offline, letzte Nachricht %{time} (%{date})", "activated": "aktiviert (%{branch})", diff --git a/public/locale/en.json b/public/locale/en.json index 93403aa6..f6142677 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -9,6 +9,7 @@ "distance": "Distance", "connectionType": "Connection type", "tq": "Transmit quality", + "throughput": "Throughput", "lastOnline": "online, last message %{time} (%{date})", "lastOffline": "offline, last message %{time} (%{date})", "activated": "activated (%{branch})", diff --git a/public/locale/fr.json b/public/locale/fr.json index 71945beb..09aae71c 100644 --- a/public/locale/fr.json +++ b/public/locale/fr.json @@ -9,6 +9,7 @@ "distance": "Distance", "connectionType": "Type de connexion", "tq": "Qualité de transmission", + "throughput": "Débit", "lastOnline": "en ligne, dernier message %{time} (%{date})", "lastOffline": "hors ligne, dernier message %{time} (%{date})", "activated": "activé (%{branch})", diff --git a/public/locale/ru.json b/public/locale/ru.json index 247a0975..d685f87c 100644 --- a/public/locale/ru.json +++ b/public/locale/ru.json @@ -9,6 +9,7 @@ "distance": "Расстояние", "connectionType": "Тип подключения", "tq": "Качество связи", + "throughput": "Пропускная способность", "lastOnline": "в сети, последнее сообщение %{time} (%{date})", "lastOffline": "не в сети, последнее сообщение %{time} (%{date})", "activated": "активировано (%{branch})", diff --git a/public/locale/tr.json b/public/locale/tr.json index 43a0ca01..59abf0a9 100644 --- a/public/locale/tr.json +++ b/public/locale/tr.json @@ -9,6 +9,7 @@ "distance": "Mesafe", "connectionType": "Bağlantı türü", "tq": "İletim kalitesi", + "throughput": "Aktarım hızı", "lastOnline": "çevrimiçi, son mesaj %{time} (%{date})", "lastOffline": "çevrimdışı, son mesaj %{time} (%{date})", "activated": "aktif (%{branch})", diff --git a/scss/modules/_infobox.scss b/scss/modules/_infobox.scss index 37d3abd8..b8d9b34b 100644 --- a/scss/modules/_infobox.scss +++ b/scss/modules/_infobox.scss @@ -22,6 +22,10 @@ th, td { + &:nth-child(2) { + width: 42%; + } + &:nth-child(3), &:nth-child(5) { width: 12%;