blob: 4f8f76e1434da3ea992a67b804d54e03a29e4215 [file] [log] [blame]
Simon Hunt0b05d4a2014-10-21 21:50:15 -07001/*
Thomas Vachuska4f1a60c2014-10-28 13:39:07 -07002 * Copyright 2014 Open Networking Laboratory
Thomas Vachuska781d18b2014-10-27 10:31:25 -07003 *
Thomas Vachuska4f1a60c2014-10-28 13:39:07 -07004 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
Thomas Vachuska781d18b2014-10-27 10:31:25 -07007 *
Thomas Vachuska4f1a60c2014-10-28 13:39:07 -07008 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
Thomas Vachuska781d18b2014-10-27 10:31:25 -070015 */
16
17/*
Simon Hunt0b05d4a2014-10-21 21:50:15 -070018 ONOS network topology viewer - PoC version 1.0
19
20 @author Simon Hunt
21 */
22
23(function (onos) {
24 'use strict';
25
Simon Huntd35961b2014-10-28 08:49:48 -070026 // reference to the framework api
Simon Hunt0b05d4a2014-10-21 21:50:15 -070027 var api = onos.api;
28
Simon Huntd35961b2014-10-28 08:49:48 -070029 // configuration data
Simon Hunt0b05d4a2014-10-21 21:50:15 -070030 var config = {
Simon Huntd35961b2014-10-28 08:49:48 -070031 debugOn: false,
32 debug: {
Simon Hunt9a16c822014-10-28 16:09:19 -070033 showNodeXY: false,
34 showKeyHandler: true
Simon Huntd35961b2014-10-28 08:49:48 -070035 },
Simon Hunt2c9e0c22014-10-23 15:12:58 -070036 options: {
Simon Hunt19cb0982014-10-23 16:44:49 -070037 layering: true,
Simon Huntd35961b2014-10-28 08:49:48 -070038 collisionPrevention: true
Simon Hunt2c9e0c22014-10-23 15:12:58 -070039 },
Thomas Vachuska598924e2014-10-23 22:26:07 -070040 XjsonUrl: 'rs/topology/graph',
Simon Huntcc267562014-10-29 10:22:17 -070041 jsonUrl: 'json/network.json',
42 jsonPrefix: 'json/',
Simon Hunt68ae6652014-10-22 13:58:07 -070043 iconUrl: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070044 device: 'img/device.png',
45 host: 'img/host.png',
46 pkt: 'img/pkt.png',
47 opt: 'img/opt.png'
Simon Hunt68ae6652014-10-22 13:58:07 -070048 },
Simon Hunt19cb0982014-10-23 16:44:49 -070049 mastHeight: 36,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070050 force: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070051 note: 'node.class or link.class is used to differentiate',
52 linkDistance: {
Simon Hunt6f376a32014-10-28 12:38:30 -070053 infra: 200,
Simon Hunt9a16c822014-10-28 16:09:19 -070054 host: 40
Simon Hunt2c9e0c22014-10-23 15:12:58 -070055 },
56 linkStrength: {
57 infra: 1.0,
Simon Hunt6f376a32014-10-28 12:38:30 -070058 host: 1.0
Simon Hunt2c9e0c22014-10-23 15:12:58 -070059 },
60 charge: {
61 device: -800,
Simon Hunt9a16c822014-10-28 16:09:19 -070062 host: -1000
Simon Hunt2c9e0c22014-10-23 15:12:58 -070063 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070064 ticksWithoutCollisions: 50,
65 marginLR: 20,
66 marginTB: 20,
67 translate: function() {
68 return 'translate(' +
69 config.force.marginLR + ',' +
70 config.force.marginTB + ')';
71 }
72 },
73 labels: {
Simon Hunt19cb0982014-10-23 16:44:49 -070074 imgPad: 16,
Simon Hunt1c5f8b62014-10-22 14:43:01 -070075 padLR: 8,
76 padTB: 6,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070077 marginLR: 3,
78 marginTB: 2
79 },
Simon Hunt2c9e0c22014-10-23 15:12:58 -070080 icons: {
81 w: 32,
82 h: 32,
83 xoff: -12,
Simon Hunt19cb0982014-10-23 16:44:49 -070084 yoff: -8
Simon Hunt2c9e0c22014-10-23 15:12:58 -070085 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070086 constraints: {
87 ypos: {
Simon Hunt9a16c822014-10-28 16:09:19 -070088 host: 0.05,
Simon Hunt2c9e0c22014-10-23 15:12:58 -070089 switch: 0.3,
90 roadm: 0.7
Simon Hunt0b05d4a2014-10-21 21:50:15 -070091 }
Simon Hunt2c9e0c22014-10-23 15:12:58 -070092 },
93 hostLinkWidth: 1.0,
Simon Hunt6f376a32014-10-28 12:38:30 -070094 hostRadius: 7,
Simon Hunt2c9e0c22014-10-23 15:12:58 -070095 mouseOutTimerDelayMs: 120
Simon Huntd35961b2014-10-28 08:49:48 -070096 };
97
98 // state variables
99 var view = {},
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700100 network = {},
101 selected = {},
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700102 highlighted = null,
Simon Hunt9a16c822014-10-28 16:09:19 -0700103 hovered = null,
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700104 viewMode = 'showAll';
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700105
106
Simon Huntd35961b2014-10-28 08:49:48 -0700107 function debug(what) {
108 return config.debugOn && config.debug[what];
109 }
110
111 // load the topology view of the network
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700112 function loadNetworkView() {
113 // Hey, here I am, calling something on the ONOS api:
114 api.printTime();
115
116 resize();
117
Simon Huntd35961b2014-10-28 08:49:48 -0700118 // go get our network data from the server...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700119 d3.json(config.jsonUrl, function (err, data) {
120 if (err) {
121 alert('Oops! Error reading JSON...\n\n' +
Simon Huntae968a62014-10-22 14:54:41 -0700122 'URL: ' + config.jsonUrl + '\n\n' +
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700123 'Error: ' + err.message);
124 return;
125 }
Simon Huntd35961b2014-10-28 08:49:48 -0700126// console.log("here is the JSON data...");
127// console.log(data);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700128
129 network.data = data;
130 drawNetwork();
131 });
132
Simon Huntd35961b2014-10-28 08:49:48 -0700133 // while we wait for the data, set up the handlers...
134 setUpClickHandler();
135 setUpRadioButtonHandler();
136 setUpKeyHandler();
137 $(window).on('resize', resize);
138 }
139
140 function setUpClickHandler() {
141 // click handler for "selectable" objects
142 $(document).on('click', '.select-object', function () {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700143 // when any object of class "select-object" is clicked...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700144 var obj = network.lookup[$(this).data('id')];
145 if (obj) {
146 selectObject(obj);
147 }
148 // stop propagation of event (I think) ...
149 return false;
150 });
Simon Huntd35961b2014-10-28 08:49:48 -0700151 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700152
Simon Huntd35961b2014-10-28 08:49:48 -0700153 function setUpRadioButtonHandler() {
154 d3.selectAll('#displayModes .radio').on('click', function () {
155 var id = d3.select(this).attr('id');
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700156 if (id !== viewMode) {
157 radioButton('displayModes', id);
158 viewMode = id;
Simon Huntf967d512014-10-28 20:34:29 -0700159 doRadioAction(id);
160 }
161 });
162 }
163
164 function doRadioAction(id) {
165 showAllLayers();
166 if (id === 'showPkt') {
167 showPacketLayer();
168 } else if (id === 'showOpt') {
169 showOpticalLayer();
170 }
171 }
172
173 function showAllLayers() {
174 network.node.classed('inactive', false);
175 network.link.classed('inactive', false);
176 }
177
178 function showPacketLayer() {
179 network.node.each(function(d) {
180 // deactivate nodes that are not hosts or switches
181 if (d.class === 'device' && d.type !== 'switch') {
182 d3.select(this).classed('inactive', true);
183 }
184 });
185
186 network.link.each(function(lnk) {
187 // deactivate infrastructure links that have opt's as endpoints
188 if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') {
189 d3.select(this).classed('inactive', true);
190 }
191 });
192 }
193
194 function showOpticalLayer() {
195 network.node.each(function(d) {
196 // deactivate nodes that are not optical devices
197 if (d.type !== 'roadm') {
198 d3.select(this).classed('inactive', true);
199 }
200 });
201
202 network.link.each(function(lnk) {
203 // deactivate infrastructure links that have opt's as endpoints
204 if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') {
205 d3.select(this).classed('inactive', true);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700206 }
207 });
208 }
209
Simon Huntd35961b2014-10-28 08:49:48 -0700210 function setUpKeyHandler() {
211 d3.select('body')
212 .on('keydown', function () {
213 processKeyEvent();
214 if (debug('showKeyHandler')) {
215 network.svg.append('text')
216 .attr('x', 5)
217 .attr('y', 15)
218 .style('font-size', '20pt')
219 .text('keyCode: ' + d3.event.keyCode +
220 ' applied to : ' + contextLabel())
221 .transition().duration(2000)
222 .style('font-size', '2pt')
223 .style('fill-opacity', 0.01)
224 .remove();
225 }
226 });
227 }
228
Simon Hunt9a16c822014-10-28 16:09:19 -0700229 function contextLabel() {
230 return hovered === null ? "(nothing)" : hovered.id;
231 }
232
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700233 function radioButton(group, id) {
234 d3.selectAll("#" + group + " .radio").classed("active", false);
235 d3.select("#" + group + " #" + id).classed("active", true);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700236 }
237
Simon Huntd35961b2014-10-28 08:49:48 -0700238 function processKeyEvent() {
239 var code = d3.event.keyCode;
240 switch (code) {
241 case 76: // L
242 cycleLabels();
243 break;
244 case 80: // P
245 togglePorts();
Simon Hunt9a16c822014-10-28 16:09:19 -0700246 break;
247 case 85: // U
248 unpin();
249 break;
Simon Huntd35961b2014-10-28 08:49:48 -0700250 }
251
252 }
253
254 function cycleLabels() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700255 console.log('Cycle Labels - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700256 }
257
258 function togglePorts() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700259 console.log('Toggle Ports - context = ' + contextLabel());
260 }
261
262 function unpin() {
263 if (hovered) {
264 hovered.fixed = false;
Simon Huntf967d512014-10-28 20:34:29 -0700265 findNodeFromData(hovered).classed('fixed', false);
Simon Hunt9a16c822014-10-28 16:09:19 -0700266 network.force.resume();
267 }
268 console.log('Unpin - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700269 }
270
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700271
272 // ========================================================
273
274 function drawNetwork() {
275 $('#view').empty();
276
277 prepareNodesAndLinks();
278 createLayout();
279 console.log("\n\nHere is the augmented network object...");
Simon Hunt9a16c822014-10-28 16:09:19 -0700280 console.log(network);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700281 }
282
283 function prepareNodesAndLinks() {
284 network.lookup = {};
285 network.nodes = [];
286 network.links = [];
287
288 var nw = network.forceWidth,
289 nh = network.forceHeight;
290
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700291 function yPosConstraintForNode(n) {
292 return config.constraints.ypos[n.type || 'host'];
293 }
294
295 // Note that both 'devices' and 'hosts' get mapped into the nodes array
296
297 // first, the devices...
298 network.data.devices.forEach(function(n) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700299 var ypc = yPosConstraintForNode(n),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700300 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700301 iy = ypc * nh,
302 node = {
303 id: n.id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700304 labels: n.labels,
305 class: 'device',
306 icon: 'device',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700307 type: n.type,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700308 x: ix,
309 y: iy,
310 constraint: {
311 weight: 0.7,
312 y: iy
313 }
314 };
315 network.lookup[n.id] = node;
316 network.nodes.push(node);
317 });
318
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700319 // then, the hosts...
320 network.data.hosts.forEach(function(n) {
321 var ypc = yPosConstraintForNode(n),
322 ix = Math.random() * 0.6 * nw + 0.2 * nw,
323 iy = ypc * nh,
324 node = {
325 id: n.id,
326 labels: n.labels,
327 class: 'host',
328 icon: 'host',
329 type: n.type,
330 x: ix,
331 y: iy,
332 constraint: {
333 weight: 0.7,
334 y: iy
335 }
336 };
337 network.lookup[n.id] = node;
338 network.nodes.push(node);
339 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700340
341
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700342 // now, process the explicit links...
Simon Hunt6f376a32014-10-28 12:38:30 -0700343 network.data.links.forEach(function(lnk) {
344 var src = network.lookup[lnk.src],
345 dst = network.lookup[lnk.dst],
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700346 id = src.id + "~" + dst.id;
347
348 var link = {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700349 class: 'infra',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700350 id: id,
Simon Hunt6f376a32014-10-28 12:38:30 -0700351 type: lnk.type,
352 width: lnk.linkWidth,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700353 source: src,
354 target: dst,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700355 strength: config.force.linkStrength.infra
356 };
357 network.links.push(link);
358 });
359
360 // finally, infer host links...
361 network.data.hosts.forEach(function(n) {
362 var src = network.lookup[n.id],
363 dst = network.lookup[n.cp.device],
364 id = src.id + "~" + dst.id;
365
366 var link = {
367 class: 'host',
368 id: id,
369 type: 'hostLink',
370 width: config.hostLinkWidth,
371 source: src,
372 target: dst,
373 strength: config.force.linkStrength.host
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700374 };
375 network.links.push(link);
376 });
377 }
378
379 function createLayout() {
380
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700381 var cfg = config.force;
382
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700383 network.force = d3.layout.force()
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700384 .size([network.forceWidth, network.forceHeight])
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700385 .nodes(network.nodes)
386 .links(network.links)
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700387 .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
388 .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
389 .charge(function(d) { return cfg.charge[d.class]; })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700390 .on('tick', tick);
391
392 network.svg = d3.select('#view').append('svg')
393 .attr('width', view.width)
394 .attr('height', view.height)
395 .append('g')
Simon Huntae968a62014-10-22 14:54:41 -0700396 .attr('transform', config.force.translate());
Simon Hunt3ab76a82014-10-22 13:07:32 -0700397// .attr('id', 'zoomable')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700398// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700399
Simon Hunt3ab76a82014-10-22 13:07:32 -0700400// function zoomRedraw() {
401// d3.select("#zoomable").attr("transform",
402// "translate(" + d3.event.translate + ")"
403// + " scale(" + d3.event.scale + ")");
404// }
405
Simon Hunt3ab76a82014-10-22 13:07:32 -0700406 // TODO: move glow/blur stuff to util script
407 var glow = network.svg.append('filter')
408 .attr('x', '-50%')
409 .attr('y', '-50%')
410 .attr('width', '200%')
411 .attr('height', '200%')
412 .attr('id', 'blue-glow');
413
414 glow.append('feColorMatrix')
415 .attr('type', 'matrix')
416 .attr('values', '0 0 0 0 0 ' +
417 '0 0 0 0 0 ' +
418 '0 0 0 0 .7 ' +
419 '0 0 0 1 0 ');
420
421 glow.append('feGaussianBlur')
422 .attr('stdDeviation', 3)
423 .attr('result', 'coloredBlur');
424
425 glow.append('feMerge').selectAll('feMergeNode')
426 .data(['coloredBlur', 'SourceGraphic'])
427 .enter().append('feMergeNode')
428 .attr('in', String);
429
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700430 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700431// $('#view').on('scroll', function() {
432//
433// });
434
435
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700436 // add links to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700437 network.link = network.svg.append('g').selectAll('.link')
438 .data(network.force.links(), function(d) {return d.id})
439 .enter().append('line')
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700440 .attr('class', function(d) {return 'link ' + d.class});
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700441
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700442
Simon Hunt6f376a32014-10-28 12:38:30 -0700443 // TODO: move drag behavior into separate method.
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700444 // == define node drag behavior...
Simon Hunt3ab76a82014-10-22 13:07:32 -0700445 network.draggedThreshold = d3.scale.linear()
446 .domain([0, 0.1])
447 .range([5, 20])
448 .clamp(true);
449
450 function dragged(d) {
451 var threshold = network.draggedThreshold(network.force.alpha()),
452 dx = d.oldX - d.px,
453 dy = d.oldY - d.py;
454 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
455 d.dragged = true;
456 }
457 return d.dragged;
458 }
459
460 network.drag = d3.behavior.drag()
461 .origin(function(d) { return d; })
462 .on('dragstart', function(d) {
463 d.oldX = d.x;
464 d.oldY = d.y;
465 d.dragged = false;
466 d.fixed |= 2;
467 })
468 .on('drag', function(d) {
469 d.px = d3.event.x;
470 d.py = d3.event.y;
471 if (dragged(d)) {
472 if (!network.force.alpha()) {
473 network.force.alpha(.025);
474 }
475 }
476 })
477 .on('dragend', function(d) {
478 if (!dragged(d)) {
479 selectObject(d, this);
480 }
481 d.fixed &= ~6;
Simon Hunt9a16c822014-10-28 16:09:19 -0700482
483 // once we've finished moving, pin the node in position,
Simon Huntf967d512014-10-28 20:34:29 -0700484 // if it is a device (not a host)
Simon Hunt9a16c822014-10-28 16:09:19 -0700485 if (d.class === 'device') {
486 d.fixed = true;
Simon Huntf967d512014-10-28 20:34:29 -0700487 d3.select(this).classed('fixed', true)
Simon Hunt9a16c822014-10-28 16:09:19 -0700488 }
Simon Hunt3ab76a82014-10-22 13:07:32 -0700489 });
490
491 $('#view').on('click', function(e) {
492 if (!$(e.target).closest('.node').length) {
493 deselectObject();
494 }
495 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700496
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700497
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700498 // add nodes to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700499 network.node = network.svg.selectAll('.node')
500 .data(network.force.nodes(), function(d) {return d.id})
501 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700502 .attr('class', function(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700503 var cls = 'node ' + d.class;
504 if (d.type) {
505 cls += ' ' + d.type;
506 }
507 return cls;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700508 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700509 .attr('transform', function(d) {
510 return translate(d.x, d.y);
511 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700512 .call(network.drag)
513 .on('mouseover', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700514 // TODO: show tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700515 if (network.mouseoutTimeout) {
516 clearTimeout(network.mouseoutTimeout);
517 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700518 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700519 hoverObject(d);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700520 })
521 .on('mouseout', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700522 // TODO: hide tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700523 if (network.mouseoutTimeout) {
524 clearTimeout(network.mouseoutTimeout);
525 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700526 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700527 network.mouseoutTimeout = setTimeout(function() {
528 hoverObject(null);
529 }, config.mouseOutTimerDelayMs);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700530 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700531
Simon Hunt6f376a32014-10-28 12:38:30 -0700532
533 // deal with device nodes first
534 network.nodeRect = network.node.filter('.device')
535 .append('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700536 .attr({
537 rx: 5,
538 ry: 5,
539 width: 100,
540 height: 12
541 });
542 // note that width/height are adjusted to fit the label text
Simon Hunt6f376a32014-10-28 12:38:30 -0700543 // then padded, and space made for the icon.
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700544
Simon Hunt6f376a32014-10-28 12:38:30 -0700545 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700546 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700547 icon = iconUrl(d);
548
549 node.append('text')
550 // TODO: add label cycle behavior
551 .text(d.id)
552 .attr('dy', '1.1em');
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700553
554 if (icon) {
555 var cfg = config.icons;
556 node.append('svg:image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700557 .attr({
558 width: cfg.w,
559 height: cfg.h,
560 'xlink:href': icon
561 });
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700562 // note, icon relative positioning (x,y) is done after we have
563 // adjusted the bounds of the rectangle...
564 }
Simon Hunt68ae6652014-10-22 13:58:07 -0700565
Simon Huntd35961b2014-10-28 08:49:48 -0700566 // debug function to show the modelled x,y coordinates of nodes...
567 if (debug('showNodeXY')) {
568 node.select('rect').attr('fill-opacity', 0.5);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700569 node.append('circle')
570 .attr({
571 class: 'debug',
572 cx: 0,
573 cy: 0,
574 r: '3px'
575 });
576 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700577 });
578
Simon Hunt6f376a32014-10-28 12:38:30 -0700579 // now process host nodes
580 network.nodeCircle = network.node.filter('.host')
581 .append('circle')
582 .attr({
583 r: config.hostRadius
584 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700585
Simon Hunt6f376a32014-10-28 12:38:30 -0700586 network.node.filter('.host').each(function(d) {
587 var node = d3.select(this),
588 icon = iconUrl(d);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700589
Simon Hunt6f376a32014-10-28 12:38:30 -0700590 // debug function to show the modelled x,y coordinates of nodes...
591 if (debug('showNodeXY')) {
592 node.select('circle').attr('fill-opacity', 0.5);
593 node.append('circle')
594 .attr({
595 class: 'debug',
596 cx: 0,
597 cy: 0,
598 r: '3px'
599 });
600 }
601 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700602
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700603 // this function is scheduled to happen soon after the given thread ends
604 setTimeout(function() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700605 // post process the device nodes, to pad their size to fit the
606 // label text and attach the icon to the right location.
607 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700608 // for every node, recompute size, padding, etc. so text fits
609 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700610 text = node.select('text'),
611 box = adjustRectToFitText(node),
612 lab = config.labels;
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700613
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700614 // now make the computed adjustment
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700615 node.select('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700616 .attr(box);
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700617
618 node.select('image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700619 .attr('x', box.x + config.icons.xoff)
620 .attr('y', box.y + config.icons.yoff);
Simon Hunt1c219892014-10-22 16:32:39 -0700621
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700622 var bounds = boundsFromBox(box);
623
624 // todo: clean up extent and edge work..
Simon Hunt1c219892014-10-22 16:32:39 -0700625 d.extent = {
626 left: bounds.x1 - lab.marginLR,
627 right: bounds.x2 + lab.marginLR,
628 top: bounds.y1 - lab.marginTB,
629 bottom: bounds.y2 + lab.marginTB
630 };
631
632 d.edge = {
633 left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
634 right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
635 top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
636 bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
637 };
638
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700639 });
640
641 network.numTicks = 0;
642 network.preventCollisions = false;
643 network.force.start();
Simon Hunt1c219892014-10-22 16:32:39 -0700644 for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700645 network.force.tick();
646 }
647 network.preventCollisions = true;
648 $('#view').css('visibility', 'visible');
649 });
650
Simon Hunt6f376a32014-10-28 12:38:30 -0700651
652 // returns the newly computed bounding box of the rectangle
653 function adjustRectToFitText(n) {
654 var text = n.select('text'),
655 box = text.node().getBBox(),
656 lab = config.labels;
657
Simon Hunt9a16c822014-10-28 16:09:19 -0700658 // not sure why n.data() returns an array of 1 element...
659 var data = n.data()[0];
660
Simon Hunt6f376a32014-10-28 12:38:30 -0700661 text.attr('text-anchor', 'middle')
662 .attr('y', '-0.8em')
663 .attr('x', lab.imgPad/2)
664 ;
665
Simon Hunt6f376a32014-10-28 12:38:30 -0700666 // translate the bbox so that it is centered on [x,y]
667 box.x = -box.width / 2;
668 box.y = -box.height / 2;
669
670 // add padding
671 box.x -= (lab.padLR + lab.imgPad/2);
672 box.width += lab.padLR * 2 + lab.imgPad;
673 box.y -= lab.padTB;
674 box.height += lab.padTB * 2;
675
676 return box;
677 }
678
679 function boundsFromBox(box) {
680 return {
681 x1: box.x,
682 y1: box.y,
683 x2: box.x + box.width,
684 y2: box.y + box.height
685 };
686 }
687
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700688 }
689
Simon Hunt68ae6652014-10-22 13:58:07 -0700690 function iconUrl(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700691 return config.iconUrl[d.icon];
Simon Hunt68ae6652014-10-22 13:58:07 -0700692 }
693
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700694 function translate(x, y) {
695 return 'translate(' + x + ',' + y + ')';
696 }
697
Simon Hunt6f376a32014-10-28 12:38:30 -0700698 // prevents collisions amongst device nodes
Simon Hunt1c219892014-10-22 16:32:39 -0700699 function preventCollisions() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700700 var quadtree = d3.geom.quadtree(network.nodes),
701 hrad = config.hostRadius;
Simon Hunt1c219892014-10-22 16:32:39 -0700702
703 network.nodes.forEach(function(n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700704 var nx1, nx2, ny1, ny2;
705
706 if (n.class === 'device') {
707 nx1 = n.x + n.extent.left;
708 nx2 = n.x + n.extent.right;
709 ny1 = n.y + n.extent.top;
Simon Hunt1c219892014-10-22 16:32:39 -0700710 ny2 = n.y + n.extent.bottom;
711
Simon Hunt6f376a32014-10-28 12:38:30 -0700712 } else {
713 nx1 = n.x - hrad;
714 nx2 = n.x + hrad;
715 ny1 = n.y - hrad;
716 ny2 = n.y + hrad;
717 }
718
Simon Hunt1c219892014-10-22 16:32:39 -0700719 quadtree.visit(function(quad, x1, y1, x2, y2) {
720 if (quad.point && quad.point !== n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700721 // check if the rectangles/circles intersect
Simon Hunt1c219892014-10-22 16:32:39 -0700722 var p = quad.point,
Simon Hunt6f376a32014-10-28 12:38:30 -0700723 px1, px2, py1, py2, ix;
724
725 if (p.class === 'device') {
726 px1 = p.x + p.extent.left;
727 px2 = p.x + p.extent.right;
728 py1 = p.y + p.extent.top;
729 py2 = p.y + p.extent.bottom;
730
731 } else {
732 px1 = p.x - hrad;
733 px2 = p.x + hrad;
734 py1 = p.y - hrad;
735 py2 = p.y + hrad;
736 }
737
738 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
739
Simon Hunt1c219892014-10-22 16:32:39 -0700740 if (ix) {
741 var xa1 = nx2 - px1, // shift n left , p right
742 xa2 = px2 - nx1, // shift n right, p left
743 ya1 = ny2 - py1, // shift n up , p down
744 ya2 = py2 - ny1, // shift n down , p up
745 adj = Math.min(xa1, xa2, ya1, ya2);
746
747 if (adj == xa1) {
748 n.x -= adj / 2;
749 p.x += adj / 2;
750 } else if (adj == xa2) {
751 n.x += adj / 2;
752 p.x -= adj / 2;
753 } else if (adj == ya1) {
754 n.y -= adj / 2;
755 p.y += adj / 2;
756 } else if (adj == ya2) {
757 n.y += adj / 2;
758 p.y -= adj / 2;
759 }
760 }
761 return ix;
762 }
763 });
764
765 });
766 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700767
768 function tick(e) {
769 network.numTicks++;
770
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700771 if (config.options.layering) {
Simon Hunt68ae6652014-10-22 13:58:07 -0700772 // adjust the y-coord of each node, based on y-pos constraints
773 network.nodes.forEach(function (n) {
774 var z = e.alpha * n.constraint.weight;
775 if (!isNaN(n.constraint.y)) {
776 n.y = (n.constraint.y * z + n.y * (1 - z));
777 }
778 });
779 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700780
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700781 if (config.options.collisionPrevention && network.preventCollisions) {
Simon Hunt1c219892014-10-22 16:32:39 -0700782 preventCollisions();
783 }
784
Simon Huntd35961b2014-10-28 08:49:48 -0700785 // clip visualization of links at bounds of nodes...
786 network.link.each(function(d) {
787 var xs = d.source.x,
788 ys = d.source.y,
789 xt = d.target.x,
790 yt = d.target.y,
791 line = new geo.LineSegment(xs, ys, xt, yt),
792 e, ix;
Simon Hunt1c219892014-10-22 16:32:39 -0700793
Simon Huntd35961b2014-10-28 08:49:48 -0700794 for (e in d.source.edge) {
795 ix = line.intersect(d.source.edge[e].offset(xs, ys));
Simon Hunt1c219892014-10-22 16:32:39 -0700796 if (ix.in1 && ix.in2) {
Simon Huntd35961b2014-10-28 08:49:48 -0700797 xs = ix.x;
798 ys = ix.y;
799 break;
800 }
801 }
802
803 for (e in d.target.edge) {
804 ix = line.intersect(d.target.edge[e].offset(xt, yt));
805 if (ix.in1 && ix.in2) {
806 xt = ix.x;
807 yt = ix.y;
Simon Hunt1c219892014-10-22 16:32:39 -0700808 break;
809 }
810 }
811
812 d3.select(this)
Simon Huntd35961b2014-10-28 08:49:48 -0700813 .attr('x1', xs)
814 .attr('y1', ys)
815 .attr('x2', xt)
816 .attr('y2', yt);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700817 });
818
Simon Huntd35961b2014-10-28 08:49:48 -0700819 // position each node by translating the node (group) by x,y
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700820 network.node
821 .attr('transform', function(d) {
822 return translate(d.x, d.y);
823 });
824
825 }
826
827 // $('#docs-close').on('click', function() {
828 // deselectObject();
829 // return false;
830 // });
831
832 // $(document).on('click', '.select-object', function() {
833 // var obj = graph.data[$(this).data('name')];
834 // if (obj) {
835 // selectObject(obj);
836 // }
837 // return false;
838 // });
839
Simon Hunt6f376a32014-10-28 12:38:30 -0700840 function findNodeFromData(d) {
841 var el = null;
842 network.node.filter('.' + d.class).each(function(n) {
843 if (n.id === d.id) {
844 el = d3.select(this);
845 }
846 });
847 return el;
848 }
849
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700850 function selectObject(obj, el) {
851 var node;
852 if (el) {
853 node = d3.select(el);
854 } else {
855 network.node.each(function(d) {
856 if (d == obj) {
857 node = d3.select(el = this);
858 }
859 });
860 }
861 if (!node) return;
862
863 if (node.classed('selected')) {
864 deselectObject();
Simon Huntc586e212014-10-28 21:24:08 -0700865 flyinPane(null);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700866 return;
867 }
868 deselectObject(false);
869
870 selected = {
871 obj : obj,
872 el : el
873 };
874
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700875 node.classed('selected', true);
Simon Huntc586e212014-10-28 21:24:08 -0700876 flyinPane(obj);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700877 }
878
879 function deselectObject(doResize) {
880 // Review: logic of 'resize(...)' function.
881 if (doResize || typeof doResize == 'undefined') {
882 resize(false);
883 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700884
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700885 // deselect all nodes in the network...
886 network.node.classed('selected', false);
887 selected = {};
Simon Huntc586e212014-10-28 21:24:08 -0700888 flyinPane(null);
889 }
890
Simon Huntcc267562014-10-29 10:22:17 -0700891 function detailUrl(id) {
892 var safeId = id.replace(/[^a-z0-9]/gi, '_');
893 return config.jsonPrefix + safeId + '.json';
894 }
895
Simon Huntc586e212014-10-28 21:24:08 -0700896 function flyinPane(obj) {
897 var pane = d3.select('#flyout'),
Simon Huntcc267562014-10-29 10:22:17 -0700898 url;
Simon Huntc586e212014-10-28 21:24:08 -0700899
900 if (obj) {
Simon Huntcc267562014-10-29 10:22:17 -0700901 // go get details of the selected object from the server...
902 url = detailUrl(obj.id);
903 d3.json(url, function (err, data) {
904 if (err) {
905 alert('Oops! Error reading JSON...\n\n' +
906 'URL: ' + url + '\n\n' +
907 'Error: ' + err.message);
908 return;
909 }
910// console.log("JSON data... " + url);
911// console.log(data);
912
913 displayDetails(data, pane);
914 });
915
916 } else {
917 // hide pane
918 pane.transition().duration(750)
919 .style('right', '-320px')
920 .style('opacity', 0.0);
921 }
922 }
923
924 function displayDetails(data, pane) {
925 $('#flyout').empty();
926
927 pane.append('h2').text(data.id);
928
929 var table = pane.append("table"),
930 tbody = table.append("tbody");
931
932 // TODO: consider using d3 data bind to TR/TD
933
934 data.propOrder.forEach(function(p) {
935 addProp(tbody, p, data.props[p]);
936 });
937
938 function addProp(tbody, label, value) {
939 var tr = tbody.append('tr');
940
941 tr.append('td')
942 .attr('class', 'label')
943 .text(label + ' :');
944
945 tr.append('td')
946 .attr('class', 'value')
947 .text(value);
Simon Huntc586e212014-10-28 21:24:08 -0700948 }
949
Simon Huntcc267562014-10-29 10:22:17 -0700950 // show pane
Simon Huntc586e212014-10-28 21:24:08 -0700951 pane.transition().duration(750)
Simon Huntcc267562014-10-29 10:22:17 -0700952 .style('right', '20px')
953 .style('opacity', 1.0);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700954 }
955
956 function highlightObject(obj) {
957 if (obj) {
958 if (obj != highlighted) {
959 // TODO set or clear "inactive" class on nodes, based on criteria
960 network.node.classed('inactive', function(d) {
961 // return (obj !== d &&
962 // d.relation(obj.id));
963 return (obj !== d);
964 });
965 // TODO: same with links
966 network.link.classed('inactive', function(d) {
967 return (obj !== d.source && obj !== d.target);
968 });
969 }
970 highlighted = obj;
971 } else {
972 if (highlighted) {
973 // clear the inactive flag (no longer suppressed visually)
974 network.node.classed('inactive', false);
975 network.link.classed('inactive', false);
976 }
977 highlighted = null;
978
979 }
980 }
981
Simon Hunt9a16c822014-10-28 16:09:19 -0700982 function hoverObject(obj) {
983 if (obj) {
984 hovered = obj;
985 } else {
986 if (hovered) {
987 hovered = null;
988 }
989 }
990 }
991
992
Simon Huntc586e212014-10-28 21:24:08 -0700993 function resize() {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700994 view.height = window.innerHeight - config.mastHeight;
995 view.width = window.innerWidth;
996 $('#view')
997 .css('height', view.height + 'px')
998 .css('width', view.width + 'px');
999
1000 network.forceWidth = view.width - config.force.marginLR;
1001 network.forceHeight = view.height - config.force.marginTB;
1002 }
1003
1004 // ======================================================================
1005 // register with the UI framework
1006
1007 api.addView('network', {
1008 load: loadNetworkView
1009 });
1010
1011
1012}(ONOS));
1013