blob: 44b90f0c02a2fb4565696ebb2dc4454342ac3dd8 [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
949 function appendGlyph(svg, ox, oy, dim, iid) {
950 svg.append('use').attr({
951 class: 'glyphIcon',
952 transform: translate(ox,oy),
953 'xlink:href': iid,
954 width: dim,
955 height: dim
Simon Hunta6a9fe72014-11-20 11:17:12 -0800956 });
957 }
958
Simon Hunt61d04042014-11-11 17:27:16 -0800959 // ==============================
Simon Hunta5e89142014-11-14 07:00:33 -0800960 // onos instance panel functions
961
962 function updateInstances() {
963 var onoses = oiBox.el.selectAll('.onosInst')
964 .data(onosOrder, function (d) { return d.id; });
965
966 // operate on existing onoses if necessary
Simon Huntfcfb46c2014-11-19 12:53:38 -0800967 onoses.classed('online', function (d) { return d.online; });
Simon Hunta5e89142014-11-14 07:00:33 -0800968
969 var entering = onoses.enter()
970 .append('div')
971 .attr('class', 'onosInst')
972 .classed('online', function (d) { return d.online; })
Simon Hunt9c15eca2014-11-15 18:37:59 -0800973 .on('click', clickInst);
974
975 entering.each(function (d, i) {
976 var el = d3.select(this),
977 img;
Simon Hunt95908012014-11-20 10:20:26 -0800978 var css = window.getComputedStyle(this),
979 w = stripPx(css.width),
980 h = stripPx(css.height) / 2;
Simon Hunt9c15eca2014-11-15 18:37:59 -0800981
Simon Hunt95908012014-11-20 10:20:26 -0800982 var svg = el.append('svg').attr({
983 width: w,
984 height: h
985 });
986 var dim = 30;
Simon Hunta6a9fe72014-11-20 11:17:12 -0800987 appendGlyph(svg, 2, 2, 30, '#node');
Thomas Vachuska47635c62014-11-22 01:21:36 -0800988 svg.append('use')
989 .attr({
990 class: 'birdBadge',
991 transform: translate(8,10),
992 'xlink:href': '#bird',
993 width: 18,
994 height: 18,
995 fill: '#fff'
996 });
Simon Hunt9c15eca2014-11-15 18:37:59 -0800997
998 $('<div>').attr('class', 'onosTitle').text(d.id).appendTo(el);
999
1000 // is the UI attached to this instance?
1001 // TODO: need uiAttached boolean in instance data
Simon Hunta6a9fe72014-11-20 11:17:12 -08001002 // TODO: use SVG glyph, not png..
Simon Hunt9c15eca2014-11-15 18:37:59 -08001003 //if (d.uiAttached) {
1004 if (i === 0) {
1005 $('<img src="img/ui.png">').attr('class','ui').appendTo(el);
1006 }
1007 });
Simon Hunta5e89142014-11-14 07:00:33 -08001008
1009 // operate on existing + new onoses here
1010
1011 // the departed...
1012 var exiting = onoses.exit()
1013 .transition()
1014 .style('opacity', 0)
1015 .remove();
1016 }
1017
Simon Hunt9462e8c2014-11-14 17:28:09 -08001018 function clickInst(d) {
1019 var el = d3.select(this),
1020 aff = el.classed('affinity');
1021 if (!aff) {
1022 setAffinity(el, d);
1023 } else {
1024 cancelAffinity();
1025 }
1026 }
1027
1028 function setAffinity(el, d) {
1029 d3.selectAll('.onosInst')
1030 .classed('mastership', true)
1031 .classed('affinity', false);
1032 el.classed('affinity', true);
1033
1034 suppressLayers(true);
1035 node.each(function (n) {
1036 if (n.master === d.id) {
1037 n.el.classed('suppressed', false);
1038 }
1039 });
1040 oiShowMaster = true;
1041 }
1042
1043 function cancelAffinity() {
1044 d3.selectAll('.onosInst')
1045 .classed('mastership affinity', false);
1046 restoreLayerState();
1047 oiShowMaster = false;
1048 }
1049
Simon Hunta5e89142014-11-14 07:00:33 -08001050 // ==============================
Simon Hunt99c13842014-11-06 18:23:12 -08001051 // force layout modification functions
1052
1053 function translate(x, y) {
1054 return 'translate(' + x + ',' + y + ')';
1055 }
1056
Simon Hunte2575b62014-11-18 15:25:53 -08001057 function rotate(deg) {
1058 return 'rotate(' + deg + ')';
1059 }
1060
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001061 function missMsg(what, id) {
1062 return '\n[' + what + '] "' + id + '" missing ';
1063 }
1064
1065 function linkEndPoints(srcId, dstId) {
1066 var srcNode = network.lookup[srcId],
1067 dstNode = network.lookup[dstId],
1068 sMiss = !srcNode ? missMsg('src', srcId) : '',
1069 dMiss = !dstNode ? missMsg('dst', dstId) : '';
1070
1071 if (sMiss || dMiss) {
1072 logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
1073 return null;
1074 }
1075 return {
1076 source: srcNode,
1077 target: dstNode,
1078 x1: srcNode.x,
1079 y1: srcNode.y,
1080 x2: dstNode.x,
1081 y2: dstNode.y
1082 };
1083 }
1084
Simon Hunt56d51852014-11-09 13:03:35 -08001085 function createHostLink(host) {
1086 var src = host.id,
1087 dst = host.cp.device,
Simon Hunt7cd48f32014-11-09 23:42:50 -08001088 id = host.ingress,
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001089 lnk = linkEndPoints(src, dst);
Simon Hunt56d51852014-11-09 13:03:35 -08001090
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001091 if (!lnk) {
Simon Hunt56d51852014-11-09 13:03:35 -08001092 return null;
1093 }
1094
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001095 // Synthesize link ...
1096 $.extend(lnk, {
Simon Hunt8257f4c2014-11-16 19:34:54 -08001097 key: id,
Simon Hunt56d51852014-11-09 13:03:35 -08001098 class: 'link',
Simon Hunt8257f4c2014-11-16 19:34:54 -08001099
1100 type: function () { return 'hostLink'; },
1101 // TODO: ideally, we should see if our edge switch is online...
1102 online: function () { return true; },
1103 linkWidth: function () { return 1; }
Simon Hunt7cd48f32014-11-09 23:42:50 -08001104 });
Simon Hunt99c13842014-11-06 18:23:12 -08001105 return lnk;
1106 }
1107
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001108 function createLink(link) {
Simon Hunta6a9fe72014-11-20 11:17:12 -08001109 var lnk = linkEndPoints(link.src, link.dst);
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001110
1111 if (!lnk) {
1112 return null;
1113 }
1114
Simon Hunt8257f4c2014-11-16 19:34:54 -08001115 $.extend(lnk, {
1116 key: link.id,
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001117 class: 'link',
Simon Hunt8257f4c2014-11-16 19:34:54 -08001118 fromSource: link,
1119
1120 // functions to aggregate dual link state
1121 type: function () {
1122 var s = lnk.fromSource,
1123 t = lnk.fromTarget;
1124 return (s && s.type) || (t && t.type) || defaultLinkType;
1125 },
1126 online: function () {
1127 var s = lnk.fromSource,
1128 t = lnk.fromTarget;
1129 return (s && s.online) || (t && t.online);
1130 },
1131 linkWidth: function () {
1132 var s = lnk.fromSource,
1133 t = lnk.fromTarget,
1134 ws = (s && s.linkWidth) || 0,
1135 wt = (t && t.linkWidth) || 0;
1136 return Math.max(ws, wt);
1137 }
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001138 });
1139 return lnk;
Simon Hunt1a9eff92014-11-07 11:06:34 -08001140 }
1141
Simon Hunte2575b62014-11-18 15:25:53 -08001142 function removeLinkLabels() {
1143 network.links.forEach(function (d) {
1144 d.label = '';
1145 });
1146 }
1147
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001148 var widthRatio = 1.4,
1149 linkScale = d3.scale.linear()
1150 .domain([1, 12])
1151 .range([widthRatio, 12 * widthRatio])
1152 .clamp(true);
1153
Simon Hunt99c13842014-11-06 18:23:12 -08001154 function updateLinks() {
1155 link = linkG.selectAll('.link')
Simon Hunt8257f4c2014-11-16 19:34:54 -08001156 .data(network.links, function (d) { return d.key; });
Simon Hunt99c13842014-11-06 18:23:12 -08001157
1158 // operate on existing links, if necessary
1159 // link .foo() .bar() ...
1160
1161 // operate on entering links:
1162 var entering = link.enter()
1163 .append('line')
1164 .attr({
Simon Hunt99c13842014-11-06 18:23:12 -08001165 x1: function (d) { return d.x1; },
1166 y1: function (d) { return d.y1; },
1167 x2: function (d) { return d.x2; },
1168 y2: function (d) { return d.y2; },
Simon Hunt1a9eff92014-11-07 11:06:34 -08001169 stroke: config.topo.linkInColor,
1170 'stroke-width': config.topo.linkInWidth
Simon Hunt99c13842014-11-06 18:23:12 -08001171 });
1172
1173 // augment links
Simon Hunt7cd48f32014-11-09 23:42:50 -08001174 entering.each(function (d) {
1175 var link = d3.select(this);
1176 // provide ref to element selection from backing data....
1177 d.el = link;
Simon Hunt8257f4c2014-11-16 19:34:54 -08001178 restyleLinkElement(d);
Simon Hunt7cd48f32014-11-09 23:42:50 -08001179 });
Thomas Vachuska4830d392014-11-09 17:09:56 -08001180
1181 // operate on both existing and new links, if necessary
1182 //link .foo() .bar() ...
1183
Simon Hunte2575b62014-11-18 15:25:53 -08001184 // apply or remove labels
1185 var labelData = getLabelData();
1186 applyLinkLabels(labelData);
1187
Thomas Vachuska4830d392014-11-09 17:09:56 -08001188 // operate on exiting links:
Thomas Vachuska4830d392014-11-09 17:09:56 -08001189 link.exit()
Simon Hunt13bf9c82014-11-18 07:26:44 -08001190 .attr('stroke-dasharray', '3, 3')
1191 .style('opacity', 0.5)
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001192 .transition()
Simon Huntea80eb42014-11-11 13:46:57 -08001193 .duration(1500)
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001194 .attr({
Simon Hunt13bf9c82014-11-18 07:26:44 -08001195 'stroke-dasharray': '3, 12',
1196 stroke: config.topo.linkOutColor,
1197 'stroke-width': config.topo.linkOutWidth
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001198 })
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001199 .style('opacity', 0.0)
Thomas Vachuska4830d392014-11-09 17:09:56 -08001200 .remove();
Simon Hunte2575b62014-11-18 15:25:53 -08001201
1202 // NOTE: invoke a single tick to force the labels to position
1203 // onto their links.
1204 tick();
1205 }
1206
1207 function getLabelData() {
1208 // create the backing data for showing labels..
1209 var data = [];
1210 link.each(function (d) {
1211 if (d.label) {
1212 data.push({
1213 id: 'lab-' + d.key,
1214 key: d.key,
1215 label: d.label,
1216 ldata: d
1217 });
1218 }
1219 });
1220 return data;
1221 }
1222
1223 var linkLabelOffset = '0.3em';
1224
1225 function applyLinkLabels(data) {
1226 var entering;
1227
1228 linkLabel = linkLabelG.selectAll('.linkLabel')
1229 .data(data, function (d) { return d.id; });
1230
Simon Hunt56a2ea42014-11-19 12:39:31 -08001231 // for elements already existing, we need to update the text
1232 // and adjust the rectangle size to fit
1233 linkLabel.each(function (d) {
1234 var el = d3.select(this),
1235 rect = el.select('rect'),
1236 text = el.select('text');
1237 text.text(d.label);
1238 rect.attr(rectAroundText(el));
1239 });
Thomas Vachuskaf75b7ab2014-11-19 12:15:55 -08001240
Simon Hunte2575b62014-11-18 15:25:53 -08001241 entering = linkLabel.enter().append('g')
1242 .classed('linkLabel', true)
1243 .attr('id', function (d) { return d.id; });
1244
1245 entering.each(function (d) {
1246 var el = d3.select(this),
1247 rect,
1248 text,
1249 parms = {
1250 x1: d.ldata.x1,
1251 y1: d.ldata.y1,
1252 x2: d.ldata.x2,
1253 y2: d.ldata.y2
1254 };
1255
1256 d.el = el;
1257 rect = el.append('rect');
1258 text = el.append('text').text(d.label);
1259 rect.attr(rectAroundText(el));
1260 text.attr('dy', linkLabelOffset);
1261
1262 el.attr('transform', transformLabel(parms));
1263 });
1264
1265 // Remove any links that are no longer required.
1266 linkLabel.exit().remove();
1267 }
1268
1269 function rectAroundText(el) {
1270 var text = el.select('text'),
1271 box = text.node().getBBox();
1272
1273 // translate the bbox so that it is centered on [x,y]
1274 box.x = -box.width / 2;
1275 box.y = -box.height / 2;
1276
1277 // add padding
1278 box.x -= 1;
1279 box.width += 2;
1280 return box;
1281 }
1282
1283 function transformLabel(p) {
1284 var dx = p.x2 - p.x1,
1285 dy = p.y2 - p.y1,
1286 xMid = dx/2 + p.x1,
1287 yMid = dy/2 + p.y1;
Simon Hunte2575b62014-11-18 15:25:53 -08001288 return translate(xMid, yMid);
Simon Hunt99c13842014-11-06 18:23:12 -08001289 }
1290
1291 function createDeviceNode(device) {
1292 // start with the object as is
1293 var node = device,
Simon Huntbb282f52014-11-10 11:08:19 -08001294 type = device.type,
Simon Huntc72967b2014-11-20 09:21:42 -08001295 svgCls = type ? 'node device ' + type : 'node device',
1296 labels = device.labels || [];
1297
Simon Hunt99c13842014-11-06 18:23:12 -08001298 // Augment as needed...
1299 node.class = 'device';
Simon Huntbb282f52014-11-10 11:08:19 -08001300 node.svgClass = device.online ? svgCls + ' online' : svgCls;
Simon Hunt99c13842014-11-06 18:23:12 -08001301 positionNode(node);
Simon Hunt99c13842014-11-06 18:23:12 -08001302 return node;
1303 }
1304
Simon Hunt56d51852014-11-09 13:03:35 -08001305 function createHostNode(host) {
1306 // start with the object as is
1307 var node = host;
1308
1309 // Augment as needed...
1310 node.class = 'host';
Simon Hunt7cd48f32014-11-09 23:42:50 -08001311 if (!node.type) {
Simon Hunt209155e2014-11-21 12:16:09 -08001312 node.type = 'endstation';
Simon Hunt7cd48f32014-11-09 23:42:50 -08001313 }
Simon Hunt7fa116d2014-11-17 14:16:55 -08001314 node.svgClass = 'node host ' + node.type;
Simon Hunt56d51852014-11-09 13:03:35 -08001315 positionNode(node);
Simon Hunt56d51852014-11-09 13:03:35 -08001316 return node;
1317 }
1318
Simon Hunt99c13842014-11-06 18:23:12 -08001319 function positionNode(node) {
1320 var meta = node.metaUi,
Simon Huntac9e24f2014-11-12 10:12:21 -08001321 x = meta && meta.x,
1322 y = meta && meta.y,
1323 xy;
Simon Hunt99c13842014-11-06 18:23:12 -08001324
Simon Huntac9e24f2014-11-12 10:12:21 -08001325 // If we have [x,y] already, use that...
Simon Hunt99c13842014-11-06 18:23:12 -08001326 if (x && y) {
1327 node.fixed = true;
Simon Huntac9e24f2014-11-12 10:12:21 -08001328 node.x = x;
1329 node.y = y;
1330 return;
Simon Hunt99c13842014-11-06 18:23:12 -08001331 }
Simon Huntac9e24f2014-11-12 10:12:21 -08001332
Paul Greyson6cb8ca02014-11-12 18:09:02 -08001333 var location = node.location;
1334 if (location && location.type === 'latlng') {
1335 var coord = geoMapProjection([location.lng, location.lat]);
1336 node.fixed = true;
1337 node.x = coord[0];
1338 node.y = coord[1];
1339 return;
1340 }
1341
Simon Huntac9e24f2014-11-12 10:12:21 -08001342 // Note: Placing incoming unpinned nodes at exactly the same point
1343 // (center of the view) causes them to explode outwards when
1344 // the force layout kicks in. So, we spread them out a bit
1345 // initially, to provide a more serene layout convergence.
1346 // Additionally, if the node is a host, we place it near
1347 // the device it is connected to.
1348
1349 function spread(s) {
1350 return Math.floor((Math.random() * s) - s/2);
1351 }
1352
1353 function randDim(dim) {
1354 return dim / 2 + spread(dim * 0.7071);
1355 }
1356
1357 function rand() {
1358 return {
1359 x: randDim(network.view.width()),
1360 y: randDim(network.view.height())
1361 };
1362 }
1363
1364 function near(node) {
1365 var min = 12,
1366 dx = spread(12),
1367 dy = spread(12);
1368 return {
1369 x: node.x + min + dx,
1370 y: node.y + min + dy
1371 };
1372 }
1373
1374 function getDevice(cp) {
1375 var d = network.lookup[cp.device];
1376 return d || rand();
1377 }
1378
1379 xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
1380 $.extend(node, xy);
Simon Hunt99c13842014-11-06 18:23:12 -08001381 }
1382
Thomas Vachuska89543292014-11-19 11:28:33 -08001383 function iconUrl(d) {
1384 return 'img/' + d.type + '.png';
Simon Hunt99c13842014-11-06 18:23:12 -08001385 }
1386
Simon Huntc72967b2014-11-20 09:21:42 -08001387 function iconGlyphUrl(d) {
1388 var which = d.type || 'unknown';
1389 return '#' + which;
1390 }
1391
Simon Hunt99c13842014-11-06 18:23:12 -08001392 // returns the newly computed bounding box of the rectangle
1393 function adjustRectToFitText(n) {
1394 var text = n.select('text'),
1395 box = text.node().getBBox(),
1396 lab = config.labels;
1397
1398 text.attr('text-anchor', 'middle')
1399 .attr('y', '-0.8em')
1400 .attr('x', lab.imgPad/2);
1401
1402 // translate the bbox so that it is centered on [x,y]
1403 box.x = -box.width / 2;
1404 box.y = -box.height / 2;
1405
1406 // add padding
1407 box.x -= (lab.padLR + lab.imgPad/2);
1408 box.width += lab.padLR * 2 + lab.imgPad;
1409 box.y -= lab.padTB;
1410 box.height += lab.padTB * 2;
1411
1412 return box;
1413 }
1414
Simon Hunt1a9eff92014-11-07 11:06:34 -08001415 function mkSvgClass(d) {
1416 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
1417 }
1418
Simon Hunt7cd48f32014-11-09 23:42:50 -08001419 function hostLabel(d) {
1420 var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
1421 return d.labels[idx];
1422 }
1423 function deviceLabel(d) {
1424 var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
1425 return d.labels[idx];
1426 }
Simon Huntc72967b2014-11-20 09:21:42 -08001427 function trimLabel(label) {
1428 return (label && label.trim()) || '';
1429 }
1430
1431 function emptyBox() {
1432 return {
1433 x: -2,
1434 y: -2,
1435 width: 4,
1436 height: 4
1437 };
Simon Hunt7cd48f32014-11-09 23:42:50 -08001438 }
1439
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001440 function updateDeviceLabel(d) {
Simon Huntc72967b2014-11-20 09:21:42 -08001441 var label = trimLabel(deviceLabel(d)),
1442 noLabel = !label,
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001443 node = d.el,
Simon Huntc72967b2014-11-20 09:21:42 -08001444 box,
1445 dx,
1446 dy;
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001447
1448 node.select('text')
1449 .text(label)
1450 .style('opacity', 0)
1451 .transition()
1452 .style('opacity', 1);
1453
Simon Huntc72967b2014-11-20 09:21:42 -08001454 if (noLabel) {
1455 box = emptyBox();
1456 dx = -config.icons.device.dim/2;
1457 dy = -config.icons.device.dim/2;
1458 } else {
1459 box = adjustRectToFitText(node);
1460 dx = box.x + config.icons.xoff;
1461 dy = box.y + config.icons.yoff;
1462 }
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001463
1464 node.select('rect')
1465 .transition()
1466 .attr(box);
1467
Simon Huntc72967b2014-11-20 09:21:42 -08001468 node.select('g.deviceIcon')
Thomas Vachuska89543292014-11-19 11:28:33 -08001469 .transition()
Simon Huntc72967b2014-11-20 09:21:42 -08001470 .attr('transform', translate(dx, dy));
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001471 }
1472
1473 function updateHostLabel(d) {
1474 var label = hostLabel(d),
1475 host = d.el;
1476
1477 host.select('text').text(label);
1478 }
1479
Simon Hunta6a9fe72014-11-20 11:17:12 -08001480 // TODO: should be using updateNodes() to do the upates!
Simon Huntbb282f52014-11-10 11:08:19 -08001481 function updateDeviceState(nodeData) {
1482 nodeData.el.classed('online', nodeData.online);
1483 updateDeviceLabel(nodeData);
1484 // TODO: review what else might need to be updated
1485 }
1486
1487 function updateHostState(hostData) {
1488 updateHostLabel(hostData);
1489 // TODO: review what else might need to be updated
1490 }
1491
Simon Hunt6ac93f32014-11-13 12:17:27 -08001492 function nodeMouseOver(d) {
Simon Hunt6ac93f32014-11-13 12:17:27 -08001493 hovered = d;
Thomas Vachuska5fedb7a2014-11-20 00:55:08 -08001494 if (trafficHover() && (d.class === 'host' || d.class === 'device')) {
Thomas Vachuska29617e52014-11-20 03:17:46 -08001495 showSelectTraffic();
1496 } else if (flowsHover() && (d.class === 'device')) {
1497 showDeviceLinkFlows();
Simon Hunt6ac93f32014-11-13 12:17:27 -08001498 }
1499 }
1500
1501 function nodeMouseOut(d) {
Simon Hunt6ac93f32014-11-13 12:17:27 -08001502 hovered = null;
Thomas Vachuska5fedb7a2014-11-20 00:55:08 -08001503 if (trafficHover() && (d.class === 'host' || d.class === 'device')) {
Thomas Vachuska29617e52014-11-20 03:17:46 -08001504 showSelectTraffic();
1505 } else if (flowsHover() && (d.class === 'device')) {
1506 showDeviceLinkFlows();
Simon Hunt6ac93f32014-11-13 12:17:27 -08001507 }
1508 }
Simon Huntbb282f52014-11-10 11:08:19 -08001509
Simon Hunteb1514d2014-11-20 09:57:29 -08001510 function addHostIcon(node, radius, iid) {
Thomas Vachuska89543292014-11-19 11:28:33 -08001511 var dim = radius * 1.5,
1512 xlate = -dim / 2;
1513
Simon Hunteb1514d2014-11-20 09:57:29 -08001514 node.append('use').attr({
1515 class: 'glyphIcon hostIcon',
1516 transform: translate(xlate,xlate),
1517 'xlink:href': iid,
1518 width: dim,
1519 height: dim
1520 });
Thomas Vachuska89543292014-11-19 11:28:33 -08001521 }
1522
Simon Hunt99c13842014-11-06 18:23:12 -08001523 function updateNodes() {
1524 node = nodeG.selectAll('.node')
1525 .data(network.nodes, function (d) { return d.id; });
1526
Simon Huntc72967b2014-11-20 09:21:42 -08001527 // TODO: operate on existing nodes
Simon Hunt7cd48f32014-11-09 23:42:50 -08001528 // update host labels
Simon Hunt99c13842014-11-06 18:23:12 -08001529 //node .foo() .bar() ...
1530
1531 // operate on entering nodes:
1532 var entering = node.enter()
1533 .append('g')
1534 .attr({
1535 id: function (d) { return safeId(d.id); },
Simon Hunt1a9eff92014-11-07 11:06:34 -08001536 class: mkSvgClass,
Simon Hunt99c13842014-11-06 18:23:12 -08001537 transform: function (d) { return translate(d.x, d.y); },
1538 opacity: 0
1539 })
Simon Hunt1a9eff92014-11-07 11:06:34 -08001540 .call(network.drag)
Simon Hunt6ac93f32014-11-13 12:17:27 -08001541 .on('mouseover', nodeMouseOver)
1542 .on('mouseout', nodeMouseOut)
Simon Hunt99c13842014-11-06 18:23:12 -08001543 .transition()
1544 .attr('opacity', 1);
1545
1546 // augment device nodes...
1547 entering.filter('.device').each(function (d) {
1548 var node = d3.select(this),
Simon Huntc72967b2014-11-20 09:21:42 -08001549 label = trimLabel(deviceLabel(d)),
1550 noLabel = !label,
Simon Hunt99c13842014-11-06 18:23:12 -08001551 box;
1552
Simon Hunt7cd48f32014-11-09 23:42:50 -08001553 // provide ref to element from backing data....
1554 d.el = node;
1555
Simon Hunt99c13842014-11-06 18:23:12 -08001556 node.append('rect')
1557 .attr({
Simon Hunta3dd9572014-11-20 15:22:41 -08001558 rx: 5,
1559 ry: 5
Simon Hunt99c13842014-11-06 18:23:12 -08001560 });
1561
1562 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -08001563 .text(label)
Simon Hunt99c13842014-11-06 18:23:12 -08001564 .attr('dy', '1.1em');
1565
1566 box = adjustRectToFitText(node);
Simon Hunta3dd9572014-11-20 15:22:41 -08001567 node.select('rect').attr(box);
Simon Huntc72967b2014-11-20 09:21:42 -08001568 addDeviceIcon(node, box, noLabel, iconGlyphUrl(d));
Simon Huntc7ee0662014-11-05 16:44:37 -08001569 });
Simon Hunt934c3ce2014-11-05 11:45:07 -08001570
Thomas Vachuska89543292014-11-19 11:28:33 -08001571 // TODO: better place for this configuration state
1572 var defaultHostRadius = 9,
1573 hostRadius = {
1574 bgpSpeaker: 14,
1575 router: 14,
Thomas Vachuska60d72bf2014-11-21 13:02:00 -08001576 endstation: 14
Thomas Vachuska89543292014-11-19 11:28:33 -08001577 },
Simon Hunteb1514d2014-11-20 09:57:29 -08001578 hostGlyphId = {
Thomas Vachuska89543292014-11-19 11:28:33 -08001579 bgpSpeaker: 'bgpSpeaker',
1580 router: 'router',
Thomas Vachuska60d72bf2014-11-21 13:02:00 -08001581 endstation: 'endstation'
Thomas Vachuska89543292014-11-19 11:28:33 -08001582 };
1583
1584
Simon Hunt56d51852014-11-09 13:03:35 -08001585 // augment host nodes...
1586 entering.filter('.host').each(function (d) {
1587 var node = d3.select(this),
Thomas Vachuska89543292014-11-19 11:28:33 -08001588 r = hostRadius[d.type] || defaultHostRadius,
1589 textDy = r + 10,
Simon Hunteb1514d2014-11-20 09:57:29 -08001590 iid = iconGlyphUrl(d);
Simon Hunt56d51852014-11-09 13:03:35 -08001591
Simon Hunt7cd48f32014-11-09 23:42:50 -08001592 // provide ref to element from backing data....
1593 d.el = node;
1594
Thomas Vachuska89543292014-11-19 11:28:33 -08001595 node.append('circle')
1596 .attr('r', r);
Simon Hunt7fa116d2014-11-17 14:16:55 -08001597
Simon Hunteb1514d2014-11-20 09:57:29 -08001598 if (iid) {
1599 addHostIcon(node, r, iid);
Simon Hunt7fa116d2014-11-17 14:16:55 -08001600 }
Simon Hunt56d51852014-11-09 13:03:35 -08001601
Simon Hunt56d51852014-11-09 13:03:35 -08001602 node.append('text')
Simon Hunt7cd48f32014-11-09 23:42:50 -08001603 .text(hostLabel)
Thomas Vachuska89543292014-11-19 11:28:33 -08001604 .attr('dy', textDy)
Simon Hunt7cd48f32014-11-09 23:42:50 -08001605 .attr('text-anchor', 'middle');
Simon Hunt56d51852014-11-09 13:03:35 -08001606
1607 // debug function to show the modelled x,y coordinates of nodes...
1608 if (debug('showNodeXY')) {
1609 node.select('circle').attr('fill-opacity', 0.5);
1610 node.append('circle')
1611 .attr({
1612 class: 'debug',
1613 cx: 0,
1614 cy: 0,
1615 r: '3px'
1616 });
1617 }
1618 });
Simon Huntc7ee0662014-11-05 16:44:37 -08001619
Simon Hunt99c13842014-11-06 18:23:12 -08001620 // operate on both existing and new nodes, if necessary
1621 //node .foo() .bar() ...
Simon Huntc7ee0662014-11-05 16:44:37 -08001622
Simon Hunt99c13842014-11-06 18:23:12 -08001623 // operate on exiting nodes:
Simon Huntea80eb42014-11-11 13:46:57 -08001624 // Note that the node is removed after 2 seconds.
1625 // Sub element animations should be shorter than 2 seconds.
1626 var exiting = node.exit()
Simon Hunt44031102014-11-11 13:20:36 -08001627 .transition()
1628 .duration(2000)
Simon Huntea80eb42014-11-11 13:46:57 -08001629 .style('opacity', 0)
Simon Hunt99c13842014-11-06 18:23:12 -08001630 .remove();
Simon Huntea80eb42014-11-11 13:46:57 -08001631
1632 // host node exits....
1633 exiting.filter('.host').each(function (d) {
1634 var node = d3.select(this);
1635
1636 node.select('text')
1637 .style('opacity', 0.5)
1638 .transition()
1639 .duration(1000)
1640 .style('opacity', 0);
1641 // note, leave <g>.remove to remove this element
1642
Thomas Vachuska89543292014-11-19 11:28:33 -08001643 node.select('circle')
1644 .style('stroke-fill', '#555')
1645 .style('fill', '#888')
Simon Huntea80eb42014-11-11 13:46:57 -08001646 .style('opacity', 0.5)
1647 .transition()
1648 .duration(1500)
1649 .attr('r', 0);
1650 // note, leave <g>.remove to remove this element
1651
1652 });
1653
1654 // TODO: device node exits
Simon Huntc7ee0662014-11-05 16:44:37 -08001655 }
1656
Simon Huntc72967b2014-11-20 09:21:42 -08001657 function addDeviceIcon(node, box, noLabel, iid) {
1658 var cfg = config.icons.device,
1659 dx,
1660 dy,
1661 g;
1662
1663 if (noLabel) {
1664 box = emptyBox();
1665 dx = -cfg.dim/2;
1666 dy = -cfg.dim/2;
1667 } else {
1668 box = adjustRectToFitText(node);
1669 dx = box.x + config.icons.xoff;
1670 dy = box.y + config.icons.yoff;
1671 }
1672
Simon Hunteb1514d2014-11-20 09:57:29 -08001673 g = node.append('g')
1674 .attr('class', 'glyphIcon deviceIcon')
Simon Huntc72967b2014-11-20 09:21:42 -08001675 .attr('transform', translate(dx, dy));
1676
1677 g.append('rect').attr({
1678 x: 0,
1679 y: 0,
1680 rx: cfg.rx,
1681 width: cfg.dim,
1682 height: cfg.dim
1683 });
1684
1685 g.append('use').attr({
1686 'xlink:href': iid,
1687 width: cfg.dim,
1688 height: cfg.dim
1689 });
1690
Simon Huntc72967b2014-11-20 09:21:42 -08001691 }
1692
Simon Hunt8257f4c2014-11-16 19:34:54 -08001693 function find(key, array) {
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001694 for (var idx = 0, n = array.length; idx < n; idx++) {
Simon Hunt8257f4c2014-11-16 19:34:54 -08001695 if (array[idx].key === key) {
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001696 return idx;
1697 }
1698 }
1699 return -1;
1700 }
1701
1702 function removeLinkElement(linkData) {
Simon Hunt8257f4c2014-11-16 19:34:54 -08001703 var idx = find(linkData.key, network.links),
1704 removed;
1705 if (idx >=0) {
1706 // remove from links array
1707 removed = network.links.splice(idx, 1);
1708 // remove from lookup cache
1709 delete network.lookup[removed[0].key];
1710 updateLinks();
1711 network.force.resume();
1712 }
Simon Hunt3f03d4a2014-11-10 20:14:37 -08001713 }
Simon Huntc7ee0662014-11-05 16:44:37 -08001714
Simon Hunt44031102014-11-11 13:20:36 -08001715 function removeHostElement(hostData) {
1716 // first, remove associated hostLink...
1717 removeLinkElement(hostData.linkData);
1718
1719 // remove from lookup cache
1720 delete network.lookup[hostData.id];
1721 // remove from nodes array
1722 var idx = find(hostData.id, network.nodes);
1723 network.nodes.splice(idx, 1);
1724 // remove from SVG
1725 updateNodes();
1726 network.force.resume();
1727 }
1728
1729
Simon Huntc7ee0662014-11-05 16:44:37 -08001730 function tick() {
1731 node.attr({
Simon Hunt99c13842014-11-06 18:23:12 -08001732 transform: function (d) { return translate(d.x, d.y); }
Simon Huntc7ee0662014-11-05 16:44:37 -08001733 });
1734
1735 link.attr({
1736 x1: function (d) { return d.source.x; },
1737 y1: function (d) { return d.source.y; },
1738 x2: function (d) { return d.target.x; },
1739 y2: function (d) { return d.target.y; }
1740 });
Simon Hunte2575b62014-11-18 15:25:53 -08001741
1742 linkLabel.each(function (d) {
1743 var el = d3.select(this);
Thomas Vachuska4731f122014-11-20 04:56:19 -08001744 var lnk = findLinkById(d.key);
1745
1746 if (lnk) {
1747 var parms = {
Simon Hunte2575b62014-11-18 15:25:53 -08001748 x1: lnk.source.x,
1749 y1: lnk.source.y,
1750 x2: lnk.target.x,
1751 y2: lnk.target.y
1752 };
Thomas Vachuska4731f122014-11-20 04:56:19 -08001753 el.attr('transform', transformLabel(parms));
1754 }
Simon Hunte2575b62014-11-18 15:25:53 -08001755 });
Simon Huntc7ee0662014-11-05 16:44:37 -08001756 }
Simon Hunt934c3ce2014-11-05 11:45:07 -08001757
1758 // ==============================
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001759 // Web-Socket for live data
1760
1761 function webSockUrl() {
1762 return document.location.toString()
1763 .replace(/\#.*/, '')
1764 .replace('http://', 'ws://')
1765 .replace('https://', 'wss://')
1766 .replace('index2.html', config.webSockUrl);
1767 }
1768
1769 webSock = {
1770 ws : null,
1771
1772 connect : function() {
1773 webSock.ws = new WebSocket(webSockUrl());
1774
1775 webSock.ws.onopen = function() {
Simon Hunt0c6d4192014-11-12 12:07:10 -08001776 noWebSock(false);
Thomas Vachuska47635c62014-11-22 01:21:36 -08001777 requestSummary();
1778 oiBox.show();
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001779 };
1780
1781 webSock.ws.onmessage = function(m) {
1782 if (m.data) {
Simon Huntbb282f52014-11-10 11:08:19 -08001783 wsTraceRx(m.data);
Thomas Vachuskad472c6e2014-11-07 19:11:05 -08001784 handleServerEvent(JSON.parse(m.data));
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001785 }
1786 };
1787
1788 webSock.ws.onclose = function(m) {
1789 webSock.ws = null;
Simon Hunt0c6d4192014-11-12 12:07:10 -08001790 noWebSock(true);
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001791 };
1792 },
1793
1794 send : function(text) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001795 if (text != null) {
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001796 webSock._send(text);
1797 }
1798 },
1799
1800 _send : function(message) {
1801 if (webSock.ws) {
1802 webSock.ws.send(message);
Simon Hunta255a2c2014-11-13 22:29:35 -08001803 } else if (config.useLiveData) {
Simon Hunt56d51852014-11-09 13:03:35 -08001804 network.view.alert('no web socket open\n\n' + message);
Simon Hunta255a2c2014-11-13 22:29:35 -08001805 } else {
1806 console.log('WS Send: ' + JSON.stringify(message));
Thomas Vachuska7d638d32014-11-07 10:24:43 -08001807 }
1808 }
1809
1810 };
1811
Simon Hunt0c6d4192014-11-12 12:07:10 -08001812 function noWebSock(b) {
1813 mask.style('display',b ? 'block' : 'none');
1814 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001815
1816 function sendMessage(evType, payload) {
1817 var toSend = {
Simon Huntbb282f52014-11-10 11:08:19 -08001818 event: evType,
1819 sid: ++sid,
1820 payload: payload
1821 },
1822 asText = JSON.stringify(toSend);
1823 wsTraceTx(asText);
1824 webSock.send(asText);
Simon Huntc76ae892014-11-18 17:31:51 -08001825
1826 // Temporary measure for debugging UI behavior ...
1827 if (!config.useLiveData) {
1828 handleTestSend(toSend);
1829 }
Simon Huntbb282f52014-11-10 11:08:19 -08001830 }
1831
1832 function wsTraceTx(msg) {
1833 wsTrace('tx', msg);
1834 }
1835 function wsTraceRx(msg) {
1836 wsTrace('rx', msg);
1837 }
1838 function wsTrace(rxtx, msg) {
Simon Huntbb282f52014-11-10 11:08:19 -08001839 console.log('[' + rxtx + '] ' + msg);
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001840 }
1841
Simon Huntc76ae892014-11-18 17:31:51 -08001842 // NOTE: Temporary hardcoded example for showing detail pane
1843 // while we fine-
1844 // Probably should not merge this change...
1845 function handleTestSend(msg) {
1846 if (msg.event === 'requestDetails') {
1847 showDetails({
1848 event: 'showDetails',
1849 sid: 1001,
1850 payload: {
1851 "id": "of:0000ffffffffff09",
1852 "type": "roadm",
1853 "propOrder": [
1854 "Name",
1855 "Vendor",
1856 "H/W Version",
1857 "S/W Version",
1858 "-",
1859 "Latitude",
1860 "Longitude",
1861 "Ports"
1862 ],
1863 "props": {
1864 "Name": null,
1865 "Vendor": "Linc",
1866 "H/W Version": "OE",
1867 "S/W Version": "?",
1868 "-": "",
1869 "Latitude": "40.8",
1870 "Longitude": "73.1",
1871 "Ports": "2"
1872 }
1873 }
1874 });
1875 }
1876 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001877
1878 // ==============================
1879 // Selection stuff
1880
1881 function selectObject(obj, el) {
1882 var n,
Simon Hunt01095ff2014-11-13 16:37:29 -08001883 srcEv = d3.event.sourceEvent,
1884 meta = srcEv.metaKey,
1885 shift = srcEv.shiftKey;
1886
Simon Huntdeab4322014-11-13 18:49:07 -08001887 if ((panZoom() && !meta) || (!panZoom() && meta)) {
Simon Hunt01095ff2014-11-13 16:37:29 -08001888 return;
1889 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001890
1891 if (el) {
1892 n = d3.select(el);
1893 } else {
1894 node.each(function(d) {
1895 if (d == obj) {
1896 n = d3.select(el = this);
1897 }
1898 });
1899 }
1900 if (!n) return;
1901
Simon Hunt01095ff2014-11-13 16:37:29 -08001902 if (shift && n.classed('selected')) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001903 deselectObject(obj.id);
Simon Hunt61d04042014-11-11 17:27:16 -08001904 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001905 return;
1906 }
1907
Simon Hunt01095ff2014-11-13 16:37:29 -08001908 if (!shift) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001909 deselectAll();
1910 }
1911
Simon Huntc31d5692014-11-12 13:27:18 -08001912 selections[obj.id] = { obj: obj, el: el };
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001913 selectOrder.push(obj.id);
1914
1915 n.classed('selected', true);
Simon Hunt61d04042014-11-11 17:27:16 -08001916 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001917 }
1918
1919 function deselectObject(id) {
Simon Huntc31d5692014-11-12 13:27:18 -08001920 var obj = selections[id],
1921 idx;
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001922 if (obj) {
1923 d3.select(obj.el).classed('selected', false);
Simon Hunt61d04042014-11-11 17:27:16 -08001924 delete selections[id];
Simon Huntc31d5692014-11-12 13:27:18 -08001925 idx = $.inArray(id, selectOrder);
1926 if (idx >= 0) {
1927 selectOrder.splice(idx, 1);
1928 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001929 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001930 }
1931
1932 function deselectAll() {
1933 // deselect all nodes in the network...
1934 node.classed('selected', false);
1935 selections = {};
1936 selectOrder = [];
Simon Hunt61d04042014-11-11 17:27:16 -08001937 updateDetailPane();
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08001938 }
1939
Thomas Vachuska47635c62014-11-22 01:21:36 -08001940 // update the state of the sumary pane
1941 function updateSummaryPane() {
1942
1943 }
1944
Simon Hunt61d04042014-11-11 17:27:16 -08001945 // update the state of the detail pane, based on current selections
1946 function updateDetailPane() {
1947 var nSel = selectOrder.length;
1948 if (!nSel) {
1949 detailPane.hide();
Thomas Vachuska47635c62014-11-22 01:21:36 -08001950 cancelTraffic();
Simon Hunt61d04042014-11-11 17:27:16 -08001951 } else if (nSel === 1) {
1952 singleSelect();
1953 } else {
1954 multiSelect();
1955 }
1956 }
1957
1958 function singleSelect() {
1959 requestDetails();
Simon Huntb53e0682014-11-12 13:32:01 -08001960 // NOTE: detail pane will be shown from showDetails event callback
Simon Hunt61d04042014-11-11 17:27:16 -08001961 }
1962
1963 function multiSelect() {
Simon Huntb53e0682014-11-12 13:32:01 -08001964 populateMultiSelect();
Simon Huntb53e0682014-11-12 13:32:01 -08001965 }
1966
1967 function addSep(tbody) {
1968 var tr = tbody.append('tr');
1969 $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
1970 }
1971
1972 function addProp(tbody, label, value) {
1973 var tr = tbody.append('tr');
1974
1975 tr.append('td')
1976 .attr('class', 'label')
1977 .text(label + ' :');
1978
1979 tr.append('td')
1980 .attr('class', 'value')
1981 .text(value);
1982 }
1983
1984 function populateMultiSelect() {
1985 detailPane.empty();
1986
Simon Hunta3dd9572014-11-20 15:22:41 -08001987 var title = detailPane.append('h3'),
1988 table = detailPane.append('table'),
1989 tbody = table.append('tbody');
Simon Huntb53e0682014-11-12 13:32:01 -08001990
Thomas Vachuska4731f122014-11-20 04:56:19 -08001991 title.text('Selected Nodes');
Simon Huntb53e0682014-11-12 13:32:01 -08001992
1993 selectOrder.forEach(function (d, i) {
1994 addProp(tbody, i+1, d);
1995 });
Simon Huntd72bc702014-11-13 18:38:04 -08001996
1997 addMultiSelectActions();
Simon Hunt61d04042014-11-11 17:27:16 -08001998 }
1999
Thomas Vachuska47635c62014-11-22 01:21:36 -08002000 // TODO: refactor to consolidate with populateDetails
2001 function populateSummary(data) {
2002 summaryPane.empty();
2003
2004 var svg = summaryPane.append('svg'),
2005 iid = iconGlyphUrl(data);
2006
2007 var title = summaryPane.append('h2'),
2008 table = summaryPane.append('table'),
2009 tbody = table.append('tbody');
2010
2011 appendGlyph(svg, 0, 0, 40, iid);
2012
2013 svg.append('use')
2014 .attr({
2015 class: 'birdBadge',
2016 transform: translate(8,12),
2017 'xlink:href': '#bird',
2018 width: 24,
2019 height: 24,
2020 fill: '#fff'
2021 });
2022
2023 title.text('ONOS Summary');
2024
2025 data.propOrder.forEach(function(p) {
2026 if (p === '-') {
2027 addSep(tbody);
2028 } else {
2029 addProp(tbody, p, data.props[p]);
2030 }
2031 });
2032 }
2033
Simon Hunt61d04042014-11-11 17:27:16 -08002034 function populateDetails(data) {
2035 detailPane.empty();
2036
Simon Hunta6a9fe72014-11-20 11:17:12 -08002037 var svg = detailPane.append('svg'),
2038 iid = iconGlyphUrl(data);
2039
Simon Hunta3dd9572014-11-20 15:22:41 -08002040 var title = detailPane.append('h2'),
2041 table = detailPane.append('table'),
2042 tbody = table.append('tbody');
Simon Hunt61d04042014-11-11 17:27:16 -08002043
Simon Hunta6a9fe72014-11-20 11:17:12 -08002044 appendGlyph(svg, 0, 0, 40, iid);
2045 title.text(data.id);
Simon Hunt61d04042014-11-11 17:27:16 -08002046
2047 data.propOrder.forEach(function(p) {
2048 if (p === '-') {
2049 addSep(tbody);
2050 } else {
2051 addProp(tbody, p, data.props[p]);
2052 }
2053 });
Simon Huntd72bc702014-11-13 18:38:04 -08002054
Thomas Vachuska4731f122014-11-20 04:56:19 -08002055 addSingleSelectActions(data);
Simon Hunt61d04042014-11-11 17:27:16 -08002056 }
2057
Thomas Vachuska4731f122014-11-20 04:56:19 -08002058 function addSingleSelectActions(data) {
Simon Huntd72bc702014-11-13 18:38:04 -08002059 detailPane.append('hr');
2060 // always want to allow 'show traffic'
Thomas Vachuska4731f122014-11-20 04:56:19 -08002061 addAction(detailPane, 'Show Related Traffic', showTrafficAction);
2062
2063 if (data.type === 'switch') {
2064 addAction(detailPane, 'Show Device Flows', showDeviceLinkFlowsAction);
2065 }
Simon Huntd72bc702014-11-13 18:38:04 -08002066 }
2067
2068 function addMultiSelectActions() {
2069 detailPane.append('hr');
2070 // always want to allow 'show traffic'
Thomas Vachuska4731f122014-11-20 04:56:19 -08002071 addAction(detailPane, 'Show Related Traffic', showTrafficAction);
Simon Huntd72bc702014-11-13 18:38:04 -08002072 // if exactly two hosts are selected, also want 'add host intent'
2073 if (nSel() === 2 && allSelectionsClass('host')) {
Thomas Vachuska4731f122014-11-20 04:56:19 -08002074 addAction(detailPane, 'Add Host-to-Host Intent', addIntentAction);
Simon Huntd72bc702014-11-13 18:38:04 -08002075 }
2076 }
2077
Simon Hunta5e89142014-11-14 07:00:33 -08002078 function addAction(panel, text, cb) {
2079 panel.append('div')
Simon Huntd72bc702014-11-13 18:38:04 -08002080 .classed('actionBtn', true)
2081 .text(text)
2082 .on('click', cb);
2083 }
2084
2085
Paul Greysonfcba0e82014-11-13 10:21:16 -08002086 function zoomPan(scale, translate) {
2087 zoomPanContainer.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
2088 // keep the map lines constant width while zooming
Thomas Vachuska89543292014-11-19 11:28:33 -08002089 bgImg.style("stroke-width", 2.0 / scale + "px");
Paul Greysonfcba0e82014-11-13 10:21:16 -08002090 }
2091
2092 function resetZoomPan() {
2093 zoomPan(1, [0,0]);
2094 zoom.scale(1).translate([0,0]);
2095 }
2096
2097 function setupZoomPan() {
2098 function zoomed() {
Simon Huntdeab4322014-11-13 18:49:07 -08002099 if (!panZoom() ^ !d3.event.sourceEvent.metaKey) {
Paul Greysonfcba0e82014-11-13 10:21:16 -08002100 zoomPan(d3.event.scale, d3.event.translate);
2101 }
2102 }
2103
2104 zoom = d3.behavior.zoom()
2105 .translate([0, 0])
2106 .scale(1)
2107 .scaleExtent([1, 8])
2108 .on("zoom", zoomed);
2109
2110 svg.call(zoom);
2111 }
2112
Simon Hunt61d04042014-11-11 17:27:16 -08002113 // ==============================
2114 // Test harness code
Simon Hunt56d51852014-11-09 13:03:35 -08002115
2116 function prepareScenario(view, ctx, dbg) {
2117 var sc = scenario,
2118 urlSc = sc.evDir + ctx + sc.evScenario;
2119
2120 if (!ctx) {
2121 view.alert("No scenario specified (null ctx)");
2122 return;
2123 }
2124
2125 sc.view = view;
2126 sc.ctx = ctx;
2127 sc.debug = dbg;
2128 sc.evNumber = 0;
2129
2130 d3.json(urlSc, function(err, data) {
Simon Huntbb282f52014-11-10 11:08:19 -08002131 var p = data && data.params || {},
2132 desc = data && data.description || null,
Simon Huntfc274c92014-11-11 11:05:46 -08002133 intro = data && data.title;
Simon Huntbb282f52014-11-10 11:08:19 -08002134
Simon Hunt56d51852014-11-09 13:03:35 -08002135 if (err) {
2136 view.alert('No scenario found:\n\n' + urlSc + '\n\n' + err);
2137 } else {
2138 sc.params = p;
Simon Huntbb282f52014-11-10 11:08:19 -08002139 if (desc) {
2140 intro += '\n\n ' + desc.join('\n ');
2141 }
2142 view.alert(intro);
Simon Hunt56d51852014-11-09 13:03:35 -08002143 }
2144 });
2145
2146 }
2147
Simon Hunt01095ff2014-11-13 16:37:29 -08002148 // ==============================
2149 // Toggle Buttons in masthead
Simon Hunt0c6d4192014-11-12 12:07:10 -08002150
Simon Huntf8e5b4e02014-11-13 11:17:57 -08002151 // TODO: toggle button (and other widgets in the masthead) should be provided
2152 // by the framework; not generated by the view.
2153
Thomas Vachuska47635c62014-11-22 01:21:36 -08002154 //var showInstances;
Simon Huntf8e5b4e02014-11-13 11:17:57 -08002155
2156 function addButtonBar(view) {
2157 var bb = d3.select('#mast')
2158 .append('span').classed('right', true).attr('id', 'bb');
2159
Simon Hunta5e89142014-11-14 07:00:33 -08002160 function mkTogBtn(text, cb) {
2161 return bb.append('span')
2162 .classed('btn', true)
2163 .text(text)
2164 .on('click', cb);
2165 }
Simon Hunt01095ff2014-11-13 16:37:29 -08002166
Thomas Vachuska47635c62014-11-22 01:21:36 -08002167 //showInstances = mkTogBtn('Show Instances', toggleInst);
Simon Huntf8e5b4e02014-11-13 11:17:57 -08002168 }
2169
Thomas Vachuska47635c62014-11-22 01:21:36 -08002170 //function instShown() {
2171 // return showInstances.classed('active');
2172 //}
2173 //function toggleInst() {
2174 // showInstances.classed('active', !instShown());
2175 // if (instShown()) {
2176 // oiBox.show();
2177 // } else {
2178 // oiBox.hide();
2179 // }
2180 //}
Simon Hunt01095ff2014-11-13 16:37:29 -08002181
Simon Huntdeab4322014-11-13 18:49:07 -08002182 function panZoom() {
Simon Hunte5b71752014-11-18 20:06:07 -08002183 return false;
Simon Hunta5e89142014-11-14 07:00:33 -08002184 }
2185
2186 function trafficHover() {
Thomas Vachuska29617e52014-11-20 03:17:46 -08002187 return hoverModes[hoverMode] === 'intents';
Simon Hunta5e89142014-11-14 07:00:33 -08002188 }
Thomas Vachuska29617e52014-11-20 03:17:46 -08002189
2190 function flowsHover() {
2191 return hoverModes[hoverMode] === 'flows';
2192 }
2193
Simon Hunt7fa116d2014-11-17 14:16:55 -08002194 function loadGlyphs(svg) {
2195 var defs = svg.append('defs');
2196 gly.defBird(defs);
2197 gly.defBullhorn(defs);
Simon Huntc72967b2014-11-20 09:21:42 -08002198 gly.defGlyphs(defs);
Simon Hunt7fa116d2014-11-17 14:16:55 -08002199 }
Simon Hunt01095ff2014-11-13 16:37:29 -08002200
Thomas Vachuska7d638d32014-11-07 10:24:43 -08002201 // ==============================
Simon Hunt142d0032014-11-04 20:13:09 -08002202 // View life-cycle callbacks
Simon Hunt195cb382014-11-03 17:50:51 -08002203
Simon Huntf67722a2014-11-10 09:32:06 -08002204 function preload(view, ctx, flags) {
Simon Hunt142d0032014-11-04 20:13:09 -08002205 var w = view.width(),
2206 h = view.height(),
Simon Huntc7ee0662014-11-05 16:44:37 -08002207 fcfg = config.force,
2208 fpad = fcfg.pad,
2209 forceDim = [w - 2*fpad, h - 2*fpad];
Simon Hunt195cb382014-11-03 17:50:51 -08002210
Simon Hunt142d0032014-11-04 20:13:09 -08002211 // NOTE: view.$div is a D3 selection of the view's div
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002212 var viewBox = '0 0 ' + config.logicalSize + ' ' + config.logicalSize;
2213 svg = view.$div.append('svg').attr('viewBox', viewBox);
Simon Hunt934c3ce2014-11-05 11:45:07 -08002214 setSize(svg, view);
2215
Simon Hunt7fa116d2014-11-17 14:16:55 -08002216 loadGlyphs(svg);
Simon Hunt12ce12e2014-11-15 21:13:19 -08002217
Paul Greysonfcba0e82014-11-13 10:21:16 -08002218 zoomPanContainer = svg.append('g').attr('id', 'zoomPanContainer');
Paul Greysonfcba0e82014-11-13 10:21:16 -08002219 setupZoomPan();
2220
Simon Hunt1a9eff92014-11-07 11:06:34 -08002221 // add blue glow filter to svg layer
Paul Greysonfcba0e82014-11-13 10:21:16 -08002222 d3u.appendGlow(zoomPanContainer);
Simon Hunt1a9eff92014-11-07 11:06:34 -08002223
Simon Huntc7ee0662014-11-05 16:44:37 -08002224 // group for the topology
Paul Greysonfcba0e82014-11-13 10:21:16 -08002225 topoG = zoomPanContainer.append('g')
Simon Huntd3b7d512014-11-12 15:48:41 -08002226 .attr('id', 'topo-G')
Simon Huntc7ee0662014-11-05 16:44:37 -08002227 .attr('transform', fcfg.translate());
2228
Simon Hunte2575b62014-11-18 15:25:53 -08002229 // subgroups for links, link labels, and nodes
Simon Huntc7ee0662014-11-05 16:44:37 -08002230 linkG = topoG.append('g').attr('id', 'links');
Simon Hunte2575b62014-11-18 15:25:53 -08002231 linkLabelG = topoG.append('g').attr('id', 'linkLabels');
Simon Huntc7ee0662014-11-05 16:44:37 -08002232 nodeG = topoG.append('g').attr('id', 'nodes');
2233
Simon Hunte2575b62014-11-18 15:25:53 -08002234 // selection of links, linkLabels, and nodes
Simon Huntc7ee0662014-11-05 16:44:37 -08002235 link = linkG.selectAll('.link');
Simon Hunte2575b62014-11-18 15:25:53 -08002236 linkLabel = linkLabelG.selectAll('.linkLabel');
Simon Huntc7ee0662014-11-05 16:44:37 -08002237 node = nodeG.selectAll('.node');
2238
Simon Hunt7cd48f32014-11-09 23:42:50 -08002239 function chrg(d) {
2240 return fcfg.charge[d.class] || -12000;
2241 }
Simon Hunt99c13842014-11-06 18:23:12 -08002242 function ldist(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08002243 return fcfg.linkDistance[d.type] || 50;
Simon Hunt99c13842014-11-06 18:23:12 -08002244 }
2245 function lstrg(d) {
Simon Hunt7cd48f32014-11-09 23:42:50 -08002246 // 0.0 - 1.0
2247 return fcfg.linkStrength[d.type] || 1.0;
Simon Hunt99c13842014-11-06 18:23:12 -08002248 }
2249
Simon Hunt1a9eff92014-11-07 11:06:34 -08002250 function selectCb(d, self) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08002251 selectObject(d, self);
Simon Hunt1a9eff92014-11-07 11:06:34 -08002252 }
2253
2254 function atDragEnd(d, self) {
Simon Hunt56d51852014-11-09 13:03:35 -08002255 // once we've finished moving, pin the node in position
2256 d.fixed = true;
2257 d3.select(self).classed('fixed', true);
2258 if (config.useLiveData) {
Simon Hunt902c9922014-11-11 11:59:31 -08002259 sendUpdateMeta(d);
Simon Hunta255a2c2014-11-13 22:29:35 -08002260 } else {
2261 console.log('Moving node ' + d.id + ' to [' + d.x + ',' + d.y + ']');
Simon Hunt1a9eff92014-11-07 11:06:34 -08002262 }
2263 }
2264
Simon Hunt902c9922014-11-11 11:59:31 -08002265 function sendUpdateMeta(d) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08002266 sendMessage('updateMeta', {
2267 id: d.id,
2268 'class': d.class,
Simon Hunt902c9922014-11-11 11:59:31 -08002269 'memento': {
Simon Hunt01095ff2014-11-13 16:37:29 -08002270 x: d.x,
2271 y: d.y
Simon Hunt902c9922014-11-11 11:59:31 -08002272 }
Thomas Vachuskad1be50d2014-11-08 16:10:20 -08002273 });
2274 }
2275
Simon Huntc7ee0662014-11-05 16:44:37 -08002276 // set up the force layout
2277 network.force = d3.layout.force()
2278 .size(forceDim)
2279 .nodes(network.nodes)
2280 .links(network.links)
Simon Hunt7cd48f32014-11-09 23:42:50 -08002281 .gravity(0.4)
2282 .friction(0.7)
2283 .charge(chrg)
Simon Hunt99c13842014-11-06 18:23:12 -08002284 .linkDistance(ldist)
2285 .linkStrength(lstrg)
Simon Huntc7ee0662014-11-05 16:44:37 -08002286 .on('tick', tick);
Simon Hunt195cb382014-11-03 17:50:51 -08002287
Simon Hunt01095ff2014-11-13 16:37:29 -08002288 network.drag = d3u.createDragBehavior(network.force,
Simon Huntdeab4322014-11-13 18:49:07 -08002289 selectCb, atDragEnd, panZoom);
Simon Hunt0c6d4192014-11-12 12:07:10 -08002290
2291 // create mask layer for when we lose connection to server.
Simon Hunta5e89142014-11-14 07:00:33 -08002292 // TODO: this should be part of the framework
Simon Hunt0c6d4192014-11-12 12:07:10 -08002293 mask = view.$div.append('div').attr('id','topo-mask');
2294 para(mask, 'Oops!');
2295 para(mask, 'Web-socket connection to server closed...');
2296 para(mask, 'Try refreshing the page.');
Simon Hunt12ce12e2014-11-15 21:13:19 -08002297
2298 mask.append('svg')
2299 .attr({
2300 id: 'mask-bird',
2301 width: w,
2302 height: h
2303 })
2304 .append('g')
2305 .attr('transform', birdTranslate(w, h))
2306 .style('opacity', 0.3)
2307 .append('use')
2308 .attr({
2309 'xlink:href': '#bird',
2310 width: config.birdDim,
2311 height: config.birdDim,
2312 fill: '#111'
Thomas Vachuska89543292014-11-19 11:28:33 -08002313 })
Simon Hunt1a9eff92014-11-07 11:06:34 -08002314 }
Simon Hunt195cb382014-11-03 17:50:51 -08002315
Simon Hunt01095ff2014-11-13 16:37:29 -08002316 function para(sel, text) {
2317 sel.append('p').text(text);
2318 }
2319
2320
Simon Hunt56d51852014-11-09 13:03:35 -08002321 function load(view, ctx, flags) {
Simon Huntf67722a2014-11-10 09:32:06 -08002322 // resize, in case the window was resized while we were not loaded
2323 resize(view, ctx, flags);
2324
Simon Hunt99c13842014-11-06 18:23:12 -08002325 // cache the view token, so network topo functions can access it
2326 network.view = view;
Simon Hunt56d51852014-11-09 13:03:35 -08002327 config.useLiveData = !flags.local;
2328
2329 if (!config.useLiveData) {
2330 prepareScenario(view, ctx, flags.debug);
2331 }
Simon Hunt99c13842014-11-06 18:23:12 -08002332
2333 // set our radio buttons and key bindings
Simon Hunt9462e8c2014-11-14 17:28:09 -08002334 layerBtnSet = view.setRadio(layerButtons);
Simon Hunt934c3ce2014-11-05 11:45:07 -08002335 view.setKeys(keyDispatch);
Simon Hunt195cb382014-11-03 17:50:51 -08002336
Simon Huntf8e5b4e02014-11-13 11:17:57 -08002337 // patch in our "button bar" for now
2338 // TODO: implement a more official frameworky way of doing this..
2339 addButtonBar(view);
2340
Simon Huntd3b7d512014-11-12 15:48:41 -08002341 // Load map data asynchronously; complete startup after that..
2342 loadGeoJsonData();
Simon Hunta255a2c2014-11-13 22:29:35 -08002343 }
2344
Thomas Vachuskaa3148a72014-11-19 21:38:35 -08002345 function startAntTimer() {
Thomas Vachuska4731f122014-11-20 04:56:19 -08002346 if (!antTimer) {
2347 var pulses = [5, 3, 1.2, 3],
2348 pulse = 0;
2349 antTimer = setInterval(function () {
2350 pulse = pulse + 1;
2351 pulse = pulse === pulses.length ? 0 : pulse;
2352 d3.selectAll('.animated').style('stroke-width', pulses[pulse]);
2353 }, 200);
2354 }
Thomas Vachuskaa3148a72014-11-19 21:38:35 -08002355 }
2356
2357 function stopAntTimer() {
Simon Hunta255a2c2014-11-13 22:29:35 -08002358 if (antTimer) {
2359 clearInterval(antTimer);
2360 antTimer = null;
2361 }
Simon Huntd3b7d512014-11-12 15:48:41 -08002362 }
2363
Thomas Vachuskaa3148a72014-11-19 21:38:35 -08002364 function unload(view, ctx, flags) {
2365 stopAntTimer();
2366 }
2367
Simon Huntd3b7d512014-11-12 15:48:41 -08002368 // TODO: move these to config/state portion of script
Simon Hunta6a9fe72014-11-20 11:17:12 -08002369 var geoJsonUrl = 'json/map/continental_us.json',
Simon Huntd3b7d512014-11-12 15:48:41 -08002370 geoJson;
2371
2372 function loadGeoJsonData() {
2373 d3.json(geoJsonUrl, function (err, data) {
2374 if (err) {
2375 // fall back to USA map background
2376 loadStaticMap();
2377 } else {
2378 geoJson = data;
2379 loadGeoMap();
2380 }
2381
2382 // finally, connect to the server...
2383 if (config.useLiveData) {
2384 webSock.connect();
2385 }
2386 });
2387 }
2388
2389 function showBg() {
2390 return config.options.showBackground ? 'visible' : 'hidden';
2391 }
2392
2393 function loadStaticMap() {
2394 fnTrace('loadStaticMap', config.backgroundUrl);
2395 var w = network.view.width(),
2396 h = network.view.height();
2397
2398 // load the background image
2399 bgImg = svg.insert('svg:image', '#topo-G')
2400 .attr({
2401 id: 'topo-bg',
2402 width: w,
2403 height: h,
2404 'xlink:href': config.backgroundUrl
2405 })
2406 .style({
2407 visibility: showBg()
2408 });
2409 }
2410
2411 function loadGeoMap() {
2412 fnTrace('loadGeoMap', geoJsonUrl);
Simon Huntd3b7d512014-11-12 15:48:41 -08002413
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002414 // extracts the topojson data into geocoordinate-based geometry
2415 var topoData = topojson.feature(geoJson, geoJson.objects.states);
Simon Huntd3b7d512014-11-12 15:48:41 -08002416
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002417 // see: http://bl.ocks.org/mbostock/4707858
2418 geoMapProjection = d3.geo.mercator();
2419 var path = d3.geo.path().projection(geoMapProjection);
Simon Huntd3b7d512014-11-12 15:48:41 -08002420
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002421 geoMapProjection
2422 .scale(1)
2423 .translate([0, 0]);
Simon Huntd3b7d512014-11-12 15:48:41 -08002424
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002425 // [[x1,y1],[x2,y2]]
2426 var b = path.bounds(topoData);
Paul Greysonfcba0e82014-11-13 10:21:16 -08002427 // size map to 95% of minimum dimension to fill space
2428 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 -08002429 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 -08002430
Paul Greyson6cb8ca02014-11-12 18:09:02 -08002431 geoMapProjection
2432 .scale(s)
2433 .translate(t);
2434
Paul Greysonfcba0e82014-11-13 10:21:16 -08002435 bgImg = zoomPanContainer.insert("g", '#topo-G');
Thomas Vachuska89543292014-11-19 11:28:33 -08002436 bgImg.attr('id', 'map').selectAll('path')
2437 .data(topoData.features)
2438 .enter()
2439 .append('path')
2440 .attr('d', path);
Simon Hunt195cb382014-11-03 17:50:51 -08002441 }
2442
Simon Huntf67722a2014-11-10 09:32:06 -08002443 function resize(view, ctx, flags) {
Simon Hunt12ce12e2014-11-15 21:13:19 -08002444 var w = view.width(),
2445 h = view.height();
2446
Simon Hunt934c3ce2014-11-05 11:45:07 -08002447 setSize(svg, view);
Simon Hunt12ce12e2014-11-15 21:13:19 -08002448
2449 d3.select('#mask-bird').attr({ width: w, height: h})
2450 .select('g').attr('transform', birdTranslate(w, h));
Simon Hunt142d0032014-11-04 20:13:09 -08002451 }
2452
Simon Hunt12ce12e2014-11-15 21:13:19 -08002453 function birdTranslate(w, h) {
2454 var bdim = config.birdDim;
2455 return 'translate('+((w-bdim)*.4)+','+((h-bdim)*.1)+')';
2456 }
Simon Hunt142d0032014-11-04 20:13:09 -08002457
2458 // ==============================
2459 // View registration
Simon Hunt195cb382014-11-03 17:50:51 -08002460
Simon Hunt25248912014-11-04 11:25:48 -08002461 onos.ui.addView('topo', {
Simon Hunt142d0032014-11-04 20:13:09 -08002462 preload: preload,
2463 load: load,
Simon Hunta255a2c2014-11-13 22:29:35 -08002464 unload: unload,
Simon Hunt142d0032014-11-04 20:13:09 -08002465 resize: resize
Simon Hunt195cb382014-11-03 17:50:51 -08002466 });
2467
Thomas Vachuska47635c62014-11-22 01:21:36 -08002468 summaryPane = onos.ui.addFloatingPanel('topo-summary');
Simon Hunt61d04042014-11-11 17:27:16 -08002469 detailPane = onos.ui.addFloatingPanel('topo-detail');
Simon Hunta5e89142014-11-14 07:00:33 -08002470 oiBox = onos.ui.addFloatingPanel('topo-oibox', 'TL');
Simon Hunt61d04042014-11-11 17:27:16 -08002471
Simon Hunt195cb382014-11-03 17:50:51 -08002472}(ONOS));