blob: b84efc51b1f16dcd07b49e38739eade9d94b0749 [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 Hunt0b05d4a2014-10-21 21:50:15 -070041 jsonUrl: 'network.json',
Simon Hunt68ae6652014-10-22 13:58:07 -070042 iconUrl: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070043 device: 'img/device.png',
44 host: 'img/host.png',
45 pkt: 'img/pkt.png',
46 opt: 'img/opt.png'
Simon Hunt68ae6652014-10-22 13:58:07 -070047 },
Simon Hunt19cb0982014-10-23 16:44:49 -070048 mastHeight: 36,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070049 force: {
Simon Hunt2c9e0c22014-10-23 15:12:58 -070050 note: 'node.class or link.class is used to differentiate',
51 linkDistance: {
Simon Hunt6f376a32014-10-28 12:38:30 -070052 infra: 200,
Simon Hunt9a16c822014-10-28 16:09:19 -070053 host: 40
Simon Hunt2c9e0c22014-10-23 15:12:58 -070054 },
55 linkStrength: {
56 infra: 1.0,
Simon Hunt6f376a32014-10-28 12:38:30 -070057 host: 1.0
Simon Hunt2c9e0c22014-10-23 15:12:58 -070058 },
59 charge: {
60 device: -800,
Simon Hunt9a16c822014-10-28 16:09:19 -070061 host: -1000
Simon Hunt2c9e0c22014-10-23 15:12:58 -070062 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070063 ticksWithoutCollisions: 50,
64 marginLR: 20,
65 marginTB: 20,
66 translate: function() {
67 return 'translate(' +
68 config.force.marginLR + ',' +
69 config.force.marginTB + ')';
70 }
71 },
72 labels: {
Simon Hunt19cb0982014-10-23 16:44:49 -070073 imgPad: 16,
Simon Hunt1c5f8b62014-10-22 14:43:01 -070074 padLR: 8,
75 padTB: 6,
Simon Hunt0b05d4a2014-10-21 21:50:15 -070076 marginLR: 3,
77 marginTB: 2
78 },
Simon Hunt2c9e0c22014-10-23 15:12:58 -070079 icons: {
80 w: 32,
81 h: 32,
82 xoff: -12,
Simon Hunt19cb0982014-10-23 16:44:49 -070083 yoff: -8
Simon Hunt2c9e0c22014-10-23 15:12:58 -070084 },
Simon Hunt0b05d4a2014-10-21 21:50:15 -070085 constraints: {
86 ypos: {
Simon Hunt9a16c822014-10-28 16:09:19 -070087 host: 0.05,
Simon Hunt2c9e0c22014-10-23 15:12:58 -070088 switch: 0.3,
89 roadm: 0.7
Simon Hunt0b05d4a2014-10-21 21:50:15 -070090 }
Simon Hunt2c9e0c22014-10-23 15:12:58 -070091 },
92 hostLinkWidth: 1.0,
Simon Hunt6f376a32014-10-28 12:38:30 -070093 hostRadius: 7,
Simon Hunt2c9e0c22014-10-23 15:12:58 -070094 mouseOutTimerDelayMs: 120
Simon Huntd35961b2014-10-28 08:49:48 -070095 };
96
97 // state variables
98 var view = {},
Simon Hunt0b05d4a2014-10-21 21:50:15 -070099 network = {},
100 selected = {},
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700101 highlighted = null,
Simon Hunt9a16c822014-10-28 16:09:19 -0700102 hovered = null,
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700103 viewMode = 'showAll';
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700104
105
Simon Huntd35961b2014-10-28 08:49:48 -0700106 function debug(what) {
107 return config.debugOn && config.debug[what];
108 }
109
110 // load the topology view of the network
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700111 function loadNetworkView() {
112 // Hey, here I am, calling something on the ONOS api:
113 api.printTime();
114
115 resize();
116
Simon Huntd35961b2014-10-28 08:49:48 -0700117 // go get our network data from the server...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700118 d3.json(config.jsonUrl, function (err, data) {
119 if (err) {
120 alert('Oops! Error reading JSON...\n\n' +
Simon Huntae968a62014-10-22 14:54:41 -0700121 'URL: ' + config.jsonUrl + '\n\n' +
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700122 'Error: ' + err.message);
123 return;
124 }
Simon Huntd35961b2014-10-28 08:49:48 -0700125// console.log("here is the JSON data...");
126// console.log(data);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700127
128 network.data = data;
129 drawNetwork();
130 });
131
Simon Huntd35961b2014-10-28 08:49:48 -0700132 // while we wait for the data, set up the handlers...
133 setUpClickHandler();
134 setUpRadioButtonHandler();
135 setUpKeyHandler();
136 $(window).on('resize', resize);
137 }
138
139 function setUpClickHandler() {
140 // click handler for "selectable" objects
141 $(document).on('click', '.select-object', function () {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700142 // when any object of class "select-object" is clicked...
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700143 var obj = network.lookup[$(this).data('id')];
144 if (obj) {
145 selectObject(obj);
146 }
147 // stop propagation of event (I think) ...
148 return false;
149 });
Simon Huntd35961b2014-10-28 08:49:48 -0700150 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700151
Simon Huntd35961b2014-10-28 08:49:48 -0700152 function setUpRadioButtonHandler() {
153 d3.selectAll('#displayModes .radio').on('click', function () {
154 var id = d3.select(this).attr('id');
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700155 if (id !== viewMode) {
156 radioButton('displayModes', id);
157 viewMode = id;
Simon Huntf967d512014-10-28 20:34:29 -0700158 doRadioAction(id);
159 }
160 });
161 }
162
163 function doRadioAction(id) {
164 showAllLayers();
165 if (id === 'showPkt') {
166 showPacketLayer();
167 } else if (id === 'showOpt') {
168 showOpticalLayer();
169 }
170 }
171
172 function showAllLayers() {
173 network.node.classed('inactive', false);
174 network.link.classed('inactive', false);
175 }
176
177 function showPacketLayer() {
178 network.node.each(function(d) {
179 // deactivate nodes that are not hosts or switches
180 if (d.class === 'device' && d.type !== 'switch') {
181 d3.select(this).classed('inactive', true);
182 }
183 });
184
185 network.link.each(function(lnk) {
186 // deactivate infrastructure links that have opt's as endpoints
187 if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') {
188 d3.select(this).classed('inactive', true);
189 }
190 });
191 }
192
193 function showOpticalLayer() {
194 network.node.each(function(d) {
195 // deactivate nodes that are not optical devices
196 if (d.type !== 'roadm') {
197 d3.select(this).classed('inactive', true);
198 }
199 });
200
201 network.link.each(function(lnk) {
202 // deactivate infrastructure links that have opt's as endpoints
203 if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') {
204 d3.select(this).classed('inactive', true);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700205 }
206 });
207 }
208
Simon Huntd35961b2014-10-28 08:49:48 -0700209 function setUpKeyHandler() {
210 d3.select('body')
211 .on('keydown', function () {
212 processKeyEvent();
213 if (debug('showKeyHandler')) {
214 network.svg.append('text')
215 .attr('x', 5)
216 .attr('y', 15)
217 .style('font-size', '20pt')
218 .text('keyCode: ' + d3.event.keyCode +
219 ' applied to : ' + contextLabel())
220 .transition().duration(2000)
221 .style('font-size', '2pt')
222 .style('fill-opacity', 0.01)
223 .remove();
224 }
225 });
226 }
227
Simon Hunt9a16c822014-10-28 16:09:19 -0700228 function contextLabel() {
229 return hovered === null ? "(nothing)" : hovered.id;
230 }
231
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700232 function radioButton(group, id) {
233 d3.selectAll("#" + group + " .radio").classed("active", false);
234 d3.select("#" + group + " #" + id).classed("active", true);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700235 }
236
Simon Huntd35961b2014-10-28 08:49:48 -0700237 function processKeyEvent() {
238 var code = d3.event.keyCode;
239 switch (code) {
240 case 76: // L
241 cycleLabels();
242 break;
243 case 80: // P
244 togglePorts();
Simon Hunt9a16c822014-10-28 16:09:19 -0700245 break;
246 case 85: // U
247 unpin();
248 break;
Simon Huntd35961b2014-10-28 08:49:48 -0700249 }
250
251 }
252
253 function cycleLabels() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700254 console.log('Cycle Labels - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700255 }
256
257 function togglePorts() {
Simon Hunt9a16c822014-10-28 16:09:19 -0700258 console.log('Toggle Ports - context = ' + contextLabel());
259 }
260
261 function unpin() {
262 if (hovered) {
263 hovered.fixed = false;
Simon Huntf967d512014-10-28 20:34:29 -0700264 findNodeFromData(hovered).classed('fixed', false);
Simon Hunt9a16c822014-10-28 16:09:19 -0700265 network.force.resume();
266 }
267 console.log('Unpin - context = ' + contextLabel());
Simon Huntd35961b2014-10-28 08:49:48 -0700268 }
269
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700270
271 // ========================================================
272
273 function drawNetwork() {
274 $('#view').empty();
275
276 prepareNodesAndLinks();
277 createLayout();
278 console.log("\n\nHere is the augmented network object...");
Simon Hunt9a16c822014-10-28 16:09:19 -0700279 console.log(network);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700280 }
281
282 function prepareNodesAndLinks() {
283 network.lookup = {};
284 network.nodes = [];
285 network.links = [];
286
287 var nw = network.forceWidth,
288 nh = network.forceHeight;
289
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700290 function yPosConstraintForNode(n) {
291 return config.constraints.ypos[n.type || 'host'];
292 }
293
294 // Note that both 'devices' and 'hosts' get mapped into the nodes array
295
296 // first, the devices...
297 network.data.devices.forEach(function(n) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700298 var ypc = yPosConstraintForNode(n),
Simon Hunt3ab76a82014-10-22 13:07:32 -0700299 ix = Math.random() * 0.6 * nw + 0.2 * nw,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700300 iy = ypc * nh,
301 node = {
302 id: n.id,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700303 labels: n.labels,
304 class: 'device',
305 icon: 'device',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700306 type: n.type,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700307 x: ix,
308 y: iy,
309 constraint: {
310 weight: 0.7,
311 y: iy
312 }
313 };
314 network.lookup[n.id] = node;
315 network.nodes.push(node);
316 });
317
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700318 // then, the hosts...
319 network.data.hosts.forEach(function(n) {
320 var ypc = yPosConstraintForNode(n),
321 ix = Math.random() * 0.6 * nw + 0.2 * nw,
322 iy = ypc * nh,
323 node = {
324 id: n.id,
325 labels: n.labels,
326 class: 'host',
327 icon: 'host',
328 type: n.type,
329 x: ix,
330 y: iy,
331 constraint: {
332 weight: 0.7,
333 y: iy
334 }
335 };
336 network.lookup[n.id] = node;
337 network.nodes.push(node);
338 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700339
340
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700341 // now, process the explicit links...
Simon Hunt6f376a32014-10-28 12:38:30 -0700342 network.data.links.forEach(function(lnk) {
343 var src = network.lookup[lnk.src],
344 dst = network.lookup[lnk.dst],
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700345 id = src.id + "~" + dst.id;
346
347 var link = {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700348 class: 'infra',
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700349 id: id,
Simon Hunt6f376a32014-10-28 12:38:30 -0700350 type: lnk.type,
351 width: lnk.linkWidth,
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700352 source: src,
353 target: dst,
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700354 strength: config.force.linkStrength.infra
355 };
356 network.links.push(link);
357 });
358
359 // finally, infer host links...
360 network.data.hosts.forEach(function(n) {
361 var src = network.lookup[n.id],
362 dst = network.lookup[n.cp.device],
363 id = src.id + "~" + dst.id;
364
365 var link = {
366 class: 'host',
367 id: id,
368 type: 'hostLink',
369 width: config.hostLinkWidth,
370 source: src,
371 target: dst,
372 strength: config.force.linkStrength.host
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700373 };
374 network.links.push(link);
375 });
376 }
377
378 function createLayout() {
379
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700380 var cfg = config.force;
381
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700382 network.force = d3.layout.force()
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700383 .size([network.forceWidth, network.forceHeight])
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700384 .nodes(network.nodes)
385 .links(network.links)
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700386 .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
387 .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
388 .charge(function(d) { return cfg.charge[d.class]; })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700389 .on('tick', tick);
390
391 network.svg = d3.select('#view').append('svg')
392 .attr('width', view.width)
393 .attr('height', view.height)
394 .append('g')
Simon Huntae968a62014-10-22 14:54:41 -0700395 .attr('transform', config.force.translate());
Simon Hunt3ab76a82014-10-22 13:07:32 -0700396// .attr('id', 'zoomable')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700397// .call(d3.behavior.zoom().on("zoom", zoomRedraw));
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700398
Simon Hunt3ab76a82014-10-22 13:07:32 -0700399// function zoomRedraw() {
400// d3.select("#zoomable").attr("transform",
401// "translate(" + d3.event.translate + ")"
402// + " scale(" + d3.event.scale + ")");
403// }
404
Simon Hunt3ab76a82014-10-22 13:07:32 -0700405 // TODO: move glow/blur stuff to util script
406 var glow = network.svg.append('filter')
407 .attr('x', '-50%')
408 .attr('y', '-50%')
409 .attr('width', '200%')
410 .attr('height', '200%')
411 .attr('id', 'blue-glow');
412
413 glow.append('feColorMatrix')
414 .attr('type', 'matrix')
415 .attr('values', '0 0 0 0 0 ' +
416 '0 0 0 0 0 ' +
417 '0 0 0 0 .7 ' +
418 '0 0 0 1 0 ');
419
420 glow.append('feGaussianBlur')
421 .attr('stdDeviation', 3)
422 .attr('result', 'coloredBlur');
423
424 glow.append('feMerge').selectAll('feMergeNode')
425 .data(['coloredBlur', 'SourceGraphic'])
426 .enter().append('feMergeNode')
427 .attr('in', String);
428
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700429 // TODO: legend (and auto adjust on scroll)
Simon Hunt3ab76a82014-10-22 13:07:32 -0700430// $('#view').on('scroll', function() {
431//
432// });
433
434
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700435 // add links to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700436 network.link = network.svg.append('g').selectAll('.link')
437 .data(network.force.links(), function(d) {return d.id})
438 .enter().append('line')
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700439 .attr('class', function(d) {return 'link ' + d.class});
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700440
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700441
Simon Hunt6f376a32014-10-28 12:38:30 -0700442 // TODO: move drag behavior into separate method.
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700443 // == define node drag behavior...
Simon Hunt3ab76a82014-10-22 13:07:32 -0700444 network.draggedThreshold = d3.scale.linear()
445 .domain([0, 0.1])
446 .range([5, 20])
447 .clamp(true);
448
449 function dragged(d) {
450 var threshold = network.draggedThreshold(network.force.alpha()),
451 dx = d.oldX - d.px,
452 dy = d.oldY - d.py;
453 if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
454 d.dragged = true;
455 }
456 return d.dragged;
457 }
458
459 network.drag = d3.behavior.drag()
460 .origin(function(d) { return d; })
461 .on('dragstart', function(d) {
462 d.oldX = d.x;
463 d.oldY = d.y;
464 d.dragged = false;
465 d.fixed |= 2;
466 })
467 .on('drag', function(d) {
468 d.px = d3.event.x;
469 d.py = d3.event.y;
470 if (dragged(d)) {
471 if (!network.force.alpha()) {
472 network.force.alpha(.025);
473 }
474 }
475 })
476 .on('dragend', function(d) {
477 if (!dragged(d)) {
478 selectObject(d, this);
479 }
480 d.fixed &= ~6;
Simon Hunt9a16c822014-10-28 16:09:19 -0700481
482 // once we've finished moving, pin the node in position,
Simon Huntf967d512014-10-28 20:34:29 -0700483 // if it is a device (not a host)
Simon Hunt9a16c822014-10-28 16:09:19 -0700484 if (d.class === 'device') {
485 d.fixed = true;
Simon Huntf967d512014-10-28 20:34:29 -0700486 d3.select(this).classed('fixed', true)
Simon Hunt9a16c822014-10-28 16:09:19 -0700487 }
Simon Hunt3ab76a82014-10-22 13:07:32 -0700488 });
489
490 $('#view').on('click', function(e) {
491 if (!$(e.target).closest('.node').length) {
492 deselectObject();
493 }
494 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700495
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700496
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700497 // add nodes to the display
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700498 network.node = network.svg.selectAll('.node')
499 .data(network.force.nodes(), function(d) {return d.id})
500 .enter().append('g')
Simon Hunt3ab76a82014-10-22 13:07:32 -0700501 .attr('class', function(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700502 var cls = 'node ' + d.class;
503 if (d.type) {
504 cls += ' ' + d.type;
505 }
506 return cls;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700507 })
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700508 .attr('transform', function(d) {
509 return translate(d.x, d.y);
510 })
Simon Hunt3ab76a82014-10-22 13:07:32 -0700511 .call(network.drag)
512 .on('mouseover', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700513 // TODO: show tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700514 if (network.mouseoutTimeout) {
515 clearTimeout(network.mouseoutTimeout);
516 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700517 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700518 hoverObject(d);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700519 })
520 .on('mouseout', function(d) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700521 // TODO: hide tooltip
Simon Hunt9a16c822014-10-28 16:09:19 -0700522 if (network.mouseoutTimeout) {
523 clearTimeout(network.mouseoutTimeout);
524 network.mouseoutTimeout = null;
Simon Hunt3ab76a82014-10-22 13:07:32 -0700525 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700526 network.mouseoutTimeout = setTimeout(function() {
527 hoverObject(null);
528 }, config.mouseOutTimerDelayMs);
Simon Hunt3ab76a82014-10-22 13:07:32 -0700529 });
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700530
Simon Hunt6f376a32014-10-28 12:38:30 -0700531
532 // deal with device nodes first
533 network.nodeRect = network.node.filter('.device')
534 .append('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700535 .attr({
536 rx: 5,
537 ry: 5,
538 width: 100,
539 height: 12
540 });
541 // note that width/height are adjusted to fit the label text
Simon Hunt6f376a32014-10-28 12:38:30 -0700542 // then padded, and space made for the icon.
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700543
Simon Hunt6f376a32014-10-28 12:38:30 -0700544 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700545 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700546 icon = iconUrl(d);
547
548 node.append('text')
549 // TODO: add label cycle behavior
550 .text(d.id)
551 .attr('dy', '1.1em');
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700552
553 if (icon) {
554 var cfg = config.icons;
555 node.append('svg:image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700556 .attr({
557 width: cfg.w,
558 height: cfg.h,
559 'xlink:href': icon
560 });
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700561 // note, icon relative positioning (x,y) is done after we have
562 // adjusted the bounds of the rectangle...
563 }
Simon Hunt68ae6652014-10-22 13:58:07 -0700564
Simon Huntd35961b2014-10-28 08:49:48 -0700565 // debug function to show the modelled x,y coordinates of nodes...
566 if (debug('showNodeXY')) {
567 node.select('rect').attr('fill-opacity', 0.5);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700568 node.append('circle')
569 .attr({
570 class: 'debug',
571 cx: 0,
572 cy: 0,
573 r: '3px'
574 });
575 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700576 });
577
Simon Hunt6f376a32014-10-28 12:38:30 -0700578 // now process host nodes
579 network.nodeCircle = network.node.filter('.host')
580 .append('circle')
581 .attr({
582 r: config.hostRadius
583 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700584
Simon Hunt6f376a32014-10-28 12:38:30 -0700585 network.node.filter('.host').each(function(d) {
586 var node = d3.select(this),
587 icon = iconUrl(d);
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700588
Simon Hunt6f376a32014-10-28 12:38:30 -0700589 // debug function to show the modelled x,y coordinates of nodes...
590 if (debug('showNodeXY')) {
591 node.select('circle').attr('fill-opacity', 0.5);
592 node.append('circle')
593 .attr({
594 class: 'debug',
595 cx: 0,
596 cy: 0,
597 r: '3px'
598 });
599 }
600 });
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700601
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700602 // this function is scheduled to happen soon after the given thread ends
603 setTimeout(function() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700604 // post process the device nodes, to pad their size to fit the
605 // label text and attach the icon to the right location.
606 network.node.filter('.device').each(function(d) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700607 // for every node, recompute size, padding, etc. so text fits
608 var node = d3.select(this),
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700609 text = node.select('text'),
610 box = adjustRectToFitText(node),
611 lab = config.labels;
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700612
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700613 // now make the computed adjustment
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700614 node.select('rect')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700615 .attr(box);
Simon Hunt1c5f8b62014-10-22 14:43:01 -0700616
617 node.select('image')
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700618 .attr('x', box.x + config.icons.xoff)
619 .attr('y', box.y + config.icons.yoff);
Simon Hunt1c219892014-10-22 16:32:39 -0700620
Simon Hunt5cd0e8f2014-10-27 16:18:40 -0700621 var bounds = boundsFromBox(box);
622
623 // todo: clean up extent and edge work..
Simon Hunt1c219892014-10-22 16:32:39 -0700624 d.extent = {
625 left: bounds.x1 - lab.marginLR,
626 right: bounds.x2 + lab.marginLR,
627 top: bounds.y1 - lab.marginTB,
628 bottom: bounds.y2 + lab.marginTB
629 };
630
631 d.edge = {
632 left : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
633 right : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
634 top : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
635 bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
636 };
637
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700638 });
639
640 network.numTicks = 0;
641 network.preventCollisions = false;
642 network.force.start();
Simon Hunt1c219892014-10-22 16:32:39 -0700643 for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700644 network.force.tick();
645 }
646 network.preventCollisions = true;
647 $('#view').css('visibility', 'visible');
648 });
649
Simon Hunt6f376a32014-10-28 12:38:30 -0700650
651 // returns the newly computed bounding box of the rectangle
652 function adjustRectToFitText(n) {
653 var text = n.select('text'),
654 box = text.node().getBBox(),
655 lab = config.labels;
656
Simon Hunt9a16c822014-10-28 16:09:19 -0700657 // not sure why n.data() returns an array of 1 element...
658 var data = n.data()[0];
659
Simon Hunt6f376a32014-10-28 12:38:30 -0700660 text.attr('text-anchor', 'middle')
661 .attr('y', '-0.8em')
662 .attr('x', lab.imgPad/2)
663 ;
664
Simon Hunt6f376a32014-10-28 12:38:30 -0700665 // translate the bbox so that it is centered on [x,y]
666 box.x = -box.width / 2;
667 box.y = -box.height / 2;
668
669 // add padding
670 box.x -= (lab.padLR + lab.imgPad/2);
671 box.width += lab.padLR * 2 + lab.imgPad;
672 box.y -= lab.padTB;
673 box.height += lab.padTB * 2;
674
675 return box;
676 }
677
678 function boundsFromBox(box) {
679 return {
680 x1: box.x,
681 y1: box.y,
682 x2: box.x + box.width,
683 y2: box.y + box.height
684 };
685 }
686
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700687 }
688
Simon Hunt68ae6652014-10-22 13:58:07 -0700689 function iconUrl(d) {
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700690 return config.iconUrl[d.icon];
Simon Hunt68ae6652014-10-22 13:58:07 -0700691 }
692
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700693 function translate(x, y) {
694 return 'translate(' + x + ',' + y + ')';
695 }
696
Simon Hunt6f376a32014-10-28 12:38:30 -0700697 // prevents collisions amongst device nodes
Simon Hunt1c219892014-10-22 16:32:39 -0700698 function preventCollisions() {
Simon Hunt6f376a32014-10-28 12:38:30 -0700699 var quadtree = d3.geom.quadtree(network.nodes),
700 hrad = config.hostRadius;
Simon Hunt1c219892014-10-22 16:32:39 -0700701
702 network.nodes.forEach(function(n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700703 var nx1, nx2, ny1, ny2;
704
705 if (n.class === 'device') {
706 nx1 = n.x + n.extent.left;
707 nx2 = n.x + n.extent.right;
708 ny1 = n.y + n.extent.top;
Simon Hunt1c219892014-10-22 16:32:39 -0700709 ny2 = n.y + n.extent.bottom;
710
Simon Hunt6f376a32014-10-28 12:38:30 -0700711 } else {
712 nx1 = n.x - hrad;
713 nx2 = n.x + hrad;
714 ny1 = n.y - hrad;
715 ny2 = n.y + hrad;
716 }
717
Simon Hunt1c219892014-10-22 16:32:39 -0700718 quadtree.visit(function(quad, x1, y1, x2, y2) {
719 if (quad.point && quad.point !== n) {
Simon Hunt6f376a32014-10-28 12:38:30 -0700720 // check if the rectangles/circles intersect
Simon Hunt1c219892014-10-22 16:32:39 -0700721 var p = quad.point,
Simon Hunt6f376a32014-10-28 12:38:30 -0700722 px1, px2, py1, py2, ix;
723
724 if (p.class === 'device') {
725 px1 = p.x + p.extent.left;
726 px2 = p.x + p.extent.right;
727 py1 = p.y + p.extent.top;
728 py2 = p.y + p.extent.bottom;
729
730 } else {
731 px1 = p.x - hrad;
732 px2 = p.x + hrad;
733 py1 = p.y - hrad;
734 py2 = p.y + hrad;
735 }
736
737 ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
738
Simon Hunt1c219892014-10-22 16:32:39 -0700739 if (ix) {
740 var xa1 = nx2 - px1, // shift n left , p right
741 xa2 = px2 - nx1, // shift n right, p left
742 ya1 = ny2 - py1, // shift n up , p down
743 ya2 = py2 - ny1, // shift n down , p up
744 adj = Math.min(xa1, xa2, ya1, ya2);
745
746 if (adj == xa1) {
747 n.x -= adj / 2;
748 p.x += adj / 2;
749 } else if (adj == xa2) {
750 n.x += adj / 2;
751 p.x -= adj / 2;
752 } else if (adj == ya1) {
753 n.y -= adj / 2;
754 p.y += adj / 2;
755 } else if (adj == ya2) {
756 n.y += adj / 2;
757 p.y -= adj / 2;
758 }
759 }
760 return ix;
761 }
762 });
763
764 });
765 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700766
767 function tick(e) {
768 network.numTicks++;
769
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700770 if (config.options.layering) {
Simon Hunt68ae6652014-10-22 13:58:07 -0700771 // adjust the y-coord of each node, based on y-pos constraints
772 network.nodes.forEach(function (n) {
773 var z = e.alpha * n.constraint.weight;
774 if (!isNaN(n.constraint.y)) {
775 n.y = (n.constraint.y * z + n.y * (1 - z));
776 }
777 });
778 }
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700779
Simon Hunt2c9e0c22014-10-23 15:12:58 -0700780 if (config.options.collisionPrevention && network.preventCollisions) {
Simon Hunt1c219892014-10-22 16:32:39 -0700781 preventCollisions();
782 }
783
Simon Huntd35961b2014-10-28 08:49:48 -0700784 // clip visualization of links at bounds of nodes...
785 network.link.each(function(d) {
786 var xs = d.source.x,
787 ys = d.source.y,
788 xt = d.target.x,
789 yt = d.target.y,
790 line = new geo.LineSegment(xs, ys, xt, yt),
791 e, ix;
Simon Hunt1c219892014-10-22 16:32:39 -0700792
Simon Huntd35961b2014-10-28 08:49:48 -0700793 for (e in d.source.edge) {
794 ix = line.intersect(d.source.edge[e].offset(xs, ys));
Simon Hunt1c219892014-10-22 16:32:39 -0700795 if (ix.in1 && ix.in2) {
Simon Huntd35961b2014-10-28 08:49:48 -0700796 xs = ix.x;
797 ys = ix.y;
798 break;
799 }
800 }
801
802 for (e in d.target.edge) {
803 ix = line.intersect(d.target.edge[e].offset(xt, yt));
804 if (ix.in1 && ix.in2) {
805 xt = ix.x;
806 yt = ix.y;
Simon Hunt1c219892014-10-22 16:32:39 -0700807 break;
808 }
809 }
810
811 d3.select(this)
Simon Huntd35961b2014-10-28 08:49:48 -0700812 .attr('x1', xs)
813 .attr('y1', ys)
814 .attr('x2', xt)
815 .attr('y2', yt);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700816 });
817
Simon Huntd35961b2014-10-28 08:49:48 -0700818 // position each node by translating the node (group) by x,y
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700819 network.node
820 .attr('transform', function(d) {
821 return translate(d.x, d.y);
822 });
823
824 }
825
826 // $('#docs-close').on('click', function() {
827 // deselectObject();
828 // return false;
829 // });
830
831 // $(document).on('click', '.select-object', function() {
832 // var obj = graph.data[$(this).data('name')];
833 // if (obj) {
834 // selectObject(obj);
835 // }
836 // return false;
837 // });
838
Simon Hunt6f376a32014-10-28 12:38:30 -0700839 function findNodeFromData(d) {
840 var el = null;
841 network.node.filter('.' + d.class).each(function(n) {
842 if (n.id === d.id) {
843 el = d3.select(this);
844 }
845 });
846 return el;
847 }
848
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700849 function selectObject(obj, el) {
850 var node;
851 if (el) {
852 node = d3.select(el);
853 } else {
854 network.node.each(function(d) {
855 if (d == obj) {
856 node = d3.select(el = this);
857 }
858 });
859 }
860 if (!node) return;
861
862 if (node.classed('selected')) {
863 deselectObject();
Simon Huntc586e212014-10-28 21:24:08 -0700864 flyinPane(null);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700865 return;
866 }
867 deselectObject(false);
868
869 selected = {
870 obj : obj,
871 el : el
872 };
873
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700874 node.classed('selected', true);
Simon Huntc586e212014-10-28 21:24:08 -0700875 flyinPane(obj);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700876
877 // TODO animate incoming info pane
878 // resize(true);
879 // TODO: check bounds of selected node and scroll into view if needed
880 }
881
882 function deselectObject(doResize) {
883 // Review: logic of 'resize(...)' function.
884 if (doResize || typeof doResize == 'undefined') {
885 resize(false);
886 }
Simon Hunt9a16c822014-10-28 16:09:19 -0700887
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700888 // deselect all nodes in the network...
889 network.node.classed('selected', false);
890 selected = {};
Simon Huntc586e212014-10-28 21:24:08 -0700891 flyinPane(null);
892 }
893
894 function flyinPane(obj) {
895 var pane = d3.select('#flyout'),
896 right = (obj ? '20px' : '-320px'), // TODO: parameterize
897 opac = (obj ? 1.0 : 0.0);
898
899 if (obj) {
900 $('#flyout').empty();
901 pane.append('h2').text(obj.id);
902 pane.append('p').text('class: ' + obj.class);
903 if (obj.type) {
904 pane.append('p').text('type: ' + obj.type);
905 }
906 }
907
908 pane.transition().duration(750)
909 .style('right', right)
910 .style('opacity', opac);
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700911 }
912
913 function highlightObject(obj) {
914 if (obj) {
915 if (obj != highlighted) {
916 // TODO set or clear "inactive" class on nodes, based on criteria
917 network.node.classed('inactive', function(d) {
918 // return (obj !== d &&
919 // d.relation(obj.id));
920 return (obj !== d);
921 });
922 // TODO: same with links
923 network.link.classed('inactive', function(d) {
924 return (obj !== d.source && obj !== d.target);
925 });
926 }
927 highlighted = obj;
928 } else {
929 if (highlighted) {
930 // clear the inactive flag (no longer suppressed visually)
931 network.node.classed('inactive', false);
932 network.link.classed('inactive', false);
933 }
934 highlighted = null;
935
936 }
937 }
938
Simon Hunt9a16c822014-10-28 16:09:19 -0700939 function hoverObject(obj) {
940 if (obj) {
941 hovered = obj;
942 } else {
943 if (hovered) {
944 hovered = null;
945 }
946 }
947 }
948
949
Simon Huntc586e212014-10-28 21:24:08 -0700950 function resize() {
Simon Hunt0b05d4a2014-10-21 21:50:15 -0700951 view.height = window.innerHeight - config.mastHeight;
952 view.width = window.innerWidth;
953 $('#view')
954 .css('height', view.height + 'px')
955 .css('width', view.width + 'px');
956
957 network.forceWidth = view.width - config.force.marginLR;
958 network.forceHeight = view.height - config.force.marginTB;
959 }
960
961 // ======================================================================
962 // register with the UI framework
963
964 api.addView('network', {
965 load: loadNetworkView
966 });
967
968
969}(ONOS));
970