blob: bd5a5776ac39cd932100be8669e463117a5cc41a [file] [log] [blame]
Simon Hunt195cb382014-11-03 17:50:51 -08001/*
2 * Copyright 2014 Open Networking Laboratory
3 *
4 * 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
7 *
8 * 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.
15 */
16
17/*
Simon Hunt142d0032014-11-04 20:13:09 -080018 ONOS network topology viewer - version 1.1
Simon Hunt195cb382014-11-03 17:50:51 -080019
20 @author Simon Hunt
21 */
22
23(function (onos) {
24 'use strict';
25
Simon Hunt195cb382014-11-03 17:50:51 -080026 // configuration data
27 var config = {
Simon Hunt99c13842014-11-06 18:23:12 -080028 useLiveData: false,
Simon Hunt195cb382014-11-03 17:50:51 -080029 debugOn: false,
30 debug: {
Simon Hunt99c13842014-11-06 18:23:12 -080031 showNodeXY: true,
32 showKeyHandler: false
Simon Hunt195cb382014-11-03 17:50:51 -080033 },
34 options: {
35 layering: true,
36 collisionPrevention: true,
Simon Hunt142d0032014-11-04 20:13:09 -080037 showBackground: true
Simon Hunt195cb382014-11-03 17:50:51 -080038 },
39 backgroundUrl: 'img/us-map.png',
40 data: {
41 live: {
42 jsonUrl: 'rs/topology/graph',
43 detailPrefix: 'rs/topology/graph/',
44 detailSuffix: ''
45 },
46 fake: {
47 jsonUrl: 'json/network2.json',
48 detailPrefix: 'json/',
49 detailSuffix: '.json'
50 }
51 },
Simon Hunt99c13842014-11-06 18:23:12 -080052 labels: {
53 imgPad: 16,
54 padLR: 4,
55 padTB: 3,
56 marginLR: 3,
57 marginTB: 2,
58 port: {
59 gap: 3,
60 width: 18,
61 height: 14
62 }
63 },
64 icons: {
65 w: 28,
66 h: 28,
67 xoff: -12,
68 yoff: -8
69 },
Simon Hunt195cb382014-11-03 17:50:51 -080070 iconUrl: {
71 device: 'img/device.png',
72 host: 'img/host.png',
73 pkt: 'img/pkt.png',
74 opt: 'img/opt.png'
75 },
Simon Hunt195cb382014-11-03 17:50:51 -080076 force: {
Simon Huntc7ee0662014-11-05 16:44:37 -080077 note: 'node.class or link.class is used to differentiate',
78 linkDistance: {
79 infra: 200,
80 host: 40
81 },
82 linkStrength: {
83 infra: 1.0,
84 host: 1.0
85 },
86 charge: {
87 device: -400,
88 host: -100
89 },
90 pad: 20,
Simon Hunt195cb382014-11-03 17:50:51 -080091 translate: function() {
92 return 'translate(' +
Simon Huntc7ee0662014-11-05 16:44:37 -080093 config.force.pad + ',' +
94 config.force.pad + ')';
Simon Hunt195cb382014-11-03 17:50:51 -080095 }
Simon Hunt142d0032014-11-04 20:13:09 -080096 }
Simon Hunt195cb382014-11-03 17:50:51 -080097 };
98
Simon Hunt142d0032014-11-04 20:13:09 -080099 // radio buttons
100 var btnSet = [
Simon Hunt934c3ce2014-11-05 11:45:07 -0800101 { text: 'All Layers', cb: showAllLayers },
102 { text: 'Packet Only', cb: showPacketLayer },
103 { text: 'Optical Only', cb: showOpticalLayer }
104 ];
105
106 // key bindings
107 var keyDispatch = {
Simon Hunt99c13842014-11-06 18:23:12 -0800108 space: injectTestEvent, // TODO: remove (testing only)
109 // M: testMe, // TODO: remove (testing only)
110
Simon Hunt934c3ce2014-11-05 11:45:07 -0800111 B: toggleBg,
112 G: toggleLayout,
113 L: cycleLabels,
114 P: togglePorts,
115 U: unpin
116 };
Simon Hunt142d0032014-11-04 20:13:09 -0800117
Simon Hunt195cb382014-11-03 17:50:51 -0800118 // state variables
Simon Hunt99c13842014-11-06 18:23:12 -0800119 var network = {
120 nodes: [],
121 links: [],
122 lookup: {}
123 },
124 labelIdx = 0,
Simon Hunt195cb382014-11-03 17:50:51 -0800125 selected = {},
126 highlighted = null,
127 hovered = null,
128 viewMode = 'showAll',
129 portLabelsOn = false;
130
Simon Hunt934c3ce2014-11-05 11:45:07 -0800131 // D3 selections
132 var svg,
133 bgImg,
Simon Huntc7ee0662014-11-05 16:44:37 -0800134 topoG,
135 nodeG,
136 linkG,
137 node,
138 link;
Simon Hunt195cb382014-11-03 17:50:51 -0800139
Simon Hunt142d0032014-11-04 20:13:09 -0800140 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800141 // For Debugging / Development
Simon Hunt195cb382014-11-03 17:50:51 -0800142
Simon Hunt99c13842014-11-06 18:23:12 -0800143 var eventPrefix = 'json/eventTest_',
144 eventNumber = 0;
Simon Hunt195cb382014-11-03 17:50:51 -0800145
Simon Hunt99c13842014-11-06 18:23:12 -0800146 function note(label, msg) {
147 console.log('NOTE: ' + label + ': ' + msg);
Simon Hunt195cb382014-11-03 17:50:51 -0800148 }
149
Simon Hunt99c13842014-11-06 18:23:12 -0800150 function debug(what) {
151 return config.debugOn && config.debug[what];
Simon Hunt934c3ce2014-11-05 11:45:07 -0800152 }
153
Simon Hunt99c13842014-11-06 18:23:12 -0800154
Simon Hunt934c3ce2014-11-05 11:45:07 -0800155 // ==============================
156 // Key Callbacks
157
Simon Hunt99c13842014-11-06 18:23:12 -0800158 function testMe(view) {
159 svg.append('line')
160 .attr({
161 x1: 100,
162 y1: 100,
163 x2: 500,
164 y2: 400,
165 stroke: '#2f3',
166 'stroke-width': 8
167 })
168 .transition()
169 .duration(1200)
170 .attr({
171 stroke: '#666',
172 'stroke-width': 6
173 });
174 }
175
176 function injectTestEvent(view) {
177 eventNumber++;
178 var eventUrl = eventPrefix + eventNumber + '.json';
179
180 console.log('Fetching JSON: ' + eventUrl);
181 d3.json(eventUrl, function(err, data) {
182 if (err) {
183 view.dataLoadError(err, eventUrl);
184 } else {
185 handleServerEvent(data);
186 }
187 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800188 }
189
190 function toggleBg() {
191 var vis = bgImg.style('visibility');
192 bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
193 }
194
195 function toggleLayout(view) {
196
197 }
198
Simon Hunt99c13842014-11-06 18:23:12 -0800199 function cycleLabels() {
200 labelIdx = (labelIdx === network.deviceLabelCount - 1) ? 0 : labelIdx + 1;
201 network.nodes.forEach(function (d) {
202 var idx = (labelIdx < d.labels.length) ? labelIdx : 0,
203 node = d3.select('#' + safeId(d.id)),
204 box;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800205
Simon Hunt99c13842014-11-06 18:23:12 -0800206 node.select('text')
207 .text(d.labels[idx])
208 .style('opacity', 0)
209 .transition()
210 .style('opacity', 1);
211
212 box = adjustRectToFitText(node);
213
214 node.select('rect')
215 .transition()
216 .attr(box);
217
218 node.select('image')
219 .transition()
220 .attr('x', box.x + config.icons.xoff)
221 .attr('y', box.y + config.icons.yoff);
222 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800223 }
224
225 function togglePorts(view) {
226
227 }
228
229 function unpin(view) {
230
231 }
232
233 // ==============================
234 // Radio Button Callbacks
235
Simon Hunt195cb382014-11-03 17:50:51 -0800236 function showAllLayers() {
Simon Hunt142d0032014-11-04 20:13:09 -0800237// network.node.classed('inactive', false);
238// network.link.classed('inactive', false);
239// d3.selectAll('svg .port').classed('inactive', false);
240// d3.selectAll('svg .portText').classed('inactive', false);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800241 // TODO ...
242 console.log('showAllLayers()');
Simon Hunt195cb382014-11-03 17:50:51 -0800243 }
244
245 function showPacketLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800246 showAllLayers();
247 // TODO ...
248 console.log('showPacketLayer()');
Simon Hunt195cb382014-11-03 17:50:51 -0800249 }
250
251 function showOpticalLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800252 showAllLayers();
253 // TODO ...
254 console.log('showOpticalLayer()');
Simon Hunt195cb382014-11-03 17:50:51 -0800255 }
256
Simon Hunt142d0032014-11-04 20:13:09 -0800257 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800258 // Private functions
259
Simon Hunt99c13842014-11-06 18:23:12 -0800260 function safeId(s) {
261 return s.replace(/[^a-z0-9]/gi, '-');
262 }
263
Simon Huntc7ee0662014-11-05 16:44:37 -0800264 // set the size of the given element to that of the view (reduced if padded)
265 function setSize(el, view, pad) {
266 var padding = pad ? pad * 2 : 0;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800267 el.attr({
Simon Huntc7ee0662014-11-05 16:44:37 -0800268 width: view.width() - padding,
269 height: view.height() - padding
Simon Hunt934c3ce2014-11-05 11:45:07 -0800270 });
271 }
272
Simon Hunt99c13842014-11-06 18:23:12 -0800273 function establishWebSocket() {
274 // TODO: establish a real web-socket
275 // NOTE, for now, we are using the 'Q' key to artificially inject
276 // "events" from the server.
277 }
Simon Hunt934c3ce2014-11-05 11:45:07 -0800278
Simon Hunt99c13842014-11-06 18:23:12 -0800279 // ==============================
280 // Event handlers for server-pushed events
281
282 var eventDispatch = {
283 addDevice: addDevice,
284 updateDevice: updateDevice,
285 removeDevice: removeDevice,
286 addLink: addLink
287 };
288
289 function addDevice(data) {
290 var device = data.payload,
291 node = createDeviceNode(device);
292 note('addDevice', device.id);
293
294 network.nodes.push(node);
295 network.lookup[node.id] = node;
296 updateNodes();
297 network.force.start();
298 }
299
300 function updateDevice(data) {
301 var device = data.payload;
302 note('updateDevice', device.id);
303
304 }
305
306 function removeDevice(data) {
307 var device = data.payload;
308 note('removeDevice', device.id);
309
310 }
311
312 function addLink(data) {
313 var link = data.payload,
314 lnk = createLink(link);
315
316 if (lnk) {
317 note('addLink', lnk.id);
318
319 network.links.push(lnk);
320 updateLinks();
321 network.force.start();
322 }
323 }
324
325 // ....
326
327 function unknownEvent(data) {
328 // TODO: use dialog, not alert
329 alert('Unknown event type: "' + data.event + '"');
330 }
331
332 function handleServerEvent(data) {
333 var fn = eventDispatch[data.event] || unknownEvent;
334 fn(data);
335 }
336
337 // ==============================
338 // force layout modification functions
339
340 function translate(x, y) {
341 return 'translate(' + x + ',' + y + ')';
342 }
343
344 function createLink(link) {
345 var type = link.type,
346 src = link.src,
347 dst = link.dst,
348 w = link.linkWidth,
349 srcNode = network.lookup[src],
350 dstNode = network.lookup[dst],
351 lnk;
352
353 if (!(srcNode && dstNode)) {
354 alert('nodes not on map');
355 return null;
356 }
357
358 lnk = {
359 id: safeId(src) + '~' + safeId(dst),
360 source: srcNode,
361 target: dstNode,
362 class: 'link',
363 svgClass: type ? 'link ' + type : 'link',
364 x1: srcNode.x,
365 y1: srcNode.y,
366 x2: dstNode.x,
367 y2: dstNode.y,
368 width: w
369 };
370 return lnk;
371 }
372
373 function updateLinks() {
374 link = linkG.selectAll('.link')
375 .data(network.links, function (d) { return d.id; });
376
377 // operate on existing links, if necessary
378 // link .foo() .bar() ...
379
380 // operate on entering links:
381 var entering = link.enter()
382 .append('line')
383 .attr({
384 id: function (d) { return d.id; },
385 class: function (d) { return d.svgClass; },
386 x1: function (d) { return d.x1; },
387 y1: function (d) { return d.y1; },
388 x2: function (d) { return d.x2; },
389 y2: function (d) { return d.y2; },
390 stroke: '#66f',
391 'stroke-width': 10
392 })
393 .transition().duration(1000)
394 .attr({
395 'stroke-width': function (d) { return d.width; },
396 stroke: '#666' // TODO: remove explicit stroke, rather...
397 });
398
399 // augment links
400 // TODO: add src/dst port labels etc.
401
402 }
403
404 function createDeviceNode(device) {
405 // start with the object as is
406 var node = device,
407 type = device.type;
408
409 // Augment as needed...
410 node.class = 'device';
411 node.svgClass = type ? 'node device ' + type : 'node device';
412 positionNode(node);
413
414 // cache label array length
415 network.deviceLabelCount = device.labels.length;
416
417 return node;
418 }
419
420 function positionNode(node) {
421 var meta = node.metaUi,
422 x = 0,
423 y = 0;
424
425 if (meta) {
426 x = meta.x;
427 y = meta.y;
428 }
429 if (x && y) {
430 node.fixed = true;
431 }
432 node.x = x || network.view.width() / 2;
433 node.y = y || network.view.height() / 2;
434 }
435
436
437 function iconUrl(d) {
438 return 'img/' + d.type + '.png';
439 }
440
441 // returns the newly computed bounding box of the rectangle
442 function adjustRectToFitText(n) {
443 var text = n.select('text'),
444 box = text.node().getBBox(),
445 lab = config.labels;
446
447 text.attr('text-anchor', 'middle')
448 .attr('y', '-0.8em')
449 .attr('x', lab.imgPad/2);
450
451 // translate the bbox so that it is centered on [x,y]
452 box.x = -box.width / 2;
453 box.y = -box.height / 2;
454
455 // add padding
456 box.x -= (lab.padLR + lab.imgPad/2);
457 box.width += lab.padLR * 2 + lab.imgPad;
458 box.y -= lab.padTB;
459 box.height += lab.padTB * 2;
460
461 return box;
462 }
463
464 function updateNodes() {
465 node = nodeG.selectAll('.node')
466 .data(network.nodes, function (d) { return d.id; });
467
468 // operate on existing nodes, if necessary
469 //node .foo() .bar() ...
470
471 // operate on entering nodes:
472 var entering = node.enter()
473 .append('g')
474 .attr({
475 id: function (d) { return safeId(d.id); },
476 class: function (d) { return d.svgClass; },
477 transform: function (d) { return translate(d.x, d.y); },
478 opacity: 0
479 })
480 //.call(network.drag)
481 //.on('mouseover', function (d) {})
482 //.on('mouseover', function (d) {})
483 .transition()
484 .attr('opacity', 1);
485
486 // augment device nodes...
487 entering.filter('.device').each(function (d) {
488 var node = d3.select(this),
489 icon = iconUrl(d),
490 idx = (labelIdx < d.labels.length) ? labelIdx : 0,
491 box;
492
493 node.append('rect')
494 .attr({
495 'rx': 5,
496 'ry': 5
497 });
498
499 node.append('text')
500 .text(d.labels[idx])
501 .attr('dy', '1.1em');
502
503 box = adjustRectToFitText(node);
504
505 node.select('rect')
506 .attr(box);
507
508 if (icon) {
509 var cfg = config.icons;
510 node.append('svg:image')
511 .attr({
512 x: box.x + config.icons.xoff,
513 y: box.y + config.icons.yoff,
514 width: cfg.w,
515 height: cfg.h,
516 'xlink:href': icon
517 });
518 }
519
520 // debug function to show the modelled x,y coordinates of nodes...
521 if (debug('showNodeXY')) {
522 node.select('rect').attr('fill-opacity', 0.5);
523 node.append('circle')
524 .attr({
525 class: 'debug',
526 cx: 0,
527 cy: 0,
528 r: '3px'
529 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800530 }
531 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800532
Simon Huntc7ee0662014-11-05 16:44:37 -0800533
Simon Hunt99c13842014-11-06 18:23:12 -0800534 // operate on both existing and new nodes, if necessary
535 //node .foo() .bar() ...
Simon Huntc7ee0662014-11-05 16:44:37 -0800536
Simon Hunt99c13842014-11-06 18:23:12 -0800537 // operate on exiting nodes:
538 // TODO: figure out how to remove the node 'g' AND its children
539 node.exit()
540 .transition()
541 .duration(750)
542 .attr({
543 opacity: 0,
544 cx: 0,
545 cy: 0,
546 r: 0
547 })
548 .remove();
Simon Huntc7ee0662014-11-05 16:44:37 -0800549 }
550
551
552 function tick() {
553 node.attr({
Simon Hunt99c13842014-11-06 18:23:12 -0800554 transform: function (d) { return translate(d.x, d.y); }
Simon Huntc7ee0662014-11-05 16:44:37 -0800555 });
556
557 link.attr({
558 x1: function (d) { return d.source.x; },
559 y1: function (d) { return d.source.y; },
560 x2: function (d) { return d.target.x; },
561 y2: function (d) { return d.target.y; }
562 });
563 }
Simon Hunt934c3ce2014-11-05 11:45:07 -0800564
565 // ==============================
Simon Hunt142d0032014-11-04 20:13:09 -0800566 // View life-cycle callbacks
Simon Hunt195cb382014-11-03 17:50:51 -0800567
Simon Hunt142d0032014-11-04 20:13:09 -0800568 function preload(view, ctx) {
569 var w = view.width(),
570 h = view.height(),
571 idBg = view.uid('bg'),
Simon Huntc7ee0662014-11-05 16:44:37 -0800572 showBg = config.options.showBackground ? 'visible' : 'hidden',
573 fcfg = config.force,
574 fpad = fcfg.pad,
575 forceDim = [w - 2*fpad, h - 2*fpad];
Simon Hunt195cb382014-11-03 17:50:51 -0800576
Simon Hunt142d0032014-11-04 20:13:09 -0800577 // NOTE: view.$div is a D3 selection of the view's div
578 svg = view.$div.append('svg');
Simon Hunt934c3ce2014-11-05 11:45:07 -0800579 setSize(svg, view);
580
Simon Hunt142d0032014-11-04 20:13:09 -0800581 // load the background image
582 bgImg = svg.append('svg:image')
Simon Hunt195cb382014-11-03 17:50:51 -0800583 .attr({
Simon Hunt142d0032014-11-04 20:13:09 -0800584 id: idBg,
585 width: w,
586 height: h,
Simon Hunt195cb382014-11-03 17:50:51 -0800587 'xlink:href': config.backgroundUrl
588 })
Simon Hunt142d0032014-11-04 20:13:09 -0800589 .style({
590 visibility: showBg
Simon Hunt195cb382014-11-03 17:50:51 -0800591 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800592
593 // group for the topology
594 topoG = svg.append('g')
595 .attr('transform', fcfg.translate());
596
597 // subgroups for links and nodes
598 linkG = topoG.append('g').attr('id', 'links');
599 nodeG = topoG.append('g').attr('id', 'nodes');
600
601 // selection of nodes and links
602 link = linkG.selectAll('.link');
603 node = nodeG.selectAll('.node');
604
Simon Hunt99c13842014-11-06 18:23:12 -0800605 function ldist(d) {
606 return fcfg.linkDistance[d.class] || 150;
607 }
608 function lstrg(d) {
609 return fcfg.linkStrength[d.class] || 1;
610 }
611 function lchrg(d) {
612 return fcfg.charge[d.class] || -200;
613 }
614
Simon Huntc7ee0662014-11-05 16:44:37 -0800615 // set up the force layout
616 network.force = d3.layout.force()
617 .size(forceDim)
618 .nodes(network.nodes)
619 .links(network.links)
Simon Hunt99c13842014-11-06 18:23:12 -0800620 .charge(lchrg)
621 .linkDistance(ldist)
622 .linkStrength(lstrg)
Simon Huntc7ee0662014-11-05 16:44:37 -0800623 .on('tick', tick);
Simon Hunt195cb382014-11-03 17:50:51 -0800624 }
625
626
Simon Hunt142d0032014-11-04 20:13:09 -0800627 function load(view, ctx) {
Simon Hunt99c13842014-11-06 18:23:12 -0800628 // cache the view token, so network topo functions can access it
629 network.view = view;
630
631 // set our radio buttons and key bindings
Simon Hunt934c3ce2014-11-05 11:45:07 -0800632 view.setRadio(btnSet);
633 view.setKeys(keyDispatch);
Simon Hunt195cb382014-11-03 17:50:51 -0800634
Simon Hunt99c13842014-11-06 18:23:12 -0800635 establishWebSocket();
Simon Hunt195cb382014-11-03 17:50:51 -0800636 }
637
Simon Hunt142d0032014-11-04 20:13:09 -0800638 function resize(view, ctx) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800639 setSize(svg, view);
640 setSize(bgImg, view);
Simon Hunt99c13842014-11-06 18:23:12 -0800641
642 // TODO: hook to recompute layout, perhaps? work with zoom/pan code
643 // adjust force layout size
Simon Hunt142d0032014-11-04 20:13:09 -0800644 }
645
646
647 // ==============================
648 // View registration
Simon Hunt195cb382014-11-03 17:50:51 -0800649
Simon Hunt25248912014-11-04 11:25:48 -0800650 onos.ui.addView('topo', {
Simon Hunt142d0032014-11-04 20:13:09 -0800651 preload: preload,
652 load: load,
653 resize: resize
Simon Hunt195cb382014-11-03 17:50:51 -0800654 });
655
Simon Hunt195cb382014-11-03 17:50:51 -0800656}(ONOS));