353 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			353 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| define(['leaflet', 'rbush', 'helper', 'moment'],
 | ||
|   function (L, rbush, helper, moment) {
 | ||
|     'use strict';
 | ||
| 
 | ||
|     var groupOnline;
 | ||
|     var groupOffline;
 | ||
|     var groupNew;
 | ||
|     var groupLost;
 | ||
|     var groupLines;
 | ||
| 
 | ||
|     var labelLocations = [['left', 'middle', 0 / 8],
 | ||
|       ['center', 'top', 6 / 8],
 | ||
|       ['right', 'middle', 4 / 8],
 | ||
|       ['left', 'top', 7 / 8],
 | ||
|       ['left', 'ideographic', 1 / 8],
 | ||
|       ['right', 'top', 5 / 8],
 | ||
|       ['center', 'ideographic', 2 / 8],
 | ||
|       ['right', 'ideographic', 3 / 8]];
 | ||
|     var labelShadow;
 | ||
|     var bodyStyle = { fontFamily: 'sans-serif' };
 | ||
|     var nodeRadius = 4;
 | ||
| 
 | ||
|     var cFont = document.createElement('canvas').getContext('2d');
 | ||
| 
 | ||
|     function measureText(font, text) {
 | ||
|       cFont.font = font;
 | ||
|       return cFont.measureText(text);
 | ||
|     }
 | ||
| 
 | ||
|     function mapRTree(d) {
 | ||
|       return { minX: d.position.lat, minY: d.position.lng, maxX: d.position.lat, maxY: d.position.lng, label: d };
 | ||
|     }
 | ||
| 
 | ||
|     function prepareLabel(fillStyle, fontSize, offset, stroke) {
 | ||
|       return function (d) {
 | ||
|         var font = fontSize + 'px ' + bodyStyle.fontFamily;
 | ||
|         return {
 | ||
|           position: L.latLng(d.location.latitude, d.location.longitude),
 | ||
|           label: d.hostname,
 | ||
|           offset: offset,
 | ||
|           fillStyle: fillStyle,
 | ||
|           height: fontSize * 1.2,
 | ||
|           font: font,
 | ||
|           stroke: stroke,
 | ||
|           width: measureText(font, d.hostname).width
 | ||
|         };
 | ||
|       };
 | ||
|     }
 | ||
| 
 | ||
|     function calcOffset(offset, loc) {
 | ||
|       return [offset * Math.cos(loc[2] * 2 * Math.PI),
 | ||
|         offset * Math.sin(loc[2] * 2 * Math.PI)];
 | ||
|     }
 | ||
| 
 | ||
|     function labelRect(p, offset, anchor, label, minZoom, maxZoom, z) {
 | ||
|       var margin = 1 + 1.41 * (1 - (z - minZoom) / (maxZoom - minZoom));
 | ||
| 
 | ||
|       var width = label.width * margin;
 | ||
|       var height = label.height * margin;
 | ||
| 
 | ||
|       var dx = {
 | ||
|         left: 0,
 | ||
|         right: -width,
 | ||
|         center: -width / 2
 | ||
|       };
 | ||
| 
 | ||
|       var dy = {
 | ||
|         top: 0,
 | ||
|         ideographic: -height,
 | ||
|         middle: -height / 2
 | ||
|       };
 | ||
| 
 | ||
|       var x = p.x + offset[0] + dx[anchor[0]];
 | ||
|       var y = p.y + offset[1] + dy[anchor[1]];
 | ||
| 
 | ||
|       return { minX: x, minY: y, maxX: x + width, maxY: y + height };
 | ||
|     }
 | ||
| 
 | ||
|     function mkMarker(dict, iconFunc, router) {
 | ||
|       return function (d) {
 | ||
|         var m = L.circleMarker([d.location.latitude, d.location.longitude], iconFunc(d));
 | ||
| 
 | ||
|         m.resetStyle = function resetStyle() {
 | ||
|           m.setStyle(iconFunc(d));
 | ||
|         };
 | ||
| 
 | ||
|         m.on('click', function () {
 | ||
|           router.fullUrl({ node: d.node_id });
 | ||
|         });
 | ||
|         m.bindTooltip(d.hostname);
 | ||
| 
 | ||
|         dict[d.node_id] = m;
 | ||
| 
 | ||
|         return m;
 | ||
|       };
 | ||
|     }
 | ||
| 
 | ||
|     function addLinksToMap(dict, linkScale, graph, router) {
 | ||
|       graph = graph.filter(function (d) {
 | ||
|         return 'distance' in d && !d.vpn;
 | ||
|       });
 | ||
| 
 | ||
|       return graph.map(function (d) {
 | ||
|         var opts = {
 | ||
|           color: linkScale((d.source_tq + d.target_tq) / 2),
 | ||
|           weight: 4,
 | ||
|           opacity: 0.5,
 | ||
|           dashArray: 'none'
 | ||
|         };
 | ||
| 
 | ||
|         var line = L.polyline(d.latlngs, opts);
 | ||
| 
 | ||
|         line.resetStyle = function resetStyle() {
 | ||
|           line.setStyle(opts);
 | ||
|         };
 | ||
| 
 | ||
|         line.bindTooltip(d.source.hostname + ' – ' + d.target.hostname + '<br><strong>' + helper.showDistance(d) + ' / ' + helper.showTq(d.source_tq) + ' - ' + helper.showTq(d.target_tq) + '</strong>');
 | ||
|         line.on('click', function () {
 | ||
|           router.fullUrl({ link: d.id });
 | ||
|         });
 | ||
| 
 | ||
|         dict[d.id] = line;
 | ||
| 
 | ||
|         return line;
 | ||
|       });
 | ||
|     }
 | ||
| 
 | ||
|     function getIcon(config, color) {
 | ||
|       return Object.assign({}, config.icon.base, config.icon[color]);
 | ||
|     }
 | ||
| 
 | ||
|     return L.GridLayer.extend({
 | ||
|       onAdd: function (map) {
 | ||
|         L.GridLayer.prototype.onAdd.call(this, map);
 | ||
|         if (this.data) {
 | ||
|           this.prepareLabels();
 | ||
|         }
 | ||
|       },
 | ||
|       setData: function (data, map, nodeDict, linkDict, linkScale, router, config) {
 | ||
|         var iconOnline = getIcon(config, 'online');
 | ||
|         var iconOffline = getIcon(config, 'offline');
 | ||
|         var iconLost = getIcon(config, 'lost');
 | ||
|         var iconAlert = getIcon(config, 'alert');
 | ||
|         var iconNew = getIcon(config, 'new');
 | ||
|         // Check if init or data is already set
 | ||
|         if (groupLines) {
 | ||
|           groupOffline.clearLayers();
 | ||
|           groupOnline.clearLayers();
 | ||
|           groupNew.clearLayers();
 | ||
|           groupLost.clearLayers();
 | ||
|           groupLines.clearLayers();
 | ||
|         }
 | ||
| 
 | ||
|         var lines = addLinksToMap(linkDict, linkScale, data.links, router);
 | ||
|         groupLines = L.featureGroup(lines).addTo(map);
 | ||
| 
 | ||
|         var nodesOnline = helper.subtract(data.nodes.all.filter(helper.online), data.nodes.new);
 | ||
|         var nodesOffline = helper.subtract(data.nodes.all.filter(helper.offline), data.nodes.lost);
 | ||
| 
 | ||
|         var markersOnline = nodesOnline.filter(helper.hasLocation)
 | ||
|           .map(mkMarker(nodeDict, function () {
 | ||
|             return iconOnline;
 | ||
|           }, router));
 | ||
| 
 | ||
|         var markersOffline = nodesOffline.filter(helper.hasLocation)
 | ||
|           .map(mkMarker(nodeDict, function () {
 | ||
|             return iconOffline;
 | ||
|           }, router));
 | ||
| 
 | ||
|         var markersNew = data.nodes.new.filter(helper.hasLocation)
 | ||
|           .map(mkMarker(nodeDict, function () {
 | ||
|             return iconNew;
 | ||
|           }, router));
 | ||
| 
 | ||
|         var markersLost = data.nodes.lost.filter(helper.hasLocation)
 | ||
|           .map(mkMarker(nodeDict, function (d) {
 | ||
|             if (d.lastseen.isAfter(moment(data.now).subtract(config.maxAgeAlert, 'days'))) {
 | ||
|               return iconAlert;
 | ||
|             }
 | ||
| 
 | ||
|             if (d.lastseen.isAfter(moment(data.now).subtract(config.maxAge, 'days'))) {
 | ||
|               return iconLost;
 | ||
|             }
 | ||
|             return null;
 | ||
|           }, router));
 | ||
| 
 | ||
|         groupOffline = L.featureGroup(markersOffline).addTo(map);
 | ||
|         groupLost = L.featureGroup(markersLost).addTo(map);
 | ||
|         groupOnline = L.featureGroup(markersOnline).addTo(map);
 | ||
|         groupNew = L.featureGroup(markersNew).addTo(map);
 | ||
| 
 | ||
|         this.data = {
 | ||
|           online: nodesOnline.filter(helper.hasLocation),
 | ||
|           offline: nodesOffline.filter(helper.hasLocation),
 | ||
|           new: data.nodes.new.filter(helper.hasLocation),
 | ||
|           lost: data.nodes.lost.filter(helper.hasLocation)
 | ||
|         };
 | ||
|         this.updateLayer();
 | ||
|       },
 | ||
|       updateLayer: function () {
 | ||
|         if (this._map) {
 | ||
|           this.prepareLabels();
 | ||
|         }
 | ||
|       },
 | ||
|       prepareLabels: function () {
 | ||
|         var d = this.data;
 | ||
| 
 | ||
|         // label:
 | ||
|         // - position (WGS84 coords)
 | ||
|         // - offset (2D vector in pixels)
 | ||
|         // - anchor (tuple, textAlignment, textBaseline)
 | ||
|         // - minZoom (inclusive)
 | ||
|         // - label (string)
 | ||
|         // - color (string)
 | ||
| 
 | ||
|         var labelsOnline = d.online.map(prepareLabel(null, 11, 8, true));
 | ||
|         var labelsOffline = d.offline.map(prepareLabel('rgba(212, 62, 42, 0.9)', 9, 5, false));
 | ||
|         var labelsNew = d.new.map(prepareLabel('rgba(48, 99, 20, 0.9)', 11, 8, true));
 | ||
|         var labelsLost = d.lost.map(prepareLabel('rgba(212, 62, 42, 0.9)', 11, 8, true));
 | ||
| 
 | ||
|         var labels = []
 | ||
|           .concat(labelsNew)
 | ||
|           .concat(labelsLost)
 | ||
|           .concat(labelsOnline)
 | ||
|           .concat(labelsOffline);
 | ||
| 
 | ||
|         var minZoom = this.options.minZoom;
 | ||
|         var maxZoom = this.options.maxZoom;
 | ||
| 
 | ||
|         var trees = [];
 | ||
| 
 | ||
|         var map = this._map;
 | ||
| 
 | ||
|         function nodeToRect(z) {
 | ||
|           return function (n) {
 | ||
|             var p = map.project(n.position, z);
 | ||
|             return { minX: p.x - nodeRadius, minY: p.y - nodeRadius, maxX: p.x + nodeRadius, maxY: p.y + nodeRadius };
 | ||
|           };
 | ||
|         }
 | ||
| 
 | ||
|         for (var z = minZoom; z <= maxZoom; z++) {
 | ||
|           trees[z] = rbush(9);
 | ||
|           trees[z].load(labels.map(nodeToRect(z)));
 | ||
|         }
 | ||
| 
 | ||
|         labels = labels.map(function (n) {
 | ||
|           var best = labelLocations.map(function (loc) {
 | ||
|             var offset = calcOffset(n.offset, loc);
 | ||
|             var i;
 | ||
| 
 | ||
|             for (i = maxZoom; i >= minZoom; i--) {
 | ||
|               var p = map.project(n.position, i);
 | ||
|               var rect = labelRect(p, offset, loc, n, minZoom, maxZoom, i);
 | ||
|               var candidates = trees[i].search(rect);
 | ||
| 
 | ||
|               if (candidates.length > 0) {
 | ||
|                 break;
 | ||
|               }
 | ||
|             }
 | ||
| 
 | ||
|             return { loc: loc, z: i + 1 };
 | ||
|           }).filter(function (k) {
 | ||
|             return k.z <= maxZoom;
 | ||
|           }).sort(function (a, b) {
 | ||
|             return a.z - b.z;
 | ||
|           })[0];
 | ||
| 
 | ||
|           if (best !== undefined) {
 | ||
|             n.offset = calcOffset(n.offset, best.loc);
 | ||
|             n.minZoom = best.z;
 | ||
|             n.anchor = best.loc;
 | ||
| 
 | ||
|             for (var i = maxZoom; i >= best.z; i--) {
 | ||
|               var p = map.project(n.position, i);
 | ||
|               var rect = labelRect(p, n.offset, best.loc, n, minZoom, maxZoom, i);
 | ||
|               trees[i].insert(rect);
 | ||
|             }
 | ||
| 
 | ||
|             return n;
 | ||
|           }
 | ||
|           return undefined;
 | ||
|         }).filter(function (n) {
 | ||
|           return n !== undefined;
 | ||
|         });
 | ||
| 
 | ||
|         this.margin = 16;
 | ||
| 
 | ||
|         if (labels.length > 0) {
 | ||
|           this.margin += labels.map(function (n) {
 | ||
|             return n.width;
 | ||
|           }).sort().reverse()[0];
 | ||
|         }
 | ||
| 
 | ||
|         this.labels = rbush(9);
 | ||
|         this.labels.load(labels.map(mapRTree));
 | ||
| 
 | ||
|         this.redraw();
 | ||
|       },
 | ||
|       createTile: function (tilePoint) {
 | ||
|         var tile = L.DomUtil.create('canvas', 'leaflet-tile');
 | ||
| 
 | ||
|         var tileSize = this.options.tileSize;
 | ||
|         tile.width = tileSize;
 | ||
|         tile.height = tileSize;
 | ||
| 
 | ||
|         if (!this.labels) {
 | ||
|           return tile;
 | ||
|         }
 | ||
| 
 | ||
|         var s = tilePoint.multiplyBy(tileSize);
 | ||
|         var map = this._map;
 | ||
|         bodyStyle = window.getComputedStyle(document.querySelector('body'));
 | ||
|         labelShadow = bodyStyle.backgroundColor.replace(/rgb/i, 'rgba').replace(/\)/i, ',0.7)');
 | ||
| 
 | ||
|         function projectNodes(d) {
 | ||
|           var p = map.project(d.label.position);
 | ||
| 
 | ||
|           p.x -= s.x;
 | ||
|           p.y -= s.y;
 | ||
| 
 | ||
|           return { p: p, label: d.label };
 | ||
|         }
 | ||
| 
 | ||
|         var bbox = helper.getTileBBox(s, map, tileSize, this.margin);
 | ||
|         var labels = this.labels.search(bbox).map(projectNodes);
 | ||
|         var ctx = tile.getContext('2d');
 | ||
| 
 | ||
|         ctx.lineWidth = 5;
 | ||
|         ctx.strokeStyle = labelShadow;
 | ||
|         ctx.miterLimit = 2;
 | ||
| 
 | ||
|         function drawLabel(d) {
 | ||
|           ctx.font = d.label.font;
 | ||
|           ctx.textAlign = d.label.anchor[0];
 | ||
|           ctx.textBaseline = d.label.anchor[1];
 | ||
|           ctx.fillStyle = d.label.fillStyle === null ? bodyStyle.color : d.label.fillStyle;
 | ||
| 
 | ||
|           if (d.label.stroke) {
 | ||
|             ctx.strokeText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1]);
 | ||
|           }
 | ||
| 
 | ||
|           ctx.fillText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1]);
 | ||
|         }
 | ||
| 
 | ||
|         labels.filter(function (d) {
 | ||
|           return tilePoint.z >= d.label.minZoom;
 | ||
|         }).forEach(drawLabel);
 | ||
| 
 | ||
|         return tile;
 | ||
|       }
 | ||
|     });
 | ||
|   });
 |