//*** This library is copyright 2004 Gavin Kistner, gavin@refinery.com
//*** It is covered under the license viewable at http://phrogz.net/SVG/libraries/_ReuseLicense.txt
//*** Reuse or modification is free provided you abide by the terms of that license.
//*** (Including the first two lines above in your source code mostly satisfies the conditions.)

/*******************************************************************************
Springz, a re-usable library to implement spring-like behaviors in SVG.
v1.0   20040614  Initial Release
v1.0.1 20040626  Collisions push in both directions; some speed improvements



Feature Overview
=================================================================================
	- The strength of each Spring may be varied.
	
	- The library performs (simple) collision prevention, if desired.
	
	- The desired length of a Spring may be non-zero, pushing/pulling objects
	  to achieve a desired separation distance.
	
	- Objects can be set to have 'mass', which effects how far each object moves
	  when the Spring pulls on it.
	  (Light objects are pushed/pulled more easily than massive ones.)
	  (Note: this does not impart inertia to the movement.)
	
	- Springs are graphically depicted as lines on screen, placed below all other
	  elements.
	
	- Objects can be temporarily prevented from moving (by returning false from
	  their .moveTo() method, and/or each Spring may be temporarily inactivated.

	- The overall strength of Springs can be scaled using Springz.scale


Requirements Overview
=================================================================================
	- You need to create global svgDoc and svgRoot variables.
	
	- Each object used on the ends of a Spring needs to have:
	  a .loc property (which is an object with .x and .y properties)
	  a .moveTo() method (which takes x and y parameters)
	  a .collisionRadius property (if using noCollisions)
	  a .mass property (if using Springz.useMass)



Quick Start Example
=================================================================================
	<svg ... onload="Init(evt)">
	...
	function Init(evt){
		svgDoc = evt.target.ownerDocument;
		svgRoot = svgDoc.documentElement;
	}
	...
	var s = new Spring( obj1, obj2 );
	s.apply();



Creating a New Spring
=================================================================================
	Springz.set( obj1, obj2 [,strength [,distance]] )
	 new Spring( obj1, obj2 [,strength [,distance]] )

	Springs can be created using either new Spring(...) or Springz.set(...);
	in either case, the parameters are the same:

	- obj1, obj2
	  The objects on either end of the string.
	  As noted above, these objects must respond to .loc and .moveTo(x,y);
	  they also must respond to .collisionRadius (for collision detection) and
	  .mass (if using Springz.useMass)
	  
	- strength (optional)
	  The numeric strength of the spring.
	  The stronger a spring, the harder it pulls on the objects attached to it.
	  (All strengths are scaled by the global Stringz.strengthScale property.)
	  If omitted, an initial strength of 1 will be used for new Springs.
	  
	- distance (optional)
	  The ideal distance between the objects attached to the spring.
	  If the objects are farther than this number, they will be pulled towards
	  each other; if they are closer than this number, they will push apart.
	  If omitted, an initial distance of 0 will be used for new Springs.

	Springz.set(...) will find the Spring corresponding to the two objects
	supplied (if it exists) and return it. If no Spring exists between them,
	it will create a new Spring and set the values. (If you do not supply 
	strength and/or distance to Springz.set AND a Spring already exists between
	the options, its strength and distance will not be altered.)
	
	The lines for all Springs will be placed inside a <g> element with
	id="springGroup"; if this group does not exist, it will be created and
	placed at the start of the document (causing the lines to draw underneath
	everything else).



The .moveTo() Method and .loc Property
================================================================================
	As noted above, each object on the spring ends MUST have a .moveTo(x,y)
	method and a .loc property.
	

	.moveTo(x,y) is only responsible for moving the object itself; the library
	will take care of moving the Spring end(s) for the object when applying the
	Spring. If you don't want the spring ends to move, return false (not null
	or undefined) from a call to .moveTo() and they won't be moved.
	
	If you move the object yourself and want to get the Spring lines to move to
	the correct location, call Springz.moveSpringsFor(myObj)
	
	
	The .loc property required on every object must contain an object with
	.x and .y properties, used to determine the location of the object.
	You must keep this information up-to-date yourself; for example, your
	.moveTo() method might look like:

	MyClass.prototype.moveTo=function(x,y){
		this.x=x;
		this.y=y;
		this.circle.setAttribute('cx',x);
		this.circle.setAttribute('cy',y);
	}



Modifying a Spring
================================================================================

	mySpring.setStrength( newStrength )
	-----------------------------------
	Sets the spring strength, and sets the class on the spring line object to
	'spring strengthN' where N is the strength.
	
	For example, a line corresponding to a Spring with strength 3 can be styled
	via the CSS selector:
		#springGroup line.strength3 { stroke:red; stroke-width:4 }
	
	
	mySpring.setDistance( newDistance )
	-----------------------------------
	Set the ideal distance the Spring is attempting to achieve.

	
	mySpring.hide()
	mySpring.show()
	-----------------------------------
	Modify the line depicting the Spring:
		hide() removes it from the document
		show() re-adds it to the document
	Springs' lines are initially shown.
	Hiding the line improves performance.


	mySpring.inactivate()
	mySpring.reactive()
	-----------------------------------
	Cause a spring to not take effect until re-activated.
	Calls to mySpring.apply() will return false, and the Spring will be skipped
	in calls to Springz.applyAll();
	
	
	mySpring.removeThyself()
	-----------------------------------
	Permanently remove a Spring.



Using 'Mass' for the Objects
================================================================================
	You can give your objects 'mass', so that some objects appear heavy.
	For example, if you have two objects attached by a Spring, and one has a
	mass of 300 while the other has a mass of 100, the heavy object will move
	1/3 the distance that the light object does.

	To use mass:
	a) Set Springz.useMass=true;
	b) Make sure that every object attached by a Spring has a .mass property.



Locking an Object
================================================================================
	To make the object at one end of the Spring not move at all, you can either
	give it a REALLY enormous mass compared to other objects (see above),
	or you can cause its moveTo() method to return false.
	
	Using just the latter will cause objects at the other end of the Spring to
	move more slowly than if you set the mass to an enormous value; using both
	techniques at once may desirable.



Animating the Springs
================================================================================
	mySpring.apply(   [noCollisions] ); //Apply the force of one Spring one time
	Springz.applyAll( [noCollisions] ); //Apply the force of every Spring once

	To cause your created Springs to actually DO something, call one of the
	methods listed above. In both cases, the single parameter is the same:
	
	- noCollisions
	  Prevent the objects from drawing over top of one another (or attempt to).
	  
	For collision prevention to work, each object on the Spring ends must have
	a .collisionRadius property.
	
	If you want to perform collision prevention without actually applying any
	Springs (you just want objects to stop overlapping) you can call:
	Springz.preventCollisions();
	
	
	To let the Springs fully animate, simply call Springz.applyAll() repeatedly
	using setTimeout(); for example:

	function Animate(){
		Springz.applyAll(true);
		if (keepAnimating) setTimeout('Animate()',100);
	}


*******************************************************************************/
var Mathsqrt = Math.sqrt; // speeds things up 10-15%
var pccall=0;
Springz = {
	strengthScale:0.5,
	useMass:false,
	all:[],objs:[],lookup:{},byID:{},
	applyAll : function(noCollisions){
		var t0,t1,t2;
		noCollisions = !!noCollisions;
		//DebugOut(this+'.applyAll('+noCollisions+')',2);
		//t0 = new Date();
		for (var i=0,spring,len=this.all.length;i<len;i++) if (!(spring=this.all[i]).inactive) spring.apply(noCollisions);
		//t1 = new Date();
		if (noCollisions) Springz.preventCollisions();
		//t2 = new Date();
		//if (global.DebugOut) DebugOut("Applied "+len+" Springs "+(noCollisions?'(with collisions) ':'')+"in "+(t2-t0)+"ms (collisions was "+(t2-t1)+"ms of that)",1);
	},
	preventCollisions : function(){
		var r1,r2;
		//DebugOut(this+'.preventCollisions()',2);
		for (var i=this.objs.length-1;i>0;i--) for (var j=i-1;j>=0;j--){
			var o1 = Springz.objs[i];
			var o2 = Springz.objs[j];
			var loc1 = o1;
			var loc2 = o2;
			var dx = loc2.x-loc1.x;
			var dy = loc2.y-loc1.y;
			var combinedRadius = o1.collisionRadius+o2.collisionRadius;
			var negCombined = -combinedRadius;
			if (dx>combinedRadius || dx<negCombined || dy>combinedRadius || dy<negCombined) continue;
			var h = Mathsqrt(dx*dx+dy*dy);
			var d = combinedRadius-h;
			if (d<0) continue;
			var r = d/h;

			if (Springz.useMass){
				r2 = (1-(r1=o1.mass/(o1.mass+o2.mass)))*r;
				r1*=r;
			} else r1=r2=r/2;

			var x1 = loc1.x-dx*r2;
			var y1 = loc1.y-dy*r2;
			var x2 = loc2.x+dx*r1;
			var y2 = loc2.y+dy*r1;
			o1.moveTo(x1,y1);
			o2.moveTo(x2,y2);
		}
	},
	toString:function(){ return '[Springz]' }
}

Spring.set=function(o1,o2,strength,distance){
	//*** Find the spring between two elements;
	//*** if none exists, create a new Spring and return that.

	//DebugOut('Spring.set('+o1+','+o2+','+strength+','+distance+')',4);
	var spring;
	if (spring=Spring.find(o1,o2)){
		if (strength!==undefined) spring.setStrength(strength);
		if (strength!==distance) spring.setDistance(distance);
		return spring;
	}
	return new Spring(o1,o2,strength,distance);
}

Spring.find=function(o1,o2){
	//*** Find the spring between two elements;
	//*** if none exists, return null

	//DebugOut('Spring.find('+o1+','+o2+')',4);
	if (o1.uniqueSpringID!=null && o2.uniqueSpringID!=null){
		var i1 = o1.uniqueSpringID<o2.uniqueSpringID?o1.uniqueSpringID:o2.uniqueSpringID;
		var i2 = o1.uniqueSpringID>o2.uniqueSpringID?o1.uniqueSpringID:o2.uniqueSpringID;
		return Springz.lookup[i1] && Springz.lookup[i1][i2];
	}
	return null;
}

function Spring(o1,o2,strength,distance){
	//*** Create a new Spring between two objects
	//DebugOut('new Spring('+o1+','+o2+','+strength+','+distance+')',4);
	var oldSpring,springGroup,tmp;

	var svgNS = 'http://www.w3.org/2000/svg';
	
	if (o1.uniqueSpringID==null) o1.uniqueSpringID = Springz.objs.push(o1)-1;
	if (o2.uniqueSpringID==null) o2.uniqueSpringID = Springz.objs.push(o2)-1;

	this.o1 = o1.uniqueSpringID<o2.uniqueSpringID?o1:o2;
	this.o2 = o1.uniqueSpringID>o2.uniqueSpringID?o1:o2;
	this.strength=2,this.showing=true,this.distance=80;

	var i1 = o1.uniqueSpringID;
	var i2 = o2.uniqueSpringID;

	if (!(tmp=Springz.lookup[i1])) tmp=Springz.lookup[i1]={};
	if (oldSpring=tmp[i2]) oldSpring.removeThyself();
	tmp[i2]=this;
	
	if (!(tmp=Springz.byID[i1])) tmp=Springz.byID[i1]=[];
	tmp.push(this);
	if (!(tmp=Springz.byID[i2])) tmp=Springz.byID[i2]=[];
	tmp.push(this);

	this.setStrength(strength || 1);
	this.setDistance(distance || 0);

	Springz.all.push(this);
	//DebugOut('new '+this,2);
}
Spring.prototype.setStrength=function(strength){
	this.strength=strength;
}
Spring.prototype.setDistance=function(distance){
	this.distance=distance;
}
Spring.prototype.hide=function(){
	this.showing=false;
	this.setStrength(this.strength);
}
Spring.prototype.show=function(){
	this.showing=true;
}

Spring.prototype.apply=function(noCollisions){
	//DebugOut(this+'.apply('+noCollisions+')',4);
	if (this.inactive) return false;
	
	var l1 = this.o1;
	var l2 = this.o2;

	var dx = l2.x-l1.x;
	var dy = l2.y-l1.y;

	var minDistance = this.distance;

	if (noCollisions){
		var combinedRadius = this.o1.collisionRadius+this.o2.collisionRadius;
		if (minDistance<combinedRadius) minDistance = combinedRadius;
	}

	var h1 = Mathsqrt(dx*dx+dy*dy);
	var h2 = h1-minDistance;
	
	if (h2<h1){
		var r = h2/h1;
		dx*=r,dy*=r;
	}
	var r = Springz.strengthScale*this.strength*0.5;

	var r1,r2;
	if (Springz.useMass){
		r2 = (1-(r1=this.o1.mass/(this.o1.mass+this.o2.mass)))*r;
		r1*=r;
	} else r1=r2=r/2;

	var x1 = l1.x+dx*r2;
	var y1 = l1.y+dy*r2;
	var x2 = l2.x-dx*r1;
	var y2 = l2.y-dy*r1;
	
	this.o1.moveTo(x1,y1);
 this.o2.moveTo(x2,y2);
	
	return true;
}

Spring.prototype.inactivate=function(){
	this.inactive=true;
}

Spring.prototype.reactivate=function(){
	this.inactive=false;
}

Spring.prototype.removeThyself=function(){
	this.hide();
	delete this.g;
	delete Springz.lookup[this.o1.uniqueSpringID][this.o2.uniqueSpringID];
	
	function RemoveSpringFromArray(s,a){
		for (var i=0,len=a.length;i<len;i++) if (a[i]==this){
			a.splice(i,1);
			break;
		}
	}

	RemoveSpringFromArray(this,Springz.all);
	RemoveSpringFromArray(this,Springz.byID[this.o1.uniqueSpringID]);
	RemoveSpringFromArray(this,Springz.byID[this.o2.uniqueSpringID]);
	
}
Spring.prototype.toString=function(){
	return '[Spring '+this.o1+' x '+this.o2+' strength:'+this.strength+' distance:'+this.distance+']';
}

var global=this;

