blob: fc7c35af7ec58d62fa07db9874e1e15877b105d6 [file] [log] [blame]
Simon Hunt195cb382014-11-03 17:50:51 -08001/*
2 * Copyright 2014 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17/*
Simon Hunt142d0032014-11-04 20:13:09 -080018 ONOS network topology viewer - version 1.1
Simon Hunt195cb382014-11-03 17:50:51 -080019
20 @author Simon Hunt
21 */
22
23(function (onos) {
24 'use strict';
25
Simon Hunt1a9eff92014-11-07 11:06:34 -080026 // shorter names for library APIs
27 var d3u = onos.lib.d3util;
28
Simon Hunt195cb382014-11-03 17:50:51 -080029 // configuration data
30 var config = {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -080031 useLiveData: true,
Simon Hunt195cb382014-11-03 17:50:51 -080032 debugOn: false,
33 debug: {
Simon Hunt99c13842014-11-06 18:23:12 -080034 showNodeXY: true,
35 showKeyHandler: false
Simon Hunt195cb382014-11-03 17:50:51 -080036 },
37 options: {
38 layering: true,
39 collisionPrevention: true,
Simon Hunt142d0032014-11-04 20:13:09 -080040 showBackground: true
Simon Hunt195cb382014-11-03 17:50:51 -080041 },
42 backgroundUrl: 'img/us-map.png',
Thomas Vachuska7d638d32014-11-07 10:24:43 -080043 webSockUrl: 'ws/topology',
Simon Hunt195cb382014-11-03 17:50:51 -080044 data: {
45 live: {
46 jsonUrl: 'rs/topology/graph',
47 detailPrefix: 'rs/topology/graph/',
48 detailSuffix: ''
49 },
50 fake: {
51 jsonUrl: 'json/network2.json',
52 detailPrefix: 'json/',
53 detailSuffix: '.json'
54 }
55 },
Simon Hunt99c13842014-11-06 18:23:12 -080056 labels: {
57 imgPad: 16,
58 padLR: 4,
59 padTB: 3,
60 marginLR: 3,
61 marginTB: 2,
62 port: {
63 gap: 3,
64 width: 18,
65 height: 14
66 }
67 },
Simon Hunt1a9eff92014-11-07 11:06:34 -080068 topo: {
69 linkInColor: '#66f',
70 linkInWidth: 14
71 },
Simon Hunt99c13842014-11-06 18:23:12 -080072 icons: {
73 w: 28,
74 h: 28,
75 xoff: -12,
76 yoff: -8
77 },
Simon Hunt195cb382014-11-03 17:50:51 -080078 iconUrl: {
79 device: 'img/device.png',
80 host: 'img/host.png',
81 pkt: 'img/pkt.png',
82 opt: 'img/opt.png'
83 },
Simon Hunt195cb382014-11-03 17:50:51 -080084 force: {
Simon Huntc7ee0662014-11-05 16:44:37 -080085 note: 'node.class or link.class is used to differentiate',
86 linkDistance: {
87 infra: 200,
88 host: 40
89 },
90 linkStrength: {
91 infra: 1.0,
92 host: 1.0
93 },
94 charge: {
95 device: -400,
96 host: -100
97 },
98 pad: 20,
Simon Hunt195cb382014-11-03 17:50:51 -080099 translate: function() {
100 return 'translate(' +
Simon Huntc7ee0662014-11-05 16:44:37 -0800101 config.force.pad + ',' +
102 config.force.pad + ')';
Simon Hunt195cb382014-11-03 17:50:51 -0800103 }
Simon Hunt142d0032014-11-04 20:13:09 -0800104 }
Simon Hunt195cb382014-11-03 17:50:51 -0800105 };
106
Simon Hunt142d0032014-11-04 20:13:09 -0800107 // radio buttons
108 var btnSet = [
Simon Hunt934c3ce2014-11-05 11:45:07 -0800109 { text: 'All Layers', cb: showAllLayers },
110 { text: 'Packet Only', cb: showPacketLayer },
111 { text: 'Optical Only', cb: showOpticalLayer }
112 ];
113
114 // key bindings
115 var keyDispatch = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800116 M: testMe, // TODO: remove (testing only)
Simon Hunt50128c02014-11-08 13:36:15 -0800117 S: injectStartupEvents, // TODO: remove (testing only)
118 space: injectTestEvent, // TODO: remove (testing only)
Simon Hunt99c13842014-11-06 18:23:12 -0800119
Simon Hunt934c3ce2014-11-05 11:45:07 -0800120 B: toggleBg,
Simon Hunt934c3ce2014-11-05 11:45:07 -0800121 L: cycleLabels,
122 P: togglePorts,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800123 U: unpin,
124
125 X: requestPath
Simon Hunt934c3ce2014-11-05 11:45:07 -0800126 };
Simon Hunt142d0032014-11-04 20:13:09 -0800127
Simon Hunt195cb382014-11-03 17:50:51 -0800128 // state variables
Simon Hunt99c13842014-11-06 18:23:12 -0800129 var network = {
Simon Hunt50128c02014-11-08 13:36:15 -0800130 view: null, // view token reference
Simon Hunt99c13842014-11-06 18:23:12 -0800131 nodes: [],
132 links: [],
133 lookup: {}
134 },
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800135 webSock,
Simon Hunt99c13842014-11-06 18:23:12 -0800136 labelIdx = 0,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800137
138 //selected = {},
139 selectOrder = [],
140 selections = {},
141
Simon Hunt195cb382014-11-03 17:50:51 -0800142 highlighted = null,
143 hovered = null,
144 viewMode = 'showAll',
145 portLabelsOn = false;
146
Simon Hunt934c3ce2014-11-05 11:45:07 -0800147 // D3 selections
148 var svg,
149 bgImg,
Simon Huntc7ee0662014-11-05 16:44:37 -0800150 topoG,
151 nodeG,
152 linkG,
153 node,
154 link;
Simon Hunt195cb382014-11-03 17:50:51 -0800155
Simon Hunt142d0032014-11-04 20:13:09 -0800156 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800157 // For Debugging / Development
Simon Hunt195cb382014-11-03 17:50:51 -0800158
Simon Hunt99c13842014-11-06 18:23:12 -0800159 var eventPrefix = 'json/eventTest_',
Simon Hunt1a9eff92014-11-07 11:06:34 -0800160 eventNumber = 0,
161 alertNumber = 0;
Simon Hunt195cb382014-11-03 17:50:51 -0800162
Simon Hunt99c13842014-11-06 18:23:12 -0800163 function note(label, msg) {
164 console.log('NOTE: ' + label + ': ' + msg);
Simon Hunt195cb382014-11-03 17:50:51 -0800165 }
166
Simon Hunt99c13842014-11-06 18:23:12 -0800167 function debug(what) {
168 return config.debugOn && config.debug[what];
Simon Hunt934c3ce2014-11-05 11:45:07 -0800169 }
170
Simon Hunt99c13842014-11-06 18:23:12 -0800171
Simon Hunt934c3ce2014-11-05 11:45:07 -0800172 // ==============================
173 // Key Callbacks
174
Simon Hunt99c13842014-11-06 18:23:12 -0800175 function testMe(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800176 view.alert('test');
Simon Hunt99c13842014-11-06 18:23:12 -0800177 }
178
179 function injectTestEvent(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800180 if (config.useLiveData) {
181 view.alert("Sorry, currently using live data..");
182 return;
183 }
184
Simon Hunt99c13842014-11-06 18:23:12 -0800185 eventNumber++;
186 var eventUrl = eventPrefix + eventNumber + '.json';
187
Simon Hunt99c13842014-11-06 18:23:12 -0800188 d3.json(eventUrl, function(err, data) {
189 if (err) {
190 view.dataLoadError(err, eventUrl);
191 } else {
192 handleServerEvent(data);
193 }
194 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800195 }
196
Simon Hunt1a9eff92014-11-07 11:06:34 -0800197 function injectStartupEvents(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800198 if (config.useLiveData) {
199 view.alert("Sorry, currently using live data..");
200 return;
201 }
202
Simon Hunt1a9eff92014-11-07 11:06:34 -0800203 var lastStartupEvent = 32;
204 while (eventNumber < lastStartupEvent) {
205 injectTestEvent(view);
206 }
207 }
208
Simon Hunt934c3ce2014-11-05 11:45:07 -0800209 function toggleBg() {
210 var vis = bgImg.style('visibility');
211 bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
212 }
213
Simon Hunt99c13842014-11-06 18:23:12 -0800214 function cycleLabels() {
215 labelIdx = (labelIdx === network.deviceLabelCount - 1) ? 0 : labelIdx + 1;
216 network.nodes.forEach(function (d) {
217 var idx = (labelIdx < d.labels.length) ? labelIdx : 0,
218 node = d3.select('#' + safeId(d.id)),
219 box;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800220
Simon Hunt99c13842014-11-06 18:23:12 -0800221 node.select('text')
222 .text(d.labels[idx])
223 .style('opacity', 0)
224 .transition()
225 .style('opacity', 1);
226
227 box = adjustRectToFitText(node);
228
229 node.select('rect')
230 .transition()
231 .attr(box);
232
233 node.select('image')
234 .transition()
235 .attr('x', box.x + config.icons.xoff)
236 .attr('y', box.y + config.icons.yoff);
237 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800238 }
239
240 function togglePorts(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800241 view.alert('togglePorts() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800242 }
243
244 function unpin(view) {
Simon Hunt50128c02014-11-08 13:36:15 -0800245 view.alert('unpin() callback')
Simon Hunt934c3ce2014-11-05 11:45:07 -0800246 }
247
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800248 function requestPath(view) {
249 var payload = {
250 one: selections[selectOrder[0]].obj.id,
251 two: selections[selectOrder[1]].obj.id
252 }
253 sendMessage('requestPath', payload);
254 }
255
Simon Hunt934c3ce2014-11-05 11:45:07 -0800256 // ==============================
257 // Radio Button Callbacks
258
Simon Hunt195cb382014-11-03 17:50:51 -0800259 function showAllLayers() {
Simon Hunt142d0032014-11-04 20:13:09 -0800260// network.node.classed('inactive', false);
261// network.link.classed('inactive', false);
262// d3.selectAll('svg .port').classed('inactive', false);
263// d3.selectAll('svg .portText').classed('inactive', false);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800264 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800265 network.view.alert('showAllLayers() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800266 }
267
268 function showPacketLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800269 showAllLayers();
270 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800271 network.view.alert('showPacketLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800272 }
273
274 function showOpticalLayer() {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800275 showAllLayers();
276 // TODO ...
Simon Hunt50128c02014-11-08 13:36:15 -0800277 network.view.alert('showOpticalLayer() callback');
Simon Hunt195cb382014-11-03 17:50:51 -0800278 }
279
Simon Hunt142d0032014-11-04 20:13:09 -0800280 // ==============================
Simon Hunt934c3ce2014-11-05 11:45:07 -0800281 // Private functions
282
Simon Hunt99c13842014-11-06 18:23:12 -0800283 function safeId(s) {
284 return s.replace(/[^a-z0-9]/gi, '-');
285 }
286
Simon Huntc7ee0662014-11-05 16:44:37 -0800287 // set the size of the given element to that of the view (reduced if padded)
288 function setSize(el, view, pad) {
289 var padding = pad ? pad * 2 : 0;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800290 el.attr({
Simon Huntc7ee0662014-11-05 16:44:37 -0800291 width: view.width() - padding,
292 height: view.height() - padding
Simon Hunt934c3ce2014-11-05 11:45:07 -0800293 });
294 }
295
Simon Hunt934c3ce2014-11-05 11:45:07 -0800296
Simon Hunt99c13842014-11-06 18:23:12 -0800297 // ==============================
298 // Event handlers for server-pushed events
299
300 var eventDispatch = {
301 addDevice: addDevice,
302 updateDevice: updateDevice,
303 removeDevice: removeDevice,
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800304 addLink: addLink,
305 showPath: showPath
Simon Hunt99c13842014-11-06 18:23:12 -0800306 };
307
308 function addDevice(data) {
309 var device = data.payload,
310 node = createDeviceNode(device);
311 note('addDevice', device.id);
312
313 network.nodes.push(node);
314 network.lookup[node.id] = node;
315 updateNodes();
316 network.force.start();
317 }
318
319 function updateDevice(data) {
320 var device = data.payload;
321 note('updateDevice', device.id);
322
323 }
324
325 function removeDevice(data) {
326 var device = data.payload;
327 note('removeDevice', device.id);
328
329 }
330
331 function addLink(data) {
332 var link = data.payload,
333 lnk = createLink(link);
334
335 if (lnk) {
336 note('addLink', lnk.id);
337
338 network.links.push(lnk);
339 updateLinks();
340 network.force.start();
341 }
342 }
343
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800344 function showPath(data) {
345 network.view.alert(data.event + "\n" + data.payload.links.length);
346 }
347
Simon Hunt99c13842014-11-06 18:23:12 -0800348 // ....
349
350 function unknownEvent(data) {
Simon Hunt50128c02014-11-08 13:36:15 -0800351 network.view.alert('Unknown event type: "' + data.event + '"');
Simon Hunt99c13842014-11-06 18:23:12 -0800352 }
353
354 function handleServerEvent(data) {
355 var fn = eventDispatch[data.event] || unknownEvent;
356 fn(data);
357 }
358
359 // ==============================
360 // force layout modification functions
361
362 function translate(x, y) {
363 return 'translate(' + x + ',' + y + ')';
364 }
365
366 function createLink(link) {
367 var type = link.type,
368 src = link.src,
369 dst = link.dst,
370 w = link.linkWidth,
371 srcNode = network.lookup[src],
372 dstNode = network.lookup[dst],
373 lnk;
374
375 if (!(srcNode && dstNode)) {
Simon Hunt50128c02014-11-08 13:36:15 -0800376 // TODO: send warning message back to server on websocket
377 network.view.alert('nodes not on map for link\n\n' +
378 'src = ' + src + '\ndst = ' + dst);
Simon Hunt99c13842014-11-06 18:23:12 -0800379 return null;
380 }
381
382 lnk = {
383 id: safeId(src) + '~' + safeId(dst),
384 source: srcNode,
385 target: dstNode,
386 class: 'link',
387 svgClass: type ? 'link ' + type : 'link',
388 x1: srcNode.x,
389 y1: srcNode.y,
390 x2: dstNode.x,
391 y2: dstNode.y,
392 width: w
393 };
394 return lnk;
395 }
396
Simon Hunt1a9eff92014-11-07 11:06:34 -0800397 function linkWidth(w) {
398 // w is number of links between nodes. Scale appropriately.
Simon Hunt50128c02014-11-08 13:36:15 -0800399 // TODO: use a d3.scale (linear, log, ... ?)
Simon Hunt1a9eff92014-11-07 11:06:34 -0800400 return w * 1.2;
401 }
402
Simon Hunt99c13842014-11-06 18:23:12 -0800403 function updateLinks() {
404 link = linkG.selectAll('.link')
405 .data(network.links, function (d) { return d.id; });
406
407 // operate on existing links, if necessary
408 // link .foo() .bar() ...
409
410 // operate on entering links:
411 var entering = link.enter()
412 .append('line')
413 .attr({
414 id: function (d) { return d.id; },
415 class: function (d) { return d.svgClass; },
416 x1: function (d) { return d.x1; },
417 y1: function (d) { return d.y1; },
418 x2: function (d) { return d.x2; },
419 y2: function (d) { return d.y2; },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800420 stroke: config.topo.linkInColor,
421 'stroke-width': config.topo.linkInWidth
Simon Hunt99c13842014-11-06 18:23:12 -0800422 })
423 .transition().duration(1000)
424 .attr({
Simon Hunt1a9eff92014-11-07 11:06:34 -0800425 'stroke-width': function (d) { return linkWidth(d.width); },
Simon Hunt99c13842014-11-06 18:23:12 -0800426 stroke: '#666' // TODO: remove explicit stroke, rather...
427 });
428
429 // augment links
430 // TODO: add src/dst port labels etc.
431
432 }
433
434 function createDeviceNode(device) {
435 // start with the object as is
436 var node = device,
437 type = device.type;
438
439 // Augment as needed...
440 node.class = 'device';
441 node.svgClass = type ? 'node device ' + type : 'node device';
442 positionNode(node);
443
444 // cache label array length
445 network.deviceLabelCount = device.labels.length;
446
447 return node;
448 }
449
450 function positionNode(node) {
451 var meta = node.metaUi,
452 x = 0,
453 y = 0;
454
455 if (meta) {
456 x = meta.x;
457 y = meta.y;
458 }
459 if (x && y) {
460 node.fixed = true;
461 }
462 node.x = x || network.view.width() / 2;
463 node.y = y || network.view.height() / 2;
464 }
465
466
467 function iconUrl(d) {
468 return 'img/' + d.type + '.png';
469 }
470
471 // returns the newly computed bounding box of the rectangle
472 function adjustRectToFitText(n) {
473 var text = n.select('text'),
474 box = text.node().getBBox(),
475 lab = config.labels;
476
477 text.attr('text-anchor', 'middle')
478 .attr('y', '-0.8em')
479 .attr('x', lab.imgPad/2);
480
481 // translate the bbox so that it is centered on [x,y]
482 box.x = -box.width / 2;
483 box.y = -box.height / 2;
484
485 // add padding
486 box.x -= (lab.padLR + lab.imgPad/2);
487 box.width += lab.padLR * 2 + lab.imgPad;
488 box.y -= lab.padTB;
489 box.height += lab.padTB * 2;
490
491 return box;
492 }
493
Simon Hunt1a9eff92014-11-07 11:06:34 -0800494 function mkSvgClass(d) {
495 return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
496 }
497
Simon Hunt99c13842014-11-06 18:23:12 -0800498 function updateNodes() {
499 node = nodeG.selectAll('.node')
500 .data(network.nodes, function (d) { return d.id; });
501
502 // operate on existing nodes, if necessary
503 //node .foo() .bar() ...
504
505 // operate on entering nodes:
506 var entering = node.enter()
507 .append('g')
508 .attr({
509 id: function (d) { return safeId(d.id); },
Simon Hunt1a9eff92014-11-07 11:06:34 -0800510 class: mkSvgClass,
Simon Hunt99c13842014-11-06 18:23:12 -0800511 transform: function (d) { return translate(d.x, d.y); },
512 opacity: 0
513 })
Simon Hunt1a9eff92014-11-07 11:06:34 -0800514 .call(network.drag)
Simon Hunt99c13842014-11-06 18:23:12 -0800515 //.on('mouseover', function (d) {})
516 //.on('mouseover', function (d) {})
517 .transition()
518 .attr('opacity', 1);
519
520 // augment device nodes...
521 entering.filter('.device').each(function (d) {
522 var node = d3.select(this),
523 icon = iconUrl(d),
524 idx = (labelIdx < d.labels.length) ? labelIdx : 0,
525 box;
526
527 node.append('rect')
528 .attr({
529 'rx': 5,
530 'ry': 5
531 });
532
533 node.append('text')
534 .text(d.labels[idx])
535 .attr('dy', '1.1em');
536
537 box = adjustRectToFitText(node);
538
539 node.select('rect')
540 .attr(box);
541
542 if (icon) {
543 var cfg = config.icons;
544 node.append('svg:image')
545 .attr({
546 x: box.x + config.icons.xoff,
547 y: box.y + config.icons.yoff,
548 width: cfg.w,
549 height: cfg.h,
550 'xlink:href': icon
551 });
552 }
553
554 // debug function to show the modelled x,y coordinates of nodes...
555 if (debug('showNodeXY')) {
556 node.select('rect').attr('fill-opacity', 0.5);
557 node.append('circle')
558 .attr({
559 class: 'debug',
560 cx: 0,
561 cy: 0,
562 r: '3px'
563 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800564 }
565 });
Simon Hunt934c3ce2014-11-05 11:45:07 -0800566
Simon Huntc7ee0662014-11-05 16:44:37 -0800567
Simon Hunt99c13842014-11-06 18:23:12 -0800568 // operate on both existing and new nodes, if necessary
569 //node .foo() .bar() ...
Simon Huntc7ee0662014-11-05 16:44:37 -0800570
Simon Hunt99c13842014-11-06 18:23:12 -0800571 // operate on exiting nodes:
572 // TODO: figure out how to remove the node 'g' AND its children
573 node.exit()
574 .transition()
575 .duration(750)
576 .attr({
577 opacity: 0,
578 cx: 0,
579 cy: 0,
580 r: 0
581 })
582 .remove();
Simon Huntc7ee0662014-11-05 16:44:37 -0800583 }
584
585
586 function tick() {
587 node.attr({
Simon Hunt99c13842014-11-06 18:23:12 -0800588 transform: function (d) { return translate(d.x, d.y); }
Simon Huntc7ee0662014-11-05 16:44:37 -0800589 });
590
591 link.attr({
592 x1: function (d) { return d.source.x; },
593 y1: function (d) { return d.source.y; },
594 x2: function (d) { return d.target.x; },
595 y2: function (d) { return d.target.y; }
596 });
597 }
Simon Hunt934c3ce2014-11-05 11:45:07 -0800598
599 // ==============================
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800600 // Web-Socket for live data
601
602 function webSockUrl() {
603 return document.location.toString()
604 .replace(/\#.*/, '')
605 .replace('http://', 'ws://')
606 .replace('https://', 'wss://')
607 .replace('index2.html', config.webSockUrl);
608 }
609
610 webSock = {
611 ws : null,
612
613 connect : function() {
614 webSock.ws = new WebSocket(webSockUrl());
615
616 webSock.ws.onopen = function() {
617 webSock._send("Hi there!");
618 };
619
620 webSock.ws.onmessage = function(m) {
621 if (m.data) {
622 console.log(m.data);
Thomas Vachuskad472c6e2014-11-07 19:11:05 -0800623 handleServerEvent(JSON.parse(m.data));
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800624 }
625 };
626
627 webSock.ws.onclose = function(m) {
628 webSock.ws = null;
629 };
630 },
631
632 send : function(text) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800633 if (text != null) {
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800634 webSock._send(text);
635 }
636 },
637
638 _send : function(message) {
639 if (webSock.ws) {
640 webSock.ws.send(message);
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800641 } else {
642 network.view.alert('no web socket open');
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800643 }
644 }
645
646 };
647
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800648 var sid = 0;
649
650 function sendMessage(evType, payload) {
651 var toSend = {
652 event: evType,
653 sid: ++sid,
654 payload: payload
655 };
656 webSock.send(JSON.stringify(toSend));
657 }
658
659
660 // ==============================
661 // Selection stuff
662
663 function selectObject(obj, el) {
664 var n,
665 meta = d3.event.sourceEvent.metaKey;
666
667 if (el) {
668 n = d3.select(el);
669 } else {
670 node.each(function(d) {
671 if (d == obj) {
672 n = d3.select(el = this);
673 }
674 });
675 }
676 if (!n) return;
677
678 if (meta && n.classed('selected')) {
679 deselectObject(obj.id);
680 //flyinPane(null);
681 return;
682 }
683
684 if (!meta) {
685 deselectAll();
686 }
687
688 // TODO: allow for mutli selections
689 var selected = {
690 obj : obj,
691 el : el
692 };
693
694 selections[obj.id] = selected;
695 selectOrder.push(obj.id);
696
697 n.classed('selected', true);
698 //flyinPane(obj);
699 }
700
701 function deselectObject(id) {
702 var obj = selections[id];
703 if (obj) {
704 d3.select(obj.el).classed('selected', false);
705 selections[id] = null;
706 // TODO: use splice to remove element
707 }
708 //flyinPane(null);
709 }
710
711 function deselectAll() {
712 // deselect all nodes in the network...
713 node.classed('selected', false);
714 selections = {};
715 selectOrder = [];
716 //flyinPane(null);
717 }
718
719
720 $('#view').on('click', function(e) {
721 if (!$(e.target).closest('.node').length) {
722 if (!e.metaKey) {
723 deselectAll();
724 }
725 }
726 });
727
Thomas Vachuska7d638d32014-11-07 10:24:43 -0800728 // ==============================
Simon Hunt142d0032014-11-04 20:13:09 -0800729 // View life-cycle callbacks
Simon Hunt195cb382014-11-03 17:50:51 -0800730
Simon Hunt142d0032014-11-04 20:13:09 -0800731 function preload(view, ctx) {
732 var w = view.width(),
733 h = view.height(),
734 idBg = view.uid('bg'),
Simon Huntc7ee0662014-11-05 16:44:37 -0800735 showBg = config.options.showBackground ? 'visible' : 'hidden',
736 fcfg = config.force,
737 fpad = fcfg.pad,
738 forceDim = [w - 2*fpad, h - 2*fpad];
Simon Hunt195cb382014-11-03 17:50:51 -0800739
Simon Hunt142d0032014-11-04 20:13:09 -0800740 // NOTE: view.$div is a D3 selection of the view's div
741 svg = view.$div.append('svg');
Simon Hunt934c3ce2014-11-05 11:45:07 -0800742 setSize(svg, view);
743
Simon Hunt1a9eff92014-11-07 11:06:34 -0800744 // add blue glow filter to svg layer
745 d3u.appendGlow(svg);
746
Simon Hunt142d0032014-11-04 20:13:09 -0800747 // load the background image
748 bgImg = svg.append('svg:image')
Simon Hunt195cb382014-11-03 17:50:51 -0800749 .attr({
Simon Hunt142d0032014-11-04 20:13:09 -0800750 id: idBg,
751 width: w,
752 height: h,
Simon Hunt195cb382014-11-03 17:50:51 -0800753 'xlink:href': config.backgroundUrl
754 })
Simon Hunt142d0032014-11-04 20:13:09 -0800755 .style({
756 visibility: showBg
Simon Hunt195cb382014-11-03 17:50:51 -0800757 });
Simon Huntc7ee0662014-11-05 16:44:37 -0800758
759 // group for the topology
760 topoG = svg.append('g')
761 .attr('transform', fcfg.translate());
762
763 // subgroups for links and nodes
764 linkG = topoG.append('g').attr('id', 'links');
765 nodeG = topoG.append('g').attr('id', 'nodes');
766
767 // selection of nodes and links
768 link = linkG.selectAll('.link');
769 node = nodeG.selectAll('.node');
770
Simon Hunt99c13842014-11-06 18:23:12 -0800771 function ldist(d) {
772 return fcfg.linkDistance[d.class] || 150;
773 }
774 function lstrg(d) {
775 return fcfg.linkStrength[d.class] || 1;
776 }
777 function lchrg(d) {
778 return fcfg.charge[d.class] || -200;
779 }
780
Simon Hunt1a9eff92014-11-07 11:06:34 -0800781 function selectCb(d, self) {
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800782 selectObject(d, self);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800783 }
784
785 function atDragEnd(d, self) {
786 // once we've finished moving, pin the node in position,
787 // if it is a device (not a host)
788 if (d.class === 'device') {
789 d.fixed = true;
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800790 d3.select(self).classed('fixed', true);
791 tellServerCoords(d);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800792 // TODO: send new [x,y] back to server, via websocket.
793 }
794 }
795
Thomas Vachuskad1be50d2014-11-08 16:10:20 -0800796 function tellServerCoords(d) {
797 sendMessage('updateMeta', {
798 id: d.id,
799 'class': d.class,
800 x: d.x,
801 y: d.y
802 });
803 }
804
Simon Huntc7ee0662014-11-05 16:44:37 -0800805 // set up the force layout
806 network.force = d3.layout.force()
807 .size(forceDim)
808 .nodes(network.nodes)
809 .links(network.links)
Simon Hunt99c13842014-11-06 18:23:12 -0800810 .charge(lchrg)
811 .linkDistance(ldist)
812 .linkStrength(lstrg)
Simon Huntc7ee0662014-11-05 16:44:37 -0800813 .on('tick', tick);
Simon Hunt195cb382014-11-03 17:50:51 -0800814
Simon Hunt1a9eff92014-11-07 11:06:34 -0800815 network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
816 }
Simon Hunt195cb382014-11-03 17:50:51 -0800817
Simon Hunt142d0032014-11-04 20:13:09 -0800818 function load(view, ctx) {
Simon Hunt99c13842014-11-06 18:23:12 -0800819 // cache the view token, so network topo functions can access it
820 network.view = view;
821
822 // set our radio buttons and key bindings
Simon Hunt934c3ce2014-11-05 11:45:07 -0800823 view.setRadio(btnSet);
824 view.setKeys(keyDispatch);
Simon Hunt195cb382014-11-03 17:50:51 -0800825
Simon Hunt50128c02014-11-08 13:36:15 -0800826 if (config.useLiveData) {
827 webSock.connect();
828 }
Simon Hunt195cb382014-11-03 17:50:51 -0800829 }
830
Simon Hunt142d0032014-11-04 20:13:09 -0800831 function resize(view, ctx) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800832 setSize(svg, view);
833 setSize(bgImg, view);
Simon Hunt99c13842014-11-06 18:23:12 -0800834
835 // TODO: hook to recompute layout, perhaps? work with zoom/pan code
836 // adjust force layout size
Simon Hunt142d0032014-11-04 20:13:09 -0800837 }
838
839
840 // ==============================
841 // View registration
Simon Hunt195cb382014-11-03 17:50:51 -0800842
Simon Hunt25248912014-11-04 11:25:48 -0800843 onos.ui.addView('topo', {
Simon Hunt142d0032014-11-04 20:13:09 -0800844 preload: preload,
845 load: load,
846 resize: resize
Simon Hunt195cb382014-11-03 17:50:51 -0800847 });
848
Simon Hunt195cb382014-11-03 17:50:51 -0800849}(ONOS));