blob: d7261deb4b51bdbcad5f9090235dd3dc9fe2a7ea [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),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700103 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700104 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')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700155// .attr('id', 'zoomable')
156 .attr('transform', config.force.translate())
157// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700158
Simon Hunt3ab76a82014-10-22 13:07:32 -0700159// function zoomRedraw() {
160// d3.select("#zoomable").attr("transform",
161// "translate(" + d3.event.translate + ")"
162// + " scale(" + d3.event.scale + ")");
163// }
164
165 // TODO: svg.append('defs') for markers?
166
167 // TODO: move glow/blur stuff to util script
168 var glow = network.svg.append('filter')
169 .attr('x', '-50%')
170 .attr('y', '-50%')
171 .attr('width', '200%')
172 .attr('height', '200%')
173 .attr('id', 'blue-glow');
174
175 glow.append('feColorMatrix')
176 .attr('type', 'matrix')
177 .attr('values', '0 0 0 0 0 ' +
178 '0 0 0 0 0 ' +
179 '0 0 0 0 .7 ' +
180 '0 0 0 1 0 ');
181
182 glow.append('feGaussianBlur')
183 .attr('stdDeviation', 3)
184 .attr('result', 'coloredBlur');
185
186 glow.append('feMerge').selectAll('feMergeNode')
187 .data(['coloredBlur', 'SourceGraphic'])
188 .enter().append('feMergeNode')
189 .attr('in', String);
190
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700191 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700192// $('#view').on('scroll', function() {
193//
194// });
195
196
197
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700198
199 network.link = network.svg.append('g').selectAll('.link')
200 .data(network.force.links(), function(d) {return d.id})
201 .enter().append('line')
202 .attr('class', 'link');
203
204 // TODO: drag behavior
Simon Hunt3ab76a82014-10-22 13:07:32 -0700205 network.draggedThreshold = d3.scale.linear()
206 .domain([0, 0.1])
207 .range([5, 20])
208 .clamp(true);
209
210 function dragged(d) {
211 var threshold = network.draggedThreshold(network.force.alpha()),
212 dx = d.oldX - d.px,
213 dy = d.oldY - d.py;
214 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
215 d.dragged = true;
216 }
217 return d.dragged;
218 }
219
220 network.drag = d3.behavior.drag()
221 .origin(function(d) { return d; })
222 .on('dragstart', function(d) {
223 d.oldX = d.x;
224 d.oldY = d.y;
225 d.dragged = false;
226 d.fixed |= 2;
227 })
228 .on('drag', function(d) {
229 d.px = d3.event.x;
230 d.py = d3.event.y;
231 if (dragged(d)) {
232 if (!network.force.alpha()) {
233 network.force.alpha(.025);
234 }
235 }
236 })
237 .on('dragend', function(d) {
238 if (!dragged(d)) {
239 selectObject(d, this);
240 }
241 d.fixed &= ~6;
242 });
243
244 $('#view').on('click', function(e) {
245 if (!$(e.target).closest('.node').length) {
246 deselectObject();
247 }
248 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700249
250 // TODO: add drag, mouseover, mouseout behaviors
251 network.node = network.svg.selectAll('.node')
252 .data(network.force.nodes(), function(d) {return d.id})
253 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700254 .attr('class', function(d) {
255 return 'node ' + d.type;
256 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700257 .attr('transform', function(d) {
258 return translate(d.x, d.y);
259 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700260 .call(network.drag)
261 .on('mouseover', function(d) {
262 if (!selected.obj) {
263 if (network.mouseoutTimeout) {
264 clearTimeout(network.mouseoutTimeout);
265 network.mouseoutTimeout = null;
266 }
267 highlightObject(d);
268 }
269 })
270 .on('mouseout', function(d) {
271 if (!selected.obj) {
272 if (network.mouseoutTimeout) {
273 clearTimeout(network.mouseoutTimeout);
274 network.mouseoutTimeout = null;
275 }
276 network.mouseoutTimeout = setTimeout(function() {
277 highlightObject(null);
278 }, 160);
279 }
280 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700281
282 // TODO: augment stroke and fill functions
283 network.nodeRect = network.node.append('rect')
284 // TODO: css for node rects
285 .attr('rx', 5)
286 .attr('ry', 5)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700287// .attr('stroke', function(d) { return '#000'})
288// .attr('fill', function(d) { return '#ddf'})
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700289 .attr('width', 60)
290 .attr('height', 24);
291
292 network.node.each(function(d) {
293 var node = d3.select(this),
294 rect = node.select('rect');
295 var text = node.append('text')
296 .text(d.id)
297 .attr('dx', '1em')
298 .attr('dy', '2.1em');
299 });
300
301 // this function is scheduled to happen soon after the given thread ends
302 setTimeout(function() {
303 network.node.each(function(d) {
304 // for every node, recompute size, padding, etc. so text fits
305 var node = d3.select(this),
306 text = node.selectAll('text'),
307 bounds = {},
308 first = true;
309
310 // NOTE: probably unnecessary code if we only have one line.
311 });
312
313 network.numTicks = 0;
314 network.preventCollisions = false;
315 network.force.start();
316 for (var i = 0; i < config.ticksWithoutCollisions; i++) {
317 network.force.tick();
318 }
319 network.preventCollisions = true;
320 $('#view').css('visibility', 'visible');
321 });
322
323 }
324
325 function translate(x, y) {
326 return 'translate(' + x + ',' + y + ')';
327 }
328
329
330 function tick(e) {
331 network.numTicks++;
332
333 // adjust the y-coord of each node, based on y-pos constraints
334// network.nodes.forEach(function (n) {
335// var z = e.alpha * n.constraint.weight;
336// if (!isNaN(n.constraint.y)) {
337// n.y = (n.constraint.y * z + n.y * (1 - z));
338// }
339// });
340
341 network.link
342 .attr('x1', function(d) {
343 return d.source.x;
344 })
345 .attr('y1', function(d) {
346 return d.source.y;
347 })
348 .attr('x2', function(d) {
349 return d.target.x;
350 })
351 .attr('y2', function(d) {
352 return d.target.y;
353 });
354
355 network.node
356 .attr('transform', function(d) {
357 return translate(d.x, d.y);
358 });
359
360 }
361
362 // $('#docs-close').on('click', function() {
363 // deselectObject();
364 // return false;
365 // });
366
367 // $(document).on('click', '.select-object', function() {
368 // var obj = graph.data[$(this).data('name')];
369 // if (obj) {
370 // selectObject(obj);
371 // }
372 // return false;
373 // });
374
375 function selectObject(obj, el) {
376 var node;
377 if (el) {
378 node = d3.select(el);
379 } else {
380 network.node.each(function(d) {
381 if (d == obj) {
382 node = d3.select(el = this);
383 }
384 });
385 }
386 if (!node) return;
387
388 if (node.classed('selected')) {
389 deselectObject();
390 return;
391 }
392 deselectObject(false);
393
394 selected = {
395 obj : obj,
396 el : el
397 };
398
399 highlightObject(obj);
400
401 node.classed('selected', true);
402
403 // TODO animate incoming info pane
404 // resize(true);
405 // TODO: check bounds of selected node and scroll into view if needed
406 }
407
408 function deselectObject(doResize) {
409 // Review: logic of 'resize(...)' function.
410 if (doResize || typeof doResize == 'undefined') {
411 resize(false);
412 }
413 // deselect all nodes in the network...
414 network.node.classed('selected', false);
415 selected = {};
416 highlightObject(null);
417 }
418
419 function highlightObject(obj) {
420 if (obj) {
421 if (obj != highlighted) {
422 // TODO set or clear "inactive" class on nodes, based on criteria
423 network.node.classed('inactive', function(d) {
424 // return (obj !== d &&
425 // d.relation(obj.id));
426 return (obj !== d);
427 });
428 // TODO: same with links
429 network.link.classed('inactive', function(d) {
430 return (obj !== d.source && obj !== d.target);
431 });
432 }
433 highlighted = obj;
434 } else {
435 if (highlighted) {
436 // clear the inactive flag (no longer suppressed visually)
437 network.node.classed('inactive', false);
438 network.link.classed('inactive', false);
439 }
440 highlighted = null;
441
442 }
443 }
444
445 function resize(showDetails) {
446 console.log("resize() called...");
447
448 var $details = $('#details');
449
450 if (typeof showDetails == 'boolean') {
451 var showingDetails = showDetails;
452 // TODO: invoke $details.show() or $details.hide()...
453 // $details[showingDetails ? 'show' : 'hide']();
454 }
455
456 view.height = window.innerHeight - config.mastHeight;
457 view.width = window.innerWidth;
458 $('#view')
459 .css('height', view.height + 'px')
460 .css('width', view.width + 'px');
461
462 network.forceWidth = view.width - config.force.marginLR;
463 network.forceHeight = view.height - config.force.marginTB;
464 }
465
466 // ======================================================================
467 // register with the UI framework
468
469 api.addView('network', {
470 load: loadNetworkView
471 });
472
473
474}(ONOS));
475