blob: d58de159f2721f5a9bf1df38bea2d1f5b52a7a85 [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
Simon Hunta6a9fe72014-11-20 11:17:12 -080021 @author Thomas Vachuska
Simon Hunt195cb382014-11-03 17:50:51 -080022 */
23
24(function (onos) {
25 'use strict';
26
Simon Hunt1a9eff92014-11-07 11:06:34 -080027 // shorter names for library APIs
Simon Huntbb282f52014-11-10 11:08:19 -080028 var d3u = onos.lib.d3util,
Simon Hunta6a9fe72014-11-20 11:17:12 -080029 gly = onos.lib.glyphs;
Simon Hunt1a9eff92014-11-07 11:06:34 -080030
Simon Hunt195cb382014-11-03 17:50:51 -080031 // configuration data
32 var config = {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -080033 useLiveData: true,
Simon Huntfc274c92014-11-11 11:05:46 -080034 fnTrace: true,
Simon Hunt195cb382014-11-03 17:50:51 -080035 debugOn: false,
36 debug: {
Simon Hunt99c13842014-11-06 18:23:12 -080037 showNodeXY: true,
38 showKeyHandler: false
Simon Hunt195cb382014-11-03 17:50:51 -080039 },
Simon Hunt12ce12e2014-11-15 21:13:19 -080040 birdDim: 400,
Simon Hunt195cb382014-11-03 17:50:51 -080041 options: {
42 layering: true,
43 collisionPrevention: true,
Simon Hunt142d0032014-11-04 20:13:09 -080044 showBackground: true
Simon Hunt195cb382014-11-03 17:50:51 -080045 },
46 backgroundUrl: 'img/us-map.png',
Thomas Vachuska7d638d32014-11-07 10:24:43 -080047 webSockUrl: 'ws/topology',
Simon Hunt195cb382014-11-03 17:50:51 -080048 data: {
49 live: {
50 jsonUrl: 'rs/topology/graph',
51 detailPrefix: 'rs/topology/graph/',
52 detailSuffix: ''
53 },
54 fake: {
55 jsonUrl: 'json/network2.json',
56 detailPrefix: 'json/',
57 detailSuffix: '.json'
58 }
59 },
Simon Hunt99c13842014-11-06 18:23:12 -080060 labels: {
61 imgPad: 16,
62 padLR: 4,
63 padTB: 3,
64 marginLR: 3,
65 marginTB: 2,
66 port: {
67 gap: 3,
68 width: 18,
69 height: 14
70 }
71 },
Simon Hunt1a9eff92014-11-07 11:06:34 -080072 topo: {
Thomas Vachuska89543292014-11-19 11:28:33 -080073 linkBaseColor: '#666',
Simon Hunt1a9eff92014-11-07 11:06:34 -080074 linkInColor: '#66f',
Paul Greyson29cd58f2014-11-18 13:14:57 -080075 linkInWidth: 14,
Thomas Vachuska89543292014-11-19 11:28:33 -080076 linkOutColor: '#f00',
Simon Hunt95908012014-11-20 10:20:26 -080077 linkOutWidth: 14
Simon Hunt1a9eff92014-11-07 11:06:34 -080078 },
Paul Greyson29cd58f2014-11-18 13:14:57 -080079 icons: {
Thomas Vachuskaece59ee2014-11-19 19:06:11 -080080 w: 30,
81 h: 30,
82 xoff: -16,
Simon Huntc72967b2014-11-20 09:21:42 -080083 yoff: -14,
84
85 device: {
86 dim: 30,
87 rx: 4
88 }
Thomas Vachuska89543292014-11-19 11:28:33 -080089 },
90 iconUrl: {
91 device: 'img/device.png',
92 host: 'img/host.png',
93 pkt: 'img/pkt.png',
94 opt: 'img/opt.png'
Simon Hunt195cb382014-11-03 17:50:51 -080095 },
Simon Hunt195cb382014-11-03 17:50:51 -080096 force: {
Simon Hunt7cd48f32014-11-09 23:42:50 -080097 note_for_links: 'link.type is used to differentiate',
Simon Huntc7ee0662014-11-05 16:44:37 -080098 linkDistance: {
Simon Hunt7cd48f32014-11-09 23:42:50 -080099 direct: 100,
100 optical: 120,
Thomas Vachuska3266abf2014-11-13 09:28:46 -0800101 hostLink: 3
Simon Huntc7ee0662014-11-05 16:44:37 -0800102 },
103 linkStrength: {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800104 direct: 1.0,
105 optical: 1.0,
106 hostLink: 1.0
Simon Huntc7ee0662014-11-05 16:44:37 -0800107 },
Simon Hunt7cd48f32014-11-09 23:42:50 -0800108 note_for_nodes: 'node.class is used to differentiate',
Simon Huntc7ee0662014-11-05 16:44:37 -0800109 charge: {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800110 device: -8000,
Thomas Vachuska3266abf2014-11-13 09:28:46 -0800111 host: -5000
Simon Huntc7ee0662014-11-05 16:44:37 -0800112 },
113 pad: 20,
Simon Hunt195cb382014-11-03 17:50:51 -0800114 translate: function() {
115 return 'translate(' +
Simon Huntc7ee0662014-11-05 16:44:37 -0800116 config.force.pad + ',' +
117 config.force.pad + ')';
Simon Hunt195cb382014-11-03 17:50:51 -0800118 }
Paul Greyson6cb8ca02014-11-12 18:09:02 -0800119 },
120 // see below in creation of viewBox on main svg
121 logicalSize: 1000
Simon Hunt195cb382014-11-03 17:50:51 -0800122 };
123
Simon Hunt142d0032014-11-04 20:13:09 -0800124 // radio buttons
Simon Hunt9462e8c2014-11-14 17:28:09 -0800125 var layerButtons = [
126 { text: 'All Layers', id: 'all', cb: showAllLayers },
127 { text: 'Packet Only', id: 'pkt', cb: showPacketLayer },
128 { text: 'Optical Only', id: 'opt', cb: showOpticalLayer }
129 ],
130 layerBtnSet,
131 layerBtnDispatch = {
132 all: showAllLayers,
133 pkt: showPacketLayer,
134 opt: showOpticalLayer
135 };
Simon Hunt934c3ce2014-11-05 11:45:07 -0800136
137 // key bindings
138 var keyDispatch = {
Simon Hunt988c6fc2014-11-20 17:43:03 -0800139 // TODO: remove these "development only" bindings
140 M: testMe,
141 S: injectStartupEvents,
142 space: injectTestEvent,
Simon Hunt99c13842014-11-06 18:23:12 -0800143
Thomas Vachuska47635c62014-11-22 01:21:36 -0800144 O: [toggleSummary, 'Toggle ONOS summary pane'],
145 I: [toggleInstances, 'Toggle ONOS instances pane'],
Simon Hunt988c6fc2014-11-20 17:43:03 -0800146 B: [toggleBg, 'Toggle background image'],
147 L: [cycleLabels, 'Cycle Device labels'],
Simon Hunt934c3ce2014-11-05 11:45:07 -0800148 P: togglePorts,
Simon Hunt56ef0fe2014-11-21 08:24:43 -0800149 U: [unpin, 'Unpin node'],
150 R: [resetZoomPan, 'Reset zoom/pan'],
151 H: [cycleHoverMode, 'Cycle hover mode'],
152 V: [showTrafficAction, 'Show traffic'],
153 A: [showAllTrafficAction, 'Show all traffic'],
154 F: [showDeviceLinkFlowsAction, 'Show device link flows'],
Simon Hunt9462e8c2014-11-14 17:28:09 -0800155 esc: handleEscape
Simon Hunt934c3ce2014-11-05 11:45:07 -0800156 };
Simon Hunt142d0032014-11-04 20:13:09 -0800157
Simon Hunt195cb382014-11-03 17:50:51 -0800158 // state variables
Simon Hunt99c13842014-11-06 18:23:12 -0800159 var network = {
Simon Hunt50128c02014-11-08 13:36:15 -0800160 view: null, // view token reference
Simon Hunt99c13842014-11-06 18:23:12 -0800161 nodes: [],
162 links: [],
Simon Hunt269670f2014-11-17 16:17:43 -0800163 lookup: {},
164 revLinkToKey: {}
Simon Hunt99c13842014-11-06 18:23:12 -0800165 },
Simon Hunt56d51852014-11-09 13:03:35 -0800166 scenario = {
167 evDir: 'json/ev/',
168 evScenario: '/scenario.json',
169 evPrefix: '/ev_',
170 evOnos: '_onos.json',
171 evUi: '_ui.json',
172 ctx: null,
173 params: {},
174 evNumber: 0,
175 view: null,
176 debug: false
177 },
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800178 webSock,
Simon Hunt0c6d4192014-11-12 12:07:10 -0800179 sid = 0,
Thomas Vachuska60d72bf2014-11-21 13:02:00 -0800180 deviceLabelCount = 3,
Simon Hunt209155e2014-11-21 12:16:09 -0800181 hostLabelCount = 2,
Simon Hunt56d51852014-11-09 13:03:35 -0800182 deviceLabelIndex = 0,
183 hostLabelIndex = 0,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800184 selections = {},
Simon Hunta5e89142014-11-14 07:00:33 -0800185 selectOrder = [],
Simon Hunt6ac93f32014-11-13 12:17:27 -0800186 hovered = null,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800187 summaryPane,
Simon Hunta5e89142014-11-14 07:00:33 -0800188 detailPane,
Simon Hunta255a2c2014-11-13 22:29:35 -0800189 antTimer = null,
Simon Hunta5e89142014-11-14 07:00:33 -0800190 onosInstances = {},
191 onosOrder = [],
192 oiBox,
Simon Hunt9462e8c2014-11-14 17:28:09 -0800193 oiShowMaster = false,
Thomas Vachuska29617e52014-11-20 03:17:46 -0800194 hoverModes = [ 'none', 'intents', 'flows'],
195 hoverMode = 0,
Simon Hunt195cb382014-11-03 17:50:51 -0800196 portLabelsOn = false;
197
Simon Hunt934c3ce2014-11-05 11:45:07 -0800198 // D3 selections
199 var svg,
Paul Greysonfcba0e82014-11-13 10:21:16 -0800200 zoomPanContainer,
Simon Hunt934c3ce2014-11-05 11:45:07 -0800201 bgImg,
Simon Huntc7ee0662014-11-05 16:44:37 -0800202 topoG,
203 nodeG,
204 linkG,
Simon Hunte2575b62014-11-18 15:25:53 -0800205 linkLabelG,
Simon Huntc7ee0662014-11-05 16:44:37 -0800206 node,
Simon Hunt0c6d4192014-11-12 12:07:10 -0800207 link,
Simon Hunte2575b62014-11-18 15:25:53 -0800208 linkLabel,
Simon Hunt0c6d4192014-11-12 12:07:10 -0800209 mask;
Simon Hunt195cb382014-11-03 17:50:51 -0800210
Paul Greyson6cb8ca02014-11-12 18:09:02 -0800211 // the projection for the map background
212 var geoMapProjection;
213
Paul Greysonfcba0e82014-11-13 10:21:16 -0800214 // the zoom function
215 var zoom;
216
Simon Hunt142d0032014-11-04 20:13:09 -0800217 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800218 // For Debugging / Development
Simon Hunt195cb382014-11-03 17:50:51 -0800219
Simon Hunt99c13842014-11-06 18:23:12 -0800220 function note(label, msg) {
221 console.log('NOTE: ' + label + ': ' + msg);
Simon Hunt195cb382014-11-03 17:50:51 -0800222 }
223
Simon Hunt99c13842014-11-06 18:23:12 -0800224 function debug(what) {
225 return config.debugOn && config.debug[what];
Simon Hunt934c3ce2014-11-05 11:45:07 -0800226 }
227
Simon Huntfc274c92014-11-11 11:05:46 -0800228 function fnTrace(msg, id) {
229 if (config.fnTrace) {
230 console.log('FN: ' + msg + ' [' + id + ']');
231 }
232 }
Simon Hunt99c13842014-11-06 18:23:12 -0800233
Simon Hunta5e89142014-11-14 07:00:33 -0800234 function evTrace(data) {
235 fnTrace(data.event, data.payload.id);
236 }
237
Simon Hunt934c3ce2014-11-05 11:45:07 -0800238 // ==============================
239 // Key Callbacks
240
Simon Hunt99c13842014-11-06 18:23:12 -0800241 function testMe(view) {
Simon Hunta3dd9572014-11-20 15:22:41 -0800242 //view.alert('Theme is ' + view.theme());
243 //view.flash('This is some text');
Simon Hunt99c13842014-11-06 18:23:12 -0800244 }
245
Simon Hunt56d51852014-11-09 13:03:35 -0800246 function abortIfLive() {
Simon Hunt50128c02014-11-08 13:36:15 -0800247 if (config.useLiveData) {
Simon Huntb53e0682014-11-12 13:32:01 -0800248 network.view.alert("Sorry, currently using live data..");
Simon Hunt56d51852014-11-09 13:03:35 -0800249 return true;
Simon Hunt50128c02014-11-08 13:36:15 -0800250 }
Simon Hunt56d51852014-11-09 13:03:35 -0800251 return false;
252 }
Simon Hunt50128c02014-11-08 13:36:15 -0800253
Simon Hunt56d51852014-11-09 13:03:35 -0800254 function testDebug(msg) {
255 if (scenario.debug) {
256 scenario.view.alert(msg);
257 }
258 }
Simon Hunt99c13842014-11-06 18:23:12 -0800259
Simon Hunt56d51852014-11-09 13:03:35 -0800260 function injectTestEvent(view) {
261 if (abortIfLive()) { return; }
262 var sc = scenario,
263 evn = ++sc.evNumber,
264 pfx = sc.evDir + sc.ctx + sc.evPrefix + evn,
265 onosUrl = pfx + sc.evOnos,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800266 uiUrl = pfx + sc.evUi,
267 stack = [
268 { url: onosUrl, cb: handleServerEvent },
269 { url: uiUrl, cb: handleUiEvent }
270 ];
271 recurseFetchEvent(stack, evn);
Simon Hunt56d51852014-11-09 13:03:35 -0800272 }
273
Simon Hunt7cd48f32014-11-09 23:42:50 -0800274 function recurseFetchEvent(stack, evn) {
275 var v = scenario.view,
276 frame;
277 if (stack.length === 0) {
Simon Huntfc274c92014-11-11 11:05:46 -0800278 v.alert('Oops!\n\nNo event #' + evn + ' found.');
Simon Hunt7cd48f32014-11-09 23:42:50 -0800279 return;
280 }
281 frame = stack.shift();
282
283 d3.json(frame.url, function (err, data) {
Simon Hunt99c13842014-11-06 18:23:12 -0800284 if (err) {
Simon Hunt56d51852014-11-09 13:03:35 -0800285 if (err.status === 404) {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800286 // if we didn't find the data, try the next stack frame
287 recurseFetchEvent(stack, evn);
Simon Hunt56d51852014-11-09 13:03:35 -0800288 } else {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800289 v.alert('non-404 error:\n\n' + frame.url + '\n\n' + err);
Simon Hunt56d51852014-11-09 13:03:35 -0800290 }
Simon Hunt99c13842014-11-06 18:23:12 -0800291 } else {
Simon Hunt7cd48f32014-11-09 23:42:50 -0800292 testDebug('loaded: ' + frame.url);
Simon Hunt1712ed82014-11-17 12:56:00 -0800293 wsTrace('test', JSON.stringify(data));
Simon Hunt7cd48f32014-11-09 23:42:50 -0800294 frame.cb(data);
Simon Hunt99c13842014-11-06 18:23:12 -0800295 }
296 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800297
Simon Hunt56d51852014-11-09 13:03:35 -0800298 }
Simon Hunt50128c02014-11-08 13:36:15 -0800299
Simon Hunt56d51852014-11-09 13:03:35 -0800300 function handleUiEvent(data) {
Simon Huntbb282f52014-11-10 11:08:19 -0800301 scenario.view.alert('UI Tx: ' + data.event + '\n\n' +
302 JSON.stringify(data));
Simon Hunt56d51852014-11-09 13:03:35 -0800303 }
304
305 function injectStartupEvents(view) {
306 var last = scenario.params.lastAuto || 0;
307 if (abortIfLive()) { return; }
308
309 while (scenario.evNumber < last) {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800310 injectTestEvent(view);
311 }
312 }
313
Simon Hunt934c3ce2014-11-05 11:45:07 -0800314 function toggleBg() {
315 var vis = bgImg.style('visibility');
316 bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
317 }
318
Simon Hunt99c13842014-11-06 18:23:12 -0800319 function cycleLabels() {
Thomas Vachuska60d72bf2014-11-21 13:02:00 -0800320 deviceLabelIndex = (deviceLabelIndex === 2)
Simon Huntbb282f52014-11-10 11:08:19 -0800321 ? 0 : deviceLabelIndex + 1;
Simon Hunt5f36d342014-11-08 21:33:14 -0800322
Simon Hunt99c13842014-11-06 18:23:12 -0800323 network.nodes.forEach(function (d) {
Simon Huntbb282f52014-11-10 11:08:19 -0800324 if (d.class === 'device') {
325 updateDeviceLabel(d);
326 }
Simon Hunt99c13842014-11-06 18:23:12 -0800327 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800328 }
329
Simon Hunt56ef0fe2014-11-21 08:24:43 -0800330 function cycleHoverMode(view) {
Thomas Vachuska29617e52014-11-20 03:17:46 -0800331 hoverMode++;
332 if (hoverMode === hoverModes.length) {
333 hoverMode = 0;
334 }
Thomas Vachuska47635c62014-11-22 01:21:36 -0800335 view.flash('Mode: ' + hoverModes[hoverMode]);
Thomas Vachuskaf75b7ab2014-11-19 12:15:55 -0800336 }
337
Simon Hunt934c3ce2014-11-05 11:45:07 -0800338 function togglePorts(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800339 view.alert('togglePorts() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800340 }
341
Simon Hunt6ac93f32014-11-13 12:17:27 -0800342 function unpin() {
343 if (hovered) {
344 hovered.fixed = false;
345 hovered.el.classed('fixed', false);
346 network.force.resume();
347 }
Simon Hunt934c3ce2014-11-05 11:45:07 -0800348 }
349
Simon Hunt9462e8c2014-11-14 17:28:09 -0800350 function handleEscape(view) {
351 if (oiShowMaster) {
352 cancelAffinity();
Thomas Vachuska47635c62014-11-22 01:21:36 -0800353 } else if (detailPane.isVisible()) {
Simon Hunt9462e8c2014-11-14 17:28:09 -0800354 deselectAll();
Thomas Vachuska47635c62014-11-22 01:21:36 -0800355 } else if (oiBox.isVisible()) {
356 oiBox.hide();
357 } else if (summaryPane.isVisible()) {
358 cancelSummary();
Simon Hunt9462e8c2014-11-14 17:28:09 -0800359 }
360 }
361
Simon Hunt934c3ce2014-11-05 11:45:07 -0800362 // ==============================
363 // Radio Button Callbacks
364
Simon Hunta5e89142014-11-14 07:00:33 -0800365 var layerLookup = {
366 host: {
Simon Hunt209155e2014-11-21 12:16:09 -0800367 endstation: 'pkt', // default, if host event does not define type
Thomas Vachuska89543292014-11-19 11:28:33 -0800368 router: 'pkt',
Simon Hunta5e89142014-11-14 07:00:33 -0800369 bgpSpeaker: 'pkt'
370 },
371 device: {
372 switch: 'pkt',
373 roadm: 'opt'
374 },
375 link: {
376 hostLink: 'pkt',
377 direct: 'pkt',
Simon Hunt8257f4c2014-11-16 19:34:54 -0800378 indirect: '',
379 tunnel: '',
Simon Hunta5e89142014-11-14 07:00:33 -0800380 optical: 'opt'
381 }
382 };
383
384 function inLayer(d, layer) {
Simon Hunt8257f4c2014-11-16 19:34:54 -0800385 var type = d.class === 'link' ? d.type() : d.type,
386 look = layerLookup[d.class],
387 lyr = look && look[type];
Simon Hunta5e89142014-11-14 07:00:33 -0800388 return lyr === layer;
389 }
390
391 function unsuppressLayer(which) {
392 node.each(function (d) {
393 var node = d.el;
394 if (inLayer(d, which)) {
395 node.classed('suppressed', false);
396 }
397 });
398
399 link.each(function (d) {
400 var link = d.el;
401 if (inLayer(d, which)) {
402 link.classed('suppressed', false);
403 }
404 });
405 }
406
Simon Hunt9462e8c2014-11-14 17:28:09 -0800407 function suppressLayers(b) {
408 node.classed('suppressed', b);
409 link.classed('suppressed', b);
Simon Hunt142d0032014-11-04 20:13:09 -0800410// d3.selectAll('svg .port').classed('inactive', false);
411// d3.selectAll('svg .portText').classed('inactive', false);
Simon Hunt195cb382014-11-03 17:50:51 -0800412 }
413
Simon Hunt9462e8c2014-11-14 17:28:09 -0800414 function showAllLayers() {
415 suppressLayers(false);
416 }
417
Simon Hunt195cb382014-11-03 17:50:51 -0800418 function showPacketLayer() {
Simon Hunta5e89142014-11-14 07:00:33 -0800419 node.classed('suppressed', true);
420 link.classed('suppressed', true);
421 unsuppressLayer('pkt');
Simon Hunt195cb382014-11-03 17:50:51 -0800422 }
423
424 function showOpticalLayer() {
Simon Hunta5e89142014-11-14 07:00:33 -0800425 node.classed('suppressed', true);
426 link.classed('suppressed', true);
427 unsuppressLayer('opt');
Simon Hunt195cb382014-11-03 17:50:51 -0800428 }
429
Simon Hunt9462e8c2014-11-14 17:28:09 -0800430 function restoreLayerState() {
431 layerBtnDispatch[layerBtnSet.selected()]();
432 }
433
Simon Hunt142d0032014-11-04 20:13:09 -0800434 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800435 // Private functions
436
Simon Hunt99c13842014-11-06 18:23:12 -0800437 function safeId(s) {
438 return s.replace(/[^a-z0-9]/gi, '-');
439 }
440
Simon Huntc7ee0662014-11-05 16:44:37 -0800441 // set the size of the given element to that of the view (reduced if padded)
442 function setSize(el, view, pad) {
443 var padding = pad ? pad * 2 : 0;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800444 el.attr({
Simon Huntc7ee0662014-11-05 16:44:37 -0800445 width: view.width() - padding,
446 height: view.height() - padding
Simon Hunt934c3ce2014-11-05 11:45:07 -0800447 });
448 }
449
Simon Hunt8257f4c2014-11-16 19:34:54 -0800450 function makeNodeKey(d, what) {
451 var port = what + 'Port';
452 return d[what] + '/' + d[port];
453 }
454
455 function makeLinkKey(d, flipped) {
456 var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
457 two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
458 return one + '-' + two;
459 }
460
Simon Hunt269670f2014-11-17 16:17:43 -0800461 function findLinkById(id) {
462 // check to see if this is a reverse lookup, else default to given id
463 var key = network.revLinkToKey[id] || id;
464 return key && network.lookup[key];
465 }
466
Simon Hunt8257f4c2014-11-16 19:34:54 -0800467 function findLink(linkData, op) {
468 var key = makeLinkKey(linkData),
469 keyrev = makeLinkKey(linkData, 1),
470 link = network.lookup[key],
471 linkRev = network.lookup[keyrev],
472 result = {},
473 ldata = link || linkRev,
474 rawLink;
475
476 if (op === 'add') {
477 if (link) {
478 // trying to add a link that we already know about
479 result.ldata = link;
480 result.badLogic = 'addLink: link already added';
481
482 } else if (linkRev) {
483 // we found the reverse of the link to be added
484 result.ldata = linkRev;
485 if (linkRev.fromTarget) {
486 result.badLogic = 'addLink: link already added';
487 }
488 }
489 } else if (op === 'update') {
490 if (!ldata) {
491 result.badLogic = 'updateLink: link not found';
492 } else {
493 rawLink = link ? ldata.fromSource : ldata.fromTarget;
494 result.updateWith = function (data) {
495 $.extend(rawLink, data);
496 restyleLinkElement(ldata);
497 }
498 }
499 } else if (op === 'remove') {
500 if (!ldata) {
501 result.badLogic = 'removeLink: link not found';
502 } else {
503 rawLink = link ? ldata.fromSource : ldata.fromTarget;
504
505 if (!rawLink) {
506 result.badLogic = 'removeLink: link not found';
507
508 } else {
509 result.removeRawLink = function () {
510 if (link) {
511 // remove fromSource
512 ldata.fromSource = null;
513 if (ldata.fromTarget) {
514 // promote target into source position
515 ldata.fromSource = ldata.fromTarget;
516 ldata.fromTarget = null;
517 ldata.key = keyrev;
518 delete network.lookup[key];
519 network.lookup[keyrev] = ldata;
Simon Hunt269670f2014-11-17 16:17:43 -0800520 delete network.revLinkToKey[keyrev];
Simon Hunt8257f4c2014-11-16 19:34:54 -0800521 }
522 } else {
523 // remove fromTarget
524 ldata.fromTarget = null;
Simon Hunt269670f2014-11-17 16:17:43 -0800525 delete network.revLinkToKey[keyrev];
Simon Hunt8257f4c2014-11-16 19:34:54 -0800526 }
527 if (ldata.fromSource) {
528 restyleLinkElement(ldata);
529 } else {
530 removeLinkElement(ldata);
531 }
532 }
533 }
534 }
535 }
536 return result;
537 }
538
539 function addLinkUpdate(ldata, link) {
540 // add link event, but we already have the reverse link installed
541 ldata.fromTarget = link;
Simon Hunt269670f2014-11-17 16:17:43 -0800542 network.revLinkToKey[link.id] = ldata.key;
Simon Hunt8257f4c2014-11-16 19:34:54 -0800543 restyleLinkElement(ldata);
544 }
545
546 var allLinkTypes = 'direct indirect optical tunnel',
547 defaultLinkType = 'direct';
548
549 function restyleLinkElement(ldata) {
550 // this fn's job is to look at raw links and decide what svg classes
551 // need to be applied to the line element in the DOM
552 var el = ldata.el,
553 type = ldata.type(),
554 lw = ldata.linkWidth(),
555 online = ldata.online();
556
557 el.classed('link', true);
558 el.classed('inactive', !online);
559 el.classed(allLinkTypes, false);
560 if (type) {
561 el.classed(type, true);
562 }
563 el.transition()
564 .duration(1000)
Thomas Vachuska89543292014-11-19 11:28:33 -0800565 .attr('stroke-width', linkScale(lw))
566 .attr('stroke', config.topo.linkBaseColor);
Simon Hunt8257f4c2014-11-16 19:34:54 -0800567 }
Simon Hunt934c3ce2014-11-05 11:45:07 -0800568
Simon Hunt99c13842014-11-06 18:23:12 -0800569 // ==============================
570 // Event handlers for server-pushed events
571
Simon Huntbb282f52014-11-10 11:08:19 -0800572 function logicError(msg) {
573 // TODO, report logic error to server, via websock, so it can be logged
Simon Huntcb56cff2014-11-17 11:42:26 -0800574 //network.view.alert('Logic Error:\n\n' + msg);
Simon Huntfc274c92014-11-11 11:05:46 -0800575 console.warn(msg);
Simon Huntbb282f52014-11-10 11:08:19 -0800576 }
577
Simon Hunt99c13842014-11-06 18:23:12 -0800578 var eventDispatch = {
Simon Hunta5e89142014-11-14 07:00:33 -0800579 addInstance: addInstance,
Simon Hunt99c13842014-11-06 18:23:12 -0800580 addDevice: addDevice,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800581 addLink: addLink,
Simon Hunt56d51852014-11-09 13:03:35 -0800582 addHost: addHost,
Simon Huntb53e0682014-11-12 13:32:01 -0800583
Simon Huntfcfb46c2014-11-19 12:53:38 -0800584 updateInstance: updateInstance,
Simon Huntbb282f52014-11-10 11:08:19 -0800585 updateDevice: updateDevice,
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800586 updateLink: updateLink,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800587 updateHost: updateHost,
Simon Huntb53e0682014-11-12 13:32:01 -0800588
Simon Huntd72bc702014-11-13 18:38:04 -0800589 removeInstance: stillToImplement,
Simon Huntbb282f52014-11-10 11:08:19 -0800590 removeDevice: stillToImplement,
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800591 removeLink: removeLink,
Simon Hunt44031102014-11-11 13:20:36 -0800592 removeHost: removeHost,
Simon Huntb53e0682014-11-12 13:32:01 -0800593
Simon Hunt61d04042014-11-11 17:27:16 -0800594 showDetails: showDetails,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800595 showSummary: showSummary,
Simon Huntb53e0682014-11-12 13:32:01 -0800596 showTraffic: showTraffic
Simon Hunt99c13842014-11-06 18:23:12 -0800597 };
598
Simon Hunta5e89142014-11-14 07:00:33 -0800599 function addInstance(data) {
600 evTrace(data);
601 var inst = data.payload,
602 id = inst.id;
603 if (onosInstances[id]) {
604 logicError('ONOS instance already added: ' + id);
605 return;
606 }
607 onosInstances[id] = inst;
608 onosOrder.push(inst);
609 updateInstances();
610 }
611
Simon Hunt99c13842014-11-06 18:23:12 -0800612 function addDevice(data) {
Simon Hunta5e89142014-11-14 07:00:33 -0800613 evTrace(data);
Simon Hunt99c13842014-11-06 18:23:12 -0800614 var device = data.payload,
Simon Hunt7cd48f32014-11-09 23:42:50 -0800615 nodeData = createDeviceNode(device);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800616 network.nodes.push(nodeData);
617 network.lookup[nodeData.id] = nodeData;
Simon Hunt99c13842014-11-06 18:23:12 -0800618 updateNodes();
619 network.force.start();
620 }
621
Simon Hunt99c13842014-11-06 18:23:12 -0800622 function addLink(data) {
Simon Hunta5e89142014-11-14 07:00:33 -0800623 evTrace(data);
Simon Hunt99c13842014-11-06 18:23:12 -0800624 var link = data.payload,
Simon Hunt8257f4c2014-11-16 19:34:54 -0800625 result = findLink(link, 'add'),
626 bad = result.badLogic,
627 ldata = result.ldata;
628
629 if (bad) {
630 logicError(bad + ': ' + link.id);
631 return;
632 }
633
634 if (ldata) {
635 // we already have a backing store link for src/dst nodes
636 addLinkUpdate(ldata, link);
637 return;
638 }
639
640 // no backing store link yet
641 ldata = createLink(link);
642 if (ldata) {
643 network.links.push(ldata);
644 network.lookup[ldata.key] = ldata;
Simon Hunt99c13842014-11-06 18:23:12 -0800645 updateLinks();
646 network.force.start();
647 }
648 }
649
Simon Hunt56d51852014-11-09 13:03:35 -0800650 function addHost(data) {
Simon Hunta5e89142014-11-14 07:00:33 -0800651 evTrace(data);
Simon Hunt56d51852014-11-09 13:03:35 -0800652 var host = data.payload,
653 node = createHostNode(host),
654 lnk;
Simon Hunt56d51852014-11-09 13:03:35 -0800655 network.nodes.push(node);
656 network.lookup[host.id] = node;
657 updateNodes();
658
659 lnk = createHostLink(host);
660 if (lnk) {
Simon Hunt44031102014-11-11 13:20:36 -0800661 node.linkData = lnk; // cache ref on its host
Simon Hunt56d51852014-11-09 13:03:35 -0800662 network.links.push(lnk);
Thomas Vachuska4830d392014-11-09 17:09:56 -0800663 network.lookup[host.ingress] = lnk;
664 network.lookup[host.egress] = lnk;
Simon Hunt56d51852014-11-09 13:03:35 -0800665 updateLinks();
666 }
667 network.force.start();
668 }
669
Simon Hunt44031102014-11-11 13:20:36 -0800670 // TODO: fold updateX(...) methods into one base method; remove duplication
Simon Hunt56a2ea42014-11-19 12:39:31 -0800671
672 function updateInstance(data) {
673 evTrace(data);
674 var inst = data.payload,
675 id = inst.id,
676 instData = onosInstances[id];
677 if (instData) {
678 $.extend(instData, inst);
679 updateInstances();
Simon Hunt56a2ea42014-11-19 12:39:31 -0800680 } else {
681 logicError('updateInstance lookup fail. ID = "' + id + '"');
682 }
683 }
684
Simon Huntbb282f52014-11-10 11:08:19 -0800685 function updateDevice(data) {
Simon Hunta5e89142014-11-14 07:00:33 -0800686 evTrace(data);
Simon Huntbb282f52014-11-10 11:08:19 -0800687 var device = data.payload,
688 id = device.id,
689 nodeData = network.lookup[id];
690 if (nodeData) {
691 $.extend(nodeData, device);
692 updateDeviceState(nodeData);
693 } else {
694 logicError('updateDevice lookup fail. ID = "' + id + '"');
695 }
696 }
697
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800698 function updateLink(data) {
Simon Hunta5e89142014-11-14 07:00:33 -0800699 evTrace(data);
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800700 var link = data.payload,
Simon Hunt8257f4c2014-11-16 19:34:54 -0800701 result = findLink(link, 'update'),
702 bad = result.badLogic;
703 if (bad) {
704 logicError(bad + ': ' + link.id);
705 return;
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800706 }
Simon Hunt8257f4c2014-11-16 19:34:54 -0800707 result.updateWith(link);
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800708 }
709
Simon Hunt7cd48f32014-11-09 23:42:50 -0800710 function updateHost(data) {
Simon Hunta5e89142014-11-14 07:00:33 -0800711 evTrace(data);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800712 var host = data.payload,
Simon Huntbb282f52014-11-10 11:08:19 -0800713 id = host.id,
714 hostData = network.lookup[id];
715 if (hostData) {
716 $.extend(hostData, host);
717 updateHostState(hostData);
718 } else {
719 logicError('updateHost lookup fail. ID = "' + id + '"');
720 }
Simon Hunt7cd48f32014-11-09 23:42:50 -0800721 }
722
Simon Hunt44031102014-11-11 13:20:36 -0800723 // TODO: fold removeX(...) methods into base method - remove dup code
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800724 function removeLink(data) {
Simon Hunta5e89142014-11-14 07:00:33 -0800725 evTrace(data);
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800726 var link = data.payload,
Simon Hunt8257f4c2014-11-16 19:34:54 -0800727 result = findLink(link, 'remove'),
728 bad = result.badLogic;
729 if (bad) {
730 logicError(bad + ': ' + link.id);
731 return;
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800732 }
Simon Hunt8257f4c2014-11-16 19:34:54 -0800733 result.removeRawLink();
Simon Hunt3f03d4a2014-11-10 20:14:37 -0800734 }
735
Simon Hunt44031102014-11-11 13:20:36 -0800736 function removeHost(data) {
Simon Hunta5e89142014-11-14 07:00:33 -0800737 evTrace(data);
Simon Hunt44031102014-11-11 13:20:36 -0800738 var host = data.payload,
739 id = host.id,
740 hostData = network.lookup[id];
741 if (hostData) {
742 removeHostElement(hostData);
743 } else {
744 logicError('removeHost lookup fail. ID = "' + id + '"');
745 }
746 }
747
Thomas Vachuska47635c62014-11-22 01:21:36 -0800748 function showSummary(data) {
749 evTrace(data);
750 populateSummary(data.payload);
751 summaryPane.show();
752 }
753
Simon Hunt61d04042014-11-11 17:27:16 -0800754 function showDetails(data) {
Simon Hunta5e89142014-11-14 07:00:33 -0800755 evTrace(data);
Simon Hunt61d04042014-11-11 17:27:16 -0800756 populateDetails(data.payload);
757 detailPane.show();
758 }
759
Simon Huntb53e0682014-11-12 13:32:01 -0800760 function showTraffic(data) {
Simon Hunta5e89142014-11-14 07:00:33 -0800761 evTrace(data);
Thomas Vachuska4731f122014-11-20 04:56:19 -0800762 var paths = data.payload.paths,
763 hasTraffic = false;
Thomas Vachuskadea45ff2014-11-12 18:35:46 -0800764
Thomas Vachuska3266abf2014-11-13 09:28:46 -0800765 // Revert any links hilighted previously.
Thomas Vachuska4731f122014-11-20 04:56:19 -0800766 link.style('stroke-width', null)
Thomas Vachuskaa3148a72014-11-19 21:38:35 -0800767 .classed('primary secondary animated optical', false);
Simon Hunte2575b62014-11-18 15:25:53 -0800768 // Remove all previous labels.
769 removeLinkLabels();
Thomas Vachuska3266abf2014-11-13 09:28:46 -0800770
Simon Hunte2575b62014-11-18 15:25:53 -0800771 // Now hilight all links in the paths payload, and attach
772 // labels to them, if they are defined.
Simon Hunta255a2c2014-11-13 22:29:35 -0800773 paths.forEach(function (p) {
Simon Hunte2575b62014-11-18 15:25:53 -0800774 var n = p.links.length,
775 i,
776 ldata;
777
Thomas Vachuska4731f122014-11-20 04:56:19 -0800778 hasTraffic = hasTraffic || p.traffic;
Simon Hunte2575b62014-11-18 15:25:53 -0800779 for (i=0; i<n; i++) {
780 ldata = findLinkById(p.links[i]);
Thomas Vachuska4731f122014-11-20 04:56:19 -0800781 if (ldata && ldata.el) {
Simon Hunte2575b62014-11-18 15:25:53 -0800782 ldata.el.classed(p.class, true);
783 ldata.label = p.labels[i];
Thomas Vachuskadea45ff2014-11-12 18:35:46 -0800784 }
Simon Hunte2575b62014-11-18 15:25:53 -0800785 }
Thomas Vachuskadea45ff2014-11-12 18:35:46 -0800786 });
Thomas Vachuska4731f122014-11-20 04:56:19 -0800787
Simon Hunte2575b62014-11-18 15:25:53 -0800788 updateLinks();
Thomas Vachuska4731f122014-11-20 04:56:19 -0800789
790 if (hasTraffic && !antTimer) {
791 startAntTimer();
792 } else if (!hasTraffic && antTimer) {
793 stopAntTimer();
794 }
Simon Huntb53e0682014-11-12 13:32:01 -0800795 }
796
Simon Hunt56d51852014-11-09 13:03:35 -0800797 // ...............................
798
799 function stillToImplement(data) {
800 var p = data.payload;
801 note(data.event, p.id);
Simon Hunt7cd48f32014-11-09 23:42:50 -0800802 network.view.alert('Not yet implemented: "' + data.event + '"');
Simon Hunt56d51852014-11-09 13:03:35 -0800803 }
Simon Hunt99c13842014-11-06 18:23:12 -0800804
805 function unknownEvent(data) {
Simon Hunt50128c02014-11-08 13:36:15 -0800806 network.view.alert('Unknown event type: "' + data.event + '"');
Simon Hunt99c13842014-11-06 18:23:12 -0800807 }
808
809 function handleServerEvent(data) {
810 var fn = eventDispatch[data.event] || unknownEvent;
811 fn(data);
812 }
813
814 // ==============================
Simon Hunt61d04042014-11-11 17:27:16 -0800815 // Out-going messages...
816
Simon Huntb53e0682014-11-12 13:32:01 -0800817 function userFeedback(msg) {
818 // for now, use the alert pane as is. Maybe different alert style in
819 // the future (centered on view; dismiss button?)
820 network.view.alert(msg);
821 }
822
823 function nSel() {
824 return selectOrder.length;
825 }
Simon Hunt61d04042014-11-11 17:27:16 -0800826 function getSel(idx) {
827 return selections[selectOrder[idx]];
828 }
Simon Huntb53e0682014-11-12 13:32:01 -0800829 function getSelId(idx) {
830 return getSel(idx).obj.id;
831 }
832 function allSelectionsClass(cls) {
833 for (var i=0, n=nSel(); i<n; i++) {
834 if (getSel(i).obj.class !== cls) {
835 return false;
836 }
837 }
838 return true;
839 }
Simon Hunt61d04042014-11-11 17:27:16 -0800840
Thomas Vachuska47635c62014-11-22 01:21:36 -0800841
842 function toggleInstances() {
843 if (!oiBox.isVisible()) {
844 oiBox.show();
845 } else {
846 oiBox.hide();
847 }
848 }
849
850 function toggleSummary() {
851 if (!summaryPane.isVisible()) {
852 requestSummary();
853 } else {
854 cancelSummary();
855 }
856 }
857
858 // request overall summary data
859 function requestSummary() {
860 sendMessage('requestSummary', {});
861 }
862
863 function cancelSummary() {
864 sendMessage('cancelSummary', {});
865 summaryPane.hide();
866 }
867
Simon Hunt61d04042014-11-11 17:27:16 -0800868 // request details for the selected element
Simon Huntd72bc702014-11-13 18:38:04 -0800869 // invoked from selection of a single node.
Simon Hunt61d04042014-11-11 17:27:16 -0800870 function requestDetails() {
871 var data = getSel(0).obj,
872 payload = {
873 id: data.id,
874 class: data.class
875 };
876 sendMessage('requestDetails', payload);
877 }
878
Simon Huntd72bc702014-11-13 18:38:04 -0800879 function addIntentAction() {
880 sendMessage('addHostIntent', {
881 one: getSelId(0),
Thomas Vachuska82f2c622014-11-17 12:23:18 -0800882 two: getSelId(1),
883 ids: [ getSelId(0), getSelId(1) ]
Simon Huntd72bc702014-11-13 18:38:04 -0800884 });
Thomas Vachuskac59658c2014-11-21 13:13:36 -0800885 network.view.flash('Host-to-Host connectivity added');
Simon Huntd72bc702014-11-13 18:38:04 -0800886 }
887
888 function showTrafficAction() {
Thomas Vachuska47635c62014-11-22 01:21:36 -0800889 cancelTraffic();
Thomas Vachuska29617e52014-11-20 03:17:46 -0800890 hoverMode = 1;
891 showSelectTraffic();
Thomas Vachuskac59658c2014-11-21 13:13:36 -0800892 network.view.flash('Related Traffic');
Thomas Vachuska29617e52014-11-20 03:17:46 -0800893 }
894
Thomas Vachuska47635c62014-11-22 01:21:36 -0800895 function cancelTraffic() {
896 sendMessage('cancelTraffic', {});
897 }
898
Thomas Vachuska29617e52014-11-20 03:17:46 -0800899 function showSelectTraffic() {
Simon Huntd72bc702014-11-13 18:38:04 -0800900 // if nothing is hovered over, and nothing selected, send cancel request
901 if (!hovered && nSel() === 0) {
Thomas Vachuska47635c62014-11-22 01:21:36 -0800902 cancelTraffic();
Simon Huntd72bc702014-11-13 18:38:04 -0800903 return;
904 }
905
906 // NOTE: hover is only populated if "show traffic on hover" is
Thomas Vachuska5fedb7a2014-11-20 00:55:08 -0800907 // toggled on, and the item hovered is a host or a device...
908 var hoverId = (trafficHover() && hovered &&
909 (hovered.class === 'host' || hovered.class === 'device'))
Simon Huntd72bc702014-11-13 18:38:04 -0800910 ? hovered.id : '';
911 sendMessage('requestTraffic', {
912 ids: selectOrder,
913 hover: hoverId
914 });
915 }
916
Thomas Vachuska5fedb7a2014-11-20 00:55:08 -0800917 function showAllTrafficAction() {
Thomas Vachuska47635c62014-11-22 01:21:36 -0800918 cancelTraffic();
Thomas Vachuska5fedb7a2014-11-20 00:55:08 -0800919 sendMessage('requestAllTraffic', {});
Thomas Vachuskac59658c2014-11-21 13:13:36 -0800920 network.view.flash('All Traffic');
Thomas Vachuska5fedb7a2014-11-20 00:55:08 -0800921 }
922
Thomas Vachuska29617e52014-11-20 03:17:46 -0800923 function showDeviceLinkFlowsAction() {
Thomas Vachuska47635c62014-11-22 01:21:36 -0800924 cancelTraffic();
Thomas Vachuska29617e52014-11-20 03:17:46 -0800925 hoverMode = 2;
926 showDeviceLinkFlows();
Thomas Vachuskac59658c2014-11-21 13:13:36 -0800927 network.view.flash('Device Flows');
Thomas Vachuska29617e52014-11-20 03:17:46 -0800928 }
929
930 function showDeviceLinkFlows() {
931 // if nothing is hovered over, and nothing selected, send cancel request
932 if (!hovered && nSel() === 0) {
Thomas Vachuska47635c62014-11-22 01:21:36 -0800933 cancelTraffic();
Thomas Vachuska29617e52014-11-20 03:17:46 -0800934 return;
935 }
936 var hoverId = (flowsHover() && hovered && hovered.class === 'device') ?
937 hovered.id : '';
938 sendMessage('requestDeviceLinkFlows', {
939 ids: selectOrder,
940 hover: hoverId
941 });
942 }
Simon Huntd72bc702014-11-13 18:38:04 -0800943
Simon Hunta6a9fe72014-11-20 11:17:12 -0800944 // TODO: these should be moved out to utility module.
Simon Hunt95908012014-11-20 10:20:26 -0800945 function stripPx(s) {
946 return s.replace(/px$/,'');
947 }
Simon Hunta6a9fe72014-11-20 11:17:12 -0800948
Simon Huntb82f6902014-11-22 11:53:15 -0800949 function appendUse(svg, ox, oy, dim, iid, cls) {
950 var use = svg.append('use').attr({
Simon Hunta6a9fe72014-11-20 11:17:12 -0800951 transform: translate(ox,oy),
952 'xlink:href': iid,
953 width: dim,
954 height: dim
Simon Hunta6a9fe72014-11-20 11:17:12 -0800955 });
Simon Huntb82f6902014-11-22 11:53:15 -0800956 if (cls) {
957 use.classed(cls, true);
958 }
959 return use;
960 }
961
962 function appendGlyph(svg, ox, oy, dim, iid, cls) {
963 appendUse(svg, ox, oy, dim, iid, cls).classed('glyphIcon', true);
964 }
965
966 function appendBadge(svg, ox, oy, dim, iid, cls) {
967 appendUse(svg, ox, oy, dim, iid,cls ).classed('badgeIcon', true);
968 }
969
970 function attachUiBadge(svg) {
971 appendBadge(svg, 12, 50, 30, '#uiAttached', 'uiBadge');
Simon Hunta6a9fe72014-11-20 11:17:12 -0800972 }
973
Simon Hunt61d04042014-11-11 17:27:16 -0800974 // ==============================
Simon Hunta5e89142014-11-14 07:00:33 -0800975 // onos instance panel functions
Simon Huntb82f6902014-11-22 11:53:15 -0800976 var instW = 120;
977
978 function viewBox(w, h) {
979 return '0 0 ' + w + ' ' + h;
980 }
Simon Hunta5e89142014-11-14 07:00:33 -0800981
982 function updateInstances() {
983 var onoses = oiBox.el.selectAll('.onosInst')
Simon Huntb82f6902014-11-22 11:53:15 -0800984 .data(onosOrder, function (d) { return d.id; }),
985 boxW = instW * onosOrder.length;
Simon Hunta5e89142014-11-14 07:00:33 -0800986
Simon Huntb82f6902014-11-22 11:53:15 -0800987 // adjust the width of the panel based on number of instances...
988 oiBox.width(boxW);
Simon Hunta5e89142014-11-14 07:00:33 -0800989
Simon Huntb82f6902014-11-22 11:53:15 -0800990 // operate on existing onos instances if necessary
991 onoses.each(function (d) {
992 var el = d3.select(this),
993 svg = el.select('svg');
994
995 // update online state
996 el.classed('online', d.online);
997
998 // update ui-attached state
999 svg.select('use.uiBadge').remove();
1000 if (d.uiAttached) {
1001 attachUiBadge(svg);
1002 }
1003
1004 // TODO: update title and property values
1005 });
1006
1007
1008 // operate on new onos instances
Simon Hunta5e89142014-11-14 07:00:33 -08001009 var entering = onoses.enter()
1010 .append('div')
1011 .attr('class', 'onosInst')
1012 .classed('online', function (d) { return d.online; })
Simon Hunt9c15eca2014-11-15 18:37:59 -08001013 .on('click', clickInst);
1014
Simon Huntb82f6902014-11-22 11:53:15 -08001015 entering.each(function (d) {
Simon Hunt9c15eca2014-11-15 18:37:59 -08001016 var el = d3.select(this),
Simon Huntb82f6902014-11-22 11:53:15 -08001017 css = window.getComputedStyle(this),
Simon Hunt95908012014-11-20 10:20:26 -08001018 w = stripPx(css.width),
Simon Huntb82f6902014-11-22 11:53:15 -08001019 h = stripPx(css.height);
Simon Hunt9c15eca2014-11-15 18:37:59 -08001020
Simon Hunt95908012014-11-20 10:20:26 -08001021 var svg = el.append('svg').attr({
1022 width: w,
Simon Huntb82f6902014-11-22 11:53:15 -08001023 height: h,
1024 viewBox: viewBox(w, h)
Simon Hunt95908012014-11-20 10:20:26 -08001025 });
Simon Huntb82f6902014-11-22 11:53:15 -08001026
1027 svg.append('rect')
Thomas Vachuska47635c62014-11-22 01:21:36 -08001028 .attr({
Simon Huntb82f6902014-11-22 11:53:15 -08001029 x: 8,
1030 y: 8,
1031 width: 104,
1032 height: 84,
1033 rx: 12
Thomas Vachuska47635c62014-11-22 01:21:36 -08001034 });
Simon Hunt9c15eca2014-11-15 18:37:59 -08001035
Simon Huntb82f6902014-11-22 11:53:15 -08001036
1037 appendGlyph(svg, 9, 9, 36, '#node');
1038 appendBadge(svg, 17, 19, 21, '#bird');
1039
1040 if (d.uiAttached) {
1041 attachUiBadge(svg);
1042 }
1043
1044 //svg.append('use')
1045 // .attr({
1046 // class: 'birdBadge',
1047 // transform: translate(8,10),
1048 // 'xlink:href': '#bird',
1049 // width: 18,
1050 // height: 18,
1051 // fill: '#fff'
1052 // });
1053 //
1054 //$('<div>').attr('class', 'onosTitle').text(d.id).appendTo(el);
Simon Hunt9c15eca2014-11-15 18:37:59 -08001055
1056 // is the UI attached to this instance?
1057 // TODO: need uiAttached boolean in instance data
Simon Hunta6a9fe72014-11-20 11:17:12 -08001058 // TODO: use SVG glyph, not png..
Simon Hunt9c15eca2014-11-15 18:37:59 -08001059 //if (d.uiAttached) {
Simon Huntb82f6902014-11-22 11:53:15 -08001060 //if (i === 0) {
1061 // $('<img src="img/ui.png">').attr('class','ui').appendTo(el);
1062 //}
Simon Hunt9c15eca2014-11-15 18:37:59 -08001063 });
Simon Hunta5e89142014-11-14 07:00:33 -08001064
1065 // operate on existing + new onoses here
1066
1067 // the departed...
1068 var exiting = onoses.exit()
1069 .transition()
1070 .style('opacity', 0)
1071 .remove();
1072 }
1073
Simon Hunt9462e8c2014-11-14 17:28:09 -08001074 function clickInst(d) {
1075 var el = d3.select(this),
1076 aff = el.classed('affinity');
1077 if (!aff) {
1078 setAffinity(el, d);
1079 } else {
1080 cancelAffinity();
1081 }
1082 }
1083
1084 function setAffinity(el, d) {
1085 d3.selectAll('.onosInst')
1086 .classed('mastership', true)
1087 .classed('affinity', false);
1088 el.classed('affinity', true);
1089
1090 suppressLayers(true);
1091 node.each(function (n) {
1092 if (n.master === d.id) {
1093 n.el.classed('suppressed', false);
1094 }
1095 });
1096 oiShowMaster = true;
1097 }
1098
1099 function cancelAffinity() {
1100 d3.selectAll('.onosInst')
1101 .classed('mastership affinity', false);
1102 restoreLayerState();
1103 oiShowMaster = false;
1104 }
1105
Simon Hunta5e89142014-11-14 07:00:33 -08001106 // ==============================
Simon Hunt99c13842014-11-06 18:23:12 -08001107 // force layout modification functions
1108
1109 function translate(x, y) {
1110 return 'translate(' + x + ',' + y + ')';
1111 }
1112
Simon Hunte2575b62014-11-18 15:25:53 -08001113 function rotate(deg) {
1114 return 'rotate(' + deg + ')';
1115 }
1116
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001117 function missMsg(what, id) {
1118 return '\n[' + what + '] "' + id + '" missing ';
1119 }
1120
1121 function linkEndPoints(srcId, dstId) {
1122 var srcNode = network.lookup[srcId],
1123 dstNode = network.lookup[dstId],
1124 sMiss = !srcNode ? missMsg('src', srcId) : '',
1125 dMiss = !dstNode ? missMsg('dst', dstId) : '';
1126
1127 if (sMiss || dMiss) {
1128 logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
1129 return null;
1130 }
1131 return {
1132 source: srcNode,
1133 target: dstNode,
1134 x1: srcNode.x,
1135 y1: srcNode.y,
1136 x2: dstNode.x,
1137 y2: dstNode.y
1138 };
1139 }
1140
Simon Hunt56d51852014-11-09 13:03:35 -08001141 function createHostLink(host) {
1142 var src = host.id,
1143 dst = host.cp.device,
Simon Hunt7cd48f32014-11-09 23:42:50 -08001144 id = host.ingress,
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001145 lnk = linkEndPoints(src, dst);
Simon Hunt56d51852014-11-09 13:03:35 -08001146
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001147 if (!lnk) {
Simon Hunt56d51852014-11-09 13:03:35 -08001148 return null;
1149 }
1150
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001151 // Synthesize link ...
1152 $.extend(lnk, {
Simon Hunt8257f4c2014-11-16 19:34:54 -08001153 key: id,
Simon Hunt56d51852014-11-09 13:03:35 -08001154 class: 'link',
Simon Hunt8257f4c2014-11-16 19:34:54 -08001155
1156 type: function () { return 'hostLink'; },
1157 // TODO: ideally, we should see if our edge switch is online...
1158 online: function () { return true; },
1159 linkWidth: function () { return 1; }
Simon Hunt7cd48f32014-11-09 23:42:50 -08001160 });
Simon Hunt99c13842014-11-06 18:23:12 -08001161 return lnk;
1162 }
1163
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001164 function createLink(link) {
Simon Hunta6a9fe72014-11-20 11:17:12 -08001165 var lnk = linkEndPoints(link.src, link.dst);
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001166
1167 if (!lnk) {
1168 return null;
1169 }
1170
Simon Hunt8257f4c2014-11-16 19:34:54 -08001171 $.extend(lnk, {
1172 key: link.id,
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001173 class: 'link',
Simon Hunt8257f4c2014-11-16 19:34:54 -08001174 fromSource: link,
1175
1176 // functions to aggregate dual link state
1177 type: function () {
1178 var s = lnk.fromSource,
1179 t = lnk.fromTarget;
1180 return (s && s.type) || (t && t.type) || defaultLinkType;
1181 },
1182 online: function () {
1183 var s = lnk.fromSource,
1184 t = lnk.fromTarget;
1185 return (s && s.online) || (t && t.online);
1186 },
1187 linkWidth: function () {
1188 var s = lnk.fromSource,
1189 t = lnk.fromTarget,
1190 ws = (s && s.linkWidth) || 0,
1191 wt = (t && t.linkWidth) || 0;
1192 return Math.max(ws, wt);
1193 }
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001194 });
1195 return lnk;
Simon Hunt1a9eff92014-11-07 11:06:34 -08001196 }
1197
Simon Hunte2575b62014-11-18 15:25:53 -08001198 function removeLinkLabels() {
1199 network.links.forEach(function (d) {
1200 d.label = '';
1201 });
1202 }
1203
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001204 var widthRatio = 1.4,
1205 linkScale = d3.scale.linear()
1206 .domain([1, 12])
1207 .range([widthRatio, 12 * widthRatio])
1208 .clamp(true);
1209
Simon Hunt99c13842014-11-06 18:23:12 -08001210 function updateLinks() {
1211 link = linkG.selectAll('.link')
Simon Hunt8257f4c2014-11-16 19:34:54 -08001212 .data(network.links, function (d) { return d.key; });
Simon Hunt99c13842014-11-06 18:23:12 -08001213
1214 // operate on existing links, if necessary
1215 // link .foo() .bar() ...
1216
1217 // operate on entering links:
1218 var entering = link.enter()
1219 .append('line')
1220 .attr({
Simon Hunt99c13842014-11-06 18:23:12 -08001221 x1: function (d) { return d.x1; },
1222 y1: function (d) { return d.y1; },
1223 x2: function (d) { return d.x2; },
1224 y2: function (d) { return d.y2; },
Simon Hunt1a9eff92014-11-07 11:06:34 -08001225 stroke: config.topo.linkInColor,
1226 'stroke-width': config.topo.linkInWidth
Simon Hunt99c13842014-11-06 18:23:12 -08001227 });
1228
1229 // augment links
Simon Hunt7cd48f32014-11-09 23:42:50 -08001230 entering.each(function (d) {
1231 var link = d3.select(this);
1232 // provide ref to element selection from backing data....
1233 d.el = link;
Simon Hunt8257f4c2014-11-16 19:34:54 -08001234 restyleLinkElement(d);
Simon Hunt7cd48f32014-11-09 23:42:50 -08001235 });
Thomas Vachuska4830d392014-11-09 17:09:56 -08001236
1237 // operate on both existing and new links, if necessary
1238 //link .foo() .bar() ...
1239
Simon Hunte2575b62014-11-18 15:25:53 -08001240 // apply or remove labels
1241 var labelData = getLabelData();
1242 applyLinkLabels(labelData);
1243
Thomas Vachuska4830d392014-11-09 17:09:56 -08001244 // operate on exiting links:
Thomas Vachuska4830d392014-11-09 17:09:56 -08001245 link.exit()
Simon Hunt13bf9c82014-11-18 07:26:44 -08001246 .attr('stroke-dasharray', '3, 3')
1247 .style('opacity', 0.5)
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001248 .transition()
Simon Huntea80eb42014-11-11 13:46:57 -08001249 .duration(1500)
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001250 .attr({
Simon Hunt13bf9c82014-11-18 07:26:44 -08001251 'stroke-dasharray': '3, 12',
1252 stroke: config.topo.linkOutColor,
1253 'stroke-width': config.topo.linkOutWidth
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001254 })
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001255 .style('opacity', 0.0)
Thomas Vachuska4830d392014-11-09 17:09:56 -08001256 .remove();
Simon Hunte2575b62014-11-18 15:25:53 -08001257
1258 // NOTE: invoke a single tick to force the labels to position
1259 // onto their links.
1260 tick();
1261 }
1262
1263 function getLabelData() {
1264 // create the backing data for showing labels..
1265 var data = [];
1266 link.each(function (d) {
1267 if (d.label) {
1268 data.push({
1269 id: 'lab-' + d.key,
1270 key: d.key,
1271 label: d.label,
1272 ldata: d
1273 });
1274 }
1275 });
1276 return data;
1277 }
1278
1279 var linkLabelOffset = '0.3em';
1280
1281 function applyLinkLabels(data) {
1282 var entering;
1283
1284 linkLabel = linkLabelG.selectAll('.linkLabel')
1285 .data(data, function (d) { return d.id; });
1286
Simon Hunt56a2ea42014-11-19 12:39:31 -08001287 // for elements already existing, we need to update the text
1288 // and adjust the rectangle size to fit
1289 linkLabel.each(function (d) {
1290 var el = d3.select(this),
1291 rect = el.select('rect'),
1292 text = el.select('text');
1293 text.text(d.label);
1294 rect.attr(rectAroundText(el));
1295 });
Thomas Vachuskaf75b7ab2014-11-19 12:15:55 -08001296
Simon Hunte2575b62014-11-18 15:25:53 -08001297 entering = linkLabel.enter().append('g')
1298 .classed('linkLabel', true)
1299 .attr('id', function (d) { return d.id; });
1300
1301 entering.each(function (d) {
1302 var el = d3.select(this),
1303 rect,
1304 text,
1305 parms = {
1306 x1: d.ldata.x1,
1307 y1: d.ldata.y1,
1308 x2: d.ldata.x2,
1309 y2: d.ldata.y2
1310 };
1311
1312 d.el = el;
1313 rect = el.append('rect');
1314 text = el.append('text').text(d.label);
1315 rect.attr(rectAroundText(el));
1316 text.attr('dy', linkLabelOffset);
1317
1318 el.attr('transform', transformLabel(parms));
1319 });
1320
1321 // Remove any links that are no longer required.
1322 linkLabel.exit().remove();
1323 }
1324
1325 function rectAroundText(el) {
1326 var text = el.select('text'),
1327 box = text.node().getBBox();
1328
1329 // translate the bbox so that it is centered on [x,y]
1330 box.x = -box.width / 2;
1331 box.y = -box.height / 2;
1332
1333 // add padding
1334 box.x -= 1;
1335 box.width += 2;
1336 return box;
1337 }
1338
1339 function transformLabel(p) {
1340 var dx = p.x2 - p.x1,
1341 dy = p.y2 - p.y1,
1342 xMid = dx/2 + p.x1,
1343 yMid = dy/2 + p.y1;
Simon Hunte2575b62014-11-18 15:25:53 -08001344 return translate(xMid, yMid);
Simon Hunt99c13842014-11-06 18:23:12 -08001345 }
1346
1347 function createDeviceNode(device) {
1348 // start with the object as is
1349 var node = device,
Simon Huntbb282f52014-11-10 11:08:19 -08001350 type = device.type,
Simon Huntc72967b2014-11-20 09:21:42 -08001351 svgCls = type ? 'node device ' + type : 'node device',
1352 labels = device.labels || [];
1353
Simon Hunt99c13842014-11-06 18:23:12 -08001354 // Augment as needed...
1355 node.class = 'device';
Simon Huntbb282f52014-11-10 11:08:19 -08001356 node.svgClass = device.online ? svgCls + ' online' : svgCls;
Simon Hunt99c13842014-11-06 18:23:12 -08001357 positionNode(node);
Simon Hunt99c13842014-11-06 18:23:12 -08001358 return node;
1359 }
1360
Simon Hunt56d51852014-11-09 13:03:35 -08001361 function createHostNode(host) {
1362 // start with the object as is
1363 var node = host;
1364
1365 // Augment as needed...
1366 node.class = 'host';
Simon Hunt7cd48f32014-11-09 23:42:50 -08001367 if (!node.type) {
Simon Hunt209155e2014-11-21 12:16:09 -08001368 node.type = 'endstation';
Simon Hunt7cd48f32014-11-09 23:42:50 -08001369 }
Simon Hunt7fa116d2014-11-17 14:16:55 -08001370 node.svgClass = 'node host ' + node.type;
Simon Hunt56d51852014-11-09 13:03:35 -08001371 positionNode(node);
Simon Hunt56d51852014-11-09 13:03:35 -08001372 return node;
1373 }
1374
Simon Hunt99c13842014-11-06 18:23:12 -08001375 function positionNode(node) {
1376 var meta = node.metaUi,
Simon Huntac9e24f2014-11-12 10:12:21 -08001377 x = meta && meta.x,
1378 y = meta && meta.y,
1379 xy;
Simon Hunt99c13842014-11-06 18:23:12 -08001380
Simon Huntac9e24f2014-11-12 10:12:21 -08001381 // If we have [x,y] already, use that...
Simon Hunt99c13842014-11-06 18:23:12 -08001382 if (x && y) {
1383 node.fixed = true;
Simon Huntac9e24f2014-11-12 10:12:21 -08001384 node.x = x;
1385 node.y = y;
1386 return;
Simon Hunt99c13842014-11-06 18:23:12 -08001387 }
Simon Huntac9e24f2014-11-12 10:12:21 -08001388
Paul Greyson6cb8ca02014-11-12 18:09:02 -08001389 var location = node.location;
1390 if (location && location.type === 'latlng') {
1391 var coord = geoMapProjection([location.lng, location.lat]);
1392 node.fixed = true;
1393 node.x = coord[0];
1394 node.y = coord[1];
1395 return;
1396 }
1397
Simon Huntac9e24f2014-11-12 10:12:21 -08001398 // Note: Placing incoming unpinned nodes at exactly the same point
1399 // (center of the view) causes them to explode outwards when
1400 // the force layout kicks in. So, we spread them out a bit
1401 // initially, to provide a more serene layout convergence.
1402 // Additionally, if the node is a host, we place it near
1403 // the device it is connected to.
1404
1405 function spread(s) {
1406 return Math.floor((Math.random() * s) - s/2);
1407 }
1408
1409 function randDim(dim) {
1410 return dim / 2 + spread(dim * 0.7071);
1411 }
1412
1413 function rand() {
1414 return {
1415 x: randDim(network.view.width()),
1416 y: randDim(network.view.height())
1417 };
1418 }
1419
1420 function near(node) {
1421 var min = 12,
1422 dx = spread(12),
1423 dy = spread(12);
1424 return {
1425 x: node.x + min + dx,
1426 y: node.y + min + dy
1427 };
1428 }
1429
1430 function getDevice(cp) {
1431 var d = network.lookup[cp.device];
1432 return d || rand();
1433 }
1434
1435 xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
1436 $.extend(node, xy);
Simon Hunt99c13842014-11-06 18:23:12 -08001437 }
1438
Thomas Vachuska89543292014-11-19 11:28:33 -08001439 function iconUrl(d) {
1440 return 'img/' + d.type + '.png';
Simon Hunt99c13842014-11-06 18:23:12 -08001441 }
1442
Simon Huntc72967b2014-11-20 09:21:42 -08001443 function iconGlyphUrl(d) {
1444 var which = d.type || 'unknown';
1445 return '#' + which;
1446 }
1447
Simon Hunt99c13842014-11-06 18:23:12 -08001448 // returns the newly computed bounding box of the rectangle
1449 function adjustRectToFitText(n) {
1450 var text = n.select('text'),
1451 box = text.node().getBBox(),
1452 lab = config.labels;
1453
1454 text.attr('text-anchor', 'middle')
1455 .attr('y', '-0.8em')
1456 .attr('x', lab.imgPad/2);
1457
1458 // translate the bbox so that it is centered on [x,y]
1459 box.x = -box.width / 2;
1460 box.y = -box.height / 2;
1461
1462 // add padding
1463 box.x -= (lab.padLR + lab.imgPad/2);
1464 box.width += lab.padLR * 2 + lab.imgPad;
1465 box.y -= lab.padTB;
1466 box.height += lab.padTB * 2;
1467
1468 return box;
1469 }
1470
Simon Hunt1a9eff92014-11-07 11:06:34 -08001471 function mkSvgClass(d) {
1472 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
1473 }
1474
Simon Hunt7cd48f32014-11-09 23:42:50 -08001475 function hostLabel(d) {
1476 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
1477 return d.labels[idx];
1478 }
1479 function deviceLabel(d) {
1480 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
1481 return d.labels[idx];
1482 }
Simon Huntc72967b2014-11-20 09:21:42 -08001483 function trimLabel(label) {
1484 return (label && label.trim()) || '';
1485 }
1486
1487 function emptyBox() {
1488 return {
1489 x: -2,
1490 y: -2,
1491 width: 4,
1492 height: 4
1493 };
Simon Hunt7cd48f32014-11-09 23:42:50 -08001494 }
1495
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001496 function updateDeviceLabel(d) {
Simon Huntc72967b2014-11-20 09:21:42 -08001497 var label = trimLabel(deviceLabel(d)),
1498 noLabel = !label,
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001499 node = d.el,
Simon Huntc72967b2014-11-20 09:21:42 -08001500 box,
1501 dx,
1502 dy;
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001503
1504 node.select('text')
1505 .text(label)
1506 .style('opacity', 0)
1507 .transition()
1508 .style('opacity', 1);
1509
Simon Huntc72967b2014-11-20 09:21:42 -08001510 if (noLabel) {
1511 box = emptyBox();
1512 dx = -config.icons.device.dim/2;
1513 dy = -config.icons.device.dim/2;
1514 } else {
1515 box = adjustRectToFitText(node);
1516 dx = box.x + config.icons.xoff;
1517 dy = box.y + config.icons.yoff;
1518 }
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001519
1520 node.select('rect')
1521 .transition()
1522 .attr(box);
1523
Simon Huntc72967b2014-11-20 09:21:42 -08001524 node.select('g.deviceIcon')
Thomas Vachuska89543292014-11-19 11:28:33 -08001525 .transition()
Simon Huntc72967b2014-11-20 09:21:42 -08001526 .attr('transform', translate(dx, dy));
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001527 }
1528
1529 function updateHostLabel(d) {
1530 var label = hostLabel(d),
1531 host = d.el;
1532
1533 host.select('text').text(label);
1534 }
1535
Simon Hunta6a9fe72014-11-20 11:17:12 -08001536 // TODO: should be using updateNodes() to do the upates!
Simon Huntbb282f52014-11-10 11:08:19 -08001537 function updateDeviceState(nodeData) {
1538 nodeData.el.classed('online', nodeData.online);
1539 updateDeviceLabel(nodeData);
1540 // TODO: review what else might need to be updated
1541 }
1542
1543 function updateHostState(hostData) {
1544 updateHostLabel(hostData);
1545 // TODO: review what else might need to be updated
1546 }
1547
Simon Hunt6ac93f32014-11-13 12:17:27 -08001548 function nodeMouseOver(d) {
Simon Hunt6ac93f32014-11-13 12:17:27 -08001549 hovered = d;
Thomas Vachuska5fedb7a2014-11-20 00:55:08 -08001550 if (trafficHover() && (d.class === 'host' || d.class === 'device')) {
Thomas Vachuska29617e52014-11-20 03:17:46 -08001551 showSelectTraffic();
1552 } else if (flowsHover() && (d.class === 'device')) {
1553 showDeviceLinkFlows();
Simon Hunt6ac93f32014-11-13 12:17:27 -08001554 }
1555 }
1556
1557 function nodeMouseOut(d) {
Simon Hunt6ac93f32014-11-13 12:17:27 -08001558 hovered = null;
Thomas Vachuska5fedb7a2014-11-20 00:55:08 -08001559 if (trafficHover() && (d.class === 'host' || d.class === 'device')) {
Thomas Vachuska29617e52014-11-20 03:17:46 -08001560 showSelectTraffic();
1561 } else if (flowsHover() && (d.class === 'device')) {
1562 showDeviceLinkFlows();
Simon Hunt6ac93f32014-11-13 12:17:27 -08001563 }
1564 }
Simon Huntbb282f52014-11-10 11:08:19 -08001565
Simon Hunteb1514d2014-11-20 09:57:29 -08001566 function addHostIcon(node, radius, iid) {
Thomas Vachuska89543292014-11-19 11:28:33 -08001567 var dim = radius * 1.5,
1568 xlate = -dim / 2;
1569
Simon Hunteb1514d2014-11-20 09:57:29 -08001570 node.append('use').attr({
1571 class: 'glyphIcon hostIcon',
1572 transform: translate(xlate,xlate),
1573 'xlink:href': iid,
1574 width: dim,
1575 height: dim
1576 });
Thomas Vachuska89543292014-11-19 11:28:33 -08001577 }
1578
Simon Hunt99c13842014-11-06 18:23:12 -08001579 function updateNodes() {
1580 node = nodeG.selectAll('.node')
1581 .data(network.nodes, function (d) { return d.id; });
1582
Simon Huntc72967b2014-11-20 09:21:42 -08001583 // TODO: operate on existing nodes
Simon Hunt7cd48f32014-11-09 23:42:50 -08001584 // update host labels
Simon Hunt99c13842014-11-06 18:23:12 -08001585 //node .foo() .bar() ...
1586
1587 // operate on entering nodes:
1588 var entering = node.enter()
1589 .append('g')
1590 .attr({
1591 id: function (d) { return safeId(d.id); },
Simon Hunt1a9eff92014-11-07 11:06:34 -08001592 class: mkSvgClass,
Simon Hunt99c13842014-11-06 18:23:12 -08001593 transform: function (d) { return translate(d.x, d.y); },
1594 opacity: 0
1595 })
Simon Hunt1a9eff92014-11-07 11:06:34 -08001596 .call(network.drag)
Simon Hunt6ac93f32014-11-13 12:17:27 -08001597 .on('mouseover', nodeMouseOver)
1598 .on('mouseout', nodeMouseOut)
Simon Hunt99c13842014-11-06 18:23:12 -08001599 .transition()
1600 .attr('opacity', 1);
1601
1602 // augment device nodes...
1603 entering.filter('.device').each(function (d) {
1604 var node = d3.select(this),
Simon Huntc72967b2014-11-20 09:21:42 -08001605 label = trimLabel(deviceLabel(d)),
1606 noLabel = !label,
Simon Hunt99c13842014-11-06 18:23:12 -08001607 box;
1608
Simon Hunt7cd48f32014-11-09 23:42:50 -08001609 // provide ref to element from backing data....
1610 d.el = node;
1611
Simon Hunt99c13842014-11-06 18:23:12 -08001612 node.append('rect')
1613 .attr({
Simon Hunta3dd9572014-11-20 15:22:41 -08001614 rx: 5,
1615 ry: 5
Simon Hunt99c13842014-11-06 18:23:12 -08001616 });
1617
1618 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -08001619 .text(label)
Simon Hunt99c13842014-11-06 18:23:12 -08001620 .attr('dy', '1.1em');
1621
1622 box = adjustRectToFitText(node);
Simon Hunta3dd9572014-11-20 15:22:41 -08001623 node.select('rect').attr(box);
Simon Huntc72967b2014-11-20 09:21:42 -08001624 addDeviceIcon(node, box, noLabel, iconGlyphUrl(d));
Simon Huntc7ee0662014-11-05 16:44:37 -08001625 });
Simon Hunt934c3ce2014-11-05 11:45:07 -08001626
Thomas Vachuska89543292014-11-19 11:28:33 -08001627 // TODO: better place for this configuration state
1628 var defaultHostRadius = 9,
1629 hostRadius = {
1630 bgpSpeaker: 14,
1631 router: 14,
Thomas Vachuska60d72bf2014-11-21 13:02:00 -08001632 endstation: 14
Thomas Vachuska89543292014-11-19 11:28:33 -08001633 },
Simon Hunteb1514d2014-11-20 09:57:29 -08001634 hostGlyphId = {
Thomas Vachuska89543292014-11-19 11:28:33 -08001635 bgpSpeaker: 'bgpSpeaker',
1636 router: 'router',
Thomas Vachuska60d72bf2014-11-21 13:02:00 -08001637 endstation: 'endstation'
Thomas Vachuska89543292014-11-19 11:28:33 -08001638 };
1639
1640
Simon Hunt56d51852014-11-09 13:03:35 -08001641 // augment host nodes...
1642 entering.filter('.host').each(function (d) {
1643 var node = d3.select(this),
Thomas Vachuska89543292014-11-19 11:28:33 -08001644 r = hostRadius[d.type] || defaultHostRadius,
1645 textDy = r + 10,
Simon Hunteb1514d2014-11-20 09:57:29 -08001646 iid = iconGlyphUrl(d);
Simon Hunt56d51852014-11-09 13:03:35 -08001647
Simon Hunt7cd48f32014-11-09 23:42:50 -08001648 // provide ref to element from backing data....
1649 d.el = node;
1650
Thomas Vachuska89543292014-11-19 11:28:33 -08001651 node.append('circle')
1652 .attr('r', r);
Simon Hunt7fa116d2014-11-17 14:16:55 -08001653
Simon Hunteb1514d2014-11-20 09:57:29 -08001654 if (iid) {
1655 addHostIcon(node, r, iid);
Simon Hunt7fa116d2014-11-17 14:16:55 -08001656 }
Simon Hunt56d51852014-11-09 13:03:35 -08001657
Simon Hunt56d51852014-11-09 13:03:35 -08001658 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -08001659 .text(hostLabel)
Thomas Vachuska89543292014-11-19 11:28:33 -08001660 .attr('dy', textDy)
Simon Hunt7cd48f32014-11-09 23:42:50 -08001661 .attr('text-anchor', 'middle');
Simon Hunt56d51852014-11-09 13:03:35 -08001662
1663 // debug function to show the modelled x,y coordinates of nodes...
1664 if (debug('showNodeXY')) {
1665 node.select('circle').attr('fill-opacity', 0.5);
1666 node.append('circle')
1667 .attr({
1668 class: 'debug',
1669 cx: 0,
1670 cy: 0,
1671 r: '3px'
1672 });
1673 }
1674 });
Simon Huntc7ee0662014-11-05 16:44:37 -08001675
Simon Hunt99c13842014-11-06 18:23:12 -08001676 // operate on both existing and new nodes, if necessary
1677 //node .foo() .bar() ...
Simon Huntc7ee0662014-11-05 16:44:37 -08001678
Simon Hunt99c13842014-11-06 18:23:12 -08001679 // operate on exiting nodes:
Simon Huntea80eb42014-11-11 13:46:57 -08001680 // Note that the node is removed after 2 seconds.
1681 // Sub element animations should be shorter than 2 seconds.
1682 var exiting = node.exit()
Simon Hunt44031102014-11-11 13:20:36 -08001683 .transition()
1684 .duration(2000)
Simon Huntea80eb42014-11-11 13:46:57 -08001685 .style('opacity', 0)
Simon Hunt99c13842014-11-06 18:23:12 -08001686 .remove();
Simon Huntea80eb42014-11-11 13:46:57 -08001687
1688 // host node exits....
1689 exiting.filter('.host').each(function (d) {
1690 var node = d3.select(this);
1691
1692 node.select('text')
1693 .style('opacity', 0.5)
1694 .transition()
1695 .duration(1000)
1696 .style('opacity', 0);
1697 // note, leave <g>.remove to remove this element
1698
Thomas Vachuska89543292014-11-19 11:28:33 -08001699 node.select('circle')
1700 .style('stroke-fill', '#555')
1701 .style('fill', '#888')
Simon Huntea80eb42014-11-11 13:46:57 -08001702 .style('opacity', 0.5)
1703 .transition()
1704 .duration(1500)
1705 .attr('r', 0);
1706 // note, leave <g>.remove to remove this element
1707
1708 });
1709
1710 // TODO: device node exits
Simon Huntc7ee0662014-11-05 16:44:37 -08001711 }
1712
Simon Huntc72967b2014-11-20 09:21:42 -08001713 function addDeviceIcon(node, box, noLabel, iid) {
1714 var cfg = config.icons.device,
1715 dx,
1716 dy,
1717 g;
1718
1719 if (noLabel) {
1720 box = emptyBox();
1721 dx = -cfg.dim/2;
1722 dy = -cfg.dim/2;
1723 } else {
1724 box = adjustRectToFitText(node);
1725 dx = box.x + config.icons.xoff;
1726 dy = box.y + config.icons.yoff;
1727 }
1728
Simon Hunteb1514d2014-11-20 09:57:29 -08001729 g = node.append('g')
1730 .attr('class', 'glyphIcon deviceIcon')
Simon Huntc72967b2014-11-20 09:21:42 -08001731 .attr('transform', translate(dx, dy));
1732
1733 g.append('rect').attr({
1734 x: 0,
1735 y: 0,
1736 rx: cfg.rx,
1737 width: cfg.dim,
1738 height: cfg.dim
1739 });
1740
1741 g.append('use').attr({
1742 'xlink:href': iid,
1743 width: cfg.dim,
1744 height: cfg.dim
1745 });
1746
Simon Huntc72967b2014-11-20 09:21:42 -08001747 }
1748
Simon Hunt8257f4c2014-11-16 19:34:54 -08001749 function find(key, array) {
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001750 for (var idx = 0, n = array.length; idx < n; idx++) {
Simon Hunt8257f4c2014-11-16 19:34:54 -08001751 if (array[idx].key === key) {
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001752 return idx;
1753 }
1754 }
1755 return -1;
1756 }
1757
1758 function removeLinkElement(linkData) {
Simon Hunt8257f4c2014-11-16 19:34:54 -08001759 var idx = find(linkData.key, network.links),
1760 removed;
1761 if (idx >=0) {
1762 // remove from links array
1763 removed = network.links.splice(idx, 1);
1764 // remove from lookup cache
1765 delete network.lookup[removed[0].key];
1766 updateLinks();
1767 network.force.resume();
1768 }
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001769 }
Simon Huntc7ee0662014-11-05 16:44:37 -08001770
Simon Hunt44031102014-11-11 13:20:36 -08001771 function removeHostElement(hostData) {
1772 // first, remove associated hostLink...
1773 removeLinkElement(hostData.linkData);
1774
1775 // remove from lookup cache
1776 delete network.lookup[hostData.id];
1777 // remove from nodes array
1778 var idx = find(hostData.id, network.nodes);
1779 network.nodes.splice(idx, 1);
1780 // remove from SVG
1781 updateNodes();
1782 network.force.resume();
1783 }
1784
1785
Simon Huntc7ee0662014-11-05 16:44:37 -08001786 function tick() {
1787 node.attr({
Simon Hunt99c13842014-11-06 18:23:12 -08001788 transform: function (d) { return translate(d.x, d.y); }
Simon Huntc7ee0662014-11-05 16:44:37 -08001789 });
1790
1791 link.attr({
1792 x1: function (d) { return d.source.x; },
1793 y1: function (d) { return d.source.y; },
1794 x2: function (d) { return d.target.x; },
1795 y2: function (d) { return d.target.y; }
1796 });
Simon Hunte2575b62014-11-18 15:25:53 -08001797
1798 linkLabel.each(function (d) {
1799 var el = d3.select(this);
Thomas Vachuska4731f122014-11-20 04:56:19 -08001800 var lnk = findLinkById(d.key);
1801
1802 if (lnk) {
1803 var parms = {
Simon Hunte2575b62014-11-18 15:25:53 -08001804 x1: lnk.source.x,
1805 y1: lnk.source.y,
1806 x2: lnk.target.x,
1807 y2: lnk.target.y
1808 };
Thomas Vachuska4731f122014-11-20 04:56:19 -08001809 el.attr('transform', transformLabel(parms));
1810 }
Simon Hunte2575b62014-11-18 15:25:53 -08001811 });
Simon Huntc7ee0662014-11-05 16:44:37 -08001812 }
Simon Hunt934c3ce2014-11-05 11:45:07 -08001813
1814 // ==============================
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001815 // Web-Socket for live data
1816
1817 function webSockUrl() {
1818 return document.location.toString()
1819 .replace(/\#.*/, '')
1820 .replace('http://', 'ws://')
1821 .replace('https://', 'wss://')
1822 .replace('index2.html', config.webSockUrl);
1823 }
1824
1825 webSock = {
1826 ws : null,
1827
1828 connect : function() {
1829 webSock.ws = new WebSocket(webSockUrl());
1830
1831 webSock.ws.onopen = function() {
Simon Hunt0c6d4192014-11-12 12:07:10 -08001832 noWebSock(false);
Thomas Vachuska47635c62014-11-22 01:21:36 -08001833 requestSummary();
1834 oiBox.show();
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001835 };
1836
1837 webSock.ws.onmessage = function(m) {
1838 if (m.data) {
Simon Huntbb282f52014-11-10 11:08:19 -08001839 wsTraceRx(m.data);
Thomas Vachuskad472c6e2014-11-07 19:11:05 -08001840 handleServerEvent(JSON.parse(m.data));
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001841 }
1842 };
1843
1844 webSock.ws.onclose = function(m) {
1845 webSock.ws = null;
Simon Hunt0c6d4192014-11-12 12:07:10 -08001846 noWebSock(true);
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001847 };
1848 },
1849
1850 send : function(text) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001851 if (text != null) {
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001852 webSock._send(text);
1853 }
1854 },
1855
1856 _send : function(message) {
1857 if (webSock.ws) {
1858 webSock.ws.send(message);
Simon Hunta255a2c2014-11-13 22:29:35 -08001859 } else if (config.useLiveData) {
Simon Hunt56d51852014-11-09 13:03:35 -08001860 network.view.alert('no web socket open\n\n' + message);
Simon Hunta255a2c2014-11-13 22:29:35 -08001861 } else {
1862 console.log('WS Send: ' + JSON.stringify(message));
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001863 }
1864 }
1865
1866 };
1867
Simon Hunt0c6d4192014-11-12 12:07:10 -08001868 function noWebSock(b) {
1869 mask.style('display',b ? 'block' : 'none');
1870 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001871
1872 function sendMessage(evType, payload) {
1873 var toSend = {
Simon Huntbb282f52014-11-10 11:08:19 -08001874 event: evType,
1875 sid: ++sid,
1876 payload: payload
1877 },
1878 asText = JSON.stringify(toSend);
1879 wsTraceTx(asText);
1880 webSock.send(asText);
Simon Huntc76ae892014-11-18 17:31:51 -08001881
1882 // Temporary measure for debugging UI behavior ...
1883 if (!config.useLiveData) {
1884 handleTestSend(toSend);
1885 }
Simon Huntbb282f52014-11-10 11:08:19 -08001886 }
1887
1888 function wsTraceTx(msg) {
1889 wsTrace('tx', msg);
1890 }
1891 function wsTraceRx(msg) {
1892 wsTrace('rx', msg);
1893 }
1894 function wsTrace(rxtx, msg) {
Simon Huntbb282f52014-11-10 11:08:19 -08001895 console.log('[' + rxtx + '] ' + msg);
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001896 }
1897
Simon Huntc76ae892014-11-18 17:31:51 -08001898 // NOTE: Temporary hardcoded example for showing detail pane
1899 // while we fine-
1900 // Probably should not merge this change...
1901 function handleTestSend(msg) {
1902 if (msg.event === 'requestDetails') {
1903 showDetails({
1904 event: 'showDetails',
1905 sid: 1001,
1906 payload: {
1907 "id": "of:0000ffffffffff09",
1908 "type": "roadm",
1909 "propOrder": [
1910 "Name",
1911 "Vendor",
1912 "H/W Version",
1913 "S/W Version",
1914 "-",
1915 "Latitude",
1916 "Longitude",
1917 "Ports"
1918 ],
1919 "props": {
1920 "Name": null,
1921 "Vendor": "Linc",
1922 "H/W Version": "OE",
1923 "S/W Version": "?",
1924 "-": "",
1925 "Latitude": "40.8",
1926 "Longitude": "73.1",
1927 "Ports": "2"
1928 }
1929 }
1930 });
1931 }
1932 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001933
1934 // ==============================
1935 // Selection stuff
1936
1937 function selectObject(obj, el) {
1938 var n,
Simon Hunt01095ff2014-11-13 16:37:29 -08001939 srcEv = d3.event.sourceEvent,
1940 meta = srcEv.metaKey,
1941 shift = srcEv.shiftKey;
1942
Simon Huntdeab4322014-11-13 18:49:07 -08001943 if ((panZoom() && !meta) || (!panZoom() && meta)) {
Simon Hunt01095ff2014-11-13 16:37:29 -08001944 return;
1945 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001946
1947 if (el) {
1948 n = d3.select(el);
1949 } else {
1950 node.each(function(d) {
1951 if (d == obj) {
1952 n = d3.select(el = this);
1953 }
1954 });
1955 }
1956 if (!n) return;
1957
Simon Hunt01095ff2014-11-13 16:37:29 -08001958 if (shift && n.classed('selected')) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001959 deselectObject(obj.id);
Simon Hunt61d04042014-11-11 17:27:16 -08001960 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001961 return;
1962 }
1963
Simon Hunt01095ff2014-11-13 16:37:29 -08001964 if (!shift) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001965 deselectAll();
1966 }
1967
Simon Huntc31d5692014-11-12 13:27:18 -08001968 selections[obj.id] = { obj: obj, el: el };
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001969 selectOrder.push(obj.id);
1970
1971 n.classed('selected', true);
Simon Hunt61d04042014-11-11 17:27:16 -08001972 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001973 }
1974
1975 function deselectObject(id) {
Simon Huntc31d5692014-11-12 13:27:18 -08001976 var obj = selections[id],
1977 idx;
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001978 if (obj) {
1979 d3.select(obj.el).classed('selected', false);
Simon Hunt61d04042014-11-11 17:27:16 -08001980 delete selections[id];
Simon Huntc31d5692014-11-12 13:27:18 -08001981 idx = $.inArray(id, selectOrder);
1982 if (idx >= 0) {
1983 selectOrder.splice(idx, 1);
1984 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001985 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001986 }
1987
1988 function deselectAll() {
1989 // deselect all nodes in the network...
1990 node.classed('selected', false);
1991 selections = {};
1992 selectOrder = [];
Simon Hunt61d04042014-11-11 17:27:16 -08001993 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001994 }
1995
Thomas Vachuska47635c62014-11-22 01:21:36 -08001996 // update the state of the sumary pane
1997 function updateSummaryPane() {
1998
1999 }
2000
Simon Hunt61d04042014-11-11 17:27:16 -08002001 // update the state of the detail pane, based on current selections
2002 function updateDetailPane() {
2003 var nSel = selectOrder.length;
2004 if (!nSel) {
2005 detailPane.hide();
Thomas Vachuska47635c62014-11-22 01:21:36 -08002006 cancelTraffic();
Simon Hunt61d04042014-11-11 17:27:16 -08002007 } else if (nSel === 1) {
2008 singleSelect();
2009 } else {
2010 multiSelect();
2011 }
2012 }
2013
2014 function singleSelect() {
2015 requestDetails();
Simon Huntb53e0682014-11-12 13:32:01 -08002016 // NOTE: detail pane will be shown from showDetails event callback
Simon Hunt61d04042014-11-11 17:27:16 -08002017 }
2018
2019 function multiSelect() {
Simon Huntb53e0682014-11-12 13:32:01 -08002020 populateMultiSelect();
Simon Huntb53e0682014-11-12 13:32:01 -08002021 }
2022
2023 function addSep(tbody) {
2024 var tr = tbody.append('tr');
2025 $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
2026 }
2027
2028 function addProp(tbody, label, value) {
2029 var tr = tbody.append('tr');
2030
2031 tr.append('td')
2032 .attr('class', 'label')
2033 .text(label + ' :');
2034
2035 tr.append('td')
2036 .attr('class', 'value')
2037 .text(value);
2038 }
2039
2040 function populateMultiSelect() {
2041 detailPane.empty();
2042
Simon Hunta3dd9572014-11-20 15:22:41 -08002043 var title = detailPane.append('h3'),
2044 table = detailPane.append('table'),
2045 tbody = table.append('tbody');
Simon Huntb53e0682014-11-12 13:32:01 -08002046
Thomas Vachuska4731f122014-11-20 04:56:19 -08002047 title.text('Selected Nodes');
Simon Huntb53e0682014-11-12 13:32:01 -08002048
2049 selectOrder.forEach(function (d, i) {
2050 addProp(tbody, i+1, d);
2051 });
Simon Huntd72bc702014-11-13 18:38:04 -08002052
2053 addMultiSelectActions();
Simon Hunt61d04042014-11-11 17:27:16 -08002054 }
2055
Thomas Vachuska47635c62014-11-22 01:21:36 -08002056 // TODO: refactor to consolidate with populateDetails
2057 function populateSummary(data) {
2058 summaryPane.empty();
2059
2060 var svg = summaryPane.append('svg'),
2061 iid = iconGlyphUrl(data);
2062
2063 var title = summaryPane.append('h2'),
2064 table = summaryPane.append('table'),
2065 tbody = table.append('tbody');
2066
2067 appendGlyph(svg, 0, 0, 40, iid);
2068
2069 svg.append('use')
2070 .attr({
2071 class: 'birdBadge',
2072 transform: translate(8,12),
2073 'xlink:href': '#bird',
2074 width: 24,
2075 height: 24,
2076 fill: '#fff'
2077 });
2078
2079 title.text('ONOS Summary');
2080
2081 data.propOrder.forEach(function(p) {
2082 if (p === '-') {
2083 addSep(tbody);
2084 } else {
2085 addProp(tbody, p, data.props[p]);
2086 }
2087 });
2088 }
2089
Simon Hunt61d04042014-11-11 17:27:16 -08002090 function populateDetails(data) {
2091 detailPane.empty();
2092
Simon Hunta6a9fe72014-11-20 11:17:12 -08002093 var svg = detailPane.append('svg'),
2094 iid = iconGlyphUrl(data);
2095
Simon Hunta3dd9572014-11-20 15:22:41 -08002096 var title = detailPane.append('h2'),
2097 table = detailPane.append('table'),
2098 tbody = table.append('tbody');
Simon Hunt61d04042014-11-11 17:27:16 -08002099
Simon Hunta6a9fe72014-11-20 11:17:12 -08002100 appendGlyph(svg, 0, 0, 40, iid);
2101 title.text(data.id);
Simon Hunt61d04042014-11-11 17:27:16 -08002102
2103 data.propOrder.forEach(function(p) {
2104 if (p === '-') {
2105 addSep(tbody);
2106 } else {
2107 addProp(tbody, p, data.props[p]);
2108 }
2109 });
Simon Huntd72bc702014-11-13 18:38:04 -08002110
Thomas Vachuska4731f122014-11-20 04:56:19 -08002111 addSingleSelectActions(data);
Simon Hunt61d04042014-11-11 17:27:16 -08002112 }
2113
Thomas Vachuska4731f122014-11-20 04:56:19 -08002114 function addSingleSelectActions(data) {
Simon Huntd72bc702014-11-13 18:38:04 -08002115 detailPane.append('hr');
2116 // always want to allow 'show traffic'
Thomas Vachuska4731f122014-11-20 04:56:19 -08002117 addAction(detailPane, 'Show Related Traffic', showTrafficAction);
2118
2119 if (data.type === 'switch') {
2120 addAction(detailPane, 'Show Device Flows', showDeviceLinkFlowsAction);
2121 }
Simon Huntd72bc702014-11-13 18:38:04 -08002122 }
2123
2124 function addMultiSelectActions() {
2125 detailPane.append('hr');
2126 // always want to allow 'show traffic'
Thomas Vachuska4731f122014-11-20 04:56:19 -08002127 addAction(detailPane, 'Show Related Traffic', showTrafficAction);
Simon Huntd72bc702014-11-13 18:38:04 -08002128 // if exactly two hosts are selected, also want 'add host intent'
2129 if (nSel() === 2 && allSelectionsClass('host')) {
Thomas Vachuska4731f122014-11-20 04:56:19 -08002130 addAction(detailPane, 'Add Host-to-Host Intent', addIntentAction);
Simon Huntd72bc702014-11-13 18:38:04 -08002131 }
2132 }
2133
Simon Hunta5e89142014-11-14 07:00:33 -08002134 function addAction(panel, text, cb) {
2135 panel.append('div')
Simon Huntd72bc702014-11-13 18:38:04 -08002136 .classed('actionBtn', true)
2137 .text(text)
2138 .on('click', cb);
2139 }
2140
2141
Paul Greysonfcba0e82014-11-13 10:21:16 -08002142 function zoomPan(scale, translate) {
2143 zoomPanContainer.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
2144 // keep the map lines constant width while zooming
Thomas Vachuska89543292014-11-19 11:28:33 -08002145 bgImg.style("stroke-width", 2.0 / scale + "px");
Paul Greysonfcba0e82014-11-13 10:21:16 -08002146 }
2147
2148 function resetZoomPan() {
2149 zoomPan(1, [0,0]);
2150 zoom.scale(1).translate([0,0]);
2151 }
2152
2153 function setupZoomPan() {
2154 function zoomed() {
Simon Huntdeab4322014-11-13 18:49:07 -08002155 if (!panZoom() ^ !d3.event.sourceEvent.metaKey) {
Paul Greysonfcba0e82014-11-13 10:21:16 -08002156 zoomPan(d3.event.scale, d3.event.translate);
2157 }
2158 }
2159
2160 zoom = d3.behavior.zoom()
2161 .translate([0, 0])
2162 .scale(1)
2163 .scaleExtent([1, 8])
2164 .on("zoom", zoomed);
2165
2166 svg.call(zoom);
2167 }
2168
Simon Hunt61d04042014-11-11 17:27:16 -08002169 // ==============================
2170 // Test harness code
Simon Hunt56d51852014-11-09 13:03:35 -08002171
2172 function prepareScenario(view, ctx, dbg) {
2173 var sc = scenario,
2174 urlSc = sc.evDir + ctx + sc.evScenario;
2175
2176 if (!ctx) {
2177 view.alert("No scenario specified (null ctx)");
2178 return;
2179 }
2180
2181 sc.view = view;
2182 sc.ctx = ctx;
2183 sc.debug = dbg;
2184 sc.evNumber = 0;
2185
2186 d3.json(urlSc, function(err, data) {
Simon Huntbb282f52014-11-10 11:08:19 -08002187 var p = data && data.params || {},
2188 desc = data && data.description || null,
Simon Huntfc274c92014-11-11 11:05:46 -08002189 intro = data && data.title;
Simon Huntbb282f52014-11-10 11:08:19 -08002190
Simon Hunt56d51852014-11-09 13:03:35 -08002191 if (err) {
2192 view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
2193 } else {
2194 sc.params = p;
Simon Huntbb282f52014-11-10 11:08:19 -08002195 if (desc) {
2196 intro += '\n\n ' + desc.join('\n ');
2197 }
2198 view.alert(intro);
Simon Hunt56d51852014-11-09 13:03:35 -08002199 }
2200 });
2201
2202 }
2203
Simon Hunt01095ff2014-11-13 16:37:29 -08002204 // ==============================
2205 // Toggle Buttons in masthead
Simon Hunt0c6d4192014-11-12 12:07:10 -08002206
Simon Huntf8e5b4e02014-11-13 11:17:57 -08002207 // TODO: toggle button (and other widgets in the masthead) should be provided
2208 // by the framework; not generated by the view.
2209
Thomas Vachuska47635c62014-11-22 01:21:36 -08002210 //var showInstances;
Simon Huntf8e5b4e02014-11-13 11:17:57 -08002211
2212 function addButtonBar(view) {
2213 var bb = d3.select('#mast')
2214 .append('span').classed('right', true).attr('id', 'bb');
2215
Simon Hunta5e89142014-11-14 07:00:33 -08002216 function mkTogBtn(text, cb) {
2217 return bb.append('span')
2218 .classed('btn', true)
2219 .text(text)
2220 .on('click', cb);
2221 }
Simon Hunt01095ff2014-11-13 16:37:29 -08002222
Thomas Vachuska47635c62014-11-22 01:21:36 -08002223 //showInstances = mkTogBtn('Show Instances', toggleInst);
Simon Huntf8e5b4e02014-11-13 11:17:57 -08002224 }
2225
Thomas Vachuska47635c62014-11-22 01:21:36 -08002226 //function instShown() {
2227 // return showInstances.classed('active');
2228 //}
2229 //function toggleInst() {
2230 // showInstances.classed('active', !instShown());
2231 // if (instShown()) {
2232 // oiBox.show();
2233 // } else {
2234 // oiBox.hide();
2235 // }
2236 //}
Simon Hunt01095ff2014-11-13 16:37:29 -08002237
Simon Huntdeab4322014-11-13 18:49:07 -08002238 function panZoom() {
Simon Hunte5b71752014-11-18 20:06:07 -08002239 return false;
Simon Hunta5e89142014-11-14 07:00:33 -08002240 }
2241
2242 function trafficHover() {
Thomas Vachuska29617e52014-11-20 03:17:46 -08002243 return hoverModes[hoverMode] === 'intents';
Simon Hunta5e89142014-11-14 07:00:33 -08002244 }
Thomas Vachuska29617e52014-11-20 03:17:46 -08002245
2246 function flowsHover() {
2247 return hoverModes[hoverMode] === 'flows';
2248 }
2249
Simon Hunt7fa116d2014-11-17 14:16:55 -08002250 function loadGlyphs(svg) {
2251 var defs = svg.append('defs');
2252 gly.defBird(defs);
Simon Huntc72967b2014-11-20 09:21:42 -08002253 gly.defGlyphs(defs);
Simon Huntb82f6902014-11-22 11:53:15 -08002254 gly.defBadges(defs);
Simon Hunt7fa116d2014-11-17 14:16:55 -08002255 }
Simon Hunt01095ff2014-11-13 16:37:29 -08002256
Thomas Vachuska7d638d32014-11-07 10:24:43 -08002257 // ==============================
Simon Hunt142d0032014-11-04 20:13:09 -08002258 // View life-cycle callbacks
Simon Hunt195cb382014-11-03 17:50:51 -08002259
Simon Huntf67722a2014-11-10 09:32:06 -08002260 function preload(view, ctx, flags) {
Simon Hunt142d0032014-11-04 20:13:09 -08002261 var w = view.width(),
2262 h = view.height(),
Simon Huntc7ee0662014-11-05 16:44:37 -08002263 fcfg = config.force,
2264 fpad = fcfg.pad,
2265 forceDim = [w - 2*fpad, h - 2*fpad];
Simon Hunt195cb382014-11-03 17:50:51 -08002266
Simon Hunt142d0032014-11-04 20:13:09 -08002267 // NOTE: view.$div is a D3 selection of the view's div
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002268 var viewBox = '0 0 ' + config.logicalSize + ' ' + config.logicalSize;
2269 svg = view.$div.append('svg').attr('viewBox', viewBox);
Simon Hunt934c3ce2014-11-05 11:45:07 -08002270 setSize(svg, view);
2271
Simon Hunt7fa116d2014-11-17 14:16:55 -08002272 loadGlyphs(svg);
Simon Hunt12ce12e2014-11-15 21:13:19 -08002273
Paul Greysonfcba0e82014-11-13 10:21:16 -08002274 zoomPanContainer = svg.append('g').attr('id', 'zoomPanContainer');
Paul Greysonfcba0e82014-11-13 10:21:16 -08002275 setupZoomPan();
2276
Simon Hunt1a9eff92014-11-07 11:06:34 -08002277 // add blue glow filter to svg layer
Paul Greysonfcba0e82014-11-13 10:21:16 -08002278 d3u.appendGlow(zoomPanContainer);
Simon Hunt1a9eff92014-11-07 11:06:34 -08002279
Simon Huntc7ee0662014-11-05 16:44:37 -08002280 // group for the topology
Paul Greysonfcba0e82014-11-13 10:21:16 -08002281 topoG = zoomPanContainer.append('g')
Simon Huntd3b7d512014-11-12 15:48:41 -08002282 .attr('id', 'topo-G')
Simon Huntc7ee0662014-11-05 16:44:37 -08002283 .attr('transform', fcfg.translate());
2284
Simon Hunte2575b62014-11-18 15:25:53 -08002285 // subgroups for links, link labels, and nodes
Simon Huntc7ee0662014-11-05 16:44:37 -08002286 linkG = topoG.append('g').attr('id', 'links');
Simon Hunte2575b62014-11-18 15:25:53 -08002287 linkLabelG = topoG.append('g').attr('id', 'linkLabels');
Simon Huntc7ee0662014-11-05 16:44:37 -08002288 nodeG = topoG.append('g').attr('id', 'nodes');
2289
Simon Hunte2575b62014-11-18 15:25:53 -08002290 // selection of links, linkLabels, and nodes
Simon Huntc7ee0662014-11-05 16:44:37 -08002291 link = linkG.selectAll('.link');
Simon Hunte2575b62014-11-18 15:25:53 -08002292 linkLabel = linkLabelG.selectAll('.linkLabel');
Simon Huntc7ee0662014-11-05 16:44:37 -08002293 node = nodeG.selectAll('.node');
2294
Simon Hunt7cd48f32014-11-09 23:42:50 -08002295 function chrg(d) {
2296 return fcfg.charge[d.class] || -12000;
2297 }
Simon Hunt99c13842014-11-06 18:23:12 -08002298 function ldist(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08002299 return fcfg.linkDistance[d.type] || 50;
Simon Hunt99c13842014-11-06 18:23:12 -08002300 }
2301 function lstrg(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08002302 // 0.0 - 1.0
2303 return fcfg.linkStrength[d.type] || 1.0;
Simon Hunt99c13842014-11-06 18:23:12 -08002304 }
2305
Simon Hunt1a9eff92014-11-07 11:06:34 -08002306 function selectCb(d, self) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08002307 selectObject(d, self);
Simon Hunt1a9eff92014-11-07 11:06:34 -08002308 }
2309
2310 function atDragEnd(d, self) {
Simon Hunt56d51852014-11-09 13:03:35 -08002311 // once we've finished moving, pin the node in position
2312 d.fixed = true;
2313 d3.select(self).classed('fixed', true);
2314 if (config.useLiveData) {
Simon Hunt902c9922014-11-11 11:59:31 -08002315 sendUpdateMeta(d);
Simon Hunta255a2c2014-11-13 22:29:35 -08002316 } else {
2317 console.log('Moving node ' + d.id + ' to [' + d.x + ',' + d.y + ']');
Simon Hunt1a9eff92014-11-07 11:06:34 -08002318 }
2319 }
2320
Simon Hunt902c9922014-11-11 11:59:31 -08002321 function sendUpdateMeta(d) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08002322 sendMessage('updateMeta', {
2323 id: d.id,
2324 'class': d.class,
Simon Hunt902c9922014-11-11 11:59:31 -08002325 'memento': {
Simon Hunt01095ff2014-11-13 16:37:29 -08002326 x: d.x,
2327 y: d.y
Simon Hunt902c9922014-11-11 11:59:31 -08002328 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08002329 });
2330 }
2331
Simon Huntc7ee0662014-11-05 16:44:37 -08002332 // set up the force layout
2333 network.force = d3.layout.force()
2334 .size(forceDim)
2335 .nodes(network.nodes)
2336 .links(network.links)
Simon Hunt7cd48f32014-11-09 23:42:50 -08002337 .gravity(0.4)
2338 .friction(0.7)
2339 .charge(chrg)
Simon Hunt99c13842014-11-06 18:23:12 -08002340 .linkDistance(ldist)
2341 .linkStrength(lstrg)
Simon Huntc7ee0662014-11-05 16:44:37 -08002342 .on('tick', tick);
Simon Hunt195cb382014-11-03 17:50:51 -08002343
Simon Hunt01095ff2014-11-13 16:37:29 -08002344 network.drag = d3u.createDragBehavior(network.force,
Simon Huntdeab4322014-11-13 18:49:07 -08002345 selectCb, atDragEnd, panZoom);
Simon Hunt0c6d4192014-11-12 12:07:10 -08002346
2347 // create mask layer for when we lose connection to server.
Simon Hunta5e89142014-11-14 07:00:33 -08002348 // TODO: this should be part of the framework
Simon Hunt0c6d4192014-11-12 12:07:10 -08002349 mask = view.$div.append('div').attr('id','topo-mask');
2350 para(mask, 'Oops!');
2351 para(mask, 'Web-socket connection to server closed...');
2352 para(mask, 'Try refreshing the page.');
Simon Hunt12ce12e2014-11-15 21:13:19 -08002353
2354 mask.append('svg')
2355 .attr({
2356 id: 'mask-bird',
2357 width: w,
2358 height: h
2359 })
2360 .append('g')
2361 .attr('transform', birdTranslate(w, h))
2362 .style('opacity', 0.3)
2363 .append('use')
2364 .attr({
2365 'xlink:href': '#bird',
2366 width: config.birdDim,
2367 height: config.birdDim,
2368 fill: '#111'
Thomas Vachuska89543292014-11-19 11:28:33 -08002369 })
Simon Hunt1a9eff92014-11-07 11:06:34 -08002370 }
Simon Hunt195cb382014-11-03 17:50:51 -08002371
Simon Hunt01095ff2014-11-13 16:37:29 -08002372 function para(sel, text) {
2373 sel.append('p').text(text);
2374 }
2375
2376
Simon Hunt56d51852014-11-09 13:03:35 -08002377 function load(view, ctx, flags) {
Simon Huntf67722a2014-11-10 09:32:06 -08002378 // resize, in case the window was resized while we were not loaded
2379 resize(view, ctx, flags);
2380
Simon Hunt99c13842014-11-06 18:23:12 -08002381 // cache the view token, so network topo functions can access it
2382 network.view = view;
Simon Hunt56d51852014-11-09 13:03:35 -08002383 config.useLiveData = !flags.local;
2384
2385 if (!config.useLiveData) {
2386 prepareScenario(view, ctx, flags.debug);
2387 }
Simon Hunt99c13842014-11-06 18:23:12 -08002388
2389 // set our radio buttons and key bindings
Simon Hunt9462e8c2014-11-14 17:28:09 -08002390 layerBtnSet = view.setRadio(layerButtons);
Simon Hunt934c3ce2014-11-05 11:45:07 -08002391 view.setKeys(keyDispatch);
Simon Hunt195cb382014-11-03 17:50:51 -08002392
Simon Huntf8e5b4e02014-11-13 11:17:57 -08002393 // patch in our "button bar" for now
2394 // TODO: implement a more official frameworky way of doing this..
2395 addButtonBar(view);
2396
Simon Huntd3b7d512014-11-12 15:48:41 -08002397 // Load map data asynchronously; complete startup after that..
2398 loadGeoJsonData();
Simon Hunta255a2c2014-11-13 22:29:35 -08002399 }
2400
Thomas Vachuskaa3148a72014-11-19 21:38:35 -08002401 function startAntTimer() {
Thomas Vachuska4731f122014-11-20 04:56:19 -08002402 if (!antTimer) {
2403 var pulses = [5, 3, 1.2, 3],
2404 pulse = 0;
2405 antTimer = setInterval(function () {
2406 pulse = pulse + 1;
2407 pulse = pulse === pulses.length ? 0 : pulse;
2408 d3.selectAll('.animated').style('stroke-width', pulses[pulse]);
2409 }, 200);
2410 }
Thomas Vachuskaa3148a72014-11-19 21:38:35 -08002411 }
2412
2413 function stopAntTimer() {
Simon Hunta255a2c2014-11-13 22:29:35 -08002414 if (antTimer) {
2415 clearInterval(antTimer);
2416 antTimer = null;
2417 }
Simon Huntd3b7d512014-11-12 15:48:41 -08002418 }
2419
Thomas Vachuskaa3148a72014-11-19 21:38:35 -08002420 function unload(view, ctx, flags) {
2421 stopAntTimer();
2422 }
2423
Simon Huntd3b7d512014-11-12 15:48:41 -08002424 // TODO: move these to config/state portion of script
Simon Hunta6a9fe72014-11-20 11:17:12 -08002425 var geoJsonUrl = 'json/map/continental_us.json',
Simon Huntd3b7d512014-11-12 15:48:41 -08002426 geoJson;
2427
2428 function loadGeoJsonData() {
2429 d3.json(geoJsonUrl, function (err, data) {
2430 if (err) {
2431 // fall back to USA map background
2432 loadStaticMap();
2433 } else {
2434 geoJson = data;
2435 loadGeoMap();
2436 }
2437
2438 // finally, connect to the server...
2439 if (config.useLiveData) {
2440 webSock.connect();
2441 }
2442 });
2443 }
2444
2445 function showBg() {
2446 return config.options.showBackground ? 'visible' : 'hidden';
2447 }
2448
2449 function loadStaticMap() {
2450 fnTrace('loadStaticMap', config.backgroundUrl);
2451 var w = network.view.width(),
2452 h = network.view.height();
2453
2454 // load the background image
2455 bgImg = svg.insert('svg:image', '#topo-G')
2456 .attr({
2457 id: 'topo-bg',
2458 width: w,
2459 height: h,
2460 'xlink:href': config.backgroundUrl
2461 })
2462 .style({
2463 visibility: showBg()
2464 });
2465 }
2466
2467 function loadGeoMap() {
2468 fnTrace('loadGeoMap', geoJsonUrl);
Simon Huntd3b7d512014-11-12 15:48:41 -08002469
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002470 // extracts the topojson data into geocoordinate-based geometry
2471 var topoData = topojson.feature(geoJson, geoJson.objects.states);
Simon Huntd3b7d512014-11-12 15:48:41 -08002472
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002473 // see: http://bl.ocks.org/mbostock/4707858
2474 geoMapProjection = d3.geo.mercator();
2475 var path = d3.geo.path().projection(geoMapProjection);
Simon Huntd3b7d512014-11-12 15:48:41 -08002476
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002477 geoMapProjection
2478 .scale(1)
2479 .translate([0, 0]);
Simon Huntd3b7d512014-11-12 15:48:41 -08002480
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002481 // [[x1,y1],[x2,y2]]
2482 var b = path.bounds(topoData);
Paul Greysonfcba0e82014-11-13 10:21:16 -08002483 // size map to 95% of minimum dimension to fill space
2484 var s = .95 / Math.min((b[1][0] - b[0][0]) / config.logicalSize, (b[1][1] - b[0][1]) / config.logicalSize);
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002485 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 -08002486
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002487 geoMapProjection
2488 .scale(s)
2489 .translate(t);
2490
Paul Greysonfcba0e82014-11-13 10:21:16 -08002491 bgImg = zoomPanContainer.insert("g", '#topo-G');
Thomas Vachuska89543292014-11-19 11:28:33 -08002492 bgImg.attr('id', 'map').selectAll('path')
2493 .data(topoData.features)
2494 .enter()
2495 .append('path')
2496 .attr('d', path);
Simon Hunt195cb382014-11-03 17:50:51 -08002497 }
2498
Simon Huntf67722a2014-11-10 09:32:06 -08002499 function resize(view, ctx, flags) {
Simon Hunt12ce12e2014-11-15 21:13:19 -08002500 var w = view.width(),
2501 h = view.height();
2502
Simon Hunt934c3ce2014-11-05 11:45:07 -08002503 setSize(svg, view);
Simon Hunt12ce12e2014-11-15 21:13:19 -08002504
2505 d3.select('#mask-bird').attr({ width: w, height: h})
2506 .select('g').attr('transform', birdTranslate(w, h));
Simon Hunt142d0032014-11-04 20:13:09 -08002507 }
2508
Simon Hunt12ce12e2014-11-15 21:13:19 -08002509 function birdTranslate(w, h) {
2510 var bdim = config.birdDim;
2511 return 'translate('+((w-bdim)*.4)+','+((h-bdim)*.1)+')';
2512 }
Simon Hunt142d0032014-11-04 20:13:09 -08002513
2514 // ==============================
2515 // View registration
Simon Hunt195cb382014-11-03 17:50:51 -08002516
Simon Hunt25248912014-11-04 11:25:48 -08002517 onos.ui.addView('topo', {
Simon Hunt142d0032014-11-04 20:13:09 -08002518 preload: preload,
2519 load: load,
Simon Hunta255a2c2014-11-13 22:29:35 -08002520 unload: unload,
Simon Hunt142d0032014-11-04 20:13:09 -08002521 resize: resize
Simon Hunt195cb382014-11-03 17:50:51 -08002522 });
2523
Thomas Vachuska47635c62014-11-22 01:21:36 -08002524 summaryPane = onos.ui.addFloatingPanel('topo-summary');
Simon Hunt61d04042014-11-11 17:27:16 -08002525 detailPane = onos.ui.addFloatingPanel('topo-detail');
Simon Hunta5e89142014-11-14 07:00:33 -08002526 oiBox = onos.ui.addFloatingPanel('topo-oibox', 'TL');
Simon Huntb82f6902014-11-22 11:53:15 -08002527 oiBox.width(20);
Simon Hunt61d04042014-11-11 17:27:16 -08002528
Simon Hunt195cb382014-11-03 17:50:51 -08002529}(ONOS));