Simon Hunt
Committed by Gerrit Code Review

GUI -- Migrating the add/update device functionality to the Topology View. (WIP)

- still a lot of work to do.

Change-Id: I0453b7e2ec20a8a8149fd9d6440a13a3d43fbfd6
...@@ -70,3 +70,7 @@ svg.embeddedIcon .icon rect { ...@@ -70,3 +70,7 @@ svg.embeddedIcon .icon rect {
70 .dark svg.embeddedIcon .icon rect { 70 .dark svg.embeddedIcon .icon rect {
71 stroke: #ccc; 71 stroke: #ccc;
72 } 72 }
73 +
74 +svg .svgIcon {
75 + fill-rule: evenodd;
76 +}
......
...@@ -26,8 +26,8 @@ ...@@ -26,8 +26,8 @@
26 cornerSize = vboxSize / 10, 26 cornerSize = vboxSize / 10,
27 viewBox = '0 0 ' + vboxSize + ' ' + vboxSize; 27 viewBox = '0 0 ' + vboxSize + ' ' + vboxSize;
28 28
29 - // maps icon id to the glyph id it uses. 29 + // Maps icon ID to the glyph ID it uses.
30 - // note: icon id maps to a CSS class for styling that icon 30 + // NOTE: icon ID maps to a CSS class for styling that icon
31 var glyphMapping = { 31 var glyphMapping = {
32 deviceOnline: 'checkMark', 32 deviceOnline: 'checkMark',
33 deviceOffline: 'xMark', 33 deviceOffline: 'xMark',
...@@ -36,6 +36,8 @@ ...@@ -36,6 +36,8 @@
36 tableColSortNone: '-' 36 tableColSortNone: '-'
37 }; 37 };
38 38
39 +
40 +
39 function ensureIconLibDefs() { 41 function ensureIconLibDefs() {
40 var body = d3.select('body'), 42 var body = d3.select('body'),
41 svg = body.select('svg#IconLibDefs'), 43 svg = body.select('svg#IconLibDefs'),
...@@ -48,13 +50,6 @@ ...@@ -48,13 +50,6 @@
48 return svg.select('defs'); 50 return svg.select('defs');
49 } 51 }
50 52
51 - angular.module('onosSvg')
52 - .factory('IconService', ['$log', 'FnService', 'GlyphService',
53 - function (_$log_, _fs_, _gs_) {
54 - $log = _$log_;
55 - fs = _fs_;
56 - gs = _gs_;
57 -
58 // div is a D3 selection of the <DIV> element into which icon should load 53 // div is a D3 selection of the <DIV> element into which icon should load
59 // iconCls is the CSS class used to identify the icon 54 // iconCls is the CSS class used to identify the icon
60 // size is dimension of icon in pixels. Defaults to 20. 55 // size is dimension of icon in pixels. Defaults to 20.
...@@ -103,9 +98,73 @@ ...@@ -103,9 +98,73 @@
103 loadIcon(div, iconCls, size, true); 98 loadIcon(div, iconCls, size, true);
104 } 99 }
105 100
101 +
102 + // configuration for device and host icons in the topology view
103 + var config = {
104 + device: {
105 + dim: 36,
106 + rx: 4
107 + },
108 + host: {
109 + radius: {
110 + noGlyph: 9,
111 + withGlyph: 14
112 + },
113 + glyphed: {
114 + endstation: 1,
115 + bgpSpeaker: 1,
116 + router: 1
117 + }
118 + }
119 + };
120 +
121 +
122 + // Adds a device icon to the specified element, using the given glyph.
123 + // Returns the D3 selection of the icon.
124 + function addDeviceIcon(elem, glyphId) {
125 + var cfg = config.device,
126 + g = elem.append('g')
127 + .attr('class', 'svgIcon deviceIcon');
128 +
129 + g.append('rect').attr({
130 + x: 0,
131 + y: 0,
132 + rx: cfg.rx,
133 + width: cfg.dim,
134 + height: cfg.dim
135 + });
136 +
137 + g.append('use').attr({
138 + 'xlink:href': '#' + glyphId,
139 + width: cfg.dim,
140 + height: cfg.dim
141 + });
142 +
143 + g.dim = cfg.dim;
144 + return g;
145 + }
146 +
147 + function addHostIcon(elem, glyphId) {
148 + // TODO:
149 + }
150 +
151 +
152 + // =========================
153 + // === DEFINE THE MODULE
154 +
155 + angular.module('onosSvg')
156 + .factory('IconService', ['$log', 'FnService', 'GlyphService',
157 + function (_$log_, _fs_, _gs_) {
158 + $log = _$log_;
159 + fs = _fs_;
160 + gs = _gs_;
161 +
106 return { 162 return {
107 loadIcon: loadIcon, 163 loadIcon: loadIcon,
108 - loadEmbeddedIcon: loadEmbeddedIcon 164 + loadEmbeddedIcon: loadEmbeddedIcon,
165 + addDeviceIcon: addDeviceIcon,
166 + addHostIcon: addHostIcon,
167 + iconConfig: function () { return config; }
109 }; 168 };
110 }]); 169 }]);
111 170
......
...@@ -22,28 +22,25 @@ ...@@ -22,28 +22,25 @@
22 The Map Service provides a simple API for loading geographical maps into 22 The Map Service provides a simple API for loading geographical maps into
23 an SVG layer. For example, as a background to the Topology View. 23 an SVG layer. For example, as a background to the Topology View.
24 24
25 - e.g. var ok = MapService.loadMapInto(svgLayer, '*continental-us'); 25 + e.g. var promise = MapService.loadMapInto(svgLayer, '*continental-us');
26 26
27 The Map Service makes use of the GeoDataService to load the required data 27 The Map Service makes use of the GeoDataService to load the required data
28 from the server and to create the appropriate geographical projection. 28 from the server and to create the appropriate geographical projection.
29 29
30 + A promise is returned to the caller, which is resolved with the
31 + map projection once created.
30 */ 32 */
31 33
32 (function () { 34 (function () {
33 'use strict'; 35 'use strict';
34 36
35 // injected references 37 // injected references
36 - var $log, fs, gds; 38 + var $log, $q, fs, gds;
37 -
38 - angular.module('onosSvg')
39 - .factory('MapService', ['$log', 'FnService', 'GeoDataService',
40 - function (_$log_, _fs_, _gds_) {
41 - $log = _$log_;
42 - fs = _fs_;
43 - gds = _gds_;
44 39
45 function loadMapInto(mapLayer, id, opts) { 40 function loadMapInto(mapLayer, id, opts) {
46 - var promise = gds.fetchTopoData(id); 41 + var promise = gds.fetchTopoData(id),
42 + deferredProjection = $q.defer();
43 +
47 if (!promise) { 44 if (!promise) {
48 $log.warn('Failed to load map: ' + id); 45 $log.warn('Failed to load map: ' + id);
49 return false; 46 return false;
...@@ -52,15 +49,26 @@ ...@@ -52,15 +49,26 @@
52 promise.then(function () { 49 promise.then(function () {
53 var gen = gds.createPathGenerator(promise.topodata, opts); 50 var gen = gds.createPathGenerator(promise.topodata, opts);
54 51
52 + deferredProjection.resolve(gen.settings.projection);
53 +
55 mapLayer.selectAll('path') 54 mapLayer.selectAll('path')
56 .data(gen.geodata.features) 55 .data(gen.geodata.features)
57 .enter() 56 .enter()
58 .append('path') 57 .append('path')
59 .attr('d', gen.pathgen); 58 .attr('d', gen.pathgen);
60 }); 59 });
61 - return true; 60 + return deferredProjection.promise;
62 } 61 }
63 62
63 +
64 + angular.module('onosSvg')
65 + .factory('MapService', ['$log', '$q', 'FnService', 'GeoDataService',
66 + function (_$log_, _$q_, _fs_, _gds_) {
67 + $log = _$log_;
68 + $q = _$q_;
69 + fs = _fs_;
70 + gds = _gds_;
71 +
64 return { 72 return {
65 loadMapInto: loadMapInto 73 loadMapInto: loadMapInto
66 }; 74 };
......
...@@ -240,13 +240,18 @@ ...@@ -240,13 +240,18 @@
240 el.style('visibility', (b ? 'visible' : 'hidden')); 240 el.style('visibility', (b ? 'visible' : 'hidden'));
241 } 241 }
242 242
243 + function safeId(s) {
244 + return s.replace(/[^a-z0-9]/gi, '-');
245 + }
246 +
243 return { 247 return {
244 createDragBehavior: createDragBehavior, 248 createDragBehavior: createDragBehavior,
245 loadGlow: loadGlow, 249 loadGlow: loadGlow,
246 cat7: cat7, 250 cat7: cat7,
247 translate: translate, 251 translate: translate,
248 stripPx: stripPx, 252 stripPx: stripPx,
249 - makeVisible: makeVisible 253 + makeVisible: makeVisible,
254 + safeId: safeId
250 }; 255 };
251 }]); 256 }]);
252 }()); 257 }());
......
...@@ -245,3 +245,94 @@ ...@@ -245,3 +245,94 @@
245 /* TODO: add blue glow */ 245 /* TODO: add blue glow */
246 /*filter: url(#blue-glow);*/ 246 /*filter: url(#blue-glow);*/
247 } 247 }
248 +
249 +
250 +/* --- Topo Nodes --- */
251 +
252 +#ov-topo svg .node {
253 + cursor: pointer;
254 +}
255 +
256 +#ov-topo svg .node.selected rect,
257 +#ov-topo svg .node.selected circle {
258 + fill: #f90;
259 + /* TODO: add blue glow filter */
260 + /*filter: url(#blue-glow);*/
261 +}
262 +
263 +#ov-topo svg .node text {
264 + pointer-events: none;
265 +}
266 +
267 +/* Device Nodes */
268 +
269 +#ov-topo svg .node.device {
270 +}
271 +
272 +#ov-topo svg .node.device rect {
273 + stroke-width: 1.5;
274 +}
275 +
276 +#ov-topo svg .node.device.fixed rect {
277 + stroke-width: 1.5;
278 + stroke: #ccc;
279 +}
280 +
281 +/* note: device is offline without the 'online' class */
282 +#ov-topo svg .node.device {
283 + fill: #777;
284 +}
285 +
286 +#ov-topo svg .node.device.online {
287 + fill: #6e7fa3;
288 +}
289 +
290 +/* note: device is offline without the 'online' class */
291 +#ov-topo svg .node.device text {
292 + fill: #bbb;
293 + font: 10pt sans-serif;
294 +}
295 +
296 +#ov-topo svg .node.device.online text {
297 + fill: white;
298 +}
299 +
300 +#ov-topo svg .node.device .svgIcon rect {
301 + fill: #aaa;
302 +}
303 +#ov-topo svg .node.device .svgIcon use {
304 + fill: #777;
305 +}
306 +#ov-topo svg .node.device.selected .svgIcon rect {
307 + fill: #f90;
308 +}
309 +#ov-topo svg .node.device.online .svgIcon rect {
310 + fill: #ccc;
311 +}
312 +#ov-topo svg .node.device.online .svgIcon use {
313 + fill: #000;
314 +}
315 +#ov-topo svg .node.device.online.selected .svgIcon rect {
316 + fill: #f90;
317 +}
318 +
319 +
320 +/* Host Nodes */
321 +
322 +#ov-topo svg .node.host {
323 + stroke: #000;
324 +}
325 +
326 +#ov-topo svg .node.host text {
327 + fill: #846;
328 + stroke: none;
329 + font: 9pt sans-serif;
330 +}
331 +
332 +svg .node.host circle {
333 + stroke: #000;
334 + fill: #edb;
335 +}
336 +
337 +
338 +
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
28 ]; 28 ];
29 29
30 // references to injected services etc. 30 // references to injected services etc.
31 - var $log, fs, ks, zs, gs, ms, sus, tfs; 31 + var $log, fs, ks, zs, gs, ms, sus, tfs, tis;
32 32
33 // DOM elements 33 // DOM elements
34 var ovtopo, svg, defs, zoomLayer, mapG, forceG, noDevsLayer; 34 var ovtopo, svg, defs, zoomLayer, mapG, forceG, noDevsLayer;
...@@ -41,20 +41,61 @@ ...@@ -41,20 +41,61 @@
41 // --- Short Cut Keys ------------------------------------------------ 41 // --- Short Cut Keys ------------------------------------------------
42 42
43 var keyBindings = { 43 var keyBindings = {
44 - W: [logWarning, '(temp) log a warning'], 44 + //O: [toggleSummary, 'Toggle ONOS summary pane'],
45 - E: [logError, '(temp) log an error'], 45 + I: [toggleInstances, 'Toggle ONOS instances pane'],
46 - R: [resetZoom, 'Reset pan / zoom'] 46 + //D: [toggleDetails, 'Disable / enable details pane'],
47 +
48 + //H: [toggleHosts, 'Toggle host visibility'],
49 + //M: [toggleOffline, 'Toggle offline visibility'],
50 + //B: [toggleBg, 'Toggle background image'],
51 + //P: togglePorts,
52 +
53 + //X: [toggleNodeLock, 'Lock / unlock node positions'],
54 + //Z: [toggleOblique, 'Toggle oblique view (Experimental)'],
55 + L: [cycleLabels, 'Cycle device labels'],
56 + //U: [unpin, 'Unpin node (hover mouse over)'],
57 + R: [resetZoom, 'Reset pan / zoom'],
58 +
59 + //V: [showRelatedIntentsAction, 'Show all related intents'],
60 + //rightArrow: [showNextIntentAction, 'Show next related intent'],
61 + //leftArrow: [showPrevIntentAction, 'Show previous related intent'],
62 + //W: [showSelectedIntentTrafficAction, 'Monitor traffic of selected intent'],
63 + //A: [showAllTrafficAction, 'Monitor all traffic'],
64 + //F: [showDeviceLinkFlowsAction, 'Show device link flows'],
65 +
66 + //E: [equalizeMasters, 'Equalize mastership roles'],
67 +
68 + //esc: handleEscape,
69 +
70 + _helpFormat: [
71 + ['O', 'I', 'D', '-', 'H', 'M', 'B', 'P' ],
72 + ['X', 'Z', 'L', 'U', 'R' ],
73 + ['V', 'rightArrow', 'leftArrow', 'W', 'A', 'F', '-', 'E' ]
74 + ]
75 +
47 }; 76 };
48 77
49 - // ----------------- 78 + // mouse gestures
50 - // these functions are necessarily temporary examples.... 79 + var gestures = [
51 - function logWarning() { 80 + ['click', 'Select the item and show details'],
52 - $log.warn('You have been warned!'); 81 + ['shift-click', 'Toggle selection state'],
82 + ['drag', 'Reposition (and pin) device / host'],
83 + ['cmd-scroll', 'Zoom in / out'],
84 + ['cmd-drag', 'Pan']
85 + ];
86 +
87 + function toggleInstances() {
88 + if (tis.isVisible()) {
89 + tis.hide();
90 + } else {
91 + tis.show();
53 } 92 }
54 - function logError() { 93 + tfs.updateDeviceColors();
55 - $log.error('You are erroneous!'); 94 + }
95 +
96 + function cycleLabels() {
97 + $log.debug('Cycle Labels.....');
56 } 98 }
57 - // -----------------
58 99
59 function resetZoom() { 100 function resetZoom() {
60 zoomer.reset(); 101 zoomer.reset();
...@@ -83,7 +124,6 @@ ...@@ -83,7 +124,6 @@
83 function zoomCallback() { 124 function zoomCallback() {
84 var tr = zoomer.translate(), 125 var tr = zoomer.translate(),
85 sc = zoomer.scale(); 126 sc = zoomer.scale();
86 - $log.log('ZOOM: translate = ' + tr + ', scale = ' + sc);
87 127
88 // keep the map lines constant width while zooming 128 // keep the map lines constant width while zooming
89 mapG.style('stroke-width', (2.0 / sc) + 'px'); 129 mapG.style('stroke-width', (2.0 / sc) + 'px');
...@@ -150,16 +190,19 @@ ...@@ -150,16 +190,19 @@
150 190
151 function setUpMap() { 191 function setUpMap() {
152 mapG = zoomLayer.append('g').attr('id', 'topo-map'); 192 mapG = zoomLayer.append('g').attr('id', 'topo-map');
153 - //ms.loadMapInto(map, '*continental_us', {mapFillScale:0.5}); 193 +
154 - ms.loadMapInto(mapG, '*continental_us');
155 //showCallibrationPoints(); 194 //showCallibrationPoints();
195 + //return ms.loadMapInto(map, '*continental_us', {mapFillScale:0.5});
196 +
197 + // returns a promise for the projection...
198 + return ms.loadMapInto(mapG, '*continental_us');
156 } 199 }
157 200
158 // --- Force Layout -------------------------------------------------- 201 // --- Force Layout --------------------------------------------------
159 202
160 - function setUpForce() { 203 + function setUpForce(xlink) {
161 forceG = zoomLayer.append('g').attr('id', 'topo-force'); 204 forceG = zoomLayer.append('g').attr('id', 'topo-force');
162 - tfs.initForce(forceG, svg.attr('width'), svg.attr('height')); 205 + tfs.initForce(forceG, xlink, svg.attr('width'), svg.attr('height'));
163 } 206 }
164 207
165 // --- Controller Definition ----------------------------------------- 208 // --- Controller Definition -----------------------------------------
...@@ -174,8 +217,12 @@ ...@@ -174,8 +217,12 @@
174 'TopoInstService', 217 'TopoInstService',
175 218
176 function ($scope, _$log_, $loc, $timeout, _fs_, mast, 219 function ($scope, _$log_, $loc, $timeout, _fs_, mast,
177 - _ks_, _zs_, _gs_, _ms_, _sus_, tes, _tfs_, tps, tis) { 220 + _ks_, _zs_, _gs_, _ms_, _sus_, tes, _tfs_, tps, _tis_) {
178 - var self = this; 221 + var self = this,
222 + xlink = {
223 + showNoDevs: showNoDevs
224 + };
225 +
179 $log = _$log_; 226 $log = _$log_;
180 fs = _fs_; 227 fs = _fs_;
181 ks = _ks_; 228 ks = _ks_;
...@@ -184,6 +231,7 @@ ...@@ -184,6 +231,7 @@
184 ms = _ms_; 231 ms = _ms_;
185 sus = _sus_; 232 sus = _sus_;
186 tfs = _tfs_; 233 tfs = _tfs_;
234 + tis = _tis_;
187 235
188 self.notifyResize = function () { 236 self.notifyResize = function () {
189 svgResized(fs.windowSize(mast.mastHeight())); 237 svgResized(fs.windowSize(mast.mastHeight()));
...@@ -207,8 +255,8 @@ ...@@ -207,8 +255,8 @@
207 setUpDefs(); 255 setUpDefs();
208 setUpZoom(); 256 setUpZoom();
209 setUpNoDevs(); 257 setUpNoDevs();
210 - setUpMap(); 258 + xlink.projectionPromise = setUpMap();
211 - setUpForce(); 259 + setUpForce(xlink);
212 260
213 tis.initInst(); 261 tis.initInst();
214 tps.initPanels(); 262 tps.initPanels();
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
23 'use strict'; 23 'use strict';
24 24
25 // injected refs 25 // injected refs
26 - var $log, wss, wes, tps, tis; 26 + var $log, wss, wes, tps, tis, tfs;
27 27
28 // internal state 28 // internal state
29 var wsock; 29 var wsock;
...@@ -32,7 +32,9 @@ ...@@ -32,7 +32,9 @@
32 showSummary: showSummary, 32 showSummary: showSummary,
33 addInstance: addInstance, 33 addInstance: addInstance,
34 updateInstance: updateInstance, 34 updateInstance: updateInstance,
35 - removeInstance: removeInstance 35 + removeInstance: removeInstance,
36 + addDevice: addDevice,
37 + updateDevice: updateDevice
36 // TODO: implement remaining handlers.. 38 // TODO: implement remaining handlers..
37 39
38 }; 40 };
...@@ -63,6 +65,16 @@ ...@@ -63,6 +65,16 @@
63 tis.removeInstance(ev.payload); 65 tis.removeInstance(ev.payload);
64 } 66 }
65 67
68 + function addDevice(ev) {
69 + $log.debug(' **** Add Device **** ', ev.payload);
70 + tfs.addDevice(ev.payload);
71 + }
72 +
73 + function updateDevice(ev) {
74 + $log.debug(' **** Update Device **** ', ev.payload);
75 + tfs.updateDevice(ev.payload);
76 + }
77 +
66 // ========================== 78 // ==========================
67 79
68 var dispatcher = { 80 var dispatcher = {
...@@ -100,14 +112,15 @@ ...@@ -100,14 +112,15 @@
100 angular.module('ovTopo') 112 angular.module('ovTopo')
101 .factory('TopoEventService', 113 .factory('TopoEventService',
102 ['$log', '$location', 'WebSocketService', 'WsEventService', 114 ['$log', '$location', 'WebSocketService', 'WsEventService',
103 - 'TopoPanelService', 'TopoInstService', 115 + 'TopoPanelService', 'TopoInstService', 'TopoForceService',
104 116
105 - function (_$log_, $loc, _wss_, _wes_, _tps_, _tis_) { 117 + function (_$log_, $loc, _wss_, _wes_, _tps_, _tis_, _tfs_) {
106 $log = _$log_; 118 $log = _$log_;
107 wss = _wss_; 119 wss = _wss_;
108 wes = _wes_; 120 wes = _wes_;
109 tps = _tps_; 121 tps = _tps_;
110 tis = _tis_; 122 tis = _tis_;
123 + tfs = _tfs_;
111 124
112 function bindDispatcher(TopoDomElementsPassedHere) { 125 function bindDispatcher(TopoDomElementsPassedHere) {
113 // TODO: store refs to topo DOM elements... 126 // TODO: store refs to topo DOM elements...
......
...@@ -23,10 +23,29 @@ ...@@ -23,10 +23,29 @@
23 'use strict'; 23 'use strict';
24 24
25 // injected refs 25 // injected refs
26 - var $log, sus; 26 + var $log, sus, is, ts, tis, xlink;
27 +
28 + // configuration
29 + var labelConfig = {
30 + imgPad: 16,
31 + padLR: 4,
32 + padTB: 3,
33 + marginLR: 3,
34 + marginTB: 2,
35 + port: {
36 + gap: 3,
37 + width: 18,
38 + height: 14
39 + }
40 + };
41 +
42 + var deviceIconConfig = {
43 + xoff: -20,
44 + yoff: -18
45 + };
27 46
28 // internal state 47 // internal state
29 - var settings, 48 + var settings, // merged default settings and options
30 force, // force layout object 49 force, // force layout object
31 drag, // drag behavior handler 50 drag, // drag behavior handler
32 network = { 51 network = {
...@@ -34,8 +53,10 @@ ...@@ -34,8 +53,10 @@
34 links: [], 53 links: [],
35 lookup: {}, 54 lookup: {},
36 revLinkToKey: {} 55 revLinkToKey: {}
37 - }; 56 + },
38 - 57 + projection, // background map projection
58 + deviceLabelIndex = 0, // for device label cycling
59 + hostLabelIndex = 0; // for host label cycling
39 60
40 // SVG elements; 61 // SVG elements;
41 var linkG, linkLabelG, nodeG; 62 var linkG, linkLabelG, nodeG;
...@@ -71,12 +92,517 @@ ...@@ -71,12 +92,517 @@
71 }; 92 };
72 93
73 94
95 + // ==========================
96 + // === EVENT HANDLERS
97 +
98 + function addDevice(data) {
99 + var id = data.id,
100 + d;
101 +
102 + xlink.showNoDevs(false);
103 +
104 + // although this is an add device event, if we already have the
105 + // device, treat it as an update instead..
106 + if (network.lookup[id]) {
107 + updateDevice(data);
108 + return;
109 + }
110 +
111 + d = createDeviceNode(data);
112 + network.nodes.push(d);
113 + network.lookup[id] = d;
114 +
115 + $log.debug("Created new device.. ", d.id, d.x, d.y);
116 +
117 + updateNodes();
118 + fStart();
119 + }
120 +
121 + function updateDevice(data) {
122 + var id = data.id,
123 + d = network.lookup[id],
124 + wasOnline;
125 +
126 + if (d) {
127 + wasOnline = d.online;
128 + angular.extend(d, data);
129 + if (positionNode(d, true)) {
130 + sendUpdateMeta(d, true);
131 + }
132 + updateNodes();
133 + if (wasOnline !== d.online) {
134 + // TODO: re-instate link update, and offline visibility
135 + //findAttachedLinks(d.id).forEach(restyleLinkElement);
136 + //updateOfflineVisibility(d);
137 + }
138 + } else {
139 + // TODO: decide whether we want to capture logic errors
140 + //logicError('updateDevice lookup fail. ID = "' + id + '"');
141 + }
142 + }
143 +
144 + function sendUpdateMeta(d, store) {
145 + var metaUi = {},
146 + ll;
147 +
148 + // TODO: fix this code to send event to server...
149 + //if (store) {
150 + // ll = geoMapProj.invert([d.x, d.y]);
151 + // metaUi = {
152 + // x: d.x,
153 + // y: d.y,
154 + // lng: ll[0],
155 + // lat: ll[1]
156 + // };
157 + //}
158 + //d.metaUi = metaUi;
159 + //sendMessage('updateMeta', {
160 + // id: d.id,
161 + // 'class': d.class,
162 + // memento: metaUi
163 + //});
164 + }
165 +
166 +
167 + function updateNodes() {
168 + $log.debug('TODO updateNodes()...');
169 + // TODO...
170 + }
171 +
172 + function fStart() {
173 + $log.debug('TODO fStart()...');
174 + // TODO...
175 + }
176 +
177 + function fResume() {
178 + $log.debug('TODO fResume()...');
179 + // TODO...
180 + }
181 +
182 + // ==========================
183 + // === Devices and hosts - helper functions
184 +
185 + function coordFromLngLat(loc) {
186 + // Our hope is that the projection is installed before we start
187 + // handling incoming nodes. But if not, we'll just return the origin.
188 + return projection ? projection([loc.lng, loc.lat]) : [0, 0];
189 + }
190 +
191 + function positionNode(node, forUpdate) {
192 + var meta = node.metaUi,
193 + x = meta && meta.x,
194 + y = meta && meta.y,
195 + xy;
196 +
197 + // If we have [x,y] already, use that...
198 + if (x && y) {
199 + node.fixed = true;
200 + node.px = node.x = x;
201 + node.py = node.y = y;
202 + return;
203 + }
204 +
205 + var location = node.location,
206 + coord;
207 +
208 + if (location && location.type === 'latlng') {
209 + coord = coordFromLngLat(location);
210 + node.fixed = true;
211 + node.px = node.x = coord[0];
212 + node.py = node.y = coord[1];
213 + return true;
214 + }
215 +
216 + // if this is a node update (not a node add).. skip randomizer
217 + if (forUpdate) {
218 + return;
219 + }
220 +
221 + // Note: Placing incoming unpinned nodes at exactly the same point
222 + // (center of the view) causes them to explode outwards when
223 + // the force layout kicks in. So, we spread them out a bit
224 + // initially, to provide a more serene layout convergence.
225 + // Additionally, if the node is a host, we place it near
226 + // the device it is connected to.
227 +
228 + function spread(s) {
229 + return Math.floor((Math.random() * s) - s/2);
230 + }
231 +
232 + function randDim(dim) {
233 + return dim / 2 + spread(dim * 0.7071);
234 + }
235 +
236 + function rand() {
237 + return {
238 + x: randDim(network.view.width()),
239 + y: randDim(network.view.height())
240 + };
241 + }
242 +
243 + function near(node) {
244 + var min = 12,
245 + dx = spread(12),
246 + dy = spread(12);
247 + return {
248 + x: node.x + min + dx,
249 + y: node.y + min + dy
250 + };
251 + }
252 +
253 + function getDevice(cp) {
254 + var d = network.lookup[cp.device];
255 + return d || rand();
256 + }
257 +
258 + xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
259 + angular.extend(node, xy);
260 + }
261 +
262 + function createDeviceNode(device) {
263 + // start with the object as is
264 + var node = device,
265 + type = device.type,
266 + svgCls = type ? 'node device ' + type : 'node device';
267 +
268 + // Augment as needed...
269 + node.class = 'device';
270 + node.svgClass = device.online ? svgCls + ' online' : svgCls;
271 + positionNode(node);
272 + return node;
273 + }
274 +
275 + // ==========================
276 + // === Devices and hosts - D3 rendering
277 +
278 + // Returns the newly computed bounding box of the rectangle
279 + function adjustRectToFitText(n) {
280 + var text = n.select('text'),
281 + box = text.node().getBBox(),
282 + lab = labelConfig;
283 +
284 + text.attr('text-anchor', 'middle')
285 + .attr('y', '-0.8em')
286 + .attr('x', lab.imgPad/2);
287 +
288 + // translate the bbox so that it is centered on [x,y]
289 + box.x = -box.width / 2;
290 + box.y = -box.height / 2;
291 +
292 + // add padding
293 + box.x -= (lab.padLR + lab.imgPad/2);
294 + box.width += lab.padLR * 2 + lab.imgPad;
295 + box.y -= lab.padTB;
296 + box.height += lab.padTB * 2;
297 +
298 + return box;
299 + }
300 +
301 + function mkSvgClass(d) {
302 + return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
303 + }
304 +
305 + function hostLabel(d) {
306 + var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
307 + return d.labels[idx];
308 + }
309 + function deviceLabel(d) {
310 + var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
311 + return d.labels[idx];
312 + }
313 + function trimLabel(label) {
314 + return (label && label.trim()) || '';
315 + }
316 +
317 + function emptyBox() {
318 + return {
319 + x: -2,
320 + y: -2,
321 + width: 4,
322 + height: 4
323 + };
324 + }
325 +
326 +
327 + function updateDeviceLabel(d) {
328 + var label = trimLabel(deviceLabel(d)),
329 + noLabel = !label,
330 + node = d.el,
331 + dim = is.iconConfig().device.dim,
332 + devCfg = deviceIconConfig,
333 + box, dx, dy;
334 +
335 + node.select('text')
336 + .text(label)
337 + .style('opacity', 0)
338 + .transition()
339 + .style('opacity', 1);
340 +
341 + if (noLabel) {
342 + box = emptyBox();
343 + dx = -dim/2;
344 + dy = -dim/2;
345 + } else {
346 + box = adjustRectToFitText(node);
347 + dx = box.x + devCfg.xoff;
348 + dy = box.y + devCfg.yoff;
349 + }
350 +
351 + node.select('rect')
352 + .transition()
353 + .attr(box);
354 +
355 + node.select('g.deviceIcon')
356 + .transition()
357 + .attr('transform', sus.translate(dx, dy));
358 + }
359 +
360 + function updateHostLabel(d) {
361 + var label = trimLabel(hostLabel(d));
362 + d.el.select('text').text(label);
363 + }
364 +
365 + function nodeMouseOver(m) {
366 + // TODO
367 + $log.debug("TODO nodeMouseOver()...", m);
368 + }
369 +
370 + function nodeMouseOut(m) {
371 + // TODO
372 + $log.debug("TODO nodeMouseOut()...", m);
373 + }
374 +
375 + function updateDeviceColors(d) {
376 + if (d) {
377 + setDeviceColor(d);
378 + } else {
379 + node.filter('.device').each(function (d) {
380 + setDeviceColor(d);
381 + });
382 + }
383 + }
384 +
385 + var dCol = {
386 + black: '#000',
387 + paleblue: '#acf',
388 + offwhite: '#ddd',
389 + midgrey: '#888',
390 + lightgrey: '#bbb',
391 + orange: '#f90'
392 + };
393 +
394 + // note: these are the device icon colors without affinity
395 + var dColTheme = {
396 + light: {
397 + online: {
398 + glyph: dCol.black,
399 + rect: dCol.paleblue
400 + },
401 + offline: {
402 + glyph: dCol.midgrey,
403 + rect: dCol.lightgrey
404 + }
405 + },
406 + // TODO: theme
407 + dark: {
408 + online: {
409 + glyph: dCol.black,
410 + rect: dCol.paleblue
411 + },
412 + offline: {
413 + glyph: dCol.midgrey,
414 + rect: dCol.lightgrey
415 + }
416 + }
417 + };
418 +
419 + function devBaseColor(d) {
420 + var o = d.online ? 'online' : 'offline';
421 + return dColTheme[ts.theme()][o];
422 + }
423 +
424 + function setDeviceColor(d) {
425 + var o = d.online,
426 + s = d.el.classed('selected'),
427 + c = devBaseColor(d),
428 + a = instColor(d.master, o),
429 + g, r,
430 + icon = d.el.select('g.deviceIcon');
431 +
432 + if (s) {
433 + g = c.glyph;
434 + r = dCol.orange;
435 + } else if (tis.isVisible()) {
436 + g = o ? a : c.glyph;
437 + r = o ? dCol.offwhite : a;
438 + } else {
439 + g = c.glyph;
440 + r = c.rect;
441 + }
442 +
443 + icon.select('use')
444 + .style('fill', g);
445 + icon.select('rect')
446 + .style('fill', r);
447 + }
448 +
449 + function instColor(id, online) {
450 + return sus.cat7().getColor(id, !online, ts.theme());
451 + }
452 +
453 + //============
454 +
455 + function updateNodes() {
456 + node = nodeG.selectAll('.node')
457 + .data(network.nodes, function (d) { return d.id; });
458 +
459 + // operate on existing nodes...
460 + node.filter('.device').each(function (d) {
461 + var node = d.el;
462 + node.classed('online', d.online);
463 + updateDeviceLabel(d);
464 + positionNode(d, true);
465 + });
466 +
467 + node.filter('.host').each(function (d) {
468 + updateHostLabel(d);
469 + positionNode(d, true);
470 + });
471 +
472 + // operate on entering nodes:
473 + var entering = node.enter()
474 + .append('g')
475 + .attr({
476 + id: function (d) { return sus.safeId(d.id); },
477 + class: mkSvgClass,
478 + transform: function (d) { return sus.translate(d.x, d.y); },
479 + opacity: 0
480 + })
481 + .call(drag)
482 + .on('mouseover', nodeMouseOver)
483 + .on('mouseout', nodeMouseOut)
484 + .transition()
485 + .attr('opacity', 1);
486 +
487 + // augment device nodes...
488 + entering.filter('.device').each(function (d) {
489 + var node = d3.select(this),
490 + glyphId = d.type || 'unknown',
491 + label = trimLabel(deviceLabel(d)),
492 + noLabel = !label,
493 + box, dx, dy, icon;
494 +
495 + // provide ref to element from backing data....
496 + d.el = node;
497 +
498 + node.append('rect').attr({ rx: 5, ry: 5 });
499 + node.append('text').text(label).attr('dy', '1.1em');
500 + box = adjustRectToFitText(node);
501 + node.select('rect').attr(box);
502 +
503 + icon = is.addDeviceIcon(node, glyphId);
504 + d.iconDim = icon.dim;
505 +
506 + if (noLabel) {
507 + dx = -icon.dim/2;
508 + dy = -icon.dim/2;
509 + } else {
510 + box = adjustRectToFitText(node);
511 + dx = box.x + iconConfig.xoff;
512 + dy = box.y + iconConfig.yoff;
513 + }
514 +
515 + icon.attr('transform', sus.translate(dx, dy));
516 + });
517 +
518 + // augment host nodes...
519 + entering.filter('.host').each(function (d) {
520 + var node = d3.select(this),
521 + cfg = config.icons.host,
522 + r = cfg.radius[d.type] || cfg.defaultRadius,
523 + textDy = r + 10,
524 + //TODO: iid = iconGlyphUrl(d),
525 + _dummy;
526 +
527 + // provide ref to element from backing data....
528 + d.el = node;
529 +
530 + //TODO: showHostVis(node);
531 +
532 + node.append('circle').attr('r', r);
533 + if (iid) {
534 + //TODO: addHostIcon(node, r, iid);
535 + }
536 + node.append('text')
537 + .text(hostLabel)
538 + .attr('dy', textDy)
539 + .attr('text-anchor', 'middle');
540 + });
541 +
542 + // operate on both existing and new nodes, if necessary
543 + updateDeviceColors();
544 +
545 + // operate on exiting nodes:
546 + // Note that the node is removed after 2 seconds.
547 + // Sub element animations should be shorter than 2 seconds.
548 + var exiting = node.exit()
549 + .transition()
550 + .duration(2000)
551 + .style('opacity', 0)
552 + .remove();
553 +
554 + // host node exits....
555 + exiting.filter('.host').each(function (d) {
556 + var node = d.el;
557 + node.select('use')
558 + .style('opacity', 0.5)
559 + .transition()
560 + .duration(800)
561 + .style('opacity', 0);
562 +
563 + node.select('text')
564 + .style('opacity', 0.5)
565 + .transition()
566 + .duration(800)
567 + .style('opacity', 0);
568 +
569 + node.select('circle')
570 + .style('stroke-fill', '#555')
571 + .style('fill', '#888')
572 + .style('opacity', 0.5)
573 + .transition()
574 + .duration(1500)
575 + .attr('r', 0);
576 + });
577 +
578 + // device node exits....
579 + exiting.filter('.device').each(function (d) {
580 + var node = d.el;
581 + node.select('use')
582 + .style('opacity', 0.5)
583 + .transition()
584 + .duration(800)
585 + .style('opacity', 0);
586 +
587 + node.selectAll('rect')
588 + .style('stroke-fill', '#555')
589 + .style('fill', '#888')
590 + .style('opacity', 0.5);
591 + });
592 + fResume();
593 + }
594 +
595 +
596 + // ==========================
74 // force layout tick function 597 // force layout tick function
75 function tick() { 598 function tick() {
76 599
77 } 600 }
78 601
79 602
603 + // ==========================
604 + // === MOUSE GESTURE HANDLERS
605 +
80 function selectCb() { } 606 function selectCb() { }
81 function atDragEnd() {} 607 function atDragEnd() {}
82 function dragEnabled() {} 608 function dragEnabled() {}
...@@ -84,23 +610,38 @@ ...@@ -84,23 +610,38 @@
84 610
85 611
86 // ========================== 612 // ==========================
613 + // Module definition
87 614
88 angular.module('ovTopo') 615 angular.module('ovTopo')
89 .factory('TopoForceService', 616 .factory('TopoForceService',
90 - ['$log', 'SvgUtilService', 617 + ['$log', 'SvgUtilService', 'IconService', 'ThemeService',
618 + 'TopoInstService',
91 619
92 - function (_$log_, _sus_) { 620 + function (_$log_, _sus_, _is_, _ts_, _tis_) {
93 $log = _$log_; 621 $log = _$log_;
94 sus = _sus_; 622 sus = _sus_;
623 + is = _is_;
624 + ts = _ts_;
625 + tis = _tis_;
95 626
96 // forceG is the SVG group to display the force layout in 627 // forceG is the SVG group to display the force layout in
628 + // xlink is the cross-link api from the main topo source file
97 // w, h are the initial dimensions of the SVG 629 // w, h are the initial dimensions of the SVG
98 // opts are, well, optional :) 630 // opts are, well, optional :)
99 - function initForce(forceG, w, h, opts) { 631 + function initForce(forceG, _xlink_, w, h, opts) {
100 $log.debug('initForce().. WxH = ' + w + 'x' + h); 632 $log.debug('initForce().. WxH = ' + w + 'x' + h);
633 + xlink = _xlink_;
101 634
102 settings = angular.extend({}, defaultSettings, opts); 635 settings = angular.extend({}, defaultSettings, opts);
103 636
637 + // when the projection promise is resolved, cache the projection
638 + xlink.projectionPromise.then(
639 + function (proj) {
640 + projection = proj;
641 + $log.debug('** We installed the projection: ', proj);
642 + }
643 + );
644 +
104 linkG = forceG.append('g').attr('id', 'topo-links'); 645 linkG = forceG.append('g').attr('id', 'topo-links');
105 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels'); 646 linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
106 nodeG = forceG.append('g').attr('id', 'topo-nodes'); 647 nodeG = forceG.append('g').attr('id', 'topo-nodes');
...@@ -127,12 +668,16 @@ ...@@ -127,12 +668,16 @@
127 function resize(dim) { 668 function resize(dim) {
128 force.size([dim.width, dim.height]); 669 force.size([dim.width, dim.height]);
129 // Review -- do we need to nudge the layout ? 670 // Review -- do we need to nudge the layout ?
130 -
131 } 671 }
132 672
133 return { 673 return {
134 initForce: initForce, 674 initForce: initForce,
135 - resize: resize 675 + resize: resize,
676 +
677 + updateDeviceColors: updateDeviceColors,
678 +
679 + addDevice: addDevice,
680 + updateDevice: updateDevice
136 }; 681 };
137 }]); 682 }]);
138 }()); 683 }());
......
...@@ -325,7 +325,10 @@ ...@@ -325,7 +325,10 @@
325 destroyInst: destroyInst, 325 destroyInst: destroyInst,
326 addInstance: addInstance, 326 addInstance: addInstance,
327 updateInstance: updateInstance, 327 updateInstance: updateInstance,
328 - removeInstance: removeInstance 328 + removeInstance: removeInstance,
329 + isVisible: function () { return oiBox.isVisible(); },
330 + show: function () { oiBox.show(); },
331 + hide: function () { oiBox.hide(); }
329 }; 332 };
330 }]); 333 }]);
331 }()); 334 }());
......