blob: e5fc263895bd9c7922bfa51d78f3246310cf59ed [file] [log] [blame]
Simon Hunt737c89f2015-01-28 12:23:19 -08001/*
2 * Copyright 2015 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17/*
18 ONOS GUI -- Topology Event Module.
19 Defines event handling for events received from the server.
20 */
21
22(function () {
23 'use strict';
24
25 // injected refs
Simon Hunt5724fb42015-02-05 16:59:40 -080026 var $log, fs, sus, is, ts, flash, tis, icfg, uplink;
Simon Huntac4c6f72015-02-03 19:50:53 -080027
28 // configuration
29 var labelConfig = {
30 imgPad: 16,
31 padLR: 4,
32 padTB: 3,
33 marginLR: 3,
34 marginTB: 2,
35 port: {
36 gap: 3,
37 width: 18,
38 height: 14
39 }
40 };
41
42 var deviceIconConfig = {
43 xoff: -20,
44 yoff: -18
45 };
Simon Hunt737c89f2015-01-28 12:23:19 -080046
Simon Hunt1894d792015-02-04 17:09:20 -080047 var linkConfig = {
48 light: {
49 baseColor: '#666',
50 inColor: '#66f',
51 outColor: '#f00',
52 },
53 dark: {
Simon Hunt5724fb42015-02-05 16:59:40 -080054 baseColor: '#aaa',
Simon Hunt1894d792015-02-04 17:09:20 -080055 inColor: '#66f',
Simon Hunt5724fb42015-02-05 16:59:40 -080056 outColor: '#f66'
Simon Hunt1894d792015-02-04 17:09:20 -080057 },
58 inWidth: 12,
59 outWidth: 10
60 };
61
Simon Hunt737c89f2015-01-28 12:23:19 -080062 // internal state
Simon Huntac4c6f72015-02-03 19:50:53 -080063 var settings, // merged default settings and options
Simon Hunt737c89f2015-01-28 12:23:19 -080064 force, // force layout object
65 drag, // drag behavior handler
66 network = {
67 nodes: [],
68 links: [],
69 lookup: {},
70 revLinkToKey: {}
Simon Huntac4c6f72015-02-03 19:50:53 -080071 },
Simon Hunt1894d792015-02-04 17:09:20 -080072 lu = network.lookup, // shorthand
Simon Huntac4c6f72015-02-03 19:50:53 -080073 deviceLabelIndex = 0, // for device label cycling
Simon Hunt1894d792015-02-04 17:09:20 -080074 hostLabelIndex = 0, // for host label cycling
Simon Hunt5724fb42015-02-05 16:59:40 -080075 showHosts = true, // whether hosts are displayed
76 showOffline = true, // whether offline devices are displayed
77 oblique = false, // whether we are in the oblique view
Simon Hunt445e8152015-02-06 13:00:12 -080078 nodeLock = false, // whether nodes can be dragged or not (locked)
79 width, height, // the width and height of the force layout
80 hovered; // the node over which the mouse is hovering
Simon Hunt737c89f2015-01-28 12:23:19 -080081
82 // SVG elements;
83 var linkG, linkLabelG, nodeG;
84
85 // D3 selections;
86 var link, linkLabel, node;
87
88 // default settings for force layout
89 var defaultSettings = {
90 gravity: 0.4,
91 friction: 0.7,
92 charge: {
93 // note: key is node.class
94 device: -8000,
95 host: -5000,
96 _def_: -12000
97 },
98 linkDistance: {
99 // note: key is link.type
100 direct: 100,
101 optical: 120,
102 hostLink: 3,
103 _def_: 50
104 },
105 linkStrength: {
106 // note: key is link.type
107 // range: {0.0 ... 1.0}
108 //direct: 1.0,
109 //optical: 1.0,
110 //hostLink: 1.0,
111 _def_: 1.0
112 }
113 };
114
115
Simon Huntac4c6f72015-02-03 19:50:53 -0800116 // ==========================
117 // === EVENT HANDLERS
118
119 function addDevice(data) {
120 var id = data.id,
121 d;
122
Simon Hunt1894d792015-02-04 17:09:20 -0800123 uplink.showNoDevs(false);
Simon Huntac4c6f72015-02-03 19:50:53 -0800124
125 // although this is an add device event, if we already have the
126 // device, treat it as an update instead..
Simon Hunt1894d792015-02-04 17:09:20 -0800127 if (lu[id]) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800128 updateDevice(data);
129 return;
130 }
131
132 d = createDeviceNode(data);
133 network.nodes.push(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800134 lu[id] = d;
Simon Huntac4c6f72015-02-03 19:50:53 -0800135
136 $log.debug("Created new device.. ", d.id, d.x, d.y);
137
138 updateNodes();
139 fStart();
140 }
141
142 function updateDevice(data) {
143 var id = data.id,
Simon Hunt1894d792015-02-04 17:09:20 -0800144 d = lu[id],
Simon Huntac4c6f72015-02-03 19:50:53 -0800145 wasOnline;
146
147 if (d) {
148 wasOnline = d.online;
149 angular.extend(d, data);
150 if (positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800151 sendUpdateMeta(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800152 }
153 updateNodes();
154 if (wasOnline !== d.online) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800155 findAttachedLinks(d.id).forEach(restyleLinkElement);
156 updateOfflineVisibility(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800157 }
158 } else {
159 // TODO: decide whether we want to capture logic errors
160 //logicError('updateDevice lookup fail. ID = "' + id + '"');
161 }
162 }
163
Simon Hunt1894d792015-02-04 17:09:20 -0800164 function removeDevice(data) {
165 var id = data.id,
166 d = lu[id];
167 if (d) {
168 removeDeviceElement(d);
169 } else {
170 // TODO: decide whether we want to capture logic errors
171 //logicError('removeDevice lookup fail. ID = "' + id + '"');
172 }
173 }
174
175 function addHost(data) {
176 var id = data.id,
177 d, lnk;
178
179 // although this is an add host event, if we already have the
180 // host, treat it as an update instead..
181 if (lu[id]) {
182 updateHost(data);
183 return;
184 }
185
186 d = createHostNode(data);
187 network.nodes.push(d);
188 lu[id] = d;
189
190 $log.debug("Created new host.. ", d.id, d.x, d.y);
191
192 updateNodes();
193
194 lnk = createHostLink(data);
195 if (lnk) {
196
197 $log.debug("Created new host-link.. ", lnk.key);
198
199 d.linkData = lnk; // cache ref on its host
200 network.links.push(lnk);
201 lu[d.ingress] = lnk;
202 lu[d.egress] = lnk;
203 updateLinks();
204 }
205
206 fStart();
207 }
208
209 function updateHost(data) {
210 var id = data.id,
211 d = lu[id];
212 if (d) {
213 angular.extend(d, data);
214 if (positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800215 sendUpdateMeta(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800216 }
217 updateNodes();
218 } else {
219 // TODO: decide whether we want to capture logic errors
220 //logicError('updateHost lookup fail. ID = "' + id + '"');
221 }
222 }
223
224 function removeHost(data) {
225 var id = data.id,
226 d = lu[id];
227 if (d) {
228 removeHostElement(d, true);
229 } else {
230 // may have already removed host, if attached to removed device
231 //console.warn('removeHost lookup fail. ID = "' + id + '"');
232 }
233 }
234
235 function addLink(data) {
236 var result = findLink(data, 'add'),
237 bad = result.badLogic,
238 d = result.ldata;
239
240 if (bad) {
241 //logicError(bad + ': ' + link.id);
242 return;
243 }
244
245 if (d) {
246 // we already have a backing store link for src/dst nodes
247 addLinkUpdate(d, data);
248 return;
249 }
250
251 // no backing store link yet
252 d = createLink(data);
253 if (d) {
254 network.links.push(d);
255 lu[d.key] = d;
256 updateLinks();
257 fStart();
258 }
259 }
260
261 function updateLink(data) {
262 var result = findLink(data, 'update'),
263 bad = result.badLogic;
264 if (bad) {
265 //logicError(bad + ': ' + link.id);
266 return;
267 }
268 result.updateWith(link);
269 }
270
271 function removeLink(data) {
272 var result = findLink(data, 'remove'),
273 bad = result.badLogic;
274 if (bad) {
275 // may have already removed link, if attached to removed device
276 //console.warn(bad + ': ' + link.id);
277 return;
278 }
279 result.removeRawLink();
280 }
281
282 // ========================
283
284 function addLinkUpdate(ldata, link) {
285 // add link event, but we already have the reverse link installed
286 ldata.fromTarget = link;
287 network.revLinkToKey[link.id] = ldata.key;
288 restyleLinkElement(ldata);
289 }
290
291 function createLink(link) {
292 var lnk = linkEndPoints(link.src, link.dst);
293
294 if (!lnk) {
295 return null;
296 }
297
298 angular.extend(lnk, {
299 key: link.id,
300 class: 'link',
301 fromSource: link,
302
303 // functions to aggregate dual link state
304 type: function () {
305 var s = lnk.fromSource,
306 t = lnk.fromTarget;
307 return (s && s.type) || (t && t.type) || defaultLinkType;
308 },
309 online: function () {
310 var s = lnk.fromSource,
311 t = lnk.fromTarget,
312 both = lnk.source.online && lnk.target.online;
313 return both && ((s && s.online) || (t && t.online));
314 },
315 linkWidth: function () {
316 var s = lnk.fromSource,
317 t = lnk.fromTarget,
318 ws = (s && s.linkWidth) || 0,
319 wt = (t && t.linkWidth) || 0;
320 return Math.max(ws, wt);
321 }
322 });
323 return lnk;
324 }
325
326
327 function makeNodeKey(d, what) {
328 var port = what + 'Port';
329 return d[what] + '/' + d[port];
330 }
331
332 function makeLinkKey(d, flipped) {
333 var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
334 two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
335 return one + '-' + two;
336 }
337
338 var widthRatio = 1.4,
339 linkScale = d3.scale.linear()
340 .domain([1, 12])
341 .range([widthRatio, 12 * widthRatio])
Simon Hunt5724fb42015-02-05 16:59:40 -0800342 .clamp(true),
343 allLinkTypes = 'direct indirect optical tunnel',
Simon Hunt1894d792015-02-04 17:09:20 -0800344 defaultLinkType = 'direct';
345
346 function restyleLinkElement(ldata) {
347 // this fn's job is to look at raw links and decide what svg classes
348 // need to be applied to the line element in the DOM
349 var th = ts.theme(),
350 el = ldata.el,
351 type = ldata.type(),
352 lw = ldata.linkWidth(),
353 online = ldata.online();
354
355 el.classed('link', true);
356 el.classed('inactive', !online);
357 el.classed(allLinkTypes, false);
358 if (type) {
359 el.classed(type, true);
360 }
361 el.transition()
362 .duration(1000)
363 .attr('stroke-width', linkScale(lw))
364 .attr('stroke', linkConfig[th].baseColor);
365 }
366
Simon Hunt5724fb42015-02-05 16:59:40 -0800367 function findLinkById(id) {
368 // check to see if this is a reverse lookup, else default to given id
369 var key = network.revLinkToKey[id] || id;
370 return key && lu[key];
371 }
372
Simon Hunt1894d792015-02-04 17:09:20 -0800373 function findLink(linkData, op) {
374 var key = makeLinkKey(linkData),
375 keyrev = makeLinkKey(linkData, 1),
376 link = lu[key],
377 linkRev = lu[keyrev],
378 result = {},
379 ldata = link || linkRev,
380 rawLink;
381
382 if (op === 'add') {
383 if (link) {
384 // trying to add a link that we already know about
385 result.ldata = link;
386 result.badLogic = 'addLink: link already added';
387
388 } else if (linkRev) {
389 // we found the reverse of the link to be added
390 result.ldata = linkRev;
391 if (linkRev.fromTarget) {
392 result.badLogic = 'addLink: link already added';
393 }
394 }
395 } else if (op === 'update') {
396 if (!ldata) {
397 result.badLogic = 'updateLink: link not found';
398 } else {
399 rawLink = link ? ldata.fromSource : ldata.fromTarget;
400 result.updateWith = function (data) {
401 angular.extend(rawLink, data);
402 restyleLinkElement(ldata);
403 }
404 }
405 } else if (op === 'remove') {
406 if (!ldata) {
407 result.badLogic = 'removeLink: link not found';
408 } else {
409 rawLink = link ? ldata.fromSource : ldata.fromTarget;
410
411 if (!rawLink) {
412 result.badLogic = 'removeLink: link not found';
413
414 } else {
415 result.removeRawLink = function () {
416 if (link) {
417 // remove fromSource
418 ldata.fromSource = null;
419 if (ldata.fromTarget) {
420 // promote target into source position
421 ldata.fromSource = ldata.fromTarget;
422 ldata.fromTarget = null;
423 ldata.key = keyrev;
424 delete network.lookup[key];
425 network.lookup[keyrev] = ldata;
426 delete network.revLinkToKey[keyrev];
427 }
428 } else {
429 // remove fromTarget
430 ldata.fromTarget = null;
431 delete network.revLinkToKey[keyrev];
432 }
433 if (ldata.fromSource) {
434 restyleLinkElement(ldata);
435 } else {
436 removeLinkElement(ldata);
437 }
438 }
439 }
440 }
441 }
442 return result;
443 }
444
Simon Hunt1c367112015-02-05 18:02:46 -0800445 function findDevices(offlineOnly) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800446 var a = [];
447 network.nodes.forEach(function (d) {
Simon Hunt1c367112015-02-05 18:02:46 -0800448 if (d.class === 'device' && !(offlineOnly && d.online)) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800449 a.push(d);
450 }
451 });
452 return a;
453 }
Simon Hunt1894d792015-02-04 17:09:20 -0800454
455 function findAttachedHosts(devId) {
456 var hosts = [];
457 network.nodes.forEach(function (d) {
458 if (d.class === 'host' && d.cp.device === devId) {
459 hosts.push(d);
460 }
461 });
462 return hosts;
463 }
464
465 function findAttachedLinks(devId) {
466 var links = [];
467 network.links.forEach(function (d) {
468 if (d.source.id === devId || d.target.id === devId) {
469 links.push(d);
470 }
471 });
472 return links;
473 }
474
475 function removeLinkElement(d) {
476 var idx = fs.find(d.key, network.links, 'key'),
477 removed;
478 if (idx >=0) {
479 // remove from links array
480 removed = network.links.splice(idx, 1);
481 // remove from lookup cache
482 delete lu[removed[0].key];
483 updateLinks();
484 fResume();
485 }
486 }
487
488 function removeHostElement(d, upd) {
489 // first, remove associated hostLink...
490 removeLinkElement(d.linkData);
491
492 // remove hostLink bindings
493 delete lu[d.ingress];
494 delete lu[d.egress];
495
496 // remove from lookup cache
497 delete lu[d.id];
498 // remove from nodes array
499 var idx = fs.find(d.id, network.nodes);
500 network.nodes.splice(idx, 1);
501
502 // remove from SVG
503 // NOTE: upd is false if we were called from removeDeviceElement()
504 if (upd) {
505 updateNodes();
506 fResume();
507 }
508 }
509
510 function removeDeviceElement(d) {
511 var id = d.id;
512 // first, remove associated hosts and links..
513 findAttachedHosts(id).forEach(removeHostElement);
514 findAttachedLinks(id).forEach(removeLinkElement);
515
516 // remove from lookup cache
517 delete lu[id];
518 // remove from nodes array
519 var idx = fs.find(id, network.nodes);
520 network.nodes.splice(idx, 1);
521
522 if (!network.nodes.length) {
523 xlink.showNoDevs(true);
524 }
525
526 // remove from SVG
527 updateNodes();
528 fResume();
529 }
530
Simon Hunt5724fb42015-02-05 16:59:40 -0800531 function updateHostVisibility() {
532 sus.makeVisible(nodeG.selectAll('.host'), showHosts);
533 sus.makeVisible(linkG.selectAll('.hostLink'), showHosts);
534 }
535
536 function updateOfflineVisibility(dev) {
537 function updDev(d, show) {
538 sus.makeVisible(d.el, show);
539
540 findAttachedLinks(d.id).forEach(function (link) {
541 b = show && ((link.type() !== 'hostLink') || showHosts);
542 sus.makeVisible(link.el, b);
543 });
544 findAttachedHosts(d.id).forEach(function (host) {
545 b = show && showHosts;
546 sus.makeVisible(host.el, b);
547 });
548 }
549
550 if (dev) {
551 // updating a specific device that just toggled off/on-line
552 updDev(dev, dev.online || showOffline);
553 } else {
554 // updating all offline devices
Simon Hunt1c367112015-02-05 18:02:46 -0800555 findDevices(true).forEach(function (d) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800556 updDev(d, showOffline);
557 });
558 }
559 }
560
Simon Hunt1894d792015-02-04 17:09:20 -0800561
Simon Hunt445e8152015-02-06 13:00:12 -0800562 function sendUpdateMeta(d, clearPos) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800563 var metaUi = {},
564 ll;
565
Simon Hunt445e8152015-02-06 13:00:12 -0800566 // if we are not clearing the position data (unpinning),
567 // attach the x, y, longitude, latitude...
568 if (!clearPos) {
Simon Hunt1894d792015-02-04 17:09:20 -0800569 ll = lngLatFromCoord([d.x, d.y]);
570 metaUi = {
571 x: d.x,
572 y: d.y,
573 lng: ll[0],
574 lat: ll[1]
575 };
576 }
577 d.metaUi = metaUi;
578 uplink.sendEvent('updateMeta', {
579 id: d.id,
580 'class': d.class,
581 memento: metaUi
582 });
Simon Huntac4c6f72015-02-03 19:50:53 -0800583 }
584
Simon Hunt445e8152015-02-06 13:00:12 -0800585 function requestTrafficForMode() {
586 $log.debug('TODO: requestTrafficForMode()...');
587 }
Simon Huntac4c6f72015-02-03 19:50:53 -0800588
Simon Huntac4c6f72015-02-03 19:50:53 -0800589 // ==========================
590 // === Devices and hosts - helper functions
591
592 function coordFromLngLat(loc) {
Simon Hunt1894d792015-02-04 17:09:20 -0800593 var p = uplink.projection();
594 return p ? p([loc.lng, loc.lat]) : [0, 0];
595 }
596
597 function lngLatFromCoord(coord) {
598 var p = uplink.projection();
Simon Hunt445e8152015-02-06 13:00:12 -0800599 return p ? p.invert(coord) : [0, 0];
Simon Huntac4c6f72015-02-03 19:50:53 -0800600 }
601
602 function positionNode(node, forUpdate) {
603 var meta = node.metaUi,
604 x = meta && meta.x,
605 y = meta && meta.y,
606 xy;
607
608 // If we have [x,y] already, use that...
609 if (x && y) {
610 node.fixed = true;
611 node.px = node.x = x;
612 node.py = node.y = y;
613 return;
614 }
615
616 var location = node.location,
617 coord;
618
619 if (location && location.type === 'latlng') {
620 coord = coordFromLngLat(location);
621 node.fixed = true;
622 node.px = node.x = coord[0];
623 node.py = node.y = coord[1];
624 return true;
625 }
626
627 // if this is a node update (not a node add).. skip randomizer
628 if (forUpdate) {
629 return;
630 }
631
632 // Note: Placing incoming unpinned nodes at exactly the same point
633 // (center of the view) causes them to explode outwards when
634 // the force layout kicks in. So, we spread them out a bit
635 // initially, to provide a more serene layout convergence.
636 // Additionally, if the node is a host, we place it near
637 // the device it is connected to.
638
639 function spread(s) {
640 return Math.floor((Math.random() * s) - s/2);
641 }
642
643 function randDim(dim) {
644 return dim / 2 + spread(dim * 0.7071);
645 }
646
647 function rand() {
648 return {
Simon Hunt1894d792015-02-04 17:09:20 -0800649 x: randDim(width),
650 y: randDim(height)
Simon Huntac4c6f72015-02-03 19:50:53 -0800651 };
652 }
653
654 function near(node) {
655 var min = 12,
656 dx = spread(12),
657 dy = spread(12);
658 return {
659 x: node.x + min + dx,
660 y: node.y + min + dy
661 };
662 }
663
664 function getDevice(cp) {
Simon Hunt1894d792015-02-04 17:09:20 -0800665 var d = lu[cp.device];
Simon Huntac4c6f72015-02-03 19:50:53 -0800666 return d || rand();
667 }
668
669 xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
670 angular.extend(node, xy);
671 }
672
673 function createDeviceNode(device) {
674 // start with the object as is
675 var node = device,
676 type = device.type,
677 svgCls = type ? 'node device ' + type : 'node device';
678
679 // Augment as needed...
680 node.class = 'device';
681 node.svgClass = device.online ? svgCls + ' online' : svgCls;
682 positionNode(node);
683 return node;
684 }
685
Simon Hunt1894d792015-02-04 17:09:20 -0800686 function createHostNode(host) {
687 var node = host;
688
689 // Augment as needed...
690 node.class = 'host';
691 if (!node.type) {
692 node.type = 'endstation';
693 }
694 node.svgClass = 'node host ' + node.type;
695 positionNode(node);
696 return node;
697 }
698
699 function createHostLink(host) {
700 var src = host.id,
701 dst = host.cp.device,
702 id = host.ingress,
703 lnk = linkEndPoints(src, dst);
704
705 if (!lnk) {
706 return null;
707 }
708
709 // Synthesize link ...
710 angular.extend(lnk, {
711 key: id,
712 class: 'link',
713
714 type: function () { return 'hostLink'; },
715 online: function () {
716 // hostlink target is edge switch
717 return lnk.target.online;
718 },
719 linkWidth: function () { return 1; }
720 });
721 return lnk;
722 }
723
724 function linkEndPoints(srcId, dstId) {
725 var srcNode = lu[srcId],
726 dstNode = lu[dstId],
727 sMiss = !srcNode ? missMsg('src', srcId) : '',
728 dMiss = !dstNode ? missMsg('dst', dstId) : '';
729
730 if (sMiss || dMiss) {
731 $log.error('Node(s) not on map for link:\n' + sMiss + dMiss);
732 //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
733 return null;
734 }
735 return {
736 source: srcNode,
737 target: dstNode,
738 x1: srcNode.x,
739 y1: srcNode.y,
740 x2: dstNode.x,
741 y2: dstNode.y
742 };
743 }
744
745 function missMsg(what, id) {
746 return '\n[' + what + '] "' + id + '" missing ';
747 }
748
Simon Huntac4c6f72015-02-03 19:50:53 -0800749 // ==========================
750 // === Devices and hosts - D3 rendering
751
Simon Hunt1894d792015-02-04 17:09:20 -0800752 function nodeMouseOver(m) {
753 // TODO
Simon Hunt445e8152015-02-06 13:00:12 -0800754 if (!m.dragStarted) {
755 $log.debug("MouseOver()...", m);
756 if (hovered != m) {
757 hovered = m;
758 requestTrafficForMode();
759 }
760 }
Simon Hunt1894d792015-02-04 17:09:20 -0800761 }
762
763 function nodeMouseOut(m) {
764 // TODO
Simon Hunt445e8152015-02-06 13:00:12 -0800765 if (!m.dragStarted) {
766 if (hovered) {
767 hovered = null;
768 requestTrafficForMode();
769 }
770 $log.debug("MouseOut()...", m);
771 }
Simon Hunt1894d792015-02-04 17:09:20 -0800772 }
773
774
Simon Huntac4c6f72015-02-03 19:50:53 -0800775 // Returns the newly computed bounding box of the rectangle
776 function adjustRectToFitText(n) {
777 var text = n.select('text'),
778 box = text.node().getBBox(),
779 lab = labelConfig;
780
781 text.attr('text-anchor', 'middle')
782 .attr('y', '-0.8em')
783 .attr('x', lab.imgPad/2);
784
785 // translate the bbox so that it is centered on [x,y]
786 box.x = -box.width / 2;
787 box.y = -box.height / 2;
788
789 // add padding
790 box.x -= (lab.padLR + lab.imgPad/2);
791 box.width += lab.padLR * 2 + lab.imgPad;
792 box.y -= lab.padTB;
793 box.height += lab.padTB * 2;
794
795 return box;
796 }
797
798 function mkSvgClass(d) {
799 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
800 }
801
802 function hostLabel(d) {
803 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
804 return d.labels[idx];
805 }
806 function deviceLabel(d) {
807 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
808 return d.labels[idx];
809 }
810 function trimLabel(label) {
811 return (label && label.trim()) || '';
812 }
813
814 function emptyBox() {
815 return {
816 x: -2,
817 y: -2,
818 width: 4,
819 height: 4
820 };
821 }
822
823
824 function updateDeviceLabel(d) {
825 var label = trimLabel(deviceLabel(d)),
826 noLabel = !label,
827 node = d.el,
Simon Hunt1894d792015-02-04 17:09:20 -0800828 dim = icfg.device.dim,
Simon Huntac4c6f72015-02-03 19:50:53 -0800829 devCfg = deviceIconConfig,
830 box, dx, dy;
831
832 node.select('text')
833 .text(label)
834 .style('opacity', 0)
835 .transition()
836 .style('opacity', 1);
837
838 if (noLabel) {
839 box = emptyBox();
840 dx = -dim/2;
841 dy = -dim/2;
842 } else {
843 box = adjustRectToFitText(node);
844 dx = box.x + devCfg.xoff;
845 dy = box.y + devCfg.yoff;
846 }
847
848 node.select('rect')
849 .transition()
850 .attr(box);
851
852 node.select('g.deviceIcon')
853 .transition()
854 .attr('transform', sus.translate(dx, dy));
855 }
856
857 function updateHostLabel(d) {
858 var label = trimLabel(hostLabel(d));
859 d.el.select('text').text(label);
860 }
861
Simon Huntac4c6f72015-02-03 19:50:53 -0800862 function updateDeviceColors(d) {
863 if (d) {
864 setDeviceColor(d);
865 } else {
866 node.filter('.device').each(function (d) {
867 setDeviceColor(d);
868 });
869 }
870 }
871
Simon Hunt5724fb42015-02-05 16:59:40 -0800872 function vis(b) {
873 return b ? 'visible' : 'hidden';
874 }
875
876 function toggleHosts() {
877 showHosts = !showHosts;
878 updateHostVisibility();
879 flash.flash('Hosts ' + vis(showHosts));
880 }
881
882 function toggleOffline() {
883 showOffline = !showOffline;
884 updateOfflineVisibility();
885 flash.flash('Offline devices ' + vis(showOffline));
886 }
887
888 function cycleDeviceLabels() {
Simon Hunt1c367112015-02-05 18:02:46 -0800889 deviceLabelIndex = (deviceLabelIndex+1) % 3;
890 findDevices().forEach(function (d) {
891 updateDeviceLabel(d);
892 });
Simon Hunt5724fb42015-02-05 16:59:40 -0800893 }
894
Simon Hunt445e8152015-02-06 13:00:12 -0800895 function unpin() {
896 if (hovered) {
897 sendUpdateMeta(hovered, true);
898 hovered.fixed = false;
899 hovered.el.classed('fixed', false);
900 fResume();
901 }
902 }
903
904
Simon Hunt5724fb42015-02-05 16:59:40 -0800905 // ==========================================
906
Simon Huntac4c6f72015-02-03 19:50:53 -0800907 var dCol = {
908 black: '#000',
909 paleblue: '#acf',
910 offwhite: '#ddd',
Simon Hunt78c10bf2015-02-03 21:18:20 -0800911 darkgrey: '#444',
Simon Huntac4c6f72015-02-03 19:50:53 -0800912 midgrey: '#888',
913 lightgrey: '#bbb',
914 orange: '#f90'
915 };
916
917 // note: these are the device icon colors without affinity
918 var dColTheme = {
919 light: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800920 rfill: dCol.offwhite,
Simon Huntac4c6f72015-02-03 19:50:53 -0800921 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800922 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800923 rect: dCol.paleblue
924 },
925 offline: {
926 glyph: dCol.midgrey,
927 rect: dCol.lightgrey
928 }
929 },
Simon Huntac4c6f72015-02-03 19:50:53 -0800930 dark: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800931 rfill: dCol.midgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800932 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800933 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800934 rect: dCol.paleblue
935 },
936 offline: {
937 glyph: dCol.midgrey,
Simon Hunt78c10bf2015-02-03 21:18:20 -0800938 rect: dCol.darkgrey
Simon Huntac4c6f72015-02-03 19:50:53 -0800939 }
940 }
941 };
942
943 function devBaseColor(d) {
944 var o = d.online ? 'online' : 'offline';
945 return dColTheme[ts.theme()][o];
946 }
947
948 function setDeviceColor(d) {
949 var o = d.online,
950 s = d.el.classed('selected'),
951 c = devBaseColor(d),
952 a = instColor(d.master, o),
Simon Hunt51056592015-02-03 21:48:07 -0800953 icon = d.el.select('g.deviceIcon'),
954 g, r;
Simon Huntac4c6f72015-02-03 19:50:53 -0800955
956 if (s) {
957 g = c.glyph;
958 r = dCol.orange;
959 } else if (tis.isVisible()) {
960 g = o ? a : c.glyph;
Simon Hunt78c10bf2015-02-03 21:18:20 -0800961 r = o ? c.rfill : a;
Simon Huntac4c6f72015-02-03 19:50:53 -0800962 } else {
963 g = c.glyph;
964 r = c.rect;
965 }
966
Simon Hunt51056592015-02-03 21:48:07 -0800967 icon.select('use').style('fill', g);
968 icon.select('rect').style('fill', r);
Simon Huntac4c6f72015-02-03 19:50:53 -0800969 }
970
971 function instColor(id, online) {
972 return sus.cat7().getColor(id, !online, ts.theme());
973 }
974
Simon Hunt1894d792015-02-04 17:09:20 -0800975 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800976
977 function updateNodes() {
Simon Hunt1894d792015-02-04 17:09:20 -0800978 // select all the nodes in the layout:
Simon Huntac4c6f72015-02-03 19:50:53 -0800979 node = nodeG.selectAll('.node')
980 .data(network.nodes, function (d) { return d.id; });
981
Simon Hunt1894d792015-02-04 17:09:20 -0800982 // operate on existing nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800983 node.filter('.device').each(deviceExisting);
984 node.filter('.host').each(hostExisting);
Simon Huntac4c6f72015-02-03 19:50:53 -0800985
986 // operate on entering nodes:
987 var entering = node.enter()
988 .append('g')
989 .attr({
990 id: function (d) { return sus.safeId(d.id); },
991 class: mkSvgClass,
992 transform: function (d) { return sus.translate(d.x, d.y); },
993 opacity: 0
994 })
995 .call(drag)
996 .on('mouseover', nodeMouseOver)
997 .on('mouseout', nodeMouseOut)
998 .transition()
999 .attr('opacity', 1);
1000
Simon Hunt1894d792015-02-04 17:09:20 -08001001 // augment entering nodes:
Simon Hunt51056592015-02-03 21:48:07 -08001002 entering.filter('.device').each(deviceEnter);
1003 entering.filter('.host').each(hostEnter);
Simon Huntac4c6f72015-02-03 19:50:53 -08001004
Simon Hunt51056592015-02-03 21:48:07 -08001005 // operate on both existing and new nodes:
Simon Huntac4c6f72015-02-03 19:50:53 -08001006 updateDeviceColors();
1007
1008 // operate on exiting nodes:
1009 // Note that the node is removed after 2 seconds.
1010 // Sub element animations should be shorter than 2 seconds.
1011 var exiting = node.exit()
1012 .transition()
1013 .duration(2000)
1014 .style('opacity', 0)
1015 .remove();
1016
Simon Hunt1894d792015-02-04 17:09:20 -08001017 // exiting node specifics:
Simon Hunt51056592015-02-03 21:48:07 -08001018 exiting.filter('.host').each(hostExit);
1019 exiting.filter('.device').each(deviceExit);
Simon Huntac4c6f72015-02-03 19:50:53 -08001020
Simon Hunt51056592015-02-03 21:48:07 -08001021 // finally, resume the force layout
Simon Huntac4c6f72015-02-03 19:50:53 -08001022 fResume();
1023 }
1024
Simon Hunt51056592015-02-03 21:48:07 -08001025 // ==========================
1026 // updateNodes - subfunctions
1027
1028 function deviceExisting(d) {
1029 var node = d.el;
1030 node.classed('online', d.online);
1031 updateDeviceLabel(d);
1032 positionNode(d, true);
1033 }
1034
1035 function hostExisting(d) {
1036 updateHostLabel(d);
1037 positionNode(d, true);
1038 }
1039
1040 function deviceEnter(d) {
1041 var node = d3.select(this),
1042 glyphId = d.type || 'unknown',
1043 label = trimLabel(deviceLabel(d)),
1044 devCfg = deviceIconConfig,
1045 noLabel = !label,
1046 box, dx, dy, icon;
1047
1048 d.el = node;
1049
1050 node.append('rect').attr({ rx: 5, ry: 5 });
1051 node.append('text').text(label).attr('dy', '1.1em');
1052 box = adjustRectToFitText(node);
1053 node.select('rect').attr(box);
1054
1055 icon = is.addDeviceIcon(node, glyphId);
1056
1057 if (noLabel) {
1058 dx = -icon.dim/2;
1059 dy = -icon.dim/2;
1060 } else {
1061 box = adjustRectToFitText(node);
1062 dx = box.x + devCfg.xoff;
1063 dy = box.y + devCfg.yoff;
1064 }
1065
1066 icon.attr('transform', sus.translate(dx, dy));
1067 }
1068
1069 function hostEnter(d) {
Simon Hunt1894d792015-02-04 17:09:20 -08001070 var node = d3.select(this),
1071 gid = d.type || 'unknown',
1072 rad = icfg.host.radius,
1073 r = d.type ? rad.withGlyph : rad.noGlyph,
1074 textDy = r + 10;
Simon Hunt51056592015-02-03 21:48:07 -08001075
1076 d.el = node;
Simon Hunt1894d792015-02-04 17:09:20 -08001077 sus.makeVisible(node, showHosts);
Simon Hunt51056592015-02-03 21:48:07 -08001078
Simon Hunt1894d792015-02-04 17:09:20 -08001079 is.addHostIcon(node, r, gid);
Simon Hunt51056592015-02-03 21:48:07 -08001080
Simon Hunt51056592015-02-03 21:48:07 -08001081 node.append('text')
1082 .text(hostLabel)
Simon Hunt1894d792015-02-04 17:09:20 -08001083 .attr('dy', textDy)
Simon Hunt51056592015-02-03 21:48:07 -08001084 .attr('text-anchor', 'middle');
1085 }
1086
1087 function hostExit(d) {
1088 var node = d.el;
1089 node.select('use')
1090 .style('opacity', 0.5)
1091 .transition()
1092 .duration(800)
1093 .style('opacity', 0);
1094
1095 node.select('text')
1096 .style('opacity', 0.5)
1097 .transition()
1098 .duration(800)
1099 .style('opacity', 0);
1100
1101 node.select('circle')
1102 .style('stroke-fill', '#555')
1103 .style('fill', '#888')
1104 .style('opacity', 0.5)
1105 .transition()
1106 .duration(1500)
1107 .attr('r', 0);
1108 }
1109
1110 function deviceExit(d) {
1111 var node = d.el;
1112 node.select('use')
1113 .style('opacity', 0.5)
1114 .transition()
1115 .duration(800)
1116 .style('opacity', 0);
1117
1118 node.selectAll('rect')
1119 .style('stroke-fill', '#555')
1120 .style('fill', '#888')
1121 .style('opacity', 0.5);
1122 }
1123
Simon Hunt1894d792015-02-04 17:09:20 -08001124 // ==========================
1125
1126 function updateLinks() {
1127 var th = ts.theme();
1128
1129 link = linkG.selectAll('.link')
1130 .data(network.links, function (d) { return d.key; });
1131
1132 // operate on existing links:
1133 //link.each(linkExisting);
1134
1135 // operate on entering links:
1136 var entering = link.enter()
1137 .append('line')
1138 .attr({
1139 x1: function (d) { return d.x1; },
1140 y1: function (d) { return d.y1; },
1141 x2: function (d) { return d.x2; },
1142 y2: function (d) { return d.y2; },
1143 stroke: linkConfig[th].inColor,
1144 'stroke-width': linkConfig.inWidth
1145 });
1146
1147 // augment links
1148 entering.each(linkEntering);
1149
1150 // operate on both existing and new links:
1151 //link.each(...)
1152
1153 // apply or remove labels
1154 var labelData = getLabelData();
1155 applyLinkLabels(labelData);
1156
1157 // operate on exiting links:
1158 link.exit()
1159 .attr('stroke-dasharray', '3 3')
Simon Hunt5724fb42015-02-05 16:59:40 -08001160 .attr('stroke', linkConfig[th].outColor)
Simon Hunt1894d792015-02-04 17:09:20 -08001161 .style('opacity', 0.5)
1162 .transition()
1163 .duration(1500)
1164 .attr({
1165 'stroke-dasharray': '3 12',
Simon Hunt1894d792015-02-04 17:09:20 -08001166 'stroke-width': linkConfig.outWidth
1167 })
1168 .style('opacity', 0.0)
1169 .remove();
1170
1171 // NOTE: invoke a single tick to force the labels to position
1172 // onto their links.
1173 tick();
Simon Hunt5724fb42015-02-05 16:59:40 -08001174 // TODO: this causes undesirable behavior when in oblique view
Simon Hunt1894d792015-02-04 17:09:20 -08001175 // It causes the nodes to jump into "overhead" view positions, even
1176 // though the oblique planes are still showing...
1177 }
1178
1179 // ==========================
1180 // updateLinks - subfunctions
1181
1182 function getLabelData() {
1183 // create the backing data for showing labels..
1184 var data = [];
1185 link.each(function (d) {
1186 if (d.label) {
1187 data.push({
1188 id: 'lab-' + d.key,
1189 key: d.key,
1190 label: d.label,
1191 ldata: d
1192 });
1193 }
1194 });
1195 return data;
1196 }
1197
1198 //function linkExisting(d) { }
1199
1200 function linkEntering(d) {
1201 var link = d3.select(this);
1202 d.el = link;
1203 restyleLinkElement(d);
1204 if (d.type() === 'hostLink') {
1205 sus.makeVisible(link, showHosts);
1206 }
1207 }
1208
1209 //function linkExiting(d) { }
1210
1211 var linkLabelOffset = '0.3em';
1212
1213 function applyLinkLabels(data) {
1214 var entering;
1215
1216 linkLabel = linkLabelG.selectAll('.linkLabel')
1217 .data(data, function (d) { return d.id; });
1218
1219 // for elements already existing, we need to update the text
1220 // and adjust the rectangle size to fit
1221 linkLabel.each(function (d) {
1222 var el = d3.select(this),
1223 rect = el.select('rect'),
1224 text = el.select('text');
1225 text.text(d.label);
1226 rect.attr(rectAroundText(el));
1227 });
1228
1229 entering = linkLabel.enter().append('g')
1230 .classed('linkLabel', true)
1231 .attr('id', function (d) { return d.id; });
1232
1233 entering.each(function (d) {
1234 var el = d3.select(this),
1235 rect,
1236 text,
1237 parms = {
1238 x1: d.ldata.x1,
1239 y1: d.ldata.y1,
1240 x2: d.ldata.x2,
1241 y2: d.ldata.y2
1242 };
1243
1244 d.el = el;
1245 rect = el.append('rect');
1246 text = el.append('text').text(d.label);
1247 rect.attr(rectAroundText(el));
1248 text.attr('dy', linkLabelOffset);
1249
1250 el.attr('transform', transformLabel(parms));
1251 });
1252
1253 // Remove any labels that are no longer required.
1254 linkLabel.exit().remove();
1255 }
1256
1257 function rectAroundText(el) {
1258 var text = el.select('text'),
1259 box = text.node().getBBox();
1260
1261 // translate the bbox so that it is centered on [x,y]
1262 box.x = -box.width / 2;
1263 box.y = -box.height / 2;
1264
1265 // add padding
1266 box.x -= 1;
1267 box.width += 2;
1268 return box;
1269 }
1270
1271 function transformLabel(p) {
1272 var dx = p.x2 - p.x1,
1273 dy = p.y2 - p.y1,
1274 xMid = dx/2 + p.x1,
1275 yMid = dy/2 + p.y1;
1276 return sus.translate(xMid, yMid);
1277 }
Simon Huntac4c6f72015-02-03 19:50:53 -08001278
1279 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -08001280 // force layout tick function
Simon Hunt737c89f2015-01-28 12:23:19 -08001281
Simon Hunt5724fb42015-02-05 16:59:40 -08001282 function fResume() {
1283 if (!oblique) {
1284 force.resume();
1285 }
1286 }
1287
1288 function fStart() {
1289 if (!oblique) {
1290 force.start();
1291 }
1292 }
1293
1294 var tickStuff = {
1295 nodeAttr: {
1296 transform: function (d) { return sus.translate(d.x, d.y); }
1297 },
1298 linkAttr: {
1299 x1: function (d) { return d.source.x; },
1300 y1: function (d) { return d.source.y; },
1301 x2: function (d) { return d.target.x; },
1302 y2: function (d) { return d.target.y; }
1303 },
1304 linkLabelAttr: {
1305 transform: function (d) {
1306 var lnk = findLinkById(d.key);
1307 if (lnk) {
1308 return transformLabel({
1309 x1: lnk.source.x,
1310 y1: lnk.source.y,
1311 x2: lnk.target.x,
1312 y2: lnk.target.y
1313 });
1314 }
1315 }
1316 }
1317 };
1318
1319 function tick() {
1320 node.attr(tickStuff.nodeAttr);
1321 link.attr(tickStuff.linkAttr);
1322 linkLabel.attr(tickStuff.linkLabelAttr);
Simon Hunt737c89f2015-01-28 12:23:19 -08001323 }
1324
1325
Simon Huntac4c6f72015-02-03 19:50:53 -08001326 // ==========================
1327 // === MOUSE GESTURE HANDLERS
1328
Simon Hunt445e8152015-02-06 13:00:12 -08001329 function selectCb(d) {
1330 // this is the selected node
1331 $log.debug("\n\n\nSelect Object: ");
1332 $log.debug("d is ", d);
1333 $log.debug("this is ", this);
1334 $log.debug('\n\n');
1335 }
1336
1337 function atDragEnd(d) {
1338 // once we've finished moving, pin the node in position
1339 d.fixed = true;
1340 d3.select(this).classed('fixed', true);
1341 sendUpdateMeta(d);
1342 }
1343
1344 // predicate that indicates when dragging is active
1345 function dragEnabled() {
1346 var ev = d3.event.sourceEvent;
1347 // nodeLock means we aren't allowing nodes to be dragged...
1348 // meta or alt key pressed means we are zooming/panning...
1349 return !nodeLock && !(ev.metaKey || ev.altKey);
1350 }
1351
1352 // predicate that indicates when clicking is active
1353 function clickEnabled() {
1354 return true;
1355 }
Simon Hunt737c89f2015-01-28 12:23:19 -08001356
1357
1358 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -08001359 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -08001360
1361 angular.module('ovTopo')
1362 .factory('TopoForceService',
Simon Hunt1894d792015-02-04 17:09:20 -08001363 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
Simon Hunt5724fb42015-02-05 16:59:40 -08001364 'FlashService', 'TopoInstService',
Simon Hunt737c89f2015-01-28 12:23:19 -08001365
Simon Hunt5724fb42015-02-05 16:59:40 -08001366 function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_) {
Simon Hunt737c89f2015-01-28 12:23:19 -08001367 $log = _$log_;
Simon Hunt1894d792015-02-04 17:09:20 -08001368 fs = _fs_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001369 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001370 is = _is_;
1371 ts = _ts_;
Simon Hunt5724fb42015-02-05 16:59:40 -08001372 flash = _flash_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001373 tis = _tis_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001374
Simon Hunt1894d792015-02-04 17:09:20 -08001375 icfg = is.iconConfig();
1376
Simon Hunt737c89f2015-01-28 12:23:19 -08001377 // forceG is the SVG group to display the force layout in
Simon Huntac4c6f72015-02-03 19:50:53 -08001378 // xlink is the cross-link api from the main topo source file
Simon Hunt737c89f2015-01-28 12:23:19 -08001379 // w, h are the initial dimensions of the SVG
1380 // opts are, well, optional :)
Simon Hunt1894d792015-02-04 17:09:20 -08001381 function initForce(forceG, _uplink_, w, h, opts) {
Simon Hunta11b4eb2015-01-28 16:20:50 -08001382 $log.debug('initForce().. WxH = ' + w + 'x' + h);
Simon Hunt1894d792015-02-04 17:09:20 -08001383 uplink = _uplink_;
1384 width = w;
1385 height = h;
Simon Hunta11b4eb2015-01-28 16:20:50 -08001386
Simon Hunt737c89f2015-01-28 12:23:19 -08001387 settings = angular.extend({}, defaultSettings, opts);
1388
1389 linkG = forceG.append('g').attr('id', 'topo-links');
1390 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
1391 nodeG = forceG.append('g').attr('id', 'topo-nodes');
1392
1393 link = linkG.selectAll('.link');
1394 linkLabel = linkLabelG.selectAll('.linkLabel');
1395 node = nodeG.selectAll('.node');
1396
1397 force = d3.layout.force()
Simon Hunta11b4eb2015-01-28 16:20:50 -08001398 .size([w, h])
Simon Hunt737c89f2015-01-28 12:23:19 -08001399 .nodes(network.nodes)
1400 .links(network.links)
1401 .gravity(settings.gravity)
1402 .friction(settings.friction)
1403 .charge(settings.charge._def_)
1404 .linkDistance(settings.linkDistance._def_)
1405 .linkStrength(settings.linkStrength._def_)
1406 .on('tick', tick);
1407
1408 drag = sus.createDragBehavior(force,
1409 selectCb, atDragEnd, dragEnabled, clickEnabled);
1410 }
1411
Simon Huntb0ec1e52015-01-28 18:13:49 -08001412 function resize(dim) {
Simon Hunt1894d792015-02-04 17:09:20 -08001413 width = dim.width;
1414 height = dim.height;
1415 force.size([width, height]);
Simon Hunt737c89f2015-01-28 12:23:19 -08001416 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -08001417 }
1418
1419 return {
1420 initForce: initForce,
Simon Huntac4c6f72015-02-03 19:50:53 -08001421 resize: resize,
1422
1423 updateDeviceColors: updateDeviceColors,
Simon Hunt5724fb42015-02-05 16:59:40 -08001424 toggleHosts: toggleHosts,
1425 toggleOffline: toggleOffline,
1426 cycleDeviceLabels: cycleDeviceLabels,
Simon Hunt445e8152015-02-06 13:00:12 -08001427 unpin: unpin,
Simon Huntac4c6f72015-02-03 19:50:53 -08001428
1429 addDevice: addDevice,
Simon Hunt1894d792015-02-04 17:09:20 -08001430 updateDevice: updateDevice,
1431 removeDevice: removeDevice,
1432 addHost: addHost,
1433 updateHost: updateHost,
1434 removeHost: removeHost,
1435 addLink: addLink,
1436 updateLink: updateLink,
1437 removeLink: removeLink
Simon Hunt737c89f2015-01-28 12:23:19 -08001438 };
1439 }]);
1440}());