blob: 8353de5680f4130d22749b5f68e85869216fdf5c [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,
60 yoff: 5
61 },
Simon Hunta4242de2015-02-24 17:11:55 -080062 icfg;
63
Simon Hunt5674db92015-10-22 16:12:48 -070064 var status = {
65 i: 'badgeInfo',
66 w: 'badgeWarn',
67 e: 'badgeError'
68 };
69
70 function badgeStatus(badge) {
71 return status[badge.status] || status.i;
72 }
73
Simon Hunta4242de2015-02-24 17:11:55 -080074 // internal state
75 var deviceLabelIndex = 0,
76 hostLabelIndex = 0;
77
78
79 var dCol = {
80 black: '#000',
81 paleblue: '#acf',
82 offwhite: '#ddd',
83 darkgrey: '#444',
84 midgrey: '#888',
85 lightgrey: '#bbb',
86 orange: '#f90'
87 };
88
89 // note: these are the device icon colors without affinity
90 var dColTheme = {
91 light: {
92 rfill: dCol.offwhite,
93 online: {
94 glyph: dCol.darkgrey,
95 rect: dCol.paleblue
96 },
97 offline: {
98 glyph: dCol.midgrey,
99 rect: dCol.lightgrey
100 }
101 },
102 dark: {
103 rfill: dCol.midgrey,
104 online: {
105 glyph: dCol.darkgrey,
106 rect: dCol.paleblue
107 },
108 offline: {
109 glyph: dCol.midgrey,
110 rect: dCol.darkgrey
111 }
112 }
113 };
114
115 function devBaseColor(d) {
116 var o = d.online ? 'online' : 'offline';
117 return dColTheme[ts.theme()][o];
118 }
119
120 function setDeviceColor(d) {
121 var o = d.online,
122 s = d.el.classed('selected'),
123 c = devBaseColor(d),
124 a = instColor(d.master, o),
125 icon = d.el.select('g.deviceIcon'),
126 g, r;
127
128 if (s) {
129 g = c.glyph;
130 r = dCol.orange;
131 } else if (api.instVisible()) {
132 g = o ? a : c.glyph;
133 r = o ? c.rfill : a;
134 } else {
135 g = c.glyph;
136 r = c.rect;
137 }
138
139 icon.select('use').style('fill', g);
140 icon.select('rect').style('fill', r);
141 }
142
143 function instColor(id, online) {
144 return sus.cat7().getColor(id, !online, ts.theme());
145 }
146
147 // ====
148
149 function incDevLabIndex() {
150 deviceLabelIndex = (deviceLabelIndex+1) % 3;
Bri Prebilic Cole9cf1a8d2015-04-21 13:15:29 -0700151 switch(deviceLabelIndex) {
152 case 0: return 'Hide device labels';
153 case 1: return 'Show friendly device labels';
154 case 2: return 'Show device ID labels';
155 }
Simon Hunta4242de2015-02-24 17:11:55 -0800156 }
157
158 // Returns the newly computed bounding box of the rectangle
159 function adjustRectToFitText(n) {
160 var text = n.select('text'),
161 box = text.node().getBBox(),
162 lab = labelConfig;
163
164 text.attr('text-anchor', 'middle')
165 .attr('y', '-0.8em')
166 .attr('x', lab.imgPad/2);
167
168 // translate the bbox so that it is centered on [x,y]
169 box.x = -box.width / 2;
170 box.y = -box.height / 2;
171
172 // add padding
173 box.x -= (lab.padLR + lab.imgPad/2);
174 box.width += lab.padLR * 2 + lab.imgPad;
175 box.y -= lab.padTB;
176 box.height += lab.padTB * 2;
177
178 return box;
179 }
180
181 function hostLabel(d) {
182 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
183 return d.labels[idx];
184 }
185 function deviceLabel(d) {
186 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
187 return d.labels[idx];
188 }
189 function trimLabel(label) {
190 return (label && label.trim()) || '';
191 }
192
193 function emptyBox() {
194 return {
195 x: -2,
196 y: -2,
197 width: 4,
198 height: 4
199 };
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,
210 bcr = badgeConfig.radius;
Simon Hunta4242de2015-02-24 17:11:55 -0800211
212 node.select('text')
213 .text(label)
214 .style('opacity', 0)
215 .transition()
216 .style('opacity', 1);
217
218 if (noLabel) {
219 box = emptyBox();
220 dx = -dim/2;
221 dy = -dim/2;
222 } else {
223 box = adjustRectToFitText(node);
224 dx = box.x + devCfg.xoff;
225 dy = box.y + devCfg.yoff;
226 }
227
228 node.select('rect')
229 .transition()
230 .attr(box);
231
232 node.select('g.deviceIcon')
233 .transition()
234 .attr('transform', sus.translate(dx, dy));
Simon Hunta4242de2015-02-24 17:11:55 -0800235
Simon Hunt5674db92015-10-22 16:12:48 -0700236 // handle badge, if defined
237 if (bdg) {
Simon Hunte9343f32015-10-21 18:07:46 -0700238 bsel = node.append('g')
239 .classed('badge', true)
Simon Hunt5674db92015-10-22 16:12:48 -0700240 .classed(badgeStatus(bdg), true)
241 .attr('transform', sus.translate(dx + dim, dy));
Simon Hunte9343f32015-10-21 18:07:46 -0700242
243 bsel.append('circle')
Simon Hunt5674db92015-10-22 16:12:48 -0700244 .attr('r', bcr);
245
246 if (bdg.txt) {
247 bsel.append('text')
248 .attr('dy', badgeConfig.yoff)
249 .attr('text-anchor', 'middle')
250 .text(bdg.txt);
251 } else if (bdg.gid) {
252 bsel.append('use')
253 .attr({
254 width: bcr * 2,
255 height: bcr * 2,
256 'xlink:href': '#' + bdg.gid
257 });
258
259 }
Simon Hunte9343f32015-10-21 18:07:46 -0700260 }
261 }
262
Simon Hunta4242de2015-02-24 17:11:55 -0800263 function updateHostLabel(d) {
264 var label = trimLabel(hostLabel(d));
265 d.el.select('text').text(label);
266 }
267
268 function updateDeviceColors(d) {
269 if (d) {
270 setDeviceColor(d);
271 } else {
272 api.node().filter('.device').each(function (d) {
273 setDeviceColor(d);
274 });
275 }
276 }
277
278
279 // ==========================
280 // updateNodes - subfunctions
281
282 function deviceExisting(d) {
283 var node = d.el;
284 node.classed('online', d.online);
Simon Hunt5674db92015-10-22 16:12:48 -0700285 updateDeviceRendering(d);
Simon Hunta4242de2015-02-24 17:11:55 -0800286 api.posNode(d, true);
287 }
288
289 function hostExisting(d) {
290 updateHostLabel(d);
291 api.posNode(d, true);
292 }
293
294 function deviceEnter(d) {
295 var node = d3.select(this),
296 glyphId = d.type || 'unknown',
297 label = trimLabel(deviceLabel(d)),
298 //devCfg = deviceIconConfig,
299 noLabel = !label,
300 box, dx, dy, icon;
301
302 d.el = node;
303
304 node.append('rect').attr({ rx: 5, ry: 5 });
305 node.append('text').text(label).attr('dy', '1.1em');
306 box = adjustRectToFitText(node);
307 node.select('rect').attr(box);
308
309 icon = is.addDeviceIcon(node, glyphId);
310
311 if (noLabel) {
312 dx = -icon.dim/2;
313 dy = -icon.dim/2;
314 } else {
315 box = adjustRectToFitText(node);
316 dx = box.x + devCfg.xoff;
317 dy = box.y + devCfg.yoff;
318 }
319
320 icon.attr('transform', sus.translate(dx, dy));
321 }
322
323 function hostEnter(d) {
324 var node = d3.select(this),
325 gid = d.type || 'unknown',
326 rad = icfg.host.radius,
327 r = d.type ? rad.withGlyph : rad.noGlyph,
328 textDy = r + 10;
329
330 d.el = node;
331 sus.visible(node, api.showHosts());
332
333 is.addHostIcon(node, r, gid);
334
335 node.append('text')
336 .text(hostLabel)
337 .attr('dy', textDy)
338 .attr('text-anchor', 'middle');
339 }
340
341 function hostExit(d) {
342 var node = d.el;
343 node.select('use')
344 .style('opacity', 0.5)
345 .transition()
346 .duration(800)
347 .style('opacity', 0);
348
349 node.select('text')
350 .style('opacity', 0.5)
351 .transition()
352 .duration(800)
353 .style('opacity', 0);
354
355 node.select('circle')
356 .style('stroke-fill', '#555')
357 .style('fill', '#888')
358 .style('opacity', 0.5)
359 .transition()
360 .duration(1500)
361 .attr('r', 0);
362 }
363
364 function deviceExit(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.selectAll('rect')
373 .style('stroke-fill', '#555')
374 .style('fill', '#888')
375 .style('opacity', 0.5);
376 }
377
378
379 // ==========================
380 // updateLinks - subfunctions
381
Simon Hunta4242de2015-02-24 17:11:55 -0800382 function linkEntering(d) {
383 var link = d3.select(this);
384 d.el = link;
385 api.restyleLinkElement(d);
386 if (d.type() === 'hostLink') {
387 sus.visible(link, api.showHosts());
388 }
389 }
390
391 var linkLabelOffset = '0.3em';
392
393 function applyLinkLabels() {
394 var entering;
395
396 api.updateLinkLabelModel();
397
398 // for elements already existing, we need to update the text
399 // and adjust the rectangle size to fit
400 api.linkLabel().each(function (d) {
401 var el = d3.select(this),
402 rect = el.select('rect'),
403 text = el.select('text');
404 text.text(d.label);
405 rect.attr(rectAroundText(el));
406 });
407
408 entering = api.linkLabel().enter().append('g')
409 .classed('linkLabel', true)
410 .attr('id', function (d) { return d.id; });
411
412 entering.each(function (d) {
413 var el = d3.select(this),
414 rect,
Bri Prebilic Cole038aedd2015-07-13 15:25:16 -0700415 text;
Simon Hunta4242de2015-02-24 17:11:55 -0800416
417 if (d.ldata.type() === 'hostLink') {
418 el.classed('hostLinkLabel', true);
419 sus.visible(el, api.showHosts());
420 }
421
422 d.el = el;
423 rect = el.append('rect');
424 text = el.append('text').text(d.label);
425 rect.attr(rectAroundText(el));
426 text.attr('dy', linkLabelOffset);
427
Bri Prebilic Cole038aedd2015-07-13 15:25:16 -0700428 el.attr('transform', transformLabel(d.ldata.position));
Simon Hunta4242de2015-02-24 17:11:55 -0800429 });
430
431 // Remove any labels that are no longer required.
432 api.linkLabel().exit().remove();
433 }
434
435 function rectAroundText(el) {
436 var text = el.select('text'),
437 box = text.node().getBBox();
438
439 // translate the bbox so that it is centered on [x,y]
440 box.x = -box.width / 2;
441 box.y = -box.height / 2;
442
443 // add padding
444 box.x -= 1;
445 box.width += 2;
446 return box;
447 }
448
449 function transformLabel(p) {
450 var dx = p.x2 - p.x1,
451 dy = p.y2 - p.y1,
452 xMid = dx/2 + p.x1,
453 yMid = dy/2 + p.y1;
454 return sus.translate(xMid, yMid);
455 }
456
Simon Hunt1a5301e2015-02-25 15:31:25 -0800457 function applyPortLabels(data, portLabelG) {
458 var entering = portLabelG.selectAll('.portLabel')
459 .data(data).enter().append('g')
460 .classed('portLabel', true)
461 .attr('id', function (d) { return d.id; });
462
463 entering.each(function (d) {
464 var el = d3.select(this),
465 rect = el.append('rect'),
466 text = el.append('text').text(d.num);
467
468 rect.attr(rectAroundText(el));
469 text.attr('dy', linkLabelOffset);
Simon Hunt969b3c92015-02-25 18:11:31 -0800470 el.attr('transform', sus.translate(d.x, d.y));
Simon Hunt1a5301e2015-02-25 15:31:25 -0800471 });
472 }
473
Bri Prebilic Cole80401762015-07-16 11:36:18 -0700474 function labelPoint(linkPos) {
475 var lengthUpLine = 1 / 3,
476 dx = linkPos.x2 - linkPos.x1,
477 dy = linkPos.y2 - linkPos.y1,
478 movedX = dx * lengthUpLine,
479 movedY = dy * lengthUpLine;
480
481 return {
482 x: movedX,
483 y: movedY
484 };
485 }
486
487 function calcGroupPos(linkPos) {
488 var moved = labelPoint(linkPos);
489 return sus.translate(linkPos.x1 + moved.x, linkPos.y1 + moved.y);
490 }
491
492 // calculates where on the link that the hash line for 5+ label appears
493 function hashAttrs(linkPos) {
494 var hashLength = 25,
495 halfLength = hashLength / 2,
496 dx = linkPos.x2 - linkPos.x1,
497 dy = linkPos.y2 - linkPos.y1,
498 length = Math.sqrt((dx * dx) + (dy * dy)),
499 moveAmtX = (dx / length) * halfLength,
500 moveAmtY = (dy / length) * halfLength,
501 mid = labelPoint(linkPos),
502 angle = Math.atan(dy / dx) + 45;
503
504 return {
505 x1: mid.x - moveAmtX,
506 y1: mid.y - moveAmtY,
507 x2: mid.x + moveAmtX,
508 y2: mid.y + moveAmtY,
509 stroke: api.linkConfig()[ts.theme()].baseColor,
510 transform: 'rotate(' + angle + ',' + mid.x + ',' + mid.y + ')'
511 };
512 }
513
514 function textLabelPos(linkPos) {
515 var point = labelPoint(linkPos),
516 dist = 20;
517 return {
518 x: point.x + dist,
519 y: point.y + dist
520 };
521 }
522
523 function applyNumLinkLabels(data, lblsG) {
524 var labels = lblsG.selectAll('g.numLinkLabel')
525 .data(data, function (d) { return 'pair-' + d.id; }),
526 entering;
527
528 // update existing labels
529 labels.each(function (d) {
530 var el = d3.select(this);
531
532 el.attr({
533 transform: function (d) { return calcGroupPos(d.linkCoords); }
534 });
535 el.select('line')
536 .attr(hashAttrs(d.linkCoords));
537 el.select('text')
538 .attr(textLabelPos(d.linkCoords))
539 .text(d.num);
540 });
541
542 // add new labels
543 entering = labels
544 .enter()
545 .append('g')
546 .attr({
547 transform: function (d) { return calcGroupPos(d.linkCoords); },
548 id: function (d) { return 'pair-' + d.id; }
549 })
550 .classed('numLinkLabel', true);
551
552 entering.each(function (d) {
553 var el = d3.select(this);
554
555 el.append('line')
556 .classed('numLinkHash', true)
557 .attr(hashAttrs(d.linkCoords));
558 el.append('text')
559 .classed('numLinkText', true)
560 .attr(textLabelPos(d.linkCoords))
561 .text(d.num);
562 });
563
564 // remove old labels
565 labels.exit().remove();
566 }
567
Simon Hunta4242de2015-02-24 17:11:55 -0800568 // ==========================
569 // Module definition
570
571 angular.module('ovTopo')
572 .factory('TopoD3Service',
573 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
574
575 function (_$log_, _fs_, _sus_, _is_, _ts_) {
576 $log = _$log_;
577 fs = _fs_;
578 sus = _sus_;
579 is = _is_;
580 ts = _ts_;
581
582 icfg = is.iconConfig();
583
584 function initD3(_api_) {
585 api = _api_;
586 }
587
588 function destroyD3() { }
589
590 return {
591 initD3: initD3,
592 destroyD3: destroyD3,
593
594 incDevLabIndex: incDevLabIndex,
595 adjustRectToFitText: adjustRectToFitText,
596 hostLabel: hostLabel,
597 deviceLabel: deviceLabel,
598 trimLabel: trimLabel,
599
Simon Hunt5674db92015-10-22 16:12:48 -0700600 updateDeviceLabel: updateDeviceRendering,
Simon Hunta4242de2015-02-24 17:11:55 -0800601 updateHostLabel: updateHostLabel,
602 updateDeviceColors: updateDeviceColors,
603
604 deviceExisting: deviceExisting,
605 hostExisting: hostExisting,
606 deviceEnter: deviceEnter,
607 hostEnter: hostEnter,
608 hostExit: hostExit,
609 deviceExit: deviceExit,
610
Simon Hunta4242de2015-02-24 17:11:55 -0800611 linkEntering: linkEntering,
612 applyLinkLabels: applyLinkLabels,
Simon Hunt1a5301e2015-02-25 15:31:25 -0800613 transformLabel: transformLabel,
Bri Prebilic Cole80401762015-07-16 11:36:18 -0700614 applyPortLabels: applyPortLabels,
615 applyNumLinkLabels: applyNumLinkLabels
Simon Hunta4242de2015-02-24 17:11:55 -0800616 };
617 }]);
618}());