blob: 5268dc0946bf9927d469306c5ed2e26450f54446 [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',
Simon Hunt78c10bf2015-02-03 21:18:20 -0800389 darkgrey: '#444',
Simon Huntac4c6f72015-02-03 19:50:53 -0800390 midgrey: '#888',
391 lightgrey: '#bbb',
392 orange: '#f90'
393 };
394
395 // note: these are the device icon colors without affinity
396 var dColTheme = {
397 light: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800398 rfill: dCol.offwhite,
Simon Huntac4c6f72015-02-03 19:50:53 -0800399 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800400 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800401 rect: dCol.paleblue
402 },
403 offline: {
404 glyph: dCol.midgrey,
405 rect: dCol.lightgrey
406 }
407 },
Simon Huntac4c6f72015-02-03 19:50:53 -0800408 dark: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800409 rfill: dCol.midgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800410 online: {
Simon Hunt78c10bf2015-02-03 21:18:20 -0800411 glyph: dCol.darkgrey,
Simon Huntac4c6f72015-02-03 19:50:53 -0800412 rect: dCol.paleblue
413 },
414 offline: {
415 glyph: dCol.midgrey,
Simon Hunt78c10bf2015-02-03 21:18:20 -0800416 rect: dCol.darkgrey
Simon Huntac4c6f72015-02-03 19:50:53 -0800417 }
418 }
419 };
420
421 function devBaseColor(d) {
422 var o = d.online ? 'online' : 'offline';
423 return dColTheme[ts.theme()][o];
424 }
425
426 function setDeviceColor(d) {
427 var o = d.online,
428 s = d.el.classed('selected'),
429 c = devBaseColor(d),
430 a = instColor(d.master, o),
431 g, r,
432 icon = d.el.select('g.deviceIcon');
433
434 if (s) {
435 g = c.glyph;
436 r = dCol.orange;
437 } else if (tis.isVisible()) {
438 g = o ? a : c.glyph;
Simon Hunt78c10bf2015-02-03 21:18:20 -0800439 r = o ? c.rfill : a;
Simon Huntac4c6f72015-02-03 19:50:53 -0800440 } else {
441 g = c.glyph;
442 r = c.rect;
443 }
444
445 icon.select('use')
446 .style('fill', g);
447 icon.select('rect')
448 .style('fill', r);
449 }
450
451 function instColor(id, online) {
452 return sus.cat7().getColor(id, !online, ts.theme());
453 }
454
455 //============
456
457 function updateNodes() {
458 node = nodeG.selectAll('.node')
459 .data(network.nodes, function (d) { return d.id; });
460
461 // operate on existing nodes...
462 node.filter('.device').each(function (d) {
463 var node = d.el;
464 node.classed('online', d.online);
465 updateDeviceLabel(d);
466 positionNode(d, true);
467 });
468
469 node.filter('.host').each(function (d) {
470 updateHostLabel(d);
471 positionNode(d, true);
472 });
473
474 // operate on entering nodes:
475 var entering = node.enter()
476 .append('g')
477 .attr({
478 id: function (d) { return sus.safeId(d.id); },
479 class: mkSvgClass,
480 transform: function (d) { return sus.translate(d.x, d.y); },
481 opacity: 0
482 })
483 .call(drag)
484 .on('mouseover', nodeMouseOver)
485 .on('mouseout', nodeMouseOut)
486 .transition()
487 .attr('opacity', 1);
488
489 // augment device nodes...
490 entering.filter('.device').each(function (d) {
491 var node = d3.select(this),
492 glyphId = d.type || 'unknown',
493 label = trimLabel(deviceLabel(d)),
494 noLabel = !label,
495 box, dx, dy, icon;
496
497 // provide ref to element from backing data....
498 d.el = node;
499
500 node.append('rect').attr({ rx: 5, ry: 5 });
501 node.append('text').text(label).attr('dy', '1.1em');
502 box = adjustRectToFitText(node);
503 node.select('rect').attr(box);
504
505 icon = is.addDeviceIcon(node, glyphId);
506 d.iconDim = icon.dim;
507
508 if (noLabel) {
509 dx = -icon.dim/2;
510 dy = -icon.dim/2;
511 } else {
512 box = adjustRectToFitText(node);
513 dx = box.x + iconConfig.xoff;
514 dy = box.y + iconConfig.yoff;
515 }
516
517 icon.attr('transform', sus.translate(dx, dy));
518 });
519
520 // augment host nodes...
521 entering.filter('.host').each(function (d) {
522 var node = d3.select(this),
523 cfg = config.icons.host,
524 r = cfg.radius[d.type] || cfg.defaultRadius,
525 textDy = r + 10,
526 //TODO: iid = iconGlyphUrl(d),
527 _dummy;
528
529 // provide ref to element from backing data....
530 d.el = node;
531
532 //TODO: showHostVis(node);
533
534 node.append('circle').attr('r', r);
535 if (iid) {
536 //TODO: addHostIcon(node, r, iid);
537 }
538 node.append('text')
539 .text(hostLabel)
540 .attr('dy', textDy)
541 .attr('text-anchor', 'middle');
542 });
543
544 // operate on both existing and new nodes, if necessary
545 updateDeviceColors();
546
547 // operate on exiting nodes:
548 // Note that the node is removed after 2 seconds.
549 // Sub element animations should be shorter than 2 seconds.
550 var exiting = node.exit()
551 .transition()
552 .duration(2000)
553 .style('opacity', 0)
554 .remove();
555
556 // host node exits....
557 exiting.filter('.host').each(function (d) {
558 var node = d.el;
559 node.select('use')
560 .style('opacity', 0.5)
561 .transition()
562 .duration(800)
563 .style('opacity', 0);
564
565 node.select('text')
566 .style('opacity', 0.5)
567 .transition()
568 .duration(800)
569 .style('opacity', 0);
570
571 node.select('circle')
572 .style('stroke-fill', '#555')
573 .style('fill', '#888')
574 .style('opacity', 0.5)
575 .transition()
576 .duration(1500)
577 .attr('r', 0);
578 });
579
580 // device node exits....
581 exiting.filter('.device').each(function (d) {
582 var node = d.el;
583 node.select('use')
584 .style('opacity', 0.5)
585 .transition()
586 .duration(800)
587 .style('opacity', 0);
588
589 node.selectAll('rect')
590 .style('stroke-fill', '#555')
591 .style('fill', '#888')
592 .style('opacity', 0.5);
593 });
594 fResume();
595 }
596
597
598 // ==========================
Simon Hunt737c89f2015-01-28 12:23:19 -0800599 // force layout tick function
600 function tick() {
601
602 }
603
604
Simon Huntac4c6f72015-02-03 19:50:53 -0800605 // ==========================
606 // === MOUSE GESTURE HANDLERS
607
Simon Hunt737c89f2015-01-28 12:23:19 -0800608 function selectCb() { }
609 function atDragEnd() {}
610 function dragEnabled() {}
611 function clickEnabled() {}
612
613
614 // ==========================
Simon Huntac4c6f72015-02-03 19:50:53 -0800615 // Module definition
Simon Hunt737c89f2015-01-28 12:23:19 -0800616
617 angular.module('ovTopo')
618 .factory('TopoForceService',
Simon Huntac4c6f72015-02-03 19:50:53 -0800619 ['$log', 'SvgUtilService', 'IconService', 'ThemeService',
620 'TopoInstService',
Simon Hunt737c89f2015-01-28 12:23:19 -0800621
Simon Huntac4c6f72015-02-03 19:50:53 -0800622 function (_$log_, _sus_, _is_, _ts_, _tis_) {
Simon Hunt737c89f2015-01-28 12:23:19 -0800623 $log = _$log_;
624 sus = _sus_;
Simon Huntac4c6f72015-02-03 19:50:53 -0800625 is = _is_;
626 ts = _ts_;
627 tis = _tis_;
Simon Hunt737c89f2015-01-28 12:23:19 -0800628
629 // forceG is the SVG group to display the force layout in
Simon Huntac4c6f72015-02-03 19:50:53 -0800630 // xlink is the cross-link api from the main topo source file
Simon Hunt737c89f2015-01-28 12:23:19 -0800631 // w, h are the initial dimensions of the SVG
632 // opts are, well, optional :)
Simon Huntac4c6f72015-02-03 19:50:53 -0800633 function initForce(forceG, _xlink_, w, h, opts) {
Simon Hunta11b4eb2015-01-28 16:20:50 -0800634 $log.debug('initForce().. WxH = ' + w + 'x' + h);
Simon Huntac4c6f72015-02-03 19:50:53 -0800635 xlink = _xlink_;
Simon Hunta11b4eb2015-01-28 16:20:50 -0800636
Simon Hunt737c89f2015-01-28 12:23:19 -0800637 settings = angular.extend({}, defaultSettings, opts);
638
Simon Huntac4c6f72015-02-03 19:50:53 -0800639 // when the projection promise is resolved, cache the projection
640 xlink.projectionPromise.then(
641 function (proj) {
642 projection = proj;
643 $log.debug('** We installed the projection: ', proj);
644 }
645 );
646
Simon Hunt737c89f2015-01-28 12:23:19 -0800647 linkG = forceG.append('g').attr('id', 'topo-links');
648 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
649 nodeG = forceG.append('g').attr('id', 'topo-nodes');
650
651 link = linkG.selectAll('.link');
652 linkLabel = linkLabelG.selectAll('.linkLabel');
653 node = nodeG.selectAll('.node');
654
655 force = d3.layout.force()
Simon Hunta11b4eb2015-01-28 16:20:50 -0800656 .size([w, h])
Simon Hunt737c89f2015-01-28 12:23:19 -0800657 .nodes(network.nodes)
658 .links(network.links)
659 .gravity(settings.gravity)
660 .friction(settings.friction)
661 .charge(settings.charge._def_)
662 .linkDistance(settings.linkDistance._def_)
663 .linkStrength(settings.linkStrength._def_)
664 .on('tick', tick);
665
666 drag = sus.createDragBehavior(force,
667 selectCb, atDragEnd, dragEnabled, clickEnabled);
668 }
669
Simon Huntb0ec1e52015-01-28 18:13:49 -0800670 function resize(dim) {
671 force.size([dim.width, dim.height]);
Simon Hunt737c89f2015-01-28 12:23:19 -0800672 // Review -- do we need to nudge the layout ?
Simon Hunt737c89f2015-01-28 12:23:19 -0800673 }
674
675 return {
676 initForce: initForce,
Simon Huntac4c6f72015-02-03 19:50:53 -0800677 resize: resize,
678
679 updateDeviceColors: updateDeviceColors,
680
681 addDevice: addDevice,
682 updateDevice: updateDevice
Simon Hunt737c89f2015-01-28 12:23:19 -0800683 };
684 }]);
685}());