blob: b43f32dc2c864885c9edf67dd7851a0416bab2b1 [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 Huntb4d9d4c2014-10-30 11:27:23 -070031 useLiveData: true,
Simon Hunt73171372014-10-30 09:25:36 -070032 debugOn: false,
33 debug: {
34 showNodeXY: false,
35 showKeyHandler: true
36 },
37 options: {
38 layering: true,
Simon Huntee66e612014-10-30 14:56:31 -070039 collisionPrevention: true,
40 loadBackground: true
Simon Hunt73171372014-10-30 09:25:36 -070041 },
Simon Huntee66e612014-10-30 14:56:31 -070042 backgroundUrl: 'img/us-map.png',
Simon Huntb4d9d4c2014-10-30 11:27:23 -070043 data: {
44 live: {
45 jsonUrl: 'rs/topology/graph',
46 detailPrefix: 'rs/topology/graph/',
47 detailSuffix: ''
48 },
49 fake: {
50 jsonUrl: 'json/network2.json',
51 detailPrefix: 'json/',
52 detailSuffix: '.json'
53 }
54 },
Simon Hunt73171372014-10-30 09:25:36 -070055 iconUrl: {
56 device: 'img/device.png',
57 host: 'img/host.png',
58 pkt: 'img/pkt.png',
59 opt: 'img/opt.png'
60 },
61 mastHeight: 36,
62 force: {
63 note: 'node.class or link.class is used to differentiate',
64 linkDistance: {
65 infra: 200,
66 host: 40
Simon Huntd35961b2014-10-28 08:49:48 -070067 },
Simon Hunt73171372014-10-30 09:25:36 -070068 linkStrength: {
69 infra: 1.0,
70 host: 1.0
Simon Hunt2c9e0c22014-10-23 15:12:58 -070071 },
Simon Hunt73171372014-10-30 09:25:36 -070072 charge: {
73 device: -800,
74 host: -1000
Simon Hunt68ae6652014-10-22 13:58:07 -070075 },
Simon Hunt73171372014-10-30 09:25:36 -070076 ticksWithoutCollisions: 50,
77 marginLR: 20,
78 marginTB: 20,
79 translate: function() {
80 return 'translate(' +
81 config.force.marginLR + ',' +
82 config.force.marginTB + ')';
83 }
84 },
85 labels: {
86 imgPad: 16,
87 padLR: 8,
88 padTB: 6,
89 marginLR: 3,
90 marginTB: 2
91 },
92 icons: {
93 w: 32,
94 h: 32,
95 xoff: -12,
96 yoff: -8
97 },
98 constraints: {
99 ypos: {
100 host: 0.05,
101 switch: 0.3,
102 roadm: 0.7
103 }
104 },
105 hostLinkWidth: 1.0,
106 hostRadius: 7,
107 mouseOutTimerDelayMs: 120
108 };
Simon Huntd35961b2014-10-28 08:49:48 -0700109
110 // state variables
111 var view = {},
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700112 network = {},
113 selected = {},
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700114 highlighted = null,
Simon Hunt9a16c822014-10-28 16:09:19 -0700115 hovered = null,
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700116 viewMode = 'showAll';
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700117
118
Simon Huntd35961b2014-10-28 08:49:48 -0700119 function debug(what) {
120 return config.debugOn && config.debug[what];
121 }
122
Simon Huntb4d9d4c2014-10-30 11:27:23 -0700123 function urlData() {
124 return config.data[config.useLiveData ? 'live' : 'fake'];
125 }
126
127 function networkJsonUrl() {
128 return urlData().jsonUrl;
129 }
130
131 function detailJsonUrl(id) {
132 var u = urlData(),
133 encId = config.useLiveData ? encodeURIComponent(id)
134 : id.replace(/[^a-z0-9]/gi, '_');
135 return u.detailPrefix + encId + u.detailSuffix;
136 }
137
138
Simon Huntd35961b2014-10-28 08:49:48 -0700139 // load the topology view of the network
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700140 function loadNetworkView() {
141 // Hey, here I am, calling something on the ONOS api:
142 api.printTime();
143
144 resize();
145
Simon Huntd35961b2014-10-28 08:49:48 -0700146 // go get our network data from the server...
Simon Huntb4d9d4c2014-10-30 11:27:23 -0700147 var url = networkJsonUrl();
148 d3.json(url , function (err, data) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700149 if (err) {
150 alert('Oops! Error reading JSON...\n\n' +
Simon Huntb4d9d4c2014-10-30 11:27:23 -0700151 'URL: ' + url + '\n\n' +
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700152 'Error: ' + err.message);
153 return;
154 }
Simon Huntd35961b2014-10-28 08:49:48 -0700155// console.log("here is the JSON data...");
156// console.log(data);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700157
158 network.data = data;
159 drawNetwork();
160 });
161
Simon Huntd35961b2014-10-28 08:49:48 -0700162 // while we wait for the data, set up the handlers...
163 setUpClickHandler();
164 setUpRadioButtonHandler();
165 setUpKeyHandler();
166 $(window).on('resize', resize);
167 }
168
169 function setUpClickHandler() {
170 // click handler for "selectable" objects
171 $(document).on('click', '.select-object', function () {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700172 // when any object of class "select-object" is clicked...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700173 var obj = network.lookup[$(this).data('id')];
174 if (obj) {
175 selectObject(obj);
176 }
177 // stop propagation of event (I think) ...
178 return false;
179 });
Simon Huntd35961b2014-10-28 08:49:48 -0700180 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700181
Simon Huntd35961b2014-10-28 08:49:48 -0700182 function setUpRadioButtonHandler() {
183 d3.selectAll('#displayModes .radio').on('click', function () {
184 var id = d3.select(this).attr('id');
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700185 if (id !== viewMode) {
186 radioButton('displayModes', id);
187 viewMode = id;
Simon Huntf967d512014-10-28 20:34:29 -0700188 doRadioAction(id);
189 }
190 });
191 }
192
193 function doRadioAction(id) {
194 showAllLayers();
195 if (id === 'showPkt') {
196 showPacketLayer();
197 } else if (id === 'showOpt') {
198 showOpticalLayer();
199 }
200 }
201
202 function showAllLayers() {
203 network.node.classed('inactive', false);
204 network.link.classed('inactive', false);
205 }
206
207 function showPacketLayer() {
208 network.node.each(function(d) {
209 // deactivate nodes that are not hosts or switches
210 if (d.class === 'device' && d.type !== 'switch') {
211 d3.select(this).classed('inactive', true);
212 }
213 });
214
215 network.link.each(function(lnk) {
216 // deactivate infrastructure links that have opt's as endpoints
217 if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') {
218 d3.select(this).classed('inactive', true);
219 }
220 });
221 }
222
223 function showOpticalLayer() {
224 network.node.each(function(d) {
225 // deactivate nodes that are not optical devices
226 if (d.type !== 'roadm') {
227 d3.select(this).classed('inactive', true);
228 }
229 });
230
231 network.link.each(function(lnk) {
232 // deactivate infrastructure links that have opt's as endpoints
233 if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') {
234 d3.select(this).classed('inactive', true);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700235 }
236 });
237 }
238
Simon Huntd35961b2014-10-28 08:49:48 -0700239 function setUpKeyHandler() {
240 d3.select('body')
241 .on('keydown', function () {
242 processKeyEvent();
243 if (debug('showKeyHandler')) {
244 network.svg.append('text')
245 .attr('x', 5)
246 .attr('y', 15)
247 .style('font-size', '20pt')
248 .text('keyCode: ' + d3.event.keyCode +
249 ' applied to : ' + contextLabel())
250 .transition().duration(2000)
251 .style('font-size', '2pt')
252 .style('fill-opacity', 0.01)
253 .remove();
254 }
255 });
256 }
257
Simon Hunt9a16c822014-10-28 16:09:19 -0700258 function contextLabel() {
259 return hovered === null ? "(nothing)" : hovered.id;
260 }
261
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700262 function radioButton(group, id) {
263 d3.selectAll("#" + group + " .radio").classed("active", false);
264 d3.select("#" + group + " #" + id).classed("active", true);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700265 }
266
Simon Huntd35961b2014-10-28 08:49:48 -0700267 function processKeyEvent() {
268 var code = d3.event.keyCode;
269 switch (code) {
Simon Huntee66e612014-10-30 14:56:31 -0700270 case 66: // B
271 toggleBackground();
272 break;
Thomas Vachuska1de66012014-10-30 03:03:30 -0700273 case 71: // G
274 cycleLayout();
275 break;
Simon Huntd35961b2014-10-28 08:49:48 -0700276 case 76: // L
277 cycleLabels();
278 break;
279 case 80: // P
280 togglePorts();
Simon Hunt9a16c822014-10-28 16:09:19 -0700281 break;
282 case 85: // U
283 unpin();
284 break;
Simon Huntd35961b2014-10-28 08:49:48 -0700285 }
286
287 }
288
Simon Huntee66e612014-10-30 14:56:31 -0700289 function toggleBackground() {
290 var bg = d3.select('#bg'),
291 vis = bg.style('visibility'),
292 newvis = (vis === 'hidden') ? 'visible' : 'hidden';
293 bg.style('visibility', newvis);
294 }
295
Thomas Vachuska1de66012014-10-30 03:03:30 -0700296 function cycleLayout() {
297 config.options.layering = !config.options.layering;
298 network.force.resume();
299 }
300
Simon Huntd35961b2014-10-28 08:49:48 -0700301 function cycleLabels() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700302 console.log('Cycle Labels - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700303 }
304
305 function togglePorts() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700306 console.log('Toggle Ports - context = ' + contextLabel());
307 }
308
309 function unpin() {
310 if (hovered) {
311 hovered.fixed = false;
Simon Huntf967d512014-10-28 20:34:29 -0700312 findNodeFromData(hovered).classed('fixed', false);
Simon Hunt9a16c822014-10-28 16:09:19 -0700313 network.force.resume();
314 }
315 console.log('Unpin - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700316 }
317
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700318
319 // ========================================================
320
321 function drawNetwork() {
322 $('#view').empty();
323
324 prepareNodesAndLinks();
325 createLayout();
326 console.log("\n\nHere is the augmented network object...");
Simon Hunt9a16c822014-10-28 16:09:19 -0700327 console.log(network);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700328 }
329
330 function prepareNodesAndLinks() {
331 network.lookup = {};
332 network.nodes = [];
333 network.links = [];
334
335 var nw = network.forceWidth,
336 nh = network.forceHeight;
337
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700338 function yPosConstraintForNode(n) {
339 return config.constraints.ypos[n.type || 'host'];
340 }
341
342 // Note that both 'devices' and 'hosts' get mapped into the nodes array
343
344 // first, the devices...
345 network.data.devices.forEach(function(n) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700346 var ypc = yPosConstraintForNode(n),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700347 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700348 iy = ypc * nh,
349 node = {
350 id: n.id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700351 labels: n.labels,
352 class: 'device',
353 icon: 'device',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700354 type: n.type,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700355 x: ix,
356 y: iy,
357 constraint: {
358 weight: 0.7,
359 y: iy
360 }
361 };
362 network.lookup[n.id] = node;
363 network.nodes.push(node);
364 });
365
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700366 // then, the hosts...
367 network.data.hosts.forEach(function(n) {
368 var ypc = yPosConstraintForNode(n),
369 ix = Math.random() * 0.6 * nw + 0.2 * nw,
370 iy = ypc * nh,
371 node = {
372 id: n.id,
373 labels: n.labels,
374 class: 'host',
375 icon: 'host',
376 type: n.type,
377 x: ix,
378 y: iy,
379 constraint: {
380 weight: 0.7,
381 y: iy
382 }
383 };
384 network.lookup[n.id] = node;
385 network.nodes.push(node);
386 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700387
388
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700389 // now, process the explicit links...
Simon Hunt6f376a32014-10-28 12:38:30 -0700390 network.data.links.forEach(function(lnk) {
391 var src = network.lookup[lnk.src],
392 dst = network.lookup[lnk.dst],
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700393 id = src.id + "~" + dst.id;
394
395 var link = {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700396 class: 'infra',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700397 id: id,
Simon Hunt6f376a32014-10-28 12:38:30 -0700398 type: lnk.type,
399 width: lnk.linkWidth,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700400 source: src,
401 target: dst,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700402 strength: config.force.linkStrength.infra
403 };
404 network.links.push(link);
405 });
406
407 // finally, infer host links...
408 network.data.hosts.forEach(function(n) {
409 var src = network.lookup[n.id],
410 dst = network.lookup[n.cp.device],
411 id = src.id + "~" + dst.id;
412
413 var link = {
414 class: 'host',
415 id: id,
416 type: 'hostLink',
417 width: config.hostLinkWidth,
418 source: src,
419 target: dst,
420 strength: config.force.linkStrength.host
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700421 };
422 network.links.push(link);
423 });
424 }
425
426 function createLayout() {
427
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700428 var cfg = config.force;
429
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700430 network.force = d3.layout.force()
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700431 .size([network.forceWidth, network.forceHeight])
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700432 .nodes(network.nodes)
433 .links(network.links)
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700434 .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
435 .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
436 .charge(function(d) { return cfg.charge[d.class]; })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700437 .on('tick', tick);
438
439 network.svg = d3.select('#view').append('svg')
440 .attr('width', view.width)
441 .attr('height', view.height)
442 .append('g')
Simon Huntae968a62014-10-22 14:54:41 -0700443 .attr('transform', config.force.translate());
Simon Hunt3ab76a82014-10-22 13:07:32 -0700444// .attr('id', 'zoomable')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700445// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700446
Thomas Vachuska8cd66a52014-10-30 11:53:07 -0700447 network.svg.append('svg:image')
448 .attr({
449 id: 'bg',
450 width: view.width,
451 height: view.height,
Simon Huntee66e612014-10-30 14:56:31 -0700452 'xlink:href': config.backgroundUrl
453 })
454 .style('visibility',
455 config.options.loadBackground ? 'visible' : 'hidden');
Thomas Vachuska8cd66a52014-10-30 11:53:07 -0700456
Simon Hunt3ab76a82014-10-22 13:07:32 -0700457// function zoomRedraw() {
458// d3.select("#zoomable").attr("transform",
459// "translate(" + d3.event.translate + ")"
460// + " scale(" + d3.event.scale + ")");
461// }
462
Simon Hunt3ab76a82014-10-22 13:07:32 -0700463 // TODO: move glow/blur stuff to util script
464 var glow = network.svg.append('filter')
465 .attr('x', '-50%')
466 .attr('y', '-50%')
467 .attr('width', '200%')
468 .attr('height', '200%')
469 .attr('id', 'blue-glow');
470
471 glow.append('feColorMatrix')
472 .attr('type', 'matrix')
473 .attr('values', '0 0 0 0 0 ' +
474 '0 0 0 0 0 ' +
475 '0 0 0 0 .7 ' +
476 '0 0 0 1 0 ');
477
478 glow.append('feGaussianBlur')
479 .attr('stdDeviation', 3)
480 .attr('result', 'coloredBlur');
481
482 glow.append('feMerge').selectAll('feMergeNode')
483 .data(['coloredBlur', 'SourceGraphic'])
484 .enter().append('feMergeNode')
485 .attr('in', String);
486
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700487 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700488// $('#view').on('scroll', function() {
489//
490// });
491
492
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700493 // add links to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700494 network.link = network.svg.append('g').selectAll('.link')
495 .data(network.force.links(), function(d) {return d.id})
496 .enter().append('line')
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700497 .attr('class', function(d) {return 'link ' + d.class});
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700498
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700499
Simon Hunt6f376a32014-10-28 12:38:30 -0700500 // TODO: move drag behavior into separate method.
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700501 // == define node drag behavior...
Simon Hunt3ab76a82014-10-22 13:07:32 -0700502 network.draggedThreshold = d3.scale.linear()
503 .domain([0, 0.1])
504 .range([5, 20])
505 .clamp(true);
506
507 function dragged(d) {
508 var threshold = network.draggedThreshold(network.force.alpha()),
509 dx = d.oldX - d.px,
510 dy = d.oldY - d.py;
511 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
512 d.dragged = true;
513 }
514 return d.dragged;
515 }
516
517 network.drag = d3.behavior.drag()
518 .origin(function(d) { return d; })
519 .on('dragstart', function(d) {
520 d.oldX = d.x;
521 d.oldY = d.y;
522 d.dragged = false;
523 d.fixed |= 2;
524 })
525 .on('drag', function(d) {
526 d.px = d3.event.x;
527 d.py = d3.event.y;
528 if (dragged(d)) {
529 if (!network.force.alpha()) {
530 network.force.alpha(.025);
531 }
532 }
533 })
534 .on('dragend', function(d) {
535 if (!dragged(d)) {
536 selectObject(d, this);
537 }
538 d.fixed &= ~6;
Simon Hunt9a16c822014-10-28 16:09:19 -0700539
540 // once we've finished moving, pin the node in position,
Simon Huntf967d512014-10-28 20:34:29 -0700541 // if it is a device (not a host)
Simon Hunt9a16c822014-10-28 16:09:19 -0700542 if (d.class === 'device') {
543 d.fixed = true;
Simon Huntf967d512014-10-28 20:34:29 -0700544 d3.select(this).classed('fixed', true)
Simon Hunt9a16c822014-10-28 16:09:19 -0700545 }
Simon Hunt3ab76a82014-10-22 13:07:32 -0700546 });
547
548 $('#view').on('click', function(e) {
549 if (!$(e.target).closest('.node').length) {
550 deselectObject();
551 }
552 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700553
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700554
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700555 // add nodes to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700556 network.node = network.svg.selectAll('.node')
557 .data(network.force.nodes(), function(d) {return d.id})
558 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700559 .attr('class', function(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700560 var cls = 'node ' + d.class;
561 if (d.type) {
562 cls += ' ' + d.type;
563 }
564 return cls;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700565 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700566 .attr('transform', function(d) {
567 return translate(d.x, d.y);
568 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700569 .call(network.drag)
570 .on('mouseover', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700571 // TODO: show tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700572 if (network.mouseoutTimeout) {
573 clearTimeout(network.mouseoutTimeout);
574 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700575 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700576 hoverObject(d);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700577 })
578 .on('mouseout', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700579 // TODO: hide tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700580 if (network.mouseoutTimeout) {
581 clearTimeout(network.mouseoutTimeout);
582 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700583 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700584 network.mouseoutTimeout = setTimeout(function() {
585 hoverObject(null);
586 }, config.mouseOutTimerDelayMs);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700587 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700588
Simon Hunt6f376a32014-10-28 12:38:30 -0700589
590 // deal with device nodes first
591 network.nodeRect = network.node.filter('.device')
592 .append('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700593 .attr({
594 rx: 5,
595 ry: 5,
596 width: 100,
597 height: 12
598 });
599 // note that width/height are adjusted to fit the label text
Simon Hunt6f376a32014-10-28 12:38:30 -0700600 // then padded, and space made for the icon.
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700601
Simon Hunt6f376a32014-10-28 12:38:30 -0700602 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700603 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700604 icon = iconUrl(d);
605
606 node.append('text')
607 // TODO: add label cycle behavior
608 .text(d.id)
609 .attr('dy', '1.1em');
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700610
611 if (icon) {
612 var cfg = config.icons;
613 node.append('svg:image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700614 .attr({
615 width: cfg.w,
616 height: cfg.h,
617 'xlink:href': icon
618 });
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700619 // note, icon relative positioning (x,y) is done after we have
620 // adjusted the bounds of the rectangle...
621 }
Simon Hunt68ae6652014-10-22 13:58:07 -0700622
Simon Huntd35961b2014-10-28 08:49:48 -0700623 // debug function to show the modelled x,y coordinates of nodes...
624 if (debug('showNodeXY')) {
625 node.select('rect').attr('fill-opacity', 0.5);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700626 node.append('circle')
627 .attr({
628 class: 'debug',
629 cx: 0,
630 cy: 0,
631 r: '3px'
632 });
633 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700634 });
635
Simon Hunt6f376a32014-10-28 12:38:30 -0700636 // now process host nodes
637 network.nodeCircle = network.node.filter('.host')
638 .append('circle')
639 .attr({
640 r: config.hostRadius
641 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700642
Simon Hunt6f376a32014-10-28 12:38:30 -0700643 network.node.filter('.host').each(function(d) {
644 var node = d3.select(this),
645 icon = iconUrl(d);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700646
Simon Hunt6f376a32014-10-28 12:38:30 -0700647 // debug function to show the modelled x,y coordinates of nodes...
648 if (debug('showNodeXY')) {
649 node.select('circle').attr('fill-opacity', 0.5);
650 node.append('circle')
651 .attr({
652 class: 'debug',
653 cx: 0,
654 cy: 0,
655 r: '3px'
656 });
657 }
658 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700659
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700660 // this function is scheduled to happen soon after the given thread ends
661 setTimeout(function() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700662 // post process the device nodes, to pad their size to fit the
663 // label text and attach the icon to the right location.
664 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700665 // for every node, recompute size, padding, etc. so text fits
666 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700667 text = node.select('text'),
668 box = adjustRectToFitText(node),
669 lab = config.labels;
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700670
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700671 // now make the computed adjustment
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700672 node.select('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700673 .attr(box);
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700674
675 node.select('image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700676 .attr('x', box.x + config.icons.xoff)
677 .attr('y', box.y + config.icons.yoff);
Simon Hunt1c219892014-10-22 16:32:39 -0700678
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700679 var bounds = boundsFromBox(box);
680
681 // todo: clean up extent and edge work..
Simon Hunt1c219892014-10-22 16:32:39 -0700682 d.extent = {
683 left: bounds.x1 - lab.marginLR,
684 right: bounds.x2 + lab.marginLR,
685 top: bounds.y1 - lab.marginTB,
686 bottom: bounds.y2 + lab.marginTB
687 };
688
689 d.edge = {
690 left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
691 right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
692 top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
693 bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
694 };
695
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700696 });
697
698 network.numTicks = 0;
699 network.preventCollisions = false;
700 network.force.start();
Simon Hunt1c219892014-10-22 16:32:39 -0700701 for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700702 network.force.tick();
703 }
704 network.preventCollisions = true;
705 $('#view').css('visibility', 'visible');
706 });
707
Simon Hunt6f376a32014-10-28 12:38:30 -0700708
709 // returns the newly computed bounding box of the rectangle
710 function adjustRectToFitText(n) {
711 var text = n.select('text'),
712 box = text.node().getBBox(),
713 lab = config.labels;
714
Simon Hunt9a16c822014-10-28 16:09:19 -0700715 // not sure why n.data() returns an array of 1 element...
716 var data = n.data()[0];
717
Simon Hunt6f376a32014-10-28 12:38:30 -0700718 text.attr('text-anchor', 'middle')
719 .attr('y', '-0.8em')
720 .attr('x', lab.imgPad/2)
721 ;
722
Simon Hunt6f376a32014-10-28 12:38:30 -0700723 // translate the bbox so that it is centered on [x,y]
724 box.x = -box.width / 2;
725 box.y = -box.height / 2;
726
727 // add padding
728 box.x -= (lab.padLR + lab.imgPad/2);
729 box.width += lab.padLR * 2 + lab.imgPad;
730 box.y -= lab.padTB;
731 box.height += lab.padTB * 2;
732
733 return box;
734 }
735
736 function boundsFromBox(box) {
737 return {
738 x1: box.x,
739 y1: box.y,
740 x2: box.x + box.width,
741 y2: box.y + box.height
742 };
743 }
744
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700745 }
746
Simon Hunt68ae6652014-10-22 13:58:07 -0700747 function iconUrl(d) {
Thomas Vachuska1de66012014-10-30 03:03:30 -0700748 return 'img/' + d.type + '.png';
749// return config.iconUrl[d.icon];
Simon Hunt68ae6652014-10-22 13:58:07 -0700750 }
751
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700752 function translate(x, y) {
753 return 'translate(' + x + ',' + y + ')';
754 }
755
Simon Hunt6f376a32014-10-28 12:38:30 -0700756 // prevents collisions amongst device nodes
Simon Hunt1c219892014-10-22 16:32:39 -0700757 function preventCollisions() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700758 var quadtree = d3.geom.quadtree(network.nodes),
759 hrad = config.hostRadius;
Simon Hunt1c219892014-10-22 16:32:39 -0700760
761 network.nodes.forEach(function(n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700762 var nx1, nx2, ny1, ny2;
763
764 if (n.class === 'device') {
765 nx1 = n.x + n.extent.left;
766 nx2 = n.x + n.extent.right;
767 ny1 = n.y + n.extent.top;
Simon Hunt1c219892014-10-22 16:32:39 -0700768 ny2 = n.y + n.extent.bottom;
769
Simon Hunt6f376a32014-10-28 12:38:30 -0700770 } else {
771 nx1 = n.x - hrad;
772 nx2 = n.x + hrad;
773 ny1 = n.y - hrad;
774 ny2 = n.y + hrad;
775 }
776
Simon Hunt1c219892014-10-22 16:32:39 -0700777 quadtree.visit(function(quad, x1, y1, x2, y2) {
778 if (quad.point && quad.point !== n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700779 // check if the rectangles/circles intersect
Simon Hunt1c219892014-10-22 16:32:39 -0700780 var p = quad.point,
Simon Hunt6f376a32014-10-28 12:38:30 -0700781 px1, px2, py1, py2, ix;
782
783 if (p.class === 'device') {
784 px1 = p.x + p.extent.left;
785 px2 = p.x + p.extent.right;
786 py1 = p.y + p.extent.top;
787 py2 = p.y + p.extent.bottom;
788
789 } else {
790 px1 = p.x - hrad;
791 px2 = p.x + hrad;
792 py1 = p.y - hrad;
793 py2 = p.y + hrad;
794 }
795
796 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
797
Simon Hunt1c219892014-10-22 16:32:39 -0700798 if (ix) {
799 var xa1 = nx2 - px1, // shift n left , p right
800 xa2 = px2 - nx1, // shift n right, p left
801 ya1 = ny2 - py1, // shift n up , p down
802 ya2 = py2 - ny1, // shift n down , p up
803 adj = Math.min(xa1, xa2, ya1, ya2);
804
805 if (adj == xa1) {
806 n.x -= adj / 2;
807 p.x += adj / 2;
808 } else if (adj == xa2) {
809 n.x += adj / 2;
810 p.x -= adj / 2;
811 } else if (adj == ya1) {
812 n.y -= adj / 2;
813 p.y += adj / 2;
814 } else if (adj == ya2) {
815 n.y += adj / 2;
816 p.y -= adj / 2;
817 }
818 }
819 return ix;
820 }
821 });
822
823 });
824 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700825
826 function tick(e) {
827 network.numTicks++;
828
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700829 if (config.options.layering) {
Simon Hunt68ae6652014-10-22 13:58:07 -0700830 // adjust the y-coord of each node, based on y-pos constraints
831 network.nodes.forEach(function (n) {
832 var z = e.alpha * n.constraint.weight;
833 if (!isNaN(n.constraint.y)) {
834 n.y = (n.constraint.y * z + n.y * (1 - z));
835 }
836 });
837 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700838
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700839 if (config.options.collisionPrevention && network.preventCollisions) {
Simon Hunt1c219892014-10-22 16:32:39 -0700840 preventCollisions();
841 }
842
Simon Huntd35961b2014-10-28 08:49:48 -0700843 // clip visualization of links at bounds of nodes...
844 network.link.each(function(d) {
845 var xs = d.source.x,
846 ys = d.source.y,
847 xt = d.target.x,
848 yt = d.target.y,
849 line = new geo.LineSegment(xs, ys, xt, yt),
850 e, ix;
Simon Hunt1c219892014-10-22 16:32:39 -0700851
Simon Huntd35961b2014-10-28 08:49:48 -0700852 for (e in d.source.edge) {
853 ix = line.intersect(d.source.edge[e].offset(xs, ys));
Simon Hunt1c219892014-10-22 16:32:39 -0700854 if (ix.in1 && ix.in2) {
Simon Huntd35961b2014-10-28 08:49:48 -0700855 xs = ix.x;
856 ys = ix.y;
857 break;
858 }
859 }
860
861 for (e in d.target.edge) {
862 ix = line.intersect(d.target.edge[e].offset(xt, yt));
863 if (ix.in1 && ix.in2) {
864 xt = ix.x;
865 yt = ix.y;
Simon Hunt1c219892014-10-22 16:32:39 -0700866 break;
867 }
868 }
869
870 d3.select(this)
Simon Huntd35961b2014-10-28 08:49:48 -0700871 .attr('x1', xs)
872 .attr('y1', ys)
873 .attr('x2', xt)
874 .attr('y2', yt);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700875 });
876
Simon Huntd35961b2014-10-28 08:49:48 -0700877 // position each node by translating the node (group) by x,y
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700878 network.node
879 .attr('transform', function(d) {
880 return translate(d.x, d.y);
881 });
882
883 }
884
885 // $('#docs-close').on('click', function() {
886 // deselectObject();
887 // return false;
888 // });
889
890 // $(document).on('click', '.select-object', function() {
891 // var obj = graph.data[$(this).data('name')];
892 // if (obj) {
893 // selectObject(obj);
894 // }
895 // return false;
896 // });
897
Simon Hunt6f376a32014-10-28 12:38:30 -0700898 function findNodeFromData(d) {
899 var el = null;
900 network.node.filter('.' + d.class).each(function(n) {
901 if (n.id === d.id) {
902 el = d3.select(this);
903 }
904 });
905 return el;
906 }
907
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700908 function selectObject(obj, el) {
909 var node;
910 if (el) {
911 node = d3.select(el);
912 } else {
913 network.node.each(function(d) {
914 if (d == obj) {
915 node = d3.select(el = this);
916 }
917 });
918 }
919 if (!node) return;
920
921 if (node.classed('selected')) {
922 deselectObject();
Simon Huntc586e212014-10-28 21:24:08 -0700923 flyinPane(null);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700924 return;
925 }
926 deselectObject(false);
927
928 selected = {
929 obj : obj,
930 el : el
931 };
932
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700933 node.classed('selected', true);
Simon Huntc586e212014-10-28 21:24:08 -0700934 flyinPane(obj);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700935 }
936
937 function deselectObject(doResize) {
938 // Review: logic of 'resize(...)' function.
939 if (doResize || typeof doResize == 'undefined') {
940 resize(false);
941 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700942
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700943 // deselect all nodes in the network...
944 network.node.classed('selected', false);
945 selected = {};
Simon Huntc586e212014-10-28 21:24:08 -0700946 flyinPane(null);
947 }
948
949 function flyinPane(obj) {
950 var pane = d3.select('#flyout'),
Simon Huntcc267562014-10-29 10:22:17 -0700951 url;
Simon Huntc586e212014-10-28 21:24:08 -0700952
953 if (obj) {
Simon Huntcc267562014-10-29 10:22:17 -0700954 // go get details of the selected object from the server...
Simon Huntb4d9d4c2014-10-30 11:27:23 -0700955 url = detailJsonUrl(obj.id);
Simon Huntcc267562014-10-29 10:22:17 -0700956 d3.json(url, function (err, data) {
957 if (err) {
958 alert('Oops! Error reading JSON...\n\n' +
959 'URL: ' + url + '\n\n' +
960 'Error: ' + err.message);
961 return;
962 }
963// console.log("JSON data... " + url);
964// console.log(data);
965
966 displayDetails(data, pane);
967 });
968
969 } else {
970 // hide pane
971 pane.transition().duration(750)
972 .style('right', '-320px')
973 .style('opacity', 0.0);
974 }
975 }
976
977 function displayDetails(data, pane) {
978 $('#flyout').empty();
979
Thomas Vachuska1de66012014-10-30 03:03:30 -0700980 var title = pane.append("h2"),
981 table = pane.append("table"),
Simon Huntcc267562014-10-29 10:22:17 -0700982 tbody = table.append("tbody");
983
Thomas Vachuska1de66012014-10-30 03:03:30 -0700984 $('<img src="img/' + data.type + '.png">').appendTo(title);
985 $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
986
987
Simon Huntcc267562014-10-29 10:22:17 -0700988 // TODO: consider using d3 data bind to TR/TD
989
990 data.propOrder.forEach(function(p) {
Thomas Vachuska1de66012014-10-30 03:03:30 -0700991 if (p === '-') {
992 addSep(tbody);
993 } else {
994 addProp(tbody, p, data.props[p]);
995 }
Simon Huntcc267562014-10-29 10:22:17 -0700996 });
997
Thomas Vachuska1de66012014-10-30 03:03:30 -0700998 function addSep(tbody) {
999 var tr = tbody.append('tr');
1000 $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
1001 }
1002
Simon Huntcc267562014-10-29 10:22:17 -07001003 function addProp(tbody, label, value) {
1004 var tr = tbody.append('tr');
1005
1006 tr.append('td')
1007 .attr('class', 'label')
1008 .text(label + ' :');
1009
1010 tr.append('td')
1011 .attr('class', 'value')
1012 .text(value);
Simon Huntc586e212014-10-28 21:24:08 -07001013 }
1014
Simon Huntcc267562014-10-29 10:22:17 -07001015 // show pane
Simon Huntc586e212014-10-28 21:24:08 -07001016 pane.transition().duration(750)
Simon Huntcc267562014-10-29 10:22:17 -07001017 .style('right', '20px')
1018 .style('opacity', 1.0);
Simon Hunt0b05d4a2014-10-21 21:50:15 -07001019 }
1020
1021 function highlightObject(obj) {
1022 if (obj) {
1023 if (obj != highlighted) {
1024 // TODO set or clear "inactive" class on nodes, based on criteria
1025 network.node.classed('inactive', function(d) {
1026 // return (obj !== d &&
1027 // d.relation(obj.id));
1028 return (obj !== d);
1029 });
1030 // TODO: same with links
1031 network.link.classed('inactive', function(d) {
1032 return (obj !== d.source && obj !== d.target);
1033 });
1034 }
1035 highlighted = obj;
1036 } else {
1037 if (highlighted) {
1038 // clear the inactive flag (no longer suppressed visually)
1039 network.node.classed('inactive', false);
1040 network.link.classed('inactive', false);
1041 }
1042 highlighted = null;
1043
1044 }
1045 }
1046
Simon Hunt9a16c822014-10-28 16:09:19 -07001047 function hoverObject(obj) {
1048 if (obj) {
1049 hovered = obj;
1050 } else {
1051 if (hovered) {
1052 hovered = null;
1053 }
1054 }
1055 }
1056
1057
Simon Huntc586e212014-10-28 21:24:08 -07001058 function resize() {
Simon Hunt0b05d4a2014-10-21 21:50:15 -07001059 view.height = window.innerHeight - config.mastHeight;
1060 view.width = window.innerWidth;
1061 $('#view')
1062 .css('height', view.height + 'px')
1063 .css('width', view.width + 'px');
1064
1065 network.forceWidth = view.width - config.force.marginLR;
1066 network.forceHeight = view.height - config.force.marginTB;
1067 }
1068
1069 // ======================================================================
1070 // register with the UI framework
1071
1072 api.addView('network', {
1073 load: loadNetworkView
1074 });
1075
1076
1077}(ONOS));
1078