diff --git a/README.md b/README.md index a28bc71..61d2cc5 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Meshviewer is an online visualization app to represent nodes and links on a map #### Main differences to https://github.com/ffnord/meshviewer _Some similar features might have been implemented/merged_ +- Replaced router - including language, mode, node, link, location - Leaflet upgraded to v1 - faster on mobile - Forcegraph rewrite with d3.js v4 - Map layer modes (Allow to set a default layer based on time combined with a stylesheet) diff --git a/app.js b/app.js index 5bc3e5a..1901493 100644 --- a/app.js +++ b/app.js @@ -57,6 +57,7 @@ require.config({ baseUrl: 'lib', paths: { 'polyglot': '../node_modules/node-polyglot/build/polyglot', + 'Navigo': '../node_modules/navigo/lib/navigo', 'leaflet': '../node_modules/leaflet/dist/leaflet', 'moment': '../node_modules/moment/moment', // d3 modules indirect dependencies @@ -78,8 +79,7 @@ require.config({ 'd3-drag': '../node_modules/d3-drag/build/d3-drag', 'virtual-dom': '../node_modules/virtual-dom/dist/virtual-dom', 'rbush': '../node_modules/rbush/rbush', - 'helper': 'utils/helper', - 'language': 'utils/language' + 'helper': 'utils/helper' }, shim: { 'd3-drag': ['d3-selection'], diff --git a/lib/forcegraph.js b/lib/forcegraph.js index 43592de..4b28245 100644 --- a/lib/forcegraph.js +++ b/lib/forcegraph.js @@ -46,13 +46,12 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr var n = force.find(e[0], e[1], NODE_RADIUS_SELECT); if (n !== undefined) { - router.node(n.o.node)(); + router.fullUrl({ node: n.o.node.nodeinfo.node_id }); return; } e = { x: e[0], y: e[1] }; - var closedLink; var radius = LINK_RADIUS_SELECT; intLinks @@ -65,7 +64,7 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr }); if (closedLink !== undefined) { - router.link(closedLink.o)(); + router.fullUrl({ link: closedLink.o.id }); } } @@ -226,6 +225,11 @@ define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'utils/math', 'forcegr redraw(); }; + + self.gotoLocation = function gotoLocation() { + // ignore + }; + self.destroy = function destroy() { force.stop(); canvas.remove(); diff --git a/lib/gui.js b/lib/gui.js index a4f4b64..d317d66 100644 --- a/lib/gui.js +++ b/lib/gui.js @@ -66,11 +66,13 @@ define(['d3-interpolate', 'map', 'sidebar', 'tabs', 'container', 'legend', buttonToggle.classList.add('ion-eye', 'shadow'); buttonToggle.setAttribute('data-tooltip', _.t('button.switchView')); buttonToggle.onclick = function onclick() { + var data; if (content.constructor === Map) { - router.view('g'); + data = { view: 'graph', lat: undefined, lng: undefined, zoom: undefined }; } else { - router.view('m'); + data = { view: 'map' }; } + router.fullUrl(data, false, true); }; buttons.appendChild(buttonToggle); @@ -119,10 +121,8 @@ define(['d3-interpolate', 'map', 'sidebar', 'tabs', 'container', 'legend', router.addTarget(title); router.addTarget(infobox); - router.addView('m', mkView(Map)); - router.addView('g', mkView(ForceGraph)); - - router.view('m'); + router.addView('map', mkView(Map)); + router.addView('graph', mkView(ForceGraph)); self.setData = fanoutUnfiltered.setData; diff --git a/lib/infobox/link.js b/lib/infobox/link.js index fa653cb..2e4e36e 100644 --- a/lib/infobox/link.js +++ b/lib/infobox/link.js @@ -18,8 +18,7 @@ define(['helper'], function (helper) { var a1; if (!unknown) { a1 = document.createElement('a'); - a1.href = router.getUrl({ n: d.source.node_id }); - a1.onclick = router.node(d.source.node); + a1.href = router.generateLink({ node: d.source.node_id }); } else { a1 = document.createElement('span'); } @@ -31,8 +30,7 @@ define(['helper'], function (helper) { h2.appendChild(arrow); var a2 = document.createElement('a'); - a2.href = router.getUrl({ n: d.target.node_id }); - a2.onclick = router.node(d.target.node); + a2.href = router.generateLink({ node: d.target.node_id }); a2.textContent = d.target.node.nodeinfo.hostname; h2.appendChild(a2); el.appendChild(h2); diff --git a/lib/infobox/main.js b/lib/infobox/main.js index 679f3cf..2822ed3 100644 --- a/lib/infobox/main.js +++ b/lib/infobox/main.js @@ -28,35 +28,21 @@ define(['infobox/link', 'infobox/node', 'infobox/location'], function (link, nod var closeButton = document.createElement('button'); closeButton.classList.add('close'); closeButton.classList.add('ion-close'); - closeButton.onclick = router.reset; - el.appendChild(closeButton); - } - - function clear() { - var closeButton = el.firstChild; - while (el.firstChild) { - el.removeChild(el.firstChild); - } + closeButton.onclick = function () { + router.fullUrl(); + }; el.appendChild(closeButton); } self.resetView = destroy; - self.gotoNode = function gotoNode(d, update) { - if (update !== true) { - create(); - } else { - clear(); - } + self.gotoNode = function gotoNode(d) { + create(); node(config, el, router, d); }; - self.gotoLink = function gotoLink(d, update) { - if (update !== true) { - create(); - } else { - clear(); - } + self.gotoLink = function gotoLink(d) { + create(); link(config, el, router, d); }; diff --git a/lib/infobox/node.js b/lib/infobox/node.js index 6bd94c4..aa599c9 100644 --- a/lib/infobox/node.js +++ b/lib/infobox/node.js @@ -191,7 +191,12 @@ define(['sorttable', 'virtual-dom', 'd3-interpolate', 'moment', 'helper'], } if (!unknown) { - name.push(V.h('a', { href: router.getUrl({ n: n.node.nodeinfo.node_id }), onclick: router.node(n.node), className: 'online' }, n.node.nodeinfo.hostname)); + name.push(V.h('a', { + href: router.generateLink({ node: n.node.nodeinfo.node_id }), + onclick: function (e) { + router.fullUrl({ node: n.node.nodeinfo.node_id }, e); + }, className: 'online' + }, n.node.nodeinfo.hostname)); } else { name.push(n.link.id); } diff --git a/lib/linklist.js b/lib/linklist.js index 4f97974..5391a45 100644 --- a/lib/linklist.js +++ b/lib/linklist.js @@ -33,7 +33,12 @@ define(['sorttable', 'virtual-dom', 'helper'], function (SortTable, V, helper) { table.el.classList.add('link-list'); function renderRow(d) { - var td1Content = [V.h('a', { href: router.getUrl({ l: d.id }), onclick: router.link(d) }, linkName(d))]; + var td1Content = [V.h('a', { + href: router.generateLink({ link: d.id }), + onclick: function (e) { + router.fullUrl({ link: d.id }, e); + } + }, linkName(d))]; var td1 = V.h('td', td1Content); var td2 = V.h('td', { style: { color: linkScale(1 / d.tq) } }, helper.showTq(d)); diff --git a/lib/main.js b/lib/main.js index b1f68c9..4b863bc 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,5 +1,5 @@ -define(['polyglot', 'moment', 'router', 'leaflet', 'gui', 'helper', 'language'], - function (Polyglot, moment, Router, L, GUI, helper, Language) { +define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'], + function (moment, Router, L, GUI, helper, Language) { 'use strict'; return function (config) { @@ -117,7 +117,6 @@ define(['polyglot', 'moment', 'router', 'leaflet', 'gui', 'helper', 'language'], } }); - links.sort(function (a, b) { return b.tq - a.tq; }); @@ -138,8 +137,7 @@ define(['polyglot', 'moment', 'router', 'leaflet', 'gui', 'helper', 'language'], } var language = new Language(config); - - var router = new Router(); + var router = new Router(language); var urls = []; @@ -153,6 +151,7 @@ define(['polyglot', 'moment', 'router', 'leaflet', 'gui', 'helper', 'language'], } function update() { + language.init(router); return Promise.all(urls.map(helper.getJSON)) .then(handleData); } @@ -162,13 +161,12 @@ define(['polyglot', 'moment', 'router', 'leaflet', 'gui', 'helper', 'language'], var gui = new GUI(config, router, language); gui.setData(d); router.setData(d); - router.start(); + router.resolve(); window.setInterval(function () { update().then(function (n) { gui.setData(n); router.setData(n); - router.update(); }); }, 60000); }) diff --git a/lib/map.js b/lib/map.js index 1210838..f569e49 100644 --- a/lib/map.js +++ b/lib/map.js @@ -76,7 +76,9 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm m.setStyle(iconFunc(d)); }; - m.on('click', router.node(d)); + m.on('click', function () { + router.fullUrl({ node: d.nodeinfo.node_id }); + }); m.bindTooltip(d.nodeinfo.hostname); dict[d.nodeinfo.node_id] = m; @@ -105,7 +107,9 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm }; line.bindTooltip(d.source.node.nodeinfo.hostname + ' – ' + d.target.node.nodeinfo.hostname + '
' + helper.showDistance(d) + ' / ' + helper.showTq(d) + ''); - line.on('click', router.link(d)); + line.on('click', function () { + router.fullUrl({ link: d.id }); + }); dict[d.id] = line; @@ -230,7 +234,7 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm } function showCoordinates(e) { - router.gotoLocation(e.latlng); + router.fullUrl({ zoom: map.getZoom(), lat: e.latlng.lat, lng: e.latlng.lng }); disableCoords(); } @@ -345,8 +349,8 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm }); } - function setView(bounds) { - map.fitBounds(bounds, { paddingTopLeft: [sidebar(), 0], maxZoom: config.nodeZoom }); + function setView(bounds, zoom) { + map.fitBounds(bounds, { paddingTopLeft: [sidebar(), 0], maxZoom: (zoom ? zoom : config.nodeZoom) }); } function goto(m) { @@ -479,20 +483,21 @@ define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationm updateView(); }; - self.gotoNode = function gotoNode(d, update) { + self.gotoNode = function gotoNode(d) { disableTracking(); highlight = { type: 'node', o: d }; - updateView(update); + updateView(); }; - self.gotoLink = function gotoLink(d, update) { + self.gotoLink = function gotoLink(d) { disableTracking(); highlight = { type: 'link', o: d }; - updateView(update); + updateView(); }; - self.gotoLocation = function gotoLocation() { - // ignore + self.gotoLocation = function gotoLocation(d) { + disableTracking(); + map.setView([d.lat, d.lng], d.zoom); }; self.destroy = function destroy() { diff --git a/lib/nodelist.js b/lib/nodelist.js index 2afc083..7432b95 100644 --- a/lib/nodelist.js +++ b/lib/nodelist.js @@ -65,8 +65,10 @@ define(['sorttable', 'virtual-dom', 'helper'], function (SortTable, V, helper) { td1Content.push(V.h('a', { className: aClass.join(' '), - onclick: router.node(d), - href: router.getUrl({ n: d.nodeinfo.node_id }) + href: router.generateLink({ node: d.nodeinfo.node_id }), + onclick: function (e) { + router.fullUrl({ node: d.nodeinfo.node_id }, e); + } }, d.nodeinfo.hostname)); if (helper.hasLocation(d)) { td0Content.push(V.h('span', { className: 'icon ion-location' })); diff --git a/lib/router.js b/lib/router.js deleted file mode 100644 index 70fd75b..0000000 --- a/lib/router.js +++ /dev/null @@ -1,242 +0,0 @@ -define(['helper'], function (helper) { - 'use strict'; - - return function () { - var self = this; - var objects = { nodes: {}, links: {} }; - var targets = []; - var views = {}; - var currentView; - var currentObject; - var running = false; - - function saveState() { - var e = '#!'; - - e += 'v:' + currentView; - - if (currentObject) { - if ('node' in currentObject) { - e += ';n:' + encodeURIComponent(currentObject.node.nodeinfo.node_id); - } else if ('link' in currentObject) { - e += ';l:' + encodeURIComponent(currentObject.link.id); - } - } - - window.history.pushState(e, undefined, e); - } - - function resetView(push) { - push = helper.trueDefault(push); - - targets.forEach(function (t) { - t.resetView(); - }); - - if (push) { - currentObject = undefined; - saveState(); - } - } - - function gotoNode(d, update) { - if (!d) { - return false; - } - - targets.forEach(function (t) { - t.gotoNode(d, update); - }); - - return true; - } - - function gotoLink(d, update) { - if (!d) { - return false; - } - - targets.forEach(function (t) { - t.gotoLink(d, update); - }); - - return true; - } - - function loadState(s, update) { - if (!s) { - return false; - } - - s = decodeURIComponent(s); - - if (!s.startsWith('#!')) { - return false; - } - - var targetSet = false; - - s.slice(2).split(';').forEach(function (d) { - var args = d.split(':'); - - if (update !== true && args[0] === 'v' && args[1] in views) { - currentView = args[1]; - views[args[1]](); - } - - var id; - - if (args[0] === 'n') { - id = args[1]; - if (id in objects.nodes) { - currentObject = { node: objects.nodes[id] }; - gotoNode(objects.nodes[id], update); - targetSet = true; - } - } - - if (args[0] === 'l') { - id = args[1]; - if (id in objects.links) { - currentObject = { link: objects.links[id] }; - gotoLink(objects.links[id], update); - targetSet = true; - } - } - }); - - return targetSet; - } - - self.getUrl = function getUrl(data) { - var e = '#!'; - - if (data.n) { - e += 'n:' + encodeURIComponent(data.n); - } - - if (data.l) { - e += 'l:' + encodeURIComponent(data.l); - } - - return e; - }; - - self.start = function start() { - running = true; - - if (!loadState(window.location.hash)) { - resetView(false); - } - - window.onpopstate = function onpopstate(d) { - if (!loadState(d.state)) { - resetView(false); - } - }; - }; - - self.view = function view(d) { - if (d in views) { - views[d](); - - if (!currentView || running) { - currentView = d; - } - - if (!running) { - return; - } - - saveState(); - - if (!currentObject) { - resetView(false); - return; - } - - if ('node' in currentObject) { - gotoNode(currentObject.node); - } - - if ('link' in currentObject) { - gotoLink(currentObject.link); - } - } - }; - - self.node = function node(d) { - return function () { - if (gotoNode(d)) { - currentObject = { node: d }; - saveState(); - } - - return false; - }; - }; - - self.link = function link(d) { - return function () { - if (gotoLink(d)) { - currentObject = { link: d }; - saveState(); - } - - return false; - }; - }; - - self.gotoLocation = function gotoLocation(d) { - if (!d) { - return false; - } - - targets.forEach(function (t) { - if (!t.gotoLocation) { - console.warn('has no gotoLocation', t); - } - t.gotoLocation(d); - }); - - return true; - }; - - self.reset = function reset() { - resetView(); - }; - - self.addTarget = function addTarget(d) { - targets.push(d); - }; - - self.removeTarget = function removeTarget(d) { - targets = targets.filter(function (e) { - return d !== e; - }); - }; - - self.addView = function addView(k, d) { - views[k] = d; - }; - - self.setData = function setData(data) { - objects.nodes = {}; - objects.links = {}; - - data.nodes.all.forEach(function (d) { - objects.nodes[d.nodeinfo.node_id] = d; - }); - - data.graph.links.forEach(function (d) { - objects.links[d.id] = d; - }); - }; - - self.update = function update() { - loadState(window.location.hash, true); - }; - - return self; - }; -}); diff --git a/lib/simplenodelist.js b/lib/simplenodelist.js index 81123c7..a4e9e3f 100644 --- a/lib/simplenodelist.js +++ b/lib/simplenodelist.js @@ -41,8 +41,10 @@ define(['moment', 'virtual-dom', 'helper'], function (moment, V, helper) { td1Content.push(V.h('a', { className: aClass.join(' '), - onclick: router.node(d), - href: router.getUrl({ n: d.nodeinfo.node_id }) + href: router.generateLink({ node: d.nodeinfo.node_id }), + onclick: function (e) { + router.fullUrl({ node: d.nodeinfo.node_id }, e); + } }, d.nodeinfo.hostname)); if (helper.hasLocation(d)) { diff --git a/lib/title.js b/lib/title.js index c1ee542..be92f8d 100644 --- a/lib/title.js +++ b/lib/title.js @@ -17,15 +17,11 @@ define(function () { }; this.gotoNode = function gotoNode(d) { - if (d) { - setTitle(d.nodeinfo.hostname); - } + setTitle(d.nodeinfo.hostname); }; this.gotoLink = function gotoLink(d) { - if (d) { - setTitle((d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + ' – ' + d.target.node.nodeinfo.hostname); - } + setTitle((d.source.node ? d.source.node.nodeinfo.hostname : d.source.id) + ' – ' + d.target.node.nodeinfo.hostname); }; this.gotoLocation = function gotoLocation() { diff --git a/lib/utils/language.js b/lib/utils/language.js index 501fdd2..5364d22 100644 --- a/lib/utils/language.js +++ b/lib/utils/language.js @@ -1,10 +1,12 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) { 'use strict'; return function (config) { + var router; + function languageSelect(el) { var select = document.createElement('select'); select.className = 'language-switch'; - select.addEventListener('change', setLocale); + select.addEventListener('change', setSelectLocale); el.appendChild(select); // Keep english @@ -14,8 +16,12 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) { } } - function setLocale(event) { - localStorage.setItem('language', getLocale(event.target.value)); + function setSelectLocale(event) { + router.fullUrl({ lang: event.target.value }, false, true); + } + + function setLocale(lang) { + localStorage.setItem('language', getLocale(lang)); location.reload(); } @@ -51,10 +57,16 @@ define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) { } } - window._ = new Polyglot({ locale: getLocale(), allowMissing: true }); - helper.getJSON('locale/' + _.locale() + '.json?' + config.cacheBreaker).then(setTranslation); + function init(r) { + router = r; + window._ = new Polyglot({ locale: getLocale(router.getLang()), allowMissing: true }); + helper.getJSON('locale/' + _.locale() + '.json?' + config.cacheBreaker).then(setTranslation); + } return { + init: init, + getLocale: getLocale, + setLocale: setLocale, languageSelect: languageSelect }; }; diff --git a/lib/utils/router.js b/lib/utils/router.js new file mode 100644 index 0000000..2fb1988 --- /dev/null +++ b/lib/utils/router.js @@ -0,0 +1,156 @@ +define(['Navigo'], function (Navigo) { + 'use strict'; + + return function (language) { + var init = false; + var objects = { nodes: {}, links: {} }; + var targets = []; + var views = {}; + var current = {}; + var state = { lang: language.getLocale(), view: 'map' }; + + function resetView() { + targets.forEach(function (t) { + t.resetView(); + }); + } + + function gotoNode(d) { + if (d.nodeId in objects.nodes) { + targets.forEach(function (t) { + t.gotoNode(objects.nodes[d.nodeId]); + }); + } + } + + function gotoLink(d) { + if (d.linkId in objects.links) { + targets.forEach(function (t) { + t.gotoLink(objects.links[d.linkId]); + }); + } + } + + function view(d) { + if (d.view in views) { + views[d.view](); + state.view = d.view; + resetView(); + } + } + + function customRoute(lang, viewValue, node, link, zoom, lat, lng) { + current = { + lang: lang, + view: viewValue, + node: node, + link: link, + zoom: zoom, + lat: lat, + lng: lng + }; + + if (lang && lang !== state.lang && lang === language.getLocale(lang)) { + language.setLocale(lang); + } + + if (!init || viewValue && viewValue !== state.view) { + if (!viewValue) { + viewValue = state.view; + } + view({ view: viewValue }); + init = true; + } + + if (node) { + gotoNode({ nodeId: node }); + } else if (link) { + gotoLink({ linkId: link }); + } else if (lat) { + targets.forEach(function (t) { + t.gotoLocation({ + zoom: parseInt(zoom, 10), + lat: parseFloat(lat), + lng: parseFloat(lng) + }); + }); + } else { + resetView(); + } + } + + var router = new Navigo(null, true); + + router + .on(/^\/?#?\/([\w]{2})?\/?(map|graph)?\/?([a-f\d]{12})?([a-f\d\-]{25})?\/?(?:(\d+)\/([\d.]+)\/([\d.]+))?$/, customRoute) + .on({ + '*': function () { + router.fullUrl(); + } + }); + + router.generateLink = function generateLink(data, full, deep) { + var result = '#'; + + if (full) { + data = Object.assign({}, state, data); + } else if (deep) { + data = Object.assign({}, current, data); + } + + for (var key in data) { + if (!data.hasOwnProperty(key) || data[key] === undefined) { + continue; + } + result += '/' + data[key]; + } + + return result; + }; + + router.fullUrl = function fullUrl(data, e, deep) { + if (e) { + e.preventDefault(); + } + router.navigate(router.generateLink(data, !deep, deep)); + }; + + router.getLang = function getLang() { + var lang = location.hash.match(/^\/?#\/([\w]{2})\//); + if (lang) { + state.lang = language.getLocale(lang[1]); + return lang[1]; + } + return null; + }; + + router.addTarget = function addTarget(d) { + targets.push(d); + }; + + router.removeTarget = function removeTarget(d) { + targets = targets.filter(function (e) { + return d !== e; + }); + }; + + router.addView = function addView(k, d) { + views[k] = d; + }; + + router.setData = function setData(data) { + objects.nodes = {}; + objects.links = {}; + + data.nodes.all.forEach(function (d) { + objects.nodes[d.nodeinfo.node_id] = d; + }); + + data.graph.links.forEach(function (d) { + objects.links[d.id] = d; + }); + }; + + return router; + }; +}); diff --git a/package.json b/package.json index 6ed775c..701f3c9 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "d3-zoom": "^1.1.3", "leaflet": "^1.0.3", "moment": "^2.17.1", + "navigo": "^4.6.0", "node-polyglot": "^2.2.2", "promise-polyfill": "^6.0.2", "rbush": "^2.0.1", diff --git a/yarn.lock b/yarn.lock index 4740663..1250b61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3259,6 +3259,10 @@ natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" +navigo@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/navigo/-/navigo-4.6.0.tgz#212cf08e1658874243a4acaea041aee42c30480a" + ncname@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/ncname/-/ncname-1.0.0.tgz#5b57ad18b1ca092864ef62b0b1ed8194f383b71c"