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"