blob: 5bffcdd0936881e92ce41e7948f8fc79837ceacb [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 Hunt7cd48f32014-11-09 23:42:50 -080085 note_for_links: 'link.type is used to differentiate',
Simon Huntc7ee0662014-11-05 16:44:37 -080086 linkDistance: {
Simon Hunt7cd48f32014-11-09 23:42:50 -080087 direct: 100,
88 optical: 120,
89 hostLink: 20
Simon Huntc7ee0662014-11-05 16:44:37 -080090 },
91 linkStrength: {
Simon Hunt7cd48f32014-11-09 23:42:50 -080092 direct: 1.0,
93 optical: 1.0,
94 hostLink: 1.0
Simon Huntc7ee0662014-11-05 16:44:37 -080095 },
Simon Hunt7cd48f32014-11-09 23:42:50 -080096 note_for_nodes: 'node.class is used to differentiate',
Simon Huntc7ee0662014-11-05 16:44:37 -080097 charge: {
Simon Hunt7cd48f32014-11-09 23:42:50 -080098 device: -8000,
99 host: -300
Simon Huntc7ee0662014-11-05 16:44:37 -0800100 },
101 pad: 20,
Simon Hunt195cb382014-11-03 17:50:51 -0800102 translate: function() {
103 return 'translate(' +
Simon Huntc7ee0662014-11-05 16:44:37 -0800104 config.force.pad + ',' +
105 config.force.pad + ')';
Simon Hunt195cb382014-11-03 17:50:51 -0800106 }
Simon Hunt142d0032014-11-04 20:13:09 -0800107 }
Simon Hunt195cb382014-11-03 17:50:51 -0800108 };
109
Simon Hunt142d0032014-11-04 20:13:09 -0800110 // radio buttons
111 var btnSet = [
Simon Hunt934c3ce2014-11-05 11:45:07 -0800112 { text: 'All Layers', cb: showAllLayers },
113 { text: 'Packet Only', cb: showPacketLayer },
114 { text: 'Optical Only', cb: showOpticalLayer }
115 ];
116
117 // key bindings
118 var keyDispatch = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800119 M: testMe, // TODO: remove (testing only)
Simon Hunt50128c02014-11-08 13:36:15 -0800120 S: injectStartupEvents, // TODO: remove (testing only)
121 space: injectTestEvent, // TODO: remove (testing only)
Simon Hunt99c13842014-11-06 18:23:12 -0800122
Thomas Vachuska65368e32014-11-08 16:10:20 -0800123 B: toggleBg, // TODO: do we really need this?
Simon Hunt934c3ce2014-11-05 11:45:07 -0800124 L: cycleLabels,
125 P: togglePorts,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800126 U: unpin,
127
128 X: requestPath
Simon Hunt934c3ce2014-11-05 11:45:07 -0800129 };
Simon Hunt142d0032014-11-04 20:13:09 -0800130
Simon Hunt195cb382014-11-03 17:50:51 -0800131 // state variables
Simon Hunt99c13842014-11-06 18:23:12 -0800132 var network = {
Simon Hunt50128c02014-11-08 13:36:15 -0800133 view: null, // view token reference
Simon Hunt99c13842014-11-06 18:23:12 -0800134 nodes: [],
135 links: [],
136 lookup: {}
137 },
Simon Hunt56d51852014-11-09 13:03:35 -0800138 scenario = {
139 evDir: 'json/ev/',
140 evScenario: '/scenario.json',
141 evPrefix: '/ev_',
142 evOnos: '_onos.json',
143 evUi: '_ui.json',
144 ctx: null,
145 params: {},
146 evNumber: 0,
147 view: null,
148 debug: false
149 },
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800150 webSock,
Simon Hunt56d51852014-11-09 13:03:35 -0800151 deviceLabelIndex = 0,
152 hostLabelIndex = 0,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800153
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800154 selectOrder = [],
155 selections = {},
156
Simon Hunt195cb382014-11-03 17:50:51 -0800157 highlighted = null,
158 hovered = null,
159 viewMode = 'showAll',
160 portLabelsOn = false;
161
Simon Hunt934c3ce2014-11-05 11:45:07 -0800162 // D3 selections
163 var svg,
164 bgImg,
Simon Huntc7ee0662014-11-05 16:44:37 -0800165 topoG,
166 nodeG,
167 linkG,
168 node,
169 link;
Simon Hunt195cb382014-11-03 17:50:51 -0800170
Simon Hunt142d0032014-11-04 20:13:09 -0800171 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800172 // For Debugging / Development
Simon Hunt195cb382014-11-03 17:50:51 -0800173
Simon Hunt99c13842014-11-06 18:23:12 -0800174 function note(label, msg) {
175 console.log('NOTE: ' + label + ': ' + msg);
Simon Hunt195cb382014-11-03 17:50:51 -0800176 }
177
Simon Hunt99c13842014-11-06 18:23:12 -0800178 function debug(what) {
179 return config.debugOn && config.debug[what];
Simon Hunt934c3ce2014-11-05 11:45:07 -0800180 }
181
Simon Hunt99c13842014-11-06 18:23:12 -0800182
Simon Hunt934c3ce2014-11-05 11:45:07 -0800183 // ==============================
184 // Key Callbacks
185
Simon Hunt99c13842014-11-06 18:23:12 -0800186 function testMe(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800187 view.alert('test');
Simon Hunt99c13842014-11-06 18:23:12 -0800188 }
189
Simon Hunt56d51852014-11-09 13:03:35 -0800190 function abortIfLive() {
Simon Hunt50128c02014-11-08 13:36:15 -0800191 if (config.useLiveData) {
Simon Hunt56d51852014-11-09 13:03:35 -0800192 scenario.view.alert("Sorry, currently using live data..");
193 return true;
Simon Hunt50128c02014-11-08 13:36:15 -0800194 }
Simon Hunt56d51852014-11-09 13:03:35 -0800195 return false;
196 }
Simon Hunt50128c02014-11-08 13:36:15 -0800197
Simon Hunt56d51852014-11-09 13:03:35 -0800198 function testDebug(msg) {
199 if (scenario.debug) {
200 scenario.view.alert(msg);
201 }
202 }
Simon Hunt99c13842014-11-06 18:23:12 -0800203
Simon Hunt56d51852014-11-09 13:03:35 -0800204 function injectTestEvent(view) {
205 if (abortIfLive()) { return; }
206 var sc = scenario,
207 evn = ++sc.evNumber,
208 pfx = sc.evDir + sc.ctx + sc.evPrefix + evn,
209 onosUrl = pfx + sc.evOnos,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800210 uiUrl = pfx + sc.evUi,
211 stack = [
212 { url: onosUrl, cb: handleServerEvent },
213 { url: uiUrl, cb: handleUiEvent }
214 ];
215 recurseFetchEvent(stack, evn);
Simon Hunt56d51852014-11-09 13:03:35 -0800216 }
217
Simon Hunt7cd48f32014-11-09 23:42:50 -0800218 function recurseFetchEvent(stack, evn) {
219 var v = scenario.view,
220 frame;
221 if (stack.length === 0) {
222 v.alert('Error:\n\nNo event #' + evn + ' found.');
223 return;
224 }
225 frame = stack.shift();
226
227 d3.json(frame.url, function (err, data) {
Simon Hunt99c13842014-11-06 18:23:12 -0800228 if (err) {
Simon Hunt56d51852014-11-09 13:03:35 -0800229 if (err.status === 404) {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800230 // if we didn't find the data, try the next stack frame
231 recurseFetchEvent(stack, evn);
Simon Hunt56d51852014-11-09 13:03:35 -0800232 } else {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800233 v.alert('non-404 error:\n\n' + frame.url + '\n\n' + err);
Simon Hunt56d51852014-11-09 13:03:35 -0800234 }
Simon Hunt99c13842014-11-06 18:23:12 -0800235 } else {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800236 testDebug('loaded: ' + frame.url);
237 frame.cb(data);
Simon Hunt99c13842014-11-06 18:23:12 -0800238 }
239 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800240
Simon Hunt56d51852014-11-09 13:03:35 -0800241 }
Simon Hunt50128c02014-11-08 13:36:15 -0800242
Simon Hunt56d51852014-11-09 13:03:35 -0800243 function handleUiEvent(data) {
244 testDebug('handleUiEvent(): ' + data.event);
245 // TODO:
246 }
247
248 function injectStartupEvents(view) {
249 var last = scenario.params.lastAuto || 0;
250 if (abortIfLive()) { return; }
251
252 while (scenario.evNumber < last) {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800253 injectTestEvent(view);
254 }
255 }
256
Simon Hunt934c3ce2014-11-05 11:45:07 -0800257 function toggleBg() {
258 var vis = bgImg.style('visibility');
259 bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
260 }
261
Simon Hunt99c13842014-11-06 18:23:12 -0800262 function cycleLabels() {
Simon Hunt56d51852014-11-09 13:03:35 -0800263 deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1) ? 0 : deviceLabelIndex + 1;
Simon Hunt5f36d342014-11-08 21:33:14 -0800264
Simon Hunt99c13842014-11-06 18:23:12 -0800265 network.nodes.forEach(function (d) {
Simon Hunt56d51852014-11-09 13:03:35 -0800266 if (d.class !== 'device') { return; }
267
Simon Hunt7cd48f32014-11-09 23:42:50 -0800268 var label = niceLabel(deviceLabel(d)),
269 node = d.el,
Simon Hunt99c13842014-11-06 18:23:12 -0800270 box;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800271
Simon Hunt99c13842014-11-06 18:23:12 -0800272 node.select('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -0800273 .text(label)
Simon Hunt99c13842014-11-06 18:23:12 -0800274 .style('opacity', 0)
275 .transition()
276 .style('opacity', 1);
277
278 box = adjustRectToFitText(node);
279
280 node.select('rect')
281 .transition()
282 .attr(box);
283
284 node.select('image')
285 .transition()
286 .attr('x', box.x + config.icons.xoff)
287 .attr('y', box.y + config.icons.yoff);
288 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800289 }
290
291 function togglePorts(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800292 view.alert('togglePorts() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800293 }
294
295 function unpin(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800296 view.alert('unpin() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800297 }
298
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800299 function requestPath(view) {
300 var payload = {
301 one: selections[selectOrder[0]].obj.id,
302 two: selections[selectOrder[1]].obj.id
303 }
304 sendMessage('requestPath', payload);
305 }
306
Simon Hunt934c3ce2014-11-05 11:45:07 -0800307 // ==============================
308 // Radio Button Callbacks
309
Simon Hunt195cb382014-11-03 17:50:51 -0800310 function showAllLayers() {
Simon Hunt142d0032014-11-04 20:13:09 -0800311// network.node.classed('inactive', false);
312// network.link.classed('inactive', false);
313// d3.selectAll('svg .port').classed('inactive', false);
314// d3.selectAll('svg .portText').classed('inactive', false);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800315 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800316 network.view.alert('showAllLayers() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800317 }
318
319 function showPacketLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800320 showAllLayers();
321 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800322 network.view.alert('showPacketLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800323 }
324
325 function showOpticalLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800326 showAllLayers();
327 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800328 network.view.alert('showOpticalLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800329 }
330
Simon Hunt142d0032014-11-04 20:13:09 -0800331 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800332 // Private functions
333
Simon Hunt99c13842014-11-06 18:23:12 -0800334 function safeId(s) {
335 return s.replace(/[^a-z0-9]/gi, '-');
336 }
337
Simon Huntc7ee0662014-11-05 16:44:37 -0800338 // set the size of the given element to that of the view (reduced if padded)
339 function setSize(el, view, pad) {
340 var padding = pad ? pad * 2 : 0;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800341 el.attr({
Simon Huntc7ee0662014-11-05 16:44:37 -0800342 width: view.width() - padding,
343 height: view.height() - padding
Simon Hunt934c3ce2014-11-05 11:45:07 -0800344 });
345 }
346
Simon Hunt934c3ce2014-11-05 11:45:07 -0800347
Simon Hunt99c13842014-11-06 18:23:12 -0800348 // ==============================
349 // Event handlers for server-pushed events
350
351 var eventDispatch = {
352 addDevice: addDevice,
Simon Hunt56d51852014-11-09 13:03:35 -0800353 updateDevice: stillToImplement,
354 removeDevice: stillToImplement,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800355 addLink: addLink,
Simon Hunt56d51852014-11-09 13:03:35 -0800356 updateLink: stillToImplement,
357 removeLink: stillToImplement,
358 addHost: addHost,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800359 updateHost: updateHost,
Simon Hunt56d51852014-11-09 13:03:35 -0800360 removeHost: stillToImplement,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800361 showPath: showPath
Simon Hunt99c13842014-11-06 18:23:12 -0800362 };
363
364 function addDevice(data) {
365 var device = data.payload,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800366 nodeData = createDeviceNode(device);
Simon Hunt99c13842014-11-06 18:23:12 -0800367 note('addDevice', device.id);
368
Simon Hunt7cd48f32014-11-09 23:42:50 -0800369 network.nodes.push(nodeData);
370 network.lookup[nodeData.id] = nodeData;
Simon Hunt99c13842014-11-06 18:23:12 -0800371 updateNodes();
372 network.force.start();
373 }
374
Simon Hunt99c13842014-11-06 18:23:12 -0800375 function addLink(data) {
376 var link = data.payload,
377 lnk = createLink(link);
378
379 if (lnk) {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800380 note('addLink', link.id);
Simon Hunt99c13842014-11-06 18:23:12 -0800381
382 network.links.push(lnk);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800383 network.lookup[lnk.id] = lnk;
Simon Hunt99c13842014-11-06 18:23:12 -0800384 updateLinks();
385 network.force.start();
386 }
387 }
388
Simon Hunt56d51852014-11-09 13:03:35 -0800389 function addHost(data) {
390 var host = data.payload,
391 node = createHostNode(host),
392 lnk;
Simon Hunt56d51852014-11-09 13:03:35 -0800393 note('addHost', node.id);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800394
Simon Hunt56d51852014-11-09 13:03:35 -0800395 network.nodes.push(node);
396 network.lookup[host.id] = node;
397 updateNodes();
398
399 lnk = createHostLink(host);
400 if (lnk) {
401 network.links.push(lnk);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800402 network.lookup[host.ingress] = lnk;
403 network.lookup[host.egress] = lnk;
Simon Hunt56d51852014-11-09 13:03:35 -0800404 updateLinks();
405 }
406 network.force.start();
407 }
408
Simon Hunt7cd48f32014-11-09 23:42:50 -0800409 function updateHost(data) {
410 var host = data.payload,
411 hostData = network.lookup[host.id];
412 note('updateHost', host.id);
413
414 $.extend(hostData, host);
415 updateNodes();
416 }
417
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800418 function showPath(data) {
Thomas Vachuska4830d392014-11-09 17:09:56 -0800419 var links = data.payload.links,
420 s = [ data.event + "\n" + links.length ];
421 links.forEach(function (d, i) {
422 s.push(d);
423 });
424 network.view.alert(s.join('\n'));
425
426 links.forEach(function (d, i) {
427 var link = network.lookup[d];
428 if (link) {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800429 link.el.classed('showPath', true);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800430 }
431 });
432
433 // TODO: add selection-highlite lines to links
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800434 }
435
Simon Hunt56d51852014-11-09 13:03:35 -0800436 // ...............................
437
438 function stillToImplement(data) {
439 var p = data.payload;
440 note(data.event, p.id);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800441 network.view.alert('Not yet implemented: "' + data.event + '"');
Simon Hunt56d51852014-11-09 13:03:35 -0800442 }
Simon Hunt99c13842014-11-06 18:23:12 -0800443
444 function unknownEvent(data) {
Simon Hunt50128c02014-11-08 13:36:15 -0800445 network.view.alert('Unknown event type: "' + data.event + '"');
Simon Hunt99c13842014-11-06 18:23:12 -0800446 }
447
448 function handleServerEvent(data) {
449 var fn = eventDispatch[data.event] || unknownEvent;
450 fn(data);
451 }
452
453 // ==============================
454 // force layout modification functions
455
456 function translate(x, y) {
457 return 'translate(' + x + ',' + y + ')';
458 }
459
Simon Hunt56d51852014-11-09 13:03:35 -0800460 function createHostLink(host) {
461 var src = host.id,
462 dst = host.cp.device,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800463 id = host.ingress,
Simon Hunt56d51852014-11-09 13:03:35 -0800464 srcNode = network.lookup[src],
465 dstNode = network.lookup[dst],
466 lnk;
467
468 if (!dstNode) {
469 // TODO: send warning message back to server on websocket
470 network.view.alert('switch not on map for link\n\n' +
471 'src = ' + src + '\ndst = ' + dst);
472 return null;
473 }
474
Simon Hunt7cd48f32014-11-09 23:42:50 -0800475 // Compose link ...
Simon Hunt56d51852014-11-09 13:03:35 -0800476 lnk = {
Thomas Vachuska4830d392014-11-09 17:09:56 -0800477 id: id,
Simon Hunt56d51852014-11-09 13:03:35 -0800478 source: srcNode,
479 target: dstNode,
480 class: 'link',
Simon Hunt7cd48f32014-11-09 23:42:50 -0800481 type: 'hostLink',
Simon Hunt56d51852014-11-09 13:03:35 -0800482 svgClass: 'link hostLink',
483 x1: srcNode.x,
484 y1: srcNode.y,
485 x2: dstNode.x,
486 y2: dstNode.y,
487 width: 1
Simon Hunt7cd48f32014-11-09 23:42:50 -0800488 }
Simon Hunt56d51852014-11-09 13:03:35 -0800489 return lnk;
490 }
491
Simon Hunt99c13842014-11-06 18:23:12 -0800492 function createLink(link) {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800493 // start with the link object as is
494 var lnk = link,
495 type = link.type,
Simon Hunt99c13842014-11-06 18:23:12 -0800496 src = link.src,
497 dst = link.dst,
498 w = link.linkWidth,
499 srcNode = network.lookup[src],
Simon Hunt7cd48f32014-11-09 23:42:50 -0800500 dstNode = network.lookup[dst];
Simon Hunt99c13842014-11-06 18:23:12 -0800501
502 if (!(srcNode && dstNode)) {
Simon Hunt50128c02014-11-08 13:36:15 -0800503 // TODO: send warning message back to server on websocket
504 network.view.alert('nodes not on map for link\n\n' +
505 'src = ' + src + '\ndst = ' + dst);
Simon Hunt99c13842014-11-06 18:23:12 -0800506 return null;
507 }
508
Simon Hunt7cd48f32014-11-09 23:42:50 -0800509 // Augment as needed...
510 $.extend(lnk, {
511 source: srcNode,
512 target: dstNode,
513 class: 'link',
514 svgClass: type ? 'link ' + type : 'link',
515 x1: srcNode.x,
516 y1: srcNode.y,
517 x2: dstNode.x,
518 y2: dstNode.y,
519 width: w
520 });
Simon Hunt99c13842014-11-06 18:23:12 -0800521 return lnk;
522 }
523
Simon Hunt1a9eff92014-11-07 11:06:34 -0800524 function linkWidth(w) {
525 // w is number of links between nodes. Scale appropriately.
Simon Hunt50128c02014-11-08 13:36:15 -0800526 // TODO: use a d3.scale (linear, log, ... ?)
Simon Hunt1a9eff92014-11-07 11:06:34 -0800527 return w * 1.2;
528 }
529
Simon Hunt99c13842014-11-06 18:23:12 -0800530 function updateLinks() {
531 link = linkG.selectAll('.link')
532 .data(network.links, function (d) { return d.id; });
533
534 // operate on existing links, if necessary
535 // link .foo() .bar() ...
536
537 // operate on entering links:
538 var entering = link.enter()
539 .append('line')
540 .attr({
Simon Hunt99c13842014-11-06 18:23:12 -0800541 class: function (d) { return d.svgClass; },
542 x1: function (d) { return d.x1; },
543 y1: function (d) { return d.y1; },
544 x2: function (d) { return d.x2; },
545 y2: function (d) { return d.y2; },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800546 stroke: config.topo.linkInColor,
547 'stroke-width': config.topo.linkInWidth
Simon Hunt99c13842014-11-06 18:23:12 -0800548 })
549 .transition().duration(1000)
550 .attr({
Simon Hunt1a9eff92014-11-07 11:06:34 -0800551 'stroke-width': function (d) { return linkWidth(d.width); },
Simon Hunt99c13842014-11-06 18:23:12 -0800552 stroke: '#666' // TODO: remove explicit stroke, rather...
553 });
554
555 // augment links
Simon Hunt7cd48f32014-11-09 23:42:50 -0800556 entering.each(function (d) {
557 var link = d3.select(this);
558 // provide ref to element selection from backing data....
559 d.el = link;
Simon Hunt99c13842014-11-06 18:23:12 -0800560
Simon Hunt7cd48f32014-11-09 23:42:50 -0800561 // TODO: add src/dst port labels etc.
562 });
Thomas Vachuska4830d392014-11-09 17:09:56 -0800563
564 // operate on both existing and new links, if necessary
565 //link .foo() .bar() ...
566
567 // operate on exiting links:
568 // TODO: figure out how to remove the node 'g' AND its children
569 link.exit()
570 .transition()
571 .duration(750)
572 .attr({
573 opacity: 0
574 })
575 .remove();
Simon Hunt99c13842014-11-06 18:23:12 -0800576 }
577
578 function createDeviceNode(device) {
579 // start with the object as is
580 var node = device,
581 type = device.type;
582
583 // Augment as needed...
584 node.class = 'device';
585 node.svgClass = type ? 'node device ' + type : 'node device';
586 positionNode(node);
587
588 // cache label array length
589 network.deviceLabelCount = device.labels.length;
Simon Hunt99c13842014-11-06 18:23:12 -0800590 return node;
591 }
592
Simon Hunt56d51852014-11-09 13:03:35 -0800593 function createHostNode(host) {
594 // start with the object as is
595 var node = host;
596
597 // Augment as needed...
598 node.class = 'host';
Simon Hunt7cd48f32014-11-09 23:42:50 -0800599 if (!node.type) {
600 // TODO: perhaps type would be: {phone, tablet, laptop, endstation} ?
601 node.type = 'endstation';
602 }
Simon Hunt56d51852014-11-09 13:03:35 -0800603 node.svgClass = 'node host';
604 // TODO: consider placing near its switch, if [x,y] not defined
605 positionNode(node);
606
607 // cache label array length
608 network.hostLabelCount = host.labels.length;
Simon Hunt56d51852014-11-09 13:03:35 -0800609 return node;
610 }
611
Simon Hunt99c13842014-11-06 18:23:12 -0800612 function positionNode(node) {
613 var meta = node.metaUi,
614 x = 0,
615 y = 0;
616
617 if (meta) {
618 x = meta.x;
619 y = meta.y;
620 }
621 if (x && y) {
622 node.fixed = true;
623 }
624 node.x = x || network.view.width() / 2;
625 node.y = y || network.view.height() / 2;
626 }
627
628
629 function iconUrl(d) {
630 return 'img/' + d.type + '.png';
631 }
632
633 // returns the newly computed bounding box of the rectangle
634 function adjustRectToFitText(n) {
635 var text = n.select('text'),
636 box = text.node().getBBox(),
637 lab = config.labels;
638
639 text.attr('text-anchor', 'middle')
640 .attr('y', '-0.8em')
641 .attr('x', lab.imgPad/2);
642
643 // translate the bbox so that it is centered on [x,y]
644 box.x = -box.width / 2;
645 box.y = -box.height / 2;
646
647 // add padding
648 box.x -= (lab.padLR + lab.imgPad/2);
649 box.width += lab.padLR * 2 + lab.imgPad;
650 box.y -= lab.padTB;
651 box.height += lab.padTB * 2;
652
653 return box;
654 }
655
Simon Hunt1a9eff92014-11-07 11:06:34 -0800656 function mkSvgClass(d) {
657 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
658 }
659
Simon Hunt7cd48f32014-11-09 23:42:50 -0800660 function hostLabel(d) {
661 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
662 return d.labels[idx];
663 }
664 function deviceLabel(d) {
665 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
666 return d.labels[idx];
667 }
668 function niceLabel(label) {
669 return (label && label.trim()) ? label : '.';
670 }
671
Simon Hunt99c13842014-11-06 18:23:12 -0800672 function updateNodes() {
673 node = nodeG.selectAll('.node')
674 .data(network.nodes, function (d) { return d.id; });
675
676 // operate on existing nodes, if necessary
Simon Hunt7cd48f32014-11-09 23:42:50 -0800677 // update host labels
678 node.filter('.host').select('text')
679 .text(hostLabel);
680
Simon Hunt99c13842014-11-06 18:23:12 -0800681 //node .foo() .bar() ...
682
683 // operate on entering nodes:
684 var entering = node.enter()
685 .append('g')
686 .attr({
687 id: function (d) { return safeId(d.id); },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800688 class: mkSvgClass,
Simon Hunt99c13842014-11-06 18:23:12 -0800689 transform: function (d) { return translate(d.x, d.y); },
690 opacity: 0
691 })
Simon Hunt1a9eff92014-11-07 11:06:34 -0800692 .call(network.drag)
Simon Hunt99c13842014-11-06 18:23:12 -0800693 //.on('mouseover', function (d) {})
694 //.on('mouseover', function (d) {})
695 .transition()
696 .attr('opacity', 1);
697
698 // augment device nodes...
699 entering.filter('.device').each(function (d) {
700 var node = d3.select(this),
701 icon = iconUrl(d),
Simon Hunt7cd48f32014-11-09 23:42:50 -0800702 label = niceLabel(deviceLabel(d)),
Simon Hunt99c13842014-11-06 18:23:12 -0800703 box;
704
Simon Hunt7cd48f32014-11-09 23:42:50 -0800705 // provide ref to element from backing data....
706 d.el = node;
707
Simon Hunt99c13842014-11-06 18:23:12 -0800708 node.append('rect')
709 .attr({
710 'rx': 5,
711 'ry': 5
712 });
713
714 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -0800715 .text(label)
Simon Hunt99c13842014-11-06 18:23:12 -0800716 .attr('dy', '1.1em');
717
718 box = adjustRectToFitText(node);
719
720 node.select('rect')
721 .attr(box);
722
723 if (icon) {
724 var cfg = config.icons;
725 node.append('svg:image')
726 .attr({
727 x: box.x + config.icons.xoff,
728 y: box.y + config.icons.yoff,
729 width: cfg.w,
730 height: cfg.h,
731 'xlink:href': icon
732 });
733 }
734
735 // debug function to show the modelled x,y coordinates of nodes...
736 if (debug('showNodeXY')) {
737 node.select('rect').attr('fill-opacity', 0.5);
738 node.append('circle')
739 .attr({
740 class: 'debug',
741 cx: 0,
742 cy: 0,
743 r: '3px'
744 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800745 }
746 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800747
Simon Hunt56d51852014-11-09 13:03:35 -0800748 // augment host nodes...
749 entering.filter('.host').each(function (d) {
750 var node = d3.select(this),
Simon Hunt56d51852014-11-09 13:03:35 -0800751 box;
752
Simon Hunt7cd48f32014-11-09 23:42:50 -0800753 // provide ref to element from backing data....
754 d.el = node;
755
Simon Hunt56d51852014-11-09 13:03:35 -0800756 node.append('circle')
757 .attr('r', 8); // TODO: define host circle radius
758
759 // TODO: are we attaching labels to hosts?
760 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -0800761 .text(hostLabel)
762 .attr('dy', '1.3em')
763 .attr('text-anchor', 'middle');
Simon Hunt56d51852014-11-09 13:03:35 -0800764
765 // debug function to show the modelled x,y coordinates of nodes...
766 if (debug('showNodeXY')) {
767 node.select('circle').attr('fill-opacity', 0.5);
768 node.append('circle')
769 .attr({
770 class: 'debug',
771 cx: 0,
772 cy: 0,
773 r: '3px'
774 });
775 }
776 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800777
Simon Hunt99c13842014-11-06 18:23:12 -0800778 // operate on both existing and new nodes, if necessary
779 //node .foo() .bar() ...
Simon Huntc7ee0662014-11-05 16:44:37 -0800780
Simon Hunt99c13842014-11-06 18:23:12 -0800781 // operate on exiting nodes:
782 // TODO: figure out how to remove the node 'g' AND its children
783 node.exit()
784 .transition()
785 .duration(750)
786 .attr({
787 opacity: 0,
788 cx: 0,
789 cy: 0,
790 r: 0
791 })
792 .remove();
Simon Huntc7ee0662014-11-05 16:44:37 -0800793 }
794
795
796 function tick() {
797 node.attr({
Simon Hunt99c13842014-11-06 18:23:12 -0800798 transform: function (d) { return translate(d.x, d.y); }
Simon Huntc7ee0662014-11-05 16:44:37 -0800799 });
800
801 link.attr({
802 x1: function (d) { return d.source.x; },
803 y1: function (d) { return d.source.y; },
804 x2: function (d) { return d.target.x; },
805 y2: function (d) { return d.target.y; }
806 });
807 }
Simon Hunt934c3ce2014-11-05 11:45:07 -0800808
809 // ==============================
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800810 // Web-Socket for live data
811
812 function webSockUrl() {
813 return document.location.toString()
814 .replace(/\#.*/, '')
815 .replace('http://', 'ws://')
816 .replace('https://', 'wss://')
817 .replace('index2.html', config.webSockUrl);
818 }
819
820 webSock = {
821 ws : null,
822
823 connect : function() {
824 webSock.ws = new WebSocket(webSockUrl());
825
826 webSock.ws.onopen = function() {
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800827 };
828
829 webSock.ws.onmessage = function(m) {
830 if (m.data) {
831 console.log(m.data);
Thomas Vachuskad472c6e2014-11-07 19:11:05 -0800832 handleServerEvent(JSON.parse(m.data));
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800833 }
834 };
835
836 webSock.ws.onclose = function(m) {
837 webSock.ws = null;
838 };
839 },
840
841 send : function(text) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800842 if (text != null) {
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800843 webSock._send(text);
844 }
845 },
846
847 _send : function(message) {
848 if (webSock.ws) {
849 webSock.ws.send(message);
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800850 } else {
Simon Hunt56d51852014-11-09 13:03:35 -0800851 network.view.alert('no web socket open\n\n' + message);
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800852 }
853 }
854
855 };
856
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800857 var sid = 0;
858
859 function sendMessage(evType, payload) {
860 var toSend = {
861 event: evType,
862 sid: ++sid,
863 payload: payload
864 };
865 webSock.send(JSON.stringify(toSend));
866 }
867
868
869 // ==============================
870 // Selection stuff
871
872 function selectObject(obj, el) {
873 var n,
874 meta = d3.event.sourceEvent.metaKey;
875
876 if (el) {
877 n = d3.select(el);
878 } else {
879 node.each(function(d) {
880 if (d == obj) {
881 n = d3.select(el = this);
882 }
883 });
884 }
885 if (!n) return;
886
887 if (meta && n.classed('selected')) {
888 deselectObject(obj.id);
889 //flyinPane(null);
890 return;
891 }
892
893 if (!meta) {
894 deselectAll();
895 }
896
Simon Hunt5f36d342014-11-08 21:33:14 -0800897 selections[obj.id] = { obj: obj, el : el};
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800898 selectOrder.push(obj.id);
899
900 n.classed('selected', true);
901 //flyinPane(obj);
902 }
903
904 function deselectObject(id) {
905 var obj = selections[id];
906 if (obj) {
907 d3.select(obj.el).classed('selected', false);
908 selections[id] = null;
909 // TODO: use splice to remove element
910 }
911 //flyinPane(null);
912 }
913
914 function deselectAll() {
915 // deselect all nodes in the network...
916 node.classed('selected', false);
917 selections = {};
918 selectOrder = [];
919 //flyinPane(null);
920 }
921
Simon Hunt56d51852014-11-09 13:03:35 -0800922 // TODO: this click handler does not get unloaded when the view does
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800923 $('#view').on('click', function(e) {
924 if (!$(e.target).closest('.node').length) {
925 if (!e.metaKey) {
926 deselectAll();
927 }
928 }
929 });
930
Simon Hunt56d51852014-11-09 13:03:35 -0800931
932 function prepareScenario(view, ctx, dbg) {
933 var sc = scenario,
934 urlSc = sc.evDir + ctx + sc.evScenario;
935
936 if (!ctx) {
937 view.alert("No scenario specified (null ctx)");
938 return;
939 }
940
941 sc.view = view;
942 sc.ctx = ctx;
943 sc.debug = dbg;
944 sc.evNumber = 0;
945
946 d3.json(urlSc, function(err, data) {
947 var p = data && data.params || {};
948 if (err) {
949 view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
950 } else {
951 sc.params = p;
952 view.alert("Scenario loaded: " + ctx + '\n\n' + data.title);
953 }
954 });
955
956 }
957
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800958 // ==============================
Simon Hunt142d0032014-11-04 20:13:09 -0800959 // View life-cycle callbacks
Simon Hunt195cb382014-11-03 17:50:51 -0800960
Simon Hunt142d0032014-11-04 20:13:09 -0800961 function preload(view, ctx) {
962 var w = view.width(),
963 h = view.height(),
964 idBg = view.uid('bg'),
Simon Huntc7ee0662014-11-05 16:44:37 -0800965 showBg = config.options.showBackground ? 'visible' : 'hidden',
966 fcfg = config.force,
967 fpad = fcfg.pad,
968 forceDim = [w - 2*fpad, h - 2*fpad];
Simon Hunt195cb382014-11-03 17:50:51 -0800969
Simon Hunt142d0032014-11-04 20:13:09 -0800970 // NOTE: view.$div is a D3 selection of the view's div
971 svg = view.$div.append('svg');
Simon Hunt934c3ce2014-11-05 11:45:07 -0800972 setSize(svg, view);
973
Simon Hunt1a9eff92014-11-07 11:06:34 -0800974 // add blue glow filter to svg layer
975 d3u.appendGlow(svg);
976
Simon Hunt142d0032014-11-04 20:13:09 -0800977 // load the background image
978 bgImg = svg.append('svg:image')
Simon Hunt195cb382014-11-03 17:50:51 -0800979 .attr({
Simon Hunt142d0032014-11-04 20:13:09 -0800980 id: idBg,
981 width: w,
982 height: h,
Simon Hunt195cb382014-11-03 17:50:51 -0800983 'xlink:href': config.backgroundUrl
984 })
Simon Hunt142d0032014-11-04 20:13:09 -0800985 .style({
986 visibility: showBg
Simon Hunt195cb382014-11-03 17:50:51 -0800987 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800988
989 // group for the topology
990 topoG = svg.append('g')
991 .attr('transform', fcfg.translate());
992
993 // subgroups for links and nodes
994 linkG = topoG.append('g').attr('id', 'links');
995 nodeG = topoG.append('g').attr('id', 'nodes');
996
997 // selection of nodes and links
998 link = linkG.selectAll('.link');
999 node = nodeG.selectAll('.node');
1000
Simon Hunt7cd48f32014-11-09 23:42:50 -08001001 function chrg(d) {
1002 return fcfg.charge[d.class] || -12000;
1003 }
Simon Hunt99c13842014-11-06 18:23:12 -08001004 function ldist(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08001005 return fcfg.linkDistance[d.type] || 50;
Simon Hunt99c13842014-11-06 18:23:12 -08001006 }
1007 function lstrg(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08001008 // 0.0 - 1.0
1009 return fcfg.linkStrength[d.type] || 1.0;
Simon Hunt99c13842014-11-06 18:23:12 -08001010 }
1011
Simon Hunt1a9eff92014-11-07 11:06:34 -08001012 function selectCb(d, self) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001013 selectObject(d, self);
Simon Hunt1a9eff92014-11-07 11:06:34 -08001014 }
1015
1016 function atDragEnd(d, self) {
Simon Hunt56d51852014-11-09 13:03:35 -08001017 // once we've finished moving, pin the node in position
1018 d.fixed = true;
1019 d3.select(self).classed('fixed', true);
1020 if (config.useLiveData) {
1021 tellServerCoords(d);
Simon Hunt1a9eff92014-11-07 11:06:34 -08001022 }
1023 }
1024
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001025 function tellServerCoords(d) {
1026 sendMessage('updateMeta', {
1027 id: d.id,
1028 'class': d.class,
Simon Hunt5f36d342014-11-08 21:33:14 -08001029 x: Math.floor(d.x),
1030 y: Math.floor(d.y)
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001031 });
1032 }
1033
Simon Huntc7ee0662014-11-05 16:44:37 -08001034 // set up the force layout
1035 network.force = d3.layout.force()
1036 .size(forceDim)
1037 .nodes(network.nodes)
1038 .links(network.links)
Simon Hunt7cd48f32014-11-09 23:42:50 -08001039 .gravity(0.4)
1040 .friction(0.7)
1041 .charge(chrg)
Simon Hunt99c13842014-11-06 18:23:12 -08001042 .linkDistance(ldist)
1043 .linkStrength(lstrg)
Simon Huntc7ee0662014-11-05 16:44:37 -08001044 .on('tick', tick);
Simon Hunt195cb382014-11-03 17:50:51 -08001045
Simon Hunt1a9eff92014-11-07 11:06:34 -08001046 network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
1047 }
Simon Hunt195cb382014-11-03 17:50:51 -08001048
Simon Hunt56d51852014-11-09 13:03:35 -08001049 function load(view, ctx, flags) {
Simon Hunt99c13842014-11-06 18:23:12 -08001050 // cache the view token, so network topo functions can access it
1051 network.view = view;
Simon Hunt56d51852014-11-09 13:03:35 -08001052 config.useLiveData = !flags.local;
1053
1054 if (!config.useLiveData) {
1055 prepareScenario(view, ctx, flags.debug);
1056 }
Simon Hunt99c13842014-11-06 18:23:12 -08001057
1058 // set our radio buttons and key bindings
Simon Hunt934c3ce2014-11-05 11:45:07 -08001059 view.setRadio(btnSet);
1060 view.setKeys(keyDispatch);
Simon Hunt195cb382014-11-03 17:50:51 -08001061
Simon Hunt50128c02014-11-08 13:36:15 -08001062 if (config.useLiveData) {
1063 webSock.connect();
1064 }
Simon Hunt195cb382014-11-03 17:50:51 -08001065 }
1066
Simon Hunt142d0032014-11-04 20:13:09 -08001067 function resize(view, ctx) {
Simon Hunt934c3ce2014-11-05 11:45:07 -08001068 setSize(svg, view);
1069 setSize(bgImg, view);
Simon Hunt99c13842014-11-06 18:23:12 -08001070
1071 // TODO: hook to recompute layout, perhaps? work with zoom/pan code
1072 // adjust force layout size
Simon Hunt142d0032014-11-04 20:13:09 -08001073 }
1074
1075
1076 // ==============================
1077 // View registration
Simon Hunt195cb382014-11-03 17:50:51 -08001078
Simon Hunt25248912014-11-04 11:25:48 -08001079 onos.ui.addView('topo', {
Simon Hunt142d0032014-11-04 20:13:09 -08001080 preload: preload,
1081 load: load,
1082 resize: resize
Simon Hunt195cb382014-11-03 17:50:51 -08001083 });
1084
Simon Hunt195cb382014-11-03 17:50:51 -08001085}(ONOS));