blob: d7b9a66ab629aa4886223df1e832daca269ccdc7 [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 Hunt1c367112015-02-05 18:02:46 -0800443 function findDevices(offlineOnly) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800444 var a = [];
445 network.nodes.forEach(function (d) {
Simon Hunt1c367112015-02-05 18:02:46 -0800446 if (d.class === 'device' && !(offlineOnly && d.online)) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800447 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
Simon Hunt1c367112015-02-05 18:02:46 -0800553 findDevices(true).forEach(function (d) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800554 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() {
Simon Hunt1c367112015-02-05 18:02:46 -0800870 deviceLabelIndex = (deviceLabelIndex+1) % 3;
871 findDevices().forEach(function (d) {
872 updateDeviceLabel(d);
873 });
Simon Hunt5724fb42015-02-05 16:59:40 -0800874 }
875
876 // ==========================================
877
Simon Huntac4c6f72015-02-03 19:50:53 -0800878 var dCol = {
879 black: '#000',
880 paleblue: '#acf',
881 offwhite: '#ddd',
Simon Hunt78c10bf2015-02-03 21:18:20 -0800882 darkgrey: '#444',
Simon Huntac4c6f72015-02-03 19:50:53 -0800883 midgrey: '#888',
884 lightgrey: '#bbb',
885 orange: '#f90'
886 };
887
888 // note: these are the device icon colors without affinity
889 var dColTheme = {
890 light: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800891 rfill: dCol.offwhite,
Simon Huntac4c6f72015-02-03 19:50:53 -0800892 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800893 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800894 rect: dCol.paleblue
895 },
896 offline: {
897 glyph: dCol.midgrey,
898 rect: dCol.lightgrey
899 }
900 },
Simon Huntac4c6f72015-02-03 19:50:53 -0800901 dark: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800902 rfill: dCol.midgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800903 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800904 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800905 rect: dCol.paleblue
906 },
907 offline: {
908 glyph: dCol.midgrey,
Simon Hunt78c10bf2015-02-03 21:18:20 -0800909 rect: dCol.darkgrey
Simon Huntac4c6f72015-02-03 19:50:53 -0800910 }
911 }
912 };
913
914 function devBaseColor(d) {
915 var o = d.online ? 'online' : 'offline';
916 return dColTheme[ts.theme()][o];
917 }
918
919 function setDeviceColor(d) {
920 var o = d.online,
921 s = d.el.classed('selected'),
922 c = devBaseColor(d),
923 a = instColor(d.master, o),
Simon Hunt51056592015-02-03 21:48:07 -0800924 icon = d.el.select('g.deviceIcon'),
925 g, r;
Simon Huntac4c6f72015-02-03 19:50:53 -0800926
927 if (s) {
928 g = c.glyph;
929 r = dCol.orange;
930 } else if (tis.isVisible()) {
931 g = o ? a : c.glyph;
Simon Hunt78c10bf2015-02-03 21:18:20 -0800932 r = o ? c.rfill : a;
Simon Huntac4c6f72015-02-03 19:50:53 -0800933 } else {
934 g = c.glyph;
935 r = c.rect;
936 }
937
Simon Hunt51056592015-02-03 21:48:07 -0800938 icon.select('use').style('fill', g);
939 icon.select('rect').style('fill', r);
Simon Huntac4c6f72015-02-03 19:50:53 -0800940 }
941
942 function instColor(id, online) {
943 return sus.cat7().getColor(id, !online, ts.theme());
944 }
945
Simon Hunt1894d792015-02-04 17:09:20 -0800946 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800947
948 function updateNodes() {
Simon Hunt1894d792015-02-04 17:09:20 -0800949 // select all the nodes in the layout:
Simon Huntac4c6f72015-02-03 19:50:53 -0800950 node = nodeG.selectAll('.node')
951 .data(network.nodes, function (d) { return d.id; });
952
Simon Hunt1894d792015-02-04 17:09:20 -0800953 // operate on existing nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800954 node.filter('.device').each(deviceExisting);
955 node.filter('.host').each(hostExisting);
Simon Huntac4c6f72015-02-03 19:50:53 -0800956
957 // operate on entering nodes:
958 var entering = node.enter()
959 .append('g')
960 .attr({
961 id: function (d) { return sus.safeId(d.id); },
962 class: mkSvgClass,
963 transform: function (d) { return sus.translate(d.x, d.y); },
964 opacity: 0
965 })
966 .call(drag)
967 .on('mouseover', nodeMouseOver)
968 .on('mouseout', nodeMouseOut)
969 .transition()
970 .attr('opacity', 1);
971
Simon Hunt1894d792015-02-04 17:09:20 -0800972 // augment entering nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800973 entering.filter('.device').each(deviceEnter);
974 entering.filter('.host').each(hostEnter);
Simon Huntac4c6f72015-02-03 19:50:53 -0800975
Simon Hunt51056592015-02-03 21:48:07 -0800976 // operate on both existing and new nodes:
Simon Huntac4c6f72015-02-03 19:50:53 -0800977 updateDeviceColors();
978
979 // operate on exiting nodes:
980 // Note that the node is removed after 2 seconds.
981 // Sub element animations should be shorter than 2 seconds.
982 var exiting = node.exit()
983 .transition()
984 .duration(2000)
985 .style('opacity', 0)
986 .remove();
987
Simon Hunt1894d792015-02-04 17:09:20 -0800988 // exiting node specifics:
Simon Hunt51056592015-02-03 21:48:07 -0800989 exiting.filter('.host').each(hostExit);
990 exiting.filter('.device').each(deviceExit);
Simon Huntac4c6f72015-02-03 19:50:53 -0800991
Simon Hunt51056592015-02-03 21:48:07 -0800992 // finally, resume the force layout
Simon Huntac4c6f72015-02-03 19:50:53 -0800993 fResume();
994 }
995
Simon Hunt51056592015-02-03 21:48:07 -0800996 // ==========================
997 // updateNodes - subfunctions
998
999 function deviceExisting(d) {
1000 var node = d.el;
1001 node.classed('online', d.online);
1002 updateDeviceLabel(d);
1003 positionNode(d, true);
1004 }
1005
1006 function hostExisting(d) {
1007 updateHostLabel(d);
1008 positionNode(d, true);
1009 }
1010
1011 function deviceEnter(d) {
1012 var node = d3.select(this),
1013 glyphId = d.type || 'unknown',
1014 label = trimLabel(deviceLabel(d)),
1015 devCfg = deviceIconConfig,
1016 noLabel = !label,
1017 box, dx, dy, icon;
1018
1019 d.el = node;
1020
1021 node.append('rect').attr({ rx: 5, ry: 5 });
1022 node.append('text').text(label).attr('dy', '1.1em');
1023 box = adjustRectToFitText(node);
1024 node.select('rect').attr(box);
1025
1026 icon = is.addDeviceIcon(node, glyphId);
1027
1028 if (noLabel) {
1029 dx = -icon.dim/2;
1030 dy = -icon.dim/2;
1031 } else {
1032 box = adjustRectToFitText(node);
1033 dx = box.x + devCfg.xoff;
1034 dy = box.y + devCfg.yoff;
1035 }
1036
1037 icon.attr('transform', sus.translate(dx, dy));
1038 }
1039
1040 function hostEnter(d) {
Simon Hunt1894d792015-02-04 17:09:20 -08001041 var node = d3.select(this),
1042 gid = d.type || 'unknown',
1043 rad = icfg.host.radius,
1044 r = d.type ? rad.withGlyph : rad.noGlyph,
1045 textDy = r + 10;
Simon Hunt51056592015-02-03 21:48:07 -08001046
1047 d.el = node;
Simon Hunt1894d792015-02-04 17:09:20 -08001048 sus.makeVisible(node, showHosts);
Simon Hunt51056592015-02-03 21:48:07 -08001049
Simon Hunt1894d792015-02-04 17:09:20 -08001050 is.addHostIcon(node, r, gid);
Simon Hunt51056592015-02-03 21:48:07 -08001051
Simon Hunt51056592015-02-03 21:48:07 -08001052 node.append('text')
1053 .text(hostLabel)
Simon Hunt1894d792015-02-04 17:09:20 -08001054 .attr('dy', textDy)
Simon Hunt51056592015-02-03 21:48:07 -08001055 .attr('text-anchor', 'middle');
1056 }
1057
1058 function hostExit(d) {
1059 var node = d.el;
1060 node.select('use')
1061 .style('opacity', 0.5)
1062 .transition()
1063 .duration(800)
1064 .style('opacity', 0);
1065
1066 node.select('text')
1067 .style('opacity', 0.5)
1068 .transition()
1069 .duration(800)
1070 .style('opacity', 0);
1071
1072 node.select('circle')
1073 .style('stroke-fill', '#555')
1074 .style('fill', '#888')
1075 .style('opacity', 0.5)
1076 .transition()
1077 .duration(1500)
1078 .attr('r', 0);
1079 }
1080
1081 function deviceExit(d) {
1082 var node = d.el;
1083 node.select('use')
1084 .style('opacity', 0.5)
1085 .transition()
1086 .duration(800)
1087 .style('opacity', 0);
1088
1089 node.selectAll('rect')
1090 .style('stroke-fill', '#555')
1091 .style('fill', '#888')
1092 .style('opacity', 0.5);
1093 }
1094
Simon Hunt1894d792015-02-04 17:09:20 -08001095 // ==========================
1096
1097 function updateLinks() {
1098 var th = ts.theme();
1099
1100 link = linkG.selectAll('.link')
1101 .data(network.links, function (d) { return d.key; });
1102
1103 // operate on existing links:
1104 //link.each(linkExisting);
1105
1106 // operate on entering links:
1107 var entering = link.enter()
1108 .append('line')
1109 .attr({
1110 x1: function (d) { return d.x1; },
1111 y1: function (d) { return d.y1; },
1112 x2: function (d) { return d.x2; },
1113 y2: function (d) { return d.y2; },
1114 stroke: linkConfig[th].inColor,
1115 'stroke-width': linkConfig.inWidth
1116 });
1117
1118 // augment links
1119 entering.each(linkEntering);
1120
1121 // operate on both existing and new links:
1122 //link.each(...)
1123
1124 // apply or remove labels
1125 var labelData = getLabelData();
1126 applyLinkLabels(labelData);
1127
1128 // operate on exiting links:
1129 link.exit()
1130 .attr('stroke-dasharray', '3 3')
Simon Hunt5724fb42015-02-05 16:59:40 -08001131 .attr('stroke', linkConfig[th].outColor)
Simon Hunt1894d792015-02-04 17:09:20 -08001132 .style('opacity', 0.5)
1133 .transition()
1134 .duration(1500)
1135 .attr({
1136 'stroke-dasharray': '3 12',
Simon Hunt1894d792015-02-04 17:09:20 -08001137 'stroke-width': linkConfig.outWidth
1138 })
1139 .style('opacity', 0.0)
1140 .remove();
1141
1142 // NOTE: invoke a single tick to force the labels to position
1143 // onto their links.
1144 tick();
Simon Hunt5724fb42015-02-05 16:59:40 -08001145 // TODO: this causes undesirable behavior when in oblique view
Simon Hunt1894d792015-02-04 17:09:20 -08001146 // It causes the nodes to jump into "overhead" view positions, even
1147 // though the oblique planes are still showing...
1148 }
1149
1150 // ==========================
1151 // updateLinks - subfunctions
1152
1153 function getLabelData() {
1154 // create the backing data for showing labels..
1155 var data = [];
1156 link.each(function (d) {
1157 if (d.label) {
1158 data.push({
1159 id: 'lab-' + d.key,
1160 key: d.key,
1161 label: d.label,
1162 ldata: d
1163 });
1164 }
1165 });
1166 return data;
1167 }
1168
1169 //function linkExisting(d) { }
1170
1171 function linkEntering(d) {
1172 var link = d3.select(this);
1173 d.el = link;
1174 restyleLinkElement(d);
1175 if (d.type() === 'hostLink') {
1176 sus.makeVisible(link, showHosts);
1177 }
1178 }
1179
1180 //function linkExiting(d) { }
1181
1182 var linkLabelOffset = '0.3em';
1183
1184 function applyLinkLabels(data) {
1185 var entering;
1186
1187 linkLabel = linkLabelG.selectAll('.linkLabel')
1188 .data(data, function (d) { return d.id; });
1189
1190 // for elements already existing, we need to update the text
1191 // and adjust the rectangle size to fit
1192 linkLabel.each(function (d) {
1193 var el = d3.select(this),
1194 rect = el.select('rect'),
1195 text = el.select('text');
1196 text.text(d.label);
1197 rect.attr(rectAroundText(el));
1198 });
1199
1200 entering = linkLabel.enter().append('g')
1201 .classed('linkLabel', true)
1202 .attr('id', function (d) { return d.id; });
1203
1204 entering.each(function (d) {
1205 var el = d3.select(this),
1206 rect,
1207 text,
1208 parms = {
1209 x1: d.ldata.x1,
1210 y1: d.ldata.y1,
1211 x2: d.ldata.x2,
1212 y2: d.ldata.y2
1213 };
1214
1215 d.el = el;
1216 rect = el.append('rect');
1217 text = el.append('text').text(d.label);
1218 rect.attr(rectAroundText(el));
1219 text.attr('dy', linkLabelOffset);
1220
1221 el.attr('transform', transformLabel(parms));
1222 });
1223
1224 // Remove any labels that are no longer required.
1225 linkLabel.exit().remove();
1226 }
1227
1228 function rectAroundText(el) {
1229 var text = el.select('text'),
1230 box = text.node().getBBox();
1231
1232 // translate the bbox so that it is centered on [x,y]
1233 box.x = -box.width / 2;
1234 box.y = -box.height / 2;
1235
1236 // add padding
1237 box.x -= 1;
1238 box.width += 2;
1239 return box;
1240 }
1241
1242 function transformLabel(p) {
1243 var dx = p.x2 - p.x1,
1244 dy = p.y2 - p.y1,
1245 xMid = dx/2 + p.x1,
1246 yMid = dy/2 + p.y1;
1247 return sus.translate(xMid, yMid);
1248 }
Simon Huntac4c6f72015-02-03 19:50:53 -08001249
1250 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -08001251 // force layout tick function
Simon Hunt737c89f2015-01-28 12:23:19 -08001252
Simon Hunt5724fb42015-02-05 16:59:40 -08001253 function fResume() {
1254 if (!oblique) {
1255 force.resume();
1256 }
1257 }
1258
1259 function fStart() {
1260 if (!oblique) {
1261 force.start();
1262 }
1263 }
1264
1265 var tickStuff = {
1266 nodeAttr: {
1267 transform: function (d) { return sus.translate(d.x, d.y); }
1268 },
1269 linkAttr: {
1270 x1: function (d) { return d.source.x; },
1271 y1: function (d) { return d.source.y; },
1272 x2: function (d) { return d.target.x; },
1273 y2: function (d) { return d.target.y; }
1274 },
1275 linkLabelAttr: {
1276 transform: function (d) {
1277 var lnk = findLinkById(d.key);
1278 if (lnk) {
1279 return transformLabel({
1280 x1: lnk.source.x,
1281 y1: lnk.source.y,
1282 x2: lnk.target.x,
1283 y2: lnk.target.y
1284 });
1285 }
1286 }
1287 }
1288 };
1289
1290 function tick() {
1291 node.attr(tickStuff.nodeAttr);
1292 link.attr(tickStuff.linkAttr);
1293 linkLabel.attr(tickStuff.linkLabelAttr);
Simon Hunt737c89f2015-01-28 12:23:19 -08001294 }
1295
1296
Simon Huntac4c6f72015-02-03 19:50:53 -08001297 // ==========================
1298 // === MOUSE GESTURE HANDLERS
1299
Simon Hunt5724fb42015-02-05 16:59:40 -08001300 // FIXME:
Simon Hunt737c89f2015-01-28 12:23:19 -08001301 function selectCb() { }
1302 function atDragEnd() {}
1303 function dragEnabled() {}
1304 function clickEnabled() {}
1305
1306
1307 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -08001308 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -08001309
1310 angular.module('ovTopo')
1311 .factory('TopoForceService',
Simon Hunt1894d792015-02-04 17:09:20 -08001312 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
Simon Hunt5724fb42015-02-05 16:59:40 -08001313 'FlashService', 'TopoInstService',
Simon Hunt737c89f2015-01-28 12:23:19 -08001314
Simon Hunt5724fb42015-02-05 16:59:40 -08001315 function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_) {
Simon Hunt737c89f2015-01-28 12:23:19 -08001316 $log = _$log_;
Simon Hunt1894d792015-02-04 17:09:20 -08001317 fs = _fs_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001318 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001319 is = _is_;
1320 ts = _ts_;
Simon Hunt5724fb42015-02-05 16:59:40 -08001321 flash = _flash_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001322 tis = _tis_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001323
Simon Hunt1894d792015-02-04 17:09:20 -08001324 icfg = is.iconConfig();
1325
Simon Hunt737c89f2015-01-28 12:23:19 -08001326 // forceG is the SVG group to display the force layout in
Simon Huntac4c6f72015-02-03 19:50:53 -08001327 // xlink is the cross-link api from the main topo source file
Simon Hunt737c89f2015-01-28 12:23:19 -08001328 // w, h are the initial dimensions of the SVG
1329 // opts are, well, optional :)
Simon Hunt1894d792015-02-04 17:09:20 -08001330 function initForce(forceG, _uplink_, w, h, opts) {
Simon Hunta11b4eb2015-01-28 16:20:50 -08001331 $log.debug('initForce().. WxH = ' + w + 'x' + h);
Simon Hunt1894d792015-02-04 17:09:20 -08001332 uplink = _uplink_;
1333 width = w;
1334 height = h;
Simon Hunta11b4eb2015-01-28 16:20:50 -08001335
Simon Hunt737c89f2015-01-28 12:23:19 -08001336 settings = angular.extend({}, defaultSettings, opts);
1337
1338 linkG = forceG.append('g').attr('id', 'topo-links');
1339 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
1340 nodeG = forceG.append('g').attr('id', 'topo-nodes');
1341
1342 link = linkG.selectAll('.link');
1343 linkLabel = linkLabelG.selectAll('.linkLabel');
1344 node = nodeG.selectAll('.node');
1345
1346 force = d3.layout.force()
Simon Hunta11b4eb2015-01-28 16:20:50 -08001347 .size([w, h])
Simon Hunt737c89f2015-01-28 12:23:19 -08001348 .nodes(network.nodes)
1349 .links(network.links)
1350 .gravity(settings.gravity)
1351 .friction(settings.friction)
1352 .charge(settings.charge._def_)
1353 .linkDistance(settings.linkDistance._def_)
1354 .linkStrength(settings.linkStrength._def_)
1355 .on('tick', tick);
1356
1357 drag = sus.createDragBehavior(force,
1358 selectCb, atDragEnd, dragEnabled, clickEnabled);
1359 }
1360
Simon Huntb0ec1e52015-01-28 18:13:49 -08001361 function resize(dim) {
Simon Hunt1894d792015-02-04 17:09:20 -08001362 width = dim.width;
1363 height = dim.height;
1364 force.size([width, height]);
Simon Hunt737c89f2015-01-28 12:23:19 -08001365 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -08001366 }
1367
1368 return {
1369 initForce: initForce,
Simon Huntac4c6f72015-02-03 19:50:53 -08001370 resize: resize,
1371
1372 updateDeviceColors: updateDeviceColors,
Simon Hunt5724fb42015-02-05 16:59:40 -08001373 toggleHosts: toggleHosts,
1374 toggleOffline: toggleOffline,
1375 cycleDeviceLabels: cycleDeviceLabels,
Simon Huntac4c6f72015-02-03 19:50:53 -08001376
1377 addDevice: addDevice,
Simon Hunt1894d792015-02-04 17:09:20 -08001378 updateDevice: updateDevice,
1379 removeDevice: removeDevice,
1380 addHost: addHost,
1381 updateHost: updateHost,
1382 removeHost: removeHost,
1383 addLink: addLink,
1384 updateLink: updateLink,
1385 removeLink: removeLink
Simon Hunt737c89f2015-01-28 12:23:19 -08001386 };
1387 }]);
1388}());