Simon Hunt

GUI -- Added handling of hosts and links. (Still WIP).

Change-Id: I0ad3b16d47b264b6812f732f220230a2ae92de02
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
20 (function () { 20 (function () {
21 'use strict'; 21 'use strict';
22 22
23 - var $log, fs, gs; 23 + var $log, fs, gs, sus;
24 24
25 var vboxSize = 50, 25 var vboxSize = 50,
26 cornerSize = vboxSize / 10, 26 cornerSize = vboxSize / 10,
...@@ -144,8 +144,21 @@ ...@@ -144,8 +144,21 @@
144 return g; 144 return g;
145 } 145 }
146 146
147 - function addHostIcon(elem, glyphId) { 147 + function addHostIcon(elem, radius, glyphId) {
148 - // TODO: 148 + var dim = radius * 1.5,
149 + xlate = -dim / 2,
150 + g = elem.append('g')
151 + .attr('class', 'svgIcon hostIcon');
152 +
153 + g.append('circle').attr('r', radius);
154 +
155 + g.append('use').attr({
156 + 'xlink:href': '#' + glyphId,
157 + width: dim,
158 + height: dim,
159 + transform: sus.translate(xlate,xlate)
160 + });
161 + return g;
149 } 162 }
150 163
151 164
...@@ -154,10 +167,13 @@ ...@@ -154,10 +167,13 @@
154 167
155 angular.module('onosSvg') 168 angular.module('onosSvg')
156 .factory('IconService', ['$log', 'FnService', 'GlyphService', 169 .factory('IconService', ['$log', 'FnService', 'GlyphService',
157 - function (_$log_, _fs_, _gs_) { 170 + 'SvgUtilService',
171 +
172 + function (_$log_, _fs_, _gs_, _sus_) {
158 $log = _$log_; 173 $log = _$log_;
159 fs = _fs_; 174 fs = _fs_;
160 gs = _gs_; 175 gs = _gs_;
176 + sus = _sus_;
161 177
162 return { 178 return {
163 loadIcon: loadIcon, 179 loadIcon: loadIcon,
......
...@@ -325,19 +325,121 @@ ...@@ -325,19 +325,121 @@
325 /* Host Nodes */ 325 /* Host Nodes */
326 326
327 #ov-topo svg .node.host { 327 #ov-topo svg .node.host {
328 - stroke: #000;
329 } 328 }
330 329
331 #ov-topo svg .node.host text { 330 #ov-topo svg .node.host text {
332 - fill: #846;
333 stroke: none; 331 stroke: none;
334 font: 9pt sans-serif; 332 font: 9pt sans-serif;
335 } 333 }
334 +.light #ov-topo svg .node.host text {
335 + fill: #846;
336 +}
337 +.dark #ov-topo svg .node.host text {
338 + fill: #BB809D;
339 +}
336 340
337 -svg .node.host circle { 341 +.light svg .node.host circle {
338 stroke: #000; 342 stroke: #000;
339 fill: #edb; 343 fill: #edb;
340 } 344 }
345 +.dark svg .node.host circle {
346 + stroke: #eee;
347 + fill: #B2A180;
348 +}
349 +
350 +.light svg .node.host .svgIcon {
351 + fill: #444;
352 +}
353 +.dark svg .node.host .svgIcon {
354 + fill: #222;
355 +}
356 +
357 +/* --- Topo Links --- */
358 +
359 +#ov-topo svg .link {
360 + opacity: .9;
361 +}
362 +
363 +#ov-topo svg .link.inactive {
364 + opacity: .5;
365 + stroke-dasharray: 8 4;
366 +}
367 +
368 +#ov-topo svg .link.secondary {
369 + stroke-width: 3px;
370 +}
371 +.light #ov-topo svg .link.secondary {
372 + stroke: rgba(0,153,51,0.5);
373 +}
374 +.dark #ov-topo svg .link.secondary {
375 + stroke: rgba(121,231,158,0.5);
376 +}
377 +
378 +#ov-topo svg .link.primary {
379 + stroke-width: 4px;
380 +}
381 +.light #ov-topo svg .link.primary {
382 + stroke: #ffA300;
383 +}
384 +.dark #ov-topo svg .link.primary {
385 + stroke: #D58E0F;
386 +}
387 +
388 +.light #ov-topo svg .link.animated {
389 + stroke: #ffA300;
390 +}
391 +.dark #ov-topo svg .link.animated {
392 + stroke: #D58E0F;
393 +}
394 +
395 +#ov-topo svg .link.secondary.optical {
396 + stroke-width: 4px;
397 +}
398 +.light #ov-topo svg .link.secondary.optical {
399 + stroke: rgba(128,64,255,0.5);
400 +}
401 +.dark #ov-topo svg .link.secondary.optical {
402 + stroke: rgba(164,139,215,0.5);
403 +}
341 404
405 +#ov-topo svg .link.primary.optical {
406 + stroke-width: 6px;
407 +}
408 +.light #ov-topo svg .link.primary.optical {
409 + stroke: #74f;
410 +}
411 +.dark #ov-topo svg .link.primary.optical {
412 + stroke: #7352CD;
413 +}
414 +
415 +#ov-topo svg .link.animated.optical {
416 + stroke-width: 10px;
417 +}
418 +.light #ov-topo svg .link.animated.optical {
419 + stroke: #74f;
420 +}
421 +.dark #ov-topo svg .link.animated.optical {
422 + stroke: #7352CD;
423 +}
342 424
425 +#ov-topo svg .linkLabel rect {
426 + stroke: none;
427 +}
428 +.light #ov-topo svg .linkLabel rect {
429 + fill: #eee;
430 +}
431 +.dark #ov-topo svg .linkLabel rect {
432 + fill: #eee;
433 +}
343 434
435 +#ov-topo svg .linkLabel text {
436 + text-anchor: middle;
437 + stroke-width: 0.1;
438 + font-size: 9pt;
439 +}
440 +.light #ov-topo svg .linkLabel text {
441 + stroke: #777;
442 +}
443 +.dark #ov-topo svg .linkLabel text {
444 + stroke: #777;
445 +}
......
...@@ -198,12 +198,6 @@ ...@@ -198,12 +198,6 @@
198 return ms.loadMapInto(mapG, '*continental_us'); 198 return ms.loadMapInto(mapG, '*continental_us');
199 } 199 }
200 200
201 - // --- Force Layout --------------------------------------------------
202 -
203 - function setUpForce(xlink) {
204 - forceG = zoomLayer.append('g').attr('id', 'topo-force');
205 - tfs.initForce(forceG, xlink, svg.attr('width'), svg.attr('height'));
206 - }
207 201
208 // --- Controller Definition ----------------------------------------- 202 // --- Controller Definition -----------------------------------------
209 203
...@@ -219,8 +213,12 @@ ...@@ -219,8 +213,12 @@
219 function ($scope, _$log_, $loc, $timeout, _fs_, mast, 213 function ($scope, _$log_, $loc, $timeout, _fs_, mast,
220 _ks_, _zs_, _gs_, _ms_, _sus_, tes, _tfs_, tps, _tis_) { 214 _ks_, _zs_, _gs_, _ms_, _sus_, tes, _tfs_, tps, _tis_) {
221 var self = this, 215 var self = this,
222 - xlink = { 216 + projection,
223 - showNoDevs: showNoDevs 217 + uplink = {
218 + // provides function calls back into this space
219 + showNoDevs: showNoDevs,
220 + projection: function () { return projection; },
221 + sendEvent: tes.sendEvent
224 }; 222 };
225 223
226 $log = _$log_; 224 $log = _$log_;
...@@ -255,9 +253,15 @@ ...@@ -255,9 +253,15 @@
255 setUpDefs(); 253 setUpDefs();
256 setUpZoom(); 254 setUpZoom();
257 setUpNoDevs(); 255 setUpNoDevs();
258 - xlink.projectionPromise = setUpMap(); 256 + setUpMap().then(
259 - setUpForce(xlink); 257 + function (proj) {
260 - 258 + projection = proj;
259 + $log.debug('** We installed the projection: ', proj);
260 + }
261 + );
262 +
263 + forceG = zoomLayer.append('g').attr('id', 'topo-force');
264 + tfs.initForce(forceG, uplink, svg.attr('width'), svg.attr('height'));
261 tis.initInst(); 265 tis.initInst();
262 tps.initPanels(); 266 tps.initPanels();
263 tes.openSock(); 267 tes.openSock();
......
...@@ -34,9 +34,16 @@ ...@@ -34,9 +34,16 @@
34 updateInstance: updateInstance, 34 updateInstance: updateInstance,
35 removeInstance: removeInstance, 35 removeInstance: removeInstance,
36 addDevice: addDevice, 36 addDevice: addDevice,
37 - updateDevice: updateDevice 37 + updateDevice: updateDevice,
38 - // TODO: implement remaining handlers.. 38 + removeDevice: removeDevice,
39 + addHost: addHost,
40 + updateHost: updateHost,
41 + removeHost: removeHost,
42 + addLink: addLink,
43 + updateLink: updateLink,
44 + removeLink: removeLink
39 45
46 + // TODO: implement remaining handlers..
40 }; 47 };
41 48
42 function unknownEvent(ev) { 49 function unknownEvent(ev) {
...@@ -45,6 +52,9 @@ ...@@ -45,6 +52,9 @@
45 52
46 // === Event Handlers === 53 // === Event Handlers ===
47 54
55 + // NOTE: --- once these are done, we will collapse them into
56 + // a more compact data structure... but for now, write in full..
57 +
48 function showSummary(ev) { 58 function showSummary(ev) {
49 $log.debug(' **** Show Summary **** ', ev.payload); 59 $log.debug(' **** Show Summary **** ', ev.payload);
50 tps.showSummary(ev.payload); 60 tps.showSummary(ev.payload);
...@@ -75,6 +85,42 @@ ...@@ -75,6 +85,42 @@
75 tfs.updateDevice(ev.payload); 85 tfs.updateDevice(ev.payload);
76 } 86 }
77 87
88 + function removeDevice(ev) {
89 + $log.debug(' **** Remove Device **** ', ev.payload);
90 + tfs.removeDevice(ev.payload);
91 + }
92 +
93 + function addHost(ev) {
94 + $log.debug(' **** Add Host **** ', ev.payload);
95 + tfs.addHost(ev.payload);
96 + }
97 +
98 + function updateHost(ev) {
99 + $log.debug(' **** Update Host **** ', ev.payload);
100 + tfs.updateHost(ev.payload);
101 + }
102 +
103 + function removeHost(ev) {
104 + $log.debug(' **** Remove Host **** ', ev.payload);
105 + tfs.removeHost(ev.payload);
106 + }
107 +
108 + function addLink(ev) {
109 + $log.debug(' **** Add Link **** ', ev.payload);
110 + tfs.addLink(ev.payload);
111 + }
112 +
113 + function updateLink(ev) {
114 + $log.debug(' **** Update Link **** ', ev.payload);
115 + tfs.updateLink(ev.payload);
116 + }
117 +
118 + function removeLink(ev) {
119 + $log.debug(' **** Remove Link **** ', ev.payload);
120 + tfs.removeLink(ev.payload);
121 + }
122 +
123 +
78 // ========================== 124 // ==========================
79 125
80 var dispatcher = { 126 var dispatcher = {
...@@ -122,14 +168,9 @@ ...@@ -122,14 +168,9 @@
122 tis = _tis_; 168 tis = _tis_;
123 tfs = _tfs_; 169 tfs = _tfs_;
124 170
125 - function bindDispatcher(TopoDomElementsPassedHere) {
126 - // TODO: store refs to topo DOM elements...
127 -
128 - return dispatcher;
129 - }
130 -
131 // TODO: handle "guiSuccessor" functionality (replace host) 171 // TODO: handle "guiSuccessor" functionality (replace host)
132 // TODO: implement retry on close functionality 172 // TODO: implement retry on close functionality
173 +
133 function openSock() { 174 function openSock() {
134 wsock = wss.createWebSocket('topology', { 175 wsock = wss.createWebSocket('topology', {
135 onOpen: onWsOpen, 176 onOpen: onWsOpen,
...@@ -151,9 +192,9 @@ ...@@ -151,9 +192,9 @@
151 } 192 }
152 193
153 return { 194 return {
154 - bindDispatcher: bindDispatcher,
155 openSock: openSock, 195 openSock: openSock,
156 - closeSock: closeSock 196 + closeSock: closeSock,
197 + sendEvent: dispatcher.sendEvent
157 }; 198 };
158 }]); 199 }]);
159 }()); 200 }());
......
...@@ -23,7 +23,9 @@ ...@@ -23,7 +23,9 @@
23 'use strict'; 23 'use strict';
24 24
25 // injected refs 25 // injected refs
26 - var $log, sus, is, ts, tis, xlink; 26 + var $log, fs, sus, is, ts, tis, uplink;
27 +
28 + var icfg;
27 29
28 // configuration 30 // configuration
29 var labelConfig = { 31 var labelConfig = {
...@@ -44,6 +46,21 @@ ...@@ -44,6 +46,21 @@
44 yoff: -18 46 yoff: -18
45 }; 47 };
46 48
49 + var linkConfig = {
50 + light: {
51 + baseColor: '#666',
52 + inColor: '#66f',
53 + outColor: '#f00',
54 + },
55 + dark: {
56 + baseColor: '#666',
57 + inColor: '#66f',
58 + outColor: '#f00',
59 + },
60 + inWidth: 12,
61 + outWidth: 10
62 + };
63 +
47 // internal state 64 // internal state
48 var settings, // merged default settings and options 65 var settings, // merged default settings and options
49 force, // force layout object 66 force, // force layout object
...@@ -54,9 +71,11 @@ ...@@ -54,9 +71,11 @@
54 lookup: {}, 71 lookup: {},
55 revLinkToKey: {} 72 revLinkToKey: {}
56 }, 73 },
57 - projection, // background map projection 74 + lu = network.lookup, // shorthand
58 deviceLabelIndex = 0, // for device label cycling 75 deviceLabelIndex = 0, // for device label cycling
59 - hostLabelIndex = 0; // for host label cycling 76 + hostLabelIndex = 0, // for host label cycling
77 + showHosts = 1, // whether hosts are displayed
78 + width, height;
60 79
61 // SVG elements; 80 // SVG elements;
62 var linkG, linkLabelG, nodeG; 81 var linkG, linkLabelG, nodeG;
...@@ -99,18 +118,18 @@ ...@@ -99,18 +118,18 @@
99 var id = data.id, 118 var id = data.id,
100 d; 119 d;
101 120
102 - xlink.showNoDevs(false); 121 + uplink.showNoDevs(false);
103 122
104 // although this is an add device event, if we already have the 123 // although this is an add device event, if we already have the
105 // device, treat it as an update instead.. 124 // device, treat it as an update instead..
106 - if (network.lookup[id]) { 125 + if (lu[id]) {
107 updateDevice(data); 126 updateDevice(data);
108 return; 127 return;
109 } 128 }
110 129
111 d = createDeviceNode(data); 130 d = createDeviceNode(data);
112 network.nodes.push(d); 131 network.nodes.push(d);
113 - network.lookup[id] = d; 132 + lu[id] = d;
114 133
115 $log.debug("Created new device.. ", d.id, d.x, d.y); 134 $log.debug("Created new device.. ", d.id, d.x, d.y);
116 135
...@@ -120,7 +139,7 @@ ...@@ -120,7 +139,7 @@
120 139
121 function updateDevice(data) { 140 function updateDevice(data) {
122 var id = data.id, 141 var id = data.id,
123 - d = network.lookup[id], 142 + d = lu[id],
124 wasOnline; 143 wasOnline;
125 144
126 if (d) { 145 if (d) {
...@@ -141,26 +160,379 @@ ...@@ -141,26 +160,379 @@
141 } 160 }
142 } 161 }
143 162
163 + function removeDevice(data) {
164 + var id = data.id,
165 + d = lu[id];
166 + if (d) {
167 + removeDeviceElement(d);
168 + } else {
169 + // TODO: decide whether we want to capture logic errors
170 + //logicError('removeDevice lookup fail. ID = "' + id + '"');
171 + }
172 + }
173 +
174 + function addHost(data) {
175 + var id = data.id,
176 + d, lnk;
177 +
178 + // although this is an add host event, if we already have the
179 + // host, treat it as an update instead..
180 + if (lu[id]) {
181 + updateHost(data);
182 + return;
183 + }
184 +
185 + d = createHostNode(data);
186 + network.nodes.push(d);
187 + lu[id] = d;
188 +
189 + $log.debug("Created new host.. ", d.id, d.x, d.y);
190 +
191 + updateNodes();
192 +
193 + lnk = createHostLink(data);
194 + if (lnk) {
195 +
196 + $log.debug("Created new host-link.. ", lnk.key);
197 +
198 + d.linkData = lnk; // cache ref on its host
199 + network.links.push(lnk);
200 + lu[d.ingress] = lnk;
201 + lu[d.egress] = lnk;
202 + updateLinks();
203 + }
204 +
205 + fStart();
206 + }
207 +
208 + function updateHost(data) {
209 + var id = data.id,
210 + d = lu[id];
211 + if (d) {
212 + angular.extend(d, data);
213 + if (positionNode(d, true)) {
214 + sendUpdateMeta(d, true);
215 + }
216 + updateNodes();
217 + } else {
218 + // TODO: decide whether we want to capture logic errors
219 + //logicError('updateHost lookup fail. ID = "' + id + '"');
220 + }
221 + }
222 +
223 + function removeHost(data) {
224 + var id = data.id,
225 + d = lu[id];
226 + if (d) {
227 + removeHostElement(d, true);
228 + } else {
229 + // may have already removed host, if attached to removed device
230 + //console.warn('removeHost lookup fail. ID = "' + id + '"');
231 + }
232 + }
233 +
234 + function addLink(data) {
235 + var result = findLink(data, 'add'),
236 + bad = result.badLogic,
237 + d = result.ldata;
238 +
239 + if (bad) {
240 + //logicError(bad + ': ' + link.id);
241 + return;
242 + }
243 +
244 + if (d) {
245 + // we already have a backing store link for src/dst nodes
246 + addLinkUpdate(d, data);
247 + return;
248 + }
249 +
250 + // no backing store link yet
251 + d = createLink(data);
252 + if (d) {
253 + network.links.push(d);
254 + lu[d.key] = d;
255 + updateLinks();
256 + fStart();
257 + }
258 + }
259 +
260 + function updateLink(data) {
261 + var result = findLink(data, 'update'),
262 + bad = result.badLogic;
263 + if (bad) {
264 + //logicError(bad + ': ' + link.id);
265 + return;
266 + }
267 + result.updateWith(link);
268 + }
269 +
270 + function removeLink(data) {
271 + var result = findLink(data, 'remove'),
272 + bad = result.badLogic;
273 + if (bad) {
274 + // may have already removed link, if attached to removed device
275 + //console.warn(bad + ': ' + link.id);
276 + return;
277 + }
278 + result.removeRawLink();
279 + }
280 +
281 + // ========================
282 +
283 + function addLinkUpdate(ldata, link) {
284 + // add link event, but we already have the reverse link installed
285 + ldata.fromTarget = link;
286 + network.revLinkToKey[link.id] = ldata.key;
287 + restyleLinkElement(ldata);
288 + }
289 +
290 + function createLink(link) {
291 + var lnk = linkEndPoints(link.src, link.dst);
292 +
293 + if (!lnk) {
294 + return null;
295 + }
296 +
297 + angular.extend(lnk, {
298 + key: link.id,
299 + class: 'link',
300 + fromSource: link,
301 +
302 + // functions to aggregate dual link state
303 + type: function () {
304 + var s = lnk.fromSource,
305 + t = lnk.fromTarget;
306 + return (s && s.type) || (t && t.type) || defaultLinkType;
307 + },
308 + online: function () {
309 + var s = lnk.fromSource,
310 + t = lnk.fromTarget,
311 + both = lnk.source.online && lnk.target.online;
312 + return both && ((s && s.online) || (t && t.online));
313 + },
314 + linkWidth: function () {
315 + var s = lnk.fromSource,
316 + t = lnk.fromTarget,
317 + ws = (s && s.linkWidth) || 0,
318 + wt = (t && t.linkWidth) || 0;
319 + return Math.max(ws, wt);
320 + }
321 + });
322 + return lnk;
323 + }
324 +
325 +
326 + function makeNodeKey(d, what) {
327 + var port = what + 'Port';
328 + return d[what] + '/' + d[port];
329 + }
330 +
331 + function makeLinkKey(d, flipped) {
332 + var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
333 + two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
334 + return one + '-' + two;
335 + }
336 +
337 + var widthRatio = 1.4,
338 + linkScale = d3.scale.linear()
339 + .domain([1, 12])
340 + .range([widthRatio, 12 * widthRatio])
341 + .clamp(true);
342 +
343 + var allLinkTypes = 'direct indirect optical tunnel',
344 + defaultLinkType = 'direct';
345 +
346 + function restyleLinkElement(ldata) {
347 + // this fn's job is to look at raw links and decide what svg classes
348 + // need to be applied to the line element in the DOM
349 + var th = ts.theme(),
350 + el = ldata.el,
351 + type = ldata.type(),
352 + lw = ldata.linkWidth(),
353 + online = ldata.online();
354 +
355 + el.classed('link', true);
356 + el.classed('inactive', !online);
357 + el.classed(allLinkTypes, false);
358 + if (type) {
359 + el.classed(type, true);
360 + }
361 + el.transition()
362 + .duration(1000)
363 + .attr('stroke-width', linkScale(lw))
364 + .attr('stroke', linkConfig[th].baseColor);
365 + }
366 +
367 + function findLink(linkData, op) {
368 + var key = makeLinkKey(linkData),
369 + keyrev = makeLinkKey(linkData, 1),
370 + link = lu[key],
371 + linkRev = lu[keyrev],
372 + result = {},
373 + ldata = link || linkRev,
374 + rawLink;
375 +
376 + if (op === 'add') {
377 + if (link) {
378 + // trying to add a link that we already know about
379 + result.ldata = link;
380 + result.badLogic = 'addLink: link already added';
381 +
382 + } else if (linkRev) {
383 + // we found the reverse of the link to be added
384 + result.ldata = linkRev;
385 + if (linkRev.fromTarget) {
386 + result.badLogic = 'addLink: link already added';
387 + }
388 + }
389 + } else if (op === 'update') {
390 + if (!ldata) {
391 + result.badLogic = 'updateLink: link not found';
392 + } else {
393 + rawLink = link ? ldata.fromSource : ldata.fromTarget;
394 + result.updateWith = function (data) {
395 + angular.extend(rawLink, data);
396 + restyleLinkElement(ldata);
397 + }
398 + }
399 + } else if (op === 'remove') {
400 + if (!ldata) {
401 + result.badLogic = 'removeLink: link not found';
402 + } else {
403 + rawLink = link ? ldata.fromSource : ldata.fromTarget;
404 +
405 + if (!rawLink) {
406 + result.badLogic = 'removeLink: link not found';
407 +
408 + } else {
409 + result.removeRawLink = function () {
410 + if (link) {
411 + // remove fromSource
412 + ldata.fromSource = null;
413 + if (ldata.fromTarget) {
414 + // promote target into source position
415 + ldata.fromSource = ldata.fromTarget;
416 + ldata.fromTarget = null;
417 + ldata.key = keyrev;
418 + delete network.lookup[key];
419 + network.lookup[keyrev] = ldata;
420 + delete network.revLinkToKey[keyrev];
421 + }
422 + } else {
423 + // remove fromTarget
424 + ldata.fromTarget = null;
425 + delete network.revLinkToKey[keyrev];
426 + }
427 + if (ldata.fromSource) {
428 + restyleLinkElement(ldata);
429 + } else {
430 + removeLinkElement(ldata);
431 + }
432 + }
433 + }
434 + }
435 + }
436 + return result;
437 + }
438 +
439 +
440 + function findAttachedHosts(devId) {
441 + var hosts = [];
442 + network.nodes.forEach(function (d) {
443 + if (d.class === 'host' && d.cp.device === devId) {
444 + hosts.push(d);
445 + }
446 + });
447 + return hosts;
448 + }
449 +
450 + function findAttachedLinks(devId) {
451 + var links = [];
452 + network.links.forEach(function (d) {
453 + if (d.source.id === devId || d.target.id === devId) {
454 + links.push(d);
455 + }
456 + });
457 + return links;
458 + }
459 +
460 + function removeLinkElement(d) {
461 + var idx = fs.find(d.key, network.links, 'key'),
462 + removed;
463 + if (idx >=0) {
464 + // remove from links array
465 + removed = network.links.splice(idx, 1);
466 + // remove from lookup cache
467 + delete lu[removed[0].key];
468 + updateLinks();
469 + fResume();
470 + }
471 + }
472 +
473 + function removeHostElement(d, upd) {
474 + // first, remove associated hostLink...
475 + removeLinkElement(d.linkData);
476 +
477 + // remove hostLink bindings
478 + delete lu[d.ingress];
479 + delete lu[d.egress];
480 +
481 + // remove from lookup cache
482 + delete lu[d.id];
483 + // remove from nodes array
484 + var idx = fs.find(d.id, network.nodes);
485 + network.nodes.splice(idx, 1);
486 +
487 + // remove from SVG
488 + // NOTE: upd is false if we were called from removeDeviceElement()
489 + if (upd) {
490 + updateNodes();
491 + fResume();
492 + }
493 + }
494 +
495 + function removeDeviceElement(d) {
496 + var id = d.id;
497 + // first, remove associated hosts and links..
498 + findAttachedHosts(id).forEach(removeHostElement);
499 + findAttachedLinks(id).forEach(removeLinkElement);
500 +
501 + // remove from lookup cache
502 + delete lu[id];
503 + // remove from nodes array
504 + var idx = fs.find(id, network.nodes);
505 + network.nodes.splice(idx, 1);
506 +
507 + if (!network.nodes.length) {
508 + xlink.showNoDevs(true);
509 + }
510 +
511 + // remove from SVG
512 + updateNodes();
513 + fResume();
514 + }
515 +
516 +
144 function sendUpdateMeta(d, store) { 517 function sendUpdateMeta(d, store) {
145 var metaUi = {}, 518 var metaUi = {},
146 ll; 519 ll;
147 520
148 - // TODO: fix this code to send event to server... 521 + if (store) {
149 - //if (store) { 522 + ll = lngLatFromCoord([d.x, d.y]);
150 - // ll = geoMapProj.invert([d.x, d.y]); 523 + metaUi = {
151 - // metaUi = { 524 + x: d.x,
152 - // x: d.x, 525 + y: d.y,
153 - // y: d.y, 526 + lng: ll[0],
154 - // lng: ll[0], 527 + lat: ll[1]
155 - // lat: ll[1] 528 + };
156 - // }; 529 + }
157 - //} 530 + d.metaUi = metaUi;
158 - //d.metaUi = metaUi; 531 + uplink.sendEvent('updateMeta', {
159 - //sendMessage('updateMeta', { 532 + id: d.id,
160 - // id: d.id, 533 + 'class': d.class,
161 - // 'class': d.class, 534 + memento: metaUi
162 - // memento: metaUi 535 + });
163 - //});
164 } 536 }
165 537
166 538
...@@ -178,9 +550,13 @@ ...@@ -178,9 +550,13 @@
178 // === Devices and hosts - helper functions 550 // === Devices and hosts - helper functions
179 551
180 function coordFromLngLat(loc) { 552 function coordFromLngLat(loc) {
181 - // Our hope is that the projection is installed before we start 553 + var p = uplink.projection();
182 - // handling incoming nodes. But if not, we'll just return the origin. 554 + return p ? p([loc.lng, loc.lat]) : [0, 0];
183 - return projection ? projection([loc.lng, loc.lat]) : [0, 0]; 555 + }
556 +
557 + function lngLatFromCoord(coord) {
558 + var p = uplink.projection();
559 + return p ? p.invert([coord.x, coord.y]) : [0, 0];
184 } 560 }
185 561
186 function positionNode(node, forUpdate) { 562 function positionNode(node, forUpdate) {
...@@ -230,8 +606,8 @@ ...@@ -230,8 +606,8 @@
230 606
231 function rand() { 607 function rand() {
232 return { 608 return {
233 - x: randDim(network.view.width()), 609 + x: randDim(width),
234 - y: randDim(network.view.height()) 610 + y: randDim(height)
235 }; 611 };
236 } 612 }
237 613
...@@ -246,7 +622,7 @@ ...@@ -246,7 +622,7 @@
246 } 622 }
247 623
248 function getDevice(cp) { 624 function getDevice(cp) {
249 - var d = network.lookup[cp.device]; 625 + var d = lu[cp.device];
250 return d || rand(); 626 return d || rand();
251 } 627 }
252 628
...@@ -267,9 +643,83 @@ ...@@ -267,9 +643,83 @@
267 return node; 643 return node;
268 } 644 }
269 645
646 + function createHostNode(host) {
647 + var node = host;
648 +
649 + // Augment as needed...
650 + node.class = 'host';
651 + if (!node.type) {
652 + node.type = 'endstation';
653 + }
654 + node.svgClass = 'node host ' + node.type;
655 + positionNode(node);
656 + return node;
657 + }
658 +
659 + function createHostLink(host) {
660 + var src = host.id,
661 + dst = host.cp.device,
662 + id = host.ingress,
663 + lnk = linkEndPoints(src, dst);
664 +
665 + if (!lnk) {
666 + return null;
667 + }
668 +
669 + // Synthesize link ...
670 + angular.extend(lnk, {
671 + key: id,
672 + class: 'link',
673 +
674 + type: function () { return 'hostLink'; },
675 + online: function () {
676 + // hostlink target is edge switch
677 + return lnk.target.online;
678 + },
679 + linkWidth: function () { return 1; }
680 + });
681 + return lnk;
682 + }
683 +
684 + function linkEndPoints(srcId, dstId) {
685 + var srcNode = lu[srcId],
686 + dstNode = lu[dstId],
687 + sMiss = !srcNode ? missMsg('src', srcId) : '',
688 + dMiss = !dstNode ? missMsg('dst', dstId) : '';
689 +
690 + if (sMiss || dMiss) {
691 + $log.error('Node(s) not on map for link:\n' + sMiss + dMiss);
692 + //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
693 + return null;
694 + }
695 + return {
696 + source: srcNode,
697 + target: dstNode,
698 + x1: srcNode.x,
699 + y1: srcNode.y,
700 + x2: dstNode.x,
701 + y2: dstNode.y
702 + };
703 + }
704 +
705 + function missMsg(what, id) {
706 + return '\n[' + what + '] "' + id + '" missing ';
707 + }
708 +
270 // ========================== 709 // ==========================
271 // === Devices and hosts - D3 rendering 710 // === Devices and hosts - D3 rendering
272 711
712 + function nodeMouseOver(m) {
713 + // TODO
714 + $log.debug("TODO nodeMouseOver()...", m);
715 + }
716 +
717 + function nodeMouseOut(m) {
718 + // TODO
719 + $log.debug("TODO nodeMouseOut()...", m);
720 + }
721 +
722 +
273 // Returns the newly computed bounding box of the rectangle 723 // Returns the newly computed bounding box of the rectangle
274 function adjustRectToFitText(n) { 724 function adjustRectToFitText(n) {
275 var text = n.select('text'), 725 var text = n.select('text'),
...@@ -323,7 +773,7 @@ ...@@ -323,7 +773,7 @@
323 var label = trimLabel(deviceLabel(d)), 773 var label = trimLabel(deviceLabel(d)),
324 noLabel = !label, 774 noLabel = !label,
325 node = d.el, 775 node = d.el,
326 - dim = is.iconConfig().device.dim, 776 + dim = icfg.device.dim,
327 devCfg = deviceIconConfig, 777 devCfg = deviceIconConfig,
328 box, dx, dy; 778 box, dx, dy;
329 779
...@@ -357,16 +807,6 @@ ...@@ -357,16 +807,6 @@
357 d.el.select('text').text(label); 807 d.el.select('text').text(label);
358 } 808 }
359 809
360 - function nodeMouseOver(m) {
361 - // TODO
362 - $log.debug("TODO nodeMouseOver()...", m);
363 - }
364 -
365 - function nodeMouseOut(m) {
366 - // TODO
367 - $log.debug("TODO nodeMouseOut()...", m);
368 - }
369 -
370 function updateDeviceColors(d) { 810 function updateDeviceColors(d) {
371 if (d) { 811 if (d) {
372 setDeviceColor(d); 812 setDeviceColor(d);
...@@ -445,13 +885,14 @@ ...@@ -445,13 +885,14 @@
445 return sus.cat7().getColor(id, !online, ts.theme()); 885 return sus.cat7().getColor(id, !online, ts.theme());
446 } 886 }
447 887
448 - //============ 888 + // ==========================
449 889
450 function updateNodes() { 890 function updateNodes() {
891 + // select all the nodes in the layout:
451 node = nodeG.selectAll('.node') 892 node = nodeG.selectAll('.node')
452 .data(network.nodes, function (d) { return d.id; }); 893 .data(network.nodes, function (d) { return d.id; });
453 894
454 - // operate on existing nodes... 895 + // operate on existing nodes:
455 node.filter('.device').each(deviceExisting); 896 node.filter('.device').each(deviceExisting);
456 node.filter('.host').each(hostExisting); 897 node.filter('.host').each(hostExisting);
457 898
...@@ -470,7 +911,7 @@ ...@@ -470,7 +911,7 @@
470 .transition() 911 .transition()
471 .attr('opacity', 1); 912 .attr('opacity', 1);
472 913
473 - // augment nodes... 914 + // augment entering nodes:
474 entering.filter('.device').each(deviceEnter); 915 entering.filter('.device').each(deviceEnter);
475 entering.filter('.host').each(hostEnter); 916 entering.filter('.host').each(hostEnter);
476 917
...@@ -486,7 +927,7 @@ ...@@ -486,7 +927,7 @@
486 .style('opacity', 0) 927 .style('opacity', 0)
487 .remove(); 928 .remove();
488 929
489 - // node specific.... 930 + // exiting node specifics:
490 exiting.filter('.host').each(hostExit); 931 exiting.filter('.host').each(hostExit);
491 exiting.filter('.device').each(deviceExit); 932 exiting.filter('.device').each(deviceExit);
492 933
...@@ -539,25 +980,20 @@ ...@@ -539,25 +980,20 @@
539 } 980 }
540 981
541 function hostEnter(d) { 982 function hostEnter(d) {
542 - var node = d3.select(this); 983 + var node = d3.select(this),
543 - 984 + gid = d.type || 'unknown',
544 - //cfg = config.icons.host, 985 + rad = icfg.host.radius,
545 - //r = cfg.radius[d.type] || cfg.defaultRadius, 986 + r = d.type ? rad.withGlyph : rad.noGlyph,
546 - //textDy = r + 10, 987 + textDy = r + 10;
547 - //TODO: iid = iconGlyphUrl(d),
548 - // _dummy;
549 988
550 d.el = node; 989 d.el = node;
990 + sus.makeVisible(node, showHosts);
551 991
552 - //TODO: showHostVis(node); 992 + is.addHostIcon(node, r, gid);
553 993
554 - node.append('circle').attr('r', r);
555 - //if (iid) {
556 - //TODO: addHostIcon(node, r, iid);
557 - //}
558 node.append('text') 994 node.append('text')
559 .text(hostLabel) 995 .text(hostLabel)
560 - //.attr('dy', textDy) 996 + .attr('dy', textDy)
561 .attr('text-anchor', 'middle'); 997 .attr('text-anchor', 'middle');
562 } 998 }
563 999
...@@ -598,6 +1034,160 @@ ...@@ -598,6 +1034,160 @@
598 .style('opacity', 0.5); 1034 .style('opacity', 0.5);
599 } 1035 }
600 1036
1037 + // ==========================
1038 +
1039 + function updateLinks() {
1040 + var th = ts.theme();
1041 +
1042 + link = linkG.selectAll('.link')
1043 + .data(network.links, function (d) { return d.key; });
1044 +
1045 + // operate on existing links:
1046 + //link.each(linkExisting);
1047 +
1048 + // operate on entering links:
1049 + var entering = link.enter()
1050 + .append('line')
1051 + .attr({
1052 + x1: function (d) { return d.x1; },
1053 + y1: function (d) { return d.y1; },
1054 + x2: function (d) { return d.x2; },
1055 + y2: function (d) { return d.y2; },
1056 + stroke: linkConfig[th].inColor,
1057 + 'stroke-width': linkConfig.inWidth
1058 + });
1059 +
1060 + // augment links
1061 + entering.each(linkEntering);
1062 +
1063 + // operate on both existing and new links:
1064 + //link.each(...)
1065 +
1066 + // apply or remove labels
1067 + var labelData = getLabelData();
1068 + applyLinkLabels(labelData);
1069 +
1070 + // operate on exiting links:
1071 + link.exit()
1072 + .attr('stroke-dasharray', '3 3')
1073 + .style('opacity', 0.5)
1074 + .transition()
1075 + .duration(1500)
1076 + .attr({
1077 + 'stroke-dasharray': '3 12',
1078 + stroke: linkConfig[th].outColor,
1079 + 'stroke-width': linkConfig.outWidth
1080 + })
1081 + .style('opacity', 0.0)
1082 + .remove();
1083 +
1084 + // NOTE: invoke a single tick to force the labels to position
1085 + // onto their links.
1086 + tick();
1087 + // FIXME: this is a bug when in oblique view
1088 + // It causes the nodes to jump into "overhead" view positions, even
1089 + // though the oblique planes are still showing...
1090 + }
1091 +
1092 + // ==========================
1093 + // updateLinks - subfunctions
1094 +
1095 + function getLabelData() {
1096 + // create the backing data for showing labels..
1097 + var data = [];
1098 + link.each(function (d) {
1099 + if (d.label) {
1100 + data.push({
1101 + id: 'lab-' + d.key,
1102 + key: d.key,
1103 + label: d.label,
1104 + ldata: d
1105 + });
1106 + }
1107 + });
1108 + return data;
1109 + }
1110 +
1111 + //function linkExisting(d) { }
1112 +
1113 + function linkEntering(d) {
1114 + var link = d3.select(this);
1115 + d.el = link;
1116 + restyleLinkElement(d);
1117 + if (d.type() === 'hostLink') {
1118 + sus.makeVisible(link, showHosts);
1119 + }
1120 + }
1121 +
1122 + //function linkExiting(d) { }
1123 +
1124 + var linkLabelOffset = '0.3em';
1125 +
1126 + function applyLinkLabels(data) {
1127 + var entering;
1128 +
1129 + linkLabel = linkLabelG.selectAll('.linkLabel')
1130 + .data(data, function (d) { return d.id; });
1131 +
1132 + // for elements already existing, we need to update the text
1133 + // and adjust the rectangle size to fit
1134 + linkLabel.each(function (d) {
1135 + var el = d3.select(this),
1136 + rect = el.select('rect'),
1137 + text = el.select('text');
1138 + text.text(d.label);
1139 + rect.attr(rectAroundText(el));
1140 + });
1141 +
1142 + entering = linkLabel.enter().append('g')
1143 + .classed('linkLabel', true)
1144 + .attr('id', function (d) { return d.id; });
1145 +
1146 + entering.each(function (d) {
1147 + var el = d3.select(this),
1148 + rect,
1149 + text,
1150 + parms = {
1151 + x1: d.ldata.x1,
1152 + y1: d.ldata.y1,
1153 + x2: d.ldata.x2,
1154 + y2: d.ldata.y2
1155 + };
1156 +
1157 + d.el = el;
1158 + rect = el.append('rect');
1159 + text = el.append('text').text(d.label);
1160 + rect.attr(rectAroundText(el));
1161 + text.attr('dy', linkLabelOffset);
1162 +
1163 + el.attr('transform', transformLabel(parms));
1164 + });
1165 +
1166 + // Remove any labels that are no longer required.
1167 + linkLabel.exit().remove();
1168 + }
1169 +
1170 + function rectAroundText(el) {
1171 + var text = el.select('text'),
1172 + box = text.node().getBBox();
1173 +
1174 + // translate the bbox so that it is centered on [x,y]
1175 + box.x = -box.width / 2;
1176 + box.y = -box.height / 2;
1177 +
1178 + // add padding
1179 + box.x -= 1;
1180 + box.width += 2;
1181 + return box;
1182 + }
1183 +
1184 + function transformLabel(p) {
1185 + var dx = p.x2 - p.x1,
1186 + dy = p.y2 - p.y1,
1187 + xMid = dx/2 + p.x1,
1188 + yMid = dy/2 + p.y1;
1189 + return sus.translate(xMid, yMid);
1190 + }
601 1191
602 // ========================== 1192 // ==========================
603 // force layout tick function 1193 // force layout tick function
...@@ -620,34 +1210,31 @@ ...@@ -620,34 +1210,31 @@
620 1210
621 angular.module('ovTopo') 1211 angular.module('ovTopo')
622 .factory('TopoForceService', 1212 .factory('TopoForceService',
623 - ['$log', 'SvgUtilService', 'IconService', 'ThemeService', 1213 + ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
624 'TopoInstService', 1214 'TopoInstService',
625 1215
626 - function (_$log_, _sus_, _is_, _ts_, _tis_) { 1216 + function (_$log_, _fs_, _sus_, _is_, _ts_, _tis_) {
627 $log = _$log_; 1217 $log = _$log_;
1218 + fs = _fs_;
628 sus = _sus_; 1219 sus = _sus_;
629 is = _is_; 1220 is = _is_;
630 ts = _ts_; 1221 ts = _ts_;
631 tis = _tis_; 1222 tis = _tis_;
632 1223
1224 + icfg = is.iconConfig();
1225 +
633 // forceG is the SVG group to display the force layout in 1226 // forceG is the SVG group to display the force layout in
634 // xlink is the cross-link api from the main topo source file 1227 // xlink is the cross-link api from the main topo source file
635 // w, h are the initial dimensions of the SVG 1228 // w, h are the initial dimensions of the SVG
636 // opts are, well, optional :) 1229 // opts are, well, optional :)
637 - function initForce(forceG, _xlink_, w, h, opts) { 1230 + function initForce(forceG, _uplink_, w, h, opts) {
638 $log.debug('initForce().. WxH = ' + w + 'x' + h); 1231 $log.debug('initForce().. WxH = ' + w + 'x' + h);
639 - xlink = _xlink_; 1232 + uplink = _uplink_;
1233 + width = w;
1234 + height = h;
640 1235
641 settings = angular.extend({}, defaultSettings, opts); 1236 settings = angular.extend({}, defaultSettings, opts);
642 1237
643 - // when the projection promise is resolved, cache the projection
644 - xlink.projectionPromise.then(
645 - function (proj) {
646 - projection = proj;
647 - $log.debug('** We installed the projection: ', proj);
648 - }
649 - );
650 -
651 linkG = forceG.append('g').attr('id', 'topo-links'); 1238 linkG = forceG.append('g').attr('id', 'topo-links');
652 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels'); 1239 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
653 nodeG = forceG.append('g').attr('id', 'topo-nodes'); 1240 nodeG = forceG.append('g').attr('id', 'topo-nodes');
...@@ -672,7 +1259,9 @@ ...@@ -672,7 +1259,9 @@
672 } 1259 }
673 1260
674 function resize(dim) { 1261 function resize(dim) {
675 - force.size([dim.width, dim.height]); 1262 + width = dim.width;
1263 + height = dim.height;
1264 + force.size([width, height]);
676 // Review -- do we need to nudge the layout ? 1265 // Review -- do we need to nudge the layout ?
677 } 1266 }
678 1267
...@@ -683,7 +1272,14 @@ ...@@ -683,7 +1272,14 @@
683 updateDeviceColors: updateDeviceColors, 1272 updateDeviceColors: updateDeviceColors,
684 1273
685 addDevice: addDevice, 1274 addDevice: addDevice,
686 - updateDevice: updateDevice 1275 + updateDevice: updateDevice,
1276 + removeDevice: removeDevice,
1277 + addHost: addHost,
1278 + updateHost: updateHost,
1279 + removeHost: removeHost,
1280 + addLink: addLink,
1281 + updateLink: updateLink,
1282 + removeLink: removeLink
687 }; 1283 };
688 }]); 1284 }]);
689 }()); 1285 }());
......
...@@ -323,9 +323,11 @@ ...@@ -323,9 +323,11 @@
323 return { 323 return {
324 initInst: initInst, 324 initInst: initInst,
325 destroyInst: destroyInst, 325 destroyInst: destroyInst,
326 +
326 addInstance: addInstance, 327 addInstance: addInstance,
327 updateInstance: updateInstance, 328 updateInstance: updateInstance,
328 removeInstance: removeInstance, 329 removeInstance: removeInstance,
330 +
329 isVisible: function () { return oiBox.isVisible(); }, 331 isVisible: function () { return oiBox.isVisible(); },
330 show: function () { oiBox.show(); }, 332 show: function () { oiBox.show(); },
331 hide: function () { oiBox.hide(); } 333 hide: function () { oiBox.hide(); }
......
...@@ -34,7 +34,7 @@ describe('factory: view/topo/topoEvent.js', function() { ...@@ -34,7 +34,7 @@ describe('factory: view/topo/topoEvent.js', function() {
34 34
35 it('should define api functions', function () { 35 it('should define api functions', function () {
36 expect(fs.areFunctions(tes, [ 36 expect(fs.areFunctions(tes, [
37 - 'bindDispatcher', 'openSock', 'closeSock' 37 + 'openSock', 'closeSock', 'sendEvent'
38 ])).toBeTruthy(); 38 ])).toBeTruthy();
39 }); 39 });
40 40
......
...@@ -35,7 +35,9 @@ describe('factory: view/topo/topoForce.js', function() { ...@@ -35,7 +35,9 @@ describe('factory: view/topo/topoForce.js', function() {
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', 'resize', 'updateDeviceColors',
38 - 'addDevice', 'updateDevice' 38 + 'addDevice', 'updateDevice', 'removeDevice',
39 + 'addHost', 'updateHost', 'removeHost',
40 + 'addLink', 'updateLink', 'removeLink'
39 ])).toBeTruthy(); 41 ])).toBeTruthy();
40 }); 42 });
41 43
......
...@@ -12,6 +12,10 @@ ...@@ -12,6 +12,10 @@
12 "unknown", 12 "unknown",
13 "0E:2A:69:30:13:86" 13 "0E:2A:69:30:13:86"
14 ], 14 ],
15 + "metaUi": {
16 + "x": 800,
17 + "y": 180
18 + },
15 "props": {} 19 "props": {}
16 } 20 }
17 } 21 }
......
...@@ -12,6 +12,10 @@ ...@@ -12,6 +12,10 @@
12 "unknown", 12 "unknown",
13 "A6:96:E5:03:52:5F" 13 "A6:96:E5:03:52:5F"
14 ], 14 ],
15 + "metaUi": {
16 + "x": 520,
17 + "y": 250
18 + },
15 "props": {} 19 "props": {}
16 } 20 }
17 } 21 }
......