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
430 additions
and
6 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(); | ... | ... |
This diff is collapsed. Click to expand it.
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' | ... | ... |
This diff is collapsed. Click to expand it.
-
Please register or login to post a comment