blob: c83118820c7093773160864687195208d2c53c89 [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
Simon Huntbb282f52014-11-10 11:08:19 -080027 var d3u = onos.lib.d3util,
28 trace;
Simon Hunt1a9eff92014-11-07 11:06:34 -080029
Simon Hunt195cb382014-11-03 17:50:51 -080030 // configuration data
31 var config = {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -080032 useLiveData: true,
Simon Huntfc274c92014-11-11 11:05:46 -080033 fnTrace: true,
Simon Hunt195cb382014-11-03 17:50:51 -080034 debugOn: false,
35 debug: {
Simon Hunt99c13842014-11-06 18:23:12 -080036 showNodeXY: true,
37 showKeyHandler: false
Simon Hunt195cb382014-11-03 17:50:51 -080038 },
39 options: {
40 layering: true,
41 collisionPrevention: true,
Simon Hunt142d0032014-11-04 20:13:09 -080042 showBackground: true
Simon Hunt195cb382014-11-03 17:50:51 -080043 },
44 backgroundUrl: 'img/us-map.png',
Thomas Vachuska7d638d32014-11-07 10:24:43 -080045 webSockUrl: 'ws/topology',
Simon Hunt195cb382014-11-03 17:50:51 -080046 data: {
47 live: {
48 jsonUrl: 'rs/topology/graph',
49 detailPrefix: 'rs/topology/graph/',
50 detailSuffix: ''
51 },
52 fake: {
53 jsonUrl: 'json/network2.json',
54 detailPrefix: 'json/',
55 detailSuffix: '.json'
56 }
57 },
Simon Hunt99c13842014-11-06 18:23:12 -080058 labels: {
59 imgPad: 16,
60 padLR: 4,
61 padTB: 3,
62 marginLR: 3,
63 marginTB: 2,
64 port: {
65 gap: 3,
66 width: 18,
67 height: 14
68 }
69 },
Simon Hunt1a9eff92014-11-07 11:06:34 -080070 topo: {
71 linkInColor: '#66f',
72 linkInWidth: 14
73 },
Simon Hunt99c13842014-11-06 18:23:12 -080074 icons: {
75 w: 28,
76 h: 28,
77 xoff: -12,
78 yoff: -8
79 },
Simon Hunt195cb382014-11-03 17:50:51 -080080 iconUrl: {
81 device: 'img/device.png',
82 host: 'img/host.png',
83 pkt: 'img/pkt.png',
84 opt: 'img/opt.png'
85 },
Simon Hunt195cb382014-11-03 17:50:51 -080086 force: {
Simon Hunt7cd48f32014-11-09 23:42:50 -080087 note_for_links: 'link.type is used to differentiate',
Simon Huntc7ee0662014-11-05 16:44:37 -080088 linkDistance: {
Simon Hunt7cd48f32014-11-09 23:42:50 -080089 direct: 100,
90 optical: 120,
Simon Huntf67722a2014-11-10 09:32:06 -080091 hostLink: 5
Simon Huntc7ee0662014-11-05 16:44:37 -080092 },
93 linkStrength: {
Simon Hunt7cd48f32014-11-09 23:42:50 -080094 direct: 1.0,
95 optical: 1.0,
96 hostLink: 1.0
Simon Huntc7ee0662014-11-05 16:44:37 -080097 },
Simon Hunt7cd48f32014-11-09 23:42:50 -080098 note_for_nodes: 'node.class is used to differentiate',
Simon Huntc7ee0662014-11-05 16:44:37 -080099 charge: {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800100 device: -8000,
101 host: -300
Simon Huntc7ee0662014-11-05 16:44:37 -0800102 },
103 pad: 20,
Simon Hunt195cb382014-11-03 17:50:51 -0800104 translate: function() {
105 return 'translate(' +
Simon Huntc7ee0662014-11-05 16:44:37 -0800106 config.force.pad + ',' +
107 config.force.pad + ')';
Simon Hunt195cb382014-11-03 17:50:51 -0800108 }
Simon Hunt142d0032014-11-04 20:13:09 -0800109 }
Simon Hunt195cb382014-11-03 17:50:51 -0800110 };
111
Simon Hunt142d0032014-11-04 20:13:09 -0800112 // radio buttons
113 var btnSet = [
Simon Hunt934c3ce2014-11-05 11:45:07 -0800114 { text: 'All Layers', cb: showAllLayers },
115 { text: 'Packet Only', cb: showPacketLayer },
116 { text: 'Optical Only', cb: showOpticalLayer }
117 ];
118
119 // key bindings
120 var keyDispatch = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800121 M: testMe, // TODO: remove (testing only)
Simon Hunt50128c02014-11-08 13:36:15 -0800122 S: injectStartupEvents, // TODO: remove (testing only)
123 space: injectTestEvent, // TODO: remove (testing only)
Simon Hunt99c13842014-11-06 18:23:12 -0800124
Thomas Vachuska65368e32014-11-08 16:10:20 -0800125 B: toggleBg, // TODO: do we really need this?
Simon Hunt934c3ce2014-11-05 11:45:07 -0800126 L: cycleLabels,
127 P: togglePorts,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800128 U: unpin,
129
Simon Huntac9e24f2014-11-12 10:12:21 -0800130 Z: requestPath,
131 X: cancelMonitor
Simon Hunt934c3ce2014-11-05 11:45:07 -0800132 };
Simon Hunt142d0032014-11-04 20:13:09 -0800133
Simon Hunt195cb382014-11-03 17:50:51 -0800134 // state variables
Simon Hunt99c13842014-11-06 18:23:12 -0800135 var network = {
Simon Hunt50128c02014-11-08 13:36:15 -0800136 view: null, // view token reference
Simon Hunt99c13842014-11-06 18:23:12 -0800137 nodes: [],
138 links: [],
139 lookup: {}
140 },
Simon Hunt56d51852014-11-09 13:03:35 -0800141 scenario = {
142 evDir: 'json/ev/',
143 evScenario: '/scenario.json',
144 evPrefix: '/ev_',
145 evOnos: '_onos.json',
146 evUi: '_ui.json',
147 ctx: null,
148 params: {},
149 evNumber: 0,
150 view: null,
151 debug: false
152 },
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800153 webSock,
Simon Hunt0c6d4192014-11-12 12:07:10 -0800154 sid = 0,
Simon Hunt56d51852014-11-09 13:03:35 -0800155 deviceLabelIndex = 0,
156 hostLabelIndex = 0,
Simon Hunt61d04042014-11-11 17:27:16 -0800157 detailPane,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800158 selectOrder = [],
159 selections = {},
160
Simon Hunt195cb382014-11-03 17:50:51 -0800161 highlighted = null,
162 hovered = null,
163 viewMode = 'showAll',
164 portLabelsOn = false;
165
Simon Hunt934c3ce2014-11-05 11:45:07 -0800166 // D3 selections
167 var svg,
168 bgImg,
Simon Huntc7ee0662014-11-05 16:44:37 -0800169 topoG,
170 nodeG,
171 linkG,
172 node,
Simon Hunt0c6d4192014-11-12 12:07:10 -0800173 link,
174 mask;
Simon Hunt195cb382014-11-03 17:50:51 -0800175
Simon Hunt142d0032014-11-04 20:13:09 -0800176 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800177 // For Debugging / Development
Simon Hunt195cb382014-11-03 17:50:51 -0800178
Simon Hunt99c13842014-11-06 18:23:12 -0800179 function note(label, msg) {
180 console.log('NOTE: ' + label + ': ' + msg);
Simon Hunt195cb382014-11-03 17:50:51 -0800181 }
182
Simon Hunt99c13842014-11-06 18:23:12 -0800183 function debug(what) {
184 return config.debugOn && config.debug[what];
Simon Hunt934c3ce2014-11-05 11:45:07 -0800185 }
186
Simon Huntfc274c92014-11-11 11:05:46 -0800187 function fnTrace(msg, id) {
188 if (config.fnTrace) {
189 console.log('FN: ' + msg + ' [' + id + ']');
190 }
191 }
Simon Hunt99c13842014-11-06 18:23:12 -0800192
Simon Hunt934c3ce2014-11-05 11:45:07 -0800193 // ==============================
194 // Key Callbacks
195
Simon Hunt99c13842014-11-06 18:23:12 -0800196 function testMe(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800197 view.alert('test');
Simon Hunt99c13842014-11-06 18:23:12 -0800198 }
199
Simon Hunt56d51852014-11-09 13:03:35 -0800200 function abortIfLive() {
Simon Hunt50128c02014-11-08 13:36:15 -0800201 if (config.useLiveData) {
Simon Hunt56d51852014-11-09 13:03:35 -0800202 scenario.view.alert("Sorry, currently using live data..");
203 return true;
Simon Hunt50128c02014-11-08 13:36:15 -0800204 }
Simon Hunt56d51852014-11-09 13:03:35 -0800205 return false;
206 }
Simon Hunt50128c02014-11-08 13:36:15 -0800207
Simon Hunt56d51852014-11-09 13:03:35 -0800208 function testDebug(msg) {
209 if (scenario.debug) {
210 scenario.view.alert(msg);
211 }
212 }
Simon Hunt99c13842014-11-06 18:23:12 -0800213
Simon Hunt56d51852014-11-09 13:03:35 -0800214 function injectTestEvent(view) {
215 if (abortIfLive()) { return; }
216 var sc = scenario,
217 evn = ++sc.evNumber,
218 pfx = sc.evDir + sc.ctx + sc.evPrefix + evn,
219 onosUrl = pfx + sc.evOnos,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800220 uiUrl = pfx + sc.evUi,
221 stack = [
222 { url: onosUrl, cb: handleServerEvent },
223 { url: uiUrl, cb: handleUiEvent }
224 ];
225 recurseFetchEvent(stack, evn);
Simon Hunt56d51852014-11-09 13:03:35 -0800226 }
227
Simon Hunt7cd48f32014-11-09 23:42:50 -0800228 function recurseFetchEvent(stack, evn) {
229 var v = scenario.view,
230 frame;
231 if (stack.length === 0) {
Simon Huntfc274c92014-11-11 11:05:46 -0800232 v.alert('Oops!\n\nNo event #' + evn + ' found.');
Simon Hunt7cd48f32014-11-09 23:42:50 -0800233 return;
234 }
235 frame = stack.shift();
236
237 d3.json(frame.url, function (err, data) {
Simon Hunt99c13842014-11-06 18:23:12 -0800238 if (err) {
Simon Hunt56d51852014-11-09 13:03:35 -0800239 if (err.status === 404) {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800240 // if we didn't find the data, try the next stack frame
241 recurseFetchEvent(stack, evn);
Simon Hunt56d51852014-11-09 13:03:35 -0800242 } else {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800243 v.alert('non-404 error:\n\n' + frame.url + '\n\n' + err);
Simon Hunt56d51852014-11-09 13:03:35 -0800244 }
Simon Hunt99c13842014-11-06 18:23:12 -0800245 } else {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800246 testDebug('loaded: ' + frame.url);
247 frame.cb(data);
Simon Hunt99c13842014-11-06 18:23:12 -0800248 }
249 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800250
Simon Hunt56d51852014-11-09 13:03:35 -0800251 }
Simon Hunt50128c02014-11-08 13:36:15 -0800252
Simon Hunt56d51852014-11-09 13:03:35 -0800253 function handleUiEvent(data) {
Simon Huntbb282f52014-11-10 11:08:19 -0800254 scenario.view.alert('UI Tx: ' + data.event + '\n\n' +
255 JSON.stringify(data));
Simon Hunt56d51852014-11-09 13:03:35 -0800256 }
257
258 function injectStartupEvents(view) {
259 var last = scenario.params.lastAuto || 0;
260 if (abortIfLive()) { return; }
261
262 while (scenario.evNumber < last) {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800263 injectTestEvent(view);
264 }
265 }
266
Simon Hunt934c3ce2014-11-05 11:45:07 -0800267 function toggleBg() {
268 var vis = bgImg.style('visibility');
269 bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
270 }
271
Simon Hunt99c13842014-11-06 18:23:12 -0800272 function cycleLabels() {
Simon Huntbb282f52014-11-10 11:08:19 -0800273 deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1)
274 ? 0 : deviceLabelIndex + 1;
Simon Hunt5f36d342014-11-08 21:33:14 -0800275
Simon Hunt99c13842014-11-06 18:23:12 -0800276 network.nodes.forEach(function (d) {
Simon Huntbb282f52014-11-10 11:08:19 -0800277 if (d.class === 'device') {
278 updateDeviceLabel(d);
279 }
Simon Hunt99c13842014-11-06 18:23:12 -0800280 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800281 }
282
283 function togglePorts(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800284 view.alert('togglePorts() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800285 }
286
287 function unpin(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800288 view.alert('unpin() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800289 }
290
291 // ==============================
292 // Radio Button Callbacks
293
Simon Hunt195cb382014-11-03 17:50:51 -0800294 function showAllLayers() {
Simon Hunt142d0032014-11-04 20:13:09 -0800295// network.node.classed('inactive', false);
296// network.link.classed('inactive', false);
297// d3.selectAll('svg .port').classed('inactive', false);
298// d3.selectAll('svg .portText').classed('inactive', false);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800299 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800300 network.view.alert('showAllLayers() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800301 }
302
303 function showPacketLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800304 showAllLayers();
305 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800306 network.view.alert('showPacketLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800307 }
308
309 function showOpticalLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800310 showAllLayers();
311 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800312 network.view.alert('showOpticalLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800313 }
314
Simon Hunt142d0032014-11-04 20:13:09 -0800315 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800316 // Private functions
317
Simon Hunt99c13842014-11-06 18:23:12 -0800318 function safeId(s) {
319 return s.replace(/[^a-z0-9]/gi, '-');
320 }
321
Simon Huntc7ee0662014-11-05 16:44:37 -0800322 // set the size of the given element to that of the view (reduced if padded)
323 function setSize(el, view, pad) {
324 var padding = pad ? pad * 2 : 0;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800325 el.attr({
Simon Huntc7ee0662014-11-05 16:44:37 -0800326 width: view.width() - padding,
327 height: view.height() - padding
Simon Hunt934c3ce2014-11-05 11:45:07 -0800328 });
329 }
330
Simon Hunt934c3ce2014-11-05 11:45:07 -0800331
Simon Hunt99c13842014-11-06 18:23:12 -0800332 // ==============================
333 // Event handlers for server-pushed events
334
Simon Huntbb282f52014-11-10 11:08:19 -0800335 function logicError(msg) {
336 // TODO, report logic error to server, via websock, so it can be logged
337 network.view.alert('Logic Error:\n\n' + msg);
Simon Huntfc274c92014-11-11 11:05:46 -0800338 console.warn(msg);
Simon Huntbb282f52014-11-10 11:08:19 -0800339 }
340
Simon Hunt99c13842014-11-06 18:23:12 -0800341 var eventDispatch = {
342 addDevice: addDevice,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800343 addLink: addLink,
Simon Hunt56d51852014-11-09 13:03:35 -0800344 addHost: addHost,
Simon Huntbb282f52014-11-10 11:08:19 -0800345 updateDevice: updateDevice,
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800346 updateLink: updateLink,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800347 updateHost: updateHost,
Simon Huntbb282f52014-11-10 11:08:19 -0800348 removeDevice: stillToImplement,
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800349 removeLink: removeLink,
Simon Hunt44031102014-11-11 13:20:36 -0800350 removeHost: removeHost,
Simon Hunt61d04042014-11-11 17:27:16 -0800351 showDetails: showDetails,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800352 showPath: showPath
Simon Hunt99c13842014-11-06 18:23:12 -0800353 };
354
355 function addDevice(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800356 fnTrace('addDevice', data.payload.id);
Simon Hunt99c13842014-11-06 18:23:12 -0800357 var device = data.payload,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800358 nodeData = createDeviceNode(device);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800359 network.nodes.push(nodeData);
360 network.lookup[nodeData.id] = nodeData;
Simon Hunt99c13842014-11-06 18:23:12 -0800361 updateNodes();
362 network.force.start();
363 }
364
Simon Hunt99c13842014-11-06 18:23:12 -0800365 function addLink(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800366 fnTrace('addLink', data.payload.id);
Simon Hunt99c13842014-11-06 18:23:12 -0800367 var link = data.payload,
368 lnk = createLink(link);
Simon Hunt99c13842014-11-06 18:23:12 -0800369 if (lnk) {
Simon Hunt99c13842014-11-06 18:23:12 -0800370 network.links.push(lnk);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800371 network.lookup[lnk.id] = lnk;
Simon Hunt99c13842014-11-06 18:23:12 -0800372 updateLinks();
373 network.force.start();
374 }
375 }
376
Simon Hunt56d51852014-11-09 13:03:35 -0800377 function addHost(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800378 fnTrace('addHost', data.payload.id);
Simon Hunt56d51852014-11-09 13:03:35 -0800379 var host = data.payload,
380 node = createHostNode(host),
381 lnk;
Simon Hunt56d51852014-11-09 13:03:35 -0800382 network.nodes.push(node);
383 network.lookup[host.id] = node;
384 updateNodes();
385
386 lnk = createHostLink(host);
387 if (lnk) {
Simon Hunt44031102014-11-11 13:20:36 -0800388 node.linkData = lnk; // cache ref on its host
Simon Hunt56d51852014-11-09 13:03:35 -0800389 network.links.push(lnk);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800390 network.lookup[host.ingress] = lnk;
391 network.lookup[host.egress] = lnk;
Simon Hunt56d51852014-11-09 13:03:35 -0800392 updateLinks();
393 }
394 network.force.start();
395 }
396
Simon Hunt44031102014-11-11 13:20:36 -0800397 // TODO: fold updateX(...) methods into one base method; remove duplication
Simon Huntbb282f52014-11-10 11:08:19 -0800398 function updateDevice(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800399 fnTrace('updateDevice', data.payload.id);
Simon Huntbb282f52014-11-10 11:08:19 -0800400 var device = data.payload,
401 id = device.id,
402 nodeData = network.lookup[id];
403 if (nodeData) {
404 $.extend(nodeData, device);
405 updateDeviceState(nodeData);
406 } else {
407 logicError('updateDevice lookup fail. ID = "' + id + '"');
408 }
409 }
410
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800411 function updateLink(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800412 fnTrace('updateLink', data.payload.id);
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800413 var link = data.payload,
414 id = link.id,
415 linkData = network.lookup[id];
416 if (linkData) {
417 $.extend(linkData, link);
418 updateLinkState(linkData);
419 } else {
420 logicError('updateLink lookup fail. ID = "' + id + '"');
421 }
422 }
423
Simon Hunt7cd48f32014-11-09 23:42:50 -0800424 function updateHost(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800425 fnTrace('updateHost', data.payload.id);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800426 var host = data.payload,
Simon Huntbb282f52014-11-10 11:08:19 -0800427 id = host.id,
428 hostData = network.lookup[id];
429 if (hostData) {
430 $.extend(hostData, host);
431 updateHostState(hostData);
432 } else {
433 logicError('updateHost lookup fail. ID = "' + id + '"');
434 }
Simon Hunt7cd48f32014-11-09 23:42:50 -0800435 }
436
Simon Hunt44031102014-11-11 13:20:36 -0800437 // TODO: fold removeX(...) methods into base method - remove dup code
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800438 function removeLink(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800439 fnTrace('removeLink', data.payload.id);
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800440 var link = data.payload,
441 id = link.id,
442 linkData = network.lookup[id];
443 if (linkData) {
444 removeLinkElement(linkData);
445 } else {
446 logicError('removeLink lookup fail. ID = "' + id + '"');
447 }
448 }
449
Simon Hunt44031102014-11-11 13:20:36 -0800450 function removeHost(data) {
451 fnTrace('removeHost', data.payload.id);
452 var host = data.payload,
453 id = host.id,
454 hostData = network.lookup[id];
455 if (hostData) {
456 removeHostElement(hostData);
457 } else {
458 logicError('removeHost lookup fail. ID = "' + id + '"');
459 }
460 }
461
Simon Hunt61d04042014-11-11 17:27:16 -0800462 function showDetails(data) {
463 fnTrace('showDetails', data.payload.id);
464 populateDetails(data.payload);
465 detailPane.show();
466 }
467
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800468 function showPath(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800469 fnTrace('showPath', data.payload.id);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800470 var links = data.payload.links,
471 s = [ data.event + "\n" + links.length ];
472 links.forEach(function (d, i) {
473 s.push(d);
474 });
475 network.view.alert(s.join('\n'));
476
477 links.forEach(function (d, i) {
478 var link = network.lookup[d];
479 if (link) {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800480 link.el.classed('showPath', true);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800481 }
482 });
483
484 // TODO: add selection-highlite lines to links
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800485 }
486
Simon Hunt56d51852014-11-09 13:03:35 -0800487 // ...............................
488
489 function stillToImplement(data) {
490 var p = data.payload;
491 note(data.event, p.id);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800492 network.view.alert('Not yet implemented: "' + data.event + '"');
Simon Hunt56d51852014-11-09 13:03:35 -0800493 }
Simon Hunt99c13842014-11-06 18:23:12 -0800494
495 function unknownEvent(data) {
Simon Hunt50128c02014-11-08 13:36:15 -0800496 network.view.alert('Unknown event type: "' + data.event + '"');
Simon Hunt99c13842014-11-06 18:23:12 -0800497 }
498
499 function handleServerEvent(data) {
500 var fn = eventDispatch[data.event] || unknownEvent;
501 fn(data);
502 }
503
504 // ==============================
Simon Hunt61d04042014-11-11 17:27:16 -0800505 // Out-going messages...
506
507 function getSel(idx) {
508 return selections[selectOrder[idx]];
509 }
510
511 // for now, just a host-to-host intent, (and implicit start-monitoring)
512 function requestPath() {
513 var payload = {
514 one: getSel(0).obj.id,
515 two: getSel(1).obj.id
516 };
517 sendMessage('requestPath', payload);
518 }
519
Simon Huntac9e24f2014-11-12 10:12:21 -0800520 function cancelMonitor() {
521 var payload = {
522 id: "need_the_intent_id" // FIXME: where are we storing this?
523 };
524 sendMessage('cancelMonitor', payload);
525 }
526
Simon Hunt61d04042014-11-11 17:27:16 -0800527 // request details for the selected element
528 function requestDetails() {
529 var data = getSel(0).obj,
530 payload = {
531 id: data.id,
532 class: data.class
533 };
534 sendMessage('requestDetails', payload);
535 }
536
537 // ==============================
Simon Hunt99c13842014-11-06 18:23:12 -0800538 // force layout modification functions
539
540 function translate(x, y) {
541 return 'translate(' + x + ',' + y + ')';
542 }
543
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800544 function missMsg(what, id) {
545 return '\n[' + what + '] "' + id + '" missing ';
546 }
547
548 function linkEndPoints(srcId, dstId) {
549 var srcNode = network.lookup[srcId],
550 dstNode = network.lookup[dstId],
551 sMiss = !srcNode ? missMsg('src', srcId) : '',
552 dMiss = !dstNode ? missMsg('dst', dstId) : '';
553
554 if (sMiss || dMiss) {
555 logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
556 return null;
557 }
558 return {
559 source: srcNode,
560 target: dstNode,
561 x1: srcNode.x,
562 y1: srcNode.y,
563 x2: dstNode.x,
564 y2: dstNode.y
565 };
566 }
567
Simon Hunt56d51852014-11-09 13:03:35 -0800568 function createHostLink(host) {
569 var src = host.id,
570 dst = host.cp.device,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800571 id = host.ingress,
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800572 lnk = linkEndPoints(src, dst);
Simon Hunt56d51852014-11-09 13:03:35 -0800573
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800574 if (!lnk) {
Simon Hunt56d51852014-11-09 13:03:35 -0800575 return null;
576 }
577
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800578 // Synthesize link ...
579 $.extend(lnk, {
Thomas Vachuska4830d392014-11-09 17:09:56 -0800580 id: id,
Simon Hunt56d51852014-11-09 13:03:35 -0800581 class: 'link',
Simon Hunt7cd48f32014-11-09 23:42:50 -0800582 type: 'hostLink',
Simon Hunt56d51852014-11-09 13:03:35 -0800583 svgClass: 'link hostLink',
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800584 linkWidth: 1
Simon Hunt7cd48f32014-11-09 23:42:50 -0800585 });
Simon Hunt99c13842014-11-06 18:23:12 -0800586 return lnk;
587 }
588
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800589 function createLink(link) {
590 var lnk = linkEndPoints(link.src, link.dst),
591 type = link.type;
592
593 if (!lnk) {
594 return null;
595 }
596
597 // merge in remaining data
598 $.extend(lnk, link, {
599 class: 'link',
600 svgClass: type ? 'link ' + type : 'link'
601 });
602 return lnk;
Simon Hunt1a9eff92014-11-07 11:06:34 -0800603 }
604
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800605 var widthRatio = 1.4,
606 linkScale = d3.scale.linear()
607 .domain([1, 12])
608 .range([widthRatio, 12 * widthRatio])
609 .clamp(true);
610
611 function updateLinkWidth (d) {
612 // TODO: watch out for .showPath/.showTraffic classes
613 d.el.transition()
614 .duration(1000)
615 .attr('stroke-width', linkScale(d.linkWidth));
616 }
617
618
Simon Hunt99c13842014-11-06 18:23:12 -0800619 function updateLinks() {
620 link = linkG.selectAll('.link')
621 .data(network.links, function (d) { return d.id; });
622
623 // operate on existing links, if necessary
624 // link .foo() .bar() ...
625
626 // operate on entering links:
627 var entering = link.enter()
628 .append('line')
629 .attr({
Simon Hunt99c13842014-11-06 18:23:12 -0800630 class: function (d) { return d.svgClass; },
631 x1: function (d) { return d.x1; },
632 y1: function (d) { return d.y1; },
633 x2: function (d) { return d.x2; },
634 y2: function (d) { return d.y2; },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800635 stroke: config.topo.linkInColor,
636 'stroke-width': config.topo.linkInWidth
Simon Hunt99c13842014-11-06 18:23:12 -0800637 })
638 .transition().duration(1000)
639 .attr({
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800640 'stroke-width': function (d) { return linkScale(d.linkWidth); },
Simon Hunt99c13842014-11-06 18:23:12 -0800641 stroke: '#666' // TODO: remove explicit stroke, rather...
642 });
643
644 // augment links
Simon Hunt7cd48f32014-11-09 23:42:50 -0800645 entering.each(function (d) {
646 var link = d3.select(this);
647 // provide ref to element selection from backing data....
648 d.el = link;
Simon Hunt99c13842014-11-06 18:23:12 -0800649
Simon Hunt7cd48f32014-11-09 23:42:50 -0800650 // TODO: add src/dst port labels etc.
651 });
Thomas Vachuska4830d392014-11-09 17:09:56 -0800652
653 // operate on both existing and new links, if necessary
654 //link .foo() .bar() ...
655
656 // operate on exiting links:
Thomas Vachuska4830d392014-11-09 17:09:56 -0800657 link.exit()
Thomas Vachuska4830d392014-11-09 17:09:56 -0800658 .attr({
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800659 'stroke-dasharray': '3, 3'
Thomas Vachuska4830d392014-11-09 17:09:56 -0800660 })
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800661 .style('opacity', 0.4)
662 .transition()
Simon Huntea80eb42014-11-11 13:46:57 -0800663 .duration(1500)
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800664 .attr({
665 'stroke-dasharray': '3, 12'
666 })
667 .transition()
Simon Huntea80eb42014-11-11 13:46:57 -0800668 .duration(500)
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800669 .style('opacity', 0.0)
Thomas Vachuska4830d392014-11-09 17:09:56 -0800670 .remove();
Simon Hunt99c13842014-11-06 18:23:12 -0800671 }
672
673 function createDeviceNode(device) {
674 // start with the object as is
675 var node = device,
Simon Huntbb282f52014-11-10 11:08:19 -0800676 type = device.type,
677 svgCls = type ? 'node device ' + type : 'node device';
Simon Hunt99c13842014-11-06 18:23:12 -0800678
679 // Augment as needed...
680 node.class = 'device';
Simon Huntbb282f52014-11-10 11:08:19 -0800681 node.svgClass = device.online ? svgCls + ' online' : svgCls;
Simon Hunt99c13842014-11-06 18:23:12 -0800682 positionNode(node);
683
684 // cache label array length
685 network.deviceLabelCount = device.labels.length;
Simon Hunt99c13842014-11-06 18:23:12 -0800686 return node;
687 }
688
Simon Hunt56d51852014-11-09 13:03:35 -0800689 function createHostNode(host) {
690 // start with the object as is
691 var node = host;
692
693 // Augment as needed...
694 node.class = 'host';
Simon Hunt7cd48f32014-11-09 23:42:50 -0800695 if (!node.type) {
696 // TODO: perhaps type would be: {phone, tablet, laptop, endstation} ?
697 node.type = 'endstation';
698 }
Simon Hunt56d51852014-11-09 13:03:35 -0800699 node.svgClass = 'node host';
700 // TODO: consider placing near its switch, if [x,y] not defined
701 positionNode(node);
702
703 // cache label array length
704 network.hostLabelCount = host.labels.length;
Simon Hunt56d51852014-11-09 13:03:35 -0800705 return node;
706 }
707
Simon Hunt99c13842014-11-06 18:23:12 -0800708 function positionNode(node) {
709 var meta = node.metaUi,
Simon Huntac9e24f2014-11-12 10:12:21 -0800710 x = meta && meta.x,
711 y = meta && meta.y,
712 xy;
Simon Hunt99c13842014-11-06 18:23:12 -0800713
Simon Huntac9e24f2014-11-12 10:12:21 -0800714 // If we have [x,y] already, use that...
Simon Hunt99c13842014-11-06 18:23:12 -0800715 if (x && y) {
716 node.fixed = true;
Simon Huntac9e24f2014-11-12 10:12:21 -0800717 node.x = x;
718 node.y = y;
719 return;
Simon Hunt99c13842014-11-06 18:23:12 -0800720 }
Simon Huntac9e24f2014-11-12 10:12:21 -0800721
722 // Note: Placing incoming unpinned nodes at exactly the same point
723 // (center of the view) causes them to explode outwards when
724 // the force layout kicks in. So, we spread them out a bit
725 // initially, to provide a more serene layout convergence.
726 // Additionally, if the node is a host, we place it near
727 // the device it is connected to.
728
729 function spread(s) {
730 return Math.floor((Math.random() * s) - s/2);
731 }
732
733 function randDim(dim) {
734 return dim / 2 + spread(dim * 0.7071);
735 }
736
737 function rand() {
738 return {
739 x: randDim(network.view.width()),
740 y: randDim(network.view.height())
741 };
742 }
743
744 function near(node) {
745 var min = 12,
746 dx = spread(12),
747 dy = spread(12);
748 return {
749 x: node.x + min + dx,
750 y: node.y + min + dy
751 };
752 }
753
754 function getDevice(cp) {
755 var d = network.lookup[cp.device];
756 return d || rand();
757 }
758
759 xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
760 $.extend(node, xy);
Simon Hunt99c13842014-11-06 18:23:12 -0800761 }
762
Simon Hunt99c13842014-11-06 18:23:12 -0800763 function iconUrl(d) {
764 return 'img/' + d.type + '.png';
765 }
766
767 // returns the newly computed bounding box of the rectangle
768 function adjustRectToFitText(n) {
769 var text = n.select('text'),
770 box = text.node().getBBox(),
771 lab = config.labels;
772
773 text.attr('text-anchor', 'middle')
774 .attr('y', '-0.8em')
775 .attr('x', lab.imgPad/2);
776
777 // translate the bbox so that it is centered on [x,y]
778 box.x = -box.width / 2;
779 box.y = -box.height / 2;
780
781 // add padding
782 box.x -= (lab.padLR + lab.imgPad/2);
783 box.width += lab.padLR * 2 + lab.imgPad;
784 box.y -= lab.padTB;
785 box.height += lab.padTB * 2;
786
787 return box;
788 }
789
Simon Hunt1a9eff92014-11-07 11:06:34 -0800790 function mkSvgClass(d) {
791 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
792 }
793
Simon Hunt7cd48f32014-11-09 23:42:50 -0800794 function hostLabel(d) {
795 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
796 return d.labels[idx];
797 }
798 function deviceLabel(d) {
799 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
800 return d.labels[idx];
801 }
802 function niceLabel(label) {
803 return (label && label.trim()) ? label : '.';
804 }
805
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800806 function updateDeviceLabel(d) {
807 var label = niceLabel(deviceLabel(d)),
808 node = d.el,
809 box;
810
811 node.select('text')
812 .text(label)
813 .style('opacity', 0)
814 .transition()
815 .style('opacity', 1);
816
817 box = adjustRectToFitText(node);
818
819 node.select('rect')
820 .transition()
821 .attr(box);
822
823 node.select('image')
824 .transition()
825 .attr('x', box.x + config.icons.xoff)
826 .attr('y', box.y + config.icons.yoff);
827 }
828
829 function updateHostLabel(d) {
830 var label = hostLabel(d),
831 host = d.el;
832
833 host.select('text').text(label);
834 }
835
Simon Huntbb282f52014-11-10 11:08:19 -0800836 function updateDeviceState(nodeData) {
837 nodeData.el.classed('online', nodeData.online);
838 updateDeviceLabel(nodeData);
839 // TODO: review what else might need to be updated
840 }
841
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800842 function updateLinkState(linkData) {
843 updateLinkWidth(linkData);
844 // TODO: review what else might need to be updated
845 // update label, if showing
846 }
847
Simon Huntbb282f52014-11-10 11:08:19 -0800848 function updateHostState(hostData) {
849 updateHostLabel(hostData);
850 // TODO: review what else might need to be updated
851 }
852
853
Simon Hunt99c13842014-11-06 18:23:12 -0800854 function updateNodes() {
855 node = nodeG.selectAll('.node')
856 .data(network.nodes, function (d) { return d.id; });
857
858 // operate on existing nodes, if necessary
Simon Hunt7cd48f32014-11-09 23:42:50 -0800859 // update host labels
Simon Hunt99c13842014-11-06 18:23:12 -0800860 //node .foo() .bar() ...
861
862 // operate on entering nodes:
863 var entering = node.enter()
864 .append('g')
865 .attr({
866 id: function (d) { return safeId(d.id); },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800867 class: mkSvgClass,
Simon Hunt99c13842014-11-06 18:23:12 -0800868 transform: function (d) { return translate(d.x, d.y); },
869 opacity: 0
870 })
Simon Hunt1a9eff92014-11-07 11:06:34 -0800871 .call(network.drag)
Simon Hunt99c13842014-11-06 18:23:12 -0800872 //.on('mouseover', function (d) {})
873 //.on('mouseover', function (d) {})
874 .transition()
875 .attr('opacity', 1);
876
877 // augment device nodes...
878 entering.filter('.device').each(function (d) {
879 var node = d3.select(this),
880 icon = iconUrl(d),
Simon Hunt7cd48f32014-11-09 23:42:50 -0800881 label = niceLabel(deviceLabel(d)),
Simon Hunt99c13842014-11-06 18:23:12 -0800882 box;
883
Simon Hunt7cd48f32014-11-09 23:42:50 -0800884 // provide ref to element from backing data....
885 d.el = node;
886
Simon Hunt99c13842014-11-06 18:23:12 -0800887 node.append('rect')
888 .attr({
889 'rx': 5,
890 'ry': 5
891 });
892
893 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -0800894 .text(label)
Simon Hunt99c13842014-11-06 18:23:12 -0800895 .attr('dy', '1.1em');
896
897 box = adjustRectToFitText(node);
898
899 node.select('rect')
900 .attr(box);
901
902 if (icon) {
903 var cfg = config.icons;
904 node.append('svg:image')
905 .attr({
906 x: box.x + config.icons.xoff,
907 y: box.y + config.icons.yoff,
908 width: cfg.w,
909 height: cfg.h,
910 'xlink:href': icon
911 });
912 }
913
914 // debug function to show the modelled x,y coordinates of nodes...
915 if (debug('showNodeXY')) {
916 node.select('rect').attr('fill-opacity', 0.5);
917 node.append('circle')
918 .attr({
919 class: 'debug',
920 cx: 0,
921 cy: 0,
922 r: '3px'
923 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800924 }
925 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800926
Simon Hunt56d51852014-11-09 13:03:35 -0800927 // augment host nodes...
928 entering.filter('.host').each(function (d) {
929 var node = d3.select(this),
Simon Hunt56d51852014-11-09 13:03:35 -0800930 box;
931
Simon Hunt7cd48f32014-11-09 23:42:50 -0800932 // provide ref to element from backing data....
933 d.el = node;
934
Simon Hunt56d51852014-11-09 13:03:35 -0800935 node.append('circle')
936 .attr('r', 8); // TODO: define host circle radius
937
938 // TODO: are we attaching labels to hosts?
939 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -0800940 .text(hostLabel)
941 .attr('dy', '1.3em')
942 .attr('text-anchor', 'middle');
Simon Hunt56d51852014-11-09 13:03:35 -0800943
944 // debug function to show the modelled x,y coordinates of nodes...
945 if (debug('showNodeXY')) {
946 node.select('circle').attr('fill-opacity', 0.5);
947 node.append('circle')
948 .attr({
949 class: 'debug',
950 cx: 0,
951 cy: 0,
952 r: '3px'
953 });
954 }
955 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800956
Simon Hunt99c13842014-11-06 18:23:12 -0800957 // operate on both existing and new nodes, if necessary
958 //node .foo() .bar() ...
Simon Huntc7ee0662014-11-05 16:44:37 -0800959
Simon Hunt99c13842014-11-06 18:23:12 -0800960 // operate on exiting nodes:
Simon Huntea80eb42014-11-11 13:46:57 -0800961 // Note that the node is removed after 2 seconds.
962 // Sub element animations should be shorter than 2 seconds.
963 var exiting = node.exit()
Simon Hunt44031102014-11-11 13:20:36 -0800964 .transition()
965 .duration(2000)
Simon Huntea80eb42014-11-11 13:46:57 -0800966 .style('opacity', 0)
Simon Hunt99c13842014-11-06 18:23:12 -0800967 .remove();
Simon Huntea80eb42014-11-11 13:46:57 -0800968
969 // host node exits....
970 exiting.filter('.host').each(function (d) {
971 var node = d3.select(this);
972
973 node.select('text')
974 .style('opacity', 0.5)
975 .transition()
976 .duration(1000)
977 .style('opacity', 0);
978 // note, leave <g>.remove to remove this element
979
980 node.select('circle')
981 .style('stroke-fill', '#555')
982 .style('fill', '#888')
983 .style('opacity', 0.5)
984 .transition()
985 .duration(1500)
986 .attr('r', 0);
987 // note, leave <g>.remove to remove this element
988
989 });
990
991 // TODO: device node exits
Simon Huntc7ee0662014-11-05 16:44:37 -0800992 }
993
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800994 function find(id, array) {
995 for (var idx = 0, n = array.length; idx < n; idx++) {
996 if (array[idx].id === id) {
997 return idx;
998 }
999 }
1000 return -1;
1001 }
1002
1003 function removeLinkElement(linkData) {
1004 // remove from lookup cache
1005 delete network.lookup[linkData.id];
1006 // remove from links array
1007 var idx = find(linkData.id, network.links);
Simon Huntfc274c92014-11-11 11:05:46 -08001008 network.links.splice(idx, 1);
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001009 // remove from SVG
1010 updateLinks();
Simon Hunt44031102014-11-11 13:20:36 -08001011 network.force.resume();
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001012 }
Simon Huntc7ee0662014-11-05 16:44:37 -08001013
Simon Hunt44031102014-11-11 13:20:36 -08001014 function removeHostElement(hostData) {
1015 // first, remove associated hostLink...
1016 removeLinkElement(hostData.linkData);
1017
1018 // remove from lookup cache
1019 delete network.lookup[hostData.id];
1020 // remove from nodes array
1021 var idx = find(hostData.id, network.nodes);
1022 network.nodes.splice(idx, 1);
1023 // remove from SVG
1024 updateNodes();
1025 network.force.resume();
1026 }
1027
1028
Simon Huntc7ee0662014-11-05 16:44:37 -08001029 function tick() {
1030 node.attr({
Simon Hunt99c13842014-11-06 18:23:12 -08001031 transform: function (d) { return translate(d.x, d.y); }
Simon Huntc7ee0662014-11-05 16:44:37 -08001032 });
1033
1034 link.attr({
1035 x1: function (d) { return d.source.x; },
1036 y1: function (d) { return d.source.y; },
1037 x2: function (d) { return d.target.x; },
1038 y2: function (d) { return d.target.y; }
1039 });
1040 }
Simon Hunt934c3ce2014-11-05 11:45:07 -08001041
1042 // ==============================
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001043 // Web-Socket for live data
1044
1045 function webSockUrl() {
1046 return document.location.toString()
1047 .replace(/\#.*/, '')
1048 .replace('http://', 'ws://')
1049 .replace('https://', 'wss://')
1050 .replace('index2.html', config.webSockUrl);
1051 }
1052
1053 webSock = {
1054 ws : null,
1055
1056 connect : function() {
1057 webSock.ws = new WebSocket(webSockUrl());
1058
1059 webSock.ws.onopen = function() {
Simon Hunt0c6d4192014-11-12 12:07:10 -08001060 noWebSock(false);
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001061 };
1062
1063 webSock.ws.onmessage = function(m) {
1064 if (m.data) {
Simon Huntbb282f52014-11-10 11:08:19 -08001065 wsTraceRx(m.data);
Thomas Vachuskad472c6e2014-11-07 19:11:05 -08001066 handleServerEvent(JSON.parse(m.data));
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001067 }
1068 };
1069
1070 webSock.ws.onclose = function(m) {
1071 webSock.ws = null;
Simon Hunt0c6d4192014-11-12 12:07:10 -08001072 noWebSock(true);
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001073 };
1074 },
1075
1076 send : function(text) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001077 if (text != null) {
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001078 webSock._send(text);
1079 }
1080 },
1081
1082 _send : function(message) {
1083 if (webSock.ws) {
1084 webSock.ws.send(message);
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001085 } else {
Simon Hunt56d51852014-11-09 13:03:35 -08001086 network.view.alert('no web socket open\n\n' + message);
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001087 }
1088 }
1089
1090 };
1091
Simon Hunt0c6d4192014-11-12 12:07:10 -08001092 function noWebSock(b) {
1093 mask.style('display',b ? 'block' : 'none');
1094 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001095
Simon Hunt61d04042014-11-11 17:27:16 -08001096 // TODO: use cache of pending messages (key = sid) to reconcile responses
1097
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001098 function sendMessage(evType, payload) {
1099 var toSend = {
Simon Huntbb282f52014-11-10 11:08:19 -08001100 event: evType,
1101 sid: ++sid,
1102 payload: payload
1103 },
1104 asText = JSON.stringify(toSend);
1105 wsTraceTx(asText);
1106 webSock.send(asText);
1107 }
1108
1109 function wsTraceTx(msg) {
1110 wsTrace('tx', msg);
1111 }
1112 function wsTraceRx(msg) {
1113 wsTrace('rx', msg);
1114 }
1115 function wsTrace(rxtx, msg) {
Simon Huntbb282f52014-11-10 11:08:19 -08001116 console.log('[' + rxtx + '] ' + msg);
1117 // TODO: integrate with trace view
1118 //if (trace) {
1119 // trace.output(rxtx, msg);
1120 //}
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001121 }
1122
1123
1124 // ==============================
1125 // Selection stuff
1126
1127 function selectObject(obj, el) {
1128 var n,
1129 meta = d3.event.sourceEvent.metaKey;
1130
1131 if (el) {
1132 n = d3.select(el);
1133 } else {
1134 node.each(function(d) {
1135 if (d == obj) {
1136 n = d3.select(el = this);
1137 }
1138 });
1139 }
1140 if (!n) return;
1141
1142 if (meta && n.classed('selected')) {
1143 deselectObject(obj.id);
Simon Hunt61d04042014-11-11 17:27:16 -08001144 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001145 return;
1146 }
1147
1148 if (!meta) {
1149 deselectAll();
1150 }
1151
Simon Hunt5f36d342014-11-08 21:33:14 -08001152 selections[obj.id] = { obj: obj, el : el};
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001153 selectOrder.push(obj.id);
1154
1155 n.classed('selected', true);
Simon Hunt61d04042014-11-11 17:27:16 -08001156 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001157 }
1158
1159 function deselectObject(id) {
1160 var obj = selections[id];
1161 if (obj) {
1162 d3.select(obj.el).classed('selected', false);
Simon Hunt61d04042014-11-11 17:27:16 -08001163 delete selections[id];
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001164 }
Simon Hunt61d04042014-11-11 17:27:16 -08001165 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001166 }
1167
1168 function deselectAll() {
1169 // deselect all nodes in the network...
1170 node.classed('selected', false);
1171 selections = {};
1172 selectOrder = [];
Simon Hunt61d04042014-11-11 17:27:16 -08001173 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001174 }
1175
Simon Hunt61d04042014-11-11 17:27:16 -08001176 // FIXME: this click handler does not get unloaded when the view does
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001177 $('#view').on('click', function(e) {
1178 if (!$(e.target).closest('.node').length) {
1179 if (!e.metaKey) {
1180 deselectAll();
1181 }
1182 }
1183 });
1184
Simon Hunt61d04042014-11-11 17:27:16 -08001185 // update the state of the detail pane, based on current selections
1186 function updateDetailPane() {
1187 var nSel = selectOrder.length;
1188 if (!nSel) {
1189 detailPane.hide();
1190 } else if (nSel === 1) {
1191 singleSelect();
1192 } else {
1193 multiSelect();
1194 }
1195 }
1196
1197 function singleSelect() {
1198 requestDetails();
1199 // NOTE: detail pane will be shown from showDetails event.
1200 }
1201
1202 function multiSelect() {
1203 // TODO: use detail pane for multi-select view.
1204 //detailPane.show();
1205 }
1206
1207 function populateDetails(data) {
1208 detailPane.empty();
1209
1210 var title = detailPane.append("h2"),
1211 table = detailPane.append("table"),
1212 tbody = table.append("tbody");
1213
1214 $('<img src="img/' + data.type + '.png">').appendTo(title);
1215 $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
1216
1217 data.propOrder.forEach(function(p) {
1218 if (p === '-') {
1219 addSep(tbody);
1220 } else {
1221 addProp(tbody, p, data.props[p]);
1222 }
1223 });
1224
1225 function addSep(tbody) {
1226 var tr = tbody.append('tr');
1227 $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
1228 }
1229
1230 function addProp(tbody, label, value) {
1231 var tr = tbody.append('tr');
1232
1233 tr.append('td')
1234 .attr('class', 'label')
1235 .text(label + ' :');
1236
1237 tr.append('td')
1238 .attr('class', 'value')
1239 .text(value);
1240 }
1241 }
1242
1243 // ==============================
1244 // Test harness code
Simon Hunt56d51852014-11-09 13:03:35 -08001245
1246 function prepareScenario(view, ctx, dbg) {
1247 var sc = scenario,
1248 urlSc = sc.evDir + ctx + sc.evScenario;
1249
1250 if (!ctx) {
1251 view.alert("No scenario specified (null ctx)");
1252 return;
1253 }
1254
1255 sc.view = view;
1256 sc.ctx = ctx;
1257 sc.debug = dbg;
1258 sc.evNumber = 0;
1259
1260 d3.json(urlSc, function(err, data) {
Simon Huntbb282f52014-11-10 11:08:19 -08001261 var p = data && data.params || {},
1262 desc = data && data.description || null,
Simon Huntfc274c92014-11-11 11:05:46 -08001263 intro = data && data.title;
Simon Huntbb282f52014-11-10 11:08:19 -08001264
Simon Hunt56d51852014-11-09 13:03:35 -08001265 if (err) {
1266 view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
1267 } else {
1268 sc.params = p;
Simon Huntbb282f52014-11-10 11:08:19 -08001269 if (desc) {
1270 intro += '\n\n ' + desc.join('\n ');
1271 }
1272 view.alert(intro);
Simon Hunt56d51852014-11-09 13:03:35 -08001273 }
1274 });
1275
1276 }
1277
Simon Hunt0c6d4192014-11-12 12:07:10 -08001278
1279 function para(sel, text) {
1280 sel.append('p').text(text);
1281 }
1282
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001283 // ==============================
Simon Hunt142d0032014-11-04 20:13:09 -08001284 // View life-cycle callbacks
Simon Hunt195cb382014-11-03 17:50:51 -08001285
Simon Huntf67722a2014-11-10 09:32:06 -08001286 function preload(view, ctx, flags) {
Simon Hunt142d0032014-11-04 20:13:09 -08001287 var w = view.width(),
1288 h = view.height(),
1289 idBg = view.uid('bg'),
Simon Huntc7ee0662014-11-05 16:44:37 -08001290 showBg = config.options.showBackground ? 'visible' : 'hidden',
1291 fcfg = config.force,
1292 fpad = fcfg.pad,
1293 forceDim = [w - 2*fpad, h - 2*fpad];
Simon Hunt195cb382014-11-03 17:50:51 -08001294
Simon Huntbb282f52014-11-10 11:08:19 -08001295 // TODO: set trace api
1296 //trace = onos.exported.webSockTrace;
1297
Simon Hunt142d0032014-11-04 20:13:09 -08001298 // NOTE: view.$div is a D3 selection of the view's div
1299 svg = view.$div.append('svg');
Simon Hunt934c3ce2014-11-05 11:45:07 -08001300 setSize(svg, view);
1301
Simon Hunt1a9eff92014-11-07 11:06:34 -08001302 // add blue glow filter to svg layer
1303 d3u.appendGlow(svg);
1304
Simon Hunt142d0032014-11-04 20:13:09 -08001305 // load the background image
1306 bgImg = svg.append('svg:image')
Simon Hunt195cb382014-11-03 17:50:51 -08001307 .attr({
Simon Hunt142d0032014-11-04 20:13:09 -08001308 id: idBg,
1309 width: w,
1310 height: h,
Simon Hunt195cb382014-11-03 17:50:51 -08001311 'xlink:href': config.backgroundUrl
1312 })
Simon Hunt142d0032014-11-04 20:13:09 -08001313 .style({
1314 visibility: showBg
Simon Hunt195cb382014-11-03 17:50:51 -08001315 });
Simon Huntc7ee0662014-11-05 16:44:37 -08001316
1317 // group for the topology
1318 topoG = svg.append('g')
1319 .attr('transform', fcfg.translate());
1320
1321 // subgroups for links and nodes
1322 linkG = topoG.append('g').attr('id', 'links');
1323 nodeG = topoG.append('g').attr('id', 'nodes');
1324
1325 // selection of nodes and links
1326 link = linkG.selectAll('.link');
1327 node = nodeG.selectAll('.node');
1328
Simon Hunt7cd48f32014-11-09 23:42:50 -08001329 function chrg(d) {
1330 return fcfg.charge[d.class] || -12000;
1331 }
Simon Hunt99c13842014-11-06 18:23:12 -08001332 function ldist(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08001333 return fcfg.linkDistance[d.type] || 50;
Simon Hunt99c13842014-11-06 18:23:12 -08001334 }
1335 function lstrg(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08001336 // 0.0 - 1.0
1337 return fcfg.linkStrength[d.type] || 1.0;
Simon Hunt99c13842014-11-06 18:23:12 -08001338 }
1339
Simon Hunt1a9eff92014-11-07 11:06:34 -08001340 function selectCb(d, self) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001341 selectObject(d, self);
Simon Hunt1a9eff92014-11-07 11:06:34 -08001342 }
1343
1344 function atDragEnd(d, self) {
Simon Hunt56d51852014-11-09 13:03:35 -08001345 // once we've finished moving, pin the node in position
1346 d.fixed = true;
1347 d3.select(self).classed('fixed', true);
1348 if (config.useLiveData) {
Simon Hunt902c9922014-11-11 11:59:31 -08001349 sendUpdateMeta(d);
Simon Hunt1a9eff92014-11-07 11:06:34 -08001350 }
1351 }
1352
Simon Hunt902c9922014-11-11 11:59:31 -08001353 function sendUpdateMeta(d) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001354 sendMessage('updateMeta', {
1355 id: d.id,
1356 'class': d.class,
Simon Hunt902c9922014-11-11 11:59:31 -08001357 'memento': {
1358 x: Math.floor(d.x),
1359 y: Math.floor(d.y)
1360 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001361 });
1362 }
1363
Simon Huntc7ee0662014-11-05 16:44:37 -08001364 // set up the force layout
1365 network.force = d3.layout.force()
1366 .size(forceDim)
1367 .nodes(network.nodes)
1368 .links(network.links)
Simon Hunt7cd48f32014-11-09 23:42:50 -08001369 .gravity(0.4)
1370 .friction(0.7)
1371 .charge(chrg)
Simon Hunt99c13842014-11-06 18:23:12 -08001372 .linkDistance(ldist)
1373 .linkStrength(lstrg)
Simon Huntc7ee0662014-11-05 16:44:37 -08001374 .on('tick', tick);
Simon Hunt195cb382014-11-03 17:50:51 -08001375
Simon Hunt1a9eff92014-11-07 11:06:34 -08001376 network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
Simon Hunt0c6d4192014-11-12 12:07:10 -08001377
1378 // create mask layer for when we lose connection to server.
1379 mask = view.$div.append('div').attr('id','topo-mask');
1380 para(mask, 'Oops!');
1381 para(mask, 'Web-socket connection to server closed...');
1382 para(mask, 'Try refreshing the page.');
Simon Hunt1a9eff92014-11-07 11:06:34 -08001383 }
Simon Hunt195cb382014-11-03 17:50:51 -08001384
Simon Hunt56d51852014-11-09 13:03:35 -08001385 function load(view, ctx, flags) {
Simon Huntf67722a2014-11-10 09:32:06 -08001386 // resize, in case the window was resized while we were not loaded
1387 resize(view, ctx, flags);
1388
Simon Hunt99c13842014-11-06 18:23:12 -08001389 // cache the view token, so network topo functions can access it
1390 network.view = view;
Simon Hunt56d51852014-11-09 13:03:35 -08001391 config.useLiveData = !flags.local;
1392
1393 if (!config.useLiveData) {
1394 prepareScenario(view, ctx, flags.debug);
1395 }
Simon Hunt99c13842014-11-06 18:23:12 -08001396
1397 // set our radio buttons and key bindings
Simon Hunt934c3ce2014-11-05 11:45:07 -08001398 view.setRadio(btnSet);
1399 view.setKeys(keyDispatch);
Simon Hunt195cb382014-11-03 17:50:51 -08001400
Simon Hunt50128c02014-11-08 13:36:15 -08001401 if (config.useLiveData) {
1402 webSock.connect();
1403 }
Simon Hunt195cb382014-11-03 17:50:51 -08001404 }
1405
Simon Huntf67722a2014-11-10 09:32:06 -08001406 function resize(view, ctx, flags) {
Simon Hunt934c3ce2014-11-05 11:45:07 -08001407 setSize(svg, view);
1408 setSize(bgImg, view);
Simon Hunt99c13842014-11-06 18:23:12 -08001409
1410 // TODO: hook to recompute layout, perhaps? work with zoom/pan code
1411 // adjust force layout size
Simon Hunt142d0032014-11-04 20:13:09 -08001412 }
1413
1414
1415 // ==============================
1416 // View registration
Simon Hunt195cb382014-11-03 17:50:51 -08001417
Simon Hunt25248912014-11-04 11:25:48 -08001418 onos.ui.addView('topo', {
Simon Hunt142d0032014-11-04 20:13:09 -08001419 preload: preload,
1420 load: load,
1421 resize: resize
Simon Hunt195cb382014-11-03 17:50:51 -08001422 });
1423
Simon Hunt61d04042014-11-11 17:27:16 -08001424 detailPane = onos.ui.addFloatingPanel('topo-detail');
1425
Simon Hunt195cb382014-11-03 17:50:51 -08001426}(ONOS));