blob: fb6ca06a75f4c2bf809406496dbb53b9cef21f98 [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
167 function updateNodes() {
168 $log.debug('TODO updateNodes()...');
169 // TODO...
170 }
171
172 function fStart() {
173 $log.debug('TODO fStart()...');
174 // TODO...
175 }
176
177 function fResume() {
178 $log.debug('TODO fResume()...');
179 // TODO...
180 }
181
182 // ==========================
183 // === Devices and hosts - helper functions
184
185 function coordFromLngLat(loc) {
186 // Our hope is that the projection is installed before we start
187 // handling incoming nodes. But if not, we'll just return the origin.
188 return projection ? projection([loc.lng, loc.lat]) : [0, 0];
189 }
190
191 function positionNode(node, forUpdate) {
192 var meta = node.metaUi,
193 x = meta && meta.x,
194 y = meta && meta.y,
195 xy;
196
197 // If we have [x,y] already, use that...
198 if (x && y) {
199 node.fixed = true;
200 node.px = node.x = x;
201 node.py = node.y = y;
202 return;
203 }
204
205 var location = node.location,
206 coord;
207
208 if (location && location.type === 'latlng') {
209 coord = coordFromLngLat(location);
210 node.fixed = true;
211 node.px = node.x = coord[0];
212 node.py = node.y = coord[1];
213 return true;
214 }
215
216 // if this is a node update (not a node add).. skip randomizer
217 if (forUpdate) {
218 return;
219 }
220
221 // Note: Placing incoming unpinned nodes at exactly the same point
222 // (center of the view) causes them to explode outwards when
223 // the force layout kicks in. So, we spread them out a bit
224 // initially, to provide a more serene layout convergence.
225 // Additionally, if the node is a host, we place it near
226 // the device it is connected to.
227
228 function spread(s) {
229 return Math.floor((Math.random() * s) - s/2);
230 }
231
232 function randDim(dim) {
233 return dim / 2 + spread(dim * 0.7071);
234 }
235
236 function rand() {
237 return {
238 x: randDim(network.view.width()),
239 y: randDim(network.view.height())
240 };
241 }
242
243 function near(node) {
244 var min = 12,
245 dx = spread(12),
246 dy = spread(12);
247 return {
248 x: node.x + min + dx,
249 y: node.y + min + dy
250 };
251 }
252
253 function getDevice(cp) {
254 var d = network.lookup[cp.device];
255 return d || rand();
256 }
257
258 xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
259 angular.extend(node, xy);
260 }
261
262 function createDeviceNode(device) {
263 // start with the object as is
264 var node = device,
265 type = device.type,
266 svgCls = type ? 'node device ' + type : 'node device';
267
268 // Augment as needed...
269 node.class = 'device';
270 node.svgClass = device.online ? svgCls + ' online' : svgCls;
271 positionNode(node);
272 return node;
273 }
274
275 // ==========================
276 // === Devices and hosts - D3 rendering
277
278 // Returns the newly computed bounding box of the rectangle
279 function adjustRectToFitText(n) {
280 var text = n.select('text'),
281 box = text.node().getBBox(),
282 lab = labelConfig;
283
284 text.attr('text-anchor', 'middle')
285 .attr('y', '-0.8em')
286 .attr('x', lab.imgPad/2);
287
288 // translate the bbox so that it is centered on [x,y]
289 box.x = -box.width / 2;
290 box.y = -box.height / 2;
291
292 // add padding
293 box.x -= (lab.padLR + lab.imgPad/2);
294 box.width += lab.padLR * 2 + lab.imgPad;
295 box.y -= lab.padTB;
296 box.height += lab.padTB * 2;
297
298 return box;
299 }
300
301 function mkSvgClass(d) {
302 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
303 }
304
305 function hostLabel(d) {
306 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
307 return d.labels[idx];
308 }
309 function deviceLabel(d) {
310 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
311 return d.labels[idx];
312 }
313 function trimLabel(label) {
314 return (label && label.trim()) || '';
315 }
316
317 function emptyBox() {
318 return {
319 x: -2,
320 y: -2,
321 width: 4,
322 height: 4
323 };
324 }
325
326
327 function updateDeviceLabel(d) {
328 var label = trimLabel(deviceLabel(d)),
329 noLabel = !label,
330 node = d.el,
331 dim = is.iconConfig().device.dim,
332 devCfg = deviceIconConfig,
333 box, dx, dy;
334
335 node.select('text')
336 .text(label)
337 .style('opacity', 0)
338 .transition()
339 .style('opacity', 1);
340
341 if (noLabel) {
342 box = emptyBox();
343 dx = -dim/2;
344 dy = -dim/2;
345 } else {
346 box = adjustRectToFitText(node);
347 dx = box.x + devCfg.xoff;
348 dy = box.y + devCfg.yoff;
349 }
350
351 node.select('rect')
352 .transition()
353 .attr(box);
354
355 node.select('g.deviceIcon')
356 .transition()
357 .attr('transform', sus.translate(dx, dy));
358 }
359
360 function updateHostLabel(d) {
361 var label = trimLabel(hostLabel(d));
362 d.el.select('text').text(label);
363 }
364
365 function nodeMouseOver(m) {
366 // TODO
367 $log.debug("TODO nodeMouseOver()...", m);
368 }
369
370 function nodeMouseOut(m) {
371 // TODO
372 $log.debug("TODO nodeMouseOut()...", m);
373 }
374
375 function updateDeviceColors(d) {
376 if (d) {
377 setDeviceColor(d);
378 } else {
379 node.filter('.device').each(function (d) {
380 setDeviceColor(d);
381 });
382 }
383 }
384
385 var dCol = {
386 black: '#000',
387 paleblue: '#acf',
388 offwhite: '#ddd',
389 midgrey: '#888',
390 lightgrey: '#bbb',
391 orange: '#f90'
392 };
393
394 // note: these are the device icon colors without affinity
395 var dColTheme = {
396 light: {
397 online: {
398 glyph: dCol.black,
399 rect: dCol.paleblue
400 },
401 offline: {
402 glyph: dCol.midgrey,
403 rect: dCol.lightgrey
404 }
405 },
406 // TODO: theme
407 dark: {
408 online: {
409 glyph: dCol.black,
410 rect: dCol.paleblue
411 },
412 offline: {
413 glyph: dCol.midgrey,
414 rect: dCol.lightgrey
415 }
416 }
417 };
418
419 function devBaseColor(d) {
420 var o = d.online ? 'online' : 'offline';
421 return dColTheme[ts.theme()][o];
422 }
423
424 function setDeviceColor(d) {
425 var o = d.online,
426 s = d.el.classed('selected'),
427 c = devBaseColor(d),
428 a = instColor(d.master, o),
429 g, r,
430 icon = d.el.select('g.deviceIcon');
431
432 if (s) {
433 g = c.glyph;
434 r = dCol.orange;
435 } else if (tis.isVisible()) {
436 g = o ? a : c.glyph;
437 r = o ? dCol.offwhite : a;
438 } else {
439 g = c.glyph;
440 r = c.rect;
441 }
442
443 icon.select('use')
444 .style('fill', g);
445 icon.select('rect')
446 .style('fill', r);
447 }
448
449 function instColor(id, online) {
450 return sus.cat7().getColor(id, !online, ts.theme());
451 }
452
453 //============
454
455 function updateNodes() {
456 node = nodeG.selectAll('.node')
457 .data(network.nodes, function (d) { return d.id; });
458
459 // operate on existing nodes...
460 node.filter('.device').each(function (d) {
461 var node = d.el;
462 node.classed('online', d.online);
463 updateDeviceLabel(d);
464 positionNode(d, true);
465 });
466
467 node.filter('.host').each(function (d) {
468 updateHostLabel(d);
469 positionNode(d, true);
470 });
471
472 // operate on entering nodes:
473 var entering = node.enter()
474 .append('g')
475 .attr({
476 id: function (d) { return sus.safeId(d.id); },
477 class: mkSvgClass,
478 transform: function (d) { return sus.translate(d.x, d.y); },
479 opacity: 0
480 })
481 .call(drag)
482 .on('mouseover', nodeMouseOver)
483 .on('mouseout', nodeMouseOut)
484 .transition()
485 .attr('opacity', 1);
486
487 // augment device nodes...
488 entering.filter('.device').each(function (d) {
489 var node = d3.select(this),
490 glyphId = d.type || 'unknown',
491 label = trimLabel(deviceLabel(d)),
492 noLabel = !label,
493 box, dx, dy, icon;
494
495 // provide ref to element from backing data....
496 d.el = node;
497
498 node.append('rect').attr({ rx: 5, ry: 5 });
499 node.append('text').text(label).attr('dy', '1.1em');
500 box = adjustRectToFitText(node);
501 node.select('rect').attr(box);
502
503 icon = is.addDeviceIcon(node, glyphId);
504 d.iconDim = icon.dim;
505
506 if (noLabel) {
507 dx = -icon.dim/2;
508 dy = -icon.dim/2;
509 } else {
510 box = adjustRectToFitText(node);
511 dx = box.x + iconConfig.xoff;
512 dy = box.y + iconConfig.yoff;
513 }
514
515 icon.attr('transform', sus.translate(dx, dy));
516 });
517
518 // augment host nodes...
519 entering.filter('.host').each(function (d) {
520 var node = d3.select(this),
521 cfg = config.icons.host,
522 r = cfg.radius[d.type] || cfg.defaultRadius,
523 textDy = r + 10,
524 //TODO: iid = iconGlyphUrl(d),
525 _dummy;
526
527 // provide ref to element from backing data....
528 d.el = node;
529
530 //TODO: showHostVis(node);
531
532 node.append('circle').attr('r', r);
533 if (iid) {
534 //TODO: addHostIcon(node, r, iid);
535 }
536 node.append('text')
537 .text(hostLabel)
538 .attr('dy', textDy)
539 .attr('text-anchor', 'middle');
540 });
541
542 // operate on both existing and new nodes, if necessary
543 updateDeviceColors();
544
545 // operate on exiting nodes:
546 // Note that the node is removed after 2 seconds.
547 // Sub element animations should be shorter than 2 seconds.
548 var exiting = node.exit()
549 .transition()
550 .duration(2000)
551 .style('opacity', 0)
552 .remove();
553
554 // host node exits....
555 exiting.filter('.host').each(function (d) {
556 var node = d.el;
557 node.select('use')
558 .style('opacity', 0.5)
559 .transition()
560 .duration(800)
561 .style('opacity', 0);
562
563 node.select('text')
564 .style('opacity', 0.5)
565 .transition()
566 .duration(800)
567 .style('opacity', 0);
568
569 node.select('circle')
570 .style('stroke-fill', '#555')
571 .style('fill', '#888')
572 .style('opacity', 0.5)
573 .transition()
574 .duration(1500)
575 .attr('r', 0);
576 });
577
578 // device node exits....
579 exiting.filter('.device').each(function (d) {
580 var node = d.el;
581 node.select('use')
582 .style('opacity', 0.5)
583 .transition()
584 .duration(800)
585 .style('opacity', 0);
586
587 node.selectAll('rect')
588 .style('stroke-fill', '#555')
589 .style('fill', '#888')
590 .style('opacity', 0.5);
591 });
592 fResume();
593 }
594
595
596 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -0800597 // force layout tick function
598 function tick() {
599
600 }
601
602
Simon Huntac4c6f72015-02-03 19:50:53 -0800603 // ==========================
604 // === MOUSE GESTURE HANDLERS
605
Simon Hunt737c89f2015-01-28 12:23:19 -0800606 function selectCb() { }
607 function atDragEnd() {}
608 function dragEnabled() {}
609 function clickEnabled() {}
610
611
612 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800613 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -0800614
615 angular.module('ovTopo')
616 .factory('TopoForceService',
Simon Huntac4c6f72015-02-03 19:50:53 -0800617 ['$log', 'SvgUtilService', 'IconService', 'ThemeService',
618 'TopoInstService',
Simon Hunt737c89f2015-01-28 12:23:19 -0800619
Simon Huntac4c6f72015-02-03 19:50:53 -0800620 function (_$log_, _sus_, _is_, _ts_, _tis_) {
Simon Hunt737c89f2015-01-28 12:23:19 -0800621 $log = _$log_;
622 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -0800623 is = _is_;
624 ts = _ts_;
625 tis = _tis_;
Simon Hunt737c89f2015-01-28 12:23:19 -0800626
627 // forceG is the SVG group to display the force layout in
Simon Huntac4c6f72015-02-03 19:50:53 -0800628 // xlink is the cross-link api from the main topo source file
Simon Hunt737c89f2015-01-28 12:23:19 -0800629 // w, h are the initial dimensions of the SVG
630 // opts are, well, optional :)
Simon Huntac4c6f72015-02-03 19:50:53 -0800631 function initForce(forceG, _xlink_, w, h, opts) {
Simon Hunta11b4eb2015-01-28 16:20:50 -0800632 $log.debug('initForce().. WxH = ' + w + 'x' + h);
Simon Huntac4c6f72015-02-03 19:50:53 -0800633 xlink = _xlink_;
Simon Hunta11b4eb2015-01-28 16:20:50 -0800634
Simon Hunt737c89f2015-01-28 12:23:19 -0800635 settings = angular.extend({}, defaultSettings, opts);
636
Simon Huntac4c6f72015-02-03 19:50:53 -0800637 // when the projection promise is resolved, cache the projection
638 xlink.projectionPromise.then(
639 function (proj) {
640 projection = proj;
641 $log.debug('** We installed the projection: ', proj);
642 }
643 );
644
Simon Hunt737c89f2015-01-28 12:23:19 -0800645 linkG = forceG.append('g').attr('id', 'topo-links');
646 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
647 nodeG = forceG.append('g').attr('id', 'topo-nodes');
648
649 link = linkG.selectAll('.link');
650 linkLabel = linkLabelG.selectAll('.linkLabel');
651 node = nodeG.selectAll('.node');
652
653 force = d3.layout.force()
Simon Hunta11b4eb2015-01-28 16:20:50 -0800654 .size([w, h])
Simon Hunt737c89f2015-01-28 12:23:19 -0800655 .nodes(network.nodes)
656 .links(network.links)
657 .gravity(settings.gravity)
658 .friction(settings.friction)
659 .charge(settings.charge._def_)
660 .linkDistance(settings.linkDistance._def_)
661 .linkStrength(settings.linkStrength._def_)
662 .on('tick', tick);
663
664 drag = sus.createDragBehavior(force,
665 selectCb, atDragEnd, dragEnabled, clickEnabled);
666 }
667
Simon Huntb0ec1e52015-01-28 18:13:49 -0800668 function resize(dim) {
669 force.size([dim.width, dim.height]);
Simon Hunt737c89f2015-01-28 12:23:19 -0800670 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -0800671 }
672
673 return {
674 initForce: initForce,
Simon Huntac4c6f72015-02-03 19:50:53 -0800675 resize: resize,
676
677 updateDeviceColors: updateDeviceColors,
678
679 addDevice: addDevice,
680 updateDevice: updateDevice
Simon Hunt737c89f2015-01-28 12:23:19 -0800681 };
682 }]);
683}());