Skip to content
Snippets Groups Projects
Select Git revision
  • d28e834cd583825181f59b56db4a1572d09c0fe0
  • develop default protected
2 results

labellayer.js

Blame
  • labellayer.js 10.40 KiB
    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) {
          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(helper.escape(d.hostname));
    
            dict[d.node_id] = m;
    
            return m;
          };
        }
    
        function addLinksToMap(dict, linkScale, graph) {
          graph = graph.filter(function (d) {
            return 'distance' in d && d.type.indexOf('vpn') !== 0;
          });
    
          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(helper.escape(d.source.hostname + ' – ' + d.target.hostname) +
              '<br><strong>' + helper.showDistance(d) + ' / ' + helper.showTq(d.source_tq) + ' - ' + helper.showTq(d.target_tq) + '<br>' + d.type + '</strong>');
    
            line.on('click', function () {
              router.fullUrl({ link: d.id });
            });
    
            dict[d.id] = line;
    
            return line;
          });
        }
    
        function getIcon(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) {
            var iconOnline = getIcon('online');
            var iconOffline = getIcon('offline');
            var iconLost = getIcon('lost');
            var iconAlert = getIcon('alert');
            var iconNew = getIcon('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);
            groupLines = L.featureGroup(lines).addTo(map);
    
            var nodesOnline = helper.subtract(data.nodes.online, data.nodes.new).filter(helper.hasLocation);
            var nodesOffline = helper.subtract(data.nodes.offline, data.nodes.lost).filter(helper.hasLocation);
            var nodesNew = data.nodes.new.filter(helper.hasLocation);
            var nodesLost = data.nodes.lost.filter(helper.hasLocation);
    
            var markersOnline = nodesOnline.map(mkMarker(nodeDict, function () {
              return iconOnline;
            }));
    
            var markersOffline = nodesOffline.map(mkMarker(nodeDict, function () {
              return iconOffline;
            }));
    
            var markersNew = nodesNew.map(mkMarker(nodeDict, function () {
              return iconNew;
            }));
    
            var markersLost = nodesLost.map(mkMarker(nodeDict, function (d) {
              var age = moment(data.now).diff(d.lastseen, 'days', true);
              if (age <= config.maxAgeAlert) {
                return iconAlert;
              }
              if (age <= config.maxAge) {
                return iconLost;
              }
              return null;
            }));
    
            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,
              offline: nodesOffline,
              new: nodesNew,
              lost: nodesLost
            };
            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(config.icon.offline.color, 9, 5, false));
            var labelsNew = d.new.map(prepareLabel(config.map.labelNewColor, 11, 8, true));
            var labelsLost = d.lost.map(prepareLabel(config.icon.lost.color, 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] = new 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 = new 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;
          }
        });
      });