blob: 87b554e57ef5f89def9c14d068ff36cf338ac3bd [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 Hunt08f841d02015-02-10 14:39:20 -080026 var $log, fs, sus, is, ts, flash, tis, tms, tss, 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 Hunt08f841d02015-02-10 14:39:20 -080080 dim; // the dimensions of the force layout [w,h]
Simon Hunt737c89f2015-01-28 12:23:19 -080081
82 // SVG elements;
83 var linkG, linkLabelG, nodeG;
84
85 // D3 selections;
86 var link, linkLabel, node;
87
88 // default settings for force layout
89 var defaultSettings = {
90 gravity: 0.4,
91 friction: 0.7,
92 charge: {
93 // note: key is node.class
94 device: -8000,
95 host: -5000,
96 _def_: -12000
97 },
98 linkDistance: {
99 // note: key is link.type
100 direct: 100,
101 optical: 120,
102 hostLink: 3,
103 _def_: 50
104 },
105 linkStrength: {
106 // note: key is link.type
107 // range: {0.0 ... 1.0}
108 //direct: 1.0,
109 //optical: 1.0,
110 //hostLink: 1.0,
111 _def_: 1.0
112 }
113 };
114
115
Simon Huntac4c6f72015-02-03 19:50:53 -0800116 // ==========================
117 // === EVENT HANDLERS
118
119 function addDevice(data) {
120 var id = data.id,
121 d;
122
Simon Hunt1894d792015-02-04 17:09:20 -0800123 uplink.showNoDevs(false);
Simon Huntac4c6f72015-02-03 19:50:53 -0800124
125 // although this is an add device event, if we already have the
126 // device, treat it as an update instead..
Simon Hunt1894d792015-02-04 17:09:20 -0800127 if (lu[id]) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800128 updateDevice(data);
129 return;
130 }
131
Simon Hunt3a6eec02015-02-09 21:16:43 -0800132 d = tms.createDeviceNode(data);
Simon Huntac4c6f72015-02-03 19:50:53 -0800133 network.nodes.push(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800134 lu[id] = d;
Simon Huntac4c6f72015-02-03 19:50:53 -0800135 updateNodes();
136 fStart();
137 }
138
139 function updateDevice(data) {
140 var id = data.id,
Simon Hunt1894d792015-02-04 17:09:20 -0800141 d = lu[id],
Simon Huntac4c6f72015-02-03 19:50:53 -0800142 wasOnline;
143
144 if (d) {
145 wasOnline = d.online;
146 angular.extend(d, data);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800147 if (tms.positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800148 sendUpdateMeta(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800149 }
150 updateNodes();
151 if (wasOnline !== d.online) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800152 tms.findAttachedLinks(d.id).forEach(restyleLinkElement);
Simon Hunt5724fb42015-02-05 16:59:40 -0800153 updateOfflineVisibility(d);
Simon Huntac4c6f72015-02-03 19:50:53 -0800154 }
155 } else {
156 // TODO: decide whether we want to capture logic errors
157 //logicError('updateDevice lookup fail. ID = "' + id + '"');
158 }
159 }
160
Simon Hunt1894d792015-02-04 17:09:20 -0800161 function removeDevice(data) {
162 var id = data.id,
163 d = lu[id];
164 if (d) {
165 removeDeviceElement(d);
166 } else {
167 // TODO: decide whether we want to capture logic errors
168 //logicError('removeDevice lookup fail. ID = "' + id + '"');
169 }
170 }
171
172 function addHost(data) {
173 var id = data.id,
174 d, lnk;
175
176 // although this is an add host event, if we already have the
177 // host, treat it as an update instead..
178 if (lu[id]) {
179 updateHost(data);
180 return;
181 }
182
Simon Hunt3a6eec02015-02-09 21:16:43 -0800183 d = tms.createHostNode(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800184 network.nodes.push(d);
185 lu[id] = d;
Simon Hunt1894d792015-02-04 17:09:20 -0800186 updateNodes();
187
Simon Hunt3a6eec02015-02-09 21:16:43 -0800188 lnk = tms.createHostLink(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800189 if (lnk) {
Simon Hunt1894d792015-02-04 17:09:20 -0800190 d.linkData = lnk; // cache ref on its host
191 network.links.push(lnk);
192 lu[d.ingress] = lnk;
193 lu[d.egress] = lnk;
194 updateLinks();
195 }
196
197 fStart();
198 }
199
200 function updateHost(data) {
201 var id = data.id,
202 d = lu[id];
203 if (d) {
204 angular.extend(d, data);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800205 if (tms.positionNode(d, true)) {
Simon Hunt445e8152015-02-06 13:00:12 -0800206 sendUpdateMeta(d);
Simon Hunt1894d792015-02-04 17:09:20 -0800207 }
208 updateNodes();
209 } else {
210 // TODO: decide whether we want to capture logic errors
211 //logicError('updateHost lookup fail. ID = "' + id + '"');
212 }
213 }
214
215 function removeHost(data) {
216 var id = data.id,
217 d = lu[id];
218 if (d) {
219 removeHostElement(d, true);
220 } else {
221 // may have already removed host, if attached to removed device
222 //console.warn('removeHost lookup fail. ID = "' + id + '"');
223 }
224 }
225
226 function addLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800227 var result = tms.findLink(data, 'add'),
Simon Hunt1894d792015-02-04 17:09:20 -0800228 bad = result.badLogic,
229 d = result.ldata;
230
231 if (bad) {
232 //logicError(bad + ': ' + link.id);
233 return;
234 }
235
236 if (d) {
237 // we already have a backing store link for src/dst nodes
238 addLinkUpdate(d, data);
239 return;
240 }
241
242 // no backing store link yet
Simon Hunt3a6eec02015-02-09 21:16:43 -0800243 d = tms.createLink(data);
Simon Hunt1894d792015-02-04 17:09:20 -0800244 if (d) {
245 network.links.push(d);
246 lu[d.key] = d;
247 updateLinks();
248 fStart();
249 }
250 }
251
252 function updateLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800253 var result = tms.findLink(data, 'update'),
Simon Hunt1894d792015-02-04 17:09:20 -0800254 bad = result.badLogic;
255 if (bad) {
256 //logicError(bad + ': ' + link.id);
257 return;
258 }
259 result.updateWith(link);
260 }
261
262 function removeLink(data) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800263 var result = tms.findLink(data, 'remove'),
Simon Hunt1894d792015-02-04 17:09:20 -0800264 bad = result.badLogic;
265 if (bad) {
266 // may have already removed link, if attached to removed device
267 //console.warn(bad + ': ' + link.id);
268 return;
269 }
270 result.removeRawLink();
271 }
272
273 // ========================
274
275 function addLinkUpdate(ldata, link) {
276 // add link event, but we already have the reverse link installed
277 ldata.fromTarget = link;
Simon Huntdc6adea2015-02-09 22:29:36 -0800278 rlk[link.id] = ldata.key;
Simon Hunt1894d792015-02-04 17:09:20 -0800279 restyleLinkElement(ldata);
280 }
281
Simon Hunt1894d792015-02-04 17:09:20 -0800282
283 var widthRatio = 1.4,
284 linkScale = d3.scale.linear()
285 .domain([1, 12])
286 .range([widthRatio, 12 * widthRatio])
Simon Hunt5724fb42015-02-05 16:59:40 -0800287 .clamp(true),
Simon Hunt3a6eec02015-02-09 21:16:43 -0800288 allLinkTypes = 'direct indirect optical tunnel';
Simon Hunt1894d792015-02-04 17:09:20 -0800289
290 function restyleLinkElement(ldata) {
291 // this fn's job is to look at raw links and decide what svg classes
292 // need to be applied to the line element in the DOM
293 var th = ts.theme(),
294 el = ldata.el,
295 type = ldata.type(),
296 lw = ldata.linkWidth(),
297 online = ldata.online();
298
299 el.classed('link', true);
300 el.classed('inactive', !online);
301 el.classed(allLinkTypes, false);
302 if (type) {
303 el.classed(type, true);
304 }
305 el.transition()
306 .duration(1000)
307 .attr('stroke-width', linkScale(lw))
308 .attr('stroke', linkConfig[th].baseColor);
309 }
310
Simon Hunt1894d792015-02-04 17:09:20 -0800311 function removeLinkElement(d) {
312 var idx = fs.find(d.key, network.links, 'key'),
313 removed;
314 if (idx >=0) {
315 // remove from links array
316 removed = network.links.splice(idx, 1);
317 // remove from lookup cache
318 delete lu[removed[0].key];
319 updateLinks();
320 fResume();
321 }
322 }
323
324 function removeHostElement(d, upd) {
325 // first, remove associated hostLink...
326 removeLinkElement(d.linkData);
327
328 // remove hostLink bindings
329 delete lu[d.ingress];
330 delete lu[d.egress];
331
332 // remove from lookup cache
333 delete lu[d.id];
334 // remove from nodes array
335 var idx = fs.find(d.id, network.nodes);
336 network.nodes.splice(idx, 1);
337
338 // remove from SVG
339 // NOTE: upd is false if we were called from removeDeviceElement()
340 if (upd) {
341 updateNodes();
342 fResume();
343 }
344 }
345
346 function removeDeviceElement(d) {
347 var id = d.id;
348 // first, remove associated hosts and links..
Simon Huntdc6adea2015-02-09 22:29:36 -0800349 tms.findAttachedHosts(id).forEach(removeHostElement);
350 tms.findAttachedLinks(id).forEach(removeLinkElement);
Simon Hunt1894d792015-02-04 17:09:20 -0800351
352 // remove from lookup cache
353 delete lu[id];
354 // remove from nodes array
355 var idx = fs.find(id, network.nodes);
356 network.nodes.splice(idx, 1);
357
358 if (!network.nodes.length) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800359 uplink.showNoDevs(true);
Simon Hunt1894d792015-02-04 17:09:20 -0800360 }
361
362 // remove from SVG
363 updateNodes();
364 fResume();
365 }
366
Simon Hunt5724fb42015-02-05 16:59:40 -0800367 function updateHostVisibility() {
368 sus.makeVisible(nodeG.selectAll('.host'), showHosts);
369 sus.makeVisible(linkG.selectAll('.hostLink'), showHosts);
370 }
371
372 function updateOfflineVisibility(dev) {
373 function updDev(d, show) {
374 sus.makeVisible(d.el, show);
375
Simon Huntdc6adea2015-02-09 22:29:36 -0800376 tms.findAttachedLinks(d.id).forEach(function (link) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800377 b = show && ((link.type() !== 'hostLink') || showHosts);
378 sus.makeVisible(link.el, b);
379 });
Simon Huntdc6adea2015-02-09 22:29:36 -0800380 tms.findAttachedHosts(d.id).forEach(function (host) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800381 b = show && showHosts;
382 sus.makeVisible(host.el, b);
383 });
384 }
385
386 if (dev) {
387 // updating a specific device that just toggled off/on-line
388 updDev(dev, dev.online || showOffline);
389 } else {
390 // updating all offline devices
Simon Huntdc6adea2015-02-09 22:29:36 -0800391 tms.findDevices(true).forEach(function (d) {
Simon Hunt5724fb42015-02-05 16:59:40 -0800392 updDev(d, showOffline);
393 });
394 }
395 }
396
Simon Hunt1894d792015-02-04 17:09:20 -0800397
Simon Hunt445e8152015-02-06 13:00:12 -0800398 function sendUpdateMeta(d, clearPos) {
Simon Huntac4c6f72015-02-03 19:50:53 -0800399 var metaUi = {},
400 ll;
401
Simon Hunt445e8152015-02-06 13:00:12 -0800402 // if we are not clearing the position data (unpinning),
403 // attach the x, y, longitude, latitude...
404 if (!clearPos) {
Simon Hunt3a6eec02015-02-09 21:16:43 -0800405 ll = tms.lngLatFromCoord([d.x, d.y]);
Simon Huntdc6adea2015-02-09 22:29:36 -0800406 metaUi = {x: d.x, y: d.y, lng: ll[0], lat: ll[1]};
Simon Hunt1894d792015-02-04 17:09:20 -0800407 }
408 d.metaUi = metaUi;
409 uplink.sendEvent('updateMeta', {
410 id: d.id,
411 'class': d.class,
412 memento: metaUi
413 });
Simon Huntac4c6f72015-02-03 19:50:53 -0800414 }
415
Simon Hunt1894d792015-02-04 17:09:20 -0800416
Simon Huntac4c6f72015-02-03 19:50:53 -0800417 // ==========================
418 // === Devices and hosts - D3 rendering
419
Simon Hunt1894d792015-02-04 17:09:20 -0800420
Simon Huntac4c6f72015-02-03 19:50:53 -0800421 // Returns the newly computed bounding box of the rectangle
422 function adjustRectToFitText(n) {
423 var text = n.select('text'),
424 box = text.node().getBBox(),
425 lab = labelConfig;
426
427 text.attr('text-anchor', 'middle')
428 .attr('y', '-0.8em')
429 .attr('x', lab.imgPad/2);
430
431 // translate the bbox so that it is centered on [x,y]
432 box.x = -box.width / 2;
433 box.y = -box.height / 2;
434
435 // add padding
436 box.x -= (lab.padLR + lab.imgPad/2);
437 box.width += lab.padLR * 2 + lab.imgPad;
438 box.y -= lab.padTB;
439 box.height += lab.padTB * 2;
440
441 return box;
442 }
443
444 function mkSvgClass(d) {
445 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
446 }
447
448 function hostLabel(d) {
449 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
450 return d.labels[idx];
451 }
452 function deviceLabel(d) {
453 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
454 return d.labels[idx];
455 }
456 function trimLabel(label) {
457 return (label && label.trim()) || '';
458 }
459
460 function emptyBox() {
461 return {
462 x: -2,
463 y: -2,
464 width: 4,
465 height: 4
466 };
467 }
468
469
470 function updateDeviceLabel(d) {
471 var label = trimLabel(deviceLabel(d)),
472 noLabel = !label,
473 node = d.el,
Simon Hunt1894d792015-02-04 17:09:20 -0800474 dim = icfg.device.dim,
Simon Huntac4c6f72015-02-03 19:50:53 -0800475 devCfg = deviceIconConfig,
476 box, dx, dy;
477
478 node.select('text')
479 .text(label)
480 .style('opacity', 0)
481 .transition()
482 .style('opacity', 1);
483
484 if (noLabel) {
485 box = emptyBox();
486 dx = -dim/2;
487 dy = -dim/2;
488 } else {
489 box = adjustRectToFitText(node);
490 dx = box.x + devCfg.xoff;
491 dy = box.y + devCfg.yoff;
492 }
493
494 node.select('rect')
495 .transition()
496 .attr(box);
497
498 node.select('g.deviceIcon')
499 .transition()
500 .attr('transform', sus.translate(dx, dy));
501 }
502
503 function updateHostLabel(d) {
504 var label = trimLabel(hostLabel(d));
505 d.el.select('text').text(label);
506 }
507
Simon Huntac4c6f72015-02-03 19:50:53 -0800508 function updateDeviceColors(d) {
509 if (d) {
510 setDeviceColor(d);
511 } else {
512 node.filter('.device').each(function (d) {
513 setDeviceColor(d);
514 });
515 }
516 }
517
Simon Hunt5724fb42015-02-05 16:59:40 -0800518 function vis(b) {
519 return b ? 'visible' : 'hidden';
520 }
521
522 function toggleHosts() {
523 showHosts = !showHosts;
524 updateHostVisibility();
525 flash.flash('Hosts ' + vis(showHosts));
526 }
527
528 function toggleOffline() {
529 showOffline = !showOffline;
530 updateOfflineVisibility();
531 flash.flash('Offline devices ' + vis(showOffline));
532 }
533
534 function cycleDeviceLabels() {
Simon Hunt1c367112015-02-05 18:02:46 -0800535 deviceLabelIndex = (deviceLabelIndex+1) % 3;
Simon Huntdc6adea2015-02-09 22:29:36 -0800536 tms.findDevices().forEach(function (d) {
Simon Hunt1c367112015-02-05 18:02:46 -0800537 updateDeviceLabel(d);
538 });
Simon Hunt5724fb42015-02-05 16:59:40 -0800539 }
540
Simon Hunt445e8152015-02-06 13:00:12 -0800541 function unpin() {
Simon Hunt08f841d02015-02-10 14:39:20 -0800542 var hov = tss.hovered();
543 if (hov) {
544 sendUpdateMeta(hov, true);
545 hov.fixed = false;
546 hov.el.classed('fixed', false);
Simon Hunt445e8152015-02-06 13:00:12 -0800547 fResume();
548 }
549 }
550
551
Simon Hunt5724fb42015-02-05 16:59:40 -0800552 // ==========================================
553
Simon Huntac4c6f72015-02-03 19:50:53 -0800554 var dCol = {
555 black: '#000',
556 paleblue: '#acf',
557 offwhite: '#ddd',
Simon Hunt78c10bf2015-02-03 21:18:20 -0800558 darkgrey: '#444',
Simon Huntac4c6f72015-02-03 19:50:53 -0800559 midgrey: '#888',
560 lightgrey: '#bbb',
561 orange: '#f90'
562 };
563
564 // note: these are the device icon colors without affinity
565 var dColTheme = {
566 light: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800567 rfill: dCol.offwhite,
Simon Huntac4c6f72015-02-03 19:50:53 -0800568 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800569 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800570 rect: dCol.paleblue
571 },
572 offline: {
573 glyph: dCol.midgrey,
574 rect: dCol.lightgrey
575 }
576 },
Simon Huntac4c6f72015-02-03 19:50:53 -0800577 dark: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800578 rfill: dCol.midgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800579 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800580 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800581 rect: dCol.paleblue
582 },
583 offline: {
584 glyph: dCol.midgrey,
Simon Hunt78c10bf2015-02-03 21:18:20 -0800585 rect: dCol.darkgrey
Simon Huntac4c6f72015-02-03 19:50:53 -0800586 }
587 }
588 };
589
590 function devBaseColor(d) {
591 var o = d.online ? 'online' : 'offline';
592 return dColTheme[ts.theme()][o];
593 }
594
595 function setDeviceColor(d) {
596 var o = d.online,
597 s = d.el.classed('selected'),
598 c = devBaseColor(d),
599 a = instColor(d.master, o),
Simon Hunt51056592015-02-03 21:48:07 -0800600 icon = d.el.select('g.deviceIcon'),
601 g, r;
Simon Huntac4c6f72015-02-03 19:50:53 -0800602
603 if (s) {
604 g = c.glyph;
605 r = dCol.orange;
606 } else if (tis.isVisible()) {
607 g = o ? a : c.glyph;
Simon Hunt78c10bf2015-02-03 21:18:20 -0800608 r = o ? c.rfill : a;
Simon Huntac4c6f72015-02-03 19:50:53 -0800609 } else {
610 g = c.glyph;
611 r = c.rect;
612 }
613
Simon Hunt51056592015-02-03 21:48:07 -0800614 icon.select('use').style('fill', g);
615 icon.select('rect').style('fill', r);
Simon Huntac4c6f72015-02-03 19:50:53 -0800616 }
617
618 function instColor(id, online) {
619 return sus.cat7().getColor(id, !online, ts.theme());
620 }
621
Simon Hunt1894d792015-02-04 17:09:20 -0800622 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800623
624 function updateNodes() {
Simon Hunt1894d792015-02-04 17:09:20 -0800625 // select all the nodes in the layout:
Simon Huntac4c6f72015-02-03 19:50:53 -0800626 node = nodeG.selectAll('.node')
627 .data(network.nodes, function (d) { return d.id; });
628
Simon Hunt1894d792015-02-04 17:09:20 -0800629 // operate on existing nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800630 node.filter('.device').each(deviceExisting);
631 node.filter('.host').each(hostExisting);
Simon Huntac4c6f72015-02-03 19:50:53 -0800632
633 // operate on entering nodes:
634 var entering = node.enter()
635 .append('g')
636 .attr({
637 id: function (d) { return sus.safeId(d.id); },
638 class: mkSvgClass,
639 transform: function (d) { return sus.translate(d.x, d.y); },
640 opacity: 0
641 })
642 .call(drag)
Simon Hunt08f841d02015-02-10 14:39:20 -0800643 .on('mouseover', tss.nodeMouseOver)
644 .on('mouseout', tss.nodeMouseOut)
Simon Huntac4c6f72015-02-03 19:50:53 -0800645 .transition()
646 .attr('opacity', 1);
647
Simon Hunt1894d792015-02-04 17:09:20 -0800648 // augment entering nodes:
Simon Hunt51056592015-02-03 21:48:07 -0800649 entering.filter('.device').each(deviceEnter);
650 entering.filter('.host').each(hostEnter);
Simon Huntac4c6f72015-02-03 19:50:53 -0800651
Simon Hunt51056592015-02-03 21:48:07 -0800652 // operate on both existing and new nodes:
Simon Huntac4c6f72015-02-03 19:50:53 -0800653 updateDeviceColors();
654
655 // operate on exiting nodes:
656 // Note that the node is removed after 2 seconds.
657 // Sub element animations should be shorter than 2 seconds.
658 var exiting = node.exit()
659 .transition()
660 .duration(2000)
661 .style('opacity', 0)
662 .remove();
663
Simon Hunt1894d792015-02-04 17:09:20 -0800664 // exiting node specifics:
Simon Hunt51056592015-02-03 21:48:07 -0800665 exiting.filter('.host').each(hostExit);
666 exiting.filter('.device').each(deviceExit);
Simon Huntac4c6f72015-02-03 19:50:53 -0800667
Simon Hunt51056592015-02-03 21:48:07 -0800668 // finally, resume the force layout
Simon Huntac4c6f72015-02-03 19:50:53 -0800669 fResume();
670 }
671
Simon Hunt51056592015-02-03 21:48:07 -0800672 // ==========================
673 // updateNodes - subfunctions
674
675 function deviceExisting(d) {
676 var node = d.el;
677 node.classed('online', d.online);
678 updateDeviceLabel(d);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800679 tms.positionNode(d, true);
Simon Hunt51056592015-02-03 21:48:07 -0800680 }
681
682 function hostExisting(d) {
683 updateHostLabel(d);
Simon Hunt3a6eec02015-02-09 21:16:43 -0800684 tms.positionNode(d, true);
Simon Hunt51056592015-02-03 21:48:07 -0800685 }
686
687 function deviceEnter(d) {
688 var node = d3.select(this),
689 glyphId = d.type || 'unknown',
690 label = trimLabel(deviceLabel(d)),
691 devCfg = deviceIconConfig,
692 noLabel = !label,
693 box, dx, dy, icon;
694
695 d.el = node;
696
697 node.append('rect').attr({ rx: 5, ry: 5 });
698 node.append('text').text(label).attr('dy', '1.1em');
699 box = adjustRectToFitText(node);
700 node.select('rect').attr(box);
701
702 icon = is.addDeviceIcon(node, glyphId);
703
704 if (noLabel) {
705 dx = -icon.dim/2;
706 dy = -icon.dim/2;
707 } else {
708 box = adjustRectToFitText(node);
709 dx = box.x + devCfg.xoff;
710 dy = box.y + devCfg.yoff;
711 }
712
713 icon.attr('transform', sus.translate(dx, dy));
714 }
715
716 function hostEnter(d) {
Simon Hunt1894d792015-02-04 17:09:20 -0800717 var node = d3.select(this),
718 gid = d.type || 'unknown',
719 rad = icfg.host.radius,
720 r = d.type ? rad.withGlyph : rad.noGlyph,
721 textDy = r + 10;
Simon Hunt51056592015-02-03 21:48:07 -0800722
723 d.el = node;
Simon Hunt1894d792015-02-04 17:09:20 -0800724 sus.makeVisible(node, showHosts);
Simon Hunt51056592015-02-03 21:48:07 -0800725
Simon Hunt1894d792015-02-04 17:09:20 -0800726 is.addHostIcon(node, r, gid);
Simon Hunt51056592015-02-03 21:48:07 -0800727
Simon Hunt51056592015-02-03 21:48:07 -0800728 node.append('text')
729 .text(hostLabel)
Simon Hunt1894d792015-02-04 17:09:20 -0800730 .attr('dy', textDy)
Simon Hunt51056592015-02-03 21:48:07 -0800731 .attr('text-anchor', 'middle');
732 }
733
734 function hostExit(d) {
735 var node = d.el;
736 node.select('use')
737 .style('opacity', 0.5)
738 .transition()
739 .duration(800)
740 .style('opacity', 0);
741
742 node.select('text')
743 .style('opacity', 0.5)
744 .transition()
745 .duration(800)
746 .style('opacity', 0);
747
748 node.select('circle')
749 .style('stroke-fill', '#555')
750 .style('fill', '#888')
751 .style('opacity', 0.5)
752 .transition()
753 .duration(1500)
754 .attr('r', 0);
755 }
756
757 function deviceExit(d) {
758 var node = d.el;
759 node.select('use')
760 .style('opacity', 0.5)
761 .transition()
762 .duration(800)
763 .style('opacity', 0);
764
765 node.selectAll('rect')
766 .style('stroke-fill', '#555')
767 .style('fill', '#888')
768 .style('opacity', 0.5);
769 }
770
Simon Hunt1894d792015-02-04 17:09:20 -0800771 // ==========================
772
773 function updateLinks() {
774 var th = ts.theme();
775
776 link = linkG.selectAll('.link')
777 .data(network.links, function (d) { return d.key; });
778
779 // operate on existing links:
780 //link.each(linkExisting);
781
782 // operate on entering links:
783 var entering = link.enter()
784 .append('line')
785 .attr({
786 x1: function (d) { return d.x1; },
787 y1: function (d) { return d.y1; },
788 x2: function (d) { return d.x2; },
789 y2: function (d) { return d.y2; },
790 stroke: linkConfig[th].inColor,
791 'stroke-width': linkConfig.inWidth
792 });
793
794 // augment links
795 entering.each(linkEntering);
796
797 // operate on both existing and new links:
798 //link.each(...)
799
800 // apply or remove labels
801 var labelData = getLabelData();
802 applyLinkLabels(labelData);
803
804 // operate on exiting links:
805 link.exit()
806 .attr('stroke-dasharray', '3 3')
Simon Hunt5724fb42015-02-05 16:59:40 -0800807 .attr('stroke', linkConfig[th].outColor)
Simon Hunt1894d792015-02-04 17:09:20 -0800808 .style('opacity', 0.5)
809 .transition()
810 .duration(1500)
811 .attr({
812 'stroke-dasharray': '3 12',
Simon Hunt1894d792015-02-04 17:09:20 -0800813 'stroke-width': linkConfig.outWidth
814 })
815 .style('opacity', 0.0)
816 .remove();
817
818 // NOTE: invoke a single tick to force the labels to position
819 // onto their links.
820 tick();
Simon Hunt5724fb42015-02-05 16:59:40 -0800821 // TODO: this causes undesirable behavior when in oblique view
Simon Hunt1894d792015-02-04 17:09:20 -0800822 // It causes the nodes to jump into "overhead" view positions, even
823 // though the oblique planes are still showing...
824 }
825
826 // ==========================
827 // updateLinks - subfunctions
828
829 function getLabelData() {
830 // create the backing data for showing labels..
831 var data = [];
832 link.each(function (d) {
833 if (d.label) {
834 data.push({
835 id: 'lab-' + d.key,
836 key: d.key,
837 label: d.label,
838 ldata: d
839 });
840 }
841 });
842 return data;
843 }
844
845 //function linkExisting(d) { }
846
847 function linkEntering(d) {
848 var link = d3.select(this);
849 d.el = link;
850 restyleLinkElement(d);
851 if (d.type() === 'hostLink') {
852 sus.makeVisible(link, showHosts);
853 }
854 }
855
856 //function linkExiting(d) { }
857
858 var linkLabelOffset = '0.3em';
859
860 function applyLinkLabels(data) {
861 var entering;
862
863 linkLabel = linkLabelG.selectAll('.linkLabel')
864 .data(data, function (d) { return d.id; });
865
866 // for elements already existing, we need to update the text
867 // and adjust the rectangle size to fit
868 linkLabel.each(function (d) {
869 var el = d3.select(this),
870 rect = el.select('rect'),
871 text = el.select('text');
872 text.text(d.label);
873 rect.attr(rectAroundText(el));
874 });
875
876 entering = linkLabel.enter().append('g')
877 .classed('linkLabel', true)
878 .attr('id', function (d) { return d.id; });
879
880 entering.each(function (d) {
881 var el = d3.select(this),
882 rect,
883 text,
884 parms = {
885 x1: d.ldata.x1,
886 y1: d.ldata.y1,
887 x2: d.ldata.x2,
888 y2: d.ldata.y2
889 };
890
891 d.el = el;
892 rect = el.append('rect');
893 text = el.append('text').text(d.label);
894 rect.attr(rectAroundText(el));
895 text.attr('dy', linkLabelOffset);
896
897 el.attr('transform', transformLabel(parms));
898 });
899
900 // Remove any labels that are no longer required.
901 linkLabel.exit().remove();
902 }
903
904 function rectAroundText(el) {
905 var text = el.select('text'),
906 box = text.node().getBBox();
907
908 // translate the bbox so that it is centered on [x,y]
909 box.x = -box.width / 2;
910 box.y = -box.height / 2;
911
912 // add padding
913 box.x -= 1;
914 box.width += 2;
915 return box;
916 }
917
918 function transformLabel(p) {
919 var dx = p.x2 - p.x1,
920 dy = p.y2 - p.y1,
921 xMid = dx/2 + p.x1,
922 yMid = dy/2 + p.y1;
923 return sus.translate(xMid, yMid);
924 }
Simon Huntac4c6f72015-02-03 19:50:53 -0800925
926 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -0800927 // force layout tick function
Simon Hunt737c89f2015-01-28 12:23:19 -0800928
Simon Hunt5724fb42015-02-05 16:59:40 -0800929 function fResume() {
930 if (!oblique) {
931 force.resume();
932 }
933 }
934
935 function fStart() {
936 if (!oblique) {
937 force.start();
938 }
939 }
940
941 var tickStuff = {
942 nodeAttr: {
943 transform: function (d) { return sus.translate(d.x, d.y); }
944 },
945 linkAttr: {
946 x1: function (d) { return d.source.x; },
947 y1: function (d) { return d.source.y; },
948 x2: function (d) { return d.target.x; },
949 y2: function (d) { return d.target.y; }
950 },
951 linkLabelAttr: {
952 transform: function (d) {
Simon Huntdc6adea2015-02-09 22:29:36 -0800953 var lnk = tms.findLinkById(d.key);
Simon Hunt5724fb42015-02-05 16:59:40 -0800954 if (lnk) {
955 return transformLabel({
956 x1: lnk.source.x,
957 y1: lnk.source.y,
958 x2: lnk.target.x,
959 y2: lnk.target.y
960 });
961 }
962 }
963 }
964 };
965
966 function tick() {
967 node.attr(tickStuff.nodeAttr);
968 link.attr(tickStuff.linkAttr);
969 linkLabel.attr(tickStuff.linkLabelAttr);
Simon Hunt737c89f2015-01-28 12:23:19 -0800970 }
971
972
Simon Huntac4c6f72015-02-03 19:50:53 -0800973 // ==========================
974 // === MOUSE GESTURE HANDLERS
975
Simon Hunt205099e2015-02-07 13:12:01 -0800976 function zoomingOrPanning(ev) {
977 return ev.metaKey || ev.altKey;
Simon Hunt445e8152015-02-06 13:00:12 -0800978 }
979
980 function atDragEnd(d) {
981 // once we've finished moving, pin the node in position
982 d.fixed = true;
983 d3.select(this).classed('fixed', true);
984 sendUpdateMeta(d);
985 }
986
987 // predicate that indicates when dragging is active
988 function dragEnabled() {
989 var ev = d3.event.sourceEvent;
990 // nodeLock means we aren't allowing nodes to be dragged...
Simon Hunt205099e2015-02-07 13:12:01 -0800991 return !nodeLock && !zoomingOrPanning(ev);
Simon Hunt445e8152015-02-06 13:00:12 -0800992 }
993
994 // predicate that indicates when clicking is active
995 function clickEnabled() {
996 return true;
997 }
Simon Hunt737c89f2015-01-28 12:23:19 -0800998
999
1000 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -08001001 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -08001002
Simon Huntdc6adea2015-02-09 22:29:36 -08001003 function mkModelApi(uplink) {
1004 return {
1005 projection: uplink.projection,
1006 network: network,
1007 restyleLinkElement: restyleLinkElement,
1008 removeLinkElement: removeLinkElement
1009 };
1010 }
1011
Simon Hunt08f841d02015-02-10 14:39:20 -08001012 function mkSelectApi(uplink) {
1013 return {
1014 node: function () { return node; },
1015 zoomingOrPanning: zoomingOrPanning,
1016 updateDeviceColors: updateDeviceColors,
1017 sendEvent: uplink.sendEvent
1018 };
1019 }
1020
Simon Hunt737c89f2015-01-28 12:23:19 -08001021 angular.module('ovTopo')
1022 .factory('TopoForceService',
Simon Hunt1894d792015-02-04 17:09:20 -08001023 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
Simon Hunt3a6eec02015-02-09 21:16:43 -08001024 'FlashService', 'TopoInstService', 'TopoModelService',
Simon Hunt08f841d02015-02-10 14:39:20 -08001025 'TopoSelectService',
Simon Hunt737c89f2015-01-28 12:23:19 -08001026
Simon Hunt08f841d02015-02-10 14:39:20 -08001027 function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_, _tms_, _tss_) {
Simon Hunt737c89f2015-01-28 12:23:19 -08001028 $log = _$log_;
Simon Hunt1894d792015-02-04 17:09:20 -08001029 fs = _fs_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001030 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001031 is = _is_;
1032 ts = _ts_;
Simon Hunt5724fb42015-02-05 16:59:40 -08001033 flash = _flash_;
Simon Huntac4c6f72015-02-03 19:50:53 -08001034 tis = _tis_;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001035 tms = _tms_;
Simon Hunt08f841d02015-02-10 14:39:20 -08001036 tss = _tss_;
Simon Hunt737c89f2015-01-28 12:23:19 -08001037
Simon Hunt1894d792015-02-04 17:09:20 -08001038 icfg = is.iconConfig();
1039
Simon Hunt737c89f2015-01-28 12:23:19 -08001040 // forceG is the SVG group to display the force layout in
Simon Huntdc6adea2015-02-09 22:29:36 -08001041 // uplink is the api from the main topo source file
Simon Hunt3a6eec02015-02-09 21:16:43 -08001042 // dim is the initial dimensions of the SVG as [w,h]
Simon Hunt737c89f2015-01-28 12:23:19 -08001043 // opts are, well, optional :)
Simon Hunt3a6eec02015-02-09 21:16:43 -08001044 function initForce(forceG, _uplink_, _dim_, opts) {
Simon Hunt1894d792015-02-04 17:09:20 -08001045 uplink = _uplink_;
Simon Hunt3a6eec02015-02-09 21:16:43 -08001046 dim = _dim_;
1047
1048 $log.debug('initForce().. dim = ' + dim);
1049
Simon Huntdc6adea2015-02-09 22:29:36 -08001050 tms.initModel(mkModelApi(uplink), dim);
Simon Hunt08f841d02015-02-10 14:39:20 -08001051 tss.initSelect(mkSelectApi(uplink));
Simon Hunta11b4eb2015-01-28 16:20:50 -08001052
Simon Hunt737c89f2015-01-28 12:23:19 -08001053 settings = angular.extend({}, defaultSettings, opts);
1054
1055 linkG = forceG.append('g').attr('id', 'topo-links');
1056 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
1057 nodeG = forceG.append('g').attr('id', 'topo-nodes');
1058
1059 link = linkG.selectAll('.link');
1060 linkLabel = linkLabelG.selectAll('.linkLabel');
1061 node = nodeG.selectAll('.node');
1062
1063 force = d3.layout.force()
Simon Hunt3a6eec02015-02-09 21:16:43 -08001064 .size(dim)
Simon Hunt737c89f2015-01-28 12:23:19 -08001065 .nodes(network.nodes)
1066 .links(network.links)
1067 .gravity(settings.gravity)
1068 .friction(settings.friction)
1069 .charge(settings.charge._def_)
1070 .linkDistance(settings.linkDistance._def_)
1071 .linkStrength(settings.linkStrength._def_)
1072 .on('tick', tick);
1073
1074 drag = sus.createDragBehavior(force,
Simon Hunt08f841d02015-02-10 14:39:20 -08001075 tss.selectObject, atDragEnd, dragEnabled, clickEnabled);
Simon Hunt737c89f2015-01-28 12:23:19 -08001076 }
1077
Simon Hunt3a6eec02015-02-09 21:16:43 -08001078 function newDim(_dim_) {
1079 dim = _dim_;
1080 force.size(dim);
1081 tms.newDim(dim);
Simon Hunt737c89f2015-01-28 12:23:19 -08001082 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -08001083 }
1084
Simon Hunt3a6eec02015-02-09 21:16:43 -08001085 function destroyForce() {
1086
1087 }
1088
Simon Hunt737c89f2015-01-28 12:23:19 -08001089 return {
1090 initForce: initForce,
Simon Hunt3a6eec02015-02-09 21:16:43 -08001091 newDim: newDim,
1092 destroyForce: destroyForce,
Simon Huntac4c6f72015-02-03 19:50:53 -08001093
1094 updateDeviceColors: updateDeviceColors,
Simon Hunt5724fb42015-02-05 16:59:40 -08001095 toggleHosts: toggleHosts,
1096 toggleOffline: toggleOffline,
1097 cycleDeviceLabels: cycleDeviceLabels,
Simon Hunt445e8152015-02-06 13:00:12 -08001098 unpin: unpin,
Simon Huntac4c6f72015-02-03 19:50:53 -08001099
1100 addDevice: addDevice,
Simon Hunt1894d792015-02-04 17:09:20 -08001101 updateDevice: updateDevice,
1102 removeDevice: removeDevice,
1103 addHost: addHost,
1104 updateHost: updateHost,
1105 removeHost: removeHost,
1106 addLink: addLink,
1107 updateLink: updateLink,
1108 removeLink: removeLink
Simon Hunt737c89f2015-01-28 12:23:19 -08001109 };
1110 }]);
1111}());