blob: b7a911206695a4328ac966d7ccd42e33c34ea5dc [file] [log] [blame]
Paul Greyson7a300822013-04-09 12:57:49 -07001/***************************************************************************************************
2functions for creating and interacting with the topology view of the webui
3
4flow related topology is in flows.js
5***************************************************************************************************/
6
7function createLinkMap(links) {
8 var linkMap = {};
9 links.forEach(function (link) {
10 var srcDPID = link['src-switch'];
11 var dstDPID = link['dst-switch'];
12
13 var srcMap = linkMap[srcDPID] || {};
14
15 srcMap[dstDPID] = link;
16
17 linkMap[srcDPID] = srcMap;
18 });
19 return linkMap;
20}
21
22
23// removes links from the pending list that are now in the model
24function reconcilePendingLinks(model) {
25 var links = [];
26 model.links.forEach(function (link) {
27 links.push(link);
28 delete pendingLinks[makeLinkKey(link)]
29 })
30 var linkId;
31 for (linkId in pendingLinks) {
32 links.push(pendingLinks[linkId]);
33 }
34 return links
35}
36
37
38function createTopologyView() {
39
40 window.addEventListener('resize', function () {
41 // this is too slow. instead detect first resize event and hide the paths that have explicit matrix applied
42 // either that or is it possible to position the paths so they get the automatic transform as well?
43// updateTopology();
44 });
45
46 var svg = d3.select('#svg-container').append('svg:svg');
47
48 svg.append("svg:defs").append("svg:marker")
49 .attr("id", "arrow")
50 .attr("viewBox", "0 -5 10 10")
51 .attr("refX", -1)
52 .attr("markerWidth", 5)
53 .attr("markerHeight", 5)
54 .attr("orient", "auto")
55 .append("svg:path")
56 .attr("d", "M0,-3L10,0L0,3");
57
58 return svg.append('svg:svg').attr('id', 'viewBox').attr('viewBox', '0 0 1000 1000').attr('preserveAspectRatio', 'none').
59 attr('id', 'viewbox').append('svg:g').attr('transform', 'translate(500 500)');
60}
61
62// d3.xml("assets/map.svg", "image/svg+xml", function(xml) {
63// var importedNode = document.importNode(xml.documentElement, true);
64// var paths = importedNode.querySelectorAll('path');
65// var i;
66// for (i=0; i < paths.length; i+=1) {
67// svg.append('svg:path')
68// .attr('class', 'state')
69// .attr('d', d3.select(paths.item(i)).attr('d'))
70// .attr('transform', 'translate(-500 -500)scale(1 1.7)')
71// }
72// });
73
74
75var widths = {
76 edge: 6,
77 aggregation: 12,
78 core: 18
79}
80
81function createRingsFromModel(model) {
82 var rings = [{
83 radius: 3,
84 width: widths.edge,
85 switches: model.edgeSwitches,
86 className: 'edge',
87 angles: []
88 }, {
89 radius: 2.25,
90 width: widths.aggregation,
91 switches: model.aggregationSwitches,
92 className: 'aggregation',
93 angles: []
94 }, {
95 radius: 0.75,
96 width: widths.core,
97 switches: model.coreSwitches,
98 className: 'core',
99 angles: []
100 }];
101
102
103 var aggRanges = {};
104
105 // arrange edge switches at equal increments
106 var k = 360 / rings[0].switches.length;
107 rings[0].switches.forEach(function (s, i) {
108 var angle = k * i;
109
110 rings[0].angles[i] = angle;
111
112 // record the angle for the agg switch layout
113 var dpid = s.dpid.split(':');
114 dpid[7] = '01'; // the last component of the agg switch is always '01'
115 var aggdpid = dpid.join(':');
116 var aggRange = aggRanges[aggdpid];
117 if (!aggRange) {
118 aggRange = aggRanges[aggdpid] = {};
119 aggRange.min = aggRange.max = angle;
120 } else {
121 aggRange.max = angle;
122 }
123 });
124
125 // arrange aggregation switches to "fan out" to edge switches
126 k = 360 / rings[1].switches.length;
127 rings[1].switches.forEach(function (s, i) {
128// rings[1].angles[i] = k * i;
129 var range = aggRanges[s.dpid];
130
131 rings[1].angles[i] = (range.min + range.max)/2;
132 });
133
134 // find the association between core switches and aggregation switches
135 var aggregationSwitchMap = {};
136 model.aggregationSwitches.forEach(function (s, i) {
137 aggregationSwitchMap[s.dpid] = i;
138 });
139
140 // put core switches next to linked aggregation switches
141 k = 360 / rings[2].switches.length;
142 rings[2].switches.forEach(function (s, i) {
143// rings[2].angles[i] = k * i;
144 var associatedAggregationSwitches = model.configuration.association[s.dpid];
145 // TODO: go between if there are multiple
146 var index = aggregationSwitchMap[associatedAggregationSwitches[0]];
147
148 rings[2].angles[i] = rings[1].angles[index];
149 });
150
151 // TODO: construct this form initially rather than converting. it works better because
152 // it allows binding by dpid
153 var testRings = [];
154 rings.forEach(function (ring) {
155 var testRing = [];
156 ring.switches.forEach(function (s, i) {
157 var testSwitch = {
158 dpid: s.dpid,
159 state: s.state,
160 radius: ring.radius,
161 width: ring.width,
162 className: ring.className,
163 angle: ring.angles[i],
164 controller: s.controller
165 };
166 testRing.push(testSwitch);
167 });
168
169
170 testRings.push(testRing);
171 });
172
173
174// return rings;
175 return testRings;
176}
177
178updateTopology = function() {
179
180 // DRAW THE SWITCHES
181 var rings = svg.selectAll('.ring').data(createRingsFromModel(model));
182
183 var links = reconcilePendingLinks(model);
184 var linkMap = createLinkMap(links);
185
186 function mouseOverSwitch(data) {
187
188 d3.event.preventDefault();
189
190 d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', false);
191
192 if (data.highlighted) {
193 return;
194 }
195
196 // only highlight valid link or flow destination by checking for class of existing highlighted circle
197 var highlighted = svg.selectAll('.highlight')[0];
198 if (highlighted.length == 1) {
199 var s = d3.select(highlighted[0]).select('circle');
200 // only allow links
201 // edge->edge (flow)
202 // aggregation->core
203 // core->core
204 if (data.className == 'edge' && !s.classed('edge') ||
205 data.className == 'core' && !s.classed('core') && !s.classed('aggregation') ||
206 data.className == 'aggregation' && !s.classed('core')) {
207 return;
208 }
209
210 // don't highlight if there's already a link or flow
211 // var map = linkMap[data.dpid];
212 // console.log(map);
213 // console.log(s.data()[0].dpid);
214 // console.log(map[s.data()[0].dpid]);
215 // if (map && map[s.data()[0].dpid]) {
216 // return;
217 // }
218
219 // the second highlighted switch is the target for a link or flow
220 data.target = true;
221 }
222
223 var node = d3.select(document.getElementById(data.dpid));
224 node.classed('highlight', true).select('circle').transition().duration(100).attr("r", widths.core);
225 data.highlighted = true;
226 node.moveToFront();
227 }
228
229 function mouseOutSwitch(data) {
230 d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', true);
231
232 if (data.mouseDown)
233 return;
234
235 var node = d3.select(document.getElementById(data.dpid));
236 node.classed('highlight', false).select('circle').transition().duration(100).attr("r", widths[data.className]);
237 data.highlighted = false;
238 data.target = false;
239 }
240
241 function mouseDownSwitch(data) {
242 mouseOverSwitch(data);
243 data.mouseDown = true;
244 d3.select('#topology').classed('linking', true);
245
246 d3.select('svg')
247 .append('svg:path')
248 .attr('id', 'linkVector')
249 .attr('d', function () {
250 var s = d3.select(document.getElementById(data.dpid));
251
252 var pt = document.querySelector('svg').createSVGPoint();
253 pt.x = s.attr('x');
254 pt.y = s.attr('y');
255 pt = pt.matrixTransform(s[0][0].getCTM());
256
257 return line([pt, pt]);
258 });
259
260
261 if (data.className === 'core') {
262 d3.selectAll('.edge').classed('nodrop', true);
263 }
264 if (data.className === 'edge') {
265 d3.selectAll('.core').classed('nodrop', true);
266 d3.selectAll('.aggregation').classed('nodrop', true);
267 }
268 if (data.className === 'aggregation') {
269 d3.selectAll('.edge').classed('nodrop', true);
270 d3.selectAll('.aggregation').classed('nodrop', true);
271 }
272 }
273
274 function mouseUpSwitch(data) {
275 if (data.mouseDown) {
276 data.mouseDown = false;
277 d3.select('#topology').classed('linking', false);
278 d3.event.stopPropagation();
279 d3.selectAll('.nodrop').classed('nodrop', false);
280 }
281 }
282
283 function doubleClickSwitch(data) {
284 var circle = d3.select(document.getElementById(data.dpid)).select('circle');
285 if (data.state == 'ACTIVE') {
286 var prompt = 'Deactivate ' + data.dpid + '?';
287 if (confirm(prompt)) {
288 switchDown(data);
289 setPending(circle);
290 }
291 } else {
292 var prompt = 'Activate ' + data.dpid + '?';
293 if (confirm(prompt)) {
294 switchUp(data);
295 setPending(circle);
296 }
297 }
298 }
299
300 function ringEnter(data, i) {
301 if (!data.length) {
302 return;
303 }
304
305 // create the nodes
306 var nodes = d3.select(this).selectAll("g")
307 .data(data, function (data) {
308 return data.dpid;
309 })
310 .enter().append("svg:g")
311 .attr("id", function (data, i) {
312 return data.dpid;
313 })
314 .attr("transform", function(data, i) {
315 return "rotate(" + data.angle+ ")translate(" + data.radius * 150 + ")rotate(" + (-data.angle) + ")";
316 });
317
318 // add the cirles representing the switches
319 nodes.append("svg:circle")
320 .attr("transform", function(data, i) {
321 var m = document.querySelector('#viewbox').getTransformToElement().inverse();
322 if (data.scale) {
323 m = m.scale(data.scale);
324 }
325 return "matrix( " + m.a + " " + m.b + " " + m.c + " " + m.d + " " + m.e + " " + m.f + " )";
326 })
327 .attr("x", function (data) {
328 return -data.width / 2;
329 })
330 .attr("y", function (data) {
331 return -data.width / 2;
332 })
333 .attr("r", function (data) {
334 return data.width;
335 });
336
337 // setup the mouseover behaviors
338 nodes.on('mouseover', mouseOverSwitch);
339 nodes.on('mouseout', mouseOutSwitch);
340 nodes.on('mouseup', mouseUpSwitch);
341 nodes.on('mousedown', mouseDownSwitch);
342
343 // only do switch up/down for core switches
344 if (i == 2) {
345 nodes.on('dblclick', doubleClickSwitch);
346 }
347 }
348
349 // append switches
350 rings.enter().append("svg:g")
351 .attr("class", "ring")
352 .each(ringEnter);
353
354
355 function ringUpdate(data, i) {
356 var nodes = d3.select(this).selectAll("g")
357 .data(data, function (data) {
358 return data.dpid;
359 });
360 nodes.select('circle')
361 .each(function (data) {
362 // if there's a pending state changed and then the state changes, clear the pending class
363 var circle = d3.select(this);
364 if (data.state === 'ACTIVE' && circle.classed('inactive') ||
365 data.state === 'INACTIVE' && circle.classed('active')) {
366 circle.classed('pending', false);
367 }
368 })
369 .attr('class', function (data) {
370 if (data.state === 'ACTIVE' && data.controller) {
371 return data.className + ' active ' + controllerColorMap[data.controller];
372 } else {
373 return data.className + ' inactive ' + 'colorInactive';
374 }
375 });
376 }
377
378 // update switches
379 rings.each(ringUpdate);
380
381
382 // Now setup the labels
383 // This is done separately because SVG draws in node order and we want the labels
384 // always on top
385 var labelRings = svg.selectAll('.labelRing').data(createRingsFromModel(model));
386
387 d3.select(document.body).on('mousemove', function () {
388 if (!d3.select('#topology').classed('linking')) {
389 return;
390 }
391 var linkVector = document.getElementById('linkVector');
392 if (!linkVector) {
393 return;
394 }
395 linkVector = d3.select(linkVector);
396
397 var highlighted = svg.selectAll('.highlight')[0];
398 var s1 = null, s2 = null;
399 if (highlighted.length > 1) {
400 var s1 = d3.select(highlighted[0]);
401 var s2 = d3.select(highlighted[1]);
402
403 } else if (highlighted.length > 0) {
404 var s1 = d3.select(highlighted[0]);
405 }
406 var src = s1;
407 if (s2 && !s2.data()[0].target) {
408 src = s2;
409 }
410 if (src) {
411 linkVector.attr('d', function () {
412 var srcPt = document.querySelector('svg').createSVGPoint();
413 srcPt.x = src.attr('x');
414 srcPt.y = src.attr('y');
415 srcPt = srcPt.matrixTransform(src[0][0].getCTM());
416
417 var svg = document.getElementById('topology');
418 var mouse = d3.mouse(viewbox);
419 var dstPt = document.querySelector('svg').createSVGPoint();
420 dstPt.x = mouse[0];
421 dstPt.y = mouse[1];
422 dstPt = dstPt.matrixTransform(viewbox.getCTM());
423
424 return line([srcPt, dstPt]);
425 });
426 }
427 });
428
429 d3.select(document.body).on('mouseup', function () {
430 function clearHighlight() {
431 svg.selectAll('circle').each(function (data) {
432 data.mouseDown = false;
433 d3.select('#topology').classed('linking', false);
434 mouseOutSwitch(data);
435 });
436 d3.select('#linkVector').remove();
437 };
438
439 d3.selectAll('.nodrop').classed('nodrop', false);
440
441 function removeLink(link) {
442 var path1 = document.getElementById(link['src-switch'] + '=>' + link['dst-switch']);
443 var path2 = document.getElementById(link['dst-switch'] + '=>' + link['src-switch']);
444
445 if (path1) {
446 setPending(d3.select(path1));
447 }
448 if (path2) {
449 setPending(d3.select(path2));
450 }
451
452 linkDown(link);
453 }
454
455
456 var highlighted = svg.selectAll('.highlight')[0];
457 if (highlighted.length == 2) {
458 var s1Data = highlighted[0].__data__;
459 var s2Data = highlighted[1].__data__;
460
461 var srcData, dstData;
462 if (s1Data.target) {
463 dstData = s1Data;
464 srcData = s2Data;
465 } else {
466 dstData = s2Data;
467 srcData = s1Data;
468 }
469
470 if (s1Data.className == 'edge' && s2Data.className == 'edge') {
471 var prompt = 'Create flow from ' + srcData.dpid + ' to ' + dstData.dpid + '?';
472 if (confirm(prompt)) {
473 addFlow(srcData, dstData);
474
475 var flow = {
476 dataPath: {
477 srcPort: {
478 dpid: {
479 value: srcData.dpid
480 }
481 },
482 dstPort: {
483 dpid: {
484 value: dstData.dpid
485 }
486 }
487 },
488 srcDpid: srcData.dpid,
489 dstDpid: dstData.dpid,
490 createPending: true
491 };
492
493 selectFlow(flow);
494
495 setTimeout(function () {
496 deselectFlowIfCreatePending(flow);
497 }, pendingTimeout);
498 }
499 } else {
500 var map = linkMap[srcData.dpid];
501 if (map && map[dstData.dpid]) {
502 var prompt = 'Remove link between ' + srcData.dpid + ' and ' + dstData.dpid + '?';
503 if (confirm(prompt)) {
504 removeLink(map[dstData.dpid]);
505 }
506 } else {
507 map = linkMap[dstData.dpid];
508 if (map && map[srcData.dpid]) {
509 var prompt = 'Remove link between ' + dstData.dpid + ' and ' + srcData.dpid + '?';
510 if (confirm(prompt)) {
511 removeLink(map[srcData.dpid]);
512 }
513 } else {
514 var prompt = 'Create link between ' + srcData.dpid + ' and ' + dstData.dpid + '?';
515 if (confirm(prompt)) {
516 var link1 = {
517 'src-switch': srcData.dpid,
518 'src-port': 1,
519 'dst-switch': dstData.dpid,
520 'dst-port': 1,
521 pending: true
522 };
523 pendingLinks[makeLinkKey(link1)] = link1;
524 var link2 = {
525 'src-switch': dstData.dpid,
526 'src-port': 1,
527 'dst-switch': srcData.dpid,
528 'dst-port': 1,
529 pending: true
530 };
531 pendingLinks[makeLinkKey(link2)] = link2;
532 updateTopology();
533
534 linkUp(link1);
535
536 // remove the pending links after 10s
537 setTimeout(function () {
538 delete pendingLinks[makeLinkKey(link1)];
539 delete pendingLinks[makeLinkKey(link2)];
540
541 updateTopology();
542 }, pendingTimeout);
543 }
544 }
545 }
546 }
547
548 clearHighlight();
549 } else {
550 clearHighlight();
551 }
552
553 });
554
555 function labelRingEnter(data) {
556 if (!data.length) {
557 return;
558 }
559
560 // create the nodes
561 var nodes = d3.select(this).selectAll("g")
562 .data(data, function (data) {
563 return data.dpid;
564 })
565 .enter().append("svg:g")
566 .classed('nolabel', true)
567 .attr("id", function (data) {
568 return data.dpid + '-label';
569 })
570 .attr("transform", function(data, i) {
571 return "rotate(" + data.angle+ ")translate(" + data.radius * 150 + ")rotate(" + (-data.angle) + ")";
572 })
573
574 // add the text nodes which show on mouse over
575 nodes.append("svg:text")
576 .text(function (data) {return data.dpid;})
577 .attr("x", function (data) {
578 if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
579 if (data.className == 'edge') {
580 return - data.width*3 - 4;
581 } else {
582 return - data.width - 4;
583 }
584 } else {
585 if (data.className == 'edge') {
586 return data.width*3 + 4;
587 } else {
588 return data.width + 4;
589 }
590 }
591 })
592 .attr("y", function (data) {
593 var y;
594 if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
595 if (data.className == 'edge') {
596 y = data.width*3/2 + 4;
597 } else {
598 y = data.width/2 + 4;
599 }
600 } else {
601 if (data.className == 'edge') {
602 y = data.width*3/2 + 4;
603 } else {
604 y = data.width/2 + 4;
605 }
606 }
607 return y - 6;
608 })
609 .attr("text-anchor", function (data) {
610 if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
611 return "end";
612 } else {
613 return "start";
614 }
615 })
616 .attr("transform", function(data) {
617 var m = document.querySelector('#viewbox').getTransformToElement().inverse();
618 if (data.scale) {
619 m = m.scale(data.scale);
620 }
621 return "matrix( " + m.a + " " + m.b + " " + m.c + " " + m.d + " " + m.e + " " + m.f + " )";
622 })
623 }
624
625 labelRings.enter().append("svg:g")
626 .attr("class", "textRing")
627 .each(labelRingEnter);
628
629 // switches should not change during operation of the ui so no
630 // rings.exit()
631
632
633 // DRAW THE LINKS
634
635 // key on link dpids since these will come/go during demo
636 var links = d3.select('svg').selectAll('.link').data(links, function (d) {
637 return d['src-switch']+'->'+d['dst-switch'];
638 });
639
640 // add new links
641 links.enter().append("svg:path")
642 .attr("class", "link");
643
644 links.attr('id', function (d) {
645 return makeLinkKey(d);
646 })
647 .attr("d", function (d) {
648 var src = d3.select(document.getElementById(d['src-switch']));
649 var dst = d3.select(document.getElementById(d['dst-switch']));
650
651 var srcPt = document.querySelector('svg').createSVGPoint();
652 srcPt.x = src.attr('x');
653 srcPt.y = src.attr('y');
654 srcPt = srcPt.matrixTransform(src[0][0].getCTM());
655
656 var dstPt = document.querySelector('svg').createSVGPoint();
657 dstPt.x = dst.attr('x');
658 dstPt.y = dst.attr('y');
659 dstPt = dstPt.matrixTransform(dst[0][0].getCTM());
660
661 var midPt = document.querySelector('svg').createSVGPoint();
662 midPt.x = (srcPt.x + dstPt.x)/2;
663 midPt.y = (srcPt.y + dstPt.y)/2;
664
665 return line([srcPt, midPt, dstPt]);
666 })
667 .attr("marker-mid", function(d) { return "url(#arrow)"; })
668 .classed('pending', function (d) {
669 return d.pending;
670 });
671
672
673 // remove old links
674 links.exit().remove();
675}