<!DOCTYPE html>
<html>

  <head>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body>
    <h1>CDO</h1>
    <h2>VPN Hub & Peers</h2>
    <div id="vpn-device"></div>
    
    <h2>All Tunnels</h2>
    <button class="reset">Reset</button>
    <div id="vpn-all-devices"></div>    
  </body>

</html>
// Code goes here

var files = [
  'all-tunnels.json',
  'device-graph.json'
];

d3.json(files[0], function (data) {
  var graph = new Graph({
    element: document.querySelector('#vpn-all-devices'),
    strength: -50e0, // -8e1
    distance: 6e0,
    radius: 5e0,
    graph: data,
    draggableNodes: true,
    dragAndZoom: {scale: [.1, 2]}
  });
});

// d3.json(files[1], function (data) {
//   var graph = new Graph({
//     element: document.querySelector('#vpn-device'),
//     graph: data,
//     draggableNodes: true,
//     highlightSelectedPath: true,
//     dragAndZoom: false
//   });
// });

var Graph = function constructor(opts){
  var element = opts.element || {},
      margin = opts.margin || 20,
      width = opts.width || element.offsetWidth || 900,
      height = (opts.height || element.offsetHeight || 600) - 0.5 - margin,
      distance = opts.distance || 0.5e2,
      radius = opts.radius || 5e1,
      strength = opts.strength || -8e3,
      graph = opts.graph || {},
      onSelection = opts.onSelection || function(){};
      this.scale = (opts.dragAndZoom && opts.dragAndZoom.scale) || [0.1, 4];

  // SVG
  var svg = d3.select(element)
    .append('svg')
      .attr('width', width)
      .attr('height', height)
      
  var g =
    svg.append('g')
      .attr('class', 'graph-container');

  // Links
  var link = g.append('g')
    .attr('class', 'links')
    .selectAll('line')
    .data(graph.links)
    .enter()
    .append('line');

  // Nodes
  var node = g.append('g')
    .attr('class', 'nodes')
    .selectAll('g')
    .data(graph.nodes)
    .enter()
    .append('g');

  node.append('circle')
    .attr('r', radius)
    .classed('highlight', function(d) { return d.type === 'hub'; });

  // node.append('text')
  //   .text(function(d) { return d.id; });

  // ------------------
  // Force Simulation
  // ------------------
  var simulation = d3.forceSimulation()
    .force('link', d3.forceLink()
      .id(function(d) { return d.id; })
      .iterations(4))
      // .distance(distance))
    .force('charge', d3.forceManyBody()
      .strength(strength))
    .force('center', d3.forceCenter(width / 2, height / 2))
    .force("x", d3.forceX())
    .force("y", d3.forceY());

  simulation
    .nodes(graph.nodes)
    .on('tick', function ticked() {
      link
        .attr('x1', function(d) { return d.source.x; })
        .attr('y1', function(d) { return d.source.y; })
        .attr('x2', function(d) { return d.target.x; })
        .attr('y2', function(d) { return d.target.y; });
  
      node
        .attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
    });

  simulation.force('link')
    .links(graph.links);

  // ----- /Simulation
  
  this.svg = svg;
  this.g = g;
  this.nodes = node;
  this.links = link;
  this.data = graph;
  this.simulation = simulation;
  this.onSelection = onSelection;
  this.height = height;
  this.width = width;

  if(opts.draggableNodes){
    this.draggableNodes(); 
  }
  if(opts.highlightSelectedPath) {
    this.highlightSelectedPath(); 
  }
  
  if(opts.dragAndZoom){
    this.dragAndZoom();
  }
};

// ------------------
// Dragging nodes
// ------------------
Graph.prototype.draggableNodes = function(){
  var that = this;
  this.nodes.call(d3.drag()
    .on('start', dragstarted)
    .on('drag', dragged)
    .on('end', dragended));

  function dragstarted(d) {
    if (!d3.event.active) {
      that.simulation.alphaTarget(0.3).restart();
    }
    d.fx = d.x;
    d.fy = d.y;
  }

  function dragged(d) {
    d.fx = d3.event.x;
    d.fy = d3.event.y;
  }

  function dragended(d) {
    if (!d3.event.active) {
      that.simulation.alphaTarget(0);
    }
    d.fx = null;
    d.fy = null;
  }   
};

// ------------------
// Selecting Nodes
// ------------------
Graph.prototype.highlightSelectedPath = function () {
  var that = this;
  
  this.nodes
    .on('click', onClick)
    .on('mouseover', onMouseover)
    .on('mouseout', onMouseout);

  function onClick(d){
    if(d.type === 'hub'){
      return;
    }

    // clear spoke hightlight
    that.nodes.select('circle').classed('highlight', function(d) { return d.type === 'hub'; });
    that.links.classed('highlight', false);

    var connected = getConnectedNodes(d);
    connected.nodes.select('circle').classed('highlight', true).classed('hover', false);
    connected.links.classed('highlight', true).classed('hover', false);
    that.onSelection({data: d});
  }

  function onMouseover(){
    /* jshint validthis: true */
    var circle = d3.select(this).select('circle');
    if(circle.classed('highlight')) { return; }
    circle.classed('hover', true);
  }

  function onMouseout(){
    /* jshint validthis: true */
    var circle = d3.select(this).select('circle');
    circle.classed('hover', false);
  }

  // Utilities

  function getConnectedNodes(data){
    //Create an array logging what is connected to what
    var linkedByIndex = {};

    for (var i = 0; i < that.data.nodes.length; i++) {
      linkedByIndex[i + ',' + i] = true;
    }

    that.data.links.forEach(function (d) {
      linkedByIndex[d.source.index + ',' + d.target.index] = true;
      linkedByIndex[d.target.index + ',' + d.source.index] = true;
    });

    function neighboring(node1, node2){
      return linkedByIndex[node1.index + ',' + node2.index];
    }

    function isConnected(link, data){
      return link.source.id === data.id || data.id === link.target.id;
    }

    return {
      nodes: that.nodes.filter(function(n) { return neighboring(n, data); }),
      links: that.links.filter(function(l) { return isConnected(l, data); })
    };
  }  
};

// ------------------
// Zoom
// ------------------
Graph.prototype.dragAndZoom = function(){
  var that = this;
  
  var zoom = d3.zoom()
    .scaleExtent(that.scale)
    // .scaleExtent([0.5, 10])
    // .translateExtent([[0, -800], [600, 800]])
    .on("zoom", function zoomed() {
      console.log(d3.event.transform) 
      // console.log(d3.event.scale, d3.event.translate) // v3
      that.g.attr("transform", d3.event.transform);
    });
  
  that.svg.call(zoom);
    
  // reset button
  d3.select(".reset").on("click", function resetted() {
    that.svg.transition()
      .duration(750)
      .call(zoom.transform, d3.zoomIdentity);
  });    
};

// TODO: remove text when zoom out and add text as zoom in
// TODO: constrained zoom: https://bl.ocks.org/mbostock/5e81cc677d186b6845cb00676758a339
  
.highlight {
  stroke: #049FD9 !important;
}

.hover {
  stroke: #D4F6FF !important;
}

.links line {
  stroke-width: 1;
  stroke: white;
  stroke: #049FD9;
}

.nodes circle {
  stroke-width: 2;
  stroke: white;
  stroke: #049FD9;
  fill: white;
}

text {
  font: 8px sans-serif;
  fill: #444;
  text-anchor: middle;
  alignment-baseline: central;
}



/* CDO */
body { background-color: #E8EBF1; }

svg {
  border: 1px solid black;
}
{
  "nodes": [
    {"id": "AMS-5512X"},
    {"id": "DEN-ASA5515X"},
    {"id": "FRA-ASA5510"},
    {"id": "HYD-ASA5512X"},
    {"id": "I3VPN-ASA5585X1"},
    {"id": "62.23.180.18"},
    {"id": "168.215.121.226"},
    {"id": "80.113.16.226"},
    {"id": "174.47.105.14"},
    {"id": "82.118.170.178"},
    {"id": "62.134.198.134"},
    {"id": "203.121.0.210"},
    {"id": "217.33.37.194"},
    {"id": "202.212.180.81"},
    {"id": "191.234.32.148"},
    {"id": "138.91.116.26"},
    {"id": "191.234.49.5"},
    {"id": "193.235.50.6"},
    {"id": "Device 1"},
    {"id": "Device 2"},
    {"id": "Device 3"},
    {"id": "Device 4"},
    {"id": "Device 5"},
    {"id": "Device 6"},
    {"id": "Device 7"},
    {"id": "Device 8"},
    {"id": "Device 9"},
    {"id": "Device 10"},
    {"id": "Device A"},
    {"id": "Device B"},
    {"id": "Device C"},
    {"id": "Device D"},
    {"id": "Device E"},
    {"id": "Device F"},
    {"id": "Device G"},
    {"id": "Device H"},
    {"id": "Device I"},
    {"id": "Device J"},
    {"id": "Device K"}
  ],
  "links": [
    {"source": "AMS-5512X", "target":"62.23.180.18"},
    {"source": "AMS-5512X", "target":"168.215.121.226"},
    {"source": "AMS-5512X", "target":"80.113.16.226"},
    
    {"source": "DEN-ASA5515X", "target":"174.47.105.14"},
    
    {"source": "FRA-ASA5510", "target":"62.23.180.18"},
    {"source": "FRA-ASA5510", "target":"82.118.170.178"},
    {"source": "FRA-ASA5510", "target":"62.134.198.134"},
    
    {"source": "HYD-ASA5512X", "target":"174.47.105.14"},
    {"source": "HYD-ASA5512X", "target":"203.121.0.210"},
    {"source": "HYD-ASA5512X", "target":"217.33.37.194"},
    {"source": "HYD-ASA5512X", "target":"202.212.180.81"},
    
    {"source": "I3VPN-ASA5585X1", "target":"62.23.180.18"},
    {"source": "I3VPN-ASA5585X1", "target":"168.215.121.226"},
    {"source": "I3VPN-ASA5585X1", "target":"191.234.32.148"},
    {"source": "I3VPN-ASA5585X1", "target":"138.91.116.26"},
    {"source": "I3VPN-ASA5585X1", "target":"191.234.49.5"},
    {"source": "I3VPN-ASA5585X1", "target":"193.235.50.6"},
    
    {"source": "AMS-5512X", "target":"HYD-ASA5512X"},
    
    {"source": "193.235.50.6", "target": "Device 1"},
    {"source": "193.235.50.6", "target": "Device 2"},
    {"source": "193.235.50.6", "target": "Device 3"},
    
    {"source": "Device A", "target": "Device B"}
    ,{"source": "Device A", "target": "Device C"}
    ,{"source": "Device A", "target": "Device D"}
    ,{"source": "Device A", "target": "Device E"}
    ,{"source": "Device A", "target": "Device F"}
    ,{"source": "Device A", "target": "Device G"}
    ,{"source": "Device A", "target": "Device H"}
  ]
}
{
  "nodes": [
    {"id": "ACME", "type": "hub"},
    {"id": "Device 1", "type": "spoke"},
    {"id": "Device 2", "type": "spoke"},
    {"id": "Device 3", "type": "spoke"},
    {"id": "Device 4", "type": "spoke"},
    {"id": "Device 5", "type": "spoke"},
    {"id": "Device 6", "type": "spoke"}
  ],
  "links": [
    {"source": "ACME", "target": "Device 1"},
    {"source": "ACME", "target": "Device 2"},
    {"source": "ACME", "target": "Device 3"},
    {"source": "ACME", "target": "Device 4"},
    {"source": "ACME", "target": "Device 5"},
    {"source": "ACME", "target": "Device 6"}
  ]
}