blob: 65e84f04893b2e743c527bbb3ac46a0260618ace [file] [log] [blame]
Simon Hunt0b05d4a2014-10-21 21:50:15 -07001/*
Thomas Vachuska781d18b2014-10-27 10:31:25 -07002 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20/*
Simon Hunt0b05d4a2014-10-21 21:50:15 -070021 ONOS network topology viewer - PoC version 1.0
22
23 @author Simon Hunt
24 */
25
26(function (onos) {
27 'use strict';
28
Simon Huntd35961b2014-10-28 08:49:48 -070029 // reference to the framework api
Simon Hunt0b05d4a2014-10-21 21:50:15 -070030 var api = onos.api;
31
Simon Huntd35961b2014-10-28 08:49:48 -070032 // configuration data
Simon Hunt0b05d4a2014-10-21 21:50:15 -070033 var config = {
Simon Huntd35961b2014-10-28 08:49:48 -070034 debugOn: false,
35 debug: {
36 showNodeXY: true,
37 showKeyHandler: false
38 },
Simon Hunt2c9e0c22014-10-23 15:12:58 -070039 options: {
Simon Hunt19cb0982014-10-23 16:44:49 -070040 layering: true,
Simon Huntd35961b2014-10-28 08:49:48 -070041 collisionPrevention: true
Simon Hunt2c9e0c22014-10-23 15:12:58 -070042 },
Thomas Vachuska598924e2014-10-23 22:26:07 -070043 XjsonUrl: 'rs/topology/graph',
Simon Hunt0b05d4a2014-10-21 21:50:15 -070044 jsonUrl: 'network.json',
Simon Hunt68ae6652014-10-22 13:58:07 -070045 iconUrl: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070046 device: 'img/device.png',
47 host: 'img/host.png',
48 pkt: 'img/pkt.png',
49 opt: 'img/opt.png'
Simon Hunt68ae6652014-10-22 13:58:07 -070050 },
Simon Hunt19cb0982014-10-23 16:44:49 -070051 mastHeight: 36,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070052 force: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070053 note: 'node.class or link.class is used to differentiate',
54 linkDistance: {
55 infra: 240,
56 host: 100
57 },
58 linkStrength: {
59 infra: 1.0,
60 host: 0.4
61 },
62 charge: {
63 device: -800,
64 host: -400
65 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070066 ticksWithoutCollisions: 50,
67 marginLR: 20,
68 marginTB: 20,
69 translate: function() {
70 return 'translate(' +
71 config.force.marginLR + ',' +
72 config.force.marginTB + ')';
73 }
74 },
75 labels: {
Simon Hunt19cb0982014-10-23 16:44:49 -070076 imgPad: 16,
Simon Hunt1c5f8b62014-10-22 14:43:01 -070077 padLR: 8,
78 padTB: 6,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070079 marginLR: 3,
80 marginTB: 2
81 },
Simon Hunt2c9e0c22014-10-23 15:12:58 -070082 icons: {
83 w: 32,
84 h: 32,
85 xoff: -12,
Simon Hunt19cb0982014-10-23 16:44:49 -070086 yoff: -8
Simon Hunt2c9e0c22014-10-23 15:12:58 -070087 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070088 constraints: {
89 ypos: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070090 host: 0.15,
91 switch: 0.3,
92 roadm: 0.7
Simon Hunt0b05d4a2014-10-21 21:50:15 -070093 }
Simon Hunt2c9e0c22014-10-23 15:12:58 -070094 },
95 hostLinkWidth: 1.0,
96 mouseOutTimerDelayMs: 120
Simon Huntd35961b2014-10-28 08:49:48 -070097 };
98
99 // state variables
100 var view = {},
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700101 network = {},
102 selected = {},
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700103 highlighted = null,
104 viewMode = 'showAll';
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700105
106
Simon Huntd35961b2014-10-28 08:49:48 -0700107 function debug(what) {
108 return config.debugOn && config.debug[what];
109 }
110
111 // load the topology view of the network
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700112 function loadNetworkView() {
113 // Hey, here I am, calling something on the ONOS api:
114 api.printTime();
115
116 resize();
117
Simon Huntd35961b2014-10-28 08:49:48 -0700118 // go get our network data from the server...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700119 d3.json(config.jsonUrl, function (err, data) {
120 if (err) {
121 alert('Oops! Error reading JSON...\n\n' +
Simon Huntae968a62014-10-22 14:54:41 -0700122 'URL: ' + config.jsonUrl + '\n\n' +
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700123 'Error: ' + err.message);
124 return;
125 }
Simon Huntd35961b2014-10-28 08:49:48 -0700126// console.log("here is the JSON data...");
127// console.log(data);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700128
129 network.data = data;
130 drawNetwork();
131 });
132
Simon Huntd35961b2014-10-28 08:49:48 -0700133 // while we wait for the data, set up the handlers...
134 setUpClickHandler();
135 setUpRadioButtonHandler();
136 setUpKeyHandler();
137 $(window).on('resize', resize);
138 }
139
140 function setUpClickHandler() {
141 // click handler for "selectable" objects
142 $(document).on('click', '.select-object', function () {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700143 // when any object of class "select-object" is clicked...
144 // TODO: get a reference to the object via lookup...
145 var obj = network.lookup[$(this).data('id')];
146 if (obj) {
147 selectObject(obj);
148 }
149 // stop propagation of event (I think) ...
150 return false;
151 });
Simon Huntd35961b2014-10-28 08:49:48 -0700152 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700153
Simon Huntd35961b2014-10-28 08:49:48 -0700154 function setUpRadioButtonHandler() {
155 d3.selectAll('#displayModes .radio').on('click', function () {
156 var id = d3.select(this).attr('id');
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700157 if (id !== viewMode) {
158 radioButton('displayModes', id);
159 viewMode = id;
Simon Huntd35961b2014-10-28 08:49:48 -0700160 alert('action: ' + id);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700161 }
162 });
163 }
164
Simon Huntd35961b2014-10-28 08:49:48 -0700165 function setUpKeyHandler() {
166 d3.select('body')
167 .on('keydown', function () {
168 processKeyEvent();
169 if (debug('showKeyHandler')) {
170 network.svg.append('text')
171 .attr('x', 5)
172 .attr('y', 15)
173 .style('font-size', '20pt')
174 .text('keyCode: ' + d3.event.keyCode +
175 ' applied to : ' + contextLabel())
176 .transition().duration(2000)
177 .style('font-size', '2pt')
178 .style('fill-opacity', 0.01)
179 .remove();
180 }
181 });
182 }
183
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700184 function radioButton(group, id) {
185 d3.selectAll("#" + group + " .radio").classed("active", false);
186 d3.select("#" + group + " #" + id).classed("active", true);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700187 }
188
Simon Huntd35961b2014-10-28 08:49:48 -0700189 function contextLabel() {
190 return highlighted === null ? "(nothing)" : highlighted.id;
191 }
192
193 function processKeyEvent() {
194 var code = d3.event.keyCode;
195 switch (code) {
196 case 76: // L
197 cycleLabels();
198 break;
199 case 80: // P
200 togglePorts();
201 }
202
203 }
204
205 function cycleLabels() {
206 alert('Cycle Labels - context = ' + contextLabel());
207 }
208
209 function togglePorts() {
210 alert('Toggle Ports - context = ' + contextLabel());
211 }
212
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700213
214 // ========================================================
215
216 function drawNetwork() {
217 $('#view').empty();
218
219 prepareNodesAndLinks();
220 createLayout();
221 console.log("\n\nHere is the augmented network object...");
222 console.warn(network);
223 }
224
225 function prepareNodesAndLinks() {
226 network.lookup = {};
227 network.nodes = [];
228 network.links = [];
229
230 var nw = network.forceWidth,
231 nh = network.forceHeight;
232
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700233 function yPosConstraintForNode(n) {
234 return config.constraints.ypos[n.type || 'host'];
235 }
236
237 // Note that both 'devices' and 'hosts' get mapped into the nodes array
238
239 // first, the devices...
240 network.data.devices.forEach(function(n) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700241 var ypc = yPosConstraintForNode(n),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700242 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700243 iy = ypc * nh,
244 node = {
245 id: n.id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700246 labels: n.labels,
247 class: 'device',
248 icon: 'device',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700249 type: n.type,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700250 x: ix,
251 y: iy,
252 constraint: {
253 weight: 0.7,
254 y: iy
255 }
256 };
257 network.lookup[n.id] = node;
258 network.nodes.push(node);
259 });
260
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700261 // then, the hosts...
262 network.data.hosts.forEach(function(n) {
263 var ypc = yPosConstraintForNode(n),
264 ix = Math.random() * 0.6 * nw + 0.2 * nw,
265 iy = ypc * nh,
266 node = {
267 id: n.id,
268 labels: n.labels,
269 class: 'host',
270 icon: 'host',
271 type: n.type,
272 x: ix,
273 y: iy,
274 constraint: {
275 weight: 0.7,
276 y: iy
277 }
278 };
279 network.lookup[n.id] = node;
280 network.nodes.push(node);
281 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700282
283
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700284 // now, process the explicit links...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700285 network.data.links.forEach(function(n) {
286 var src = network.lookup[n.src],
287 dst = network.lookup[n.dst],
288 id = src.id + "~" + dst.id;
289
290 var link = {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700291 class: 'infra',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700292 id: id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700293 type: n.type,
294 width: n.linkWidth,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700295 source: src,
296 target: dst,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700297 strength: config.force.linkStrength.infra
298 };
299 network.links.push(link);
300 });
301
302 // finally, infer host links...
303 network.data.hosts.forEach(function(n) {
304 var src = network.lookup[n.id],
305 dst = network.lookup[n.cp.device],
306 id = src.id + "~" + dst.id;
307
308 var link = {
309 class: 'host',
310 id: id,
311 type: 'hostLink',
312 width: config.hostLinkWidth,
313 source: src,
314 target: dst,
315 strength: config.force.linkStrength.host
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700316 };
317 network.links.push(link);
318 });
319 }
320
321 function createLayout() {
322
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700323 var cfg = config.force;
324
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700325 network.force = d3.layout.force()
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700326 .size([network.forceWidth, network.forceHeight])
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700327 .nodes(network.nodes)
328 .links(network.links)
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700329 .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
330 .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
331 .charge(function(d) { return cfg.charge[d.class]; })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700332 .on('tick', tick);
333
334 network.svg = d3.select('#view').append('svg')
335 .attr('width', view.width)
336 .attr('height', view.height)
337 .append('g')
Simon Huntae968a62014-10-22 14:54:41 -0700338 .attr('transform', config.force.translate());
Simon Hunt3ab76a82014-10-22 13:07:32 -0700339// .attr('id', 'zoomable')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700340// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700341
Simon Hunt3ab76a82014-10-22 13:07:32 -0700342// function zoomRedraw() {
343// d3.select("#zoomable").attr("transform",
344// "translate(" + d3.event.translate + ")"
345// + " scale(" + d3.event.scale + ")");
346// }
347
Simon Hunt3ab76a82014-10-22 13:07:32 -0700348 // TODO: move glow/blur stuff to util script
349 var glow = network.svg.append('filter')
350 .attr('x', '-50%')
351 .attr('y', '-50%')
352 .attr('width', '200%')
353 .attr('height', '200%')
354 .attr('id', 'blue-glow');
355
356 glow.append('feColorMatrix')
357 .attr('type', 'matrix')
358 .attr('values', '0 0 0 0 0 ' +
359 '0 0 0 0 0 ' +
360 '0 0 0 0 .7 ' +
361 '0 0 0 1 0 ');
362
363 glow.append('feGaussianBlur')
364 .attr('stdDeviation', 3)
365 .attr('result', 'coloredBlur');
366
367 glow.append('feMerge').selectAll('feMergeNode')
368 .data(['coloredBlur', 'SourceGraphic'])
369 .enter().append('feMergeNode')
370 .attr('in', String);
371
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700372 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700373// $('#view').on('scroll', function() {
374//
375// });
376
377
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700378 // add links to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700379 network.link = network.svg.append('g').selectAll('.link')
380 .data(network.force.links(), function(d) {return d.id})
381 .enter().append('line')
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700382 .attr('class', function(d) {return 'link ' + d.class});
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700383
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700384
385 // == define node drag behavior...
Simon Hunt3ab76a82014-10-22 13:07:32 -0700386 network.draggedThreshold = d3.scale.linear()
387 .domain([0, 0.1])
388 .range([5, 20])
389 .clamp(true);
390
391 function dragged(d) {
392 var threshold = network.draggedThreshold(network.force.alpha()),
393 dx = d.oldX - d.px,
394 dy = d.oldY - d.py;
395 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
396 d.dragged = true;
397 }
398 return d.dragged;
399 }
400
401 network.drag = d3.behavior.drag()
402 .origin(function(d) { return d; })
403 .on('dragstart', function(d) {
404 d.oldX = d.x;
405 d.oldY = d.y;
406 d.dragged = false;
407 d.fixed |= 2;
408 })
409 .on('drag', function(d) {
410 d.px = d3.event.x;
411 d.py = d3.event.y;
412 if (dragged(d)) {
413 if (!network.force.alpha()) {
414 network.force.alpha(.025);
415 }
416 }
417 })
418 .on('dragend', function(d) {
419 if (!dragged(d)) {
420 selectObject(d, this);
421 }
422 d.fixed &= ~6;
423 });
424
425 $('#view').on('click', function(e) {
426 if (!$(e.target).closest('.node').length) {
427 deselectObject();
428 }
429 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700430
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700431
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700432 // add nodes to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700433 network.node = network.svg.selectAll('.node')
434 .data(network.force.nodes(), function(d) {return d.id})
435 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700436 .attr('class', function(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700437 var cls = 'node ' + d.class;
438 if (d.type) {
439 cls += ' ' + d.type;
440 }
441 return cls;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700442 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700443 .attr('transform', function(d) {
444 return translate(d.x, d.y);
445 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700446 .call(network.drag)
447 .on('mouseover', function(d) {
448 if (!selected.obj) {
449 if (network.mouseoutTimeout) {
450 clearTimeout(network.mouseoutTimeout);
451 network.mouseoutTimeout = null;
452 }
453 highlightObject(d);
454 }
455 })
456 .on('mouseout', function(d) {
457 if (!selected.obj) {
458 if (network.mouseoutTimeout) {
459 clearTimeout(network.mouseoutTimeout);
460 network.mouseoutTimeout = null;
461 }
462 network.mouseoutTimeout = setTimeout(function() {
463 highlightObject(null);
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700464 }, config.mouseOutTimerDelayMs);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700465 }
466 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700467
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700468 network.nodeRect = network.node.append('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700469 .attr({
470 rx: 5,
471 ry: 5,
472 width: 100,
473 height: 12
474 });
475 // note that width/height are adjusted to fit the label text
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700476
477 network.node.each(function(d) {
478 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700479 icon = iconUrl(d);
480
481 node.append('text')
482 // TODO: add label cycle behavior
483 .text(d.id)
484 .attr('dy', '1.1em');
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700485
486 if (icon) {
487 var cfg = config.icons;
488 node.append('svg:image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700489 .attr({
490 width: cfg.w,
491 height: cfg.h,
492 'xlink:href': icon
493 });
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700494 // note, icon relative positioning (x,y) is done after we have
495 // adjusted the bounds of the rectangle...
496 }
Simon Hunt68ae6652014-10-22 13:58:07 -0700497
Simon Huntd35961b2014-10-28 08:49:48 -0700498 // debug function to show the modelled x,y coordinates of nodes...
499 if (debug('showNodeXY')) {
500 node.select('rect').attr('fill-opacity', 0.5);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700501 node.append('circle')
502 .attr({
503 class: 'debug',
504 cx: 0,
505 cy: 0,
506 r: '3px'
507 });
508 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700509 });
510
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700511
512 // returns the newly computed bounding box
513 function adjustRectToFitText(n) {
514 var text = n.select('text'),
515 box = text.node().getBBox(),
516 lab = config.labels;
517
518 text.attr('text-anchor', 'middle')
519 .attr('y', '-0.8em')
520 .attr('x', lab.imgPad/2)
521 ;
522
523 // TODO: figure out how to access the data on selection
524 console.log("\nadjust rect for " + n.data().id);
525 console.log(box);
526
527 // translate the bbox so that it is centered on [x,y]
528 box.x = -box.width / 2;
529 box.y = -box.height / 2;
530
531 // add padding
532 box.x -= (lab.padLR + lab.imgPad/2);
533 box.width += lab.padLR * 2 + lab.imgPad;
534 box.y -= lab.padTB;
535 box.height += lab.padTB * 2;
536
537 return box;
538 }
539
540 function boundsFromBox(box) {
541 return {
542 x1: box.x,
543 y1: box.y,
544 x2: box.x + box.width,
545 y2: box.y + box.height
546 };
547 }
548
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700549 // this function is scheduled to happen soon after the given thread ends
550 setTimeout(function() {
551 network.node.each(function(d) {
552 // for every node, recompute size, padding, etc. so text fits
553 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700554 text = node.select('text'),
555 box = adjustRectToFitText(node),
556 lab = config.labels;
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700557
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700558 // now make the computed adjustment
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700559 node.select('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700560 .attr(box);
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700561
562 node.select('image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700563 .attr('x', box.x + config.icons.xoff)
564 .attr('y', box.y + config.icons.yoff);
Simon Hunt1c219892014-10-22 16:32:39 -0700565
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700566 var bounds = boundsFromBox(box);
567
568 // todo: clean up extent and edge work..
Simon Hunt1c219892014-10-22 16:32:39 -0700569 d.extent = {
570 left: bounds.x1 - lab.marginLR,
571 right: bounds.x2 + lab.marginLR,
572 top: bounds.y1 - lab.marginTB,
573 bottom: bounds.y2 + lab.marginTB
574 };
575
576 d.edge = {
577 left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
578 right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
579 top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
580 bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
581 };
582
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700583 });
584
585 network.numTicks = 0;
586 network.preventCollisions = false;
587 network.force.start();
Simon Hunt1c219892014-10-22 16:32:39 -0700588 for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700589 network.force.tick();
590 }
591 network.preventCollisions = true;
592 $('#view').css('visibility', 'visible');
593 });
594
595 }
596
Simon Hunt68ae6652014-10-22 13:58:07 -0700597 function iconUrl(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700598 return config.iconUrl[d.icon];
Simon Hunt68ae6652014-10-22 13:58:07 -0700599 }
600
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700601 function translate(x, y) {
602 return 'translate(' + x + ',' + y + ')';
603 }
604
Simon Hunt1c219892014-10-22 16:32:39 -0700605 function preventCollisions() {
606 var quadtree = d3.geom.quadtree(network.nodes);
607
608 network.nodes.forEach(function(n) {
609 var nx1 = n.x + n.extent.left,
610 nx2 = n.x + n.extent.right,
611 ny1 = n.y + n.extent.top,
612 ny2 = n.y + n.extent.bottom;
613
614 quadtree.visit(function(quad, x1, y1, x2, y2) {
615 if (quad.point && quad.point !== n) {
616 // check if the rectangles intersect
617 var p = quad.point,
618 px1 = p.x + p.extent.left,
619 px2 = p.x + p.extent.right,
620 py1 = p.y + p.extent.top,
621 py2 = p.y + p.extent.bottom,
622 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
623 if (ix) {
624 var xa1 = nx2 - px1, // shift n left , p right
625 xa2 = px2 - nx1, // shift n right, p left
626 ya1 = ny2 - py1, // shift n up , p down
627 ya2 = py2 - ny1, // shift n down , p up
628 adj = Math.min(xa1, xa2, ya1, ya2);
629
630 if (adj == xa1) {
631 n.x -= adj / 2;
632 p.x += adj / 2;
633 } else if (adj == xa2) {
634 n.x += adj / 2;
635 p.x -= adj / 2;
636 } else if (adj == ya1) {
637 n.y -= adj / 2;
638 p.y += adj / 2;
639 } else if (adj == ya2) {
640 n.y += adj / 2;
641 p.y -= adj / 2;
642 }
643 }
644 return ix;
645 }
646 });
647
648 });
649 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700650
651 function tick(e) {
652 network.numTicks++;
653
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700654 if (config.options.layering) {
Simon Hunt68ae6652014-10-22 13:58:07 -0700655 // adjust the y-coord of each node, based on y-pos constraints
656 network.nodes.forEach(function (n) {
657 var z = e.alpha * n.constraint.weight;
658 if (!isNaN(n.constraint.y)) {
659 n.y = (n.constraint.y * z + n.y * (1 - z));
660 }
661 });
662 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700663
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700664 if (config.options.collisionPrevention && network.preventCollisions) {
Simon Hunt1c219892014-10-22 16:32:39 -0700665 preventCollisions();
666 }
667
Simon Huntd35961b2014-10-28 08:49:48 -0700668 // clip visualization of links at bounds of nodes...
669 network.link.each(function(d) {
670 var xs = d.source.x,
671 ys = d.source.y,
672 xt = d.target.x,
673 yt = d.target.y,
674 line = new geo.LineSegment(xs, ys, xt, yt),
675 e, ix;
Simon Hunt1c219892014-10-22 16:32:39 -0700676
Simon Huntd35961b2014-10-28 08:49:48 -0700677 for (e in d.source.edge) {
678 ix = line.intersect(d.source.edge[e].offset(xs, ys));
Simon Hunt1c219892014-10-22 16:32:39 -0700679 if (ix.in1 && ix.in2) {
Simon Huntd35961b2014-10-28 08:49:48 -0700680 xs = ix.x;
681 ys = ix.y;
682 break;
683 }
684 }
685
686 for (e in d.target.edge) {
687 ix = line.intersect(d.target.edge[e].offset(xt, yt));
688 if (ix.in1 && ix.in2) {
689 xt = ix.x;
690 yt = ix.y;
Simon Hunt1c219892014-10-22 16:32:39 -0700691 break;
692 }
693 }
694
695 d3.select(this)
Simon Huntd35961b2014-10-28 08:49:48 -0700696 .attr('x1', xs)
697 .attr('y1', ys)
698 .attr('x2', xt)
699 .attr('y2', yt);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700700 });
701
Simon Huntd35961b2014-10-28 08:49:48 -0700702 // position each node by translating the node (group) by x,y
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700703 network.node
704 .attr('transform', function(d) {
705 return translate(d.x, d.y);
706 });
707
708 }
709
710 // $('#docs-close').on('click', function() {
711 // deselectObject();
712 // return false;
713 // });
714
715 // $(document).on('click', '.select-object', function() {
716 // var obj = graph.data[$(this).data('name')];
717 // if (obj) {
718 // selectObject(obj);
719 // }
720 // return false;
721 // });
722
723 function selectObject(obj, el) {
724 var node;
725 if (el) {
726 node = d3.select(el);
727 } else {
728 network.node.each(function(d) {
729 if (d == obj) {
730 node = d3.select(el = this);
731 }
732 });
733 }
734 if (!node) return;
735
736 if (node.classed('selected')) {
737 deselectObject();
738 return;
739 }
740 deselectObject(false);
741
742 selected = {
743 obj : obj,
744 el : el
745 };
746
747 highlightObject(obj);
748
749 node.classed('selected', true);
750
751 // TODO animate incoming info pane
752 // resize(true);
753 // TODO: check bounds of selected node and scroll into view if needed
754 }
755
756 function deselectObject(doResize) {
757 // Review: logic of 'resize(...)' function.
758 if (doResize || typeof doResize == 'undefined') {
759 resize(false);
760 }
761 // deselect all nodes in the network...
762 network.node.classed('selected', false);
763 selected = {};
764 highlightObject(null);
765 }
766
767 function highlightObject(obj) {
768 if (obj) {
769 if (obj != highlighted) {
770 // TODO set or clear "inactive" class on nodes, based on criteria
771 network.node.classed('inactive', function(d) {
772 // return (obj !== d &&
773 // d.relation(obj.id));
774 return (obj !== d);
775 });
776 // TODO: same with links
777 network.link.classed('inactive', function(d) {
778 return (obj !== d.source && obj !== d.target);
779 });
780 }
781 highlighted = obj;
782 } else {
783 if (highlighted) {
784 // clear the inactive flag (no longer suppressed visually)
785 network.node.classed('inactive', false);
786 network.link.classed('inactive', false);
787 }
788 highlighted = null;
789
790 }
791 }
792
793 function resize(showDetails) {
794 console.log("resize() called...");
795
796 var $details = $('#details');
797
798 if (typeof showDetails == 'boolean') {
799 var showingDetails = showDetails;
800 // TODO: invoke $details.show() or $details.hide()...
801 // $details[showingDetails ? 'show' : 'hide']();
802 }
803
804 view.height = window.innerHeight - config.mastHeight;
805 view.width = window.innerWidth;
806 $('#view')
807 .css('height', view.height + 'px')
808 .css('width', view.width + 'px');
809
810 network.forceWidth = view.width - config.force.marginLR;
811 network.forceHeight = view.height - config.force.marginTB;
812 }
813
814 // ======================================================================
815 // register with the UI framework
816
817 api.addView('network', {
818 load: loadNetworkView
819 });
820
821
822}(ONOS));
823