blob: 18aa917ecc8251782f6797c18e5ba864615b9007 [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);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800386 network.lookup[lnk.id] = lnk;
Simon Hunt99c13842014-11-06 18:23:12 -0800387 updateLinks();
388 network.force.start();
389 }
390 }
391
Simon Hunt56d51852014-11-09 13:03:35 -0800392 function addHost(data) {
393 var host = data.payload,
394 node = createHostNode(host),
395 lnk;
396
397 note('addHost', node.id);
398 network.nodes.push(node);
399 network.lookup[host.id] = node;
400 updateNodes();
401
402 lnk = createHostLink(host);
403 if (lnk) {
404 network.links.push(lnk);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800405 network.lookup[host.ingress] = lnk;
406 network.lookup[host.egress] = lnk;
Simon Hunt56d51852014-11-09 13:03:35 -0800407 updateLinks();
408 }
409 network.force.start();
410 }
411
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800412 function showPath(data) {
Thomas Vachuska4830d392014-11-09 17:09:56 -0800413 var links = data.payload.links,
414 s = [ data.event + "\n" + links.length ];
415 links.forEach(function (d, i) {
416 s.push(d);
417 });
418 network.view.alert(s.join('\n'));
419
420 links.forEach(function (d, i) {
421 var link = network.lookup[d];
422 if (link) {
423 d3.select('#' + link.svgId).classed('showPath', true);
424 }
425 });
426
427 // TODO: add selection-highlite lines to links
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800428 }
429
Simon Hunt56d51852014-11-09 13:03:35 -0800430 // ...............................
431
432 function stillToImplement(data) {
433 var p = data.payload;
434 note(data.event, p.id);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800435 //network.view.alert('Not yet implemented: "' + data.event + '"');
Simon Hunt56d51852014-11-09 13:03:35 -0800436 }
Simon Hunt99c13842014-11-06 18:23:12 -0800437
438 function unknownEvent(data) {
Simon Hunt50128c02014-11-08 13:36:15 -0800439 network.view.alert('Unknown event type: "' + data.event + '"');
Simon Hunt99c13842014-11-06 18:23:12 -0800440 }
441
442 function handleServerEvent(data) {
443 var fn = eventDispatch[data.event] || unknownEvent;
444 fn(data);
445 }
446
447 // ==============================
448 // force layout modification functions
449
450 function translate(x, y) {
451 return 'translate(' + x + ',' + y + ')';
452 }
453
Simon Hunt56d51852014-11-09 13:03:35 -0800454 function createHostLink(host) {
455 var src = host.id,
456 dst = host.cp.device,
Thomas Vachuska4830d392014-11-09 17:09:56 -0800457 id = host.id,
Simon Hunt56d51852014-11-09 13:03:35 -0800458 srcNode = network.lookup[src],
459 dstNode = network.lookup[dst],
460 lnk;
461
462 if (!dstNode) {
463 // TODO: send warning message back to server on websocket
464 network.view.alert('switch not on map for link\n\n' +
465 'src = ' + src + '\ndst = ' + dst);
466 return null;
467 }
468
469 lnk = {
Thomas Vachuska4830d392014-11-09 17:09:56 -0800470 svgId: safeId(src) + '-' + safeId(dst),
471 id: id,
Simon Hunt56d51852014-11-09 13:03:35 -0800472 source: srcNode,
473 target: dstNode,
474 class: 'link',
475 svgClass: 'link hostLink',
476 x1: srcNode.x,
477 y1: srcNode.y,
478 x2: dstNode.x,
479 y2: dstNode.y,
480 width: 1
481 };
482 return lnk;
483 }
484
Simon Hunt99c13842014-11-06 18:23:12 -0800485 function createLink(link) {
486 var type = link.type,
487 src = link.src,
488 dst = link.dst,
Thomas Vachuska4830d392014-11-09 17:09:56 -0800489 id = link.id,
Simon Hunt99c13842014-11-06 18:23:12 -0800490 w = link.linkWidth,
491 srcNode = network.lookup[src],
492 dstNode = network.lookup[dst],
493 lnk;
494
495 if (!(srcNode && dstNode)) {
Simon Hunt50128c02014-11-08 13:36:15 -0800496 // TODO: send warning message back to server on websocket
497 network.view.alert('nodes not on map for link\n\n' +
498 'src = ' + src + '\ndst = ' + dst);
Simon Hunt99c13842014-11-06 18:23:12 -0800499 return null;
500 }
501
502 lnk = {
Thomas Vachuska4830d392014-11-09 17:09:56 -0800503 svgId: safeId(src) + '-' + safeId(dst),
504 id: id,
Simon Hunt99c13842014-11-06 18:23:12 -0800505 source: srcNode,
506 target: dstNode,
507 class: 'link',
508 svgClass: type ? 'link ' + type : 'link',
509 x1: srcNode.x,
510 y1: srcNode.y,
511 x2: dstNode.x,
512 y2: dstNode.y,
513 width: w
514 };
515 return lnk;
516 }
517
Simon Hunt1a9eff92014-11-07 11:06:34 -0800518 function linkWidth(w) {
519 // w is number of links between nodes. Scale appropriately.
Simon Hunt50128c02014-11-08 13:36:15 -0800520 // TODO: use a d3.scale (linear, log, ... ?)
Simon Hunt1a9eff92014-11-07 11:06:34 -0800521 return w * 1.2;
522 }
523
Simon Hunt99c13842014-11-06 18:23:12 -0800524 function updateLinks() {
525 link = linkG.selectAll('.link')
526 .data(network.links, function (d) { return d.id; });
527
528 // operate on existing links, if necessary
529 // link .foo() .bar() ...
530
531 // operate on entering links:
532 var entering = link.enter()
533 .append('line')
534 .attr({
Thomas Vachuska4830d392014-11-09 17:09:56 -0800535 id: function (d) { return d.svgId; },
Simon Hunt99c13842014-11-06 18:23:12 -0800536 class: function (d) { return d.svgClass; },
537 x1: function (d) { return d.x1; },
538 y1: function (d) { return d.y1; },
539 x2: function (d) { return d.x2; },
540 y2: function (d) { return d.y2; },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800541 stroke: config.topo.linkInColor,
542 'stroke-width': config.topo.linkInWidth
Simon Hunt99c13842014-11-06 18:23:12 -0800543 })
544 .transition().duration(1000)
545 .attr({
Simon Hunt1a9eff92014-11-07 11:06:34 -0800546 'stroke-width': function (d) { return linkWidth(d.width); },
Simon Hunt99c13842014-11-06 18:23:12 -0800547 stroke: '#666' // TODO: remove explicit stroke, rather...
548 });
549
550 // augment links
551 // TODO: add src/dst port labels etc.
552
Thomas Vachuska4830d392014-11-09 17:09:56 -0800553
554 // operate on both existing and new links, if necessary
555 //link .foo() .bar() ...
556
557 // operate on exiting links:
558 // TODO: figure out how to remove the node 'g' AND its children
559 link.exit()
560 .transition()
561 .duration(750)
562 .attr({
563 opacity: 0
564 })
565 .remove();
Simon Hunt99c13842014-11-06 18:23:12 -0800566 }
567
568 function createDeviceNode(device) {
569 // start with the object as is
570 var node = device,
571 type = device.type;
572
573 // Augment as needed...
574 node.class = 'device';
575 node.svgClass = type ? 'node device ' + type : 'node device';
576 positionNode(node);
577
578 // cache label array length
579 network.deviceLabelCount = device.labels.length;
580
581 return node;
582 }
583
Simon Hunt56d51852014-11-09 13:03:35 -0800584 function createHostNode(host) {
585 // start with the object as is
586 var node = host;
587
588 // Augment as needed...
589 node.class = 'host';
590 node.svgClass = 'node host';
591 // TODO: consider placing near its switch, if [x,y] not defined
592 positionNode(node);
593
594 // cache label array length
595 network.hostLabelCount = host.labels.length;
596
597 return node;
598 }
599
Simon Hunt99c13842014-11-06 18:23:12 -0800600 function positionNode(node) {
601 var meta = node.metaUi,
602 x = 0,
603 y = 0;
604
605 if (meta) {
606 x = meta.x;
607 y = meta.y;
608 }
609 if (x && y) {
610 node.fixed = true;
611 }
612 node.x = x || network.view.width() / 2;
613 node.y = y || network.view.height() / 2;
614 }
615
616
617 function iconUrl(d) {
618 return 'img/' + d.type + '.png';
619 }
620
621 // returns the newly computed bounding box of the rectangle
622 function adjustRectToFitText(n) {
623 var text = n.select('text'),
624 box = text.node().getBBox(),
625 lab = config.labels;
626
627 text.attr('text-anchor', 'middle')
628 .attr('y', '-0.8em')
629 .attr('x', lab.imgPad/2);
630
631 // translate the bbox so that it is centered on [x,y]
632 box.x = -box.width / 2;
633 box.y = -box.height / 2;
634
635 // add padding
636 box.x -= (lab.padLR + lab.imgPad/2);
637 box.width += lab.padLR * 2 + lab.imgPad;
638 box.y -= lab.padTB;
639 box.height += lab.padTB * 2;
640
641 return box;
642 }
643
Simon Hunt1a9eff92014-11-07 11:06:34 -0800644 function mkSvgClass(d) {
645 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
646 }
647
Simon Hunt99c13842014-11-06 18:23:12 -0800648 function updateNodes() {
649 node = nodeG.selectAll('.node')
650 .data(network.nodes, function (d) { return d.id; });
651
652 // operate on existing nodes, if necessary
653 //node .foo() .bar() ...
654
655 // operate on entering nodes:
656 var entering = node.enter()
657 .append('g')
658 .attr({
659 id: function (d) { return safeId(d.id); },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800660 class: mkSvgClass,
Simon Hunt99c13842014-11-06 18:23:12 -0800661 transform: function (d) { return translate(d.x, d.y); },
662 opacity: 0
663 })
Simon Hunt1a9eff92014-11-07 11:06:34 -0800664 .call(network.drag)
Simon Hunt99c13842014-11-06 18:23:12 -0800665 //.on('mouseover', function (d) {})
666 //.on('mouseover', function (d) {})
667 .transition()
668 .attr('opacity', 1);
669
670 // augment device nodes...
671 entering.filter('.device').each(function (d) {
672 var node = d3.select(this),
673 icon = iconUrl(d),
Simon Hunt56d51852014-11-09 13:03:35 -0800674 idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0,
Simon Hunt99c13842014-11-06 18:23:12 -0800675 box;
676
677 node.append('rect')
678 .attr({
679 'rx': 5,
680 'ry': 5
681 });
682
683 node.append('text')
684 .text(d.labels[idx])
685 .attr('dy', '1.1em');
686
687 box = adjustRectToFitText(node);
688
689 node.select('rect')
690 .attr(box);
691
692 if (icon) {
693 var cfg = config.icons;
694 node.append('svg:image')
695 .attr({
696 x: box.x + config.icons.xoff,
697 y: box.y + config.icons.yoff,
698 width: cfg.w,
699 height: cfg.h,
700 'xlink:href': icon
701 });
702 }
703
704 // debug function to show the modelled x,y coordinates of nodes...
705 if (debug('showNodeXY')) {
706 node.select('rect').attr('fill-opacity', 0.5);
707 node.append('circle')
708 .attr({
709 class: 'debug',
710 cx: 0,
711 cy: 0,
712 r: '3px'
713 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800714 }
715 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800716
Simon Hunt56d51852014-11-09 13:03:35 -0800717 // augment host nodes...
718 entering.filter('.host').each(function (d) {
719 var node = d3.select(this),
720 idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0,
721 box;
722
723 node.append('circle')
724 .attr('r', 8); // TODO: define host circle radius
725
726 // TODO: are we attaching labels to hosts?
727 node.append('text')
728 .text(d.labels[idx])
729 .attr('dy', '1.1em');
730
731 // debug function to show the modelled x,y coordinates of nodes...
732 if (debug('showNodeXY')) {
733 node.select('circle').attr('fill-opacity', 0.5);
734 node.append('circle')
735 .attr({
736 class: 'debug',
737 cx: 0,
738 cy: 0,
739 r: '3px'
740 });
741 }
742 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800743
Simon Hunt99c13842014-11-06 18:23:12 -0800744 // operate on both existing and new nodes, if necessary
745 //node .foo() .bar() ...
Simon Huntc7ee0662014-11-05 16:44:37 -0800746
Simon Hunt99c13842014-11-06 18:23:12 -0800747 // operate on exiting nodes:
748 // TODO: figure out how to remove the node 'g' AND its children
749 node.exit()
750 .transition()
751 .duration(750)
752 .attr({
753 opacity: 0,
754 cx: 0,
755 cy: 0,
756 r: 0
757 })
758 .remove();
Simon Huntc7ee0662014-11-05 16:44:37 -0800759 }
760
761
762 function tick() {
763 node.attr({
Simon Hunt99c13842014-11-06 18:23:12 -0800764 transform: function (d) { return translate(d.x, d.y); }
Simon Huntc7ee0662014-11-05 16:44:37 -0800765 });
766
767 link.attr({
768 x1: function (d) { return d.source.x; },
769 y1: function (d) { return d.source.y; },
770 x2: function (d) { return d.target.x; },
771 y2: function (d) { return d.target.y; }
772 });
773 }
Simon Hunt934c3ce2014-11-05 11:45:07 -0800774
775 // ==============================
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800776 // Web-Socket for live data
777
778 function webSockUrl() {
779 return document.location.toString()
780 .replace(/\#.*/, '')
781 .replace('http://', 'ws://')
782 .replace('https://', 'wss://')
783 .replace('index2.html', config.webSockUrl);
784 }
785
786 webSock = {
787 ws : null,
788
789 connect : function() {
790 webSock.ws = new WebSocket(webSockUrl());
791
792 webSock.ws.onopen = function() {
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800793 };
794
795 webSock.ws.onmessage = function(m) {
796 if (m.data) {
797 console.log(m.data);
Thomas Vachuskad472c6e2014-11-07 19:11:05 -0800798 handleServerEvent(JSON.parse(m.data));
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800799 }
800 };
801
802 webSock.ws.onclose = function(m) {
803 webSock.ws = null;
804 };
805 },
806
807 send : function(text) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800808 if (text != null) {
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800809 webSock._send(text);
810 }
811 },
812
813 _send : function(message) {
814 if (webSock.ws) {
815 webSock.ws.send(message);
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800816 } else {
Simon Hunt56d51852014-11-09 13:03:35 -0800817 network.view.alert('no web socket open\n\n' + message);
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800818 }
819 }
820
821 };
822
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800823 var sid = 0;
824
825 function sendMessage(evType, payload) {
826 var toSend = {
827 event: evType,
828 sid: ++sid,
829 payload: payload
830 };
831 webSock.send(JSON.stringify(toSend));
832 }
833
834
835 // ==============================
836 // Selection stuff
837
838 function selectObject(obj, el) {
839 var n,
840 meta = d3.event.sourceEvent.metaKey;
841
842 if (el) {
843 n = d3.select(el);
844 } else {
845 node.each(function(d) {
846 if (d == obj) {
847 n = d3.select(el = this);
848 }
849 });
850 }
851 if (!n) return;
852
853 if (meta && n.classed('selected')) {
854 deselectObject(obj.id);
855 //flyinPane(null);
856 return;
857 }
858
859 if (!meta) {
860 deselectAll();
861 }
862
Simon Hunt5f36d342014-11-08 21:33:14 -0800863 selections[obj.id] = { obj: obj, el : el};
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800864 selectOrder.push(obj.id);
865
866 n.classed('selected', true);
867 //flyinPane(obj);
868 }
869
870 function deselectObject(id) {
871 var obj = selections[id];
872 if (obj) {
873 d3.select(obj.el).classed('selected', false);
874 selections[id] = null;
875 // TODO: use splice to remove element
876 }
877 //flyinPane(null);
878 }
879
880 function deselectAll() {
881 // deselect all nodes in the network...
882 node.classed('selected', false);
883 selections = {};
884 selectOrder = [];
885 //flyinPane(null);
886 }
887
Simon Hunt56d51852014-11-09 13:03:35 -0800888 // TODO: this click handler does not get unloaded when the view does
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800889 $('#view').on('click', function(e) {
890 if (!$(e.target).closest('.node').length) {
891 if (!e.metaKey) {
892 deselectAll();
893 }
894 }
895 });
896
Simon Hunt56d51852014-11-09 13:03:35 -0800897
898 function prepareScenario(view, ctx, dbg) {
899 var sc = scenario,
900 urlSc = sc.evDir + ctx + sc.evScenario;
901
902 if (!ctx) {
903 view.alert("No scenario specified (null ctx)");
904 return;
905 }
906
907 sc.view = view;
908 sc.ctx = ctx;
909 sc.debug = dbg;
910 sc.evNumber = 0;
911
912 d3.json(urlSc, function(err, data) {
913 var p = data && data.params || {};
914 if (err) {
915 view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
916 } else {
917 sc.params = p;
918 view.alert("Scenario loaded: " + ctx + '\n\n' + data.title);
919 }
920 });
921
922 }
923
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800924 // ==============================
Simon Hunt142d0032014-11-04 20:13:09 -0800925 // View life-cycle callbacks
Simon Hunt195cb382014-11-03 17:50:51 -0800926
Simon Hunt142d0032014-11-04 20:13:09 -0800927 function preload(view, ctx) {
928 var w = view.width(),
929 h = view.height(),
930 idBg = view.uid('bg'),
Simon Huntc7ee0662014-11-05 16:44:37 -0800931 showBg = config.options.showBackground ? 'visible' : 'hidden',
932 fcfg = config.force,
933 fpad = fcfg.pad,
934 forceDim = [w - 2*fpad, h - 2*fpad];
Simon Hunt195cb382014-11-03 17:50:51 -0800935
Simon Hunt142d0032014-11-04 20:13:09 -0800936 // NOTE: view.$div is a D3 selection of the view's div
937 svg = view.$div.append('svg');
Simon Hunt934c3ce2014-11-05 11:45:07 -0800938 setSize(svg, view);
939
Simon Hunt1a9eff92014-11-07 11:06:34 -0800940 // add blue glow filter to svg layer
941 d3u.appendGlow(svg);
942
Simon Hunt142d0032014-11-04 20:13:09 -0800943 // load the background image
944 bgImg = svg.append('svg:image')
Simon Hunt195cb382014-11-03 17:50:51 -0800945 .attr({
Simon Hunt142d0032014-11-04 20:13:09 -0800946 id: idBg,
947 width: w,
948 height: h,
Simon Hunt195cb382014-11-03 17:50:51 -0800949 'xlink:href': config.backgroundUrl
950 })
Simon Hunt142d0032014-11-04 20:13:09 -0800951 .style({
952 visibility: showBg
Simon Hunt195cb382014-11-03 17:50:51 -0800953 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800954
955 // group for the topology
956 topoG = svg.append('g')
957 .attr('transform', fcfg.translate());
958
959 // subgroups for links and nodes
960 linkG = topoG.append('g').attr('id', 'links');
961 nodeG = topoG.append('g').attr('id', 'nodes');
962
963 // selection of nodes and links
964 link = linkG.selectAll('.link');
965 node = nodeG.selectAll('.node');
966
Simon Hunt99c13842014-11-06 18:23:12 -0800967 function ldist(d) {
Thomas Vachuska4830d392014-11-09 17:09:56 -0800968 return 2 * 30;
969 //return fcfg.linkDistance[d.class] || 150;
Simon Hunt99c13842014-11-06 18:23:12 -0800970 }
971 function lstrg(d) {
Thomas Vachuska4830d392014-11-09 17:09:56 -0800972 return 2 * 0.6;
973 //return fcfg.linkStrength[d.class] || 1;
Simon Hunt99c13842014-11-06 18:23:12 -0800974 }
975 function lchrg(d) {
976 return fcfg.charge[d.class] || -200;
977 }
978
Simon Hunt1a9eff92014-11-07 11:06:34 -0800979 function selectCb(d, self) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800980 selectObject(d, self);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800981 }
982
983 function atDragEnd(d, self) {
Simon Hunt56d51852014-11-09 13:03:35 -0800984 // once we've finished moving, pin the node in position
985 d.fixed = true;
986 d3.select(self).classed('fixed', true);
987 if (config.useLiveData) {
988 tellServerCoords(d);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800989 }
990 }
991
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800992 function tellServerCoords(d) {
993 sendMessage('updateMeta', {
994 id: d.id,
995 'class': d.class,
Simon Hunt5f36d342014-11-08 21:33:14 -0800996 x: Math.floor(d.x),
997 y: Math.floor(d.y)
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800998 });
999 }
1000
Simon Huntc7ee0662014-11-05 16:44:37 -08001001 // set up the force layout
1002 network.force = d3.layout.force()
1003 .size(forceDim)
1004 .nodes(network.nodes)
1005 .links(network.links)
Thomas Vachuska4830d392014-11-09 17:09:56 -08001006 .gravity(0.3)
1007 .charge(-15000)
1008 .friction(0.1)
1009 //.charge(lchrg)
Simon Hunt99c13842014-11-06 18:23:12 -08001010 .linkDistance(ldist)
1011 .linkStrength(lstrg)
Simon Huntc7ee0662014-11-05 16:44:37 -08001012 .on('tick', tick);
Simon Hunt195cb382014-11-03 17:50:51 -08001013
Thomas Vachuska4830d392014-11-09 17:09:56 -08001014 // TVUE
1015 //.gravity(0.3)
1016 //.charge(-15000)
1017 //.friction(0.1)
1018 //.linkDistance(function(d) { return d.value * 30; })
1019 //.linkStrength(function(d) { return d.value * 0.6; })
1020 //.size([w, h])
1021 //.start();
1022
Simon Hunt1a9eff92014-11-07 11:06:34 -08001023 network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
1024 }
Simon Hunt195cb382014-11-03 17:50:51 -08001025
Simon Hunt56d51852014-11-09 13:03:35 -08001026 function load(view, ctx, flags) {
Simon Hunt99c13842014-11-06 18:23:12 -08001027 // cache the view token, so network topo functions can access it
1028 network.view = view;
Simon Hunt56d51852014-11-09 13:03:35 -08001029 config.useLiveData = !flags.local;
1030
1031 if (!config.useLiveData) {
1032 prepareScenario(view, ctx, flags.debug);
1033 }
Simon Hunt99c13842014-11-06 18:23:12 -08001034
1035 // set our radio buttons and key bindings
Simon Hunt934c3ce2014-11-05 11:45:07 -08001036 view.setRadio(btnSet);
1037 view.setKeys(keyDispatch);
Simon Hunt195cb382014-11-03 17:50:51 -08001038
Simon Hunt50128c02014-11-08 13:36:15 -08001039 if (config.useLiveData) {
1040 webSock.connect();
1041 }
Simon Hunt195cb382014-11-03 17:50:51 -08001042 }
1043
Simon Hunt142d0032014-11-04 20:13:09 -08001044 function resize(view, ctx) {
Simon Hunt934c3ce2014-11-05 11:45:07 -08001045 setSize(svg, view);
1046 setSize(bgImg, view);
Simon Hunt99c13842014-11-06 18:23:12 -08001047
1048 // TODO: hook to recompute layout, perhaps? work with zoom/pan code
1049 // adjust force layout size
Simon Hunt142d0032014-11-04 20:13:09 -08001050 }
1051
1052
1053 // ==============================
1054 // View registration
Simon Hunt195cb382014-11-03 17:50:51 -08001055
Simon Hunt25248912014-11-04 11:25:48 -08001056 onos.ui.addView('topo', {
Simon Hunt142d0032014-11-04 20:13:09 -08001057 preload: preload,
1058 load: load,
1059 resize: resize
Simon Hunt195cb382014-11-03 17:50:51 -08001060 });
1061
Simon Hunt195cb382014-11-03 17:50:51 -08001062}(ONOS));