Simon Hunt

GUI -- TopoView - Migrated helper functions to topoModel.js.

- moved randomized functions to random.js (so we can mock them).

Change-Id: Ic56ce64c036d36f34798f0df9f03a7d09335a2ab
1 +/*
2 + * Copyright 2014,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 -- Random -- Encapsulated randomness
19 + */
20 +(function () {
21 + 'use strict';
22 +
23 + var $log, fs;
24 +
25 + var halfRoot2 = 0.7071;
26 +
27 + // given some value, s, returns an integer between -s/2 and s/2
28 + // e.g. s = 100; result in the range [-50..50)
29 + function spread(s) {
30 + return Math.floor((Math.random() * s) - s / 2);
31 + }
32 +
33 + // for a given dimension, d, choose a random value somewhere between
34 + // 0 and d where the value is within (d / (2 * sqrt(2))) of d/2.
35 + function randDim(d) {
36 + return d / 2 + spread(d * halfRoot2);
37 + }
38 +
39 + angular.module('onosUtil')
40 + .factory('RandomService', ['$log', 'FnService',
41 +
42 + function (_$log_, _fs_) {
43 + $log = _$log_;
44 + fs = _fs_;
45 +
46 + return {
47 + spread: spread,
48 + randDim: randDim
49 + };
50 + }]);
51 +}());
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
35 35
36 <script src="fw/util/util.js"></script> 36 <script src="fw/util/util.js"></script>
37 <script src="fw/util/fn.js"></script> 37 <script src="fw/util/fn.js"></script>
38 + <script src="fw/util/random.js"></script>
38 <script src="fw/util/theme.js"></script> 39 <script src="fw/util/theme.js"></script>
39 <script src="fw/util/keys.js"></script> 40 <script src="fw/util/keys.js"></script>
40 41
...@@ -81,6 +82,7 @@ ...@@ -81,6 +82,7 @@
81 <script src="view/topo/topo.js"></script> 82 <script src="view/topo/topo.js"></script>
82 <script src="view/topo/topoEvent.js"></script> 83 <script src="view/topo/topoEvent.js"></script>
83 <script src="view/topo/topoForce.js"></script> 84 <script src="view/topo/topoForce.js"></script>
85 + <script src="view/topo/topoModel.js"></script>
84 <script src="view/topo/topoPanel.js"></script> 86 <script src="view/topo/topoPanel.js"></script>
85 <script src="view/topo/topoInst.js"></script> 87 <script src="view/topo/topoInst.js"></script>
86 <script src="view/device/device.js"></script> 88 <script src="view/device/device.js"></script>
......
...@@ -130,8 +130,8 @@ ...@@ -130,8 +130,8 @@
130 130
131 131
132 // callback invoked when the SVG view has been resized.. 132 // callback invoked when the SVG view has been resized..
133 - function svgResized(dim) { 133 + function svgResized(s) {
134 - tfs.resize(dim); 134 + tfs.newDim([s.width, s.height]);
135 } 135 }
136 136
137 // --- Background Map ------------------------------------------------ 137 // --- Background Map ------------------------------------------------
...@@ -203,6 +203,7 @@ ...@@ -203,6 +203,7 @@
203 _ks_, _zs_, _gs_, _ms_, _sus_, tes, _tfs_, tps, _tis_) { 203 _ks_, _zs_, _gs_, _ms_, _sus_, tes, _tfs_, tps, _tis_) {
204 var self = this, 204 var self = this,
205 projection, 205 projection,
206 + dim,
206 uplink = { 207 uplink = {
207 // provides function calls back into this space 208 // provides function calls back into this space
208 showNoDevs: showNoDevs, 209 showNoDevs: showNoDevs,
...@@ -230,6 +231,7 @@ ...@@ -230,6 +231,7 @@
230 tes.closeSock(); 231 tes.closeSock();
231 tps.destroyPanels(); 232 tps.destroyPanels();
232 tis.destroyInst(); 233 tis.destroyInst();
234 + tfs.destroyForce();
233 }); 235 });
234 236
235 // svg layer and initialization of components 237 // svg layer and initialization of components
...@@ -237,6 +239,7 @@ ...@@ -237,6 +239,7 @@
237 svg = ovtopo.select('svg'); 239 svg = ovtopo.select('svg');
238 // set the svg size to match that of the window, less the masthead 240 // set the svg size to match that of the window, less the masthead
239 svg.attr(fs.windowSize(mast.mastHeight())); 241 svg.attr(fs.windowSize(mast.mastHeight()));
242 + dim = [svg.attr('width'), svg.attr('height')];
240 243
241 setUpKeys(); 244 setUpKeys();
242 setUpDefs(); 245 setUpDefs();
...@@ -250,7 +253,7 @@ ...@@ -250,7 +253,7 @@
250 ); 253 );
251 254
252 forceG = zoomLayer.append('g').attr('id', 'topo-force'); 255 forceG = zoomLayer.append('g').attr('id', 'topo-force');
253 - tfs.initForce(forceG, uplink, svg.attr('width'), svg.attr('height')); 256 + tfs.initForce(forceG, uplink, dim);
254 tis.initInst(); 257 tis.initInst();
255 tps.initPanels(); 258 tps.initPanels();
256 tes.openSock(); 259 tes.openSock();
......
...@@ -15,15 +15,15 @@ ...@@ -15,15 +15,15 @@
15 */ 15 */
16 16
17 /* 17 /*
18 - ONOS GUI -- Topology Event Module. 18 + ONOS GUI -- Topology Force Module.
19 - Defines event handling for events received from the server. 19 + Visualization of the topology in an SVG layer, using a D3 Force Layout.
20 */ 20 */
21 21
22 (function () { 22 (function () {
23 'use strict'; 23 'use strict';
24 24
25 // injected refs 25 // injected refs
26 - var $log, fs, sus, is, ts, flash, tis, icfg, uplink; 26 + var $log, fs, sus, is, ts, flash, tis, tms, icfg, uplink;
27 27
28 // configuration 28 // configuration
29 var labelConfig = { 29 var labelConfig = {
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
48 light: { 48 light: {
49 baseColor: '#666', 49 baseColor: '#666',
50 inColor: '#66f', 50 inColor: '#66f',
51 - outColor: '#f00', 51 + outColor: '#f00'
52 }, 52 },
53 dark: { 53 dark: {
54 baseColor: '#aaa', 54 baseColor: '#aaa',
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
76 showOffline = true, // whether offline devices are displayed 76 showOffline = true, // whether offline devices are displayed
77 oblique = false, // whether we are in the oblique view 77 oblique = false, // whether we are in the oblique view
78 nodeLock = false, // whether nodes can be dragged or not (locked) 78 nodeLock = false, // whether nodes can be dragged or not (locked)
79 - width, height, // the width and height of the force layout 79 + dim, // the dimensions of the force layout [w,h]
80 hovered, // the node over which the mouse is hovering 80 hovered, // the node over which the mouse is hovering
81 selections = {}, // what is currently selected 81 selections = {}, // what is currently selected
82 selectOrder = []; // the order in which we made selections 82 selectOrder = []; // the order in which we made selections
...@@ -131,7 +131,7 @@ ...@@ -131,7 +131,7 @@
131 return; 131 return;
132 } 132 }
133 133
134 - d = createDeviceNode(data); 134 + d = tms.createDeviceNode(data);
135 network.nodes.push(d); 135 network.nodes.push(d);
136 lu[id] = d; 136 lu[id] = d;
137 137
...@@ -149,7 +149,7 @@ ...@@ -149,7 +149,7 @@
149 if (d) { 149 if (d) {
150 wasOnline = d.online; 150 wasOnline = d.online;
151 angular.extend(d, data); 151 angular.extend(d, data);
152 - if (positionNode(d, true)) { 152 + if (tms.positionNode(d, true)) {
153 sendUpdateMeta(d); 153 sendUpdateMeta(d);
154 } 154 }
155 updateNodes(); 155 updateNodes();
...@@ -185,7 +185,7 @@ ...@@ -185,7 +185,7 @@
185 return; 185 return;
186 } 186 }
187 187
188 - d = createHostNode(data); 188 + d = tms.createHostNode(data);
189 network.nodes.push(d); 189 network.nodes.push(d);
190 lu[id] = d; 190 lu[id] = d;
191 191
...@@ -193,7 +193,7 @@ ...@@ -193,7 +193,7 @@
193 193
194 updateNodes(); 194 updateNodes();
195 195
196 - lnk = createHostLink(data); 196 + lnk = tms.createHostLink(data);
197 if (lnk) { 197 if (lnk) {
198 198
199 $log.debug("Created new host-link.. ", lnk.key); 199 $log.debug("Created new host-link.. ", lnk.key);
...@@ -213,7 +213,7 @@ ...@@ -213,7 +213,7 @@
213 d = lu[id]; 213 d = lu[id];
214 if (d) { 214 if (d) {
215 angular.extend(d, data); 215 angular.extend(d, data);
216 - if (positionNode(d, true)) { 216 + if (tms.positionNode(d, true)) {
217 sendUpdateMeta(d); 217 sendUpdateMeta(d);
218 } 218 }
219 updateNodes(); 219 updateNodes();
...@@ -251,7 +251,7 @@ ...@@ -251,7 +251,7 @@
251 } 251 }
252 252
253 // no backing store link yet 253 // no backing store link yet
254 - d = createLink(data); 254 + d = tms.createLink(data);
255 if (d) { 255 if (d) {
256 network.links.push(d); 256 network.links.push(d);
257 lu[d.key] = d; 257 lu[d.key] = d;
...@@ -290,42 +290,6 @@ ...@@ -290,42 +290,6 @@
290 restyleLinkElement(ldata); 290 restyleLinkElement(ldata);
291 } 291 }
292 292
293 - function createLink(link) {
294 - var lnk = linkEndPoints(link.src, link.dst);
295 -
296 - if (!lnk) {
297 - return null;
298 - }
299 -
300 - angular.extend(lnk, {
301 - key: link.id,
302 - class: 'link',
303 - fromSource: link,
304 -
305 - // functions to aggregate dual link state
306 - type: function () {
307 - var s = lnk.fromSource,
308 - t = lnk.fromTarget;
309 - return (s && s.type) || (t && t.type) || defaultLinkType;
310 - },
311 - online: function () {
312 - var s = lnk.fromSource,
313 - t = lnk.fromTarget,
314 - both = lnk.source.online && lnk.target.online;
315 - return both && ((s && s.online) || (t && t.online));
316 - },
317 - linkWidth: function () {
318 - var s = lnk.fromSource,
319 - t = lnk.fromTarget,
320 - ws = (s && s.linkWidth) || 0,
321 - wt = (t && t.linkWidth) || 0;
322 - return Math.max(ws, wt);
323 - }
324 - });
325 - return lnk;
326 - }
327 -
328 -
329 function makeNodeKey(d, what) { 293 function makeNodeKey(d, what) {
330 var port = what + 'Port'; 294 var port = what + 'Port';
331 return d[what] + '/' + d[port]; 295 return d[what] + '/' + d[port];
...@@ -342,8 +306,7 @@ ...@@ -342,8 +306,7 @@
342 .domain([1, 12]) 306 .domain([1, 12])
343 .range([widthRatio, 12 * widthRatio]) 307 .range([widthRatio, 12 * widthRatio])
344 .clamp(true), 308 .clamp(true),
345 - allLinkTypes = 'direct indirect optical tunnel', 309 + allLinkTypes = 'direct indirect optical tunnel';
346 - defaultLinkType = 'direct';
347 310
348 function restyleLinkElement(ldata) { 311 function restyleLinkElement(ldata) {
349 // this fn's job is to look at raw links and decide what svg classes 312 // this fn's job is to look at raw links and decide what svg classes
...@@ -568,7 +531,7 @@ ...@@ -568,7 +531,7 @@
568 // if we are not clearing the position data (unpinning), 531 // if we are not clearing the position data (unpinning),
569 // attach the x, y, longitude, latitude... 532 // attach the x, y, longitude, latitude...
570 if (!clearPos) { 533 if (!clearPos) {
571 - ll = lngLatFromCoord([d.x, d.y]); 534 + ll = tms.lngLatFromCoord([d.x, d.y]);
572 metaUi = { 535 metaUi = {
573 x: d.x, 536 x: d.x,
574 y: d.y, 537 y: d.y,
...@@ -588,171 +551,11 @@ ...@@ -588,171 +551,11 @@
588 $log.debug('TODO: requestTrafficForMode()...'); 551 $log.debug('TODO: requestTrafficForMode()...');
589 } 552 }
590 553
591 - // ==========================
592 - // === Devices and hosts - helper functions
593 -
594 - function coordFromLngLat(loc) {
595 - var p = uplink.projection();
596 - return p ? p([loc.lng, loc.lat]) : [0, 0];
597 - }
598 -
599 - function lngLatFromCoord(coord) {
600 - var p = uplink.projection();
601 - return p ? p.invert(coord) : [0, 0];
602 - }
603 -
604 - function positionNode(node, forUpdate) {
605 - var meta = node.metaUi,
606 - x = meta && meta.x,
607 - y = meta && meta.y,
608 - xy;
609 -
610 - // If we have [x,y] already, use that...
611 - if (x && y) {
612 - node.fixed = true;
613 - node.px = node.x = x;
614 - node.py = node.y = y;
615 - return;
616 - }
617 -
618 - var location = node.location,
619 - coord;
620 -
621 - if (location && location.type === 'latlng') {
622 - coord = coordFromLngLat(location);
623 - node.fixed = true;
624 - node.px = node.x = coord[0];
625 - node.py = node.y = coord[1];
626 - return true;
627 - }
628 -
629 - // if this is a node update (not a node add).. skip randomizer
630 - if (forUpdate) {
631 - return;
632 - }
633 -
634 - // Note: Placing incoming unpinned nodes at exactly the same point
635 - // (center of the view) causes them to explode outwards when
636 - // the force layout kicks in. So, we spread them out a bit
637 - // initially, to provide a more serene layout convergence.
638 - // Additionally, if the node is a host, we place it near
639 - // the device it is connected to.
640 -
641 - function spread(s) {
642 - return Math.floor((Math.random() * s) - s/2);
643 - }
644 -
645 - function randDim(dim) {
646 - return dim / 2 + spread(dim * 0.7071);
647 - }
648 -
649 - function rand() {
650 - return {
651 - x: randDim(width),
652 - y: randDim(height)
653 - };
654 - }
655 -
656 - function near(node) {
657 - var min = 12,
658 - dx = spread(12),
659 - dy = spread(12);
660 - return {
661 - x: node.x + min + dx,
662 - y: node.y + min + dy
663 - };
664 - }
665 -
666 - function getDevice(cp) {
667 - var d = lu[cp.device];
668 - return d || rand();
669 - }
670 -
671 - xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
672 - angular.extend(node, xy);
673 - }
674 -
675 - function createDeviceNode(device) {
676 - // start with the object as is
677 - var node = device,
678 - type = device.type,
679 - svgCls = type ? 'node device ' + type : 'node device';
680 -
681 - // Augment as needed...
682 - node.class = 'device';
683 - node.svgClass = device.online ? svgCls + ' online' : svgCls;
684 - positionNode(node);
685 - return node;
686 - }
687 -
688 - function createHostNode(host) {
689 - var node = host;
690 -
691 - // Augment as needed...
692 - node.class = 'host';
693 - if (!node.type) {
694 - node.type = 'endstation';
695 - }
696 - node.svgClass = 'node host ' + node.type;
697 - positionNode(node);
698 - return node;
699 - }
700 -
701 - function createHostLink(host) {
702 - var src = host.id,
703 - dst = host.cp.device,
704 - id = host.ingress,
705 - lnk = linkEndPoints(src, dst);
706 -
707 - if (!lnk) {
708 - return null;
709 - }
710 -
711 - // Synthesize link ...
712 - angular.extend(lnk, {
713 - key: id,
714 - class: 'link',
715 -
716 - type: function () { return 'hostLink'; },
717 - online: function () {
718 - // hostlink target is edge switch
719 - return lnk.target.online;
720 - },
721 - linkWidth: function () { return 1; }
722 - });
723 - return lnk;
724 - }
725 -
726 - function linkEndPoints(srcId, dstId) {
727 - var srcNode = lu[srcId],
728 - dstNode = lu[dstId],
729 - sMiss = !srcNode ? missMsg('src', srcId) : '',
730 - dMiss = !dstNode ? missMsg('dst', dstId) : '';
731 -
732 - if (sMiss || dMiss) {
733 - $log.error('Node(s) not on map for link:\n' + sMiss + dMiss);
734 - //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
735 - return null;
736 - }
737 - return {
738 - source: srcNode,
739 - target: dstNode,
740 - x1: srcNode.x,
741 - y1: srcNode.y,
742 - x2: dstNode.x,
743 - y2: dstNode.y
744 - };
745 - }
746 -
747 - function missMsg(what, id) {
748 - return '\n[' + what + '] "' + id + '" missing ';
749 - }
750 554
751 // ========================== 555 // ==========================
752 // === Devices and hosts - D3 rendering 556 // === Devices and hosts - D3 rendering
753 557
754 function nodeMouseOver(m) { 558 function nodeMouseOver(m) {
755 - // TODO
756 if (!m.dragStarted) { 559 if (!m.dragStarted) {
757 $log.debug("MouseOver()...", m); 560 $log.debug("MouseOver()...", m);
758 if (hovered != m) { 561 if (hovered != m) {
...@@ -763,7 +566,6 @@ ...@@ -763,7 +566,6 @@
763 } 566 }
764 567
765 function nodeMouseOut(m) { 568 function nodeMouseOut(m) {
766 - // TODO
767 if (!m.dragStarted) { 569 if (!m.dragStarted) {
768 if (hovered) { 570 if (hovered) {
769 hovered = null; 571 hovered = null;
...@@ -1031,12 +833,12 @@ ...@@ -1031,12 +833,12 @@
1031 var node = d.el; 833 var node = d.el;
1032 node.classed('online', d.online); 834 node.classed('online', d.online);
1033 updateDeviceLabel(d); 835 updateDeviceLabel(d);
1034 - positionNode(d, true); 836 + tms.positionNode(d, true);
1035 } 837 }
1036 838
1037 function hostExisting(d) { 839 function hostExisting(d) {
1038 updateHostLabel(d); 840 updateHostLabel(d);
1039 - positionNode(d, true); 841 + tms.positionNode(d, true);
1040 } 842 }
1041 843
1042 function deviceEnter(d) { 844 function deviceEnter(d) {
...@@ -1424,9 +1226,9 @@ ...@@ -1424,9 +1226,9 @@
1424 angular.module('ovTopo') 1226 angular.module('ovTopo')
1425 .factory('TopoForceService', 1227 .factory('TopoForceService',
1426 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService', 1228 ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
1427 - 'FlashService', 'TopoInstService', 1229 + 'FlashService', 'TopoInstService', 'TopoModelService',
1428 1230
1429 - function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_) { 1231 + function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_, _tis_, _tms_) {
1430 $log = _$log_; 1232 $log = _$log_;
1431 fs = _fs_; 1233 fs = _fs_;
1432 sus = _sus_; 1234 sus = _sus_;
...@@ -1434,18 +1236,24 @@ ...@@ -1434,18 +1236,24 @@
1434 ts = _ts_; 1236 ts = _ts_;
1435 flash = _flash_; 1237 flash = _flash_;
1436 tis = _tis_; 1238 tis = _tis_;
1239 + tms = _tms_;
1437 1240
1438 icfg = is.iconConfig(); 1241 icfg = is.iconConfig();
1439 1242
1440 // forceG is the SVG group to display the force layout in 1243 // forceG is the SVG group to display the force layout in
1441 // xlink is the cross-link api from the main topo source file 1244 // xlink is the cross-link api from the main topo source file
1442 - // w, h are the initial dimensions of the SVG 1245 + // dim is the initial dimensions of the SVG as [w,h]
1443 // opts are, well, optional :) 1246 // opts are, well, optional :)
1444 - function initForce(forceG, _uplink_, w, h, opts) { 1247 + function initForce(forceG, _uplink_, _dim_, opts) {
1445 - $log.debug('initForce().. WxH = ' + w + 'x' + h);
1446 uplink = _uplink_; 1248 uplink = _uplink_;
1447 - width = w; 1249 + dim = _dim_;
1448 - height = h; 1250 +
1251 + $log.debug('initForce().. dim = ' + dim);
1252 +
1253 + tms.initModel({
1254 + projection: uplink.projection,
1255 + lookup: network.lookup
1256 + }, dim);
1449 1257
1450 settings = angular.extend({}, defaultSettings, opts); 1258 settings = angular.extend({}, defaultSettings, opts);
1451 1259
...@@ -1458,7 +1266,7 @@ ...@@ -1458,7 +1266,7 @@
1458 node = nodeG.selectAll('.node'); 1266 node = nodeG.selectAll('.node');
1459 1267
1460 force = d3.layout.force() 1268 force = d3.layout.force()
1461 - .size([w, h]) 1269 + .size(dim)
1462 .nodes(network.nodes) 1270 .nodes(network.nodes)
1463 .links(network.links) 1271 .links(network.links)
1464 .gravity(settings.gravity) 1272 .gravity(settings.gravity)
...@@ -1472,16 +1280,21 @@ ...@@ -1472,16 +1280,21 @@
1472 selectObject, atDragEnd, dragEnabled, clickEnabled); 1280 selectObject, atDragEnd, dragEnabled, clickEnabled);
1473 } 1281 }
1474 1282
1475 - function resize(dim) { 1283 + function newDim(_dim_) {
1476 - width = dim.width; 1284 + dim = _dim_;
1477 - height = dim.height; 1285 + force.size(dim);
1478 - force.size([width, height]); 1286 + tms.newDim(dim);
1479 // Review -- do we need to nudge the layout ? 1287 // Review -- do we need to nudge the layout ?
1480 } 1288 }
1481 1289
1290 + function destroyForce() {
1291 +
1292 + }
1293 +
1482 return { 1294 return {
1483 initForce: initForce, 1295 initForce: initForce,
1484 - resize: resize, 1296 + newDim: newDim,
1297 + destroyForce: destroyForce,
1485 1298
1486 updateDeviceColors: updateDeviceColors, 1299 updateDeviceColors: updateDeviceColors,
1487 toggleHosts: toggleHosts, 1300 toggleHosts: toggleHosts,
......
1 +/*
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 Model Module.
19 + Auxiliary functions for the model of the topology; that is, our internal
20 + representations of devices, hosts, links, etc.
21 + */
22 +
23 +(function () {
24 + 'use strict';
25 +
26 + // injected refs
27 + var $log, fs, rnd, api;
28 +
29 + var dim; // dimensions of layout, as [w,h]
30 +
31 + // configuration 'constants'
32 + var defaultLinkType = 'direct',
33 + nearDist = 15;
34 +
35 +
36 + function coordFromLngLat(loc) {
37 + var p = api.projection();
38 + return p ? p([loc.lng, loc.lat]) : [0, 0];
39 + }
40 +
41 + function lngLatFromCoord(coord) {
42 + var p = api.projection();
43 + return p ? p.invert(coord) : [0, 0];
44 + }
45 +
46 + function positionNode(node, forUpdate) {
47 + var meta = node.metaUi,
48 + x = meta && meta.x,
49 + y = meta && meta.y,
50 + xy;
51 +
52 + // If we have [x,y] already, use that...
53 + if (x && y) {
54 + node.fixed = true;
55 + node.px = node.x = x;
56 + node.py = node.y = y;
57 + return;
58 + }
59 +
60 + var location = node.location,
61 + coord;
62 +
63 + if (location && location.type === 'latlng') {
64 + coord = coordFromLngLat(location);
65 + node.fixed = true;
66 + node.px = node.x = coord[0];
67 + node.py = node.y = coord[1];
68 + return true;
69 + }
70 +
71 + // if this is a node update (not a node add).. skip randomizer
72 + if (forUpdate) {
73 + return;
74 + }
75 +
76 + // Note: Placing incoming unpinned nodes at exactly the same point
77 + // (center of the view) causes them to explode outwards when
78 + // the force layout kicks in. So, we spread them out a bit
79 + // initially, to provide a more serene layout convergence.
80 + // Additionally, if the node is a host, we place it near
81 + // the device it is connected to.
82 +
83 + function rand() {
84 + return {
85 + x: rnd.randDim(dim[0]),
86 + y: rnd.randDim(dim[1])
87 + };
88 + }
89 +
90 + function near(node) {
91 + return {
92 + x: node.x + nearDist + rnd.spread(nearDist),
93 + y: node.y + nearDist + rnd.spread(nearDist)
94 + };
95 + }
96 +
97 + function getDevice(cp) {
98 + var d = api.lookup[cp.device];
99 + return d || rand();
100 + }
101 +
102 + xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
103 + angular.extend(node, xy);
104 + }
105 +
106 + function mkSvgCls(dh, t, on) {
107 + var ndh = 'node ' + dh,
108 + ndht = t ? ndh + ' ' + t : ndh;
109 + return on ? ndht + ' online' : ndht;
110 + }
111 +
112 + function createDeviceNode(device) {
113 + var node = device;
114 +
115 + // Augment as needed...
116 + node.class = 'device';
117 + node.svgClass = mkSvgCls('device', device.type, device.online);
118 + positionNode(node);
119 + return node;
120 + }
121 +
122 + function createHostNode(host) {
123 + var node = host;
124 +
125 + // Augment as needed...
126 + node.class = 'host';
127 + if (!node.type) {
128 + node.type = 'endstation';
129 + }
130 + node.svgClass = mkSvgCls('host', node.type);
131 + positionNode(node);
132 + return node;
133 + }
134 +
135 + function createHostLink(host) {
136 + var src = host.id,
137 + dst = host.cp.device,
138 + id = host.ingress,
139 + lnk = linkEndPoints(src, dst);
140 +
141 + if (!lnk) {
142 + return null;
143 + }
144 +
145 + // Synthesize link ...
146 + angular.extend(lnk, {
147 + key: id,
148 + class: 'link',
149 +
150 + type: function () { return 'hostLink'; },
151 + online: function () {
152 + // hostlink target is edge switch
153 + return lnk.target.online;
154 + },
155 + linkWidth: function () { return 1; }
156 + });
157 + return lnk;
158 + }
159 +
160 + function createLink(link) {
161 + var lnk = linkEndPoints(link.src, link.dst);
162 +
163 + if (!lnk) {
164 + return null;
165 + }
166 +
167 + angular.extend(lnk, {
168 + key: link.id,
169 + class: 'link',
170 + fromSource: link,
171 +
172 + // functions to aggregate dual link state
173 + type: function () {
174 + var s = lnk.fromSource,
175 + t = lnk.fromTarget;
176 + return (s && s.type) || (t && t.type) || defaultLinkType;
177 + },
178 + online: function () {
179 + var s = lnk.fromSource,
180 + t = lnk.fromTarget,
181 + both = lnk.source.online && lnk.target.online;
182 + return both && ((s && s.online) || (t && t.online));
183 + },
184 + linkWidth: function () {
185 + var s = lnk.fromSource,
186 + t = lnk.fromTarget,
187 + ws = (s && s.linkWidth) || 0,
188 + wt = (t && t.linkWidth) || 0;
189 + return Math.max(ws, wt);
190 + }
191 + });
192 + return lnk;
193 + }
194 +
195 +
196 + function linkEndPoints(srcId, dstId) {
197 + var srcNode = api.lookup[srcId],
198 + dstNode = api.lookup[dstId],
199 + sMiss = !srcNode ? missMsg('src', srcId) : '',
200 + dMiss = !dstNode ? missMsg('dst', dstId) : '';
201 +
202 + if (sMiss || dMiss) {
203 + $log.error('Node(s) not on map for link:' + sMiss + dMiss);
204 + //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
205 + return null;
206 + }
207 + return {
208 + source: srcNode,
209 + target: dstNode,
210 + x1: srcNode.x,
211 + y1: srcNode.y,
212 + x2: dstNode.x,
213 + y2: dstNode.y
214 + };
215 + }
216 +
217 + function missMsg(what, id) {
218 + return '\n[' + what + '] "' + id + '" missing';
219 + }
220 +
221 + // ==========================
222 + // Module definition
223 +
224 + angular.module('ovTopo')
225 + .factory('TopoModelService',
226 + ['$log', 'FnService', 'RandomService',
227 +
228 + function (_$log_, _fs_, _rnd_) {
229 + $log = _$log_;
230 + fs = _fs_;
231 + rnd = _rnd_;
232 +
233 + function initModel(_api_, _dim_) {
234 + api = _api_;
235 + dim = _dim_;
236 + }
237 +
238 + function newDim(_dim_) {
239 + dim = _dim_;
240 + }
241 +
242 + return {
243 + initModel: initModel,
244 + newDim: newDim,
245 +
246 + positionNode: positionNode,
247 + createDeviceNode: createDeviceNode,
248 + createHostNode: createHostNode,
249 + createHostLink: createHostLink,
250 + createLink: createLink,
251 + coordFromLngLat: coordFromLngLat,
252 + lngLatFromCoord: lngLatFromCoord,
253 + }
254 + }]);
255 +}());
1 +/*
2 + * Copyright 2014,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 -- Util -- Random Service - Unit Tests
19 + */
20 +describe('factory: fw/util/random.js', function() {
21 + var rnd, $log, fs;
22 +
23 + beforeEach(module('onosUtil'));
24 +
25 + beforeEach(inject(function (RandomService, _$log_, FnService) {
26 + rnd = RandomService;
27 + $log = _$log_;
28 + fs = FnService;
29 + }));
30 +
31 + // interesting use of a custom matcher...
32 + beforeEach(function () {
33 + jasmine.addMatchers({
34 + toBeWithinOf: function () {
35 + return {
36 + compare: function (actual, distance, base) {
37 + var lower = base - distance,
38 + upper = base + distance,
39 + result = {};
40 +
41 + result.pass = Math.abs(actual - base) <= distance;
42 +
43 + if (result.pass) {
44 + // for negation with ".not"
45 + result.message = 'Expected ' + actual +
46 + ' to be outside ' + lower + ' and ' +
47 + upper + ' (inclusive)';
48 + } else {
49 + result.message = 'Expected ' + actual +
50 + ' to be between ' + lower + ' and ' +
51 + upper + ' (inclusive)';
52 + }
53 + return result;
54 + }
55 + }
56 + }
57 + });
58 + });
59 +
60 + it('should define RandomService', function () {
61 + expect(rnd).toBeDefined();
62 + });
63 +
64 + it('should define api functions', function () {
65 + expect(fs.areFunctions(rnd, [
66 + 'spread', 'randDim'
67 + ])).toBeTruthy();
68 + });
69 +
70 + // really, can only do this heuristically.. hope this doesn't break
71 + it('should spread results across the range', function () {
72 + var load = 1000,
73 + s = 12,
74 + low = 0,
75 + high = 0,
76 + i, res,
77 + which = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
78 + minCount = load / s * 0.5; // generous error
79 +
80 + for (i=0; i<load; i++) {
81 + res = rnd.spread(s);
82 + if (res < low) low = res;
83 + if (res > high) high = res;
84 + which[res + s/2]++;
85 + }
86 + expect(low).toBe(-6);
87 + expect(high).toBe(5);
88 +
89 + // check we got a good number of hits in each bucket
90 + for (i=0; i<s; i++) {
91 + expect(which[i]).toBeGreaterThan(minCount);
92 + }
93 + });
94 +
95 + // really, can only do this heuristically.. hope this doesn't break
96 + it('should choose results across the dimension', function () {
97 + var load = 1000,
98 + dim = 100,
99 + low = 999,
100 + high = 0,
101 + i, res;
102 +
103 + for (i=0; i<load; i++) {
104 + res = rnd.randDim(dim);
105 + if (res < low) low = res;
106 + if (res > high) high = res;
107 + expect(res).toBeWithinOf(36, 50);
108 + }
109 + });
110 +});
...@@ -29,7 +29,7 @@ describe('factory: fw/util/theme.js', function() { ...@@ -29,7 +29,7 @@ describe('factory: fw/util/theme.js', function() {
29 ts.init(); 29 ts.init();
30 })); 30 }));
31 31
32 - it('should define MapService', function () { 32 + it('should define ThemeService', function () {
33 expect(ts).toBeDefined(); 33 expect(ts).toBeDefined();
34 }); 34 });
35 35
......
...@@ -34,8 +34,11 @@ describe('factory: view/topo/topoForce.js', function() { ...@@ -34,8 +34,11 @@ describe('factory: view/topo/topoForce.js', function() {
34 34
35 it('should define api functions', function () { 35 it('should define api functions', function () {
36 expect(fs.areFunctions(tfs, [ 36 expect(fs.areFunctions(tfs, [
37 - 'initForce', 'resize', 'updateDeviceColors', 37 + 'initForce', 'newDim', 'destroyForce',
38 - 'toggleHosts', 'toggleOffline','cycleDeviceLabels', 'unpin', 38 +
39 + 'updateDeviceColors', 'toggleHosts', 'toggleOffline',
40 + 'cycleDeviceLabels', 'unpin',
41 +
39 'addDevice', 'updateDevice', 'removeDevice', 42 'addDevice', 'updateDevice', 'removeDevice',
40 'addHost', 'updateHost', 'removeHost', 43 'addHost', 'updateHost', 'removeHost',
41 'addLink', 'updateLink', 'removeLink' 44 'addLink', 'updateLink', 'removeLink'
......
1 +/*
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 -- Topo View -- Topo Model Service - Unit Tests
19 + */
20 +describe('factory: view/topo/topoModel.js', function() {
21 + var $log, fs, rnd, tms;
22 +
23 + // stop random numbers from being quite so random
24 + var mockRandom = {
25 + // mock spread returns s + 1
26 + spread: function (s) {
27 + return s + 1;
28 + },
29 + // mock random dimension returns d / 2 - 1
30 + randDim: function (d) {
31 + return d/2 - 1;
32 + },
33 + mock: 'yup'
34 + };
35 +
36 + // to mock out the [lng,lat] <=> [x,y] transformations, we will
37 + // add/subtract 2000, 3000 respectively:
38 + // lng:2005 === x:5, lat:3004 === y:4
39 +
40 + var mockProjection = function (lnglat) {
41 + return [lnglat[0] - 2000, lnglat[1] - 3000];
42 + };
43 +
44 + mockProjection.invert = function (xy) {
45 + return [xy[0] + 2000, xy[1] + 3000];
46 + };
47 +
48 + // our test device lookup
49 + var lu = {
50 + dev1: {
51 + 'class': 'device',
52 + id: 'dev1',
53 + x: 17,
54 + y: 27,
55 + online: true
56 + },
57 + dev2: {
58 + 'class': 'device',
59 + id: 'dev2',
60 + x: 18,
61 + y: 28,
62 + online: true
63 + },
64 + host1: {
65 + 'class': 'host',
66 + id: 'host1',
67 + x: 23,
68 + y: 33,
69 + cp: {
70 + device: 'dev1',
71 + port: 7
72 + },
73 + ingress: 'dev1/7-host1'
74 + },
75 + host2: {
76 + 'class': 'host',
77 + id: 'host2',
78 + x: 24,
79 + y: 34,
80 + cp: {
81 + device: 'dev0',
82 + port: 0
83 + },
84 + ingress: 'dev0/0-host2'
85 + }
86 + };
87 +
88 + // our test api
89 + var api = {
90 + projection: function () { return mockProjection; },
91 + lookup: lu
92 + };
93 +
94 + // our test dimensions and well known locations..
95 + var dim = [20, 40],
96 + randLoc = [9, 19], // random location using randDim(): d/2-1
97 + randHostLoc = [40, 50], // host "near" random location
98 + // given that 'nearDist' = 15
99 + // and spread(15) = 16
100 + // 9 + 15 + 16 = 40; 19 + 15 + 16 = 50
101 + nearDev1 = [48,58], // [17+15+16, 27+15+16]
102 + dev1Loc = [17,27],
103 + dev2Loc = [18,28],
104 + host1Loc = [23,33],
105 + host2Loc = [24,34];
106 +
107 + // implement some custom matchers...
108 + beforeEach(function () {
109 + jasmine.addMatchers({
110 + toBePositionedAt: function () {
111 + return {
112 + compare: function (actual, xy) {
113 + var result = {},
114 + actCoord = [actual.x, actual.y];
115 +
116 + result.pass = (actual.x === xy[0]) && (actual.y === xy[1]);
117 +
118 + if (result.pass) {
119 + // for negation with ".not"
120 + result.message = 'Expected [' + actCoord +
121 + '] NOT to be positioned at [' + xy + ']';
122 + } else {
123 + result.message = 'Expected [' + actCoord +
124 + '] to be positioned at [' + xy + ']';
125 + }
126 + return result;
127 + }
128 + }
129 + },
130 + toHaveEndPoints: function () {
131 + return {
132 + compare: function (actual, xy1, xy2) {
133 + var result = {};
134 +
135 + result.pass = (actual.x1 === xy1[0]) && (actual.y1 === xy1[1]) &&
136 + (actual.x2 === xy2[0]) && (actual.y2 === xy2[1]);
137 +
138 + if (result.pass) {
139 + // for negation with ".not"
140 + result.message = 'Expected ' + actual +
141 + ' NOT to have endpoints [' + xy1 + ']-[' + xy2 + ']';
142 + } else {
143 + result.message = 'Expected ' + actual +
144 + ' to have endpoints [' + xy1 + ']-[' + xy2 + ']';
145 + }
146 + return result;
147 + }
148 + }
149 + },
150 + toBeFixed: function () {
151 + return {
152 + compare: function (actual) {
153 + var result = {
154 + pass: actual.fixed
155 + };
156 + if (result.pass) {
157 + result.message = 'Expected ' + actual +
158 + ' NOT to be fixed!';
159 + } else {
160 + result.message = 'Expected ' + actual +
161 + ' to be fixed!';
162 + }
163 + return result;
164 + }
165 + }
166 + }
167 + });
168 + });
169 +
170 + beforeEach(module('ovTopo', 'onosUtil'));
171 +
172 + beforeEach(function () {
173 + module(function ($provide) {
174 + $provide.value('RandomService', mockRandom);
175 + });
176 + });
177 +
178 + beforeEach(inject(function (_$log_, FnService, RandomService, TopoModelService) {
179 + $log = _$log_;
180 + fs = FnService;
181 + rnd = RandomService;
182 + tms = TopoModelService;
183 + tms.initModel(api, dim);
184 + }));
185 +
186 +
187 + it('should install the mock random service', function () {
188 + expect(rnd.mock).toBe('yup');
189 + expect(rnd.spread(4)).toBe(5);
190 + expect(rnd.randDim(8)).toBe(3);
191 + });
192 +
193 + it('should install the mock projection', function () {
194 + expect(tms.coordFromLngLat({lng: 2005, lat: 3004})).toEqual([5,4]);
195 + expect(tms.lngLatFromCoord([5,4])).toEqual([2005,3004]);
196 + });
197 +
198 + it('should define TopoModelService', function () {
199 + expect(tms).toBeDefined();
200 + });
201 +
202 + it('should define api functions', function () {
203 + expect(fs.areFunctions(tms, [
204 + 'initModel', 'newDim',
205 + 'positionNode', 'createDeviceNode', 'createHostNode',
206 + 'createHostLink', 'createLink',
207 + 'coordFromLngLat', 'lngLatFromCoord'
208 + ])).toBeTruthy();
209 + });
210 +
211 + // === unit tests for positionNode()
212 +
213 + it('should position a node using meta x/y', function () {
214 + var node = {
215 + metaUi: { x:37, y:48 }
216 + };
217 + tms.positionNode(node);
218 + expect(node).toBePositionedAt([37,48]);
219 + expect(node).toBeFixed();
220 + });
221 +
222 + it('should position a node by translating lng/lat', function () {
223 + var node = {
224 + location: {
225 + type: 'latlng',
226 + lng: 2008,
227 + lat: 3009
228 + }
229 + };
230 + tms.positionNode(node);
231 + expect(node).toBePositionedAt([8,9]);
232 + expect(node).toBeFixed();
233 + });
234 +
235 + it('should position a device with no location randomly', function () {
236 + var node = { 'class': 'device' };
237 + tms.positionNode(node);
238 + expect(node).toBePositionedAt(randLoc);
239 + expect(node).not.toBeFixed();
240 + });
241 +
242 + it('should position a device randomly even if x/y set', function () {
243 + var node = { 'class': 'device', x: 1, y: 2 };
244 + tms.positionNode(node);
245 + expect(node).toBePositionedAt(randLoc);
246 + expect(node).not.toBeFixed();
247 + });
248 +
249 + it('should NOT reposition a device randomly on update', function () {
250 + var node = { 'class': 'device', x: 1, y: 2 };
251 + tms.positionNode(node, true);
252 + expect(node).toBePositionedAt([1,2]);
253 + expect(node).not.toBeFixed();
254 + });
255 +
256 + it('should position a host close to its device', function () {
257 + var node = { 'class': 'host', cp: { device: 'dev1' } };
258 + tms.positionNode(node);
259 +
260 + // note: nearDist is 15; spread(15) adds 16; dev1 at [17,27]
261 +
262 + expect(node).toBePositionedAt(nearDev1);
263 + expect(node).not.toBeFixed();
264 + });
265 +
266 + it('should randomize host with no assoc device', function () {
267 + var node = { 'class': 'host', cp: { device: 'dev0' } };
268 + tms.positionNode(node);
269 +
270 + // note: no device gives 'rand loc' [9,19]
271 + // nearDist is 15; spread(15) adds 16
272 +
273 + expect(node).toBePositionedAt(randHostLoc);
274 + expect(node).not.toBeFixed();
275 + });
276 +
277 + // === unit tests for createDeviceNode()
278 +
279 + it('should create a basic device node', function () {
280 + var node = tms.createDeviceNode({ id: 'foo' });
281 + expect(node).toBePositionedAt(randLoc);
282 + expect(node).not.toBeFixed();
283 + expect(node.class).toEqual('device');
284 + expect(node.svgClass).toEqual('node device');
285 + expect(node.id).toEqual('foo');
286 + });
287 +
288 + it('should create device node with type', function () {
289 + var node = tms.createDeviceNode({ id: 'foo', type: 'cool' });
290 + expect(node).toBePositionedAt(randLoc);
291 + expect(node).not.toBeFixed();
292 + expect(node.class).toEqual('device');
293 + expect(node.svgClass).toEqual('node device cool');
294 + expect(node.id).toEqual('foo');
295 + });
296 +
297 + it('should create online device node with type', function () {
298 + var node = tms.createDeviceNode({ id: 'foo', type: 'cool', online: true });
299 + expect(node).toBePositionedAt(randLoc);
300 + expect(node).not.toBeFixed();
301 + expect(node.class).toEqual('device');
302 + expect(node.svgClass).toEqual('node device cool online');
303 + expect(node.id).toEqual('foo');
304 + });
305 +
306 + it('should create online device node with type and lng/lat', function () {
307 + var node = tms.createDeviceNode({
308 + id: 'foo',
309 + type: 'yowser',
310 + online: true,
311 + location: {
312 + type: 'latlng',
313 + lng: 2048,
314 + lat: 3096
315 + }
316 + });
317 + expect(node).toBePositionedAt([48,96]);
318 + expect(node).toBeFixed();
319 + expect(node.class).toEqual('device');
320 + expect(node.svgClass).toEqual('node device yowser online');
321 + expect(node.id).toEqual('foo');
322 + });
323 +
324 + // === unit tests for createHostNode()
325 +
326 + it('should create a basic host node', function () {
327 + var node = tms.createHostNode({ id: 'bar', cp: { device: 'dev0' } });
328 + expect(node).toBePositionedAt(randHostLoc);
329 + expect(node).not.toBeFixed();
330 + expect(node.class).toEqual('host');
331 + expect(node.svgClass).toEqual('node host endstation');
332 + expect(node.id).toEqual('bar');
333 + });
334 +
335 + it('should create a host with type', function () {
336 + var node = tms.createHostNode({
337 + id: 'bar',
338 + type: 'classic',
339 + cp: { device: 'dev1' }
340 + });
341 + expect(node).toBePositionedAt(nearDev1);
342 + expect(node).not.toBeFixed();
343 + expect(node.class).toEqual('host');
344 + expect(node.svgClass).toEqual('node host classic');
345 + expect(node.id).toEqual('bar');
346 + });
347 +
348 + // === unit tests for createHostLink()
349 +
350 + it('should create a basic host link', function () {
351 + var link = tms.createHostLink(lu.host1);
352 + expect(link.source).toEqual(lu.host1);
353 + expect(link.target).toEqual(lu.dev1);
354 + expect(link).toHaveEndPoints(host1Loc, dev1Loc);
355 + expect(link.key).toEqual('dev1/7-host1');
356 + expect(link.class).toEqual('link');
357 + expect(link.type()).toEqual('hostLink');
358 + expect(link.linkWidth()).toEqual(1);
359 + expect(link.online()).toEqual(true);
360 + });
361 +
362 + it('should return null for failed endpoint lookup', function () {
363 + spyOn($log, 'error');
364 + var link = tms.createHostLink(lu.host2);
365 + expect(link).toBeNull();
366 + expect($log.error).toHaveBeenCalledWith(
367 + 'Node(s) not on map for link:\n[dst] "dev0" missing'
368 + );
369 + });
370 +
371 + // === unit tests for createLink()
372 +
373 + it('should return null for missing endpoints', function () {
374 + spyOn($log, 'error');
375 + var link = tms.createLink({src: 'dev0', dst: 'dev00'});
376 + expect(link).toBeNull();
377 + expect($log.error).toHaveBeenCalledWith(
378 + 'Node(s) not on map for link:\n[src] "dev0" missing\n[dst] "dev00" missing'
379 + );
380 + });
381 +
382 + it('should create a basic link', function () {
383 + var linkData = {
384 + src: 'dev1',
385 + dst: 'dev2',
386 + id: 'baz',
387 + type: 'zoo',
388 + online: true,
389 + linkWidth: 1.5
390 + },
391 + link = tms.createLink(linkData);
392 + expect(link.source).toEqual(lu.dev1);
393 + expect(link.target).toEqual(lu.dev2);
394 + expect(link).toHaveEndPoints(dev1Loc, dev2Loc);
395 + expect(link.key).toEqual('baz');
396 + expect(link.class).toEqual('link');
397 + expect(link.fromSource).toBe(linkData);
398 + expect(link.type()).toEqual('zoo');
399 + expect(link.online()).toEqual(true);
400 + expect(link.linkWidth()).toEqual(1.5);
401 + });
402 +
403 +});