blob: 01b719e4302e5a50cf9fe37428f08f710cff12dd [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 Hunt1894d792015-02-04 17:09:20 -080026 var $log, fs, sus, is, ts, tis, uplink;
27
28 var icfg;
Simon Huntac4c6f72015-02-03 19:50:53 -080029
30 // configuration
31 var labelConfig = {
32 imgPad: 16,
33 padLR: 4,
34 padTB: 3,
35 marginLR: 3,
36 marginTB: 2,
37 port: {
38 gap: 3,
39 width: 18,
40 height: 14
41 }
42 };
43
44 var deviceIconConfig = {
45 xoff: -20,
46 yoff: -18
47 };
Simon Hunt737c89f2015-01-28 12:23:19 -080048
Simon Hunt1894d792015-02-04 17:09:20 -080049 var linkConfig = {
50 light: {
51 baseColor: '#666',
52 inColor: '#66f',
53 outColor: '#f00',
54 },
55 dark: {
56 baseColor: '#666',
57 inColor: '#66f',
58 outColor: '#f00',
59 },
60 inWidth: 12,
61 outWidth: 10
62 };
63
Simon Hunt737c89f2015-01-28 12:23:19 -080064 // internal state
Simon Huntac4c6f72015-02-03 19:50:53 -080065 var settings, // merged default settings and options
Simon Hunt737c89f2015-01-28 12:23:19 -080066 force, // force layout object
67 drag, // drag behavior handler
68 network = {
69 nodes: [],
70 links: [],
71 lookup: {},
72 revLinkToKey: {}
Simon Huntac4c6f72015-02-03 19:50:53 -080073 },
Simon Hunt1894d792015-02-04 17:09:20 -080074 lu = network.lookup, // shorthand
Simon Huntac4c6f72015-02-03 19:50:53 -080075 deviceLabelIndex = 0, // for device label cycling
Simon Hunt1894d792015-02-04 17:09:20 -080076 hostLabelIndex = 0, // for host label cycling
77 showHosts = 1, // whether hosts are displayed
78 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) {
153 // TODO: re-instate link update, and offline visibility
154 //findAttachedLinks(d.id).forEach(restyleLinkElement);
155 //updateOfflineVisibility(d);
156 }
157 } else {
158 // TODO: decide whether we want to capture logic errors
159 //logicError('updateDevice lookup fail. ID = "' + id + '"');
160 }
161 }
162
Simon Hunt1894d792015-02-04 17:09:20 -0800163 function removeDevice(data) {
164 var id = data.id,
165 d = lu[id];
166 if (d) {
167 removeDeviceElement(d);
168 } else {
169 // TODO: decide whether we want to capture logic errors
170 //logicError('removeDevice lookup fail. ID = "' + id + '"');
171 }
172 }
173
174 function addHost(data) {
175 var id = data.id,
176 d, lnk;
177
178 // although this is an add host event, if we already have the
179 // host, treat it as an update instead..
180 if (lu[id]) {
181 updateHost(data);
182 return;
183 }
184
185 d = createHostNode(data);
186 network.nodes.push(d);
187 lu[id] = d;
188
189 $log.debug("Created new host.. ", d.id, d.x, d.y);
190
191 updateNodes();
192
193 lnk = createHostLink(data);
194 if (lnk) {
195
196 $log.debug("Created new host-link.. ", lnk.key);
197
198 d.linkData = lnk; // cache ref on its host
199 network.links.push(lnk);
200 lu[d.ingress] = lnk;
201 lu[d.egress] = lnk;
202 updateLinks();
203 }
204
205 fStart();
206 }
207
208 function updateHost(data) {
209 var id = data.id,
210 d = lu[id];
211 if (d) {
212 angular.extend(d, data);
213 if (positionNode(d, true)) {
214 sendUpdateMeta(d, true);
215 }
216 updateNodes();
217 } else {
218 // TODO: decide whether we want to capture logic errors
219 //logicError('updateHost lookup fail. ID = "' + id + '"');
220 }
221 }
222
223 function removeHost(data) {
224 var id = data.id,
225 d = lu[id];
226 if (d) {
227 removeHostElement(d, true);
228 } else {
229 // may have already removed host, if attached to removed device
230 //console.warn('removeHost lookup fail. ID = "' + id + '"');
231 }
232 }
233
234 function addLink(data) {
235 var result = findLink(data, 'add'),
236 bad = result.badLogic,
237 d = result.ldata;
238
239 if (bad) {
240 //logicError(bad + ': ' + link.id);
241 return;
242 }
243
244 if (d) {
245 // we already have a backing store link for src/dst nodes
246 addLinkUpdate(d, data);
247 return;
248 }
249
250 // no backing store link yet
251 d = createLink(data);
252 if (d) {
253 network.links.push(d);
254 lu[d.key] = d;
255 updateLinks();
256 fStart();
257 }
258 }
259
260 function updateLink(data) {
261 var result = findLink(data, 'update'),
262 bad = result.badLogic;
263 if (bad) {
264 //logicError(bad + ': ' + link.id);
265 return;
266 }
267 result.updateWith(link);
268 }
269
270 function removeLink(data) {
271 var result = findLink(data, 'remove'),
272 bad = result.badLogic;
273 if (bad) {
274 // may have already removed link, if attached to removed device
275 //console.warn(bad + ': ' + link.id);
276 return;
277 }
278 result.removeRawLink();
279 }
280
281 // ========================
282
283 function addLinkUpdate(ldata, link) {
284 // add link event, but we already have the reverse link installed
285 ldata.fromTarget = link;
286 network.revLinkToKey[link.id] = ldata.key;
287 restyleLinkElement(ldata);
288 }
289
290 function createLink(link) {
291 var lnk = linkEndPoints(link.src, link.dst);
292
293 if (!lnk) {
294 return null;
295 }
296
297 angular.extend(lnk, {
298 key: link.id,
299 class: 'link',
300 fromSource: link,
301
302 // functions to aggregate dual link state
303 type: function () {
304 var s = lnk.fromSource,
305 t = lnk.fromTarget;
306 return (s && s.type) || (t && t.type) || defaultLinkType;
307 },
308 online: function () {
309 var s = lnk.fromSource,
310 t = lnk.fromTarget,
311 both = lnk.source.online && lnk.target.online;
312 return both && ((s && s.online) || (t && t.online));
313 },
314 linkWidth: function () {
315 var s = lnk.fromSource,
316 t = lnk.fromTarget,
317 ws = (s && s.linkWidth) || 0,
318 wt = (t && t.linkWidth) || 0;
319 return Math.max(ws, wt);
320 }
321 });
322 return lnk;
323 }
324
325
326 function makeNodeKey(d, what) {
327 var port = what + 'Port';
328 return d[what] + '/' + d[port];
329 }
330
331 function makeLinkKey(d, flipped) {
332 var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
333 two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
334 return one + '-' + two;
335 }
336
337 var widthRatio = 1.4,
338 linkScale = d3.scale.linear()
339 .domain([1, 12])
340 .range([widthRatio, 12 * widthRatio])
341 .clamp(true);
342
343 var allLinkTypes = 'direct indirect optical tunnel',
344 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
367 function findLink(linkData, op) {
368 var key = makeLinkKey(linkData),
369 keyrev = makeLinkKey(linkData, 1),
370 link = lu[key],
371 linkRev = lu[keyrev],
372 result = {},
373 ldata = link || linkRev,
374 rawLink;
375
376 if (op === 'add') {
377 if (link) {
378 // trying to add a link that we already know about
379 result.ldata = link;
380 result.badLogic = 'addLink: link already added';
381
382 } else if (linkRev) {
383 // we found the reverse of the link to be added
384 result.ldata = linkRev;
385 if (linkRev.fromTarget) {
386 result.badLogic = 'addLink: link already added';
387 }
388 }
389 } else if (op === 'update') {
390 if (!ldata) {
391 result.badLogic = 'updateLink: link not found';
392 } else {
393 rawLink = link ? ldata.fromSource : ldata.fromTarget;
394 result.updateWith = function (data) {
395 angular.extend(rawLink, data);
396 restyleLinkElement(ldata);
397 }
398 }
399 } else if (op === 'remove') {
400 if (!ldata) {
401 result.badLogic = 'removeLink: link not found';
402 } else {
403 rawLink = link ? ldata.fromSource : ldata.fromTarget;
404
405 if (!rawLink) {
406 result.badLogic = 'removeLink: link not found';
407
408 } else {
409 result.removeRawLink = function () {
410 if (link) {
411 // remove fromSource
412 ldata.fromSource = null;
413 if (ldata.fromTarget) {
414 // promote target into source position
415 ldata.fromSource = ldata.fromTarget;
416 ldata.fromTarget = null;
417 ldata.key = keyrev;
418 delete network.lookup[key];
419 network.lookup[keyrev] = ldata;
420 delete network.revLinkToKey[keyrev];
421 }
422 } else {
423 // remove fromTarget
424 ldata.fromTarget = null;
425 delete network.revLinkToKey[keyrev];
426 }
427 if (ldata.fromSource) {
428 restyleLinkElement(ldata);
429 } else {
430 removeLinkElement(ldata);
431 }
432 }
433 }
434 }
435 }
436 return result;
437 }
438
439
440 function findAttachedHosts(devId) {
441 var hosts = [];
442 network.nodes.forEach(function (d) {
443 if (d.class === 'host' && d.cp.device === devId) {
444 hosts.push(d);
445 }
446 });
447 return hosts;
448 }
449
450 function findAttachedLinks(devId) {
451 var links = [];
452 network.links.forEach(function (d) {
453 if (d.source.id === devId || d.target.id === devId) {
454 links.push(d);
455 }
456 });
457 return links;
458 }
459
460 function removeLinkElement(d) {
461 var idx = fs.find(d.key, network.links, 'key'),
462 removed;
463 if (idx >=0) {
464 // remove from links array
465 removed = network.links.splice(idx, 1);
466 // remove from lookup cache
467 delete lu[removed[0].key];
468 updateLinks();
469 fResume();
470 }
471 }
472
473 function removeHostElement(d, upd) {
474 // first, remove associated hostLink...
475 removeLinkElement(d.linkData);
476
477 // remove hostLink bindings
478 delete lu[d.ingress];
479 delete lu[d.egress];
480
481 // remove from lookup cache
482 delete lu[d.id];
483 // remove from nodes array
484 var idx = fs.find(d.id, network.nodes);
485 network.nodes.splice(idx, 1);
486
487 // remove from SVG
488 // NOTE: upd is false if we were called from removeDeviceElement()
489 if (upd) {
490 updateNodes();
491 fResume();
492 }
493 }
494
495 function removeDeviceElement(d) {
496 var id = d.id;
497 // first, remove associated hosts and links..
498 findAttachedHosts(id).forEach(removeHostElement);
499 findAttachedLinks(id).forEach(removeLinkElement);
500
501 // remove from lookup cache
502 delete lu[id];
503 // remove from nodes array
504 var idx = fs.find(id, network.nodes);
505 network.nodes.splice(idx, 1);
506
507 if (!network.nodes.length) {
508 xlink.showNoDevs(true);
509 }
510
511 // remove from SVG
512 updateNodes();
513 fResume();
514 }
515
516
Simon Huntac4c6f72015-02-03 19:50:53 -0800517 function sendUpdateMeta(d, store) {
518 var metaUi = {},
519 ll;
520
Simon Hunt1894d792015-02-04 17:09:20 -0800521 if (store) {
522 ll = lngLatFromCoord([d.x, d.y]);
523 metaUi = {
524 x: d.x,
525 y: d.y,
526 lng: ll[0],
527 lat: ll[1]
528 };
529 }
530 d.metaUi = metaUi;
531 uplink.sendEvent('updateMeta', {
532 id: d.id,
533 'class': d.class,
534 memento: metaUi
535 });
Simon Huntac4c6f72015-02-03 19:50:53 -0800536 }
537
538
Simon Huntac4c6f72015-02-03 19:50:53 -0800539 function fStart() {
540 $log.debug('TODO fStart()...');
541 // TODO...
542 }
543
544 function fResume() {
545 $log.debug('TODO fResume()...');
546 // TODO...
547 }
548
549 // ==========================
550 // === Devices and hosts - helper functions
551
552 function coordFromLngLat(loc) {
Simon Hunt1894d792015-02-04 17:09:20 -0800553 var p = uplink.projection();
554 return p ? p([loc.lng, loc.lat]) : [0, 0];
555 }
556
557 function lngLatFromCoord(coord) {
558 var p = uplink.projection();
559 return p ? p.invert([coord.x, coord.y]) : [0, 0];
Simon Huntac4c6f72015-02-03 19:50:53 -0800560 }
561
562 function positionNode(node, forUpdate) {
563 var meta = node.metaUi,
564 x = meta && meta.x,
565 y = meta && meta.y,
566 xy;
567
568 // If we have [x,y] already, use that...
569 if (x && y) {
570 node.fixed = true;
571 node.px = node.x = x;
572 node.py = node.y = y;
573 return;
574 }
575
576 var location = node.location,
577 coord;
578
579 if (location && location.type === 'latlng') {
580 coord = coordFromLngLat(location);
581 node.fixed = true;
582 node.px = node.x = coord[0];
583 node.py = node.y = coord[1];
584 return true;
585 }
586
587 // if this is a node update (not a node add).. skip randomizer
588 if (forUpdate) {
589 return;
590 }
591
592 // Note: Placing incoming unpinned nodes at exactly the same point
593 // (center of the view) causes them to explode outwards when
594 // the force layout kicks in. So, we spread them out a bit
595 // initially, to provide a more serene layout convergence.
596 // Additionally, if the node is a host, we place it near
597 // the device it is connected to.
598
599 function spread(s) {
600 return Math.floor((Math.random() * s) - s/2);
601 }
602
603 function randDim(dim) {
604 return dim / 2 + spread(dim * 0.7071);
605 }
606
607 function rand() {
608 return {
Simon Hunt1894d792015-02-04 17:09:20 -0800609 x: randDim(width),
610 y: randDim(height)
Simon Huntac4c6f72015-02-03 19:50:53 -0800611 };
612 }
613
614 function near(node) {
615 var min = 12,
616 dx = spread(12),
617 dy = spread(12);
618 return {
619 x: node.x + min + dx,
620 y: node.y + min + dy
621 };
622 }
623
624 function getDevice(cp) {
Simon Hunt1894d792015-02-04 17:09:20 -0800625 var d = lu[cp.device];
Simon Huntac4c6f72015-02-03 19:50:53 -0800626 return d || rand();
627 }
628
629 xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
630 angular.extend(node, xy);
631 }
632
633 function createDeviceNode(device) {
634 // start with the object as is
635 var node = device,
636 type = device.type,
637 svgCls = type ? 'node device ' + type : 'node device';
638
639 // Augment as needed...
640 node.class = 'device';
641 node.svgClass = device.online ? svgCls + ' online' : svgCls;
642 positionNode(node);
643 return node;
644 }
645
Simon Hunt1894d792015-02-04 17:09:20 -0800646 function createHostNode(host) {
647 var node = host;
648
649 // Augment as needed...
650 node.class = 'host';
651 if (!node.type) {
652 node.type = 'endstation';
653 }
654 node.svgClass = 'node host ' + node.type;
655 positionNode(node);
656 return node;
657 }
658
659 function createHostLink(host) {
660 var src = host.id,
661 dst = host.cp.device,
662 id = host.ingress,
663 lnk = linkEndPoints(src, dst);
664
665 if (!lnk) {
666 return null;
667 }
668
669 // Synthesize link ...
670 angular.extend(lnk, {
671 key: id,
672 class: 'link',
673
674 type: function () { return 'hostLink'; },
675 online: function () {
676 // hostlink target is edge switch
677 return lnk.target.online;
678 },
679 linkWidth: function () { return 1; }
680 });
681 return lnk;
682 }
683
684 function linkEndPoints(srcId, dstId) {
685 var srcNode = lu[srcId],
686 dstNode = lu[dstId],
687 sMiss = !srcNode ? missMsg('src', srcId) : '',
688 dMiss = !dstNode ? missMsg('dst', dstId) : '';
689
690 if (sMiss || dMiss) {
691 $log.error('Node(s) not on map for link:\n' + sMiss + dMiss);
692 //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
693 return null;
694 }
695 return {
696 source: srcNode,
697 target: dstNode,
698 x1: srcNode.x,
699 y1: srcNode.y,
700 x2: dstNode.x,
701 y2: dstNode.y
702 };
703 }
704
705 function missMsg(what, id) {
706 return '\n[' + what + '] "' + id + '" missing ';
707 }
708
Simon Huntac4c6f72015-02-03 19:50:53 -0800709 // ==========================
710 // === Devices and hosts - D3 rendering
711
Simon Hunt1894d792015-02-04 17:09:20 -0800712 function nodeMouseOver(m) {
713 // TODO
714 $log.debug("TODO nodeMouseOver()...", m);
715 }
716
717 function nodeMouseOut(m) {
718 // TODO
719 $log.debug("TODO nodeMouseOut()...", m);
720 }
721
722
Simon Huntac4c6f72015-02-03 19:50:53 -0800723 // Returns the newly computed bounding box of the rectangle
724 function adjustRectToFitText(n) {
725 var text = n.select('text'),
726 box = text.node().getBBox(),
727 lab = labelConfig;
728
729 text.attr('text-anchor', 'middle')
730 .attr('y', '-0.8em')
731 .attr('x', lab.imgPad/2);
732
733 // translate the bbox so that it is centered on [x,y]
734 box.x = -box.width / 2;
735 box.y = -box.height / 2;
736
737 // add padding
738 box.x -= (lab.padLR + lab.imgPad/2);
739 box.width += lab.padLR * 2 + lab.imgPad;
740 box.y -= lab.padTB;
741 box.height += lab.padTB * 2;
742
743 return box;
744 }
745
746 function mkSvgClass(d) {
747 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
748 }
749
750 function hostLabel(d) {
751 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
752 return d.labels[idx];
753 }
754 function deviceLabel(d) {
755 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
756 return d.labels[idx];
757 }
758 function trimLabel(label) {
759 return (label && label.trim()) || '';
760 }
761
762 function emptyBox() {
763 return {
764 x: -2,
765 y: -2,
766 width: 4,
767 height: 4
768 };
769 }
770
771
772 function updateDeviceLabel(d) {
773 var label = trimLabel(deviceLabel(d)),
774 noLabel = !label,
775 node = d.el,
Simon Hunt1894d792015-02-04 17:09:20 -0800776 dim = icfg.device.dim,
Simon Huntac4c6f72015-02-03 19:50:53 -0800777 devCfg = deviceIconConfig,
778 box, dx, dy;
779
780 node.select('text')
781 .text(label)
782 .style('opacity', 0)
783 .transition()
784 .style('opacity', 1);
785
786 if (noLabel) {
787 box = emptyBox();
788 dx = -dim/2;
789 dy = -dim/2;
790 } else {
791 box = adjustRectToFitText(node);
792 dx = box.x + devCfg.xoff;
793 dy = box.y + devCfg.yoff;
794 }
795
796 node.select('rect')
797 .transition()
798 .attr(box);
799
800 node.select('g.deviceIcon')
801 .transition()
802 .attr('transform', sus.translate(dx, dy));
803 }
804
805 function updateHostLabel(d) {
806 var label = trimLabel(hostLabel(d));
807 d.el.select('text').text(label);
808 }
809
Simon Huntac4c6f72015-02-03 19:50:53 -0800810 function updateDeviceColors(d) {
811 if (d) {
812 setDeviceColor(d);
813 } else {
814 node.filter('.device').each(function (d) {
815 setDeviceColor(d);
816 });
817 }
818 }
819
820 var dCol = {
821 black: '#000',
822 paleblue: '#acf',
823 offwhite: '#ddd',
Simon Hunt78c10bf2015-02-03 21:18:20 -0800824 darkgrey: '#444',
Simon Huntac4c6f72015-02-03 19:50:53 -0800825 midgrey: '#888',
826 lightgrey: '#bbb',
827 orange: '#f90'
828 };
829
830 // note: these are the device icon colors without affinity
831 var dColTheme = {
832 light: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800833 rfill: dCol.offwhite,
Simon Huntac4c6f72015-02-03 19:50:53 -0800834 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800835 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800836 rect: dCol.paleblue
837 },
838 offline: {
839 glyph: dCol.midgrey,
840 rect: dCol.lightgrey
841 }
842 },
Simon Huntac4c6f72015-02-03 19:50:53 -0800843 dark: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800844 rfill: dCol.midgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800845 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800846 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800847 rect: dCol.paleblue
848 },
849 offline: {
850 glyph: dCol.midgrey,
Simon Hunt78c10bf2015-02-03 21:18:20 -0800851 rect: dCol.darkgrey
Simon Huntac4c6f72015-02-03 19:50:53 -0800852 }
853 }
854 };
855
856 function devBaseColor(d) {
857 var o = d.online ? 'online' : 'offline';
858 return dColTheme[ts.theme()][o];
859 }
860
861 function setDeviceColor(d) {
862 var o = d.online,
863 s = d.el.classed('selected'),
864 c = devBaseColor(d),
865 a = instColor(d.master, o),
Simon Hunt51056592015-02-03 21:48:07 -0800866 icon = d.el.select('g.deviceIcon'),
867 g, r;
Simon Huntac4c6f72015-02-03 19:50:53 -0800868
869 if (s) {
870 g = c.glyph;
871 r = dCol.orange;
872 } else if (tis.isVisible()) {
873 g = o ? a : c.glyph;
Simon Hunt78c10bf2015-02-03 21:18:20 -0800874 r = o ? c.rfill : a;
Simon Huntac4c6f72015-02-03 19:50:53 -0800875 } else {
876 g = c.glyph;
877 r = c.rect;
878 }
879
Simon Hunt51056592015-02-03 21:48:07 -0800880 icon.select('use').style('fill', g);
881 icon.select('rect').style('fill', r);
Simon Huntac4c6f72015-02-03 19:50:53 -0800882 }
883
884 function instColor(id, online) {
885 return sus.cat7().getColor(id, !online, ts.theme());
886 }
887
Simon Hunt1894d792015-02-04 17:09:20 -0800888 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800889
890 function updateNodes() {
Simon Hunt1894d792015-02-04 17:09:20 -0800891 // select all the nodes in the layout:
Simon Huntac4c6f72015-02-03 19:50:53 -0800892 node = nodeG.selectAll('.node')
893 .data(network.nodes, function (d) { return d.id; });
894
Simon Hunt1894d792015-02-04 17:09:20 -0800895 // operate on existing nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800896 node.filter('.device').each(deviceExisting);
897 node.filter('.host').each(hostExisting);
Simon Huntac4c6f72015-02-03 19:50:53 -0800898
899 // operate on entering nodes:
900 var entering = node.enter()
901 .append('g')
902 .attr({
903 id: function (d) { return sus.safeId(d.id); },
904 class: mkSvgClass,
905 transform: function (d) { return sus.translate(d.x, d.y); },
906 opacity: 0
907 })
908 .call(drag)
909 .on('mouseover', nodeMouseOver)
910 .on('mouseout', nodeMouseOut)
911 .transition()
912 .attr('opacity', 1);
913
Simon Hunt1894d792015-02-04 17:09:20 -0800914 // augment entering nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800915 entering.filter('.device').each(deviceEnter);
916 entering.filter('.host').each(hostEnter);
Simon Huntac4c6f72015-02-03 19:50:53 -0800917
Simon Hunt51056592015-02-03 21:48:07 -0800918 // operate on both existing and new nodes:
Simon Huntac4c6f72015-02-03 19:50:53 -0800919 updateDeviceColors();
920
921 // operate on exiting nodes:
922 // Note that the node is removed after 2 seconds.
923 // Sub element animations should be shorter than 2 seconds.
924 var exiting = node.exit()
925 .transition()
926 .duration(2000)
927 .style('opacity', 0)
928 .remove();
929
Simon Hunt1894d792015-02-04 17:09:20 -0800930 // exiting node specifics:
Simon Hunt51056592015-02-03 21:48:07 -0800931 exiting.filter('.host').each(hostExit);
932 exiting.filter('.device').each(deviceExit);
Simon Huntac4c6f72015-02-03 19:50:53 -0800933
Simon Hunt51056592015-02-03 21:48:07 -0800934 // finally, resume the force layout
Simon Huntac4c6f72015-02-03 19:50:53 -0800935 fResume();
936 }
937
Simon Hunt51056592015-02-03 21:48:07 -0800938 // ==========================
939 // updateNodes - subfunctions
940
941 function deviceExisting(d) {
942 var node = d.el;
943 node.classed('online', d.online);
944 updateDeviceLabel(d);
945 positionNode(d, true);
946 }
947
948 function hostExisting(d) {
949 updateHostLabel(d);
950 positionNode(d, true);
951 }
952
953 function deviceEnter(d) {
954 var node = d3.select(this),
955 glyphId = d.type || 'unknown',
956 label = trimLabel(deviceLabel(d)),
957 devCfg = deviceIconConfig,
958 noLabel = !label,
959 box, dx, dy, icon;
960
961 d.el = node;
962
963 node.append('rect').attr({ rx: 5, ry: 5 });
964 node.append('text').text(label).attr('dy', '1.1em');
965 box = adjustRectToFitText(node);
966 node.select('rect').attr(box);
967
968 icon = is.addDeviceIcon(node, glyphId);
969
970 if (noLabel) {
971 dx = -icon.dim/2;
972 dy = -icon.dim/2;
973 } else {
974 box = adjustRectToFitText(node);
975 dx = box.x + devCfg.xoff;
976 dy = box.y + devCfg.yoff;
977 }
978
979 icon.attr('transform', sus.translate(dx, dy));
980 }
981
982 function hostEnter(d) {
Simon Hunt1894d792015-02-04 17:09:20 -0800983 var node = d3.select(this),
984 gid = d.type || 'unknown',
985 rad = icfg.host.radius,
986 r = d.type ? rad.withGlyph : rad.noGlyph,
987 textDy = r + 10;
Simon Hunt51056592015-02-03 21:48:07 -0800988
989 d.el = node;
Simon Hunt1894d792015-02-04 17:09:20 -0800990 sus.makeVisible(node, showHosts);
Simon Hunt51056592015-02-03 21:48:07 -0800991
Simon Hunt1894d792015-02-04 17:09:20 -0800992 is.addHostIcon(node, r, gid);
Simon Hunt51056592015-02-03 21:48:07 -0800993
Simon Hunt51056592015-02-03 21:48:07 -0800994 node.append('text')
995 .text(hostLabel)
Simon Hunt1894d792015-02-04 17:09:20 -0800996 .attr('dy', textDy)
Simon Hunt51056592015-02-03 21:48:07 -0800997 .attr('text-anchor', 'middle');
998 }
999
1000 function hostExit(d) {
1001 var node = d.el;
1002 node.select('use')
1003 .style('opacity', 0.5)
1004 .transition()
1005 .duration(800)
1006 .style('opacity', 0);
1007
1008 node.select('text')
1009 .style('opacity', 0.5)
1010 .transition()
1011 .duration(800)
1012 .style('opacity', 0);
1013
1014 node.select('circle')
1015 .style('stroke-fill', '#555')
1016 .style('fill', '#888')
1017 .style('opacity', 0.5)
1018 .transition()
1019 .duration(1500)
1020 .attr('r', 0);
1021 }
1022
1023 function deviceExit(d) {
1024 var node = d.el;
1025 node.select('use')
1026 .style('opacity', 0.5)
1027 .transition()
1028 .duration(800)
1029 .style('opacity', 0);
1030
1031 node.selectAll('rect')
1032 .style('stroke-fill', '#555')
1033 .style('fill', '#888')
1034 .style('opacity', 0.5);
1035 }
1036
Simon Hunt1894d792015-02-04 17:09:20 -08001037 // ==========================
1038
1039 function updateLinks() {
1040 var th = ts.theme();
1041
1042 link = linkG.selectAll('.link')
1043 .data(network.links, function (d) { return d.key; });
1044
1045 // operate on existing links:
1046 //link.each(linkExisting);
1047
1048 // operate on entering links:
1049 var entering = link.enter()
1050 .append('line')
1051 .attr({
1052 x1: function (d) { return d.x1; },
1053 y1: function (d) { return d.y1; },
1054 x2: function (d) { return d.x2; },
1055 y2: function (d) { return d.y2; },
1056 stroke: linkConfig[th].inColor,
1057 'stroke-width': linkConfig.inWidth
1058 });
1059
1060 // augment links
1061 entering.each(linkEntering);
1062
1063 // operate on both existing and new links:
1064 //link.each(...)
1065
1066 // apply or remove labels
1067 var labelData = getLabelData();
1068 applyLinkLabels(labelData);
1069
1070 // operate on exiting links:
1071 link.exit()
1072 .attr('stroke-dasharray', '3 3')
1073 .style('opacity', 0.5)
1074 .transition()
1075 .duration(1500)
1076 .attr({
1077 'stroke-dasharray': '3 12',
1078 stroke: linkConfig[th].outColor,
1079 'stroke-width': linkConfig.outWidth
1080 })
1081 .style('opacity', 0.0)
1082 .remove();
1083
1084 // NOTE: invoke a single tick to force the labels to position
1085 // onto their links.
1086 tick();
1087 // FIXME: this is a bug when in oblique view
1088 // It causes the nodes to jump into "overhead" view positions, even
1089 // though the oblique planes are still showing...
1090 }
1091
1092 // ==========================
1093 // updateLinks - subfunctions
1094
1095 function getLabelData() {
1096 // create the backing data for showing labels..
1097 var data = [];
1098 link.each(function (d) {
1099 if (d.label) {
1100 data.push({
1101 id: 'lab-' + d.key,
1102 key: d.key,
1103 label: d.label,
1104 ldata: d
1105 });
1106 }
1107 });
1108 return data;
1109 }
1110
1111 //function linkExisting(d) { }
1112
1113 function linkEntering(d) {
1114 var link = d3.select(this);
1115 d.el = link;
1116 restyleLinkElement(d);
1117 if (d.type() === 'hostLink') {
1118 sus.makeVisible(link, showHosts);
1119 }
1120 }
1121
1122 //function linkExiting(d) { }
1123
1124 var linkLabelOffset = '0.3em';
1125
1126 function applyLinkLabels(data) {
1127 var entering;
1128
1129 linkLabel = linkLabelG.selectAll('.linkLabel')
1130 .data(data, function (d) { return d.id; });
1131
1132 // for elements already existing, we need to update the text
1133 // and adjust the rectangle size to fit
1134 linkLabel.each(function (d) {
1135 var el = d3.select(this),
1136 rect = el.select('rect'),
1137 text = el.select('text');
1138 text.text(d.label);
1139 rect.attr(rectAroundText(el));
1140 });
1141
1142 entering = linkLabel.enter().append('g')
1143 .classed('linkLabel', true)
1144 .attr('id', function (d) { return d.id; });
1145
1146 entering.each(function (d) {
1147 var el = d3.select(this),
1148 rect,
1149 text,
1150 parms = {
1151 x1: d.ldata.x1,
1152 y1: d.ldata.y1,
1153 x2: d.ldata.x2,
1154 y2: d.ldata.y2
1155 };
1156
1157 d.el = el;
1158 rect = el.append('rect');
1159 text = el.append('text').text(d.label);
1160 rect.attr(rectAroundText(el));
1161 text.attr('dy', linkLabelOffset);
1162
1163 el.attr('transform', transformLabel(parms));
1164 });
1165
1166 // Remove any labels that are no longer required.
1167 linkLabel.exit().remove();
1168 }
1169
1170 function rectAroundText(el) {
1171 var text = el.select('text'),
1172 box = text.node().getBBox();
1173
1174 // translate the bbox so that it is centered on [x,y]
1175 box.x = -box.width / 2;
1176 box.y = -box.height / 2;
1177
1178 // add padding
1179 box.x -= 1;
1180 box.width += 2;
1181 return box;
1182 }
1183
1184 function transformLabel(p) {
1185 var dx = p.x2 - p.x1,
1186 dy = p.y2 - p.y1,
1187 xMid = dx/2 + p.x1,
1188 yMid = dy/2 + p.y1;
1189 return sus.translate(xMid, yMid);
1190 }
Simon Huntac4c6f72015-02-03 19:50:53 -08001191
1192 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -08001193 // force layout tick function
1194 function tick() {
1195
1196 }
1197
1198
Simon Huntac4c6f72015-02-03 19:50:53 -08001199 // ==========================
1200 // === MOUSE GESTURE HANDLERS
1201
Simon Hunt737c89f2015-01-28 12:23:19 -08001202 function selectCb() { }
1203 function atDragEnd() {}
1204 function dragEnabled() {}
1205 function clickEnabled() {}
1206
1207
1208 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -08001209 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -08001210
1211 angular.module('ovTopo')
1212 .factory('TopoForceService',
Simon Hunt1894d792015-02-04 17:09:20 -08001213 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
Simon Huntac4c6f72015-02-03 19:50:53 -08001214 'TopoInstService',
Simon Hunt737c89f2015-01-28 12:23:19 -08001215
Simon Hunt1894d792015-02-04 17:09:20 -08001216 function (_$log_, _fs_, _sus_, _is_, _ts_, _tis_) {
Simon Hunt737c89f2015-01-28 12:23:19 -08001217 $log = _$log_;
Simon Hunt1894d792015-02-04 17:09:20 -08001218 fs = _fs_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001219 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001220 is = _is_;
1221 ts = _ts_;
1222 tis = _tis_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001223
Simon Hunt1894d792015-02-04 17:09:20 -08001224 icfg = is.iconConfig();
1225
Simon Hunt737c89f2015-01-28 12:23:19 -08001226 // forceG is the SVG group to display the force layout in
Simon Huntac4c6f72015-02-03 19:50:53 -08001227 // xlink is the cross-link api from the main topo source file
Simon Hunt737c89f2015-01-28 12:23:19 -08001228 // w, h are the initial dimensions of the SVG
1229 // opts are, well, optional :)
Simon Hunt1894d792015-02-04 17:09:20 -08001230 function initForce(forceG, _uplink_, w, h, opts) {
Simon Hunta11b4eb2015-01-28 16:20:50 -08001231 $log.debug('initForce().. WxH = ' + w + 'x' + h);
Simon Hunt1894d792015-02-04 17:09:20 -08001232 uplink = _uplink_;
1233 width = w;
1234 height = h;
Simon Hunta11b4eb2015-01-28 16:20:50 -08001235
Simon Hunt737c89f2015-01-28 12:23:19 -08001236 settings = angular.extend({}, defaultSettings, opts);
1237
1238 linkG = forceG.append('g').attr('id', 'topo-links');
1239 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
1240 nodeG = forceG.append('g').attr('id', 'topo-nodes');
1241
1242 link = linkG.selectAll('.link');
1243 linkLabel = linkLabelG.selectAll('.linkLabel');
1244 node = nodeG.selectAll('.node');
1245
1246 force = d3.layout.force()
Simon Hunta11b4eb2015-01-28 16:20:50 -08001247 .size([w, h])
Simon Hunt737c89f2015-01-28 12:23:19 -08001248 .nodes(network.nodes)
1249 .links(network.links)
1250 .gravity(settings.gravity)
1251 .friction(settings.friction)
1252 .charge(settings.charge._def_)
1253 .linkDistance(settings.linkDistance._def_)
1254 .linkStrength(settings.linkStrength._def_)
1255 .on('tick', tick);
1256
1257 drag = sus.createDragBehavior(force,
1258 selectCb, atDragEnd, dragEnabled, clickEnabled);
1259 }
1260
Simon Huntb0ec1e52015-01-28 18:13:49 -08001261 function resize(dim) {
Simon Hunt1894d792015-02-04 17:09:20 -08001262 width = dim.width;
1263 height = dim.height;
1264 force.size([width, height]);
Simon Hunt737c89f2015-01-28 12:23:19 -08001265 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -08001266 }
1267
1268 return {
1269 initForce: initForce,
Simon Huntac4c6f72015-02-03 19:50:53 -08001270 resize: resize,
1271
1272 updateDeviceColors: updateDeviceColors,
1273
1274 addDevice: addDevice,
Simon Hunt1894d792015-02-04 17:09:20 -08001275 updateDevice: updateDevice,
1276 removeDevice: removeDevice,
1277 addHost: addHost,
1278 updateHost: updateHost,
1279 removeHost: removeHost,
1280 addLink: addLink,
1281 updateLink: updateLink,
1282 removeLink: removeLink
Simon Hunt737c89f2015-01-28 12:23:19 -08001283 };
1284 }]);
1285}());