Drawing slices, expanding and collapsing slices
Drawing forms
Added keybindings

Change-Id: Ic1f06eef20a6e1e7c0f1fea51752fe738f86d600
diff --git a/views/ngXosViews/mcord-slicing/src/js/slicing-topo.directive.js b/views/ngXosViews/mcord-slicing/src/js/slicing-topo.directive.js
new file mode 100644
index 0000000..b91c8f4
--- /dev/null
+++ b/views/ngXosViews/mcord-slicing/src/js/slicing-topo.directive.js
@@ -0,0 +1,502 @@
+(function () {
+  'use strict';
+
+  angular.module('xos.mcord-slicing')
+  .directive('slicingTopo', function(){
+    return {
+      restrict: 'E',
+      scope: {},
+      bindToController: true,
+      controllerAs: 'vm',
+      templateUrl: 'templates/slicing-topo.tpl.html',
+      controller: function($element, SliceGraph, McordSlicingTopo, _, NodePositioner, FormHandler){
+
+        let svg;
+        let nodes, links;
+        let nodeGroup, linkGroup, formGroup;
+        let dragLine, dragStartNode, dragEndNode, selectedLink;
+        let selectedNode, nodeSiblings;
+
+        var t = d3.transition()
+          .duration(500);
+
+        this.activeSlices = [];
+
+        const resetDragInfo = () => {
+          // reset drag nodes
+          dragStartNode = null;
+          dragEndNode = null;
+
+          // hide dragLine
+          dragLine
+            .classed('hidden', true);
+        };
+
+        McordSlicingTopo.query().$promise
+        .then((topology) => {
+          NodePositioner.storeEl($element[0]);
+          handleSvg($element[0]);
+          SliceGraph.buildGraph(topology);
+          _nodes = SliceGraph.positionGraph($element[0]);
+          _links = SliceGraph.getGraphLinks(_nodes);
+          drawGraph();
+        })
+        .catch((e) => {
+          throw new Error(e);
+        });
+
+        const handleSvg = (el) => {
+          this.el = el;
+          d3.select(el).select('svg').remove();
+
+          svg = d3.select(el)
+          .append('svg')
+          .style('width', `${el.clientWidth}px`)
+          .style('height', `${el.clientHeight}px`);
+
+          linkGroup = svg.append('g')
+            .attr({
+              class: 'link-group'
+            });
+
+          nodeGroup = svg.append('g')
+            .attr({
+              class: 'node-group'
+            });
+
+          formGroup = d3.select(el)
+            .append('div')
+            .attr({
+              class: 'form-container'
+            });
+
+          // line displayed when dragging nodes
+          dragLine = svg.append('svg:path')
+            .attr('class', 'dragline hidden')
+            .attr('d', 'M0,0L0,0');
+        };
+
+        const tick = () => {
+          svg.selectAll('.link')
+            .attr('x1', d => d.source.x)
+            .attr('y1', d => d.source.y)
+            .attr('x2', d => d.target.x)
+            .attr('y2', d => d.target.y);
+        };
+
+        // prepare the data to show all slices
+        let _nodes = [];
+        let _links = [];
+
+        // attach slice details
+        const attachSliceDetails = n => {
+          let [newNodes, newLinks] = SliceGraph.getSliceDetail(n);
+          _nodes = _nodes.concat(newNodes);
+          _links = _links.concat(newLinks);
+          drawGraph();
+        };
+
+        // remove slice detail
+        const removeSliceDetails = sliceId => {
+
+          SliceGraph.removeActiveSlice(sliceId);
+
+          // if the selected node is part of the slice I'm closing
+          // deselect the node
+          if(selectedNode && selectedNode.sliceId === sliceId){
+            selectedNode = null;
+            nodeSiblings = null;
+          }
+
+          // remove control plane nodes related to this slice
+          _nodes = _.filter(_nodes, n => {
+            if(n.sliceId === sliceId && (n.plane === 'control' || n.type === 'button')){
+              // if I remove the node check that there is no form attached
+              FormHandler.removeFormByParentNode(n, linkGroup, formGroup);
+              return false;
+            }
+            return true;
+          });
+
+          // remove sliceId from data plane element
+          _nodes = _.map(_nodes, n => {
+            if(n.sliceId === sliceId){
+              delete n.sliceId;
+            }
+            return n;
+          });
+
+          // remove control plane links related to this slice
+          _links = _.filter(_links, l => {
+            if(_.findIndex(_nodes, {id: l.data.source}) === -1){
+              return false;
+            }
+            if(_.findIndex(_nodes, {id: l.data.target}) === -1){
+              return false;
+            }
+            return true;
+          });
+          drawGraph();
+        };
+
+        const deleteLink = linkId => {
+          // TODO
+          // [ ] delete from graphlib
+          // [ ] delete from backend
+          console.log(_links);
+          _.remove(_links, (l) => {
+            return l.data.id === linkId;
+          });
+          console.log(_links);
+          drawGraph();
+        };
+
+        const expandNode = (n) => {
+          console.log('exp', n);
+          resetDragInfo();
+          const sliceComponents = ['ran-ru', 'ran-cu', 'pgw', 'sgw'];
+          if(sliceComponents.indexOf(n.type) > -1 && n.plane === 'data' && !n.sliceId){
+            attachSliceDetails(n);
+          }
+          else if (n.type === 'button'){
+            removeSliceDetails(n.sliceId);
+          }
+          else if (!n.formAttached && n.model){
+            n.formAttached = true;
+            FormHandler.drawForm(n, linkGroup, formGroup);
+          }
+          else if (n.formAttached){
+            n.formAttached = false;
+            FormHandler.removeFormByParentNode(n, linkGroup, formGroup);
+          }
+        };
+
+        const selectNextNode = () => {
+          if(!selectedNode){
+            selectedNode = _nodes[0];
+            selectedNode.selected = true;
+          }
+          else {
+            // TODO if no sliceId check only data plane successors
+            nodeSiblings = SliceGraph.getNodeSuccessors(selectedNode);
+
+            if(nodeSiblings.length === 0){
+              return;
+            };
+            // reset current selected node
+            selectedNode.selected = false;
+            // find next node
+            let nextNodeId = _.findIndex(_nodes, {id: nodeSiblings[0].id});
+            selectedNode = _nodes[nextNodeId];
+            selectedNode.selected = true;
+
+            // NOTE I need to update sibling with successor of the parent
+            // to enable vertical navigation
+            let parents = SliceGraph.getNodeSuccessors(selectedNode);
+            if(parents.lenght > 0){
+              nodeSiblings = SliceGraph.getNodePredecessors(parents[0]);
+            }
+            else {
+              nodeSiblings = null;
+            }
+          }
+          drawGraph();
+        };
+
+        const selectPrevNode = () => {
+          if(!selectedNode){
+            selectedNode = _nodes[0];
+            selectedNode.selected = true;
+          }
+          else {
+            nodeSiblings = SliceGraph.getNodePredecessors(selectedNode);
+
+            if(nodeSiblings.length === 0){
+              return;
+            };
+            // reset current selected node
+            selectedNode.selected = false;
+            // find next node
+            let prev = _.findIndex(_nodes, {id: nodeSiblings[0].id});
+
+            if(prev < 0){
+              prev = _nodes.length - 1;
+            }
+            selectedNode = _nodes[prev];
+            selectedNode.selected = true;
+          }
+          drawGraph();
+        };
+
+        const sortByY = (a, b) => {
+          if (a.y < b.y)
+            return 1;
+          if (a.y > b.y)
+            return -1;
+          return 0;
+        };
+
+        const getSameTypeNodes = (selectedNode) => {
+          return _.filter(_nodes, n => {
+            if(selectedNode.type === 'pgw' && n.type === 'button'){
+              return true;
+            }
+            if(selectedNode.type === 'button' && n.type === 'pgw'){
+              return true;
+            }
+            if (selectedNode.type === 'sgw' && n.type === 'mme'){
+              return true;
+            }
+            if (selectedNode.type === 'mme' && n.type === 'sgw'){
+              return true;
+            }
+            return n.type === selectedNode.type;
+          }).sort(sortByY);
+        };
+
+        const selectNextSibling = () => {
+          if(!selectedNode){
+            selectedNode = _nodes[0];
+            selectedNode.selected = true;
+          }
+          else {
+            // reset current selected node
+            selectedNode.selected = false;
+
+            // find next node
+            let sameTypeNodes = getSameTypeNodes(selectedNode);
+
+            let nextSiblingId = _.findIndex(sameTypeNodes, {id: selectedNode.id}) + 1;
+            if(nextSiblingId === sameTypeNodes.length){
+              nextSiblingId = 0;
+            }
+            let nextNodeId = _.findIndex(_nodes, {id: sameTypeNodes[nextSiblingId].id});
+            selectedNode = _nodes[nextNodeId];
+            selectedNode.selected = true;
+          }
+          drawGraph();
+        };
+
+        const selectPrevSibling = () => {
+          if(!selectedNode){
+            selectedNode = _nodes[0];
+            selectedNode.selected = true;
+          }
+          else {
+            // reset current selected node
+            selectedNode.selected = false;
+
+            // find next node
+            let sameTypeNodes = getSameTypeNodes(selectedNode);
+
+            let nextSiblingId = _.findIndex(sameTypeNodes, {id: selectedNode.id}) - 1;
+            if(nextSiblingId < 0){
+              nextSiblingId = sameTypeNodes.length - 1;
+            }
+            let nextNodeId = _.findIndex(_nodes, {id: sameTypeNodes[nextSiblingId].id});
+            selectedNode = _nodes[nextNodeId];
+            selectedNode.selected = true;
+          }
+          drawGraph();
+        };
+
+        const drawGraph = () => {
+
+          // svg.selectAll('.node-container').remove();
+          // svg.selectAll('.link-container').remove();
+
+          var force = d3.layout.force()
+            .nodes(_nodes)
+            .links(_links)
+            .charge(-1060)
+            .gravity(0.1)
+            .linkDistance(200)
+            .size([this.el.clientWidth, this.el.clientHeight])
+            .on('tick', tick)
+            .start();
+
+          links = linkGroup.selectAll('.link-container')
+            .data(_links, d => d.data.id)
+            .enter()
+            .insert('g')
+            .attr({
+              class: 'link-container',
+              opacity: 0
+            });
+
+          links
+            .transition(t)
+            .attr({
+              opacity: 1
+            });
+
+          links.insert('line')
+            .attr('class', d => `link ${d.data.plane}`)
+            .on('click', function(link ){
+              selectedLink = link;
+
+              // deselect all other links
+              d3.selectAll('.link').classed('selected', false);
+
+              d3.select(this).classed('selected', true);
+            });
+
+          nodes = nodeGroup.selectAll('.node')
+            .data(_nodes, d => d.id)
+            .attr({
+              class: d => `node ${d.plane} ${d.type} ${d.selected ? 'selected': ''}`,
+            });
+
+          nodes
+            .enter()
+            .append('g')
+            .attr({
+              class: 'node-container',
+              transform: d => d.transform,
+              opacity: 0
+            });
+
+          nodes.transition(t)
+            .attr({
+              opacity: 1
+            });
+
+          nodes.append('rect')
+            .attr({
+              class: d => `node ${d.plane} ${d.type} ${d.selected ? 'selected': ''}`,
+              width: 100,
+              height: 50,
+              x: -50,
+              y: -25
+            });
+
+          nodes.append('text')
+            .attr({
+              'text-anchor': 'middle',
+              'alignment-baseline': 'middle'
+            })
+            // .text(d => `${d.id} ${d.name}`);
+            .text(d => `${d.name}`);
+
+          nodes.on('click', (n) => {
+            expandNode(n);
+          });
+
+          nodes
+            .on('mousedown', (n) => {
+              // save a reference to dragStart
+              dragStartNode = n;
+
+              dragLine
+                .classed('hidden', false)
+                .attr('d', 'M' + dragStartNode.x + ',' + dragStartNode.y + 'L' + dragStartNode.x + ',' + dragStartNode.y);
+            })
+            .on('mouseover', (n) => {
+              if(dragStartNode){
+                dragEndNode = n;
+              }
+            });
+
+          svg
+            .on('mousemove', function(){
+              if(!dragStartNode){
+                return;
+              }
+              dragLine.attr('d', 'M' + dragStartNode.x + ',' + dragStartNode.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]);
+            })
+            .on('mouseup', () => {
+              if(!dragStartNode || !dragEndNode){
+                resetDragInfo();
+                return;
+              }
+
+              // TODO
+              // [X] check that I can connect the two nodes
+              // [X] check link direction
+              // [ ] save the new link in the BE
+
+              // check that I can connect the 2 nodes
+              const successorType = SliceGraph.getNodeDataPlaneSuccessors(dragStartNode)[0].type;
+              if(dragEndNode.type !== successorType){
+                resetDragInfo();
+                return;
+              }
+
+              // create the link
+              _links.push({
+                source: dragStartNode,
+                target: dragEndNode,
+                data: {
+                  id: `${dragStartNode.id}.${dragEndNode.id}`,
+                  source: dragStartNode.id,
+                  target: dragEndNode.id
+                }
+              });
+
+              // update the graph
+              // TODO recalculate graph positions
+              drawGraph();
+
+              resetDragInfo();
+            });
+
+          // remove exiting nodes
+          svg.selectAll('.node-container')
+            .data(_nodes, d => d.id)
+            .exit()
+            .transition(t)
+            .attr({
+              opacity: 0
+            })
+            .remove();
+
+          // remove exiting links
+          svg.selectAll('.link-container')
+            .data(_links, d => d.data.id)
+            .exit()
+            .transition(t)
+            .attr({
+              opacity: 0
+            })
+            .remove();
+        };
+
+        d3.select('body')
+          .on('keydown', function(){
+            // console.log(d3.event.code);
+            if(d3.event.code === 'Backspace' && selectedLink){
+              // delete link
+              deleteLink(selectedLink.data.id);
+            }
+            if(d3.event.code === 'Enter' && selectedNode){
+              d3.event.preventDefault();
+              expandNode(selectedNode);
+            }
+            if(d3.event.code === 'Escape' && selectedNode){
+              selectedNode.selected = false;
+              selectedNode = null;
+              nodeSiblings = null;
+              drawGraph();
+            }
+            if(d3.event.code === 'ArrowRight'){
+              d3.event.preventDefault();
+              selectNextNode();
+            }
+            if(d3.event.code === 'ArrowLeft'){
+              d3.event.preventDefault();
+              selectPrevNode();
+            }
+            if(d3.event.code === 'ArrowUp'){
+              d3.event.preventDefault();
+              selectNextSibling();
+            }
+            if(d3.event.code === 'ArrowDown'){
+              d3.event.preventDefault();
+              selectPrevSibling();
+            }
+
+          });
+      }
+    }
+  });
+})();
\ No newline at end of file