blob: a23f48dd73b0af1d03ad36688bfd65edec02cbb4 [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
130 X: requestPath
Simon Hunt934c3ce2014-11-05 11:45:07 -0800131 };
Simon Hunt142d0032014-11-04 20:13:09 -0800132
Simon Hunt195cb382014-11-03 17:50:51 -0800133 // state variables
Simon Hunt99c13842014-11-06 18:23:12 -0800134 var network = {
Simon Hunt50128c02014-11-08 13:36:15 -0800135 view: null, // view token reference
Simon Hunt99c13842014-11-06 18:23:12 -0800136 nodes: [],
137 links: [],
138 lookup: {}
139 },
Simon Hunt56d51852014-11-09 13:03:35 -0800140 scenario = {
141 evDir: 'json/ev/',
142 evScenario: '/scenario.json',
143 evPrefix: '/ev_',
144 evOnos: '_onos.json',
145 evUi: '_ui.json',
146 ctx: null,
147 params: {},
148 evNumber: 0,
149 view: null,
150 debug: false
151 },
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800152 webSock,
Simon Hunt56d51852014-11-09 13:03:35 -0800153 deviceLabelIndex = 0,
154 hostLabelIndex = 0,
Simon Hunt61d04042014-11-11 17:27:16 -0800155 detailPane,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800156 selectOrder = [],
157 selections = {},
158
Simon Hunt195cb382014-11-03 17:50:51 -0800159 highlighted = null,
160 hovered = null,
161 viewMode = 'showAll',
162 portLabelsOn = false;
163
Simon Hunt934c3ce2014-11-05 11:45:07 -0800164 // D3 selections
165 var svg,
166 bgImg,
Simon Huntc7ee0662014-11-05 16:44:37 -0800167 topoG,
168 nodeG,
169 linkG,
170 node,
171 link;
Simon Hunt195cb382014-11-03 17:50:51 -0800172
Simon Hunt142d0032014-11-04 20:13:09 -0800173 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800174 // For Debugging / Development
Simon Hunt195cb382014-11-03 17:50:51 -0800175
Simon Hunt99c13842014-11-06 18:23:12 -0800176 function note(label, msg) {
177 console.log('NOTE: ' + label + ': ' + msg);
Simon Hunt195cb382014-11-03 17:50:51 -0800178 }
179
Simon Hunt99c13842014-11-06 18:23:12 -0800180 function debug(what) {
181 return config.debugOn && config.debug[what];
Simon Hunt934c3ce2014-11-05 11:45:07 -0800182 }
183
Simon Huntfc274c92014-11-11 11:05:46 -0800184 function fnTrace(msg, id) {
185 if (config.fnTrace) {
186 console.log('FN: ' + msg + ' [' + id + ']');
187 }
188 }
Simon Hunt99c13842014-11-06 18:23:12 -0800189
Simon Hunt934c3ce2014-11-05 11:45:07 -0800190 // ==============================
191 // Key Callbacks
192
Simon Hunt99c13842014-11-06 18:23:12 -0800193 function testMe(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800194 view.alert('test');
Simon Hunt61d04042014-11-11 17:27:16 -0800195 detailPane.show();
196 setTimeout(function () {
197 detailPane.hide();
198 }, 3000);
Simon Hunt99c13842014-11-06 18:23:12 -0800199 }
200
Simon Hunt56d51852014-11-09 13:03:35 -0800201 function abortIfLive() {
Simon Hunt50128c02014-11-08 13:36:15 -0800202 if (config.useLiveData) {
Simon Hunt56d51852014-11-09 13:03:35 -0800203 scenario.view.alert("Sorry, currently using live data..");
204 return true;
Simon Hunt50128c02014-11-08 13:36:15 -0800205 }
Simon Hunt56d51852014-11-09 13:03:35 -0800206 return false;
207 }
Simon Hunt50128c02014-11-08 13:36:15 -0800208
Simon Hunt56d51852014-11-09 13:03:35 -0800209 function testDebug(msg) {
210 if (scenario.debug) {
211 scenario.view.alert(msg);
212 }
213 }
Simon Hunt99c13842014-11-06 18:23:12 -0800214
Simon Hunt56d51852014-11-09 13:03:35 -0800215 function injectTestEvent(view) {
216 if (abortIfLive()) { return; }
217 var sc = scenario,
218 evn = ++sc.evNumber,
219 pfx = sc.evDir + sc.ctx + sc.evPrefix + evn,
220 onosUrl = pfx + sc.evOnos,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800221 uiUrl = pfx + sc.evUi,
222 stack = [
223 { url: onosUrl, cb: handleServerEvent },
224 { url: uiUrl, cb: handleUiEvent }
225 ];
226 recurseFetchEvent(stack, evn);
Simon Hunt56d51852014-11-09 13:03:35 -0800227 }
228
Simon Hunt7cd48f32014-11-09 23:42:50 -0800229 function recurseFetchEvent(stack, evn) {
230 var v = scenario.view,
231 frame;
232 if (stack.length === 0) {
Simon Huntfc274c92014-11-11 11:05:46 -0800233 v.alert('Oops!\n\nNo event #' + evn + ' found.');
Simon Hunt7cd48f32014-11-09 23:42:50 -0800234 return;
235 }
236 frame = stack.shift();
237
238 d3.json(frame.url, function (err, data) {
Simon Hunt99c13842014-11-06 18:23:12 -0800239 if (err) {
Simon Hunt56d51852014-11-09 13:03:35 -0800240 if (err.status === 404) {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800241 // if we didn't find the data, try the next stack frame
242 recurseFetchEvent(stack, evn);
Simon Hunt56d51852014-11-09 13:03:35 -0800243 } else {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800244 v.alert('non-404 error:\n\n' + frame.url + '\n\n' + err);
Simon Hunt56d51852014-11-09 13:03:35 -0800245 }
Simon Hunt99c13842014-11-06 18:23:12 -0800246 } else {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800247 testDebug('loaded: ' + frame.url);
248 frame.cb(data);
Simon Hunt99c13842014-11-06 18:23:12 -0800249 }
250 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800251
Simon Hunt56d51852014-11-09 13:03:35 -0800252 }
Simon Hunt50128c02014-11-08 13:36:15 -0800253
Simon Hunt56d51852014-11-09 13:03:35 -0800254 function handleUiEvent(data) {
Simon Huntbb282f52014-11-10 11:08:19 -0800255 scenario.view.alert('UI Tx: ' + data.event + '\n\n' +
256 JSON.stringify(data));
Simon Hunt56d51852014-11-09 13:03:35 -0800257 }
258
259 function injectStartupEvents(view) {
260 var last = scenario.params.lastAuto || 0;
261 if (abortIfLive()) { return; }
262
263 while (scenario.evNumber < last) {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800264 injectTestEvent(view);
265 }
266 }
267
Simon Hunt934c3ce2014-11-05 11:45:07 -0800268 function toggleBg() {
269 var vis = bgImg.style('visibility');
270 bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
271 }
272
Simon Hunt99c13842014-11-06 18:23:12 -0800273 function cycleLabels() {
Simon Huntbb282f52014-11-10 11:08:19 -0800274 deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1)
275 ? 0 : deviceLabelIndex + 1;
Simon Hunt5f36d342014-11-08 21:33:14 -0800276
Simon Hunt99c13842014-11-06 18:23:12 -0800277 network.nodes.forEach(function (d) {
Simon Huntbb282f52014-11-10 11:08:19 -0800278 if (d.class === 'device') {
279 updateDeviceLabel(d);
280 }
Simon Hunt99c13842014-11-06 18:23:12 -0800281 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800282 }
283
284 function togglePorts(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800285 view.alert('togglePorts() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800286 }
287
288 function unpin(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800289 view.alert('unpin() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800290 }
291
292 // ==============================
293 // Radio Button Callbacks
294
Simon Hunt195cb382014-11-03 17:50:51 -0800295 function showAllLayers() {
Simon Hunt142d0032014-11-04 20:13:09 -0800296// network.node.classed('inactive', false);
297// network.link.classed('inactive', false);
298// d3.selectAll('svg .port').classed('inactive', false);
299// d3.selectAll('svg .portText').classed('inactive', false);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800300 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800301 network.view.alert('showAllLayers() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800302 }
303
304 function showPacketLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800305 showAllLayers();
306 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800307 network.view.alert('showPacketLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800308 }
309
310 function showOpticalLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800311 showAllLayers();
312 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800313 network.view.alert('showOpticalLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800314 }
315
Simon Hunt142d0032014-11-04 20:13:09 -0800316 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800317 // Private functions
318
Simon Hunt99c13842014-11-06 18:23:12 -0800319 function safeId(s) {
320 return s.replace(/[^a-z0-9]/gi, '-');
321 }
322
Simon Huntc7ee0662014-11-05 16:44:37 -0800323 // set the size of the given element to that of the view (reduced if padded)
324 function setSize(el, view, pad) {
325 var padding = pad ? pad * 2 : 0;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800326 el.attr({
Simon Huntc7ee0662014-11-05 16:44:37 -0800327 width: view.width() - padding,
328 height: view.height() - padding
Simon Hunt934c3ce2014-11-05 11:45:07 -0800329 });
330 }
331
Simon Hunt934c3ce2014-11-05 11:45:07 -0800332
Simon Hunt99c13842014-11-06 18:23:12 -0800333 // ==============================
334 // Event handlers for server-pushed events
335
Simon Huntbb282f52014-11-10 11:08:19 -0800336 function logicError(msg) {
337 // TODO, report logic error to server, via websock, so it can be logged
338 network.view.alert('Logic Error:\n\n' + msg);
Simon Huntfc274c92014-11-11 11:05:46 -0800339 console.warn(msg);
Simon Huntbb282f52014-11-10 11:08:19 -0800340 }
341
Simon Hunt99c13842014-11-06 18:23:12 -0800342 var eventDispatch = {
343 addDevice: addDevice,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800344 addLink: addLink,
Simon Hunt56d51852014-11-09 13:03:35 -0800345 addHost: addHost,
Simon Huntbb282f52014-11-10 11:08:19 -0800346 updateDevice: updateDevice,
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800347 updateLink: updateLink,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800348 updateHost: updateHost,
Simon Huntbb282f52014-11-10 11:08:19 -0800349 removeDevice: stillToImplement,
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800350 removeLink: removeLink,
Simon Hunt44031102014-11-11 13:20:36 -0800351 removeHost: removeHost,
Simon Hunt61d04042014-11-11 17:27:16 -0800352 showDetails: showDetails,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800353 showPath: showPath
Simon Hunt99c13842014-11-06 18:23:12 -0800354 };
355
356 function addDevice(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800357 fnTrace('addDevice', data.payload.id);
Simon Hunt99c13842014-11-06 18:23:12 -0800358 var device = data.payload,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800359 nodeData = createDeviceNode(device);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800360 network.nodes.push(nodeData);
361 network.lookup[nodeData.id] = nodeData;
Simon Hunt99c13842014-11-06 18:23:12 -0800362 updateNodes();
363 network.force.start();
364 }
365
Simon Hunt99c13842014-11-06 18:23:12 -0800366 function addLink(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800367 fnTrace('addLink', data.payload.id);
Simon Hunt99c13842014-11-06 18:23:12 -0800368 var link = data.payload,
369 lnk = createLink(link);
Simon Hunt99c13842014-11-06 18:23:12 -0800370 if (lnk) {
Simon Hunt99c13842014-11-06 18:23:12 -0800371 network.links.push(lnk);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800372 network.lookup[lnk.id] = lnk;
Simon Hunt99c13842014-11-06 18:23:12 -0800373 updateLinks();
374 network.force.start();
375 }
376 }
377
Simon Hunt56d51852014-11-09 13:03:35 -0800378 function addHost(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800379 fnTrace('addHost', data.payload.id);
Simon Hunt56d51852014-11-09 13:03:35 -0800380 var host = data.payload,
381 node = createHostNode(host),
382 lnk;
Simon Hunt56d51852014-11-09 13:03:35 -0800383 network.nodes.push(node);
384 network.lookup[host.id] = node;
385 updateNodes();
386
387 lnk = createHostLink(host);
388 if (lnk) {
Simon Hunt44031102014-11-11 13:20:36 -0800389 node.linkData = lnk; // cache ref on its host
Simon Hunt56d51852014-11-09 13:03:35 -0800390 network.links.push(lnk);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800391 network.lookup[host.ingress] = lnk;
392 network.lookup[host.egress] = lnk;
Simon Hunt56d51852014-11-09 13:03:35 -0800393 updateLinks();
394 }
395 network.force.start();
396 }
397
Simon Hunt44031102014-11-11 13:20:36 -0800398 // TODO: fold updateX(...) methods into one base method; remove duplication
Simon Huntbb282f52014-11-10 11:08:19 -0800399 function updateDevice(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800400 fnTrace('updateDevice', data.payload.id);
Simon Huntbb282f52014-11-10 11:08:19 -0800401 var device = data.payload,
402 id = device.id,
403 nodeData = network.lookup[id];
404 if (nodeData) {
405 $.extend(nodeData, device);
406 updateDeviceState(nodeData);
407 } else {
408 logicError('updateDevice lookup fail. ID = "' + id + '"');
409 }
410 }
411
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800412 function updateLink(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800413 fnTrace('updateLink', data.payload.id);
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800414 var link = data.payload,
415 id = link.id,
416 linkData = network.lookup[id];
417 if (linkData) {
418 $.extend(linkData, link);
419 updateLinkState(linkData);
420 } else {
421 logicError('updateLink lookup fail. ID = "' + id + '"');
422 }
423 }
424
Simon Hunt7cd48f32014-11-09 23:42:50 -0800425 function updateHost(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800426 fnTrace('updateHost', data.payload.id);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800427 var host = data.payload,
Simon Huntbb282f52014-11-10 11:08:19 -0800428 id = host.id,
429 hostData = network.lookup[id];
430 if (hostData) {
431 $.extend(hostData, host);
432 updateHostState(hostData);
433 } else {
434 logicError('updateHost lookup fail. ID = "' + id + '"');
435 }
Simon Hunt7cd48f32014-11-09 23:42:50 -0800436 }
437
Simon Hunt44031102014-11-11 13:20:36 -0800438 // TODO: fold removeX(...) methods into base method - remove dup code
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800439 function removeLink(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800440 fnTrace('removeLink', data.payload.id);
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800441 var link = data.payload,
442 id = link.id,
443 linkData = network.lookup[id];
444 if (linkData) {
445 removeLinkElement(linkData);
446 } else {
447 logicError('removeLink lookup fail. ID = "' + id + '"');
448 }
449 }
450
Simon Hunt44031102014-11-11 13:20:36 -0800451 function removeHost(data) {
452 fnTrace('removeHost', data.payload.id);
453 var host = data.payload,
454 id = host.id,
455 hostData = network.lookup[id];
456 if (hostData) {
457 removeHostElement(hostData);
458 } else {
459 logicError('removeHost lookup fail. ID = "' + id + '"');
460 }
461 }
462
Simon Hunt61d04042014-11-11 17:27:16 -0800463 function showDetails(data) {
464 fnTrace('showDetails', data.payload.id);
465 populateDetails(data.payload);
466 detailPane.show();
467 }
468
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800469 function showPath(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800470 fnTrace('showPath', data.payload.id);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800471 var links = data.payload.links,
472 s = [ data.event + "\n" + links.length ];
473 links.forEach(function (d, i) {
474 s.push(d);
475 });
476 network.view.alert(s.join('\n'));
477
478 links.forEach(function (d, i) {
479 var link = network.lookup[d];
480 if (link) {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800481 link.el.classed('showPath', true);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800482 }
483 });
484
485 // TODO: add selection-highlite lines to links
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800486 }
487
Simon Hunt56d51852014-11-09 13:03:35 -0800488 // ...............................
489
490 function stillToImplement(data) {
491 var p = data.payload;
492 note(data.event, p.id);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800493 network.view.alert('Not yet implemented: "' + data.event + '"');
Simon Hunt56d51852014-11-09 13:03:35 -0800494 }
Simon Hunt99c13842014-11-06 18:23:12 -0800495
496 function unknownEvent(data) {
Simon Hunt50128c02014-11-08 13:36:15 -0800497 network.view.alert('Unknown event type: "' + data.event + '"');
Simon Hunt99c13842014-11-06 18:23:12 -0800498 }
499
500 function handleServerEvent(data) {
501 var fn = eventDispatch[data.event] || unknownEvent;
502 fn(data);
503 }
504
505 // ==============================
Simon Hunt61d04042014-11-11 17:27:16 -0800506 // Out-going messages...
507
508 function getSel(idx) {
509 return selections[selectOrder[idx]];
510 }
511
512 // for now, just a host-to-host intent, (and implicit start-monitoring)
513 function requestPath() {
514 var payload = {
515 one: getSel(0).obj.id,
516 two: getSel(1).obj.id
517 };
518 sendMessage('requestPath', payload);
519 }
520
521 // request details for the selected element
522 function requestDetails() {
523 var data = getSel(0).obj,
524 payload = {
525 id: data.id,
526 class: data.class
527 };
528 sendMessage('requestDetails', payload);
529 }
530
531 // ==============================
Simon Hunt99c13842014-11-06 18:23:12 -0800532 // force layout modification functions
533
534 function translate(x, y) {
535 return 'translate(' + x + ',' + y + ')';
536 }
537
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800538 function missMsg(what, id) {
539 return '\n[' + what + '] "' + id + '" missing ';
540 }
541
542 function linkEndPoints(srcId, dstId) {
543 var srcNode = network.lookup[srcId],
544 dstNode = network.lookup[dstId],
545 sMiss = !srcNode ? missMsg('src', srcId) : '',
546 dMiss = !dstNode ? missMsg('dst', dstId) : '';
547
548 if (sMiss || dMiss) {
549 logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
550 return null;
551 }
552 return {
553 source: srcNode,
554 target: dstNode,
555 x1: srcNode.x,
556 y1: srcNode.y,
557 x2: dstNode.x,
558 y2: dstNode.y
559 };
560 }
561
Simon Hunt56d51852014-11-09 13:03:35 -0800562 function createHostLink(host) {
563 var src = host.id,
564 dst = host.cp.device,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800565 id = host.ingress,
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800566 lnk = linkEndPoints(src, dst);
Simon Hunt56d51852014-11-09 13:03:35 -0800567
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800568 if (!lnk) {
Simon Hunt56d51852014-11-09 13:03:35 -0800569 return null;
570 }
571
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800572 // Synthesize link ...
573 $.extend(lnk, {
Thomas Vachuska4830d392014-11-09 17:09:56 -0800574 id: id,
Simon Hunt56d51852014-11-09 13:03:35 -0800575 class: 'link',
Simon Hunt7cd48f32014-11-09 23:42:50 -0800576 type: 'hostLink',
Simon Hunt56d51852014-11-09 13:03:35 -0800577 svgClass: 'link hostLink',
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800578 linkWidth: 1
Simon Hunt7cd48f32014-11-09 23:42:50 -0800579 });
Simon Hunt99c13842014-11-06 18:23:12 -0800580 return lnk;
581 }
582
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800583 function createLink(link) {
584 var lnk = linkEndPoints(link.src, link.dst),
585 type = link.type;
586
587 if (!lnk) {
588 return null;
589 }
590
591 // merge in remaining data
592 $.extend(lnk, link, {
593 class: 'link',
594 svgClass: type ? 'link ' + type : 'link'
595 });
596 return lnk;
Simon Hunt1a9eff92014-11-07 11:06:34 -0800597 }
598
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800599 var widthRatio = 1.4,
600 linkScale = d3.scale.linear()
601 .domain([1, 12])
602 .range([widthRatio, 12 * widthRatio])
603 .clamp(true);
604
605 function updateLinkWidth (d) {
606 // TODO: watch out for .showPath/.showTraffic classes
607 d.el.transition()
608 .duration(1000)
609 .attr('stroke-width', linkScale(d.linkWidth));
610 }
611
612
Simon Hunt99c13842014-11-06 18:23:12 -0800613 function updateLinks() {
614 link = linkG.selectAll('.link')
615 .data(network.links, function (d) { return d.id; });
616
617 // operate on existing links, if necessary
618 // link .foo() .bar() ...
619
620 // operate on entering links:
621 var entering = link.enter()
622 .append('line')
623 .attr({
Simon Hunt99c13842014-11-06 18:23:12 -0800624 class: function (d) { return d.svgClass; },
625 x1: function (d) { return d.x1; },
626 y1: function (d) { return d.y1; },
627 x2: function (d) { return d.x2; },
628 y2: function (d) { return d.y2; },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800629 stroke: config.topo.linkInColor,
630 'stroke-width': config.topo.linkInWidth
Simon Hunt99c13842014-11-06 18:23:12 -0800631 })
632 .transition().duration(1000)
633 .attr({
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800634 'stroke-width': function (d) { return linkScale(d.linkWidth); },
Simon Hunt99c13842014-11-06 18:23:12 -0800635 stroke: '#666' // TODO: remove explicit stroke, rather...
636 });
637
638 // augment links
Simon Hunt7cd48f32014-11-09 23:42:50 -0800639 entering.each(function (d) {
640 var link = d3.select(this);
641 // provide ref to element selection from backing data....
642 d.el = link;
Simon Hunt99c13842014-11-06 18:23:12 -0800643
Simon Hunt7cd48f32014-11-09 23:42:50 -0800644 // TODO: add src/dst port labels etc.
645 });
Thomas Vachuska4830d392014-11-09 17:09:56 -0800646
647 // operate on both existing and new links, if necessary
648 //link .foo() .bar() ...
649
650 // operate on exiting links:
Thomas Vachuska4830d392014-11-09 17:09:56 -0800651 link.exit()
Thomas Vachuska4830d392014-11-09 17:09:56 -0800652 .attr({
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800653 'stroke-dasharray': '3, 3'
Thomas Vachuska4830d392014-11-09 17:09:56 -0800654 })
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800655 .style('opacity', 0.4)
656 .transition()
Simon Huntea80eb42014-11-11 13:46:57 -0800657 .duration(1500)
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800658 .attr({
659 'stroke-dasharray': '3, 12'
660 })
661 .transition()
Simon Huntea80eb42014-11-11 13:46:57 -0800662 .duration(500)
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800663 .style('opacity', 0.0)
Thomas Vachuska4830d392014-11-09 17:09:56 -0800664 .remove();
Simon Hunt99c13842014-11-06 18:23:12 -0800665 }
666
667 function createDeviceNode(device) {
668 // start with the object as is
669 var node = device,
Simon Huntbb282f52014-11-10 11:08:19 -0800670 type = device.type,
671 svgCls = type ? 'node device ' + type : 'node device';
Simon Hunt99c13842014-11-06 18:23:12 -0800672
673 // Augment as needed...
674 node.class = 'device';
Simon Huntbb282f52014-11-10 11:08:19 -0800675 node.svgClass = device.online ? svgCls + ' online' : svgCls;
Simon Hunt99c13842014-11-06 18:23:12 -0800676 positionNode(node);
677
678 // cache label array length
679 network.deviceLabelCount = device.labels.length;
Simon Hunt99c13842014-11-06 18:23:12 -0800680 return node;
681 }
682
Simon Hunt56d51852014-11-09 13:03:35 -0800683 function createHostNode(host) {
684 // start with the object as is
685 var node = host;
686
687 // Augment as needed...
688 node.class = 'host';
Simon Hunt7cd48f32014-11-09 23:42:50 -0800689 if (!node.type) {
690 // TODO: perhaps type would be: {phone, tablet, laptop, endstation} ?
691 node.type = 'endstation';
692 }
Simon Hunt56d51852014-11-09 13:03:35 -0800693 node.svgClass = 'node host';
694 // TODO: consider placing near its switch, if [x,y] not defined
695 positionNode(node);
696
697 // cache label array length
698 network.hostLabelCount = host.labels.length;
Simon Hunt56d51852014-11-09 13:03:35 -0800699 return node;
700 }
701
Simon Hunt99c13842014-11-06 18:23:12 -0800702 function positionNode(node) {
703 var meta = node.metaUi,
704 x = 0,
705 y = 0;
706
707 if (meta) {
708 x = meta.x;
709 y = meta.y;
710 }
711 if (x && y) {
712 node.fixed = true;
713 }
714 node.x = x || network.view.width() / 2;
715 node.y = y || network.view.height() / 2;
716 }
717
Simon Hunt99c13842014-11-06 18:23:12 -0800718 function iconUrl(d) {
719 return 'img/' + d.type + '.png';
720 }
721
722 // returns the newly computed bounding box of the rectangle
723 function adjustRectToFitText(n) {
724 var text = n.select('text'),
725 box = text.node().getBBox(),
726 lab = config.labels;
727
728 text.attr('text-anchor', 'middle')
729 .attr('y', '-0.8em')
730 .attr('x', lab.imgPad/2);
731
732 // translate the bbox so that it is centered on [x,y]
733 box.x = -box.width / 2;
734 box.y = -box.height / 2;
735
736 // add padding
737 box.x -= (lab.padLR + lab.imgPad/2);
738 box.width += lab.padLR * 2 + lab.imgPad;
739 box.y -= lab.padTB;
740 box.height += lab.padTB * 2;
741
742 return box;
743 }
744
Simon Hunt1a9eff92014-11-07 11:06:34 -0800745 function mkSvgClass(d) {
746 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
747 }
748
Simon Hunt7cd48f32014-11-09 23:42:50 -0800749 function hostLabel(d) {
750 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
751 return d.labels[idx];
752 }
753 function deviceLabel(d) {
754 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
755 return d.labels[idx];
756 }
757 function niceLabel(label) {
758 return (label && label.trim()) ? label : '.';
759 }
760
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800761 function updateDeviceLabel(d) {
762 var label = niceLabel(deviceLabel(d)),
763 node = d.el,
764 box;
765
766 node.select('text')
767 .text(label)
768 .style('opacity', 0)
769 .transition()
770 .style('opacity', 1);
771
772 box = adjustRectToFitText(node);
773
774 node.select('rect')
775 .transition()
776 .attr(box);
777
778 node.select('image')
779 .transition()
780 .attr('x', box.x + config.icons.xoff)
781 .attr('y', box.y + config.icons.yoff);
782 }
783
784 function updateHostLabel(d) {
785 var label = hostLabel(d),
786 host = d.el;
787
788 host.select('text').text(label);
789 }
790
Simon Huntbb282f52014-11-10 11:08:19 -0800791 function updateDeviceState(nodeData) {
792 nodeData.el.classed('online', nodeData.online);
793 updateDeviceLabel(nodeData);
794 // TODO: review what else might need to be updated
795 }
796
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800797 function updateLinkState(linkData) {
798 updateLinkWidth(linkData);
799 // TODO: review what else might need to be updated
800 // update label, if showing
801 }
802
Simon Huntbb282f52014-11-10 11:08:19 -0800803 function updateHostState(hostData) {
804 updateHostLabel(hostData);
805 // TODO: review what else might need to be updated
806 }
807
808
Simon Hunt99c13842014-11-06 18:23:12 -0800809 function updateNodes() {
810 node = nodeG.selectAll('.node')
811 .data(network.nodes, function (d) { return d.id; });
812
813 // operate on existing nodes, if necessary
Simon Hunt7cd48f32014-11-09 23:42:50 -0800814 // update host labels
Simon Hunt99c13842014-11-06 18:23:12 -0800815 //node .foo() .bar() ...
816
817 // operate on entering nodes:
818 var entering = node.enter()
819 .append('g')
820 .attr({
821 id: function (d) { return safeId(d.id); },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800822 class: mkSvgClass,
Simon Hunt99c13842014-11-06 18:23:12 -0800823 transform: function (d) { return translate(d.x, d.y); },
824 opacity: 0
825 })
Simon Hunt1a9eff92014-11-07 11:06:34 -0800826 .call(network.drag)
Simon Hunt99c13842014-11-06 18:23:12 -0800827 //.on('mouseover', function (d) {})
828 //.on('mouseover', function (d) {})
829 .transition()
830 .attr('opacity', 1);
831
832 // augment device nodes...
833 entering.filter('.device').each(function (d) {
834 var node = d3.select(this),
835 icon = iconUrl(d),
Simon Hunt7cd48f32014-11-09 23:42:50 -0800836 label = niceLabel(deviceLabel(d)),
Simon Hunt99c13842014-11-06 18:23:12 -0800837 box;
838
Simon Hunt7cd48f32014-11-09 23:42:50 -0800839 // provide ref to element from backing data....
840 d.el = node;
841
Simon Hunt99c13842014-11-06 18:23:12 -0800842 node.append('rect')
843 .attr({
844 'rx': 5,
845 'ry': 5
846 });
847
848 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -0800849 .text(label)
Simon Hunt99c13842014-11-06 18:23:12 -0800850 .attr('dy', '1.1em');
851
852 box = adjustRectToFitText(node);
853
854 node.select('rect')
855 .attr(box);
856
857 if (icon) {
858 var cfg = config.icons;
859 node.append('svg:image')
860 .attr({
861 x: box.x + config.icons.xoff,
862 y: box.y + config.icons.yoff,
863 width: cfg.w,
864 height: cfg.h,
865 'xlink:href': icon
866 });
867 }
868
869 // debug function to show the modelled x,y coordinates of nodes...
870 if (debug('showNodeXY')) {
871 node.select('rect').attr('fill-opacity', 0.5);
872 node.append('circle')
873 .attr({
874 class: 'debug',
875 cx: 0,
876 cy: 0,
877 r: '3px'
878 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800879 }
880 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800881
Simon Hunt56d51852014-11-09 13:03:35 -0800882 // augment host nodes...
883 entering.filter('.host').each(function (d) {
884 var node = d3.select(this),
Simon Hunt56d51852014-11-09 13:03:35 -0800885 box;
886
Simon Hunt7cd48f32014-11-09 23:42:50 -0800887 // provide ref to element from backing data....
888 d.el = node;
889
Simon Hunt56d51852014-11-09 13:03:35 -0800890 node.append('circle')
891 .attr('r', 8); // TODO: define host circle radius
892
893 // TODO: are we attaching labels to hosts?
894 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -0800895 .text(hostLabel)
896 .attr('dy', '1.3em')
897 .attr('text-anchor', 'middle');
Simon Hunt56d51852014-11-09 13:03:35 -0800898
899 // debug function to show the modelled x,y coordinates of nodes...
900 if (debug('showNodeXY')) {
901 node.select('circle').attr('fill-opacity', 0.5);
902 node.append('circle')
903 .attr({
904 class: 'debug',
905 cx: 0,
906 cy: 0,
907 r: '3px'
908 });
909 }
910 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800911
Simon Hunt99c13842014-11-06 18:23:12 -0800912 // operate on both existing and new nodes, if necessary
913 //node .foo() .bar() ...
Simon Huntc7ee0662014-11-05 16:44:37 -0800914
Simon Hunt99c13842014-11-06 18:23:12 -0800915 // operate on exiting nodes:
Simon Huntea80eb42014-11-11 13:46:57 -0800916 // Note that the node is removed after 2 seconds.
917 // Sub element animations should be shorter than 2 seconds.
918 var exiting = node.exit()
Simon Hunt44031102014-11-11 13:20:36 -0800919 .transition()
920 .duration(2000)
Simon Huntea80eb42014-11-11 13:46:57 -0800921 .style('opacity', 0)
Simon Hunt99c13842014-11-06 18:23:12 -0800922 .remove();
Simon Huntea80eb42014-11-11 13:46:57 -0800923
924 // host node exits....
925 exiting.filter('.host').each(function (d) {
926 var node = d3.select(this);
927
928 node.select('text')
929 .style('opacity', 0.5)
930 .transition()
931 .duration(1000)
932 .style('opacity', 0);
933 // note, leave <g>.remove to remove this element
934
935 node.select('circle')
936 .style('stroke-fill', '#555')
937 .style('fill', '#888')
938 .style('opacity', 0.5)
939 .transition()
940 .duration(1500)
941 .attr('r', 0);
942 // note, leave <g>.remove to remove this element
943
944 });
945
946 // TODO: device node exits
Simon Huntc7ee0662014-11-05 16:44:37 -0800947 }
948
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800949 function find(id, array) {
950 for (var idx = 0, n = array.length; idx < n; idx++) {
951 if (array[idx].id === id) {
952 return idx;
953 }
954 }
955 return -1;
956 }
957
958 function removeLinkElement(linkData) {
959 // remove from lookup cache
960 delete network.lookup[linkData.id];
961 // remove from links array
962 var idx = find(linkData.id, network.links);
Simon Huntfc274c92014-11-11 11:05:46 -0800963 network.links.splice(idx, 1);
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800964 // remove from SVG
965 updateLinks();
Simon Hunt44031102014-11-11 13:20:36 -0800966 network.force.resume();
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800967 }
Simon Huntc7ee0662014-11-05 16:44:37 -0800968
Simon Hunt44031102014-11-11 13:20:36 -0800969 function removeHostElement(hostData) {
970 // first, remove associated hostLink...
971 removeLinkElement(hostData.linkData);
972
973 // remove from lookup cache
974 delete network.lookup[hostData.id];
975 // remove from nodes array
976 var idx = find(hostData.id, network.nodes);
977 network.nodes.splice(idx, 1);
978 // remove from SVG
979 updateNodes();
980 network.force.resume();
981 }
982
983
Simon Huntc7ee0662014-11-05 16:44:37 -0800984 function tick() {
985 node.attr({
Simon Hunt99c13842014-11-06 18:23:12 -0800986 transform: function (d) { return translate(d.x, d.y); }
Simon Huntc7ee0662014-11-05 16:44:37 -0800987 });
988
989 link.attr({
990 x1: function (d) { return d.source.x; },
991 y1: function (d) { return d.source.y; },
992 x2: function (d) { return d.target.x; },
993 y2: function (d) { return d.target.y; }
994 });
995 }
Simon Hunt934c3ce2014-11-05 11:45:07 -0800996
997 // ==============================
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800998 // Web-Socket for live data
999
1000 function webSockUrl() {
1001 return document.location.toString()
1002 .replace(/\#.*/, '')
1003 .replace('http://', 'ws://')
1004 .replace('https://', 'wss://')
1005 .replace('index2.html', config.webSockUrl);
1006 }
1007
1008 webSock = {
1009 ws : null,
1010
1011 connect : function() {
1012 webSock.ws = new WebSocket(webSockUrl());
1013
1014 webSock.ws.onopen = function() {
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001015 };
1016
1017 webSock.ws.onmessage = function(m) {
1018 if (m.data) {
Simon Huntbb282f52014-11-10 11:08:19 -08001019 wsTraceRx(m.data);
Thomas Vachuskad472c6e2014-11-07 19:11:05 -08001020 handleServerEvent(JSON.parse(m.data));
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001021 }
1022 };
1023
1024 webSock.ws.onclose = function(m) {
1025 webSock.ws = null;
1026 };
1027 },
1028
1029 send : function(text) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001030 if (text != null) {
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001031 webSock._send(text);
1032 }
1033 },
1034
1035 _send : function(message) {
1036 if (webSock.ws) {
1037 webSock.ws.send(message);
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001038 } else {
Simon Hunt56d51852014-11-09 13:03:35 -08001039 network.view.alert('no web socket open\n\n' + message);
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001040 }
1041 }
1042
1043 };
1044
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001045 var sid = 0;
1046
Simon Hunt61d04042014-11-11 17:27:16 -08001047 // TODO: use cache of pending messages (key = sid) to reconcile responses
1048
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001049 function sendMessage(evType, payload) {
1050 var toSend = {
Simon Huntbb282f52014-11-10 11:08:19 -08001051 event: evType,
1052 sid: ++sid,
1053 payload: payload
1054 },
1055 asText = JSON.stringify(toSend);
1056 wsTraceTx(asText);
1057 webSock.send(asText);
1058 }
1059
1060 function wsTraceTx(msg) {
1061 wsTrace('tx', msg);
1062 }
1063 function wsTraceRx(msg) {
1064 wsTrace('rx', msg);
1065 }
1066 function wsTrace(rxtx, msg) {
Simon Huntbb282f52014-11-10 11:08:19 -08001067 console.log('[' + rxtx + '] ' + msg);
1068 // TODO: integrate with trace view
1069 //if (trace) {
1070 // trace.output(rxtx, msg);
1071 //}
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001072 }
1073
1074
1075 // ==============================
1076 // Selection stuff
1077
1078 function selectObject(obj, el) {
1079 var n,
1080 meta = d3.event.sourceEvent.metaKey;
1081
1082 if (el) {
1083 n = d3.select(el);
1084 } else {
1085 node.each(function(d) {
1086 if (d == obj) {
1087 n = d3.select(el = this);
1088 }
1089 });
1090 }
1091 if (!n) return;
1092
1093 if (meta && n.classed('selected')) {
1094 deselectObject(obj.id);
Simon Hunt61d04042014-11-11 17:27:16 -08001095 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001096 return;
1097 }
1098
1099 if (!meta) {
1100 deselectAll();
1101 }
1102
Simon Hunt5f36d342014-11-08 21:33:14 -08001103 selections[obj.id] = { obj: obj, el : el};
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001104 selectOrder.push(obj.id);
1105
1106 n.classed('selected', true);
Simon Hunt61d04042014-11-11 17:27:16 -08001107 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001108 }
1109
1110 function deselectObject(id) {
1111 var obj = selections[id];
1112 if (obj) {
1113 d3.select(obj.el).classed('selected', false);
Simon Hunt61d04042014-11-11 17:27:16 -08001114 delete selections[id];
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001115 }
Simon Hunt61d04042014-11-11 17:27:16 -08001116 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001117 }
1118
1119 function deselectAll() {
1120 // deselect all nodes in the network...
1121 node.classed('selected', false);
1122 selections = {};
1123 selectOrder = [];
Simon Hunt61d04042014-11-11 17:27:16 -08001124 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001125 }
1126
Simon Hunt61d04042014-11-11 17:27:16 -08001127 // FIXME: this click handler does not get unloaded when the view does
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001128 $('#view').on('click', function(e) {
1129 if (!$(e.target).closest('.node').length) {
1130 if (!e.metaKey) {
1131 deselectAll();
1132 }
1133 }
1134 });
1135
Simon Hunt61d04042014-11-11 17:27:16 -08001136 // update the state of the detail pane, based on current selections
1137 function updateDetailPane() {
1138 var nSel = selectOrder.length;
1139 if (!nSel) {
1140 detailPane.hide();
1141 } else if (nSel === 1) {
1142 singleSelect();
1143 } else {
1144 multiSelect();
1145 }
1146 }
1147
1148 function singleSelect() {
1149 requestDetails();
1150 // NOTE: detail pane will be shown from showDetails event.
1151 }
1152
1153 function multiSelect() {
1154 // TODO: use detail pane for multi-select view.
1155 //detailPane.show();
1156 }
1157
1158 function populateDetails(data) {
1159 detailPane.empty();
1160
1161 var title = detailPane.append("h2"),
1162 table = detailPane.append("table"),
1163 tbody = table.append("tbody");
1164
1165 $('<img src="img/' + data.type + '.png">').appendTo(title);
1166 $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
1167
1168 data.propOrder.forEach(function(p) {
1169 if (p === '-') {
1170 addSep(tbody);
1171 } else {
1172 addProp(tbody, p, data.props[p]);
1173 }
1174 });
1175
1176 function addSep(tbody) {
1177 var tr = tbody.append('tr');
1178 $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
1179 }
1180
1181 function addProp(tbody, label, value) {
1182 var tr = tbody.append('tr');
1183
1184 tr.append('td')
1185 .attr('class', 'label')
1186 .text(label + ' :');
1187
1188 tr.append('td')
1189 .attr('class', 'value')
1190 .text(value);
1191 }
1192 }
1193
1194 // ==============================
1195 // Test harness code
Simon Hunt56d51852014-11-09 13:03:35 -08001196
1197 function prepareScenario(view, ctx, dbg) {
1198 var sc = scenario,
1199 urlSc = sc.evDir + ctx + sc.evScenario;
1200
1201 if (!ctx) {
1202 view.alert("No scenario specified (null ctx)");
1203 return;
1204 }
1205
1206 sc.view = view;
1207 sc.ctx = ctx;
1208 sc.debug = dbg;
1209 sc.evNumber = 0;
1210
1211 d3.json(urlSc, function(err, data) {
Simon Huntbb282f52014-11-10 11:08:19 -08001212 var p = data && data.params || {},
1213 desc = data && data.description || null,
Simon Huntfc274c92014-11-11 11:05:46 -08001214 intro = data && data.title;
Simon Huntbb282f52014-11-10 11:08:19 -08001215
Simon Hunt56d51852014-11-09 13:03:35 -08001216 if (err) {
1217 view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
1218 } else {
1219 sc.params = p;
Simon Huntbb282f52014-11-10 11:08:19 -08001220 if (desc) {
1221 intro += '\n\n ' + desc.join('\n ');
1222 }
1223 view.alert(intro);
Simon Hunt56d51852014-11-09 13:03:35 -08001224 }
1225 });
1226
1227 }
1228
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001229 // ==============================
Simon Hunt142d0032014-11-04 20:13:09 -08001230 // View life-cycle callbacks
Simon Hunt195cb382014-11-03 17:50:51 -08001231
Simon Huntf67722a2014-11-10 09:32:06 -08001232 function preload(view, ctx, flags) {
Simon Hunt142d0032014-11-04 20:13:09 -08001233 var w = view.width(),
1234 h = view.height(),
1235 idBg = view.uid('bg'),
Simon Huntc7ee0662014-11-05 16:44:37 -08001236 showBg = config.options.showBackground ? 'visible' : 'hidden',
1237 fcfg = config.force,
1238 fpad = fcfg.pad,
1239 forceDim = [w - 2*fpad, h - 2*fpad];
Simon Hunt195cb382014-11-03 17:50:51 -08001240
Simon Huntbb282f52014-11-10 11:08:19 -08001241 // TODO: set trace api
1242 //trace = onos.exported.webSockTrace;
1243
Simon Hunt142d0032014-11-04 20:13:09 -08001244 // NOTE: view.$div is a D3 selection of the view's div
1245 svg = view.$div.append('svg');
Simon Hunt934c3ce2014-11-05 11:45:07 -08001246 setSize(svg, view);
1247
Simon Hunt1a9eff92014-11-07 11:06:34 -08001248 // add blue glow filter to svg layer
1249 d3u.appendGlow(svg);
1250
Simon Hunt142d0032014-11-04 20:13:09 -08001251 // load the background image
1252 bgImg = svg.append('svg:image')
Simon Hunt195cb382014-11-03 17:50:51 -08001253 .attr({
Simon Hunt142d0032014-11-04 20:13:09 -08001254 id: idBg,
1255 width: w,
1256 height: h,
Simon Hunt195cb382014-11-03 17:50:51 -08001257 'xlink:href': config.backgroundUrl
1258 })
Simon Hunt142d0032014-11-04 20:13:09 -08001259 .style({
1260 visibility: showBg
Simon Hunt195cb382014-11-03 17:50:51 -08001261 });
Simon Huntc7ee0662014-11-05 16:44:37 -08001262
1263 // group for the topology
1264 topoG = svg.append('g')
1265 .attr('transform', fcfg.translate());
1266
1267 // subgroups for links and nodes
1268 linkG = topoG.append('g').attr('id', 'links');
1269 nodeG = topoG.append('g').attr('id', 'nodes');
1270
1271 // selection of nodes and links
1272 link = linkG.selectAll('.link');
1273 node = nodeG.selectAll('.node');
1274
Simon Hunt7cd48f32014-11-09 23:42:50 -08001275 function chrg(d) {
1276 return fcfg.charge[d.class] || -12000;
1277 }
Simon Hunt99c13842014-11-06 18:23:12 -08001278 function ldist(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08001279 return fcfg.linkDistance[d.type] || 50;
Simon Hunt99c13842014-11-06 18:23:12 -08001280 }
1281 function lstrg(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08001282 // 0.0 - 1.0
1283 return fcfg.linkStrength[d.type] || 1.0;
Simon Hunt99c13842014-11-06 18:23:12 -08001284 }
1285
Simon Hunt1a9eff92014-11-07 11:06:34 -08001286 function selectCb(d, self) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001287 selectObject(d, self);
Simon Hunt1a9eff92014-11-07 11:06:34 -08001288 }
1289
1290 function atDragEnd(d, self) {
Simon Hunt56d51852014-11-09 13:03:35 -08001291 // once we've finished moving, pin the node in position
1292 d.fixed = true;
1293 d3.select(self).classed('fixed', true);
1294 if (config.useLiveData) {
Simon Hunt902c9922014-11-11 11:59:31 -08001295 sendUpdateMeta(d);
Simon Hunt1a9eff92014-11-07 11:06:34 -08001296 }
1297 }
1298
Simon Hunt902c9922014-11-11 11:59:31 -08001299 function sendUpdateMeta(d) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001300 sendMessage('updateMeta', {
1301 id: d.id,
1302 'class': d.class,
Simon Hunt902c9922014-11-11 11:59:31 -08001303 'memento': {
1304 x: Math.floor(d.x),
1305 y: Math.floor(d.y)
1306 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001307 });
1308 }
1309
Simon Huntc7ee0662014-11-05 16:44:37 -08001310 // set up the force layout
1311 network.force = d3.layout.force()
1312 .size(forceDim)
1313 .nodes(network.nodes)
1314 .links(network.links)
Simon Hunt7cd48f32014-11-09 23:42:50 -08001315 .gravity(0.4)
1316 .friction(0.7)
1317 .charge(chrg)
Simon Hunt99c13842014-11-06 18:23:12 -08001318 .linkDistance(ldist)
1319 .linkStrength(lstrg)
Simon Huntc7ee0662014-11-05 16:44:37 -08001320 .on('tick', tick);
Simon Hunt195cb382014-11-03 17:50:51 -08001321
Simon Hunt1a9eff92014-11-07 11:06:34 -08001322 network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
1323 }
Simon Hunt195cb382014-11-03 17:50:51 -08001324
Simon Hunt56d51852014-11-09 13:03:35 -08001325 function load(view, ctx, flags) {
Simon Huntf67722a2014-11-10 09:32:06 -08001326 // resize, in case the window was resized while we were not loaded
1327 resize(view, ctx, flags);
1328
Simon Hunt99c13842014-11-06 18:23:12 -08001329 // cache the view token, so network topo functions can access it
1330 network.view = view;
Simon Hunt56d51852014-11-09 13:03:35 -08001331 config.useLiveData = !flags.local;
1332
1333 if (!config.useLiveData) {
1334 prepareScenario(view, ctx, flags.debug);
1335 }
Simon Hunt99c13842014-11-06 18:23:12 -08001336
1337 // set our radio buttons and key bindings
Simon Hunt934c3ce2014-11-05 11:45:07 -08001338 view.setRadio(btnSet);
1339 view.setKeys(keyDispatch);
Simon Hunt195cb382014-11-03 17:50:51 -08001340
Simon Hunt50128c02014-11-08 13:36:15 -08001341 if (config.useLiveData) {
1342 webSock.connect();
1343 }
Simon Hunt195cb382014-11-03 17:50:51 -08001344 }
1345
Simon Huntf67722a2014-11-10 09:32:06 -08001346 function resize(view, ctx, flags) {
Simon Hunt934c3ce2014-11-05 11:45:07 -08001347 setSize(svg, view);
1348 setSize(bgImg, view);
Simon Hunt99c13842014-11-06 18:23:12 -08001349
1350 // TODO: hook to recompute layout, perhaps? work with zoom/pan code
1351 // adjust force layout size
Simon Hunt142d0032014-11-04 20:13:09 -08001352 }
1353
1354
1355 // ==============================
1356 // View registration
Simon Hunt195cb382014-11-03 17:50:51 -08001357
Simon Hunt25248912014-11-04 11:25:48 -08001358 onos.ui.addView('topo', {
Simon Hunt142d0032014-11-04 20:13:09 -08001359 preload: preload,
1360 load: load,
1361 resize: resize
Simon Hunt195cb382014-11-03 17:50:51 -08001362 });
1363
Simon Hunt61d04042014-11-11 17:27:16 -08001364 detailPane = onos.ui.addFloatingPanel('topo-detail');
1365
Simon Hunt195cb382014-11-03 17:50:51 -08001366}(ONOS));