diff --git a/history.html b/history.html
index 029ca34..e2b6e13 100644
--- a/history.html
+++ b/history.html
@@ -288,7 +288,7 @@
-
+
diff --git a/history.js b/history.js
deleted file mode 100644
index 1b19f77..0000000
--- a/history.js
+++ /dev/null
@@ -1,778 +0,0 @@
-document.addEventListener('DOMContentLoaded', main)
-
-function main() {
- getJSON("config.json").then( function (config) {
- moment.locale("de")
-
- var options = { worldCopyJump: true,
- zoomControl: false
- }
-
- var linkScale = chroma.scale(chroma.interpolate.bezier(['green', 'yellow', 'red'])).domain([1, 5])
-
- var mapDiv = document.createElement("div")
- mapDiv.classList.add("map")
- document.body.insertBefore(mapDiv, document.body.firstChild)
-
- var map = L.map(mapDiv, options)
- var sidebar = mkSidebar(document.body)
- var infobox = new Infobox(sidebar)
- var gotoAnything = new gotoBuilder(config, infobox, showNodeinfo, showLinkinfo)
-
- var urls = [ config.dataPath + 'nodes.json',
- config.dataPath + 'graph.json'
- ]
-
- var p = Promise.all(urls.map(getJSON))
- p.then(handle_data(config, linkScale, sidebar, infobox, map, gotoAnything))
- })
-}
-
-function handle_data(config, linkScale, sidebar, infobox, map, gotoAnything) {
- return function (data) {
- var nodedict = data[0]
- var nodes = Object.keys(nodedict.nodes).map(function (key) { return nodedict.nodes[key] })
-
- nodes = nodes.filter( function (d) {
- return "firstseen" in d && "lastseen" in d
- })
-
- nodes.forEach( function(node) {
- node.firstseen = moment.utc(node.firstseen)
- node.lastseen = moment.utc(node.lastseen)
- })
-
- var now = moment()
- var age = moment(now).subtract(14, 'days')
-
- var newnodes = limit("firstseen", age, sortByKey("firstseen", nodes).filter(online))
- var lostnodes = limit("lastseen", age, sortByKey("lastseen", nodes).filter(offline))
-
- var onlinenodes = nodes.filter(online)
-
- var graph = data[1].batadv
- var graphnodes = data[0].nodes
-
- graph.nodes.forEach( function (d) {
- if (d.node_id in graphnodes)
- d.node = graphnodes[d.node_id]
- })
-
- graph.links.forEach( function (d) {
- if (graph.nodes[d.source].node)
- d.source = graph.nodes[d.source]
- else
- d.source = undefined
-
- if (graph.nodes[d.target].node)
- d.target = graph.nodes[d.target]
- else
- d.target = undefined
- })
-
- var links = graph.links.filter( function (d) {
- return d.source !== undefined && d.target !== undefined
- })
-
- links.forEach( function (d) {
- if (!("location" in d.source.node.nodeinfo && "location" in d.target.node.nodeinfo))
- return
-
- d.latlngs = []
- d.latlngs.push(L.latLng(d.source.node.nodeinfo.location.latitude, d.source.node.nodeinfo.location.longitude))
- d.latlngs.push(L.latLng(d.target.node.nodeinfo.location.latitude, d.target.node.nodeinfo.location.longitude))
-
- d.distance = d.latlngs[0].distanceTo(d.latlngs[1])
- })
-
- nodes.forEach( function (d) {
- d.neighbours = []
- })
-
- links.forEach( function (d) {
- d.source.node.neighbours.push({ node: d.target.node, link: d })
- d.target.node.neighbours.push({ node: d.source.node, link: d })
- })
-
- var markers = mkmap(map, linkScale, sidebar, now, newnodes, lostnodes, onlinenodes, links, gotoAnything)
-
- gotoAnything.addMarkers(markers)
-
- showMeshstats(sidebar, nodes)
- mkNodesList(sidebar, config.showContact, "firstseen", gotoAnything.node, "Neue Knoten", newnodes)
- mkNodesList(sidebar, config.showContact, "lastseen", gotoAnything.node, "Verschwundene Knoten", lostnodes)
- mkLinkList(sidebar, linkScale, gotoAnything.link, links)
-
- var historyDict = { nodes: {}, links: {} }
-
- nodes.forEach( function (d) {
- historyDict.nodes[d.nodeinfo.node_id] = d
- })
-
- links.forEach( function (d) {
- historyDict.links[linkId(d)] = d
- })
-
- gotoHistory(gotoAnything, historyDict, window.location.hash)
-
- window.onpopstate = function (d) {
- gotoHistory(gotoAnything, historyDict, d.state)
- }
- }
-}
-
-function mkSidebar(el) {
- var sidebar = document.createElement("div")
- sidebar.classList.add("sidebar")
- el.appendChild(sidebar)
-
- var button = document.createElement("button")
- sidebar.appendChild(button)
-
- button.classList.add("sidebarhandle")
- button.onclick = function () {
- sidebar.classList.toggle("hidden")
- }
-
- var container = document.createElement("div")
- container.classList.add("container")
- sidebar.appendChild(container)
-
- container.getWidth = function () {
- var small = window.matchMedia("(max-width: 60em)");
- return small.matches ? 0 : sidebar.offsetWidth
- }
-
- return container
-}
-
-function mkmap(map, linkScale, sidebar, now, newnodes, lostnodes, onlinenodes, graph, gotoAnything) {
- function mkMarker(dict, iconFunc) {
- return function (d) {
- var opt = { icon: iconFunc(d),
- title: d.nodeinfo.hostname
- }
-
- var m = L.marker([d.nodeinfo.location.latitude, d.nodeinfo.location.longitude], opt)
-
- m.on('click', gotoAnything.node(d, false))
- m.bindPopup(d.nodeinfo.hostname)
-
- dict[d.nodeinfo.node_id] = m
-
- return m
- }
- }
-
- var iconBase = { iconUrl: 'img/circlemarker.png',
- iconRetinaUrl: 'img/circlemarker@2x.png',
- iconSize: [17, 17],
- iconAnchor: [8, 8],
- popupAnchor: [0, -3]
- }
-
- var iconOnline = Object.assign({}, iconBase)
- iconOnline.className = "node-online"
- iconOnline = L.icon(iconOnline)
-
- var iconOffline = Object.assign({}, iconBase)
- iconOffline.className = "node-offline"
- iconOffline = L.icon(iconOffline)
-
- var iconNew = Object.assign({}, iconBase)
- iconNew.className = "node-new"
- iconNew = L.icon(iconNew)
-
- var iconOfflineAlert = Object.assign({}, iconBase)
- iconOfflineAlert.className = "node-offline node-alert"
- iconOfflineAlert = L.icon(iconOfflineAlert)
-
- L.control.zoom({ position: "topright" }).addTo(map)
-
- L.tileLayer("https://otile{s}-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.jpg", {
- subdomains: "1234",
- type: "osm",
- attribution: "Map data Tiles © MapQuest , Map data © OpenStreetMap contributors, CC-BY-SA",
- maxZoom: 18
- }).addTo(map)
-
- var markersDict = addLinksToMap(linkScale, map, graph, gotoAnything)
-
- var nodes = newnodes.concat(lostnodes).filter(has_location)
-
- var markers = nodes.map(mkMarker(markersDict, function (d) {
- if (d.flags.online)
- return iconNew
-
- if (d.lastseen.isAfter(moment(now).subtract(1, 'days')))
- return iconOfflineAlert
- else
- return iconOffline
- }))
-
- var onlinemarkers = subtract(onlinenodes.filter(has_location), newnodes)
- .map(mkMarker(markersDict, function (d) { return iconOnline } ))
-
- var groupOnline = L.featureGroup(onlinemarkers).addTo(map)
- var group = L.featureGroup(markers).addTo(map)
-
- var bounds = group.getBounds()
-
- if (!bounds.isValid())
- bounds = groupOnline.getBounds()
-
- if (bounds.isValid())
- map.fitBounds(bounds, {paddingTopLeft: [sidebar.getWidth(), 0]})
-
- var funcDict = {}
-
- Object.keys(markersDict).map( function(k) {
- funcDict[k] = function (d) {
- var m = markersDict[k]
- var bounds
-
- if ("getBounds" in m)
- bounds = m.getBounds()
- else
- bounds = L.latLngBounds([m.getLatLng()])
-
- map.fitBounds(bounds, {paddingTopLeft: [sidebar.getWidth(), 0]})
- m.openPopup(bounds.getCenter())
- }
- })
-
- return funcDict
-}
-
-function addLinksToMap(linkScale, map, graph, gotoAnything) {
- var markersDict = {}
-
- graph = graph.filter( function (d) {
- return "distance" in d
- })
-
- var lines = graph.map( function (d) {
- var opts = { color: linkScale(d.tq).hex(),
- weight: 4
- }
-
- var line = L.polyline(d.latlngs, opts)
-
- line.bindPopup(d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname + "
" + showDistance(d) + " / " + showTq(d) + "")
- line.on('click', gotoAnything.link(d, false))
-
- markersDict[linkId(d)] = line
-
- return line
- })
-
- var group = L.featureGroup(lines).addTo(map)
-
- return markersDict
-}
-
-function mkLinkList(el, linkScale, gotoProxy, links) {
- if (links.length == 0)
- return
-
- var h2 = document.createElement("h2")
- h2.textContent = "Verbindungen"
- el.appendChild(h2)
-
- var table = document.createElement("table")
- var thead = document.createElement("thead")
-
- var tr = document.createElement("tr")
- var th1 = document.createElement("th")
- th1.textContent = "Knoten"
- tr.appendChild(th1)
-
- var th2 = document.createElement("th")
- th2.textContent = "TQ"
- tr.appendChild(th2)
-
- var th3 = document.createElement("th")
- th3.textContent = "Entfernung"
- th3.classList.add("sort-default")
- tr.appendChild(th3)
-
- thead.appendChild(tr)
-
- table.appendChild(thead)
-
- var tbody = document.createElement("tbody")
-
- links.forEach( function (d) {
- var row = document.createElement("tr")
- var td1 = document.createElement("td")
- var a = document.createElement("a")
- a.textContent = d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname
- a.href = "#"
- a.onclick = gotoProxy(d)
- td1.appendChild(a)
- row.appendChild(td1)
-
- if (d.vpn)
- td1.appendChild(document.createTextNode(" (VPN)"))
-
- var td2 = document.createElement("td")
- td2.textContent = showTq(d)
- td2.style.color = linkScale(d.tq)
- row.appendChild(td2)
-
- var td3 = document.createElement("td")
- td3.textContent = showDistance(d)
- td3.setAttribute("data-sort", d.distance !== undefined ? -d.distance : 1)
- row.appendChild(td3)
-
- tbody.appendChild(row)
- })
-
- table.appendChild(tbody)
-
- new Tablesort(table)
-
- el.appendChild(table)
-}
-
-function mkNodesList(el, showContact, tf, gotoProxy, title, list) {
- if (list.length == 0)
- return
-
- var h2 = document.createElement("h2")
- h2.textContent = title
- el.appendChild(h2)
- var table = document.createElement("table")
- el.appendChild(table)
-
- var tbody = document.createElement("tbody")
-
- list.forEach( function (d) {
- var time = moment(d[tf]).fromNow()
-
- var row = document.createElement("tr")
- var td1 = document.createElement("td")
- var a = document.createElement("a")
- a.classList.add("hostname")
- a.classList.add(d.flags.online ? "online" : "offline")
- a.textContent = d.nodeinfo.hostname
- a.href = "#"
- a.onclick = gotoProxy(d)
- td1.appendChild(a)
-
- if (has_location(d)) {
- var span = document.createElement("span")
- span.classList.add("icon")
- span.classList.add("ion-location")
- td1.appendChild(span)
- }
-
- if ("owner" in d.nodeinfo && showContact) {
- var contact = d.nodeinfo.owner.contact
- td1.appendChild(document.createTextNode(" – " + contact + ""))
- }
-
- var td2 = document.createElement("td")
- td2.textContent = time
-
- row.appendChild(td1)
- row.appendChild(td2)
- tbody.appendChild(row)
- })
-
- table.appendChild(tbody)
- el.appendChild(table)
-}
-
-function showMeshstats(el, nodes) {
- var h2 = document.createElement("h2")
- h2.textContent = "Übersicht"
- el.appendChild(h2)
-
- var p = document.createElement("p")
-
- var totalNodes = sum(nodes.filter(online).map(one))
- var totalClients = sum(nodes.filter(online).map( function (d) {
- return d.statistics.clients
- }))
- var totalGateways = sum(nodes.filter(online).filter( function (d) {
- return d.flags.gateway
- }).map(one))
-
- p.textContent = totalNodes + " Knoten (online), " +
- totalClients + " Clients, " +
- totalGateways + " Gateways"
-
- p.appendChild(document.createElement("br"))
- p.appendChild(document.createTextNode("Diese Daten sind " + moment.utc(nodes.timestamp).fromNow(true) + " alt."))
- el.appendChild(p)
-}
-
-function Infobox(sidebar) {
- var self = this
- el = undefined
-
- function close() {
- destroy()
- pushHistory()
- }
-
- function destroy() {
- if (el && el.parentNode) {
- el.parentNode.removeChild(el)
- el = undefined
- }
- }
-
- self.create = function () {
- destroy()
-
- el = document.createElement("div")
- sidebar.insertBefore(el, sidebar.firstChild)
-
- el.scrollIntoView(false)
- el.classList.add("infobox")
- el.close = close
- el.destroy = destroy
-
- var closeButton = document.createElement("button")
- closeButton.classList.add("close")
- closeButton.onclick = close
- el.appendChild(closeButton)
-
- return el
- }
-
- return self
-}
-
-function showNodeinfo(config, infobox, gotoAnything, d) {
- var el = infobox.create()
-
- var h2 = document.createElement("h2")
- h2.textContent = d.nodeinfo.hostname
- var span = document.createElement("span")
- span.classList.add(d.flags.online ? "online" : "offline")
- span.textContent = " (" + (d.flags.online ? "online" : "offline, " + d.lastseen.fromNow(true)) + ")"
- h2.appendChild(span)
- el.appendChild(h2)
-
- var attributes = document.createElement("table")
- attributes.classList.add("attributes")
-
- attributeEntry(attributes, "Gateway", d.flags.gateway ? "ja" : null)
- attributeEntry(attributes, "In der Karte", has_location(d) ? "ja" : "nein")
-
- if (config.showContact)
- attributeEntry(attributes, "Kontakt", dictGet(d.nodeinfo, ["owner", "contact"]))
-
- attributeEntry(attributes, "Hardware", dictGet(d.nodeinfo, ["hardware", "model"]))
- attributeEntry(attributes, "Primäre MAC", dictGet(d.nodeinfo, ["network", "mac"]))
- attributeEntry(attributes, "Firmware", showFirmware(d))
- attributeEntry(attributes, "Uptime", showUptime(d))
- attributeEntry(attributes, "Teil des Netzes", showFirstseen(d))
- attributeEntry(attributes, "Arbeitsspeicher", showRAM(d))
- attributeEntry(attributes, "IP Adressen", showIPs(d))
- attributeEntry(attributes, "Clients", showClients(d))
- el.appendChild(attributes)
-
- if (d.neighbours.length > 0) {
- var h3 = document.createElement("h3")
- h3.textContent = "Nachbarknoten (" + d.neighbours.length + ")"
- el.appendChild(h3)
-
- var table = document.createElement("table")
- var thead = document.createElement("thead")
-
- var tr = document.createElement("tr")
- var th1 = document.createElement("th")
- th1.textContent = "Knoten"
- th1.classList.add("sort-default")
- tr.appendChild(th1)
-
- var th2 = document.createElement("th")
- th2.textContent = "TQ"
- tr.appendChild(th2)
-
- var th3 = document.createElement("th")
- th3.textContent = "Entfernung"
- tr.appendChild(th3)
-
- thead.appendChild(tr)
- table.appendChild(thead)
-
- var tbody = document.createElement("tbody")
-
- d.neighbours.forEach( function (d) {
- var tr = document.createElement("tr")
-
- var td1 = document.createElement("td")
- var a1 = document.createElement("a")
- a1.classList.add("hostname")
- a1.textContent = d.node.nodeinfo.hostname
- a1.href = "#"
- a1.onclick = gotoAnything.node(d.node)
- td1.appendChild(a1)
-
- if (d.link.vpn)
- td1.appendChild(document.createTextNode(" (VPN)"))
-
- if (has_location(d.node)) {
- var span = document.createElement("span")
- span.classList.add("icon")
- span.classList.add("ion-location")
- td1.appendChild(span)
- }
-
- tr.appendChild(td1)
-
- var td2 = document.createElement("td")
- var a2 = document.createElement("a")
- a2.href = "#"
- a2.textContent = showTq(d.link)
- a2.onclick = gotoAnything.link(d.link)
- td2.appendChild(a2)
- tr.appendChild(td2)
-
- var td3 = document.createElement("td")
- var a3 = document.createElement("a")
- a3.href = "#"
- a3.textContent = showDistance(d.link)
- a3.onclick = gotoAnything.link(d.link)
- td3.appendChild(a3)
- td3.setAttribute("data-sort", d.link.distance !== undefined ? -d.link.distance : 1)
- tr.appendChild(td3)
-
- tbody.appendChild(tr)
- })
-
- table.appendChild(tbody)
-
- new Tablesort(table)
-
- el.appendChild(table)
- }
-
- function showFirmware(d) {
- var release = dictGet(d.nodeinfo, ["software", "firmware", "release"])
- var base = dictGet(d.nodeinfo, ["software", "firmware", "base"])
-
- if (release === null || base === null)
- return
-
- return release + " / " + base
- }
-
- function showUptime(d) {
- if (!("uptime" in d.statistics))
- return
-
- return moment.duration(d.statistics.uptime, "seconds").humanize()
- }
-
- function showFirstseen(d) {
- if (!("firstseen" in d))
- return
-
- return d.firstseen.fromNow(true)
- }
-
- function showClients(d) {
- if (!d.flags.online)
- return
-
- return function (el) {
- el.appendChild(document.createTextNode(d.statistics.clients > 0 ? d.statistics.clients : "keine"))
- el.appendChild(document.createElement("br"))
-
- var span = document.createElement("span")
- span.classList.add("clients")
- span.textContent = " ".repeat(d.statistics.clients)
- el.appendChild(span)
- }
- }
-
- function showIPs(d) {
- var ips = dictGet(d.nodeinfo, ["network", "addresses"])
- if (ips === null)
- return
-
- ips.sort()
-
- return function (el) {
- ips.forEach( function (ip, i) {
- var link = !ip.startsWith("fe80:")
-
- if (i > 0)
- el.appendChild(document.createElement("br"))
-
- if (link) {
- var a = document.createElement("a")
- a.href = "http://[" + ip + "]/"
- a.textContent = ip
- el.appendChild(a)
- } else
- el.appendChild(document.createTextNode(ip))
- })
- }
- }
-
- function showRAM(d) {
- if (!("memory_usage" in d.statistics))
- return
-
- return function (el) {
- el.appendChild(showBar("memory-usage", d.statistics.memory_usage))
- }
- }
-}
-
-function attributeEntry(el, label, value) {
- if (value === null || value == undefined)
- return
-
- var tr = document.createElement("tr")
- var th = document.createElement("th")
- th.textContent = label
- tr.appendChild(th)
-
- var td = document.createElement("td")
-
- if (typeof value == "function")
- value(td)
- else
- td.appendChild(document.createTextNode(value))
-
- tr.appendChild(td)
-
- el.appendChild(tr)
-
- return td
-}
-
-function showBar(className, v) {
- var span = document.createElement("span")
- span.classList.add("bar")
- span.classList.add(className)
-
- var bar = document.createElement("span")
- bar.style.width = (v * 100) + "%"
- span.appendChild(bar)
-
- var label = document.createElement("label")
- label.textContent = (Math.round(v * 100)) + " %"
- span.appendChild(label)
-
- return span
-}
-
-function showLinkinfo(config, infobox, gotoAnything, d) {
- var el = infobox.create()
-
- var h2 = document.createElement("h2")
- a1 = document.createElement("a")
- a1.href = "#"
- a1.onclick = gotoAnything.node(d.source.node)
- a1.textContent = d.source.node.nodeinfo.hostname
- h2.appendChild(a1)
- h2.appendChild(document.createTextNode(" – "))
- a2 = document.createElement("a")
- a2.href = "#"
- a2.onclick = gotoAnything.node(d.target.node)
- a2.textContent = d.target.node.nodeinfo.hostname
- h2.appendChild(a2)
- el.appendChild(h2)
-
- var attributes = document.createElement("table")
- attributes.classList.add("attributes")
-
- attributeEntry(attributes, "TQ", showTq(d))
- attributeEntry(attributes, "Entfernung", showDistance(d))
- attributeEntry(attributes, "VPN", d.vpn ? "ja" : "nein")
-
- el.appendChild(attributes)
-}
-
-function pushHistory(d) {
- var s = "#!"
-
- if (d) {
- if ("node" in d)
- s += "n:" + d.node.nodeinfo.node_id
-
- if ("link" in d)
- s += "l:" + linkId(d.link)
- }
-
- window.history.pushState(s, undefined, s)
-}
-
-function gotoHistory(gotoAnything, dict, s) {
- if (!s.startsWith("#!"))
- return
-
- s = s.slice(2)
-
- var args = s.split(":")
-
- if (args[0] === "n") {
- var id = args[1]
-
- if (id in dict.nodes)
- gotoAnything.node(dict.nodes[id], true, false)()
- }
-
- if (args[0] === "l") {
- var id = args[1]
-
- if (id in dict.links)
- gotoAnything.link(dict.links[id], true, false)()
- }
-}
-
-function gotoBuilder(config, infobox, nodes, links) {
- var markers = {}
- var self = this
-
- function gotoNode(d, showMap, push) {
- showMap = trueDefault(showMap)
- push = trueDefault(push)
-
- if (showMap && d.nodeinfo.node_id in markers)
- markers[d.nodeinfo.node_id]()
-
- nodes(config, infobox, self, d)
-
- if (push)
- pushHistory( { node: d })
-
- return false
- }
-
- function gotoLink(d, showMap, push) {
- showMap = trueDefault(showMap)
- push = trueDefault(push)
-
- if (showMap && linkId(d) in markers)
- markers[linkId(d)]()
-
- links(config, infobox, self, d)
-
- if (push)
- pushHistory( { link: d })
-
- return false
- }
-
- function addMarkers(d) {
- markers = d
- }
-
- this.node = function (d, m, p) { return function () { return gotoNode(d, m, p) }}
- this.link = function (d, m, p) { return function () { return gotoLink(d, m, p) }}
- this.addMarkers = function (d) {
- markers = d
- }
-
- return this
-}
diff --git a/lib/history.js b/lib/history.js
new file mode 100644
index 0000000..67beda6
--- /dev/null
+++ b/lib/history.js
@@ -0,0 +1,649 @@
+require(["map"], function (Map) {
+ main()
+
+ function main() {
+ getJSON("config.json").then( function (config) {
+ moment.locale("de")
+
+ var linkScale = chroma.scale(chroma.interpolate.bezier(['green', 'yellow', 'red'])).domain([1, 5])
+
+ var map = new Map()
+ document.body.insertBefore(map.div, document.body.firstChild)
+
+ var sidebar = mkSidebar(document.body)
+ var infobox = new Infobox(sidebar)
+ var gotoAnything = new gotoBuilder(config, infobox, showNodeinfo, showLinkinfo)
+
+ var urls = [ config.dataPath + 'nodes.json',
+ config.dataPath + 'graph.json'
+ ]
+
+ var p = Promise.all(urls.map(getJSON))
+ p.then(handle_data(config, linkScale, sidebar, infobox, map, gotoAnything))
+ })
+ }
+
+ function handle_data(config, linkScale, sidebar, infobox, map, gotoAnything) {
+ return function (data) {
+ var nodedict = data[0]
+ var nodes = Object.keys(nodedict.nodes).map(function (key) { return nodedict.nodes[key] })
+
+ nodes = nodes.filter( function (d) {
+ return "firstseen" in d && "lastseen" in d
+ })
+
+ nodes.forEach( function(node) {
+ node.firstseen = moment.utc(node.firstseen)
+ node.lastseen = moment.utc(node.lastseen)
+ })
+
+ var now = moment()
+ var age = moment(now).subtract(14, 'days')
+
+ var newnodes = limit("firstseen", age, sortByKey("firstseen", nodes).filter(online))
+ var lostnodes = limit("lastseen", age, sortByKey("lastseen", nodes).filter(offline))
+
+ var onlinenodes = nodes.filter(online)
+
+ var graph = data[1].batadv
+ var graphnodes = data[0].nodes
+
+ graph.nodes.forEach( function (d) {
+ if (d.node_id in graphnodes)
+ d.node = graphnodes[d.node_id]
+ })
+
+ graph.links.forEach( function (d) {
+ if (graph.nodes[d.source].node)
+ d.source = graph.nodes[d.source]
+ else
+ d.source = undefined
+
+ if (graph.nodes[d.target].node)
+ d.target = graph.nodes[d.target]
+ else
+ d.target = undefined
+ })
+
+ var links = graph.links.filter( function (d) {
+ return d.source !== undefined && d.target !== undefined
+ })
+
+ links.forEach( function (d) {
+ if (!("location" in d.source.node.nodeinfo && "location" in d.target.node.nodeinfo))
+ return
+
+ d.latlngs = []
+ d.latlngs.push(L.latLng(d.source.node.nodeinfo.location.latitude, d.source.node.nodeinfo.location.longitude))
+ d.latlngs.push(L.latLng(d.target.node.nodeinfo.location.latitude, d.target.node.nodeinfo.location.longitude))
+
+ d.distance = d.latlngs[0].distanceTo(d.latlngs[1])
+ })
+
+ nodes.forEach( function (d) {
+ d.neighbours = []
+ })
+
+ links.forEach( function (d) {
+ d.source.node.neighbours.push({ node: d.target.node, link: d })
+ d.target.node.neighbours.push({ node: d.source.node, link: d })
+ })
+
+ var markers = map.setData(linkScale, sidebar, now, newnodes, lostnodes, onlinenodes, links, gotoAnything)
+
+ gotoAnything.addMarkers(markers)
+
+ showMeshstats(sidebar, nodes)
+ mkNodesList(sidebar, config.showContact, "firstseen", gotoAnything.node, "Neue Knoten", newnodes)
+ mkNodesList(sidebar, config.showContact, "lastseen", gotoAnything.node, "Verschwundene Knoten", lostnodes)
+ mkLinkList(sidebar, linkScale, gotoAnything.link, links)
+
+ var historyDict = { nodes: {}, links: {} }
+
+ nodes.forEach( function (d) {
+ historyDict.nodes[d.nodeinfo.node_id] = d
+ })
+
+ links.forEach( function (d) {
+ historyDict.links[linkId(d)] = d
+ })
+
+ gotoHistory(gotoAnything, historyDict, window.location.hash)
+
+ window.onpopstate = function (d) {
+ gotoHistory(gotoAnything, historyDict, d.state)
+ }
+ }
+ }
+
+ function mkSidebar(el) {
+ var sidebar = document.createElement("div")
+ sidebar.classList.add("sidebar")
+ el.appendChild(sidebar)
+
+ var button = document.createElement("button")
+ sidebar.appendChild(button)
+
+ button.classList.add("sidebarhandle")
+ button.onclick = function () {
+ sidebar.classList.toggle("hidden")
+ }
+
+ var container = document.createElement("div")
+ container.classList.add("container")
+ sidebar.appendChild(container)
+
+ container.getWidth = function () {
+ var small = window.matchMedia("(max-width: 60em)");
+ return small.matches ? 0 : sidebar.offsetWidth
+ }
+
+ return container
+ }
+
+ function mkLinkList(el, linkScale, gotoProxy, links) {
+ if (links.length == 0)
+ return
+
+ var h2 = document.createElement("h2")
+ h2.textContent = "Verbindungen"
+ el.appendChild(h2)
+
+ var table = document.createElement("table")
+ var thead = document.createElement("thead")
+
+ var tr = document.createElement("tr")
+ var th1 = document.createElement("th")
+ th1.textContent = "Knoten"
+ tr.appendChild(th1)
+
+ var th2 = document.createElement("th")
+ th2.textContent = "TQ"
+ tr.appendChild(th2)
+
+ var th3 = document.createElement("th")
+ th3.textContent = "Entfernung"
+ th3.classList.add("sort-default")
+ tr.appendChild(th3)
+
+ thead.appendChild(tr)
+
+ table.appendChild(thead)
+
+ var tbody = document.createElement("tbody")
+
+ links.forEach( function (d) {
+ var row = document.createElement("tr")
+ var td1 = document.createElement("td")
+ var a = document.createElement("a")
+ a.textContent = d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname
+ a.href = "#"
+ a.onclick = gotoProxy(d)
+ td1.appendChild(a)
+ row.appendChild(td1)
+
+ if (d.vpn)
+ td1.appendChild(document.createTextNode(" (VPN)"))
+
+ var td2 = document.createElement("td")
+ td2.textContent = showTq(d)
+ td2.style.color = linkScale(d.tq)
+ row.appendChild(td2)
+
+ var td3 = document.createElement("td")
+ td3.textContent = showDistance(d)
+ td3.setAttribute("data-sort", d.distance !== undefined ? -d.distance : 1)
+ row.appendChild(td3)
+
+ tbody.appendChild(row)
+ })
+
+ table.appendChild(tbody)
+
+ new Tablesort(table)
+
+ el.appendChild(table)
+ }
+
+ function mkNodesList(el, showContact, tf, gotoProxy, title, list) {
+ if (list.length == 0)
+ return
+
+ var h2 = document.createElement("h2")
+ h2.textContent = title
+ el.appendChild(h2)
+ var table = document.createElement("table")
+ el.appendChild(table)
+
+ var tbody = document.createElement("tbody")
+
+ list.forEach( function (d) {
+ var time = moment(d[tf]).fromNow()
+
+ var row = document.createElement("tr")
+ var td1 = document.createElement("td")
+ var a = document.createElement("a")
+ a.classList.add("hostname")
+ a.classList.add(d.flags.online ? "online" : "offline")
+ a.textContent = d.nodeinfo.hostname
+ a.href = "#"
+ a.onclick = gotoProxy(d)
+ td1.appendChild(a)
+
+ if (has_location(d)) {
+ var span = document.createElement("span")
+ span.classList.add("icon")
+ span.classList.add("ion-location")
+ td1.appendChild(span)
+ }
+
+ if ("owner" in d.nodeinfo && showContact) {
+ var contact = d.nodeinfo.owner.contact
+ td1.appendChild(document.createTextNode(" – " + contact + ""))
+ }
+
+ var td2 = document.createElement("td")
+ td2.textContent = time
+
+ row.appendChild(td1)
+ row.appendChild(td2)
+ tbody.appendChild(row)
+ })
+
+ table.appendChild(tbody)
+ el.appendChild(table)
+ }
+
+ function showMeshstats(el, nodes) {
+ var h2 = document.createElement("h2")
+ h2.textContent = "Übersicht"
+ el.appendChild(h2)
+
+ var p = document.createElement("p")
+
+ var totalNodes = sum(nodes.filter(online).map(one))
+ var totalClients = sum(nodes.filter(online).map( function (d) {
+ return d.statistics.clients
+ }))
+ var totalGateways = sum(nodes.filter(online).filter( function (d) {
+ return d.flags.gateway
+ }).map(one))
+
+ p.textContent = totalNodes + " Knoten (online), " +
+ totalClients + " Clients, " +
+ totalGateways + " Gateways"
+
+ p.appendChild(document.createElement("br"))
+ p.appendChild(document.createTextNode("Diese Daten sind " + moment.utc(nodes.timestamp).fromNow(true) + " alt."))
+ el.appendChild(p)
+ }
+
+ function Infobox(sidebar) {
+ var self = this
+ el = undefined
+
+ function close() {
+ destroy()
+ pushHistory()
+ }
+
+ function destroy() {
+ if (el && el.parentNode) {
+ el.parentNode.removeChild(el)
+ el = undefined
+ }
+ }
+
+ self.create = function () {
+ destroy()
+
+ el = document.createElement("div")
+ sidebar.insertBefore(el, sidebar.firstChild)
+
+ el.scrollIntoView(false)
+ el.classList.add("infobox")
+ el.close = close
+ el.destroy = destroy
+
+ var closeButton = document.createElement("button")
+ closeButton.classList.add("close")
+ closeButton.onclick = close
+ el.appendChild(closeButton)
+
+ return el
+ }
+
+ return self
+ }
+
+ function showNodeinfo(config, infobox, gotoAnything, d) {
+ var el = infobox.create()
+
+ var h2 = document.createElement("h2")
+ h2.textContent = d.nodeinfo.hostname
+ var span = document.createElement("span")
+ span.classList.add(d.flags.online ? "online" : "offline")
+ span.textContent = " (" + (d.flags.online ? "online" : "offline, " + d.lastseen.fromNow(true)) + ")"
+ h2.appendChild(span)
+ el.appendChild(h2)
+
+ var attributes = document.createElement("table")
+ attributes.classList.add("attributes")
+
+ attributeEntry(attributes, "Gateway", d.flags.gateway ? "ja" : null)
+ attributeEntry(attributes, "In der Karte", has_location(d) ? "ja" : "nein")
+
+ if (config.showContact)
+ attributeEntry(attributes, "Kontakt", dictGet(d.nodeinfo, ["owner", "contact"]))
+
+ attributeEntry(attributes, "Hardware", dictGet(d.nodeinfo, ["hardware", "model"]))
+ attributeEntry(attributes, "Primäre MAC", dictGet(d.nodeinfo, ["network", "mac"]))
+ attributeEntry(attributes, "Firmware", showFirmware(d))
+ attributeEntry(attributes, "Uptime", showUptime(d))
+ attributeEntry(attributes, "Teil des Netzes", showFirstseen(d))
+ attributeEntry(attributes, "Arbeitsspeicher", showRAM(d))
+ attributeEntry(attributes, "IP Adressen", showIPs(d))
+ attributeEntry(attributes, "Clients", showClients(d))
+ el.appendChild(attributes)
+
+ if (d.neighbours.length > 0) {
+ var h3 = document.createElement("h3")
+ h3.textContent = "Nachbarknoten (" + d.neighbours.length + ")"
+ el.appendChild(h3)
+
+ var table = document.createElement("table")
+ var thead = document.createElement("thead")
+
+ var tr = document.createElement("tr")
+ var th1 = document.createElement("th")
+ th1.textContent = "Knoten"
+ th1.classList.add("sort-default")
+ tr.appendChild(th1)
+
+ var th2 = document.createElement("th")
+ th2.textContent = "TQ"
+ tr.appendChild(th2)
+
+ var th3 = document.createElement("th")
+ th3.textContent = "Entfernung"
+ tr.appendChild(th3)
+
+ thead.appendChild(tr)
+ table.appendChild(thead)
+
+ var tbody = document.createElement("tbody")
+
+ d.neighbours.forEach( function (d) {
+ var tr = document.createElement("tr")
+
+ var td1 = document.createElement("td")
+ var a1 = document.createElement("a")
+ a1.classList.add("hostname")
+ a1.textContent = d.node.nodeinfo.hostname
+ a1.href = "#"
+ a1.onclick = gotoAnything.node(d.node)
+ td1.appendChild(a1)
+
+ if (d.link.vpn)
+ td1.appendChild(document.createTextNode(" (VPN)"))
+
+ if (has_location(d.node)) {
+ var span = document.createElement("span")
+ span.classList.add("icon")
+ span.classList.add("ion-location")
+ td1.appendChild(span)
+ }
+
+ tr.appendChild(td1)
+
+ var td2 = document.createElement("td")
+ var a2 = document.createElement("a")
+ a2.href = "#"
+ a2.textContent = showTq(d.link)
+ a2.onclick = gotoAnything.link(d.link)
+ td2.appendChild(a2)
+ tr.appendChild(td2)
+
+ var td3 = document.createElement("td")
+ var a3 = document.createElement("a")
+ a3.href = "#"
+ a3.textContent = showDistance(d.link)
+ a3.onclick = gotoAnything.link(d.link)
+ td3.appendChild(a3)
+ td3.setAttribute("data-sort", d.link.distance !== undefined ? -d.link.distance : 1)
+ tr.appendChild(td3)
+
+ tbody.appendChild(tr)
+ })
+
+ table.appendChild(tbody)
+
+ new Tablesort(table)
+
+ el.appendChild(table)
+ }
+
+ function showFirmware(d) {
+ var release = dictGet(d.nodeinfo, ["software", "firmware", "release"])
+ var base = dictGet(d.nodeinfo, ["software", "firmware", "base"])
+
+ if (release === null || base === null)
+ return
+
+ return release + " / " + base
+ }
+
+ function showUptime(d) {
+ if (!("uptime" in d.statistics))
+ return
+
+ return moment.duration(d.statistics.uptime, "seconds").humanize()
+ }
+
+ function showFirstseen(d) {
+ if (!("firstseen" in d))
+ return
+
+ return d.firstseen.fromNow(true)
+ }
+
+ function showClients(d) {
+ if (!d.flags.online)
+ return
+
+ return function (el) {
+ el.appendChild(document.createTextNode(d.statistics.clients > 0 ? d.statistics.clients : "keine"))
+ el.appendChild(document.createElement("br"))
+
+ var span = document.createElement("span")
+ span.classList.add("clients")
+ span.textContent = " ".repeat(d.statistics.clients)
+ el.appendChild(span)
+ }
+ }
+
+ function showIPs(d) {
+ var ips = dictGet(d.nodeinfo, ["network", "addresses"])
+ if (ips === null)
+ return
+
+ ips.sort()
+
+ return function (el) {
+ ips.forEach( function (ip, i) {
+ var link = !ip.startsWith("fe80:")
+
+ if (i > 0)
+ el.appendChild(document.createElement("br"))
+
+ if (link) {
+ var a = document.createElement("a")
+ a.href = "http://[" + ip + "]/"
+ a.textContent = ip
+ el.appendChild(a)
+ } else
+ el.appendChild(document.createTextNode(ip))
+ })
+ }
+ }
+
+ function showRAM(d) {
+ if (!("memory_usage" in d.statistics))
+ return
+
+ return function (el) {
+ el.appendChild(showBar("memory-usage", d.statistics.memory_usage))
+ }
+ }
+ }
+
+ function attributeEntry(el, label, value) {
+ if (value === null || value == undefined)
+ return
+
+ var tr = document.createElement("tr")
+ var th = document.createElement("th")
+ th.textContent = label
+ tr.appendChild(th)
+
+ var td = document.createElement("td")
+
+ if (typeof value == "function")
+ value(td)
+ else
+ td.appendChild(document.createTextNode(value))
+
+ tr.appendChild(td)
+
+ el.appendChild(tr)
+
+ return td
+ }
+
+ function showBar(className, v) {
+ var span = document.createElement("span")
+ span.classList.add("bar")
+ span.classList.add(className)
+
+ var bar = document.createElement("span")
+ bar.style.width = (v * 100) + "%"
+ span.appendChild(bar)
+
+ var label = document.createElement("label")
+ label.textContent = (Math.round(v * 100)) + " %"
+ span.appendChild(label)
+
+ return span
+ }
+
+ function showLinkinfo(config, infobox, gotoAnything, d) {
+ var el = infobox.create()
+
+ var h2 = document.createElement("h2")
+ a1 = document.createElement("a")
+ a1.href = "#"
+ a1.onclick = gotoAnything.node(d.source.node)
+ a1.textContent = d.source.node.nodeinfo.hostname
+ h2.appendChild(a1)
+ h2.appendChild(document.createTextNode(" – "))
+ a2 = document.createElement("a")
+ a2.href = "#"
+ a2.onclick = gotoAnything.node(d.target.node)
+ a2.textContent = d.target.node.nodeinfo.hostname
+ h2.appendChild(a2)
+ el.appendChild(h2)
+
+ var attributes = document.createElement("table")
+ attributes.classList.add("attributes")
+
+ attributeEntry(attributes, "TQ", showTq(d))
+ attributeEntry(attributes, "Entfernung", showDistance(d))
+ attributeEntry(attributes, "VPN", d.vpn ? "ja" : "nein")
+
+ el.appendChild(attributes)
+ }
+
+ function pushHistory(d) {
+ var s = "#!"
+
+ if (d) {
+ if ("node" in d)
+ s += "n:" + d.node.nodeinfo.node_id
+
+ if ("link" in d)
+ s += "l:" + linkId(d.link)
+ }
+
+ window.history.pushState(s, undefined, s)
+ }
+
+ function gotoHistory(gotoAnything, dict, s) {
+ if (!s.startsWith("#!"))
+ return
+
+ s = s.slice(2)
+
+ var args = s.split(":")
+
+ if (args[0] === "n") {
+ var id = args[1]
+
+ if (id in dict.nodes)
+ gotoAnything.node(dict.nodes[id], true, false)()
+ }
+
+ if (args[0] === "l") {
+ var id = args[1]
+
+ if (id in dict.links)
+ gotoAnything.link(dict.links[id], true, false)()
+ }
+ }
+
+ function gotoBuilder(config, infobox, nodes, links) {
+ var markers = {}
+ var self = this
+
+ function gotoNode(d, showMap, push) {
+ showMap = trueDefault(showMap)
+ push = trueDefault(push)
+
+ if (showMap && d.nodeinfo.node_id in markers)
+ markers[d.nodeinfo.node_id]()
+
+ nodes(config, infobox, self, d)
+
+ if (push)
+ pushHistory( { node: d })
+
+ return false
+ }
+
+ function gotoLink(d, showMap, push) {
+ showMap = trueDefault(showMap)
+ push = trueDefault(push)
+
+ if (showMap && linkId(d) in markers)
+ markers[linkId(d)]()
+
+ links(config, infobox, self, d)
+
+ if (push)
+ pushHistory( { link: d })
+
+ return false
+ }
+
+ function addMarkers(d) {
+ markers = d
+ }
+
+ this.node = function (d, m, p) { return function () { return gotoNode(d, m, p) }}
+ this.link = function (d, m, p) { return function () { return gotoLink(d, m, p) }}
+ this.addMarkers = function (d) {
+ markers = d
+ }
+
+ return this
+ }
+})
diff --git a/lib/map.js b/lib/map.js
new file mode 100644
index 0000000..a7ba5f1
--- /dev/null
+++ b/lib/map.js
@@ -0,0 +1,140 @@
+define(function () {
+ var options = { worldCopyJump: true,
+ zoomControl: false
+ }
+ function mkMarker(dict, iconFunc, gotoAnything) {
+ return function (d) {
+ var opt = { icon: iconFunc(d),
+ title: d.nodeinfo.hostname
+ }
+
+ var m = L.marker([d.nodeinfo.location.latitude, d.nodeinfo.location.longitude], opt)
+
+ m.on('click', gotoAnything.node(d, false))
+ m.bindPopup(d.nodeinfo.hostname)
+
+ dict[d.nodeinfo.node_id] = m
+
+ return m
+ }
+ }
+
+ function addLinksToMap(dict, linkScale, graph, gotoAnything) {
+ graph = graph.filter( function (d) {
+ return "distance" in d
+ })
+
+ var lines = graph.map( function (d) {
+ var opts = { color: linkScale(d.tq).hex(),
+ weight: 4
+ }
+
+ var line = L.polyline(d.latlngs, opts)
+
+ line.bindPopup(d.source.node.nodeinfo.hostname + " – " + d.target.node.nodeinfo.hostname + "
" + showDistance(d) + " / " + showTq(d) + "")
+ line.on('click', gotoAnything.link(d, false))
+
+ dict[linkId(d)] = line
+
+ return line
+ })
+
+ return lines
+ }
+
+ var iconBase = { iconUrl: 'img/circlemarker.png',
+ iconRetinaUrl: 'img/circlemarker@2x.png',
+ iconSize: [17, 17],
+ iconAnchor: [8, 8],
+ popupAnchor: [0, -3]
+ }
+
+ var iconOnline = Object.assign({}, iconBase)
+ iconOnline.className = "node-online"
+ iconOnline = L.icon(iconOnline)
+
+ var iconOffline = Object.assign({}, iconBase)
+ iconOffline.className = "node-offline"
+ iconOffline = L.icon(iconOffline)
+
+ var iconNew = Object.assign({}, iconBase)
+ iconNew.className = "node-new"
+ iconNew = L.icon(iconNew)
+
+ var iconOfflineAlert = Object.assign({}, iconBase)
+ iconOfflineAlert.className = "node-offline node-alert"
+ iconOfflineAlert = L.icon(iconOfflineAlert)
+
+ return function () {
+ var self = this
+
+ var el = document.createElement("div")
+ el.classList.add("map")
+ self.div = el
+
+ var map = L.map(el, options)
+
+ L.control.zoom({ position: "topright" }).addTo(map)
+
+ L.tileLayer("https://otile{s}-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.jpg", {
+ subdomains: "1234",
+ type: "osm",
+ attribution: "Map data Tiles © MapQuest , Map data © OpenStreetMap contributors, CC-BY-SA",
+ maxZoom: 18
+ }).addTo(map)
+
+ self.setData = function (linkScale, sidebar, now, newnodes, lostnodes, onlinenodes, links, gotoAnything) {
+ var markersDict = {}
+
+ var lines = addLinksToMap(markersDict, linkScale, links, gotoAnything)
+
+ var nodes = newnodes.concat(lostnodes).filter(has_location)
+
+ var markers = nodes.map(mkMarker(markersDict, function (d) {
+ if (d.flags.online)
+ return iconNew
+
+ if (d.lastseen.isAfter(moment(now).subtract(1, 'days')))
+ return iconOfflineAlert
+ else
+ return iconOffline
+ }, gotoAnything))
+
+ var onlinemarkers = subtract(onlinenodes.filter(has_location), newnodes)
+ .map(mkMarker(markersDict, function (d) { return iconOnline }, gotoAnything))
+
+ var groupLines = L.featureGroup(lines).addTo(map)
+ var groupOnline = L.featureGroup(onlinemarkers).addTo(map)
+ var group = L.featureGroup(markers).addTo(map)
+
+ var bounds = group.getBounds()
+
+ if (!bounds.isValid())
+ bounds = groupOnline.getBounds()
+
+ if (bounds.isValid())
+ map.fitBounds(bounds, {paddingTopLeft: [sidebar.getWidth(), 0]})
+
+ var funcDict = {}
+
+ Object.keys(markersDict).map( function(k) {
+ funcDict[k] = function (d) {
+ var m = markersDict[k]
+ var bounds
+
+ if ("getBounds" in m)
+ bounds = m.getBounds()
+ else
+ bounds = L.latLngBounds([m.getLatLng()])
+
+ map.fitBounds(bounds, {paddingTopLeft: [sidebar.getWidth(), 0]})
+ m.openPopup(bounds.getCenter())
+ }
+ })
+
+ return funcDict
+ }
+
+ return self
+ }
+})