blob: 168e9ce9985b30d138ac7b9a58eef61199b5452c [file] [log] [blame]
Simon Hunta4242de2015-02-24 17:11:55 -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 D3 Module.
19 Functions for manipulating the D3 visualizations of the Topology
20 */
21
22(function () {
23 'use strict';
24
25 // injected refs
26 var $log, fs, sus, is, ts;
27
28 // api to topoForce
29 var api;
30 /*
31 node() // get ref to D3 selection of nodes
32 link() // get ref to D3 selection of links
33 linkLabel() // get ref to D3 selection of link labels
34 instVisible() // true if instances panel is visible
35 posNode() // position node
36 showHosts() // true if hosts are to be shown
37 restyleLinkElement() // update link styles based on backing data
38 updateLinkLabelModel() // update backing data for link labels
39 */
40
41 // configuration
42 var devCfg = {
43 xoff: -20,
44 yoff: -18
45 },
46 labelConfig = {
47 imgPad: 16,
48 padLR: 4,
49 padTB: 3,
50 marginLR: 3,
51 marginTB: 2,
52 port: {
53 gap: 3,
54 width: 18,
55 height: 14
56 }
57 },
Simon Hunt5674db92015-10-22 16:12:48 -070058 badgeConfig = {
59 radius: 12,
Simon Hunt004fc2c2015-10-23 11:55:58 -070060 yoff: 5,
61 gdelta: 10
Simon Hunt5674db92015-10-22 16:12:48 -070062 },
Simon Hunta4242de2015-02-24 17:11:55 -080063 icfg;
64
Simon Hunt5674db92015-10-22 16:12:48 -070065 var status = {
66 i: 'badgeInfo',
67 w: 'badgeWarn',
68 e: 'badgeError'
69 };
70
71 function badgeStatus(badge) {
72 return status[badge.status] || status.i;
73 }
74
Simon Hunta4242de2015-02-24 17:11:55 -080075 // internal state
76 var deviceLabelIndex = 0,
77 hostLabelIndex = 0;
78
79
80 var dCol = {
81 black: '#000',
82 paleblue: '#acf',
83 offwhite: '#ddd',
84 darkgrey: '#444',
85 midgrey: '#888',
86 lightgrey: '#bbb',
87 orange: '#f90'
88 };
89
90 // note: these are the device icon colors without affinity
91 var dColTheme = {
92 light: {
93 rfill: dCol.offwhite,
94 online: {
95 glyph: dCol.darkgrey,
96 rect: dCol.paleblue
97 },
98 offline: {
99 glyph: dCol.midgrey,
100 rect: dCol.lightgrey
101 }
102 },
103 dark: {
104 rfill: dCol.midgrey,
105 online: {
106 glyph: dCol.darkgrey,
107 rect: dCol.paleblue
108 },
109 offline: {
110 glyph: dCol.midgrey,
111 rect: dCol.darkgrey
112 }
113 }
114 };
115
116 function devBaseColor(d) {
117 var o = d.online ? 'online' : 'offline';
118 return dColTheme[ts.theme()][o];
119 }
120
121 function setDeviceColor(d) {
122 var o = d.online,
123 s = d.el.classed('selected'),
124 c = devBaseColor(d),
125 a = instColor(d.master, o),
126 icon = d.el.select('g.deviceIcon'),
127 g, r;
128
129 if (s) {
130 g = c.glyph;
131 r = dCol.orange;
132 } else if (api.instVisible()) {
133 g = o ? a : c.glyph;
134 r = o ? c.rfill : a;
135 } else {
136 g = c.glyph;
137 r = c.rect;
138 }
139
140 icon.select('use').style('fill', g);
141 icon.select('rect').style('fill', r);
142 }
143
144 function instColor(id, online) {
145 return sus.cat7().getColor(id, !online, ts.theme());
146 }
147
148 // ====
149
150 function incDevLabIndex() {
151 deviceLabelIndex = (deviceLabelIndex+1) % 3;
Bri Prebilic Cole9cf1a8d2015-04-21 13:15:29 -0700152 switch(deviceLabelIndex) {
153 case 0: return 'Hide device labels';
154 case 1: return 'Show friendly device labels';
155 case 2: return 'Show device ID labels';
156 }
Simon Hunta4242de2015-02-24 17:11:55 -0800157 }
158
159 // Returns the newly computed bounding box of the rectangle
160 function adjustRectToFitText(n) {
161 var text = n.select('text'),
162 box = text.node().getBBox(),
163 lab = labelConfig;
164
165 text.attr('text-anchor', 'middle')
166 .attr('y', '-0.8em')
167 .attr('x', lab.imgPad/2);
168
169 // translate the bbox so that it is centered on [x,y]
170 box.x = -box.width / 2;
171 box.y = -box.height / 2;
172
173 // add padding
174 box.x -= (lab.padLR + lab.imgPad/2);
175 box.width += lab.padLR * 2 + lab.imgPad;
176 box.y -= lab.padTB;
177 box.height += lab.padTB * 2;
178
179 return box;
180 }
181
182 function hostLabel(d) {
183 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
184 return d.labels[idx];
185 }
186 function deviceLabel(d) {
187 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
188 return d.labels[idx];
189 }
190 function trimLabel(label) {
191 return (label && label.trim()) || '';
192 }
193
194 function emptyBox() {
195 return {
196 x: -2,
197 y: -2,
198 width: 4,
199 height: 4
200 };
201 }
202
203
Simon Hunt5674db92015-10-22 16:12:48 -0700204 function updateDeviceRendering(d) {
Simon Hunta4242de2015-02-24 17:11:55 -0800205 var label = trimLabel(deviceLabel(d)),
206 noLabel = !label,
207 node = d.el,
208 dim = icfg.device.dim,
Simon Hunt5674db92015-10-22 16:12:48 -0700209 box, dx, dy, bsel,
210 bdg = d.badge,
Simon Hunt004fc2c2015-10-23 11:55:58 -0700211 bcr = badgeConfig.radius,
212 bcgd = badgeConfig.gdelta;
Simon Hunta4242de2015-02-24 17:11:55 -0800213
214 node.select('text')
215 .text(label)
216 .style('opacity', 0)
217 .transition()
218 .style('opacity', 1);
219
220 if (noLabel) {
221 box = emptyBox();
222 dx = -dim/2;
223 dy = -dim/2;
224 } else {
225 box = adjustRectToFitText(node);
226 dx = box.x + devCfg.xoff;
227 dy = box.y + devCfg.yoff;
228 }
229
230 node.select('rect')
231 .transition()
232 .attr(box);
233
234 node.select('g.deviceIcon')
235 .transition()
236 .attr('transform', sus.translate(dx, dy));
Simon Hunta4242de2015-02-24 17:11:55 -0800237
Simon Hunt5674db92015-10-22 16:12:48 -0700238 // handle badge, if defined
239 if (bdg) {
Simon Hunt004fc2c2015-10-23 11:55:58 -0700240 node.select('g.badge').remove();
241
Simon Hunte9343f32015-10-21 18:07:46 -0700242 bsel = node.append('g')
243 .classed('badge', true)
Simon Hunt5674db92015-10-22 16:12:48 -0700244 .classed(badgeStatus(bdg), true)
245 .attr('transform', sus.translate(dx + dim, dy));
Simon Hunte9343f32015-10-21 18:07:46 -0700246
247 bsel.append('circle')
Simon Hunt5674db92015-10-22 16:12:48 -0700248 .attr('r', bcr);
249
250 if (bdg.txt) {
251 bsel.append('text')
252 .attr('dy', badgeConfig.yoff)
253 .attr('text-anchor', 'middle')
254 .text(bdg.txt);
255 } else if (bdg.gid) {
256 bsel.append('use')
257 .attr({
Simon Hunt004fc2c2015-10-23 11:55:58 -0700258 width: bcgd * 2,
259 height: bcgd * 2,
260 transform: sus.translate(-bcgd, -bcgd),
Simon Hunt5674db92015-10-22 16:12:48 -0700261 'xlink:href': '#' + bdg.gid
262 });
263
264 }
Simon Hunte9343f32015-10-21 18:07:46 -0700265 }
266 }
267
Andrea Campanella52125412015-12-03 14:50:40 -0800268 function updateHostRendering(d) {
269 var node = d.el,
270 dim = icfg.host.radius.withGlyph,
271 box, dx, dy, bsel,
272 bdg = d.badge,
273 bcr = badgeConfig.radius,
274 bcgd = badgeConfig.gdelta;
275
276
277 updateHostLabel(d);
278
279 // TODO: fine-tune dx, dy for badge placement relative to host Circle.
280 dx = -dim/2;
281 dy = -dim/2;
282
283 // handle badge, if defined
284 if (bdg) {
285 node.select('g.badge').remove();
286
287 bsel = node.append('g')
288 .classed('badge', true)
289 .classed(badgeStatus(bdg), true)
290 .attr('transform', sus.translate(dx + dim, dy));
291
292 bsel.append('circle')
293 .attr('r', bcr);
294
295 if (bdg.txt) {
296 bsel.append('text')
297 .attr('dy', badgeConfig.yoff)
298 .attr('text-anchor', 'middle')
299 .text(bdg.txt);
300 } else if (bdg.gid) {
301 bsel.append('use')
302 .attr({
303 width: bcgd * 2,
304 height: bcgd * 2,
305 transform: sus.translate(-bcgd, -bcgd),
306 'xlink:href': '#' + bdg.gid
307 });
308
309 }
310 }
311 }
312
Simon Hunta4242de2015-02-24 17:11:55 -0800313 function updateHostLabel(d) {
314 var label = trimLabel(hostLabel(d));
315 d.el.select('text').text(label);
316 }
317
318 function updateDeviceColors(d) {
319 if (d) {
320 setDeviceColor(d);
321 } else {
322 api.node().filter('.device').each(function (d) {
323 setDeviceColor(d);
324 });
325 }
326 }
327
328
329 // ==========================
330 // updateNodes - subfunctions
331
332 function deviceExisting(d) {
333 var node = d.el;
334 node.classed('online', d.online);
Simon Hunt5674db92015-10-22 16:12:48 -0700335 updateDeviceRendering(d);
Simon Hunta4242de2015-02-24 17:11:55 -0800336 api.posNode(d, true);
337 }
338
339 function hostExisting(d) {
Andrea Campanella52125412015-12-03 14:50:40 -0800340 updateHostRendering(d);
Simon Hunta4242de2015-02-24 17:11:55 -0800341 api.posNode(d, true);
342 }
343
344 function deviceEnter(d) {
345 var node = d3.select(this),
346 glyphId = d.type || 'unknown',
347 label = trimLabel(deviceLabel(d)),
348 //devCfg = deviceIconConfig,
349 noLabel = !label,
350 box, dx, dy, icon;
351
352 d.el = node;
353
354 node.append('rect').attr({ rx: 5, ry: 5 });
355 node.append('text').text(label).attr('dy', '1.1em');
356 box = adjustRectToFitText(node);
357 node.select('rect').attr(box);
358
359 icon = is.addDeviceIcon(node, glyphId);
360
361 if (noLabel) {
362 dx = -icon.dim/2;
363 dy = -icon.dim/2;
364 } else {
365 box = adjustRectToFitText(node);
366 dx = box.x + devCfg.xoff;
367 dy = box.y + devCfg.yoff;
368 }
369
370 icon.attr('transform', sus.translate(dx, dy));
371 }
372
373 function hostEnter(d) {
374 var node = d3.select(this),
375 gid = d.type || 'unknown',
376 rad = icfg.host.radius,
377 r = d.type ? rad.withGlyph : rad.noGlyph,
378 textDy = r + 10;
379
380 d.el = node;
381 sus.visible(node, api.showHosts());
382
383 is.addHostIcon(node, r, gid);
384
385 node.append('text')
386 .text(hostLabel)
387 .attr('dy', textDy)
388 .attr('text-anchor', 'middle');
389 }
390
391 function hostExit(d) {
392 var node = d.el;
393 node.select('use')
394 .style('opacity', 0.5)
395 .transition()
396 .duration(800)
397 .style('opacity', 0);
398
399 node.select('text')
400 .style('opacity', 0.5)
401 .transition()
402 .duration(800)
403 .style('opacity', 0);
404
405 node.select('circle')
406 .style('stroke-fill', '#555')
407 .style('fill', '#888')
408 .style('opacity', 0.5)
409 .transition()
410 .duration(1500)
411 .attr('r', 0);
412 }
413
414 function deviceExit(d) {
415 var node = d.el;
416 node.select('use')
417 .style('opacity', 0.5)
418 .transition()
419 .duration(800)
420 .style('opacity', 0);
421
422 node.selectAll('rect')
423 .style('stroke-fill', '#555')
424 .style('fill', '#888')
425 .style('opacity', 0.5);
426 }
427
428
429 // ==========================
430 // updateLinks - subfunctions
431
Simon Hunta4242de2015-02-24 17:11:55 -0800432 function linkEntering(d) {
433 var link = d3.select(this);
434 d.el = link;
435 api.restyleLinkElement(d);
436 if (d.type() === 'hostLink') {
437 sus.visible(link, api.showHosts());
438 }
439 }
440
441 var linkLabelOffset = '0.3em';
442
443 function applyLinkLabels() {
444 var entering;
445
446 api.updateLinkLabelModel();
447
448 // for elements already existing, we need to update the text
449 // and adjust the rectangle size to fit
450 api.linkLabel().each(function (d) {
451 var el = d3.select(this),
452 rect = el.select('rect'),
453 text = el.select('text');
454 text.text(d.label);
455 rect.attr(rectAroundText(el));
456 });
457
458 entering = api.linkLabel().enter().append('g')
459 .classed('linkLabel', true)
460 .attr('id', function (d) { return d.id; });
461
462 entering.each(function (d) {
463 var el = d3.select(this),
464 rect,
Bri Prebilic Cole038aedd2015-07-13 15:25:16 -0700465 text;
Simon Hunta4242de2015-02-24 17:11:55 -0800466
467 if (d.ldata.type() === 'hostLink') {
468 el.classed('hostLinkLabel', true);
469 sus.visible(el, api.showHosts());
470 }
471
472 d.el = el;
473 rect = el.append('rect');
474 text = el.append('text').text(d.label);
475 rect.attr(rectAroundText(el));
476 text.attr('dy', linkLabelOffset);
477
Bri Prebilic Cole038aedd2015-07-13 15:25:16 -0700478 el.attr('transform', transformLabel(d.ldata.position));
Simon Hunta4242de2015-02-24 17:11:55 -0800479 });
480
481 // Remove any labels that are no longer required.
482 api.linkLabel().exit().remove();
483 }
484
485 function rectAroundText(el) {
486 var text = el.select('text'),
487 box = text.node().getBBox();
488
489 // translate the bbox so that it is centered on [x,y]
490 box.x = -box.width / 2;
491 box.y = -box.height / 2;
492
493 // add padding
494 box.x -= 1;
495 box.width += 2;
496 return box;
497 }
498
499 function transformLabel(p) {
500 var dx = p.x2 - p.x1,
501 dy = p.y2 - p.y1,
502 xMid = dx/2 + p.x1,
503 yMid = dy/2 + p.y1;
504 return sus.translate(xMid, yMid);
505 }
506
Simon Hunt1a5301e2015-02-25 15:31:25 -0800507 function applyPortLabels(data, portLabelG) {
508 var entering = portLabelG.selectAll('.portLabel')
509 .data(data).enter().append('g')
510 .classed('portLabel', true)
511 .attr('id', function (d) { return d.id; });
512
513 entering.each(function (d) {
514 var el = d3.select(this),
515 rect = el.append('rect'),
516 text = el.append('text').text(d.num);
517
518 rect.attr(rectAroundText(el));
519 text.attr('dy', linkLabelOffset);
Simon Hunt969b3c92015-02-25 18:11:31 -0800520 el.attr('transform', sus.translate(d.x, d.y));
Simon Hunt1a5301e2015-02-25 15:31:25 -0800521 });
522 }
523
Bri Prebilic Cole80401762015-07-16 11:36:18 -0700524 function labelPoint(linkPos) {
525 var lengthUpLine = 1 / 3,
526 dx = linkPos.x2 - linkPos.x1,
527 dy = linkPos.y2 - linkPos.y1,
528 movedX = dx * lengthUpLine,
529 movedY = dy * lengthUpLine;
530
531 return {
532 x: movedX,
533 y: movedY
534 };
535 }
536
537 function calcGroupPos(linkPos) {
538 var moved = labelPoint(linkPos);
539 return sus.translate(linkPos.x1 + moved.x, linkPos.y1 + moved.y);
540 }
541
542 // calculates where on the link that the hash line for 5+ label appears
543 function hashAttrs(linkPos) {
544 var hashLength = 25,
545 halfLength = hashLength / 2,
546 dx = linkPos.x2 - linkPos.x1,
547 dy = linkPos.y2 - linkPos.y1,
548 length = Math.sqrt((dx * dx) + (dy * dy)),
549 moveAmtX = (dx / length) * halfLength,
550 moveAmtY = (dy / length) * halfLength,
551 mid = labelPoint(linkPos),
552 angle = Math.atan(dy / dx) + 45;
553
554 return {
555 x1: mid.x - moveAmtX,
556 y1: mid.y - moveAmtY,
557 x2: mid.x + moveAmtX,
558 y2: mid.y + moveAmtY,
559 stroke: api.linkConfig()[ts.theme()].baseColor,
560 transform: 'rotate(' + angle + ',' + mid.x + ',' + mid.y + ')'
561 };
562 }
563
564 function textLabelPos(linkPos) {
565 var point = labelPoint(linkPos),
566 dist = 20;
567 return {
568 x: point.x + dist,
569 y: point.y + dist
570 };
571 }
572
573 function applyNumLinkLabels(data, lblsG) {
574 var labels = lblsG.selectAll('g.numLinkLabel')
575 .data(data, function (d) { return 'pair-' + d.id; }),
576 entering;
577
578 // update existing labels
579 labels.each(function (d) {
580 var el = d3.select(this);
581
582 el.attr({
583 transform: function (d) { return calcGroupPos(d.linkCoords); }
584 });
585 el.select('line')
586 .attr(hashAttrs(d.linkCoords));
587 el.select('text')
588 .attr(textLabelPos(d.linkCoords))
589 .text(d.num);
590 });
591
592 // add new labels
593 entering = labels
594 .enter()
595 .append('g')
596 .attr({
597 transform: function (d) { return calcGroupPos(d.linkCoords); },
598 id: function (d) { return 'pair-' + d.id; }
599 })
600 .classed('numLinkLabel', true);
601
602 entering.each(function (d) {
603 var el = d3.select(this);
604
605 el.append('line')
606 .classed('numLinkHash', true)
607 .attr(hashAttrs(d.linkCoords));
608 el.append('text')
609 .classed('numLinkText', true)
610 .attr(textLabelPos(d.linkCoords))
611 .text(d.num);
612 });
613
614 // remove old labels
615 labels.exit().remove();
616 }
617
Simon Hunta4242de2015-02-24 17:11:55 -0800618 // ==========================
619 // Module definition
620
621 angular.module('ovTopo')
622 .factory('TopoD3Service',
623 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
624
625 function (_$log_, _fs_, _sus_, _is_, _ts_) {
626 $log = _$log_;
627 fs = _fs_;
628 sus = _sus_;
629 is = _is_;
630 ts = _ts_;
631
632 icfg = is.iconConfig();
633
634 function initD3(_api_) {
635 api = _api_;
636 }
637
638 function destroyD3() { }
639
640 return {
641 initD3: initD3,
642 destroyD3: destroyD3,
643
644 incDevLabIndex: incDevLabIndex,
645 adjustRectToFitText: adjustRectToFitText,
646 hostLabel: hostLabel,
647 deviceLabel: deviceLabel,
648 trimLabel: trimLabel,
649
Simon Hunt5674db92015-10-22 16:12:48 -0700650 updateDeviceLabel: updateDeviceRendering,
Simon Hunta4242de2015-02-24 17:11:55 -0800651 updateHostLabel: updateHostLabel,
652 updateDeviceColors: updateDeviceColors,
653
654 deviceExisting: deviceExisting,
655 hostExisting: hostExisting,
656 deviceEnter: deviceEnter,
657 hostEnter: hostEnter,
658 hostExit: hostExit,
659 deviceExit: deviceExit,
660
Simon Hunta4242de2015-02-24 17:11:55 -0800661 linkEntering: linkEntering,
662 applyLinkLabels: applyLinkLabels,
Simon Hunt1a5301e2015-02-25 15:31:25 -0800663 transformLabel: transformLabel,
Bri Prebilic Cole80401762015-07-16 11:36:18 -0700664 applyPortLabels: applyPortLabels,
665 applyNumLinkLabels: applyNumLinkLabels
Simon Hunta4242de2015-02-24 17:11:55 -0800666 };
667 }]);
668}());