blob: c4e72f1cd07a4950e9263d3d17855195fb1e6ed0 [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: {
Simon Hunt9a16c822014-10-28 16:09:19 -070033 showNodeXY: false,
34 showKeyHandler: true
Simon Huntd35961b2014-10-28 08:49:48 -070035 },
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,
Simon Hunt9a16c822014-10-28 16:09:19 -070053 host: 40
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,
Simon Hunt9a16c822014-10-28 16:09:19 -070061 host: -1000
Simon Hunt2c9e0c22014-10-23 15:12:58 -070062 },
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 Hunt9a16c822014-10-28 16:09:19 -070087 host: 0.05,
Simon Hunt2c9e0c22014-10-23 15:12:58 -070088 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,
Simon Hunt9a16c822014-10-28 16:09:19 -0700102 hovered = null,
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700103 viewMode = 'showAll';
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700104
105
Simon Huntd35961b2014-10-28 08:49:48 -0700106 function debug(what) {
107 return config.debugOn && config.debug[what];
108 }
109
110 // load the topology view of the network
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700111 function loadNetworkView() {
112 // Hey, here I am, calling something on the ONOS api:
113 api.printTime();
114
115 resize();
116
Simon Huntd35961b2014-10-28 08:49:48 -0700117 // go get our network data from the server...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700118 d3.json(config.jsonUrl, function (err, data) {
119 if (err) {
120 alert('Oops! Error reading JSON...\n\n' +
Simon Huntae968a62014-10-22 14:54:41 -0700121 'URL: ' + config.jsonUrl + '\n\n' +
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700122 'Error: ' + err.message);
123 return;
124 }
Simon Huntd35961b2014-10-28 08:49:48 -0700125// console.log("here is the JSON data...");
126// console.log(data);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700127
128 network.data = data;
129 drawNetwork();
130 });
131
Simon Huntd35961b2014-10-28 08:49:48 -0700132 // while we wait for the data, set up the handlers...
133 setUpClickHandler();
134 setUpRadioButtonHandler();
135 setUpKeyHandler();
136 $(window).on('resize', resize);
137 }
138
139 function setUpClickHandler() {
140 // click handler for "selectable" objects
141 $(document).on('click', '.select-object', function () {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700142 // when any object of class "select-object" is clicked...
143 // TODO: get a reference to the object via lookup...
144 var obj = network.lookup[$(this).data('id')];
145 if (obj) {
146 selectObject(obj);
147 }
148 // stop propagation of event (I think) ...
149 return false;
150 });
Simon Huntd35961b2014-10-28 08:49:48 -0700151 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700152
Simon Huntd35961b2014-10-28 08:49:48 -0700153 function setUpRadioButtonHandler() {
154 d3.selectAll('#displayModes .radio').on('click', function () {
155 var id = d3.select(this).attr('id');
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700156 if (id !== viewMode) {
157 radioButton('displayModes', id);
158 viewMode = id;
Simon Huntd35961b2014-10-28 08:49:48 -0700159 alert('action: ' + id);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700160 }
161 });
162 }
163
Simon Huntd35961b2014-10-28 08:49:48 -0700164 function setUpKeyHandler() {
165 d3.select('body')
166 .on('keydown', function () {
167 processKeyEvent();
168 if (debug('showKeyHandler')) {
169 network.svg.append('text')
170 .attr('x', 5)
171 .attr('y', 15)
172 .style('font-size', '20pt')
173 .text('keyCode: ' + d3.event.keyCode +
174 ' applied to : ' + contextLabel())
175 .transition().duration(2000)
176 .style('font-size', '2pt')
177 .style('fill-opacity', 0.01)
178 .remove();
179 }
180 });
181 }
182
Simon Hunt9a16c822014-10-28 16:09:19 -0700183 function contextLabel() {
184 return hovered === null ? "(nothing)" : hovered.id;
185 }
186
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700187 function radioButton(group, id) {
188 d3.selectAll("#" + group + " .radio").classed("active", false);
189 d3.select("#" + group + " #" + id).classed("active", true);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700190 }
191
Simon Huntd35961b2014-10-28 08:49:48 -0700192 function processKeyEvent() {
193 var code = d3.event.keyCode;
194 switch (code) {
195 case 76: // L
196 cycleLabels();
197 break;
198 case 80: // P
199 togglePorts();
Simon Hunt9a16c822014-10-28 16:09:19 -0700200 break;
201 case 85: // U
202 unpin();
203 break;
Simon Huntd35961b2014-10-28 08:49:48 -0700204 }
205
206 }
207
208 function cycleLabels() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700209 console.log('Cycle Labels - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700210 }
211
212 function togglePorts() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700213 console.log('Toggle Ports - context = ' + contextLabel());
214 }
215
216 function unpin() {
217 if (hovered) {
218 hovered.fixed = false;
219 network.force.resume();
220 }
221 console.log('Unpin - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700222 }
223
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700224
225 // ========================================================
226
227 function drawNetwork() {
228 $('#view').empty();
229
230 prepareNodesAndLinks();
231 createLayout();
232 console.log("\n\nHere is the augmented network object...");
Simon Hunt9a16c822014-10-28 16:09:19 -0700233 console.log(network);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700234 }
235
236 function prepareNodesAndLinks() {
237 network.lookup = {};
238 network.nodes = [];
239 network.links = [];
240
241 var nw = network.forceWidth,
242 nh = network.forceHeight;
243
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700244 function yPosConstraintForNode(n) {
245 return config.constraints.ypos[n.type || 'host'];
246 }
247
248 // Note that both 'devices' and 'hosts' get mapped into the nodes array
249
250 // first, the devices...
251 network.data.devices.forEach(function(n) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700252 var ypc = yPosConstraintForNode(n),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700253 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700254 iy = ypc * nh,
255 node = {
256 id: n.id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700257 labels: n.labels,
258 class: 'device',
259 icon: 'device',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700260 type: n.type,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700261 x: ix,
262 y: iy,
263 constraint: {
264 weight: 0.7,
265 y: iy
266 }
267 };
268 network.lookup[n.id] = node;
269 network.nodes.push(node);
270 });
271
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700272 // then, the hosts...
273 network.data.hosts.forEach(function(n) {
274 var ypc = yPosConstraintForNode(n),
275 ix = Math.random() * 0.6 * nw + 0.2 * nw,
276 iy = ypc * nh,
277 node = {
278 id: n.id,
279 labels: n.labels,
280 class: 'host',
281 icon: 'host',
282 type: n.type,
283 x: ix,
284 y: iy,
285 constraint: {
286 weight: 0.7,
287 y: iy
288 }
289 };
290 network.lookup[n.id] = node;
291 network.nodes.push(node);
292 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700293
294
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700295 // now, process the explicit links...
Simon Hunt6f376a32014-10-28 12:38:30 -0700296 network.data.links.forEach(function(lnk) {
297 var src = network.lookup[lnk.src],
298 dst = network.lookup[lnk.dst],
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700299 id = src.id + "~" + dst.id;
300
301 var link = {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700302 class: 'infra',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700303 id: id,
Simon Hunt6f376a32014-10-28 12:38:30 -0700304 type: lnk.type,
305 width: lnk.linkWidth,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700306 source: src,
307 target: dst,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700308 strength: config.force.linkStrength.infra
309 };
310 network.links.push(link);
311 });
312
313 // finally, infer host links...
314 network.data.hosts.forEach(function(n) {
315 var src = network.lookup[n.id],
316 dst = network.lookup[n.cp.device],
317 id = src.id + "~" + dst.id;
318
319 var link = {
320 class: 'host',
321 id: id,
322 type: 'hostLink',
323 width: config.hostLinkWidth,
324 source: src,
325 target: dst,
326 strength: config.force.linkStrength.host
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700327 };
328 network.links.push(link);
329 });
330 }
331
332 function createLayout() {
333
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700334 var cfg = config.force;
335
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700336 network.force = d3.layout.force()
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700337 .size([network.forceWidth, network.forceHeight])
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700338 .nodes(network.nodes)
339 .links(network.links)
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700340 .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
341 .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
342 .charge(function(d) { return cfg.charge[d.class]; })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700343 .on('tick', tick);
344
345 network.svg = d3.select('#view').append('svg')
346 .attr('width', view.width)
347 .attr('height', view.height)
348 .append('g')
Simon Huntae968a62014-10-22 14:54:41 -0700349 .attr('transform', config.force.translate());
Simon Hunt3ab76a82014-10-22 13:07:32 -0700350// .attr('id', 'zoomable')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700351// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700352
Simon Hunt3ab76a82014-10-22 13:07:32 -0700353// function zoomRedraw() {
354// d3.select("#zoomable").attr("transform",
355// "translate(" + d3.event.translate + ")"
356// + " scale(" + d3.event.scale + ")");
357// }
358
Simon Hunt3ab76a82014-10-22 13:07:32 -0700359 // TODO: move glow/blur stuff to util script
360 var glow = network.svg.append('filter')
361 .attr('x', '-50%')
362 .attr('y', '-50%')
363 .attr('width', '200%')
364 .attr('height', '200%')
365 .attr('id', 'blue-glow');
366
367 glow.append('feColorMatrix')
368 .attr('type', 'matrix')
369 .attr('values', '0 0 0 0 0 ' +
370 '0 0 0 0 0 ' +
371 '0 0 0 0 .7 ' +
372 '0 0 0 1 0 ');
373
374 glow.append('feGaussianBlur')
375 .attr('stdDeviation', 3)
376 .attr('result', 'coloredBlur');
377
378 glow.append('feMerge').selectAll('feMergeNode')
379 .data(['coloredBlur', 'SourceGraphic'])
380 .enter().append('feMergeNode')
381 .attr('in', String);
382
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700383 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700384// $('#view').on('scroll', function() {
385//
386// });
387
388
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700389 // add links to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700390 network.link = network.svg.append('g').selectAll('.link')
391 .data(network.force.links(), function(d) {return d.id})
392 .enter().append('line')
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700393 .attr('class', function(d) {return 'link ' + d.class});
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700394
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700395
Simon Hunt6f376a32014-10-28 12:38:30 -0700396 // TODO: move drag behavior into separate method.
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700397 // == define node drag behavior...
Simon Hunt3ab76a82014-10-22 13:07:32 -0700398 network.draggedThreshold = d3.scale.linear()
399 .domain([0, 0.1])
400 .range([5, 20])
401 .clamp(true);
402
403 function dragged(d) {
404 var threshold = network.draggedThreshold(network.force.alpha()),
405 dx = d.oldX - d.px,
406 dy = d.oldY - d.py;
407 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
408 d.dragged = true;
409 }
410 return d.dragged;
411 }
412
413 network.drag = d3.behavior.drag()
414 .origin(function(d) { return d; })
415 .on('dragstart', function(d) {
416 d.oldX = d.x;
417 d.oldY = d.y;
418 d.dragged = false;
419 d.fixed |= 2;
420 })
421 .on('drag', function(d) {
422 d.px = d3.event.x;
423 d.py = d3.event.y;
424 if (dragged(d)) {
425 if (!network.force.alpha()) {
426 network.force.alpha(.025);
427 }
428 }
429 })
430 .on('dragend', function(d) {
431 if (!dragged(d)) {
432 selectObject(d, this);
433 }
434 d.fixed &= ~6;
Simon Hunt9a16c822014-10-28 16:09:19 -0700435
436 // once we've finished moving, pin the node in position,
437 // if it is a device
438 if (d.class === 'device') {
439 d.fixed = true;
440 }
Simon Hunt3ab76a82014-10-22 13:07:32 -0700441 });
442
443 $('#view').on('click', function(e) {
444 if (!$(e.target).closest('.node').length) {
445 deselectObject();
446 }
447 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700448
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700449
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700450 // add nodes to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700451 network.node = network.svg.selectAll('.node')
452 .data(network.force.nodes(), function(d) {return d.id})
453 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700454 .attr('class', function(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700455 var cls = 'node ' + d.class;
456 if (d.type) {
457 cls += ' ' + d.type;
458 }
459 return cls;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700460 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700461 .attr('transform', function(d) {
462 return translate(d.x, d.y);
463 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700464 .call(network.drag)
465 .on('mouseover', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700466 // TODO: show tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700467 if (network.mouseoutTimeout) {
468 clearTimeout(network.mouseoutTimeout);
469 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700470 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700471 hoverObject(d);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700472 })
473 .on('mouseout', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700474 // TODO: hide tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700475 if (network.mouseoutTimeout) {
476 clearTimeout(network.mouseoutTimeout);
477 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700478 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700479 network.mouseoutTimeout = setTimeout(function() {
480 hoverObject(null);
481 }, config.mouseOutTimerDelayMs);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700482 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700483
Simon Hunt6f376a32014-10-28 12:38:30 -0700484
485 // deal with device nodes first
486 network.nodeRect = network.node.filter('.device')
487 .append('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700488 .attr({
489 rx: 5,
490 ry: 5,
491 width: 100,
492 height: 12
493 });
494 // note that width/height are adjusted to fit the label text
Simon Hunt6f376a32014-10-28 12:38:30 -0700495 // then padded, and space made for the icon.
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700496
Simon Hunt6f376a32014-10-28 12:38:30 -0700497 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700498 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700499 icon = iconUrl(d);
500
501 node.append('text')
502 // TODO: add label cycle behavior
503 .text(d.id)
504 .attr('dy', '1.1em');
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700505
506 if (icon) {
507 var cfg = config.icons;
508 node.append('svg:image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700509 .attr({
510 width: cfg.w,
511 height: cfg.h,
512 'xlink:href': icon
513 });
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700514 // note, icon relative positioning (x,y) is done after we have
515 // adjusted the bounds of the rectangle...
516 }
Simon Hunt68ae6652014-10-22 13:58:07 -0700517
Simon Huntd35961b2014-10-28 08:49:48 -0700518 // debug function to show the modelled x,y coordinates of nodes...
519 if (debug('showNodeXY')) {
520 node.select('rect').attr('fill-opacity', 0.5);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700521 node.append('circle')
522 .attr({
523 class: 'debug',
524 cx: 0,
525 cy: 0,
526 r: '3px'
527 });
528 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700529 });
530
Simon Hunt6f376a32014-10-28 12:38:30 -0700531 // now process host nodes
532 network.nodeCircle = network.node.filter('.host')
533 .append('circle')
534 .attr({
535 r: config.hostRadius
536 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700537
Simon Hunt6f376a32014-10-28 12:38:30 -0700538 network.node.filter('.host').each(function(d) {
539 var node = d3.select(this),
540 icon = iconUrl(d);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700541
Simon Hunt6f376a32014-10-28 12:38:30 -0700542 // debug function to show the modelled x,y coordinates of nodes...
543 if (debug('showNodeXY')) {
544 node.select('circle').attr('fill-opacity', 0.5);
545 node.append('circle')
546 .attr({
547 class: 'debug',
548 cx: 0,
549 cy: 0,
550 r: '3px'
551 });
552 }
553 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700554
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700555 // this function is scheduled to happen soon after the given thread ends
556 setTimeout(function() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700557 // post process the device nodes, to pad their size to fit the
558 // label text and attach the icon to the right location.
559 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700560 // for every node, recompute size, padding, etc. so text fits
561 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700562 text = node.select('text'),
563 box = adjustRectToFitText(node),
564 lab = config.labels;
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700565
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700566 // now make the computed adjustment
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700567 node.select('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700568 .attr(box);
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700569
570 node.select('image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700571 .attr('x', box.x + config.icons.xoff)
572 .attr('y', box.y + config.icons.yoff);
Simon Hunt1c219892014-10-22 16:32:39 -0700573
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700574 var bounds = boundsFromBox(box);
575
576 // todo: clean up extent and edge work..
Simon Hunt1c219892014-10-22 16:32:39 -0700577 d.extent = {
578 left: bounds.x1 - lab.marginLR,
579 right: bounds.x2 + lab.marginLR,
580 top: bounds.y1 - lab.marginTB,
581 bottom: bounds.y2 + lab.marginTB
582 };
583
584 d.edge = {
585 left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
586 right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
587 top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
588 bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
589 };
590
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700591 });
592
593 network.numTicks = 0;
594 network.preventCollisions = false;
595 network.force.start();
Simon Hunt1c219892014-10-22 16:32:39 -0700596 for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700597 network.force.tick();
598 }
599 network.preventCollisions = true;
600 $('#view').css('visibility', 'visible');
601 });
602
Simon Hunt6f376a32014-10-28 12:38:30 -0700603
604 // returns the newly computed bounding box of the rectangle
605 function adjustRectToFitText(n) {
606 var text = n.select('text'),
607 box = text.node().getBBox(),
608 lab = config.labels;
609
Simon Hunt9a16c822014-10-28 16:09:19 -0700610 // not sure why n.data() returns an array of 1 element...
611 var data = n.data()[0];
612
Simon Hunt6f376a32014-10-28 12:38:30 -0700613 text.attr('text-anchor', 'middle')
614 .attr('y', '-0.8em')
615 .attr('x', lab.imgPad/2)
616 ;
617
Simon Hunt6f376a32014-10-28 12:38:30 -0700618 // translate the bbox so that it is centered on [x,y]
619 box.x = -box.width / 2;
620 box.y = -box.height / 2;
621
622 // add padding
623 box.x -= (lab.padLR + lab.imgPad/2);
624 box.width += lab.padLR * 2 + lab.imgPad;
625 box.y -= lab.padTB;
626 box.height += lab.padTB * 2;
627
628 return box;
629 }
630
631 function boundsFromBox(box) {
632 return {
633 x1: box.x,
634 y1: box.y,
635 x2: box.x + box.width,
636 y2: box.y + box.height
637 };
638 }
639
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700640 }
641
Simon Hunt68ae6652014-10-22 13:58:07 -0700642 function iconUrl(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700643 return config.iconUrl[d.icon];
Simon Hunt68ae6652014-10-22 13:58:07 -0700644 }
645
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700646 function translate(x, y) {
647 return 'translate(' + x + ',' + y + ')';
648 }
649
Simon Hunt6f376a32014-10-28 12:38:30 -0700650 // prevents collisions amongst device nodes
Simon Hunt1c219892014-10-22 16:32:39 -0700651 function preventCollisions() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700652 var quadtree = d3.geom.quadtree(network.nodes),
653 hrad = config.hostRadius;
Simon Hunt1c219892014-10-22 16:32:39 -0700654
655 network.nodes.forEach(function(n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700656 var nx1, nx2, ny1, ny2;
657
658 if (n.class === 'device') {
659 nx1 = n.x + n.extent.left;
660 nx2 = n.x + n.extent.right;
661 ny1 = n.y + n.extent.top;
Simon Hunt1c219892014-10-22 16:32:39 -0700662 ny2 = n.y + n.extent.bottom;
663
Simon Hunt6f376a32014-10-28 12:38:30 -0700664 } else {
665 nx1 = n.x - hrad;
666 nx2 = n.x + hrad;
667 ny1 = n.y - hrad;
668 ny2 = n.y + hrad;
669 }
670
Simon Hunt1c219892014-10-22 16:32:39 -0700671 quadtree.visit(function(quad, x1, y1, x2, y2) {
672 if (quad.point && quad.point !== n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700673 // check if the rectangles/circles intersect
Simon Hunt1c219892014-10-22 16:32:39 -0700674 var p = quad.point,
Simon Hunt6f376a32014-10-28 12:38:30 -0700675 px1, px2, py1, py2, ix;
676
677 if (p.class === 'device') {
678 px1 = p.x + p.extent.left;
679 px2 = p.x + p.extent.right;
680 py1 = p.y + p.extent.top;
681 py2 = p.y + p.extent.bottom;
682
683 } else {
684 px1 = p.x - hrad;
685 px2 = p.x + hrad;
686 py1 = p.y - hrad;
687 py2 = p.y + hrad;
688 }
689
690 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
691
Simon Hunt1c219892014-10-22 16:32:39 -0700692 if (ix) {
693 var xa1 = nx2 - px1, // shift n left , p right
694 xa2 = px2 - nx1, // shift n right, p left
695 ya1 = ny2 - py1, // shift n up , p down
696 ya2 = py2 - ny1, // shift n down , p up
697 adj = Math.min(xa1, xa2, ya1, ya2);
698
699 if (adj == xa1) {
700 n.x -= adj / 2;
701 p.x += adj / 2;
702 } else if (adj == xa2) {
703 n.x += adj / 2;
704 p.x -= adj / 2;
705 } else if (adj == ya1) {
706 n.y -= adj / 2;
707 p.y += adj / 2;
708 } else if (adj == ya2) {
709 n.y += adj / 2;
710 p.y -= adj / 2;
711 }
712 }
713 return ix;
714 }
715 });
716
717 });
718 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700719
720 function tick(e) {
721 network.numTicks++;
722
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700723 if (config.options.layering) {
Simon Hunt68ae6652014-10-22 13:58:07 -0700724 // adjust the y-coord of each node, based on y-pos constraints
725 network.nodes.forEach(function (n) {
726 var z = e.alpha * n.constraint.weight;
727 if (!isNaN(n.constraint.y)) {
728 n.y = (n.constraint.y * z + n.y * (1 - z));
729 }
730 });
731 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700732
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700733 if (config.options.collisionPrevention && network.preventCollisions) {
Simon Hunt1c219892014-10-22 16:32:39 -0700734 preventCollisions();
735 }
736
Simon Huntd35961b2014-10-28 08:49:48 -0700737 // clip visualization of links at bounds of nodes...
738 network.link.each(function(d) {
739 var xs = d.source.x,
740 ys = d.source.y,
741 xt = d.target.x,
742 yt = d.target.y,
743 line = new geo.LineSegment(xs, ys, xt, yt),
744 e, ix;
Simon Hunt1c219892014-10-22 16:32:39 -0700745
Simon Huntd35961b2014-10-28 08:49:48 -0700746 for (e in d.source.edge) {
747 ix = line.intersect(d.source.edge[e].offset(xs, ys));
Simon Hunt1c219892014-10-22 16:32:39 -0700748 if (ix.in1 && ix.in2) {
Simon Huntd35961b2014-10-28 08:49:48 -0700749 xs = ix.x;
750 ys = ix.y;
751 break;
752 }
753 }
754
755 for (e in d.target.edge) {
756 ix = line.intersect(d.target.edge[e].offset(xt, yt));
757 if (ix.in1 && ix.in2) {
758 xt = ix.x;
759 yt = ix.y;
Simon Hunt1c219892014-10-22 16:32:39 -0700760 break;
761 }
762 }
763
764 d3.select(this)
Simon Huntd35961b2014-10-28 08:49:48 -0700765 .attr('x1', xs)
766 .attr('y1', ys)
767 .attr('x2', xt)
768 .attr('y2', yt);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700769 });
770
Simon Huntd35961b2014-10-28 08:49:48 -0700771 // position each node by translating the node (group) by x,y
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700772 network.node
773 .attr('transform', function(d) {
774 return translate(d.x, d.y);
775 });
776
777 }
778
779 // $('#docs-close').on('click', function() {
780 // deselectObject();
781 // return false;
782 // });
783
784 // $(document).on('click', '.select-object', function() {
785 // var obj = graph.data[$(this).data('name')];
786 // if (obj) {
787 // selectObject(obj);
788 // }
789 // return false;
790 // });
791
Simon Hunt6f376a32014-10-28 12:38:30 -0700792 function findNodeFromData(d) {
793 var el = null;
794 network.node.filter('.' + d.class).each(function(n) {
795 if (n.id === d.id) {
796 el = d3.select(this);
797 }
798 });
799 return el;
800 }
801
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700802 function selectObject(obj, el) {
803 var node;
804 if (el) {
805 node = d3.select(el);
806 } else {
807 network.node.each(function(d) {
808 if (d == obj) {
809 node = d3.select(el = this);
810 }
811 });
812 }
813 if (!node) return;
814
815 if (node.classed('selected')) {
816 deselectObject();
817 return;
818 }
819 deselectObject(false);
820
821 selected = {
822 obj : obj,
823 el : el
824 };
825
Simon Hunt9a16c822014-10-28 16:09:19 -0700826// highlightObject(obj);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700827
828 node.classed('selected', true);
829
830 // TODO animate incoming info pane
831 // resize(true);
832 // TODO: check bounds of selected node and scroll into view if needed
833 }
834
835 function deselectObject(doResize) {
836 // Review: logic of 'resize(...)' function.
837 if (doResize || typeof doResize == 'undefined') {
838 resize(false);
839 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700840
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700841 // deselect all nodes in the network...
842 network.node.classed('selected', false);
843 selected = {};
Simon Hunt9a16c822014-10-28 16:09:19 -0700844// highlightObject(null);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700845 }
846
847 function highlightObject(obj) {
848 if (obj) {
849 if (obj != highlighted) {
850 // TODO set or clear "inactive" class on nodes, based on criteria
851 network.node.classed('inactive', function(d) {
852 // return (obj !== d &&
853 // d.relation(obj.id));
854 return (obj !== d);
855 });
856 // TODO: same with links
857 network.link.classed('inactive', function(d) {
858 return (obj !== d.source && obj !== d.target);
859 });
860 }
861 highlighted = obj;
862 } else {
863 if (highlighted) {
864 // clear the inactive flag (no longer suppressed visually)
865 network.node.classed('inactive', false);
866 network.link.classed('inactive', false);
867 }
868 highlighted = null;
869
870 }
871 }
872
Simon Hunt9a16c822014-10-28 16:09:19 -0700873 function hoverObject(obj) {
874 if (obj) {
875 hovered = obj;
876 } else {
877 if (hovered) {
878 hovered = null;
879 }
880 }
881 }
882
883
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700884 function resize(showDetails) {
885 console.log("resize() called...");
886
887 var $details = $('#details');
888
889 if (typeof showDetails == 'boolean') {
890 var showingDetails = showDetails;
891 // TODO: invoke $details.show() or $details.hide()...
892 // $details[showingDetails ? 'show' : 'hide']();
893 }
894
895 view.height = window.innerHeight - config.mastHeight;
896 view.width = window.innerWidth;
897 $('#view')
898 .css('height', view.height + 'px')
899 .css('width', view.width + 'px');
900
901 network.forceWidth = view.width - config.force.marginLR;
902 network.forceHeight = view.height - config.force.marginTB;
903 }
904
905 // ======================================================================
906 // register with the UI framework
907
908 api.addView('network', {
909 load: loadNetworkView
910 });
911
912
913}(ONOS));
914