blob: 5bf2eb4fa43779c9ee78a8c8669b0a6eefda2e08 [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
Simon Hunt5674db92015-10-22 16:12:48 -0700203 function updateDeviceRendering(d) {
Simon Hunta4242de2015-02-24 17:11:55 -0800204 var label = trimLabel(deviceLabel(d)),
205 noLabel = !label,
206 node = d.el,
207 dim = icfg.device.dim,
Simon Hunt5674db92015-10-22 16:12:48 -0700208 box, dx, dy, bsel,
209 bdg = d.badge,
Simon Hunt004fc2c2015-10-23 11:55:58 -0700210 bcr = badgeConfig.radius,
211 bcgd = badgeConfig.gdelta;
Simon Hunta4242de2015-02-24 17:11:55 -0800212
213 node.select('text')
214 .text(label)
215 .style('opacity', 0)
216 .transition()
217 .style('opacity', 1);
218
219 if (noLabel) {
220 box = emptyBox();
221 dx = -dim/2;
222 dy = -dim/2;
223 } else {
224 box = adjustRectToFitText(node);
225 dx = box.x + devCfg.xoff;
226 dy = box.y + devCfg.yoff;
227 }
228
229 node.select('rect')
230 .transition()
231 .attr(box);
232
233 node.select('g.deviceIcon')
234 .transition()
235 .attr('transform', sus.translate(dx, dy));
Simon Hunta4242de2015-02-24 17:11:55 -0800236
Simon Hunt5674db92015-10-22 16:12:48 -0700237 // handle badge, if defined
238 if (bdg) {
Simon Huntc2bfe332015-12-04 11:01:24 -0800239 renderBadge(node, bdg, { dx: dx + dim, dy: dy });
Simon Hunte9343f32015-10-21 18:07:46 -0700240 }
241 }
242
Andrea Campanella52125412015-12-03 14:50:40 -0800243 function updateHostRendering(d) {
244 var node = d.el,
Simon Huntc2bfe332015-12-04 11:01:24 -0800245 bdg = d.badge;
Andrea Campanella52125412015-12-03 14:50:40 -0800246
247 updateHostLabel(d);
Andrea Campanella52125412015-12-03 14:50:40 -0800248
249 // handle badge, if defined
250 if (bdg) {
Simon Huntc2bfe332015-12-04 11:01:24 -0800251 renderBadge(node, bdg, icfg.host.badge);
252 }
253 }
Andrea Campanella52125412015-12-03 14:50:40 -0800254
Simon Huntc2bfe332015-12-04 11:01:24 -0800255 function renderBadge(node, bdg, boff) {
256 var bsel,
257 bcr = badgeConfig.radius,
258 bcgd = badgeConfig.gdelta;
Andrea Campanella52125412015-12-03 14:50:40 -0800259
Simon Huntc2bfe332015-12-04 11:01:24 -0800260 node.select('g.badge').remove();
Andrea Campanella52125412015-12-03 14:50:40 -0800261
Simon Huntc2bfe332015-12-04 11:01:24 -0800262 bsel = node.append('g')
263 .classed('badge', true)
264 .classed(badgeStatus(bdg), true)
265 .attr('transform', sus.translate(boff.dx, boff.dy));
Andrea Campanella52125412015-12-03 14:50:40 -0800266
Simon Huntc2bfe332015-12-04 11:01:24 -0800267 bsel.append('circle')
268 .attr('r', bcr);
269
270 if (bdg.txt) {
271 bsel.append('text')
272 .attr('dy', badgeConfig.yoff)
273 .attr('text-anchor', 'middle')
274 .text(bdg.txt);
275 } else if (bdg.gid) {
276 bsel.append('use')
277 .attr({
278 width: bcgd * 2,
279 height: bcgd * 2,
280 transform: sus.translate(-bcgd, -bcgd),
281 'xlink:href': '#' + bdg.gid
282 });
Andrea Campanella52125412015-12-03 14:50:40 -0800283 }
284 }
285
Simon Hunta4242de2015-02-24 17:11:55 -0800286 function updateHostLabel(d) {
287 var label = trimLabel(hostLabel(d));
288 d.el.select('text').text(label);
289 }
290
291 function updateDeviceColors(d) {
292 if (d) {
293 setDeviceColor(d);
294 } else {
295 api.node().filter('.device').each(function (d) {
296 setDeviceColor(d);
297 });
298 }
299 }
300
301
302 // ==========================
303 // updateNodes - subfunctions
304
305 function deviceExisting(d) {
306 var node = d.el;
307 node.classed('online', d.online);
Simon Hunt5674db92015-10-22 16:12:48 -0700308 updateDeviceRendering(d);
Simon Hunta4242de2015-02-24 17:11:55 -0800309 api.posNode(d, true);
310 }
311
312 function hostExisting(d) {
Andrea Campanella52125412015-12-03 14:50:40 -0800313 updateHostRendering(d);
Simon Hunta4242de2015-02-24 17:11:55 -0800314 api.posNode(d, true);
315 }
316
317 function deviceEnter(d) {
318 var node = d3.select(this),
319 glyphId = d.type || 'unknown',
320 label = trimLabel(deviceLabel(d)),
321 //devCfg = deviceIconConfig,
322 noLabel = !label,
323 box, dx, dy, icon;
324
325 d.el = node;
326
327 node.append('rect').attr({ rx: 5, ry: 5 });
328 node.append('text').text(label).attr('dy', '1.1em');
329 box = adjustRectToFitText(node);
330 node.select('rect').attr(box);
331
332 icon = is.addDeviceIcon(node, glyphId);
333
334 if (noLabel) {
335 dx = -icon.dim/2;
336 dy = -icon.dim/2;
337 } else {
338 box = adjustRectToFitText(node);
339 dx = box.x + devCfg.xoff;
340 dy = box.y + devCfg.yoff;
341 }
342
343 icon.attr('transform', sus.translate(dx, dy));
344 }
345
346 function hostEnter(d) {
347 var node = d3.select(this),
348 gid = d.type || 'unknown',
349 rad = icfg.host.radius,
350 r = d.type ? rad.withGlyph : rad.noGlyph,
351 textDy = r + 10;
352
353 d.el = node;
354 sus.visible(node, api.showHosts());
355
356 is.addHostIcon(node, r, gid);
357
358 node.append('text')
359 .text(hostLabel)
360 .attr('dy', textDy)
361 .attr('text-anchor', 'middle');
362 }
363
364 function hostExit(d) {
365 var node = d.el;
366 node.select('use')
367 .style('opacity', 0.5)
368 .transition()
369 .duration(800)
370 .style('opacity', 0);
371
372 node.select('text')
373 .style('opacity', 0.5)
374 .transition()
375 .duration(800)
376 .style('opacity', 0);
377
378 node.select('circle')
379 .style('stroke-fill', '#555')
380 .style('fill', '#888')
381 .style('opacity', 0.5)
382 .transition()
383 .duration(1500)
384 .attr('r', 0);
385 }
386
387 function deviceExit(d) {
388 var node = d.el;
389 node.select('use')
390 .style('opacity', 0.5)
391 .transition()
392 .duration(800)
393 .style('opacity', 0);
394
395 node.selectAll('rect')
396 .style('stroke-fill', '#555')
397 .style('fill', '#888')
398 .style('opacity', 0.5);
399 }
400
401
402 // ==========================
403 // updateLinks - subfunctions
404
Simon Hunta4242de2015-02-24 17:11:55 -0800405 function linkEntering(d) {
406 var link = d3.select(this);
407 d.el = link;
408 api.restyleLinkElement(d);
409 if (d.type() === 'hostLink') {
410 sus.visible(link, api.showHosts());
411 }
412 }
413
414 var linkLabelOffset = '0.3em';
415
416 function applyLinkLabels() {
417 var entering;
418
419 api.updateLinkLabelModel();
420
421 // for elements already existing, we need to update the text
422 // and adjust the rectangle size to fit
423 api.linkLabel().each(function (d) {
424 var el = d3.select(this),
425 rect = el.select('rect'),
426 text = el.select('text');
427 text.text(d.label);
428 rect.attr(rectAroundText(el));
429 });
430
431 entering = api.linkLabel().enter().append('g')
432 .classed('linkLabel', true)
433 .attr('id', function (d) { return d.id; });
434
435 entering.each(function (d) {
436 var el = d3.select(this),
437 rect,
Bri Prebilic Cole038aedd2015-07-13 15:25:16 -0700438 text;
Simon Hunta4242de2015-02-24 17:11:55 -0800439
440 if (d.ldata.type() === 'hostLink') {
441 el.classed('hostLinkLabel', true);
442 sus.visible(el, api.showHosts());
443 }
444
445 d.el = el;
446 rect = el.append('rect');
447 text = el.append('text').text(d.label);
448 rect.attr(rectAroundText(el));
449 text.attr('dy', linkLabelOffset);
450
Bri Prebilic Cole038aedd2015-07-13 15:25:16 -0700451 el.attr('transform', transformLabel(d.ldata.position));
Simon Hunta4242de2015-02-24 17:11:55 -0800452 });
453
454 // Remove any labels that are no longer required.
455 api.linkLabel().exit().remove();
456 }
457
458 function rectAroundText(el) {
459 var text = el.select('text'),
460 box = text.node().getBBox();
461
462 // translate the bbox so that it is centered on [x,y]
463 box.x = -box.width / 2;
464 box.y = -box.height / 2;
465
466 // add padding
467 box.x -= 1;
468 box.width += 2;
469 return box;
470 }
471
472 function transformLabel(p) {
473 var dx = p.x2 - p.x1,
474 dy = p.y2 - p.y1,
475 xMid = dx/2 + p.x1,
476 yMid = dy/2 + p.y1;
477 return sus.translate(xMid, yMid);
478 }
479
Simon Hunt1a5301e2015-02-25 15:31:25 -0800480 function applyPortLabels(data, portLabelG) {
481 var entering = portLabelG.selectAll('.portLabel')
482 .data(data).enter().append('g')
483 .classed('portLabel', true)
484 .attr('id', function (d) { return d.id; });
485
486 entering.each(function (d) {
487 var el = d3.select(this),
488 rect = el.append('rect'),
489 text = el.append('text').text(d.num);
490
491 rect.attr(rectAroundText(el));
492 text.attr('dy', linkLabelOffset);
Simon Hunt969b3c92015-02-25 18:11:31 -0800493 el.attr('transform', sus.translate(d.x, d.y));
Simon Hunt1a5301e2015-02-25 15:31:25 -0800494 });
495 }
496
Bri Prebilic Cole80401762015-07-16 11:36:18 -0700497 function labelPoint(linkPos) {
498 var lengthUpLine = 1 / 3,
499 dx = linkPos.x2 - linkPos.x1,
500 dy = linkPos.y2 - linkPos.y1,
501 movedX = dx * lengthUpLine,
502 movedY = dy * lengthUpLine;
503
504 return {
505 x: movedX,
506 y: movedY
507 };
508 }
509
510 function calcGroupPos(linkPos) {
511 var moved = labelPoint(linkPos);
512 return sus.translate(linkPos.x1 + moved.x, linkPos.y1 + moved.y);
513 }
514
515 // calculates where on the link that the hash line for 5+ label appears
516 function hashAttrs(linkPos) {
517 var hashLength = 25,
518 halfLength = hashLength / 2,
519 dx = linkPos.x2 - linkPos.x1,
520 dy = linkPos.y2 - linkPos.y1,
521 length = Math.sqrt((dx * dx) + (dy * dy)),
522 moveAmtX = (dx / length) * halfLength,
523 moveAmtY = (dy / length) * halfLength,
524 mid = labelPoint(linkPos),
525 angle = Math.atan(dy / dx) + 45;
526
527 return {
528 x1: mid.x - moveAmtX,
529 y1: mid.y - moveAmtY,
530 x2: mid.x + moveAmtX,
531 y2: mid.y + moveAmtY,
532 stroke: api.linkConfig()[ts.theme()].baseColor,
533 transform: 'rotate(' + angle + ',' + mid.x + ',' + mid.y + ')'
534 };
535 }
536
537 function textLabelPos(linkPos) {
538 var point = labelPoint(linkPos),
539 dist = 20;
540 return {
541 x: point.x + dist,
542 y: point.y + dist
543 };
544 }
545
546 function applyNumLinkLabels(data, lblsG) {
547 var labels = lblsG.selectAll('g.numLinkLabel')
548 .data(data, function (d) { return 'pair-' + d.id; }),
549 entering;
550
551 // update existing labels
552 labels.each(function (d) {
553 var el = d3.select(this);
554
555 el.attr({
556 transform: function (d) { return calcGroupPos(d.linkCoords); }
557 });
558 el.select('line')
559 .attr(hashAttrs(d.linkCoords));
560 el.select('text')
561 .attr(textLabelPos(d.linkCoords))
562 .text(d.num);
563 });
564
565 // add new labels
566 entering = labels
567 .enter()
568 .append('g')
569 .attr({
570 transform: function (d) { return calcGroupPos(d.linkCoords); },
571 id: function (d) { return 'pair-' + d.id; }
572 })
573 .classed('numLinkLabel', true);
574
575 entering.each(function (d) {
576 var el = d3.select(this);
577
578 el.append('line')
579 .classed('numLinkHash', true)
580 .attr(hashAttrs(d.linkCoords));
581 el.append('text')
582 .classed('numLinkText', true)
583 .attr(textLabelPos(d.linkCoords))
584 .text(d.num);
585 });
586
587 // remove old labels
588 labels.exit().remove();
589 }
590
Simon Hunta4242de2015-02-24 17:11:55 -0800591 // ==========================
592 // Module definition
593
594 angular.module('ovTopo')
595 .factory('TopoD3Service',
596 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
597
598 function (_$log_, _fs_, _sus_, _is_, _ts_) {
599 $log = _$log_;
600 fs = _fs_;
601 sus = _sus_;
602 is = _is_;
603 ts = _ts_;
604
605 icfg = is.iconConfig();
606
607 function initD3(_api_) {
608 api = _api_;
609 }
610
611 function destroyD3() { }
612
613 return {
614 initD3: initD3,
615 destroyD3: destroyD3,
616
617 incDevLabIndex: incDevLabIndex,
618 adjustRectToFitText: adjustRectToFitText,
619 hostLabel: hostLabel,
620 deviceLabel: deviceLabel,
621 trimLabel: trimLabel,
622
Simon Hunt5674db92015-10-22 16:12:48 -0700623 updateDeviceLabel: updateDeviceRendering,
Simon Hunta4242de2015-02-24 17:11:55 -0800624 updateHostLabel: updateHostLabel,
625 updateDeviceColors: updateDeviceColors,
626
627 deviceExisting: deviceExisting,
628 hostExisting: hostExisting,
629 deviceEnter: deviceEnter,
630 hostEnter: hostEnter,
631 hostExit: hostExit,
632 deviceExit: deviceExit,
633
Simon Hunta4242de2015-02-24 17:11:55 -0800634 linkEntering: linkEntering,
635 applyLinkLabels: applyLinkLabels,
Simon Hunt1a5301e2015-02-25 15:31:25 -0800636 transformLabel: transformLabel,
Bri Prebilic Cole80401762015-07-16 11:36:18 -0700637 applyPortLabels: applyPortLabels,
638 applyNumLinkLabels: applyNumLinkLabels
Simon Hunta4242de2015-02-24 17:11:55 -0800639 };
640 }]);
641}());