blob: 7dadaeba9bf20cd36767f2ab49a19b3c18383204 [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 Hunteb0fa052015-02-17 19:20:28 -080026 var $log, fs, sus, is, ts, flash, tis, tms, tss, tts, fltr,
27 icfg, uplink;
Simon Huntac4c6f72015-02-03 19:50:53 -080028
29 // configuration
30 var labelConfig = {
31 imgPad: 16,
32 padLR: 4,
33 padTB: 3,
34 marginLR: 3,
35 marginTB: 2,
36 port: {
37 gap: 3,
38 width: 18,
39 height: 14
40 }
41 };
42
43 var deviceIconConfig = {
44 xoff: -20,
45 yoff: -18
46 };
Simon Hunt737c89f2015-01-28 12:23:19 -080047
Simon Hunt1894d792015-02-04 17:09:20 -080048 var linkConfig = {
49 light: {
50 baseColor: '#666',
51 inColor: '#66f',
Simon Hunt3a6eec02015-02-09 21:16:43 -080052 outColor: '#f00'
Simon Hunt1894d792015-02-04 17:09:20 -080053 },
54 dark: {
Simon Hunt5724fb42015-02-05 16:59:40 -080055 baseColor: '#aaa',
Simon Hunt1894d792015-02-04 17:09:20 -080056 inColor: '#66f',
Simon Hunt5724fb42015-02-05 16:59:40 -080057 outColor: '#f66'
Simon Hunt1894d792015-02-04 17:09:20 -080058 },
59 inWidth: 12,
60 outWidth: 10
61 };
62
Simon Hunt737c89f2015-01-28 12:23:19 -080063 // internal state
Simon Huntac4c6f72015-02-03 19:50:53 -080064 var settings, // merged default settings and options
Simon Hunt737c89f2015-01-28 12:23:19 -080065 force, // force layout object
66 drag, // drag behavior handler
67 network = {
68 nodes: [],
69 links: [],
70 lookup: {},
71 revLinkToKey: {}
Simon Huntac4c6f72015-02-03 19:50:53 -080072 },
Simon Hunt1894d792015-02-04 17:09:20 -080073 lu = network.lookup, // shorthand
Simon Huntdc6adea2015-02-09 22:29:36 -080074 rlk = network.revLinkToKey,
Simon Huntac4c6f72015-02-03 19:50:53 -080075 deviceLabelIndex = 0, // for device label cycling
Simon Hunt1894d792015-02-04 17:09:20 -080076 hostLabelIndex = 0, // for host label cycling
Simon Hunta142dd22015-02-12 22:07:51 -080077 showHosts = false, // whether hosts are displayed
Simon Hunt5724fb42015-02-05 16:59:40 -080078 showOffline = true, // whether offline devices are displayed
79 oblique = false, // whether we are in the oblique view
Simon Hunt445e8152015-02-06 13:00:12 -080080 nodeLock = false, // whether nodes can be dragged or not (locked)
Simon Hunt08f841d02015-02-10 14:39:20 -080081 dim; // the dimensions of the force layout [w,h]
Simon Hunt737c89f2015-01-28 12:23:19 -080082
83 // SVG elements;
84 var linkG, linkLabelG, nodeG;
85
86 // D3 selections;
87 var link, linkLabel, node;
88
89 // default settings for force layout
90 var defaultSettings = {
91 gravity: 0.4,
92 friction: 0.7,
93 charge: {
94 // note: key is node.class
95 device: -8000,
96 host: -5000,
97 _def_: -12000
98 },
99 linkDistance: {
100 // note: key is link.type
101 direct: 100,
102 optical: 120,
103 hostLink: 3,
104 _def_: 50
105 },
106 linkStrength: {
107 // note: key is link.type
108 // range: {0.0 ... 1.0}
109 //direct: 1.0,
110 //optical: 1.0,
111 //hostLink: 1.0,
112 _def_: 1.0
113 }
114 };
115
116
Simon Huntac4c6f72015-02-03 19:50:53 -0800117 // ==========================
118 // === EVENT HANDLERS
119
120 function addDevice(data) {
121 var id = data.id,
122 d;
123
Simon Hunt1894d792015-02-04 17:09:20 -0800124 uplink.showNoDevs(false);
Simon Huntac4c6f72015-02-03 19:50:53 -0800125
126 // although this is an add device event, if we already have the
127 // device, treat it as an update instead..
Simon Hunt1894d792015-02-04 17:09:20 -0800128 if (lu[id]) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800129 updateDevice(data);
130 return;
131 }
132
Simon Hunt3a6eec02015-02-09 21:16:43 -0800133 d = tms.createDeviceNode(data);
Simon Huntac4c6f72015-02-03 19:50:53 -0800134 network.nodes.push(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800135 lu[id] = d;
Simon Huntac4c6f72015-02-03 19:50:53 -0800136 updateNodes();
137 fStart();
138 }
139
140 function updateDevice(data) {
141 var id = data.id,
Simon Hunt1894d792015-02-04 17:09:20 -0800142 d = lu[id],
Simon Huntac4c6f72015-02-03 19:50:53 -0800143 wasOnline;
144
145 if (d) {
146 wasOnline = d.online;
147 angular.extend(d, data);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800148 if (tms.positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800149 sendUpdateMeta(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800150 }
151 updateNodes();
152 if (wasOnline !== d.online) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800153 tms.findAttachedLinks(d.id).forEach(restyleLinkElement);
Simon Hunt5724fb42015-02-05 16:59:40 -0800154 updateOfflineVisibility(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800155 }
156 } else {
157 // TODO: decide whether we want to capture logic errors
158 //logicError('updateDevice lookup fail. ID = "' + id + '"');
159 }
160 }
161
Simon Hunt1894d792015-02-04 17:09:20 -0800162 function removeDevice(data) {
163 var id = data.id,
164 d = lu[id];
165 if (d) {
166 removeDeviceElement(d);
167 } else {
168 // TODO: decide whether we want to capture logic errors
169 //logicError('removeDevice lookup fail. ID = "' + id + '"');
170 }
171 }
172
173 function addHost(data) {
174 var id = data.id,
175 d, lnk;
176
177 // although this is an add host event, if we already have the
178 // host, treat it as an update instead..
179 if (lu[id]) {
180 updateHost(data);
181 return;
182 }
183
Simon Hunt3a6eec02015-02-09 21:16:43 -0800184 d = tms.createHostNode(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800185 network.nodes.push(d);
186 lu[id] = d;
Simon Hunt1894d792015-02-04 17:09:20 -0800187 updateNodes();
188
Simon Hunt3a6eec02015-02-09 21:16:43 -0800189 lnk = tms.createHostLink(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800190 if (lnk) {
Simon Hunt1894d792015-02-04 17:09:20 -0800191 d.linkData = lnk; // cache ref on its host
192 network.links.push(lnk);
193 lu[d.ingress] = lnk;
194 lu[d.egress] = lnk;
195 updateLinks();
196 }
197
198 fStart();
199 }
200
201 function updateHost(data) {
202 var id = data.id,
203 d = lu[id];
204 if (d) {
205 angular.extend(d, data);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800206 if (tms.positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800207 sendUpdateMeta(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800208 }
209 updateNodes();
210 } else {
211 // TODO: decide whether we want to capture logic errors
212 //logicError('updateHost lookup fail. ID = "' + id + '"');
213 }
214 }
215
216 function removeHost(data) {
217 var id = data.id,
218 d = lu[id];
219 if (d) {
220 removeHostElement(d, true);
221 } else {
222 // may have already removed host, if attached to removed device
223 //console.warn('removeHost lookup fail. ID = "' + id + '"');
224 }
225 }
226
227 function addLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800228 var result = tms.findLink(data, 'add'),
Simon Hunt1894d792015-02-04 17:09:20 -0800229 bad = result.badLogic,
230 d = result.ldata;
231
232 if (bad) {
233 //logicError(bad + ': ' + link.id);
234 return;
235 }
236
237 if (d) {
238 // we already have a backing store link for src/dst nodes
239 addLinkUpdate(d, data);
240 return;
241 }
242
243 // no backing store link yet
Simon Hunt3a6eec02015-02-09 21:16:43 -0800244 d = tms.createLink(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800245 if (d) {
246 network.links.push(d);
247 lu[d.key] = d;
248 updateLinks();
249 fStart();
250 }
251 }
252
253 function updateLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800254 var result = tms.findLink(data, 'update'),
Simon Hunt1894d792015-02-04 17:09:20 -0800255 bad = result.badLogic;
256 if (bad) {
257 //logicError(bad + ': ' + link.id);
258 return;
259 }
260 result.updateWith(link);
261 }
262
263 function removeLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800264 var result = tms.findLink(data, 'remove'),
Simon Hunt1894d792015-02-04 17:09:20 -0800265 bad = result.badLogic;
266 if (bad) {
267 // may have already removed link, if attached to removed device
268 //console.warn(bad + ': ' + link.id);
269 return;
270 }
271 result.removeRawLink();
272 }
273
274 // ========================
275
276 function addLinkUpdate(ldata, link) {
277 // add link event, but we already have the reverse link installed
278 ldata.fromTarget = link;
Simon Huntdc6adea2015-02-09 22:29:36 -0800279 rlk[link.id] = ldata.key;
Simon Hunt1894d792015-02-04 17:09:20 -0800280 restyleLinkElement(ldata);
281 }
282
Simon Hunt1894d792015-02-04 17:09:20 -0800283
284 var widthRatio = 1.4,
285 linkScale = d3.scale.linear()
286 .domain([1, 12])
287 .range([widthRatio, 12 * widthRatio])
Simon Hunt5724fb42015-02-05 16:59:40 -0800288 .clamp(true),
Simon Hunt3a6eec02015-02-09 21:16:43 -0800289 allLinkTypes = 'direct indirect optical tunnel';
Simon Hunt1894d792015-02-04 17:09:20 -0800290
Simon Hunta142dd22015-02-12 22:07:51 -0800291 function restyleLinkElement(ldata, immediate) {
Simon Hunt1894d792015-02-04 17:09:20 -0800292 // this fn's job is to look at raw links and decide what svg classes
293 // need to be applied to the line element in the DOM
294 var th = ts.theme(),
295 el = ldata.el,
296 type = ldata.type(),
297 lw = ldata.linkWidth(),
Simon Hunta142dd22015-02-12 22:07:51 -0800298 online = ldata.online(),
299 delay = immediate ? 0 : 1000;
Simon Hunt1894d792015-02-04 17:09:20 -0800300
301 el.classed('link', true);
302 el.classed('inactive', !online);
303 el.classed(allLinkTypes, false);
304 if (type) {
305 el.classed(type, true);
306 }
307 el.transition()
Simon Hunta142dd22015-02-12 22:07:51 -0800308 .duration(delay)
Simon Hunt1894d792015-02-04 17:09:20 -0800309 .attr('stroke-width', linkScale(lw))
310 .attr('stroke', linkConfig[th].baseColor);
311 }
312
Simon Hunt1894d792015-02-04 17:09:20 -0800313 function removeLinkElement(d) {
314 var idx = fs.find(d.key, network.links, 'key'),
315 removed;
316 if (idx >=0) {
317 // remove from links array
318 removed = network.links.splice(idx, 1);
319 // remove from lookup cache
320 delete lu[removed[0].key];
321 updateLinks();
322 fResume();
323 }
324 }
325
326 function removeHostElement(d, upd) {
327 // first, remove associated hostLink...
328 removeLinkElement(d.linkData);
329
330 // remove hostLink bindings
331 delete lu[d.ingress];
332 delete lu[d.egress];
333
334 // remove from lookup cache
335 delete lu[d.id];
336 // remove from nodes array
337 var idx = fs.find(d.id, network.nodes);
338 network.nodes.splice(idx, 1);
339
340 // remove from SVG
341 // NOTE: upd is false if we were called from removeDeviceElement()
342 if (upd) {
343 updateNodes();
344 fResume();
345 }
346 }
347
348 function removeDeviceElement(d) {
349 var id = d.id;
350 // first, remove associated hosts and links..
Simon Huntdc6adea2015-02-09 22:29:36 -0800351 tms.findAttachedHosts(id).forEach(removeHostElement);
352 tms.findAttachedLinks(id).forEach(removeLinkElement);
Simon Hunt1894d792015-02-04 17:09:20 -0800353
354 // remove from lookup cache
355 delete lu[id];
356 // remove from nodes array
357 var idx = fs.find(id, network.nodes);
358 network.nodes.splice(idx, 1);
359
360 if (!network.nodes.length) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800361 uplink.showNoDevs(true);
Simon Hunt1894d792015-02-04 17:09:20 -0800362 }
363
364 // remove from SVG
365 updateNodes();
366 fResume();
367 }
368
Simon Hunt5724fb42015-02-05 16:59:40 -0800369 function updateHostVisibility() {
Simon Hunt18bf9822015-02-12 17:35:45 -0800370 sus.visible(nodeG.selectAll('.host'), showHosts);
371 sus.visible(linkG.selectAll('.hostLink'), showHosts);
Simon Hunt5724fb42015-02-05 16:59:40 -0800372 }
373
374 function updateOfflineVisibility(dev) {
375 function updDev(d, show) {
Simon Hunt18bf9822015-02-12 17:35:45 -0800376 sus.visible(d.el, show);
Simon Hunt5724fb42015-02-05 16:59:40 -0800377
Simon Huntdc6adea2015-02-09 22:29:36 -0800378 tms.findAttachedLinks(d.id).forEach(function (link) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800379 b = show && ((link.type() !== 'hostLink') || showHosts);
Simon Hunt18bf9822015-02-12 17:35:45 -0800380 sus.visible(link.el, b);
Simon Hunt5724fb42015-02-05 16:59:40 -0800381 });
Simon Huntdc6adea2015-02-09 22:29:36 -0800382 tms.findAttachedHosts(d.id).forEach(function (host) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800383 b = show && showHosts;
Simon Hunt18bf9822015-02-12 17:35:45 -0800384 sus.visible(host.el, b);
Simon Hunt5724fb42015-02-05 16:59:40 -0800385 });
386 }
387
388 if (dev) {
389 // updating a specific device that just toggled off/on-line
390 updDev(dev, dev.online || showOffline);
391 } else {
392 // updating all offline devices
Simon Huntdc6adea2015-02-09 22:29:36 -0800393 tms.findDevices(true).forEach(function (d) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800394 updDev(d, showOffline);
395 });
396 }
397 }
398
Simon Hunt1894d792015-02-04 17:09:20 -0800399
Simon Hunt445e8152015-02-06 13:00:12 -0800400 function sendUpdateMeta(d, clearPos) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800401 var metaUi = {},
402 ll;
403
Simon Hunt445e8152015-02-06 13:00:12 -0800404 // if we are not clearing the position data (unpinning),
405 // attach the x, y, longitude, latitude...
406 if (!clearPos) {
Simon Hunt3a6eec02015-02-09 21:16:43 -0800407 ll = tms.lngLatFromCoord([d.x, d.y]);
Simon Huntdc6adea2015-02-09 22:29:36 -0800408 metaUi = {x: d.x, y: d.y, lng: ll[0], lat: ll[1]};
Simon Hunt1894d792015-02-04 17:09:20 -0800409 }
410 d.metaUi = metaUi;
411 uplink.sendEvent('updateMeta', {
412 id: d.id,
413 'class': d.class,
414 memento: metaUi
415 });
Simon Huntac4c6f72015-02-03 19:50:53 -0800416 }
417
Simon Hunt1894d792015-02-04 17:09:20 -0800418
Simon Huntac4c6f72015-02-03 19:50:53 -0800419 // ==========================
420 // === Devices and hosts - D3 rendering
421
Simon Hunt1894d792015-02-04 17:09:20 -0800422
Simon Huntac4c6f72015-02-03 19:50:53 -0800423 // Returns the newly computed bounding box of the rectangle
424 function adjustRectToFitText(n) {
425 var text = n.select('text'),
426 box = text.node().getBBox(),
427 lab = labelConfig;
428
429 text.attr('text-anchor', 'middle')
430 .attr('y', '-0.8em')
431 .attr('x', lab.imgPad/2);
432
433 // translate the bbox so that it is centered on [x,y]
434 box.x = -box.width / 2;
435 box.y = -box.height / 2;
436
437 // add padding
438 box.x -= (lab.padLR + lab.imgPad/2);
439 box.width += lab.padLR * 2 + lab.imgPad;
440 box.y -= lab.padTB;
441 box.height += lab.padTB * 2;
442
443 return box;
444 }
445
446 function mkSvgClass(d) {
447 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
448 }
449
450 function hostLabel(d) {
451 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
452 return d.labels[idx];
453 }
454 function deviceLabel(d) {
455 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
456 return d.labels[idx];
457 }
458 function trimLabel(label) {
459 return (label && label.trim()) || '';
460 }
461
462 function emptyBox() {
463 return {
464 x: -2,
465 y: -2,
466 width: 4,
467 height: 4
468 };
469 }
470
471
472 function updateDeviceLabel(d) {
473 var label = trimLabel(deviceLabel(d)),
474 noLabel = !label,
475 node = d.el,
Simon Hunt1894d792015-02-04 17:09:20 -0800476 dim = icfg.device.dim,
Simon Huntac4c6f72015-02-03 19:50:53 -0800477 devCfg = deviceIconConfig,
478 box, dx, dy;
479
480 node.select('text')
481 .text(label)
482 .style('opacity', 0)
483 .transition()
484 .style('opacity', 1);
485
486 if (noLabel) {
487 box = emptyBox();
488 dx = -dim/2;
489 dy = -dim/2;
490 } else {
491 box = adjustRectToFitText(node);
492 dx = box.x + devCfg.xoff;
493 dy = box.y + devCfg.yoff;
494 }
495
496 node.select('rect')
497 .transition()
498 .attr(box);
499
500 node.select('g.deviceIcon')
501 .transition()
502 .attr('transform', sus.translate(dx, dy));
503 }
504
505 function updateHostLabel(d) {
506 var label = trimLabel(hostLabel(d));
507 d.el.select('text').text(label);
508 }
509
Simon Huntac4c6f72015-02-03 19:50:53 -0800510 function updateDeviceColors(d) {
511 if (d) {
512 setDeviceColor(d);
513 } else {
514 node.filter('.device').each(function (d) {
515 setDeviceColor(d);
516 });
517 }
518 }
519
Simon Hunt5724fb42015-02-05 16:59:40 -0800520 function vis(b) {
521 return b ? 'visible' : 'hidden';
522 }
523
524 function toggleHosts() {
525 showHosts = !showHosts;
526 updateHostVisibility();
527 flash.flash('Hosts ' + vis(showHosts));
528 }
529
530 function toggleOffline() {
531 showOffline = !showOffline;
532 updateOfflineVisibility();
533 flash.flash('Offline devices ' + vis(showOffline));
534 }
535
536 function cycleDeviceLabels() {
Simon Hunt1c367112015-02-05 18:02:46 -0800537 deviceLabelIndex = (deviceLabelIndex+1) % 3;
Simon Huntdc6adea2015-02-09 22:29:36 -0800538 tms.findDevices().forEach(function (d) {
Simon Hunt1c367112015-02-05 18:02:46 -0800539 updateDeviceLabel(d);
540 });
Simon Hunt5724fb42015-02-05 16:59:40 -0800541 }
542
Simon Hunt445e8152015-02-06 13:00:12 -0800543 function unpin() {
Simon Hunt08f841d02015-02-10 14:39:20 -0800544 var hov = tss.hovered();
545 if (hov) {
546 sendUpdateMeta(hov, true);
547 hov.fixed = false;
548 hov.el.classed('fixed', false);
Simon Hunt445e8152015-02-06 13:00:12 -0800549 fResume();
550 }
551 }
552
Simon Hunta142dd22015-02-12 22:07:51 -0800553 function showMastership(masterId) {
554 if (!masterId) {
555 restoreLayerState();
556 } else {
557 showMastershipFor(masterId);
558 }
559 }
560
561 function restoreLayerState() {
562 // NOTE: this level of indirection required, for when we have
563 // the layer filter functionality re-implemented
564 suppressLayers(false);
565 }
566
567 function showMastershipFor(id) {
568 suppressLayers(true);
569 node.each(function (n) {
570 if (n.master === id) {
571 n.el.classed('suppressed', false);
572 }
573 });
574 }
575
576 function suppressLayers(b) {
577 node.classed('suppressed', b);
578 link.classed('suppressed', b);
579// d3.selectAll('svg .port').classed('inactive', b);
580// d3.selectAll('svg .portText').classed('inactive', b);
581 }
Simon Hunt445e8152015-02-06 13:00:12 -0800582
Simon Hunt5724fb42015-02-05 16:59:40 -0800583 // ==========================================
584
Simon Huntac4c6f72015-02-03 19:50:53 -0800585 var dCol = {
586 black: '#000',
587 paleblue: '#acf',
588 offwhite: '#ddd',
Simon Hunt78c10bf2015-02-03 21:18:20 -0800589 darkgrey: '#444',
Simon Huntac4c6f72015-02-03 19:50:53 -0800590 midgrey: '#888',
591 lightgrey: '#bbb',
592 orange: '#f90'
593 };
594
595 // note: these are the device icon colors without affinity
596 var dColTheme = {
597 light: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800598 rfill: dCol.offwhite,
Simon Huntac4c6f72015-02-03 19:50:53 -0800599 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800600 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800601 rect: dCol.paleblue
602 },
603 offline: {
604 glyph: dCol.midgrey,
605 rect: dCol.lightgrey
606 }
607 },
Simon Huntac4c6f72015-02-03 19:50:53 -0800608 dark: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800609 rfill: dCol.midgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800610 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800611 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800612 rect: dCol.paleblue
613 },
614 offline: {
615 glyph: dCol.midgrey,
Simon Hunt78c10bf2015-02-03 21:18:20 -0800616 rect: dCol.darkgrey
Simon Huntac4c6f72015-02-03 19:50:53 -0800617 }
618 }
619 };
620
621 function devBaseColor(d) {
622 var o = d.online ? 'online' : 'offline';
623 return dColTheme[ts.theme()][o];
624 }
625
626 function setDeviceColor(d) {
627 var o = d.online,
628 s = d.el.classed('selected'),
629 c = devBaseColor(d),
630 a = instColor(d.master, o),
Simon Hunt51056592015-02-03 21:48:07 -0800631 icon = d.el.select('g.deviceIcon'),
632 g, r;
Simon Huntac4c6f72015-02-03 19:50:53 -0800633
634 if (s) {
635 g = c.glyph;
636 r = dCol.orange;
637 } else if (tis.isVisible()) {
638 g = o ? a : c.glyph;
Simon Hunt78c10bf2015-02-03 21:18:20 -0800639 r = o ? c.rfill : a;
Simon Huntac4c6f72015-02-03 19:50:53 -0800640 } else {
641 g = c.glyph;
642 r = c.rect;
643 }
644
Simon Hunt51056592015-02-03 21:48:07 -0800645 icon.select('use').style('fill', g);
646 icon.select('rect').style('fill', r);
Simon Huntac4c6f72015-02-03 19:50:53 -0800647 }
648
649 function instColor(id, online) {
650 return sus.cat7().getColor(id, !online, ts.theme());
651 }
652
Simon Hunt1894d792015-02-04 17:09:20 -0800653 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800654
655 function updateNodes() {
Simon Hunt1894d792015-02-04 17:09:20 -0800656 // select all the nodes in the layout:
Simon Huntac4c6f72015-02-03 19:50:53 -0800657 node = nodeG.selectAll('.node')
658 .data(network.nodes, function (d) { return d.id; });
659
Simon Hunt1894d792015-02-04 17:09:20 -0800660 // operate on existing nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800661 node.filter('.device').each(deviceExisting);
662 node.filter('.host').each(hostExisting);
Simon Huntac4c6f72015-02-03 19:50:53 -0800663
664 // operate on entering nodes:
665 var entering = node.enter()
666 .append('g')
667 .attr({
668 id: function (d) { return sus.safeId(d.id); },
669 class: mkSvgClass,
670 transform: function (d) { return sus.translate(d.x, d.y); },
671 opacity: 0
672 })
673 .call(drag)
Simon Hunt08f841d02015-02-10 14:39:20 -0800674 .on('mouseover', tss.nodeMouseOver)
675 .on('mouseout', tss.nodeMouseOut)
Simon Huntac4c6f72015-02-03 19:50:53 -0800676 .transition()
677 .attr('opacity', 1);
678
Simon Hunt1894d792015-02-04 17:09:20 -0800679 // augment entering nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800680 entering.filter('.device').each(deviceEnter);
681 entering.filter('.host').each(hostEnter);
Simon Huntac4c6f72015-02-03 19:50:53 -0800682
Simon Hunt51056592015-02-03 21:48:07 -0800683 // operate on both existing and new nodes:
Simon Huntac4c6f72015-02-03 19:50:53 -0800684 updateDeviceColors();
685
686 // operate on exiting nodes:
687 // Note that the node is removed after 2 seconds.
688 // Sub element animations should be shorter than 2 seconds.
689 var exiting = node.exit()
690 .transition()
691 .duration(2000)
692 .style('opacity', 0)
693 .remove();
694
Simon Hunt1894d792015-02-04 17:09:20 -0800695 // exiting node specifics:
Simon Hunt51056592015-02-03 21:48:07 -0800696 exiting.filter('.host').each(hostExit);
697 exiting.filter('.device').each(deviceExit);
Simon Huntac4c6f72015-02-03 19:50:53 -0800698
Simon Hunt51056592015-02-03 21:48:07 -0800699 // finally, resume the force layout
Simon Huntac4c6f72015-02-03 19:50:53 -0800700 fResume();
701 }
702
Simon Hunt51056592015-02-03 21:48:07 -0800703 // ==========================
704 // updateNodes - subfunctions
705
706 function deviceExisting(d) {
707 var node = d.el;
708 node.classed('online', d.online);
709 updateDeviceLabel(d);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800710 tms.positionNode(d, true);
Simon Hunt51056592015-02-03 21:48:07 -0800711 }
712
713 function hostExisting(d) {
714 updateHostLabel(d);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800715 tms.positionNode(d, true);
Simon Hunt51056592015-02-03 21:48:07 -0800716 }
717
718 function deviceEnter(d) {
719 var node = d3.select(this),
720 glyphId = d.type || 'unknown',
721 label = trimLabel(deviceLabel(d)),
722 devCfg = deviceIconConfig,
723 noLabel = !label,
724 box, dx, dy, icon;
725
726 d.el = node;
727
728 node.append('rect').attr({ rx: 5, ry: 5 });
729 node.append('text').text(label).attr('dy', '1.1em');
730 box = adjustRectToFitText(node);
731 node.select('rect').attr(box);
732
733 icon = is.addDeviceIcon(node, glyphId);
734
735 if (noLabel) {
736 dx = -icon.dim/2;
737 dy = -icon.dim/2;
738 } else {
739 box = adjustRectToFitText(node);
740 dx = box.x + devCfg.xoff;
741 dy = box.y + devCfg.yoff;
742 }
743
744 icon.attr('transform', sus.translate(dx, dy));
745 }
746
747 function hostEnter(d) {
Simon Hunt1894d792015-02-04 17:09:20 -0800748 var node = d3.select(this),
749 gid = d.type || 'unknown',
750 rad = icfg.host.radius,
751 r = d.type ? rad.withGlyph : rad.noGlyph,
752 textDy = r + 10;
Simon Hunt51056592015-02-03 21:48:07 -0800753
754 d.el = node;
Simon Hunt18bf9822015-02-12 17:35:45 -0800755 sus.visible(node, showHosts);
Simon Hunt51056592015-02-03 21:48:07 -0800756
Simon Hunt1894d792015-02-04 17:09:20 -0800757 is.addHostIcon(node, r, gid);
Simon Hunt51056592015-02-03 21:48:07 -0800758
Simon Hunt51056592015-02-03 21:48:07 -0800759 node.append('text')
760 .text(hostLabel)
Simon Hunt1894d792015-02-04 17:09:20 -0800761 .attr('dy', textDy)
Simon Hunt51056592015-02-03 21:48:07 -0800762 .attr('text-anchor', 'middle');
763 }
764
765 function hostExit(d) {
766 var node = d.el;
767 node.select('use')
768 .style('opacity', 0.5)
769 .transition()
770 .duration(800)
771 .style('opacity', 0);
772
773 node.select('text')
774 .style('opacity', 0.5)
775 .transition()
776 .duration(800)
777 .style('opacity', 0);
778
779 node.select('circle')
780 .style('stroke-fill', '#555')
781 .style('fill', '#888')
782 .style('opacity', 0.5)
783 .transition()
784 .duration(1500)
785 .attr('r', 0);
786 }
787
788 function deviceExit(d) {
789 var node = d.el;
790 node.select('use')
791 .style('opacity', 0.5)
792 .transition()
793 .duration(800)
794 .style('opacity', 0);
795
796 node.selectAll('rect')
797 .style('stroke-fill', '#555')
798 .style('fill', '#888')
799 .style('opacity', 0.5);
800 }
801
Simon Hunt1894d792015-02-04 17:09:20 -0800802 // ==========================
803
804 function updateLinks() {
805 var th = ts.theme();
806
807 link = linkG.selectAll('.link')
808 .data(network.links, function (d) { return d.key; });
809
810 // operate on existing links:
Simon Hunta142dd22015-02-12 22:07:51 -0800811 link.each(linkExisting);
Simon Hunt1894d792015-02-04 17:09:20 -0800812
813 // operate on entering links:
814 var entering = link.enter()
815 .append('line')
816 .attr({
817 x1: function (d) { return d.x1; },
818 y1: function (d) { return d.y1; },
819 x2: function (d) { return d.x2; },
820 y2: function (d) { return d.y2; },
821 stroke: linkConfig[th].inColor,
822 'stroke-width': linkConfig.inWidth
823 });
824
825 // augment links
826 entering.each(linkEntering);
827
828 // operate on both existing and new links:
829 //link.each(...)
830
831 // apply or remove labels
832 var labelData = getLabelData();
833 applyLinkLabels(labelData);
834
835 // operate on exiting links:
836 link.exit()
837 .attr('stroke-dasharray', '3 3')
Simon Hunt5724fb42015-02-05 16:59:40 -0800838 .attr('stroke', linkConfig[th].outColor)
Simon Hunt1894d792015-02-04 17:09:20 -0800839 .style('opacity', 0.5)
840 .transition()
841 .duration(1500)
842 .attr({
843 'stroke-dasharray': '3 12',
Simon Hunt1894d792015-02-04 17:09:20 -0800844 'stroke-width': linkConfig.outWidth
845 })
846 .style('opacity', 0.0)
847 .remove();
848
849 // NOTE: invoke a single tick to force the labels to position
850 // onto their links.
851 tick();
Simon Hunt5724fb42015-02-05 16:59:40 -0800852 // TODO: this causes undesirable behavior when in oblique view
Simon Hunt1894d792015-02-04 17:09:20 -0800853 // It causes the nodes to jump into "overhead" view positions, even
854 // though the oblique planes are still showing...
855 }
856
857 // ==========================
858 // updateLinks - subfunctions
859
860 function getLabelData() {
861 // create the backing data for showing labels..
862 var data = [];
863 link.each(function (d) {
864 if (d.label) {
865 data.push({
866 id: 'lab-' + d.key,
867 key: d.key,
868 label: d.label,
869 ldata: d
870 });
871 }
872 });
873 return data;
874 }
875
Simon Hunta142dd22015-02-12 22:07:51 -0800876 function linkExisting(d) {
877 restyleLinkElement(d, true);
878 }
Simon Hunt1894d792015-02-04 17:09:20 -0800879
880 function linkEntering(d) {
881 var link = d3.select(this);
882 d.el = link;
883 restyleLinkElement(d);
884 if (d.type() === 'hostLink') {
Simon Hunt18bf9822015-02-12 17:35:45 -0800885 sus.visible(link, showHosts);
Simon Hunt1894d792015-02-04 17:09:20 -0800886 }
887 }
888
889 //function linkExiting(d) { }
890
891 var linkLabelOffset = '0.3em';
892
893 function applyLinkLabels(data) {
894 var entering;
895
896 linkLabel = linkLabelG.selectAll('.linkLabel')
897 .data(data, function (d) { return d.id; });
898
899 // for elements already existing, we need to update the text
900 // and adjust the rectangle size to fit
901 linkLabel.each(function (d) {
902 var el = d3.select(this),
903 rect = el.select('rect'),
904 text = el.select('text');
905 text.text(d.label);
906 rect.attr(rectAroundText(el));
907 });
908
909 entering = linkLabel.enter().append('g')
910 .classed('linkLabel', true)
911 .attr('id', function (d) { return d.id; });
912
913 entering.each(function (d) {
914 var el = d3.select(this),
915 rect,
916 text,
917 parms = {
918 x1: d.ldata.x1,
919 y1: d.ldata.y1,
920 x2: d.ldata.x2,
921 y2: d.ldata.y2
922 };
923
924 d.el = el;
925 rect = el.append('rect');
926 text = el.append('text').text(d.label);
927 rect.attr(rectAroundText(el));
928 text.attr('dy', linkLabelOffset);
929
930 el.attr('transform', transformLabel(parms));
931 });
932
933 // Remove any labels that are no longer required.
934 linkLabel.exit().remove();
935 }
936
937 function rectAroundText(el) {
938 var text = el.select('text'),
939 box = text.node().getBBox();
940
941 // translate the bbox so that it is centered on [x,y]
942 box.x = -box.width / 2;
943 box.y = -box.height / 2;
944
945 // add padding
946 box.x -= 1;
947 box.width += 2;
948 return box;
949 }
950
951 function transformLabel(p) {
952 var dx = p.x2 - p.x1,
953 dy = p.y2 - p.y1,
954 xMid = dx/2 + p.x1,
955 yMid = dy/2 + p.y1;
956 return sus.translate(xMid, yMid);
957 }
Simon Huntac4c6f72015-02-03 19:50:53 -0800958
959 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -0800960 // force layout tick function
Simon Hunt737c89f2015-01-28 12:23:19 -0800961
Simon Hunt5724fb42015-02-05 16:59:40 -0800962 function fResume() {
963 if (!oblique) {
964 force.resume();
965 }
966 }
967
968 function fStart() {
969 if (!oblique) {
970 force.start();
971 }
972 }
973
974 var tickStuff = {
975 nodeAttr: {
976 transform: function (d) { return sus.translate(d.x, d.y); }
977 },
978 linkAttr: {
979 x1: function (d) { return d.source.x; },
980 y1: function (d) { return d.source.y; },
981 x2: function (d) { return d.target.x; },
982 y2: function (d) { return d.target.y; }
983 },
984 linkLabelAttr: {
985 transform: function (d) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800986 var lnk = tms.findLinkById(d.key);
Simon Hunt5724fb42015-02-05 16:59:40 -0800987 if (lnk) {
988 return transformLabel({
989 x1: lnk.source.x,
990 y1: lnk.source.y,
991 x2: lnk.target.x,
992 y2: lnk.target.y
993 });
994 }
995 }
996 }
997 };
998
999 function tick() {
1000 node.attr(tickStuff.nodeAttr);
1001 link.attr(tickStuff.linkAttr);
1002 linkLabel.attr(tickStuff.linkLabelAttr);
Simon Hunt737c89f2015-01-28 12:23:19 -08001003 }
1004
1005
Simon Huntac4c6f72015-02-03 19:50:53 -08001006 // ==========================
1007 // === MOUSE GESTURE HANDLERS
1008
Simon Hunt205099e2015-02-07 13:12:01 -08001009 function zoomingOrPanning(ev) {
1010 return ev.metaKey || ev.altKey;
Simon Hunt445e8152015-02-06 13:00:12 -08001011 }
1012
1013 function atDragEnd(d) {
1014 // once we've finished moving, pin the node in position
1015 d.fixed = true;
1016 d3.select(this).classed('fixed', true);
1017 sendUpdateMeta(d);
1018 }
1019
1020 // predicate that indicates when dragging is active
1021 function dragEnabled() {
1022 var ev = d3.event.sourceEvent;
1023 // nodeLock means we aren't allowing nodes to be dragged...
Simon Hunt205099e2015-02-07 13:12:01 -08001024 return !nodeLock && !zoomingOrPanning(ev);
Simon Hunt445e8152015-02-06 13:00:12 -08001025 }
1026
1027 // predicate that indicates when clicking is active
1028 function clickEnabled() {
1029 return true;
1030 }
Simon Hunt737c89f2015-01-28 12:23:19 -08001031
Simon Huntf542d842015-02-11 16:20:33 -08001032 // ==========================
1033 // function entry points for traffic module
1034
1035 var allTrafficClasses = 'primary secondary animated optical';
1036
1037 function clearLinkTrafficStyle() {
1038 link.style('stroke-width', null)
1039 .classed(allTrafficClasses, false);
1040 }
1041
1042 function removeLinkLabels() {
1043 network.links.forEach(function (d) {
1044 d.label = '';
1045 });
1046 }
Simon Hunt737c89f2015-01-28 12:23:19 -08001047
1048 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -08001049 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -08001050
Simon Huntdc6adea2015-02-09 22:29:36 -08001051 function mkModelApi(uplink) {
1052 return {
1053 projection: uplink.projection,
1054 network: network,
1055 restyleLinkElement: restyleLinkElement,
1056 removeLinkElement: removeLinkElement
1057 };
1058 }
1059
Simon Hunt08f841d02015-02-10 14:39:20 -08001060 function mkSelectApi(uplink) {
1061 return {
1062 node: function () { return node; },
1063 zoomingOrPanning: zoomingOrPanning,
1064 updateDeviceColors: updateDeviceColors,
1065 sendEvent: uplink.sendEvent
1066 };
1067 }
1068
Simon Huntf542d842015-02-11 16:20:33 -08001069 function mkTrafficApi(uplink) {
1070 return {
1071 clearLinkTrafficStyle: clearLinkTrafficStyle,
1072 removeLinkLabels: removeLinkLabels,
1073 updateLinks: updateLinks,
1074 findLinkById: tms.findLinkById,
1075 hovered: tss.hovered,
1076 validateSelectionContext: tss.validateSelectionContext,
1077 selectOrder: tss.selectOrder,
1078 sendEvent: uplink.sendEvent
1079 }
1080 }
1081
Simon Hunteb0fa052015-02-17 19:20:28 -08001082 function mkFilterApi(uplink) {
1083 return {
1084 node: function () { return node; },
1085 link: function () { return link; }
1086 };
1087 }
1088
Simon Hunt737c89f2015-01-28 12:23:19 -08001089 angular.module('ovTopo')
1090 .factory('TopoForceService',
Simon Hunt1894d792015-02-04 17:09:20 -08001091 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
Simon Hunt3a6eec02015-02-09 21:16:43 -08001092 'FlashService', 'TopoInstService', 'TopoModelService',
Simon Hunteb0fa052015-02-17 19:20:28 -08001093 'TopoSelectService', 'TopoTrafficService', 'TopoFilterService',
Simon Hunt737c89f2015-01-28 12:23:19 -08001094
Simon Huntf542d842015-02-11 16:20:33 -08001095 function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_,
Simon Hunteb0fa052015-02-17 19:20:28 -08001096 _tis_, _tms_, _tss_, _tts_, _fltr_) {
Simon Hunt737c89f2015-01-28 12:23:19 -08001097 $log = _$log_;
Simon Hunt1894d792015-02-04 17:09:20 -08001098 fs = _fs_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001099 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001100 is = _is_;
1101 ts = _ts_;
Simon Hunt5724fb42015-02-05 16:59:40 -08001102 flash = _flash_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001103 tis = _tis_;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001104 tms = _tms_;
Simon Hunt08f841d02015-02-10 14:39:20 -08001105 tss = _tss_;
Simon Huntf542d842015-02-11 16:20:33 -08001106 tts = _tts_;
Simon Hunteb0fa052015-02-17 19:20:28 -08001107 fltr = _fltr_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001108
Simon Hunt1894d792015-02-04 17:09:20 -08001109 icfg = is.iconConfig();
1110
Simon Hunta142dd22015-02-12 22:07:51 -08001111 var themeListener = ts.addListener(function () {
1112 updateLinks();
1113 updateNodes();
1114 });
1115
Simon Hunt737c89f2015-01-28 12:23:19 -08001116 // forceG is the SVG group to display the force layout in
Simon Huntdc6adea2015-02-09 22:29:36 -08001117 // uplink is the api from the main topo source file
Simon Hunt3a6eec02015-02-09 21:16:43 -08001118 // dim is the initial dimensions of the SVG as [w,h]
Simon Hunt737c89f2015-01-28 12:23:19 -08001119 // opts are, well, optional :)
Simon Hunt3a6eec02015-02-09 21:16:43 -08001120 function initForce(forceG, _uplink_, _dim_, opts) {
Simon Hunt1894d792015-02-04 17:09:20 -08001121 uplink = _uplink_;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001122 dim = _dim_;
1123
1124 $log.debug('initForce().. dim = ' + dim);
1125
Simon Huntdc6adea2015-02-09 22:29:36 -08001126 tms.initModel(mkModelApi(uplink), dim);
Simon Hunt08f841d02015-02-10 14:39:20 -08001127 tss.initSelect(mkSelectApi(uplink));
Simon Huntf542d842015-02-11 16:20:33 -08001128 tts.initTraffic(mkTrafficApi(uplink));
Simon Hunteb0fa052015-02-17 19:20:28 -08001129 fltr.initFilter(mkFilterApi(uplink), d3.select('#mast-right'));
Simon Hunta11b4eb2015-01-28 16:20:50 -08001130
Simon Hunt737c89f2015-01-28 12:23:19 -08001131 settings = angular.extend({}, defaultSettings, opts);
1132
1133 linkG = forceG.append('g').attr('id', 'topo-links');
1134 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
1135 nodeG = forceG.append('g').attr('id', 'topo-nodes');
1136
1137 link = linkG.selectAll('.link');
1138 linkLabel = linkLabelG.selectAll('.linkLabel');
1139 node = nodeG.selectAll('.node');
1140
1141 force = d3.layout.force()
Simon Hunt3a6eec02015-02-09 21:16:43 -08001142 .size(dim)
Simon Hunt737c89f2015-01-28 12:23:19 -08001143 .nodes(network.nodes)
1144 .links(network.links)
1145 .gravity(settings.gravity)
1146 .friction(settings.friction)
1147 .charge(settings.charge._def_)
1148 .linkDistance(settings.linkDistance._def_)
1149 .linkStrength(settings.linkStrength._def_)
1150 .on('tick', tick);
1151
1152 drag = sus.createDragBehavior(force,
Simon Hunt08f841d02015-02-10 14:39:20 -08001153 tss.selectObject, atDragEnd, dragEnabled, clickEnabled);
Simon Hunt737c89f2015-01-28 12:23:19 -08001154 }
1155
Simon Hunt3a6eec02015-02-09 21:16:43 -08001156 function newDim(_dim_) {
1157 dim = _dim_;
1158 force.size(dim);
1159 tms.newDim(dim);
Simon Hunt737c89f2015-01-28 12:23:19 -08001160 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -08001161 }
1162
Simon Hunt3a6eec02015-02-09 21:16:43 -08001163 function destroyForce() {
Simon Hunteb0fa052015-02-17 19:20:28 -08001164 fltr.destroyFilter();
Simon Huntf542d842015-02-11 16:20:33 -08001165 tts.destroyTraffic();
1166 tss.destroySelect();
1167 tms.destroyModel();
Simon Hunta142dd22015-02-12 22:07:51 -08001168 ts.removeListener(themeListener);
1169 themeListener = null;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001170 }
1171
Simon Hunt737c89f2015-01-28 12:23:19 -08001172 return {
1173 initForce: initForce,
Simon Hunt3a6eec02015-02-09 21:16:43 -08001174 newDim: newDim,
1175 destroyForce: destroyForce,
Simon Huntac4c6f72015-02-03 19:50:53 -08001176
1177 updateDeviceColors: updateDeviceColors,
Simon Hunt5724fb42015-02-05 16:59:40 -08001178 toggleHosts: toggleHosts,
1179 toggleOffline: toggleOffline,
1180 cycleDeviceLabels: cycleDeviceLabels,
Simon Hunt445e8152015-02-06 13:00:12 -08001181 unpin: unpin,
Simon Hunta142dd22015-02-12 22:07:51 -08001182 showMastership: showMastership,
Simon Huntac4c6f72015-02-03 19:50:53 -08001183
1184 addDevice: addDevice,
Simon Hunt1894d792015-02-04 17:09:20 -08001185 updateDevice: updateDevice,
1186 removeDevice: removeDevice,
1187 addHost: addHost,
1188 updateHost: updateHost,
1189 removeHost: removeHost,
1190 addLink: addLink,
1191 updateLink: updateLink,
1192 removeLink: removeLink
Simon Hunt737c89f2015-01-28 12:23:19 -08001193 };
1194 }]);
1195}());