blob: 8934ec3b4636f5d0d85d2cd4509d9dc3ab912f2f [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 }
Paul Greyson6cb8ca02014-11-12 18:09:02 -0800109 },
110 // see below in creation of viewBox on main svg
111 logicalSize: 1000
Simon Hunt195cb382014-11-03 17:50:51 -0800112 };
113
Simon Hunt142d0032014-11-04 20:13:09 -0800114 // radio buttons
115 var btnSet = [
Simon Hunt934c3ce2014-11-05 11:45:07 -0800116 { text: 'All Layers', cb: showAllLayers },
117 { text: 'Packet Only', cb: showPacketLayer },
118 { text: 'Optical Only', cb: showOpticalLayer }
119 ];
120
121 // key bindings
122 var keyDispatch = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800123 M: testMe, // TODO: remove (testing only)
Simon Hunt50128c02014-11-08 13:36:15 -0800124 S: injectStartupEvents, // TODO: remove (testing only)
125 space: injectTestEvent, // TODO: remove (testing only)
Simon Hunt99c13842014-11-06 18:23:12 -0800126
Thomas Vachuska65368e32014-11-08 16:10:20 -0800127 B: toggleBg, // TODO: do we really need this?
Simon Hunt934c3ce2014-11-05 11:45:07 -0800128 L: cycleLabels,
129 P: togglePorts,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800130 U: unpin,
131
Simon Huntb53e0682014-11-12 13:32:01 -0800132 W: requestTraffic, // bag of selections
133 Z: requestPath, // host-to-host intent (and monitor)
Simon Huntac9e24f2014-11-12 10:12:21 -0800134 X: cancelMonitor
Simon Hunt934c3ce2014-11-05 11:45:07 -0800135 };
Simon Hunt142d0032014-11-04 20:13:09 -0800136
Simon Hunt195cb382014-11-03 17:50:51 -0800137 // state variables
Simon Hunt99c13842014-11-06 18:23:12 -0800138 var network = {
Simon Hunt50128c02014-11-08 13:36:15 -0800139 view: null, // view token reference
Simon Hunt99c13842014-11-06 18:23:12 -0800140 nodes: [],
141 links: [],
142 lookup: {}
143 },
Simon Hunt56d51852014-11-09 13:03:35 -0800144 scenario = {
145 evDir: 'json/ev/',
146 evScenario: '/scenario.json',
147 evPrefix: '/ev_',
148 evOnos: '_onos.json',
149 evUi: '_ui.json',
150 ctx: null,
151 params: {},
152 evNumber: 0,
153 view: null,
154 debug: false
155 },
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800156 webSock,
Simon Hunt0c6d4192014-11-12 12:07:10 -0800157 sid = 0,
Simon Hunt56d51852014-11-09 13:03:35 -0800158 deviceLabelIndex = 0,
159 hostLabelIndex = 0,
Simon Hunt61d04042014-11-11 17:27:16 -0800160 detailPane,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800161 selectOrder = [],
162 selections = {},
163
Simon Hunt195cb382014-11-03 17:50:51 -0800164 highlighted = null,
165 hovered = null,
166 viewMode = 'showAll',
167 portLabelsOn = false;
168
Simon Hunt934c3ce2014-11-05 11:45:07 -0800169 // D3 selections
170 var svg,
171 bgImg,
Simon Huntc7ee0662014-11-05 16:44:37 -0800172 topoG,
173 nodeG,
174 linkG,
175 node,
Simon Hunt0c6d4192014-11-12 12:07:10 -0800176 link,
177 mask;
Simon Hunt195cb382014-11-03 17:50:51 -0800178
Paul Greyson6cb8ca02014-11-12 18:09:02 -0800179 // the projection for the map background
180 var geoMapProjection;
181
Simon Hunt142d0032014-11-04 20:13:09 -0800182 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800183 // For Debugging / Development
Simon Hunt195cb382014-11-03 17:50:51 -0800184
Simon Hunt99c13842014-11-06 18:23:12 -0800185 function note(label, msg) {
186 console.log('NOTE: ' + label + ': ' + msg);
Simon Hunt195cb382014-11-03 17:50:51 -0800187 }
188
Simon Hunt99c13842014-11-06 18:23:12 -0800189 function debug(what) {
190 return config.debugOn && config.debug[what];
Simon Hunt934c3ce2014-11-05 11:45:07 -0800191 }
192
Simon Huntfc274c92014-11-11 11:05:46 -0800193 function fnTrace(msg, id) {
194 if (config.fnTrace) {
195 console.log('FN: ' + msg + ' [' + id + ']');
196 }
197 }
Simon Hunt99c13842014-11-06 18:23:12 -0800198
Simon Hunt934c3ce2014-11-05 11:45:07 -0800199 // ==============================
200 // Key Callbacks
201
Simon Hunt99c13842014-11-06 18:23:12 -0800202 function testMe(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800203 view.alert('test');
Simon Hunt99c13842014-11-06 18:23:12 -0800204 }
205
Simon Hunt56d51852014-11-09 13:03:35 -0800206 function abortIfLive() {
Simon Hunt50128c02014-11-08 13:36:15 -0800207 if (config.useLiveData) {
Simon Huntb53e0682014-11-12 13:32:01 -0800208 network.view.alert("Sorry, currently using live data..");
Simon Hunt56d51852014-11-09 13:03:35 -0800209 return true;
Simon Hunt50128c02014-11-08 13:36:15 -0800210 }
Simon Hunt56d51852014-11-09 13:03:35 -0800211 return false;
212 }
Simon Hunt50128c02014-11-08 13:36:15 -0800213
Simon Hunt56d51852014-11-09 13:03:35 -0800214 function testDebug(msg) {
215 if (scenario.debug) {
216 scenario.view.alert(msg);
217 }
218 }
Simon Hunt99c13842014-11-06 18:23:12 -0800219
Simon Hunt56d51852014-11-09 13:03:35 -0800220 function injectTestEvent(view) {
221 if (abortIfLive()) { return; }
222 var sc = scenario,
223 evn = ++sc.evNumber,
224 pfx = sc.evDir + sc.ctx + sc.evPrefix + evn,
225 onosUrl = pfx + sc.evOnos,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800226 uiUrl = pfx + sc.evUi,
227 stack = [
228 { url: onosUrl, cb: handleServerEvent },
229 { url: uiUrl, cb: handleUiEvent }
230 ];
231 recurseFetchEvent(stack, evn);
Simon Hunt56d51852014-11-09 13:03:35 -0800232 }
233
Simon Hunt7cd48f32014-11-09 23:42:50 -0800234 function recurseFetchEvent(stack, evn) {
235 var v = scenario.view,
236 frame;
237 if (stack.length === 0) {
Simon Huntfc274c92014-11-11 11:05:46 -0800238 v.alert('Oops!\n\nNo event #' + evn + ' found.');
Simon Hunt7cd48f32014-11-09 23:42:50 -0800239 return;
240 }
241 frame = stack.shift();
242
243 d3.json(frame.url, function (err, data) {
Simon Hunt99c13842014-11-06 18:23:12 -0800244 if (err) {
Simon Hunt56d51852014-11-09 13:03:35 -0800245 if (err.status === 404) {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800246 // if we didn't find the data, try the next stack frame
247 recurseFetchEvent(stack, evn);
Simon Hunt56d51852014-11-09 13:03:35 -0800248 } else {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800249 v.alert('non-404 error:\n\n' + frame.url + '\n\n' + err);
Simon Hunt56d51852014-11-09 13:03:35 -0800250 }
Simon Hunt99c13842014-11-06 18:23:12 -0800251 } else {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800252 testDebug('loaded: ' + frame.url);
253 frame.cb(data);
Simon Hunt99c13842014-11-06 18:23:12 -0800254 }
255 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800256
Simon Hunt56d51852014-11-09 13:03:35 -0800257 }
Simon Hunt50128c02014-11-08 13:36:15 -0800258
Simon Hunt56d51852014-11-09 13:03:35 -0800259 function handleUiEvent(data) {
Simon Huntbb282f52014-11-10 11:08:19 -0800260 scenario.view.alert('UI Tx: ' + data.event + '\n\n' +
261 JSON.stringify(data));
Simon Hunt56d51852014-11-09 13:03:35 -0800262 }
263
264 function injectStartupEvents(view) {
265 var last = scenario.params.lastAuto || 0;
266 if (abortIfLive()) { return; }
267
268 while (scenario.evNumber < last) {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800269 injectTestEvent(view);
270 }
271 }
272
Simon Hunt934c3ce2014-11-05 11:45:07 -0800273 function toggleBg() {
274 var vis = bgImg.style('visibility');
275 bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
276 }
277
Simon Hunt99c13842014-11-06 18:23:12 -0800278 function cycleLabels() {
Simon Huntbb282f52014-11-10 11:08:19 -0800279 deviceLabelIndex = (deviceLabelIndex === network.deviceLabelCount - 1)
280 ? 0 : deviceLabelIndex + 1;
Simon Hunt5f36d342014-11-08 21:33:14 -0800281
Simon Hunt99c13842014-11-06 18:23:12 -0800282 network.nodes.forEach(function (d) {
Simon Huntbb282f52014-11-10 11:08:19 -0800283 if (d.class === 'device') {
284 updateDeviceLabel(d);
285 }
Simon Hunt99c13842014-11-06 18:23:12 -0800286 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800287 }
288
289 function togglePorts(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800290 view.alert('togglePorts() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800291 }
292
293 function unpin(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800294 view.alert('unpin() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800295 }
296
297 // ==============================
298 // Radio Button Callbacks
299
Simon Hunt195cb382014-11-03 17:50:51 -0800300 function showAllLayers() {
Simon Hunt142d0032014-11-04 20:13:09 -0800301// network.node.classed('inactive', false);
302// network.link.classed('inactive', false);
303// d3.selectAll('svg .port').classed('inactive', false);
304// d3.selectAll('svg .portText').classed('inactive', false);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800305 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800306 network.view.alert('showAllLayers() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800307 }
308
309 function showPacketLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800310 showAllLayers();
311 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800312 network.view.alert('showPacketLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800313 }
314
315 function showOpticalLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800316 showAllLayers();
317 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800318 network.view.alert('showOpticalLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800319 }
320
Simon Hunt142d0032014-11-04 20:13:09 -0800321 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800322 // Private functions
323
Simon Hunt99c13842014-11-06 18:23:12 -0800324 function safeId(s) {
325 return s.replace(/[^a-z0-9]/gi, '-');
326 }
327
Simon Huntc7ee0662014-11-05 16:44:37 -0800328 // set the size of the given element to that of the view (reduced if padded)
329 function setSize(el, view, pad) {
330 var padding = pad ? pad * 2 : 0;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800331 el.attr({
Simon Huntc7ee0662014-11-05 16:44:37 -0800332 width: view.width() - padding,
333 height: view.height() - padding
Simon Hunt934c3ce2014-11-05 11:45:07 -0800334 });
335 }
336
Simon Hunt934c3ce2014-11-05 11:45:07 -0800337
Simon Hunt99c13842014-11-06 18:23:12 -0800338 // ==============================
339 // Event handlers for server-pushed events
340
Simon Huntbb282f52014-11-10 11:08:19 -0800341 function logicError(msg) {
342 // TODO, report logic error to server, via websock, so it can be logged
343 network.view.alert('Logic Error:\n\n' + msg);
Simon Huntfc274c92014-11-11 11:05:46 -0800344 console.warn(msg);
Simon Huntbb282f52014-11-10 11:08:19 -0800345 }
346
Simon Hunt99c13842014-11-06 18:23:12 -0800347 var eventDispatch = {
348 addDevice: addDevice,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800349 addLink: addLink,
Simon Hunt56d51852014-11-09 13:03:35 -0800350 addHost: addHost,
Simon Huntb53e0682014-11-12 13:32:01 -0800351
Simon Huntbb282f52014-11-10 11:08:19 -0800352 updateDevice: updateDevice,
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800353 updateLink: updateLink,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800354 updateHost: updateHost,
Simon Huntb53e0682014-11-12 13:32:01 -0800355
Simon Huntbb282f52014-11-10 11:08:19 -0800356 removeDevice: stillToImplement,
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800357 removeLink: removeLink,
Simon Hunt44031102014-11-11 13:20:36 -0800358 removeHost: removeHost,
Simon Huntb53e0682014-11-12 13:32:01 -0800359
Simon Hunt61d04042014-11-11 17:27:16 -0800360 showDetails: showDetails,
Simon Huntb53e0682014-11-12 13:32:01 -0800361 showPath: showPath,
362 showTraffic: showTraffic
Simon Hunt99c13842014-11-06 18:23:12 -0800363 };
364
365 function addDevice(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800366 fnTrace('addDevice', data.payload.id);
Simon Hunt99c13842014-11-06 18:23:12 -0800367 var device = data.payload,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800368 nodeData = createDeviceNode(device);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800369 network.nodes.push(nodeData);
370 network.lookup[nodeData.id] = nodeData;
Simon Hunt99c13842014-11-06 18:23:12 -0800371 updateNodes();
372 network.force.start();
373 }
374
Simon Hunt99c13842014-11-06 18:23:12 -0800375 function addLink(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800376 fnTrace('addLink', data.payload.id);
Simon Hunt99c13842014-11-06 18:23:12 -0800377 var link = data.payload,
378 lnk = createLink(link);
Simon Hunt99c13842014-11-06 18:23:12 -0800379 if (lnk) {
Simon Hunt99c13842014-11-06 18:23:12 -0800380 network.links.push(lnk);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800381 network.lookup[lnk.id] = lnk;
Simon Hunt99c13842014-11-06 18:23:12 -0800382 updateLinks();
383 network.force.start();
384 }
385 }
386
Simon Hunt56d51852014-11-09 13:03:35 -0800387 function addHost(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800388 fnTrace('addHost', data.payload.id);
Simon Hunt56d51852014-11-09 13:03:35 -0800389 var host = data.payload,
390 node = createHostNode(host),
391 lnk;
Simon Hunt56d51852014-11-09 13:03:35 -0800392 network.nodes.push(node);
393 network.lookup[host.id] = node;
394 updateNodes();
395
396 lnk = createHostLink(host);
397 if (lnk) {
Simon Hunt44031102014-11-11 13:20:36 -0800398 node.linkData = lnk; // cache ref on its host
Simon Hunt56d51852014-11-09 13:03:35 -0800399 network.links.push(lnk);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800400 network.lookup[host.ingress] = lnk;
401 network.lookup[host.egress] = lnk;
Simon Hunt56d51852014-11-09 13:03:35 -0800402 updateLinks();
403 }
404 network.force.start();
405 }
406
Simon Hunt44031102014-11-11 13:20:36 -0800407 // TODO: fold updateX(...) methods into one base method; remove duplication
Simon Huntbb282f52014-11-10 11:08:19 -0800408 function updateDevice(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800409 fnTrace('updateDevice', data.payload.id);
Simon Huntbb282f52014-11-10 11:08:19 -0800410 var device = data.payload,
411 id = device.id,
412 nodeData = network.lookup[id];
413 if (nodeData) {
414 $.extend(nodeData, device);
415 updateDeviceState(nodeData);
416 } else {
417 logicError('updateDevice lookup fail. ID = "' + id + '"');
418 }
419 }
420
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800421 function updateLink(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800422 fnTrace('updateLink', data.payload.id);
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800423 var link = data.payload,
424 id = link.id,
425 linkData = network.lookup[id];
426 if (linkData) {
427 $.extend(linkData, link);
428 updateLinkState(linkData);
429 } else {
430 logicError('updateLink lookup fail. ID = "' + id + '"');
431 }
432 }
433
Simon Hunt7cd48f32014-11-09 23:42:50 -0800434 function updateHost(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800435 fnTrace('updateHost', data.payload.id);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800436 var host = data.payload,
Simon Huntbb282f52014-11-10 11:08:19 -0800437 id = host.id,
438 hostData = network.lookup[id];
439 if (hostData) {
440 $.extend(hostData, host);
441 updateHostState(hostData);
442 } else {
443 logicError('updateHost lookup fail. ID = "' + id + '"');
444 }
Simon Hunt7cd48f32014-11-09 23:42:50 -0800445 }
446
Simon Hunt44031102014-11-11 13:20:36 -0800447 // TODO: fold removeX(...) methods into base method - remove dup code
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800448 function removeLink(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800449 fnTrace('removeLink', data.payload.id);
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800450 var link = data.payload,
451 id = link.id,
452 linkData = network.lookup[id];
453 if (linkData) {
454 removeLinkElement(linkData);
455 } else {
456 logicError('removeLink lookup fail. ID = "' + id + '"');
457 }
458 }
459
Simon Hunt44031102014-11-11 13:20:36 -0800460 function removeHost(data) {
461 fnTrace('removeHost', data.payload.id);
462 var host = data.payload,
463 id = host.id,
464 hostData = network.lookup[id];
465 if (hostData) {
466 removeHostElement(hostData);
467 } else {
468 logicError('removeHost lookup fail. ID = "' + id + '"');
469 }
470 }
471
Simon Hunt61d04042014-11-11 17:27:16 -0800472 function showDetails(data) {
473 fnTrace('showDetails', data.payload.id);
474 populateDetails(data.payload);
Simon Huntb53e0682014-11-12 13:32:01 -0800475 // TODO: Add single-select actions ...
Simon Hunt61d04042014-11-11 17:27:16 -0800476 detailPane.show();
477 }
478
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800479 function showPath(data) {
Simon Huntfc274c92014-11-11 11:05:46 -0800480 fnTrace('showPath', data.payload.id);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800481 var links = data.payload.links,
482 s = [ data.event + "\n" + links.length ];
483 links.forEach(function (d, i) {
484 s.push(d);
485 });
486 network.view.alert(s.join('\n'));
487
488 links.forEach(function (d, i) {
489 var link = network.lookup[d];
490 if (link) {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800491 link.el.classed('showPath', true);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800492 }
493 });
494
495 // TODO: add selection-highlite lines to links
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800496 }
497
Simon Huntb53e0682014-11-12 13:32:01 -0800498 function showTraffic(data) {
499 network.view.alert("showTraffic() -- TODO")
500 }
501
Simon Hunt56d51852014-11-09 13:03:35 -0800502 // ...............................
503
504 function stillToImplement(data) {
505 var p = data.payload;
506 note(data.event, p.id);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800507 network.view.alert('Not yet implemented: "' + data.event + '"');
Simon Hunt56d51852014-11-09 13:03:35 -0800508 }
Simon Hunt99c13842014-11-06 18:23:12 -0800509
510 function unknownEvent(data) {
Simon Hunt50128c02014-11-08 13:36:15 -0800511 network.view.alert('Unknown event type: "' + data.event + '"');
Simon Hunt99c13842014-11-06 18:23:12 -0800512 }
513
514 function handleServerEvent(data) {
515 var fn = eventDispatch[data.event] || unknownEvent;
516 fn(data);
517 }
518
519 // ==============================
Simon Hunt61d04042014-11-11 17:27:16 -0800520 // Out-going messages...
521
Simon Huntb53e0682014-11-12 13:32:01 -0800522 function userFeedback(msg) {
523 // for now, use the alert pane as is. Maybe different alert style in
524 // the future (centered on view; dismiss button?)
525 network.view.alert(msg);
526 }
527
528 function nSel() {
529 return selectOrder.length;
530 }
Simon Hunt61d04042014-11-11 17:27:16 -0800531 function getSel(idx) {
532 return selections[selectOrder[idx]];
533 }
Simon Huntb53e0682014-11-12 13:32:01 -0800534 function getSelId(idx) {
535 return getSel(idx).obj.id;
536 }
537 function allSelectionsClass(cls) {
538 for (var i=0, n=nSel(); i<n; i++) {
539 if (getSel(i).obj.class !== cls) {
540 return false;
541 }
542 }
543 return true;
544 }
Simon Hunt61d04042014-11-11 17:27:16 -0800545
Simon Huntb53e0682014-11-12 13:32:01 -0800546 function requestTraffic() {
547 if (nSel() > 0) {
548 sendMessage('requestTraffic', {
549 ids: selectOrder
550 });
551 } else {
552 userFeedback('Request-Traffic requires one or\n' +
553 'more items to be selected.');
554 }
555 }
556
Simon Hunt61d04042014-11-11 17:27:16 -0800557 function requestPath() {
Simon Huntb53e0682014-11-12 13:32:01 -0800558 if (nSel() === 2 && allSelectionsClass('host')) {
559 sendMessage('requestPath', {
560 one: getSelId(0),
561 two: getSelId(1)
562 });
563 } else {
564 userFeedback('Request-Path requires two\n' +
565 'hosts to be selected.');
566 }
Simon Hunt61d04042014-11-11 17:27:16 -0800567 }
568
Simon Huntac9e24f2014-11-12 10:12:21 -0800569 function cancelMonitor() {
Simon Huntb53e0682014-11-12 13:32:01 -0800570 // FIXME: from where do we get the intent id(s) to send to the server?
571 sendMessage('cancelMonitor', {
572 ids: ["need_the_intent_id"]
573 });
Simon Huntac9e24f2014-11-12 10:12:21 -0800574 }
575
Simon Hunt61d04042014-11-11 17:27:16 -0800576 // request details for the selected element
577 function requestDetails() {
578 var data = getSel(0).obj,
579 payload = {
580 id: data.id,
581 class: data.class
582 };
583 sendMessage('requestDetails', payload);
584 }
585
586 // ==============================
Simon Hunt99c13842014-11-06 18:23:12 -0800587 // force layout modification functions
588
589 function translate(x, y) {
590 return 'translate(' + x + ',' + y + ')';
591 }
592
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800593 function missMsg(what, id) {
594 return '\n[' + what + '] "' + id + '" missing ';
595 }
596
597 function linkEndPoints(srcId, dstId) {
598 var srcNode = network.lookup[srcId],
599 dstNode = network.lookup[dstId],
600 sMiss = !srcNode ? missMsg('src', srcId) : '',
601 dMiss = !dstNode ? missMsg('dst', dstId) : '';
602
603 if (sMiss || dMiss) {
604 logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
605 return null;
606 }
607 return {
608 source: srcNode,
609 target: dstNode,
610 x1: srcNode.x,
611 y1: srcNode.y,
612 x2: dstNode.x,
613 y2: dstNode.y
614 };
615 }
616
Simon Hunt56d51852014-11-09 13:03:35 -0800617 function createHostLink(host) {
618 var src = host.id,
619 dst = host.cp.device,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800620 id = host.ingress,
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800621 lnk = linkEndPoints(src, dst);
Simon Hunt56d51852014-11-09 13:03:35 -0800622
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800623 if (!lnk) {
Simon Hunt56d51852014-11-09 13:03:35 -0800624 return null;
625 }
626
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800627 // Synthesize link ...
628 $.extend(lnk, {
Thomas Vachuska4830d392014-11-09 17:09:56 -0800629 id: id,
Simon Hunt56d51852014-11-09 13:03:35 -0800630 class: 'link',
Simon Hunt7cd48f32014-11-09 23:42:50 -0800631 type: 'hostLink',
Simon Hunt56d51852014-11-09 13:03:35 -0800632 svgClass: 'link hostLink',
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800633 linkWidth: 1
Simon Hunt7cd48f32014-11-09 23:42:50 -0800634 });
Simon Hunt99c13842014-11-06 18:23:12 -0800635 return lnk;
636 }
637
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800638 function createLink(link) {
639 var lnk = linkEndPoints(link.src, link.dst),
640 type = link.type;
641
642 if (!lnk) {
643 return null;
644 }
645
646 // merge in remaining data
647 $.extend(lnk, link, {
648 class: 'link',
649 svgClass: type ? 'link ' + type : 'link'
650 });
651 return lnk;
Simon Hunt1a9eff92014-11-07 11:06:34 -0800652 }
653
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800654 var widthRatio = 1.4,
655 linkScale = d3.scale.linear()
656 .domain([1, 12])
657 .range([widthRatio, 12 * widthRatio])
658 .clamp(true);
659
660 function updateLinkWidth (d) {
661 // TODO: watch out for .showPath/.showTraffic classes
662 d.el.transition()
663 .duration(1000)
664 .attr('stroke-width', linkScale(d.linkWidth));
665 }
666
667
Simon Hunt99c13842014-11-06 18:23:12 -0800668 function updateLinks() {
669 link = linkG.selectAll('.link')
670 .data(network.links, function (d) { return d.id; });
671
672 // operate on existing links, if necessary
673 // link .foo() .bar() ...
674
675 // operate on entering links:
676 var entering = link.enter()
677 .append('line')
678 .attr({
Simon Hunt99c13842014-11-06 18:23:12 -0800679 class: function (d) { return d.svgClass; },
680 x1: function (d) { return d.x1; },
681 y1: function (d) { return d.y1; },
682 x2: function (d) { return d.x2; },
683 y2: function (d) { return d.y2; },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800684 stroke: config.topo.linkInColor,
685 'stroke-width': config.topo.linkInWidth
Simon Hunt99c13842014-11-06 18:23:12 -0800686 })
687 .transition().duration(1000)
688 .attr({
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800689 'stroke-width': function (d) { return linkScale(d.linkWidth); },
Simon Hunt99c13842014-11-06 18:23:12 -0800690 stroke: '#666' // TODO: remove explicit stroke, rather...
691 });
692
693 // augment links
Simon Hunt7cd48f32014-11-09 23:42:50 -0800694 entering.each(function (d) {
695 var link = d3.select(this);
696 // provide ref to element selection from backing data....
697 d.el = link;
Simon Hunt99c13842014-11-06 18:23:12 -0800698
Simon Hunt7cd48f32014-11-09 23:42:50 -0800699 // TODO: add src/dst port labels etc.
700 });
Thomas Vachuska4830d392014-11-09 17:09:56 -0800701
702 // operate on both existing and new links, if necessary
703 //link .foo() .bar() ...
704
705 // operate on exiting links:
Thomas Vachuska4830d392014-11-09 17:09:56 -0800706 link.exit()
Thomas Vachuska4830d392014-11-09 17:09:56 -0800707 .attr({
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800708 'stroke-dasharray': '3, 3'
Thomas Vachuska4830d392014-11-09 17:09:56 -0800709 })
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800710 .style('opacity', 0.4)
711 .transition()
Simon Huntea80eb42014-11-11 13:46:57 -0800712 .duration(1500)
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800713 .attr({
714 'stroke-dasharray': '3, 12'
715 })
716 .transition()
Simon Huntea80eb42014-11-11 13:46:57 -0800717 .duration(500)
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800718 .style('opacity', 0.0)
Thomas Vachuska4830d392014-11-09 17:09:56 -0800719 .remove();
Simon Hunt99c13842014-11-06 18:23:12 -0800720 }
721
722 function createDeviceNode(device) {
723 // start with the object as is
724 var node = device,
Simon Huntbb282f52014-11-10 11:08:19 -0800725 type = device.type,
726 svgCls = type ? 'node device ' + type : 'node device';
Simon Hunt99c13842014-11-06 18:23:12 -0800727
728 // Augment as needed...
729 node.class = 'device';
Simon Huntbb282f52014-11-10 11:08:19 -0800730 node.svgClass = device.online ? svgCls + ' online' : svgCls;
Simon Hunt99c13842014-11-06 18:23:12 -0800731 positionNode(node);
732
733 // cache label array length
734 network.deviceLabelCount = device.labels.length;
Simon Hunt99c13842014-11-06 18:23:12 -0800735 return node;
736 }
737
Simon Hunt56d51852014-11-09 13:03:35 -0800738 function createHostNode(host) {
739 // start with the object as is
740 var node = host;
741
742 // Augment as needed...
743 node.class = 'host';
Simon Hunt7cd48f32014-11-09 23:42:50 -0800744 if (!node.type) {
745 // TODO: perhaps type would be: {phone, tablet, laptop, endstation} ?
746 node.type = 'endstation';
747 }
Simon Hunt56d51852014-11-09 13:03:35 -0800748 node.svgClass = 'node host';
749 // TODO: consider placing near its switch, if [x,y] not defined
750 positionNode(node);
751
752 // cache label array length
753 network.hostLabelCount = host.labels.length;
Simon Hunt56d51852014-11-09 13:03:35 -0800754 return node;
755 }
756
Simon Hunt99c13842014-11-06 18:23:12 -0800757 function positionNode(node) {
758 var meta = node.metaUi,
Simon Huntac9e24f2014-11-12 10:12:21 -0800759 x = meta && meta.x,
760 y = meta && meta.y,
761 xy;
Simon Hunt99c13842014-11-06 18:23:12 -0800762
Simon Huntac9e24f2014-11-12 10:12:21 -0800763 // If we have [x,y] already, use that...
Simon Hunt99c13842014-11-06 18:23:12 -0800764 if (x && y) {
765 node.fixed = true;
Simon Huntac9e24f2014-11-12 10:12:21 -0800766 node.x = x;
767 node.y = y;
768 return;
Simon Hunt99c13842014-11-06 18:23:12 -0800769 }
Simon Huntac9e24f2014-11-12 10:12:21 -0800770
Paul Greyson6cb8ca02014-11-12 18:09:02 -0800771 var location = node.location;
772 if (location && location.type === 'latlng') {
773 var coord = geoMapProjection([location.lng, location.lat]);
774 node.fixed = true;
775 node.x = coord[0];
776 node.y = coord[1];
777 return;
778 }
779
Simon Huntac9e24f2014-11-12 10:12:21 -0800780 // Note: Placing incoming unpinned nodes at exactly the same point
781 // (center of the view) causes them to explode outwards when
782 // the force layout kicks in. So, we spread them out a bit
783 // initially, to provide a more serene layout convergence.
784 // Additionally, if the node is a host, we place it near
785 // the device it is connected to.
786
787 function spread(s) {
788 return Math.floor((Math.random() * s) - s/2);
789 }
790
791 function randDim(dim) {
792 return dim / 2 + spread(dim * 0.7071);
793 }
794
795 function rand() {
796 return {
797 x: randDim(network.view.width()),
798 y: randDim(network.view.height())
799 };
800 }
801
802 function near(node) {
803 var min = 12,
804 dx = spread(12),
805 dy = spread(12);
806 return {
807 x: node.x + min + dx,
808 y: node.y + min + dy
809 };
810 }
811
812 function getDevice(cp) {
813 var d = network.lookup[cp.device];
814 return d || rand();
815 }
816
817 xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
818 $.extend(node, xy);
Simon Hunt99c13842014-11-06 18:23:12 -0800819 }
820
Simon Hunt99c13842014-11-06 18:23:12 -0800821 function iconUrl(d) {
822 return 'img/' + d.type + '.png';
823 }
824
825 // returns the newly computed bounding box of the rectangle
826 function adjustRectToFitText(n) {
827 var text = n.select('text'),
828 box = text.node().getBBox(),
829 lab = config.labels;
830
831 text.attr('text-anchor', 'middle')
832 .attr('y', '-0.8em')
833 .attr('x', lab.imgPad/2);
834
835 // translate the bbox so that it is centered on [x,y]
836 box.x = -box.width / 2;
837 box.y = -box.height / 2;
838
839 // add padding
840 box.x -= (lab.padLR + lab.imgPad/2);
841 box.width += lab.padLR * 2 + lab.imgPad;
842 box.y -= lab.padTB;
843 box.height += lab.padTB * 2;
844
845 return box;
846 }
847
Simon Hunt1a9eff92014-11-07 11:06:34 -0800848 function mkSvgClass(d) {
849 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
850 }
851
Simon Hunt7cd48f32014-11-09 23:42:50 -0800852 function hostLabel(d) {
853 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
854 return d.labels[idx];
855 }
856 function deviceLabel(d) {
857 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
858 return d.labels[idx];
859 }
860 function niceLabel(label) {
861 return (label && label.trim()) ? label : '.';
862 }
863
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800864 function updateDeviceLabel(d) {
865 var label = niceLabel(deviceLabel(d)),
866 node = d.el,
867 box;
868
869 node.select('text')
870 .text(label)
871 .style('opacity', 0)
872 .transition()
873 .style('opacity', 1);
874
875 box = adjustRectToFitText(node);
876
877 node.select('rect')
878 .transition()
879 .attr(box);
880
881 node.select('image')
882 .transition()
883 .attr('x', box.x + config.icons.xoff)
884 .attr('y', box.y + config.icons.yoff);
885 }
886
887 function updateHostLabel(d) {
888 var label = hostLabel(d),
889 host = d.el;
890
891 host.select('text').text(label);
892 }
893
Simon Huntbb282f52014-11-10 11:08:19 -0800894 function updateDeviceState(nodeData) {
895 nodeData.el.classed('online', nodeData.online);
896 updateDeviceLabel(nodeData);
897 // TODO: review what else might need to be updated
898 }
899
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800900 function updateLinkState(linkData) {
901 updateLinkWidth(linkData);
902 // TODO: review what else might need to be updated
903 // update label, if showing
904 }
905
Simon Huntbb282f52014-11-10 11:08:19 -0800906 function updateHostState(hostData) {
907 updateHostLabel(hostData);
908 // TODO: review what else might need to be updated
909 }
910
911
Simon Hunt99c13842014-11-06 18:23:12 -0800912 function updateNodes() {
913 node = nodeG.selectAll('.node')
914 .data(network.nodes, function (d) { return d.id; });
915
916 // operate on existing nodes, if necessary
Simon Hunt7cd48f32014-11-09 23:42:50 -0800917 // update host labels
Simon Hunt99c13842014-11-06 18:23:12 -0800918 //node .foo() .bar() ...
919
920 // operate on entering nodes:
921 var entering = node.enter()
922 .append('g')
923 .attr({
924 id: function (d) { return safeId(d.id); },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800925 class: mkSvgClass,
Simon Hunt99c13842014-11-06 18:23:12 -0800926 transform: function (d) { return translate(d.x, d.y); },
927 opacity: 0
928 })
Simon Hunt1a9eff92014-11-07 11:06:34 -0800929 .call(network.drag)
Simon Hunt99c13842014-11-06 18:23:12 -0800930 //.on('mouseover', function (d) {})
931 //.on('mouseover', function (d) {})
932 .transition()
933 .attr('opacity', 1);
934
935 // augment device nodes...
936 entering.filter('.device').each(function (d) {
937 var node = d3.select(this),
938 icon = iconUrl(d),
Simon Hunt7cd48f32014-11-09 23:42:50 -0800939 label = niceLabel(deviceLabel(d)),
Simon Hunt99c13842014-11-06 18:23:12 -0800940 box;
941
Simon Hunt7cd48f32014-11-09 23:42:50 -0800942 // provide ref to element from backing data....
943 d.el = node;
944
Simon Hunt99c13842014-11-06 18:23:12 -0800945 node.append('rect')
946 .attr({
947 'rx': 5,
948 'ry': 5
949 });
950
951 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -0800952 .text(label)
Simon Hunt99c13842014-11-06 18:23:12 -0800953 .attr('dy', '1.1em');
954
955 box = adjustRectToFitText(node);
956
957 node.select('rect')
958 .attr(box);
959
960 if (icon) {
961 var cfg = config.icons;
962 node.append('svg:image')
963 .attr({
964 x: box.x + config.icons.xoff,
965 y: box.y + config.icons.yoff,
966 width: cfg.w,
967 height: cfg.h,
968 'xlink:href': icon
969 });
970 }
971
972 // debug function to show the modelled x,y coordinates of nodes...
973 if (debug('showNodeXY')) {
974 node.select('rect').attr('fill-opacity', 0.5);
975 node.append('circle')
976 .attr({
977 class: 'debug',
978 cx: 0,
979 cy: 0,
980 r: '3px'
981 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800982 }
983 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800984
Simon Hunt56d51852014-11-09 13:03:35 -0800985 // augment host nodes...
986 entering.filter('.host').each(function (d) {
987 var node = d3.select(this),
Simon Hunt56d51852014-11-09 13:03:35 -0800988 box;
989
Simon Hunt7cd48f32014-11-09 23:42:50 -0800990 // provide ref to element from backing data....
991 d.el = node;
992
Simon Hunt56d51852014-11-09 13:03:35 -0800993 node.append('circle')
994 .attr('r', 8); // TODO: define host circle radius
995
996 // TODO: are we attaching labels to hosts?
997 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -0800998 .text(hostLabel)
999 .attr('dy', '1.3em')
1000 .attr('text-anchor', 'middle');
Simon Hunt56d51852014-11-09 13:03:35 -08001001
1002 // debug function to show the modelled x,y coordinates of nodes...
1003 if (debug('showNodeXY')) {
1004 node.select('circle').attr('fill-opacity', 0.5);
1005 node.append('circle')
1006 .attr({
1007 class: 'debug',
1008 cx: 0,
1009 cy: 0,
1010 r: '3px'
1011 });
1012 }
1013 });
Simon Huntc7ee0662014-11-05 16:44:37 -08001014
Simon Hunt99c13842014-11-06 18:23:12 -08001015 // operate on both existing and new nodes, if necessary
1016 //node .foo() .bar() ...
Simon Huntc7ee0662014-11-05 16:44:37 -08001017
Simon Hunt99c13842014-11-06 18:23:12 -08001018 // operate on exiting nodes:
Simon Huntea80eb42014-11-11 13:46:57 -08001019 // Note that the node is removed after 2 seconds.
1020 // Sub element animations should be shorter than 2 seconds.
1021 var exiting = node.exit()
Simon Hunt44031102014-11-11 13:20:36 -08001022 .transition()
1023 .duration(2000)
Simon Huntea80eb42014-11-11 13:46:57 -08001024 .style('opacity', 0)
Simon Hunt99c13842014-11-06 18:23:12 -08001025 .remove();
Simon Huntea80eb42014-11-11 13:46:57 -08001026
1027 // host node exits....
1028 exiting.filter('.host').each(function (d) {
1029 var node = d3.select(this);
1030
1031 node.select('text')
1032 .style('opacity', 0.5)
1033 .transition()
1034 .duration(1000)
1035 .style('opacity', 0);
1036 // note, leave <g>.remove to remove this element
1037
1038 node.select('circle')
1039 .style('stroke-fill', '#555')
1040 .style('fill', '#888')
1041 .style('opacity', 0.5)
1042 .transition()
1043 .duration(1500)
1044 .attr('r', 0);
1045 // note, leave <g>.remove to remove this element
1046
1047 });
1048
1049 // TODO: device node exits
Simon Huntc7ee0662014-11-05 16:44:37 -08001050 }
1051
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001052 function find(id, array) {
1053 for (var idx = 0, n = array.length; idx < n; idx++) {
1054 if (array[idx].id === id) {
1055 return idx;
1056 }
1057 }
1058 return -1;
1059 }
1060
1061 function removeLinkElement(linkData) {
1062 // remove from lookup cache
1063 delete network.lookup[linkData.id];
1064 // remove from links array
1065 var idx = find(linkData.id, network.links);
Simon Huntfc274c92014-11-11 11:05:46 -08001066 network.links.splice(idx, 1);
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001067 // remove from SVG
1068 updateLinks();
Simon Hunt44031102014-11-11 13:20:36 -08001069 network.force.resume();
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001070 }
Simon Huntc7ee0662014-11-05 16:44:37 -08001071
Simon Hunt44031102014-11-11 13:20:36 -08001072 function removeHostElement(hostData) {
1073 // first, remove associated hostLink...
1074 removeLinkElement(hostData.linkData);
1075
1076 // remove from lookup cache
1077 delete network.lookup[hostData.id];
1078 // remove from nodes array
1079 var idx = find(hostData.id, network.nodes);
1080 network.nodes.splice(idx, 1);
1081 // remove from SVG
1082 updateNodes();
1083 network.force.resume();
1084 }
1085
1086
Simon Huntc7ee0662014-11-05 16:44:37 -08001087 function tick() {
1088 node.attr({
Simon Hunt99c13842014-11-06 18:23:12 -08001089 transform: function (d) { return translate(d.x, d.y); }
Simon Huntc7ee0662014-11-05 16:44:37 -08001090 });
1091
1092 link.attr({
1093 x1: function (d) { return d.source.x; },
1094 y1: function (d) { return d.source.y; },
1095 x2: function (d) { return d.target.x; },
1096 y2: function (d) { return d.target.y; }
1097 });
1098 }
Simon Hunt934c3ce2014-11-05 11:45:07 -08001099
1100 // ==============================
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001101 // Web-Socket for live data
1102
1103 function webSockUrl() {
1104 return document.location.toString()
1105 .replace(/\#.*/, '')
1106 .replace('http://', 'ws://')
1107 .replace('https://', 'wss://')
1108 .replace('index2.html', config.webSockUrl);
1109 }
1110
1111 webSock = {
1112 ws : null,
1113
1114 connect : function() {
1115 webSock.ws = new WebSocket(webSockUrl());
1116
1117 webSock.ws.onopen = function() {
Simon Hunt0c6d4192014-11-12 12:07:10 -08001118 noWebSock(false);
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001119 };
1120
1121 webSock.ws.onmessage = function(m) {
1122 if (m.data) {
Simon Huntbb282f52014-11-10 11:08:19 -08001123 wsTraceRx(m.data);
Thomas Vachuskad472c6e2014-11-07 19:11:05 -08001124 handleServerEvent(JSON.parse(m.data));
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001125 }
1126 };
1127
1128 webSock.ws.onclose = function(m) {
1129 webSock.ws = null;
Simon Hunt0c6d4192014-11-12 12:07:10 -08001130 noWebSock(true);
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001131 };
1132 },
1133
1134 send : function(text) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001135 if (text != null) {
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001136 webSock._send(text);
1137 }
1138 },
1139
1140 _send : function(message) {
1141 if (webSock.ws) {
1142 webSock.ws.send(message);
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001143 } else {
Simon Hunt56d51852014-11-09 13:03:35 -08001144 network.view.alert('no web socket open\n\n' + message);
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001145 }
1146 }
1147
1148 };
1149
Simon Hunt0c6d4192014-11-12 12:07:10 -08001150 function noWebSock(b) {
1151 mask.style('display',b ? 'block' : 'none');
1152 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001153
Simon Hunt61d04042014-11-11 17:27:16 -08001154 // TODO: use cache of pending messages (key = sid) to reconcile responses
1155
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001156 function sendMessage(evType, payload) {
1157 var toSend = {
Simon Huntbb282f52014-11-10 11:08:19 -08001158 event: evType,
1159 sid: ++sid,
1160 payload: payload
1161 },
1162 asText = JSON.stringify(toSend);
1163 wsTraceTx(asText);
1164 webSock.send(asText);
1165 }
1166
1167 function wsTraceTx(msg) {
1168 wsTrace('tx', msg);
1169 }
1170 function wsTraceRx(msg) {
1171 wsTrace('rx', msg);
1172 }
1173 function wsTrace(rxtx, msg) {
Simon Huntbb282f52014-11-10 11:08:19 -08001174 console.log('[' + rxtx + '] ' + msg);
1175 // TODO: integrate with trace view
1176 //if (trace) {
1177 // trace.output(rxtx, msg);
1178 //}
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001179 }
1180
1181
1182 // ==============================
1183 // Selection stuff
1184
1185 function selectObject(obj, el) {
1186 var n,
1187 meta = d3.event.sourceEvent.metaKey;
1188
1189 if (el) {
1190 n = d3.select(el);
1191 } else {
1192 node.each(function(d) {
1193 if (d == obj) {
1194 n = d3.select(el = this);
1195 }
1196 });
1197 }
1198 if (!n) return;
1199
1200 if (meta && n.classed('selected')) {
1201 deselectObject(obj.id);
Simon Hunt61d04042014-11-11 17:27:16 -08001202 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001203 return;
1204 }
1205
1206 if (!meta) {
1207 deselectAll();
1208 }
1209
Simon Huntc31d5692014-11-12 13:27:18 -08001210 selections[obj.id] = { obj: obj, el: el };
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001211 selectOrder.push(obj.id);
1212
1213 n.classed('selected', true);
Simon Hunt61d04042014-11-11 17:27:16 -08001214 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001215 }
1216
1217 function deselectObject(id) {
Simon Huntc31d5692014-11-12 13:27:18 -08001218 var obj = selections[id],
1219 idx;
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001220 if (obj) {
1221 d3.select(obj.el).classed('selected', false);
Simon Hunt61d04042014-11-11 17:27:16 -08001222 delete selections[id];
Simon Huntc31d5692014-11-12 13:27:18 -08001223 idx = $.inArray(id, selectOrder);
1224 if (idx >= 0) {
1225 selectOrder.splice(idx, 1);
1226 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001227 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001228 }
1229
1230 function deselectAll() {
1231 // deselect all nodes in the network...
1232 node.classed('selected', false);
1233 selections = {};
1234 selectOrder = [];
Simon Hunt61d04042014-11-11 17:27:16 -08001235 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001236 }
1237
Simon Hunt61d04042014-11-11 17:27:16 -08001238 // FIXME: this click handler does not get unloaded when the view does
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001239 $('#view').on('click', function(e) {
1240 if (!$(e.target).closest('.node').length) {
1241 if (!e.metaKey) {
1242 deselectAll();
1243 }
1244 }
1245 });
1246
Simon Hunt61d04042014-11-11 17:27:16 -08001247 // update the state of the detail pane, based on current selections
1248 function updateDetailPane() {
1249 var nSel = selectOrder.length;
1250 if (!nSel) {
1251 detailPane.hide();
1252 } else if (nSel === 1) {
1253 singleSelect();
1254 } else {
1255 multiSelect();
1256 }
1257 }
1258
1259 function singleSelect() {
1260 requestDetails();
Simon Huntb53e0682014-11-12 13:32:01 -08001261 // NOTE: detail pane will be shown from showDetails event callback
Simon Hunt61d04042014-11-11 17:27:16 -08001262 }
1263
1264 function multiSelect() {
Simon Huntb53e0682014-11-12 13:32:01 -08001265 populateMultiSelect();
1266 // TODO: Add multi-select actions ...
1267 }
1268
1269 function addSep(tbody) {
1270 var tr = tbody.append('tr');
1271 $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
1272 }
1273
1274 function addProp(tbody, label, value) {
1275 var tr = tbody.append('tr');
1276
1277 tr.append('td')
1278 .attr('class', 'label')
1279 .text(label + ' :');
1280
1281 tr.append('td')
1282 .attr('class', 'value')
1283 .text(value);
1284 }
1285
1286 function populateMultiSelect() {
1287 detailPane.empty();
1288
1289 var title = detailPane.append("h2"),
1290 table = detailPane.append("table"),
1291 tbody = table.append("tbody");
1292
1293 title.text('Multi-Select...');
1294
1295 selectOrder.forEach(function (d, i) {
1296 addProp(tbody, i+1, d);
1297 });
Simon Hunt61d04042014-11-11 17:27:16 -08001298 }
1299
1300 function populateDetails(data) {
1301 detailPane.empty();
1302
1303 var title = detailPane.append("h2"),
1304 table = detailPane.append("table"),
1305 tbody = table.append("tbody");
1306
1307 $('<img src="img/' + data.type + '.png">').appendTo(title);
1308 $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
1309
1310 data.propOrder.forEach(function(p) {
1311 if (p === '-') {
1312 addSep(tbody);
1313 } else {
1314 addProp(tbody, p, data.props[p]);
1315 }
1316 });
Simon Hunt61d04042014-11-11 17:27:16 -08001317 }
1318
1319 // ==============================
1320 // Test harness code
Simon Hunt56d51852014-11-09 13:03:35 -08001321
1322 function prepareScenario(view, ctx, dbg) {
1323 var sc = scenario,
1324 urlSc = sc.evDir + ctx + sc.evScenario;
1325
1326 if (!ctx) {
1327 view.alert("No scenario specified (null ctx)");
1328 return;
1329 }
1330
1331 sc.view = view;
1332 sc.ctx = ctx;
1333 sc.debug = dbg;
1334 sc.evNumber = 0;
1335
1336 d3.json(urlSc, function(err, data) {
Simon Huntbb282f52014-11-10 11:08:19 -08001337 var p = data && data.params || {},
1338 desc = data && data.description || null,
Simon Huntfc274c92014-11-11 11:05:46 -08001339 intro = data && data.title;
Simon Huntbb282f52014-11-10 11:08:19 -08001340
Simon Hunt56d51852014-11-09 13:03:35 -08001341 if (err) {
1342 view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
1343 } else {
1344 sc.params = p;
Simon Huntbb282f52014-11-10 11:08:19 -08001345 if (desc) {
1346 intro += '\n\n ' + desc.join('\n ');
1347 }
1348 view.alert(intro);
Simon Hunt56d51852014-11-09 13:03:35 -08001349 }
1350 });
1351
1352 }
1353
Simon Hunt0c6d4192014-11-12 12:07:10 -08001354
1355 function para(sel, text) {
1356 sel.append('p').text(text);
1357 }
1358
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001359 // ==============================
Simon Hunt142d0032014-11-04 20:13:09 -08001360 // View life-cycle callbacks
Simon Hunt195cb382014-11-03 17:50:51 -08001361
Simon Huntf67722a2014-11-10 09:32:06 -08001362 function preload(view, ctx, flags) {
Simon Hunt142d0032014-11-04 20:13:09 -08001363 var w = view.width(),
1364 h = view.height(),
Simon Huntc7ee0662014-11-05 16:44:37 -08001365 fcfg = config.force,
1366 fpad = fcfg.pad,
1367 forceDim = [w - 2*fpad, h - 2*fpad];
Simon Hunt195cb382014-11-03 17:50:51 -08001368
Simon Huntbb282f52014-11-10 11:08:19 -08001369 // TODO: set trace api
1370 //trace = onos.exported.webSockTrace;
1371
Simon Hunt142d0032014-11-04 20:13:09 -08001372 // NOTE: view.$div is a D3 selection of the view's div
Paul Greyson6cb8ca02014-11-12 18:09:02 -08001373 var viewBox = '0 0 ' + config.logicalSize + ' ' + config.logicalSize;
1374 svg = view.$div.append('svg').attr('viewBox', viewBox);
Simon Hunt934c3ce2014-11-05 11:45:07 -08001375 setSize(svg, view);
1376
Simon Hunt1a9eff92014-11-07 11:06:34 -08001377 // add blue glow filter to svg layer
1378 d3u.appendGlow(svg);
1379
Simon Huntc7ee0662014-11-05 16:44:37 -08001380 // group for the topology
1381 topoG = svg.append('g')
Simon Huntd3b7d512014-11-12 15:48:41 -08001382 .attr('id', 'topo-G')
Simon Huntc7ee0662014-11-05 16:44:37 -08001383 .attr('transform', fcfg.translate());
1384
1385 // subgroups for links and nodes
1386 linkG = topoG.append('g').attr('id', 'links');
1387 nodeG = topoG.append('g').attr('id', 'nodes');
1388
1389 // selection of nodes and links
1390 link = linkG.selectAll('.link');
1391 node = nodeG.selectAll('.node');
1392
Simon Hunt7cd48f32014-11-09 23:42:50 -08001393 function chrg(d) {
1394 return fcfg.charge[d.class] || -12000;
1395 }
Simon Hunt99c13842014-11-06 18:23:12 -08001396 function ldist(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08001397 return fcfg.linkDistance[d.type] || 50;
Simon Hunt99c13842014-11-06 18:23:12 -08001398 }
1399 function lstrg(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08001400 // 0.0 - 1.0
1401 return fcfg.linkStrength[d.type] || 1.0;
Simon Hunt99c13842014-11-06 18:23:12 -08001402 }
1403
Simon Hunt1a9eff92014-11-07 11:06:34 -08001404 function selectCb(d, self) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001405 selectObject(d, self);
Simon Hunt1a9eff92014-11-07 11:06:34 -08001406 }
1407
1408 function atDragEnd(d, self) {
Simon Hunt56d51852014-11-09 13:03:35 -08001409 // once we've finished moving, pin the node in position
1410 d.fixed = true;
1411 d3.select(self).classed('fixed', true);
1412 if (config.useLiveData) {
Simon Hunt902c9922014-11-11 11:59:31 -08001413 sendUpdateMeta(d);
Simon Hunt1a9eff92014-11-07 11:06:34 -08001414 }
1415 }
1416
Simon Hunt902c9922014-11-11 11:59:31 -08001417 function sendUpdateMeta(d) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001418 sendMessage('updateMeta', {
1419 id: d.id,
1420 'class': d.class,
Simon Hunt902c9922014-11-11 11:59:31 -08001421 'memento': {
1422 x: Math.floor(d.x),
1423 y: Math.floor(d.y)
1424 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001425 });
1426 }
1427
Simon Huntc7ee0662014-11-05 16:44:37 -08001428 // set up the force layout
1429 network.force = d3.layout.force()
1430 .size(forceDim)
1431 .nodes(network.nodes)
1432 .links(network.links)
Simon Hunt7cd48f32014-11-09 23:42:50 -08001433 .gravity(0.4)
1434 .friction(0.7)
1435 .charge(chrg)
Simon Hunt99c13842014-11-06 18:23:12 -08001436 .linkDistance(ldist)
1437 .linkStrength(lstrg)
Simon Huntc7ee0662014-11-05 16:44:37 -08001438 .on('tick', tick);
Simon Hunt195cb382014-11-03 17:50:51 -08001439
Simon Hunt1a9eff92014-11-07 11:06:34 -08001440 network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
Simon Hunt0c6d4192014-11-12 12:07:10 -08001441
1442 // create mask layer for when we lose connection to server.
1443 mask = view.$div.append('div').attr('id','topo-mask');
1444 para(mask, 'Oops!');
1445 para(mask, 'Web-socket connection to server closed...');
1446 para(mask, 'Try refreshing the page.');
Simon Hunt1a9eff92014-11-07 11:06:34 -08001447 }
Simon Hunt195cb382014-11-03 17:50:51 -08001448
Simon Hunt56d51852014-11-09 13:03:35 -08001449 function load(view, ctx, flags) {
Simon Huntf67722a2014-11-10 09:32:06 -08001450 // resize, in case the window was resized while we were not loaded
1451 resize(view, ctx, flags);
1452
Simon Hunt99c13842014-11-06 18:23:12 -08001453 // cache the view token, so network topo functions can access it
1454 network.view = view;
Simon Hunt56d51852014-11-09 13:03:35 -08001455 config.useLiveData = !flags.local;
1456
1457 if (!config.useLiveData) {
1458 prepareScenario(view, ctx, flags.debug);
1459 }
Simon Hunt99c13842014-11-06 18:23:12 -08001460
1461 // set our radio buttons and key bindings
Simon Hunt934c3ce2014-11-05 11:45:07 -08001462 view.setRadio(btnSet);
1463 view.setKeys(keyDispatch);
Simon Hunt195cb382014-11-03 17:50:51 -08001464
Simon Huntd3b7d512014-11-12 15:48:41 -08001465 // Load map data asynchronously; complete startup after that..
1466 loadGeoJsonData();
1467 }
1468
1469 // TODO: move these to config/state portion of script
Paul Greyson6cb8ca02014-11-12 18:09:02 -08001470 var geoJsonUrl = 'json/map/continental_us.json', // TODO: Paul
Simon Huntd3b7d512014-11-12 15:48:41 -08001471 geoJson;
1472
1473 function loadGeoJsonData() {
1474 d3.json(geoJsonUrl, function (err, data) {
1475 if (err) {
1476 // fall back to USA map background
1477 loadStaticMap();
1478 } else {
1479 geoJson = data;
1480 loadGeoMap();
1481 }
1482
1483 // finally, connect to the server...
1484 if (config.useLiveData) {
1485 webSock.connect();
1486 }
1487 });
1488 }
1489
1490 function showBg() {
1491 return config.options.showBackground ? 'visible' : 'hidden';
1492 }
1493
1494 function loadStaticMap() {
1495 fnTrace('loadStaticMap', config.backgroundUrl);
1496 var w = network.view.width(),
1497 h = network.view.height();
1498
1499 // load the background image
1500 bgImg = svg.insert('svg:image', '#topo-G')
1501 .attr({
1502 id: 'topo-bg',
1503 width: w,
1504 height: h,
1505 'xlink:href': config.backgroundUrl
1506 })
1507 .style({
1508 visibility: showBg()
1509 });
1510 }
1511
1512 function loadGeoMap() {
1513 fnTrace('loadGeoMap', geoJsonUrl);
Simon Huntd3b7d512014-11-12 15:48:41 -08001514
Paul Greyson6cb8ca02014-11-12 18:09:02 -08001515 // extracts the topojson data into geocoordinate-based geometry
1516 var topoData = topojson.feature(geoJson, geoJson.objects.states);
Simon Huntd3b7d512014-11-12 15:48:41 -08001517
Paul Greyson6cb8ca02014-11-12 18:09:02 -08001518 // see: http://bl.ocks.org/mbostock/4707858
1519 geoMapProjection = d3.geo.mercator();
1520 var path = d3.geo.path().projection(geoMapProjection);
Simon Huntd3b7d512014-11-12 15:48:41 -08001521
Paul Greyson6cb8ca02014-11-12 18:09:02 -08001522 geoMapProjection
1523 .scale(1)
1524 .translate([0, 0]);
Simon Huntd3b7d512014-11-12 15:48:41 -08001525
Paul Greyson6cb8ca02014-11-12 18:09:02 -08001526 // [[x1,y1],[x2,y2]]
1527 var b = path.bounds(topoData);
1528 // TODO: why 1.75?
1529 var s = 1.75 / Math.max((b[1][0] - b[0][0]) / config.logicalSize, (b[1][1] - b[0][1]) / config.logicalSize);
1530 var t = [(config.logicalSize - s * (b[1][0] + b[0][0])) / 2, (config.logicalSize - s * (b[1][1] + b[0][1])) / 2];
Simon Huntd3b7d512014-11-12 15:48:41 -08001531
Paul Greyson6cb8ca02014-11-12 18:09:02 -08001532 geoMapProjection
1533 .scale(s)
1534 .translate(t);
1535
1536 bgImg = svg.insert("g", '#topo-G');
1537 bgImg.attr('id', 'map').selectAll('path')
1538 .data(topoData.features)
1539 .enter()
1540 .append('path')
1541 .attr('d', path);
Simon Hunt195cb382014-11-03 17:50:51 -08001542 }
1543
Simon Huntf67722a2014-11-10 09:32:06 -08001544 function resize(view, ctx, flags) {
Simon Hunt934c3ce2014-11-05 11:45:07 -08001545 setSize(svg, view);
Simon Hunt99c13842014-11-06 18:23:12 -08001546
1547 // TODO: hook to recompute layout, perhaps? work with zoom/pan code
1548 // adjust force layout size
Simon Hunt142d0032014-11-04 20:13:09 -08001549 }
1550
1551
1552 // ==============================
1553 // View registration
Simon Hunt195cb382014-11-03 17:50:51 -08001554
Simon Hunt25248912014-11-04 11:25:48 -08001555 onos.ui.addView('topo', {
Simon Hunt142d0032014-11-04 20:13:09 -08001556 preload: preload,
1557 load: load,
1558 resize: resize
Simon Hunt195cb382014-11-03 17:50:51 -08001559 });
1560
Simon Hunt61d04042014-11-11 17:27:16 -08001561 detailPane = onos.ui.addFloatingPanel('topo-detail');
1562
Simon Hunt195cb382014-11-03 17:50:51 -08001563}(ONOS));