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