blob: e29c6c095d1605800188bfc5dd74bafd489f9e95 [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 Hunt3a6eec02015-02-09 21:16:43 -080026 var $log, fs, sus, is, ts, flash, tis, tms, 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 Huntac4c6f72015-02-03 19:50:53 -080073 deviceLabelIndex = 0, // for device label cycling
Simon Hunt1894d792015-02-04 17:09:20 -080074 hostLabelIndex = 0, // for host label cycling
Simon Hunt5724fb42015-02-05 16:59:40 -080075 showHosts = true, // whether hosts are displayed
76 showOffline = true, // whether offline devices are displayed
77 oblique = false, // whether we are in the oblique view
Simon Hunt445e8152015-02-06 13:00:12 -080078 nodeLock = false, // whether nodes can be dragged or not (locked)
Simon Hunt3a6eec02015-02-09 21:16:43 -080079 dim, // the dimensions of the force layout [w,h]
Simon Hunt205099e2015-02-07 13:12:01 -080080 hovered, // the node over which the mouse is hovering
81 selections = {}, // what is currently selected
82 selectOrder = []; // the order in which we made selections
Simon Hunt737c89f2015-01-28 12:23:19 -080083
84 // SVG elements;
85 var linkG, linkLabelG, nodeG;
86
87 // D3 selections;
88 var link, linkLabel, node;
89
90 // default settings for force layout
91 var defaultSettings = {
92 gravity: 0.4,
93 friction: 0.7,
94 charge: {
95 // note: key is node.class
96 device: -8000,
97 host: -5000,
98 _def_: -12000
99 },
100 linkDistance: {
101 // note: key is link.type
102 direct: 100,
103 optical: 120,
104 hostLink: 3,
105 _def_: 50
106 },
107 linkStrength: {
108 // note: key is link.type
109 // range: {0.0 ... 1.0}
110 //direct: 1.0,
111 //optical: 1.0,
112 //hostLink: 1.0,
113 _def_: 1.0
114 }
115 };
116
117
Simon Huntac4c6f72015-02-03 19:50:53 -0800118 // ==========================
119 // === EVENT HANDLERS
120
121 function addDevice(data) {
122 var id = data.id,
123 d;
124
Simon Hunt1894d792015-02-04 17:09:20 -0800125 uplink.showNoDevs(false);
Simon Huntac4c6f72015-02-03 19:50:53 -0800126
127 // although this is an add device event, if we already have the
128 // device, treat it as an update instead..
Simon Hunt1894d792015-02-04 17:09:20 -0800129 if (lu[id]) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800130 updateDevice(data);
131 return;
132 }
133
Simon Hunt3a6eec02015-02-09 21:16:43 -0800134 d = tms.createDeviceNode(data);
Simon Huntac4c6f72015-02-03 19:50:53 -0800135 network.nodes.push(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800136 lu[id] = d;
Simon Huntac4c6f72015-02-03 19:50:53 -0800137
138 $log.debug("Created new device.. ", d.id, d.x, d.y);
139
140 updateNodes();
141 fStart();
142 }
143
144 function updateDevice(data) {
145 var id = data.id,
Simon Hunt1894d792015-02-04 17:09:20 -0800146 d = lu[id],
Simon Huntac4c6f72015-02-03 19:50:53 -0800147 wasOnline;
148
149 if (d) {
150 wasOnline = d.online;
151 angular.extend(d, data);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800152 if (tms.positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800153 sendUpdateMeta(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800154 }
155 updateNodes();
156 if (wasOnline !== d.online) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800157 findAttachedLinks(d.id).forEach(restyleLinkElement);
158 updateOfflineVisibility(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800159 }
160 } else {
161 // TODO: decide whether we want to capture logic errors
162 //logicError('updateDevice lookup fail. ID = "' + id + '"');
163 }
164 }
165
Simon Hunt1894d792015-02-04 17:09:20 -0800166 function removeDevice(data) {
167 var id = data.id,
168 d = lu[id];
169 if (d) {
170 removeDeviceElement(d);
171 } else {
172 // TODO: decide whether we want to capture logic errors
173 //logicError('removeDevice lookup fail. ID = "' + id + '"');
174 }
175 }
176
177 function addHost(data) {
178 var id = data.id,
179 d, lnk;
180
181 // although this is an add host event, if we already have the
182 // host, treat it as an update instead..
183 if (lu[id]) {
184 updateHost(data);
185 return;
186 }
187
Simon Hunt3a6eec02015-02-09 21:16:43 -0800188 d = tms.createHostNode(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800189 network.nodes.push(d);
190 lu[id] = d;
191
192 $log.debug("Created new host.. ", d.id, d.x, d.y);
193
194 updateNodes();
195
Simon Hunt3a6eec02015-02-09 21:16:43 -0800196 lnk = tms.createHostLink(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800197 if (lnk) {
198
199 $log.debug("Created new host-link.. ", lnk.key);
200
201 d.linkData = lnk; // cache ref on its host
202 network.links.push(lnk);
203 lu[d.ingress] = lnk;
204 lu[d.egress] = lnk;
205 updateLinks();
206 }
207
208 fStart();
209 }
210
211 function updateHost(data) {
212 var id = data.id,
213 d = lu[id];
214 if (d) {
215 angular.extend(d, data);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800216 if (tms.positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800217 sendUpdateMeta(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800218 }
219 updateNodes();
220 } else {
221 // TODO: decide whether we want to capture logic errors
222 //logicError('updateHost lookup fail. ID = "' + id + '"');
223 }
224 }
225
226 function removeHost(data) {
227 var id = data.id,
228 d = lu[id];
229 if (d) {
230 removeHostElement(d, true);
231 } else {
232 // may have already removed host, if attached to removed device
233 //console.warn('removeHost lookup fail. ID = "' + id + '"');
234 }
235 }
236
237 function addLink(data) {
238 var result = findLink(data, 'add'),
239 bad = result.badLogic,
240 d = result.ldata;
241
242 if (bad) {
243 //logicError(bad + ': ' + link.id);
244 return;
245 }
246
247 if (d) {
248 // we already have a backing store link for src/dst nodes
249 addLinkUpdate(d, data);
250 return;
251 }
252
253 // no backing store link yet
Simon Hunt3a6eec02015-02-09 21:16:43 -0800254 d = tms.createLink(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800255 if (d) {
256 network.links.push(d);
257 lu[d.key] = d;
258 updateLinks();
259 fStart();
260 }
261 }
262
263 function updateLink(data) {
264 var result = findLink(data, 'update'),
265 bad = result.badLogic;
266 if (bad) {
267 //logicError(bad + ': ' + link.id);
268 return;
269 }
270 result.updateWith(link);
271 }
272
273 function removeLink(data) {
274 var result = findLink(data, 'remove'),
275 bad = result.badLogic;
276 if (bad) {
277 // may have already removed link, if attached to removed device
278 //console.warn(bad + ': ' + link.id);
279 return;
280 }
281 result.removeRawLink();
282 }
283
284 // ========================
285
286 function addLinkUpdate(ldata, link) {
287 // add link event, but we already have the reverse link installed
288 ldata.fromTarget = link;
289 network.revLinkToKey[link.id] = ldata.key;
290 restyleLinkElement(ldata);
291 }
292
Simon Hunt1894d792015-02-04 17:09:20 -0800293 function makeNodeKey(d, what) {
294 var port = what + 'Port';
295 return d[what] + '/' + d[port];
296 }
297
298 function makeLinkKey(d, flipped) {
299 var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
300 two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
301 return one + '-' + two;
302 }
303
304 var widthRatio = 1.4,
305 linkScale = d3.scale.linear()
306 .domain([1, 12])
307 .range([widthRatio, 12 * widthRatio])
Simon Hunt5724fb42015-02-05 16:59:40 -0800308 .clamp(true),
Simon Hunt3a6eec02015-02-09 21:16:43 -0800309 allLinkTypes = 'direct indirect optical tunnel';
Simon Hunt1894d792015-02-04 17:09:20 -0800310
311 function restyleLinkElement(ldata) {
312 // this fn's job is to look at raw links and decide what svg classes
313 // need to be applied to the line element in the DOM
314 var th = ts.theme(),
315 el = ldata.el,
316 type = ldata.type(),
317 lw = ldata.linkWidth(),
318 online = ldata.online();
319
320 el.classed('link', true);
321 el.classed('inactive', !online);
322 el.classed(allLinkTypes, false);
323 if (type) {
324 el.classed(type, true);
325 }
326 el.transition()
327 .duration(1000)
328 .attr('stroke-width', linkScale(lw))
329 .attr('stroke', linkConfig[th].baseColor);
330 }
331
Simon Hunt5724fb42015-02-05 16:59:40 -0800332 function findLinkById(id) {
333 // check to see if this is a reverse lookup, else default to given id
334 var key = network.revLinkToKey[id] || id;
335 return key && lu[key];
336 }
337
Simon Hunt1894d792015-02-04 17:09:20 -0800338 function findLink(linkData, op) {
339 var key = makeLinkKey(linkData),
340 keyrev = makeLinkKey(linkData, 1),
341 link = lu[key],
342 linkRev = lu[keyrev],
343 result = {},
344 ldata = link || linkRev,
345 rawLink;
346
347 if (op === 'add') {
348 if (link) {
349 // trying to add a link that we already know about
350 result.ldata = link;
351 result.badLogic = 'addLink: link already added';
352
353 } else if (linkRev) {
354 // we found the reverse of the link to be added
355 result.ldata = linkRev;
356 if (linkRev.fromTarget) {
357 result.badLogic = 'addLink: link already added';
358 }
359 }
360 } else if (op === 'update') {
361 if (!ldata) {
362 result.badLogic = 'updateLink: link not found';
363 } else {
364 rawLink = link ? ldata.fromSource : ldata.fromTarget;
365 result.updateWith = function (data) {
366 angular.extend(rawLink, data);
367 restyleLinkElement(ldata);
368 }
369 }
370 } else if (op === 'remove') {
371 if (!ldata) {
372 result.badLogic = 'removeLink: link not found';
373 } else {
374 rawLink = link ? ldata.fromSource : ldata.fromTarget;
375
376 if (!rawLink) {
377 result.badLogic = 'removeLink: link not found';
378
379 } else {
380 result.removeRawLink = function () {
381 if (link) {
382 // remove fromSource
383 ldata.fromSource = null;
384 if (ldata.fromTarget) {
385 // promote target into source position
386 ldata.fromSource = ldata.fromTarget;
387 ldata.fromTarget = null;
388 ldata.key = keyrev;
389 delete network.lookup[key];
390 network.lookup[keyrev] = ldata;
391 delete network.revLinkToKey[keyrev];
392 }
393 } else {
394 // remove fromTarget
395 ldata.fromTarget = null;
396 delete network.revLinkToKey[keyrev];
397 }
398 if (ldata.fromSource) {
399 restyleLinkElement(ldata);
400 } else {
401 removeLinkElement(ldata);
402 }
403 }
404 }
405 }
406 }
407 return result;
408 }
409
Simon Hunt1c367112015-02-05 18:02:46 -0800410 function findDevices(offlineOnly) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800411 var a = [];
412 network.nodes.forEach(function (d) {
Simon Hunt1c367112015-02-05 18:02:46 -0800413 if (d.class === 'device' && !(offlineOnly && d.online)) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800414 a.push(d);
415 }
416 });
417 return a;
418 }
Simon Hunt1894d792015-02-04 17:09:20 -0800419
420 function findAttachedHosts(devId) {
421 var hosts = [];
422 network.nodes.forEach(function (d) {
423 if (d.class === 'host' && d.cp.device === devId) {
424 hosts.push(d);
425 }
426 });
427 return hosts;
428 }
429
430 function findAttachedLinks(devId) {
431 var links = [];
432 network.links.forEach(function (d) {
433 if (d.source.id === devId || d.target.id === devId) {
434 links.push(d);
435 }
436 });
437 return links;
438 }
439
440 function removeLinkElement(d) {
441 var idx = fs.find(d.key, network.links, 'key'),
442 removed;
443 if (idx >=0) {
444 // remove from links array
445 removed = network.links.splice(idx, 1);
446 // remove from lookup cache
447 delete lu[removed[0].key];
448 updateLinks();
449 fResume();
450 }
451 }
452
453 function removeHostElement(d, upd) {
454 // first, remove associated hostLink...
455 removeLinkElement(d.linkData);
456
457 // remove hostLink bindings
458 delete lu[d.ingress];
459 delete lu[d.egress];
460
461 // remove from lookup cache
462 delete lu[d.id];
463 // remove from nodes array
464 var idx = fs.find(d.id, network.nodes);
465 network.nodes.splice(idx, 1);
466
467 // remove from SVG
468 // NOTE: upd is false if we were called from removeDeviceElement()
469 if (upd) {
470 updateNodes();
471 fResume();
472 }
473 }
474
475 function removeDeviceElement(d) {
476 var id = d.id;
477 // first, remove associated hosts and links..
478 findAttachedHosts(id).forEach(removeHostElement);
479 findAttachedLinks(id).forEach(removeLinkElement);
480
481 // remove from lookup cache
482 delete lu[id];
483 // remove from nodes array
484 var idx = fs.find(id, network.nodes);
485 network.nodes.splice(idx, 1);
486
487 if (!network.nodes.length) {
488 xlink.showNoDevs(true);
489 }
490
491 // remove from SVG
492 updateNodes();
493 fResume();
494 }
495
Simon Hunt5724fb42015-02-05 16:59:40 -0800496 function updateHostVisibility() {
497 sus.makeVisible(nodeG.selectAll('.host'), showHosts);
498 sus.makeVisible(linkG.selectAll('.hostLink'), showHosts);
499 }
500
501 function updateOfflineVisibility(dev) {
502 function updDev(d, show) {
503 sus.makeVisible(d.el, show);
504
505 findAttachedLinks(d.id).forEach(function (link) {
506 b = show && ((link.type() !== 'hostLink') || showHosts);
507 sus.makeVisible(link.el, b);
508 });
509 findAttachedHosts(d.id).forEach(function (host) {
510 b = show && showHosts;
511 sus.makeVisible(host.el, b);
512 });
513 }
514
515 if (dev) {
516 // updating a specific device that just toggled off/on-line
517 updDev(dev, dev.online || showOffline);
518 } else {
519 // updating all offline devices
Simon Hunt1c367112015-02-05 18:02:46 -0800520 findDevices(true).forEach(function (d) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800521 updDev(d, showOffline);
522 });
523 }
524 }
525
Simon Hunt1894d792015-02-04 17:09:20 -0800526
Simon Hunt445e8152015-02-06 13:00:12 -0800527 function sendUpdateMeta(d, clearPos) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800528 var metaUi = {},
529 ll;
530
Simon Hunt445e8152015-02-06 13:00:12 -0800531 // if we are not clearing the position data (unpinning),
532 // attach the x, y, longitude, latitude...
533 if (!clearPos) {
Simon Hunt3a6eec02015-02-09 21:16:43 -0800534 ll = tms.lngLatFromCoord([d.x, d.y]);
Simon Hunt1894d792015-02-04 17:09:20 -0800535 metaUi = {
536 x: d.x,
537 y: d.y,
538 lng: ll[0],
539 lat: ll[1]
540 };
541 }
542 d.metaUi = metaUi;
543 uplink.sendEvent('updateMeta', {
544 id: d.id,
545 'class': d.class,
546 memento: metaUi
547 });
Simon Huntac4c6f72015-02-03 19:50:53 -0800548 }
549
Simon Hunt445e8152015-02-06 13:00:12 -0800550 function requestTrafficForMode() {
551 $log.debug('TODO: requestTrafficForMode()...');
552 }
Simon Huntac4c6f72015-02-03 19:50:53 -0800553
Simon Hunt1894d792015-02-04 17:09:20 -0800554
Simon Huntac4c6f72015-02-03 19:50:53 -0800555 // ==========================
556 // === Devices and hosts - D3 rendering
557
Simon Hunt1894d792015-02-04 17:09:20 -0800558 function nodeMouseOver(m) {
Simon Hunt445e8152015-02-06 13:00:12 -0800559 if (!m.dragStarted) {
560 $log.debug("MouseOver()...", m);
561 if (hovered != m) {
562 hovered = m;
563 requestTrafficForMode();
564 }
565 }
Simon Hunt1894d792015-02-04 17:09:20 -0800566 }
567
568 function nodeMouseOut(m) {
Simon Hunt445e8152015-02-06 13:00:12 -0800569 if (!m.dragStarted) {
570 if (hovered) {
571 hovered = null;
572 requestTrafficForMode();
573 }
574 $log.debug("MouseOut()...", m);
575 }
Simon Hunt1894d792015-02-04 17:09:20 -0800576 }
577
578
Simon Huntac4c6f72015-02-03 19:50:53 -0800579 // Returns the newly computed bounding box of the rectangle
580 function adjustRectToFitText(n) {
581 var text = n.select('text'),
582 box = text.node().getBBox(),
583 lab = labelConfig;
584
585 text.attr('text-anchor', 'middle')
586 .attr('y', '-0.8em')
587 .attr('x', lab.imgPad/2);
588
589 // translate the bbox so that it is centered on [x,y]
590 box.x = -box.width / 2;
591 box.y = -box.height / 2;
592
593 // add padding
594 box.x -= (lab.padLR + lab.imgPad/2);
595 box.width += lab.padLR * 2 + lab.imgPad;
596 box.y -= lab.padTB;
597 box.height += lab.padTB * 2;
598
599 return box;
600 }
601
602 function mkSvgClass(d) {
603 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
604 }
605
606 function hostLabel(d) {
607 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
608 return d.labels[idx];
609 }
610 function deviceLabel(d) {
611 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
612 return d.labels[idx];
613 }
614 function trimLabel(label) {
615 return (label && label.trim()) || '';
616 }
617
618 function emptyBox() {
619 return {
620 x: -2,
621 y: -2,
622 width: 4,
623 height: 4
624 };
625 }
626
627
628 function updateDeviceLabel(d) {
629 var label = trimLabel(deviceLabel(d)),
630 noLabel = !label,
631 node = d.el,
Simon Hunt1894d792015-02-04 17:09:20 -0800632 dim = icfg.device.dim,
Simon Huntac4c6f72015-02-03 19:50:53 -0800633 devCfg = deviceIconConfig,
634 box, dx, dy;
635
636 node.select('text')
637 .text(label)
638 .style('opacity', 0)
639 .transition()
640 .style('opacity', 1);
641
642 if (noLabel) {
643 box = emptyBox();
644 dx = -dim/2;
645 dy = -dim/2;
646 } else {
647 box = adjustRectToFitText(node);
648 dx = box.x + devCfg.xoff;
649 dy = box.y + devCfg.yoff;
650 }
651
652 node.select('rect')
653 .transition()
654 .attr(box);
655
656 node.select('g.deviceIcon')
657 .transition()
658 .attr('transform', sus.translate(dx, dy));
659 }
660
661 function updateHostLabel(d) {
662 var label = trimLabel(hostLabel(d));
663 d.el.select('text').text(label);
664 }
665
Simon Huntac4c6f72015-02-03 19:50:53 -0800666 function updateDeviceColors(d) {
667 if (d) {
668 setDeviceColor(d);
669 } else {
670 node.filter('.device').each(function (d) {
671 setDeviceColor(d);
672 });
673 }
674 }
675
Simon Hunt5724fb42015-02-05 16:59:40 -0800676 function vis(b) {
677 return b ? 'visible' : 'hidden';
678 }
679
680 function toggleHosts() {
681 showHosts = !showHosts;
682 updateHostVisibility();
683 flash.flash('Hosts ' + vis(showHosts));
684 }
685
686 function toggleOffline() {
687 showOffline = !showOffline;
688 updateOfflineVisibility();
689 flash.flash('Offline devices ' + vis(showOffline));
690 }
691
692 function cycleDeviceLabels() {
Simon Hunt1c367112015-02-05 18:02:46 -0800693 deviceLabelIndex = (deviceLabelIndex+1) % 3;
694 findDevices().forEach(function (d) {
695 updateDeviceLabel(d);
696 });
Simon Hunt5724fb42015-02-05 16:59:40 -0800697 }
698
Simon Hunt445e8152015-02-06 13:00:12 -0800699 function unpin() {
700 if (hovered) {
701 sendUpdateMeta(hovered, true);
702 hovered.fixed = false;
703 hovered.el.classed('fixed', false);
704 fResume();
705 }
706 }
707
708
Simon Hunt5724fb42015-02-05 16:59:40 -0800709 // ==========================================
710
Simon Huntac4c6f72015-02-03 19:50:53 -0800711 var dCol = {
712 black: '#000',
713 paleblue: '#acf',
714 offwhite: '#ddd',
Simon Hunt78c10bf2015-02-03 21:18:20 -0800715 darkgrey: '#444',
Simon Huntac4c6f72015-02-03 19:50:53 -0800716 midgrey: '#888',
717 lightgrey: '#bbb',
718 orange: '#f90'
719 };
720
721 // note: these are the device icon colors without affinity
722 var dColTheme = {
723 light: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800724 rfill: dCol.offwhite,
Simon Huntac4c6f72015-02-03 19:50:53 -0800725 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800726 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800727 rect: dCol.paleblue
728 },
729 offline: {
730 glyph: dCol.midgrey,
731 rect: dCol.lightgrey
732 }
733 },
Simon Huntac4c6f72015-02-03 19:50:53 -0800734 dark: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800735 rfill: dCol.midgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800736 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800737 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800738 rect: dCol.paleblue
739 },
740 offline: {
741 glyph: dCol.midgrey,
Simon Hunt78c10bf2015-02-03 21:18:20 -0800742 rect: dCol.darkgrey
Simon Huntac4c6f72015-02-03 19:50:53 -0800743 }
744 }
745 };
746
747 function devBaseColor(d) {
748 var o = d.online ? 'online' : 'offline';
749 return dColTheme[ts.theme()][o];
750 }
751
752 function setDeviceColor(d) {
753 var o = d.online,
754 s = d.el.classed('selected'),
755 c = devBaseColor(d),
756 a = instColor(d.master, o),
Simon Hunt51056592015-02-03 21:48:07 -0800757 icon = d.el.select('g.deviceIcon'),
758 g, r;
Simon Huntac4c6f72015-02-03 19:50:53 -0800759
760 if (s) {
761 g = c.glyph;
762 r = dCol.orange;
763 } else if (tis.isVisible()) {
764 g = o ? a : c.glyph;
Simon Hunt78c10bf2015-02-03 21:18:20 -0800765 r = o ? c.rfill : a;
Simon Huntac4c6f72015-02-03 19:50:53 -0800766 } else {
767 g = c.glyph;
768 r = c.rect;
769 }
770
Simon Hunt51056592015-02-03 21:48:07 -0800771 icon.select('use').style('fill', g);
772 icon.select('rect').style('fill', r);
Simon Huntac4c6f72015-02-03 19:50:53 -0800773 }
774
775 function instColor(id, online) {
776 return sus.cat7().getColor(id, !online, ts.theme());
777 }
778
Simon Hunt1894d792015-02-04 17:09:20 -0800779 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800780
781 function updateNodes() {
Simon Hunt1894d792015-02-04 17:09:20 -0800782 // select all the nodes in the layout:
Simon Huntac4c6f72015-02-03 19:50:53 -0800783 node = nodeG.selectAll('.node')
784 .data(network.nodes, function (d) { return d.id; });
785
Simon Hunt1894d792015-02-04 17:09:20 -0800786 // operate on existing nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800787 node.filter('.device').each(deviceExisting);
788 node.filter('.host').each(hostExisting);
Simon Huntac4c6f72015-02-03 19:50:53 -0800789
790 // operate on entering nodes:
791 var entering = node.enter()
792 .append('g')
793 .attr({
794 id: function (d) { return sus.safeId(d.id); },
795 class: mkSvgClass,
796 transform: function (d) { return sus.translate(d.x, d.y); },
797 opacity: 0
798 })
799 .call(drag)
800 .on('mouseover', nodeMouseOver)
801 .on('mouseout', nodeMouseOut)
802 .transition()
803 .attr('opacity', 1);
804
Simon Hunt1894d792015-02-04 17:09:20 -0800805 // augment entering nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800806 entering.filter('.device').each(deviceEnter);
807 entering.filter('.host').each(hostEnter);
Simon Huntac4c6f72015-02-03 19:50:53 -0800808
Simon Hunt51056592015-02-03 21:48:07 -0800809 // operate on both existing and new nodes:
Simon Huntac4c6f72015-02-03 19:50:53 -0800810 updateDeviceColors();
811
812 // operate on exiting nodes:
813 // Note that the node is removed after 2 seconds.
814 // Sub element animations should be shorter than 2 seconds.
815 var exiting = node.exit()
816 .transition()
817 .duration(2000)
818 .style('opacity', 0)
819 .remove();
820
Simon Hunt1894d792015-02-04 17:09:20 -0800821 // exiting node specifics:
Simon Hunt51056592015-02-03 21:48:07 -0800822 exiting.filter('.host').each(hostExit);
823 exiting.filter('.device').each(deviceExit);
Simon Huntac4c6f72015-02-03 19:50:53 -0800824
Simon Hunt51056592015-02-03 21:48:07 -0800825 // finally, resume the force layout
Simon Huntac4c6f72015-02-03 19:50:53 -0800826 fResume();
827 }
828
Simon Hunt51056592015-02-03 21:48:07 -0800829 // ==========================
830 // updateNodes - subfunctions
831
832 function deviceExisting(d) {
833 var node = d.el;
834 node.classed('online', d.online);
835 updateDeviceLabel(d);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800836 tms.positionNode(d, true);
Simon Hunt51056592015-02-03 21:48:07 -0800837 }
838
839 function hostExisting(d) {
840 updateHostLabel(d);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800841 tms.positionNode(d, true);
Simon Hunt51056592015-02-03 21:48:07 -0800842 }
843
844 function deviceEnter(d) {
845 var node = d3.select(this),
846 glyphId = d.type || 'unknown',
847 label = trimLabel(deviceLabel(d)),
848 devCfg = deviceIconConfig,
849 noLabel = !label,
850 box, dx, dy, icon;
851
852 d.el = node;
853
854 node.append('rect').attr({ rx: 5, ry: 5 });
855 node.append('text').text(label).attr('dy', '1.1em');
856 box = adjustRectToFitText(node);
857 node.select('rect').attr(box);
858
859 icon = is.addDeviceIcon(node, glyphId);
860
861 if (noLabel) {
862 dx = -icon.dim/2;
863 dy = -icon.dim/2;
864 } else {
865 box = adjustRectToFitText(node);
866 dx = box.x + devCfg.xoff;
867 dy = box.y + devCfg.yoff;
868 }
869
870 icon.attr('transform', sus.translate(dx, dy));
871 }
872
873 function hostEnter(d) {
Simon Hunt1894d792015-02-04 17:09:20 -0800874 var node = d3.select(this),
875 gid = d.type || 'unknown',
876 rad = icfg.host.radius,
877 r = d.type ? rad.withGlyph : rad.noGlyph,
878 textDy = r + 10;
Simon Hunt51056592015-02-03 21:48:07 -0800879
880 d.el = node;
Simon Hunt1894d792015-02-04 17:09:20 -0800881 sus.makeVisible(node, showHosts);
Simon Hunt51056592015-02-03 21:48:07 -0800882
Simon Hunt1894d792015-02-04 17:09:20 -0800883 is.addHostIcon(node, r, gid);
Simon Hunt51056592015-02-03 21:48:07 -0800884
Simon Hunt51056592015-02-03 21:48:07 -0800885 node.append('text')
886 .text(hostLabel)
Simon Hunt1894d792015-02-04 17:09:20 -0800887 .attr('dy', textDy)
Simon Hunt51056592015-02-03 21:48:07 -0800888 .attr('text-anchor', 'middle');
889 }
890
891 function hostExit(d) {
892 var node = d.el;
893 node.select('use')
894 .style('opacity', 0.5)
895 .transition()
896 .duration(800)
897 .style('opacity', 0);
898
899 node.select('text')
900 .style('opacity', 0.5)
901 .transition()
902 .duration(800)
903 .style('opacity', 0);
904
905 node.select('circle')
906 .style('stroke-fill', '#555')
907 .style('fill', '#888')
908 .style('opacity', 0.5)
909 .transition()
910 .duration(1500)
911 .attr('r', 0);
912 }
913
914 function deviceExit(d) {
915 var node = d.el;
916 node.select('use')
917 .style('opacity', 0.5)
918 .transition()
919 .duration(800)
920 .style('opacity', 0);
921
922 node.selectAll('rect')
923 .style('stroke-fill', '#555')
924 .style('fill', '#888')
925 .style('opacity', 0.5);
926 }
927
Simon Hunt1894d792015-02-04 17:09:20 -0800928 // ==========================
929
930 function updateLinks() {
931 var th = ts.theme();
932
933 link = linkG.selectAll('.link')
934 .data(network.links, function (d) { return d.key; });
935
936 // operate on existing links:
937 //link.each(linkExisting);
938
939 // operate on entering links:
940 var entering = link.enter()
941 .append('line')
942 .attr({
943 x1: function (d) { return d.x1; },
944 y1: function (d) { return d.y1; },
945 x2: function (d) { return d.x2; },
946 y2: function (d) { return d.y2; },
947 stroke: linkConfig[th].inColor,
948 'stroke-width': linkConfig.inWidth
949 });
950
951 // augment links
952 entering.each(linkEntering);
953
954 // operate on both existing and new links:
955 //link.each(...)
956
957 // apply or remove labels
958 var labelData = getLabelData();
959 applyLinkLabels(labelData);
960
961 // operate on exiting links:
962 link.exit()
963 .attr('stroke-dasharray', '3 3')
Simon Hunt5724fb42015-02-05 16:59:40 -0800964 .attr('stroke', linkConfig[th].outColor)
Simon Hunt1894d792015-02-04 17:09:20 -0800965 .style('opacity', 0.5)
966 .transition()
967 .duration(1500)
968 .attr({
969 'stroke-dasharray': '3 12',
Simon Hunt1894d792015-02-04 17:09:20 -0800970 'stroke-width': linkConfig.outWidth
971 })
972 .style('opacity', 0.0)
973 .remove();
974
975 // NOTE: invoke a single tick to force the labels to position
976 // onto their links.
977 tick();
Simon Hunt5724fb42015-02-05 16:59:40 -0800978 // TODO: this causes undesirable behavior when in oblique view
Simon Hunt1894d792015-02-04 17:09:20 -0800979 // It causes the nodes to jump into "overhead" view positions, even
980 // though the oblique planes are still showing...
981 }
982
983 // ==========================
984 // updateLinks - subfunctions
985
986 function getLabelData() {
987 // create the backing data for showing labels..
988 var data = [];
989 link.each(function (d) {
990 if (d.label) {
991 data.push({
992 id: 'lab-' + d.key,
993 key: d.key,
994 label: d.label,
995 ldata: d
996 });
997 }
998 });
999 return data;
1000 }
1001
1002 //function linkExisting(d) { }
1003
1004 function linkEntering(d) {
1005 var link = d3.select(this);
1006 d.el = link;
1007 restyleLinkElement(d);
1008 if (d.type() === 'hostLink') {
1009 sus.makeVisible(link, showHosts);
1010 }
1011 }
1012
1013 //function linkExiting(d) { }
1014
1015 var linkLabelOffset = '0.3em';
1016
1017 function applyLinkLabels(data) {
1018 var entering;
1019
1020 linkLabel = linkLabelG.selectAll('.linkLabel')
1021 .data(data, function (d) { return d.id; });
1022
1023 // for elements already existing, we need to update the text
1024 // and adjust the rectangle size to fit
1025 linkLabel.each(function (d) {
1026 var el = d3.select(this),
1027 rect = el.select('rect'),
1028 text = el.select('text');
1029 text.text(d.label);
1030 rect.attr(rectAroundText(el));
1031 });
1032
1033 entering = linkLabel.enter().append('g')
1034 .classed('linkLabel', true)
1035 .attr('id', function (d) { return d.id; });
1036
1037 entering.each(function (d) {
1038 var el = d3.select(this),
1039 rect,
1040 text,
1041 parms = {
1042 x1: d.ldata.x1,
1043 y1: d.ldata.y1,
1044 x2: d.ldata.x2,
1045 y2: d.ldata.y2
1046 };
1047
1048 d.el = el;
1049 rect = el.append('rect');
1050 text = el.append('text').text(d.label);
1051 rect.attr(rectAroundText(el));
1052 text.attr('dy', linkLabelOffset);
1053
1054 el.attr('transform', transformLabel(parms));
1055 });
1056
1057 // Remove any labels that are no longer required.
1058 linkLabel.exit().remove();
1059 }
1060
1061 function rectAroundText(el) {
1062 var text = el.select('text'),
1063 box = text.node().getBBox();
1064
1065 // translate the bbox so that it is centered on [x,y]
1066 box.x = -box.width / 2;
1067 box.y = -box.height / 2;
1068
1069 // add padding
1070 box.x -= 1;
1071 box.width += 2;
1072 return box;
1073 }
1074
1075 function transformLabel(p) {
1076 var dx = p.x2 - p.x1,
1077 dy = p.y2 - p.y1,
1078 xMid = dx/2 + p.x1,
1079 yMid = dy/2 + p.y1;
1080 return sus.translate(xMid, yMid);
1081 }
Simon Huntac4c6f72015-02-03 19:50:53 -08001082
1083 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -08001084 // force layout tick function
Simon Hunt737c89f2015-01-28 12:23:19 -08001085
Simon Hunt5724fb42015-02-05 16:59:40 -08001086 function fResume() {
1087 if (!oblique) {
1088 force.resume();
1089 }
1090 }
1091
1092 function fStart() {
1093 if (!oblique) {
1094 force.start();
1095 }
1096 }
1097
1098 var tickStuff = {
1099 nodeAttr: {
1100 transform: function (d) { return sus.translate(d.x, d.y); }
1101 },
1102 linkAttr: {
1103 x1: function (d) { return d.source.x; },
1104 y1: function (d) { return d.source.y; },
1105 x2: function (d) { return d.target.x; },
1106 y2: function (d) { return d.target.y; }
1107 },
1108 linkLabelAttr: {
1109 transform: function (d) {
1110 var lnk = findLinkById(d.key);
1111 if (lnk) {
1112 return transformLabel({
1113 x1: lnk.source.x,
1114 y1: lnk.source.y,
1115 x2: lnk.target.x,
1116 y2: lnk.target.y
1117 });
1118 }
1119 }
1120 }
1121 };
1122
1123 function tick() {
1124 node.attr(tickStuff.nodeAttr);
1125 link.attr(tickStuff.linkAttr);
1126 linkLabel.attr(tickStuff.linkLabelAttr);
Simon Hunt737c89f2015-01-28 12:23:19 -08001127 }
1128
1129
Simon Hunt205099e2015-02-07 13:12:01 -08001130 function updateDetailPanel() {
1131 // TODO update detail panel
1132 $log.debug("TODO: updateDetailPanel() ...");
1133 }
1134
1135
1136 // ==========================
1137 // === SELECTION / DESELECTION
1138
1139 function selectObject(obj) {
1140 var el = this,
1141 ev = d3.event.sourceEvent,
1142 n;
1143
1144 if (zoomingOrPanning(ev)) {
1145 return;
1146 }
1147
1148 if (el) {
1149 n = d3.select(el);
1150 } else {
1151 node.each(function (d) {
1152 if (d == obj) {
1153 n = d3.select(el = this);
1154 }
1155 });
1156 }
1157 if (!n) return;
1158
1159 if (ev.shiftKey && n.classed('selected')) {
1160 deselectObject(obj.id);
1161 updateDetailPanel();
1162 return;
1163 }
1164
1165 if (!ev.shiftKey) {
1166 deselectAll();
1167 }
1168
1169 selections[obj.id] = { obj: obj, el: el };
1170 selectOrder.push(obj.id);
1171
1172 n.classed('selected', true);
1173 updateDeviceColors(obj);
1174 updateDetailPanel();
1175 }
1176
1177 function deselectObject(id) {
1178 var obj = selections[id];
1179 if (obj) {
1180 d3.select(obj.el).classed('selected', false);
1181 delete selections[id];
1182 fs.removeFromArray(id, selectOrder);
1183 updateDeviceColors(obj.obj);
1184 }
1185 }
1186
1187 function deselectAll() {
1188 // deselect all nodes in the network...
1189 node.classed('selected', false);
1190 selections = {};
1191 selectOrder = [];
1192 updateDeviceColors();
1193 updateDetailPanel();
1194 }
1195
Simon Huntac4c6f72015-02-03 19:50:53 -08001196 // ==========================
1197 // === MOUSE GESTURE HANDLERS
1198
Simon Hunt205099e2015-02-07 13:12:01 -08001199 function zoomingOrPanning(ev) {
1200 return ev.metaKey || ev.altKey;
Simon Hunt445e8152015-02-06 13:00:12 -08001201 }
1202
1203 function atDragEnd(d) {
1204 // once we've finished moving, pin the node in position
1205 d.fixed = true;
1206 d3.select(this).classed('fixed', true);
1207 sendUpdateMeta(d);
1208 }
1209
1210 // predicate that indicates when dragging is active
1211 function dragEnabled() {
1212 var ev = d3.event.sourceEvent;
1213 // nodeLock means we aren't allowing nodes to be dragged...
Simon Hunt205099e2015-02-07 13:12:01 -08001214 return !nodeLock && !zoomingOrPanning(ev);
Simon Hunt445e8152015-02-06 13:00:12 -08001215 }
1216
1217 // predicate that indicates when clicking is active
1218 function clickEnabled() {
1219 return true;
1220 }
Simon Hunt737c89f2015-01-28 12:23:19 -08001221
1222
1223 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -08001224 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -08001225
1226 angular.module('ovTopo')
1227 .factory('TopoForceService',
Simon Hunt1894d792015-02-04 17:09:20 -08001228 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
Simon Hunt3a6eec02015-02-09 21:16:43 -08001229 'FlashService', 'TopoInstService', 'TopoModelService',
Simon Hunt737c89f2015-01-28 12:23:19 -08001230
Simon Hunt3a6eec02015-02-09 21:16:43 -08001231 function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_, _tms_) {
Simon Hunt737c89f2015-01-28 12:23:19 -08001232 $log = _$log_;
Simon Hunt1894d792015-02-04 17:09:20 -08001233 fs = _fs_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001234 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001235 is = _is_;
1236 ts = _ts_;
Simon Hunt5724fb42015-02-05 16:59:40 -08001237 flash = _flash_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001238 tis = _tis_;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001239 tms = _tms_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001240
Simon Hunt1894d792015-02-04 17:09:20 -08001241 icfg = is.iconConfig();
1242
Simon Hunt737c89f2015-01-28 12:23:19 -08001243 // forceG is the SVG group to display the force layout in
Simon Huntac4c6f72015-02-03 19:50:53 -08001244 // xlink is the cross-link api from the main topo source file
Simon Hunt3a6eec02015-02-09 21:16:43 -08001245 // dim is the initial dimensions of the SVG as [w,h]
Simon Hunt737c89f2015-01-28 12:23:19 -08001246 // opts are, well, optional :)
Simon Hunt3a6eec02015-02-09 21:16:43 -08001247 function initForce(forceG, _uplink_, _dim_, opts) {
Simon Hunt1894d792015-02-04 17:09:20 -08001248 uplink = _uplink_;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001249 dim = _dim_;
1250
1251 $log.debug('initForce().. dim = ' + dim);
1252
1253 tms.initModel({
1254 projection: uplink.projection,
1255 lookup: network.lookup
1256 }, dim);
Simon Hunta11b4eb2015-01-28 16:20:50 -08001257
Simon Hunt737c89f2015-01-28 12:23:19 -08001258 settings = angular.extend({}, defaultSettings, opts);
1259
1260 linkG = forceG.append('g').attr('id', 'topo-links');
1261 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
1262 nodeG = forceG.append('g').attr('id', 'topo-nodes');
1263
1264 link = linkG.selectAll('.link');
1265 linkLabel = linkLabelG.selectAll('.linkLabel');
1266 node = nodeG.selectAll('.node');
1267
1268 force = d3.layout.force()
Simon Hunt3a6eec02015-02-09 21:16:43 -08001269 .size(dim)
Simon Hunt737c89f2015-01-28 12:23:19 -08001270 .nodes(network.nodes)
1271 .links(network.links)
1272 .gravity(settings.gravity)
1273 .friction(settings.friction)
1274 .charge(settings.charge._def_)
1275 .linkDistance(settings.linkDistance._def_)
1276 .linkStrength(settings.linkStrength._def_)
1277 .on('tick', tick);
1278
1279 drag = sus.createDragBehavior(force,
Simon Hunt205099e2015-02-07 13:12:01 -08001280 selectObject, atDragEnd, dragEnabled, clickEnabled);
Simon Hunt737c89f2015-01-28 12:23:19 -08001281 }
1282
Simon Hunt3a6eec02015-02-09 21:16:43 -08001283 function newDim(_dim_) {
1284 dim = _dim_;
1285 force.size(dim);
1286 tms.newDim(dim);
Simon Hunt737c89f2015-01-28 12:23:19 -08001287 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -08001288 }
1289
Simon Hunt3a6eec02015-02-09 21:16:43 -08001290 function destroyForce() {
1291
1292 }
1293
Simon Hunt737c89f2015-01-28 12:23:19 -08001294 return {
1295 initForce: initForce,
Simon Hunt3a6eec02015-02-09 21:16:43 -08001296 newDim: newDim,
1297 destroyForce: destroyForce,
Simon Huntac4c6f72015-02-03 19:50:53 -08001298
1299 updateDeviceColors: updateDeviceColors,
Simon Hunt5724fb42015-02-05 16:59:40 -08001300 toggleHosts: toggleHosts,
1301 toggleOffline: toggleOffline,
1302 cycleDeviceLabels: cycleDeviceLabels,
Simon Hunt445e8152015-02-06 13:00:12 -08001303 unpin: unpin,
Simon Huntac4c6f72015-02-03 19:50:53 -08001304
1305 addDevice: addDevice,
Simon Hunt1894d792015-02-04 17:09:20 -08001306 updateDevice: updateDevice,
1307 removeDevice: removeDevice,
1308 addHost: addHost,
1309 updateHost: updateHost,
1310 removeHost: removeHost,
1311 addLink: addLink,
1312 updateLink: updateLink,
1313 removeLink: removeLink
Simon Hunt737c89f2015-01-28 12:23:19 -08001314 };
1315 }]);
1316}());