/***********
Ring Menu Javascript Object
Copyright 2008 Robert Stegmann
All rights reserved.

Last update 3/1/08
Use an eccentric anomaly calculation to incorporate perspective into the spacing of the item locations in the trajectory

The current algorithm only allows tilted rings with north-south or east-west major axes, no axis at an angle -
it computes the ellipse by stretching a circle.

Usage:
In the html page header section, include the ringmenu-objdefn.js script:
<script type="text/javascript" src="ringmenu-objdefn.js"></script>

then create a ringmenu-instance.js script - within it...
first, declare
var myRM = new ringMenu(... see parameters below)
then, for every item you want in the ring, call
myRM.addItem(...);
note that the first add appears front center, next counter-clockwise from it, etc.
then finally, call
myRM.start();

include the ringmenu-instance.js script in the body of your html page
you may wish to put the ring menu inside a centered table cell:
<body>
...
<center><table border="0" cellspacing="0" cellpadding="0" ><tr><td>
<script type="text/javascript" src="ringmenu-instance.js"></script>
</td></tr></table></center>

************/

function ringMenu(radius,separation,tilt,sense,itemHeight,itemWidth,updateInterval,ringSpanColor,shrink) {
	this.r = ((radius > 0) ? radius : 1);	// radius of the ring in pixels
	this.ih = itemHeight;	// height of an item, should equal image height
	this.iw = itemWidth;	// width of an item

	// seperation multiplier between items in trajectory - a value of 1 only allocates one stop per item
	// which results in a fast, jerky spin.
	// Higher numbers result in smoother, but slow, bouncy trajectories.
	// The more items you have, the lower the # you should use. For around 30 items, try 4.
	this.sep = ((separation > 0) ? separation : 1);

	// this.tiltFactor = (t * t) / (this.r * this.r);	// will range from close to 0, to 1
	// above is OBSOLETE

	// tilt should be between 0 (seen from overhead) <= tilt <= 90 degrees (seen edge-on)
	// for a nearly flat ring, use a value around 88 degrees
	// validate tilt
	var t = ( (tilt >= 0) ? ((tilt <= 90) ? tilt : 90) : 0 );
	// convert tilt from degrees to radians
	var tr = (t * Math.PI)/180;
	this.tiltFactor = Math.cos(tr);	// will range from 0 to 1
	// this.ecc = Math.sin(tr);	// eccentricity also ranges from 0 to 1
	this.ecc = Math.sin(tr/3);	// FAKE eccentricity also ranges from 0 to 1
	// large tilts result in ellipses with large eccentricities
	this.taCoeff = Math.sqrt( (1 + this.ecc) / (1 - this.ecc) );

	// tilt sense - false means pitch, i.e. tilt the ring around a horizontal axis
	// true means yaw - i.e. tilt the ring around a vertical axis
	this.tiltSense = sense;

	this.upd = updateInterval;	// timer interval for rotation in milliseconds - try 10
	this.rsbgc = ringSpanColor;	// background color for the ring
	this.shrink = shrink;		// true if you want perspective adjustment of item by scaling the HEIGHT
						// NOTE corresponding scaling of the WIDTH is NOT SUPPORTED
						// so for now horizontal rings work better than vertical rings

	// These will all be calculated...
	this.nitems = 0;
	this.nsteps = 0;
	this.trajectory = new Array(6);	// x,y pairs, plus scaled height, theta or mean anomaly, eccentric anomaly,
		// scaleFactor
	this.locoIndex = 0;
	// define the centroid of the ring (ellipse) - modify in calcSizes
	this.cdx = this.r;
	this.cdy = this.r;
	this.dir = 1;
	this.intv = 0;
	this.rsw = 0;
	this.rsh = 0;
	this.bsw = 0;
	this.bsh = 0;
	this.minheight = 16;	// used to prevent distant items from totally disappearing
	this.minOpacity = 0.6;	// also used to prevent distant items from totally disappearing
	this.shortRadius = (this.r * this.tiltFactor);
	this.shortAxis = (2 * this.shortRadius);

	// utility to convert radians back to degrees for debug display
	this.rad2deg = function(rads) {
		var d = rads * 180 / Math.PI;
		return d;
	}

	// add an item
	this.addItem = function(msg,linkstr,imgsrc,alttxt,ltarget) {

		// alert(this.nitems);	// for debug

		var str = "<div ";
		str += "id='itemid" + this.nitems + "' ";
		str += "style='";
		str += "cursor:pointer;"; // adjust the div cursor to a hand pointing
		str += "position:absolute;";
		str += "top:0px;";
		str += "left:0px;";
// var divht = this.ih + 2;
// str += "height:" + divht + "px;";
		str += "height:" + this.ih + "px;";
		str += "width:" + this.iw + "px;";
// str += "border: thin solid black;";		// usually for debug, to put a visible boundary around the item
		str += "overflow:hidden;";
		str += "text-align:center;";
		str += "background-color:transparent;";
//		str += "background-color: #ff0000;";	// for debug, to see the item div's extent
// 3/2/08 - try opacity
// 3/3 - seems like too much of a performance hit
// 		str += "opacity:1.0; filter:alpha(opacity=100);";
		str += "' ";
		// set the onclick property of the div to go to the link
		str += "onclick='window.open(\""+linkstr+"\""+(ltarget?(",\""+ltarget+"\""):",\"_self\"")+")'";
		str += " >";
		if(imgsrc) {
// put an image within the div tag and set its height to 100% so it scales to the div
// omit its width, so it maintains its own aspect ratio
// because the div overflow is set to hidden, it will clip if necessary
			str += "<img src='" + imgsrc + "' title='" + alttxt + "' border='0' ";
			str += "height='100%' ";
			str += " />";
		}
/* images WITH messages don't work for now...
the item div is set to ih and the image is set to 100% of the available div height, leaving no room for the msg
within the div
if the image is not set to 100%, then the shrinking effect applied to the div doesn't shrink the image.
To make images with messages work, I'll have to treat the img and the div separately
*/
		else if( msg.length > 0 ) {
			str += msg;
		}
		str += "</div>";

		document.write(str);

		this.nitems++;	// last thing
	}

	this.start = function() {

		// write out the ring span closing tag
		document.write("</div>");
		// write out the controls
		this.writeControls();
		// write out the bounding span closing tag
		document.write("</div>");

		// pre-compute trajectory
		this.nsteps = this.nitems * this.sep;
/*
alert("nsteps= "+this.nsteps+"; tiltFactor= "+this.tiltFactor + "; ecc= " +
this.ecc + "; taCoeff= " + this.taCoeff);	// debug
*/
		this.trajectory[0] = new Array(this.nsteps);
		this.trajectory[1] = new Array(this.nsteps);
		this.trajectory[2] = new Array(this.nsteps);
		this.trajectory[3] = new Array(this.nsteps); // keep theta, the mean anomaly
		this.trajectory[4] = new Array(this.nsteps); // keep E, the eccentric anomaly
		this.trajectory[5] = new Array(this.nsteps);	// keep the scaleFactor, to use in opacity for distance effects

		var thetaIncr = 2 * Math.PI / this.nsteps;
 //   alert("thetaIncr="+thetaIncr + "; degrees= " + this.rad2deg(thetaIncr) );	// debug
		var dx, dy, coord, scaleFactor, hscale, e, angle;

		// init theta so 1st item is properly positioned front and center
		// var theta = Math.PI / 2;
		var theta = 2* Math.PI;

		for( var i=0; i < this.nsteps; i++, theta -= thetaIncr ) {
			// alert("i="+i+",theta="+theta);

			this.trajectory[3][i] = theta;
			e = this.computeE(theta);
			this.trajectory[4][i] = e;

			// for evenly spaced trajectory, use angle = theta
			angle = e;

			// calc as tho it's a circle, then scale to an ellipse according to sense
			if(this.tiltSense) {	// true means yaw - compress the x coord or width
				dx = this.r + (Math.cos(angle) * this.r);
				dy = this.r + (Math.sin(angle) * this.r);
				dx *= this.tiltFactor;
				coord = dx;
			} else {	// false means pitch - compress the y coord or height
				dx = this.r + (Math.sin(angle) * this.r);
				dy = this.r + (Math.cos(angle) * this.r);
				dy *= this.tiltFactor;
				coord = dy;
			}

			this.trajectory[0][i] = Math.round(dx);
			this.trajectory[1][i] = Math.round(dy);

			if(this.shrink) {
				// also calculate the scaled HEIGHT, not width
				// we don't want distant items to totally disappear,
				// but we also don't want foremost items
				// to ever exceed full size (ih)...
				scaleFactor = coord / this.shortAxis;
				hscale = (this.ih - this.minheight) * scaleFactor + this.minheight;
				hscale = Math.round(hscale);
				// alert("sf="+scaleFactor+", hs="+hscale);
				this.trajectory[2][i] = hscale;
				this.trajectory[5][i] = (1.0 - this.minOpacity) * scaleFactor + this.minOpacity;
					// OR try hscale / this.ih;
			} else {
				this.trajectory[5][i] = 1.0;
				this.trajectory[2][i] = this.ih;
			}
		}

		// this.dumpTrajectory();	// for debug only

		// display the items in their proper places for the first time
		this.updateItems();
	}

	this.computeE = function(m) {

		var e1 = m + (this.ecc * Math.sin(m) * ( 1.0 + this.ecc * Math.cos(m)));
		var e2;

		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
		e1 = e2;
		e2 = e1 - ( (e1 - this.ecc * Math.sin(e1) - m) / (1.0 - this.ecc * Math.cos(e1)));
/*
		var diff = e2 - e1;

		while( diff > 0.1 ) {
		}
*/
		return(e2);
		// return(m);
	}

	this.updateItems = function() {
		var iobj, istyle, istr, slot, coord;
		for( var i=0; i < this.nitems; i++ ) {
			slot = (this.locoIndex + i * this.sep) % this.nsteps;
			istr = "itemid" + i + "";
			iobj = document.getElementById(istr);
			istyle = iobj.style;
			istyle.top = this.trajectory[1][slot] + "px";
			istyle.left = this.trajectory[0][slot] + "px";
			// set the z-index - if horizontal, use the y coord
			if(this.tiltSense) {	// true means yaw/vertical
				coord = this.trajectory[0][slot];
			} else {	// false means pitch
				coord = this.trajectory[1][slot];
			}
			istyle.zIndex = coord;
			if(this.shrink) {
				istyle.height = this.trajectory[2][slot] + "px";
				// istyle.opacity = this.trajectory[5][slot];
				// istyle.filter = "alpha(opacity=" + this.trajectory[5][slot]*100 + ")";
			}
		}
	}

	this.dumpTrajectory = function() {
		var str = "nsteps="+ this.nsteps +"\n";
		for( var i=0; i < this.nsteps; i++ ) {
			str += " " + i + ". (" + this.trajectory[0][i] + "," + this.trajectory[1][i] + 
				"," + this.rad2deg(this.trajectory[3][i]).toFixed(2) +
				"," + this.rad2deg(this.trajectory[4][i]).toFixed(2) +
				"," + this.trajectory[5][i].toFixed(2) +
				")   ";
			if( (i%30) == 0 ) str += "\n";
		}
		alert(str);
	}

	// advance, given direction
	this.advance = function() {
		if( this.dir > 0 ) {
			this.locoIndex++;
			if( this.locoIndex >= this.nsteps ) this.locoIndex = 0;
		} else {
			this.locoIndex--;
			if( this.locoIndex < 0 ) this.locoIndex = this.nsteps - 1;
		}
		this.updateItems();
	}

	// onmouseover function for cw control
	this.spinCW = function(damper) {
		this.dir = 1;
		var updfreq = this.upd;
		updfreq *= (damper+1);
		updfreq = Math.round(updfreq);
		var myself = this;
		this.intv = window.setInterval(function(){myself.advance()},updfreq);
	}

	// onmouseout function for both controls
	this.stopSpin = function() {
		window.clearInterval(this.intv);
	}

	// onmouseover function for ccw control
	this.spinCCW = function(damper) {
		this.dir = -1;
		var updfreq = this.upd;
		updfreq *= (damper+1);
		updfreq = Math.round(updfreq);
		var myself = this;
		this.intv = window.setInterval(function(){myself.advance()},updfreq);
	}

	// figure out the width and height for the bounding span and the ring span
	// since the bounding span includes the ring span, do the ring span first
	// update the centroid coords based on tiltFactor and sense
	this.calcSizes = function() {
		// figure out the width and height of the ring span
		this.rsw = this.rsh = 2 * this.r;
		if(this.tiltSense) {
			this.rsw *= this.tiltFactor;
			this.cdx *= this.tiltfactor;
		} else {
			this.rsh *= this.tiltFactor;
			this.cdy *= this.tiltfactor;
		}
		this.rsw += this.iw;
		this.rsw = Math.round(this.rsw);
		this.rsh += this.ih;
		this.rsh = Math.round(this.rsh);

		// figure out the width and height of the bounding span
		// it must include the ring and the controls

		// for now assume controls are always lined up along bottom
		// bounding span width = ring span width
		this.bsw = this.rsw;

		// bounding span height?
		// it must include the ring span height plus the control button height
		// but they don't exist yet!
		// oh well, since overflow is not set to hidden, it will grow as required!
		this.bsh = this.rsh;
	}

	this.openRing = function() {
		// write the ring span opening tag
		var str = "<div id='ringspan' ";
		str += "style='";
//		str += "display : inline;";
		str += "margin-bottom : 0px;";
		str += "position:relative;";
		str += "width:" + this.rsw + "px;";
		str += "height:" + this.rsh + "px;";
		str += "background-color: " + this.rsbgc + ";";
		str += "' ";
		str += ">";
		document.write(str);
	}

	this.openBound = function() {
		// write the bounding span opening tag
		var str = "<div id='bspan' ";
		str += "style='";
//		str += "display : inline;";
		str += "position:relative;";
		str += "width:" + this.bsw + "px;";
		str += "height:" + this.bsh + "px;";
		str += "background-color: " + this.rsbgc + ";";
		str += "' ";
		str += ">";
		document.write(str);
	}

/*
	this.create = function() {
		this.calcSizes();
		this.openBound();
		this.openRing();
	}
*/

	this.writeControls = function() {
		// figure out apropos width for the button - assume equally spread across bounding width bsw
		var butwidth = this.bsw / 7;
		butwidth = Math.floor(butwidth);

		var butbase = "<button style='border:0;width:";
		var omoverccw = "px;' onmouseover='myRM.spinCCW(";
		var omovercw = "px;' onmouseover='myRM.spinCW(";
		var mout = ");' onmouseout='myRM.stopSpin();' >";
		var butend = "</button>";

		var str = "<br style=\"clear:'both';\" />";
		str += butbase + butwidth + omoverccw + 0 + mout + "&lt;&lt;&lt;" + butend;
		str += butbase + butwidth + omoverccw + 4 + mout + "&lt;&lt;" + butend;
		str += butbase + butwidth + omoverccw + 10 + mout + "&lt;" + butend;
		str += butbase + butwidth + "px;' >0</button>";
		str += butbase + butwidth + omovercw + 10 + mout + "&gt;" + butend;
		str += butbase + butwidth + omovercw + 4 + mout + "&gt;&gt;" + butend;
		str += butbase + butwidth + omovercw + 0 + mout + "&gt;&gt;&gt;" + butend;

		document.write(str);
	}

		/* call these functions on new, and eliminate the call to create()... */
	this.calcSizes();
	this.openBound();
	this.openRing();


}
