[CORD-1043] Fine-grained service graph first draft

Change-Id: I16566b0c38dda64fa920120ce16ea699ca157279
diff --git a/src/app/service-graph/components/fine-grained/fine-grained.component.ts b/src/app/service-graph/components/fine-grained/fine-grained.component.ts
new file mode 100644
index 0000000..b0f71a6
--- /dev/null
+++ b/src/app/service-graph/components/fine-grained/fine-grained.component.ts
@@ -0,0 +1,270 @@
+import {IXosServiceGraphStore} from '../../services/graph.store';
+import './fine-grained.component.scss';
+import * as d3 from 'd3';
+import * as $ from 'jquery';
+import {Subscription} from 'rxjs';
+import {XosServiceGraphConfig as config} from '../../graph.config';
+import {IXosDebouncer} from '../../../core/services/helpers/debounce.helper';
+import {IXosServiceGraph, IXosServiceGraphLink, IXosServiceGraphNode} from '../../interfaces';
+
+class XosFineGrainedTenancyGraphCtrl {
+  static $inject = [
+    '$log',
+    'XosServiceGraphStore',
+    'XosDebouncer'
+  ];
+
+  public graph: IXosServiceGraph;
+
+  private GraphSubscription: Subscription;
+  private svg;
+  private forceLayout;
+  private linkGroup;
+  private nodeGroup;
+
+  // debounced functions
+  private renderGraph;
+
+  constructor(
+    private $log: ng.ILogService,
+    private XosServiceGraphStore: IXosServiceGraphStore,
+    private XosDebouncer: IXosDebouncer
+  ) {
+    this.handleSvg();
+    this.setupForceLayout();
+    this.renderGraph = this.XosDebouncer.debounce(this._renderGraph, 500, this);
+
+    $(window).on('resize', () => {
+      this.setupForceLayout();
+      this.renderGraph();
+    });
+
+    this.GraphSubscription = this.XosServiceGraphStore.get()
+      .subscribe(
+        (graph) => {
+
+          if (!graph.nodes || graph.nodes.length === 0 || !graph.links || graph.links.length === 0) {
+            return;
+          }
+
+          this.$log.debug(`[XosFineGrainedTenancyGraphCtrl] Coarse Event and render`, graph);
+          this.graph = graph;
+          this.renderGraph();
+        },
+        (err) => {
+          this.$log.error(`[XosFineGrainedTenancyGraphCtrl] Error: `, err);
+        }
+      );
+  }
+
+  $onDestroy() {
+    this.GraphSubscription.unsubscribe();
+  }
+
+  private _renderGraph() {
+    this.addNodeLinksToForceLayout(this.graph);
+    this.renderNodes(this.graph.nodes);
+    this.renderLinks(this.graph.links);
+  }
+
+  private getSvgDimensions(): {width: number, heigth: number} {
+    return {
+      width: $('xos-fine-grained-tenancy-graph svg').width(),
+      heigth: $('xos-fine-grained-tenancy-graph svg').height()
+    };
+  }
+
+  private handleSvg() {
+    this.svg = d3.select('svg');
+
+    this.linkGroup = this.svg.append('g')
+      .attr({
+        class: 'link-group'
+      });
+
+    this.nodeGroup = this.svg.append('g')
+      .attr({
+        class: 'node-group'
+      });
+  }
+
+  private setupForceLayout() {
+
+    const tick = () => {
+      this.nodeGroup.selectAll('g.node')
+        .attr({
+          transform: d => `translate(${d.x}, ${d.y})`
+        });
+
+      this.linkGroup.selectAll('line')
+        .attr({
+          x1: l => l.source.x || 0,
+          y1: l => l.source.y || 0,
+          x2: l => l.target.x || 0,
+          y2: l => l.target.y || 0,
+        });
+    };
+    const svgDim = this.getSvgDimensions();
+    this.forceLayout = d3.layout.force()
+      .size([svgDim.width, svgDim.heigth])
+      .linkDistance(config.force.linkDistance)
+      .charge(config.force.charge)
+      .gravity(config.force.gravity)
+      .on('tick', tick);
+  }
+
+  private addNodeLinksToForceLayout(data: IXosServiceGraph) {
+    this.forceLayout
+      .nodes(data.nodes)
+      .links(data.links)
+      .start();
+  }
+
+  private getSiblingTextBBox(contex: any /* D3 this */) {
+    return d3.select(contex.parentNode).select('text').node().getBBox();
+  }
+
+  private renderServiceNodes(nodes: any) {
+
+    const self = this;
+    nodes.append('rect')
+    .attr({
+      rx: config.node.radius,
+      ry: config.node.radius
+    });
+
+    nodes.append('text')
+      .attr({
+        'text-anchor': 'middle'
+      })
+      .text(n => n.label);
+    // .text(n => `${n.id} - ${n.label}`);
+
+    const existing = nodes.selectAll('rect');
+
+    // resize node > rect as contained text
+    existing.each(function() {
+      const textBBox = self.getSiblingTextBBox(this);
+      const rect = d3.select(this);
+      rect.attr({
+        width: textBBox.width + config.node.padding,
+        height: textBBox.height + config.node.padding,
+        x: textBBox.x - (config.node.padding / 2),
+        y: textBBox.y - (config.node.padding / 2)
+      });
+    });
+  }
+
+  private renderTenantNodes(nodes: any) {
+    nodes.append('rect')
+      .attr({
+        width: 40,
+        height: 40,
+        x: -25,
+        y: -25,
+        transform: `rotate(45)`
+      });
+
+    nodes.append('text')
+      .attr({
+        'text-anchor': 'middle'
+      })
+      .text(n => n.label);
+  }
+
+  private renderNetworkNodes(nodes: any) {
+    const self = this;
+    nodes.append('circle');
+
+    nodes.append('text')
+      .attr({
+        'text-anchor': 'middle'
+      })
+      .text(n => n.label);
+
+    const existing = nodes.selectAll('circle');
+
+    // resize node > rect as contained text
+    existing.each(function() {
+      const textBBox = self.getSiblingTextBBox(this);
+      const rect = d3.select(this);
+      rect.attr({
+        r: (textBBox.width / 2) + config.node.padding,
+        cy: - (textBBox.height / 4)
+      });
+    });
+  }
+
+  private renderSubscriberNodes(nodes: any) {
+    const self = this;
+    nodes.append('rect');
+
+    nodes.append('text')
+      .attr({
+        'text-anchor': 'middle'
+      })
+      .text(n => n.label);
+
+    const existing = nodes.selectAll('rect');
+
+    // resize node > rect as contained text
+    existing.each(function() {
+      const textBBox = self.getSiblingTextBBox(this);
+      const rect = d3.select(this);
+      rect.attr({
+        width: textBBox.width + config.node.padding,
+        height: textBBox.height + config.node.padding,
+        x: textBBox.x - (config.node.padding / 2),
+        y: textBBox.y - (config.node.padding / 2)
+      });
+    });
+  }
+
+  private renderNodes(nodes: IXosServiceGraphNode[]) {
+    const node = this.nodeGroup
+      .selectAll('g.node')
+      .data(nodes, n => n.id);
+
+    const svgDim = this.getSvgDimensions();
+    const hStep = svgDim.width / (nodes.length - 1);
+    const vStep = svgDim.heigth / (nodes.length - 1);
+    const entering = node.enter()
+      .append('g')
+      .attr({
+        class: n => `node ${n.type}`,
+        transform: (n, i) => `translate(${hStep * i}, ${vStep * i})`
+      })
+      .call(this.forceLayout.drag)
+      .on('mousedown', () => {
+        d3.event.stopPropagation();
+      })
+      .on('mouseup', (d) => {
+        d.fixed = true;
+      });
+
+    this.renderServiceNodes(entering.filter('.service'));
+    this.renderTenantNodes(entering.filter('.tenant'));
+    this.renderNetworkNodes(entering.filter('.network'));
+    this.renderSubscriberNodes(entering.filter('.subscriber'));
+  }
+
+  private renderLinks(links: IXosServiceGraphLink[]) {
+    const link = this.linkGroup
+      .selectAll('line')
+      .data(links, l => l.id);
+
+    const entering = link.enter();
+
+    entering.append('line')
+      .attr({
+        class: 'link',
+        'marker-start': 'url(#arrow)'
+      });
+  }
+}
+
+export const XosFineGrainedTenancyGraph: angular.IComponentOptions = {
+  template: require('./fine-grained.component.html'),
+  controllerAs: 'vm',
+  controller: XosFineGrainedTenancyGraphCtrl,
+};