blob: 3c7507a0e160b1eff4fe2e60adabaeb79c37d3d6 [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',
Thomas Vachuskae972ea52014-10-30 00:14:16 -070041 XjsonPrefix: '',
Simon Huntcc267562014-10-29 10:22:17 -070042 jsonUrl: 'json/network.json',
43 jsonPrefix: 'json/',
Simon Hunt68ae6652014-10-22 13:58:07 -070044 iconUrl: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070045 device: 'img/device.png',
46 host: 'img/host.png',
47 pkt: 'img/pkt.png',
48 opt: 'img/opt.png'
Simon Hunt68ae6652014-10-22 13:58:07 -070049 },
Simon Hunt19cb0982014-10-23 16:44:49 -070050 mastHeight: 36,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070051 force: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070052 note: 'node.class or link.class is used to differentiate',
53 linkDistance: {
Simon Hunt6f376a32014-10-28 12:38:30 -070054 infra: 200,
Simon Hunt9a16c822014-10-28 16:09:19 -070055 host: 40
Simon Hunt2c9e0c22014-10-23 15:12:58 -070056 },
57 linkStrength: {
58 infra: 1.0,
Simon Hunt6f376a32014-10-28 12:38:30 -070059 host: 1.0
Simon Hunt2c9e0c22014-10-23 15:12:58 -070060 },
61 charge: {
62 device: -800,
Simon Hunt9a16c822014-10-28 16:09:19 -070063 host: -1000
Simon Hunt2c9e0c22014-10-23 15:12:58 -070064 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070065 ticksWithoutCollisions: 50,
66 marginLR: 20,
67 marginTB: 20,
68 translate: function() {
69 return 'translate(' +
70 config.force.marginLR + ',' +
71 config.force.marginTB + ')';
72 }
73 },
74 labels: {
Simon Hunt19cb0982014-10-23 16:44:49 -070075 imgPad: 16,
Simon Hunt1c5f8b62014-10-22 14:43:01 -070076 padLR: 8,
77 padTB: 6,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070078 marginLR: 3,
79 marginTB: 2
80 },
Simon Hunt2c9e0c22014-10-23 15:12:58 -070081 icons: {
82 w: 32,
83 h: 32,
84 xoff: -12,
Simon Hunt19cb0982014-10-23 16:44:49 -070085 yoff: -8
Simon Hunt2c9e0c22014-10-23 15:12:58 -070086 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070087 constraints: {
88 ypos: {
Simon Hunt9a16c822014-10-28 16:09:19 -070089 host: 0.05,
Simon Hunt2c9e0c22014-10-23 15:12:58 -070090 switch: 0.3,
91 roadm: 0.7
Simon Hunt0b05d4a2014-10-21 21:50:15 -070092 }
Simon Hunt2c9e0c22014-10-23 15:12:58 -070093 },
94 hostLinkWidth: 1.0,
Simon Hunt6f376a32014-10-28 12:38:30 -070095 hostRadius: 7,
Simon Hunt2c9e0c22014-10-23 15:12:58 -070096 mouseOutTimerDelayMs: 120
Simon Huntd35961b2014-10-28 08:49:48 -070097 };
98
99 // state variables
100 var view = {},
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700101 network = {},
102 selected = {},
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700103 highlighted = null,
Simon Hunt9a16c822014-10-28 16:09:19 -0700104 hovered = null,
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700105 viewMode = 'showAll';
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700106
107
Simon Huntd35961b2014-10-28 08:49:48 -0700108 function debug(what) {
109 return config.debugOn && config.debug[what];
110 }
111
112 // load the topology view of the network
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700113 function loadNetworkView() {
114 // Hey, here I am, calling something on the ONOS api:
115 api.printTime();
116
117 resize();
118
Simon Huntd35961b2014-10-28 08:49:48 -0700119 // go get our network data from the server...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700120 d3.json(config.jsonUrl, function (err, data) {
121 if (err) {
122 alert('Oops! Error reading JSON...\n\n' +
Simon Huntae968a62014-10-22 14:54:41 -0700123 'URL: ' + config.jsonUrl + '\n\n' +
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700124 'Error: ' + err.message);
125 return;
126 }
Simon Huntd35961b2014-10-28 08:49:48 -0700127// console.log("here is the JSON data...");
128// console.log(data);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700129
130 network.data = data;
131 drawNetwork();
132 });
133
Simon Huntd35961b2014-10-28 08:49:48 -0700134 // while we wait for the data, set up the handlers...
135 setUpClickHandler();
136 setUpRadioButtonHandler();
137 setUpKeyHandler();
138 $(window).on('resize', resize);
139 }
140
141 function setUpClickHandler() {
142 // click handler for "selectable" objects
143 $(document).on('click', '.select-object', function () {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700144 // when any object of class "select-object" is clicked...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700145 var obj = network.lookup[$(this).data('id')];
146 if (obj) {
147 selectObject(obj);
148 }
149 // stop propagation of event (I think) ...
150 return false;
151 });
Simon Huntd35961b2014-10-28 08:49:48 -0700152 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700153
Simon Huntd35961b2014-10-28 08:49:48 -0700154 function setUpRadioButtonHandler() {
155 d3.selectAll('#displayModes .radio').on('click', function () {
156 var id = d3.select(this).attr('id');
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700157 if (id !== viewMode) {
158 radioButton('displayModes', id);
159 viewMode = id;
Simon Huntf967d512014-10-28 20:34:29 -0700160 doRadioAction(id);
161 }
162 });
163 }
164
165 function doRadioAction(id) {
166 showAllLayers();
167 if (id === 'showPkt') {
168 showPacketLayer();
169 } else if (id === 'showOpt') {
170 showOpticalLayer();
171 }
172 }
173
174 function showAllLayers() {
175 network.node.classed('inactive', false);
176 network.link.classed('inactive', false);
177 }
178
179 function showPacketLayer() {
180 network.node.each(function(d) {
181 // deactivate nodes that are not hosts or switches
182 if (d.class === 'device' && d.type !== 'switch') {
183 d3.select(this).classed('inactive', true);
184 }
185 });
186
187 network.link.each(function(lnk) {
188 // deactivate infrastructure links that have opt's as endpoints
189 if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') {
190 d3.select(this).classed('inactive', true);
191 }
192 });
193 }
194
195 function showOpticalLayer() {
196 network.node.each(function(d) {
197 // deactivate nodes that are not optical devices
198 if (d.type !== 'roadm') {
199 d3.select(this).classed('inactive', true);
200 }
201 });
202
203 network.link.each(function(lnk) {
204 // deactivate infrastructure links that have opt's as endpoints
205 if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') {
206 d3.select(this).classed('inactive', true);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700207 }
208 });
209 }
210
Simon Huntd35961b2014-10-28 08:49:48 -0700211 function setUpKeyHandler() {
212 d3.select('body')
213 .on('keydown', function () {
214 processKeyEvent();
215 if (debug('showKeyHandler')) {
216 network.svg.append('text')
217 .attr('x', 5)
218 .attr('y', 15)
219 .style('font-size', '20pt')
220 .text('keyCode: ' + d3.event.keyCode +
221 ' applied to : ' + contextLabel())
222 .transition().duration(2000)
223 .style('font-size', '2pt')
224 .style('fill-opacity', 0.01)
225 .remove();
226 }
227 });
228 }
229
Simon Hunt9a16c822014-10-28 16:09:19 -0700230 function contextLabel() {
231 return hovered === null ? "(nothing)" : hovered.id;
232 }
233
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700234 function radioButton(group, id) {
235 d3.selectAll("#" + group + " .radio").classed("active", false);
236 d3.select("#" + group + " #" + id).classed("active", true);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700237 }
238
Simon Huntd35961b2014-10-28 08:49:48 -0700239 function processKeyEvent() {
240 var code = d3.event.keyCode;
241 switch (code) {
242 case 76: // L
243 cycleLabels();
244 break;
245 case 80: // P
246 togglePorts();
Simon Hunt9a16c822014-10-28 16:09:19 -0700247 break;
248 case 85: // U
249 unpin();
250 break;
Simon Huntd35961b2014-10-28 08:49:48 -0700251 }
252
253 }
254
255 function cycleLabels() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700256 console.log('Cycle Labels - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700257 }
258
259 function togglePorts() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700260 console.log('Toggle Ports - context = ' + contextLabel());
261 }
262
263 function unpin() {
264 if (hovered) {
265 hovered.fixed = false;
Simon Huntf967d512014-10-28 20:34:29 -0700266 findNodeFromData(hovered).classed('fixed', false);
Simon Hunt9a16c822014-10-28 16:09:19 -0700267 network.force.resume();
268 }
269 console.log('Unpin - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700270 }
271
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700272
273 // ========================================================
274
275 function drawNetwork() {
276 $('#view').empty();
277
278 prepareNodesAndLinks();
279 createLayout();
280 console.log("\n\nHere is the augmented network object...");
Simon Hunt9a16c822014-10-28 16:09:19 -0700281 console.log(network);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700282 }
283
284 function prepareNodesAndLinks() {
285 network.lookup = {};
286 network.nodes = [];
287 network.links = [];
288
289 var nw = network.forceWidth,
290 nh = network.forceHeight;
291
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700292 function yPosConstraintForNode(n) {
293 return config.constraints.ypos[n.type || 'host'];
294 }
295
296 // Note that both 'devices' and 'hosts' get mapped into the nodes array
297
298 // first, the devices...
299 network.data.devices.forEach(function(n) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700300 var ypc = yPosConstraintForNode(n),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700301 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700302 iy = ypc * nh,
303 node = {
304 id: n.id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700305 labels: n.labels,
306 class: 'device',
307 icon: 'device',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700308 type: n.type,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700309 x: ix,
310 y: iy,
311 constraint: {
312 weight: 0.7,
313 y: iy
314 }
315 };
316 network.lookup[n.id] = node;
317 network.nodes.push(node);
318 });
319
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700320 // then, the hosts...
321 network.data.hosts.forEach(function(n) {
322 var ypc = yPosConstraintForNode(n),
323 ix = Math.random() * 0.6 * nw + 0.2 * nw,
324 iy = ypc * nh,
325 node = {
326 id: n.id,
327 labels: n.labels,
328 class: 'host',
329 icon: 'host',
330 type: n.type,
331 x: ix,
332 y: iy,
333 constraint: {
334 weight: 0.7,
335 y: iy
336 }
337 };
338 network.lookup[n.id] = node;
339 network.nodes.push(node);
340 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700341
342
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700343 // now, process the explicit links...
Simon Hunt6f376a32014-10-28 12:38:30 -0700344 network.data.links.forEach(function(lnk) {
345 var src = network.lookup[lnk.src],
346 dst = network.lookup[lnk.dst],
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700347 id = src.id + "~" + dst.id;
348
349 var link = {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700350 class: 'infra',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700351 id: id,
Simon Hunt6f376a32014-10-28 12:38:30 -0700352 type: lnk.type,
353 width: lnk.linkWidth,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700354 source: src,
355 target: dst,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700356 strength: config.force.linkStrength.infra
357 };
358 network.links.push(link);
359 });
360
361 // finally, infer host links...
362 network.data.hosts.forEach(function(n) {
363 var src = network.lookup[n.id],
364 dst = network.lookup[n.cp.device],
365 id = src.id + "~" + dst.id;
366
367 var link = {
368 class: 'host',
369 id: id,
370 type: 'hostLink',
371 width: config.hostLinkWidth,
372 source: src,
373 target: dst,
374 strength: config.force.linkStrength.host
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700375 };
376 network.links.push(link);
377 });
378 }
379
380 function createLayout() {
381
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700382 var cfg = config.force;
383
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700384 network.force = d3.layout.force()
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700385 .size([network.forceWidth, network.forceHeight])
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700386 .nodes(network.nodes)
387 .links(network.links)
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700388 .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
389 .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
390 .charge(function(d) { return cfg.charge[d.class]; })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700391 .on('tick', tick);
392
393 network.svg = d3.select('#view').append('svg')
394 .attr('width', view.width)
395 .attr('height', view.height)
396 .append('g')
Simon Huntae968a62014-10-22 14:54:41 -0700397 .attr('transform', config.force.translate());
Simon Hunt3ab76a82014-10-22 13:07:32 -0700398// .attr('id', 'zoomable')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700399// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700400
Simon Hunt3ab76a82014-10-22 13:07:32 -0700401// function zoomRedraw() {
402// d3.select("#zoomable").attr("transform",
403// "translate(" + d3.event.translate + ")"
404// + " scale(" + d3.event.scale + ")");
405// }
406
Simon Hunt3ab76a82014-10-22 13:07:32 -0700407 // TODO: move glow/blur stuff to util script
408 var glow = network.svg.append('filter')
409 .attr('x', '-50%')
410 .attr('y', '-50%')
411 .attr('width', '200%')
412 .attr('height', '200%')
413 .attr('id', 'blue-glow');
414
415 glow.append('feColorMatrix')
416 .attr('type', 'matrix')
417 .attr('values', '0 0 0 0 0 ' +
418 '0 0 0 0 0 ' +
419 '0 0 0 0 .7 ' +
420 '0 0 0 1 0 ');
421
422 glow.append('feGaussianBlur')
423 .attr('stdDeviation', 3)
424 .attr('result', 'coloredBlur');
425
426 glow.append('feMerge').selectAll('feMergeNode')
427 .data(['coloredBlur', 'SourceGraphic'])
428 .enter().append('feMergeNode')
429 .attr('in', String);
430
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700431 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700432// $('#view').on('scroll', function() {
433//
434// });
435
436
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700437 // add links to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700438 network.link = network.svg.append('g').selectAll('.link')
439 .data(network.force.links(), function(d) {return d.id})
440 .enter().append('line')
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700441 .attr('class', function(d) {return 'link ' + d.class});
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700442
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700443
Simon Hunt6f376a32014-10-28 12:38:30 -0700444 // TODO: move drag behavior into separate method.
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700445 // == define node drag behavior...
Simon Hunt3ab76a82014-10-22 13:07:32 -0700446 network.draggedThreshold = d3.scale.linear()
447 .domain([0, 0.1])
448 .range([5, 20])
449 .clamp(true);
450
451 function dragged(d) {
452 var threshold = network.draggedThreshold(network.force.alpha()),
453 dx = d.oldX - d.px,
454 dy = d.oldY - d.py;
455 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
456 d.dragged = true;
457 }
458 return d.dragged;
459 }
460
461 network.drag = d3.behavior.drag()
462 .origin(function(d) { return d; })
463 .on('dragstart', function(d) {
464 d.oldX = d.x;
465 d.oldY = d.y;
466 d.dragged = false;
467 d.fixed |= 2;
468 })
469 .on('drag', function(d) {
470 d.px = d3.event.x;
471 d.py = d3.event.y;
472 if (dragged(d)) {
473 if (!network.force.alpha()) {
474 network.force.alpha(.025);
475 }
476 }
477 })
478 .on('dragend', function(d) {
479 if (!dragged(d)) {
480 selectObject(d, this);
481 }
482 d.fixed &= ~6;
Simon Hunt9a16c822014-10-28 16:09:19 -0700483
484 // once we've finished moving, pin the node in position,
Simon Huntf967d512014-10-28 20:34:29 -0700485 // if it is a device (not a host)
Simon Hunt9a16c822014-10-28 16:09:19 -0700486 if (d.class === 'device') {
487 d.fixed = true;
Simon Huntf967d512014-10-28 20:34:29 -0700488 d3.select(this).classed('fixed', true)
Simon Hunt9a16c822014-10-28 16:09:19 -0700489 }
Simon Hunt3ab76a82014-10-22 13:07:32 -0700490 });
491
492 $('#view').on('click', function(e) {
493 if (!$(e.target).closest('.node').length) {
494 deselectObject();
495 }
496 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700497
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700498
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700499 // add nodes to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700500 network.node = network.svg.selectAll('.node')
501 .data(network.force.nodes(), function(d) {return d.id})
502 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700503 .attr('class', function(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700504 var cls = 'node ' + d.class;
505 if (d.type) {
506 cls += ' ' + d.type;
507 }
508 return cls;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700509 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700510 .attr('transform', function(d) {
511 return translate(d.x, d.y);
512 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700513 .call(network.drag)
514 .on('mouseover', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700515 // TODO: show tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700516 if (network.mouseoutTimeout) {
517 clearTimeout(network.mouseoutTimeout);
518 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700519 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700520 hoverObject(d);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700521 })
522 .on('mouseout', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700523 // TODO: hide tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700524 if (network.mouseoutTimeout) {
525 clearTimeout(network.mouseoutTimeout);
526 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700527 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700528 network.mouseoutTimeout = setTimeout(function() {
529 hoverObject(null);
530 }, config.mouseOutTimerDelayMs);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700531 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700532
Simon Hunt6f376a32014-10-28 12:38:30 -0700533
534 // deal with device nodes first
535 network.nodeRect = network.node.filter('.device')
536 .append('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700537 .attr({
538 rx: 5,
539 ry: 5,
540 width: 100,
541 height: 12
542 });
543 // note that width/height are adjusted to fit the label text
Simon Hunt6f376a32014-10-28 12:38:30 -0700544 // then padded, and space made for the icon.
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700545
Simon Hunt6f376a32014-10-28 12:38:30 -0700546 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700547 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700548 icon = iconUrl(d);
549
550 node.append('text')
551 // TODO: add label cycle behavior
552 .text(d.id)
553 .attr('dy', '1.1em');
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700554
555 if (icon) {
556 var cfg = config.icons;
557 node.append('svg:image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700558 .attr({
559 width: cfg.w,
560 height: cfg.h,
561 'xlink:href': icon
562 });
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700563 // note, icon relative positioning (x,y) is done after we have
564 // adjusted the bounds of the rectangle...
565 }
Simon Hunt68ae6652014-10-22 13:58:07 -0700566
Simon Huntd35961b2014-10-28 08:49:48 -0700567 // debug function to show the modelled x,y coordinates of nodes...
568 if (debug('showNodeXY')) {
569 node.select('rect').attr('fill-opacity', 0.5);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700570 node.append('circle')
571 .attr({
572 class: 'debug',
573 cx: 0,
574 cy: 0,
575 r: '3px'
576 });
577 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700578 });
579
Simon Hunt6f376a32014-10-28 12:38:30 -0700580 // now process host nodes
581 network.nodeCircle = network.node.filter('.host')
582 .append('circle')
583 .attr({
584 r: config.hostRadius
585 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700586
Simon Hunt6f376a32014-10-28 12:38:30 -0700587 network.node.filter('.host').each(function(d) {
588 var node = d3.select(this),
589 icon = iconUrl(d);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700590
Simon Hunt6f376a32014-10-28 12:38:30 -0700591 // debug function to show the modelled x,y coordinates of nodes...
592 if (debug('showNodeXY')) {
593 node.select('circle').attr('fill-opacity', 0.5);
594 node.append('circle')
595 .attr({
596 class: 'debug',
597 cx: 0,
598 cy: 0,
599 r: '3px'
600 });
601 }
602 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700603
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700604 // this function is scheduled to happen soon after the given thread ends
605 setTimeout(function() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700606 // post process the device nodes, to pad their size to fit the
607 // label text and attach the icon to the right location.
608 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700609 // for every node, recompute size, padding, etc. so text fits
610 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700611 text = node.select('text'),
612 box = adjustRectToFitText(node),
613 lab = config.labels;
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700614
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700615 // now make the computed adjustment
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700616 node.select('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700617 .attr(box);
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700618
619 node.select('image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700620 .attr('x', box.x + config.icons.xoff)
621 .attr('y', box.y + config.icons.yoff);
Simon Hunt1c219892014-10-22 16:32:39 -0700622
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700623 var bounds = boundsFromBox(box);
624
625 // todo: clean up extent and edge work..
Simon Hunt1c219892014-10-22 16:32:39 -0700626 d.extent = {
627 left: bounds.x1 - lab.marginLR,
628 right: bounds.x2 + lab.marginLR,
629 top: bounds.y1 - lab.marginTB,
630 bottom: bounds.y2 + lab.marginTB
631 };
632
633 d.edge = {
634 left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
635 right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
636 top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
637 bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
638 };
639
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700640 });
641
642 network.numTicks = 0;
643 network.preventCollisions = false;
644 network.force.start();
Simon Hunt1c219892014-10-22 16:32:39 -0700645 for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700646 network.force.tick();
647 }
648 network.preventCollisions = true;
649 $('#view').css('visibility', 'visible');
650 });
651
Simon Hunt6f376a32014-10-28 12:38:30 -0700652
653 // returns the newly computed bounding box of the rectangle
654 function adjustRectToFitText(n) {
655 var text = n.select('text'),
656 box = text.node().getBBox(),
657 lab = config.labels;
658
Simon Hunt9a16c822014-10-28 16:09:19 -0700659 // not sure why n.data() returns an array of 1 element...
660 var data = n.data()[0];
661
Simon Hunt6f376a32014-10-28 12:38:30 -0700662 text.attr('text-anchor', 'middle')
663 .attr('y', '-0.8em')
664 .attr('x', lab.imgPad/2)
665 ;
666
Simon Hunt6f376a32014-10-28 12:38:30 -0700667 // translate the bbox so that it is centered on [x,y]
668 box.x = -box.width / 2;
669 box.y = -box.height / 2;
670
671 // add padding
672 box.x -= (lab.padLR + lab.imgPad/2);
673 box.width += lab.padLR * 2 + lab.imgPad;
674 box.y -= lab.padTB;
675 box.height += lab.padTB * 2;
676
677 return box;
678 }
679
680 function boundsFromBox(box) {
681 return {
682 x1: box.x,
683 y1: box.y,
684 x2: box.x + box.width,
685 y2: box.y + box.height
686 };
687 }
688
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700689 }
690
Simon Hunt68ae6652014-10-22 13:58:07 -0700691 function iconUrl(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700692 return config.iconUrl[d.icon];
Simon Hunt68ae6652014-10-22 13:58:07 -0700693 }
694
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700695 function translate(x, y) {
696 return 'translate(' + x + ',' + y + ')';
697 }
698
Simon Hunt6f376a32014-10-28 12:38:30 -0700699 // prevents collisions amongst device nodes
Simon Hunt1c219892014-10-22 16:32:39 -0700700 function preventCollisions() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700701 var quadtree = d3.geom.quadtree(network.nodes),
702 hrad = config.hostRadius;
Simon Hunt1c219892014-10-22 16:32:39 -0700703
704 network.nodes.forEach(function(n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700705 var nx1, nx2, ny1, ny2;
706
707 if (n.class === 'device') {
708 nx1 = n.x + n.extent.left;
709 nx2 = n.x + n.extent.right;
710 ny1 = n.y + n.extent.top;
Simon Hunt1c219892014-10-22 16:32:39 -0700711 ny2 = n.y + n.extent.bottom;
712
Simon Hunt6f376a32014-10-28 12:38:30 -0700713 } else {
714 nx1 = n.x - hrad;
715 nx2 = n.x + hrad;
716 ny1 = n.y - hrad;
717 ny2 = n.y + hrad;
718 }
719
Simon Hunt1c219892014-10-22 16:32:39 -0700720 quadtree.visit(function(quad, x1, y1, x2, y2) {
721 if (quad.point && quad.point !== n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700722 // check if the rectangles/circles intersect
Simon Hunt1c219892014-10-22 16:32:39 -0700723 var p = quad.point,
Simon Hunt6f376a32014-10-28 12:38:30 -0700724 px1, px2, py1, py2, ix;
725
726 if (p.class === 'device') {
727 px1 = p.x + p.extent.left;
728 px2 = p.x + p.extent.right;
729 py1 = p.y + p.extent.top;
730 py2 = p.y + p.extent.bottom;
731
732 } else {
733 px1 = p.x - hrad;
734 px2 = p.x + hrad;
735 py1 = p.y - hrad;
736 py2 = p.y + hrad;
737 }
738
739 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
740
Simon Hunt1c219892014-10-22 16:32:39 -0700741 if (ix) {
742 var xa1 = nx2 - px1, // shift n left , p right
743 xa2 = px2 - nx1, // shift n right, p left
744 ya1 = ny2 - py1, // shift n up , p down
745 ya2 = py2 - ny1, // shift n down , p up
746 adj = Math.min(xa1, xa2, ya1, ya2);
747
748 if (adj == xa1) {
749 n.x -= adj / 2;
750 p.x += adj / 2;
751 } else if (adj == xa2) {
752 n.x += adj / 2;
753 p.x -= adj / 2;
754 } else if (adj == ya1) {
755 n.y -= adj / 2;
756 p.y += adj / 2;
757 } else if (adj == ya2) {
758 n.y += adj / 2;
759 p.y -= adj / 2;
760 }
761 }
762 return ix;
763 }
764 });
765
766 });
767 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700768
769 function tick(e) {
770 network.numTicks++;
771
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700772 if (config.options.layering) {
Simon Hunt68ae6652014-10-22 13:58:07 -0700773 // adjust the y-coord of each node, based on y-pos constraints
774 network.nodes.forEach(function (n) {
775 var z = e.alpha * n.constraint.weight;
776 if (!isNaN(n.constraint.y)) {
777 n.y = (n.constraint.y * z + n.y * (1 - z));
778 }
779 });
780 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700781
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700782 if (config.options.collisionPrevention && network.preventCollisions) {
Simon Hunt1c219892014-10-22 16:32:39 -0700783 preventCollisions();
784 }
785
Simon Huntd35961b2014-10-28 08:49:48 -0700786 // clip visualization of links at bounds of nodes...
787 network.link.each(function(d) {
788 var xs = d.source.x,
789 ys = d.source.y,
790 xt = d.target.x,
791 yt = d.target.y,
792 line = new geo.LineSegment(xs, ys, xt, yt),
793 e, ix;
Simon Hunt1c219892014-10-22 16:32:39 -0700794
Simon Huntd35961b2014-10-28 08:49:48 -0700795 for (e in d.source.edge) {
796 ix = line.intersect(d.source.edge[e].offset(xs, ys));
Simon Hunt1c219892014-10-22 16:32:39 -0700797 if (ix.in1 && ix.in2) {
Simon Huntd35961b2014-10-28 08:49:48 -0700798 xs = ix.x;
799 ys = ix.y;
800 break;
801 }
802 }
803
804 for (e in d.target.edge) {
805 ix = line.intersect(d.target.edge[e].offset(xt, yt));
806 if (ix.in1 && ix.in2) {
807 xt = ix.x;
808 yt = ix.y;
Simon Hunt1c219892014-10-22 16:32:39 -0700809 break;
810 }
811 }
812
813 d3.select(this)
Simon Huntd35961b2014-10-28 08:49:48 -0700814 .attr('x1', xs)
815 .attr('y1', ys)
816 .attr('x2', xt)
817 .attr('y2', yt);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700818 });
819
Simon Huntd35961b2014-10-28 08:49:48 -0700820 // position each node by translating the node (group) by x,y
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700821 network.node
822 .attr('transform', function(d) {
823 return translate(d.x, d.y);
824 });
825
826 }
827
828 // $('#docs-close').on('click', function() {
829 // deselectObject();
830 // return false;
831 // });
832
833 // $(document).on('click', '.select-object', function() {
834 // var obj = graph.data[$(this).data('name')];
835 // if (obj) {
836 // selectObject(obj);
837 // }
838 // return false;
839 // });
840
Simon Hunt6f376a32014-10-28 12:38:30 -0700841 function findNodeFromData(d) {
842 var el = null;
843 network.node.filter('.' + d.class).each(function(n) {
844 if (n.id === d.id) {
845 el = d3.select(this);
846 }
847 });
848 return el;
849 }
850
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700851 function selectObject(obj, el) {
852 var node;
853 if (el) {
854 node = d3.select(el);
855 } else {
856 network.node.each(function(d) {
857 if (d == obj) {
858 node = d3.select(el = this);
859 }
860 });
861 }
862 if (!node) return;
863
864 if (node.classed('selected')) {
865 deselectObject();
Simon Huntc586e212014-10-28 21:24:08 -0700866 flyinPane(null);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700867 return;
868 }
869 deselectObject(false);
870
871 selected = {
872 obj : obj,
873 el : el
874 };
875
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700876 node.classed('selected', true);
Simon Huntc586e212014-10-28 21:24:08 -0700877 flyinPane(obj);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700878 }
879
880 function deselectObject(doResize) {
881 // Review: logic of 'resize(...)' function.
882 if (doResize || typeof doResize == 'undefined') {
883 resize(false);
884 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700885
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700886 // deselect all nodes in the network...
887 network.node.classed('selected', false);
888 selected = {};
Simon Huntc586e212014-10-28 21:24:08 -0700889 flyinPane(null);
890 }
891
Simon Huntcc267562014-10-29 10:22:17 -0700892 function detailUrl(id) {
Thomas Vachuskae972ea52014-10-30 00:14:16 -0700893 if (config.jsonPrefix) {
894 var safeId = id.replace(/[^a-z0-9]/gi, '_');
895 return config.jsonPrefix + safeId + '.json';
896 }
897 return config.jsonUrl + '/' + encodeURIComponent(id);
Simon Huntcc267562014-10-29 10:22:17 -0700898 }
899
Simon Huntc586e212014-10-28 21:24:08 -0700900 function flyinPane(obj) {
901 var pane = d3.select('#flyout'),
Simon Huntcc267562014-10-29 10:22:17 -0700902 url;
Simon Huntc586e212014-10-28 21:24:08 -0700903
904 if (obj) {
Simon Huntcc267562014-10-29 10:22:17 -0700905 // go get details of the selected object from the server...
906 url = detailUrl(obj.id);
907 d3.json(url, function (err, data) {
908 if (err) {
909 alert('Oops! Error reading JSON...\n\n' +
910 'URL: ' + url + '\n\n' +
911 'Error: ' + err.message);
912 return;
913 }
914// console.log("JSON data... " + url);
915// console.log(data);
916
917 displayDetails(data, pane);
918 });
919
920 } else {
921 // hide pane
922 pane.transition().duration(750)
923 .style('right', '-320px')
924 .style('opacity', 0.0);
925 }
926 }
927
928 function displayDetails(data, pane) {
929 $('#flyout').empty();
930
931 pane.append('h2').text(data.id);
932
933 var table = pane.append("table"),
934 tbody = table.append("tbody");
935
936 // TODO: consider using d3 data bind to TR/TD
937
938 data.propOrder.forEach(function(p) {
939 addProp(tbody, p, data.props[p]);
940 });
941
942 function addProp(tbody, label, value) {
943 var tr = tbody.append('tr');
944
945 tr.append('td')
946 .attr('class', 'label')
947 .text(label + ' :');
948
949 tr.append('td')
950 .attr('class', 'value')
951 .text(value);
Simon Huntc586e212014-10-28 21:24:08 -0700952 }
953
Simon Huntcc267562014-10-29 10:22:17 -0700954 // show pane
Simon Huntc586e212014-10-28 21:24:08 -0700955 pane.transition().duration(750)
Simon Huntcc267562014-10-29 10:22:17 -0700956 .style('right', '20px')
957 .style('opacity', 1.0);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700958 }
959
960 function highlightObject(obj) {
961 if (obj) {
962 if (obj != highlighted) {
963 // TODO set or clear "inactive" class on nodes, based on criteria
964 network.node.classed('inactive', function(d) {
965 // return (obj !== d &&
966 // d.relation(obj.id));
967 return (obj !== d);
968 });
969 // TODO: same with links
970 network.link.classed('inactive', function(d) {
971 return (obj !== d.source && obj !== d.target);
972 });
973 }
974 highlighted = obj;
975 } else {
976 if (highlighted) {
977 // clear the inactive flag (no longer suppressed visually)
978 network.node.classed('inactive', false);
979 network.link.classed('inactive', false);
980 }
981 highlighted = null;
982
983 }
984 }
985
Simon Hunt9a16c822014-10-28 16:09:19 -0700986 function hoverObject(obj) {
987 if (obj) {
988 hovered = obj;
989 } else {
990 if (hovered) {
991 hovered = null;
992 }
993 }
994 }
995
996
Simon Huntc586e212014-10-28 21:24:08 -0700997 function resize() {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700998 view.height = window.innerHeight - config.mastHeight;
999 view.width = window.innerWidth;
1000 $('#view')
1001 .css('height', view.height + 'px')
1002 .css('width', view.width + 'px');
1003
1004 network.forceWidth = view.width - config.force.marginLR;
1005 network.forceHeight = view.height - config.force.marginTB;
1006 }
1007
1008 // ======================================================================
1009 // register with the UI framework
1010
1011 api.addView('network', {
1012 load: loadNetworkView
1013 });
1014
1015
1016}(ONOS));
1017