blob: 458679610c83632e5ee21a8fb630844eea2e5cae [file] [log] [blame]
Matteo Scandolo6bc31bf2016-08-29 10:17:31 -07001(function () {
2 'use strict';
3
4 angular.module('xos.mcord-slicing')
5 .directive('slicingTopo', function(){
6 return {
7 restrict: 'E',
8 scope: {},
9 bindToController: true,
10 controllerAs: 'vm',
11 templateUrl: 'templates/slicing-topo.tpl.html',
Matteo Scandolo4d121c22016-10-31 13:20:31 +010012 controller: function($element, SliceGraph, McordSlicingTopo, _, NodePositioner, FormHandler, mCordSlicingIcons){
Matteo Scandolo6bc31bf2016-08-29 10:17:31 -070013
14 let svg;
15 let nodes, links;
16 let nodeGroup, linkGroup, formGroup;
17 let dragLine, dragStartNode, dragEndNode, selectedLink;
18 let selectedNode, nodeSiblings;
19
20 var t = d3.transition()
21 .duration(500);
22
23 this.activeSlices = [];
24
25 const resetDragInfo = () => {
26 // reset drag nodes
27 dragStartNode = null;
28 dragEndNode = null;
29
30 // hide dragLine
31 dragLine
32 .classed('hidden', true);
33 };
34
35 McordSlicingTopo.query().$promise
36 .then((topology) => {
37 NodePositioner.storeEl($element[0]);
38 handleSvg($element[0]);
39 SliceGraph.buildGraph(topology);
40 _nodes = SliceGraph.positionGraph($element[0]);
41 _links = SliceGraph.getGraphLinks(_nodes);
42 drawGraph();
43 })
44 .catch((e) => {
45 throw new Error(e);
46 });
47
48 const handleSvg = (el) => {
49 this.el = el;
50 d3.select(el).select('svg').remove();
51
52 svg = d3.select(el)
53 .append('svg')
54 .style('width', `${el.clientWidth}px`)
55 .style('height', `${el.clientHeight}px`);
56
57 linkGroup = svg.append('g')
58 .attr({
59 class: 'link-group'
60 });
61
62 nodeGroup = svg.append('g')
63 .attr({
64 class: 'node-group'
65 });
66
67 formGroup = d3.select(el)
68 .append('div')
69 .attr({
70 class: 'form-container'
71 });
72
73 // line displayed when dragging nodes
74 dragLine = svg.append('svg:path')
75 .attr('class', 'dragline hidden')
76 .attr('d', 'M0,0L0,0');
77 };
78
79 const tick = () => {
Matteo Scandolo4d121c22016-10-31 13:20:31 +010080
81 // svg.selectAll('.node')
82 // .attr({
83 // y: (n) => {
84 // console.log(n.y);
85 // return n.y;
86 // }
87 // });
88
Matteo Scandolo6bc31bf2016-08-29 10:17:31 -070089 svg.selectAll('.link')
90 .attr('x1', d => d.source.x)
91 .attr('y1', d => d.source.y)
92 .attr('x2', d => d.target.x)
93 .attr('y2', d => d.target.y);
94 };
95
96 // prepare the data to show all slices
97 let _nodes = [];
98 let _links = [];
99
100 // attach slice details
101 const attachSliceDetails = n => {
102 let [newNodes, newLinks] = SliceGraph.getSliceDetail(n);
103 _nodes = _nodes.concat(newNodes);
104 _links = _links.concat(newLinks);
105 drawGraph();
106 };
107
108 // remove slice detail
109 const removeSliceDetails = sliceId => {
110
111 SliceGraph.removeActiveSlice(sliceId);
112
113 // if the selected node is part of the slice I'm closing
114 // deselect the node
115 if(selectedNode && selectedNode.sliceId === sliceId){
116 selectedNode = null;
117 nodeSiblings = null;
118 }
119
120 // remove control plane nodes related to this slice
121 _nodes = _.filter(_nodes, n => {
122 if(n.sliceId === sliceId && (n.plane === 'control' || n.type === 'button')){
123 // if I remove the node check that there is no form attached
124 FormHandler.removeFormByParentNode(n, linkGroup, formGroup);
125 return false;
126 }
127 return true;
128 });
129
130 // remove sliceId from data plane element
131 _nodes = _.map(_nodes, n => {
132 if(n.sliceId === sliceId){
133 delete n.sliceId;
134 }
135 return n;
136 });
137
138 // remove control plane links related to this slice
139 _links = _.filter(_links, l => {
140 if(_.findIndex(_nodes, {id: l.data.source}) === -1){
141 return false;
142 }
143 if(_.findIndex(_nodes, {id: l.data.target}) === -1){
144 return false;
145 }
146 return true;
147 });
148 drawGraph();
149 };
150
151 const deleteLink = linkId => {
152 // TODO
153 // [ ] delete from graphlib
154 // [ ] delete from backend
155 console.log(_links);
156 _.remove(_links, (l) => {
157 return l.data.id === linkId;
158 });
159 console.log(_links);
160 drawGraph();
161 };
162
163 const expandNode = (n) => {
164 console.log('exp', n);
165 resetDragInfo();
166 const sliceComponents = ['ran-ru', 'ran-cu', 'pgw', 'sgw'];
167 if(sliceComponents.indexOf(n.type) > -1 && n.plane === 'data' && !n.sliceId){
168 attachSliceDetails(n);
169 }
170 else if (n.type === 'button'){
171 removeSliceDetails(n.sliceId);
172 }
173 else if (!n.formAttached && n.model){
174 n.formAttached = true;
175 FormHandler.drawForm(n, linkGroup, formGroup);
176 }
177 else if (n.formAttached){
178 n.formAttached = false;
179 FormHandler.removeFormByParentNode(n, linkGroup, formGroup);
180 }
181 };
182
183 const selectNextNode = () => {
184 if(!selectedNode){
185 selectedNode = _nodes[0];
186 selectedNode.selected = true;
187 }
188 else {
189 // TODO if no sliceId check only data plane successors
190 nodeSiblings = SliceGraph.getNodeSuccessors(selectedNode);
191
192 if(nodeSiblings.length === 0){
193 return;
194 };
195 // reset current selected node
196 selectedNode.selected = false;
197 // find next node
198 let nextNodeId = _.findIndex(_nodes, {id: nodeSiblings[0].id});
199 selectedNode = _nodes[nextNodeId];
200 selectedNode.selected = true;
201
202 // NOTE I need to update sibling with successor of the parent
203 // to enable vertical navigation
204 let parents = SliceGraph.getNodeSuccessors(selectedNode);
205 if(parents.lenght > 0){
206 nodeSiblings = SliceGraph.getNodePredecessors(parents[0]);
207 }
208 else {
209 nodeSiblings = null;
210 }
211 }
212 drawGraph();
213 };
214
215 const selectPrevNode = () => {
216 if(!selectedNode){
217 selectedNode = _nodes[0];
218 selectedNode.selected = true;
219 }
220 else {
221 nodeSiblings = SliceGraph.getNodePredecessors(selectedNode);
222
223 if(nodeSiblings.length === 0){
224 return;
225 };
226 // reset current selected node
227 selectedNode.selected = false;
228 // find next node
229 let prev = _.findIndex(_nodes, {id: nodeSiblings[0].id});
230
231 if(prev < 0){
232 prev = _nodes.length - 1;
233 }
234 selectedNode = _nodes[prev];
235 selectedNode.selected = true;
236 }
237 drawGraph();
238 };
239
240 const sortByY = (a, b) => {
241 if (a.y < b.y)
242 return 1;
243 if (a.y > b.y)
244 return -1;
245 return 0;
246 };
247
248 const getSameTypeNodes = (selectedNode) => {
249 return _.filter(_nodes, n => {
250 if(selectedNode.type === 'pgw' && n.type === 'button'){
251 return true;
252 }
253 if(selectedNode.type === 'button' && n.type === 'pgw'){
254 return true;
255 }
256 if (selectedNode.type === 'sgw' && n.type === 'mme'){
257 return true;
258 }
259 if (selectedNode.type === 'mme' && n.type === 'sgw'){
260 return true;
261 }
262 return n.type === selectedNode.type;
263 }).sort(sortByY);
264 };
265
266 const selectNextSibling = () => {
267 if(!selectedNode){
268 selectedNode = _nodes[0];
269 selectedNode.selected = true;
270 }
271 else {
272 // reset current selected node
273 selectedNode.selected = false;
274
275 // find next node
276 let sameTypeNodes = getSameTypeNodes(selectedNode);
277
278 let nextSiblingId = _.findIndex(sameTypeNodes, {id: selectedNode.id}) + 1;
279 if(nextSiblingId === sameTypeNodes.length){
280 nextSiblingId = 0;
281 }
282 let nextNodeId = _.findIndex(_nodes, {id: sameTypeNodes[nextSiblingId].id});
283 selectedNode = _nodes[nextNodeId];
284 selectedNode.selected = true;
285 }
286 drawGraph();
287 };
288
289 const selectPrevSibling = () => {
290 if(!selectedNode){
291 selectedNode = _nodes[0];
292 selectedNode.selected = true;
293 }
294 else {
295 // reset current selected node
296 selectedNode.selected = false;
297
298 // find next node
299 let sameTypeNodes = getSameTypeNodes(selectedNode);
300
301 let nextSiblingId = _.findIndex(sameTypeNodes, {id: selectedNode.id}) - 1;
302 if(nextSiblingId < 0){
303 nextSiblingId = sameTypeNodes.length - 1;
304 }
305 let nextNodeId = _.findIndex(_nodes, {id: sameTypeNodes[nextSiblingId].id});
306 selectedNode = _nodes[nextNodeId];
307 selectedNode.selected = true;
308 }
309 drawGraph();
310 };
311
312 const drawGraph = () => {
313
314 // svg.selectAll('.node-container').remove();
315 // svg.selectAll('.link-container').remove();
316
317 var force = d3.layout.force()
318 .nodes(_nodes)
319 .links(_links)
320 .charge(-1060)
321 .gravity(0.1)
322 .linkDistance(200)
323 .size([this.el.clientWidth, this.el.clientHeight])
324 .on('tick', tick)
325 .start();
326
327 links = linkGroup.selectAll('.link-container')
328 .data(_links, d => d.data.id)
329 .enter()
330 .insert('g')
331 .attr({
332 class: 'link-container',
333 opacity: 0
334 });
335
336 links
337 .transition(t)
338 .attr({
339 opacity: 1
340 });
341
342 links.insert('line')
343 .attr('class', d => `link ${d.data.plane}`)
344 .on('click', function(link ){
345 selectedLink = link;
346
347 // deselect all other links
348 d3.selectAll('.link').classed('selected', false);
349
350 d3.select(this).classed('selected', true);
351 });
352
353 nodes = nodeGroup.selectAll('.node')
354 .data(_nodes, d => d.id)
355 .attr({
356 class: d => `node ${d.plane} ${d.type} ${d.selected ? 'selected': ''}`,
357 });
358
359 nodes
360 .enter()
361 .append('g')
362 .attr({
363 class: 'node-container',
364 transform: d => d.transform,
365 opacity: 0
366 });
367
368 nodes.transition(t)
369 .attr({
370 opacity: 1
371 });
372
373 nodes.append('rect')
374 .attr({
375 class: d => `node ${d.plane} ${d.type} ${d.selected ? 'selected': ''}`,
376 width: 100,
377 height: 50,
378 x: -50,
379 y: -25
380 });
381
382 nodes.append('text')
383 .attr({
Matteo Scandolo4d121c22016-10-31 13:20:31 +0100384 'text-anchor': 'left',
385 'alignment-baseline': 'middle',
386 x: -20
Matteo Scandolo6bc31bf2016-08-29 10:17:31 -0700387 })
Matteo Scandolo6bc31bf2016-08-29 10:17:31 -0700388 .text(d => `${d.name}`);
389
390 nodes.on('click', (n) => {
391 expandNode(n);
392 });
393
Matteo Scandolo4d121c22016-10-31 13:20:31 +0100394 // draw icons
395 const ues = nodes.filter(n => n.type === 'ue');
396 ues.append('path')
397 .attr({
398 d: mCordSlicingIcons.mobile,
399 class: 'icon',
400 transform: `translate(-40, -12.5), scale(0.5)`
401 });
402
403 const profiles = nodes.filter(n => n.type === 'profile');
404 profiles.append('path')
405 .attr({
406 d: mCordSlicingIcons.profile,
407 class: 'icon',
408 transform: `translate(-40, -12.5), scale(0.5)`
409 });
410
411 const rru = nodes.filter(n => n.type === 'ran-ru');
412 rru.append('path')
413 .attr({
414 d: mCordSlicingIcons.rru,
415 class: 'icon',
416 transform: `translate(-40, -12.5), scale(0.5)`
417 });
418
419 const rcu = nodes.filter(n => n.type === 'ran-cu');
420 rcu.append('path')
421 .attr({
422 d: mCordSlicingIcons.rcu,
423 class: 'icon',
424 transform: `translate(-40, -12.5), scale(0.5)`
425 });
426
427 const sgw = nodes.filter(n => n.type === 'sgw');
428 sgw.append('path')
429 .attr({
430 d: mCordSlicingIcons.sgw,
431 class: 'icon',
432 transform: `translate(-40, -12.5), scale(0.5)`
433 });
434
435 const pgw = nodes.filter(n => n.type === 'pgw');
436 pgw.append('path')
437 .attr({
438 d: mCordSlicingIcons.pgw,
439 class: 'icon',
440 transform: `translate(-40, -12.5), scale(0.5)`
441 });
442
443 const mme = nodes.filter(n => n.type === 'mme');
444 mme.append('path')
445 .attr({
446 d: mCordSlicingIcons.mme,
447 class: 'icon',
448 transform: `translate(-40, -12.5), scale(0.5)`
449 });
450
Matteo Scandolo6bc31bf2016-08-29 10:17:31 -0700451 nodes
452 .on('mousedown', (n) => {
453 // save a reference to dragStart
454 dragStartNode = n;
455
456 dragLine
457 .classed('hidden', false)
458 .attr('d', 'M' + dragStartNode.x + ',' + dragStartNode.y + 'L' + dragStartNode.x + ',' + dragStartNode.y);
459 })
460 .on('mouseover', (n) => {
461 if(dragStartNode){
462 dragEndNode = n;
463 }
464 });
465
466 svg
467 .on('mousemove', function(){
468 if(!dragStartNode){
469 return;
470 }
471 dragLine.attr('d', 'M' + dragStartNode.x + ',' + dragStartNode.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]);
472 })
473 .on('mouseup', () => {
474 if(!dragStartNode || !dragEndNode){
475 resetDragInfo();
476 return;
477 }
478
479 // TODO
480 // [X] check that I can connect the two nodes
481 // [X] check link direction
482 // [ ] save the new link in the BE
483
484 // check that I can connect the 2 nodes
485 const successorType = SliceGraph.getNodeDataPlaneSuccessors(dragStartNode)[0].type;
486 if(dragEndNode.type !== successorType){
487 resetDragInfo();
488 return;
489 }
490
491 // create the link
492 _links.push({
493 source: dragStartNode,
494 target: dragEndNode,
495 data: {
496 id: `${dragStartNode.id}.${dragEndNode.id}`,
497 source: dragStartNode.id,
498 target: dragEndNode.id
499 }
500 });
501
502 // update the graph
503 // TODO recalculate graph positions
504 drawGraph();
505
506 resetDragInfo();
507 });
508
509 // remove exiting nodes
510 svg.selectAll('.node-container')
511 .data(_nodes, d => d.id)
512 .exit()
513 .transition(t)
514 .attr({
515 opacity: 0
516 })
517 .remove();
518
519 // remove exiting links
520 svg.selectAll('.link-container')
521 .data(_links, d => d.data.id)
522 .exit()
523 .transition(t)
524 .attr({
525 opacity: 0
526 })
527 .remove();
528 };
529
530 d3.select('body')
531 .on('keydown', function(){
532 // console.log(d3.event.code);
533 if(d3.event.code === 'Backspace' && selectedLink){
534 // delete link
535 deleteLink(selectedLink.data.id);
536 }
537 if(d3.event.code === 'Enter' && selectedNode){
538 d3.event.preventDefault();
539 expandNode(selectedNode);
540 }
541 if(d3.event.code === 'Escape' && selectedNode){
542 selectedNode.selected = false;
543 selectedNode = null;
544 nodeSiblings = null;
545 drawGraph();
546 }
547 if(d3.event.code === 'ArrowRight'){
548 d3.event.preventDefault();
549 selectNextNode();
550 }
551 if(d3.event.code === 'ArrowLeft'){
552 d3.event.preventDefault();
553 selectPrevNode();
554 }
555 if(d3.event.code === 'ArrowUp'){
556 d3.event.preventDefault();
557 selectNextSibling();
558 }
559 if(d3.event.code === 'ArrowDown'){
560 d3.event.preventDefault();
561 selectPrevSibling();
562 }
563
564 });
565 }
566 }
567 });
568})();