blob: 10e924f11d90dfba73995f9ce06cf66d8c33f29b [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/*
Simon Hunt3a6eec02015-02-09 21:16:43 -080018 ONOS GUI -- Topology Force Module.
19 Visualization of the topology in an SVG layer, using a D3 Force Layout.
Simon Hunt737c89f2015-01-28 12:23:19 -080020 */
21
22(function () {
23 'use strict';
24
25 // injected refs
Simon Huntf542d842015-02-11 16:20:33 -080026 var $log, fs, sus, is, ts, flash, tis, tms, tss, tts, icfg, uplink;
Simon Huntac4c6f72015-02-03 19:50:53 -080027
28 // configuration
29 var labelConfig = {
30 imgPad: 16,
31 padLR: 4,
32 padTB: 3,
33 marginLR: 3,
34 marginTB: 2,
35 port: {
36 gap: 3,
37 width: 18,
38 height: 14
39 }
40 };
41
42 var deviceIconConfig = {
43 xoff: -20,
44 yoff: -18
45 };
Simon Hunt737c89f2015-01-28 12:23:19 -080046
Simon Hunt1894d792015-02-04 17:09:20 -080047 var linkConfig = {
48 light: {
49 baseColor: '#666',
50 inColor: '#66f',
Simon Hunt3a6eec02015-02-09 21:16:43 -080051 outColor: '#f00'
Simon Hunt1894d792015-02-04 17:09:20 -080052 },
53 dark: {
Simon Hunt5724fb42015-02-05 16:59:40 -080054 baseColor: '#aaa',
Simon Hunt1894d792015-02-04 17:09:20 -080055 inColor: '#66f',
Simon Hunt5724fb42015-02-05 16:59:40 -080056 outColor: '#f66'
Simon Hunt1894d792015-02-04 17:09:20 -080057 },
58 inWidth: 12,
59 outWidth: 10
60 };
61
Simon Hunt737c89f2015-01-28 12:23:19 -080062 // internal state
Simon Huntac4c6f72015-02-03 19:50:53 -080063 var settings, // merged default settings and options
Simon Hunt737c89f2015-01-28 12:23:19 -080064 force, // force layout object
65 drag, // drag behavior handler
66 network = {
67 nodes: [],
68 links: [],
69 lookup: {},
70 revLinkToKey: {}
Simon Huntac4c6f72015-02-03 19:50:53 -080071 },
Simon Hunt1894d792015-02-04 17:09:20 -080072 lu = network.lookup, // shorthand
Simon Huntdc6adea2015-02-09 22:29:36 -080073 rlk = network.revLinkToKey,
Simon Huntac4c6f72015-02-03 19:50:53 -080074 deviceLabelIndex = 0, // for device label cycling
Simon Hunt1894d792015-02-04 17:09:20 -080075 hostLabelIndex = 0, // for host label cycling
Simon Hunta142dd22015-02-12 22:07:51 -080076 showHosts = false, // whether hosts are displayed
Simon Hunt5724fb42015-02-05 16:59:40 -080077 showOffline = true, // whether offline devices are displayed
78 oblique = false, // whether we are in the oblique view
Simon Hunt445e8152015-02-06 13:00:12 -080079 nodeLock = false, // whether nodes can be dragged or not (locked)
Simon Hunt08f841d02015-02-10 14:39:20 -080080 dim; // the dimensions of the force layout [w,h]
Simon Hunt737c89f2015-01-28 12:23:19 -080081
82 // SVG elements;
83 var linkG, linkLabelG, nodeG;
84
85 // D3 selections;
86 var link, linkLabel, node;
87
88 // default settings for force layout
89 var defaultSettings = {
90 gravity: 0.4,
91 friction: 0.7,
92 charge: {
93 // note: key is node.class
94 device: -8000,
95 host: -5000,
96 _def_: -12000
97 },
98 linkDistance: {
99 // note: key is link.type
100 direct: 100,
101 optical: 120,
102 hostLink: 3,
103 _def_: 50
104 },
105 linkStrength: {
106 // note: key is link.type
107 // range: {0.0 ... 1.0}
108 //direct: 1.0,
109 //optical: 1.0,
110 //hostLink: 1.0,
111 _def_: 1.0
112 }
113 };
114
115
Simon Huntac4c6f72015-02-03 19:50:53 -0800116 // ==========================
117 // === EVENT HANDLERS
118
119 function addDevice(data) {
120 var id = data.id,
121 d;
122
Simon Hunt1894d792015-02-04 17:09:20 -0800123 uplink.showNoDevs(false);
Simon Huntac4c6f72015-02-03 19:50:53 -0800124
125 // although this is an add device event, if we already have the
126 // device, treat it as an update instead..
Simon Hunt1894d792015-02-04 17:09:20 -0800127 if (lu[id]) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800128 updateDevice(data);
129 return;
130 }
131
Simon Hunt3a6eec02015-02-09 21:16:43 -0800132 d = tms.createDeviceNode(data);
Simon Huntac4c6f72015-02-03 19:50:53 -0800133 network.nodes.push(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800134 lu[id] = d;
Simon Huntac4c6f72015-02-03 19:50:53 -0800135 updateNodes();
136 fStart();
137 }
138
139 function updateDevice(data) {
140 var id = data.id,
Simon Hunt1894d792015-02-04 17:09:20 -0800141 d = lu[id],
Simon Huntac4c6f72015-02-03 19:50:53 -0800142 wasOnline;
143
144 if (d) {
145 wasOnline = d.online;
146 angular.extend(d, data);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800147 if (tms.positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800148 sendUpdateMeta(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800149 }
150 updateNodes();
151 if (wasOnline !== d.online) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800152 tms.findAttachedLinks(d.id).forEach(restyleLinkElement);
Simon Hunt5724fb42015-02-05 16:59:40 -0800153 updateOfflineVisibility(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800154 }
155 } else {
156 // TODO: decide whether we want to capture logic errors
157 //logicError('updateDevice lookup fail. ID = "' + id + '"');
158 }
159 }
160
Simon Hunt1894d792015-02-04 17:09:20 -0800161 function removeDevice(data) {
162 var id = data.id,
163 d = lu[id];
164 if (d) {
165 removeDeviceElement(d);
166 } else {
167 // TODO: decide whether we want to capture logic errors
168 //logicError('removeDevice lookup fail. ID = "' + id + '"');
169 }
170 }
171
172 function addHost(data) {
173 var id = data.id,
174 d, lnk;
175
176 // although this is an add host event, if we already have the
177 // host, treat it as an update instead..
178 if (lu[id]) {
179 updateHost(data);
180 return;
181 }
182
Simon Hunt3a6eec02015-02-09 21:16:43 -0800183 d = tms.createHostNode(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800184 network.nodes.push(d);
185 lu[id] = d;
Simon Hunt1894d792015-02-04 17:09:20 -0800186 updateNodes();
187
Simon Hunt3a6eec02015-02-09 21:16:43 -0800188 lnk = tms.createHostLink(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800189 if (lnk) {
Simon Hunt1894d792015-02-04 17:09:20 -0800190 d.linkData = lnk; // cache ref on its host
191 network.links.push(lnk);
192 lu[d.ingress] = lnk;
193 lu[d.egress] = lnk;
194 updateLinks();
195 }
196
197 fStart();
198 }
199
200 function updateHost(data) {
201 var id = data.id,
202 d = lu[id];
203 if (d) {
204 angular.extend(d, data);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800205 if (tms.positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800206 sendUpdateMeta(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800207 }
208 updateNodes();
209 } else {
210 // TODO: decide whether we want to capture logic errors
211 //logicError('updateHost lookup fail. ID = "' + id + '"');
212 }
213 }
214
215 function removeHost(data) {
216 var id = data.id,
217 d = lu[id];
218 if (d) {
219 removeHostElement(d, true);
220 } else {
221 // may have already removed host, if attached to removed device
222 //console.warn('removeHost lookup fail. ID = "' + id + '"');
223 }
224 }
225
226 function addLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800227 var result = tms.findLink(data, 'add'),
Simon Hunt1894d792015-02-04 17:09:20 -0800228 bad = result.badLogic,
229 d = result.ldata;
230
231 if (bad) {
232 //logicError(bad + ': ' + link.id);
233 return;
234 }
235
236 if (d) {
237 // we already have a backing store link for src/dst nodes
238 addLinkUpdate(d, data);
239 return;
240 }
241
242 // no backing store link yet
Simon Hunt3a6eec02015-02-09 21:16:43 -0800243 d = tms.createLink(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800244 if (d) {
245 network.links.push(d);
246 lu[d.key] = d;
247 updateLinks();
248 fStart();
249 }
250 }
251
252 function updateLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800253 var result = tms.findLink(data, 'update'),
Simon Hunt1894d792015-02-04 17:09:20 -0800254 bad = result.badLogic;
255 if (bad) {
256 //logicError(bad + ': ' + link.id);
257 return;
258 }
259 result.updateWith(link);
260 }
261
262 function removeLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800263 var result = tms.findLink(data, 'remove'),
Simon Hunt1894d792015-02-04 17:09:20 -0800264 bad = result.badLogic;
265 if (bad) {
266 // may have already removed link, if attached to removed device
267 //console.warn(bad + ': ' + link.id);
268 return;
269 }
270 result.removeRawLink();
271 }
272
273 // ========================
274
275 function addLinkUpdate(ldata, link) {
276 // add link event, but we already have the reverse link installed
277 ldata.fromTarget = link;
Simon Huntdc6adea2015-02-09 22:29:36 -0800278 rlk[link.id] = ldata.key;
Simon Hunt1894d792015-02-04 17:09:20 -0800279 restyleLinkElement(ldata);
280 }
281
Simon Hunt1894d792015-02-04 17:09:20 -0800282
283 var widthRatio = 1.4,
284 linkScale = d3.scale.linear()
285 .domain([1, 12])
286 .range([widthRatio, 12 * widthRatio])
Simon Hunt5724fb42015-02-05 16:59:40 -0800287 .clamp(true),
Simon Hunt3a6eec02015-02-09 21:16:43 -0800288 allLinkTypes = 'direct indirect optical tunnel';
Simon Hunt1894d792015-02-04 17:09:20 -0800289
Simon Hunta142dd22015-02-12 22:07:51 -0800290 function restyleLinkElement(ldata, immediate) {
Simon Hunt1894d792015-02-04 17:09:20 -0800291 // this fn's job is to look at raw links and decide what svg classes
292 // need to be applied to the line element in the DOM
293 var th = ts.theme(),
294 el = ldata.el,
295 type = ldata.type(),
296 lw = ldata.linkWidth(),
Simon Hunta142dd22015-02-12 22:07:51 -0800297 online = ldata.online(),
298 delay = immediate ? 0 : 1000;
Simon Hunt1894d792015-02-04 17:09:20 -0800299
300 el.classed('link', true);
301 el.classed('inactive', !online);
302 el.classed(allLinkTypes, false);
303 if (type) {
304 el.classed(type, true);
305 }
306 el.transition()
Simon Hunta142dd22015-02-12 22:07:51 -0800307 .duration(delay)
Simon Hunt1894d792015-02-04 17:09:20 -0800308 .attr('stroke-width', linkScale(lw))
309 .attr('stroke', linkConfig[th].baseColor);
310 }
311
Simon Hunt1894d792015-02-04 17:09:20 -0800312 function removeLinkElement(d) {
313 var idx = fs.find(d.key, network.links, 'key'),
314 removed;
315 if (idx >=0) {
316 // remove from links array
317 removed = network.links.splice(idx, 1);
318 // remove from lookup cache
319 delete lu[removed[0].key];
320 updateLinks();
321 fResume();
322 }
323 }
324
325 function removeHostElement(d, upd) {
326 // first, remove associated hostLink...
327 removeLinkElement(d.linkData);
328
329 // remove hostLink bindings
330 delete lu[d.ingress];
331 delete lu[d.egress];
332
333 // remove from lookup cache
334 delete lu[d.id];
335 // remove from nodes array
336 var idx = fs.find(d.id, network.nodes);
337 network.nodes.splice(idx, 1);
338
339 // remove from SVG
340 // NOTE: upd is false if we were called from removeDeviceElement()
341 if (upd) {
342 updateNodes();
343 fResume();
344 }
345 }
346
347 function removeDeviceElement(d) {
348 var id = d.id;
349 // first, remove associated hosts and links..
Simon Huntdc6adea2015-02-09 22:29:36 -0800350 tms.findAttachedHosts(id).forEach(removeHostElement);
351 tms.findAttachedLinks(id).forEach(removeLinkElement);
Simon Hunt1894d792015-02-04 17:09:20 -0800352
353 // remove from lookup cache
354 delete lu[id];
355 // remove from nodes array
356 var idx = fs.find(id, network.nodes);
357 network.nodes.splice(idx, 1);
358
359 if (!network.nodes.length) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800360 uplink.showNoDevs(true);
Simon Hunt1894d792015-02-04 17:09:20 -0800361 }
362
363 // remove from SVG
364 updateNodes();
365 fResume();
366 }
367
Simon Hunt5724fb42015-02-05 16:59:40 -0800368 function updateHostVisibility() {
Simon Hunt18bf9822015-02-12 17:35:45 -0800369 sus.visible(nodeG.selectAll('.host'), showHosts);
370 sus.visible(linkG.selectAll('.hostLink'), showHosts);
Simon Hunt5724fb42015-02-05 16:59:40 -0800371 }
372
373 function updateOfflineVisibility(dev) {
374 function updDev(d, show) {
Simon Hunt18bf9822015-02-12 17:35:45 -0800375 sus.visible(d.el, show);
Simon Hunt5724fb42015-02-05 16:59:40 -0800376
Simon Huntdc6adea2015-02-09 22:29:36 -0800377 tms.findAttachedLinks(d.id).forEach(function (link) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800378 b = show && ((link.type() !== 'hostLink') || showHosts);
Simon Hunt18bf9822015-02-12 17:35:45 -0800379 sus.visible(link.el, b);
Simon Hunt5724fb42015-02-05 16:59:40 -0800380 });
Simon Huntdc6adea2015-02-09 22:29:36 -0800381 tms.findAttachedHosts(d.id).forEach(function (host) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800382 b = show && showHosts;
Simon Hunt18bf9822015-02-12 17:35:45 -0800383 sus.visible(host.el, b);
Simon Hunt5724fb42015-02-05 16:59:40 -0800384 });
385 }
386
387 if (dev) {
388 // updating a specific device that just toggled off/on-line
389 updDev(dev, dev.online || showOffline);
390 } else {
391 // updating all offline devices
Simon Huntdc6adea2015-02-09 22:29:36 -0800392 tms.findDevices(true).forEach(function (d) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800393 updDev(d, showOffline);
394 });
395 }
396 }
397
Simon Hunt1894d792015-02-04 17:09:20 -0800398
Simon Hunt445e8152015-02-06 13:00:12 -0800399 function sendUpdateMeta(d, clearPos) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800400 var metaUi = {},
401 ll;
402
Simon Hunt445e8152015-02-06 13:00:12 -0800403 // if we are not clearing the position data (unpinning),
404 // attach the x, y, longitude, latitude...
405 if (!clearPos) {
Simon Hunt3a6eec02015-02-09 21:16:43 -0800406 ll = tms.lngLatFromCoord([d.x, d.y]);
Simon Huntdc6adea2015-02-09 22:29:36 -0800407 metaUi = {x: d.x, y: d.y, lng: ll[0], lat: ll[1]};
Simon Hunt1894d792015-02-04 17:09:20 -0800408 }
409 d.metaUi = metaUi;
410 uplink.sendEvent('updateMeta', {
411 id: d.id,
412 'class': d.class,
413 memento: metaUi
414 });
Simon Huntac4c6f72015-02-03 19:50:53 -0800415 }
416
Simon Hunt1894d792015-02-04 17:09:20 -0800417
Simon Huntac4c6f72015-02-03 19:50:53 -0800418 // ==========================
419 // === Devices and hosts - D3 rendering
420
Simon Hunt1894d792015-02-04 17:09:20 -0800421
Simon Huntac4c6f72015-02-03 19:50:53 -0800422 // Returns the newly computed bounding box of the rectangle
423 function adjustRectToFitText(n) {
424 var text = n.select('text'),
425 box = text.node().getBBox(),
426 lab = labelConfig;
427
428 text.attr('text-anchor', 'middle')
429 .attr('y', '-0.8em')
430 .attr('x', lab.imgPad/2);
431
432 // translate the bbox so that it is centered on [x,y]
433 box.x = -box.width / 2;
434 box.y = -box.height / 2;
435
436 // add padding
437 box.x -= (lab.padLR + lab.imgPad/2);
438 box.width += lab.padLR * 2 + lab.imgPad;
439 box.y -= lab.padTB;
440 box.height += lab.padTB * 2;
441
442 return box;
443 }
444
445 function mkSvgClass(d) {
446 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
447 }
448
449 function hostLabel(d) {
450 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
451 return d.labels[idx];
452 }
453 function deviceLabel(d) {
454 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
455 return d.labels[idx];
456 }
457 function trimLabel(label) {
458 return (label && label.trim()) || '';
459 }
460
461 function emptyBox() {
462 return {
463 x: -2,
464 y: -2,
465 width: 4,
466 height: 4
467 };
468 }
469
470
471 function updateDeviceLabel(d) {
472 var label = trimLabel(deviceLabel(d)),
473 noLabel = !label,
474 node = d.el,
Simon Hunt1894d792015-02-04 17:09:20 -0800475 dim = icfg.device.dim,
Simon Huntac4c6f72015-02-03 19:50:53 -0800476 devCfg = deviceIconConfig,
477 box, dx, dy;
478
479 node.select('text')
480 .text(label)
481 .style('opacity', 0)
482 .transition()
483 .style('opacity', 1);
484
485 if (noLabel) {
486 box = emptyBox();
487 dx = -dim/2;
488 dy = -dim/2;
489 } else {
490 box = adjustRectToFitText(node);
491 dx = box.x + devCfg.xoff;
492 dy = box.y + devCfg.yoff;
493 }
494
495 node.select('rect')
496 .transition()
497 .attr(box);
498
499 node.select('g.deviceIcon')
500 .transition()
501 .attr('transform', sus.translate(dx, dy));
502 }
503
504 function updateHostLabel(d) {
505 var label = trimLabel(hostLabel(d));
506 d.el.select('text').text(label);
507 }
508
Simon Huntac4c6f72015-02-03 19:50:53 -0800509 function updateDeviceColors(d) {
510 if (d) {
511 setDeviceColor(d);
512 } else {
513 node.filter('.device').each(function (d) {
514 setDeviceColor(d);
515 });
516 }
517 }
518
Simon Hunt5724fb42015-02-05 16:59:40 -0800519 function vis(b) {
520 return b ? 'visible' : 'hidden';
521 }
522
523 function toggleHosts() {
524 showHosts = !showHosts;
525 updateHostVisibility();
526 flash.flash('Hosts ' + vis(showHosts));
527 }
528
529 function toggleOffline() {
530 showOffline = !showOffline;
531 updateOfflineVisibility();
532 flash.flash('Offline devices ' + vis(showOffline));
533 }
534
535 function cycleDeviceLabels() {
Simon Hunt1c367112015-02-05 18:02:46 -0800536 deviceLabelIndex = (deviceLabelIndex+1) % 3;
Simon Huntdc6adea2015-02-09 22:29:36 -0800537 tms.findDevices().forEach(function (d) {
Simon Hunt1c367112015-02-05 18:02:46 -0800538 updateDeviceLabel(d);
539 });
Simon Hunt5724fb42015-02-05 16:59:40 -0800540 }
541
Simon Hunt445e8152015-02-06 13:00:12 -0800542 function unpin() {
Simon Hunt08f841d02015-02-10 14:39:20 -0800543 var hov = tss.hovered();
544 if (hov) {
545 sendUpdateMeta(hov, true);
546 hov.fixed = false;
547 hov.el.classed('fixed', false);
Simon Hunt445e8152015-02-06 13:00:12 -0800548 fResume();
549 }
550 }
551
Simon Hunta142dd22015-02-12 22:07:51 -0800552 function showMastership(masterId) {
553 if (!masterId) {
554 restoreLayerState();
555 } else {
556 showMastershipFor(masterId);
557 }
558 }
559
560 function restoreLayerState() {
561 // NOTE: this level of indirection required, for when we have
562 // the layer filter functionality re-implemented
563 suppressLayers(false);
564 }
565
566 function showMastershipFor(id) {
567 suppressLayers(true);
568 node.each(function (n) {
569 if (n.master === id) {
570 n.el.classed('suppressed', false);
571 }
572 });
573 }
574
575 function suppressLayers(b) {
576 node.classed('suppressed', b);
577 link.classed('suppressed', b);
578// d3.selectAll('svg .port').classed('inactive', b);
579// d3.selectAll('svg .portText').classed('inactive', b);
580 }
Simon Hunt445e8152015-02-06 13:00:12 -0800581
Simon Hunt5724fb42015-02-05 16:59:40 -0800582 // ==========================================
583
Simon Huntac4c6f72015-02-03 19:50:53 -0800584 var dCol = {
585 black: '#000',
586 paleblue: '#acf',
587 offwhite: '#ddd',
Simon Hunt78c10bf2015-02-03 21:18:20 -0800588 darkgrey: '#444',
Simon Huntac4c6f72015-02-03 19:50:53 -0800589 midgrey: '#888',
590 lightgrey: '#bbb',
591 orange: '#f90'
592 };
593
594 // note: these are the device icon colors without affinity
595 var dColTheme = {
596 light: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800597 rfill: dCol.offwhite,
Simon Huntac4c6f72015-02-03 19:50:53 -0800598 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800599 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800600 rect: dCol.paleblue
601 },
602 offline: {
603 glyph: dCol.midgrey,
604 rect: dCol.lightgrey
605 }
606 },
Simon Huntac4c6f72015-02-03 19:50:53 -0800607 dark: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800608 rfill: dCol.midgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800609 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800610 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800611 rect: dCol.paleblue
612 },
613 offline: {
614 glyph: dCol.midgrey,
Simon Hunt78c10bf2015-02-03 21:18:20 -0800615 rect: dCol.darkgrey
Simon Huntac4c6f72015-02-03 19:50:53 -0800616 }
617 }
618 };
619
620 function devBaseColor(d) {
621 var o = d.online ? 'online' : 'offline';
622 return dColTheme[ts.theme()][o];
623 }
624
625 function setDeviceColor(d) {
626 var o = d.online,
627 s = d.el.classed('selected'),
628 c = devBaseColor(d),
629 a = instColor(d.master, o),
Simon Hunt51056592015-02-03 21:48:07 -0800630 icon = d.el.select('g.deviceIcon'),
631 g, r;
Simon Huntac4c6f72015-02-03 19:50:53 -0800632
633 if (s) {
634 g = c.glyph;
635 r = dCol.orange;
636 } else if (tis.isVisible()) {
637 g = o ? a : c.glyph;
Simon Hunt78c10bf2015-02-03 21:18:20 -0800638 r = o ? c.rfill : a;
Simon Huntac4c6f72015-02-03 19:50:53 -0800639 } else {
640 g = c.glyph;
641 r = c.rect;
642 }
643
Simon Hunt51056592015-02-03 21:48:07 -0800644 icon.select('use').style('fill', g);
645 icon.select('rect').style('fill', r);
Simon Huntac4c6f72015-02-03 19:50:53 -0800646 }
647
648 function instColor(id, online) {
649 return sus.cat7().getColor(id, !online, ts.theme());
650 }
651
Simon Hunt1894d792015-02-04 17:09:20 -0800652 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800653
654 function updateNodes() {
Simon Hunt1894d792015-02-04 17:09:20 -0800655 // select all the nodes in the layout:
Simon Huntac4c6f72015-02-03 19:50:53 -0800656 node = nodeG.selectAll('.node')
657 .data(network.nodes, function (d) { return d.id; });
658
Simon Hunt1894d792015-02-04 17:09:20 -0800659 // operate on existing nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800660 node.filter('.device').each(deviceExisting);
661 node.filter('.host').each(hostExisting);
Simon Huntac4c6f72015-02-03 19:50:53 -0800662
663 // operate on entering nodes:
664 var entering = node.enter()
665 .append('g')
666 .attr({
667 id: function (d) { return sus.safeId(d.id); },
668 class: mkSvgClass,
669 transform: function (d) { return sus.translate(d.x, d.y); },
670 opacity: 0
671 })
672 .call(drag)
Simon Hunt08f841d02015-02-10 14:39:20 -0800673 .on('mouseover', tss.nodeMouseOver)
674 .on('mouseout', tss.nodeMouseOut)
Simon Huntac4c6f72015-02-03 19:50:53 -0800675 .transition()
676 .attr('opacity', 1);
677
Simon Hunt1894d792015-02-04 17:09:20 -0800678 // augment entering nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800679 entering.filter('.device').each(deviceEnter);
680 entering.filter('.host').each(hostEnter);
Simon Huntac4c6f72015-02-03 19:50:53 -0800681
Simon Hunt51056592015-02-03 21:48:07 -0800682 // operate on both existing and new nodes:
Simon Huntac4c6f72015-02-03 19:50:53 -0800683 updateDeviceColors();
684
685 // operate on exiting nodes:
686 // Note that the node is removed after 2 seconds.
687 // Sub element animations should be shorter than 2 seconds.
688 var exiting = node.exit()
689 .transition()
690 .duration(2000)
691 .style('opacity', 0)
692 .remove();
693
Simon Hunt1894d792015-02-04 17:09:20 -0800694 // exiting node specifics:
Simon Hunt51056592015-02-03 21:48:07 -0800695 exiting.filter('.host').each(hostExit);
696 exiting.filter('.device').each(deviceExit);
Simon Huntac4c6f72015-02-03 19:50:53 -0800697
Simon Hunt51056592015-02-03 21:48:07 -0800698 // finally, resume the force layout
Simon Huntac4c6f72015-02-03 19:50:53 -0800699 fResume();
700 }
701
Simon Hunt51056592015-02-03 21:48:07 -0800702 // ==========================
703 // updateNodes - subfunctions
704
705 function deviceExisting(d) {
706 var node = d.el;
707 node.classed('online', d.online);
708 updateDeviceLabel(d);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800709 tms.positionNode(d, true);
Simon Hunt51056592015-02-03 21:48:07 -0800710 }
711
712 function hostExisting(d) {
713 updateHostLabel(d);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800714 tms.positionNode(d, true);
Simon Hunt51056592015-02-03 21:48:07 -0800715 }
716
717 function deviceEnter(d) {
718 var node = d3.select(this),
719 glyphId = d.type || 'unknown',
720 label = trimLabel(deviceLabel(d)),
721 devCfg = deviceIconConfig,
722 noLabel = !label,
723 box, dx, dy, icon;
724
725 d.el = node;
726
727 node.append('rect').attr({ rx: 5, ry: 5 });
728 node.append('text').text(label).attr('dy', '1.1em');
729 box = adjustRectToFitText(node);
730 node.select('rect').attr(box);
731
732 icon = is.addDeviceIcon(node, glyphId);
733
734 if (noLabel) {
735 dx = -icon.dim/2;
736 dy = -icon.dim/2;
737 } else {
738 box = adjustRectToFitText(node);
739 dx = box.x + devCfg.xoff;
740 dy = box.y + devCfg.yoff;
741 }
742
743 icon.attr('transform', sus.translate(dx, dy));
744 }
745
746 function hostEnter(d) {
Simon Hunt1894d792015-02-04 17:09:20 -0800747 var node = d3.select(this),
748 gid = d.type || 'unknown',
749 rad = icfg.host.radius,
750 r = d.type ? rad.withGlyph : rad.noGlyph,
751 textDy = r + 10;
Simon Hunt51056592015-02-03 21:48:07 -0800752
753 d.el = node;
Simon Hunt18bf9822015-02-12 17:35:45 -0800754 sus.visible(node, showHosts);
Simon Hunt51056592015-02-03 21:48:07 -0800755
Simon Hunt1894d792015-02-04 17:09:20 -0800756 is.addHostIcon(node, r, gid);
Simon Hunt51056592015-02-03 21:48:07 -0800757
Simon Hunt51056592015-02-03 21:48:07 -0800758 node.append('text')
759 .text(hostLabel)
Simon Hunt1894d792015-02-04 17:09:20 -0800760 .attr('dy', textDy)
Simon Hunt51056592015-02-03 21:48:07 -0800761 .attr('text-anchor', 'middle');
762 }
763
764 function hostExit(d) {
765 var node = d.el;
766 node.select('use')
767 .style('opacity', 0.5)
768 .transition()
769 .duration(800)
770 .style('opacity', 0);
771
772 node.select('text')
773 .style('opacity', 0.5)
774 .transition()
775 .duration(800)
776 .style('opacity', 0);
777
778 node.select('circle')
779 .style('stroke-fill', '#555')
780 .style('fill', '#888')
781 .style('opacity', 0.5)
782 .transition()
783 .duration(1500)
784 .attr('r', 0);
785 }
786
787 function deviceExit(d) {
788 var node = d.el;
789 node.select('use')
790 .style('opacity', 0.5)
791 .transition()
792 .duration(800)
793 .style('opacity', 0);
794
795 node.selectAll('rect')
796 .style('stroke-fill', '#555')
797 .style('fill', '#888')
798 .style('opacity', 0.5);
799 }
800
Simon Hunt1894d792015-02-04 17:09:20 -0800801 // ==========================
802
803 function updateLinks() {
804 var th = ts.theme();
805
806 link = linkG.selectAll('.link')
807 .data(network.links, function (d) { return d.key; });
808
809 // operate on existing links:
Simon Hunta142dd22015-02-12 22:07:51 -0800810 link.each(linkExisting);
Simon Hunt1894d792015-02-04 17:09:20 -0800811
812 // operate on entering links:
813 var entering = link.enter()
814 .append('line')
815 .attr({
816 x1: function (d) { return d.x1; },
817 y1: function (d) { return d.y1; },
818 x2: function (d) { return d.x2; },
819 y2: function (d) { return d.y2; },
820 stroke: linkConfig[th].inColor,
821 'stroke-width': linkConfig.inWidth
822 });
823
824 // augment links
825 entering.each(linkEntering);
826
827 // operate on both existing and new links:
828 //link.each(...)
829
830 // apply or remove labels
831 var labelData = getLabelData();
832 applyLinkLabels(labelData);
833
834 // operate on exiting links:
835 link.exit()
836 .attr('stroke-dasharray', '3 3')
Simon Hunt5724fb42015-02-05 16:59:40 -0800837 .attr('stroke', linkConfig[th].outColor)
Simon Hunt1894d792015-02-04 17:09:20 -0800838 .style('opacity', 0.5)
839 .transition()
840 .duration(1500)
841 .attr({
842 'stroke-dasharray': '3 12',
Simon Hunt1894d792015-02-04 17:09:20 -0800843 'stroke-width': linkConfig.outWidth
844 })
845 .style('opacity', 0.0)
846 .remove();
847
848 // NOTE: invoke a single tick to force the labels to position
849 // onto their links.
850 tick();
Simon Hunt5724fb42015-02-05 16:59:40 -0800851 // TODO: this causes undesirable behavior when in oblique view
Simon Hunt1894d792015-02-04 17:09:20 -0800852 // It causes the nodes to jump into "overhead" view positions, even
853 // though the oblique planes are still showing...
854 }
855
856 // ==========================
857 // updateLinks - subfunctions
858
859 function getLabelData() {
860 // create the backing data for showing labels..
861 var data = [];
862 link.each(function (d) {
863 if (d.label) {
864 data.push({
865 id: 'lab-' + d.key,
866 key: d.key,
867 label: d.label,
868 ldata: d
869 });
870 }
871 });
872 return data;
873 }
874
Simon Hunta142dd22015-02-12 22:07:51 -0800875 function linkExisting(d) {
876 restyleLinkElement(d, true);
877 }
Simon Hunt1894d792015-02-04 17:09:20 -0800878
879 function linkEntering(d) {
880 var link = d3.select(this);
881 d.el = link;
882 restyleLinkElement(d);
883 if (d.type() === 'hostLink') {
Simon Hunt18bf9822015-02-12 17:35:45 -0800884 sus.visible(link, showHosts);
Simon Hunt1894d792015-02-04 17:09:20 -0800885 }
886 }
887
888 //function linkExiting(d) { }
889
890 var linkLabelOffset = '0.3em';
891
892 function applyLinkLabels(data) {
893 var entering;
894
895 linkLabel = linkLabelG.selectAll('.linkLabel')
896 .data(data, function (d) { return d.id; });
897
898 // for elements already existing, we need to update the text
899 // and adjust the rectangle size to fit
900 linkLabel.each(function (d) {
901 var el = d3.select(this),
902 rect = el.select('rect'),
903 text = el.select('text');
904 text.text(d.label);
905 rect.attr(rectAroundText(el));
906 });
907
908 entering = linkLabel.enter().append('g')
909 .classed('linkLabel', true)
910 .attr('id', function (d) { return d.id; });
911
912 entering.each(function (d) {
913 var el = d3.select(this),
914 rect,
915 text,
916 parms = {
917 x1: d.ldata.x1,
918 y1: d.ldata.y1,
919 x2: d.ldata.x2,
920 y2: d.ldata.y2
921 };
922
923 d.el = el;
924 rect = el.append('rect');
925 text = el.append('text').text(d.label);
926 rect.attr(rectAroundText(el));
927 text.attr('dy', linkLabelOffset);
928
929 el.attr('transform', transformLabel(parms));
930 });
931
932 // Remove any labels that are no longer required.
933 linkLabel.exit().remove();
934 }
935
936 function rectAroundText(el) {
937 var text = el.select('text'),
938 box = text.node().getBBox();
939
940 // translate the bbox so that it is centered on [x,y]
941 box.x = -box.width / 2;
942 box.y = -box.height / 2;
943
944 // add padding
945 box.x -= 1;
946 box.width += 2;
947 return box;
948 }
949
950 function transformLabel(p) {
951 var dx = p.x2 - p.x1,
952 dy = p.y2 - p.y1,
953 xMid = dx/2 + p.x1,
954 yMid = dy/2 + p.y1;
955 return sus.translate(xMid, yMid);
956 }
Simon Huntac4c6f72015-02-03 19:50:53 -0800957
958 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -0800959 // force layout tick function
Simon Hunt737c89f2015-01-28 12:23:19 -0800960
Simon Hunt5724fb42015-02-05 16:59:40 -0800961 function fResume() {
962 if (!oblique) {
963 force.resume();
964 }
965 }
966
967 function fStart() {
968 if (!oblique) {
969 force.start();
970 }
971 }
972
973 var tickStuff = {
974 nodeAttr: {
975 transform: function (d) { return sus.translate(d.x, d.y); }
976 },
977 linkAttr: {
978 x1: function (d) { return d.source.x; },
979 y1: function (d) { return d.source.y; },
980 x2: function (d) { return d.target.x; },
981 y2: function (d) { return d.target.y; }
982 },
983 linkLabelAttr: {
984 transform: function (d) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800985 var lnk = tms.findLinkById(d.key);
Simon Hunt5724fb42015-02-05 16:59:40 -0800986 if (lnk) {
987 return transformLabel({
988 x1: lnk.source.x,
989 y1: lnk.source.y,
990 x2: lnk.target.x,
991 y2: lnk.target.y
992 });
993 }
994 }
995 }
996 };
997
998 function tick() {
999 node.attr(tickStuff.nodeAttr);
1000 link.attr(tickStuff.linkAttr);
1001 linkLabel.attr(tickStuff.linkLabelAttr);
Simon Hunt737c89f2015-01-28 12:23:19 -08001002 }
1003
1004
Simon Huntac4c6f72015-02-03 19:50:53 -08001005 // ==========================
1006 // === MOUSE GESTURE HANDLERS
1007
Simon Hunt205099e2015-02-07 13:12:01 -08001008 function zoomingOrPanning(ev) {
1009 return ev.metaKey || ev.altKey;
Simon Hunt445e8152015-02-06 13:00:12 -08001010 }
1011
1012 function atDragEnd(d) {
1013 // once we've finished moving, pin the node in position
1014 d.fixed = true;
1015 d3.select(this).classed('fixed', true);
1016 sendUpdateMeta(d);
1017 }
1018
1019 // predicate that indicates when dragging is active
1020 function dragEnabled() {
1021 var ev = d3.event.sourceEvent;
1022 // nodeLock means we aren't allowing nodes to be dragged...
Simon Hunt205099e2015-02-07 13:12:01 -08001023 return !nodeLock && !zoomingOrPanning(ev);
Simon Hunt445e8152015-02-06 13:00:12 -08001024 }
1025
1026 // predicate that indicates when clicking is active
1027 function clickEnabled() {
1028 return true;
1029 }
Simon Hunt737c89f2015-01-28 12:23:19 -08001030
Simon Huntf542d842015-02-11 16:20:33 -08001031 // ==========================
1032 // function entry points for traffic module
1033
1034 var allTrafficClasses = 'primary secondary animated optical';
1035
1036 function clearLinkTrafficStyle() {
1037 link.style('stroke-width', null)
1038 .classed(allTrafficClasses, false);
1039 }
1040
1041 function removeLinkLabels() {
1042 network.links.forEach(function (d) {
1043 d.label = '';
1044 });
1045 }
Simon Hunt737c89f2015-01-28 12:23:19 -08001046
1047 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -08001048 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -08001049
Simon Huntdc6adea2015-02-09 22:29:36 -08001050 function mkModelApi(uplink) {
1051 return {
1052 projection: uplink.projection,
1053 network: network,
1054 restyleLinkElement: restyleLinkElement,
1055 removeLinkElement: removeLinkElement
1056 };
1057 }
1058
Simon Hunt08f841d02015-02-10 14:39:20 -08001059 function mkSelectApi(uplink) {
1060 return {
1061 node: function () { return node; },
1062 zoomingOrPanning: zoomingOrPanning,
1063 updateDeviceColors: updateDeviceColors,
1064 sendEvent: uplink.sendEvent
1065 };
1066 }
1067
Simon Huntf542d842015-02-11 16:20:33 -08001068 function mkTrafficApi(uplink) {
1069 return {
1070 clearLinkTrafficStyle: clearLinkTrafficStyle,
1071 removeLinkLabels: removeLinkLabels,
1072 updateLinks: updateLinks,
1073 findLinkById: tms.findLinkById,
1074 hovered: tss.hovered,
1075 validateSelectionContext: tss.validateSelectionContext,
1076 selectOrder: tss.selectOrder,
1077 sendEvent: uplink.sendEvent
1078 }
1079 }
1080
Simon Hunt737c89f2015-01-28 12:23:19 -08001081 angular.module('ovTopo')
1082 .factory('TopoForceService',
Simon Hunt1894d792015-02-04 17:09:20 -08001083 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
Simon Hunt3a6eec02015-02-09 21:16:43 -08001084 'FlashService', 'TopoInstService', 'TopoModelService',
Simon Huntf542d842015-02-11 16:20:33 -08001085 'TopoSelectService', 'TopoTrafficService',
Simon Hunt737c89f2015-01-28 12:23:19 -08001086
Simon Huntf542d842015-02-11 16:20:33 -08001087 function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_,
1088 _tis_, _tms_, _tss_, _tts_) {
Simon Hunt737c89f2015-01-28 12:23:19 -08001089 $log = _$log_;
Simon Hunt1894d792015-02-04 17:09:20 -08001090 fs = _fs_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001091 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001092 is = _is_;
1093 ts = _ts_;
Simon Hunt5724fb42015-02-05 16:59:40 -08001094 flash = _flash_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001095 tis = _tis_;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001096 tms = _tms_;
Simon Hunt08f841d02015-02-10 14:39:20 -08001097 tss = _tss_;
Simon Huntf542d842015-02-11 16:20:33 -08001098 tts = _tts_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001099
Simon Hunt1894d792015-02-04 17:09:20 -08001100 icfg = is.iconConfig();
1101
Simon Hunta142dd22015-02-12 22:07:51 -08001102 var themeListener = ts.addListener(function () {
1103 updateLinks();
1104 updateNodes();
1105 });
1106
Simon Hunt737c89f2015-01-28 12:23:19 -08001107 // forceG is the SVG group to display the force layout in
Simon Huntdc6adea2015-02-09 22:29:36 -08001108 // uplink is the api from the main topo source file
Simon Hunt3a6eec02015-02-09 21:16:43 -08001109 // dim is the initial dimensions of the SVG as [w,h]
Simon Hunt737c89f2015-01-28 12:23:19 -08001110 // opts are, well, optional :)
Simon Hunt3a6eec02015-02-09 21:16:43 -08001111 function initForce(forceG, _uplink_, _dim_, opts) {
Simon Hunt1894d792015-02-04 17:09:20 -08001112 uplink = _uplink_;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001113 dim = _dim_;
1114
1115 $log.debug('initForce().. dim = ' + dim);
1116
Simon Huntdc6adea2015-02-09 22:29:36 -08001117 tms.initModel(mkModelApi(uplink), dim);
Simon Hunt08f841d02015-02-10 14:39:20 -08001118 tss.initSelect(mkSelectApi(uplink));
Simon Huntf542d842015-02-11 16:20:33 -08001119 tts.initTraffic(mkTrafficApi(uplink));
Simon Hunta11b4eb2015-01-28 16:20:50 -08001120
Simon Hunt737c89f2015-01-28 12:23:19 -08001121 settings = angular.extend({}, defaultSettings, opts);
1122
1123 linkG = forceG.append('g').attr('id', 'topo-links');
1124 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
1125 nodeG = forceG.append('g').attr('id', 'topo-nodes');
1126
1127 link = linkG.selectAll('.link');
1128 linkLabel = linkLabelG.selectAll('.linkLabel');
1129 node = nodeG.selectAll('.node');
1130
1131 force = d3.layout.force()
Simon Hunt3a6eec02015-02-09 21:16:43 -08001132 .size(dim)
Simon Hunt737c89f2015-01-28 12:23:19 -08001133 .nodes(network.nodes)
1134 .links(network.links)
1135 .gravity(settings.gravity)
1136 .friction(settings.friction)
1137 .charge(settings.charge._def_)
1138 .linkDistance(settings.linkDistance._def_)
1139 .linkStrength(settings.linkStrength._def_)
1140 .on('tick', tick);
1141
1142 drag = sus.createDragBehavior(force,
Simon Hunt08f841d02015-02-10 14:39:20 -08001143 tss.selectObject, atDragEnd, dragEnabled, clickEnabled);
Simon Hunt737c89f2015-01-28 12:23:19 -08001144 }
1145
Simon Hunt3a6eec02015-02-09 21:16:43 -08001146 function newDim(_dim_) {
1147 dim = _dim_;
1148 force.size(dim);
1149 tms.newDim(dim);
Simon Hunt737c89f2015-01-28 12:23:19 -08001150 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -08001151 }
1152
Simon Hunt3a6eec02015-02-09 21:16:43 -08001153 function destroyForce() {
Simon Huntf542d842015-02-11 16:20:33 -08001154 tts.destroyTraffic();
1155 tss.destroySelect();
1156 tms.destroyModel();
Simon Hunta142dd22015-02-12 22:07:51 -08001157 ts.removeListener(themeListener);
1158 themeListener = null;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001159 }
1160
Simon Hunt737c89f2015-01-28 12:23:19 -08001161 return {
1162 initForce: initForce,
Simon Hunt3a6eec02015-02-09 21:16:43 -08001163 newDim: newDim,
1164 destroyForce: destroyForce,
Simon Huntac4c6f72015-02-03 19:50:53 -08001165
1166 updateDeviceColors: updateDeviceColors,
Simon Hunt5724fb42015-02-05 16:59:40 -08001167 toggleHosts: toggleHosts,
1168 toggleOffline: toggleOffline,
1169 cycleDeviceLabels: cycleDeviceLabels,
Simon Hunt445e8152015-02-06 13:00:12 -08001170 unpin: unpin,
Simon Hunta142dd22015-02-12 22:07:51 -08001171 showMastership: showMastership,
Simon Huntac4c6f72015-02-03 19:50:53 -08001172
1173 addDevice: addDevice,
Simon Hunt1894d792015-02-04 17:09:20 -08001174 updateDevice: updateDevice,
1175 removeDevice: removeDevice,
1176 addHost: addHost,
1177 updateHost: updateHost,
1178 removeHost: removeHost,
1179 addLink: addLink,
1180 updateLink: updateLink,
1181 removeLink: removeLink
Simon Hunt737c89f2015-01-28 12:23:19 -08001182 };
1183 }]);
1184}());