blob: 703212f79b658e99301fdd767f14a2964536b4b3 [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 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 Hunt5724fb42015-02-05 16:59:40 -080076 showHosts = true, // whether hosts are displayed
77 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 Hunt3a6eec02015-02-09 21:16:43 -080080 dim, // the dimensions of the force layout [w,h]
Simon Hunt205099e2015-02-07 13:12:01 -080081 hovered, // the node over which the mouse is hovering
82 selections = {}, // what is currently selected
83 selectOrder = []; // the order in which we made selections
Simon Hunt737c89f2015-01-28 12:23:19 -080084
85 // SVG elements;
86 var linkG, linkLabelG, nodeG;
87
88 // D3 selections;
89 var link, linkLabel, node;
90
91 // default settings for force layout
92 var defaultSettings = {
93 gravity: 0.4,
94 friction: 0.7,
95 charge: {
96 // note: key is node.class
97 device: -8000,
98 host: -5000,
99 _def_: -12000
100 },
101 linkDistance: {
102 // note: key is link.type
103 direct: 100,
104 optical: 120,
105 hostLink: 3,
106 _def_: 50
107 },
108 linkStrength: {
109 // note: key is link.type
110 // range: {0.0 ... 1.0}
111 //direct: 1.0,
112 //optical: 1.0,
113 //hostLink: 1.0,
114 _def_: 1.0
115 }
116 };
117
118
Simon Huntac4c6f72015-02-03 19:50:53 -0800119 // ==========================
120 // === EVENT HANDLERS
121
122 function addDevice(data) {
123 var id = data.id,
124 d;
125
Simon Hunt1894d792015-02-04 17:09:20 -0800126 uplink.showNoDevs(false);
Simon Huntac4c6f72015-02-03 19:50:53 -0800127
128 // although this is an add device event, if we already have the
129 // device, treat it as an update instead..
Simon Hunt1894d792015-02-04 17:09:20 -0800130 if (lu[id]) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800131 updateDevice(data);
132 return;
133 }
134
Simon Hunt3a6eec02015-02-09 21:16:43 -0800135 d = tms.createDeviceNode(data);
Simon Huntac4c6f72015-02-03 19:50:53 -0800136 network.nodes.push(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800137 lu[id] = d;
Simon Huntac4c6f72015-02-03 19:50:53 -0800138 updateNodes();
139 fStart();
140 }
141
142 function updateDevice(data) {
143 var id = data.id,
Simon Hunt1894d792015-02-04 17:09:20 -0800144 d = lu[id],
Simon Huntac4c6f72015-02-03 19:50:53 -0800145 wasOnline;
146
147 if (d) {
148 wasOnline = d.online;
149 angular.extend(d, data);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800150 if (tms.positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800151 sendUpdateMeta(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800152 }
153 updateNodes();
154 if (wasOnline !== d.online) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800155 tms.findAttachedLinks(d.id).forEach(restyleLinkElement);
Simon Hunt5724fb42015-02-05 16:59:40 -0800156 updateOfflineVisibility(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800157 }
158 } else {
159 // TODO: decide whether we want to capture logic errors
160 //logicError('updateDevice lookup fail. ID = "' + id + '"');
161 }
162 }
163
Simon Hunt1894d792015-02-04 17:09:20 -0800164 function removeDevice(data) {
165 var id = data.id,
166 d = lu[id];
167 if (d) {
168 removeDeviceElement(d);
169 } else {
170 // TODO: decide whether we want to capture logic errors
171 //logicError('removeDevice lookup fail. ID = "' + id + '"');
172 }
173 }
174
175 function addHost(data) {
176 var id = data.id,
177 d, lnk;
178
179 // although this is an add host event, if we already have the
180 // host, treat it as an update instead..
181 if (lu[id]) {
182 updateHost(data);
183 return;
184 }
185
Simon Hunt3a6eec02015-02-09 21:16:43 -0800186 d = tms.createHostNode(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800187 network.nodes.push(d);
188 lu[id] = d;
Simon Hunt1894d792015-02-04 17:09:20 -0800189 updateNodes();
190
Simon Hunt3a6eec02015-02-09 21:16:43 -0800191 lnk = tms.createHostLink(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800192 if (lnk) {
Simon Hunt1894d792015-02-04 17:09:20 -0800193 d.linkData = lnk; // cache ref on its host
194 network.links.push(lnk);
195 lu[d.ingress] = lnk;
196 lu[d.egress] = lnk;
197 updateLinks();
198 }
199
200 fStart();
201 }
202
203 function updateHost(data) {
204 var id = data.id,
205 d = lu[id];
206 if (d) {
207 angular.extend(d, data);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800208 if (tms.positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800209 sendUpdateMeta(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800210 }
211 updateNodes();
212 } else {
213 // TODO: decide whether we want to capture logic errors
214 //logicError('updateHost lookup fail. ID = "' + id + '"');
215 }
216 }
217
218 function removeHost(data) {
219 var id = data.id,
220 d = lu[id];
221 if (d) {
222 removeHostElement(d, true);
223 } else {
224 // may have already removed host, if attached to removed device
225 //console.warn('removeHost lookup fail. ID = "' + id + '"');
226 }
227 }
228
229 function addLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800230 var result = tms.findLink(data, 'add'),
Simon Hunt1894d792015-02-04 17:09:20 -0800231 bad = result.badLogic,
232 d = result.ldata;
233
234 if (bad) {
235 //logicError(bad + ': ' + link.id);
236 return;
237 }
238
239 if (d) {
240 // we already have a backing store link for src/dst nodes
241 addLinkUpdate(d, data);
242 return;
243 }
244
245 // no backing store link yet
Simon Hunt3a6eec02015-02-09 21:16:43 -0800246 d = tms.createLink(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800247 if (d) {
248 network.links.push(d);
249 lu[d.key] = d;
250 updateLinks();
251 fStart();
252 }
253 }
254
255 function updateLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800256 var result = tms.findLink(data, 'update'),
Simon Hunt1894d792015-02-04 17:09:20 -0800257 bad = result.badLogic;
258 if (bad) {
259 //logicError(bad + ': ' + link.id);
260 return;
261 }
262 result.updateWith(link);
263 }
264
265 function removeLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800266 var result = tms.findLink(data, 'remove'),
Simon Hunt1894d792015-02-04 17:09:20 -0800267 bad = result.badLogic;
268 if (bad) {
269 // may have already removed link, if attached to removed device
270 //console.warn(bad + ': ' + link.id);
271 return;
272 }
273 result.removeRawLink();
274 }
275
276 // ========================
277
278 function addLinkUpdate(ldata, link) {
279 // add link event, but we already have the reverse link installed
280 ldata.fromTarget = link;
Simon Huntdc6adea2015-02-09 22:29:36 -0800281 rlk[link.id] = ldata.key;
Simon Hunt1894d792015-02-04 17:09:20 -0800282 restyleLinkElement(ldata);
283 }
284
Simon Hunt1894d792015-02-04 17:09:20 -0800285
286 var widthRatio = 1.4,
287 linkScale = d3.scale.linear()
288 .domain([1, 12])
289 .range([widthRatio, 12 * widthRatio])
Simon Hunt5724fb42015-02-05 16:59:40 -0800290 .clamp(true),
Simon Hunt3a6eec02015-02-09 21:16:43 -0800291 allLinkTypes = 'direct indirect optical tunnel';
Simon Hunt1894d792015-02-04 17:09:20 -0800292
293 function restyleLinkElement(ldata) {
294 // this fn's job is to look at raw links and decide what svg classes
295 // need to be applied to the line element in the DOM
296 var th = ts.theme(),
297 el = ldata.el,
298 type = ldata.type(),
299 lw = ldata.linkWidth(),
300 online = ldata.online();
301
302 el.classed('link', true);
303 el.classed('inactive', !online);
304 el.classed(allLinkTypes, false);
305 if (type) {
306 el.classed(type, true);
307 }
308 el.transition()
309 .duration(1000)
310 .attr('stroke-width', linkScale(lw))
311 .attr('stroke', linkConfig[th].baseColor);
312 }
313
Simon Hunt5724fb42015-02-05 16:59:40 -0800314
Simon Hunt1894d792015-02-04 17:09:20 -0800315
316 function removeLinkElement(d) {
317 var idx = fs.find(d.key, network.links, 'key'),
318 removed;
319 if (idx >=0) {
320 // remove from links array
321 removed = network.links.splice(idx, 1);
322 // remove from lookup cache
323 delete lu[removed[0].key];
324 updateLinks();
325 fResume();
326 }
327 }
328
329 function removeHostElement(d, upd) {
330 // first, remove associated hostLink...
331 removeLinkElement(d.linkData);
332
333 // remove hostLink bindings
334 delete lu[d.ingress];
335 delete lu[d.egress];
336
337 // remove from lookup cache
338 delete lu[d.id];
339 // remove from nodes array
340 var idx = fs.find(d.id, network.nodes);
341 network.nodes.splice(idx, 1);
342
343 // remove from SVG
344 // NOTE: upd is false if we were called from removeDeviceElement()
345 if (upd) {
346 updateNodes();
347 fResume();
348 }
349 }
350
351 function removeDeviceElement(d) {
352 var id = d.id;
353 // first, remove associated hosts and links..
Simon Huntdc6adea2015-02-09 22:29:36 -0800354 tms.findAttachedHosts(id).forEach(removeHostElement);
355 tms.findAttachedLinks(id).forEach(removeLinkElement);
Simon Hunt1894d792015-02-04 17:09:20 -0800356
357 // remove from lookup cache
358 delete lu[id];
359 // remove from nodes array
360 var idx = fs.find(id, network.nodes);
361 network.nodes.splice(idx, 1);
362
363 if (!network.nodes.length) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800364 uplink.showNoDevs(true);
Simon Hunt1894d792015-02-04 17:09:20 -0800365 }
366
367 // remove from SVG
368 updateNodes();
369 fResume();
370 }
371
Simon Hunt5724fb42015-02-05 16:59:40 -0800372 function updateHostVisibility() {
373 sus.makeVisible(nodeG.selectAll('.host'), showHosts);
374 sus.makeVisible(linkG.selectAll('.hostLink'), showHosts);
375 }
376
377 function updateOfflineVisibility(dev) {
378 function updDev(d, show) {
379 sus.makeVisible(d.el, show);
380
Simon Huntdc6adea2015-02-09 22:29:36 -0800381 tms.findAttachedLinks(d.id).forEach(function (link) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800382 b = show && ((link.type() !== 'hostLink') || showHosts);
383 sus.makeVisible(link.el, b);
384 });
Simon Huntdc6adea2015-02-09 22:29:36 -0800385 tms.findAttachedHosts(d.id).forEach(function (host) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800386 b = show && showHosts;
387 sus.makeVisible(host.el, b);
388 });
389 }
390
391 if (dev) {
392 // updating a specific device that just toggled off/on-line
393 updDev(dev, dev.online || showOffline);
394 } else {
395 // updating all offline devices
Simon Huntdc6adea2015-02-09 22:29:36 -0800396 tms.findDevices(true).forEach(function (d) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800397 updDev(d, showOffline);
398 });
399 }
400 }
401
Simon Hunt1894d792015-02-04 17:09:20 -0800402
Simon Hunt445e8152015-02-06 13:00:12 -0800403 function sendUpdateMeta(d, clearPos) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800404 var metaUi = {},
405 ll;
406
Simon Hunt445e8152015-02-06 13:00:12 -0800407 // if we are not clearing the position data (unpinning),
408 // attach the x, y, longitude, latitude...
409 if (!clearPos) {
Simon Hunt3a6eec02015-02-09 21:16:43 -0800410 ll = tms.lngLatFromCoord([d.x, d.y]);
Simon Huntdc6adea2015-02-09 22:29:36 -0800411 metaUi = {x: d.x, y: d.y, lng: ll[0], lat: ll[1]};
Simon Hunt1894d792015-02-04 17:09:20 -0800412 }
413 d.metaUi = metaUi;
414 uplink.sendEvent('updateMeta', {
415 id: d.id,
416 'class': d.class,
417 memento: metaUi
418 });
Simon Huntac4c6f72015-02-03 19:50:53 -0800419 }
420
Simon Hunt445e8152015-02-06 13:00:12 -0800421 function requestTrafficForMode() {
422 $log.debug('TODO: requestTrafficForMode()...');
423 }
Simon Huntac4c6f72015-02-03 19:50:53 -0800424
Simon Hunt1894d792015-02-04 17:09:20 -0800425
Simon Huntac4c6f72015-02-03 19:50:53 -0800426 // ==========================
427 // === Devices and hosts - D3 rendering
428
Simon Hunt1894d792015-02-04 17:09:20 -0800429 function nodeMouseOver(m) {
Simon Hunt445e8152015-02-06 13:00:12 -0800430 if (!m.dragStarted) {
431 $log.debug("MouseOver()...", m);
432 if (hovered != m) {
433 hovered = m;
434 requestTrafficForMode();
435 }
436 }
Simon Hunt1894d792015-02-04 17:09:20 -0800437 }
438
439 function nodeMouseOut(m) {
Simon Hunt445e8152015-02-06 13:00:12 -0800440 if (!m.dragStarted) {
441 if (hovered) {
442 hovered = null;
443 requestTrafficForMode();
444 }
445 $log.debug("MouseOut()...", m);
446 }
Simon Hunt1894d792015-02-04 17:09:20 -0800447 }
448
449
Simon Huntac4c6f72015-02-03 19:50:53 -0800450 // Returns the newly computed bounding box of the rectangle
451 function adjustRectToFitText(n) {
452 var text = n.select('text'),
453 box = text.node().getBBox(),
454 lab = labelConfig;
455
456 text.attr('text-anchor', 'middle')
457 .attr('y', '-0.8em')
458 .attr('x', lab.imgPad/2);
459
460 // translate the bbox so that it is centered on [x,y]
461 box.x = -box.width / 2;
462 box.y = -box.height / 2;
463
464 // add padding
465 box.x -= (lab.padLR + lab.imgPad/2);
466 box.width += lab.padLR * 2 + lab.imgPad;
467 box.y -= lab.padTB;
468 box.height += lab.padTB * 2;
469
470 return box;
471 }
472
473 function mkSvgClass(d) {
474 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
475 }
476
477 function hostLabel(d) {
478 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
479 return d.labels[idx];
480 }
481 function deviceLabel(d) {
482 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
483 return d.labels[idx];
484 }
485 function trimLabel(label) {
486 return (label && label.trim()) || '';
487 }
488
489 function emptyBox() {
490 return {
491 x: -2,
492 y: -2,
493 width: 4,
494 height: 4
495 };
496 }
497
498
499 function updateDeviceLabel(d) {
500 var label = trimLabel(deviceLabel(d)),
501 noLabel = !label,
502 node = d.el,
Simon Hunt1894d792015-02-04 17:09:20 -0800503 dim = icfg.device.dim,
Simon Huntac4c6f72015-02-03 19:50:53 -0800504 devCfg = deviceIconConfig,
505 box, dx, dy;
506
507 node.select('text')
508 .text(label)
509 .style('opacity', 0)
510 .transition()
511 .style('opacity', 1);
512
513 if (noLabel) {
514 box = emptyBox();
515 dx = -dim/2;
516 dy = -dim/2;
517 } else {
518 box = adjustRectToFitText(node);
519 dx = box.x + devCfg.xoff;
520 dy = box.y + devCfg.yoff;
521 }
522
523 node.select('rect')
524 .transition()
525 .attr(box);
526
527 node.select('g.deviceIcon')
528 .transition()
529 .attr('transform', sus.translate(dx, dy));
530 }
531
532 function updateHostLabel(d) {
533 var label = trimLabel(hostLabel(d));
534 d.el.select('text').text(label);
535 }
536
Simon Huntac4c6f72015-02-03 19:50:53 -0800537 function updateDeviceColors(d) {
538 if (d) {
539 setDeviceColor(d);
540 } else {
541 node.filter('.device').each(function (d) {
542 setDeviceColor(d);
543 });
544 }
545 }
546
Simon Hunt5724fb42015-02-05 16:59:40 -0800547 function vis(b) {
548 return b ? 'visible' : 'hidden';
549 }
550
551 function toggleHosts() {
552 showHosts = !showHosts;
553 updateHostVisibility();
554 flash.flash('Hosts ' + vis(showHosts));
555 }
556
557 function toggleOffline() {
558 showOffline = !showOffline;
559 updateOfflineVisibility();
560 flash.flash('Offline devices ' + vis(showOffline));
561 }
562
563 function cycleDeviceLabels() {
Simon Hunt1c367112015-02-05 18:02:46 -0800564 deviceLabelIndex = (deviceLabelIndex+1) % 3;
Simon Huntdc6adea2015-02-09 22:29:36 -0800565 tms.findDevices().forEach(function (d) {
Simon Hunt1c367112015-02-05 18:02:46 -0800566 updateDeviceLabel(d);
567 });
Simon Hunt5724fb42015-02-05 16:59:40 -0800568 }
569
Simon Hunt445e8152015-02-06 13:00:12 -0800570 function unpin() {
571 if (hovered) {
572 sendUpdateMeta(hovered, true);
573 hovered.fixed = false;
574 hovered.el.classed('fixed', false);
575 fResume();
576 }
577 }
578
579
Simon Hunt5724fb42015-02-05 16:59:40 -0800580 // ==========================================
581
Simon Huntac4c6f72015-02-03 19:50:53 -0800582 var dCol = {
583 black: '#000',
584 paleblue: '#acf',
585 offwhite: '#ddd',
Simon Hunt78c10bf2015-02-03 21:18:20 -0800586 darkgrey: '#444',
Simon Huntac4c6f72015-02-03 19:50:53 -0800587 midgrey: '#888',
588 lightgrey: '#bbb',
589 orange: '#f90'
590 };
591
592 // note: these are the device icon colors without affinity
593 var dColTheme = {
594 light: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800595 rfill: dCol.offwhite,
Simon Huntac4c6f72015-02-03 19:50:53 -0800596 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800597 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800598 rect: dCol.paleblue
599 },
600 offline: {
601 glyph: dCol.midgrey,
602 rect: dCol.lightgrey
603 }
604 },
Simon Huntac4c6f72015-02-03 19:50:53 -0800605 dark: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800606 rfill: dCol.midgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800607 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800608 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800609 rect: dCol.paleblue
610 },
611 offline: {
612 glyph: dCol.midgrey,
Simon Hunt78c10bf2015-02-03 21:18:20 -0800613 rect: dCol.darkgrey
Simon Huntac4c6f72015-02-03 19:50:53 -0800614 }
615 }
616 };
617
618 function devBaseColor(d) {
619 var o = d.online ? 'online' : 'offline';
620 return dColTheme[ts.theme()][o];
621 }
622
623 function setDeviceColor(d) {
624 var o = d.online,
625 s = d.el.classed('selected'),
626 c = devBaseColor(d),
627 a = instColor(d.master, o),
Simon Hunt51056592015-02-03 21:48:07 -0800628 icon = d.el.select('g.deviceIcon'),
629 g, r;
Simon Huntac4c6f72015-02-03 19:50:53 -0800630
631 if (s) {
632 g = c.glyph;
633 r = dCol.orange;
634 } else if (tis.isVisible()) {
635 g = o ? a : c.glyph;
Simon Hunt78c10bf2015-02-03 21:18:20 -0800636 r = o ? c.rfill : a;
Simon Huntac4c6f72015-02-03 19:50:53 -0800637 } else {
638 g = c.glyph;
639 r = c.rect;
640 }
641
Simon Hunt51056592015-02-03 21:48:07 -0800642 icon.select('use').style('fill', g);
643 icon.select('rect').style('fill', r);
Simon Huntac4c6f72015-02-03 19:50:53 -0800644 }
645
646 function instColor(id, online) {
647 return sus.cat7().getColor(id, !online, ts.theme());
648 }
649
Simon Hunt1894d792015-02-04 17:09:20 -0800650 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800651
652 function updateNodes() {
Simon Hunt1894d792015-02-04 17:09:20 -0800653 // select all the nodes in the layout:
Simon Huntac4c6f72015-02-03 19:50:53 -0800654 node = nodeG.selectAll('.node')
655 .data(network.nodes, function (d) { return d.id; });
656
Simon Hunt1894d792015-02-04 17:09:20 -0800657 // operate on existing nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800658 node.filter('.device').each(deviceExisting);
659 node.filter('.host').each(hostExisting);
Simon Huntac4c6f72015-02-03 19:50:53 -0800660
661 // operate on entering nodes:
662 var entering = node.enter()
663 .append('g')
664 .attr({
665 id: function (d) { return sus.safeId(d.id); },
666 class: mkSvgClass,
667 transform: function (d) { return sus.translate(d.x, d.y); },
668 opacity: 0
669 })
670 .call(drag)
671 .on('mouseover', nodeMouseOver)
672 .on('mouseout', nodeMouseOut)
673 .transition()
674 .attr('opacity', 1);
675
Simon Hunt1894d792015-02-04 17:09:20 -0800676 // augment entering nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800677 entering.filter('.device').each(deviceEnter);
678 entering.filter('.host').each(hostEnter);
Simon Huntac4c6f72015-02-03 19:50:53 -0800679
Simon Hunt51056592015-02-03 21:48:07 -0800680 // operate on both existing and new nodes:
Simon Huntac4c6f72015-02-03 19:50:53 -0800681 updateDeviceColors();
682
683 // operate on exiting nodes:
684 // Note that the node is removed after 2 seconds.
685 // Sub element animations should be shorter than 2 seconds.
686 var exiting = node.exit()
687 .transition()
688 .duration(2000)
689 .style('opacity', 0)
690 .remove();
691
Simon Hunt1894d792015-02-04 17:09:20 -0800692 // exiting node specifics:
Simon Hunt51056592015-02-03 21:48:07 -0800693 exiting.filter('.host').each(hostExit);
694 exiting.filter('.device').each(deviceExit);
Simon Huntac4c6f72015-02-03 19:50:53 -0800695
Simon Hunt51056592015-02-03 21:48:07 -0800696 // finally, resume the force layout
Simon Huntac4c6f72015-02-03 19:50:53 -0800697 fResume();
698 }
699
Simon Hunt51056592015-02-03 21:48:07 -0800700 // ==========================
701 // updateNodes - subfunctions
702
703 function deviceExisting(d) {
704 var node = d.el;
705 node.classed('online', d.online);
706 updateDeviceLabel(d);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800707 tms.positionNode(d, true);
Simon Hunt51056592015-02-03 21:48:07 -0800708 }
709
710 function hostExisting(d) {
711 updateHostLabel(d);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800712 tms.positionNode(d, true);
Simon Hunt51056592015-02-03 21:48:07 -0800713 }
714
715 function deviceEnter(d) {
716 var node = d3.select(this),
717 glyphId = d.type || 'unknown',
718 label = trimLabel(deviceLabel(d)),
719 devCfg = deviceIconConfig,
720 noLabel = !label,
721 box, dx, dy, icon;
722
723 d.el = node;
724
725 node.append('rect').attr({ rx: 5, ry: 5 });
726 node.append('text').text(label).attr('dy', '1.1em');
727 box = adjustRectToFitText(node);
728 node.select('rect').attr(box);
729
730 icon = is.addDeviceIcon(node, glyphId);
731
732 if (noLabel) {
733 dx = -icon.dim/2;
734 dy = -icon.dim/2;
735 } else {
736 box = adjustRectToFitText(node);
737 dx = box.x + devCfg.xoff;
738 dy = box.y + devCfg.yoff;
739 }
740
741 icon.attr('transform', sus.translate(dx, dy));
742 }
743
744 function hostEnter(d) {
Simon Hunt1894d792015-02-04 17:09:20 -0800745 var node = d3.select(this),
746 gid = d.type || 'unknown',
747 rad = icfg.host.radius,
748 r = d.type ? rad.withGlyph : rad.noGlyph,
749 textDy = r + 10;
Simon Hunt51056592015-02-03 21:48:07 -0800750
751 d.el = node;
Simon Hunt1894d792015-02-04 17:09:20 -0800752 sus.makeVisible(node, showHosts);
Simon Hunt51056592015-02-03 21:48:07 -0800753
Simon Hunt1894d792015-02-04 17:09:20 -0800754 is.addHostIcon(node, r, gid);
Simon Hunt51056592015-02-03 21:48:07 -0800755
Simon Hunt51056592015-02-03 21:48:07 -0800756 node.append('text')
757 .text(hostLabel)
Simon Hunt1894d792015-02-04 17:09:20 -0800758 .attr('dy', textDy)
Simon Hunt51056592015-02-03 21:48:07 -0800759 .attr('text-anchor', 'middle');
760 }
761
762 function hostExit(d) {
763 var node = d.el;
764 node.select('use')
765 .style('opacity', 0.5)
766 .transition()
767 .duration(800)
768 .style('opacity', 0);
769
770 node.select('text')
771 .style('opacity', 0.5)
772 .transition()
773 .duration(800)
774 .style('opacity', 0);
775
776 node.select('circle')
777 .style('stroke-fill', '#555')
778 .style('fill', '#888')
779 .style('opacity', 0.5)
780 .transition()
781 .duration(1500)
782 .attr('r', 0);
783 }
784
785 function deviceExit(d) {
786 var node = d.el;
787 node.select('use')
788 .style('opacity', 0.5)
789 .transition()
790 .duration(800)
791 .style('opacity', 0);
792
793 node.selectAll('rect')
794 .style('stroke-fill', '#555')
795 .style('fill', '#888')
796 .style('opacity', 0.5);
797 }
798
Simon Hunt1894d792015-02-04 17:09:20 -0800799 // ==========================
800
801 function updateLinks() {
802 var th = ts.theme();
803
804 link = linkG.selectAll('.link')
805 .data(network.links, function (d) { return d.key; });
806
807 // operate on existing links:
808 //link.each(linkExisting);
809
810 // operate on entering links:
811 var entering = link.enter()
812 .append('line')
813 .attr({
814 x1: function (d) { return d.x1; },
815 y1: function (d) { return d.y1; },
816 x2: function (d) { return d.x2; },
817 y2: function (d) { return d.y2; },
818 stroke: linkConfig[th].inColor,
819 'stroke-width': linkConfig.inWidth
820 });
821
822 // augment links
823 entering.each(linkEntering);
824
825 // operate on both existing and new links:
826 //link.each(...)
827
828 // apply or remove labels
829 var labelData = getLabelData();
830 applyLinkLabels(labelData);
831
832 // operate on exiting links:
833 link.exit()
834 .attr('stroke-dasharray', '3 3')
Simon Hunt5724fb42015-02-05 16:59:40 -0800835 .attr('stroke', linkConfig[th].outColor)
Simon Hunt1894d792015-02-04 17:09:20 -0800836 .style('opacity', 0.5)
837 .transition()
838 .duration(1500)
839 .attr({
840 'stroke-dasharray': '3 12',
Simon Hunt1894d792015-02-04 17:09:20 -0800841 'stroke-width': linkConfig.outWidth
842 })
843 .style('opacity', 0.0)
844 .remove();
845
846 // NOTE: invoke a single tick to force the labels to position
847 // onto their links.
848 tick();
Simon Hunt5724fb42015-02-05 16:59:40 -0800849 // TODO: this causes undesirable behavior when in oblique view
Simon Hunt1894d792015-02-04 17:09:20 -0800850 // It causes the nodes to jump into "overhead" view positions, even
851 // though the oblique planes are still showing...
852 }
853
854 // ==========================
855 // updateLinks - subfunctions
856
857 function getLabelData() {
858 // create the backing data for showing labels..
859 var data = [];
860 link.each(function (d) {
861 if (d.label) {
862 data.push({
863 id: 'lab-' + d.key,
864 key: d.key,
865 label: d.label,
866 ldata: d
867 });
868 }
869 });
870 return data;
871 }
872
873 //function linkExisting(d) { }
874
875 function linkEntering(d) {
876 var link = d3.select(this);
877 d.el = link;
878 restyleLinkElement(d);
879 if (d.type() === 'hostLink') {
880 sus.makeVisible(link, showHosts);
881 }
882 }
883
884 //function linkExiting(d) { }
885
886 var linkLabelOffset = '0.3em';
887
888 function applyLinkLabels(data) {
889 var entering;
890
891 linkLabel = linkLabelG.selectAll('.linkLabel')
892 .data(data, function (d) { return d.id; });
893
894 // for elements already existing, we need to update the text
895 // and adjust the rectangle size to fit
896 linkLabel.each(function (d) {
897 var el = d3.select(this),
898 rect = el.select('rect'),
899 text = el.select('text');
900 text.text(d.label);
901 rect.attr(rectAroundText(el));
902 });
903
904 entering = linkLabel.enter().append('g')
905 .classed('linkLabel', true)
906 .attr('id', function (d) { return d.id; });
907
908 entering.each(function (d) {
909 var el = d3.select(this),
910 rect,
911 text,
912 parms = {
913 x1: d.ldata.x1,
914 y1: d.ldata.y1,
915 x2: d.ldata.x2,
916 y2: d.ldata.y2
917 };
918
919 d.el = el;
920 rect = el.append('rect');
921 text = el.append('text').text(d.label);
922 rect.attr(rectAroundText(el));
923 text.attr('dy', linkLabelOffset);
924
925 el.attr('transform', transformLabel(parms));
926 });
927
928 // Remove any labels that are no longer required.
929 linkLabel.exit().remove();
930 }
931
932 function rectAroundText(el) {
933 var text = el.select('text'),
934 box = text.node().getBBox();
935
936 // translate the bbox so that it is centered on [x,y]
937 box.x = -box.width / 2;
938 box.y = -box.height / 2;
939
940 // add padding
941 box.x -= 1;
942 box.width += 2;
943 return box;
944 }
945
946 function transformLabel(p) {
947 var dx = p.x2 - p.x1,
948 dy = p.y2 - p.y1,
949 xMid = dx/2 + p.x1,
950 yMid = dy/2 + p.y1;
951 return sus.translate(xMid, yMid);
952 }
Simon Huntac4c6f72015-02-03 19:50:53 -0800953
954 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -0800955 // force layout tick function
Simon Hunt737c89f2015-01-28 12:23:19 -0800956
Simon Hunt5724fb42015-02-05 16:59:40 -0800957 function fResume() {
958 if (!oblique) {
959 force.resume();
960 }
961 }
962
963 function fStart() {
964 if (!oblique) {
965 force.start();
966 }
967 }
968
969 var tickStuff = {
970 nodeAttr: {
971 transform: function (d) { return sus.translate(d.x, d.y); }
972 },
973 linkAttr: {
974 x1: function (d) { return d.source.x; },
975 y1: function (d) { return d.source.y; },
976 x2: function (d) { return d.target.x; },
977 y2: function (d) { return d.target.y; }
978 },
979 linkLabelAttr: {
980 transform: function (d) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800981 var lnk = tms.findLinkById(d.key);
Simon Hunt5724fb42015-02-05 16:59:40 -0800982 if (lnk) {
983 return transformLabel({
984 x1: lnk.source.x,
985 y1: lnk.source.y,
986 x2: lnk.target.x,
987 y2: lnk.target.y
988 });
989 }
990 }
991 }
992 };
993
994 function tick() {
995 node.attr(tickStuff.nodeAttr);
996 link.attr(tickStuff.linkAttr);
997 linkLabel.attr(tickStuff.linkLabelAttr);
Simon Hunt737c89f2015-01-28 12:23:19 -0800998 }
999
1000
Simon Hunt205099e2015-02-07 13:12:01 -08001001 function updateDetailPanel() {
1002 // TODO update detail panel
1003 $log.debug("TODO: updateDetailPanel() ...");
1004 }
1005
1006
1007 // ==========================
1008 // === SELECTION / DESELECTION
1009
1010 function selectObject(obj) {
1011 var el = this,
1012 ev = d3.event.sourceEvent,
1013 n;
1014
1015 if (zoomingOrPanning(ev)) {
1016 return;
1017 }
1018
1019 if (el) {
1020 n = d3.select(el);
1021 } else {
1022 node.each(function (d) {
1023 if (d == obj) {
1024 n = d3.select(el = this);
1025 }
1026 });
1027 }
1028 if (!n) return;
1029
1030 if (ev.shiftKey && n.classed('selected')) {
1031 deselectObject(obj.id);
1032 updateDetailPanel();
1033 return;
1034 }
1035
1036 if (!ev.shiftKey) {
1037 deselectAll();
1038 }
1039
1040 selections[obj.id] = { obj: obj, el: el };
1041 selectOrder.push(obj.id);
1042
1043 n.classed('selected', true);
1044 updateDeviceColors(obj);
1045 updateDetailPanel();
1046 }
1047
1048 function deselectObject(id) {
1049 var obj = selections[id];
1050 if (obj) {
1051 d3.select(obj.el).classed('selected', false);
1052 delete selections[id];
1053 fs.removeFromArray(id, selectOrder);
1054 updateDeviceColors(obj.obj);
1055 }
1056 }
1057
1058 function deselectAll() {
1059 // deselect all nodes in the network...
1060 node.classed('selected', false);
1061 selections = {};
1062 selectOrder = [];
1063 updateDeviceColors();
1064 updateDetailPanel();
1065 }
1066
Simon Huntac4c6f72015-02-03 19:50:53 -08001067 // ==========================
1068 // === MOUSE GESTURE HANDLERS
1069
Simon Hunt205099e2015-02-07 13:12:01 -08001070 function zoomingOrPanning(ev) {
1071 return ev.metaKey || ev.altKey;
Simon Hunt445e8152015-02-06 13:00:12 -08001072 }
1073
1074 function atDragEnd(d) {
1075 // once we've finished moving, pin the node in position
1076 d.fixed = true;
1077 d3.select(this).classed('fixed', true);
1078 sendUpdateMeta(d);
1079 }
1080
1081 // predicate that indicates when dragging is active
1082 function dragEnabled() {
1083 var ev = d3.event.sourceEvent;
1084 // nodeLock means we aren't allowing nodes to be dragged...
Simon Hunt205099e2015-02-07 13:12:01 -08001085 return !nodeLock && !zoomingOrPanning(ev);
Simon Hunt445e8152015-02-06 13:00:12 -08001086 }
1087
1088 // predicate that indicates when clicking is active
1089 function clickEnabled() {
1090 return true;
1091 }
Simon Hunt737c89f2015-01-28 12:23:19 -08001092
1093
1094 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -08001095 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -08001096
Simon Huntdc6adea2015-02-09 22:29:36 -08001097 function mkModelApi(uplink) {
1098 return {
1099 projection: uplink.projection,
1100 network: network,
1101 restyleLinkElement: restyleLinkElement,
1102 removeLinkElement: removeLinkElement
1103 };
1104 }
1105
Simon Hunt737c89f2015-01-28 12:23:19 -08001106 angular.module('ovTopo')
1107 .factory('TopoForceService',
Simon Hunt1894d792015-02-04 17:09:20 -08001108 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
Simon Hunt3a6eec02015-02-09 21:16:43 -08001109 'FlashService', 'TopoInstService', 'TopoModelService',
Simon Hunt737c89f2015-01-28 12:23:19 -08001110
Simon Hunt3a6eec02015-02-09 21:16:43 -08001111 function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_, _tms_) {
Simon Hunt737c89f2015-01-28 12:23:19 -08001112 $log = _$log_;
Simon Hunt1894d792015-02-04 17:09:20 -08001113 fs = _fs_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001114 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001115 is = _is_;
1116 ts = _ts_;
Simon Hunt5724fb42015-02-05 16:59:40 -08001117 flash = _flash_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001118 tis = _tis_;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001119 tms = _tms_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001120
Simon Hunt1894d792015-02-04 17:09:20 -08001121 icfg = is.iconConfig();
1122
Simon Hunt737c89f2015-01-28 12:23:19 -08001123 // forceG is the SVG group to display the force layout in
Simon Huntdc6adea2015-02-09 22:29:36 -08001124 // uplink is the api from the main topo source file
Simon Hunt3a6eec02015-02-09 21:16:43 -08001125 // dim is the initial dimensions of the SVG as [w,h]
Simon Hunt737c89f2015-01-28 12:23:19 -08001126 // opts are, well, optional :)
Simon Hunt3a6eec02015-02-09 21:16:43 -08001127 function initForce(forceG, _uplink_, _dim_, opts) {
Simon Hunt1894d792015-02-04 17:09:20 -08001128 uplink = _uplink_;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001129 dim = _dim_;
1130
1131 $log.debug('initForce().. dim = ' + dim);
1132
Simon Huntdc6adea2015-02-09 22:29:36 -08001133 tms.initModel(mkModelApi(uplink), dim);
Simon Hunta11b4eb2015-01-28 16:20:50 -08001134
Simon Hunt737c89f2015-01-28 12:23:19 -08001135 settings = angular.extend({}, defaultSettings, opts);
1136
1137 linkG = forceG.append('g').attr('id', 'topo-links');
1138 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
1139 nodeG = forceG.append('g').attr('id', 'topo-nodes');
1140
1141 link = linkG.selectAll('.link');
1142 linkLabel = linkLabelG.selectAll('.linkLabel');
1143 node = nodeG.selectAll('.node');
1144
1145 force = d3.layout.force()
Simon Hunt3a6eec02015-02-09 21:16:43 -08001146 .size(dim)
Simon Hunt737c89f2015-01-28 12:23:19 -08001147 .nodes(network.nodes)
1148 .links(network.links)
1149 .gravity(settings.gravity)
1150 .friction(settings.friction)
1151 .charge(settings.charge._def_)
1152 .linkDistance(settings.linkDistance._def_)
1153 .linkStrength(settings.linkStrength._def_)
1154 .on('tick', tick);
1155
1156 drag = sus.createDragBehavior(force,
Simon Hunt205099e2015-02-07 13:12:01 -08001157 selectObject, atDragEnd, dragEnabled, clickEnabled);
Simon Hunt737c89f2015-01-28 12:23:19 -08001158 }
1159
Simon Hunt3a6eec02015-02-09 21:16:43 -08001160 function newDim(_dim_) {
1161 dim = _dim_;
1162 force.size(dim);
1163 tms.newDim(dim);
Simon Hunt737c89f2015-01-28 12:23:19 -08001164 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -08001165 }
1166
Simon Hunt3a6eec02015-02-09 21:16:43 -08001167 function destroyForce() {
1168
1169 }
1170
Simon Hunt737c89f2015-01-28 12:23:19 -08001171 return {
1172 initForce: initForce,
Simon Hunt3a6eec02015-02-09 21:16:43 -08001173 newDim: newDim,
1174 destroyForce: destroyForce,
Simon Huntac4c6f72015-02-03 19:50:53 -08001175
1176 updateDeviceColors: updateDeviceColors,
Simon Hunt5724fb42015-02-05 16:59:40 -08001177 toggleHosts: toggleHosts,
1178 toggleOffline: toggleOffline,
1179 cycleDeviceLabels: cycleDeviceLabels,
Simon Hunt445e8152015-02-06 13:00:12 -08001180 unpin: unpin,
Simon Huntac4c6f72015-02-03 19:50:53 -08001181
1182 addDevice: addDevice,
Simon Hunt1894d792015-02-04 17:09:20 -08001183 updateDevice: updateDevice,
1184 removeDevice: removeDevice,
1185 addHost: addHost,
1186 updateHost: updateHost,
1187 removeHost: removeHost,
1188 addLink: addLink,
1189 updateLink: updateLink,
1190 removeLink: removeLink
Simon Hunt737c89f2015-01-28 12:23:19 -08001191 };
1192 }]);
1193}());