Completed
Pull Request — develop (#123)
by Xaver
01:09
created

map.js ➔ ... ➔ line.click   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
nc 1
dl 0
loc 3
rs 10
nop 0
1
define(['map/clientlayer', 'map/labellayer', 'leaflet', 'moment', 'map/locationmarker', 'rbush', 'helper'],
2
  function (ClientLayer, LabelLayer, L, moment, LocationMarker, rbush, helper) {
3
    'use strict';
4
5
    var options = {
6
      worldCopyJump: true,
7
      zoomControl: true,
8
      minZoom: 0
9
    };
10
11
    var ButtonBase = L.Control.extend({
12
      options: {
13
        position: 'bottomright'
14
      },
15
16
      active: false,
17
      button: undefined,
18
19
      initialize: function (f, o) {
20
        L.Util.setOptions(this, o);
21
        this.f = f;
22
      },
23
24
      update: function () {
25
        this.button.classList.toggle('active', this.active);
26
      },
27
28
      set: function (v) {
29
        this.active = v;
30
        this.update();
31
      }
32
    });
33
34
    var LocateButton = ButtonBase.extend({
35
      onAdd: function () {
36
        var button = L.DomUtil.create('button', 'ion-locate shadow');
37
        button.setAttribute('data-tooltip', _.t('button.tracking'));
38
        L.DomEvent.disableClickPropagation(button);
39
        L.DomEvent.addListener(button, 'click', this.onClick, this);
40
41
        this.button = button;
42
43
        return button;
44
      },
45
46
      onClick: function () {
47
        this.f(!this.active);
48
      }
49
    });
50
51
    var CoordsPickerButton = ButtonBase.extend({
52
      onAdd: function () {
53
        var button = L.DomUtil.create('button', 'ion-pin shadow');
54
        button.setAttribute('data-tooltip', _.t('button.location'));
55
56
        // Click propagation isn't disabled as this causes problems with the
57
        // location picking mode; instead propagation is stopped in onClick().
58
        L.DomEvent.addListener(button, 'click', this.onClick, this);
59
60
        this.button = button;
61
62
        return button;
63
      },
64
65
      onClick: function (e) {
66
        L.DomEvent.stopPropagation(e);
67
        this.f(!this.active);
68
      }
69
    });
70
71
    function mkMarker(dict, iconFunc, router) {
72
      return function (d) {
73
        var m = L.circleMarker([d.nodeinfo.location.latitude, d.nodeinfo.location.longitude], iconFunc(d));
74
75
        m.resetStyle = function resetStyle() {
76
          m.setStyle(iconFunc(d));
77
        };
78
79
        m.on('click', function () {
80
          router.fullUrl({ node: d.nodeinfo.node_id });
81
        });
82
        m.bindTooltip(d.nodeinfo.hostname);
83
84
        dict[d.nodeinfo.node_id] = m;
85
86
        return m;
87
      };
88
    }
89
90
    function addLinksToMap(dict, linkScale, graph, router) {
91
      graph = graph.filter(function (d) {
92
        return 'distance' in d && !d.vpn;
93
      });
94
95
      return graph.map(function (d) {
96
        var opts = {
97
          color: linkScale(1 / d.tq),
98
          weight: 4,
99
          opacity: 0.5,
100
          dashArray: 'none'
101
        };
102
103
        var line = L.polyline(d.latlngs, opts);
104
105
        line.resetStyle = function resetStyle() {
106
          line.setStyle(opts);
107
        };
108
109
        line.bindTooltip(d.source.node.nodeinfo.hostname + ' – ' + d.target.node.nodeinfo.hostname + '<br><strong>' + helper.showDistance(d) + ' / ' + helper.showTq(d) + '</strong>');
110
        line.on('click', function () {
111
          router.fullUrl({ link: d.id });
112
        });
113
114
        dict[d.id] = line;
115
116
        return line;
117
      });
118
    }
119
120
    var iconOnline = {
121
      color: '#1566A9',
122
      fillColor: '#1566A9',
123
      radius: 6,
124
      fillOpacity: 0.5,
125
      opacity: 0.5,
126
      weight: 2,
127
      className: 'stroke-first'
128
    };
129
    var iconOffline = {
130
      color: '#D43E2A',
131
      fillColor: '#D43E2A',
132
      radius: 3,
133
      fillOpacity: 0.5,
134
      opacity: 0.5,
135
      weight: 1,
136
      className: 'stroke-first'
137
    };
138
    var iconLost = {
139
      color: '#D43E2A',
140
      fillColor: '#D43E2A',
141
      radius: 4,
142
      fillOpacity: 0.8,
143
      opacity: 0.8,
144
      weight: 1,
145
      className: 'stroke-first'
146
    };
147
    var iconAlert = {
148
      color: '#D43E2A',
149
      fillColor: '#D43E2A',
150
      radius: 5,
151
      fillOpacity: 0.8,
152
      opacity: 0.8,
153
      weight: 2,
154
      className: 'stroke-first'
155
    };
156
    var iconNew = { color: '#1566A9', fillColor: '#93E929', radius: 6, fillOpacity: 1.0, opacity: 0.5, weight: 2 };
157
158
    return function (config, linkScale, sidebar, router, buttons) {
159
      var self = this;
160
      var groupOnline;
161
      var groupOffline;
162
      var groupNew;
163
      var groupLost;
164
      var groupLines;
165
      var savedView;
166
167
      var map;
168
      var userLocation;
169
      var layerControl;
170
      var baseLayers = {};
171
172
      var locateUserButton = new LocateButton(function (d) {
173
        if (d) {
174
          enableTracking();
175
        } else {
176
          disableTracking();
177
        }
178
      });
179
180
      var mybuttons = [];
181
182
      function addButton(button) {
183
        var el = button.onAdd();
184
        mybuttons.push(el);
185
        buttons.appendChild(el);
186
      }
187
188
      function clearButtons() {
189
        mybuttons.forEach(function (d) {
190
          buttons.removeChild(d);
191
        });
192
      }
193
194
      var showCoordsPickerButton = new CoordsPickerButton(function (d) {
195
        if (d) {
196
          enableCoords();
197
        } else {
198
          disableCoords();
199
        }
200
      });
201
202
      function saveView() {
203
        savedView = {
204
          center: map.getCenter(),
205
          zoom: map.getZoom()
206
        };
207
      }
208
209
      function enableTracking() {
210
        map.locate({
211
          watch: true,
212
          enableHighAccuracy: true,
213
          setView: true
214
        });
215
        locateUserButton.set(true);
216
      }
217
218
      function disableTracking() {
219
        map.stopLocate();
220
        locationError();
221
        locateUserButton.set(false);
222
      }
223
224
      function enableCoords() {
225
        map.getContainer().classList.add('pick-coordinates');
226
        map.on('click', showCoordinates);
227
        showCoordsPickerButton.set(true);
228
      }
229
230
      function disableCoords() {
231
        map.getContainer().classList.remove('pick-coordinates');
232
        map.off('click', showCoordinates);
233
        showCoordsPickerButton.set(false);
234
      }
235
236
      function showCoordinates(e) {
237
        router.fullUrl({ zoom: map.getZoom(), lat: e.latlng.lat, lng: e.latlng.lng });
238
        disableCoords();
239
      }
240
241
      function locationFound(e) {
242
        if (!userLocation) {
243
          userLocation = new LocationMarker(e.latlng).addTo(map);
244
        }
245
246
        userLocation.setLatLng(e.latlng);
247
        userLocation.setAccuracy(e.accuracy);
248
      }
249
250
      function locationError() {
251
        if (userLocation) {
252
          map.removeLayer(userLocation);
253
          userLocation = null;
254
        }
255
      }
256
257
      function contextMenuOpenLayerMenu() {
258
        document.querySelector('.leaflet-control-layers').classList.add('leaflet-control-layers-expanded');
259
      }
260
261
      var el = document.createElement('div');
262
      el.classList.add('map');
263
264
      map = L.map(el, options);
265
      var now = new Date();
266
      config.mapLayers.forEach(function (item, i) {
267
        if ((typeof item.config.start === 'number' && item.config.start <= now.getHours()) || (typeof item.config.end === 'number' && item.config.end > now.getHours())) {
268
          item.config.order = item.config.start * -1;
269
        } else {
270
          item.config.order = i;
271
        }
272
      });
273
274
      config.mapLayers = config.mapLayers.sort(function (a, b) {
275
        return a.config.order - b.config.order;
276
      });
277
278
      var layers = config.mapLayers.map(function (d) {
279
        return {
280
          'name': d.name,
281
          'layer': 'url' in d ? L.tileLayer(d.url.replace('{retina}', L.Browser.retina ? '@2x' : ''), d.config) : console.warn('Missing map url')
282
        };
283
      });
284
285
      map.addLayer(layers[0].layer);
286
287
      layers.forEach(function (d) {
288
        baseLayers[d.name] = d.layer;
289
      });
290
291
      map.on('locationfound', locationFound);
292
      map.on('locationerror', locationError);
293
      map.on('dragend', saveView);
294
      map.on('contextmenu', contextMenuOpenLayerMenu);
295
296
      addButton(locateUserButton);
297
      addButton(showCoordsPickerButton);
298
299
      layerControl = L.control.layers(baseLayers, [], { position: 'bottomright' });
300
      layerControl.addTo(map);
301
302
      map.zoomControl.setPosition('topright');
303
304
      var clientLayer = new ClientLayer({ minZoom: config.clientZoom });
305
      clientLayer.addTo(map);
306
      clientLayer.setZIndex(5);
307
308
      var labelLayer = new LabelLayer({ minZoom: config.labelZoom });
309
      labelLayer.addTo(map);
310
      labelLayer.setZIndex(6);
311
312
      map.on('zoom', function () {
313
        clientLayer.redraw();
314
        labelLayer.redraw();
315
      });
316
317
      map.on('baselayerchange', function (e) {
318
        map.options.maxZoom = e.layer.options.maxZoom;
319
        clientLayer.options.maxZoom = map.options.maxZoom;
320
        labelLayer.options.maxZoom = map.options.maxZoom;
321
        if (map.getZoom() > map.options.maxZoom) {
322
          map.setZoom(map.options.maxZoom);
323
        }
324
325
        var style = document.querySelector('.css-mode:not([media="not"])');
326
        if (style && e.layer.options.mode !== '' && !style.classList.contains(e.layer.options.mode)) {
327
          style.media = 'not';
328
          labelLayer.updateLayer();
329
        }
330
        if (e.layer.options.mode) {
331
          var newStyle = document.querySelector('.css-mode.' + e.layer.options.mode);
332
          newStyle.media = '';
333
          newStyle.appendChild(document.createTextNode(''));
334
          labelLayer.updateLayer();
335
        }
336
      });
337
338
      var nodeDict = {};
339
      var linkDict = {};
340
      var highlight;
341
342
      function resetMarkerStyles(nodes, links) {
343
        Object.keys(nodes).forEach(function (d) {
344
          nodes[d].resetStyle();
345
        });
346
347
        Object.keys(links).forEach(function (d) {
348
          links[d].resetStyle();
349
        });
350
      }
351
352
      function setView(bounds, zoom) {
353
        map.fitBounds(bounds, { paddingTopLeft: [sidebar(), 0], maxZoom: (zoom ? zoom : config.nodeZoom) });
354
      }
355
356
      function goto(m) {
357
        var bounds;
358
359
        if ('getBounds' in m) {
360
          bounds = m.getBounds();
361
        } else {
362
          bounds = L.latLngBounds([m.getLatLng()]);
363
        }
364
365
        setView(bounds);
366
367
        return m;
368
      }
369
370
      function updateView(nopanzoom) {
371
        resetMarkerStyles(nodeDict, linkDict);
372
        var m;
373
374
        if (highlight !== undefined) {
375
          if (highlight.type === 'node' && nodeDict[highlight.o.nodeinfo.node_id]) {
376
            m = nodeDict[highlight.o.nodeinfo.node_id];
377
            m.setStyle({ color: 'orange', weight: 20, fillOpacity: 1, opacity: 0.7, className: 'stroke-first' });
378
          } else if (highlight.type === 'link' && linkDict[highlight.o.id]) {
379
            m = linkDict[highlight.o.id];
380
            m.setStyle({ weight: 4, opacity: 1, dashArray: '5, 10' });
381
          }
382
        }
383
384
        if (!nopanzoom) {
385
          if (m) {
386
            goto(m);
387
          } else if (savedView) {
388
            map.setView(savedView.center, savedView.zoom);
389
          } else {
390
            setView(config.fixedCenter);
391
          }
392
        }
393
      }
394
395
      function mapRTree(d) {
396
        return {
397
          minX: d.nodeinfo.location.latitude, minY: d.nodeinfo.location.longitude,
398
          maxX: d.nodeinfo.location.latitude, maxY: d.nodeinfo.location.longitude,
399
          node: d
400
        };
401
      }
402
403
      self.setData = function setData(data) {
404
        nodeDict = {};
405
        linkDict = {};
406
407
        if (groupOffline) {
408
          groupOffline.clearLayers();
409
        }
410
411
        if (groupOnline) {
412
          groupOnline.clearLayers();
413
        }
414
415
        if (groupNew) {
416
          groupNew.clearLayers();
417
        }
418
419
        if (groupLost) {
420
          groupLost.clearLayers();
421
        }
422
423
        if (groupLines) {
424
          groupLines.clearLayers();
425
        }
426
427
        var lines = addLinksToMap(linkDict, linkScale, data.graph.links, router);
428
        groupLines = L.featureGroup(lines).addTo(map);
429
430
        var nodesOnline = helper.subtract(data.nodes.all.filter(helper.online), data.nodes.new);
431
        var nodesOffline = helper.subtract(data.nodes.all.filter(helper.offline), data.nodes.lost);
432
433
        var markersOnline = nodesOnline.filter(helper.hasLocation)
434
          .map(mkMarker(nodeDict, function () {
435
            return iconOnline;
436
          }, router));
437
438
        var markersOffline = nodesOffline.filter(helper.hasLocation)
439
          .map(mkMarker(nodeDict, function () {
440
            return iconOffline;
441
          }, router));
442
443
        var markersNew = data.nodes.new.filter(helper.hasLocation)
444
          .map(mkMarker(nodeDict, function () {
445
            return iconNew;
446
          }, router));
447
448
        var markersLost = data.nodes.lost.filter(helper.hasLocation)
449
          .map(mkMarker(nodeDict, function (d) {
450
            if (d.lastseen.isAfter(moment(data.now).subtract(config.maxAgeAlert, 'days'))) {
451
              return iconAlert;
452
            }
453
454
            if (d.lastseen.isAfter(moment(data.now).subtract(config.maxAge, 'days'))) {
455
              return iconLost;
456
            }
457
            return null;
458
          }, router));
459
460
        groupOffline = L.featureGroup(markersOffline).addTo(map);
461
        groupLost = L.featureGroup(markersLost).addTo(map);
462
        groupOnline = L.featureGroup(markersOnline).addTo(map);
463
        groupNew = L.featureGroup(markersNew).addTo(map);
464
465
        var rtreeOnlineAll = rbush(9);
466
467
        rtreeOnlineAll.load(data.nodes.all.filter(helper.online).filter(helper.hasLocation).map(mapRTree));
468
469
        clientLayer.setData(rtreeOnlineAll);
470
        labelLayer.setData({
471
          online: nodesOnline.filter(helper.hasLocation),
472
          offline: nodesOffline.filter(helper.hasLocation),
473
          new: data.nodes.new.filter(helper.hasLocation),
474
          lost: data.nodes.lost.filter(helper.hasLocation)
475
        });
476
477
        updateView(true);
478
      };
479
480
      self.resetView = function resetView() {
481
        disableTracking();
482
        highlight = undefined;
483
        updateView();
484
      };
485
486
      self.gotoNode = function gotoNode(d) {
487
        disableTracking();
488
        highlight = { type: 'node', o: d };
489
        updateView();
490
      };
491
492
      self.gotoLink = function gotoLink(d) {
493
        disableTracking();
494
        highlight = { type: 'link', o: d };
495
        updateView();
496
      };
497
498
      self.gotoLocation = function gotoLocation(d) {
499
        disableTracking();
500
        map.setView([d.lat, d.lng], d.zoom);
501
      };
502
503
      self.destroy = function destroy() {
504
        clearButtons();
505
        map.remove();
506
507
        if (el.parentNode) {
508
          el.parentNode.removeChild(el);
509
        }
510
      };
511
512
      self.render = function render(d) {
513
        d.appendChild(el);
514
        map.invalidateSize();
515
      };
516
517
      return self;
518
    };
519
  });
520