Completed
Pull Request — develop (#85)
by Xaver
01:12
created

map.js ➔ ... ➔ map.zoom   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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