blob: a116faa430b58f0b3df496ec64df05d7dea42fac [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,
39 collisionPrevention: true
40 },
Simon Huntb4d9d4c2014-10-30 11:27:23 -070041 data: {
42 live: {
43 jsonUrl: 'rs/topology/graph',
44 detailPrefix: 'rs/topology/graph/',
45 detailSuffix: ''
46 },
47 fake: {
48 jsonUrl: 'json/network2.json',
49 detailPrefix: 'json/',
50 detailSuffix: '.json'
51 }
52 },
Simon Hunt73171372014-10-30 09:25:36 -070053 iconUrl: {
54 device: 'img/device.png',
55 host: 'img/host.png',
56 pkt: 'img/pkt.png',
57 opt: 'img/opt.png'
58 },
59 mastHeight: 36,
60 force: {
61 note: 'node.class or link.class is used to differentiate',
62 linkDistance: {
63 infra: 200,
64 host: 40
Simon Huntd35961b2014-10-28 08:49:48 -070065 },
Simon Hunt73171372014-10-30 09:25:36 -070066 linkStrength: {
67 infra: 1.0,
68 host: 1.0
Simon Hunt2c9e0c22014-10-23 15:12:58 -070069 },
Simon Hunt73171372014-10-30 09:25:36 -070070 charge: {
71 device: -800,
72 host: -1000
Simon Hunt68ae6652014-10-22 13:58:07 -070073 },
Simon Hunt73171372014-10-30 09:25:36 -070074 ticksWithoutCollisions: 50,
75 marginLR: 20,
76 marginTB: 20,
77 translate: function() {
78 return 'translate(' +
79 config.force.marginLR + ',' +
80 config.force.marginTB + ')';
81 }
82 },
83 labels: {
84 imgPad: 16,
85 padLR: 8,
86 padTB: 6,
87 marginLR: 3,
88 marginTB: 2
89 },
90 icons: {
91 w: 32,
92 h: 32,
93 xoff: -12,
94 yoff: -8
95 },
96 constraints: {
97 ypos: {
98 host: 0.05,
99 switch: 0.3,
100 roadm: 0.7
101 }
102 },
103 hostLinkWidth: 1.0,
104 hostRadius: 7,
105 mouseOutTimerDelayMs: 120
106 };
Simon Huntd35961b2014-10-28 08:49:48 -0700107
108 // state variables
109 var view = {},
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700110 network = {},
111 selected = {},
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700112 highlighted = null,
Simon Hunt9a16c822014-10-28 16:09:19 -0700113 hovered = null,
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700114 viewMode = 'showAll';
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700115
116
Simon Huntd35961b2014-10-28 08:49:48 -0700117 function debug(what) {
118 return config.debugOn && config.debug[what];
119 }
120
Simon Huntb4d9d4c2014-10-30 11:27:23 -0700121 function urlData() {
122 return config.data[config.useLiveData ? 'live' : 'fake'];
123 }
124
125 function networkJsonUrl() {
126 return urlData().jsonUrl;
127 }
128
129 function detailJsonUrl(id) {
130 var u = urlData(),
131 encId = config.useLiveData ? encodeURIComponent(id)
132 : id.replace(/[^a-z0-9]/gi, '_');
133 return u.detailPrefix + encId + u.detailSuffix;
134 }
135
136
Simon Huntd35961b2014-10-28 08:49:48 -0700137 // load the topology view of the network
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700138 function loadNetworkView() {
139 // Hey, here I am, calling something on the ONOS api:
140 api.printTime();
141
142 resize();
143
Simon Huntd35961b2014-10-28 08:49:48 -0700144 // go get our network data from the server...
Simon Huntb4d9d4c2014-10-30 11:27:23 -0700145 var url = networkJsonUrl();
146 d3.json(url , function (err, data) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700147 if (err) {
148 alert('Oops! Error reading JSON...\n\n' +
Simon Huntb4d9d4c2014-10-30 11:27:23 -0700149 'URL: ' + url + '\n\n' +
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700150 'Error: ' + err.message);
151 return;
152 }
Simon Huntd35961b2014-10-28 08:49:48 -0700153// console.log("here is the JSON data...");
154// console.log(data);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700155
156 network.data = data;
157 drawNetwork();
158 });
159
Simon Huntd35961b2014-10-28 08:49:48 -0700160 // while we wait for the data, set up the handlers...
161 setUpClickHandler();
162 setUpRadioButtonHandler();
163 setUpKeyHandler();
164 $(window).on('resize', resize);
165 }
166
167 function setUpClickHandler() {
168 // click handler for "selectable" objects
169 $(document).on('click', '.select-object', function () {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700170 // when any object of class "select-object" is clicked...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700171 var obj = network.lookup[$(this).data('id')];
172 if (obj) {
173 selectObject(obj);
174 }
175 // stop propagation of event (I think) ...
176 return false;
177 });
Simon Huntd35961b2014-10-28 08:49:48 -0700178 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700179
Simon Huntd35961b2014-10-28 08:49:48 -0700180 function setUpRadioButtonHandler() {
181 d3.selectAll('#displayModes .radio').on('click', function () {
182 var id = d3.select(this).attr('id');
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700183 if (id !== viewMode) {
184 radioButton('displayModes', id);
185 viewMode = id;
Simon Huntf967d512014-10-28 20:34:29 -0700186 doRadioAction(id);
187 }
188 });
189 }
190
191 function doRadioAction(id) {
192 showAllLayers();
193 if (id === 'showPkt') {
194 showPacketLayer();
195 } else if (id === 'showOpt') {
196 showOpticalLayer();
197 }
198 }
199
200 function showAllLayers() {
201 network.node.classed('inactive', false);
202 network.link.classed('inactive', false);
203 }
204
205 function showPacketLayer() {
206 network.node.each(function(d) {
207 // deactivate nodes that are not hosts or switches
208 if (d.class === 'device' && d.type !== 'switch') {
209 d3.select(this).classed('inactive', true);
210 }
211 });
212
213 network.link.each(function(lnk) {
214 // deactivate infrastructure links that have opt's as endpoints
215 if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') {
216 d3.select(this).classed('inactive', true);
217 }
218 });
219 }
220
221 function showOpticalLayer() {
222 network.node.each(function(d) {
223 // deactivate nodes that are not optical devices
224 if (d.type !== 'roadm') {
225 d3.select(this).classed('inactive', true);
226 }
227 });
228
229 network.link.each(function(lnk) {
230 // deactivate infrastructure links that have opt's as endpoints
231 if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') {
232 d3.select(this).classed('inactive', true);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700233 }
234 });
235 }
236
Simon Huntd35961b2014-10-28 08:49:48 -0700237 function setUpKeyHandler() {
238 d3.select('body')
239 .on('keydown', function () {
240 processKeyEvent();
241 if (debug('showKeyHandler')) {
242 network.svg.append('text')
243 .attr('x', 5)
244 .attr('y', 15)
245 .style('font-size', '20pt')
246 .text('keyCode: ' + d3.event.keyCode +
247 ' applied to : ' + contextLabel())
248 .transition().duration(2000)
249 .style('font-size', '2pt')
250 .style('fill-opacity', 0.01)
251 .remove();
252 }
253 });
254 }
255
Simon Hunt9a16c822014-10-28 16:09:19 -0700256 function contextLabel() {
257 return hovered === null ? "(nothing)" : hovered.id;
258 }
259
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700260 function radioButton(group, id) {
261 d3.selectAll("#" + group + " .radio").classed("active", false);
262 d3.select("#" + group + " #" + id).classed("active", true);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700263 }
264
Simon Huntd35961b2014-10-28 08:49:48 -0700265 function processKeyEvent() {
266 var code = d3.event.keyCode;
267 switch (code) {
Thomas Vachuska1de66012014-10-30 03:03:30 -0700268 case 71: // G
269 cycleLayout();
270 break;
Simon Huntd35961b2014-10-28 08:49:48 -0700271 case 76: // L
272 cycleLabels();
273 break;
274 case 80: // P
275 togglePorts();
Simon Hunt9a16c822014-10-28 16:09:19 -0700276 break;
277 case 85: // U
278 unpin();
279 break;
Simon Huntd35961b2014-10-28 08:49:48 -0700280 }
281
282 }
283
Thomas Vachuska1de66012014-10-30 03:03:30 -0700284 function cycleLayout() {
285 config.options.layering = !config.options.layering;
286 network.force.resume();
287 }
288
Simon Huntd35961b2014-10-28 08:49:48 -0700289 function cycleLabels() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700290 console.log('Cycle Labels - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700291 }
292
293 function togglePorts() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700294 console.log('Toggle Ports - context = ' + contextLabel());
295 }
296
297 function unpin() {
298 if (hovered) {
299 hovered.fixed = false;
Simon Huntf967d512014-10-28 20:34:29 -0700300 findNodeFromData(hovered).classed('fixed', false);
Simon Hunt9a16c822014-10-28 16:09:19 -0700301 network.force.resume();
302 }
303 console.log('Unpin - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700304 }
305
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700306
307 // ========================================================
308
309 function drawNetwork() {
310 $('#view').empty();
311
312 prepareNodesAndLinks();
313 createLayout();
314 console.log("\n\nHere is the augmented network object...");
Simon Hunt9a16c822014-10-28 16:09:19 -0700315 console.log(network);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700316 }
317
318 function prepareNodesAndLinks() {
319 network.lookup = {};
320 network.nodes = [];
321 network.links = [];
322
323 var nw = network.forceWidth,
324 nh = network.forceHeight;
325
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700326 function yPosConstraintForNode(n) {
327 return config.constraints.ypos[n.type || 'host'];
328 }
329
330 // Note that both 'devices' and 'hosts' get mapped into the nodes array
331
332 // first, the devices...
333 network.data.devices.forEach(function(n) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700334 var ypc = yPosConstraintForNode(n),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700335 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700336 iy = ypc * nh,
337 node = {
338 id: n.id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700339 labels: n.labels,
340 class: 'device',
341 icon: 'device',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700342 type: n.type,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700343 x: ix,
344 y: iy,
345 constraint: {
346 weight: 0.7,
347 y: iy
348 }
349 };
350 network.lookup[n.id] = node;
351 network.nodes.push(node);
352 });
353
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700354 // then, the hosts...
355 network.data.hosts.forEach(function(n) {
356 var ypc = yPosConstraintForNode(n),
357 ix = Math.random() * 0.6 * nw + 0.2 * nw,
358 iy = ypc * nh,
359 node = {
360 id: n.id,
361 labels: n.labels,
362 class: 'host',
363 icon: 'host',
364 type: n.type,
365 x: ix,
366 y: iy,
367 constraint: {
368 weight: 0.7,
369 y: iy
370 }
371 };
372 network.lookup[n.id] = node;
373 network.nodes.push(node);
374 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700375
376
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700377 // now, process the explicit links...
Simon Hunt6f376a32014-10-28 12:38:30 -0700378 network.data.links.forEach(function(lnk) {
379 var src = network.lookup[lnk.src],
380 dst = network.lookup[lnk.dst],
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700381 id = src.id + "~" + dst.id;
382
383 var link = {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700384 class: 'infra',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700385 id: id,
Simon Hunt6f376a32014-10-28 12:38:30 -0700386 type: lnk.type,
387 width: lnk.linkWidth,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700388 source: src,
389 target: dst,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700390 strength: config.force.linkStrength.infra
391 };
392 network.links.push(link);
393 });
394
395 // finally, infer host links...
396 network.data.hosts.forEach(function(n) {
397 var src = network.lookup[n.id],
398 dst = network.lookup[n.cp.device],
399 id = src.id + "~" + dst.id;
400
401 var link = {
402 class: 'host',
403 id: id,
404 type: 'hostLink',
405 width: config.hostLinkWidth,
406 source: src,
407 target: dst,
408 strength: config.force.linkStrength.host
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700409 };
410 network.links.push(link);
411 });
412 }
413
414 function createLayout() {
415
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700416 var cfg = config.force;
417
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700418 network.force = d3.layout.force()
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700419 .size([network.forceWidth, network.forceHeight])
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700420 .nodes(network.nodes)
421 .links(network.links)
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700422 .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
423 .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
424 .charge(function(d) { return cfg.charge[d.class]; })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700425 .on('tick', tick);
426
427 network.svg = d3.select('#view').append('svg')
428 .attr('width', view.width)
429 .attr('height', view.height)
430 .append('g')
Simon Huntae968a62014-10-22 14:54:41 -0700431 .attr('transform', config.force.translate());
Simon Hunt3ab76a82014-10-22 13:07:32 -0700432// .attr('id', 'zoomable')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700433// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700434
Thomas Vachuska8cd66a52014-10-30 11:53:07 -0700435 network.svg.append('svg:image')
436 .attr({
437 id: 'bg',
438 width: view.width,
439 height: view.height,
440 'xlink:href': 'img/us-map.png'
441 });
442
Simon Hunt3ab76a82014-10-22 13:07:32 -0700443// function zoomRedraw() {
444// d3.select("#zoomable").attr("transform",
445// "translate(" + d3.event.translate + ")"
446// + " scale(" + d3.event.scale + ")");
447// }
448
Simon Hunt3ab76a82014-10-22 13:07:32 -0700449 // TODO: move glow/blur stuff to util script
450 var glow = network.svg.append('filter')
451 .attr('x', '-50%')
452 .attr('y', '-50%')
453 .attr('width', '200%')
454 .attr('height', '200%')
455 .attr('id', 'blue-glow');
456
457 glow.append('feColorMatrix')
458 .attr('type', 'matrix')
459 .attr('values', '0 0 0 0 0 ' +
460 '0 0 0 0 0 ' +
461 '0 0 0 0 .7 ' +
462 '0 0 0 1 0 ');
463
464 glow.append('feGaussianBlur')
465 .attr('stdDeviation', 3)
466 .attr('result', 'coloredBlur');
467
468 glow.append('feMerge').selectAll('feMergeNode')
469 .data(['coloredBlur', 'SourceGraphic'])
470 .enter().append('feMergeNode')
471 .attr('in', String);
472
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700473 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700474// $('#view').on('scroll', function() {
475//
476// });
477
478
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700479 // add links to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700480 network.link = network.svg.append('g').selectAll('.link')
481 .data(network.force.links(), function(d) {return d.id})
482 .enter().append('line')
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700483 .attr('class', function(d) {return 'link ' + d.class});
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700484
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700485
Simon Hunt6f376a32014-10-28 12:38:30 -0700486 // TODO: move drag behavior into separate method.
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700487 // == define node drag behavior...
Simon Hunt3ab76a82014-10-22 13:07:32 -0700488 network.draggedThreshold = d3.scale.linear()
489 .domain([0, 0.1])
490 .range([5, 20])
491 .clamp(true);
492
493 function dragged(d) {
494 var threshold = network.draggedThreshold(network.force.alpha()),
495 dx = d.oldX - d.px,
496 dy = d.oldY - d.py;
497 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
498 d.dragged = true;
499 }
500 return d.dragged;
501 }
502
503 network.drag = d3.behavior.drag()
504 .origin(function(d) { return d; })
505 .on('dragstart', function(d) {
506 d.oldX = d.x;
507 d.oldY = d.y;
508 d.dragged = false;
509 d.fixed |= 2;
510 })
511 .on('drag', function(d) {
512 d.px = d3.event.x;
513 d.py = d3.event.y;
514 if (dragged(d)) {
515 if (!network.force.alpha()) {
516 network.force.alpha(.025);
517 }
518 }
519 })
520 .on('dragend', function(d) {
521 if (!dragged(d)) {
522 selectObject(d, this);
523 }
524 d.fixed &= ~6;
Simon Hunt9a16c822014-10-28 16:09:19 -0700525
526 // once we've finished moving, pin the node in position,
Simon Huntf967d512014-10-28 20:34:29 -0700527 // if it is a device (not a host)
Simon Hunt9a16c822014-10-28 16:09:19 -0700528 if (d.class === 'device') {
529 d.fixed = true;
Simon Huntf967d512014-10-28 20:34:29 -0700530 d3.select(this).classed('fixed', true)
Simon Hunt9a16c822014-10-28 16:09:19 -0700531 }
Simon Hunt3ab76a82014-10-22 13:07:32 -0700532 });
533
534 $('#view').on('click', function(e) {
535 if (!$(e.target).closest('.node').length) {
536 deselectObject();
537 }
538 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700539
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700540
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700541 // add nodes to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700542 network.node = network.svg.selectAll('.node')
543 .data(network.force.nodes(), function(d) {return d.id})
544 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700545 .attr('class', function(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700546 var cls = 'node ' + d.class;
547 if (d.type) {
548 cls += ' ' + d.type;
549 }
550 return cls;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700551 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700552 .attr('transform', function(d) {
553 return translate(d.x, d.y);
554 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700555 .call(network.drag)
556 .on('mouseover', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700557 // TODO: show tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700558 if (network.mouseoutTimeout) {
559 clearTimeout(network.mouseoutTimeout);
560 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700561 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700562 hoverObject(d);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700563 })
564 .on('mouseout', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700565 // TODO: hide tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700566 if (network.mouseoutTimeout) {
567 clearTimeout(network.mouseoutTimeout);
568 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700569 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700570 network.mouseoutTimeout = setTimeout(function() {
571 hoverObject(null);
572 }, config.mouseOutTimerDelayMs);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700573 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700574
Simon Hunt6f376a32014-10-28 12:38:30 -0700575
576 // deal with device nodes first
577 network.nodeRect = network.node.filter('.device')
578 .append('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700579 .attr({
580 rx: 5,
581 ry: 5,
582 width: 100,
583 height: 12
584 });
585 // note that width/height are adjusted to fit the label text
Simon Hunt6f376a32014-10-28 12:38:30 -0700586 // then padded, and space made for the icon.
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700587
Simon Hunt6f376a32014-10-28 12:38:30 -0700588 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700589 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700590 icon = iconUrl(d);
591
592 node.append('text')
593 // TODO: add label cycle behavior
594 .text(d.id)
595 .attr('dy', '1.1em');
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700596
597 if (icon) {
598 var cfg = config.icons;
599 node.append('svg:image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700600 .attr({
601 width: cfg.w,
602 height: cfg.h,
603 'xlink:href': icon
604 });
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700605 // note, icon relative positioning (x,y) is done after we have
606 // adjusted the bounds of the rectangle...
607 }
Simon Hunt68ae6652014-10-22 13:58:07 -0700608
Simon Huntd35961b2014-10-28 08:49:48 -0700609 // debug function to show the modelled x,y coordinates of nodes...
610 if (debug('showNodeXY')) {
611 node.select('rect').attr('fill-opacity', 0.5);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700612 node.append('circle')
613 .attr({
614 class: 'debug',
615 cx: 0,
616 cy: 0,
617 r: '3px'
618 });
619 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700620 });
621
Simon Hunt6f376a32014-10-28 12:38:30 -0700622 // now process host nodes
623 network.nodeCircle = network.node.filter('.host')
624 .append('circle')
625 .attr({
626 r: config.hostRadius
627 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700628
Simon Hunt6f376a32014-10-28 12:38:30 -0700629 network.node.filter('.host').each(function(d) {
630 var node = d3.select(this),
631 icon = iconUrl(d);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700632
Simon Hunt6f376a32014-10-28 12:38:30 -0700633 // debug function to show the modelled x,y coordinates of nodes...
634 if (debug('showNodeXY')) {
635 node.select('circle').attr('fill-opacity', 0.5);
636 node.append('circle')
637 .attr({
638 class: 'debug',
639 cx: 0,
640 cy: 0,
641 r: '3px'
642 });
643 }
644 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700645
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700646 // this function is scheduled to happen soon after the given thread ends
647 setTimeout(function() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700648 // post process the device nodes, to pad their size to fit the
649 // label text and attach the icon to the right location.
650 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700651 // for every node, recompute size, padding, etc. so text fits
652 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700653 text = node.select('text'),
654 box = adjustRectToFitText(node),
655 lab = config.labels;
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700656
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700657 // now make the computed adjustment
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700658 node.select('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700659 .attr(box);
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700660
661 node.select('image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700662 .attr('x', box.x + config.icons.xoff)
663 .attr('y', box.y + config.icons.yoff);
Simon Hunt1c219892014-10-22 16:32:39 -0700664
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700665 var bounds = boundsFromBox(box);
666
667 // todo: clean up extent and edge work..
Simon Hunt1c219892014-10-22 16:32:39 -0700668 d.extent = {
669 left: bounds.x1 - lab.marginLR,
670 right: bounds.x2 + lab.marginLR,
671 top: bounds.y1 - lab.marginTB,
672 bottom: bounds.y2 + lab.marginTB
673 };
674
675 d.edge = {
676 left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
677 right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
678 top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
679 bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
680 };
681
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700682 });
683
684 network.numTicks = 0;
685 network.preventCollisions = false;
686 network.force.start();
Simon Hunt1c219892014-10-22 16:32:39 -0700687 for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700688 network.force.tick();
689 }
690 network.preventCollisions = true;
691 $('#view').css('visibility', 'visible');
692 });
693
Simon Hunt6f376a32014-10-28 12:38:30 -0700694
695 // returns the newly computed bounding box of the rectangle
696 function adjustRectToFitText(n) {
697 var text = n.select('text'),
698 box = text.node().getBBox(),
699 lab = config.labels;
700
Simon Hunt9a16c822014-10-28 16:09:19 -0700701 // not sure why n.data() returns an array of 1 element...
702 var data = n.data()[0];
703
Simon Hunt6f376a32014-10-28 12:38:30 -0700704 text.attr('text-anchor', 'middle')
705 .attr('y', '-0.8em')
706 .attr('x', lab.imgPad/2)
707 ;
708
Simon Hunt6f376a32014-10-28 12:38:30 -0700709 // translate the bbox so that it is centered on [x,y]
710 box.x = -box.width / 2;
711 box.y = -box.height / 2;
712
713 // add padding
714 box.x -= (lab.padLR + lab.imgPad/2);
715 box.width += lab.padLR * 2 + lab.imgPad;
716 box.y -= lab.padTB;
717 box.height += lab.padTB * 2;
718
719 return box;
720 }
721
722 function boundsFromBox(box) {
723 return {
724 x1: box.x,
725 y1: box.y,
726 x2: box.x + box.width,
727 y2: box.y + box.height
728 };
729 }
730
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700731 }
732
Simon Hunt68ae6652014-10-22 13:58:07 -0700733 function iconUrl(d) {
Thomas Vachuska1de66012014-10-30 03:03:30 -0700734 return 'img/' + d.type + '.png';
735// return config.iconUrl[d.icon];
Simon Hunt68ae6652014-10-22 13:58:07 -0700736 }
737
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700738 function translate(x, y) {
739 return 'translate(' + x + ',' + y + ')';
740 }
741
Simon Hunt6f376a32014-10-28 12:38:30 -0700742 // prevents collisions amongst device nodes
Simon Hunt1c219892014-10-22 16:32:39 -0700743 function preventCollisions() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700744 var quadtree = d3.geom.quadtree(network.nodes),
745 hrad = config.hostRadius;
Simon Hunt1c219892014-10-22 16:32:39 -0700746
747 network.nodes.forEach(function(n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700748 var nx1, nx2, ny1, ny2;
749
750 if (n.class === 'device') {
751 nx1 = n.x + n.extent.left;
752 nx2 = n.x + n.extent.right;
753 ny1 = n.y + n.extent.top;
Simon Hunt1c219892014-10-22 16:32:39 -0700754 ny2 = n.y + n.extent.bottom;
755
Simon Hunt6f376a32014-10-28 12:38:30 -0700756 } else {
757 nx1 = n.x - hrad;
758 nx2 = n.x + hrad;
759 ny1 = n.y - hrad;
760 ny2 = n.y + hrad;
761 }
762
Simon Hunt1c219892014-10-22 16:32:39 -0700763 quadtree.visit(function(quad, x1, y1, x2, y2) {
764 if (quad.point && quad.point !== n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700765 // check if the rectangles/circles intersect
Simon Hunt1c219892014-10-22 16:32:39 -0700766 var p = quad.point,
Simon Hunt6f376a32014-10-28 12:38:30 -0700767 px1, px2, py1, py2, ix;
768
769 if (p.class === 'device') {
770 px1 = p.x + p.extent.left;
771 px2 = p.x + p.extent.right;
772 py1 = p.y + p.extent.top;
773 py2 = p.y + p.extent.bottom;
774
775 } else {
776 px1 = p.x - hrad;
777 px2 = p.x + hrad;
778 py1 = p.y - hrad;
779 py2 = p.y + hrad;
780 }
781
782 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
783
Simon Hunt1c219892014-10-22 16:32:39 -0700784 if (ix) {
785 var xa1 = nx2 - px1, // shift n left , p right
786 xa2 = px2 - nx1, // shift n right, p left
787 ya1 = ny2 - py1, // shift n up , p down
788 ya2 = py2 - ny1, // shift n down , p up
789 adj = Math.min(xa1, xa2, ya1, ya2);
790
791 if (adj == xa1) {
792 n.x -= adj / 2;
793 p.x += adj / 2;
794 } else if (adj == xa2) {
795 n.x += adj / 2;
796 p.x -= adj / 2;
797 } else if (adj == ya1) {
798 n.y -= adj / 2;
799 p.y += adj / 2;
800 } else if (adj == ya2) {
801 n.y += adj / 2;
802 p.y -= adj / 2;
803 }
804 }
805 return ix;
806 }
807 });
808
809 });
810 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700811
812 function tick(e) {
813 network.numTicks++;
814
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700815 if (config.options.layering) {
Simon Hunt68ae6652014-10-22 13:58:07 -0700816 // adjust the y-coord of each node, based on y-pos constraints
817 network.nodes.forEach(function (n) {
818 var z = e.alpha * n.constraint.weight;
819 if (!isNaN(n.constraint.y)) {
820 n.y = (n.constraint.y * z + n.y * (1 - z));
821 }
822 });
823 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700824
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700825 if (config.options.collisionPrevention && network.preventCollisions) {
Simon Hunt1c219892014-10-22 16:32:39 -0700826 preventCollisions();
827 }
828
Simon Huntd35961b2014-10-28 08:49:48 -0700829 // clip visualization of links at bounds of nodes...
830 network.link.each(function(d) {
831 var xs = d.source.x,
832 ys = d.source.y,
833 xt = d.target.x,
834 yt = d.target.y,
835 line = new geo.LineSegment(xs, ys, xt, yt),
836 e, ix;
Simon Hunt1c219892014-10-22 16:32:39 -0700837
Simon Huntd35961b2014-10-28 08:49:48 -0700838 for (e in d.source.edge) {
839 ix = line.intersect(d.source.edge[e].offset(xs, ys));
Simon Hunt1c219892014-10-22 16:32:39 -0700840 if (ix.in1 && ix.in2) {
Simon Huntd35961b2014-10-28 08:49:48 -0700841 xs = ix.x;
842 ys = ix.y;
843 break;
844 }
845 }
846
847 for (e in d.target.edge) {
848 ix = line.intersect(d.target.edge[e].offset(xt, yt));
849 if (ix.in1 && ix.in2) {
850 xt = ix.x;
851 yt = ix.y;
Simon Hunt1c219892014-10-22 16:32:39 -0700852 break;
853 }
854 }
855
856 d3.select(this)
Simon Huntd35961b2014-10-28 08:49:48 -0700857 .attr('x1', xs)
858 .attr('y1', ys)
859 .attr('x2', xt)
860 .attr('y2', yt);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700861 });
862
Simon Huntd35961b2014-10-28 08:49:48 -0700863 // position each node by translating the node (group) by x,y
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700864 network.node
865 .attr('transform', function(d) {
866 return translate(d.x, d.y);
867 });
868
869 }
870
871 // $('#docs-close').on('click', function() {
872 // deselectObject();
873 // return false;
874 // });
875
876 // $(document).on('click', '.select-object', function() {
877 // var obj = graph.data[$(this).data('name')];
878 // if (obj) {
879 // selectObject(obj);
880 // }
881 // return false;
882 // });
883
Simon Hunt6f376a32014-10-28 12:38:30 -0700884 function findNodeFromData(d) {
885 var el = null;
886 network.node.filter('.' + d.class).each(function(n) {
887 if (n.id === d.id) {
888 el = d3.select(this);
889 }
890 });
891 return el;
892 }
893
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700894 function selectObject(obj, el) {
895 var node;
896 if (el) {
897 node = d3.select(el);
898 } else {
899 network.node.each(function(d) {
900 if (d == obj) {
901 node = d3.select(el = this);
902 }
903 });
904 }
905 if (!node) return;
906
907 if (node.classed('selected')) {
908 deselectObject();
Simon Huntc586e212014-10-28 21:24:08 -0700909 flyinPane(null);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700910 return;
911 }
912 deselectObject(false);
913
914 selected = {
915 obj : obj,
916 el : el
917 };
918
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700919 node.classed('selected', true);
Simon Huntc586e212014-10-28 21:24:08 -0700920 flyinPane(obj);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700921 }
922
923 function deselectObject(doResize) {
924 // Review: logic of 'resize(...)' function.
925 if (doResize || typeof doResize == 'undefined') {
926 resize(false);
927 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700928
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700929 // deselect all nodes in the network...
930 network.node.classed('selected', false);
931 selected = {};
Simon Huntc586e212014-10-28 21:24:08 -0700932 flyinPane(null);
933 }
934
935 function flyinPane(obj) {
936 var pane = d3.select('#flyout'),
Simon Huntcc267562014-10-29 10:22:17 -0700937 url;
Simon Huntc586e212014-10-28 21:24:08 -0700938
939 if (obj) {
Simon Huntcc267562014-10-29 10:22:17 -0700940 // go get details of the selected object from the server...
Simon Huntb4d9d4c2014-10-30 11:27:23 -0700941 url = detailJsonUrl(obj.id);
Simon Huntcc267562014-10-29 10:22:17 -0700942 d3.json(url, function (err, data) {
943 if (err) {
944 alert('Oops! Error reading JSON...\n\n' +
945 'URL: ' + url + '\n\n' +
946 'Error: ' + err.message);
947 return;
948 }
949// console.log("JSON data... " + url);
950// console.log(data);
951
952 displayDetails(data, pane);
953 });
954
955 } else {
956 // hide pane
957 pane.transition().duration(750)
958 .style('right', '-320px')
959 .style('opacity', 0.0);
960 }
961 }
962
963 function displayDetails(data, pane) {
964 $('#flyout').empty();
965
Thomas Vachuska1de66012014-10-30 03:03:30 -0700966 var title = pane.append("h2"),
967 table = pane.append("table"),
Simon Huntcc267562014-10-29 10:22:17 -0700968 tbody = table.append("tbody");
969
Thomas Vachuska1de66012014-10-30 03:03:30 -0700970 $('<img src="img/' + data.type + '.png">').appendTo(title);
971 $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
972
973
Simon Huntcc267562014-10-29 10:22:17 -0700974 // TODO: consider using d3 data bind to TR/TD
975
976 data.propOrder.forEach(function(p) {
Thomas Vachuska1de66012014-10-30 03:03:30 -0700977 if (p === '-') {
978 addSep(tbody);
979 } else {
980 addProp(tbody, p, data.props[p]);
981 }
Simon Huntcc267562014-10-29 10:22:17 -0700982 });
983
Thomas Vachuska1de66012014-10-30 03:03:30 -0700984 function addSep(tbody) {
985 var tr = tbody.append('tr');
986 $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
987 }
988
Simon Huntcc267562014-10-29 10:22:17 -0700989 function addProp(tbody, label, value) {
990 var tr = tbody.append('tr');
991
992 tr.append('td')
993 .attr('class', 'label')
994 .text(label + ' :');
995
996 tr.append('td')
997 .attr('class', 'value')
998 .text(value);
Simon Huntc586e212014-10-28 21:24:08 -0700999 }
1000
Simon Huntcc267562014-10-29 10:22:17 -07001001 // show pane
Simon Huntc586e212014-10-28 21:24:08 -07001002 pane.transition().duration(750)
Simon Huntcc267562014-10-29 10:22:17 -07001003 .style('right', '20px')
1004 .style('opacity', 1.0);
Simon Hunt0b05d4a2014-10-21 21:50:15 -07001005 }
1006
1007 function highlightObject(obj) {
1008 if (obj) {
1009 if (obj != highlighted) {
1010 // TODO set or clear "inactive" class on nodes, based on criteria
1011 network.node.classed('inactive', function(d) {
1012 // return (obj !== d &&
1013 // d.relation(obj.id));
1014 return (obj !== d);
1015 });
1016 // TODO: same with links
1017 network.link.classed('inactive', function(d) {
1018 return (obj !== d.source && obj !== d.target);
1019 });
1020 }
1021 highlighted = obj;
1022 } else {
1023 if (highlighted) {
1024 // clear the inactive flag (no longer suppressed visually)
1025 network.node.classed('inactive', false);
1026 network.link.classed('inactive', false);
1027 }
1028 highlighted = null;
1029
1030 }
1031 }
1032
Simon Hunt9a16c822014-10-28 16:09:19 -07001033 function hoverObject(obj) {
1034 if (obj) {
1035 hovered = obj;
1036 } else {
1037 if (hovered) {
1038 hovered = null;
1039 }
1040 }
1041 }
1042
1043
Simon Huntc586e212014-10-28 21:24:08 -07001044 function resize() {
Simon Hunt0b05d4a2014-10-21 21:50:15 -07001045 view.height = window.innerHeight - config.mastHeight;
1046 view.width = window.innerWidth;
1047 $('#view')
1048 .css('height', view.height + 'px')
1049 .css('width', view.width + 'px');
1050
1051 network.forceWidth = view.width - config.force.marginLR;
1052 network.forceHeight = view.height - config.force.marginTB;
1053 }
1054
1055 // ======================================================================
1056 // register with the UI framework
1057
1058 api.addView('network', {
1059 load: loadNetworkView
1060 });
1061
1062
1063}(ONOS));
1064