blob: 00dee390aec8d94a6703edebedad4830aac1cf27 [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
Simon Hunt3ab76a82014-10-22 13:07:32 -0700435// function zoomRedraw() {
436// d3.select("#zoomable").attr("transform",
437// "translate(" + d3.event.translate + ")"
438// + " scale(" + d3.event.scale + ")");
439// }
440
Simon Hunt3ab76a82014-10-22 13:07:32 -0700441 // TODO: move glow/blur stuff to util script
442 var glow = network.svg.append('filter')
443 .attr('x', '-50%')
444 .attr('y', '-50%')
445 .attr('width', '200%')
446 .attr('height', '200%')
447 .attr('id', 'blue-glow');
448
449 glow.append('feColorMatrix')
450 .attr('type', 'matrix')
451 .attr('values', '0 0 0 0 0 ' +
452 '0 0 0 0 0 ' +
453 '0 0 0 0 .7 ' +
454 '0 0 0 1 0 ');
455
456 glow.append('feGaussianBlur')
457 .attr('stdDeviation', 3)
458 .attr('result', 'coloredBlur');
459
460 glow.append('feMerge').selectAll('feMergeNode')
461 .data(['coloredBlur', 'SourceGraphic'])
462 .enter().append('feMergeNode')
463 .attr('in', String);
464
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700465 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700466// $('#view').on('scroll', function() {
467//
468// });
469
470
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700471 // add links to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700472 network.link = network.svg.append('g').selectAll('.link')
473 .data(network.force.links(), function(d) {return d.id})
474 .enter().append('line')
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700475 .attr('class', function(d) {return 'link ' + d.class});
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700476
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700477
Simon Hunt6f376a32014-10-28 12:38:30 -0700478 // TODO: move drag behavior into separate method.
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700479 // == define node drag behavior...
Simon Hunt3ab76a82014-10-22 13:07:32 -0700480 network.draggedThreshold = d3.scale.linear()
481 .domain([0, 0.1])
482 .range([5, 20])
483 .clamp(true);
484
485 function dragged(d) {
486 var threshold = network.draggedThreshold(network.force.alpha()),
487 dx = d.oldX - d.px,
488 dy = d.oldY - d.py;
489 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
490 d.dragged = true;
491 }
492 return d.dragged;
493 }
494
495 network.drag = d3.behavior.drag()
496 .origin(function(d) { return d; })
497 .on('dragstart', function(d) {
498 d.oldX = d.x;
499 d.oldY = d.y;
500 d.dragged = false;
501 d.fixed |= 2;
502 })
503 .on('drag', function(d) {
504 d.px = d3.event.x;
505 d.py = d3.event.y;
506 if (dragged(d)) {
507 if (!network.force.alpha()) {
508 network.force.alpha(.025);
509 }
510 }
511 })
512 .on('dragend', function(d) {
513 if (!dragged(d)) {
514 selectObject(d, this);
515 }
516 d.fixed &= ~6;
Simon Hunt9a16c822014-10-28 16:09:19 -0700517
518 // once we've finished moving, pin the node in position,
Simon Huntf967d512014-10-28 20:34:29 -0700519 // if it is a device (not a host)
Simon Hunt9a16c822014-10-28 16:09:19 -0700520 if (d.class === 'device') {
521 d.fixed = true;
Simon Huntf967d512014-10-28 20:34:29 -0700522 d3.select(this).classed('fixed', true)
Simon Hunt9a16c822014-10-28 16:09:19 -0700523 }
Simon Hunt3ab76a82014-10-22 13:07:32 -0700524 });
525
526 $('#view').on('click', function(e) {
527 if (!$(e.target).closest('.node').length) {
528 deselectObject();
529 }
530 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700531
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700532
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700533 // add nodes to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700534 network.node = network.svg.selectAll('.node')
535 .data(network.force.nodes(), function(d) {return d.id})
536 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700537 .attr('class', function(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700538 var cls = 'node ' + d.class;
539 if (d.type) {
540 cls += ' ' + d.type;
541 }
542 return cls;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700543 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700544 .attr('transform', function(d) {
545 return translate(d.x, d.y);
546 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700547 .call(network.drag)
548 .on('mouseover', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700549 // TODO: show tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700550 if (network.mouseoutTimeout) {
551 clearTimeout(network.mouseoutTimeout);
552 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700553 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700554 hoverObject(d);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700555 })
556 .on('mouseout', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700557 // TODO: hide 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 network.mouseoutTimeout = setTimeout(function() {
563 hoverObject(null);
564 }, config.mouseOutTimerDelayMs);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700565 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700566
Simon Hunt6f376a32014-10-28 12:38:30 -0700567
568 // deal with device nodes first
569 network.nodeRect = network.node.filter('.device')
570 .append('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700571 .attr({
572 rx: 5,
573 ry: 5,
574 width: 100,
575 height: 12
576 });
577 // note that width/height are adjusted to fit the label text
Simon Hunt6f376a32014-10-28 12:38:30 -0700578 // then padded, and space made for the icon.
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700579
Simon Hunt6f376a32014-10-28 12:38:30 -0700580 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700581 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700582 icon = iconUrl(d);
583
584 node.append('text')
585 // TODO: add label cycle behavior
586 .text(d.id)
587 .attr('dy', '1.1em');
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700588
589 if (icon) {
590 var cfg = config.icons;
591 node.append('svg:image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700592 .attr({
593 width: cfg.w,
594 height: cfg.h,
595 'xlink:href': icon
596 });
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700597 // note, icon relative positioning (x,y) is done after we have
598 // adjusted the bounds of the rectangle...
599 }
Simon Hunt68ae6652014-10-22 13:58:07 -0700600
Simon Huntd35961b2014-10-28 08:49:48 -0700601 // debug function to show the modelled x,y coordinates of nodes...
602 if (debug('showNodeXY')) {
603 node.select('rect').attr('fill-opacity', 0.5);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700604 node.append('circle')
605 .attr({
606 class: 'debug',
607 cx: 0,
608 cy: 0,
609 r: '3px'
610 });
611 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700612 });
613
Simon Hunt6f376a32014-10-28 12:38:30 -0700614 // now process host nodes
615 network.nodeCircle = network.node.filter('.host')
616 .append('circle')
617 .attr({
618 r: config.hostRadius
619 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700620
Simon Hunt6f376a32014-10-28 12:38:30 -0700621 network.node.filter('.host').each(function(d) {
622 var node = d3.select(this),
623 icon = iconUrl(d);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700624
Simon Hunt6f376a32014-10-28 12:38:30 -0700625 // debug function to show the modelled x,y coordinates of nodes...
626 if (debug('showNodeXY')) {
627 node.select('circle').attr('fill-opacity', 0.5);
628 node.append('circle')
629 .attr({
630 class: 'debug',
631 cx: 0,
632 cy: 0,
633 r: '3px'
634 });
635 }
636 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700637
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700638 // this function is scheduled to happen soon after the given thread ends
639 setTimeout(function() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700640 // post process the device nodes, to pad their size to fit the
641 // label text and attach the icon to the right location.
642 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700643 // for every node, recompute size, padding, etc. so text fits
644 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700645 text = node.select('text'),
646 box = adjustRectToFitText(node),
647 lab = config.labels;
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700648
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700649 // now make the computed adjustment
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700650 node.select('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700651 .attr(box);
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700652
653 node.select('image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700654 .attr('x', box.x + config.icons.xoff)
655 .attr('y', box.y + config.icons.yoff);
Simon Hunt1c219892014-10-22 16:32:39 -0700656
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700657 var bounds = boundsFromBox(box);
658
659 // todo: clean up extent and edge work..
Simon Hunt1c219892014-10-22 16:32:39 -0700660 d.extent = {
661 left: bounds.x1 - lab.marginLR,
662 right: bounds.x2 + lab.marginLR,
663 top: bounds.y1 - lab.marginTB,
664 bottom: bounds.y2 + lab.marginTB
665 };
666
667 d.edge = {
668 left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
669 right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
670 top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
671 bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
672 };
673
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700674 });
675
676 network.numTicks = 0;
677 network.preventCollisions = false;
678 network.force.start();
Simon Hunt1c219892014-10-22 16:32:39 -0700679 for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700680 network.force.tick();
681 }
682 network.preventCollisions = true;
683 $('#view').css('visibility', 'visible');
684 });
685
Simon Hunt6f376a32014-10-28 12:38:30 -0700686
687 // returns the newly computed bounding box of the rectangle
688 function adjustRectToFitText(n) {
689 var text = n.select('text'),
690 box = text.node().getBBox(),
691 lab = config.labels;
692
Simon Hunt9a16c822014-10-28 16:09:19 -0700693 // not sure why n.data() returns an array of 1 element...
694 var data = n.data()[0];
695
Simon Hunt6f376a32014-10-28 12:38:30 -0700696 text.attr('text-anchor', 'middle')
697 .attr('y', '-0.8em')
698 .attr('x', lab.imgPad/2)
699 ;
700
Simon Hunt6f376a32014-10-28 12:38:30 -0700701 // translate the bbox so that it is centered on [x,y]
702 box.x = -box.width / 2;
703 box.y = -box.height / 2;
704
705 // add padding
706 box.x -= (lab.padLR + lab.imgPad/2);
707 box.width += lab.padLR * 2 + lab.imgPad;
708 box.y -= lab.padTB;
709 box.height += lab.padTB * 2;
710
711 return box;
712 }
713
714 function boundsFromBox(box) {
715 return {
716 x1: box.x,
717 y1: box.y,
718 x2: box.x + box.width,
719 y2: box.y + box.height
720 };
721 }
722
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700723 }
724
Simon Hunt68ae6652014-10-22 13:58:07 -0700725 function iconUrl(d) {
Thomas Vachuska1de66012014-10-30 03:03:30 -0700726 return 'img/' + d.type + '.png';
727// return config.iconUrl[d.icon];
Simon Hunt68ae6652014-10-22 13:58:07 -0700728 }
729
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700730 function translate(x, y) {
731 return 'translate(' + x + ',' + y + ')';
732 }
733
Simon Hunt6f376a32014-10-28 12:38:30 -0700734 // prevents collisions amongst device nodes
Simon Hunt1c219892014-10-22 16:32:39 -0700735 function preventCollisions() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700736 var quadtree = d3.geom.quadtree(network.nodes),
737 hrad = config.hostRadius;
Simon Hunt1c219892014-10-22 16:32:39 -0700738
739 network.nodes.forEach(function(n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700740 var nx1, nx2, ny1, ny2;
741
742 if (n.class === 'device') {
743 nx1 = n.x + n.extent.left;
744 nx2 = n.x + n.extent.right;
745 ny1 = n.y + n.extent.top;
Simon Hunt1c219892014-10-22 16:32:39 -0700746 ny2 = n.y + n.extent.bottom;
747
Simon Hunt6f376a32014-10-28 12:38:30 -0700748 } else {
749 nx1 = n.x - hrad;
750 nx2 = n.x + hrad;
751 ny1 = n.y - hrad;
752 ny2 = n.y + hrad;
753 }
754
Simon Hunt1c219892014-10-22 16:32:39 -0700755 quadtree.visit(function(quad, x1, y1, x2, y2) {
756 if (quad.point && quad.point !== n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700757 // check if the rectangles/circles intersect
Simon Hunt1c219892014-10-22 16:32:39 -0700758 var p = quad.point,
Simon Hunt6f376a32014-10-28 12:38:30 -0700759 px1, px2, py1, py2, ix;
760
761 if (p.class === 'device') {
762 px1 = p.x + p.extent.left;
763 px2 = p.x + p.extent.right;
764 py1 = p.y + p.extent.top;
765 py2 = p.y + p.extent.bottom;
766
767 } else {
768 px1 = p.x - hrad;
769 px2 = p.x + hrad;
770 py1 = p.y - hrad;
771 py2 = p.y + hrad;
772 }
773
774 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
775
Simon Hunt1c219892014-10-22 16:32:39 -0700776 if (ix) {
777 var xa1 = nx2 - px1, // shift n left , p right
778 xa2 = px2 - nx1, // shift n right, p left
779 ya1 = ny2 - py1, // shift n up , p down
780 ya2 = py2 - ny1, // shift n down , p up
781 adj = Math.min(xa1, xa2, ya1, ya2);
782
783 if (adj == xa1) {
784 n.x -= adj / 2;
785 p.x += adj / 2;
786 } else if (adj == xa2) {
787 n.x += adj / 2;
788 p.x -= adj / 2;
789 } else if (adj == ya1) {
790 n.y -= adj / 2;
791 p.y += adj / 2;
792 } else if (adj == ya2) {
793 n.y += adj / 2;
794 p.y -= adj / 2;
795 }
796 }
797 return ix;
798 }
799 });
800
801 });
802 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700803
804 function tick(e) {
805 network.numTicks++;
806
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700807 if (config.options.layering) {
Simon Hunt68ae6652014-10-22 13:58:07 -0700808 // adjust the y-coord of each node, based on y-pos constraints
809 network.nodes.forEach(function (n) {
810 var z = e.alpha * n.constraint.weight;
811 if (!isNaN(n.constraint.y)) {
812 n.y = (n.constraint.y * z + n.y * (1 - z));
813 }
814 });
815 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700816
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700817 if (config.options.collisionPrevention && network.preventCollisions) {
Simon Hunt1c219892014-10-22 16:32:39 -0700818 preventCollisions();
819 }
820
Simon Huntd35961b2014-10-28 08:49:48 -0700821 // clip visualization of links at bounds of nodes...
822 network.link.each(function(d) {
823 var xs = d.source.x,
824 ys = d.source.y,
825 xt = d.target.x,
826 yt = d.target.y,
827 line = new geo.LineSegment(xs, ys, xt, yt),
828 e, ix;
Simon Hunt1c219892014-10-22 16:32:39 -0700829
Simon Huntd35961b2014-10-28 08:49:48 -0700830 for (e in d.source.edge) {
831 ix = line.intersect(d.source.edge[e].offset(xs, ys));
Simon Hunt1c219892014-10-22 16:32:39 -0700832 if (ix.in1 && ix.in2) {
Simon Huntd35961b2014-10-28 08:49:48 -0700833 xs = ix.x;
834 ys = ix.y;
835 break;
836 }
837 }
838
839 for (e in d.target.edge) {
840 ix = line.intersect(d.target.edge[e].offset(xt, yt));
841 if (ix.in1 && ix.in2) {
842 xt = ix.x;
843 yt = ix.y;
Simon Hunt1c219892014-10-22 16:32:39 -0700844 break;
845 }
846 }
847
848 d3.select(this)
Simon Huntd35961b2014-10-28 08:49:48 -0700849 .attr('x1', xs)
850 .attr('y1', ys)
851 .attr('x2', xt)
852 .attr('y2', yt);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700853 });
854
Simon Huntd35961b2014-10-28 08:49:48 -0700855 // position each node by translating the node (group) by x,y
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700856 network.node
857 .attr('transform', function(d) {
858 return translate(d.x, d.y);
859 });
860
861 }
862
863 // $('#docs-close').on('click', function() {
864 // deselectObject();
865 // return false;
866 // });
867
868 // $(document).on('click', '.select-object', function() {
869 // var obj = graph.data[$(this).data('name')];
870 // if (obj) {
871 // selectObject(obj);
872 // }
873 // return false;
874 // });
875
Simon Hunt6f376a32014-10-28 12:38:30 -0700876 function findNodeFromData(d) {
877 var el = null;
878 network.node.filter('.' + d.class).each(function(n) {
879 if (n.id === d.id) {
880 el = d3.select(this);
881 }
882 });
883 return el;
884 }
885
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700886 function selectObject(obj, el) {
887 var node;
888 if (el) {
889 node = d3.select(el);
890 } else {
891 network.node.each(function(d) {
892 if (d == obj) {
893 node = d3.select(el = this);
894 }
895 });
896 }
897 if (!node) return;
898
899 if (node.classed('selected')) {
900 deselectObject();
Simon Huntc586e212014-10-28 21:24:08 -0700901 flyinPane(null);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700902 return;
903 }
904 deselectObject(false);
905
906 selected = {
907 obj : obj,
908 el : el
909 };
910
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700911 node.classed('selected', true);
Simon Huntc586e212014-10-28 21:24:08 -0700912 flyinPane(obj);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700913 }
914
915 function deselectObject(doResize) {
916 // Review: logic of 'resize(...)' function.
917 if (doResize || typeof doResize == 'undefined') {
918 resize(false);
919 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700920
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700921 // deselect all nodes in the network...
922 network.node.classed('selected', false);
923 selected = {};
Simon Huntc586e212014-10-28 21:24:08 -0700924 flyinPane(null);
925 }
926
927 function flyinPane(obj) {
928 var pane = d3.select('#flyout'),
Simon Huntcc267562014-10-29 10:22:17 -0700929 url;
Simon Huntc586e212014-10-28 21:24:08 -0700930
931 if (obj) {
Simon Huntcc267562014-10-29 10:22:17 -0700932 // go get details of the selected object from the server...
Simon Huntb4d9d4c2014-10-30 11:27:23 -0700933 url = detailJsonUrl(obj.id);
Simon Huntcc267562014-10-29 10:22:17 -0700934 d3.json(url, function (err, data) {
935 if (err) {
936 alert('Oops! Error reading JSON...\n\n' +
937 'URL: ' + url + '\n\n' +
938 'Error: ' + err.message);
939 return;
940 }
941// console.log("JSON data... " + url);
942// console.log(data);
943
944 displayDetails(data, pane);
945 });
946
947 } else {
948 // hide pane
949 pane.transition().duration(750)
950 .style('right', '-320px')
951 .style('opacity', 0.0);
952 }
953 }
954
955 function displayDetails(data, pane) {
956 $('#flyout').empty();
957
Thomas Vachuska1de66012014-10-30 03:03:30 -0700958 var title = pane.append("h2"),
959 table = pane.append("table"),
Simon Huntcc267562014-10-29 10:22:17 -0700960 tbody = table.append("tbody");
961
Thomas Vachuska1de66012014-10-30 03:03:30 -0700962 $('<img src="img/' + data.type + '.png">').appendTo(title);
963 $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
964
965
Simon Huntcc267562014-10-29 10:22:17 -0700966 // TODO: consider using d3 data bind to TR/TD
967
968 data.propOrder.forEach(function(p) {
Thomas Vachuska1de66012014-10-30 03:03:30 -0700969 if (p === '-') {
970 addSep(tbody);
971 } else {
972 addProp(tbody, p, data.props[p]);
973 }
Simon Huntcc267562014-10-29 10:22:17 -0700974 });
975
Thomas Vachuska1de66012014-10-30 03:03:30 -0700976 function addSep(tbody) {
977 var tr = tbody.append('tr');
978 $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
979 }
980
Simon Huntcc267562014-10-29 10:22:17 -0700981 function addProp(tbody, label, value) {
982 var tr = tbody.append('tr');
983
984 tr.append('td')
985 .attr('class', 'label')
986 .text(label + ' :');
987
988 tr.append('td')
989 .attr('class', 'value')
990 .text(value);
Simon Huntc586e212014-10-28 21:24:08 -0700991 }
992
Simon Huntcc267562014-10-29 10:22:17 -0700993 // show pane
Simon Huntc586e212014-10-28 21:24:08 -0700994 pane.transition().duration(750)
Simon Huntcc267562014-10-29 10:22:17 -0700995 .style('right', '20px')
996 .style('opacity', 1.0);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700997 }
998
999 function highlightObject(obj) {
1000 if (obj) {
1001 if (obj != highlighted) {
1002 // TODO set or clear "inactive" class on nodes, based on criteria
1003 network.node.classed('inactive', function(d) {
1004 // return (obj !== d &&
1005 // d.relation(obj.id));
1006 return (obj !== d);
1007 });
1008 // TODO: same with links
1009 network.link.classed('inactive', function(d) {
1010 return (obj !== d.source && obj !== d.target);
1011 });
1012 }
1013 highlighted = obj;
1014 } else {
1015 if (highlighted) {
1016 // clear the inactive flag (no longer suppressed visually)
1017 network.node.classed('inactive', false);
1018 network.link.classed('inactive', false);
1019 }
1020 highlighted = null;
1021
1022 }
1023 }
1024
Simon Hunt9a16c822014-10-28 16:09:19 -07001025 function hoverObject(obj) {
1026 if (obj) {
1027 hovered = obj;
1028 } else {
1029 if (hovered) {
1030 hovered = null;
1031 }
1032 }
1033 }
1034
1035
Simon Huntc586e212014-10-28 21:24:08 -07001036 function resize() {
Simon Hunt0b05d4a2014-10-21 21:50:15 -07001037 view.height = window.innerHeight - config.mastHeight;
1038 view.width = window.innerWidth;
1039 $('#view')
1040 .css('height', view.height + 'px')
1041 .css('width', view.width + 'px');
1042
1043 network.forceWidth = view.width - config.force.marginLR;
1044 network.forceHeight = view.height - config.force.marginTB;
1045 }
1046
1047 // ======================================================================
1048 // register with the UI framework
1049
1050 api.addView('network', {
1051 load: loadNetworkView
1052 });
1053
1054
1055}(ONOS));
1056