blob: 81105dc917c65c34fc403a09c78d8552f78e8e9f [file] [log] [blame]
Simon Hunt0b05d4a2014-10-21 21:50:15 -07001/*
2 ONOS network topology viewer - PoC version 1.0
3
4 @author Simon Hunt
5 */
6
7(function (onos) {
8 'use strict';
9
10 var api = onos.api;
11
12 var config = {
13 jsonUrl: 'network.json',
14 mastHeight: 32,
15 force: {
16 linkDistance: 150,
17 linkStrength: 0.9,
18 charge: -400,
19 ticksWithoutCollisions: 50,
20 marginLR: 20,
21 marginTB: 20,
22 translate: function() {
23 return 'translate(' +
24 config.force.marginLR + ',' +
25 config.force.marginTB + ')';
26 }
27 },
28 labels: {
29 padLR: 3,
30 padTB: 2,
31 marginLR: 3,
32 marginTB: 2
33 },
34 constraints: {
35 ypos: {
36 pkt: 0.3,
37 opt: 0.7
38 }
39 }
40 },
41 view = {},
42 network = {},
43 selected = {},
44 highlighted = null;
45
46
47 function loadNetworkView() {
48 // Hey, here I am, calling something on the ONOS api:
49 api.printTime();
50
51 resize();
52
53 d3.json(config.jsonUrl, function (err, data) {
54 if (err) {
55 alert('Oops! Error reading JSON...\n\n' +
56 'URL: ' + jsonUrl + '\n\n' +
57 'Error: ' + err.message);
58 return;
59 }
60 console.log("here is the JSON data...");
61 console.log(data);
62
63 network.data = data;
64 drawNetwork();
65 });
66
67 $(document).on('click', '.select-object', function() {
68 // when any object of class "select-object" is clicked...
69 // TODO: get a reference to the object via lookup...
70 var obj = network.lookup[$(this).data('id')];
71 if (obj) {
72 selectObject(obj);
73 }
74 // stop propagation of event (I think) ...
75 return false;
76 });
77
78 $(window).on('resize', resize);
79 }
80
81
82 // ========================================================
83
84 function drawNetwork() {
85 $('#view').empty();
86
87 prepareNodesAndLinks();
88 createLayout();
89 console.log("\n\nHere is the augmented network object...");
90 console.warn(network);
91 }
92
93 function prepareNodesAndLinks() {
94 network.lookup = {};
95 network.nodes = [];
96 network.links = [];
97
98 var nw = network.forceWidth,
99 nh = network.forceHeight;
100
101 network.data.nodes.forEach(function(n) {
102 var ypc = yPosConstraintForNode(n),
103 ix = Math.random() * 0.8 * nw + 0.1 * nw,
104 iy = ypc * nh,
105 node = {
106 id: n.id,
107 type: n.type,
108 status: n.status,
109 x: ix,
110 y: iy,
111 constraint: {
112 weight: 0.7,
113 y: iy
114 }
115 };
116 network.lookup[n.id] = node;
117 network.nodes.push(node);
118 });
119
120 function yPosConstraintForNode(n) {
121 return config.constraints.ypos[n.type] || 0.5;
122 }
123
124
125 network.data.links.forEach(function(n) {
126 var src = network.lookup[n.src],
127 dst = network.lookup[n.dst],
128 id = src.id + "~" + dst.id;
129
130 var link = {
131 id: id,
132 source: src,
133 target: dst,
134 strength: config.force.linkStrength
135 };
136 network.links.push(link);
137 });
138 }
139
140 function createLayout() {
141
142 network.force = d3.layout.force()
143 .nodes(network.nodes)
144 .links(network.links)
145 .linkStrength(function(d) { return d.strength; })
146 .size([network.forceWidth, network.forceHeight])
147 .linkDistance(config.force.linkDistance)
148 .charge(config.force.charge)
149 .on('tick', tick);
150
151 network.svg = d3.select('#view').append('svg')
152 .attr('width', view.width)
153 .attr('height', view.height)
154 .append('g')
155 .attr('transform', config.force.translate());
156
157 // TODO: svg.append('defs')
158 // TODO: glow/blur stuff
159 // TODO: legend (and auto adjust on scroll)
160
161 network.link = network.svg.append('g').selectAll('.link')
162 .data(network.force.links(), function(d) {return d.id})
163 .enter().append('line')
164 .attr('class', 'link');
165
166 // TODO: drag behavior
167 // TODO: closest node deselect
168
169 // TODO: add drag, mouseover, mouseout behaviors
170 network.node = network.svg.selectAll('.node')
171 .data(network.force.nodes(), function(d) {return d.id})
172 .enter().append('g')
173 .attr('class', 'node')
174 .attr('transform', function(d) {
175 return translate(d.x, d.y);
176 })
177 // .call(network.drag)
178 .on('mouseover', function(d) {})
179 .on('mouseout', function(d) {});
180
181 // TODO: augment stroke and fill functions
182 network.nodeRect = network.node.append('rect')
183 // TODO: css for node rects
184 .attr('rx', 5)
185 .attr('ry', 5)
186 .attr('stroke', function(d) { return '#000'})
187 .attr('fill', function(d) { return '#ddf'})
188 .attr('width', 60)
189 .attr('height', 24);
190
191 network.node.each(function(d) {
192 var node = d3.select(this),
193 rect = node.select('rect');
194 var text = node.append('text')
195 .text(d.id)
196 .attr('dx', '1em')
197 .attr('dy', '2.1em');
198 });
199
200 // this function is scheduled to happen soon after the given thread ends
201 setTimeout(function() {
202 network.node.each(function(d) {
203 // for every node, recompute size, padding, etc. so text fits
204 var node = d3.select(this),
205 text = node.selectAll('text'),
206 bounds = {},
207 first = true;
208
209 // NOTE: probably unnecessary code if we only have one line.
210 });
211
212 network.numTicks = 0;
213 network.preventCollisions = false;
214 network.force.start();
215 for (var i = 0; i < config.ticksWithoutCollisions; i++) {
216 network.force.tick();
217 }
218 network.preventCollisions = true;
219 $('#view').css('visibility', 'visible');
220 });
221
222 }
223
224 function translate(x, y) {
225 return 'translate(' + x + ',' + y + ')';
226 }
227
228
229 function tick(e) {
230 network.numTicks++;
231
232 // adjust the y-coord of each node, based on y-pos constraints
233// network.nodes.forEach(function (n) {
234// var z = e.alpha * n.constraint.weight;
235// if (!isNaN(n.constraint.y)) {
236// n.y = (n.constraint.y * z + n.y * (1 - z));
237// }
238// });
239
240 network.link
241 .attr('x1', function(d) {
242 return d.source.x;
243 })
244 .attr('y1', function(d) {
245 return d.source.y;
246 })
247 .attr('x2', function(d) {
248 return d.target.x;
249 })
250 .attr('y2', function(d) {
251 return d.target.y;
252 });
253
254 network.node
255 .attr('transform', function(d) {
256 return translate(d.x, d.y);
257 });
258
259 }
260
261 // $('#docs-close').on('click', function() {
262 // deselectObject();
263 // return false;
264 // });
265
266 // $(document).on('click', '.select-object', function() {
267 // var obj = graph.data[$(this).data('name')];
268 // if (obj) {
269 // selectObject(obj);
270 // }
271 // return false;
272 // });
273
274 function selectObject(obj, el) {
275 var node;
276 if (el) {
277 node = d3.select(el);
278 } else {
279 network.node.each(function(d) {
280 if (d == obj) {
281 node = d3.select(el = this);
282 }
283 });
284 }
285 if (!node) return;
286
287 if (node.classed('selected')) {
288 deselectObject();
289 return;
290 }
291 deselectObject(false);
292
293 selected = {
294 obj : obj,
295 el : el
296 };
297
298 highlightObject(obj);
299
300 node.classed('selected', true);
301
302 // TODO animate incoming info pane
303 // resize(true);
304 // TODO: check bounds of selected node and scroll into view if needed
305 }
306
307 function deselectObject(doResize) {
308 // Review: logic of 'resize(...)' function.
309 if (doResize || typeof doResize == 'undefined') {
310 resize(false);
311 }
312 // deselect all nodes in the network...
313 network.node.classed('selected', false);
314 selected = {};
315 highlightObject(null);
316 }
317
318 function highlightObject(obj) {
319 if (obj) {
320 if (obj != highlighted) {
321 // TODO set or clear "inactive" class on nodes, based on criteria
322 network.node.classed('inactive', function(d) {
323 // return (obj !== d &&
324 // d.relation(obj.id));
325 return (obj !== d);
326 });
327 // TODO: same with links
328 network.link.classed('inactive', function(d) {
329 return (obj !== d.source && obj !== d.target);
330 });
331 }
332 highlighted = obj;
333 } else {
334 if (highlighted) {
335 // clear the inactive flag (no longer suppressed visually)
336 network.node.classed('inactive', false);
337 network.link.classed('inactive', false);
338 }
339 highlighted = null;
340
341 }
342 }
343
344 function resize(showDetails) {
345 console.log("resize() called...");
346
347 var $details = $('#details');
348
349 if (typeof showDetails == 'boolean') {
350 var showingDetails = showDetails;
351 // TODO: invoke $details.show() or $details.hide()...
352 // $details[showingDetails ? 'show' : 'hide']();
353 }
354
355 view.height = window.innerHeight - config.mastHeight;
356 view.width = window.innerWidth;
357 $('#view')
358 .css('height', view.height + 'px')
359 .css('width', view.width + 'px');
360
361 network.forceWidth = view.width - config.force.marginLR;
362 network.forceHeight = view.height - config.force.marginTB;
363 }
364
365 // ======================================================================
366 // register with the UI framework
367
368 api.addView('network', {
369 load: loadNetworkView
370 });
371
372
373}(ONOS));
374