blob: 578aa3a1906a7134d98ca30771a7d4555f5a819c [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 Vachuska1de66012014-10-30 03:03:30 -070040 jsonUrl: 'rs/topology/graph',
41 jsonPrefix: '',
42 XjsonUrl: 'json/network.json',
43 XjsonPrefix: 'json/',
Simon Hunt68ae6652014-10-22 13:58:07 -070044 iconUrl: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070045 device: 'img/device.png',
46 host: 'img/host.png',
47 pkt: 'img/pkt.png',
48 opt: 'img/opt.png'
Simon Hunt68ae6652014-10-22 13:58:07 -070049 },
Simon Hunt19cb0982014-10-23 16:44:49 -070050 mastHeight: 36,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070051 force: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070052 note: 'node.class or link.class is used to differentiate',
53 linkDistance: {
Simon Hunt6f376a32014-10-28 12:38:30 -070054 infra: 200,
Simon Hunt9a16c822014-10-28 16:09:19 -070055 host: 40
Simon Hunt2c9e0c22014-10-23 15:12:58 -070056 },
57 linkStrength: {
58 infra: 1.0,
Simon Hunt6f376a32014-10-28 12:38:30 -070059 host: 1.0
Simon Hunt2c9e0c22014-10-23 15:12:58 -070060 },
61 charge: {
62 device: -800,
Simon Hunt9a16c822014-10-28 16:09:19 -070063 host: -1000
Simon Hunt2c9e0c22014-10-23 15:12:58 -070064 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070065 ticksWithoutCollisions: 50,
66 marginLR: 20,
67 marginTB: 20,
68 translate: function() {
69 return 'translate(' +
70 config.force.marginLR + ',' +
71 config.force.marginTB + ')';
72 }
73 },
74 labels: {
Simon Hunt19cb0982014-10-23 16:44:49 -070075 imgPad: 16,
Simon Hunt1c5f8b62014-10-22 14:43:01 -070076 padLR: 8,
77 padTB: 6,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070078 marginLR: 3,
79 marginTB: 2
80 },
Simon Hunt2c9e0c22014-10-23 15:12:58 -070081 icons: {
82 w: 32,
83 h: 32,
84 xoff: -12,
Simon Hunt19cb0982014-10-23 16:44:49 -070085 yoff: -8
Simon Hunt2c9e0c22014-10-23 15:12:58 -070086 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070087 constraints: {
88 ypos: {
Simon Hunt9a16c822014-10-28 16:09:19 -070089 host: 0.05,
Simon Hunt2c9e0c22014-10-23 15:12:58 -070090 switch: 0.3,
91 roadm: 0.7
Simon Hunt0b05d4a2014-10-21 21:50:15 -070092 }
Simon Hunt2c9e0c22014-10-23 15:12:58 -070093 },
94 hostLinkWidth: 1.0,
Simon Hunt6f376a32014-10-28 12:38:30 -070095 hostRadius: 7,
Simon Hunt2c9e0c22014-10-23 15:12:58 -070096 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,
Simon Hunt9a16c822014-10-28 16:09:19 -0700104 hovered = null,
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700105 viewMode = 'showAll';
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700106
107
Simon Huntd35961b2014-10-28 08:49:48 -0700108 function debug(what) {
109 return config.debugOn && config.debug[what];
110 }
111
112 // load the topology view of the network
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700113 function loadNetworkView() {
114 // Hey, here I am, calling something on the ONOS api:
115 api.printTime();
116
117 resize();
118
Simon Huntd35961b2014-10-28 08:49:48 -0700119 // go get our network data from the server...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700120 d3.json(config.jsonUrl, function (err, data) {
121 if (err) {
122 alert('Oops! Error reading JSON...\n\n' +
Simon Huntae968a62014-10-22 14:54:41 -0700123 'URL: ' + config.jsonUrl + '\n\n' +
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700124 'Error: ' + err.message);
125 return;
126 }
Simon Huntd35961b2014-10-28 08:49:48 -0700127// console.log("here is the JSON data...");
128// console.log(data);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700129
130 network.data = data;
131 drawNetwork();
132 });
133
Simon Huntd35961b2014-10-28 08:49:48 -0700134 // while we wait for the data, set up the handlers...
135 setUpClickHandler();
136 setUpRadioButtonHandler();
137 setUpKeyHandler();
138 $(window).on('resize', resize);
139 }
140
141 function setUpClickHandler() {
142 // click handler for "selectable" objects
143 $(document).on('click', '.select-object', function () {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700144 // when any object of class "select-object" is clicked...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700145 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 Huntf967d512014-10-28 20:34:29 -0700160 doRadioAction(id);
161 }
162 });
163 }
164
165 function doRadioAction(id) {
166 showAllLayers();
167 if (id === 'showPkt') {
168 showPacketLayer();
169 } else if (id === 'showOpt') {
170 showOpticalLayer();
171 }
172 }
173
174 function showAllLayers() {
175 network.node.classed('inactive', false);
176 network.link.classed('inactive', false);
177 }
178
179 function showPacketLayer() {
180 network.node.each(function(d) {
181 // deactivate nodes that are not hosts or switches
182 if (d.class === 'device' && d.type !== 'switch') {
183 d3.select(this).classed('inactive', true);
184 }
185 });
186
187 network.link.each(function(lnk) {
188 // deactivate infrastructure links that have opt's as endpoints
189 if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') {
190 d3.select(this).classed('inactive', true);
191 }
192 });
193 }
194
195 function showOpticalLayer() {
196 network.node.each(function(d) {
197 // deactivate nodes that are not optical devices
198 if (d.type !== 'roadm') {
199 d3.select(this).classed('inactive', true);
200 }
201 });
202
203 network.link.each(function(lnk) {
204 // deactivate infrastructure links that have opt's as endpoints
205 if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') {
206 d3.select(this).classed('inactive', true);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700207 }
208 });
209 }
210
Simon Huntd35961b2014-10-28 08:49:48 -0700211 function setUpKeyHandler() {
212 d3.select('body')
213 .on('keydown', function () {
214 processKeyEvent();
215 if (debug('showKeyHandler')) {
216 network.svg.append('text')
217 .attr('x', 5)
218 .attr('y', 15)
219 .style('font-size', '20pt')
220 .text('keyCode: ' + d3.event.keyCode +
221 ' applied to : ' + contextLabel())
222 .transition().duration(2000)
223 .style('font-size', '2pt')
224 .style('fill-opacity', 0.01)
225 .remove();
226 }
227 });
228 }
229
Simon Hunt9a16c822014-10-28 16:09:19 -0700230 function contextLabel() {
231 return hovered === null ? "(nothing)" : hovered.id;
232 }
233
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700234 function radioButton(group, id) {
235 d3.selectAll("#" + group + " .radio").classed("active", false);
236 d3.select("#" + group + " #" + id).classed("active", true);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700237 }
238
Simon Huntd35961b2014-10-28 08:49:48 -0700239 function processKeyEvent() {
240 var code = d3.event.keyCode;
241 switch (code) {
Thomas Vachuska1de66012014-10-30 03:03:30 -0700242 case 71: // G
243 cycleLayout();
244 break;
Simon Huntd35961b2014-10-28 08:49:48 -0700245 case 76: // L
246 cycleLabels();
247 break;
248 case 80: // P
249 togglePorts();
Simon Hunt9a16c822014-10-28 16:09:19 -0700250 break;
251 case 85: // U
252 unpin();
253 break;
Simon Huntd35961b2014-10-28 08:49:48 -0700254 }
255
256 }
257
Thomas Vachuska1de66012014-10-30 03:03:30 -0700258 function cycleLayout() {
259 config.options.layering = !config.options.layering;
260 network.force.resume();
261 }
262
Simon Huntd35961b2014-10-28 08:49:48 -0700263 function cycleLabels() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700264 console.log('Cycle Labels - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700265 }
266
267 function togglePorts() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700268 console.log('Toggle Ports - context = ' + contextLabel());
269 }
270
271 function unpin() {
272 if (hovered) {
273 hovered.fixed = false;
Simon Huntf967d512014-10-28 20:34:29 -0700274 findNodeFromData(hovered).classed('fixed', false);
Simon Hunt9a16c822014-10-28 16:09:19 -0700275 network.force.resume();
276 }
277 console.log('Unpin - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700278 }
279
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700280
281 // ========================================================
282
283 function drawNetwork() {
284 $('#view').empty();
285
286 prepareNodesAndLinks();
287 createLayout();
288 console.log("\n\nHere is the augmented network object...");
Simon Hunt9a16c822014-10-28 16:09:19 -0700289 console.log(network);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700290 }
291
292 function prepareNodesAndLinks() {
293 network.lookup = {};
294 network.nodes = [];
295 network.links = [];
296
297 var nw = network.forceWidth,
298 nh = network.forceHeight;
299
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700300 function yPosConstraintForNode(n) {
301 return config.constraints.ypos[n.type || 'host'];
302 }
303
304 // Note that both 'devices' and 'hosts' get mapped into the nodes array
305
306 // first, the devices...
307 network.data.devices.forEach(function(n) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700308 var ypc = yPosConstraintForNode(n),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700309 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700310 iy = ypc * nh,
311 node = {
312 id: n.id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700313 labels: n.labels,
314 class: 'device',
315 icon: 'device',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700316 type: n.type,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700317 x: ix,
318 y: iy,
319 constraint: {
320 weight: 0.7,
321 y: iy
322 }
323 };
324 network.lookup[n.id] = node;
325 network.nodes.push(node);
326 });
327
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700328 // then, the hosts...
329 network.data.hosts.forEach(function(n) {
330 var ypc = yPosConstraintForNode(n),
331 ix = Math.random() * 0.6 * nw + 0.2 * nw,
332 iy = ypc * nh,
333 node = {
334 id: n.id,
335 labels: n.labels,
336 class: 'host',
337 icon: 'host',
338 type: n.type,
339 x: ix,
340 y: iy,
341 constraint: {
342 weight: 0.7,
343 y: iy
344 }
345 };
346 network.lookup[n.id] = node;
347 network.nodes.push(node);
348 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700349
350
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700351 // now, process the explicit links...
Simon Hunt6f376a32014-10-28 12:38:30 -0700352 network.data.links.forEach(function(lnk) {
353 var src = network.lookup[lnk.src],
354 dst = network.lookup[lnk.dst],
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700355 id = src.id + "~" + dst.id;
356
357 var link = {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700358 class: 'infra',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700359 id: id,
Simon Hunt6f376a32014-10-28 12:38:30 -0700360 type: lnk.type,
361 width: lnk.linkWidth,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700362 source: src,
363 target: dst,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700364 strength: config.force.linkStrength.infra
365 };
366 network.links.push(link);
367 });
368
369 // finally, infer host links...
370 network.data.hosts.forEach(function(n) {
371 var src = network.lookup[n.id],
372 dst = network.lookup[n.cp.device],
373 id = src.id + "~" + dst.id;
374
375 var link = {
376 class: 'host',
377 id: id,
378 type: 'hostLink',
379 width: config.hostLinkWidth,
380 source: src,
381 target: dst,
382 strength: config.force.linkStrength.host
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700383 };
384 network.links.push(link);
385 });
386 }
387
388 function createLayout() {
389
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700390 var cfg = config.force;
391
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700392 network.force = d3.layout.force()
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700393 .size([network.forceWidth, network.forceHeight])
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700394 .nodes(network.nodes)
395 .links(network.links)
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700396 .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
397 .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
398 .charge(function(d) { return cfg.charge[d.class]; })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700399 .on('tick', tick);
400
401 network.svg = d3.select('#view').append('svg')
402 .attr('width', view.width)
403 .attr('height', view.height)
404 .append('g')
Simon Huntae968a62014-10-22 14:54:41 -0700405 .attr('transform', config.force.translate());
Simon Hunt3ab76a82014-10-22 13:07:32 -0700406// .attr('id', 'zoomable')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700407// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700408
Simon Hunt3ab76a82014-10-22 13:07:32 -0700409// function zoomRedraw() {
410// d3.select("#zoomable").attr("transform",
411// "translate(" + d3.event.translate + ")"
412// + " scale(" + d3.event.scale + ")");
413// }
414
Simon Hunt3ab76a82014-10-22 13:07:32 -0700415 // TODO: move glow/blur stuff to util script
416 var glow = network.svg.append('filter')
417 .attr('x', '-50%')
418 .attr('y', '-50%')
419 .attr('width', '200%')
420 .attr('height', '200%')
421 .attr('id', 'blue-glow');
422
423 glow.append('feColorMatrix')
424 .attr('type', 'matrix')
425 .attr('values', '0 0 0 0 0 ' +
426 '0 0 0 0 0 ' +
427 '0 0 0 0 .7 ' +
428 '0 0 0 1 0 ');
429
430 glow.append('feGaussianBlur')
431 .attr('stdDeviation', 3)
432 .attr('result', 'coloredBlur');
433
434 glow.append('feMerge').selectAll('feMergeNode')
435 .data(['coloredBlur', 'SourceGraphic'])
436 .enter().append('feMergeNode')
437 .attr('in', String);
438
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700439 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700440// $('#view').on('scroll', function() {
441//
442// });
443
444
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700445 // add links to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700446 network.link = network.svg.append('g').selectAll('.link')
447 .data(network.force.links(), function(d) {return d.id})
448 .enter().append('line')
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700449 .attr('class', function(d) {return 'link ' + d.class});
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700450
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700451
Simon Hunt6f376a32014-10-28 12:38:30 -0700452 // TODO: move drag behavior into separate method.
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700453 // == define node drag behavior...
Simon Hunt3ab76a82014-10-22 13:07:32 -0700454 network.draggedThreshold = d3.scale.linear()
455 .domain([0, 0.1])
456 .range([5, 20])
457 .clamp(true);
458
459 function dragged(d) {
460 var threshold = network.draggedThreshold(network.force.alpha()),
461 dx = d.oldX - d.px,
462 dy = d.oldY - d.py;
463 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
464 d.dragged = true;
465 }
466 return d.dragged;
467 }
468
469 network.drag = d3.behavior.drag()
470 .origin(function(d) { return d; })
471 .on('dragstart', function(d) {
472 d.oldX = d.x;
473 d.oldY = d.y;
474 d.dragged = false;
475 d.fixed |= 2;
476 })
477 .on('drag', function(d) {
478 d.px = d3.event.x;
479 d.py = d3.event.y;
480 if (dragged(d)) {
481 if (!network.force.alpha()) {
482 network.force.alpha(.025);
483 }
484 }
485 })
486 .on('dragend', function(d) {
487 if (!dragged(d)) {
488 selectObject(d, this);
489 }
490 d.fixed &= ~6;
Simon Hunt9a16c822014-10-28 16:09:19 -0700491
492 // once we've finished moving, pin the node in position,
Simon Huntf967d512014-10-28 20:34:29 -0700493 // if it is a device (not a host)
Simon Hunt9a16c822014-10-28 16:09:19 -0700494 if (d.class === 'device') {
495 d.fixed = true;
Simon Huntf967d512014-10-28 20:34:29 -0700496 d3.select(this).classed('fixed', true)
Simon Hunt9a16c822014-10-28 16:09:19 -0700497 }
Simon Hunt3ab76a82014-10-22 13:07:32 -0700498 });
499
500 $('#view').on('click', function(e) {
501 if (!$(e.target).closest('.node').length) {
502 deselectObject();
503 }
504 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700505
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700506
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700507 // add nodes to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700508 network.node = network.svg.selectAll('.node')
509 .data(network.force.nodes(), function(d) {return d.id})
510 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700511 .attr('class', function(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700512 var cls = 'node ' + d.class;
513 if (d.type) {
514 cls += ' ' + d.type;
515 }
516 return cls;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700517 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700518 .attr('transform', function(d) {
519 return translate(d.x, d.y);
520 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700521 .call(network.drag)
522 .on('mouseover', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700523 // TODO: show tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700524 if (network.mouseoutTimeout) {
525 clearTimeout(network.mouseoutTimeout);
526 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700527 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700528 hoverObject(d);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700529 })
530 .on('mouseout', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700531 // TODO: hide tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700532 if (network.mouseoutTimeout) {
533 clearTimeout(network.mouseoutTimeout);
534 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700535 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700536 network.mouseoutTimeout = setTimeout(function() {
537 hoverObject(null);
538 }, config.mouseOutTimerDelayMs);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700539 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700540
Simon Hunt6f376a32014-10-28 12:38:30 -0700541
542 // deal with device nodes first
543 network.nodeRect = network.node.filter('.device')
544 .append('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700545 .attr({
546 rx: 5,
547 ry: 5,
548 width: 100,
549 height: 12
550 });
551 // note that width/height are adjusted to fit the label text
Simon Hunt6f376a32014-10-28 12:38:30 -0700552 // then padded, and space made for the icon.
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700553
Simon Hunt6f376a32014-10-28 12:38:30 -0700554 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700555 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700556 icon = iconUrl(d);
557
558 node.append('text')
559 // TODO: add label cycle behavior
560 .text(d.id)
561 .attr('dy', '1.1em');
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700562
563 if (icon) {
564 var cfg = config.icons;
565 node.append('svg:image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700566 .attr({
567 width: cfg.w,
568 height: cfg.h,
569 'xlink:href': icon
570 });
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700571 // note, icon relative positioning (x,y) is done after we have
572 // adjusted the bounds of the rectangle...
573 }
Simon Hunt68ae6652014-10-22 13:58:07 -0700574
Simon Huntd35961b2014-10-28 08:49:48 -0700575 // debug function to show the modelled x,y coordinates of nodes...
576 if (debug('showNodeXY')) {
577 node.select('rect').attr('fill-opacity', 0.5);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700578 node.append('circle')
579 .attr({
580 class: 'debug',
581 cx: 0,
582 cy: 0,
583 r: '3px'
584 });
585 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700586 });
587
Simon Hunt6f376a32014-10-28 12:38:30 -0700588 // now process host nodes
589 network.nodeCircle = network.node.filter('.host')
590 .append('circle')
591 .attr({
592 r: config.hostRadius
593 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700594
Simon Hunt6f376a32014-10-28 12:38:30 -0700595 network.node.filter('.host').each(function(d) {
596 var node = d3.select(this),
597 icon = iconUrl(d);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700598
Simon Hunt6f376a32014-10-28 12:38:30 -0700599 // debug function to show the modelled x,y coordinates of nodes...
600 if (debug('showNodeXY')) {
601 node.select('circle').attr('fill-opacity', 0.5);
602 node.append('circle')
603 .attr({
604 class: 'debug',
605 cx: 0,
606 cy: 0,
607 r: '3px'
608 });
609 }
610 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700611
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700612 // this function is scheduled to happen soon after the given thread ends
613 setTimeout(function() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700614 // post process the device nodes, to pad their size to fit the
615 // label text and attach the icon to the right location.
616 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700617 // for every node, recompute size, padding, etc. so text fits
618 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700619 text = node.select('text'),
620 box = adjustRectToFitText(node),
621 lab = config.labels;
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700622
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700623 // now make the computed adjustment
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700624 node.select('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700625 .attr(box);
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700626
627 node.select('image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700628 .attr('x', box.x + config.icons.xoff)
629 .attr('y', box.y + config.icons.yoff);
Simon Hunt1c219892014-10-22 16:32:39 -0700630
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700631 var bounds = boundsFromBox(box);
632
633 // todo: clean up extent and edge work..
Simon Hunt1c219892014-10-22 16:32:39 -0700634 d.extent = {
635 left: bounds.x1 - lab.marginLR,
636 right: bounds.x2 + lab.marginLR,
637 top: bounds.y1 - lab.marginTB,
638 bottom: bounds.y2 + lab.marginTB
639 };
640
641 d.edge = {
642 left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
643 right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
644 top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
645 bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
646 };
647
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700648 });
649
650 network.numTicks = 0;
651 network.preventCollisions = false;
652 network.force.start();
Simon Hunt1c219892014-10-22 16:32:39 -0700653 for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700654 network.force.tick();
655 }
656 network.preventCollisions = true;
657 $('#view').css('visibility', 'visible');
658 });
659
Simon Hunt6f376a32014-10-28 12:38:30 -0700660
661 // returns the newly computed bounding box of the rectangle
662 function adjustRectToFitText(n) {
663 var text = n.select('text'),
664 box = text.node().getBBox(),
665 lab = config.labels;
666
Simon Hunt9a16c822014-10-28 16:09:19 -0700667 // not sure why n.data() returns an array of 1 element...
668 var data = n.data()[0];
669
Simon Hunt6f376a32014-10-28 12:38:30 -0700670 text.attr('text-anchor', 'middle')
671 .attr('y', '-0.8em')
672 .attr('x', lab.imgPad/2)
673 ;
674
Simon Hunt6f376a32014-10-28 12:38:30 -0700675 // translate the bbox so that it is centered on [x,y]
676 box.x = -box.width / 2;
677 box.y = -box.height / 2;
678
679 // add padding
680 box.x -= (lab.padLR + lab.imgPad/2);
681 box.width += lab.padLR * 2 + lab.imgPad;
682 box.y -= lab.padTB;
683 box.height += lab.padTB * 2;
684
685 return box;
686 }
687
688 function boundsFromBox(box) {
689 return {
690 x1: box.x,
691 y1: box.y,
692 x2: box.x + box.width,
693 y2: box.y + box.height
694 };
695 }
696
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700697 }
698
Simon Hunt68ae6652014-10-22 13:58:07 -0700699 function iconUrl(d) {
Thomas Vachuska1de66012014-10-30 03:03:30 -0700700 return 'img/' + d.type + '.png';
701// return config.iconUrl[d.icon];
Simon Hunt68ae6652014-10-22 13:58:07 -0700702 }
703
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700704 function translate(x, y) {
705 return 'translate(' + x + ',' + y + ')';
706 }
707
Simon Hunt6f376a32014-10-28 12:38:30 -0700708 // prevents collisions amongst device nodes
Simon Hunt1c219892014-10-22 16:32:39 -0700709 function preventCollisions() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700710 var quadtree = d3.geom.quadtree(network.nodes),
711 hrad = config.hostRadius;
Simon Hunt1c219892014-10-22 16:32:39 -0700712
713 network.nodes.forEach(function(n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700714 var nx1, nx2, ny1, ny2;
715
716 if (n.class === 'device') {
717 nx1 = n.x + n.extent.left;
718 nx2 = n.x + n.extent.right;
719 ny1 = n.y + n.extent.top;
Simon Hunt1c219892014-10-22 16:32:39 -0700720 ny2 = n.y + n.extent.bottom;
721
Simon Hunt6f376a32014-10-28 12:38:30 -0700722 } else {
723 nx1 = n.x - hrad;
724 nx2 = n.x + hrad;
725 ny1 = n.y - hrad;
726 ny2 = n.y + hrad;
727 }
728
Simon Hunt1c219892014-10-22 16:32:39 -0700729 quadtree.visit(function(quad, x1, y1, x2, y2) {
730 if (quad.point && quad.point !== n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700731 // check if the rectangles/circles intersect
Simon Hunt1c219892014-10-22 16:32:39 -0700732 var p = quad.point,
Simon Hunt6f376a32014-10-28 12:38:30 -0700733 px1, px2, py1, py2, ix;
734
735 if (p.class === 'device') {
736 px1 = p.x + p.extent.left;
737 px2 = p.x + p.extent.right;
738 py1 = p.y + p.extent.top;
739 py2 = p.y + p.extent.bottom;
740
741 } else {
742 px1 = p.x - hrad;
743 px2 = p.x + hrad;
744 py1 = p.y - hrad;
745 py2 = p.y + hrad;
746 }
747
748 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
749
Simon Hunt1c219892014-10-22 16:32:39 -0700750 if (ix) {
751 var xa1 = nx2 - px1, // shift n left , p right
752 xa2 = px2 - nx1, // shift n right, p left
753 ya1 = ny2 - py1, // shift n up , p down
754 ya2 = py2 - ny1, // shift n down , p up
755 adj = Math.min(xa1, xa2, ya1, ya2);
756
757 if (adj == xa1) {
758 n.x -= adj / 2;
759 p.x += adj / 2;
760 } else if (adj == xa2) {
761 n.x += adj / 2;
762 p.x -= adj / 2;
763 } else if (adj == ya1) {
764 n.y -= adj / 2;
765 p.y += adj / 2;
766 } else if (adj == ya2) {
767 n.y += adj / 2;
768 p.y -= adj / 2;
769 }
770 }
771 return ix;
772 }
773 });
774
775 });
776 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700777
778 function tick(e) {
779 network.numTicks++;
780
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700781 if (config.options.layering) {
Simon Hunt68ae6652014-10-22 13:58:07 -0700782 // adjust the y-coord of each node, based on y-pos constraints
783 network.nodes.forEach(function (n) {
784 var z = e.alpha * n.constraint.weight;
785 if (!isNaN(n.constraint.y)) {
786 n.y = (n.constraint.y * z + n.y * (1 - z));
787 }
788 });
789 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700790
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700791 if (config.options.collisionPrevention && network.preventCollisions) {
Simon Hunt1c219892014-10-22 16:32:39 -0700792 preventCollisions();
793 }
794
Simon Huntd35961b2014-10-28 08:49:48 -0700795 // clip visualization of links at bounds of nodes...
796 network.link.each(function(d) {
797 var xs = d.source.x,
798 ys = d.source.y,
799 xt = d.target.x,
800 yt = d.target.y,
801 line = new geo.LineSegment(xs, ys, xt, yt),
802 e, ix;
Simon Hunt1c219892014-10-22 16:32:39 -0700803
Simon Huntd35961b2014-10-28 08:49:48 -0700804 for (e in d.source.edge) {
805 ix = line.intersect(d.source.edge[e].offset(xs, ys));
Simon Hunt1c219892014-10-22 16:32:39 -0700806 if (ix.in1 && ix.in2) {
Simon Huntd35961b2014-10-28 08:49:48 -0700807 xs = ix.x;
808 ys = ix.y;
809 break;
810 }
811 }
812
813 for (e in d.target.edge) {
814 ix = line.intersect(d.target.edge[e].offset(xt, yt));
815 if (ix.in1 && ix.in2) {
816 xt = ix.x;
817 yt = ix.y;
Simon Hunt1c219892014-10-22 16:32:39 -0700818 break;
819 }
820 }
821
822 d3.select(this)
Simon Huntd35961b2014-10-28 08:49:48 -0700823 .attr('x1', xs)
824 .attr('y1', ys)
825 .attr('x2', xt)
826 .attr('y2', yt);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700827 });
828
Simon Huntd35961b2014-10-28 08:49:48 -0700829 // position each node by translating the node (group) by x,y
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700830 network.node
831 .attr('transform', function(d) {
832 return translate(d.x, d.y);
833 });
834
835 }
836
837 // $('#docs-close').on('click', function() {
838 // deselectObject();
839 // return false;
840 // });
841
842 // $(document).on('click', '.select-object', function() {
843 // var obj = graph.data[$(this).data('name')];
844 // if (obj) {
845 // selectObject(obj);
846 // }
847 // return false;
848 // });
849
Simon Hunt6f376a32014-10-28 12:38:30 -0700850 function findNodeFromData(d) {
851 var el = null;
852 network.node.filter('.' + d.class).each(function(n) {
853 if (n.id === d.id) {
854 el = d3.select(this);
855 }
856 });
857 return el;
858 }
859
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700860 function selectObject(obj, el) {
861 var node;
862 if (el) {
863 node = d3.select(el);
864 } else {
865 network.node.each(function(d) {
866 if (d == obj) {
867 node = d3.select(el = this);
868 }
869 });
870 }
871 if (!node) return;
872
873 if (node.classed('selected')) {
874 deselectObject();
Simon Huntc586e212014-10-28 21:24:08 -0700875 flyinPane(null);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700876 return;
877 }
878 deselectObject(false);
879
880 selected = {
881 obj : obj,
882 el : el
883 };
884
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700885 node.classed('selected', true);
Simon Huntc586e212014-10-28 21:24:08 -0700886 flyinPane(obj);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700887 }
888
889 function deselectObject(doResize) {
890 // Review: logic of 'resize(...)' function.
891 if (doResize || typeof doResize == 'undefined') {
892 resize(false);
893 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700894
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700895 // deselect all nodes in the network...
896 network.node.classed('selected', false);
897 selected = {};
Simon Huntc586e212014-10-28 21:24:08 -0700898 flyinPane(null);
899 }
900
Simon Huntcc267562014-10-29 10:22:17 -0700901 function detailUrl(id) {
Thomas Vachuskae972ea52014-10-30 00:14:16 -0700902 if (config.jsonPrefix) {
903 var safeId = id.replace(/[^a-z0-9]/gi, '_');
904 return config.jsonPrefix + safeId + '.json';
905 }
906 return config.jsonUrl + '/' + encodeURIComponent(id);
Simon Huntcc267562014-10-29 10:22:17 -0700907 }
908
Simon Huntc586e212014-10-28 21:24:08 -0700909 function flyinPane(obj) {
910 var pane = d3.select('#flyout'),
Simon Huntcc267562014-10-29 10:22:17 -0700911 url;
Simon Huntc586e212014-10-28 21:24:08 -0700912
913 if (obj) {
Simon Huntcc267562014-10-29 10:22:17 -0700914 // go get details of the selected object from the server...
915 url = detailUrl(obj.id);
916 d3.json(url, function (err, data) {
917 if (err) {
918 alert('Oops! Error reading JSON...\n\n' +
919 'URL: ' + url + '\n\n' +
920 'Error: ' + err.message);
921 return;
922 }
923// console.log("JSON data... " + url);
924// console.log(data);
925
926 displayDetails(data, pane);
927 });
928
929 } else {
930 // hide pane
931 pane.transition().duration(750)
932 .style('right', '-320px')
933 .style('opacity', 0.0);
934 }
935 }
936
937 function displayDetails(data, pane) {
938 $('#flyout').empty();
939
Thomas Vachuska1de66012014-10-30 03:03:30 -0700940 var title = pane.append("h2"),
941 table = pane.append("table"),
Simon Huntcc267562014-10-29 10:22:17 -0700942 tbody = table.append("tbody");
943
Thomas Vachuska1de66012014-10-30 03:03:30 -0700944 $('<img src="img/' + data.type + '.png">').appendTo(title);
945 $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
946
947
Simon Huntcc267562014-10-29 10:22:17 -0700948 // TODO: consider using d3 data bind to TR/TD
949
950 data.propOrder.forEach(function(p) {
Thomas Vachuska1de66012014-10-30 03:03:30 -0700951 if (p === '-') {
952 addSep(tbody);
953 } else {
954 addProp(tbody, p, data.props[p]);
955 }
Simon Huntcc267562014-10-29 10:22:17 -0700956 });
957
Thomas Vachuska1de66012014-10-30 03:03:30 -0700958 function addSep(tbody) {
959 var tr = tbody.append('tr');
960 $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
961 }
962
Simon Huntcc267562014-10-29 10:22:17 -0700963 function addProp(tbody, label, value) {
964 var tr = tbody.append('tr');
965
966 tr.append('td')
967 .attr('class', 'label')
968 .text(label + ' :');
969
970 tr.append('td')
971 .attr('class', 'value')
972 .text(value);
Simon Huntc586e212014-10-28 21:24:08 -0700973 }
974
Simon Huntcc267562014-10-29 10:22:17 -0700975 // show pane
Simon Huntc586e212014-10-28 21:24:08 -0700976 pane.transition().duration(750)
Simon Huntcc267562014-10-29 10:22:17 -0700977 .style('right', '20px')
978 .style('opacity', 1.0);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700979 }
980
981 function highlightObject(obj) {
982 if (obj) {
983 if (obj != highlighted) {
984 // TODO set or clear "inactive" class on nodes, based on criteria
985 network.node.classed('inactive', function(d) {
986 // return (obj !== d &&
987 // d.relation(obj.id));
988 return (obj !== d);
989 });
990 // TODO: same with links
991 network.link.classed('inactive', function(d) {
992 return (obj !== d.source && obj !== d.target);
993 });
994 }
995 highlighted = obj;
996 } else {
997 if (highlighted) {
998 // clear the inactive flag (no longer suppressed visually)
999 network.node.classed('inactive', false);
1000 network.link.classed('inactive', false);
1001 }
1002 highlighted = null;
1003
1004 }
1005 }
1006
Simon Hunt9a16c822014-10-28 16:09:19 -07001007 function hoverObject(obj) {
1008 if (obj) {
1009 hovered = obj;
1010 } else {
1011 if (hovered) {
1012 hovered = null;
1013 }
1014 }
1015 }
1016
1017
Simon Huntc586e212014-10-28 21:24:08 -07001018 function resize() {
Simon Hunt0b05d4a2014-10-21 21:50:15 -07001019 view.height = window.innerHeight - config.mastHeight;
1020 view.width = window.innerWidth;
1021 $('#view')
1022 .css('height', view.height + 'px')
1023 .css('width', view.width + 'px');
1024
1025 network.forceWidth = view.width - config.force.marginLR;
1026 network.forceHeight = view.height - config.force.marginTB;
1027 }
1028
1029 // ======================================================================
1030 // register with the UI framework
1031
1032 api.addView('network', {
1033 load: loadNetworkView
1034 });
1035
1036
1037}(ONOS));
1038