blob: 75fb99b8ee9454adb4dd55c5c0c6fb32aead8a36 [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 Hunt1a9eff92014-11-07 11:06:34 -080026 // shorter names for library APIs
27 var d3u = onos.lib.d3util;
28
Simon Hunt195cb382014-11-03 17:50:51 -080029 // configuration data
30 var config = {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -080031 useLiveData: true,
Simon Hunt195cb382014-11-03 17:50:51 -080032 debugOn: false,
33 debug: {
Simon Hunt99c13842014-11-06 18:23:12 -080034 showNodeXY: true,
35 showKeyHandler: false
Simon Hunt195cb382014-11-03 17:50:51 -080036 },
37 options: {
38 layering: true,
39 collisionPrevention: true,
Simon Hunt142d0032014-11-04 20:13:09 -080040 showBackground: true
Simon Hunt195cb382014-11-03 17:50:51 -080041 },
42 backgroundUrl: 'img/us-map.png',
Thomas Vachuska7d638d32014-11-07 10:24:43 -080043 webSockUrl: 'ws/topology',
Simon Hunt195cb382014-11-03 17:50:51 -080044 data: {
45 live: {
46 jsonUrl: 'rs/topology/graph',
47 detailPrefix: 'rs/topology/graph/',
48 detailSuffix: ''
49 },
50 fake: {
51 jsonUrl: 'json/network2.json',
52 detailPrefix: 'json/',
53 detailSuffix: '.json'
54 }
55 },
Simon Hunt99c13842014-11-06 18:23:12 -080056 labels: {
57 imgPad: 16,
58 padLR: 4,
59 padTB: 3,
60 marginLR: 3,
61 marginTB: 2,
62 port: {
63 gap: 3,
64 width: 18,
65 height: 14
66 }
67 },
Simon Hunt1a9eff92014-11-07 11:06:34 -080068 topo: {
69 linkInColor: '#66f',
70 linkInWidth: 14
71 },
Simon Hunt99c13842014-11-06 18:23:12 -080072 icons: {
73 w: 28,
74 h: 28,
75 xoff: -12,
76 yoff: -8
77 },
Simon Hunt195cb382014-11-03 17:50:51 -080078 iconUrl: {
79 device: 'img/device.png',
80 host: 'img/host.png',
81 pkt: 'img/pkt.png',
82 opt: 'img/opt.png'
83 },
Simon Hunt195cb382014-11-03 17:50:51 -080084 force: {
Simon Huntc7ee0662014-11-05 16:44:37 -080085 note: 'node.class or link.class is used to differentiate',
86 linkDistance: {
87 infra: 200,
88 host: 40
89 },
90 linkStrength: {
91 infra: 1.0,
92 host: 1.0
93 },
94 charge: {
95 device: -400,
96 host: -100
97 },
98 pad: 20,
Simon Hunt195cb382014-11-03 17:50:51 -080099 translate: function() {
100 return 'translate(' +
Simon Huntc7ee0662014-11-05 16:44:37 -0800101 config.force.pad + ',' +
102 config.force.pad + ')';
Simon Hunt195cb382014-11-03 17:50:51 -0800103 }
Simon Hunt142d0032014-11-04 20:13:09 -0800104 }
Simon Hunt195cb382014-11-03 17:50:51 -0800105 };
106
Simon Hunt142d0032014-11-04 20:13:09 -0800107 // radio buttons
108 var btnSet = [
Simon Hunt934c3ce2014-11-05 11:45:07 -0800109 { text: 'All Layers', cb: showAllLayers },
110 { text: 'Packet Only', cb: showPacketLayer },
111 { text: 'Optical Only', cb: showOpticalLayer }
112 ];
113
114 // key bindings
115 var keyDispatch = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800116 M: testMe, // TODO: remove (testing only)
Simon Hunt50128c02014-11-08 13:36:15 -0800117 S: injectStartupEvents, // TODO: remove (testing only)
118 space: injectTestEvent, // TODO: remove (testing only)
Simon Hunt99c13842014-11-06 18:23:12 -0800119
Thomas Vachuska65368e32014-11-08 16:10:20 -0800120 B: toggleBg, // TODO: do we really need this?
Simon Hunt934c3ce2014-11-05 11:45:07 -0800121 L: cycleLabels,
122 P: togglePorts,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800123 U: unpin,
124
125 X: requestPath
Simon Hunt934c3ce2014-11-05 11:45:07 -0800126 };
Simon Hunt142d0032014-11-04 20:13:09 -0800127
Simon Hunt195cb382014-11-03 17:50:51 -0800128 // state variables
Simon Hunt99c13842014-11-06 18:23:12 -0800129 var network = {
Simon Hunt50128c02014-11-08 13:36:15 -0800130 view: null, // view token reference
Simon Hunt99c13842014-11-06 18:23:12 -0800131 nodes: [],
132 links: [],
133 lookup: {}
134 },
Simon Hunt56d51852014-11-09 13:03:35 -0800135 scenario = {
136 evDir: 'json/ev/',
137 evScenario: '/scenario.json',
138 evPrefix: '/ev_',
139 evOnos: '_onos.json',
140 evUi: '_ui.json',
141 ctx: null,
142 params: {},
143 evNumber: 0,
144 view: null,
145 debug: false
146 },
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800147 webSock,
Simon Hunt56d51852014-11-09 13:03:35 -0800148 deviceLabelIndex = 0,
149 hostLabelIndex = 0,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800150
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800151 selectOrder = [],
152 selections = {},
153
Simon Hunt195cb382014-11-03 17:50:51 -0800154 highlighted = null,
155 hovered = null,
156 viewMode = 'showAll',
157 portLabelsOn = false;
158
Simon Hunt934c3ce2014-11-05 11:45:07 -0800159 // D3 selections
160 var svg,
161 bgImg,
Simon Huntc7ee0662014-11-05 16:44:37 -0800162 topoG,
163 nodeG,
164 linkG,
165 node,
166 link;
Simon Hunt195cb382014-11-03 17:50:51 -0800167
Simon Hunt142d0032014-11-04 20:13:09 -0800168 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800169 // For Debugging / Development
Simon Hunt195cb382014-11-03 17:50:51 -0800170
Simon Hunt99c13842014-11-06 18:23:12 -0800171 function note(label, msg) {
172 console.log('NOTE: ' + label + ': ' + msg);
Simon Hunt195cb382014-11-03 17:50:51 -0800173 }
174
Simon Hunt99c13842014-11-06 18:23:12 -0800175 function debug(what) {
176 return config.debugOn && config.debug[what];
Simon Hunt934c3ce2014-11-05 11:45:07 -0800177 }
178
Simon Hunt99c13842014-11-06 18:23:12 -0800179
Simon Hunt934c3ce2014-11-05 11:45:07 -0800180 // ==============================
181 // Key Callbacks
182
Simon Hunt99c13842014-11-06 18:23:12 -0800183 function testMe(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800184 view.alert('test');
Simon Hunt99c13842014-11-06 18:23:12 -0800185 }
186
Simon Hunt56d51852014-11-09 13:03:35 -0800187 function abortIfLive() {
Simon Hunt50128c02014-11-08 13:36:15 -0800188 if (config.useLiveData) {
Simon Hunt56d51852014-11-09 13:03:35 -0800189 scenario.view.alert("Sorry, currently using live data..");
190 return true;
Simon Hunt50128c02014-11-08 13:36:15 -0800191 }
Simon Hunt56d51852014-11-09 13:03:35 -0800192 return false;
193 }
Simon Hunt50128c02014-11-08 13:36:15 -0800194
Simon Hunt56d51852014-11-09 13:03:35 -0800195 function testDebug(msg) {
196 if (scenario.debug) {
197 scenario.view.alert(msg);
198 }
199 }
Simon Hunt99c13842014-11-06 18:23:12 -0800200
Simon Hunt56d51852014-11-09 13:03:35 -0800201 function injectTestEvent(view) {
202 if (abortIfLive()) { return; }
203 var sc = scenario,
204 evn = ++sc.evNumber,
205 pfx = sc.evDir + sc.ctx + sc.evPrefix + evn,
206 onosUrl = pfx + sc.evOnos,
207 uiUrl = pfx + sc.evUi;
208
209 tryOnosEvent(onosUrl, uiUrl);
210 }
211
212 // TODO: tryOnosEvent/tryUiEvent folded into recursive function.
213 function tryOnosEvent(onosUrl, uiUrl) {
214 var v = scenario.view;
215 d3.json(onosUrl, function(err, data) {
Simon Hunt99c13842014-11-06 18:23:12 -0800216 if (err) {
Simon Hunt56d51852014-11-09 13:03:35 -0800217 if (err.status === 404) {
218 tryUiEvent(uiUrl);
219 } else {
220 v.alert('non-404 error:\n\n' + onosUrl + '\n\n' + err);
221 }
Simon Hunt99c13842014-11-06 18:23:12 -0800222 } else {
Simon Hunt56d51852014-11-09 13:03:35 -0800223 testDebug('loaded: ' + onosUrl);
Simon Hunt99c13842014-11-06 18:23:12 -0800224 handleServerEvent(data);
225 }
226 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800227 }
228
Simon Hunt56d51852014-11-09 13:03:35 -0800229 function tryUiEvent(uiUrl) {
230 var v = scenario.view;
231 d3.json(uiUrl, function(err, data) {
232 if (err) {
233 v.alert('Error:\n\n' + uiUrl + '\n\n' +
234 err.status + ': ' + err.statusText);
235 } else {
236 testDebug('loaded: ' + uiUrl);
237 handleUiEvent(data);
238 }
239 });
240 }
Simon Hunt50128c02014-11-08 13:36:15 -0800241
Simon Hunt56d51852014-11-09 13:03:35 -0800242 function handleUiEvent(data) {
243 testDebug('handleUiEvent(): ' + data.event);
244 // TODO:
245 }
246
247 function injectStartupEvents(view) {
248 var last = scenario.params.lastAuto || 0;
249 if (abortIfLive()) { return; }
250
251 while (scenario.evNumber < last) {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800252 injectTestEvent(view);
253 }
254 }
255
Simon Hunt934c3ce2014-11-05 11:45:07 -0800256 function toggleBg() {
257 var vis = bgImg.style('visibility');
258 bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
259 }
260
Simon Hunt99c13842014-11-06 18:23:12 -0800261 function cycleLabels() {
Simon Hunt56d51852014-11-09 13:03:35 -0800262 deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1) ? 0 : deviceLabelIndex + 1;
Simon Hunt5f36d342014-11-08 21:33:14 -0800263
264 function niceLabel(label) {
265 return (label && label.trim()) ? label : '.';
266 }
267
Simon Hunt99c13842014-11-06 18:23:12 -0800268 network.nodes.forEach(function (d) {
Simon Hunt56d51852014-11-09 13:03:35 -0800269 if (d.class !== 'device') { return; }
270
271 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0,
Simon Hunt99c13842014-11-06 18:23:12 -0800272 node = d3.select('#' + safeId(d.id)),
273 box;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800274
Simon Hunt99c13842014-11-06 18:23:12 -0800275 node.select('text')
Simon Hunt5f36d342014-11-08 21:33:14 -0800276 .text(niceLabel(d.labels[idx]))
Simon Hunt99c13842014-11-06 18:23:12 -0800277 .style('opacity', 0)
278 .transition()
279 .style('opacity', 1);
280
281 box = adjustRectToFitText(node);
282
283 node.select('rect')
284 .transition()
285 .attr(box);
286
287 node.select('image')
288 .transition()
289 .attr('x', box.x + config.icons.xoff)
290 .attr('y', box.y + config.icons.yoff);
291 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800292 }
293
294 function togglePorts(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800295 view.alert('togglePorts() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800296 }
297
298 function unpin(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800299 view.alert('unpin() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800300 }
301
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800302 function requestPath(view) {
303 var payload = {
304 one: selections[selectOrder[0]].obj.id,
305 two: selections[selectOrder[1]].obj.id
306 }
307 sendMessage('requestPath', payload);
308 }
309
Simon Hunt934c3ce2014-11-05 11:45:07 -0800310 // ==============================
311 // Radio Button Callbacks
312
Simon Hunt195cb382014-11-03 17:50:51 -0800313 function showAllLayers() {
Simon Hunt142d0032014-11-04 20:13:09 -0800314// network.node.classed('inactive', false);
315// network.link.classed('inactive', false);
316// d3.selectAll('svg .port').classed('inactive', false);
317// d3.selectAll('svg .portText').classed('inactive', false);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800318 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800319 network.view.alert('showAllLayers() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800320 }
321
322 function showPacketLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800323 showAllLayers();
324 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800325 network.view.alert('showPacketLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800326 }
327
328 function showOpticalLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800329 showAllLayers();
330 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800331 network.view.alert('showOpticalLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800332 }
333
Simon Hunt142d0032014-11-04 20:13:09 -0800334 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800335 // Private functions
336
Simon Hunt99c13842014-11-06 18:23:12 -0800337 function safeId(s) {
338 return s.replace(/[^a-z0-9]/gi, '-');
339 }
340
Simon Huntc7ee0662014-11-05 16:44:37 -0800341 // set the size of the given element to that of the view (reduced if padded)
342 function setSize(el, view, pad) {
343 var padding = pad ? pad * 2 : 0;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800344 el.attr({
Simon Huntc7ee0662014-11-05 16:44:37 -0800345 width: view.width() - padding,
346 height: view.height() - padding
Simon Hunt934c3ce2014-11-05 11:45:07 -0800347 });
348 }
349
Simon Hunt934c3ce2014-11-05 11:45:07 -0800350
Simon Hunt99c13842014-11-06 18:23:12 -0800351 // ==============================
352 // Event handlers for server-pushed events
353
354 var eventDispatch = {
355 addDevice: addDevice,
Simon Hunt56d51852014-11-09 13:03:35 -0800356 updateDevice: stillToImplement,
357 removeDevice: stillToImplement,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800358 addLink: addLink,
Simon Hunt56d51852014-11-09 13:03:35 -0800359 updateLink: stillToImplement,
360 removeLink: stillToImplement,
361 addHost: addHost,
362 updateHost: stillToImplement,
363 removeHost: stillToImplement,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800364 showPath: showPath
Simon Hunt99c13842014-11-06 18:23:12 -0800365 };
366
367 function addDevice(data) {
368 var device = data.payload,
369 node = createDeviceNode(device);
370 note('addDevice', device.id);
371
372 network.nodes.push(node);
373 network.lookup[node.id] = node;
374 updateNodes();
375 network.force.start();
376 }
377
Simon Hunt99c13842014-11-06 18:23:12 -0800378 function addLink(data) {
379 var link = data.payload,
380 lnk = createLink(link);
381
382 if (lnk) {
383 note('addLink', lnk.id);
384
385 network.links.push(lnk);
386 updateLinks();
387 network.force.start();
388 }
389 }
390
Simon Hunt56d51852014-11-09 13:03:35 -0800391 function addHost(data) {
392 var host = data.payload,
393 node = createHostNode(host),
394 lnk;
395
396 note('addHost', node.id);
397 network.nodes.push(node);
398 network.lookup[host.id] = node;
399 updateNodes();
400
401 lnk = createHostLink(host);
402 if (lnk) {
403 network.links.push(lnk);
404 updateLinks();
405 }
406 network.force.start();
407 }
408
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800409 function showPath(data) {
410 network.view.alert(data.event + "\n" + data.payload.links.length);
411 }
412
Simon Hunt56d51852014-11-09 13:03:35 -0800413 // ...............................
414
415 function stillToImplement(data) {
416 var p = data.payload;
417 note(data.event, p.id);
418 network.view.alert('Not yet implemented: "' + data.event + '"');
419 }
Simon Hunt99c13842014-11-06 18:23:12 -0800420
421 function unknownEvent(data) {
Simon Hunt50128c02014-11-08 13:36:15 -0800422 network.view.alert('Unknown event type: "' + data.event + '"');
Simon Hunt99c13842014-11-06 18:23:12 -0800423 }
424
425 function handleServerEvent(data) {
426 var fn = eventDispatch[data.event] || unknownEvent;
427 fn(data);
428 }
429
430 // ==============================
431 // force layout modification functions
432
433 function translate(x, y) {
434 return 'translate(' + x + ',' + y + ')';
435 }
436
Simon Hunt56d51852014-11-09 13:03:35 -0800437 function createHostLink(host) {
438 var src = host.id,
439 dst = host.cp.device,
440 srcNode = network.lookup[src],
441 dstNode = network.lookup[dst],
442 lnk;
443
444 if (!dstNode) {
445 // TODO: send warning message back to server on websocket
446 network.view.alert('switch not on map for link\n\n' +
447 'src = ' + src + '\ndst = ' + dst);
448 return null;
449 }
450
451 lnk = {
452 id: safeId(src) + '~' + safeId(dst),
453 source: srcNode,
454 target: dstNode,
455 class: 'link',
456 svgClass: 'link hostLink',
457 x1: srcNode.x,
458 y1: srcNode.y,
459 x2: dstNode.x,
460 y2: dstNode.y,
461 width: 1
462 };
463 return lnk;
464 }
465
Simon Hunt99c13842014-11-06 18:23:12 -0800466 function createLink(link) {
467 var type = link.type,
468 src = link.src,
469 dst = link.dst,
470 w = link.linkWidth,
471 srcNode = network.lookup[src],
472 dstNode = network.lookup[dst],
473 lnk;
474
475 if (!(srcNode && dstNode)) {
Simon Hunt50128c02014-11-08 13:36:15 -0800476 // TODO: send warning message back to server on websocket
477 network.view.alert('nodes not on map for link\n\n' +
478 'src = ' + src + '\ndst = ' + dst);
Simon Hunt99c13842014-11-06 18:23:12 -0800479 return null;
480 }
481
482 lnk = {
483 id: safeId(src) + '~' + safeId(dst),
484 source: srcNode,
485 target: dstNode,
486 class: 'link',
487 svgClass: type ? 'link ' + type : 'link',
488 x1: srcNode.x,
489 y1: srcNode.y,
490 x2: dstNode.x,
491 y2: dstNode.y,
492 width: w
493 };
494 return lnk;
495 }
496
Simon Hunt1a9eff92014-11-07 11:06:34 -0800497 function linkWidth(w) {
498 // w is number of links between nodes. Scale appropriately.
Simon Hunt50128c02014-11-08 13:36:15 -0800499 // TODO: use a d3.scale (linear, log, ... ?)
Simon Hunt1a9eff92014-11-07 11:06:34 -0800500 return w * 1.2;
501 }
502
Simon Hunt99c13842014-11-06 18:23:12 -0800503 function updateLinks() {
504 link = linkG.selectAll('.link')
505 .data(network.links, function (d) { return d.id; });
506
507 // operate on existing links, if necessary
508 // link .foo() .bar() ...
509
510 // operate on entering links:
511 var entering = link.enter()
512 .append('line')
513 .attr({
514 id: function (d) { return d.id; },
515 class: function (d) { return d.svgClass; },
516 x1: function (d) { return d.x1; },
517 y1: function (d) { return d.y1; },
518 x2: function (d) { return d.x2; },
519 y2: function (d) { return d.y2; },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800520 stroke: config.topo.linkInColor,
521 'stroke-width': config.topo.linkInWidth
Simon Hunt99c13842014-11-06 18:23:12 -0800522 })
523 .transition().duration(1000)
524 .attr({
Simon Hunt1a9eff92014-11-07 11:06:34 -0800525 'stroke-width': function (d) { return linkWidth(d.width); },
Simon Hunt99c13842014-11-06 18:23:12 -0800526 stroke: '#666' // TODO: remove explicit stroke, rather...
527 });
528
529 // augment links
530 // TODO: add src/dst port labels etc.
531
532 }
533
534 function createDeviceNode(device) {
535 // start with the object as is
536 var node = device,
537 type = device.type;
538
539 // Augment as needed...
540 node.class = 'device';
541 node.svgClass = type ? 'node device ' + type : 'node device';
542 positionNode(node);
543
544 // cache label array length
545 network.deviceLabelCount = device.labels.length;
546
547 return node;
548 }
549
Simon Hunt56d51852014-11-09 13:03:35 -0800550 function createHostNode(host) {
551 // start with the object as is
552 var node = host;
553
554 // Augment as needed...
555 node.class = 'host';
556 node.svgClass = 'node host';
557 // TODO: consider placing near its switch, if [x,y] not defined
558 positionNode(node);
559
560 // cache label array length
561 network.hostLabelCount = host.labels.length;
562
563 return node;
564 }
565
Simon Hunt99c13842014-11-06 18:23:12 -0800566 function positionNode(node) {
567 var meta = node.metaUi,
568 x = 0,
569 y = 0;
570
571 if (meta) {
572 x = meta.x;
573 y = meta.y;
574 }
575 if (x && y) {
576 node.fixed = true;
577 }
578 node.x = x || network.view.width() / 2;
579 node.y = y || network.view.height() / 2;
580 }
581
582
583 function iconUrl(d) {
584 return 'img/' + d.type + '.png';
585 }
586
587 // returns the newly computed bounding box of the rectangle
588 function adjustRectToFitText(n) {
589 var text = n.select('text'),
590 box = text.node().getBBox(),
591 lab = config.labels;
592
593 text.attr('text-anchor', 'middle')
594 .attr('y', '-0.8em')
595 .attr('x', lab.imgPad/2);
596
597 // translate the bbox so that it is centered on [x,y]
598 box.x = -box.width / 2;
599 box.y = -box.height / 2;
600
601 // add padding
602 box.x -= (lab.padLR + lab.imgPad/2);
603 box.width += lab.padLR * 2 + lab.imgPad;
604 box.y -= lab.padTB;
605 box.height += lab.padTB * 2;
606
607 return box;
608 }
609
Simon Hunt1a9eff92014-11-07 11:06:34 -0800610 function mkSvgClass(d) {
611 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
612 }
613
Simon Hunt99c13842014-11-06 18:23:12 -0800614 function updateNodes() {
615 node = nodeG.selectAll('.node')
616 .data(network.nodes, function (d) { return d.id; });
617
618 // operate on existing nodes, if necessary
619 //node .foo() .bar() ...
620
621 // operate on entering nodes:
622 var entering = node.enter()
623 .append('g')
624 .attr({
625 id: function (d) { return safeId(d.id); },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800626 class: mkSvgClass,
Simon Hunt99c13842014-11-06 18:23:12 -0800627 transform: function (d) { return translate(d.x, d.y); },
628 opacity: 0
629 })
Simon Hunt1a9eff92014-11-07 11:06:34 -0800630 .call(network.drag)
Simon Hunt99c13842014-11-06 18:23:12 -0800631 //.on('mouseover', function (d) {})
632 //.on('mouseover', function (d) {})
633 .transition()
634 .attr('opacity', 1);
635
636 // augment device nodes...
637 entering.filter('.device').each(function (d) {
638 var node = d3.select(this),
639 icon = iconUrl(d),
Simon Hunt56d51852014-11-09 13:03:35 -0800640 idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0,
Simon Hunt99c13842014-11-06 18:23:12 -0800641 box;
642
643 node.append('rect')
644 .attr({
645 'rx': 5,
646 'ry': 5
647 });
648
649 node.append('text')
650 .text(d.labels[idx])
651 .attr('dy', '1.1em');
652
653 box = adjustRectToFitText(node);
654
655 node.select('rect')
656 .attr(box);
657
658 if (icon) {
659 var cfg = config.icons;
660 node.append('svg:image')
661 .attr({
662 x: box.x + config.icons.xoff,
663 y: box.y + config.icons.yoff,
664 width: cfg.w,
665 height: cfg.h,
666 'xlink:href': icon
667 });
668 }
669
670 // debug function to show the modelled x,y coordinates of nodes...
671 if (debug('showNodeXY')) {
672 node.select('rect').attr('fill-opacity', 0.5);
673 node.append('circle')
674 .attr({
675 class: 'debug',
676 cx: 0,
677 cy: 0,
678 r: '3px'
679 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800680 }
681 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800682
Simon Hunt56d51852014-11-09 13:03:35 -0800683 // augment host nodes...
684 entering.filter('.host').each(function (d) {
685 var node = d3.select(this),
686 idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0,
687 box;
688
689 node.append('circle')
690 .attr('r', 8); // TODO: define host circle radius
691
692 // TODO: are we attaching labels to hosts?
693 node.append('text')
694 .text(d.labels[idx])
695 .attr('dy', '1.1em');
696
697 // debug function to show the modelled x,y coordinates of nodes...
698 if (debug('showNodeXY')) {
699 node.select('circle').attr('fill-opacity', 0.5);
700 node.append('circle')
701 .attr({
702 class: 'debug',
703 cx: 0,
704 cy: 0,
705 r: '3px'
706 });
707 }
708 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800709
Simon Hunt99c13842014-11-06 18:23:12 -0800710 // operate on both existing and new nodes, if necessary
711 //node .foo() .bar() ...
Simon Huntc7ee0662014-11-05 16:44:37 -0800712
Simon Hunt99c13842014-11-06 18:23:12 -0800713 // operate on exiting nodes:
714 // TODO: figure out how to remove the node 'g' AND its children
715 node.exit()
716 .transition()
717 .duration(750)
718 .attr({
719 opacity: 0,
720 cx: 0,
721 cy: 0,
722 r: 0
723 })
724 .remove();
Simon Huntc7ee0662014-11-05 16:44:37 -0800725 }
726
727
728 function tick() {
729 node.attr({
Simon Hunt99c13842014-11-06 18:23:12 -0800730 transform: function (d) { return translate(d.x, d.y); }
Simon Huntc7ee0662014-11-05 16:44:37 -0800731 });
732
733 link.attr({
734 x1: function (d) { return d.source.x; },
735 y1: function (d) { return d.source.y; },
736 x2: function (d) { return d.target.x; },
737 y2: function (d) { return d.target.y; }
738 });
739 }
Simon Hunt934c3ce2014-11-05 11:45:07 -0800740
741 // ==============================
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800742 // Web-Socket for live data
743
744 function webSockUrl() {
745 return document.location.toString()
746 .replace(/\#.*/, '')
747 .replace('http://', 'ws://')
748 .replace('https://', 'wss://')
749 .replace('index2.html', config.webSockUrl);
750 }
751
752 webSock = {
753 ws : null,
754
755 connect : function() {
756 webSock.ws = new WebSocket(webSockUrl());
757
758 webSock.ws.onopen = function() {
759 webSock._send("Hi there!");
760 };
761
762 webSock.ws.onmessage = function(m) {
763 if (m.data) {
764 console.log(m.data);
Thomas Vachuskad472c6e2014-11-07 19:11:05 -0800765 handleServerEvent(JSON.parse(m.data));
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800766 }
767 };
768
769 webSock.ws.onclose = function(m) {
770 webSock.ws = null;
771 };
772 },
773
774 send : function(text) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800775 if (text != null) {
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800776 webSock._send(text);
777 }
778 },
779
780 _send : function(message) {
781 if (webSock.ws) {
782 webSock.ws.send(message);
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800783 } else {
Simon Hunt56d51852014-11-09 13:03:35 -0800784 network.view.alert('no web socket open\n\n' + message);
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800785 }
786 }
787
788 };
789
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800790 var sid = 0;
791
792 function sendMessage(evType, payload) {
793 var toSend = {
794 event: evType,
795 sid: ++sid,
796 payload: payload
797 };
798 webSock.send(JSON.stringify(toSend));
799 }
800
801
802 // ==============================
803 // Selection stuff
804
805 function selectObject(obj, el) {
806 var n,
807 meta = d3.event.sourceEvent.metaKey;
808
809 if (el) {
810 n = d3.select(el);
811 } else {
812 node.each(function(d) {
813 if (d == obj) {
814 n = d3.select(el = this);
815 }
816 });
817 }
818 if (!n) return;
819
820 if (meta && n.classed('selected')) {
821 deselectObject(obj.id);
822 //flyinPane(null);
823 return;
824 }
825
826 if (!meta) {
827 deselectAll();
828 }
829
Simon Hunt5f36d342014-11-08 21:33:14 -0800830 selections[obj.id] = { obj: obj, el : el};
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800831 selectOrder.push(obj.id);
832
833 n.classed('selected', true);
834 //flyinPane(obj);
835 }
836
837 function deselectObject(id) {
838 var obj = selections[id];
839 if (obj) {
840 d3.select(obj.el).classed('selected', false);
841 selections[id] = null;
842 // TODO: use splice to remove element
843 }
844 //flyinPane(null);
845 }
846
847 function deselectAll() {
848 // deselect all nodes in the network...
849 node.classed('selected', false);
850 selections = {};
851 selectOrder = [];
852 //flyinPane(null);
853 }
854
Simon Hunt56d51852014-11-09 13:03:35 -0800855 // TODO: this click handler does not get unloaded when the view does
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800856 $('#view').on('click', function(e) {
857 if (!$(e.target).closest('.node').length) {
858 if (!e.metaKey) {
859 deselectAll();
860 }
861 }
862 });
863
Simon Hunt56d51852014-11-09 13:03:35 -0800864
865 function prepareScenario(view, ctx, dbg) {
866 var sc = scenario,
867 urlSc = sc.evDir + ctx + sc.evScenario;
868
869 if (!ctx) {
870 view.alert("No scenario specified (null ctx)");
871 return;
872 }
873
874 sc.view = view;
875 sc.ctx = ctx;
876 sc.debug = dbg;
877 sc.evNumber = 0;
878
879 d3.json(urlSc, function(err, data) {
880 var p = data && data.params || {};
881 if (err) {
882 view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
883 } else {
884 sc.params = p;
885 view.alert("Scenario loaded: " + ctx + '\n\n' + data.title);
886 }
887 });
888
889 }
890
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800891 // ==============================
Simon Hunt142d0032014-11-04 20:13:09 -0800892 // View life-cycle callbacks
Simon Hunt195cb382014-11-03 17:50:51 -0800893
Simon Hunt142d0032014-11-04 20:13:09 -0800894 function preload(view, ctx) {
895 var w = view.width(),
896 h = view.height(),
897 idBg = view.uid('bg'),
Simon Huntc7ee0662014-11-05 16:44:37 -0800898 showBg = config.options.showBackground ? 'visible' : 'hidden',
899 fcfg = config.force,
900 fpad = fcfg.pad,
901 forceDim = [w - 2*fpad, h - 2*fpad];
Simon Hunt195cb382014-11-03 17:50:51 -0800902
Simon Hunt142d0032014-11-04 20:13:09 -0800903 // NOTE: view.$div is a D3 selection of the view's div
904 svg = view.$div.append('svg');
Simon Hunt934c3ce2014-11-05 11:45:07 -0800905 setSize(svg, view);
906
Simon Hunt1a9eff92014-11-07 11:06:34 -0800907 // add blue glow filter to svg layer
908 d3u.appendGlow(svg);
909
Simon Hunt142d0032014-11-04 20:13:09 -0800910 // load the background image
911 bgImg = svg.append('svg:image')
Simon Hunt195cb382014-11-03 17:50:51 -0800912 .attr({
Simon Hunt142d0032014-11-04 20:13:09 -0800913 id: idBg,
914 width: w,
915 height: h,
Simon Hunt195cb382014-11-03 17:50:51 -0800916 'xlink:href': config.backgroundUrl
917 })
Simon Hunt142d0032014-11-04 20:13:09 -0800918 .style({
919 visibility: showBg
Simon Hunt195cb382014-11-03 17:50:51 -0800920 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800921
922 // group for the topology
923 topoG = svg.append('g')
924 .attr('transform', fcfg.translate());
925
926 // subgroups for links and nodes
927 linkG = topoG.append('g').attr('id', 'links');
928 nodeG = topoG.append('g').attr('id', 'nodes');
929
930 // selection of nodes and links
931 link = linkG.selectAll('.link');
932 node = nodeG.selectAll('.node');
933
Simon Hunt99c13842014-11-06 18:23:12 -0800934 function ldist(d) {
935 return fcfg.linkDistance[d.class] || 150;
936 }
937 function lstrg(d) {
938 return fcfg.linkStrength[d.class] || 1;
939 }
940 function lchrg(d) {
941 return fcfg.charge[d.class] || -200;
942 }
943
Simon Hunt1a9eff92014-11-07 11:06:34 -0800944 function selectCb(d, self) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800945 selectObject(d, self);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800946 }
947
948 function atDragEnd(d, self) {
Simon Hunt56d51852014-11-09 13:03:35 -0800949 // once we've finished moving, pin the node in position
950 d.fixed = true;
951 d3.select(self).classed('fixed', true);
952 if (config.useLiveData) {
953 tellServerCoords(d);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800954 }
955 }
956
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800957 function tellServerCoords(d) {
958 sendMessage('updateMeta', {
959 id: d.id,
960 'class': d.class,
Simon Hunt5f36d342014-11-08 21:33:14 -0800961 x: Math.floor(d.x),
962 y: Math.floor(d.y)
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800963 });
964 }
965
Simon Huntc7ee0662014-11-05 16:44:37 -0800966 // set up the force layout
967 network.force = d3.layout.force()
968 .size(forceDim)
969 .nodes(network.nodes)
970 .links(network.links)
Simon Hunt99c13842014-11-06 18:23:12 -0800971 .charge(lchrg)
972 .linkDistance(ldist)
973 .linkStrength(lstrg)
Simon Huntc7ee0662014-11-05 16:44:37 -0800974 .on('tick', tick);
Simon Hunt195cb382014-11-03 17:50:51 -0800975
Simon Hunt1a9eff92014-11-07 11:06:34 -0800976 network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
977 }
Simon Hunt195cb382014-11-03 17:50:51 -0800978
Simon Hunt56d51852014-11-09 13:03:35 -0800979 function load(view, ctx, flags) {
Simon Hunt99c13842014-11-06 18:23:12 -0800980 // cache the view token, so network topo functions can access it
981 network.view = view;
Simon Hunt56d51852014-11-09 13:03:35 -0800982 config.useLiveData = !flags.local;
983
984 if (!config.useLiveData) {
985 prepareScenario(view, ctx, flags.debug);
986 }
Simon Hunt99c13842014-11-06 18:23:12 -0800987
988 // set our radio buttons and key bindings
Simon Hunt934c3ce2014-11-05 11:45:07 -0800989 view.setRadio(btnSet);
990 view.setKeys(keyDispatch);
Simon Hunt195cb382014-11-03 17:50:51 -0800991
Simon Hunt50128c02014-11-08 13:36:15 -0800992 if (config.useLiveData) {
993 webSock.connect();
994 }
Simon Hunt195cb382014-11-03 17:50:51 -0800995 }
996
Simon Hunt142d0032014-11-04 20:13:09 -0800997 function resize(view, ctx) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800998 setSize(svg, view);
999 setSize(bgImg, view);
Simon Hunt99c13842014-11-06 18:23:12 -08001000
1001 // TODO: hook to recompute layout, perhaps? work with zoom/pan code
1002 // adjust force layout size
Simon Hunt142d0032014-11-04 20:13:09 -08001003 }
1004
1005
1006 // ==============================
1007 // View registration
Simon Hunt195cb382014-11-03 17:50:51 -08001008
Simon Hunt25248912014-11-04 11:25:48 -08001009 onos.ui.addView('topo', {
Simon Hunt142d0032014-11-04 20:13:09 -08001010 preload: preload,
1011 load: load,
1012 resize: resize
Simon Hunt195cb382014-11-03 17:50:51 -08001013 });
1014
Simon Hunt195cb382014-11-03 17:50:51 -08001015}(ONOS));