text: /**
    @author David Piegza
  
    Implements a sphere graph drawing with force-directed placement.
    
    It uses the force-directed-layout implemented in:
    https://github.com/davidpiegza/Graph-Visualization/blob/master/layouts/force-directed-layout.js
    
    Drawing is done with Three.js: http://github.com/mrdoob/three.js
  
    To use this drawing, include the graph-min.js file and create a SphereGraph object:
    
    <!DOCTYPE html>
    <html>
      <head>
        <title>Graph Visualization</title>
        <script type="text/javascript" src="path/to/graph-min.js"></script>
      </head>
      <body onload="new Drawing.SphereGraph({showStats: true, showInfo: true})">
      </bod>
    </html>
    
    Parameters:
    options = {
      layout: "2d" or "3d"
  
      showStats: <bool>, displays FPS box
      showInfo: <bool>, displays some info on the graph and layout
                The info box is created as <div id="graph-info">, it must be
                styled and positioned with CSS.
  
  
      selection: <bool>, enables selection of nodes on mouse over (it displays some info
                 when the showInfo flag is set)
  
  
      limit: <int>, maximum number of nodes
    
      numNodes: <int> - sets the number of nodes to create.
      numEdges: <int> - sets the maximum number of edges for a node. A node will have 
                1 to numEdges edges, this is set randomly.
    }
    
  
    Feel free to contribute a new drawing!
  
   */
   
  
  var Drawing = Drawing || {};
  
  Drawing.SphereGraph = function(options) {
    var options = options || {};
    
    this.layout = options.layout || "2d";
    this.show_stats = options.showStats || false;
    this.show_info = options.showInfo || false;
    this.selection = options.selection || false;
    this.limit = options.limit || 10;
    this.nodes_count = options.numNodes || 20;
    this.edges_count = options.numEdges || 10;
  
    var camera, scene, renderer, interaction, geometry, object_selection;
    var stats;
    var info_text = {};
    var graph = new Graph({limit: options.limit});
    
    var geometries = [];
  
    var sphere_radius = 4900;
    var max_X = sphere_radius;
    var min_X = -sphere_radius;
    var max_Y = sphere_radius;
    var min_Y = -sphere_radius;
    
    var that=this;
  
    init();
    createGraph();
    animate();
  
    function init() {
      // Three.js initialization
      renderer = new THREE.WebGLRenderer({antialias: true});
      renderer.setSize( window.innerWidth, window.innerHeight );
      
      camera = new THREE.TrackballCamera({
        fov: 35, 
        aspect: window.innerWidth / window.innerHeight,
        near: 1,
        far: 100000,
  
        rotateSpeed: 0.5,
        zoomSpeed: 5.2,
        panSpeed: 1,
  
        noZoom: false,
        noPan: false,
  
        staticMoving: false,
        dynamicDampingFactor: 0.3,
        
        domElement: renderer.domElement,
  
        keys: [ 65, 83, 68 ]
      });
      camera.position.z = 10000;
  
      scene = new THREE.Scene();
  
      // Create sphere geometry and add it to the scene
      var sphere_geometry = new THREE.SphereGeometry(sphere_radius, 110, 100);
      material = new THREE.MeshBasicMaterial({ color: 0x000000, opacity: 0.8 });
      mesh = new THREE.Mesh(sphere_geometry, material);    
      scene.addObject(mesh);
  
      // Create node geometry (will be used in drawNode())
      geometry = new THREE.SphereGeometry( 25, 25, 0 );
  
      // Create node selection, if set
      if(that.selection) {
        object_selection = new THREE.ObjectSelection({
          domElement: renderer.domElement,
          selected: function(obj) {
            // display info
            if(obj != null) {
              info_text.select = "Object " + obj.id;
            } else {
              delete info_text.select;
            }          
          }
        });
      }
  
      document.body.appendChild( renderer.domElement );
    
      // Stats.js
      if(that.show_stats) {
        stats = new Stats();
        stats.domElement.style.position = 'absolute';
        stats.domElement.style.top = '0px';
        document.body.appendChild( stats.domElement );
      }
  
      // Create info box
      if(that.show_info) {
        var info = document.createElement("div");
        var id_attr = document.createAttribute("id");
        id_attr.nodeValue = "graph-info";
        info.setAttributeNode(id_attr);
        document.body.appendChild( info );
      }
    }
    
  
    /**
     *  Creates a graph with random nodes and edges.
     *  Number of nodes and edges can be set with
     *  numNodes and numEdges.
     */
    function createGraph() {
      var node = new Node(0);
      graph.addNode(node);
      drawNode(node);
  
      var nodes = [];
      nodes.push(node);
    
      var steps = 1;
      while(nodes.length != 0 && steps < that.nodes_count) {
        var node = nodes.shift();
  
        var numEdges = randomFromTo(1, that.edges_count);
        for(var i=1; i <= numEdges; i++) {
          var target_node = new Node(i*steps);
          if(graph.addNode(target_node)) {
            drawNode(target_node);
            nodes.push(target_node);
            if(graph.addEdge(node, target_node)) {
              drawEdge(node, target_node);
            }
          }
        }
        steps++;
      }
    
      // Transform a lat, lng-position to x,y.
      graph.layout = new Layout.ForceDirected(graph, {width: 2000, height: 2000, iterations: 1000, positionUpdated: function(node) {
        max_X = Math.max(max_X, node.position.x);
        min_X = Math.min(min_X, node.position.x);
        max_Y = Math.max(max_Y, node.position.y);
        min_Y = Math.min(min_Y, node.position.y);
        
        var lat, lng;
        if(node.position.x < 0) {
          lat = (-90/min_X) * node.position.x;
        } else {
          lat = (90/max_X) * node.position.x;
        }
        if(node.position.y < 0) {
          lng = (-180/min_Y) * node.position.y;
        } else {
          lng = (180/max_Y) * node.position.y;
        }
  
        var area = 5000;
        var phi = (90 - lat) * Math.PI / 180;
        var theta = (180 - lng) * Math.PI / 180;
        node.data.draw_object.position.x = area * Math.sin(phi) * Math.cos(theta);
        node.data.draw_object.position.y = area * Math.cos(phi);
        node.data.draw_object.position.z = area * Math.sin(phi) * Math.sin(theta);
        
      }});
      graph.layout.init();
      info_text.nodes = "Nodes " + graph.nodes.length;
      info_text.edges = "Edges " + graph.edges.length;
    }
  
  
    /**
     *  Create a node object and add it to the scene.
     */
    function drawNode(node) {
      var draw_object = new THREE.Mesh( geometry, [ new THREE.MeshBasicMaterial( {  color: Math.random() * 0xffffff } ) ] );
  
      var area = 2000;
      draw_object.position.x = Math.floor(Math.random() * (area + area + 1) - area);
      draw_object.position.y = Math.floor(Math.random() * (area + area + 1) - area);
      
      node.position.x = Math.floor(Math.random() * (area + area + 1) - area);
      node.position.y = Math.floor(Math.random() * (area + area + 1) - area);
      
      draw_object.id = node.id;
      node.data.draw_object = draw_object;
      node.layout = {}
      node.layout.max_X = 90;
      node.layout.min_X = -90;
      node.layout.max_Y = 180;
      node.layout.min_Y = -180;
      
      // node.position = draw_object.position;
      scene.addObject( node.data.draw_object );
    }
  
  
    /**
     *  Create an edge object (line) and add it to the scene.
     */
    function drawEdge(source, target) {
      material = new THREE.LineBasicMaterial( { color: 0xCCCCCC, opacity: 0.5, linewidth: 0.5 } );
      var tmp_geo = new THREE.Geometry();
  
      tmp_geo.vertices.push(new THREE.Vertex(source.data.draw_object.position));
      tmp_geo.vertices.push(new THREE.Vertex(target.data.draw_object.position));
  
      line = new THREE.Line( tmp_geo, material, THREE.LinePieces );
      line.scale.x = line.scale.y = line.scale.z = 1;
      line.originalScale = 1;
  
      line.geometry.__dirtyVertices = true;
  
      geometries.push(tmp_geo);
  
      scene.addObject( line );
    }
  
  
    function animate() {
      requestAnimationFrame( animate );
      render();
      if(that.show_info) {
        printInfo();
      }
    }
  
  
    function render() {
      // Generate layout if not finished
      if(!graph.layout.finished) {
        info_text.calc = "<span style='color: red'>Calculating layout...</span>";
        graph.layout.generate();
      } else {
        info_text.calc = "";
      }
    
      // Update position of lines (edges)
      for(var i=0; i<geometries.length; i++) {
        geometries[i].__dirtyVertices = true;
      }
      
      // set lookat of nodes to camera
      for(var i=0; i<graph.nodes.length; i++) {
        graph.nodes[i].data.draw_object.lookAt(camera.position);
      }
  
      // render selection
      if(that.selection) {
        object_selection.render(scene, camera);
      }
      
      // update stats
      if(that.show_stats) {
        stats.update();
      }
  
      // render scene
      renderer.render( scene, camera );
    }
  
  
    /**
     *  Prints info from the attribute info_text.
     */
    function printInfo(text) {
      var str = '';
      for(var index in info_text) {
        if(str != '' && info_text[index] != '') {
          str += " - ";
        }
        str += info_text[index];
      }
      document.getElementById("graph-info").innerHTML = str;
    }
  
  
    // Generate random number
    function randomFromTo(from, to) {
      return Math.floor(Math.random() * (to - from + 1) + from);
    }
  
    // Stop layout calculation
    this.stop_calculating = function() {
      graph.layout.stop_calculating();
    }
  }
  