blob: a72ddecbb6af5db926a619a6ecf4f6b2d9a1f28d [file] [log] [blame]
Simon Hunt0b05d4a2014-10-21 21:50:15 -07001/*
Thomas Vachuska4f1a60c2014-10-28 13:39:07 -07002 * Copyright 2014 Open Networking Laboratory
Thomas Vachuska781d18b2014-10-27 10:31:25 -07003 *
Thomas Vachuska4f1a60c2014-10-28 13:39:07 -07004 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
Thomas Vachuska781d18b2014-10-27 10:31:25 -07007 *
Thomas Vachuska4f1a60c2014-10-28 13:39:07 -07008 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
Thomas Vachuska781d18b2014-10-27 10:31:25 -070015 */
16
17/*
Simon Hunt0b05d4a2014-10-21 21:50:15 -070018 ONOS network topology viewer - PoC version 1.0
19
20 @author Simon Hunt
21 */
22
23(function (onos) {
24 'use strict';
25
Simon Huntd35961b2014-10-28 08:49:48 -070026 // reference to the framework api
Simon Hunt0b05d4a2014-10-21 21:50:15 -070027 var api = onos.api;
28
Simon Huntd35961b2014-10-28 08:49:48 -070029 // configuration data
Simon Hunt0b05d4a2014-10-21 21:50:15 -070030 var config = {
Simon Huntd35961b2014-10-28 08:49:48 -070031 debugOn: false,
32 debug: {
33 showNodeXY: true,
34 showKeyHandler: false
35 },
Simon Hunt2c9e0c22014-10-23 15:12:58 -070036 options: {
Simon Hunt19cb0982014-10-23 16:44:49 -070037 layering: true,
Simon Huntd35961b2014-10-28 08:49:48 -070038 collisionPrevention: true
Simon Hunt2c9e0c22014-10-23 15:12:58 -070039 },
Thomas Vachuska598924e2014-10-23 22:26:07 -070040 XjsonUrl: 'rs/topology/graph',
Simon Hunt0b05d4a2014-10-21 21:50:15 -070041 jsonUrl: 'network.json',
Simon Hunt68ae6652014-10-22 13:58:07 -070042 iconUrl: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070043 device: 'img/device.png',
44 host: 'img/host.png',
45 pkt: 'img/pkt.png',
46 opt: 'img/opt.png'
Simon Hunt68ae6652014-10-22 13:58:07 -070047 },
Simon Hunt19cb0982014-10-23 16:44:49 -070048 mastHeight: 36,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070049 force: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070050 note: 'node.class or link.class is used to differentiate',
51 linkDistance: {
52 infra: 240,
53 host: 100
54 },
55 linkStrength: {
56 infra: 1.0,
57 host: 0.4
58 },
59 charge: {
60 device: -800,
61 host: -400
62 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070063 ticksWithoutCollisions: 50,
64 marginLR: 20,
65 marginTB: 20,
66 translate: function() {
67 return 'translate(' +
68 config.force.marginLR + ',' +
69 config.force.marginTB + ')';
70 }
71 },
72 labels: {
Simon Hunt19cb0982014-10-23 16:44:49 -070073 imgPad: 16,
Simon Hunt1c5f8b62014-10-22 14:43:01 -070074 padLR: 8,
75 padTB: 6,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070076 marginLR: 3,
77 marginTB: 2
78 },
Simon Hunt2c9e0c22014-10-23 15:12:58 -070079 icons: {
80 w: 32,
81 h: 32,
82 xoff: -12,
Simon Hunt19cb0982014-10-23 16:44:49 -070083 yoff: -8
Simon Hunt2c9e0c22014-10-23 15:12:58 -070084 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070085 constraints: {
86 ypos: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070087 host: 0.15,
88 switch: 0.3,
89 roadm: 0.7
Simon Hunt0b05d4a2014-10-21 21:50:15 -070090 }
Simon Hunt2c9e0c22014-10-23 15:12:58 -070091 },
92 hostLinkWidth: 1.0,
93 mouseOutTimerDelayMs: 120
Simon Huntd35961b2014-10-28 08:49:48 -070094 };
95
96 // state variables
97 var view = {},
Simon Hunt0b05d4a2014-10-21 21:50:15 -070098 network = {},
99 selected = {},
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700100 highlighted = null,
101 viewMode = 'showAll';
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700102
103
Simon Huntd35961b2014-10-28 08:49:48 -0700104 function debug(what) {
105 return config.debugOn && config.debug[what];
106 }
107
108 // load the topology view of the network
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700109 function loadNetworkView() {
110 // Hey, here I am, calling something on the ONOS api:
111 api.printTime();
112
113 resize();
114
Simon Huntd35961b2014-10-28 08:49:48 -0700115 // go get our network data from the server...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700116 d3.json(config.jsonUrl, function (err, data) {
117 if (err) {
118 alert('Oops! Error reading JSON...\n\n' +
Simon Huntae968a62014-10-22 14:54:41 -0700119 'URL: ' + config.jsonUrl + '\n\n' +
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700120 'Error: ' + err.message);
121 return;
122 }
Simon Huntd35961b2014-10-28 08:49:48 -0700123// console.log("here is the JSON data...");
124// console.log(data);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700125
126 network.data = data;
127 drawNetwork();
128 });
129
Simon Huntd35961b2014-10-28 08:49:48 -0700130 // while we wait for the data, set up the handlers...
131 setUpClickHandler();
132 setUpRadioButtonHandler();
133 setUpKeyHandler();
134 $(window).on('resize', resize);
135 }
136
137 function setUpClickHandler() {
138 // click handler for "selectable" objects
139 $(document).on('click', '.select-object', function () {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700140 // when any object of class "select-object" is clicked...
141 // TODO: get a reference to the object via lookup...
142 var obj = network.lookup[$(this).data('id')];
143 if (obj) {
144 selectObject(obj);
145 }
146 // stop propagation of event (I think) ...
147 return false;
148 });
Simon Huntd35961b2014-10-28 08:49:48 -0700149 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700150
Simon Huntd35961b2014-10-28 08:49:48 -0700151 function setUpRadioButtonHandler() {
152 d3.selectAll('#displayModes .radio').on('click', function () {
153 var id = d3.select(this).attr('id');
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700154 if (id !== viewMode) {
155 radioButton('displayModes', id);
156 viewMode = id;
Simon Huntd35961b2014-10-28 08:49:48 -0700157 alert('action: ' + id);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700158 }
159 });
160 }
161
Simon Huntd35961b2014-10-28 08:49:48 -0700162 function setUpKeyHandler() {
163 d3.select('body')
164 .on('keydown', function () {
165 processKeyEvent();
166 if (debug('showKeyHandler')) {
167 network.svg.append('text')
168 .attr('x', 5)
169 .attr('y', 15)
170 .style('font-size', '20pt')
171 .text('keyCode: ' + d3.event.keyCode +
172 ' applied to : ' + contextLabel())
173 .transition().duration(2000)
174 .style('font-size', '2pt')
175 .style('fill-opacity', 0.01)
176 .remove();
177 }
178 });
179 }
180
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700181 function radioButton(group, id) {
182 d3.selectAll("#" + group + " .radio").classed("active", false);
183 d3.select("#" + group + " #" + id).classed("active", true);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700184 }
185
Simon Huntd35961b2014-10-28 08:49:48 -0700186 function contextLabel() {
187 return highlighted === null ? "(nothing)" : highlighted.id;
188 }
189
190 function processKeyEvent() {
191 var code = d3.event.keyCode;
192 switch (code) {
193 case 76: // L
194 cycleLabels();
195 break;
196 case 80: // P
197 togglePorts();
198 }
199
200 }
201
202 function cycleLabels() {
203 alert('Cycle Labels - context = ' + contextLabel());
204 }
205
206 function togglePorts() {
207 alert('Toggle Ports - context = ' + contextLabel());
208 }
209
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700210
211 // ========================================================
212
213 function drawNetwork() {
214 $('#view').empty();
215
216 prepareNodesAndLinks();
217 createLayout();
218 console.log("\n\nHere is the augmented network object...");
219 console.warn(network);
220 }
221
222 function prepareNodesAndLinks() {
223 network.lookup = {};
224 network.nodes = [];
225 network.links = [];
226
227 var nw = network.forceWidth,
228 nh = network.forceHeight;
229
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700230 function yPosConstraintForNode(n) {
231 return config.constraints.ypos[n.type || 'host'];
232 }
233
234 // Note that both 'devices' and 'hosts' get mapped into the nodes array
235
236 // first, the devices...
237 network.data.devices.forEach(function(n) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700238 var ypc = yPosConstraintForNode(n),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700239 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700240 iy = ypc * nh,
241 node = {
242 id: n.id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700243 labels: n.labels,
244 class: 'device',
245 icon: 'device',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700246 type: n.type,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700247 x: ix,
248 y: iy,
249 constraint: {
250 weight: 0.7,
251 y: iy
252 }
253 };
254 network.lookup[n.id] = node;
255 network.nodes.push(node);
256 });
257
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700258 // then, the hosts...
259 network.data.hosts.forEach(function(n) {
260 var ypc = yPosConstraintForNode(n),
261 ix = Math.random() * 0.6 * nw + 0.2 * nw,
262 iy = ypc * nh,
263 node = {
264 id: n.id,
265 labels: n.labels,
266 class: 'host',
267 icon: 'host',
268 type: n.type,
269 x: ix,
270 y: iy,
271 constraint: {
272 weight: 0.7,
273 y: iy
274 }
275 };
276 network.lookup[n.id] = node;
277 network.nodes.push(node);
278 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700279
280
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700281 // now, process the explicit links...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700282 network.data.links.forEach(function(n) {
283 var src = network.lookup[n.src],
284 dst = network.lookup[n.dst],
285 id = src.id + "~" + dst.id;
286
287 var link = {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700288 class: 'infra',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700289 id: id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700290 type: n.type,
291 width: n.linkWidth,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700292 source: src,
293 target: dst,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700294 strength: config.force.linkStrength.infra
295 };
296 network.links.push(link);
297 });
298
299 // finally, infer host links...
300 network.data.hosts.forEach(function(n) {
301 var src = network.lookup[n.id],
302 dst = network.lookup[n.cp.device],
303 id = src.id + "~" + dst.id;
304
305 var link = {
306 class: 'host',
307 id: id,
308 type: 'hostLink',
309 width: config.hostLinkWidth,
310 source: src,
311 target: dst,
312 strength: config.force.linkStrength.host
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700313 };
314 network.links.push(link);
315 });
316 }
317
318 function createLayout() {
319
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700320 var cfg = config.force;
321
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700322 network.force = d3.layout.force()
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700323 .size([network.forceWidth, network.forceHeight])
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700324 .nodes(network.nodes)
325 .links(network.links)
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700326 .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
327 .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
328 .charge(function(d) { return cfg.charge[d.class]; })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700329 .on('tick', tick);
330
331 network.svg = d3.select('#view').append('svg')
332 .attr('width', view.width)
333 .attr('height', view.height)
334 .append('g')
Simon Huntae968a62014-10-22 14:54:41 -0700335 .attr('transform', config.force.translate());
Simon Hunt3ab76a82014-10-22 13:07:32 -0700336// .attr('id', 'zoomable')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700337// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700338
Simon Hunt3ab76a82014-10-22 13:07:32 -0700339// function zoomRedraw() {
340// d3.select("#zoomable").attr("transform",
341// "translate(" + d3.event.translate + ")"
342// + " scale(" + d3.event.scale + ")");
343// }
344
Simon Hunt3ab76a82014-10-22 13:07:32 -0700345 // TODO: move glow/blur stuff to util script
346 var glow = network.svg.append('filter')
347 .attr('x', '-50%')
348 .attr('y', '-50%')
349 .attr('width', '200%')
350 .attr('height', '200%')
351 .attr('id', 'blue-glow');
352
353 glow.append('feColorMatrix')
354 .attr('type', 'matrix')
355 .attr('values', '0 0 0 0 0 ' +
356 '0 0 0 0 0 ' +
357 '0 0 0 0 .7 ' +
358 '0 0 0 1 0 ');
359
360 glow.append('feGaussianBlur')
361 .attr('stdDeviation', 3)
362 .attr('result', 'coloredBlur');
363
364 glow.append('feMerge').selectAll('feMergeNode')
365 .data(['coloredBlur', 'SourceGraphic'])
366 .enter().append('feMergeNode')
367 .attr('in', String);
368
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700369 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700370// $('#view').on('scroll', function() {
371//
372// });
373
374
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700375 // add links to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700376 network.link = network.svg.append('g').selectAll('.link')
377 .data(network.force.links(), function(d) {return d.id})
378 .enter().append('line')
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700379 .attr('class', function(d) {return 'link ' + d.class});
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700380
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700381
382 // == define node drag behavior...
Simon Hunt3ab76a82014-10-22 13:07:32 -0700383 network.draggedThreshold = d3.scale.linear()
384 .domain([0, 0.1])
385 .range([5, 20])
386 .clamp(true);
387
388 function dragged(d) {
389 var threshold = network.draggedThreshold(network.force.alpha()),
390 dx = d.oldX - d.px,
391 dy = d.oldY - d.py;
392 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
393 d.dragged = true;
394 }
395 return d.dragged;
396 }
397
398 network.drag = d3.behavior.drag()
399 .origin(function(d) { return d; })
400 .on('dragstart', function(d) {
401 d.oldX = d.x;
402 d.oldY = d.y;
403 d.dragged = false;
404 d.fixed |= 2;
405 })
406 .on('drag', function(d) {
407 d.px = d3.event.x;
408 d.py = d3.event.y;
409 if (dragged(d)) {
410 if (!network.force.alpha()) {
411 network.force.alpha(.025);
412 }
413 }
414 })
415 .on('dragend', function(d) {
416 if (!dragged(d)) {
417 selectObject(d, this);
418 }
419 d.fixed &= ~6;
420 });
421
422 $('#view').on('click', function(e) {
423 if (!$(e.target).closest('.node').length) {
424 deselectObject();
425 }
426 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700427
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700428
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700429 // add nodes to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700430 network.node = network.svg.selectAll('.node')
431 .data(network.force.nodes(), function(d) {return d.id})
432 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700433 .attr('class', function(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700434 var cls = 'node ' + d.class;
435 if (d.type) {
436 cls += ' ' + d.type;
437 }
438 return cls;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700439 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700440 .attr('transform', function(d) {
441 return translate(d.x, d.y);
442 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700443 .call(network.drag)
444 .on('mouseover', function(d) {
445 if (!selected.obj) {
446 if (network.mouseoutTimeout) {
447 clearTimeout(network.mouseoutTimeout);
448 network.mouseoutTimeout = null;
449 }
450 highlightObject(d);
451 }
452 })
453 .on('mouseout', function(d) {
454 if (!selected.obj) {
455 if (network.mouseoutTimeout) {
456 clearTimeout(network.mouseoutTimeout);
457 network.mouseoutTimeout = null;
458 }
459 network.mouseoutTimeout = setTimeout(function() {
460 highlightObject(null);
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700461 }, config.mouseOutTimerDelayMs);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700462 }
463 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700464
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700465 network.nodeRect = network.node.append('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700466 .attr({
467 rx: 5,
468 ry: 5,
469 width: 100,
470 height: 12
471 });
472 // note that width/height are adjusted to fit the label text
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700473
474 network.node.each(function(d) {
475 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700476 icon = iconUrl(d);
477
478 node.append('text')
479 // TODO: add label cycle behavior
480 .text(d.id)
481 .attr('dy', '1.1em');
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700482
483 if (icon) {
484 var cfg = config.icons;
485 node.append('svg:image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700486 .attr({
487 width: cfg.w,
488 height: cfg.h,
489 'xlink:href': icon
490 });
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700491 // note, icon relative positioning (x,y) is done after we have
492 // adjusted the bounds of the rectangle...
493 }
Simon Hunt68ae6652014-10-22 13:58:07 -0700494
Simon Huntd35961b2014-10-28 08:49:48 -0700495 // debug function to show the modelled x,y coordinates of nodes...
496 if (debug('showNodeXY')) {
497 node.select('rect').attr('fill-opacity', 0.5);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700498 node.append('circle')
499 .attr({
500 class: 'debug',
501 cx: 0,
502 cy: 0,
503 r: '3px'
504 });
505 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700506 });
507
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700508
509 // returns the newly computed bounding box
510 function adjustRectToFitText(n) {
511 var text = n.select('text'),
512 box = text.node().getBBox(),
513 lab = config.labels;
514
515 text.attr('text-anchor', 'middle')
516 .attr('y', '-0.8em')
517 .attr('x', lab.imgPad/2)
518 ;
519
520 // TODO: figure out how to access the data on selection
521 console.log("\nadjust rect for " + n.data().id);
522 console.log(box);
523
524 // translate the bbox so that it is centered on [x,y]
525 box.x = -box.width / 2;
526 box.y = -box.height / 2;
527
528 // add padding
529 box.x -= (lab.padLR + lab.imgPad/2);
530 box.width += lab.padLR * 2 + lab.imgPad;
531 box.y -= lab.padTB;
532 box.height += lab.padTB * 2;
533
534 return box;
535 }
536
537 function boundsFromBox(box) {
538 return {
539 x1: box.x,
540 y1: box.y,
541 x2: box.x + box.width,
542 y2: box.y + box.height
543 };
544 }
545
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700546 // this function is scheduled to happen soon after the given thread ends
547 setTimeout(function() {
548 network.node.each(function(d) {
549 // for every node, recompute size, padding, etc. so text fits
550 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700551 text = node.select('text'),
552 box = adjustRectToFitText(node),
553 lab = config.labels;
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700554
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700555 // now make the computed adjustment
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700556 node.select('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700557 .attr(box);
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700558
559 node.select('image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700560 .attr('x', box.x + config.icons.xoff)
561 .attr('y', box.y + config.icons.yoff);
Simon Hunt1c219892014-10-22 16:32:39 -0700562
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700563 var bounds = boundsFromBox(box);
564
565 // todo: clean up extent and edge work..
Simon Hunt1c219892014-10-22 16:32:39 -0700566 d.extent = {
567 left: bounds.x1 - lab.marginLR,
568 right: bounds.x2 + lab.marginLR,
569 top: bounds.y1 - lab.marginTB,
570 bottom: bounds.y2 + lab.marginTB
571 };
572
573 d.edge = {
574 left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
575 right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
576 top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
577 bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
578 };
579
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700580 });
581
582 network.numTicks = 0;
583 network.preventCollisions = false;
584 network.force.start();
Simon Hunt1c219892014-10-22 16:32:39 -0700585 for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700586 network.force.tick();
587 }
588 network.preventCollisions = true;
589 $('#view').css('visibility', 'visible');
590 });
591
592 }
593
Simon Hunt68ae6652014-10-22 13:58:07 -0700594 function iconUrl(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700595 return config.iconUrl[d.icon];
Simon Hunt68ae6652014-10-22 13:58:07 -0700596 }
597
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700598 function translate(x, y) {
599 return 'translate(' + x + ',' + y + ')';
600 }
601
Simon Hunt1c219892014-10-22 16:32:39 -0700602 function preventCollisions() {
603 var quadtree = d3.geom.quadtree(network.nodes);
604
605 network.nodes.forEach(function(n) {
606 var nx1 = n.x + n.extent.left,
607 nx2 = n.x + n.extent.right,
608 ny1 = n.y + n.extent.top,
609 ny2 = n.y + n.extent.bottom;
610
611 quadtree.visit(function(quad, x1, y1, x2, y2) {
612 if (quad.point && quad.point !== n) {
613 // check if the rectangles intersect
614 var p = quad.point,
615 px1 = p.x + p.extent.left,
616 px2 = p.x + p.extent.right,
617 py1 = p.y + p.extent.top,
618 py2 = p.y + p.extent.bottom,
619 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
620 if (ix) {
621 var xa1 = nx2 - px1, // shift n left , p right
622 xa2 = px2 - nx1, // shift n right, p left
623 ya1 = ny2 - py1, // shift n up , p down
624 ya2 = py2 - ny1, // shift n down , p up
625 adj = Math.min(xa1, xa2, ya1, ya2);
626
627 if (adj == xa1) {
628 n.x -= adj / 2;
629 p.x += adj / 2;
630 } else if (adj == xa2) {
631 n.x += adj / 2;
632 p.x -= adj / 2;
633 } else if (adj == ya1) {
634 n.y -= adj / 2;
635 p.y += adj / 2;
636 } else if (adj == ya2) {
637 n.y += adj / 2;
638 p.y -= adj / 2;
639 }
640 }
641 return ix;
642 }
643 });
644
645 });
646 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700647
648 function tick(e) {
649 network.numTicks++;
650
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700651 if (config.options.layering) {
Simon Hunt68ae6652014-10-22 13:58:07 -0700652 // adjust the y-coord of each node, based on y-pos constraints
653 network.nodes.forEach(function (n) {
654 var z = e.alpha * n.constraint.weight;
655 if (!isNaN(n.constraint.y)) {
656 n.y = (n.constraint.y * z + n.y * (1 - z));
657 }
658 });
659 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700660
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700661 if (config.options.collisionPrevention && network.preventCollisions) {
Simon Hunt1c219892014-10-22 16:32:39 -0700662 preventCollisions();
663 }
664
Simon Huntd35961b2014-10-28 08:49:48 -0700665 // clip visualization of links at bounds of nodes...
666 network.link.each(function(d) {
667 var xs = d.source.x,
668 ys = d.source.y,
669 xt = d.target.x,
670 yt = d.target.y,
671 line = new geo.LineSegment(xs, ys, xt, yt),
672 e, ix;
Simon Hunt1c219892014-10-22 16:32:39 -0700673
Simon Huntd35961b2014-10-28 08:49:48 -0700674 for (e in d.source.edge) {
675 ix = line.intersect(d.source.edge[e].offset(xs, ys));
Simon Hunt1c219892014-10-22 16:32:39 -0700676 if (ix.in1 && ix.in2) {
Simon Huntd35961b2014-10-28 08:49:48 -0700677 xs = ix.x;
678 ys = ix.y;
679 break;
680 }
681 }
682
683 for (e in d.target.edge) {
684 ix = line.intersect(d.target.edge[e].offset(xt, yt));
685 if (ix.in1 && ix.in2) {
686 xt = ix.x;
687 yt = ix.y;
Simon Hunt1c219892014-10-22 16:32:39 -0700688 break;
689 }
690 }
691
692 d3.select(this)
Simon Huntd35961b2014-10-28 08:49:48 -0700693 .attr('x1', xs)
694 .attr('y1', ys)
695 .attr('x2', xt)
696 .attr('y2', yt);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700697 });
698
Simon Huntd35961b2014-10-28 08:49:48 -0700699 // position each node by translating the node (group) by x,y
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700700 network.node
701 .attr('transform', function(d) {
702 return translate(d.x, d.y);
703 });
704
705 }
706
707 // $('#docs-close').on('click', function() {
708 // deselectObject();
709 // return false;
710 // });
711
712 // $(document).on('click', '.select-object', function() {
713 // var obj = graph.data[$(this).data('name')];
714 // if (obj) {
715 // selectObject(obj);
716 // }
717 // return false;
718 // });
719
720 function selectObject(obj, el) {
721 var node;
722 if (el) {
723 node = d3.select(el);
724 } else {
725 network.node.each(function(d) {
726 if (d == obj) {
727 node = d3.select(el = this);
728 }
729 });
730 }
731 if (!node) return;
732
733 if (node.classed('selected')) {
734 deselectObject();
735 return;
736 }
737 deselectObject(false);
738
739 selected = {
740 obj : obj,
741 el : el
742 };
743
744 highlightObject(obj);
745
746 node.classed('selected', true);
747
748 // TODO animate incoming info pane
749 // resize(true);
750 // TODO: check bounds of selected node and scroll into view if needed
751 }
752
753 function deselectObject(doResize) {
754 // Review: logic of 'resize(...)' function.
755 if (doResize || typeof doResize == 'undefined') {
756 resize(false);
757 }
758 // deselect all nodes in the network...
759 network.node.classed('selected', false);
760 selected = {};
761 highlightObject(null);
762 }
763
764 function highlightObject(obj) {
765 if (obj) {
766 if (obj != highlighted) {
767 // TODO set or clear "inactive" class on nodes, based on criteria
768 network.node.classed('inactive', function(d) {
769 // return (obj !== d &&
770 // d.relation(obj.id));
771 return (obj !== d);
772 });
773 // TODO: same with links
774 network.link.classed('inactive', function(d) {
775 return (obj !== d.source && obj !== d.target);
776 });
777 }
778 highlighted = obj;
779 } else {
780 if (highlighted) {
781 // clear the inactive flag (no longer suppressed visually)
782 network.node.classed('inactive', false);
783 network.link.classed('inactive', false);
784 }
785 highlighted = null;
786
787 }
788 }
789
790 function resize(showDetails) {
791 console.log("resize() called...");
792
793 var $details = $('#details');
794
795 if (typeof showDetails == 'boolean') {
796 var showingDetails = showDetails;
797 // TODO: invoke $details.show() or $details.hide()...
798 // $details[showingDetails ? 'show' : 'hide']();
799 }
800
801 view.height = window.innerHeight - config.mastHeight;
802 view.width = window.innerWidth;
803 $('#view')
804 .css('height', view.height + 'px')
805 .css('width', view.width + 'px');
806
807 network.forceWidth = view.width - config.force.marginLR;
808 network.forceHeight = view.height - config.force.marginTB;
809 }
810
811 // ======================================================================
812 // register with the UI framework
813
814 api.addView('network', {
815 load: loadNetworkView
816 });
817
818
819}(ONOS));
820