GUI -- TopoView - Migrated helper functions to topoModel.js.
- moved randomized functions to random.js (so we can mock them). Change-Id: Ic56ce64c036d36f34798f0df9f03a7d09335a2ab
Showing
9 changed files
with
872 additions
and
232 deletions
| 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 | +}); |
-
Please register or login to post a comment