/*  Graph JavaScript framework, version 0.0.1_pre_r140
 *  (c) 2006 Aslak Hellesoy <aslak.hellesoy@gmail.com>
 *  (c) 2006 Dave Hoover <dave.hoover@gmail.com>
 *
 *  Graph is freely distributable under the terms of an MIT-style license.
 *  For details, see the Graph wiki: http://dev.buildpatterns.com/trac/wiki/GraphProject
 *
 *  This file is generated. See http://buildpatterns.com/svn/repos/graph/trunk/README

              Extensively modified by John Jones for group diagrams.

/*--------------------------------------------------------------------------*/

function aalert(a,b,c,d,e,f,g,h,i,j) {
  if(1>2) alert(a,b,c,d,e,f,g,h,i,j);
}

Graph = {};
Graph.Model = Class.create();
Graph.Model.prototype = {
  initialize: function() {
    this.nodeHash = $H({});
    this.edgeHash = $H({});
  },

  addNodes: function(values) {
    for(var j=0, item; item = values[j]; j++) {
      for(var k=0, item2; item2 = item[k]; k++) {
        var opts = {level: j};

        var holdNode1=null, holdNode2=null;
        for(var l=0, ln=item2.length; l<ln; l++) {
          opts['type'] = ln==1 ? 'normal' : 'circle';
          holdNode1=this.addNode(item2[l], opts);
          holdNode1.prev = holdNode2;
	  if(holdNode2) holdNode2.next = holdNode1;
	  holdNode2 = holdNode1;
        }
	holdNode1.next = null;
      }
    }
  },

  addNode: function(value, options) {
    var node = this.nodeHash[value];
    if(node == undefined) {
      node = new Graph.Node(value);
      this.nodeHash[value] = node;
      this.cache();
      node.setOptions(options);
    }
    return node;
  },

  // Uniqueness must be ensured by caller
  addEdge: function(source, target) {
    var s = this.addNode(source);
    var t = this.addNode(target);
    var edge = {source: s, target: t};
    this.edgeHash[this.edgeKey(source, target)] = edge;
    this.cache();
    return edge;
  },

  removeEdge: function(source, target) {
	var edge = this.edgeHash[this.edgeKey(source, target)];
	delete this.edgeHash[this.edgeKey(source, target)];
	this.cache();
	return edge;
  },

  edgeKey: function(source, target) {
    return source + "->" + target;
  },

  cache: function() {
	this.nodes = this.nodeHash.collect(function(pair){
      return pair[1];
	});
	this.edges = this.edgeHash.collect(function(pair){
      return pair[1];
	});
  }

};

Graph.Node = Class.create();
Graph.Node.prototype = {
  initialize: function(value) {
    /* var tnode = document.createTextNode(value);
    var nodelem = document.createElement('span');
    nodelem.appendChild(tnode);
    this.value = nodelem; */
    this.value = value; 
    this.selected = false;
  },

  setOptions: function(options) {
    this.options = {};
    this.options.level=0;
/* = {
      level: 0,
      type: 'normal'
    }; */
    Object.extend(this.options, options || {});
  },

  select: function() {
    this.selected = true;
  },

  unselect: function() {
    this.selected = false;
  },

  // f is the font
  setTextSize: function(f) {
    var s = this.value;
    var i, j, t, h=f.h, w=0;
    if(!f.f){
      f.f=[t=0],i=0,j=f.w.length;
      while(++i<j)f.f[i]=t+=f.w[i-1];
    }
    s=s.split(''),i=0,j=s.length;
    while(i<j)if((t=f.c.indexOf(s[i++]))>=0)
        w +=f.w[t];
      else if(s[i-1]=='\n') h+=f.h;
    this.textHeight = h;
    this.textWidth = w;
  }

};

Graph.Renderer = {};
Graph.Renderer.Default = Class.create();
Graph.Renderer.Default.prototype = {
  setOptions: function(options) {
    this.options = {
      radius: 20,
      arrowHeadLength: null,
      arrowAngle: Math.PI/10,
      font: tahoma8,
      edgeColor: 'blue'
    }

    Object.extend(this.options, options || {});
  },

  initialize: function(element, graph, options) {
    this.element = element;
    this.graph = graph;
    this.setOptions(options);
    this.ctx = element.getContext("2d");
aalert("Element size "+element.width+" and "+element.height);
    this.factorX = (element.width - 2 * this.options.radius) / (graph.layoutMaxX - graph.layoutMinX);
    this.factorY = (element.height - 2 * this.options.radius) / (graph.layoutMaxY - graph.layoutMinY);

    // Reposition nodes from virtual coords
    this.reposition();
    
    // Clump conj class
    for (var i = 0; i < this.graph.nodes.length; i++) {
        if(this.graph.nodes[i].prev==null && this.graph.nodes[i].next != null) {
          var mynode = this.graph.nodes[i];
          // Check to see if we will go off the canvas
          var mycentx = mynode.center[0];
          while(mynode != null) {
            mycentx += 43;
            mynode = mynode.next;
          }
          mynode = this.graph.nodes[i];
          
          if(mycentx > element.width-this.options.radius) {
            mynode.center = this.shift(mynode.center, element.width-this.options.radius - mycentx,0);
            mynode.layoutPosX = this.untranslate(mynode.center)[0];
          }

          mynode = mynode.next;
          while(mynode != null) {
            mynode.center = this.shift(mynode.prev.center, 43, 0);
            mynode.layoutPosX = this.untranslate(mynode.center)[0];
            mynode = mynode.next;
          }
        }
    }
    /*    for(var i = 0; i<10; i++) { this.nudgeall(1); }
          this.reposition(); */
  },

  reposition: function() {
    for (var i = 0; i < this.graph.nodes.length; i++) {
      var node = this.graph.nodes[i];
      node.center = this.translate([node.layoutPosX, node.layoutPosY]);
    }
  },
  
  /* Expose graph adjustments */

  graphRepel: function() {
    this.options.layout.repel();
    this.reposition();
    this.draw();
  },
  
  graphAttract: function() {
    this.options.layout.attract();
    this.reposition();
    this.draw();
  },
  
  graphCenter: function() {
    this.options.layout.centering();
    this.reposition();
    this.draw();
  },
  graphClump: function() {
    this.options.layout.clump(this.factorX);
    this.reposition();
    this.draw();
  },
  graphSpread: function() {
    this.options.layout.spread((this.graph.layoutMaxX - this.graph.layoutMinX)/2- this.options.radius/this.factorX);
    this.reposition();
    this.draw();
  },

  // Repelling force of node2 on node1
  rforce: function(node1, node2) {
    var dx = node1.layoutPosX-node2.layoutPosX;
    var dy = node1.layoutPosY-node2.layoutPosY;
    return(1000*dx/Math.pow(dx*dx+dy*dy, 3/2));
  },
  
  nudgeall: function(damper) {
    // Walk through all nodes (except G and <e>) computing their forces
    var levs = this.options.layout.levs;
    var Ledge = this.graph.layoutMinX;
    var Redge = this.graph.layoutMaxX;
    var numlevs = this.options.layout.numlevs;
    
    for(var i=1; i<numlevs-1; i++) {
      for(var k=0, len=levs[i].length; k<len; k++) {
        var nodeforce=0;
        var node = levs[i][k];
        for(var j=0, jlen=node.connected.length; j<jlen; j++) {
          // attraction is strong than repelling
          nodeforce -= 1.5*this.rforce(node, node.connected[j]);
        }
        for(var j=0, jlen=node.repel.length; j<jlen; j++) {
          nodeforce += this.rforce(node, node.repel[j]);
        }
        node.nodeforce = nodeforce;
      }
    }
    // Set the force to 0 on G and <e>
    levs[numlevs-1][0].nodeforce = 0;    
    levs[0][0].nodeforce = 0;
    // Go through again moving things
    // Use total force on conjugacy class
    var Lnode, Rnode; // left and right sides of CC
    for (var i = 0; i < this.graph.nodes.length; i++) {
      var totforce=0; // force on a CC-clump
      var cnt = 0;
      var mynode;
      if(this.graph.nodes[i].prev==null) {
        mynode = this.graph.nodes[i];
        Lnode = mynode;
        while(mynode != null) {
          totforce += mynode.nodeforce;
          Rnode = mynode;
          mynode = mynode.next;
          cnt++;
        }
      }
      totforce = damper*totforce/cnt;
      // Stop at left/right edges
      // This is where to stop at left/right edges
      if(Lnode.layoutPosX+totforce < Ledge) {
        totforce = Ledge-Lnode.layoutPosX;
      }
      if(Rnode.layoutPosX+totforce > Redge) {
        totforce = Redge-Rnode.layoutPosX;
      }
      
      // Now move them
      mynode = Lnode;
      while(mynode != null) {
        mynode.layoutPosX += totforce;
        mynode = mynode.next;
      }
    }
    
  },

  untranslate: function(point) {
    return [
      (point[0] - this.options.radius)/ this.factorX +this.graph.layoutMinX,
      (point[1] - this.options.radius)/ this.factorY +this.graph.layoutMinY
    ];
  },

  translate: function(point) {
    return [
      (point[0] - this.graph.layoutMinX) * this.factorX + this.options.radius,
      (point[1] - this.graph.layoutMinY) * this.factorY + this.options.radius
    ];
  },

  shift: function(point, dx, dy) {
    return [point[0]+dx, point[1]+dy];
  },

  rotate: function(point, length, angle) {
    var dx = length * Math.cos(angle);
    var dy = length * Math.sin(angle);
    return [point[0]+dx, point[1]+dy];
  },

  clear: function() {
    this.ctx.clearRect(0, 0, this.element.width, this.element.height);
  },

  unselectNodes: function() {
    for (var i = 0, node; node= this.graph.nodes[i]; i++) {
      node.unselect();
    }
  },

  selectedSub: function() {
    for (var i = 0, node; node= this.graph.nodes[i]; i++) {
      if(node.selected) { return(node.sublink); }
    }
    alert('Could not find selected subgroup from diagram');
    return(null);
  },

  nodeFromSub: function(subin) {
    for (var i = 0; i < this.graph.nodes.length; i++) {
      var thissub = this.graph.nodes[i].sublink;
      if(subin.isSame(thissub)) { return(this.graph.nodes[i]); }
    }
    alert('Error: cannot find subgroup in diagram.');
    return(null);
  },

  draw: function() {
    this.ctx.clearRect(0, 0, this.element.width, this.element.height);
     /* drawString(this.ctx, "My Group", this.options.font, 230, 60);  */
    for (var i = 0, node; node= this.graph.nodes[i]; i++) {
      this.drawNode(node);
    }
    for (var i = 0, edge; edge=this.graph.edges[i]; i++) {
      this.drawEdge(edge);
    }
  },

  drawNode: function(node) {
    // Draw the text (or if the value is an element, move it)
    if(typeof node.value == 'string') {
      // make the text appear centered.
      if(!(node.textHeight && node.textWidth))
        node.setTextSize(this.options.font);

aalert("Drawing text at "+ (node.center[0]-(node.textWidth/2)) +" and "+( node.center[1]-(node.textHeight/2)));
      drawString(this.ctx, node.value, this.options.font, node.center[0]-(node.textWidth/2), node.center[1]-(node.textHeight/2));
    } else {
      node.value.style.position = 'absolute';
      node.value.style.left     = node.center[0] + 'px';
      node.value.style.top      = node.center[1] + 'px';
    }

    this.ctx.moveTo(0,0);
    var radius = this.options.radius;
    this.ctx.strokeStyle = 'black';
    if(node.selected) { this.ctx.strokeStyle = 'yellow'; }
    this.ctx.beginPath();
    switch(node.options.type) {
      case 'diamond':
      case 'normal':
aalert("Corner at "+ node.center[0]+ " and "+(node.center[1]-radius));
        this.ctx.moveTo(node.center[0], node.center[1]-radius);
        this.ctx.lineTo(node.center[0]+radius, node.center[1]);
        this.ctx.lineTo(node.center[0], node.center[1]+radius);
        this.ctx.lineTo(node.center[0]-radius, node.center[1]);
        this.ctx.lineTo(node.center[0], node.center[1]-radius);
        break;
      case 'square':
        this.ctx.moveTo(node.center[0]-radius, node.center[1]-radius);
        this.ctx.lineTo(node.center[0]-radius, node.center[1]+radius);
        this.ctx.lineTo(node.center[0]+radius, node.center[1]+radius);
        this.ctx.lineTo(node.center[0]+radius, node.center[1]-radius);
        this.ctx.lineTo(node.center[0]-radius, node.center[1]-radius);
        break;
      case 'circle':
      default:
        this.ctx.arc(node.center[0], node.center[1], radius, 0, Math.PI*2, true);
    }
    this.ctx.closePath();
    this.ctx.stroke();
  },

  drawEdge: function(edge) {
    var source = edge.source.center;
    var target = edge.target.center;

    // find the angle of the edge
    var tan = (target[1] - source[1]) / (target[0] - source[0]);
    var theta = Math.atan(tan);
    if(source[0] <= target[0]) {theta += Math.PI}
    source = this.rotate(source, -this.options.radius, theta);
    target = this.rotate(target, this.options.radius, theta);

    // draw the edge
    var color = edge.color ? edge.color : this.options.edgeColor;
    this.ctx.strokeStyle = color;
    this.ctx.fillStyle = color;
    this.ctx.lineWidth = 1.0;
    this.ctx.beginPath();
    this.ctx.moveTo(source[0], source[1]);
    this.ctx.lineTo(target[0], target[1]);
    this.ctx.stroke();

    if(this.options.arrowHeads)
      this.drawArrowHead(theta, source[0], source[1], target[0], target[1]);
  },

  drawArrowHead: function(theta, startx, starty, endx, endy) {
	var length = this.options.arrowHeadLength ? this.options.arrowHeadLength : this.options.radius;
    var top = this.rotate([endx, endy], length, theta + this.options.arrowAngle);
    var bottom = this.rotate([endx, endy], length, theta - this.options.arrowAngle);
    this.ctx.beginPath();
    this.ctx.moveTo(endx, endy);
    this.ctx.lineTo(top[0], top[1]);
    this.ctx.lineTo(bottom[0], bottom[1]);
    this.ctx.fill();
  },

  nodeAt: function(point) {
    var node = undefined;
    var mind = Infinity;
    var rsquared = this.options.radius*this.options.radius;
    for (var i = 0, n; n=this.graph.nodes[i]; i++) {
      var np = this.translate([n.layoutPosX, n.layoutPosY]);
      var dx = point[0] - np[0];
      var dy = point[1] - np[1];
      var d = dx * dx + dy * dy;
      if(d < mind && d <= rsquared) {
        mind = d;
        node = n;
      }
    }
    return node;
  }
};


// From Benjamin Joffe, http://www.random.abrahamjoffe.com.au/public/JavaScripts/canvas/fonts.htm

var tahoma8=new Image();
tahoma8.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAikAAAANAgMAAAARXxaEAAAABlBMVEX///8AAABVwtN+AAACCklEQVR4Xu3UwaokKxgD4ATiPkL7PhHsvQf0/V/lgl1F02fuYZjF7CabsqQWX4Vf8UP+RQCMAgIi/kZyPxUAawV5bdKEcn3zthiEBz+Idx5llq9V1s62VnPFfEw3Ls6W9lyptCtdNxrSOFlWKatxuixU5CEIU8O+RJdFuiwl/maBfrBIJoUQlul+LOmU7CC3xUR6MlQEQxmyicrVhUpxflp4WzrworQyW5mrla9VR22PltWya3kOZXgoz9mlEAPRsVRijFFRab4tHoqQBFEVAmfIffY90o10aUTHYgPEy0KgYF4WDMXXj43ufsrmw5Iy3J24lxniOfdAW+bjw9KQeGjEipGEmL3MDTfJwxnKw4RnEmGvXywTBUEh0I5lH8uTt8UPSVUjqci7l2YdS1evqMtsQW4LcSyCdpQkfR0LJ3pABdECIuNY9prsZ4UOuAM5FnzrxRy3hdzjttAy622B6bdlEK8XiDiWyMPPjYFhkMDdC9694PRCwI/bwpel3hZjSCJpiRj37PKa3SgzHZKNJDZPgCMQpXQkRt80BmWQwT0vJr/NyyvteWbX5Ysexd1eLduNfYmUVL7wfJ3pYFtrua4osxGc1kZaLstG49x9krMRjbFBl0kGWIZ9LDKptwUdxGcM5F7xY/vnGL9JmQCCK28LhOt++b+UgPhTS/mjexfXvasIKhugPyz/AfyHcZZwWdfAAAAAAElFTkSuQmCC';
tahoma8.src='tahoma8.png';
tahoma8.c='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789!@#$%^&*()-=[]\\;\',./_+{}|:"<>?`~';
tahoma8.w=[6,6,5,6,6,4,6,6,2,3,5,2,8,6,6,6,6,4,5,4,6,6,8,6,6,5,7,6,7,7,6,6,7,7,4,5,6,5,8,7,8,6,8,7,6,6,7,6,10,6,6,6,3,6,6,6,6,6,6,6,6,6,6,4,10,8,6,11,8,7,6,4,4,4,8,4,4,4,4,2,4,4,4,6,8,5,5,4,4,4,8,8,5,6,8];
tahoma8.h=13;
tahoma8.h=14;

var tahoma8bold=new Image();
tahoma8bold.src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoEAAAANAgMAAACr0FyhAAAABlBMVEX///8AAABVwtN+AAACSUlEQVR4Xu3VQYrjQAwF0C+w9m6Q7qOC1P4LSve/yiA7cUK6p2mGmd38hVMKmHpIdhl/If8jAYAQwrpQ/OsE7jFkrzVXwIDt0CRBZFy0p1ABAzP0XX/FpaScnqVl6mMxozcZpUuXU1dl5JiqZswKpaJkYXeWltJHVMiGcAFCKDaCLYRdwi4uoYOfhJBvhWPCzCQMpgrLES7JKVNNzQwql5ASZn2HgiaBQz2wy6b9K7saSh7CHQbsgDyFJEag46NkOUtHOTPH9GTF0ZbUXMwxrbd15mihPoUl4TPXZ6HSQ8NMIaFyCK2FN7952S1v2DY1sQmD1oTtilehNVuv/uSY4DWc2VsdpVmZxCUEc5TC1LMM3ssl8bHSH0KnSrBkmoSHo4WKtdFHwZTMW7Yw8rZ2EdKag0ZtLYxXIQEq4AL4Kay7cL0I6SPIUmPJknjpoZuewp2pU+Yydeo6hbUkFOsQGnQncoSZeQu3XekfE1CL5tQpxCmstaDBpgKbAWMAPIWhXwg1D2G1UKXyEupjyjtSphx/E3YKuZQqPISE+mohzdDCWmI4hAroOeVLeD2HZw8NiPVJOF+FaKHJGL2DxCG0pzBHC3EX2ik0kNYrHFdVkHchN2OtD8NooQSuKYsiB96fw3s8zzdlSp20FvJ4U+ix/CH0jLyfNqFl6pPMauEohZ5zRsVDWKH0UWOBHgV2NYA9lEdnZBES/J0QJAxvaTRfCns7dr4N8ZNU7CG4cgkxwQ3P8/DryDogfyh0/DQvwuubghtiQ7Ug+Sb8BYH3sjwcbInrAAAAAElFTkSuQmCC';
tahoma8bold.c='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789!@#$%^&*()-=[]\\;\',./_+{}|:"<>?`~';
tahoma8bold.w=[7,7,6,7,7,4,7,7,3,4,7,3,11,7,7,7,7,5,6,5,7,7,9,7,7,6,8,7,7,8,6,6,8,8,5,6,7,6,10,7,8,7,8,8,7,7,8,7,11,7,7,7,3,7,7,7,7,7,7,7,7,7,7,3,10,9,7,13,9,9,7,5,5,5,9,5,5,6,3,3,3,3,6,7,9,7,7,7,3,6,9,9,6,6,10];
tahoma8bold.h=15;

drawString = function(ctx, s, f, x, y){
        ctx.beginPath();
        ctx.closePath();
	y=Math.round(y);
	var z=x=Math.round(x),t,i,j;
	if(!f.f){
		f.f=[t=0],i=0,j=f.w.length;
		while(++i<j)f.f[i]=t+=f.w[i-1];
	}
	s=s.split(''),i=0,j=s.length;
	while(i<j)if((t=f.c.indexOf(s[i++]))>=0)
		ctx.drawImage(f,f.f[t],0,f.w[t],f.height,x,y,f.w[t],f.height),x+=f.w[t];
		else if(s[i-1]=='\n')x=z,y+=f.h;
}

Graph.Layout = {};
/* My layout */

Graph.Layout.Levels = Class.create();
Graph.Layout.Levels.prototype = {
  initialize: function(graph) {
    this.graph = graph;
    this.margin = 3;
  },

  layout: function() {
    this.layoutPrepare();
    for(var jj=0; jj<10; jj++) this.layoutIterate();
    /* finish it off */
    this.repel();
    this.repel();
    this.repel();
    this.spread();
    this.clump();
    this.centering();
    this.spread();
    
    this.layoutCalcBounds();
  },

  layoutPrepare: function() {
    this.levs = new Array();
    for (var i = 0, node; node = this.graph.nodes[i]; i++) {
      var thisLevel = node.options.level || 0;
      if(typeof(this.levs[thisLevel])=='undefined') {
        this.levs[thisLevel] = new Array();
      }
      this.levs[thisLevel].push(node);
      node.layoutPosX = 0; /* Math.round(Math.random()*100); */
      node.layoutPosY = -10*thisLevel;
    }
    this.numlevs = this.levs.length;
    for (var i = 0, lev; lev = this.levs[i]; i++) {
      for(var k=0, len=lev.length; k<len; k++) {
         var node = lev[k];
         node.layoutPosX = 20*k-10*len +10;/* 0.1*Math.random(); */
      }
    }
    for (var i=0, node; node = this.graph.nodes[i]; i++) {
      node.connected = new Array();
      node.bound = new Array();
      node.repel = new Array();
    }
    for (var i=0, edge; edge = this.graph.edges[i]; i++) {
      edge.source.connected.push(edge.target);
      edge.target.connected.push(edge.source);
    }
    for (var i=0, node; node = this.graph.nodes[i]; i++) {
      for (var j=0, node2; node2 = this.graph.nodes[j]; j++) {
        if(node != node2 && ! node.sublink.contains(node2.sublink)  &&
             ! node2.sublink.contains(node.sublink) ) {
           node.repel.push(node2);
        }
      }
    }
  },

  layoutIterate: function() {
    this.attract();
    for(var j = 0; j<10; j++) { this.repel(); }
    this.centering();
    this.clump();
  },

  /* Run through attractions */
  attract: function() {
    for(var i=1; i<this.numlevs-1; i++) {
      for(var k=0, len=this.levs[i].length; k<len; k++) {
        var newx = 0, cnt=0;
        var node = this.levs[i][k];
        for(var j=0, jlen=node.connected.length; j<jlen; j++) {
          newx += node.connected[j].layoutPosX;
          cnt++;
        }
aalert('Node '+node.value+' moved from '+node.layoutPosX+' by '+newx);
        node.layoutPosX = newx/cnt;
      }
    }
  },

  // Gather conj. classes
  clump: function(factorx) {
    if(typeof(factorx) == 'undefined') {
      factorx = 20;
    }
    for (var i = 0; i < this.graph.nodes.length; i++) {
      if(this.graph.nodes[i].prev==null && this.graph.nodes[i].next != null) {
        var thiscc = new Array();
        thiscc.push(this.graph.nodes[i]);
        var mynode = this.graph.nodes[i].next;
        while(mynode != null) {
          thiscc.push(mynode);
          mynode = mynode.next;
        }
        
        // Now sort them
        var tmpnode, cclen=thiscc.length;
        for(var ii = 0; ii<cclen-1; ii++) {
          for(var j = ii+1; j<cclen; j++) {
            if(thiscc[ii].layoutPosX > thiscc[j].layoutPosX) {
              tmpnode = thiscc[ii];
              thiscc[ii]=thiscc[j];
              thiscc[j] = tmpnode;
            }
          }
        }
        
        // Now clump them
        
        var mynode = thiscc[0];
        thiscc[0].prev = null;
        for(var ii = 0; ii<cclen-1; ii++) {
          thiscc[ii+1].layoutPosX = thiscc[ii].layoutPosX+43/factorx;
          thiscc[ii].next = thiscc[ii+1];
          thiscc[ii+1].prev = thiscc[ii];          
        }
        thiscc[cclen-1].next = null;
      }
    }
  },

  centering: function() {
    /* Find average of x-coords */
    var maxx=-100, minx=100, cnt=0;
    for(var i=1; i<this.numlevs-1; i++) {
      for(var k=0, len=this.levs[i].length; k<len; k++) {
        var nx= this.levs[i][k].layoutPosX;
        if(nx<minx) { minx = nx; }
        if(nx>maxx) { maxx = nx; }
      }
    }
    dx = (maxx+minx)/2;
    /* Move everyone -dx */
    for(var i=1; i<this.numlevs-1; i++) {
      for(var k=0, len=this.levs[i].length; k<len; k++) {
        this.levs[i][k].layoutPosX -= dx;
      }
    }
  },

  alertXcoords: function() {
    var txt='';
    for(var i=0; i<this.numlevs; i++) {
      for(var k=0, len=this.levs[i].length; k<len; k++) {
        txt += this.levs[i][k].layoutPosX+',';
      }
    }
    alert('Centered to '+txt);
  },
  
  /* Run through repeling */
  repel: function() {
    for(var i=1; i<this.numlevs-1; i++) {
      for(var k=0, len=this.levs[i].length; k<len; k++) {
        var dx = 0;
        var node = this.levs[i][k];
        for(var j=0, jlen=node.repel.length; j<jlen; j++) {
          var delx = (node.layoutPosX -node.repel[j].layoutPosX);
          var dely = Math.abs(node.repel[j].options.level-node.options.level);
          var sgn = -1;
          if(delx>0) sgn = 1;
          /*          dx += delx/(1+Math.abs(node.repel[j].options.level-node.options.level)); */
          /*          dx += 1000/(0.125+delx*delx+dely*dely); */
          dx += sgn* 1.2/(delx*delx/500+dely*dely+1);
        }
        node.layoutPosX += dx;
      }
    }
  },

  spread: function(width) {
    if(typeof(width) == 'undefined') {
      width = 100;
    }
    
    var maxabs=0;
    for(var i=0; i<this.numlevs; i++) {
      for(var k=0, len=this.levs[i].length; k<len; k++) {
        var thisone = Math.abs(this.levs[i][k].layoutPosX);
        if(thisone > maxabs) maxabs = thisone;
      }
    }
    if(maxabs>0) {
      maxabs = width/maxabs;
      for(var i=0; i<this.numlevs; i++) {
        for(var k=0, len=this.levs[i].length; k<len; k++) {
          this.levs[i][k].layoutPosX *= maxabs;
        }
      }
    }
    
  },

  layoutCalcBounds: function() {
    var minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;

    for (var i = 0; i < this.graph.nodes.length; i++) {
      var x = this.graph.nodes[i].layoutPosX;
      var y = this.graph.nodes[i].layoutPosY;

      if(x > maxx) maxx = x;
      if(x < minx) minx = x;
      if(y > maxy) maxy = y;
      if(y < miny) miny = y;
    }

    this.graph.layoutMinX = minx-this.margin;
    this.graph.layoutMaxX = maxx+this.margin;
    this.graph.layoutMinY = miny-this.margin;
    this.graph.layoutMaxY = maxy+this.margin;
  }
}


Graph.EventHandler = Class.create();
Graph.EventHandler.prototype = {
  setOptions: function(options) {
    this.options = {
      initNodeDrag:   Prototype.emptyFunction,
      updateNodeDrag: Prototype.emptyFunction,
      endNodeDrag:    Prototype.emptyFunction,
      initEdgeDrag:   Prototype.emptyFunction,
      updateEdgeDrag: Prototype.emptyFunction,
      endEdgeDrag:    Prototype.emptyFunction,
      moveNodeOnDrag: true
    }

    Object.extend(this.options, options || {});
  },

  initialize: function(renderer, options) {
    this.renderer = renderer;
    this.setOptions(options);

    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
    this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
    Event.observe(renderer.element, "mousedown", this.eventMouseDown);
    Event.observe(renderer.element, "mousemove", this.eventMouseMove);
    Event.observe(renderer.element, "mouseup", this.eventMouseUp);
  },

  offset: function(event) {
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    var pos     = Position.cumulativeOffset(this.renderer.element);
    return [0,1].map( function(i) { return (pointer[i] - pos[i]) });
  },

  initDrag: function(event) {
    if(Event.isLeftClick(event)) {
      this.activeNode = this.renderer.nodeAt(this.offset(event));
      if(this.activeNode != null) {
        this.options.initNodeDrag(this.activeNode);
        showsubinfo(this.activeNode);
        this.renderer.unselectNodes();
        this.activeNode.select();
        $('senddiasubbutton').disabled = false;
        this.renderer.draw();
      }
      Event.stop(event);
    }
  },

  updateDrag: function(event) {
    if(this.activeNode) {
	  if(this.options.moveNodeOnDrag) {
        this.activeNode.center = this.offset(event);
        for(var mynode = this.activeNode.next; mynode != null; 
              mynode = mynode.next) {
          mynode.center = this.renderer.shift(mynode.prev.center, 43, 0);
        }
        for(var mynode = this.activeNode.prev; mynode != null; 
              mynode = mynode.prev) {
          mynode.center = this.renderer.shift(mynode.next.center, -43, 0);
        }
      }
      this.options.updateNodeDrag(this.activeNode);
    } else if(this.activeEdge) {
      this.options.updateEdgeDrag(this.activeEdge);
    }
  },

  endDrag: function(event) {
    if(this.activeNode) {
      var node = this.activeNode;
      var position = this.renderer.untranslate(this.offset(event));
      node.layoutPosX = position[0];
      node.layoutPosY = position[1];
      for(var mynode = this.activeNode.next; mynode != null; 
            mynode = mynode.next) {
        position =  this.renderer.untranslate(mynode.center);
        mynode.layoutPosX = position[0];
        mynode.layoutPosY = position[1];
      }
      for(var mynode = this.activeNode.prev; mynode != null; 
            mynode = mynode.prev) {
        position =  this.renderer.untranslate(mynode.center);
        mynode.layoutPosX = position[0];
        mynode.layoutPosY = position[1];
      }
      this.options.endNodeDrag(this.activeNode);
      this.activeNode = null;
    } else if(this.activeEdge) {
      this.options.endEdgeDrag(this.activeEdge);
      this.activeEdge = null;
    }
  }
}

// Install event listeners
var onClickHandler = function(event) {
  var pos = this.eventPos(event);

  var node = this.nodeAt(pos);
  if(node && this.options.onnodeclick) {
    this.options.onnodeclick(node);
    return;
  }
};

/*  Set up a group */


function makegroup(canv, gdata, gedges, subinfo) {
  var g = new Graph.Model();
  g.addNodes(gdata);

  var j,k,edge,node;
  if(typeof(subinfo) != 'undefined') {
    for(j = 0, node; node = g.nodes[j]; j++) {
      k=0;  while(subinfo[k].dianame != node.value) {k++;}
      node.sublink = subinfo[k];
    }
  }

  for(j=0, edge; edge= gedges[j]; j++) {
    g.addEdge(edge[0], edge[1]);
  }

  var layout = new Graph.Layout.Levels(g);
  layout.layout();

  var renderer = new Graph.Renderer.Default($(canv), g, {
    radius: 20,
    layout: layout
  });
  renderer.draw();     

  new Graph.EventHandler(renderer, {
    updateNodeDrag: function(node, event) {
      renderer.draw();
        }
  });
  return(renderer);
}

function showsubinfo(node) {
  var txt = '<u>'+node.value+'</u><br><br>';
  var sub = node.sublink;
  txt += 'Size: '+sub.size()+'<br><br>';
  txt += 'A generating set: {';
  for(var k=0, gen; gen= sub.gens[k]; k++) {
    if(k>0) txt += ',';
    txt += sub.g.names[gen];
  }
  txt += '}<br><br>';

  /*
  txt += 'Next: ';
  var mynode = node.next;
  while(mynode != null) {
    txt += mynode.value+', ';
    mynode = mynode.next; 
  }
  txt += '<br><br>Prev: ';
  var mynode = node.prev;
  while(mynode != null) {
    txt += mynode.value+', ';
    mynode = mynode.prev;
  }
  txt += '<br><br>';
  */
  /*  txt += 'Repels: ';
  for(var i=0; i<node.repel.length; i++) {
    txt += node.repel[i].value+', ';
  }
  txt += 'Position: '+node.layoutPosX;
  txt += '<br><br>Force: '+node.nodeforce;
  */
  
  subinfo(txt);
}
  
function repel() {
  curg.subdiagram.graphRepel();
}
function clump() {
  curg.subdiagram.graphClump();
}
function spread() {
  curg.subdiagram.graphSpread();
}
function attract() {
  curg.subdiagram.graphAttract();
}
function centering() {
  curg.subdiagram.graphCenter();
}
function nudge() {
  curg.subdiagram.nudgeall(1);
  curg.subdiagram.reposition();
  curg.subdiagram.draw();  
}
