blob: 73cf9bfb306fc5e2e7cfb6c40d326dd1f328aa26 [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/*
18 ONOS GUI -- Topology Event Module.
19 Defines event handling for events received from the server.
20 */
21
22(function () {
23 'use strict';
24
25 // injected refs
Simon Huntac4c6f72015-02-03 19:50:53 -080026 var $log, sus, is, ts, tis, xlink;
27
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
47 // internal state
Simon Huntac4c6f72015-02-03 19:50:53 -080048 var settings, // merged default settings and options
Simon Hunt737c89f2015-01-28 12:23:19 -080049 force, // force layout object
50 drag, // drag behavior handler
51 network = {
52 nodes: [],
53 links: [],
54 lookup: {},
55 revLinkToKey: {}
Simon Huntac4c6f72015-02-03 19:50:53 -080056 },
57 projection, // background map projection
58 deviceLabelIndex = 0, // for device label cycling
59 hostLabelIndex = 0; // for host label cycling
Simon Hunt737c89f2015-01-28 12:23:19 -080060
61 // SVG elements;
62 var linkG, linkLabelG, nodeG;
63
64 // D3 selections;
65 var link, linkLabel, node;
66
67 // default settings for force layout
68 var defaultSettings = {
69 gravity: 0.4,
70 friction: 0.7,
71 charge: {
72 // note: key is node.class
73 device: -8000,
74 host: -5000,
75 _def_: -12000
76 },
77 linkDistance: {
78 // note: key is link.type
79 direct: 100,
80 optical: 120,
81 hostLink: 3,
82 _def_: 50
83 },
84 linkStrength: {
85 // note: key is link.type
86 // range: {0.0 ... 1.0}
87 //direct: 1.0,
88 //optical: 1.0,
89 //hostLink: 1.0,
90 _def_: 1.0
91 }
92 };
93
94
Simon Huntac4c6f72015-02-03 19:50:53 -080095 // ==========================
96 // === EVENT HANDLERS
97
98 function addDevice(data) {
99 var id = data.id,
100 d;
101
102 xlink.showNoDevs(false);
103
104 // although this is an add device event, if we already have the
105 // device, treat it as an update instead..
106 if (network.lookup[id]) {
107 updateDevice(data);
108 return;
109 }
110
111 d = createDeviceNode(data);
112 network.nodes.push(d);
113 network.lookup[id] = d;
114
115 $log.debug("Created new device.. ", d.id, d.x, d.y);
116
117 updateNodes();
118 fStart();
119 }
120
121 function updateDevice(data) {
122 var id = data.id,
123 d = network.lookup[id],
124 wasOnline;
125
126 if (d) {
127 wasOnline = d.online;
128 angular.extend(d, data);
129 if (positionNode(d, true)) {
130 sendUpdateMeta(d, true);
131 }
132 updateNodes();
133 if (wasOnline !== d.online) {
134 // TODO: re-instate link update, and offline visibility
135 //findAttachedLinks(d.id).forEach(restyleLinkElement);
136 //updateOfflineVisibility(d);
137 }
138 } else {
139 // TODO: decide whether we want to capture logic errors
140 //logicError('updateDevice lookup fail. ID = "' + id + '"');
141 }
142 }
143
144 function sendUpdateMeta(d, store) {
145 var metaUi = {},
146 ll;
147
148 // TODO: fix this code to send event to server...
149 //if (store) {
150 // ll = geoMapProj.invert([d.x, d.y]);
151 // metaUi = {
152 // x: d.x,
153 // y: d.y,
154 // lng: ll[0],
155 // lat: ll[1]
156 // };
157 //}
158 //d.metaUi = metaUi;
159 //sendMessage('updateMeta', {
160 // id: d.id,
161 // 'class': d.class,
162 // memento: metaUi
163 //});
164 }
165
166
Simon Huntac4c6f72015-02-03 19:50:53 -0800167 function fStart() {
168 $log.debug('TODO fStart()...');
169 // TODO...
170 }
171
172 function fResume() {
173 $log.debug('TODO fResume()...');
174 // TODO...
175 }
176
177 // ==========================
178 // === Devices and hosts - helper functions
179
180 function coordFromLngLat(loc) {
181 // Our hope is that the projection is installed before we start
182 // handling incoming nodes. But if not, we'll just return the origin.
183 return projection ? projection([loc.lng, loc.lat]) : [0, 0];
184 }
185
186 function positionNode(node, forUpdate) {
187 var meta = node.metaUi,
188 x = meta && meta.x,
189 y = meta && meta.y,
190 xy;
191
192 // If we have [x,y] already, use that...
193 if (x && y) {
194 node.fixed = true;
195 node.px = node.x = x;
196 node.py = node.y = y;
197 return;
198 }
199
200 var location = node.location,
201 coord;
202
203 if (location && location.type === 'latlng') {
204 coord = coordFromLngLat(location);
205 node.fixed = true;
206 node.px = node.x = coord[0];
207 node.py = node.y = coord[1];
208 return true;
209 }
210
211 // if this is a node update (not a node add).. skip randomizer
212 if (forUpdate) {
213 return;
214 }
215
216 // Note: Placing incoming unpinned nodes at exactly the same point
217 // (center of the view) causes them to explode outwards when
218 // the force layout kicks in. So, we spread them out a bit
219 // initially, to provide a more serene layout convergence.
220 // Additionally, if the node is a host, we place it near
221 // the device it is connected to.
222
223 function spread(s) {
224 return Math.floor((Math.random() * s) - s/2);
225 }
226
227 function randDim(dim) {
228 return dim / 2 + spread(dim * 0.7071);
229 }
230
231 function rand() {
232 return {
233 x: randDim(network.view.width()),
234 y: randDim(network.view.height())
235 };
236 }
237
238 function near(node) {
239 var min = 12,
240 dx = spread(12),
241 dy = spread(12);
242 return {
243 x: node.x + min + dx,
244 y: node.y + min + dy
245 };
246 }
247
248 function getDevice(cp) {
249 var d = network.lookup[cp.device];
250 return d || rand();
251 }
252
253 xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
254 angular.extend(node, xy);
255 }
256
257 function createDeviceNode(device) {
258 // start with the object as is
259 var node = device,
260 type = device.type,
261 svgCls = type ? 'node device ' + type : 'node device';
262
263 // Augment as needed...
264 node.class = 'device';
265 node.svgClass = device.online ? svgCls + ' online' : svgCls;
266 positionNode(node);
267 return node;
268 }
269
270 // ==========================
271 // === Devices and hosts - D3 rendering
272
273 // Returns the newly computed bounding box of the rectangle
274 function adjustRectToFitText(n) {
275 var text = n.select('text'),
276 box = text.node().getBBox(),
277 lab = labelConfig;
278
279 text.attr('text-anchor', 'middle')
280 .attr('y', '-0.8em')
281 .attr('x', lab.imgPad/2);
282
283 // translate the bbox so that it is centered on [x,y]
284 box.x = -box.width / 2;
285 box.y = -box.height / 2;
286
287 // add padding
288 box.x -= (lab.padLR + lab.imgPad/2);
289 box.width += lab.padLR * 2 + lab.imgPad;
290 box.y -= lab.padTB;
291 box.height += lab.padTB * 2;
292
293 return box;
294 }
295
296 function mkSvgClass(d) {
297 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
298 }
299
300 function hostLabel(d) {
301 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
302 return d.labels[idx];
303 }
304 function deviceLabel(d) {
305 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
306 return d.labels[idx];
307 }
308 function trimLabel(label) {
309 return (label && label.trim()) || '';
310 }
311
312 function emptyBox() {
313 return {
314 x: -2,
315 y: -2,
316 width: 4,
317 height: 4
318 };
319 }
320
321
322 function updateDeviceLabel(d) {
323 var label = trimLabel(deviceLabel(d)),
324 noLabel = !label,
325 node = d.el,
326 dim = is.iconConfig().device.dim,
327 devCfg = deviceIconConfig,
328 box, dx, dy;
329
330 node.select('text')
331 .text(label)
332 .style('opacity', 0)
333 .transition()
334 .style('opacity', 1);
335
336 if (noLabel) {
337 box = emptyBox();
338 dx = -dim/2;
339 dy = -dim/2;
340 } else {
341 box = adjustRectToFitText(node);
342 dx = box.x + devCfg.xoff;
343 dy = box.y + devCfg.yoff;
344 }
345
346 node.select('rect')
347 .transition()
348 .attr(box);
349
350 node.select('g.deviceIcon')
351 .transition()
352 .attr('transform', sus.translate(dx, dy));
353 }
354
355 function updateHostLabel(d) {
356 var label = trimLabel(hostLabel(d));
357 d.el.select('text').text(label);
358 }
359
360 function nodeMouseOver(m) {
361 // TODO
362 $log.debug("TODO nodeMouseOver()...", m);
363 }
364
365 function nodeMouseOut(m) {
366 // TODO
367 $log.debug("TODO nodeMouseOut()...", m);
368 }
369
370 function updateDeviceColors(d) {
371 if (d) {
372 setDeviceColor(d);
373 } else {
374 node.filter('.device').each(function (d) {
375 setDeviceColor(d);
376 });
377 }
378 }
379
380 var dCol = {
381 black: '#000',
382 paleblue: '#acf',
383 offwhite: '#ddd',
Simon Hunt78c10bf2015-02-03 21:18:20 -0800384 darkgrey: '#444',
Simon Huntac4c6f72015-02-03 19:50:53 -0800385 midgrey: '#888',
386 lightgrey: '#bbb',
387 orange: '#f90'
388 };
389
390 // note: these are the device icon colors without affinity
391 var dColTheme = {
392 light: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800393 rfill: dCol.offwhite,
Simon Huntac4c6f72015-02-03 19:50:53 -0800394 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800395 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800396 rect: dCol.paleblue
397 },
398 offline: {
399 glyph: dCol.midgrey,
400 rect: dCol.lightgrey
401 }
402 },
Simon Huntac4c6f72015-02-03 19:50:53 -0800403 dark: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800404 rfill: dCol.midgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800405 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800406 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800407 rect: dCol.paleblue
408 },
409 offline: {
410 glyph: dCol.midgrey,
Simon Hunt78c10bf2015-02-03 21:18:20 -0800411 rect: dCol.darkgrey
Simon Huntac4c6f72015-02-03 19:50:53 -0800412 }
413 }
414 };
415
416 function devBaseColor(d) {
417 var o = d.online ? 'online' : 'offline';
418 return dColTheme[ts.theme()][o];
419 }
420
421 function setDeviceColor(d) {
422 var o = d.online,
423 s = d.el.classed('selected'),
424 c = devBaseColor(d),
425 a = instColor(d.master, o),
Simon Hunt51056592015-02-03 21:48:07 -0800426 icon = d.el.select('g.deviceIcon'),
427 g, r;
Simon Huntac4c6f72015-02-03 19:50:53 -0800428
429 if (s) {
430 g = c.glyph;
431 r = dCol.orange;
432 } else if (tis.isVisible()) {
433 g = o ? a : c.glyph;
Simon Hunt78c10bf2015-02-03 21:18:20 -0800434 r = o ? c.rfill : a;
Simon Huntac4c6f72015-02-03 19:50:53 -0800435 } else {
436 g = c.glyph;
437 r = c.rect;
438 }
439
Simon Hunt51056592015-02-03 21:48:07 -0800440 icon.select('use').style('fill', g);
441 icon.select('rect').style('fill', r);
Simon Huntac4c6f72015-02-03 19:50:53 -0800442 }
443
444 function instColor(id, online) {
445 return sus.cat7().getColor(id, !online, ts.theme());
446 }
447
448 //============
449
450 function updateNodes() {
451 node = nodeG.selectAll('.node')
452 .data(network.nodes, function (d) { return d.id; });
453
454 // operate on existing nodes...
Simon Hunt51056592015-02-03 21:48:07 -0800455 node.filter('.device').each(deviceExisting);
456 node.filter('.host').each(hostExisting);
Simon Huntac4c6f72015-02-03 19:50:53 -0800457
458 // operate on entering nodes:
459 var entering = node.enter()
460 .append('g')
461 .attr({
462 id: function (d) { return sus.safeId(d.id); },
463 class: mkSvgClass,
464 transform: function (d) { return sus.translate(d.x, d.y); },
465 opacity: 0
466 })
467 .call(drag)
468 .on('mouseover', nodeMouseOver)
469 .on('mouseout', nodeMouseOut)
470 .transition()
471 .attr('opacity', 1);
472
Simon Hunt51056592015-02-03 21:48:07 -0800473 // augment nodes...
474 entering.filter('.device').each(deviceEnter);
475 entering.filter('.host').each(hostEnter);
Simon Huntac4c6f72015-02-03 19:50:53 -0800476
Simon Hunt51056592015-02-03 21:48:07 -0800477 // operate on both existing and new nodes:
Simon Huntac4c6f72015-02-03 19:50:53 -0800478 updateDeviceColors();
479
480 // operate on exiting nodes:
481 // Note that the node is removed after 2 seconds.
482 // Sub element animations should be shorter than 2 seconds.
483 var exiting = node.exit()
484 .transition()
485 .duration(2000)
486 .style('opacity', 0)
487 .remove();
488
Simon Hunt51056592015-02-03 21:48:07 -0800489 // node specific....
490 exiting.filter('.host').each(hostExit);
491 exiting.filter('.device').each(deviceExit);
Simon Huntac4c6f72015-02-03 19:50:53 -0800492
Simon Hunt51056592015-02-03 21:48:07 -0800493 // finally, resume the force layout
Simon Huntac4c6f72015-02-03 19:50:53 -0800494 fResume();
495 }
496
Simon Hunt51056592015-02-03 21:48:07 -0800497 // ==========================
498 // updateNodes - subfunctions
499
500 function deviceExisting(d) {
501 var node = d.el;
502 node.classed('online', d.online);
503 updateDeviceLabel(d);
504 positionNode(d, true);
505 }
506
507 function hostExisting(d) {
508 updateHostLabel(d);
509 positionNode(d, true);
510 }
511
512 function deviceEnter(d) {
513 var node = d3.select(this),
514 glyphId = d.type || 'unknown',
515 label = trimLabel(deviceLabel(d)),
516 devCfg = deviceIconConfig,
517 noLabel = !label,
518 box, dx, dy, icon;
519
520 d.el = node;
521
522 node.append('rect').attr({ rx: 5, ry: 5 });
523 node.append('text').text(label).attr('dy', '1.1em');
524 box = adjustRectToFitText(node);
525 node.select('rect').attr(box);
526
527 icon = is.addDeviceIcon(node, glyphId);
528
529 if (noLabel) {
530 dx = -icon.dim/2;
531 dy = -icon.dim/2;
532 } else {
533 box = adjustRectToFitText(node);
534 dx = box.x + devCfg.xoff;
535 dy = box.y + devCfg.yoff;
536 }
537
538 icon.attr('transform', sus.translate(dx, dy));
539 }
540
541 function hostEnter(d) {
542 var node = d3.select(this);
543
544 //cfg = config.icons.host,
545 //r = cfg.radius[d.type] || cfg.defaultRadius,
546 //textDy = r + 10,
547 //TODO: iid = iconGlyphUrl(d),
548 // _dummy;
549
550 d.el = node;
551
552 //TODO: showHostVis(node);
553
554 node.append('circle').attr('r', r);
555 //if (iid) {
556 //TODO: addHostIcon(node, r, iid);
557 //}
558 node.append('text')
559 .text(hostLabel)
560 //.attr('dy', textDy)
561 .attr('text-anchor', 'middle');
562 }
563
564 function hostExit(d) {
565 var node = d.el;
566 node.select('use')
567 .style('opacity', 0.5)
568 .transition()
569 .duration(800)
570 .style('opacity', 0);
571
572 node.select('text')
573 .style('opacity', 0.5)
574 .transition()
575 .duration(800)
576 .style('opacity', 0);
577
578 node.select('circle')
579 .style('stroke-fill', '#555')
580 .style('fill', '#888')
581 .style('opacity', 0.5)
582 .transition()
583 .duration(1500)
584 .attr('r', 0);
585 }
586
587 function deviceExit(d) {
588 var node = d.el;
589 node.select('use')
590 .style('opacity', 0.5)
591 .transition()
592 .duration(800)
593 .style('opacity', 0);
594
595 node.selectAll('rect')
596 .style('stroke-fill', '#555')
597 .style('fill', '#888')
598 .style('opacity', 0.5);
599 }
600
Simon Huntac4c6f72015-02-03 19:50:53 -0800601
602 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -0800603 // force layout tick function
604 function tick() {
605
606 }
607
608
Simon Huntac4c6f72015-02-03 19:50:53 -0800609 // ==========================
610 // === MOUSE GESTURE HANDLERS
611
Simon Hunt737c89f2015-01-28 12:23:19 -0800612 function selectCb() { }
613 function atDragEnd() {}
614 function dragEnabled() {}
615 function clickEnabled() {}
616
617
618 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800619 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -0800620
621 angular.module('ovTopo')
622 .factory('TopoForceService',
Simon Huntac4c6f72015-02-03 19:50:53 -0800623 ['$log', 'SvgUtilService', 'IconService', 'ThemeService',
624 'TopoInstService',
Simon Hunt737c89f2015-01-28 12:23:19 -0800625
Simon Huntac4c6f72015-02-03 19:50:53 -0800626 function (_$log_, _sus_, _is_, _ts_, _tis_) {
Simon Hunt737c89f2015-01-28 12:23:19 -0800627 $log = _$log_;
628 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -0800629 is = _is_;
630 ts = _ts_;
631 tis = _tis_;
Simon Hunt737c89f2015-01-28 12:23:19 -0800632
633 // forceG is the SVG group to display the force layout in
Simon Huntac4c6f72015-02-03 19:50:53 -0800634 // xlink is the cross-link api from the main topo source file
Simon Hunt737c89f2015-01-28 12:23:19 -0800635 // w, h are the initial dimensions of the SVG
636 // opts are, well, optional :)
Simon Huntac4c6f72015-02-03 19:50:53 -0800637 function initForce(forceG, _xlink_, w, h, opts) {
Simon Hunta11b4eb2015-01-28 16:20:50 -0800638 $log.debug('initForce().. WxH = ' + w + 'x' + h);
Simon Huntac4c6f72015-02-03 19:50:53 -0800639 xlink = _xlink_;
Simon Hunta11b4eb2015-01-28 16:20:50 -0800640
Simon Hunt737c89f2015-01-28 12:23:19 -0800641 settings = angular.extend({}, defaultSettings, opts);
642
Simon Huntac4c6f72015-02-03 19:50:53 -0800643 // when the projection promise is resolved, cache the projection
644 xlink.projectionPromise.then(
645 function (proj) {
646 projection = proj;
647 $log.debug('** We installed the projection: ', proj);
648 }
649 );
650
Simon Hunt737c89f2015-01-28 12:23:19 -0800651 linkG = forceG.append('g').attr('id', 'topo-links');
652 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
653 nodeG = forceG.append('g').attr('id', 'topo-nodes');
654
655 link = linkG.selectAll('.link');
656 linkLabel = linkLabelG.selectAll('.linkLabel');
657 node = nodeG.selectAll('.node');
658
659 force = d3.layout.force()
Simon Hunta11b4eb2015-01-28 16:20:50 -0800660 .size([w, h])
Simon Hunt737c89f2015-01-28 12:23:19 -0800661 .nodes(network.nodes)
662 .links(network.links)
663 .gravity(settings.gravity)
664 .friction(settings.friction)
665 .charge(settings.charge._def_)
666 .linkDistance(settings.linkDistance._def_)
667 .linkStrength(settings.linkStrength._def_)
668 .on('tick', tick);
669
670 drag = sus.createDragBehavior(force,
671 selectCb, atDragEnd, dragEnabled, clickEnabled);
672 }
673
Simon Huntb0ec1e52015-01-28 18:13:49 -0800674 function resize(dim) {
675 force.size([dim.width, dim.height]);
Simon Hunt737c89f2015-01-28 12:23:19 -0800676 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -0800677 }
678
679 return {
680 initForce: initForce,
Simon Huntac4c6f72015-02-03 19:50:53 -0800681 resize: resize,
682
683 updateDeviceColors: updateDeviceColors,
684
685 addDevice: addDevice,
686 updateDevice: updateDevice
Simon Hunt737c89f2015-01-28 12:23:19 -0800687 };
688 }]);
689}());