blob: b3c8eaa5ea8a5a1efe813cf4174b397eafd03c0f [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 Hunt1894d792015-02-04 17:09:20 -080078 width, height;
Simon Hunt737c89f2015-01-28 12:23:19 -080079
80 // SVG elements;
81 var linkG, linkLabelG, nodeG;
82
83 // D3 selections;
84 var link, linkLabel, node;
85
86 // default settings for force layout
87 var defaultSettings = {
88 gravity: 0.4,
89 friction: 0.7,
90 charge: {
91 // note: key is node.class
92 device: -8000,
93 host: -5000,
94 _def_: -12000
95 },
96 linkDistance: {
97 // note: key is link.type
98 direct: 100,
99 optical: 120,
100 hostLink: 3,
101 _def_: 50
102 },
103 linkStrength: {
104 // note: key is link.type
105 // range: {0.0 ... 1.0}
106 //direct: 1.0,
107 //optical: 1.0,
108 //hostLink: 1.0,
109 _def_: 1.0
110 }
111 };
112
113
Simon Huntac4c6f72015-02-03 19:50:53 -0800114 // ==========================
115 // === EVENT HANDLERS
116
117 function addDevice(data) {
118 var id = data.id,
119 d;
120
Simon Hunt1894d792015-02-04 17:09:20 -0800121 uplink.showNoDevs(false);
Simon Huntac4c6f72015-02-03 19:50:53 -0800122
123 // although this is an add device event, if we already have the
124 // device, treat it as an update instead..
Simon Hunt1894d792015-02-04 17:09:20 -0800125 if (lu[id]) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800126 updateDevice(data);
127 return;
128 }
129
130 d = createDeviceNode(data);
131 network.nodes.push(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800132 lu[id] = d;
Simon Huntac4c6f72015-02-03 19:50:53 -0800133
134 $log.debug("Created new device.. ", d.id, d.x, d.y);
135
136 updateNodes();
137 fStart();
138 }
139
140 function updateDevice(data) {
141 var id = data.id,
Simon Hunt1894d792015-02-04 17:09:20 -0800142 d = lu[id],
Simon Huntac4c6f72015-02-03 19:50:53 -0800143 wasOnline;
144
145 if (d) {
146 wasOnline = d.online;
147 angular.extend(d, data);
148 if (positionNode(d, true)) {
149 sendUpdateMeta(d, true);
150 }
151 updateNodes();
152 if (wasOnline !== d.online) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800153 findAttachedLinks(d.id).forEach(restyleLinkElement);
154 updateOfflineVisibility(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800155 }
156 } else {
157 // TODO: decide whether we want to capture logic errors
158 //logicError('updateDevice lookup fail. ID = "' + id + '"');
159 }
160 }
161
Simon Hunt1894d792015-02-04 17:09:20 -0800162 function removeDevice(data) {
163 var id = data.id,
164 d = lu[id];
165 if (d) {
166 removeDeviceElement(d);
167 } else {
168 // TODO: decide whether we want to capture logic errors
169 //logicError('removeDevice lookup fail. ID = "' + id + '"');
170 }
171 }
172
173 function addHost(data) {
174 var id = data.id,
175 d, lnk;
176
177 // although this is an add host event, if we already have the
178 // host, treat it as an update instead..
179 if (lu[id]) {
180 updateHost(data);
181 return;
182 }
183
184 d = createHostNode(data);
185 network.nodes.push(d);
186 lu[id] = d;
187
188 $log.debug("Created new host.. ", d.id, d.x, d.y);
189
190 updateNodes();
191
192 lnk = createHostLink(data);
193 if (lnk) {
194
195 $log.debug("Created new host-link.. ", lnk.key);
196
197 d.linkData = lnk; // cache ref on its host
198 network.links.push(lnk);
199 lu[d.ingress] = lnk;
200 lu[d.egress] = lnk;
201 updateLinks();
202 }
203
204 fStart();
205 }
206
207 function updateHost(data) {
208 var id = data.id,
209 d = lu[id];
210 if (d) {
211 angular.extend(d, data);
212 if (positionNode(d, true)) {
213 sendUpdateMeta(d, true);
214 }
215 updateNodes();
216 } else {
217 // TODO: decide whether we want to capture logic errors
218 //logicError('updateHost lookup fail. ID = "' + id + '"');
219 }
220 }
221
222 function removeHost(data) {
223 var id = data.id,
224 d = lu[id];
225 if (d) {
226 removeHostElement(d, true);
227 } else {
228 // may have already removed host, if attached to removed device
229 //console.warn('removeHost lookup fail. ID = "' + id + '"');
230 }
231 }
232
233 function addLink(data) {
234 var result = findLink(data, 'add'),
235 bad = result.badLogic,
236 d = result.ldata;
237
238 if (bad) {
239 //logicError(bad + ': ' + link.id);
240 return;
241 }
242
243 if (d) {
244 // we already have a backing store link for src/dst nodes
245 addLinkUpdate(d, data);
246 return;
247 }
248
249 // no backing store link yet
250 d = createLink(data);
251 if (d) {
252 network.links.push(d);
253 lu[d.key] = d;
254 updateLinks();
255 fStart();
256 }
257 }
258
259 function updateLink(data) {
260 var result = findLink(data, 'update'),
261 bad = result.badLogic;
262 if (bad) {
263 //logicError(bad + ': ' + link.id);
264 return;
265 }
266 result.updateWith(link);
267 }
268
269 function removeLink(data) {
270 var result = findLink(data, 'remove'),
271 bad = result.badLogic;
272 if (bad) {
273 // may have already removed link, if attached to removed device
274 //console.warn(bad + ': ' + link.id);
275 return;
276 }
277 result.removeRawLink();
278 }
279
280 // ========================
281
282 function addLinkUpdate(ldata, link) {
283 // add link event, but we already have the reverse link installed
284 ldata.fromTarget = link;
285 network.revLinkToKey[link.id] = ldata.key;
286 restyleLinkElement(ldata);
287 }
288
289 function createLink(link) {
290 var lnk = linkEndPoints(link.src, link.dst);
291
292 if (!lnk) {
293 return null;
294 }
295
296 angular.extend(lnk, {
297 key: link.id,
298 class: 'link',
299 fromSource: link,
300
301 // functions to aggregate dual link state
302 type: function () {
303 var s = lnk.fromSource,
304 t = lnk.fromTarget;
305 return (s && s.type) || (t && t.type) || defaultLinkType;
306 },
307 online: function () {
308 var s = lnk.fromSource,
309 t = lnk.fromTarget,
310 both = lnk.source.online && lnk.target.online;
311 return both && ((s && s.online) || (t && t.online));
312 },
313 linkWidth: function () {
314 var s = lnk.fromSource,
315 t = lnk.fromTarget,
316 ws = (s && s.linkWidth) || 0,
317 wt = (t && t.linkWidth) || 0;
318 return Math.max(ws, wt);
319 }
320 });
321 return lnk;
322 }
323
324
325 function makeNodeKey(d, what) {
326 var port = what + 'Port';
327 return d[what] + '/' + d[port];
328 }
329
330 function makeLinkKey(d, flipped) {
331 var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
332 two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
333 return one + '-' + two;
334 }
335
336 var widthRatio = 1.4,
337 linkScale = d3.scale.linear()
338 .domain([1, 12])
339 .range([widthRatio, 12 * widthRatio])
Simon Hunt5724fb42015-02-05 16:59:40 -0800340 .clamp(true),
341 allLinkTypes = 'direct indirect optical tunnel',
Simon Hunt1894d792015-02-04 17:09:20 -0800342 defaultLinkType = 'direct';
343
344 function restyleLinkElement(ldata) {
345 // this fn's job is to look at raw links and decide what svg classes
346 // need to be applied to the line element in the DOM
347 var th = ts.theme(),
348 el = ldata.el,
349 type = ldata.type(),
350 lw = ldata.linkWidth(),
351 online = ldata.online();
352
353 el.classed('link', true);
354 el.classed('inactive', !online);
355 el.classed(allLinkTypes, false);
356 if (type) {
357 el.classed(type, true);
358 }
359 el.transition()
360 .duration(1000)
361 .attr('stroke-width', linkScale(lw))
362 .attr('stroke', linkConfig[th].baseColor);
363 }
364
Simon Hunt5724fb42015-02-05 16:59:40 -0800365 function findLinkById(id) {
366 // check to see if this is a reverse lookup, else default to given id
367 var key = network.revLinkToKey[id] || id;
368 return key && lu[key];
369 }
370
Simon Hunt1894d792015-02-04 17:09:20 -0800371 function findLink(linkData, op) {
372 var key = makeLinkKey(linkData),
373 keyrev = makeLinkKey(linkData, 1),
374 link = lu[key],
375 linkRev = lu[keyrev],
376 result = {},
377 ldata = link || linkRev,
378 rawLink;
379
380 if (op === 'add') {
381 if (link) {
382 // trying to add a link that we already know about
383 result.ldata = link;
384 result.badLogic = 'addLink: link already added';
385
386 } else if (linkRev) {
387 // we found the reverse of the link to be added
388 result.ldata = linkRev;
389 if (linkRev.fromTarget) {
390 result.badLogic = 'addLink: link already added';
391 }
392 }
393 } else if (op === 'update') {
394 if (!ldata) {
395 result.badLogic = 'updateLink: link not found';
396 } else {
397 rawLink = link ? ldata.fromSource : ldata.fromTarget;
398 result.updateWith = function (data) {
399 angular.extend(rawLink, data);
400 restyleLinkElement(ldata);
401 }
402 }
403 } else if (op === 'remove') {
404 if (!ldata) {
405 result.badLogic = 'removeLink: link not found';
406 } else {
407 rawLink = link ? ldata.fromSource : ldata.fromTarget;
408
409 if (!rawLink) {
410 result.badLogic = 'removeLink: link not found';
411
412 } else {
413 result.removeRawLink = function () {
414 if (link) {
415 // remove fromSource
416 ldata.fromSource = null;
417 if (ldata.fromTarget) {
418 // promote target into source position
419 ldata.fromSource = ldata.fromTarget;
420 ldata.fromTarget = null;
421 ldata.key = keyrev;
422 delete network.lookup[key];
423 network.lookup[keyrev] = ldata;
424 delete network.revLinkToKey[keyrev];
425 }
426 } else {
427 // remove fromTarget
428 ldata.fromTarget = null;
429 delete network.revLinkToKey[keyrev];
430 }
431 if (ldata.fromSource) {
432 restyleLinkElement(ldata);
433 } else {
434 removeLinkElement(ldata);
435 }
436 }
437 }
438 }
439 }
440 return result;
441 }
442
Simon Hunt5724fb42015-02-05 16:59:40 -0800443 function findOfflineNodes() {
444 var a = [];
445 network.nodes.forEach(function (d) {
446 if (d.class === 'device' && !d.online) {
447 a.push(d);
448 }
449 });
450 return a;
451 }
Simon Hunt1894d792015-02-04 17:09:20 -0800452
453 function findAttachedHosts(devId) {
454 var hosts = [];
455 network.nodes.forEach(function (d) {
456 if (d.class === 'host' && d.cp.device === devId) {
457 hosts.push(d);
458 }
459 });
460 return hosts;
461 }
462
463 function findAttachedLinks(devId) {
464 var links = [];
465 network.links.forEach(function (d) {
466 if (d.source.id === devId || d.target.id === devId) {
467 links.push(d);
468 }
469 });
470 return links;
471 }
472
473 function removeLinkElement(d) {
474 var idx = fs.find(d.key, network.links, 'key'),
475 removed;
476 if (idx >=0) {
477 // remove from links array
478 removed = network.links.splice(idx, 1);
479 // remove from lookup cache
480 delete lu[removed[0].key];
481 updateLinks();
482 fResume();
483 }
484 }
485
486 function removeHostElement(d, upd) {
487 // first, remove associated hostLink...
488 removeLinkElement(d.linkData);
489
490 // remove hostLink bindings
491 delete lu[d.ingress];
492 delete lu[d.egress];
493
494 // remove from lookup cache
495 delete lu[d.id];
496 // remove from nodes array
497 var idx = fs.find(d.id, network.nodes);
498 network.nodes.splice(idx, 1);
499
500 // remove from SVG
501 // NOTE: upd is false if we were called from removeDeviceElement()
502 if (upd) {
503 updateNodes();
504 fResume();
505 }
506 }
507
508 function removeDeviceElement(d) {
509 var id = d.id;
510 // first, remove associated hosts and links..
511 findAttachedHosts(id).forEach(removeHostElement);
512 findAttachedLinks(id).forEach(removeLinkElement);
513
514 // remove from lookup cache
515 delete lu[id];
516 // remove from nodes array
517 var idx = fs.find(id, network.nodes);
518 network.nodes.splice(idx, 1);
519
520 if (!network.nodes.length) {
521 xlink.showNoDevs(true);
522 }
523
524 // remove from SVG
525 updateNodes();
526 fResume();
527 }
528
Simon Hunt5724fb42015-02-05 16:59:40 -0800529 function updateHostVisibility() {
530 sus.makeVisible(nodeG.selectAll('.host'), showHosts);
531 sus.makeVisible(linkG.selectAll('.hostLink'), showHosts);
532 }
533
534 function updateOfflineVisibility(dev) {
535 function updDev(d, show) {
536 sus.makeVisible(d.el, show);
537
538 findAttachedLinks(d.id).forEach(function (link) {
539 b = show && ((link.type() !== 'hostLink') || showHosts);
540 sus.makeVisible(link.el, b);
541 });
542 findAttachedHosts(d.id).forEach(function (host) {
543 b = show && showHosts;
544 sus.makeVisible(host.el, b);
545 });
546 }
547
548 if (dev) {
549 // updating a specific device that just toggled off/on-line
550 updDev(dev, dev.online || showOffline);
551 } else {
552 // updating all offline devices
553 findOfflineNodes().forEach(function (d) {
554 updDev(d, showOffline);
555 });
556 }
557 }
558
Simon Hunt1894d792015-02-04 17:09:20 -0800559
Simon Huntac4c6f72015-02-03 19:50:53 -0800560 function sendUpdateMeta(d, store) {
561 var metaUi = {},
562 ll;
563
Simon Hunt1894d792015-02-04 17:09:20 -0800564 if (store) {
565 ll = lngLatFromCoord([d.x, d.y]);
566 metaUi = {
567 x: d.x,
568 y: d.y,
569 lng: ll[0],
570 lat: ll[1]
571 };
572 }
573 d.metaUi = metaUi;
574 uplink.sendEvent('updateMeta', {
575 id: d.id,
576 'class': d.class,
577 memento: metaUi
578 });
Simon Huntac4c6f72015-02-03 19:50:53 -0800579 }
580
581
Simon Huntac4c6f72015-02-03 19:50:53 -0800582 // ==========================
583 // === Devices and hosts - helper functions
584
585 function coordFromLngLat(loc) {
Simon Hunt1894d792015-02-04 17:09:20 -0800586 var p = uplink.projection();
587 return p ? p([loc.lng, loc.lat]) : [0, 0];
588 }
589
590 function lngLatFromCoord(coord) {
591 var p = uplink.projection();
592 return p ? p.invert([coord.x, coord.y]) : [0, 0];
Simon Huntac4c6f72015-02-03 19:50:53 -0800593 }
594
595 function positionNode(node, forUpdate) {
596 var meta = node.metaUi,
597 x = meta && meta.x,
598 y = meta && meta.y,
599 xy;
600
601 // If we have [x,y] already, use that...
602 if (x && y) {
603 node.fixed = true;
604 node.px = node.x = x;
605 node.py = node.y = y;
606 return;
607 }
608
609 var location = node.location,
610 coord;
611
612 if (location && location.type === 'latlng') {
613 coord = coordFromLngLat(location);
614 node.fixed = true;
615 node.px = node.x = coord[0];
616 node.py = node.y = coord[1];
617 return true;
618 }
619
620 // if this is a node update (not a node add).. skip randomizer
621 if (forUpdate) {
622 return;
623 }
624
625 // Note: Placing incoming unpinned nodes at exactly the same point
626 // (center of the view) causes them to explode outwards when
627 // the force layout kicks in. So, we spread them out a bit
628 // initially, to provide a more serene layout convergence.
629 // Additionally, if the node is a host, we place it near
630 // the device it is connected to.
631
632 function spread(s) {
633 return Math.floor((Math.random() * s) - s/2);
634 }
635
636 function randDim(dim) {
637 return dim / 2 + spread(dim * 0.7071);
638 }
639
640 function rand() {
641 return {
Simon Hunt1894d792015-02-04 17:09:20 -0800642 x: randDim(width),
643 y: randDim(height)
Simon Huntac4c6f72015-02-03 19:50:53 -0800644 };
645 }
646
647 function near(node) {
648 var min = 12,
649 dx = spread(12),
650 dy = spread(12);
651 return {
652 x: node.x + min + dx,
653 y: node.y + min + dy
654 };
655 }
656
657 function getDevice(cp) {
Simon Hunt1894d792015-02-04 17:09:20 -0800658 var d = lu[cp.device];
Simon Huntac4c6f72015-02-03 19:50:53 -0800659 return d || rand();
660 }
661
662 xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
663 angular.extend(node, xy);
664 }
665
666 function createDeviceNode(device) {
667 // start with the object as is
668 var node = device,
669 type = device.type,
670 svgCls = type ? 'node device ' + type : 'node device';
671
672 // Augment as needed...
673 node.class = 'device';
674 node.svgClass = device.online ? svgCls + ' online' : svgCls;
675 positionNode(node);
676 return node;
677 }
678
Simon Hunt1894d792015-02-04 17:09:20 -0800679 function createHostNode(host) {
680 var node = host;
681
682 // Augment as needed...
683 node.class = 'host';
684 if (!node.type) {
685 node.type = 'endstation';
686 }
687 node.svgClass = 'node host ' + node.type;
688 positionNode(node);
689 return node;
690 }
691
692 function createHostLink(host) {
693 var src = host.id,
694 dst = host.cp.device,
695 id = host.ingress,
696 lnk = linkEndPoints(src, dst);
697
698 if (!lnk) {
699 return null;
700 }
701
702 // Synthesize link ...
703 angular.extend(lnk, {
704 key: id,
705 class: 'link',
706
707 type: function () { return 'hostLink'; },
708 online: function () {
709 // hostlink target is edge switch
710 return lnk.target.online;
711 },
712 linkWidth: function () { return 1; }
713 });
714 return lnk;
715 }
716
717 function linkEndPoints(srcId, dstId) {
718 var srcNode = lu[srcId],
719 dstNode = lu[dstId],
720 sMiss = !srcNode ? missMsg('src', srcId) : '',
721 dMiss = !dstNode ? missMsg('dst', dstId) : '';
722
723 if (sMiss || dMiss) {
724 $log.error('Node(s) not on map for link:\n' + sMiss + dMiss);
725 //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
726 return null;
727 }
728 return {
729 source: srcNode,
730 target: dstNode,
731 x1: srcNode.x,
732 y1: srcNode.y,
733 x2: dstNode.x,
734 y2: dstNode.y
735 };
736 }
737
738 function missMsg(what, id) {
739 return '\n[' + what + '] "' + id + '" missing ';
740 }
741
Simon Huntac4c6f72015-02-03 19:50:53 -0800742 // ==========================
743 // === Devices and hosts - D3 rendering
744
Simon Hunt1894d792015-02-04 17:09:20 -0800745 function nodeMouseOver(m) {
746 // TODO
747 $log.debug("TODO nodeMouseOver()...", m);
748 }
749
750 function nodeMouseOut(m) {
751 // TODO
752 $log.debug("TODO nodeMouseOut()...", m);
753 }
754
755
Simon Huntac4c6f72015-02-03 19:50:53 -0800756 // Returns the newly computed bounding box of the rectangle
757 function adjustRectToFitText(n) {
758 var text = n.select('text'),
759 box = text.node().getBBox(),
760 lab = labelConfig;
761
762 text.attr('text-anchor', 'middle')
763 .attr('y', '-0.8em')
764 .attr('x', lab.imgPad/2);
765
766 // translate the bbox so that it is centered on [x,y]
767 box.x = -box.width / 2;
768 box.y = -box.height / 2;
769
770 // add padding
771 box.x -= (lab.padLR + lab.imgPad/2);
772 box.width += lab.padLR * 2 + lab.imgPad;
773 box.y -= lab.padTB;
774 box.height += lab.padTB * 2;
775
776 return box;
777 }
778
779 function mkSvgClass(d) {
780 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
781 }
782
783 function hostLabel(d) {
784 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
785 return d.labels[idx];
786 }
787 function deviceLabel(d) {
788 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
789 return d.labels[idx];
790 }
791 function trimLabel(label) {
792 return (label && label.trim()) || '';
793 }
794
795 function emptyBox() {
796 return {
797 x: -2,
798 y: -2,
799 width: 4,
800 height: 4
801 };
802 }
803
804
805 function updateDeviceLabel(d) {
806 var label = trimLabel(deviceLabel(d)),
807 noLabel = !label,
808 node = d.el,
Simon Hunt1894d792015-02-04 17:09:20 -0800809 dim = icfg.device.dim,
Simon Huntac4c6f72015-02-03 19:50:53 -0800810 devCfg = deviceIconConfig,
811 box, dx, dy;
812
813 node.select('text')
814 .text(label)
815 .style('opacity', 0)
816 .transition()
817 .style('opacity', 1);
818
819 if (noLabel) {
820 box = emptyBox();
821 dx = -dim/2;
822 dy = -dim/2;
823 } else {
824 box = adjustRectToFitText(node);
825 dx = box.x + devCfg.xoff;
826 dy = box.y + devCfg.yoff;
827 }
828
829 node.select('rect')
830 .transition()
831 .attr(box);
832
833 node.select('g.deviceIcon')
834 .transition()
835 .attr('transform', sus.translate(dx, dy));
836 }
837
838 function updateHostLabel(d) {
839 var label = trimLabel(hostLabel(d));
840 d.el.select('text').text(label);
841 }
842
Simon Huntac4c6f72015-02-03 19:50:53 -0800843 function updateDeviceColors(d) {
844 if (d) {
845 setDeviceColor(d);
846 } else {
847 node.filter('.device').each(function (d) {
848 setDeviceColor(d);
849 });
850 }
851 }
852
Simon Hunt5724fb42015-02-05 16:59:40 -0800853 function vis(b) {
854 return b ? 'visible' : 'hidden';
855 }
856
857 function toggleHosts() {
858 showHosts = !showHosts;
859 updateHostVisibility();
860 flash.flash('Hosts ' + vis(showHosts));
861 }
862
863 function toggleOffline() {
864 showOffline = !showOffline;
865 updateOfflineVisibility();
866 flash.flash('Offline devices ' + vis(showOffline));
867 }
868
869 function cycleDeviceLabels() {
870 // TODO cycle device labels
871 }
872
873 // ==========================================
874
Simon Huntac4c6f72015-02-03 19:50:53 -0800875 var dCol = {
876 black: '#000',
877 paleblue: '#acf',
878 offwhite: '#ddd',
Simon Hunt78c10bf2015-02-03 21:18:20 -0800879 darkgrey: '#444',
Simon Huntac4c6f72015-02-03 19:50:53 -0800880 midgrey: '#888',
881 lightgrey: '#bbb',
882 orange: '#f90'
883 };
884
885 // note: these are the device icon colors without affinity
886 var dColTheme = {
887 light: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800888 rfill: dCol.offwhite,
Simon Huntac4c6f72015-02-03 19:50:53 -0800889 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800890 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800891 rect: dCol.paleblue
892 },
893 offline: {
894 glyph: dCol.midgrey,
895 rect: dCol.lightgrey
896 }
897 },
Simon Huntac4c6f72015-02-03 19:50:53 -0800898 dark: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800899 rfill: dCol.midgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800900 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800901 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800902 rect: dCol.paleblue
903 },
904 offline: {
905 glyph: dCol.midgrey,
Simon Hunt78c10bf2015-02-03 21:18:20 -0800906 rect: dCol.darkgrey
Simon Huntac4c6f72015-02-03 19:50:53 -0800907 }
908 }
909 };
910
911 function devBaseColor(d) {
912 var o = d.online ? 'online' : 'offline';
913 return dColTheme[ts.theme()][o];
914 }
915
916 function setDeviceColor(d) {
917 var o = d.online,
918 s = d.el.classed('selected'),
919 c = devBaseColor(d),
920 a = instColor(d.master, o),
Simon Hunt51056592015-02-03 21:48:07 -0800921 icon = d.el.select('g.deviceIcon'),
922 g, r;
Simon Huntac4c6f72015-02-03 19:50:53 -0800923
924 if (s) {
925 g = c.glyph;
926 r = dCol.orange;
927 } else if (tis.isVisible()) {
928 g = o ? a : c.glyph;
Simon Hunt78c10bf2015-02-03 21:18:20 -0800929 r = o ? c.rfill : a;
Simon Huntac4c6f72015-02-03 19:50:53 -0800930 } else {
931 g = c.glyph;
932 r = c.rect;
933 }
934
Simon Hunt51056592015-02-03 21:48:07 -0800935 icon.select('use').style('fill', g);
936 icon.select('rect').style('fill', r);
Simon Huntac4c6f72015-02-03 19:50:53 -0800937 }
938
939 function instColor(id, online) {
940 return sus.cat7().getColor(id, !online, ts.theme());
941 }
942
Simon Hunt1894d792015-02-04 17:09:20 -0800943 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800944
945 function updateNodes() {
Simon Hunt1894d792015-02-04 17:09:20 -0800946 // select all the nodes in the layout:
Simon Huntac4c6f72015-02-03 19:50:53 -0800947 node = nodeG.selectAll('.node')
948 .data(network.nodes, function (d) { return d.id; });
949
Simon Hunt1894d792015-02-04 17:09:20 -0800950 // operate on existing nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800951 node.filter('.device').each(deviceExisting);
952 node.filter('.host').each(hostExisting);
Simon Huntac4c6f72015-02-03 19:50:53 -0800953
954 // operate on entering nodes:
955 var entering = node.enter()
956 .append('g')
957 .attr({
958 id: function (d) { return sus.safeId(d.id); },
959 class: mkSvgClass,
960 transform: function (d) { return sus.translate(d.x, d.y); },
961 opacity: 0
962 })
963 .call(drag)
964 .on('mouseover', nodeMouseOver)
965 .on('mouseout', nodeMouseOut)
966 .transition()
967 .attr('opacity', 1);
968
Simon Hunt1894d792015-02-04 17:09:20 -0800969 // augment entering nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800970 entering.filter('.device').each(deviceEnter);
971 entering.filter('.host').each(hostEnter);
Simon Huntac4c6f72015-02-03 19:50:53 -0800972
Simon Hunt51056592015-02-03 21:48:07 -0800973 // operate on both existing and new nodes:
Simon Huntac4c6f72015-02-03 19:50:53 -0800974 updateDeviceColors();
975
976 // operate on exiting nodes:
977 // Note that the node is removed after 2 seconds.
978 // Sub element animations should be shorter than 2 seconds.
979 var exiting = node.exit()
980 .transition()
981 .duration(2000)
982 .style('opacity', 0)
983 .remove();
984
Simon Hunt1894d792015-02-04 17:09:20 -0800985 // exiting node specifics:
Simon Hunt51056592015-02-03 21:48:07 -0800986 exiting.filter('.host').each(hostExit);
987 exiting.filter('.device').each(deviceExit);
Simon Huntac4c6f72015-02-03 19:50:53 -0800988
Simon Hunt51056592015-02-03 21:48:07 -0800989 // finally, resume the force layout
Simon Huntac4c6f72015-02-03 19:50:53 -0800990 fResume();
991 }
992
Simon Hunt51056592015-02-03 21:48:07 -0800993 // ==========================
994 // updateNodes - subfunctions
995
996 function deviceExisting(d) {
997 var node = d.el;
998 node.classed('online', d.online);
999 updateDeviceLabel(d);
1000 positionNode(d, true);
1001 }
1002
1003 function hostExisting(d) {
1004 updateHostLabel(d);
1005 positionNode(d, true);
1006 }
1007
1008 function deviceEnter(d) {
1009 var node = d3.select(this),
1010 glyphId = d.type || 'unknown',
1011 label = trimLabel(deviceLabel(d)),
1012 devCfg = deviceIconConfig,
1013 noLabel = !label,
1014 box, dx, dy, icon;
1015
1016 d.el = node;
1017
1018 node.append('rect').attr({ rx: 5, ry: 5 });
1019 node.append('text').text(label).attr('dy', '1.1em');
1020 box = adjustRectToFitText(node);
1021 node.select('rect').attr(box);
1022
1023 icon = is.addDeviceIcon(node, glyphId);
1024
1025 if (noLabel) {
1026 dx = -icon.dim/2;
1027 dy = -icon.dim/2;
1028 } else {
1029 box = adjustRectToFitText(node);
1030 dx = box.x + devCfg.xoff;
1031 dy = box.y + devCfg.yoff;
1032 }
1033
1034 icon.attr('transform', sus.translate(dx, dy));
1035 }
1036
1037 function hostEnter(d) {
Simon Hunt1894d792015-02-04 17:09:20 -08001038 var node = d3.select(this),
1039 gid = d.type || 'unknown',
1040 rad = icfg.host.radius,
1041 r = d.type ? rad.withGlyph : rad.noGlyph,
1042 textDy = r + 10;
Simon Hunt51056592015-02-03 21:48:07 -08001043
1044 d.el = node;
Simon Hunt1894d792015-02-04 17:09:20 -08001045 sus.makeVisible(node, showHosts);
Simon Hunt51056592015-02-03 21:48:07 -08001046
Simon Hunt1894d792015-02-04 17:09:20 -08001047 is.addHostIcon(node, r, gid);
Simon Hunt51056592015-02-03 21:48:07 -08001048
Simon Hunt51056592015-02-03 21:48:07 -08001049 node.append('text')
1050 .text(hostLabel)
Simon Hunt1894d792015-02-04 17:09:20 -08001051 .attr('dy', textDy)
Simon Hunt51056592015-02-03 21:48:07 -08001052 .attr('text-anchor', 'middle');
1053 }
1054
1055 function hostExit(d) {
1056 var node = d.el;
1057 node.select('use')
1058 .style('opacity', 0.5)
1059 .transition()
1060 .duration(800)
1061 .style('opacity', 0);
1062
1063 node.select('text')
1064 .style('opacity', 0.5)
1065 .transition()
1066 .duration(800)
1067 .style('opacity', 0);
1068
1069 node.select('circle')
1070 .style('stroke-fill', '#555')
1071 .style('fill', '#888')
1072 .style('opacity', 0.5)
1073 .transition()
1074 .duration(1500)
1075 .attr('r', 0);
1076 }
1077
1078 function deviceExit(d) {
1079 var node = d.el;
1080 node.select('use')
1081 .style('opacity', 0.5)
1082 .transition()
1083 .duration(800)
1084 .style('opacity', 0);
1085
1086 node.selectAll('rect')
1087 .style('stroke-fill', '#555')
1088 .style('fill', '#888')
1089 .style('opacity', 0.5);
1090 }
1091
Simon Hunt1894d792015-02-04 17:09:20 -08001092 // ==========================
1093
1094 function updateLinks() {
1095 var th = ts.theme();
1096
1097 link = linkG.selectAll('.link')
1098 .data(network.links, function (d) { return d.key; });
1099
1100 // operate on existing links:
1101 //link.each(linkExisting);
1102
1103 // operate on entering links:
1104 var entering = link.enter()
1105 .append('line')
1106 .attr({
1107 x1: function (d) { return d.x1; },
1108 y1: function (d) { return d.y1; },
1109 x2: function (d) { return d.x2; },
1110 y2: function (d) { return d.y2; },
1111 stroke: linkConfig[th].inColor,
1112 'stroke-width': linkConfig.inWidth
1113 });
1114
1115 // augment links
1116 entering.each(linkEntering);
1117
1118 // operate on both existing and new links:
1119 //link.each(...)
1120
1121 // apply or remove labels
1122 var labelData = getLabelData();
1123 applyLinkLabels(labelData);
1124
1125 // operate on exiting links:
1126 link.exit()
1127 .attr('stroke-dasharray', '3 3')
Simon Hunt5724fb42015-02-05 16:59:40 -08001128 .attr('stroke', linkConfig[th].outColor)
Simon Hunt1894d792015-02-04 17:09:20 -08001129 .style('opacity', 0.5)
1130 .transition()
1131 .duration(1500)
1132 .attr({
1133 'stroke-dasharray': '3 12',
Simon Hunt1894d792015-02-04 17:09:20 -08001134 'stroke-width': linkConfig.outWidth
1135 })
1136 .style('opacity', 0.0)
1137 .remove();
1138
1139 // NOTE: invoke a single tick to force the labels to position
1140 // onto their links.
1141 tick();
Simon Hunt5724fb42015-02-05 16:59:40 -08001142 // TODO: this causes undesirable behavior when in oblique view
Simon Hunt1894d792015-02-04 17:09:20 -08001143 // It causes the nodes to jump into "overhead" view positions, even
1144 // though the oblique planes are still showing...
1145 }
1146
1147 // ==========================
1148 // updateLinks - subfunctions
1149
1150 function getLabelData() {
1151 // create the backing data for showing labels..
1152 var data = [];
1153 link.each(function (d) {
1154 if (d.label) {
1155 data.push({
1156 id: 'lab-' + d.key,
1157 key: d.key,
1158 label: d.label,
1159 ldata: d
1160 });
1161 }
1162 });
1163 return data;
1164 }
1165
1166 //function linkExisting(d) { }
1167
1168 function linkEntering(d) {
1169 var link = d3.select(this);
1170 d.el = link;
1171 restyleLinkElement(d);
1172 if (d.type() === 'hostLink') {
1173 sus.makeVisible(link, showHosts);
1174 }
1175 }
1176
1177 //function linkExiting(d) { }
1178
1179 var linkLabelOffset = '0.3em';
1180
1181 function applyLinkLabels(data) {
1182 var entering;
1183
1184 linkLabel = linkLabelG.selectAll('.linkLabel')
1185 .data(data, function (d) { return d.id; });
1186
1187 // for elements already existing, we need to update the text
1188 // and adjust the rectangle size to fit
1189 linkLabel.each(function (d) {
1190 var el = d3.select(this),
1191 rect = el.select('rect'),
1192 text = el.select('text');
1193 text.text(d.label);
1194 rect.attr(rectAroundText(el));
1195 });
1196
1197 entering = linkLabel.enter().append('g')
1198 .classed('linkLabel', true)
1199 .attr('id', function (d) { return d.id; });
1200
1201 entering.each(function (d) {
1202 var el = d3.select(this),
1203 rect,
1204 text,
1205 parms = {
1206 x1: d.ldata.x1,
1207 y1: d.ldata.y1,
1208 x2: d.ldata.x2,
1209 y2: d.ldata.y2
1210 };
1211
1212 d.el = el;
1213 rect = el.append('rect');
1214 text = el.append('text').text(d.label);
1215 rect.attr(rectAroundText(el));
1216 text.attr('dy', linkLabelOffset);
1217
1218 el.attr('transform', transformLabel(parms));
1219 });
1220
1221 // Remove any labels that are no longer required.
1222 linkLabel.exit().remove();
1223 }
1224
1225 function rectAroundText(el) {
1226 var text = el.select('text'),
1227 box = text.node().getBBox();
1228
1229 // translate the bbox so that it is centered on [x,y]
1230 box.x = -box.width / 2;
1231 box.y = -box.height / 2;
1232
1233 // add padding
1234 box.x -= 1;
1235 box.width += 2;
1236 return box;
1237 }
1238
1239 function transformLabel(p) {
1240 var dx = p.x2 - p.x1,
1241 dy = p.y2 - p.y1,
1242 xMid = dx/2 + p.x1,
1243 yMid = dy/2 + p.y1;
1244 return sus.translate(xMid, yMid);
1245 }
Simon Huntac4c6f72015-02-03 19:50:53 -08001246
1247 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -08001248 // force layout tick function
Simon Hunt737c89f2015-01-28 12:23:19 -08001249
Simon Hunt5724fb42015-02-05 16:59:40 -08001250 function fResume() {
1251 if (!oblique) {
1252 force.resume();
1253 }
1254 }
1255
1256 function fStart() {
1257 if (!oblique) {
1258 force.start();
1259 }
1260 }
1261
1262 var tickStuff = {
1263 nodeAttr: {
1264 transform: function (d) { return sus.translate(d.x, d.y); }
1265 },
1266 linkAttr: {
1267 x1: function (d) { return d.source.x; },
1268 y1: function (d) { return d.source.y; },
1269 x2: function (d) { return d.target.x; },
1270 y2: function (d) { return d.target.y; }
1271 },
1272 linkLabelAttr: {
1273 transform: function (d) {
1274 var lnk = findLinkById(d.key);
1275 if (lnk) {
1276 return transformLabel({
1277 x1: lnk.source.x,
1278 y1: lnk.source.y,
1279 x2: lnk.target.x,
1280 y2: lnk.target.y
1281 });
1282 }
1283 }
1284 }
1285 };
1286
1287 function tick() {
1288 node.attr(tickStuff.nodeAttr);
1289 link.attr(tickStuff.linkAttr);
1290 linkLabel.attr(tickStuff.linkLabelAttr);
Simon Hunt737c89f2015-01-28 12:23:19 -08001291 }
1292
1293
Simon Huntac4c6f72015-02-03 19:50:53 -08001294 // ==========================
1295 // === MOUSE GESTURE HANDLERS
1296
Simon Hunt5724fb42015-02-05 16:59:40 -08001297 // FIXME:
Simon Hunt737c89f2015-01-28 12:23:19 -08001298 function selectCb() { }
1299 function atDragEnd() {}
1300 function dragEnabled() {}
1301 function clickEnabled() {}
1302
1303
1304 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -08001305 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -08001306
1307 angular.module('ovTopo')
1308 .factory('TopoForceService',
Simon Hunt1894d792015-02-04 17:09:20 -08001309 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
Simon Hunt5724fb42015-02-05 16:59:40 -08001310 'FlashService', 'TopoInstService',
Simon Hunt737c89f2015-01-28 12:23:19 -08001311
Simon Hunt5724fb42015-02-05 16:59:40 -08001312 function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_) {
Simon Hunt737c89f2015-01-28 12:23:19 -08001313 $log = _$log_;
Simon Hunt1894d792015-02-04 17:09:20 -08001314 fs = _fs_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001315 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001316 is = _is_;
1317 ts = _ts_;
Simon Hunt5724fb42015-02-05 16:59:40 -08001318 flash = _flash_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001319 tis = _tis_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001320
Simon Hunt1894d792015-02-04 17:09:20 -08001321 icfg = is.iconConfig();
1322
Simon Hunt737c89f2015-01-28 12:23:19 -08001323 // forceG is the SVG group to display the force layout in
Simon Huntac4c6f72015-02-03 19:50:53 -08001324 // xlink is the cross-link api from the main topo source file
Simon Hunt737c89f2015-01-28 12:23:19 -08001325 // w, h are the initial dimensions of the SVG
1326 // opts are, well, optional :)
Simon Hunt1894d792015-02-04 17:09:20 -08001327 function initForce(forceG, _uplink_, w, h, opts) {
Simon Hunta11b4eb2015-01-28 16:20:50 -08001328 $log.debug('initForce().. WxH = ' + w + 'x' + h);
Simon Hunt1894d792015-02-04 17:09:20 -08001329 uplink = _uplink_;
1330 width = w;
1331 height = h;
Simon Hunta11b4eb2015-01-28 16:20:50 -08001332
Simon Hunt737c89f2015-01-28 12:23:19 -08001333 settings = angular.extend({}, defaultSettings, opts);
1334
1335 linkG = forceG.append('g').attr('id', 'topo-links');
1336 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
1337 nodeG = forceG.append('g').attr('id', 'topo-nodes');
1338
1339 link = linkG.selectAll('.link');
1340 linkLabel = linkLabelG.selectAll('.linkLabel');
1341 node = nodeG.selectAll('.node');
1342
1343 force = d3.layout.force()
Simon Hunta11b4eb2015-01-28 16:20:50 -08001344 .size([w, h])
Simon Hunt737c89f2015-01-28 12:23:19 -08001345 .nodes(network.nodes)
1346 .links(network.links)
1347 .gravity(settings.gravity)
1348 .friction(settings.friction)
1349 .charge(settings.charge._def_)
1350 .linkDistance(settings.linkDistance._def_)
1351 .linkStrength(settings.linkStrength._def_)
1352 .on('tick', tick);
1353
1354 drag = sus.createDragBehavior(force,
1355 selectCb, atDragEnd, dragEnabled, clickEnabled);
1356 }
1357
Simon Huntb0ec1e52015-01-28 18:13:49 -08001358 function resize(dim) {
Simon Hunt1894d792015-02-04 17:09:20 -08001359 width = dim.width;
1360 height = dim.height;
1361 force.size([width, height]);
Simon Hunt737c89f2015-01-28 12:23:19 -08001362 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -08001363 }
1364
1365 return {
1366 initForce: initForce,
Simon Huntac4c6f72015-02-03 19:50:53 -08001367 resize: resize,
1368
1369 updateDeviceColors: updateDeviceColors,
Simon Hunt5724fb42015-02-05 16:59:40 -08001370 toggleHosts: toggleHosts,
1371 toggleOffline: toggleOffline,
1372 cycleDeviceLabels: cycleDeviceLabels,
Simon Huntac4c6f72015-02-03 19:50:53 -08001373
1374 addDevice: addDevice,
Simon Hunt1894d792015-02-04 17:09:20 -08001375 updateDevice: updateDevice,
1376 removeDevice: removeDevice,
1377 addHost: addHost,
1378 updateHost: updateHost,
1379 removeHost: removeHost,
1380 addLink: addLink,
1381 updateLink: updateLink,
1382 removeLink: removeLink
Simon Hunt737c89f2015-01-28 12:23:19 -08001383 };
1384 }]);
1385}());