// gpUtilsMulti.js
// GeoPlayer instance code, designed for GeoPlayerWEBDEV.
//
// This code provides general access to the GeoPlayer instance. Note that the
// gpOrbitXXXX routines require that orbit.js be already loaded. And, for the sun subsolar
// point to be properly registered, equationOfTime.js must be already loaded.

// For debugging and recording, all function calls are made to this.gpObject rather than
// the true GeoPlayer object. this.gpObject is set up when debug.js is loaded; if debug.js
// is not loaded then this.gpObject is just set to a copy of GeoPlayer. (This doesn't work
// for setting GeoPlayer properties, so they are set directly and can't be traced.)

/********************************* Main access variables *********************************/

// This makes sure that there are "debug" and "debugln" functions, even if they
// do nothing.
if (this.debug === undefined) {
	this.debug = function(msg) {};
}
if (this.debugln === undefined) {
	this.debugln = function(msg) {};
}

// These variables are all properties of the top window object. Only initialize them if they
// don't already exist.
if (top.gpObjects == undefined) {
	top.gpObjects = new Object();		// Property list of GPObject objects
	top.gpCount = 0;					// Number of objects attached (incremented after
										// objects created/registered)
	top.gpListener = new Object();		// Listener object for GeoPlayer events
}

/********************************* Main access functions *********************************/

// This attaches to a new GeoPlayer object and assigns it a name. Note that this cannot
// be called until the GeoPlayer tag exists! Arguments:
//	attachName: name that will refer to the GPObject
//	tagName: name of tag that instantiated GeoPlayer object
function gpAttach(attachName, tagName) {
//	alert("gpAttach(" + attachName + ", " + tagName + ")");
	// The ID of the chosen object is the number of objects already created. Note that
	// this will NOT be unique when several pages are open. However, since there's no
	// way to route callbacks to the correct object if the callback and object are on
	// separate pages, we just gotta hope that the callback comes to the correct
	// page.
	var obj = new GPObject(tagName, top.gpCount, attachName);
	top.gpObjects[attachName] = obj;
	top.gpCount++;
//	alert("Attachment done: (typeof top.gpObjects[attachName]) " + (typeof top.gpObjects[attachName]));
}

// This makes a method call on a GeoPlayer object, returning any result.
function gpCall(attachName, methodName, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) {
	// Assume that receiving method ignores excess arguments
	if (!top.gpObjects[attachName]) {
		alert("**** Error in gpCall(\"" + attachName + "\", \"" + methodName 
					+ "\"): no such GeoPlayer object " + attachName + " ****");
		return null;
	} else if (typeof top.gpObjects[attachName][methodName] != "function") {
		alert("**** Error in gpCall: no such method " + methodName + " ****");
		return null;
	} else {
		debugln("gpCall(" + attachName + ", " + methodName + "...)");
		var result = top.gpObjects[attachName][methodName](arg1, arg2, arg3, arg4, arg5, 
							arg6, arg7, arg8);
		return result;
	}
}

// This takes a relative URL and converts it to an absolute URL based on the
// current window's URL. Also removes any leading "file:///" (GeoPlayer chokes)
function gpRelToAbs(relURL) {
	var docURL = location.href;
	
	// Create ugly absolute URL
	var absURL = docURL.substring(0, docURL.lastIndexOf("/") + 1) + relURL;
	
	// Split into /-separated parts
	var parts = absURL.split("/"); 
	
	// After protocol and host spec, remove all "/./"s and "<someDir>/../"s
	for (var i = 4; i < parts.length; i++) {
		if (parts[i] == "..") {
			// Delete .. and preceeding directory
			parts.splice(i - 1, 2);
			i -= 2;		// Will be incremented by for loop
		} else if (parts[i] == ".") {
			// Delete .
			parts.splice(i, 1);
			i--;		// Will be incremented by for loop
		}
	}
	
	// Rejoin
	absURL = parts.join("/");
	
	// If starts with "file:///", then delete that
	if (absURL.indexOf("file:///") == 0) {
		absURL = absURL.slice(8);
	}
	
	return absURL;
}

// This is the global listener, which receives all callbacks from GPObjects.
// It identifies the objects by their id. It just calls every known object,
// specifying the id; only the correct object will handle the call.
function gpCallback(messageID, messageData, objectID) {
//	alert("gpCallback(" + messageID + ", " + messageData + ", " + objectID + ")");
	for (var attachName in top.gpObjects) {
		var obj = top.gpObjects[attachName];
		obj.callback(messageID, messageData, objectID);
	}
}

top.gpListener.GeoPlayerCB = gpCallback;		// New-style callback
// top.gpListener.sessionTasksCB = gpCallback;		// Old-style callback (pre-2,0,0,11)

/********************************* Lagger object *********************************/

// This creates an object which watches a changing input value and decides when the change
// is significant. Fast changes are quickly significant; slow changes take a while before
// they are significant.
// NOTE: I've actually disabled the subtlety of this code, because for it to work it would
// have to be working on user-scaled values rather than GeoPlayer-scaled values, and that
// would make integrating it with the code a royal pain in the wazoo. So, it now declares
// any change significant.

// Constructor. Arguments:
//		threshold: Amount a value must change by to be immediately declared significant
//		decay: Amount threshold is multiplied by each time a new value is not declared
//				significant. Will be somewhat less than 1, so that
//				as time passes without a "significant" change the threshold slowly
//				reduces. Eventually a very small change will be significant.
//		name: Name of logger (for debugging)
function Lagger(threshold, decay, name) {
	this.initialThreshold = threshold;
	this.threshold = null;				// Will be set when value is set
	this.decay = decay;
	this.lastValue = null;				// Null => first input value will be significant
	this.thisValue = null;
	this.name = name;
	this.debugging = false;				// If true then calls debugln on each access
//	debugln("Created logger with name " + this.name);
}

// This accepts a new value, returning true if the value change is significant.
Lagger.prototype.setValue = function(theValue) {
	this.thisValue = theValue;
	if (this.lastValue === null) {
		this.debug("setValue(" + theValue + "): first time, so returned true");
		return true;
	} else {
//		var significant = (Math.abs(this.thisValue - this.lastValue) > this.threshold);
		var significant = (Math.abs(this.thisValue - this.lastValue) > 0);
		this.threshold *= this.decay;
		this.debug("setValue(" + theValue + "): " 
								+ (significant ? "" : "not") + " significant");
		return significant;
	}
}

// This returns the most recent value, resetting the threshold.
Lagger.prototype.getValue = function() {
	this.debug("getValue(): returning " + this.thisValue);
	this.lastValue = this.thisValue;
	this.threshold = this.initialThreshold;
	return this.thisValue;
}

// If debugging, then this displays an message using debugln
Lagger.prototype.debug = function(msg) {
	if (this.debugging) {
		debugln("Lagger " + this.name + ": " + msg + "; debugging " + this.debugging);
	}
}

/********************************* GeoPlayer constants *********************************/

// New values
var gmxTRUE = 1;
var gmxFALSE = 0;

var gmxNULL = 0;
var gmxNONE = -1;
var gmxSTRING = 0;
var gmxVEC1B = 1;
var gmxVEC2B = 2;
var gmxVEC3B = 3;
var gmxVEC4B = 4;
var gmxVEC1I = 5;
var gmxVEC2I = 6;
var gmxVEC3I = 7;
var gmxVEC4I = 8;
var gmxVEC1F = 9;
var gmxVEC2F = 10;
var gmxVEC3F = 11;
var gmxVEC4F = 12;
var gmxVEC1D = 13;
var gmxVEC2D = 14;
var gmxVEC3D = 15;
var gmxVEC4D = 16;
var gmxSVECTOR = 17;
var gmxQVECTOR = 18;
var gmxMATRIX = 19;
var gmxVIEW_PARAMETERS = 0x30000;
var gmxORBVECTOR = 0x30001;

var gmxTHRESHOLD = 0;
var gmxLINEAR = 1;
var gmxCUBIC = 3;
  
var gmxMANAGER = 0;
var gmxVIEW_MGR = 0x20000;
var gmxFONT_MGR = 0x20002;
var gmxIMAGE_MGR = 0x20004;
var gmxMODE_ENABLE = 0x20005;
var gmxW3D_MGR = 0x40000;
var gmxW2D_MGR = 0x40001;

var gmxDATASET = 0x20000;
var gmxGLOBE = 0x20001;
var gmxSCENE = 0x20002;
var gmxSCRIPT = 0x20003;
var gmxTEXTURE = 0x30000;		// Changed from 0x20004 to 0x30000; using for image/texture
var gmxLOCATION = 0x40000;
var gmxSCREEN_OBJECT = 0x40000;
var gmxWORLD_OBJECT = 0x40000;
var gmxW3D_INSTANCE = 0x30000;
var gmxFONT = 0x30000;
var gmxBITMAP_FONT = 0x30000;
var gmxTEXTURE_FONT = 0x30001;
var gmxCREDIT = 0x20000;
var gmxTEXT_BOX = 0x20001;
var gmxBILLBOARD = 0x20002;
var gmxVECTOR_OBJECT = 0x20003;
var gmxLABEL = 0x20004;
var gmxTAG_ATTRIBUTES = 0x20000;

var gmxGLOBEVIEW = 0x20000;
var gmxDATAVIEW = 0x20000;
var gmxEVALUATOR = 0x20000;
var gmxKEYPOINT = 0x20000;
var gmxWINDOW = 0x50000;
var gmxVIEWPORT = 0x20000;

var gmxBILLBOARD_INSTANCE = 0x20002;
var gmxVECTOR_OBJECT_INSTANCE = 0x20003;

var gmxFLY_DIRECT = 0;
var gmxFLY_PARABOLA = 1;
var gmxFLY_IN = 0;
var gmxFLY_OUT = 1;
var gmxFLY_OVER = 3;

// ListOperation() methods
var mFIRST = 0x000000;
var mLAST = 0x00001;
var mPREVIOUS = 0x00002;
var mNEXT = 0x00003;
var mBEFORE = 0x00004;
var mAFTER = 0x00005;

// This may change!!!!!!!!!
var mVECTOR1 = 0x3000e;
var mFRUSTUM_ANGLE = 0x3000c;

var gpSESSION_FINISHED = 500;
var gpTASK_FINISHED = 501;
var gpFLY_TO_FINISHED = 502;
var gpOBJECT_CLICKED = 503;
var gpGLOBE_CLICKED = 504;


/********************************* GPObject definition *********************************/

// Constructor
function GPObject(tagName, idArg, attachNameArg) {
//	alert("GPObject(" + tagName + ", " + idArg + ")");
	this.name = tagName;
	this.id = idArg;
	this.attachName = attachNameArg;
//	alert("Getting by ID " + tagName + "...");
	this.gpObject = document.getElementById(tagName);
//	alert("gotten, typeof " + (typeof this.gpObject));

	this.initialized = false;		// True if the GeoPlayer has been initialized
	this.listener;					// Object that listens to GeoPlayer [not used??]
	this.sceneID;					// The ID of the first scene
	this.globeviewID;				// The ID of the first scene's first globe attachment.
	this.globeID;					// The ID of the first scene's first globe.
	
	// var gpBillboardTextureID = 1112;
	// var gpBillboardID = 1108;
	// var gpLabelID = 1109;
	// var gpBillboardInstanceID = 1110;
	// var gpLabelInstanceID = 1111;
	
	this.animating;					// True iff we're animating
	this.animationScriptID;			// The ID of the created animation script
	this.animationEvaluatorID;		// The ID of the created animation evaluator
	this.animationStartTime;		// Time of animation start (from (new Date()).getTime() )
	this.animationKeypoints;		// Array of keypoints, stored with older keypoints at lower
									// indices. Each keypoint is represented by an object:
									//		time: Keypoint time (after this.animationStartTime)
									//		id: ID of keypoint
									//		lat: Latitude
									//		lon: Longitude
									//		alt: Altitude
	this.animationKeypointID;		// ID of most recently created keypoint
	this.maxKeypointID = 1000;		// Maximum value of keypoint ID
	this.animationBearing;			// Bearing, in degrees, of current motion while animating
	
	this.orbiting;					// True iff we're orbiting
	this.orbitTimer;				// Interval timer for orbit handling
	this.orbitKeypoints = 5;		// Number of keypoints maintained at any one time
	this.orbitInterval = 2 * 1000;	// Interval between orbital keypoints (ms)
	this.orbitAltitudeOffset = 0;	// Added to altitude when orbiting
	
	this.seekingNorth;				// True if we should seek north
	this.seekingForwards;			// True if we should seek forwards while animating.
	this.seekingForwardsOffset;		// Offset from true forwards-seeking direction
	this.sunNow = true;				// True if sun should track "now" (offset by dateOffset)
	this.subsolar;					// Location of sun (Object with lat and lon properties)
	this.radius;					// Radius of Earth in km
		
	this.flying = false;			// True if in the middle of a gpFlyToLocation
	this.flyToLocationID;			// The ID of the created fly-to location
	this.flyToEvaluatorID = 1001	// ID of created fly-to frustum evaluator (fixed for now)
	this.flyToEvaluatorKeyCount = 10;	// Number of keys. IDs of keys are assigned starting
									// at 0.
	this.flyingStarted;				// Time [(new Date()).getTime()] of start of flight
	this.flyingDuration;			// Duration (in ms) of flight
	
	this.updateInterval = 0.050;	// Time between motion updates (in seconds)
	this.lastUpdate;				// Time of last view update (from (new Date()).getTime() )
	this.sessionLoaded = false;		// Set to true by callback when session file loaded
	this.dateOffset;				// Milliseconds offset from real time.
	
	this.worldObjectInstanceTypes	// Map from world object IDs to types of their instances
			= new Object();
	// Hack: for using the billboard created in the session file
	this.worldObjectInstanceTypes[1] = gmxBILLBOARD_INSTANCE;
	
	/********************************* Motion variables *********************************/
	
	// Since we don't allow tipping the view left or right, this code has six degrees of
	// freedom (including visual zoom). Each degree is represented by a pair of values:
	// one in User space (that has nice speed values when looking or moving around) and 
	// one in GeoPlayer space (that matches a GeoPlayer property). In general, the User space 
	// values are used for speeds, and the GeoPlayer space values are used for absolute values.
	// Note that sometimes the user and GeoPlayer values are of opposite sign.
	// The degrees are:
	//
	//	User		GeoPlayer		Description
	//  ------------------------------------------------------------------------------
	//	MoveFB	   -OrbitY			Movement forward (+) or back (-), relative to view direction
	//									(adjusted to be in the horizontal plane)
	//	MoveRL	   -OrbitX			Movement right (+) or left (-), relative to view direction
	//	MoveUD		Elevate			Movement up (+) or down (-)
	//	LookRL		Spin			Looking right (+) or left (-)
	//	LookUD		Tilt			Looking up (+) or down (-)
	//	Zoomfactor	Frustum			How much the view is magnified (name avoids func gpZoom)
	//	Stretchfactor mTERRAIN_SCALE How much terrain is magnified vertically
	//								(name avoids function gpStretch)
	//
	// (Zoomfactor is synthetic; it is equal to pixels per degree at the center of the screen.
	// This avoids the sudden changes in the FRUSTUM value when the window is resized.)
	//
	// Each of these values can have up to eight variables defined by suffixes added to the
	// base name. The suffixes are:
	//		gpXXXX				Base value
	//		gpXXXXMax			Maximum value
	//		gpXXXXMin			Minimum value
	//		gpXXXXControl		User request for change (1 => increase, -1 => decrease, 0 => none)
	//								Note that the values can be fractions, for example:
	//								(this.moveFBControl == 0.5) means user wants to move forward
	//								at half-speed
	//		gpXXXXControlOld	Previous user request
	//		gpXXXXSpeed			Change in value per second (linear or multiplicative)
	//		gpXXXXSpeedMax		Maximum gpXXXXSpeed
	//		gpXXXXSpeedTC		Time constant for smoothing speed
	// Note that most values will have only some of these variables defined as globals, as they
	// aren't used more than temporarily.
	
	// MoveFB/OrbitY
	this.moveFBControl = 0;
	this.moveFBSpeed = 0;
	this.moveFBSpeedMax = 50;		// Angular velocity at Zoomfactor 1
	this.moveFBSpeedTC = 3;			// Seconds
	
	// MoveRL/OrbitX
	this.moveRLControl = 0;
	this.moveRLSpeed = 0;
	this.moveRLSpeedMax = 50;		// Angular velocity at Zoomfactor 1
	this.moveRLSpeedTC = 3;			// Seconds
	
	// MoveUD/Elevate
	this.moveUDControl = 0;
	this.moveUDSpeed = 0;
	this.moveUDSpeedMax = 1000;
	this.moveUDSpeedTC = 5;			// Seconds
	
	// LookRL/Spin
	this.lookRLControl = 0;			// User request
	this.lookRLSpeed = 0;
	this.lookRLSpeedMax = 200;		// Pixels per second
	this.lookRLSpeedMaxAngle = 10;	// Degrees per second
	this.lookRLSpeedTC = 1;			// Seconds
	
	// LookUD/Tilt
	this.lookUDControl = 0;			// User request
	this.lookUDSpeed = 0;
	this.lookUDSpeedMax = 200;		// Pixels per second
	this.lookUDSpeedTC = 2;			// Seconds
	
	// Zoomfactor/Frustum
	this.zoomfactorControl = 0;		// User request
	this.zoomfactorSpeed = 0;
	this.zoomfactorSpeedMax = 0.4;	// Zoom factor change, ratio per second, minus 1
	this.zoomfactorSpeedTC = 2;
	this.zoomfactorMin = 20.7;		// Minimum gpZoomfactor (20.7 = 0.59 km/pixel at 700km)
	this.zoomfactorMax = 2000;		// Maximum gpZoomfactor (680= 0.018km/pixel at 700km)
	
	// Stretch/TERRAIN_SCALE
	this.stretchfactor = 1;			// current stretch
	this.StretchfactorControl = 0;
	this.StretchfactorSpeed = 1.2;	// Ratio change in stretch per second (constant)
	this.StretchfactorMax = 10;
	this.StretchfactorMin = 0.5;
	
	// Laggers. These objects determine when a value has changed enough to be sent to
	// the GeoPlayer instance.
	this.tiltSpeedLagger = new Lagger(0.1, 0.9, "tiltSpeed");
	this.spinSpeedLagger = new Lagger(0.1, 0.9, "spinSpeed");
	this.frustumSpeedLagger = new Lagger(0.1, 0.9, "frustumSpeed");
	this.orbitHSpeedLagger = new Lagger(0.001, 0.9, "orbitHSpeed");
	this.orbitVSpeedLagger = new Lagger(0.001, 0.9, "orbitVSpeed");
	this.elevateSpeedLagger = new Lagger(0.1, 0.9, "elevateSpeed");
	this.stretchfactorLagger = new Lagger(0.1, 0.9, "stretchfactor");
//	alert("typeof this.gpObject " + (typeof this.gpObject));
//	alert("typeof this.gpObject.AddListener " + (typeof this.gpObject.AddListener));
	
	// Set up listening to GeoPlayer events (skip if no gpObject)
	if (this.gpObject && (typeof this.gpObject.AddListener != "undefined")) {
//		alert("About to add listener...");
		this.gpObject.AddListener(this.id, top.gpListener);
//		alert("Added listener.");
	} else {
//		alert("Not adding listener");
	}
}

// All the GPObject methods will be attached below.

/********************************* gpTracer object *********************************/

// This creates a new GPTracer object that can watch a series of periods and monitor the
// min/max/average. A period is added by a pair of begin()/end() calls, and equals the time
// (in ms) between the two calls.
function GPTracer(numSamples) {
	// This stores the max number of samples
	this.numSamples = numSamples;
	// This stores the samples
	this.samples = new Array();
	// This is the time of the latest begin() call. Null if none since the last end().
	this.beginTime = null;
}

// This starts a collection period
GPTracer.prototype.begin = function() {
	this.beginTime = (new Date()).getTime();
}

// This ends a collection period
GPTracer.prototype.end = function() {
	if (this.beginTime != null) {
		var diff = (new Date()).getTime() - this.beginTime;
		this.samples.unshift(diff);
		if (this.samples.length > this.numSamples) {
			this.samples.pop();
		}
		this.beginTime = null;
	}
}

// This returns an object describing the GPTracer's status. The object has
// properties min, max and average.
GPTracer.prototype.status = function() {
	// Special case if no samples yet
	if (this.samples.length == 0) {
		return "(no samples)";
	} else {
		// Find min, max, sum
		var min = Number.MAX_VALUE;
		var max = -Number.MAX_VALUE;
		var sum = 0;
		for (var x = this.samples.length - 1; x >= 0; x--) {
			min = Math.min(min, this.samples[x]);
			max = Math.max(max, this.samples[x]);
			sum += this.samples[x];
		}
		// Build and return object
		return {min:min, max:max, average:(sum / this.samples.length)};
	}
}

/********************************* Debugging *********************************/

var gpNoDeleteKeypoint = false;	// True if shouldn't delete keypoints
debugging = false;
debugFlushAlways = false;

// Tracing
var gpTraceViewPeriod = new GPTracer(50);	// Time between this.updateView() calls
var gpTraceViewDuration = new GPTracer(50);		// Duration of this.updateView() calls
var gpTraceLocationPeriod = new GPTracer(10);	// Time between this.getLocation() calls
var gpTraceLocationDuration = new GPTracer(10);	// Duration of this.getLocation() calls

// Returns current status as an object with properties. Properties are:
//		traceViewPeriod: time between gpUpdateView calls (in ms)
//		traceViewDuration: time taken by gpUpdateView calls (in ms)
//		traceLocationPeriod: time between gpGetLocation calls (in ms)
//		traceLocationDuration: time taken by gpGetLocation calls (in ms)
// Each property is itself an object with three properties: min, max and average.
GPObject.prototype.status = function() {
	var tvp = gpTraceViewPeriod.status();
	var tvd = gpTraceViewDuration.status();
	var tlp = gpTraceLocationPeriod.status();
	var tld = gpTraceLocationDuration.status();
	return {traceViewPeriod:tvp, 
			traceViewDuration:tvd,
			traceLocationPeriod:tlp,
			traceLocationDuration:tld};
}

/********************************* Initialization *********************************/

// Handles GeoPlayer events. Ignores ones with wrong id.
GPObject.prototype.callback = function(objectID, messageID, messageData) {
	if (this.id == objectID) {
		// That's me!
		// Handle both new and old messages (delete latter once 2,0,0,11 comes out)
//		alert("callback(" + objectID + ", " + messageID + ", \"" + messageData + "\")");
		if (messageID == gpSESSION_FINISHED) {	// Old: messageData == "session complete"
			this.sessionLoaded = true;
			//debugln("      sessionTasksCB: succeeded");
		} else if (messageID == gpFLY_TO_FINISHED) {
//			alert("Fly-to done!");
			//gpFlying = false;
		} else if (messageID == gpOBJECT_CLICKED) {
//			alert("Object clicked: " + messageData);
		} else {
			/* ************ Remove this before production! ************** */
			//alert("gpCallback(" + id + ", \"" + data + "\"); unknown message");
		}
	}
}

// Initializes the GeoPlayer interface. Takes the radius of the current globe. 
// Returns true if succeeds; if not, then try again later. If you call other routines
// before this returns true, then those calls will have no effect. Arguments:
//		theRadius: the radius of the globe (in km)
//		theDateOffset: optional offset (in ms) from the real "now" for orbit purposes
GPObject.prototype.init = function(theRadius, theDateOffset) {
	debugln("init(" + theRadius + ", " + theDateOffset + ")." + this.name + " called...");
	
	if(!this.sessionLoaded) {
		debugln("  this.sessionLoaded = false");
		return false;
	}
	
	debugln("  this.sessionLoaded = true, proceeding ...");
	if (theDateOffset !== undefined) {
		this.dateOffset = theDateOffset;
	} else {
		this.dateOffset = 0;
	}
	
	this.radius = theRadius;
	
	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	var result = this.gpObject.Get("VERSION");
//	debugln("      sessionTasksCB: Version=" + result);

	// Initialize local state
	this.animationKeypoints = new Array();
	this.animating = false;
	this.orbiting = false;
	this.seekingNorth = false;
	this.seekingForwards = false;
	this.animationKeypointID = 0;
	this.lastUpdate = (new Date()).getTime();
	
	// Set global properties of GeoPlayer. (Do this here so it happens on the first
	// call to this.init(), even if session file not yet loaded.)
	// Note: setting this value to ANYTHING turns on the progress bar, while setting
	// it again to ANY value turns it back off.
//	this.gpObject.showprogress = 0;
//	this.gpObject.SetInput(0, false); // keyboard input off
//	this.gpObject.SetInput(1, false); // mouse input off
	
	// Get ID of first scene, and first globeview; if fail, return false.
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.sceneID = this.gpObject.ListOperation(mFIRST, gmxSCENE, gmxNONE);
	if (this.sceneID != -1) {
		this.globeviewID = this.gpObject.ListOperation(mFIRST, gmxGLOBEVIEW, gmxNONE);
		this.setStoreScene("NORTH_UP", "true");
	} else {
		return false;
	}

  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.globeID = this.gpObject.ListOperation(mFIRST, gmxGLOBE, gmxNONE);

	// Create script and assign its ID
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.Create(gmxSCRIPT, gmxNONE);
	this.animationScriptID = this.gpObject.Get("ID");
	
	this.setupFlyTo();

	// Set GeoPlayer to yield() after every frame, and reduce the priority.
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
  	// Reduce GeoPlayer's priority. Possible values:
  	//		-1: Normal (GeoPlayer draws whenever it can)
  	//		 0: Drawing thread yields after every frame
  	//		 1: Drawing thread priority reduced 1 step
  	//		 2: Drawing thread priority reduced 2 steps
	this.gpObject.Set("REDUCE_PRIORITY", 1);
	
	setInterval(gpMakeDelegate(this, this.updateView), this.updateInterval * 1000);

	this.initialized = true;
	this.setSunTime(0);
	
//	debugln("    init() succeeded!");
	
	return true;
}

/********************************* Orbiting *********************************/

// The gpOrbitXXXX functions use two facilities:
//		- the orbit.js orbital calculation functions
//		- the gpAnimationXXXX functions
// So, to use the gpOrbitXXXX you must:
//		- have loaded the orbit.js code
//		- keep your hands off the gpAnimationXXXX functions

// This starts orbiting with a two-line element. Arguments:
//		tle: The two-line element
//		altitudeDiff: Added to altitude at each orbit point (optional)
//		dateOffsetArg: New value for dateOffset (null if keep value)
GPObject.prototype.orbitStart = function(tle, altitudeOffset, dateOffsetArg) {
	debugln("orbitStart(<tle>, " + altitudeOffset + ")." + this.name 
				+ " called, with dateOffset " + this.dateOffset);
	if (!this.initialized) {
		return;
	}
	this.orbitStop();
	if (dateOffsetArg != null) {
		this.dateOffset = dateOffsetArg;
	}
	this.setSunTime(0);
	if (altitudeOffset !== undefined) {
		this.orbitAltitudeOffset = altitudeOffset;
	} else {
		this.orbitAltitudeOffset = 0;
	}
		
	orbitSetTLE(tle);
	var nowMS = (new Date()).getTime() + this.dateOffset;
	var keyTime = nowMS - (this.orbitKeypoints/2 - 1) * this.orbitInterval;
	this.animationPrep(nowMS);
	for(var x = 0; x < this.orbitKeypoints; x++) {
		var loc = orbitGetLocation(new Date(keyTime));
		this.animationAdd(loc.lat, loc.lon, loc.alt + this.orbitAltitudeOffset, keyTime);
		keyTime += this.orbitInterval;
	}
	this.animationStart();
	this.orbitTimer = setInterval(gpMakeDelegate(this, this.updateOrbit), this.orbitInterval);
	this.orbiting = true;
	debugln("    orbitStart()." + this.name + "  returned.");
	this.setSunTime(0);
}

// This goes to a specific point in the orbit,. Arguments:
//		tle: The two-line element
//		time: Time since epoch in ms
//		altitudeOffset: Added to height before display (optional)
GPObject.prototype.orbitGo = function(tle, time, altitudeOffset) {
	debugln("orbitGo(tle, " + time + ", " + altitudeOffset + ")." + this.name);
	if (altitudeOffset === undefined) {
		altitudeOffset = 0;
	}
	this.orbitStop();			// Just in case still orbiting
	this.setSunTime(time);
	orbitSetTLE(tle);
	var loc = orbitGetLocation(new Date(time));
	if (loc.lon > 180) {
		loc.lon -= 360;
	}
	var curLocation = this.getLocation();
	var newLocation = new Object();
	newLocation.lla = [loc.lat, loc.lon, loc.alt + altitudeOffset];
	newLocation.att = curLocation.att;
	newLocation.att[0] = 90;			// !!!! HACK !!!!
//	if (this.seekingForwards) {
//		// Should adjust azimuth, but Not Just Yet.
//	}
	newLocation.dist = curLocation.dist;
	newLocation.mode = curLocation.mode;
//	debugln("    lat " + newLocation.lla[0] + ", lon " + newLocation.lla[1]);
//	this.setLocation(newLocation);
//	curLocation = this.getLocation();
//	debugln("    => set to lat " + curLocation.lla[0] + ", lon " + curLocation.lla[1]);
	this.flyToLocation(newLocation, [8.4, 8.4], 1, "DIRECT");
	this.seekingForwards = false;
}

// This stops orbiting.
GPObject.prototype.orbitStop = function() {
	if (!this.initialized || !this.orbiting) {
		return;
	}
	clearInterval(this.orbitTimer);
	this.orbiting = false;
	this.animationClear();
}

/********************************* Animation *********************************/

// This code must handle a GeoPlayer flaw: the keypoint times can't be very large. So,
// everything is done relative to the start time, as stored in gAnimationStartTime.
// This in turn means that gAnimationPrep(), gAnimationAdd() and gAnimationStart() must
// be called in quick succession. Sorry.

// This prepares the animation code. Must be called right before
// building an animation.
GPObject.prototype.animationPrep = function(startMS) {
//	debugln("animationPrep() called...");
	this.animationStartTime = startMS;
	
	this.animationKeypointID = 0;
	
	// Create evaluator and get its ID
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCRIPT, this.animationScriptID);
	this.gpObject.Set("ENABLE", "false");
	this.gpObject.Create(gmxEVALUATOR, gmxORBVECTOR);
	this.animationEvaluatorID = this.gpObject.Get("ID");
//	this.gpObject.Set("INTERPOLATION", gmxCUBIC);
	this.gpObject.Set("INTERPOLATION", "CUBIC");
	
	// Bind the animation evaluator to the scene object.
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCENE, this.sceneID);
	this.gpObject.Set("BIND_EVALUATOR", 
				"[" + this.animationScriptID + "," + this.animationEvaluatorID 
				+ "," + mVECTOR1 + "]");
//	debugln("    animationPrep()." + this.name + " returned.");
}	

// This adds a keypoint to the current animation. The arguments are:
//		lat: Latitude
//		lon: Longitude
//		alt: Altitude
//		time: The keypoint in ms (e.g. from (new Date()).getTime() )
// Note: time should already have been offset by this.dateOffset.
GPObject.prototype.animationAddOld = function(lat, lon, alt, time) {	
//	debugln("animationAdd(" + lat + ", " + lon + ", " + alt + ", " 
//							+ time + ")." + this.name + " called");
	if (!this.initialized) {
		return;
	}

	// Navigate to evaluator
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCRIPT, this.animationScriptID);
	this.gpObject.Set("ENABLE", "false");	// Disable script (no effect if not animating)
	this.gpObject.AccessByID(gmxEVALUATOR, this.animationEvaluatorID);
	
	// Create and configure keypoint
	this.gpObject.Create(gmxKEYPOINT, gmxNONE);	// access now set to keypoint
	// Wrap keypoint ID
	if (++this.animationKeypointID > this.maxKeypointID) {
		this.animationKeypointID = 1;
	}
	this.gpObject.Set("ID", this.animationKeypointID);
	var keyTime = (time - this.animationStartTime)/1000;
	this.gpObject.Set("TIME", keyTime);
	var keypointValue = "{\"vec\":[" + lat + "," + lon
							+ "," + alt + "],\"radius\":" + this.radius + "}";
//	debugln("    adding keypoint " + keypointValue);
	this.gpObject.Set("VALUE", keypointValue);
	
	// Remember this control point
	var keypointDesc = new Object();
	keypointDesc.time = time;
	keypointDesc.id = this.animationKeypointID;
	keypointDesc.lat = lat;
	keypointDesc.lon = lon;
	keypointDesc.alt = alt;
	this.animationKeypoints.push(keypointDesc);
		
	// Navigate up to evaluator
	this.gpObject.AccessByID(gmxNONE, this.animationEvaluatorID);
	
	// Delete all keypoints where second subsequent point is still in the past
	// (so that "now" is still in smooth region after first two keypoints)
	if (!gpNoDeleteKeypoint) {
	// **** NOTE!!! The following commented-out code is necessary when adding more than this.orbitKeypoints
	// **** keypoints into the future. It was removed in the hopes that it would
	// **** improve reliability; along with other changes, it has, so we'll leave
	// **** it in place.
	//	var now = (new Date()).getTime() + this.dateOffset;
	//	while (this.animationKeypoints.length >= 3 && this.animationKeypoints[2].time < now) {
		if (this.animationKeypoints.length > this.orbitKeypoints) {
	//		debugln("    deleting keypoint id " + this.animationKeypoints[0].id
	//					+ " at time " + this.animationKeypoints[2].time);
//			this.gpObject.AccessByID(gmxKEYPOINT, this.animationKeypoints[0].id);
//			this.gpObject.Delete();
			this.gpObject.DeleteByID(gmxKEYPOINT, this.animationKeypoints[0].id);
			this.animationKeypoints.shift();
		
	//		debugln("    Deleted keypoint");
		}
	}
	
	// If animating, recompute control points and enable script
	if (this.animating) {
		this.gpObject.Set("CONTROL_POINTS", "true");	// compute control points
		// Navigate up to script, enable it
		this.gpObject.AccessByID(gmxNONE, this.animationScriptID);
		this.gpObject.Set("ENABLE", "true");
	}
//	debugln("    animationAdd()." + this.name + " returned");
}	

GPObject.prototype.animationAdd = function(lat, lon, alt, time) {	
//	debugln("animationAdd(" + lat + ", " + lon + ", " + alt + ", " 
//							+ time + ")." + this.name + " called");
	if (!this.initialized) {
		return;
	}
	var xmlstr = "\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\n" +
	"<gmx_document>\n" +
	"<gmx_session version=\\\"2.0.0\\\">\n";

	// Navigate to evaluator
  	xmlstr += "<access_by_name type=\\\"GMX_MANAGER\\\" name=\\\"VIEW_MANAGER\\\">\n";
  	xmlstr += "  <access_by_id type=\\\"SCRIPT\\\" id=\\\"" + this.animationScriptID + "\\\">\n";
  	xmlstr += "    <set m=\\\"ENABLE\\\">false</set>\n";
  	xmlstr += "    <access_by_id type=\\\"EVALUATOR\\\" id=\\\"" + this.animationEvaluatorID + "\\\">\n";
  	
	// Wrap keypoint ID
	if (++this.animationKeypointID > this.maxKeypointID) {
		this.animationKeypointID = 1;
	}

	// Create and configure keypoint
  	xmlstr += "      <create_by_id type=\\\"KEYPOINT\\\" subtype=\\\"NONE\\\" id=\\\"" + this.animationKeypointID + "\\\">\n";
	var keyTime = (time - this.animationStartTime)/1000;
  	xmlstr += "        <set m=\\\"TIME\\\">" + keyTime + "</set>\n";
	var keypointValue = "{\\\"vec\\\":[" + lat + "," + lon + "," + alt + "],\\\"radius\\\":" + this.radius + "}";
//	debugln("    adding keypoint " + keypointValue);
  	xmlstr += "        <set m=\\\"VALUE\\\">" + keypointValue + "</set>\n";

	// Remember this control point
	var keypointDesc = new Object();
	keypointDesc.time = time;
	keypointDesc.id = this.animationKeypointID;
	keypointDesc.lat = lat;
	keypointDesc.lon = lon;
	keypointDesc.alt = alt;
	this.animationKeypoints.push(keypointDesc);
		
	// Navigate up to evaluator
  	xmlstr += "      </create_by_id>\n";
	
	// Delete all keypoints where second subsequent point is still in the past
	// (so that "now" is still in smooth region after first two keypoints)
	if (!gpNoDeleteKeypoint) {
	// **** NOTE!!! The following commented-out code is necessary when adding more than this.orbitKeypoints
	// **** keypoints into the future. It was removed in the hopes that it would
	// **** improve reliability; along with other changes, it has, so we'll leave
	// **** it in place.
	//	var now = (new Date()).getTime() + this.dateOffset;
	//	while (this.animationKeypoints.length >= 3 && this.animationKeypoints[2].time < now) {
		if (this.animationKeypoints.length > this.orbitKeypoints) {
	//		debugln("    deleting keypoint id " + this.animationKeypoints[0].id
	//					+ " at time " + this.animationKeypoints[2].time);
  			xmlstr += "      <delete_by_id type=\\\"KEYPOINT\\\" id=\\\"" + this.animationKeypoints[0].id + "\\\"></delete_by_id>\n";
			this.animationKeypoints.shift();
		
	//		debugln("    Deleted keypoint");
		}
	}
	
	//this.gpObject.Set("CONTROL_POINTS", "true");	// compute control points
  	xmlstr += "    <set m=\\\"CONTROL_POINTS\\\">true</set>\n";
  	xmlstr += "    </access_by_id>\n";

	// If animating, recompute control points and enable script
	if (this.animating) {
		// Navigate up to script, enable it
  		xmlstr += "    <set m=\\\"ENABLE\\\">true</set>\n";
	}
  	xmlstr += "  </access_by_id>\n";
  	xmlstr += "</access_by_name>\n";
  	xmlstr += "</gmx_session>\n";
  	xmlstr += "</gmx_document>\"";
  	
  	//alert(xmlstr);
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
  	this.gpObject.SetStore("PARSE_XML", xmlstr);	// Send XML to player to be parsed
//	debugln("    animationAdd()." + this.name + " returned");
}	

// This starts the animation at the current time.
GPObject.prototype.animationStart = function() {
//	debugln("animationStart()." + this.name + " called...");
	if (!this.initialized) {
		return;
	}
	// Compute control points
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCRIPT, this.animationScriptID);
	this.gpObject.AccessByID(gmxEVALUATOR, this.animationEvaluatorID);
	this.gpObject.Set("CONTROL_POINTS", "true");	// compute control points
	
	// Navigate to script
	this.gpObject.AccessByID(gmxNONE, this.animationScriptID);
	var time = ((new Date()).getTime() + this.dateOffset - this.animationStartTime)/1000;
	this.gpObject.Set("TIME", time);
	this.gpObject.Set("ENABLE", "true");
	this.gpObject.Set("EVALUATORS_READY", "true");
	
	this.animating = true;
	
	if (this.seekingForwards) {
		this.calcAnimationBearing();
		this.setStoreScene("mSPIN_SPEED", 0);
		this.setStoreScene("mSPIN", this.animationBearing);
	}
//	debugln("    animationStart()." + this.name + " returned");
}

// This stops and clears the animation, deleting all the keypoints.
GPObject.prototype.animationClear = function() {
	debugln("animationClear() called...");
	if (this.initialized && this.animating) {
		this.animating = false;
		
		// Detach the orbit evaluator and delete it
  		this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
		this.gpObject.AccessByID(gmxSCRIPT, this.animationScriptID);
		this.gpObject.Set("ENABLE", "false");
		this.gpObject.Set("EVALUATORS_READY", "false");
// 		if(this.gpObject.AccessByID(gmxEVALUATOR, this.animationEvaluatorID)
// 				== true) {
// 			this.gpObject.Set("ENABLE", "false");
// 			this.gpObject.Delete();
// 		}
		this.gpObject.DeleteByID(gmxEVALUATOR, this.animationEvaluatorID)
		this.animationKeypoints = new Array();
	}
}

/************************** Screen objects **************************/

// This code displays object on the screen. You can rotate or scale them

// This creates a new Screen Credit (image) Object, returning its ID.
// Note that the session file must have <param credits="TRUE"/>, or the images (credits) 
// will not be visible. Arguments:
//	uri: Location of image. URI can be in one of three formats:
//								"http://www.aServer.com/..." - remote absolute
//								"C:/topDirectory/nextDirectory..." - local absolute
//								"./aDirectory/theFile" - relative to HTML page's directory
//			Image must be PNG (with transparency) or uncompressed TIFF
//	posX: X position on screen. -1 is left edge, 1 is right edge. Default 0.
//	posY: Y position on screen. -1 is bottom edge, 1 is top edge. Default 0.
//	refX: X location of reference point in image. 0 is left edge, 1 is right edge. Default 0.
//	refY: Y location of reference point in image. 0 is bottom edge, 1 is top edge. Default 0.
//	rot: Rotation of object (in degrees CCW?). Default is 0.
//	scale: Scale of object (radio of screen pixels to image pixels). Default is 1.0.
GPObject.prototype.screenObjectCreditCreate = function(uri, posX, posY,
												refX, refY, rot, scale) {
	if (uri.substr(0, 1) == ".") {
		var newURI = gpRelToAbs(uri);
//		alert("screenObjectCreate(" + uri + "): changed to <" + newURI + ">");
		uri = newURI;
	} else {
//		alert("screenObjectCreate(" + uri + "); using as-is");
	}
	if (posX == undefined) {
		posX = 0;
	}
	if (posY == undefined) {
		posY = 0;
	}
	if (refX == undefined) {
		refX = 0;
	}
	if (refY == undefined) {
		refY = 0;
	}
	if (rot == undefined) {
		rot = 0;
	}
	if (scale == undefined) {
		scale = 1.0;
	}
  	this.gpObject.AccessByID(gmxMANAGER, gmxW2D_MGR);
	this.gpObject.Create(gmxSCREEN_OBJECT, gmxCREDIT);
	var id = parseInt(this.gpObject.Get("ID"));
	this.gpObject.Set("TEXTURE_BY_URI", "\"" + uri + "\"");
	this.gpObject.Set("POSITION", "[" + posX + "," + posY + "]");
	this.gpObject.Set("REFERENCE", "[" + refX + "," + refY + "]");
	this.gpObject.Set("ROTATION", rot);
	this.gpObject.Set("SCALE", scale);
	this.gpObject.Set("CONSTANT", "true");
	this.gpObject.Set("ENABLE", "true"); 
	return id;
}
// Backward compatibility; allow previous name
GPObject.prototype.screenObjectCreate = GPObject.prototype.screenObjectCreditCreate;

// This creates a new Screen Text Box Object, returning its ID. Arguments:
//	fontID: ID of font to use
//	textureID: ID of texture to use (null if none)
//	posX: X position on screen. -1 is left edge, 1 is right edge. Default 0.5
//	posY: Y position on screen. -1 is bottom edge, 1 is top edge. Default 0.5
//	refX: X location of reference point in text. 0 is left edge, 1 is right edge. Default 0.
//	refY: Y location of reference point in text. 0 is bottom edge, 1 is top edge. Default 0.
//	rot: Rotation of object (in degrees CCW?). Default is 0.
//	scale: Scale of object (radio of screen pixels to image pixels). Default is 1.0.
GPObject.prototype.screenObjectTextBoxCreate = function(fontID, textureID, posX, posY,
												refX, refY, rot, scale) {
	if (posX == undefined) {
		posX = 0;
	}
	if (posY == undefined) {
		posY = 0;
	}
	if (refX == undefined) {
		refX = 0.5;
	}
	if (refY == undefined) {
		refY = 0.5;
	}
	if (rot == undefined) {
		rot = 0;
	}
	if (scale == undefined) {
		scale = 1.0;
	}
  	this.gpObject.AccessByID(gmxMANAGER, gmxW2D_MGR);
	this.gpObject.Create(gmxSCREEN_OBJECT, gmxTEXT_BOX);
	var id = parseInt(this.gpObject.Get("ID"));
	this.gpObject.Set("FONT_BY_ID", fontID);
	if (textureID != null) {
		this.gpObject.Set("TEXTURE_BY_ID", textureID);
	}
	this.gpObject.Set("POSITION", "[" + posX + "," + posY + "]");
	this.gpObject.Set("REFERENCE", "[" + refX + "," + refY + "]");
	this.gpObject.Set("ROTATION", rot);
	this.gpObject.Set("SCALE", scale);
//	this.gpObject.Set("CONSTANT", "true");
//	this.gpObject.Set("ENABLE", "false");
	return id;
}

// This sets the position of an existing ScreenObject. Arguments:
//	id: The ID of the ScreenObject (as returned by this.screenObjectCreate() )
//	posX: X position on screen. -1 is left edge, 1 is right edge.
//	posY: Y position on screen. -1 is bottom edge, 1 is top edge.
GPObject.prototype.screenObjectSetPosition = function(id, posX, posY) {
  	this.gpObject.AccessByID(gmxMANAGER, gmxW2D_MGR);
	this.gpObject.AccessByID(gmxSCREEN_OBJECT, id);
	this.gpObject.SetStore("POSITION", "[" + posX + "," + posY + "]");
}

// This sets the reference of an existing ScreenObject. Arguments:
//	id: The ID of the ScreenObject (as returned by this.screenObjectCreate() )
//	refX: X location of reference point in image. 0 is left edge, 1 is right edge.
//	refY: Y location of reference point in image. 0 is bottom edge, 1 is top edge.
GPObject.prototype.screenObjectSetReference = function(id, refX, refY) {
  	this.gpObject.AccessByID(gmxMANAGER, gmxW2D_MGR);
	this.gpObject.AccessByID(gmxSCREEN_OBJECT, id);
	this.gpObject.SetStore("REFERENCE", "[" + refX + "," + refY + "]");
}

// This sets the rotation of an existing ScreenObject. Arguments:
//	id: The ID of the ScreenObject (as returned by this.screenObjectCreate() )
//	rot: Rotation of object (degrees CCW?)
GPObject.prototype.screenObjectSetRotation = function(id, rot) {
  	this.gpObject.AccessByID(gmxMANAGER, gmxW2D_MGR);
	this.gpObject.AccessByID(gmxSCREEN_OBJECT, id);
	this.gpObject.Set("ROTATION", rot);
}

// This sets the color of an existing ScreenObject. Arguments:
//	id: The ID of the ScreenObject (as returned by this.screenObjectCreate() )
//	color: Color of object, as four-element array with values in the range 0-1:
//			[red, green, blue, alpha]
GPObject.prototype.screenObjectSetColor = function(id, color) {
  	this.gpObject.AccessByID(gmxMANAGER, gmxW2D_MGR);
	this.gpObject.AccessByID(gmxSCREEN_OBJECT, id);
	var colorString = "[" + color.join(",") + "]";
//	alert(colorString);
	this.gpObject.Set("COLOR", colorString);
}

// This sets the scale of an existing ScreenObject. Arguments:
//	id: The ID of the ScreenObject (as returned by this.screenObjectCreate() )
//	scale: Scale of object (radio of screen pixels to image pixels)
GPObject.prototype.screenObjectSetScale = function(id, scale) {
  	this.gpObject.AccessByID(gmxMANAGER, gmxW2D_MGR);
	this.gpObject.AccessByID(gmxSCREEN_OBJECT, id);
	if (typeof scale != "number" || isNaN(scale)) {
		alert("screenObjectSetScale: bad scale value " + scale + " (typeof " + (typeof scale) + ")");
	}
	this.gpObject.Set("SCALE", scale);
}

// This sets the URI of an existing ScreenObject. Arguments:
//	id: The ID of the ScreenObject (as returned by this.screenObjectCreate() )
//	uri: Location of image. URI can be in one of three formats:
//								"http://www.aServer.com/..." - remote absolute
//								"C:/topDirectory/nextDirectory..." - local absolute
//								"./aDirectory/theFile" - relative to HTML page's directory
GPObject.prototype.screenObjectSetURI = function(id, uri) {
	if (uri.substr(0, 1) == ".") {
		uri = gpRelToAbs(uri);
	}
  	this.gpObject.AccessByID(gmxMANAGER, gmxW2D_MGR);
	this.gpObject.AccessByID(gmxSCREEN_OBJECT, id);
	//var enable = (this.gpObject.Get("ENABLE") == "true");
	//this.gpObject.Set("ENABLE", "false");
	this.gpObject.SetStore("TEXTURE_BY_URI", "\"" + uri + "\"");
	//if (enable) {
	//	this.gpObject.Set("ENABLE", "true");
	//}
}

// This enables/disables an existing ScreenObject. Arguments:
//	id: The ID of the ScreenObject (as returned by this.screenObjectCreate() )
//	enable: Flag: true iff the object should be enabled (visible)
GPObject.prototype.screenObjectSetEnable = function(id, enable) {
  	this.gpObject.AccessByID(gmxMANAGER, gmxW2D_MGR);
	this.gpObject.AccessByID(gmxSCREEN_OBJECT, id);
	this.gpObject.SetStore("ENABLE", (enable ? "true" : "false"));
}

// This sets the enable interval for an existing ScreenObject. Arguments:
//	id: The ID of the ScreenObject (as returned by this.screenObjectCreate() )
//	minDistance: Minimum distance (in km)
//	maxDistance: Maximum distance (in km)
GPObject.prototype.screenObjectSetInterval = function(id, minDistance, maxDistance) {
  	this.gpObject.AccessByID(gmxMANAGER, gmxW2D_MGR);
	this.gpObject.AccessByID(gmxSCREEN_OBJECT, id);
	// The following should be SetStore(), but that doesn't work in 2,0,0,19.
	this.gpObject.Set("INTERVAL", "[" + minDistance + "," + maxDistance + "]");
}

// This deletes a Screen Object. Arguments:
//	id: The ID of the ScreenObject (as returned by this.screenObjectCreate() )
GPObject.prototype.screenObjectDelete = function(id) {
  	this.gpObject.AccessByID(gmxMANAGER, gmxW2D_MGR);
// 	this.gpObject.AccessByID(gmxSCREEN_OBJECT, id);
// 	this.gpObject.Delete();
	this.gpObject.DeleteByID(gmxSCREEN_OBJECT, id);
}


/************************** World objects **************************/

// This code creates and then attaches world objects. 

// This creates (but does NOT attach) a vector world object. Note that this
// can create two types of vector objects:
// 		1: Those specified in relative X,Y coordinates, whose vertices are secified
//			here, and placement/scale/rotation will be specified when attached
//		2: Those specified in absolute lat/lon coordinates, whose vertices are specified
//			when attached, and placement/scale/rotation is automatically calculated based
//			on the vertices.
// Arguments:
//		vertices: Array of vertexes. Each vertex is:
//			Two-elt array of X and Y offset (in km at scale == 1.0) from center point
//			If this will be a lat/lon-specified object, then this should be null.
//		closed: True if should connect first and last points
//		altitude: Height in km (default == 0)
//		lineWidth: Width of line (default == 1)
//		color: Array of color values. Each value is in range [0.0 - 1.0]. If
//			three values then they're R, G, B, with alpha assumed 1.0. If
//			four values then they're R, G, B, A. Default [1.0, 1.0, 1.0, 1.0].
//		textBoxID: ID of text box to attach to object (null if none)
GPObject.prototype.worldObjectVectorCreate = function(vertices, closed, altitude, 
															lineWidth, color, textBoxID) {
//	alert("worldObjectVectorCreate(<" + vertices + ">, " + closed + ", " + altitude
//				+ ", " + lineWidth + ", <" + color + ">)");
	if (altitude == undefined) {
		altitude = 0;
	}
	if (lineWidth == undefined) {
		lineWidth = 1;
	}
	if (color == undefined) {
		color = [1.0, 1.0, 1.0, 1.0];
	} else if (color.length == 3) {
		color = [color[0], color[1], color[2], 1.0];
	}
	
	// Set up the object
	this.gpObject.AccessByID(gmxMANAGER, gmxW3D_MGR);
	var result = this.gpObject.Create(gmxWORLD_OBJECT, gmxVECTOR_OBJECT);
	var id = parseInt(this.gpObject.Get("ID"));
	
	this.gpObject.Set("ALTITUDE_MODE", "LAT_LON_RELATIVE");
	this.gpObject.Set("LINE_WIDTH", lineWidth);
	this.gpObject.Set("LOOPS", (closed ? "true" : "false"));
	this.gpObject.Set("COLOR", "[" + color.join(",") + "]");
	this.gpObject.Set("GROUPS_PER_OBJECT", 1);
	this.gpObject.Set("ALTITUDE", altitude);
	if (textBoxID != undefined) {
		this.gpObject.Set("TEXTBOX_BY_ID", textBoxID);
	}
	
	if (vertices) {
		this.gpObject.Set("SIZE", 1.0);
		// Add the vertices
		this.gpObject.Set("VERTEXES_PER_GROUP", "[0," + vertices.length + "]");
		this.gpObject.Set("VERTEXES_PER_SUBGROUP", "[0,0," + vertices.length + "]");
		for (var x = 0; x < vertices.length; x++) {
			this.gpObject.Set("VERTEX_COORD_XY", "[" + vertices[x].join(",") + "]");
		}
	}
	this.gpObject.Set("ENABLE", "true");
	this.worldObjectInstanceTypes[id] = gmxVECTOR_OBJECT_INSTANCE;
	return id;
}

// This creates (but does NOT attach) a billboard world object.
// Arguments:
//		uri: Location of image. URI can be in one of three formats:
//				"http://www.aServer.com/..." - remote absolute
//				"C:/topDirectory/nextDirectory..." - local absolute
//				"./aDirectory/theFile" - relative to HTML page's directory
//		refX: X location of ref point. 0 is left edge, 1 is right edge. Default 0.5
//		refY: Y location of ref point. 0 is bottom edge, 1 is top edge. Default 0.5
//		size: Size of billboard (in km)
//		altitude: Height in km (default == 0)
//		textBoxID: ID of text box to attach to object (null if none)
GPObject.prototype.worldObjectBillboardCreate = function(uri, refX, refY, size, altitude, textBoxID) {
	if (altitude == undefined) {
		altitude = 0;
	}
	if (uri.substr(0, 1) == ".") {
		uri = gpRelToAbs(uri);
	}
	if (refX == undefined) {
		refX = 0.5;
	}
	if (refY == undefined) {
		refY = 0.5;
	}
	
	// Set up the object
	this.gpObject.AccessByID(gmxMANAGER, gmxW3D_MGR);
	var result = this.gpObject.Create(gmxWORLD_OBJECT, gmxBILLBOARD);
	var id = parseInt(this.gpObject.Get("ID"));
	
	this.gpObject.Set("ALTITUDE_MODE", "LAT_LON_RELATIVE");
	this.gpObject.Set("ALTITUDE", altitude);
	this.gpObject.Set("URI", "\"" + uri + "\"");
	this.gpObject.Set("SIZE", size);
	if (textBoxID != undefined) {
		this.gpObject.Set("TEXTBOX_BY_ID", textBoxID);
	}
	this.gpObject.Set("REFERENCE", "[" + refX + "," + refY + "]");
	this.gpObject.Set("ENABLE", "true");
	this.worldObjectInstanceTypes[id] = gmxBILLBOARD_INSTANCE;
//	alert("Created billboard: id " + id 
//					+ ", instance type " + this.worldObjectInstanceTypes[id]);
	return id;
}

// This deletes a World Object. Arguments:
//	id: The ID of the WorldObject
GPObject.prototype.worldObjectDelete = function(id) {
	this.gpObject.AccessByID(gmxMANAGER, gmxW3D_MGR);
	this.gpObject.AccessByID(gmxWORLD_OBJECT, id);
	this.gpObject.DeleteStore();
	delete this.worldObjectInstanceTypes[id];
}

// This attaches the specified WorldObject to the specified globe. Note that if this
// object's vertices were previously specified in X/Y then the vertices argument should
// be null; if the vertices are specified here in LAT/LON then the lat, lon, scale and rotate
// arguments will be ignored.
// Arguments:
//		worldID: World object ID
//		globeName: String name of globe
//		lat, lon: Latitude and longitude of attachment; ignored if vertices not null
//		scale: Scale of object (default 1.0); ignored if vertices not null
//		rotate: Rotation of object (degrees CCW; default 0.0); ignored if vertices not null
//		vertices: Array of vertexes in Lat/Lon format (null if to be ignored). Each vertex is:
//			Two-elt array of latitude and longitude
//		text: Text of attached TextBox (null if none). This can be multi-line, but the line
//			separator must be handled correctly. From JavaScript, the separator must be
//			a backslash followed by "n", which means you'd type "Line One\\nLine Two".
//			From Flash, you have to escape the backslashes, which means you'd type
//			"Line One\\\\nLine Two"!
GPObject.prototype.worldObjectAttach = function(worldID, globeName, lat, lon, scale, rotate, 
												vertices, text) {
//	alert("worldObjectAttach(" + worldID + ", " + globeName + ", " + lat + ", " + lon
//							+ ", " + scale + ", " + rotate + ", <vertices>)");
	var lonOffset = 0;
	
	if (worldID == undefined || globeName == undefined) {
		alert("Bad arg in worldObjectAttach(" + worldID + ", " + globeName + ", ...)");
	}
	
	if (scale == undefined) {
		scale = 1.0;
	}
	if (rotate == undefined) {
		rotate = 0.0;
	}
	
	// If vertices specified, then must set world object's VERTEXES_PER_GROUP property
	if (vertices) {
		if (vertices.length == 0) {
			alert("Zero-length vertices array; text " + text);
		}
		this.gpObject.AccessByID(gmxMANAGER, gmxW3D_MGR);
		this.gpObject.AccessByID(gmxWORLD_OBJECT, worldID);
		this.gpObject.Set("VERTEXES_PER_GROUP", "[0," + vertices.length + "]");
		if (vertices.length > 0) {
			// Move everything towards longitude 0
			lonOffset = vertices[0][1];
			for (var x = 0; x < vertices.length; x++) {
				var newLon = vertices[x][1] - lonOffset;
				if (newLon > 180) {
					newLon -= 360;
				} else if (newLon < -180) {
					newLon += 360;
				}
				vertices[x][1] = newLon;
			}
		}
//		this.gpObject.Set("VERTEXES_PER_SUBGROUP", "[0,0," + vertices.length + "]");
	}
	
	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByName(gmxGLOBE, globeName);
	var result = this.gpObject.Create(gmxW3D_INSTANCE, this.worldObjectInstanceTypes[worldID]);
	this.gpObject.Set("ENABLE", "false");
	var id = parseInt(this.gpObject.Get("ID"));
	this.gpObject.Set("WORLD_OBJECT_BY_ID", worldID);
//	this.gpObject.Set("PICKING", "false");
	this.gpObject.Set("PICKING", "FINE_PICKING");
	// If vertices specified, then specify points; otherwise specify position/rotation/scale
	if (vertices) {
		for (var x = 0; x < vertices.length; x++) {
			var vertex = "[" + vertices[x].join(",") + "]";
			this.gpObject.Set("VERTEX_COORD_LAT_LON", vertex);
		}
		// Undo vertex offset
		var pos2 = eval(this.gpObject.Get("POSITION_2"));
		pos2[1] += lonOffset;
		this.gpObject.Set("POSITION_2", "[" + pos2.join(",") + "]");
	} else {
		this.gpObject.Set("POSITION_2", "[" + lat + "," + lon + "]");
		this.gpObject.Set("ROTATE", rotate);
		this.gpObject.Set("SCALE", scale);
	}
	if (text != null) {
		this.gpObject.Set("TEXT", this.encodeString(text));
	}
	this.gpObject.Set("ENABLE", "true");
	debugln("<" + this.attachName + ">.worldObjectAttach(" 
				+ worldID + ", " + globeName + "....) => " + id);
	return id;
}

// This sets the text of a world object attachment's TextBox. Arguments:
//		globeName: String name of globe
//		attachmentID: ID of attachment object
//		text: Text of TextBox (empty string if should be none)
GPObject.prototype.worldObjectAttachmentSetText = function(globeName, attachmentID, text) {
	var result;
	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	result = this.gpObject.AccessByName(gmxGLOBE, globeName);
	result = this.gpObject.AccessByID(gmxW3D_INSTANCE, attachmentID);
	this.gpObject.Set("TEXT", this.encodeString(text));
}

// This deletes a WorldObject attachment from the given globe. Arguments:
//		globeName: String name of globe
//		attachmentID: ID of attachment object
GPObject.prototype.worldObjectDetach = function(globeName, attachmentID) {
	debugln("worldObjectDetach(" + globeName + ", " + attachmentID + ")");
	if (attachmentID == undefined || globeName == undefined) {
		alert("Bad arg in worldObjectDetach(" + globeName + ", " + attachmentID + ")");
	}
	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByName(gmxGLOBE, globeName);
	var accessResult = this.gpObject.AccessByID(gmxW3D_INSTANCE, attachmentID);
	var deleteResult = this.gpObject.DeleteStore();
	if (!accessResult || !deleteResult) {
		alert("Error in worldObjectDetach(" + globeName + ", " + attachmentID 
				+ "); accessResult " + accessResult
				+ ", deleteResult " + deleteResult);
	}
}


/************************** Fonts **************************/

// Creates a texture font. Arguments:
//		name: Name of internal font (default "arial_rounded_roman")
//		pointSize: Point size (default 14)
//		textColor: Text color as array of [Red, Green, Blue, Alpha], all items in range [0-1]
//				(default [0, 0, 0, 1])
//		shadowSize: Size of shadow (default 1.0)
//		shadowColor: Color of shadow (default [0, 0, 0, 1])
GPObject.prototype.fontTextureCreate = function(name, pointSize, textColor,
				shadowSize, shadowColor) {
	// Fill in defaults
	if (name == null) {
		name = "arial_rounded_roman";
	}
	if (pointSize == null) {
		pointSize = 24;
	}
	if (textColor == null) {
		textColor = [0, 0, 0, 1];
	}
	if (shadowSize == null) {
		shadowSize = 1.0;
	}
	if (shadowColor == null) {
		shadowColor = [0, 0, 0, 1];
	}
	
	// Create font
	this.gpObject.AccessByID(gmxMANAGER, gmxFONT_MGR);
	this.gpObject.Create(gmxFONT, gmxTEXTURE_FONT);
	var id = parseInt(this.gpObject.Get("ID"));
	this.gpObject.Set("FONT_NAME", "\"" + name + "\"");
	this.gpObject.Set("POINT_SIZE", pointSize);
	this.gpObject.Set("TEXT_COLOR", "[" + textColor.join(",") + "]");
	this.gpObject.Set("SHADOW_SIZE", shadowSize);
	this.gpObject.Set("SHADOW_COLOR", "[" + shadowColor.join(",") + "]");
	this.gpObject.Set("ENABLE", "true");
	return id;
}

/************************** Images/textures **************************/
	
// Creates a texture. Arguments:
//	uri: Location of image. URI can be in one of three formats:
//								"http://www.aServer.com/..." - remote absolute
//								"C:/topDirectory/nextDirectory..." - local absolute
//								"./aDirectory/theFile" - relative to HTML page's directory
//			Image must be PNG (with transparency) or uncompressed TIFF
GPObject.prototype.imageTextureCreate = function(uri) {
	if (uri.substr(0, 1) == ".") {
		var newURI = gpRelToAbs(uri);
		uri = newURI;
	}
	// Create texture
	this.gpObject.AccessByID(gmxMANAGER, gmxIMAGE_MGR);
	this.gpObject.Create(gmxTEXTURE, gmxNONE);
	var id = parseInt(this.gpObject.Get("ID"));
	this.gpObject.Set("URI", "\"" + uri + "\"");
	this.gpObject.Set("ENABLE", "true");
	return id;
}

/************************** Getting/setting properties **************************/

// This returns the current location as an Object with properties.
// Basic (GeoPlayer-generated) properties
//		lla: Array with three elements:
//			lla[0]: Latitude
//			lla[1]: Longitude
//			lla[2]: Altitude
//		att: Array with three elements
//			att[0]: Azimuth (angle about horizon)
//			att[1]: Tilt (angle above horizon)
//			att[2]: Twist (angle about view direction)
//		dist: Distance to view point (in object mode)
//		mode: Current navigation mode (0 - object, 1 - flight)
// Added properties:
//		frustum: Frustum
//		stretch: Current terrain stretch factor
//		view: Information about view window. Object with properties:
//			size: Size of GeoPlayer window in pixels. Object with properties:
//				width
//				height
//			pixelDegrees: Width of pixel in degrees
//			pixelKilometers: Width of pixel in kilometers (perpendicular to view axis)
//			distance: Distance to viewed point
//			center: 3D location of viewed point. Object with properties
//				lat: Latitude
//				lon: Longitude
//		subsolar: Location of subsolar point (null if undefined). Object with properties:
//			lat: Latitude
//			lon: Longitude
GPObject.prototype.getLocation = function() {
	gpTraceLocationPeriod.end();
	gpTraceLocationPeriod.begin();
	gpTraceLocationDuration.begin();

	var location = this.getScene("VIEW_PARAMETERS");
	var frustum = this.getScene("FRUSTUM");
	location.frustum = frustum;
	location.stretch = this.stretchfactor;
	location.view = new Object();
	location.view.size = {width:this.gpObject.offsetWidth, height:this.gpObject.offsetHeight};
	
	var gpTilt = location.att[1];
 	var gpSpin = location.att[0];
	
	// Update Zoomfactor
	var gpZoomfactor = this.getZoomfactor(frustum);	// Pixels per degree
	var pixelAngle = 1 / gpZoomfactor;				// Degrees per pixel
	location.view.pixelDegrees = pixelAngle;
	
	// Calculate distance to target
	var distance;
	if (location.mode == 0) {
		distance = location.dist;
	} else {
		var heightAboveGlobe = location.lla[2];
		var heightAboveCenter = heightAboveGlobe + this.radius;
		var viewAngleAboveNadir = 90 + gpTilt;
		var targetAngleAboveNadir = 180 - gpRadToDeg(Math.asin(heightAboveCenter
													* Math.sin(gpDegToRad(viewAngleAboveNadir))
													/ this.radius));
		// (Set minimum value for targetAngleAboveNadir to avoid weird horizon effects)
		if (isNaN(targetAngleAboveNadir) || targetAngleAboveNadir < 100) {
			targetAngleAboveNadir = 100;
			viewAngleAboveNadir = gpRadToDeg(Math.asin(this.radius
													* Math.sin(gpDegToRad(targetAngleAboveNadir))
													/ heightAboveCenter));
		}
		var centerAngle = 180 - (viewAngleAboveNadir + targetAngleAboveNadir);
		distance = Math.sqrt(heightAboveCenter * heightAboveCenter
					+ this.radius * this.radius
					- 2 * heightAboveCenter * this.radius * Math.cos(gpDegToRad(centerAngle)));
	}
	location.view.distance = distance;
	location.view.pixelKilometers = distance * Math.tan(gpDegToRad(pixelAngle));
	
	// Get location of screen center (cheat down to avoid limb)
	location.view.center = this.screenToGlobe(0, -0.01);
	
	// Add subsolar point
	if (this.subsolar) {
		location.subsolar = new Object();
		location.subsolar.lat = this.subsolar.lat;
		location.subsolar.lon = this.subsolar.lon;
	} else {
		location.subsolar = null;
	}
	
	gpTraceLocationDuration.end();
	return location;
}

// This sets the current location. Uses the same Object structure as
// this.getLocation(), but with only the Basic (GeoPlayer-native) properties
GPObject.prototype.setLocation = function(location) {
	this.setStoreScene("VIEW_PARAMETERS", location);
}

// This sets up a fly-to object. Arguments: none
GPObject.prototype.setupFlyTo = function() {
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.Create(gmxSCRIPT, gmxLOCATION);
	this.flyToLocationID = this.gpObject.Get("ID");
  	this.gpObject.Set("ENABLE", "true");
  	
	// Set this location to be the scene's location object
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCENE, this.sceneID);
	this.gpObject.Set("LOCATION", this.flyToLocationID);
	
	// Create evaluator for frustum animation on location object
	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCRIPT, this.flyToLocationID);
	this.gpObject.Create(gmxEVALUATOR, gmxVEC1D);
	this.gpObject.Set("ID", this.flyToEvaluatorID);			// Mis-allocates, so must set
//	this.gpObject.Set("INTERPOLATION", gmxCUBIC);
	this.gpObject.Set("INTERPOLATION", "LINEAR");
	
	// Bind the flyto frustum evaluator to the scene object.
 	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCENE, this.sceneID);
	this.gpObject.Set("BIND_EVALUATOR", "["+ this.flyToLocationID +","+ this.flyToEvaluatorID +","+ mFRUSTUM_ANGLE +"]");
	
	// Create flyToEvaluatorKeyCount keypoints for the evaluator
	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCRIPT, this.flyToLocationID);
	this.gpObject.AccessByID(gmxEVALUATOR, this.flyToEvaluatorID)
	for (var x = 1; x <= this.flyToEvaluatorKeyCount; x++) {
		this.gpObject.Create(gmxKEYPOINT, gmxNONE);	// access now set to keypoint
		this.gpObject.Set("ID", x);
		this.gpObject.AccessByID(gmxNONE, this.flyToEvaluatorID);	// back up to evaluator
	}
}

// This flies to the given location. Arguments:
//		location: Destination, specified with the same Object structure as gpGetLocation
//		sigma: Array of two floats controlling flight behavior. Try [8.4, 8.4].
//		duration: Duration in seconds
//		type: Type of flight, integer, either "PARABOLA" or "DIRECT"
//		zoom: Target zoomfactor. If omitted, no change in zoom.
GPObject.prototype.flyToLocation = function(location, sigma, duration, type, zoom) {
// 	alert("flyToLocation(lla[" + location.lla[0] + ", " + location.lla[1]
// 			+ ", " + location.lla[2] + "]/att[" + location.att[0] + ", " + location.att[1]
// 			+ ", " + location.att[2] + "]/dist " + location.dist
// 			+ "/mode " + location.mode + ", [" + sigma.join(",") + "], " + duration + ", \"" + type + "\", " 
// 						+ zoom + ") called...");
	// Set duration in ms.
	this.flying = true;
	this.flyingDuration = duration * 1000;
	this.flyingStarted = (new Date()).getTime();
	// Get initial frustum value
	var startFrustum = this.getScene("FRUSTUM");
	var endFrustum;
	if (zoom !== undefined) {
		endFrustum = this.gpObject.offsetWidth / zoom;
	} else {
 		// No frustum animation
 		endFrustum = startFrustum;
	}
	
	// Set up the FlyTo
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCRIPT, this.flyToLocationID);
	// Configure location
	var formattedLocation = '{"lla":[' + location.lla.join(',') + '],'
			+ '"att":[' + location.att.join(',') + '],'
			+ '"dist":' + location.dist + ',"mode":' + location.mode + '}'
	this.gpObject.Set("VIEW", formattedLocation);
	this.gpObject.Set("SIGMA", "[" + sigma.join(",") + "]");
	this.gpObject.Set("DURATION", duration);
	this.gpObject.Set("FLY_TYPE", (type == "PARABOLA" ? gmxFLY_PARABOLA: gmxFLY_DIRECT));
	
	// Configure frustum evaluator's keypoints to ease in
	this.gpObject.AccessByID(gmxEVALUATOR, this.flyToEvaluatorID);		// => evaluator
	for (var x = 1; x <= this.flyToEvaluatorKeyCount; x++) {
		this.gpObject.AccessByID(gmxKEYPOINT, x);
		var ratio = (x - 1) / (this.flyToEvaluatorKeyCount - 1);
		var easeInRatio = Math.sin(ratio * Math.PI/2);
		var value = startFrustum + (endFrustum - startFrustum) * easeInRatio;
		this.gpObject.Set("VALUE", value);
		var time = duration * ratio;
		this.gpObject.Set("TIME", time);
		this.gpObject.AccessByID(gmxNONE, this.flyToEvaluatorID);	// back up to evaluator
	}	
	
	
// Math.easeInSine = function (t, b, c, d) { 
// return c * (1 - Math.cos(t/d * (Math.PI/2))) + b; 
// }; 

	// Right! Off you go!
	this.setStoreScene("FLY_TO", true);
}

// This sets a property of the first scene in the session. 
GPObject.prototype.setStoreScene = function(methodName, data) {
	this.setScene(methodName, data, true);
}

// This sets a property of the first scene in the session. 
GPObject.prototype.setScene = function(methodName, data, setStore) {
	if (!this.initialized) {
		return;
	} else if (setStore == undefined) {
		setStore = false;
	}
	var stringData;
	// Handle some special cases
	if (methodName == "mVIEW_PARAMETERS") {
		// Gotta format that sucker correctly.
		stringData = '{"lla":[' + data.lla.join(',') + '],'
				+ '"att":[' + data.att.join(',') + '],'
				+ '"dist":' + data.dist + ',"mode":' + data.mode + '}'
	} else if (methodName == "SUN_POSITION") {
		stringData = '{"vec":[' + data.vec.join(',') + '],'
				+ '"radius":' + data.radius + '}';
	} else if (data instanceof Array) {
		stringData = "[" + data.join(",") + "]";
	} else if (data instanceof String) {
		stringData = this.encodeString(data);
	} else {
		stringData = data.toString();
	}

//	debugln("setScene(\"" + methodName + "\", \"" + stringData + "\");");
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCENE, this.sceneID);
	// If method name starts with "m", then trim it off
	if (methodName.substr(0, 1) == "m") {
		methodName = methodName.substr(1);
	}
	
	if (setStore) {
		this.gpObject.SetStore(methodName, stringData);
	} else {
		this.gpObject.Set(methodName, stringData);
	}
}

// This function sets the absolute stretch factor. Stops any smooth stretch motion
// in progress.
GPObject.prototype.setStretch = function(stretchFactor) {
	this.stretchfactor = stretchFactor;
	this.StretchfactorControl = 0;
}

// This function sets the scene's FRUSTUM value (in degrees).
GPObject.prototype.setFrustum = function(frustum) {
	this.setScene("FRUSTUM", frustum);
	this.zoomfactorSpeed = 0;
}

// This fetches a property of the first scene in the session. Returns null if
// the GeoPlayer has not yet been initialized, or if a bad value was returned.
GPObject.prototype.getScene = function(methodName) {
	if (!this.initialized) {
		return null;
	}
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCENE, this.sceneID);
	// If method name starts with "m", then trim it off
	if (methodName.substr(0, 1) == "m") {
		methodName = methodName.substr(1);
	}
	var result = this.gpObject.Get(methodName);
	// If not a special value, evaluate the string
	switch (methodName) {
		case "NAME":
			// Don't evaluate
			break;
		
		default:
			// Intercept errors
			try {
				result = eval('(' + result + ')');
			} catch (exception) {
				debugln("gpGetScene: couldn't eval " + result);
				result = null;
			}
			break;
	}
	return result;
}

// This sets one of the configuration variables.
GPObject.prototype.setConfiguration = function(configName, configValue) {
	this[configName] = configValue;
}

// This sets the enabled flag of the given (named) globe.
GPObject.prototype.globeViewSetEnable = function(theGlobe, theFlag) {
//	debugln("globeViewSetEnable(\"" + theGlobe + "\", " + theFlag + ")");
	if (!this.initialized) {
		return;
	}
	this.gpObject.GlobeViewSetEnable(theFlag, theGlobe);
}

// This sets the alpha blend of the given (named) globe.
GPObject.prototype.globeViewSetAlpha = function(theGlobe, theAlpha) {
//	debugln("globeViewSetAlpha(\"" + theGlobe + "\", " + theAlpha + ")");
	if (!this.initialized) {
		return;
	}
	this.gpObject.GlobeViewSetAlpha(theAlpha, theGlobe);
}

// This turns on and off the connection between GeoPlayer and the screen.
GPObject.prototype.display = function(isOn) {
	this.gpObject.SetWindowShow(isOn);
}

// This returns the version of the GeoPlayer plugin.
GPObject.prototype.version = function() {
	return this.gpObject.GetVersion();		// Broken as of GeoPlayer 2,0,0,6
}

// This returns the current height of the GeoPlayer instance.
GPObject.prototype.height = function() {
	return this.gpObject.offsetHeight;
}

// This returns the current width of the GeoPlayer instance.
GPObject.prototype.width = function() {
	var result = this.gpObject.offsetWidth;
	return result;
}

/********************************* Movement *********************************/

// This looks in the given direction. Arguments:
//		upOrDown: 1 if looking up, -1 if down, 0 if no vertical change
// 		rightOrLeft: 1 if looking right, -1 if left, 0 if no horizontal change
// Fractional values are allowed.
// Warning: rightOrLeft won't work properly if currently seeking North.
GPObject.prototype.look = function(upOrDown, rightOrLeft) {
	if (!this.initialized) {
		return;
	}
	this.lookUDControl = upOrDown;
	this.lookRLControl = rightOrLeft;
}

// This turns north-seeking on or off. When turning on, it immediately sets
// the view to point exactly North.
GPObject.prototype.seekNorth = function(turnOn) {
	if (turnOn) {
		this.setStoreScene("mSPIN_SPEED", 0);
		this.setStoreScene("mSPIN", 0);
		this.seekingForwards = false;
	}
	this.seekingNorth = turnOn;
}

// This turns forward-seeking on or off. Only functions while animating.
// When turning on, it disables north-seeking; if animating, it also immediately
// sets the view to point exactly forwards. Arguments:
//		turnOn: true if should be seeking forwards
//		offset: direction offset from true forwards-seeking
GPObject.prototype.seekForwards = function(turnOn, offset) {
	if (turnOn) {
		this.seekingNorth = false;
		if (offset == undefined) {
			offset = 0;
		}
		this.seekingForwardsOffset = offset;
		if (this.animating) {
			this.calcAnimationBearing();
			this.setStoreScene("mSPIN_SPEED", 0);
			this.setStoreScene("mSPIN", this.animationBearing);
		}
	}
	this.seekingForwards = turnOn;
}

// This turns day/night display on or off.
GPObject.prototype.setDayNight = function(enable) {
	if (enable) {
		color = [0.2, 0.2, 0.4, 1.0];
		this.setStoreScene("AMBIENT_LIGHT", "[" + color.join(",") + "]");
		var sunWidth = 7;		// Angular sun width (radius? diameter?)
		var brightness = 15;	// Brightness of sun
		this.setStoreScene("SUN_DISC", [sunWidth, brightness]);
		this.setStoreScene("MODE_ENABLE", [true, true, false, false]);
	} else {
		this.setStoreScene("MODE_ENABLE", [false, true, false, false]);
	}
}

// This sets the current time/date of the sun. If argument is 0, then
// sets sun time to "now" (as controlled by dateOffset). Ignored if equationOfTime.js
// hasn't been loaded.
GPObject.prototype.setSunTime = function(time) {
	if (time == 0) {
		this.sunNow = true;
		time = (new Date()).getTime() + this.dateOffset;
	} else {
		this.sunNow = false;
	}
	this.setSunTimeInternal(time);
}

// Make sure eotSubsolar exists, even if vacuous
if (this.eotSubsolar === undefined) {
	this.eotSubsolar = null;
}

// This does the actual work of setting the sun time.
GPObject.prototype.setSunTimeInternal = function(time) {
	// This never throws an error because of the code about five lines above here.
	if (eotSubsolar) {
		this.subsolar = eotSubsolar(new Date(time));
		this.setSunPosition(this.subsolar.lat, this.subsolar.lon);
	}
}

// This sets the sun above the given lat/lon
GPObject.prototype.setSunPosition = function(lat, lon) {
	var val = {vec:[lat, lon, 1000000.0], radius:6377.84};
	this.setStoreScene("SUN_POSITION", val);
}

// This moves in the given direction (relative to the view direction). Arguments:
//		forwardOrBack: 1 if moving forward, -1 if moving back 
//		rightOrLeft: 1 if sliding right, -1 if sliding left
//		upOrDown: 1 if increasing altitude, -1 if decreasing
// Fractional values are allowed.
GPObject.prototype.move = function(forwardOrBack, rightOrLeft, upOrDown) {
	this.moveFBControl = forwardOrBack;
	this.moveRLControl = rightOrLeft
	this.moveUDControl = upOrDown;
}

// This zooms in or out. Argument:
//		inOrOut: 1 if zooming in, -1 if zooming out, 0 if no zoom change.
GPObject.prototype.zoom = function(inOrOut) {
	if (!this.initialized) {
		return;
	}
	this.zoomfactorControl = inOrOut;
}

// This stretches the terrain up or down. Argument:
//		upOrDown: 1 if stretching up, -1 if flattening out, 0 if no change.
// Fractional values are allowed.
GPObject.prototype.stretch = function(upOrDown) {
	if (!this.initialized) {
		return;
	}
	this.StretchfactorControl = upOrDown;
	debugln("stretch(" + upOrDown + ")");
}

/********************************* Public utilities *********************************/

// This takes a screen location and maps it to a latitude/longitude pair. Args:
//		screenX: X location on screen (-1 is left edge, 1 is right edge)
//		screenY: Y location on screeen (-1 is bottom edge, 1 is top ledge)
// Returns null if not on globe, or object with properties if it is:
//		lat: Latitude
//		lon: Longitude
GPObject.prototype.screenToGlobe = function(screenX, screenY) {
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCENE, this.sceneID);
	var argument = "[" + screenX + "," + screenY + ",0.0]";
	var result = this.gpObject.GetUtility("MAP_TO_GLOBE", argument);
	if (result != 0) {
		var coords = result.split(/\[|,|\]/);
		var lat = parseFloat(coords[0]);
		var lon = parseFloat(coords[1]);
		return {lat:lat, lon:lon};
	} else {
//		debugln("screenToGlobe(" + screenX + ", " + screenY + ") got result \"" + result + "\"; failed");
		return null;
	}
}

// This takes a globe lat/lon and maps it to a screen X/Y pair. Args:
//		lat: Latitude
//		lon: Longitude
//		screenX: X location on screen (-1 is left edge, 1 is right edge)
//		screenY: Y location on screeen (-1 is bottom edge, 1 is top ledge)
// If visible (if perhaps off-screen), returns object with properties:
//		x: X location on screen (-1 is left edge, 1 is right edge; can return off-screen)
//		y: Y location on screen (-1 is bottom edge, 1 is top edge; can return off-screen)
// If behind limb of globe, returns null.
GPObject.prototype.globeToScreen = function(lat, lon) {
  	this.gpObject.AccessByID(gmxMANAGER, gmxVIEW_MGR);
	this.gpObject.AccessByID(gmxSCENE, this.sceneID);
	var argument = "[" + lat + "," + lon + ",0.0]";
	var result = this.gpObject.GetUtility("MAP_TO_SCREEN", argument);
	if (result != 0) {
		var coords = result.split(/\[|,|\]/);
		var xPos = parseFloat(coords[0]);
		var yPos = parseFloat(coords[1]);
//		debugln("globeToScreen(" + lat + ", " + lon + ") got result \"" + result + "\"; returning {x:" + xPos + ", y:" + yPos + "}");
		return {x:xPos, y:yPos};
	} else {
//		debugln("globeToScreen(" + lat + ", " + lon + ") got result \"" + result + "\"; failed");
		return null;
	}
}

// This takes a screen shot of the given type to the given file. Arguments:
//	theFile: Target file, either relative or absolute (with "file:///" prefix).
//				File type suffix will be added.
//	theType: Either "JPEG" or "TIFF". 
// (Note: currently broken, don't know why)
GPObject.prototype.screenShot = function(theFile, theType) {
	// Clean up path
	if (theFile.indexOf("file:///") == 0) {
		// Absolute path. Trim off "file:///" prefix.
		theFile = theFile.slice(8);
	} else {
		// Relative path. Convert to absolute (without "file:///" prefix)
		theFile = gpRelToAbs(theFile);
	}
	// Convert type string to integer
	var theTypeID = (theType == "TIFF" ? 0 : 1);
	
	// Do it!
	result = this.gpObject.ScreenCapture(theFile, theTypeID);
	debugln("screenShot() to \"" + theFile + "\", type " + theTypeID + ", result " + result);
}	

// This is used for random testing, replacing the contents as necessary.
GPObject.prototype.test = function() {
	
}

/********************************* Internal methods *********************************/

// This converts a frustum value into a Zoomfactor value, defined as pixels per degree
// in the center of the screen.
GPObject.prototype.getZoomfactor = function(frustum) {
	var eye_distance = (this.gpObject.offsetWidth/2) * Math.tan(gpDegToRad(90 - frustum/2));
	var degreesPerPixel = gpRadToDeg(Math.atan(1 / eye_distance));
	var zoomfactor = 1 / degreesPerPixel;
	return zoomfactor;
}

// This updates the view parameters based on the most recent view function calls.
// Adjusts speeds to the same approximate visual velocity, based on the view parameters
// and zoomfactor.
GPObject.prototype.updateView = function() {

	gpTraceViewPeriod.end();
	gpTraceViewPeriod.begin();
	gpTraceViewDuration.begin();
	
	// Get change in time
	var thisTime = (new Date()).getTime();
	var diffTime = (thisTime - this.lastUpdate)/1000;
	this.lastUpdate = thisTime;
		
	if (this.sunNow && !this.orbiting) {
		this.setSunTimeInternal(thisTime + this.dateOffset);
	}
	
	var frustum = this.getScene("FRUSTUM");
	
	// Check if we're flying
	if (this.flying) {
		var duration = (new Date()).getTime() - this.flyingStarted;
		// See if we're done flying. Note that, in versions up to (and beyond?)
		// 2,0,6,1, FLY_TO sometimes doesn't take effect (and become true)
		// until AFTER the first time we fetch its value. So, here we always
		// fetch FLY_TO (to start the flight), but don't trust any false values
		// until 1/2 of the flight time has passed.
		var flyTo = this.getScene("FLY_TO");
//		debugln("updateView(): flyTo " + flyTo);

		if (!flyTo && duration > this.flyingDuration / 2) {
			// Done, baby.
			this.flying = false;
			// Make sure there's no initial motion
			this.zoomfactorSpeed = 1;
			this.lookUDSpeed = 0;
			this.lookRLSpeed = 0;
			this.moveFBSpeed = 0;
			this.moveLRSpeed = 0;
			this.moveUDSpeed = 0;
		}
		// Show debugging info
		if (document.w_debug) {
			w_debug.value = "diffTime: " + diffTime + "\n"
					+ "duration: " + duration + "\n"
					+ "this.flyingDuration: " + this.flyingDuration + "\n"
					+ "FRUSTUM: " + this.getScene("FRUSTUM") + "\n"
					+ "frustum: " + frustum + "\n"
					+ "FLY_TO: " + flyTo + "\n";
		}
		gpTraceViewDuration.end();
		return;			// Don't adjust view
	}
	
	// Get current state
	var view = this.getScene("VIEW_PARAMETERS");
	if (view === null) {
		return;
	}
 	var gpTilt = view.att[1];
 	var gpSpin = view.att[0];
	
	// Update Zoomfactor
	var gpZoomfactor = this.getZoomfactor(frustum);
	this.zoomfactorSpeed = 1 + gpLinearSmooth(this.zoomfactorSpeed - 1,
								this.zoomfactorControl, this.zoomfactorSpeedMax,
								diffTime, this.zoomfactorSpeedTC);
	if (gpZoomfactor > this.zoomfactorMax && this.zoomfactorSpeed > 1) {
		this.zoomfactorSpeed = 1;
	} else if (gpZoomfactor < this.zoomfactorMin && this.zoomfactorSpeed < 1) {
		this.zoomfactorSpeed = 1;
	}
	
/*	// Calculate distance to target
	var view = this.getScene("mVIEW_PARAMETERS");
	var heightAboveGlobe = view.lla[2];
	var heightAboveCenter = heightAboveGlobe + this.radius;
	var viewAngleAboveNadir = 90 + gpTilt;
	var targetAngleAboveNadir = 180 - gpRadToDeg(Math.asin(heightAboveCenter
												* Math.sin(gpDegToRad(viewAngleAboveNadir))
												/ this.radius));
	// Set minimum value for targetAngleAboveNadir to avoid weird horizon effects
	if (isNaN(targetAngleAboveNadir) || targetAngleAboveNadir < 100) {
		targetAngleAboveNadir = 100;
		viewAngleAboveNadir = gpRadToDeg(Math.asin(this.radius
												* Math.sin(gpDegToRad(targetAngleAboveNadir))
												/ heightAboveCenter));
	}
	var centerAngle = 180 - (viewAngleAboveNadir + targetAngleAboveNadir);
	var distance = Math.sqrt(heightAboveCenter * heightAboveCenter
				+ this.radius * this.radius
				- 2 * heightAboveCenter * this.radius * Math.cos(gpDegToRad(centerAngle)));
*/

	// Calculate new values
	var pixelAngle = 1 / gpZoomfactor;
	this.lookUDSpeed = gpLinearSmooth(this.lookUDSpeed, this.lookUDControl,
								this.lookUDSpeedMax, diffTime, this.lookUDSpeedTC);
	var tiltSpeed = this.lookUDSpeed * pixelAngle;
	var spinSpeed;
	if (this.seekingNorth) {
		// Normalize the spin to +/- 180
		var spin = (gpSpin + 720) % 360;
		spin = (spin > 180 ? spin - 360 : spin);
		// Calculate correction coefficient
		var correction;
		var absLatitude = Math.abs(view.lla[0]);
		if (absLatitude < 60) {
			correction = 10;
		} else if (absLatitude < 80) {
			correction = 1 + (80 - absLatitude)/2;
		} else {
			correction = 1;
		}
		spinSpeed = -spin * correction / 2;
		// If we're almost there, stop spinning (saves the CPU).
		if (Math.abs(spinSpeed) < 0.01) {
			spinSpeed = 0;
		}
	} else if (this.seekingForwards) {
		// Seek towards this.animationBearing. First, get spin error.
		if (this.animationBearing != undefined) {
			var spinError = ((gpSpin - this.animationBearing) + 720) % 360
			spinError = (spinError > 180 ? spinError - 360 : spinError);
			spinSpeed = -spinError;
		} else {
			spinSpeed = 0;
		}
		debugln("<" + this.name + ">.updateView: seekingForwards true, spSpin " + gpSpin
				+ ", animationBearing " + this.animationBearing + ", spinError "
				+ spinError + "; setting spinSpeed " + spinSpeed);
	} else {
		// Calculate max spin speed based on pixel velocity
		var spinMax = this.lookRLSpeedMax * pixelAngle;
		// Adjust based on vertical angle
		var spinSpeedup = Math.cos(gpDegToRad(gpTilt));
		// Make sure we don't adjust too far
		if (spinSpeedup == 0) {
			spinMax = this.lookRLSpeedMaxAngle;
		} else {
			spinMax /= spinSpeedup;
			if (spinMax > this.lookRLSpeedMaxAngle) {
				spinMax = this.lookRLSpeedMaxAngle;
			}
		}
		this.lookRLSpeed = gpLinearSmooth(this.lookRLSpeed, this.lookRLControl, spinMax,
			diffTime, this.lookRLSpeedTC);
		spinSpeed = this.lookRLSpeed;
	}
	var frustum_speed = frustum * (1 - this.zoomfactorSpeed);
	this.moveFBSpeed = gpLinearSmooth(this.moveFBSpeed, this.moveFBControl,
							this.moveFBSpeedMax, diffTime, this.moveFBSpeedTC);
	var orbitVSpeed = -this.moveFBSpeed / (gpZoomfactor * gpZoomfactor);
	this.moveRLSpeed = gpLinearSmooth(this.moveRLSpeed, this.moveRLControl, 
							this.moveRLSpeedMax, diffTime, this.moveRLSpeedTC);
	var orbitHSpeed = -this.moveRLSpeed / (gpZoomfactor * gpZoomfactor);
	this.moveUDSpeed = gpLinearSmooth(this.moveUDSpeed, this.moveUDControl, 
							this.moveUDSpeedMax, diffTime, this.moveUDSpeedTC);
	var elevateSpeed = this.moveUDSpeed;
	this.stretchfactor = gpGeometricSmooth(this.stretchfactor, this.StretchfactorControl, 
		this.StretchfactorMin, this.StretchfactorMax, diffTime, this.StretchfactorSpeed);
		
	// Show debugging info
	if (document.w_debug) {
		var orbit = this.getScene("mORBIT");
		w_debug.value = "pixelAngle: " + pixelAngle + "\n"
					+ "this.moveFBSpeed: " + this.moveFBSpeed + "\n"
					+ "orbitVSpeed: " + orbitVSpeed + "\n"
					+ "gpTilt: " + gpTilt + "\n"
					+ "this.lookRLSpeed: " + this.lookRLSpeed + "\n"
					+ "spinSpeed: " + spinSpeed + "\n"
					+ "gpSpin: " + gpSpin + "\n"
					+ "diffTime: " + diffTime + "\n"
					+ "gpZoomfactor: " + gpZoomfactor + "\n"
					+ "this.zoomfactorSpeed: " + this.zoomfactorSpeed + "\n"
					+ "offsetWidth: " + this.gpObject.offsetWidth + "\n"
					+ "frustum_speed: " + frustum_speed + "\n"
					+ "FRUSTUM: " + this.getScene("FRUSTUM") + "\n"
					+ "FLY_TO: " + this.getScene("FLY_TO") + "\n"
					+ "view.mode: " + view.mode;
	}
	
	if (this.tiltSpeedLagger.setValue(-tiltSpeed)) {
		this.setStoreScene("TILT_SPEED", this.tiltSpeedLagger.getValue());
	}
	if (this.spinSpeedLagger.setValue(spinSpeed) || true) {	// Disable lagger
		var temp = this.spinSpeedLagger.getValue();
		debugln("<" + this.name + ">.updateView: setStoreScene(\"SPIN_SPEED\", " + temp + ")");
		this.setStoreScene("SPIN_SPEED", temp);
	}
	if (this.frustumSpeedLagger.setValue(frustum_speed)) {
		this.setStoreScene("FRUSTUM_SPEED", this.frustumSpeedLagger.getValue());
	}
	var hSignificant = this.orbitHSpeedLagger.setValue(orbitHSpeed);
	var vSignificant = this.orbitVSpeedLagger.setValue(orbitVSpeed);
	if (hSignificant || vSignificant) {
		this.setStoreScene("ORBIT_SPEED", [this.orbitHSpeedLagger.getValue(), 
											this.orbitVSpeedLagger.getValue()]);
	}
	if (this.elevateSpeedLagger.setValue(elevateSpeed)) {
		this.setStoreScene("ELEVATE_SPEED", this.elevateSpeedLagger.getValue());
	}
	if (this.stretchfactorLagger.setValue(this.stretchfactor)) {
		this.gpObject.SceneSetTerrainScale(this.stretchfactorLagger.getValue());
	}

	gpTraceViewDuration.end();
}

// This updates the current orbit, adding/deleting keypoints as necessary.
GPObject.prototype.updateOrbit = function() {
	if (this.orbiting) {
//		debugln("updateOrbit()." + this.name + " called; orbiting");
		// Create new keypoint for moving time "window". Time of keypoint is X orbit intervals in
		// future, where X = this.orbitKeypoints/2
		var nowMS = (new Date()).getTime() + this.dateOffset;
		if (this.sunNow) {
			this.setSunTimeInternal(nowMS + this.orbitInterval/2);
		}
		var keyTime = nowMS + (this.orbitKeypoints/2) * this.orbitInterval;
		var keyLoc = orbitGetLocation(new Date(keyTime));
		this.animationAdd(keyLoc.lat, keyLoc.lon, 
						keyLoc.alt + this.orbitAltitudeOffset, keyTime);
		if (this.seekingForwards) {
			this.calcAnimationBearing()
		}
//		debugln("    updateOrbit()." + this.name + " returned");
	}
}

/********************************* Internal utilities *********************************/

// Calculate animationBearing
GPObject.prototype.calcAnimationBearing = function() {
	// Find bearing between two middle keypoints. First, get keypoints.
	var firstIndex = Math.floor((this.animationKeypoints.length - 1) / 2);
	var key1 = this.animationKeypoints[firstIndex];
	var key2 = this.animationKeypoints[firstIndex + 1];
	// Get values in radians
	var lon1 = gpDegToRad(key1.lon);
	var lat1 = gpDegToRad(key1.lat);
	var lon2 = gpDegToRad(key2.lon);
	var lat2 = gpDegToRad(key2.lat);
	// Now, calculate distance
	var y = Math.sin(lon2 - lon1) * Math.cos(lat2);
	var x = Math.cos(lat1)*Math.sin(lat2) -
			Math.sin(lat1)*Math.cos(lat2)*Math.cos(lon2 - lon1);
	this.animationBearing = Math.atan2(y, x) * 360 / (Math.PI * 2)
						+ this.seekingForwardsOffset;
	while (this.animationBearing > 180) {
		this.animationBearing -= 360;
	}
	while (this.animationBearing < -180) {
		this.animationBearing += 360;
	}
	// Debugging info
	var spinSpeed = this.getScene("SPIN_SPEED");
	var spin = this.getScene("SPIN");
	debugln("<" + this.name + ">.calcAnimationBearing(): spin " + fourDecimals(spin)
				+ ", spinSpeed " + fourDecimals(spinSpeed)
				+ ", dLat " + fourDecimals(lat2 - lat1) 
				+ ", dLon " + fourDecimals(lon2 - lon1) 
				+ ", x " + fourDecimals(x) + ", y " + fourDecimals(y) 
				+ ", animationBearing " + fourDecimals(this.animationBearing));
}

// Takes a value and returns it with four decimal places
function fourDecimals(theNumber) {
	var s = theNumber.toString();
	var pos = s.indexOf(".");
	if (pos != -1) {
		s = s.slice(0, pos + 5);
	}
	return s;
}

// Converts radians to degrees.
function gpRadToDeg(rad) {
	return rad * 180 / Math.PI;
}

// Converts degrees to radians.
function gpDegToRad(deg) {
	return deg * Math.PI / 180;
}

// This takes an integer and returns a hex string.
function gpIntToHex(theInt) {
	if (theInt < 0) {
		return (new Number(theInt)).toString();
	} else {
		var theString = (new Number(theInt)).toString(16);
		return "0x" + theString;
	}
}

// This is used to smooth a value that is being controlled. Smoothed value is a linear
// progression to the maximum value. Arguments:
//		oldValue: Previous value
//		control: User control over value. 1 => increase, -1 => decrease, 0 => set to 0.
//				Fractional values allowed.
//		max: Maximum value (minimum is -max)
//		time: Time (seconds) since last change
//		timeConstant: time to reach max
function gpLinearSmooth(oldValue, control, max, time, timeConstant) {
	if (control == 0) {
		return 0;
	} else {
		var targetValue = max * control;
		var direction = (targetValue - oldValue > 0 ? 1 : -1);
		oldValue += direction * max * (time / timeConstant);
		if (direction == 1 && oldValue > targetValue) {
			oldValue = targetValue;
		} else if (direction == -1 && oldValue < targetValue) {
			oldValue = targetValue;
		}
		return oldValue;
	}
}


function gpLinearSmoothOld(oldValue, control, max, time, timeConstant) {
	if (control == 0) {
		return 0;
	} else {
		// Linear progress to max value
		var direction = (control > 0 ? 1 : -1);
		oldValue += direction * max * (time / timeConstant);
		if (oldValue > max) {
			oldValue = max;
		} else if (oldValue < -max) {
			oldValue = -max;
		}
		return oldValue;
	}
}

// This is used to smooth a value that is being controlled. Smoothed value is
// geometrically adjusted towards the max or min value. Arguments:
//		oldValue: Previous value
//		control: User control over value. 1 => increase, -1 => decrease, 0 => leave the same.
//				Fractional values allowed.
//		min: Minimum value (must be positive)
//		max: Maximum value (must be positive)
//		time: Time (seconds) since last change
//		ratio: Ratio change of value each second (must be greater than 1)
function gpGeometricSmooth(oldValue, control, min, max, time, ratio) {
	if (control > 0) {
		oldValue *= Math.pow(ratio, time * control);
		if (oldValue > max) {
			oldValue = max;
		}
	} else if (control < 0) {
		oldValue /= Math.pow(ratio, time * -control);
		if (oldValue < min) {
			oldValue = min;
		}
	}
	return oldValue;
}

// This is used to smooth a value that is being controlled. Smoothed value is an
// exponential decay to the maximum value. Arguments:
//		oldValue: Previous value
//		control: User control over value. 1 => increase, -1 => decrease, 0 => set to 0.
//		max: Maximum value (minimum is -max)
//		time: Time (seconds) since last change
//		timeConstant: time to reduce error by factor of e
// (This function isn't currently used.)
function gpExponentialSmooth(oldValue, control, max, time, timeConstant) {
	if (control == 0) {
		return 0;
	} else {
		// Exponential decay to max value
		oldValue += (control * max - oldValue) * (time / timeConstant);
		return oldValue;
	}
}

// This makes a Delegate function that, when called, calls the argument function
// in the context of the given object.
function gpMakeDelegate(objArg, funcArg) {
	var f = function() {
		var target = arguments.callee.target;
		var func = arguments.callee.func;
		
		return func.apply(target, arguments);
	}
	
	f.target = objArg;
	f.func = funcArg;
	
	return f;
}

GPObject.prototype.encodeString = function(s) {
	// Encode linefeeds
	s = s.split("\n").join("\\n");
	return "\"" + s + "\"";
}
