blob: 35a62bf3ba60ed97b9dc69464383e618d6ce5bd4 [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: {
Simon Hunt6f376a32014-10-28 12:38:30 -070052 infra: 200,
53 host: 20
Simon Hunt2c9e0c22014-10-23 15:12:58 -070054 },
55 linkStrength: {
56 infra: 1.0,
Simon Hunt6f376a32014-10-28 12:38:30 -070057 host: 1.0
Simon Hunt2c9e0c22014-10-23 15:12:58 -070058 },
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,
Simon Hunt6f376a32014-10-28 12:38:30 -070093 hostRadius: 7,
Simon Hunt2c9e0c22014-10-23 15:12:58 -070094 mouseOutTimerDelayMs: 120
Simon Huntd35961b2014-10-28 08:49:48 -070095 };
96
97 // state variables
98 var view = {},
Simon Hunt0b05d4a2014-10-21 21:50:15 -070099 network = {},
100 selected = {},
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700101 highlighted = null,
102 viewMode = 'showAll';
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700103
104
Simon Huntd35961b2014-10-28 08:49:48 -0700105 function debug(what) {
106 return config.debugOn && config.debug[what];
107 }
108
109 // load the topology view of the network
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700110 function loadNetworkView() {
111 // Hey, here I am, calling something on the ONOS api:
112 api.printTime();
113
114 resize();
115
Simon Huntd35961b2014-10-28 08:49:48 -0700116 // go get our network data from the server...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700117 d3.json(config.jsonUrl, function (err, data) {
118 if (err) {
119 alert('Oops! Error reading JSON...\n\n' +
Simon Huntae968a62014-10-22 14:54:41 -0700120 'URL: ' + config.jsonUrl + '\n\n' +
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700121 'Error: ' + err.message);
122 return;
123 }
Simon Huntd35961b2014-10-28 08:49:48 -0700124// console.log("here is the JSON data...");
125// console.log(data);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700126
127 network.data = data;
128 drawNetwork();
129 });
130
Simon Huntd35961b2014-10-28 08:49:48 -0700131 // while we wait for the data, set up the handlers...
132 setUpClickHandler();
133 setUpRadioButtonHandler();
134 setUpKeyHandler();
135 $(window).on('resize', resize);
136 }
137
138 function setUpClickHandler() {
139 // click handler for "selectable" objects
140 $(document).on('click', '.select-object', function () {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700141 // when any object of class "select-object" is clicked...
142 // TODO: get a reference to the object via lookup...
143 var obj = network.lookup[$(this).data('id')];
144 if (obj) {
145 selectObject(obj);
146 }
147 // stop propagation of event (I think) ...
148 return false;
149 });
Simon Huntd35961b2014-10-28 08:49:48 -0700150 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700151
Simon Huntd35961b2014-10-28 08:49:48 -0700152 function setUpRadioButtonHandler() {
153 d3.selectAll('#displayModes .radio').on('click', function () {
154 var id = d3.select(this).attr('id');
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700155 if (id !== viewMode) {
156 radioButton('displayModes', id);
157 viewMode = id;
Simon Huntd35961b2014-10-28 08:49:48 -0700158 alert('action: ' + id);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700159 }
160 });
161 }
162
Simon Huntd35961b2014-10-28 08:49:48 -0700163 function setUpKeyHandler() {
164 d3.select('body')
165 .on('keydown', function () {
166 processKeyEvent();
167 if (debug('showKeyHandler')) {
168 network.svg.append('text')
169 .attr('x', 5)
170 .attr('y', 15)
171 .style('font-size', '20pt')
172 .text('keyCode: ' + d3.event.keyCode +
173 ' applied to : ' + contextLabel())
174 .transition().duration(2000)
175 .style('font-size', '2pt')
176 .style('fill-opacity', 0.01)
177 .remove();
178 }
179 });
180 }
181
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700182 function radioButton(group, id) {
183 d3.selectAll("#" + group + " .radio").classed("active", false);
184 d3.select("#" + group + " #" + id).classed("active", true);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700185 }
186
Simon Huntd35961b2014-10-28 08:49:48 -0700187 function contextLabel() {
188 return highlighted === null ? "(nothing)" : highlighted.id;
189 }
190
191 function processKeyEvent() {
192 var code = d3.event.keyCode;
193 switch (code) {
194 case 76: // L
195 cycleLabels();
196 break;
197 case 80: // P
198 togglePorts();
199 }
200
201 }
202
203 function cycleLabels() {
204 alert('Cycle Labels - context = ' + contextLabel());
205 }
206
207 function togglePorts() {
208 alert('Toggle Ports - context = ' + contextLabel());
209 }
210
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700211
212 // ========================================================
213
214 function drawNetwork() {
215 $('#view').empty();
216
217 prepareNodesAndLinks();
218 createLayout();
219 console.log("\n\nHere is the augmented network object...");
220 console.warn(network);
221 }
222
223 function prepareNodesAndLinks() {
224 network.lookup = {};
225 network.nodes = [];
226 network.links = [];
227
228 var nw = network.forceWidth,
229 nh = network.forceHeight;
230
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700231 function yPosConstraintForNode(n) {
232 return config.constraints.ypos[n.type || 'host'];
233 }
234
235 // Note that both 'devices' and 'hosts' get mapped into the nodes array
236
237 // first, the devices...
238 network.data.devices.forEach(function(n) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700239 var ypc = yPosConstraintForNode(n),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700240 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700241 iy = ypc * nh,
242 node = {
243 id: n.id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700244 labels: n.labels,
245 class: 'device',
246 icon: 'device',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700247 type: n.type,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700248 x: ix,
249 y: iy,
250 constraint: {
251 weight: 0.7,
252 y: iy
253 }
254 };
255 network.lookup[n.id] = node;
256 network.nodes.push(node);
257 });
258
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700259 // then, the hosts...
260 network.data.hosts.forEach(function(n) {
261 var ypc = yPosConstraintForNode(n),
262 ix = Math.random() * 0.6 * nw + 0.2 * nw,
263 iy = ypc * nh,
264 node = {
265 id: n.id,
266 labels: n.labels,
267 class: 'host',
268 icon: 'host',
269 type: n.type,
270 x: ix,
271 y: iy,
272 constraint: {
273 weight: 0.7,
274 y: iy
275 }
276 };
277 network.lookup[n.id] = node;
278 network.nodes.push(node);
279 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700280
281
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700282 // now, process the explicit links...
Simon Hunt6f376a32014-10-28 12:38:30 -0700283 network.data.links.forEach(function(lnk) {
284 var src = network.lookup[lnk.src],
285 dst = network.lookup[lnk.dst],
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700286 id = src.id + "~" + dst.id;
287
288 var link = {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700289 class: 'infra',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700290 id: id,
Simon Hunt6f376a32014-10-28 12:38:30 -0700291 type: lnk.type,
292 width: lnk.linkWidth,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700293 source: src,
294 target: dst,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700295 strength: config.force.linkStrength.infra
296 };
297 network.links.push(link);
298 });
299
300 // finally, infer host links...
301 network.data.hosts.forEach(function(n) {
302 var src = network.lookup[n.id],
303 dst = network.lookup[n.cp.device],
304 id = src.id + "~" + dst.id;
305
306 var link = {
307 class: 'host',
308 id: id,
309 type: 'hostLink',
310 width: config.hostLinkWidth,
311 source: src,
312 target: dst,
313 strength: config.force.linkStrength.host
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700314 };
315 network.links.push(link);
316 });
317 }
318
319 function createLayout() {
320
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700321 var cfg = config.force;
322
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700323 network.force = d3.layout.force()
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700324 .size([network.forceWidth, network.forceHeight])
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700325 .nodes(network.nodes)
326 .links(network.links)
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700327 .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
328 .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
329 .charge(function(d) { return cfg.charge[d.class]; })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700330 .on('tick', tick);
331
332 network.svg = d3.select('#view').append('svg')
333 .attr('width', view.width)
334 .attr('height', view.height)
335 .append('g')
Simon Huntae968a62014-10-22 14:54:41 -0700336 .attr('transform', config.force.translate());
Simon Hunt3ab76a82014-10-22 13:07:32 -0700337// .attr('id', 'zoomable')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700338// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700339
Simon Hunt3ab76a82014-10-22 13:07:32 -0700340// function zoomRedraw() {
341// d3.select("#zoomable").attr("transform",
342// "translate(" + d3.event.translate + ")"
343// + " scale(" + d3.event.scale + ")");
344// }
345
Simon Hunt3ab76a82014-10-22 13:07:32 -0700346 // TODO: move glow/blur stuff to util script
347 var glow = network.svg.append('filter')
348 .attr('x', '-50%')
349 .attr('y', '-50%')
350 .attr('width', '200%')
351 .attr('height', '200%')
352 .attr('id', 'blue-glow');
353
354 glow.append('feColorMatrix')
355 .attr('type', 'matrix')
356 .attr('values', '0 0 0 0 0 ' +
357 '0 0 0 0 0 ' +
358 '0 0 0 0 .7 ' +
359 '0 0 0 1 0 ');
360
361 glow.append('feGaussianBlur')
362 .attr('stdDeviation', 3)
363 .attr('result', 'coloredBlur');
364
365 glow.append('feMerge').selectAll('feMergeNode')
366 .data(['coloredBlur', 'SourceGraphic'])
367 .enter().append('feMergeNode')
368 .attr('in', String);
369
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700370 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700371// $('#view').on('scroll', function() {
372//
373// });
374
375
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700376 // add links to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700377 network.link = network.svg.append('g').selectAll('.link')
378 .data(network.force.links(), function(d) {return d.id})
379 .enter().append('line')
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700380 .attr('class', function(d) {return 'link ' + d.class});
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700381
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700382
Simon Hunt6f376a32014-10-28 12:38:30 -0700383 // TODO: move drag behavior into separate method.
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700384 // == define node drag behavior...
Simon Hunt3ab76a82014-10-22 13:07:32 -0700385 network.draggedThreshold = d3.scale.linear()
386 .domain([0, 0.1])
387 .range([5, 20])
388 .clamp(true);
389
390 function dragged(d) {
391 var threshold = network.draggedThreshold(network.force.alpha()),
392 dx = d.oldX - d.px,
393 dy = d.oldY - d.py;
394 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
395 d.dragged = true;
396 }
397 return d.dragged;
398 }
399
400 network.drag = d3.behavior.drag()
401 .origin(function(d) { return d; })
402 .on('dragstart', function(d) {
403 d.oldX = d.x;
404 d.oldY = d.y;
405 d.dragged = false;
406 d.fixed |= 2;
407 })
408 .on('drag', function(d) {
409 d.px = d3.event.x;
410 d.py = d3.event.y;
411 if (dragged(d)) {
412 if (!network.force.alpha()) {
413 network.force.alpha(.025);
414 }
415 }
416 })
417 .on('dragend', function(d) {
418 if (!dragged(d)) {
419 selectObject(d, this);
420 }
421 d.fixed &= ~6;
422 });
423
424 $('#view').on('click', function(e) {
425 if (!$(e.target).closest('.node').length) {
426 deselectObject();
427 }
428 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700429
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700430
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700431 // add nodes to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700432 network.node = network.svg.selectAll('.node')
433 .data(network.force.nodes(), function(d) {return d.id})
434 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700435 .attr('class', function(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700436 var cls = 'node ' + d.class;
437 if (d.type) {
438 cls += ' ' + d.type;
439 }
440 return cls;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700441 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700442 .attr('transform', function(d) {
443 return translate(d.x, d.y);
444 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700445 .call(network.drag)
446 .on('mouseover', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700447 // TODO: show tooltip
448/*
Simon Hunt3ab76a82014-10-22 13:07:32 -0700449 if (!selected.obj) {
450 if (network.mouseoutTimeout) {
451 clearTimeout(network.mouseoutTimeout);
452 network.mouseoutTimeout = null;
453 }
454 highlightObject(d);
455 }
Simon Hunt6f376a32014-10-28 12:38:30 -0700456*/
Simon Hunt3ab76a82014-10-22 13:07:32 -0700457 })
458 .on('mouseout', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700459 // TODO: hide tooltip
460/*
Simon Hunt3ab76a82014-10-22 13:07:32 -0700461 if (!selected.obj) {
462 if (network.mouseoutTimeout) {
463 clearTimeout(network.mouseoutTimeout);
464 network.mouseoutTimeout = null;
465 }
466 network.mouseoutTimeout = setTimeout(function() {
467 highlightObject(null);
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700468 }, config.mouseOutTimerDelayMs);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700469 }
Simon Hunt6f376a32014-10-28 12:38:30 -0700470*/
Simon Hunt3ab76a82014-10-22 13:07:32 -0700471 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700472
Simon Hunt6f376a32014-10-28 12:38:30 -0700473
474 // deal with device nodes first
475 network.nodeRect = network.node.filter('.device')
476 .append('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700477 .attr({
478 rx: 5,
479 ry: 5,
480 width: 100,
481 height: 12
482 });
483 // note that width/height are adjusted to fit the label text
Simon Hunt6f376a32014-10-28 12:38:30 -0700484 // then padded, and space made for the icon.
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700485
Simon Hunt6f376a32014-10-28 12:38:30 -0700486 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700487 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700488 icon = iconUrl(d);
489
490 node.append('text')
491 // TODO: add label cycle behavior
492 .text(d.id)
493 .attr('dy', '1.1em');
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700494
495 if (icon) {
496 var cfg = config.icons;
497 node.append('svg:image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700498 .attr({
499 width: cfg.w,
500 height: cfg.h,
501 'xlink:href': icon
502 });
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700503 // note, icon relative positioning (x,y) is done after we have
504 // adjusted the bounds of the rectangle...
505 }
Simon Hunt68ae6652014-10-22 13:58:07 -0700506
Simon Huntd35961b2014-10-28 08:49:48 -0700507 // debug function to show the modelled x,y coordinates of nodes...
508 if (debug('showNodeXY')) {
509 node.select('rect').attr('fill-opacity', 0.5);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700510 node.append('circle')
511 .attr({
512 class: 'debug',
513 cx: 0,
514 cy: 0,
515 r: '3px'
516 });
517 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700518 });
519
Simon Hunt6f376a32014-10-28 12:38:30 -0700520 // now process host nodes
521 network.nodeCircle = network.node.filter('.host')
522 .append('circle')
523 .attr({
524 r: config.hostRadius
525 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700526
Simon Hunt6f376a32014-10-28 12:38:30 -0700527 network.node.filter('.host').each(function(d) {
528 var node = d3.select(this),
529 icon = iconUrl(d);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700530
Simon Hunt6f376a32014-10-28 12:38:30 -0700531 // debug function to show the modelled x,y coordinates of nodes...
532 if (debug('showNodeXY')) {
533 node.select('circle').attr('fill-opacity', 0.5);
534 node.append('circle')
535 .attr({
536 class: 'debug',
537 cx: 0,
538 cy: 0,
539 r: '3px'
540 });
541 }
542 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700543
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700544 // this function is scheduled to happen soon after the given thread ends
545 setTimeout(function() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700546 // post process the device nodes, to pad their size to fit the
547 // label text and attach the icon to the right location.
548 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700549 // 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
Simon Hunt6f376a32014-10-28 12:38:30 -0700592
593 // returns the newly computed bounding box of the rectangle
594 function adjustRectToFitText(n) {
595 var text = n.select('text'),
596 box = text.node().getBBox(),
597 lab = config.labels;
598
599 text.attr('text-anchor', 'middle')
600 .attr('y', '-0.8em')
601 .attr('x', lab.imgPad/2)
602 ;
603
604 // TODO: figure out how to access the data on selection
605 console.log("\nadjust rect for " + n.data().id);
606 console.log(box);
607
608 // translate the bbox so that it is centered on [x,y]
609 box.x = -box.width / 2;
610 box.y = -box.height / 2;
611
612 // add padding
613 box.x -= (lab.padLR + lab.imgPad/2);
614 box.width += lab.padLR * 2 + lab.imgPad;
615 box.y -= lab.padTB;
616 box.height += lab.padTB * 2;
617
618 return box;
619 }
620
621 function boundsFromBox(box) {
622 return {
623 x1: box.x,
624 y1: box.y,
625 x2: box.x + box.width,
626 y2: box.y + box.height
627 };
628 }
629
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700630 }
631
Simon Hunt68ae6652014-10-22 13:58:07 -0700632 function iconUrl(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700633 return config.iconUrl[d.icon];
Simon Hunt68ae6652014-10-22 13:58:07 -0700634 }
635
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700636 function translate(x, y) {
637 return 'translate(' + x + ',' + y + ')';
638 }
639
Simon Hunt6f376a32014-10-28 12:38:30 -0700640 // prevents collisions amongst device nodes
Simon Hunt1c219892014-10-22 16:32:39 -0700641 function preventCollisions() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700642 var quadtree = d3.geom.quadtree(network.nodes),
643 hrad = config.hostRadius;
Simon Hunt1c219892014-10-22 16:32:39 -0700644
645 network.nodes.forEach(function(n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700646 var nx1, nx2, ny1, ny2;
647
648 if (n.class === 'device') {
649 nx1 = n.x + n.extent.left;
650 nx2 = n.x + n.extent.right;
651 ny1 = n.y + n.extent.top;
Simon Hunt1c219892014-10-22 16:32:39 -0700652 ny2 = n.y + n.extent.bottom;
653
Simon Hunt6f376a32014-10-28 12:38:30 -0700654 } else {
655 nx1 = n.x - hrad;
656 nx2 = n.x + hrad;
657 ny1 = n.y - hrad;
658 ny2 = n.y + hrad;
659 }
660
Simon Hunt1c219892014-10-22 16:32:39 -0700661 quadtree.visit(function(quad, x1, y1, x2, y2) {
662 if (quad.point && quad.point !== n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700663 // check if the rectangles/circles intersect
Simon Hunt1c219892014-10-22 16:32:39 -0700664 var p = quad.point,
Simon Hunt6f376a32014-10-28 12:38:30 -0700665 px1, px2, py1, py2, ix;
666
667 if (p.class === 'device') {
668 px1 = p.x + p.extent.left;
669 px2 = p.x + p.extent.right;
670 py1 = p.y + p.extent.top;
671 py2 = p.y + p.extent.bottom;
672
673 } else {
674 px1 = p.x - hrad;
675 px2 = p.x + hrad;
676 py1 = p.y - hrad;
677 py2 = p.y + hrad;
678 }
679
680 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
681
Simon Hunt1c219892014-10-22 16:32:39 -0700682 if (ix) {
683 var xa1 = nx2 - px1, // shift n left , p right
684 xa2 = px2 - nx1, // shift n right, p left
685 ya1 = ny2 - py1, // shift n up , p down
686 ya2 = py2 - ny1, // shift n down , p up
687 adj = Math.min(xa1, xa2, ya1, ya2);
688
689 if (adj == xa1) {
690 n.x -= adj / 2;
691 p.x += adj / 2;
692 } else if (adj == xa2) {
693 n.x += adj / 2;
694 p.x -= adj / 2;
695 } else if (adj == ya1) {
696 n.y -= adj / 2;
697 p.y += adj / 2;
698 } else if (adj == ya2) {
699 n.y += adj / 2;
700 p.y -= adj / 2;
701 }
702 }
703 return ix;
704 }
705 });
706
707 });
708 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700709
710 function tick(e) {
711 network.numTicks++;
712
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700713 if (config.options.layering) {
Simon Hunt68ae6652014-10-22 13:58:07 -0700714 // adjust the y-coord of each node, based on y-pos constraints
715 network.nodes.forEach(function (n) {
716 var z = e.alpha * n.constraint.weight;
717 if (!isNaN(n.constraint.y)) {
718 n.y = (n.constraint.y * z + n.y * (1 - z));
719 }
720 });
721 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700722
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700723 if (config.options.collisionPrevention && network.preventCollisions) {
Simon Hunt1c219892014-10-22 16:32:39 -0700724 preventCollisions();
725 }
726
Simon Huntd35961b2014-10-28 08:49:48 -0700727 // clip visualization of links at bounds of nodes...
728 network.link.each(function(d) {
729 var xs = d.source.x,
730 ys = d.source.y,
731 xt = d.target.x,
732 yt = d.target.y,
733 line = new geo.LineSegment(xs, ys, xt, yt),
734 e, ix;
Simon Hunt1c219892014-10-22 16:32:39 -0700735
Simon Huntd35961b2014-10-28 08:49:48 -0700736 for (e in d.source.edge) {
737 ix = line.intersect(d.source.edge[e].offset(xs, ys));
Simon Hunt1c219892014-10-22 16:32:39 -0700738 if (ix.in1 && ix.in2) {
Simon Huntd35961b2014-10-28 08:49:48 -0700739 xs = ix.x;
740 ys = ix.y;
741 break;
742 }
743 }
744
745 for (e in d.target.edge) {
746 ix = line.intersect(d.target.edge[e].offset(xt, yt));
747 if (ix.in1 && ix.in2) {
748 xt = ix.x;
749 yt = ix.y;
Simon Hunt1c219892014-10-22 16:32:39 -0700750 break;
751 }
752 }
753
754 d3.select(this)
Simon Huntd35961b2014-10-28 08:49:48 -0700755 .attr('x1', xs)
756 .attr('y1', ys)
757 .attr('x2', xt)
758 .attr('y2', yt);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700759 });
760
Simon Huntd35961b2014-10-28 08:49:48 -0700761 // position each node by translating the node (group) by x,y
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700762 network.node
763 .attr('transform', function(d) {
764 return translate(d.x, d.y);
765 });
766
767 }
768
769 // $('#docs-close').on('click', function() {
770 // deselectObject();
771 // return false;
772 // });
773
774 // $(document).on('click', '.select-object', function() {
775 // var obj = graph.data[$(this).data('name')];
776 // if (obj) {
777 // selectObject(obj);
778 // }
779 // return false;
780 // });
781
Simon Hunt6f376a32014-10-28 12:38:30 -0700782 function findNodeFromData(d) {
783 var el = null;
784 network.node.filter('.' + d.class).each(function(n) {
785 if (n.id === d.id) {
786 el = d3.select(this);
787 }
788 });
789 return el;
790 }
791
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700792 function selectObject(obj, el) {
793 var node;
794 if (el) {
795 node = d3.select(el);
796 } else {
797 network.node.each(function(d) {
798 if (d == obj) {
799 node = d3.select(el = this);
800 }
801 });
802 }
803 if (!node) return;
804
805 if (node.classed('selected')) {
806 deselectObject();
807 return;
808 }
809 deselectObject(false);
810
811 selected = {
812 obj : obj,
813 el : el
814 };
815
816 highlightObject(obj);
817
818 node.classed('selected', true);
819
820 // TODO animate incoming info pane
821 // resize(true);
822 // TODO: check bounds of selected node and scroll into view if needed
823 }
824
825 function deselectObject(doResize) {
826 // Review: logic of 'resize(...)' function.
827 if (doResize || typeof doResize == 'undefined') {
828 resize(false);
829 }
830 // deselect all nodes in the network...
831 network.node.classed('selected', false);
832 selected = {};
833 highlightObject(null);
834 }
835
836 function highlightObject(obj) {
837 if (obj) {
838 if (obj != highlighted) {
839 // TODO set or clear "inactive" class on nodes, based on criteria
840 network.node.classed('inactive', function(d) {
841 // return (obj !== d &&
842 // d.relation(obj.id));
843 return (obj !== d);
844 });
845 // TODO: same with links
846 network.link.classed('inactive', function(d) {
847 return (obj !== d.source && obj !== d.target);
848 });
849 }
850 highlighted = obj;
851 } else {
852 if (highlighted) {
853 // clear the inactive flag (no longer suppressed visually)
854 network.node.classed('inactive', false);
855 network.link.classed('inactive', false);
856 }
857 highlighted = null;
858
859 }
860 }
861
862 function resize(showDetails) {
863 console.log("resize() called...");
864
865 var $details = $('#details');
866
867 if (typeof showDetails == 'boolean') {
868 var showingDetails = showDetails;
869 // TODO: invoke $details.show() or $details.hide()...
870 // $details[showingDetails ? 'show' : 'hide']();
871 }
872
873 view.height = window.innerHeight - config.mastHeight;
874 view.width = window.innerWidth;
875 $('#view')
876 .css('height', view.height + 'px')
877 .css('width', view.width + 'px');
878
879 network.forceWidth = view.width - config.force.marginLR;
880 network.forceHeight = view.height - config.force.marginTB;
881 }
882
883 // ======================================================================
884 // register with the UI framework
885
886 api.addView('network', {
887 load: loadNetworkView
888 });
889
890
891}(ONOS));
892