blob: 80d11b7c77a38d478d0a0682f7f3f8c225355739 [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' +
Simon Huntae968a62014-10-22 14:54:41 -070062 'URL: ' + config.jsonUrl + '\n\n' +
Simon Hunt0b05d4a2014-10-21 21:50:15 -070063 '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 Huntae968a62014-10-22 14:54:41 -0700161 .attr('transform', config.force.translate());
Simon Hunt3ab76a82014-10-22 13:07:32 -0700162// .attr('id', 'zoomable')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700163// .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);
Simon Hunt1c219892014-10-22 16:32:39 -0700356
357 d.extent = {
358 left: bounds.x1 - lab.marginLR,
359 right: bounds.x2 + lab.marginLR,
360 top: bounds.y1 - lab.marginTB,
361 bottom: bounds.y2 + lab.marginTB
362 };
363
364 d.edge = {
365 left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
366 right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
367 top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
368 bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
369 };
370
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700371 // ====
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700372 });
373
374 network.numTicks = 0;
375 network.preventCollisions = false;
376 network.force.start();
Simon Hunt1c219892014-10-22 16:32:39 -0700377 for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700378 network.force.tick();
379 }
380 network.preventCollisions = true;
381 $('#view').css('visibility', 'visible');
382 });
383
384 }
385
Simon Hunt68ae6652014-10-22 13:58:07 -0700386 function iconUrl(d) {
387 return config.iconUrl[d.type];
388 }
389
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700390 function translate(x, y) {
391 return 'translate(' + x + ',' + y + ')';
392 }
393
Simon Hunt1c219892014-10-22 16:32:39 -0700394 function preventCollisions() {
395 var quadtree = d3.geom.quadtree(network.nodes);
396
397 network.nodes.forEach(function(n) {
398 var nx1 = n.x + n.extent.left,
399 nx2 = n.x + n.extent.right,
400 ny1 = n.y + n.extent.top,
401 ny2 = n.y + n.extent.bottom;
402
403 quadtree.visit(function(quad, x1, y1, x2, y2) {
404 if (quad.point && quad.point !== n) {
405 // check if the rectangles intersect
406 var p = quad.point,
407 px1 = p.x + p.extent.left,
408 px2 = p.x + p.extent.right,
409 py1 = p.y + p.extent.top,
410 py2 = p.y + p.extent.bottom,
411 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
412 if (ix) {
413 var xa1 = nx2 - px1, // shift n left , p right
414 xa2 = px2 - nx1, // shift n right, p left
415 ya1 = ny2 - py1, // shift n up , p down
416 ya2 = py2 - ny1, // shift n down , p up
417 adj = Math.min(xa1, xa2, ya1, ya2);
418
419 if (adj == xa1) {
420 n.x -= adj / 2;
421 p.x += adj / 2;
422 } else if (adj == xa2) {
423 n.x += adj / 2;
424 p.x -= adj / 2;
425 } else if (adj == ya1) {
426 n.y -= adj / 2;
427 p.y += adj / 2;
428 } else if (adj == ya2) {
429 n.y += adj / 2;
430 p.y -= adj / 2;
431 }
432 }
433 return ix;
434 }
435 });
436
437 });
438 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700439
440 function tick(e) {
441 network.numTicks++;
442
Simon Hunt68ae6652014-10-22 13:58:07 -0700443 if (config.layering) {
444 // adjust the y-coord of each node, based on y-pos constraints
445 network.nodes.forEach(function (n) {
446 var z = e.alpha * n.constraint.weight;
447 if (!isNaN(n.constraint.y)) {
448 n.y = (n.constraint.y * z + n.y * (1 - z));
449 }
450 });
451 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700452
Simon Hunt1c219892014-10-22 16:32:39 -0700453 if (network.preventCollisions) {
454 preventCollisions();
455 }
456
457 // TODO: use intersection technique for source end of link also
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700458 network.link
459 .attr('x1', function(d) {
460 return d.source.x;
461 })
462 .attr('y1', function(d) {
463 return d.source.y;
464 })
Simon Hunt1c219892014-10-22 16:32:39 -0700465 .each(function(d) {
466 var x = d.target.x,
467 y = d.target.y,
468 line = new geo.LineSegment(d.source.x, d.source.y, x, y);
469
470 for (var e in d.target.edge) {
471 var ix = line.intersect(d.target.edge[e].offset(x,y));
472 if (ix.in1 && ix.in2) {
473 x = ix.x;
474 y = ix.y;
475 break;
476 }
477 }
478
479 d3.select(this)
480 .attr('x2', x)
481 .attr('y2', y);
482
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700483 });
484
485 network.node
486 .attr('transform', function(d) {
487 return translate(d.x, d.y);
488 });
489
490 }
491
492 // $('#docs-close').on('click', function() {
493 // deselectObject();
494 // return false;
495 // });
496
497 // $(document).on('click', '.select-object', function() {
498 // var obj = graph.data[$(this).data('name')];
499 // if (obj) {
500 // selectObject(obj);
501 // }
502 // return false;
503 // });
504
505 function selectObject(obj, el) {
506 var node;
507 if (el) {
508 node = d3.select(el);
509 } else {
510 network.node.each(function(d) {
511 if (d == obj) {
512 node = d3.select(el = this);
513 }
514 });
515 }
516 if (!node) return;
517
518 if (node.classed('selected')) {
519 deselectObject();
520 return;
521 }
522 deselectObject(false);
523
524 selected = {
525 obj : obj,
526 el : el
527 };
528
529 highlightObject(obj);
530
531 node.classed('selected', true);
532
533 // TODO animate incoming info pane
534 // resize(true);
535 // TODO: check bounds of selected node and scroll into view if needed
536 }
537
538 function deselectObject(doResize) {
539 // Review: logic of 'resize(...)' function.
540 if (doResize || typeof doResize == 'undefined') {
541 resize(false);
542 }
543 // deselect all nodes in the network...
544 network.node.classed('selected', false);
545 selected = {};
546 highlightObject(null);
547 }
548
549 function highlightObject(obj) {
550 if (obj) {
551 if (obj != highlighted) {
552 // TODO set or clear "inactive" class on nodes, based on criteria
553 network.node.classed('inactive', function(d) {
554 // return (obj !== d &&
555 // d.relation(obj.id));
556 return (obj !== d);
557 });
558 // TODO: same with links
559 network.link.classed('inactive', function(d) {
560 return (obj !== d.source && obj !== d.target);
561 });
562 }
563 highlighted = obj;
564 } else {
565 if (highlighted) {
566 // clear the inactive flag (no longer suppressed visually)
567 network.node.classed('inactive', false);
568 network.link.classed('inactive', false);
569 }
570 highlighted = null;
571
572 }
573 }
574
575 function resize(showDetails) {
576 console.log("resize() called...");
577
578 var $details = $('#details');
579
580 if (typeof showDetails == 'boolean') {
581 var showingDetails = showDetails;
582 // TODO: invoke $details.show() or $details.hide()...
583 // $details[showingDetails ? 'show' : 'hide']();
584 }
585
586 view.height = window.innerHeight - config.mastHeight;
587 view.width = window.innerWidth;
588 $('#view')
589 .css('height', view.height + 'px')
590 .css('width', view.width + 'px');
591
592 network.forceWidth = view.width - config.force.marginLR;
593 network.forceHeight = view.height - config.force.marginTB;
594 }
595
596 // ======================================================================
597 // register with the UI framework
598
599 api.addView('network', {
600 load: loadNetworkView
601 });
602
603
604}(ONOS));
605