blob: ced16563660f91afec95a89ef2b2186a4489f884 [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 },
58 icfg;
59
60 // internal state
61 var deviceLabelIndex = 0,
62 hostLabelIndex = 0;
63
64
65 var dCol = {
66 black: '#000',
67 paleblue: '#acf',
68 offwhite: '#ddd',
69 darkgrey: '#444',
70 midgrey: '#888',
71 lightgrey: '#bbb',
72 orange: '#f90'
73 };
74
75 // note: these are the device icon colors without affinity
76 var dColTheme = {
77 light: {
78 rfill: dCol.offwhite,
79 online: {
80 glyph: dCol.darkgrey,
81 rect: dCol.paleblue
82 },
83 offline: {
84 glyph: dCol.midgrey,
85 rect: dCol.lightgrey
86 }
87 },
88 dark: {
89 rfill: dCol.midgrey,
90 online: {
91 glyph: dCol.darkgrey,
92 rect: dCol.paleblue
93 },
94 offline: {
95 glyph: dCol.midgrey,
96 rect: dCol.darkgrey
97 }
98 }
99 };
100
101 function devBaseColor(d) {
102 var o = d.online ? 'online' : 'offline';
103 return dColTheme[ts.theme()][o];
104 }
105
106 function setDeviceColor(d) {
107 var o = d.online,
108 s = d.el.classed('selected'),
109 c = devBaseColor(d),
110 a = instColor(d.master, o),
111 icon = d.el.select('g.deviceIcon'),
112 g, r;
113
114 if (s) {
115 g = c.glyph;
116 r = dCol.orange;
117 } else if (api.instVisible()) {
118 g = o ? a : c.glyph;
119 r = o ? c.rfill : a;
120 } else {
121 g = c.glyph;
122 r = c.rect;
123 }
124
125 icon.select('use').style('fill', g);
126 icon.select('rect').style('fill', r);
127 }
128
129 function instColor(id, online) {
130 return sus.cat7().getColor(id, !online, ts.theme());
131 }
132
133 // ====
134
135 function incDevLabIndex() {
136 deviceLabelIndex = (deviceLabelIndex+1) % 3;
137 }
138
139 // Returns the newly computed bounding box of the rectangle
140 function adjustRectToFitText(n) {
141 var text = n.select('text'),
142 box = text.node().getBBox(),
143 lab = labelConfig;
144
145 text.attr('text-anchor', 'middle')
146 .attr('y', '-0.8em')
147 .attr('x', lab.imgPad/2);
148
149 // translate the bbox so that it is centered on [x,y]
150 box.x = -box.width / 2;
151 box.y = -box.height / 2;
152
153 // add padding
154 box.x -= (lab.padLR + lab.imgPad/2);
155 box.width += lab.padLR * 2 + lab.imgPad;
156 box.y -= lab.padTB;
157 box.height += lab.padTB * 2;
158
159 return box;
160 }
161
162 function hostLabel(d) {
163 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
164 return d.labels[idx];
165 }
166 function deviceLabel(d) {
167 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
168 return d.labels[idx];
169 }
170 function trimLabel(label) {
171 return (label && label.trim()) || '';
172 }
173
174 function emptyBox() {
175 return {
176 x: -2,
177 y: -2,
178 width: 4,
179 height: 4
180 };
181 }
182
183
184 function updateDeviceLabel(d) {
185 var label = trimLabel(deviceLabel(d)),
186 noLabel = !label,
187 node = d.el,
188 dim = icfg.device.dim,
189 box, dx, dy;
190
191 node.select('text')
192 .text(label)
193 .style('opacity', 0)
194 .transition()
195 .style('opacity', 1);
196
197 if (noLabel) {
198 box = emptyBox();
199 dx = -dim/2;
200 dy = -dim/2;
201 } else {
202 box = adjustRectToFitText(node);
203 dx = box.x + devCfg.xoff;
204 dy = box.y + devCfg.yoff;
205 }
206
207 node.select('rect')
208 .transition()
209 .attr(box);
210
211 node.select('g.deviceIcon')
212 .transition()
213 .attr('transform', sus.translate(dx, dy));
214 }
215
216 function updateHostLabel(d) {
217 var label = trimLabel(hostLabel(d));
218 d.el.select('text').text(label);
219 }
220
221 function updateDeviceColors(d) {
222 if (d) {
223 setDeviceColor(d);
224 } else {
225 api.node().filter('.device').each(function (d) {
226 setDeviceColor(d);
227 });
228 }
229 }
230
231
232 // ==========================
233 // updateNodes - subfunctions
234
235 function deviceExisting(d) {
236 var node = d.el;
237 node.classed('online', d.online);
238 updateDeviceLabel(d);
239 api.posNode(d, true);
240 }
241
242 function hostExisting(d) {
243 updateHostLabel(d);
244 api.posNode(d, true);
245 }
246
247 function deviceEnter(d) {
248 var node = d3.select(this),
249 glyphId = d.type || 'unknown',
250 label = trimLabel(deviceLabel(d)),
251 //devCfg = deviceIconConfig,
252 noLabel = !label,
253 box, dx, dy, icon;
254
255 d.el = node;
256
257 node.append('rect').attr({ rx: 5, ry: 5 });
258 node.append('text').text(label).attr('dy', '1.1em');
259 box = adjustRectToFitText(node);
260 node.select('rect').attr(box);
261
262 icon = is.addDeviceIcon(node, glyphId);
263
264 if (noLabel) {
265 dx = -icon.dim/2;
266 dy = -icon.dim/2;
267 } else {
268 box = adjustRectToFitText(node);
269 dx = box.x + devCfg.xoff;
270 dy = box.y + devCfg.yoff;
271 }
272
273 icon.attr('transform', sus.translate(dx, dy));
274 }
275
276 function hostEnter(d) {
277 var node = d3.select(this),
278 gid = d.type || 'unknown',
279 rad = icfg.host.radius,
280 r = d.type ? rad.withGlyph : rad.noGlyph,
281 textDy = r + 10;
282
283 d.el = node;
284 sus.visible(node, api.showHosts());
285
286 is.addHostIcon(node, r, gid);
287
288 node.append('text')
289 .text(hostLabel)
290 .attr('dy', textDy)
291 .attr('text-anchor', 'middle');
292 }
293
294 function hostExit(d) {
295 var node = d.el;
296 node.select('use')
297 .style('opacity', 0.5)
298 .transition()
299 .duration(800)
300 .style('opacity', 0);
301
302 node.select('text')
303 .style('opacity', 0.5)
304 .transition()
305 .duration(800)
306 .style('opacity', 0);
307
308 node.select('circle')
309 .style('stroke-fill', '#555')
310 .style('fill', '#888')
311 .style('opacity', 0.5)
312 .transition()
313 .duration(1500)
314 .attr('r', 0);
315 }
316
317 function deviceExit(d) {
318 var node = d.el;
319 node.select('use')
320 .style('opacity', 0.5)
321 .transition()
322 .duration(800)
323 .style('opacity', 0);
324
325 node.selectAll('rect')
326 .style('stroke-fill', '#555')
327 .style('fill', '#888')
328 .style('opacity', 0.5);
329 }
330
331
332 // ==========================
333 // updateLinks - subfunctions
334
335 function linkExisting(d) {
336 // this is supposed to be an existing link, but we have observed
337 // occasions (where links are deleted and added rapidly?) where
338 // the DOM element has not been defined. So protection against that...
339 if (d.el) {
340 api.restyleLinkElement(d, true);
341 }
342 }
343
344 function linkEntering(d) {
345 var link = d3.select(this);
346 d.el = link;
347 api.restyleLinkElement(d);
348 if (d.type() === 'hostLink') {
349 sus.visible(link, api.showHosts());
350 }
351 }
352
353 var linkLabelOffset = '0.3em';
354
355 function applyLinkLabels() {
356 var entering;
357
358 api.updateLinkLabelModel();
359
360 // for elements already existing, we need to update the text
361 // and adjust the rectangle size to fit
362 api.linkLabel().each(function (d) {
363 var el = d3.select(this),
364 rect = el.select('rect'),
365 text = el.select('text');
366 text.text(d.label);
367 rect.attr(rectAroundText(el));
368 });
369
370 entering = api.linkLabel().enter().append('g')
371 .classed('linkLabel', true)
372 .attr('id', function (d) { return d.id; });
373
374 entering.each(function (d) {
375 var el = d3.select(this),
376 rect,
377 text,
378 parms = {
379 x1: d.ldata.source.x,
380 y1: d.ldata.source.y,
381 x2: d.ldata.target.x,
382 y2: d.ldata.target.y
383 };
384
385 if (d.ldata.type() === 'hostLink') {
386 el.classed('hostLinkLabel', true);
387 sus.visible(el, api.showHosts());
388 }
389
390 d.el = el;
391 rect = el.append('rect');
392 text = el.append('text').text(d.label);
393 rect.attr(rectAroundText(el));
394 text.attr('dy', linkLabelOffset);
395
396 el.attr('transform', transformLabel(parms));
397 });
398
399 // Remove any labels that are no longer required.
400 api.linkLabel().exit().remove();
401 }
402
403 function rectAroundText(el) {
404 var text = el.select('text'),
405 box = text.node().getBBox();
406
407 // translate the bbox so that it is centered on [x,y]
408 box.x = -box.width / 2;
409 box.y = -box.height / 2;
410
411 // add padding
412 box.x -= 1;
413 box.width += 2;
414 return box;
415 }
416
417 function transformLabel(p) {
418 var dx = p.x2 - p.x1,
419 dy = p.y2 - p.y1,
420 xMid = dx/2 + p.x1,
421 yMid = dy/2 + p.y1;
422 return sus.translate(xMid, yMid);
423 }
424
425
426 // ==========================
427 // Module definition
428
429 angular.module('ovTopo')
430 .factory('TopoD3Service',
431 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
432
433 function (_$log_, _fs_, _sus_, _is_, _ts_) {
434 $log = _$log_;
435 fs = _fs_;
436 sus = _sus_;
437 is = _is_;
438 ts = _ts_;
439
440 icfg = is.iconConfig();
441
442 function initD3(_api_) {
443 api = _api_;
444 }
445
446 function destroyD3() { }
447
448 return {
449 initD3: initD3,
450 destroyD3: destroyD3,
451
452 incDevLabIndex: incDevLabIndex,
453 adjustRectToFitText: adjustRectToFitText,
454 hostLabel: hostLabel,
455 deviceLabel: deviceLabel,
456 trimLabel: trimLabel,
457
458 updateDeviceLabel: updateDeviceLabel,
459 updateHostLabel: updateHostLabel,
460 updateDeviceColors: updateDeviceColors,
461
462 deviceExisting: deviceExisting,
463 hostExisting: hostExisting,
464 deviceEnter: deviceEnter,
465 hostEnter: hostEnter,
466 hostExit: hostExit,
467 deviceExit: deviceExit,
468
469 linkExisting: linkExisting,
470 linkEntering: linkEntering,
471 applyLinkLabels: applyLinkLabels,
472
473 transformLabel: transformLabel
474 };
475 }]);
476}());