blob: 5587e42af3761a7bd5a86a600c9cdb2f2c31cec7 [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 = {
Simon Hunt68ae6652014-10-22 13:58:07 -070013 layering: false,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070014 jsonUrl: 'network.json',
Simon Hunt68ae6652014-10-22 13:58:07 -070015 iconUrl: {
16 pkt: 'pkt.png',
17 opt: 'opt.png'
18 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070019 mastHeight: 32,
20 force: {
Simon Hunt68ae6652014-10-22 13:58:07 -070021 linkDistance: 240,
22 linkStrength: 0.8,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070023 charge: -400,
24 ticksWithoutCollisions: 50,
25 marginLR: 20,
26 marginTB: 20,
27 translate: function() {
28 return 'translate(' +
29 config.force.marginLR + ',' +
30 config.force.marginTB + ')';
31 }
32 },
33 labels: {
34 padLR: 3,
35 padTB: 2,
36 marginLR: 3,
37 marginTB: 2
38 },
39 constraints: {
40 ypos: {
41 pkt: 0.3,
42 opt: 0.7
43 }
44 }
45 },
46 view = {},
47 network = {},
48 selected = {},
49 highlighted = null;
50
51
52 function loadNetworkView() {
53 // Hey, here I am, calling something on the ONOS api:
54 api.printTime();
55
56 resize();
57
58 d3.json(config.jsonUrl, function (err, data) {
59 if (err) {
60 alert('Oops! Error reading JSON...\n\n' +
61 'URL: ' + jsonUrl + '\n\n' +
62 'Error: ' + err.message);
63 return;
64 }
65 console.log("here is the JSON data...");
66 console.log(data);
67
68 network.data = data;
69 drawNetwork();
70 });
71
72 $(document).on('click', '.select-object', function() {
73 // when any object of class "select-object" is clicked...
74 // TODO: get a reference to the object via lookup...
75 var obj = network.lookup[$(this).data('id')];
76 if (obj) {
77 selectObject(obj);
78 }
79 // stop propagation of event (I think) ...
80 return false;
81 });
82
83 $(window).on('resize', resize);
84 }
85
86
87 // ========================================================
88
89 function drawNetwork() {
90 $('#view').empty();
91
92 prepareNodesAndLinks();
93 createLayout();
94 console.log("\n\nHere is the augmented network object...");
95 console.warn(network);
96 }
97
98 function prepareNodesAndLinks() {
99 network.lookup = {};
100 network.nodes = [];
101 network.links = [];
102
103 var nw = network.forceWidth,
104 nh = network.forceHeight;
105
106 network.data.nodes.forEach(function(n) {
107 var ypc = yPosConstraintForNode(n),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700108 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700109 iy = ypc * nh,
110 node = {
111 id: n.id,
112 type: n.type,
113 status: n.status,
114 x: ix,
115 y: iy,
116 constraint: {
117 weight: 0.7,
118 y: iy
119 }
120 };
121 network.lookup[n.id] = node;
122 network.nodes.push(node);
123 });
124
125 function yPosConstraintForNode(n) {
126 return config.constraints.ypos[n.type] || 0.5;
127 }
128
129
130 network.data.links.forEach(function(n) {
131 var src = network.lookup[n.src],
132 dst = network.lookup[n.dst],
133 id = src.id + "~" + dst.id;
134
135 var link = {
136 id: id,
137 source: src,
138 target: dst,
139 strength: config.force.linkStrength
140 };
141 network.links.push(link);
142 });
143 }
144
145 function createLayout() {
146
147 network.force = d3.layout.force()
148 .nodes(network.nodes)
149 .links(network.links)
150 .linkStrength(function(d) { return d.strength; })
151 .size([network.forceWidth, network.forceHeight])
152 .linkDistance(config.force.linkDistance)
153 .charge(config.force.charge)
154 .on('tick', tick);
155
156 network.svg = d3.select('#view').append('svg')
157 .attr('width', view.width)
158 .attr('height', view.height)
159 .append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700160// .attr('id', 'zoomable')
161 .attr('transform', config.force.translate())
162// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700163
Simon Hunt3ab76a82014-10-22 13:07:32 -0700164// function zoomRedraw() {
165// d3.select("#zoomable").attr("transform",
166// "translate(" + d3.event.translate + ")"
167// + " scale(" + d3.event.scale + ")");
168// }
169
170 // TODO: svg.append('defs') for markers?
171
172 // TODO: move glow/blur stuff to util script
173 var glow = network.svg.append('filter')
174 .attr('x', '-50%')
175 .attr('y', '-50%')
176 .attr('width', '200%')
177 .attr('height', '200%')
178 .attr('id', 'blue-glow');
179
180 glow.append('feColorMatrix')
181 .attr('type', 'matrix')
182 .attr('values', '0 0 0 0 0 ' +
183 '0 0 0 0 0 ' +
184 '0 0 0 0 .7 ' +
185 '0 0 0 1 0 ');
186
187 glow.append('feGaussianBlur')
188 .attr('stdDeviation', 3)
189 .attr('result', 'coloredBlur');
190
191 glow.append('feMerge').selectAll('feMergeNode')
192 .data(['coloredBlur', 'SourceGraphic'])
193 .enter().append('feMergeNode')
194 .attr('in', String);
195
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700196 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700197// $('#view').on('scroll', function() {
198//
199// });
200
201
202
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700203
204 network.link = network.svg.append('g').selectAll('.link')
205 .data(network.force.links(), function(d) {return d.id})
206 .enter().append('line')
207 .attr('class', 'link');
208
209 // TODO: drag behavior
Simon Hunt3ab76a82014-10-22 13:07:32 -0700210 network.draggedThreshold = d3.scale.linear()
211 .domain([0, 0.1])
212 .range([5, 20])
213 .clamp(true);
214
215 function dragged(d) {
216 var threshold = network.draggedThreshold(network.force.alpha()),
217 dx = d.oldX - d.px,
218 dy = d.oldY - d.py;
219 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
220 d.dragged = true;
221 }
222 return d.dragged;
223 }
224
225 network.drag = d3.behavior.drag()
226 .origin(function(d) { return d; })
227 .on('dragstart', function(d) {
228 d.oldX = d.x;
229 d.oldY = d.y;
230 d.dragged = false;
231 d.fixed |= 2;
232 })
233 .on('drag', function(d) {
234 d.px = d3.event.x;
235 d.py = d3.event.y;
236 if (dragged(d)) {
237 if (!network.force.alpha()) {
238 network.force.alpha(.025);
239 }
240 }
241 })
242 .on('dragend', function(d) {
243 if (!dragged(d)) {
244 selectObject(d, this);
245 }
246 d.fixed &= ~6;
247 });
248
249 $('#view').on('click', function(e) {
250 if (!$(e.target).closest('.node').length) {
251 deselectObject();
252 }
253 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700254
255 // TODO: add drag, mouseover, mouseout behaviors
256 network.node = network.svg.selectAll('.node')
257 .data(network.force.nodes(), function(d) {return d.id})
258 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700259 .attr('class', function(d) {
260 return 'node ' + d.type;
261 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700262 .attr('transform', function(d) {
263 return translate(d.x, d.y);
264 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700265 .call(network.drag)
266 .on('mouseover', function(d) {
267 if (!selected.obj) {
268 if (network.mouseoutTimeout) {
269 clearTimeout(network.mouseoutTimeout);
270 network.mouseoutTimeout = null;
271 }
272 highlightObject(d);
273 }
274 })
275 .on('mouseout', function(d) {
276 if (!selected.obj) {
277 if (network.mouseoutTimeout) {
278 clearTimeout(network.mouseoutTimeout);
279 network.mouseoutTimeout = null;
280 }
281 network.mouseoutTimeout = setTimeout(function() {
282 highlightObject(null);
283 }, 160);
284 }
285 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700286
287 // TODO: augment stroke and fill functions
288 network.nodeRect = network.node.append('rect')
289 // TODO: css for node rects
290 .attr('rx', 5)
291 .attr('ry', 5)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700292// .attr('stroke', function(d) { return '#000'})
293// .attr('fill', function(d) { return '#ddf'})
Simon Hunt68ae6652014-10-22 13:58:07 -0700294 .attr('width', 126)
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700295 .attr('height', 24);
296
297 network.node.each(function(d) {
298 var node = d3.select(this),
Simon Hunt68ae6652014-10-22 13:58:07 -0700299 rect = node.select('rect'),
300 img = node.append('svg:image')
301 .attr('x', -9)
302 .attr('y', -12)
303 .attr('width', 32)
304 .attr('height', 32)
305 .attr('xlink:href', iconUrl(d)),
306 text = node.append('text')
307 .text(d.id)
308 .attr('dx', '1.0em')
309 .attr('dy', '1.8em'),
310 dummy;
311
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700312 });
313
314 // this function is scheduled to happen soon after the given thread ends
315 setTimeout(function() {
316 network.node.each(function(d) {
317 // for every node, recompute size, padding, etc. so text fits
318 var node = d3.select(this),
319 text = node.selectAll('text'),
320 bounds = {},
321 first = true;
322
323 // NOTE: probably unnecessary code if we only have one line.
324 });
325
326 network.numTicks = 0;
327 network.preventCollisions = false;
328 network.force.start();
329 for (var i = 0; i < config.ticksWithoutCollisions; i++) {
330 network.force.tick();
331 }
332 network.preventCollisions = true;
333 $('#view').css('visibility', 'visible');
334 });
335
336 }
337
Simon Hunt68ae6652014-10-22 13:58:07 -0700338 function iconUrl(d) {
339 return config.iconUrl[d.type];
340 }
341
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700342 function translate(x, y) {
343 return 'translate(' + x + ',' + y + ')';
344 }
345
346
347 function tick(e) {
348 network.numTicks++;
349
Simon Hunt68ae6652014-10-22 13:58:07 -0700350 if (config.layering) {
351 // adjust the y-coord of each node, based on y-pos constraints
352 network.nodes.forEach(function (n) {
353 var z = e.alpha * n.constraint.weight;
354 if (!isNaN(n.constraint.y)) {
355 n.y = (n.constraint.y * z + n.y * (1 - z));
356 }
357 });
358 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700359
360 network.link
361 .attr('x1', function(d) {
362 return d.source.x;
363 })
364 .attr('y1', function(d) {
365 return d.source.y;
366 })
367 .attr('x2', function(d) {
368 return d.target.x;
369 })
370 .attr('y2', function(d) {
371 return d.target.y;
372 });
373
374 network.node
375 .attr('transform', function(d) {
376 return translate(d.x, d.y);
377 });
378
379 }
380
381 // $('#docs-close').on('click', function() {
382 // deselectObject();
383 // return false;
384 // });
385
386 // $(document).on('click', '.select-object', function() {
387 // var obj = graph.data[$(this).data('name')];
388 // if (obj) {
389 // selectObject(obj);
390 // }
391 // return false;
392 // });
393
394 function selectObject(obj, el) {
395 var node;
396 if (el) {
397 node = d3.select(el);
398 } else {
399 network.node.each(function(d) {
400 if (d == obj) {
401 node = d3.select(el = this);
402 }
403 });
404 }
405 if (!node) return;
406
407 if (node.classed('selected')) {
408 deselectObject();
409 return;
410 }
411 deselectObject(false);
412
413 selected = {
414 obj : obj,
415 el : el
416 };
417
418 highlightObject(obj);
419
420 node.classed('selected', true);
421
422 // TODO animate incoming info pane
423 // resize(true);
424 // TODO: check bounds of selected node and scroll into view if needed
425 }
426
427 function deselectObject(doResize) {
428 // Review: logic of 'resize(...)' function.
429 if (doResize || typeof doResize == 'undefined') {
430 resize(false);
431 }
432 // deselect all nodes in the network...
433 network.node.classed('selected', false);
434 selected = {};
435 highlightObject(null);
436 }
437
438 function highlightObject(obj) {
439 if (obj) {
440 if (obj != highlighted) {
441 // TODO set or clear "inactive" class on nodes, based on criteria
442 network.node.classed('inactive', function(d) {
443 // return (obj !== d &&
444 // d.relation(obj.id));
445 return (obj !== d);
446 });
447 // TODO: same with links
448 network.link.classed('inactive', function(d) {
449 return (obj !== d.source && obj !== d.target);
450 });
451 }
452 highlighted = obj;
453 } else {
454 if (highlighted) {
455 // clear the inactive flag (no longer suppressed visually)
456 network.node.classed('inactive', false);
457 network.link.classed('inactive', false);
458 }
459 highlighted = null;
460
461 }
462 }
463
464 function resize(showDetails) {
465 console.log("resize() called...");
466
467 var $details = $('#details');
468
469 if (typeof showDetails == 'boolean') {
470 var showingDetails = showDetails;
471 // TODO: invoke $details.show() or $details.hide()...
472 // $details[showingDetails ? 'show' : 'hide']();
473 }
474
475 view.height = window.innerHeight - config.mastHeight;
476 view.width = window.innerWidth;
477 $('#view')
478 .css('height', view.height + 'px')
479 .css('width', view.width + 'px');
480
481 network.forceWidth = view.width - config.force.marginLR;
482 network.forceHeight = view.height - config.force.marginTB;
483 }
484
485 // ======================================================================
486 // register with the UI framework
487
488 api.addView('network', {
489 load: loadNetworkView
490 });
491
492
493}(ONOS));
494