/*
 * Path class
 * Encapsulates the notion of the path to an element of the web site.  Instances of this class are immutable.
 */
 
// Constructs an instance of this class from the given array of elements.  The first element must be "Home".
function Path( pElements ) {
	this.elements = pElements;
}

// returns the directory (within the site domain) represented by this path...
Path.prototype.getDirLocation = function() {
	var vResults = new Array();
	for( var vI = 1; vI < this.elements.length; vI++ ) {
		if( vI > 1 ) vResults.push( "/" );
		vResults.push( this.elements[ vI ] );
	}
	return vResults.join( "" );
}

// returns the directory and document name (within the site domain) represented by this path and the given document name...
Path.prototype.getDocLocation = function( pDoc ) {
	var vResults = this.getDirLocation();
	if( vResults.length > 0 ) vResults += "/";
	vResults += pDoc;
	return vResults;
}

// returns the fully qualified path represented by this instance...
Path.prototype.getFQPath = function() {
	return this.elements.join( "/" );
}

// returns the given child path of the path represented by this instance...
Path.prototype.getChild = function( pChild ) {
	return Path.fromFQPath( this.getFQPath() + "/" + pChild );
}

// returns the parent path with the given number of elements of the path represented by this instance (immediate parent with no parameter)...
Path.prototype.getParent = function( pNum ) {
	var vElems = new Array();
	var vNum = pNum;
	if( (vNum == undefined) || (vNum > this.elements.length) ) {
		vNum = this.elements.length - 1;
	}
	vElems.push( "Home" );
	for( var vI = 1; vI < vNum; vI++ ) {
		vElems.push( this.elements[ vI ] );
	}
	return new Path( vElems );
}

// returns the size (number of elements) of this instance...
Path.prototype.size = function() {
	return this.elements.length;
}

// returns the given element within this instance...
Path.prototype.getElement = function( pI ) {
	return this.elements[ pI ];
}

// returns a Path instance made from the given directory (within the site domain)...
Path.fromDirLocation = function( pLoc ) {
	return Path.fromFQPath( "Home/" + pLoc );
}

// returns a Path instance made from the given fully qualified path...
Path.fromFQPath = function( pPath ) {
	var vElems = pPath.split( "/" );
	return new Path( vElems );
}

// returns a Path instance representing the root...
Path.getRoot = function() {
	return new Path( ["Home"] );
}

// returns true if the two Paths given are exactly the same...
Path.same = function( pA, pB ) {
	var vResults = false;
	if( pA.size() == pB.size() ) {
		var vSame = true;
		for( var vI = 0; vI < pA.size(); vI++ ) {
			if( pA.getElement( vI ) != pB.getElement( vI ) ) {
				vSame = false;
			}
		}
		vResults = vSame;
	}
	return vResults;
}

/**********************************/

/*
 * MyItem class
 * Encapsulates the notion an item in the collection.  Instances of this class may accumulate information; they are NOT immutable.
 */
 
// the initializer is an instance of Object created from the collection JSON.
function MyItem( pInit ) {
	this.pathElementName = pInit.path;
	this.dirName = pInit.dir;
	this.query = pInit.query;
	this.brandQuery = pInit.brandQuery;
	this.dateAdded = null;
	if( pInit.added != undefined) this.dateAdded = new Date( pInit.added[0], pInit.added[1]-1, pInit.added[2] );
	this.photos = pInit.photos;
	this.brand = pInit.brand;
	this.kids = new Array();
	this.kidsByName = new Object();
	this.pater = null;
	this.pathObj = null;
	this.collectionType = pInit.collectionType;
}

// add a child item to this instance (e.g., add a model to a maker).
MyItem.prototype.addChild = function( pChild ) {
	pChild.setParent( this );
	this.kids.push( pChild );
	this.kidsByName[ pChild.getDirectoryName() ] = pChild;
}

// accumulate the elements of this items path in the given Array instance.
MyItem.prototype.genPath = function( pElems ) {
	if( this.pater != null ) this.pater.genPath( pElems );
	pElems.push( this.dirName );
}

// set the parent pointer in this instance.
MyItem.prototype.setParent = function( pParent ) {
	this.pater = pParent;
	var vElems = new Array();
	this.genPath( vElems )
	this.pathObj = new Path( vElems );
}

// get the path element name in this instance.
MyItem.prototype.getPathElementName = function() {
	return this.pathElementName;
}

// get this instance's path.
MyItem.prototype.getPath = function() {
	return this.pathObj;
}

// get the directory name in this instance.
MyItem.prototype.getDirectoryName = function() {
	return this.dirName;
}

// get the date added in this instance.
MyItem.prototype.getDateAdded = function() {
	return this.dateAdded;
}

// get the photos in this instance, in an ordered array.
MyItem.prototype.getPhotos = function() {
	return this.photos;
}

// get the children of this instance, in an ordered array.
MyItem.prototype.getChildren = function() {
	return this.kids;
}

// get the query in this instance.
MyItem.prototype.getQuery = function() {
	return this.query;
}

// get the brand in this instance.
MyItem.prototype.getBrand = function() {
	var vBrand = this.brand;
 	if( ! vBrand ) {
 		if( this.pater ) vBrand = this.pater.getPathElementName();
	}
	return vBrand;
}

// get the brand query in this instance.
MyItem.prototype.getBrandQuery = function() {
	var vBrand = this.brandQuery;
 	if( ! vBrand ) {
 		if( this.pater ) vBrand = this.pater.getQuery();
	}
	return vBrand;
}

// get a child by directory name from this instance.
MyItem.prototype.getChild = function( pDirName ) {
	return this.kidsByName[ pDirName ];
}

// get the parent of this instance.
MyItem.prototype.getParent = function() {
	return this.pater;
}

// return true if item is a collection type (default true).
MyItem.prototype.isCollectionType = function() {
	var vResult = true;
	if( this.collectionType != undefined ) {
		vResult = this.collectionType;
	}
	return vResult;
}

/**********************************/


/*
 * MyCollection class
 * Encapsulates the notion of the collection (of MyItems).  Instances of this class are NOT immutable, and should be singletons.
 */

// The collection is initialized with the MyItem instance representing the root of the collection (e.g., "Home").
function MyCollection( pRoot ) {
	this.root = pRoot;
	this.root.pathObj = Path.fromFQPath( "Home" );
}

// get an item from the collection its path...
MyCollection.prototype.getItem = function( pPath ) {
	var vResult = this.root;
	for( var vI = 1; vI < pPath.size(); vI++ ) {
		vResult = vResult.getChild( pPath.getElement( vI ) );
	}
	return vResult;
}



/**********************************/


/*
 * PatternSub class
 * Encapsulates the notion of a string pattern with replaceable parameters.  The general idea is to create a PatternSub instance with a
 * particular pattern (which is immutable), and then execute it one or more times with the parameters to be substituted.  The results of
 * the executions are concatenated, and are available through the toString() method.
 * The pattern syntax is very simple: the replacement parameters are numbered, from 1 to n.  Each place in the pattern where a replacement
 * should occur has the number surrounded by percentage marks.  A given replacement parameter number may appear any number of times in the
 * pattern; all appearances will be replaced with the same value.  There is no requirement that the parameter numbers appear in any
 * particular order.  For example, the string "This is %3%, showing %1% for %2% times.".  If executed with
 *     execute( "the speed", 2, "a recording speedometer" )
 * the resulting string would be "This is a recording speedometer, showing the speed for 2 times.".
 */

// create a new instance of this class.
function PatternSub( pPattern ) {
	this.pattern = "" + pPattern;
	this.subs = new Array();
}

// execute the pattern substitution with the given parameters, each of which must be present in the pattern.
PatternSub.prototype.execute = function() {
	var vArgs = new Array();
	for( var vI = 0; vI < arguments.length; vI++ ) vArgs.push( arguments[ vI ] );
	this.subs.push( this.pattern.replace( /%([0-9])+%/g, parmSub	) );
	
	function parmSub( pParm, pNum ) {
		var vOff = pNum - 1;
		var vResults = pParm;
		if( (vOff >= 0) && (vOff < vArgs.length) ) {
			vResults = vArgs[ vOff ];
		}
		return "" + vResults;
	}
}

// returns the current set of executions in the instance as a string.
PatternSub.prototype.toString = function() {
	var vResult = "";
	if( (this.subs != undefined) && (this.subs.length > 0) ) {
		vResult = this.subs.join( "" );
	}
	return vResult;
}

/**********************************/


/*
 * ServerObjects class
 * Handles requesting, caching, and events for several types of server objects, including text files, JSON files, and image files. Also 
 * provides a mechanism for context to be provided for events (e.g., the requestor's context), which normally isn't available.
 */
 
// creates a new instance of this class, which really should be a singleton.
// pMaxCacheSize: specifies the maximum size (in objects) of the cache.  Defaults to 25.
// pBusyNote: the ManagedEvent instance that is shown or hidden to provide a busy indication
// pDefaultErrorHandler: the event handler function that will be called when there is an error on a request that didn't specify its own error handler
function ServerObjects( pMaxCacheSize, pBusyNote, pDefaultErrorHandler ) {
	this.maxSize = pMaxCacheSize ? pMaxCacheSize : 25;
	this.defaultErrorHandler = pDefaultErrorHandler;
	this.objectCache = new Object();
	this.busyNote = pBusyNote;
	this.cacheSize = 0;
	this.cacheLRU = new Array();
	this.handlerCache = new Object();
	this.handlerIndex = 0;
	this.handlerCount = 0;
	ServerObjects.singleton = this;
}

// a reference to the (presumed) singleton instance of this class...
ServerObjects.singleton = null;

// Retrieves a text-based object -- from the cache if possible, from the server if necessary.
// pPath: the string relative path to the object being requested (e.g., "SlideRules/AcuFestus/description.html")
// pContext: an arbitrary (and optional) context to be passed to the event handlers
// pOnSuccess: a function that is called when the object has been retrieved.  These arguments are passed:
//   pText: a string with the contents of the retrieved text object
//   pContext: the context passed into this method
// pOnFailure: an optional function that is called when there is an error retrieving this object.  If undefined, the default error 
//   handler is called instead.  These arguments are passed:
//   pStatus: the status code as supplied by the server
//   pStatusText: the status text as supplied by the server
//   pContext: the context passed into this method
// pType: the type of request ("text" or "json")
// pNoCache: true if this request shouldn't be cached (defaults false)
// Returns nothing.
ServerObjects.prototype.getTextBasedObject = function( pPath, pContext, pOnSuccess, pOnFailure, pType, pNoCache ) {
	
	// do we have this object in the cache already?
	var vCached = this.getCached( pPath );
	if( vCached ) {
		
		// just call the success handler...
		pOnSuccess( vCached, pContext );
	}
	
	// not in the cache, so fetch it...
	else {
		
		// create our handler spec and store it away...
		var vOnFailure = pOnFailure ? pOnFailure : this.defaultErrorHandler;
		var vSpec = new HandlerSpec( pPath, pContext, pOnSuccess, vOnFailure, pType, pNoCache );
		var vSpecNum = this.addHandlerSpec( vSpec );
		
		// if there is no query in the specified path, add a random one to force browser cache misses...
		if( pPath.indexOf( '?' ) < 0 ) {
			pPath += ("?cacheBreaker=" + Math.random());
		}
		
		// make our event handler function wrapper...
		var vWrapper;
		eval( "vWrapper = function(pReq){ServerObjects.eventHandler(pReq," + vSpecNum + ");}"	);
		
		// and make our request...
		var vRequest = new Ajax.Request( 
			pPath,
			{
				method: "get",
				onComplete: vWrapper
			}
		);
	}
}

// Retrieves a text object -- from the cache if possible, from the server if necessary.
// pPath: the string relative path to the object being requested (e.g., "SlideRules/AcuFestus/description.html")
// pContext: an arbitrary (and optional) context to be passed to the event handlers
// pOnSuccess: a function that is called when the object has been retrieved.  These arguments are passed:
//   pText: a string with the contents of the retrieved text object
//   pContext: the context passed into this method
// pOnFailure: an optional function that is called when there is an error retrieving this object.  If undefined, the default error 
//   handler is called instead.  These arguments are passed:
//   pStatus: the status code as supplied by the server
//   pStatusText: the status text as supplied by the server
//   pContext: the context passed into this method
// pNoCache: true if this request shouldn't be cached (defaults false)
// Returns nothing.
ServerObjects.prototype.getTextObject = function( pPath, pContext, pOnSuccess, pOnFailure, pNoCache ) {
	this.getTextBasedObject( pPath, pContext, pOnSuccess, pOnFailure, "text", pNoCache );
}

// Retrieves a JSON object -- from the cache if possible, from the server if necessary.
// pPath: the string relative path to the object being requested (e.g., "SlideRules/AcuFestus/info.json")
// pContext: an arbitrary (and optional) context to be passed to the event handlers
// pOnSuccess: a function that is called when the object has been retrieved.  These arguments are passed:
//   pText: a string with the contents of the retrieved text object
//   pContext: the context passed into this method
// pOnFailure: an optional function that is called when there is an error retrieving this object.  If undefined, the default error 
//   handler is called instead.  These arguments are passed:
//   pStatus: the status code as supplied by the server
//   pStatusText: the status text as supplied by the server
//   pContext: the context passed into this method
// pNoCache: true if this request shouldn't be cached (defaults false)
// Returns nothing.
ServerObjects.prototype.getJSONObject = function( pPath, pContext, pOnSuccess, pOnFailure, pNoCache ) {
	this.getTextBasedObject( pPath, pContext, pOnSuccess, pOnFailure, "json", pNoCache );
}

// Retrieves an image object -- from the cache if possible, from the server if necessary.
// pPath: the string relative path to the object being requested (e.g., "SlideRules/AcuFestus/10D/Front.jpg")
// pContext: an arbitrary (and optional) context to be passed to the event handlers
// pOnSuccess: a function that is called when the object has been retrieved.  These arguments are passed:
//   pText: a string with the contents of the retrieved text object
//   pContext: the context passed into this method
// pOnFailure: an optional function that is called when there is an error retrieving this object.  If undefined, the default error 
//   handler is called instead.  These arguments are passed:
//   pStatus: the status code as supplied by the server
//   pStatusText: the status text as supplied by the server
//   pContext: the context passed into this method
// pNoCache: true if this request shouldn't be cached (defaults false)
// Returns nothing.
ServerObjects.prototype.getImageObject = function( pPath, pContext, pOnSuccess, pOnFailure, pNoCache ) {
	
	// do we have this object in the cache already?
	var vCached = this.getCached( pPath );
	if( vCached ) {
		
		// just call the success handler...
		pOnSuccess( vCached, pContext );
	}
	
	// not in the cache, so fetch it...
	else {
		
		// create our handler spec and store it away...
		var vOnFailure = pOnFailure ? pOnFailure : this.defaultErrorHandler;
		var vImage = new Image();
		var vSpec = new HandlerSpec( pPath, pContext, pOnSuccess, vOnFailure, "image", pNoCache, vImage );
		var vSpecNum = this.addHandlerSpec( vSpec );
		
		// make and attach our Image object and image event handler function wrappers...
		eval( "vImage.onload = function(){ServerObjects.imageEventHandler(" + vSpecNum + ",'L');}"	);
		eval( "vImage.onabort = function(){ServerObjects.imageEventHandler(" + vSpecNum + ",'A');}"	);
		eval( "vImage.onerror = function(){ServerObjects.imageEventHandler(" + vSpecNum + ",'E');}"	);
		
		// and make our request...
		vImage.src = pPath;
	}
}

// global function implementing the generic ServerObject image event handler, actually a wrapper for the standard event handler...
// pSpecNum: the handler spec number for this event
// pType: the code for the event type ("L" for onload, "E" for onerror, or "A" for onabort)
ServerObjects.imageEventHandler = function( pSpecNum, pType ) {
	
	// fish out our handler spec...
	var vSpec = ServerObjects.singleton.peekHandlerSpec( pSpecNum );
	
	// make our response object...
	var vReq = new Object();
	vReq[ "image" ] = vSpec.getImage();
	switch( pType ) {
		case "L":
			vReq[ "status" ] = 200;
			vReq[ "statusText" ] = "OK";
			break;
		case "A":
			vReq[ "status" ] = 409;
			vReq[ "statusText" ] = "Aborted";
			break;
		case "E":
			vReq[ "status" ] = 404;
			vReq[ "statusText" ] = "Error";
			break;
	}
	
	// now call the usual event handler with our newly minted response object...
	ServerObjects.eventHandler( vReq, pSpecNum );	
}

// global function implementing the generic ServerObject event handler...
// pReq: the response object or image object
// pSpecNum: the handler spec number for this event
ServerObjects.eventHandler = function( pReq, pSpecNum ) {
	
	// fish out our handler spec...
	var vSpec = ServerObjects.singleton.getHandlerSpec( pSpecNum );
	
	// did we succeed?
	if( (pReq.status < 100) || ((pReq.status >= 200) && (pReq.status < 300)) ) {
		
		// get the result...
		var vResult;
		var vError = 0;
		var vReason = "";
		switch( vSpec.getType() ) {
			case "text":
				vResult = pReq.responseText;
				break;
			case "json":
				try {
					vResult = eval( "(" + pReq.responseText + ")" );
				}
				catch( e ) {
					vError = 999;
					vReason = e.name + ": " + e.message;
				}
				break;
			case "image":
			  vResult = pReq.image;
				break;
		}
		
		// if all was ok...
		if( vError == 0 ) {
			
			// cache the result, if we're supposed to...
			if( ! vSpec.isNoCache() ) {
				ServerObjects.singleton.addObject( vSpec.getPath(), vResult );
			}
			
			// call the success handler...
			var vOnSuccess = vSpec.getSuccess();
			if( vOnSuccess ) vOnSuccess( vResult, vSpec.getContext() );
		}
		
		// oops, something went wrong...
		else {
		
			// call the failure handler...
			var vOnFailure = vSpec.getFailure();
			if( vOnFailure ) vOnFailure( vError, vReason, vSpec.getContext() );
		}
	}
	
	// nope, we got an error...
	else {
		
		// call the failure handler...
		var vOnFailure = vSpec.getFailure();
		if( vOnFailure ) vOnFailure( pReq.status, pReq.statusText, vSpec.getContext() );
	}
}

// add the given handler spec to our handler cache...
ServerObjects.prototype.addHandlerSpec = function( pSpec ) {
	var vResult = this.handlerIndex++;
	if( this.busyNote && (this.handlerCount == 0 ) ) this.busyNote.show();
	this.handlerCount++;
	this.handlerCache[ "h" + vResult ] = pSpec;
	return vResult;
}

// get the given handler spec, removing it from our cache...
ServerObjects.prototype.getHandlerSpec = function( pSpecNum ) {
	var vPropName = "h" + pSpecNum;
	var vResult = this.handlerCache[ vPropName ];
	delete this.handlerCache[ vPropName ];
	this.handlerCount--;
	if( this.busyNote && (this.handlerCount == 0 ) ) this.busyNote.hide();
	return vResult;
}

// get the given handler spec, without removing it from our cache...
ServerObjects.prototype.peekHandlerSpec = function( pSpecNum ) {
	var vPropName = "h" + pSpecNum;
	var vResult = this.handlerCache[ vPropName ];
	return vResult;
}

// add the given object to our object cache...
// pPath: the relative path of the object on the server
// pObj: the object
ServerObjects.prototype.addObject = function( pPath, pObj ) {
	if( ! this.objectCache[ pPath ] ) {
		if( this.cacheSize >= this.maxSize ) {
			delete this.objectCache[ this.cacheLRU.shift() ];
			this.cacheSize--;
		}
		this.objectCache[ pPath ] = pObj;
		this.cacheLRU.push( pPath );
		this.cacheSize++;
	}
}

// get the given object from our cache, if possible...
// pPath: the relative path of the object on the server
ServerObjects.prototype.getCached = function( pPath ) {
	var vResult = this.objectCache[ pPath ];
	if( vResult ) {
		
		// find the path in the LRU and move it to the end (most recent)...
		var vI = 0;
		var vFound = false;
		while( ! vFound ) {
			if( this.cacheLRU[ vI ] == pPath ) {
				vFound = true;
				var vThis = this.cacheLRU.splice( vI, 1 );
				this.cacheLRU.push( vThis[0] );
			} else {
				vI++;
			}
		}
	}
	return vResult;
}

/**********************************/


/*
 * HandlerSpec class
 * Bean to hold details that a ServerObjects event handler needs.
 * pPath: the string relative path to the object being requested
 * pContext: the optional arbitrary context to pass to the user's event handlers
 * pSuccess: the function to be called when the request succeeds
 * pFailure: the function to be called when the request fails
 * pType: the type of handler (one of "text", "json", or "image" )
 * pImage: the image, if this is an image type
 */
 
function HandlerSpec( pPath, pContext, pSuccess, pFailure, pType, pNoCache, pImage ) {
	this.mPath = pPath;
	this.mContext = pContext;
	this.mSuccess = pSuccess;
	this.mFailure = pFailure;
	this.mType = pType;
	this.mImage = pImage;
	this.mNoCache = pNoCache;
}

/**********************************/

// getters...
HandlerSpec.prototype.getPath = function() { return this.mPath; }
HandlerSpec.prototype.getContext = function() { return this.mContext; }
HandlerSpec.prototype.getSuccess = function() { return this.mSuccess; }
HandlerSpec.prototype.getFailure = function() { return this.mFailure; }
HandlerSpec.prototype.getType = function() { return this.mType; }
HandlerSpec.prototype.getImage = function() { return this.mImage; }
HandlerSpec.prototype.isNoCache = function() { return this.mNoCache; }


/*
 * ContextHandles class
 * Provides a way to associate an arbitrary object (e.g., a "context") with a numeric handle.  Primary intended use is to allow a numeric
 * handle in a call to an event handler (e.g., "onclick") to retrieve the arbitrary object/context.
 */
 
// Creates a new instance of this class.
function ContextHandles() {
	this.reset();
}

// Resets the object.  This is called in the constructor, and typically when a page is cleared.
ContextHandles.prototype.reset = function() {
	this.contextStore = new Object();
	this.contextCount = 0;
}

// Stores a new context, returning the assigned handle.
ContextHandles.prototype.store = function( pContext ) {
	this.contextStore[ this.contextCount ] = pContext;
	var vResult = this.contextCount;
	this.contextCount++;
	return vResult;
}

// Retrieves a context, given the handle.
ContextHandles.prototype.retrieve = function( pHandle ) {
	return this.contextStore[ pHandle ];
}

// Removes (from this instance) the context with the given handle.
ContextHandles.prototype.remove = function( pHandle ) {
	delete this.contextStore[ pHandle ];
}

/**********************************/


/*
 * IDMaker class
 * Provides a simple mechanism for generating unique DOM IDs.
 */

// create a new instance of this class.
function IDMaker() {
	this.reset();
}

// Reset this instance.  Typically invoked on screen clearing.
IDMaker.prototype.reset = function() {
	this.count = 0;
}

// Generates a new ID from this instance.
IDMaker.prototype.generate = function() {
	return "IDM0" + this.count++;
}

/**********************************/

/**********************************/


/*
 * Tag class
 * Provides a convenient means for generating HTML tags.
 */

// Create a new instance of this class.
// pTag: the tag name (e.g., the "a" in "<a>")
// pContent: the optional content between the opening and closing tags (which may be a Tag instance)
function Tag( pTag, pContent ) {
	this.tagKind = pTag;
	this.tagAttributes = new Object();
	this.tagContent = new Array();
	if( pContent ) this.addContent( pContent );
}

// tags with no closing tag
Tag.noClose = { tagimg: true };

// Adds content (ordered) to this instance (including other instances of Tag).  Handles any number of parameters.  Returns "this" for dot-chaining.
Tag.prototype.addContent = function() {
	for( var vI = 0; vI < arguments.length; vI++ ) {
		this.tagContent.push( arguments[ vI ] );
	}
	return this;
}

// Adds attributes to this instance.  Handles any number of name, value pairs of parameters.  Returns "this" for dot-chaining.
Tag.prototype.addAttribute = function( pName, pValue ) {
	for( var vI = 0; (vI+1) < arguments.length; vI += 2 ) {
		this.tagAttributes[ arguments[vI].toLowerCase() ] = arguments[vI+1];
	}
	return this;
}

// Renders this instance into a string.  
Tag.prototype.toString = function() {
	var vResult = new Array();
	vResult.push( "<" );
	vResult.push( this.tagKind );
	for( var vAttrName in this.tagAttributes ) {
		vResult.push( " " );
		vResult.push( vAttrName );
		vResult.push( "='" );
		vResult.push( this.tagAttributes[ vAttrName ] );
		vResult.push( "'" );
	}
	vResult.push( ">" );
	for( var vI = 0; vI < this.tagContent.length; vI++ ) {
		vResult.push( "" + this.tagContent[ vI ] );
	}
	if( ! Tag.noClose[ "tag" + this.tagKind ] ) {
		vResult.push( "</" );
		vResult.push( this.tagKind );
		vResult.push( ">" );
	}
	return vResult.join( "" );
}

/**********************************/


/*
 * ManagedElement class
 * Simplifies the management of DOM elements whose content and/or visibility is controlled by events.
 * pID: the value of the 'id' tag of the element to be managed.
 * pVisible: true if the element should be visible initially; false otherwise.
 * pRegenerative: true if mouse over/out of the managed element should control visibility (defaults false).
 * pContent: the initial content to assign to the element (content unchanged if null or undefined).
 */
function ManagedElement( pID, pVisible, pRegenerative, pContent ) {
	this.elementID = pID;
	this.elementRef = $( pID );
	this.setVisibility( pVisible );
	if( (pRegenerative) && pRegenerative ) this.setRegenerativeHandlers();
	this.setContent( pContent );
	this.onclickMode = null;
	this.onmouseoverMode = null;
	this.onmouseoutMode = null;
	ManagedElement.addInstance( this );
}

// Class property to hold a list of instances.
ManagedElement.instances = [];

// Class property to hold global event handler name.
ManagedElement.globalEventHandlerName = "ME";

// Class properties for handler type codes.
ManagedElement.ONCLICK = 0;
ManagedElement.ONMOUSEOVER = 1;
ManagedElement.ONMOUSEOUT = 2;

// Class property to hold context handles.
ManagedElement.contextHandles = new ContextHandles();

// Global event handler.
function ME( pHandle ) {
	
	// some setup...
	var vContext = ManagedElement.contextHandles.retrieve( pHandle );
	var vThis = vContext.instance;
	
	// get our mode...
	var vMode;
	switch( vContext.handlerType ) {
		case ManagedElement.ONCLICK:
			vMode = vThis.onclickMode;
			break;
		case ManagedElement.ONMOUSEOVER:
			vMode = vThis.onmouseoverMode;
			break;
		case ManagedElement.ONMOUSEOUT:
			vMode = vThis.onmouseoutMode;
			break;
	}
	
	// pursue the instructions in the mode...
	if( vMode.handler ) {
		vMode.handler( vThis, vContext.userHandle );
	}
	if( vMode.content ) {
		vThis.setContent( vMode.content );
	}
	if( vMode.visible != undefined ) {
		vThis.setVisibility( vMode.visible );
	}
	
	// skedaddle, with a false return to abort any further action on the event...
	return false;
}

// for private use only; adds this instance to our list of instances.  This enables global methods that affect all instances.
ManagedElement.addInstance = function( pInstance ) {
	ManagedElement.instances.push( pInstance );
}

// Sets the visibility of the managed element.
// pVisible: true for visible, false for invisible.
ManagedElement.prototype.setVisibility = function( pVisible ) {
	this.visible = pVisible;
	this.elementRef.style.visibility = pVisible ? "visible" : "hidden";
}

// Sets the given content of the element, replacing any content already there.
// pContent: the initial content to assign to the element (content unchanged if null or undefined).
ManagedElement.prototype.setContent = function( pContent ) {
	if( pContent ) {
		this.elementRef.innerHTML = "" + pContent;
	}
}

// hides the element.
ManagedElement.prototype.hide = function() {
	this.setVisibility( false );
}

// sets the left position of the element.
ManagedElement.prototype.setLeft = function( pPos ) {
	this.elementRef.style.left = ""+ pPos + "px";
}
// sets the width of the element.
ManagedElement.prototype.setWidth = function( pSize ) {
	this.elementRef.style.width = ""+ pSize + "px";
}
// sets the top position of the element.
ManagedElement.prototype.setTop = function( pPos ) {
	this.elementRef.style.top = ""+ pPos + "px";
}
// sets the height of element.
ManagedElement.prototype.setHeight = function( pSize ) {
	this.elementRef.style.height = ""+ pSize + "px";
}

// sets the element's position and size (left, top, width, height ).
ManagedElement.prototype.setLoc = function( pLeft, pTop, pWidth, pHeight ) {
	this.setLeft( pLeft );
	this.setTop( pTop );
	this.setWidth( pWidth );
	this.setHeight( pHeight );
}

// computes and set the location for a popup element that overlaps the element it is popping up over.
// pOverItem: the HTMLElement we're popping up over, and overlapping
// pVerticalOffset: the desired vertical offset of the popup relative to pOverItem (- means up)
ManagedElement.prototype.setOverlappingLocation = function( pOverItem, pVerticalOffset ) {
	var vWindowSize = windowSize();
	var vScreenWidth = vWindowSize[0];
	var vPopUpWidth = vScreenWidth * 0.2;
	var vOverWidth = pOverItem.offsetWidth;
	var vOff = Position.cumulativeOffset( pOverItem );
	var vLeftie = ((vOff[0] + vOverWidth/2) > vScreenWidth / 2);
	var vPopUpLeft;
	if( vLeftie ) {
		vPopUpLeft = vOff[0] + (pOverItem.offsetWidth / 2) - vPopUpWidth;
	} else {
		vPopUpLeft = vOff[0] + pOverItem.offsetWidth / 2;
	}
	vPopUpTop = vOff[1] + pVerticalOffset;
	this.setLeft( vPopUpLeft );
	this.setTop( vPopUpTop );
	this.setWidth( vPopUpWidth );
}

// shows the element with the given content (unchanged if null or undefined ).
ManagedElement.prototype.show = function( pContent ) {
	this.setContent( pContent );
	this.setVisibility( true );
}

// sets the onclick mode for the instance.
// pMode: object whose properties define the behavior of the onclick handler.  Properties that can be set:
//   visible: set to true or false to control visibility when the event occurs; if absent visibility is not affected 
//   content: set to text that should be set as the content of the element when the event occurs
//   handler: set to a function to be called upon this event (before above actions); function will be called with two arguments:
//      pME: the ManagedElement instance related to the call
//      pHandle: the handle for the context (provided in the call to the event handler)
ManagedElement.prototype.setOnclickMode = function( pMode ) {
	this.onclickMode = pMode;
}

// sets the onmouseover mode for the instance.
// pMode: object whose properties define the behavior of the onmouseover handler.  Properties that can be set:
//   visible: set to true or false to control visibility when the event occurs; if absent visibility is not affected 
//   content: set to text that should be set as the content of the element when the event occurs
//   handler: set to a function to be called upon this event (before above actions); function will be called with two arguments:
//      pME: the ManagedElement instance related to the call
//      pHandle: the handle for the context (provided in the call to the event handler)
ManagedElement.prototype.setOnmouseoverMode = function( pMode ) {
	this.onmouseoverMode = pMode;
}

// sets the onmouseout mode for the instance.
// pMode: object whose properties define the behavior of the onmouseout handler.  Properties that can be set:
//   visible: set to true or false to control visibility when the event occurs; if absent visibility is not affected 
//   content: set to text that should be set as the content of the element when the event occurs
//   handler: set to a function to be called upon this event (before above actions); function will be called with two arguments:
//      pME: the ManagedElement instance related to the call
//      pHandle: the handle for the context (provided in the call to the event handler)
ManagedElement.prototype.setOnmouseoutMode = function( pMode ) {
	this.onmouseoutMode = pMode;
}

// provides the onclick attribute string to insert into an HTML element.
// pHandle: the user-program supplied context handle
ManagedElement.prototype.getOnclickString = function( pHandle ) {
	var vHandle = ManagedElement.contextHandles.store( 
		{ 
			userHandle: pHandle, 
			handlerType: ManagedElement.ONCLICK,
			instance: this
		} 
	);
	var vResults = [];
	vResults.push( "return onclick='" );
	vResults.push( ManagedElement.globalEventHandlerName );
	vResults.push( "(" );
	vResults.push( vHandle );
	vResults.push( ");'" );
	return vResults.join( "" );
}

// provides the onmouseover attribute string to insert into an HTML element.
// pHandle: the user-program supplied context handle
ManagedElement.prototype.getOnMouseoverString = function( pHandle ) {
	var vHandle = ManagedElement.contextHandles.store( 
		{ 
			userHandle: pHandle, 
			handlerType: ManagedElement.ONMOUSEOVER,
			instance: this
		} 
	);
	var vResults = [];
	vResults.push( "return onmouseover='" );
	vResults.push( ManagedElement.globalEventHandlerName );
	vResults.push( "(" );
	vResults.push( vHandle );
	vResults.push( ");'" );
	return vResults.join( "" );
}

// provides the onmouseout attribute string to insert into an HTML element.
// pHandle: the user-program supplied context handle
ManagedElement.prototype.getOnMouseoutString = function( pHandle ) {
	var vHandle = ManagedElement.contextHandles.store( 
		{ 
			userHandle: pHandle, 
			handlerType: ManagedElement.ONMOUSEOUT,
			instance: this
		} 
	);
	var vResults = [];
	vResults.push( "return onmouseout='" );
	vResults.push( ManagedElement.globalEventHandlerName );
	vResults.push( "(" );
	vResults.push( vHandle );
	vResults.push( ");'" );
	return vResults.join( "" );
}

// convenience method that produces mouseover and mouseout strings at the same time, using the same handle.
// pHandle: the user-program supplied context handle
ManagedElement.prototype.getOnMouseOverOutString = function( pHandle ) {
	return this.getOnMouseoverString( pHandle ) + " " + this.getOnMouseoutString( pHandle );
}

// sets onmouseover and onmouseout handlers on managed element, to allow regenerative visibility control
ManagedElement.prototype.setRegenerativeHandlers = function() {
	var vRef = this.elementRef;
	var vHandle = ManagedElement.contextHandles.store( this );
	eval( "vRef.onmouseover = function() {ManagedElement.regenerativeEventHandler(true," + vHandle + "); return false;}" );
	eval( "vRef.onmouseout = function() {ManagedElement.regenerativeEventHandler(false," + vHandle + "); return false;}" );
}

// the regenerative global event handler.
ManagedElement.regenerativeEventHandler = function( pOver, pHandle ) {
	var vInstance = ManagedElement.contextHandles.retrieve( pHandle );
	vInstance.setVisibility( pOver );
}

/**********************************/
/**********************************/


/*
 * ImageViewer class
 * Implements a very flexible viewer for an image, including zoom and pan capabilities.
 * pImage: the image to be viewed
 * pTitle: the title of the image
 * pMargin: the margin (in pixels) around the viewer (defaults to zero)
 * pDPI: the scale of the image in dots/inch, if known
 * pDefaultClick: the integer value of the default click mode
 * pDefaultDrag: the integer value of the default drag mode
 */
function ImageViewer( pImage, pTitle, pMargin, pDPI, pDefaultClick, pDefaultDrag ) {
	this.mouseOver = false;
	this.mouseDown = false;
	this.marks = [];
	this.image = pImage;
	this.title = pTitle;
	this.margin = (pMargin) ? pMargin : 0;
	this.dpi = (pDPI) ? Number( pDPI ) : null;
	this.setHTML();
	this.defaultClick = (pDefaultClick) ? pDefaultClick : ImageViewer.CLICK_CENTER;
	this.defaultDrag = (pDefaultDrag) ? pDefaultDrag : ImageViewer.DRAG_MAG;
	this.fit( true );
	this.show();
	this.bodyArea = this.bodyRef.offsetWidth * this.bodyRef.offsetHeight;
}

// class variables for constants:
ImageViewer.CLICK_CENTER = 1;
ImageViewer.CLICK_CENTER_ZOOM = 2;
ImageViewer.CLICK_MEASURE = 3;
ImageViewer.DRAG_ZOOM_BOX = 1;
ImageViewer.DRAG_GRAB = 2;
ImageViewer.DRAG_MAG = 3;
ImageViewer.CLICK_MS = 250;   // the max time (in ms) that the button can be down on a click. 
ImageViewer.MAG_SIZE = 0.4;  // the size of the magnifier box relative to the viewer dimensions.

// class variable for current instance.
ImageViewer.currentInstance;

// class method for handling onclick in the close button.
ImageViewer.onClose = function() {
	ImageViewer.currentInstance.hide();
	return false;
}

// class method for handling onclick in the help button.
ImageViewer.onHelp = function() {
	ImageViewer.currentInstance.showHelp();
	return false;
}

// class method for handling onclick in the fit button.
ImageViewer.onFit = function( pLarge ) {
	ImageViewer.currentInstance.fit( pLarge );
	return false;
}

// class method for handling onclick in the full button.
ImageViewer.onFull = function() {
	var vThis = ImageViewer.currentInstance;
	vThis.setZoom( 1.0 );
	vThis.centerImage();
	vThis.placeImage();
	return false;
}

// class method for handling resize.
ImageViewer.onResize = function() {
	var vThis = ImageViewer.currentInstance;
	if( vThis ) vThis.placeImage();
}

// class method for handling zoom out.
ImageViewer.onZoomOut = function() {
	var vThis = ImageViewer.currentInstance;
	vThis.setMagnification( vThis.magnification * 0.5 );
	vThis.placeImage();
	return false;
}

// class method for handling zoom in.
ImageViewer.onZoomIn = function() {
	var vThis = ImageViewer.currentInstance;
	vThis.setMagnification( vThis.magnification * 2 );
	vThis.placeImage();
	return false;
}

// class method for handling onmousedown for ivDiv.
ImageViewer.onMouseDown = function( pEvent ) {
	
	// some setup...
	var vThis = ImageViewer.currentInstance;
	vThis.mouseOver = true; // just to make sure...
	
	// only proceed if we're not helping...
	if( ! vThis.showingHelp ) {
		
		// get the event (IE or not)...
		if( ! pEvent ) pEvent = window.event;
		
		// record where we were when the button dropped...
		vThis.mouseDownLoc = vThis.ivDivEx.getMouseLocation( pEvent );
		vThis.mouseOrigDownLoc = vThis.mouseDownLoc;
		
		// figure out what just happened...
		var vLeftButtonDown = false;
		var vIEButt = pEvent.button;
		if( pEvent.which ) { // if we're non-IE...
			if( pEvent.which == 1 ) {
				vLeftButtonDown = true;
			}
		}
		else if( vIEButt ) { // if we're IE...
			if( (vIEButt & 1) == 1 ) {
				vLeftButtonDown = true;
			}
		}
		
		// some things we want to do only if the left button just went down...
		if( vLeftButtonDown ) {
			
			// record our downness...
			vThis.mouseDown = true;
			
			// register move and up handlers, if the left button went down...
			Event.observe( vThis.ivDiv, "mouseup", ImageViewer.onMouseUp, false );
			Event.observe( vThis.ivDiv, "mousemove", ImageViewer.onMouseMove, false );
			Event.observe( vThis.ivDiv, "mouseout", ImageViewer.onMouseOut, false );
			Event.observe( vThis.ivDiv, "mouseover", ImageViewer.onMouseOver, false );
			
			// start the "begin drag" timer...
			setTimeout( "ImageViewer.beginDrag();", ImageViewer.CLICK_MS );
			
			// if we're in drag zoom box mode, set up our box...
			if( Number( $F( vThis.ivDragDD )) == ImageViewer.DRAG_ZOOM_BOX ) {
				vThis.zoomOrigin = vThis.ivDivEx.getMouseLocation( pEvent );
				vThis.zoomBox = new ElementRectangle( vThis.zoomOrigin, 0, 0 );
			}
		}
		
		vThis.mouseDownTime = new Date().getTime();
		Event.stop( pEvent );
	}
}

// class method for handling the begin drag timer event.
ImageViewer.beginDrag = function() {
	
	var vThis = ImageViewer.currentInstance;
	if( vThis.mouseDown ) {
		switch( Number( $F( vThis.ivDragDD )) ) {
			
			case ImageViewer.DRAG_MAG:
				ImageViewer.showMag();
				break;
				
			case ImageViewer.DRAG_GRAB:
				vThis.ivDivEx.setCursor( "move" );
				break;
		}
	}
}

// class method for showing magnifier.
// pMouseLoc: ElementLocation on the ivDiv giving the mouse location.
ImageViewer.showMag = function() {
	
	// some setup...
	var vThis = ImageViewer.currentInstance;
	
	// make sure we've got the size set properly...
	if( ! vThis.ivMagDivEx.isVisible() ) {
		vThis.magWidth  = Math.round( ImageViewer.MAG_SIZE * vThis.ivDivEx.getContentWidth() & 0xFFFFFFFE );
		vThis.magHeight = Math.round( ImageViewer.MAG_SIZE * vThis.ivDivEx.getContentHeight() & 0xFFFFFFFE );
	}
	
	// get our box...
	var vMagCtr = vThis.mouseDownLoc;
	var vMagBoxOrigin = ElementLocation
		.fromContentPoint( 
			vThis.ivDivEx,
			new Point( vMagCtr.getContentPoint().getX() - vThis.magWidth / 2,
								 vMagCtr.getContentPoint().getY() - vThis.magHeight / 2 ) 
		);
	var vMagBox = new ElementRectangle( vMagBoxOrigin, vThis.magHeight, vThis.magWidth );
	
	// put the magnifier in the right place on the image...
	vThis.ivMagDivEx.setContentBox( vMagBox );
	vThis.ivMagDivEx.setVisibility( true );
	
	// now figure out where we are on the magnified image...
	var vImgDivCtr = vThis.ivImgDivEx.getEquivParentLocation( vMagCtr );
	var vMagBoxOff = new Point( -vThis.magWidth / 4, -vThis.magHeight / 4 );
	var vImgCtr = new Point( vImgDivCtr.getContentPoint().getX() / vThis.zoom,
													 vImgDivCtr.getContentPoint().getY() / vThis.zoom );
	var vMagImgBoxOrigin = ElementLocation.fromContentPoint( vThis.ivMagDivEx, vMagBoxOff.add( vImgCtr ).mul( -2 ) );
	vThis.ivMagImgDivEx.setContentLocation( vMagImgBoxOrigin );
}

// class method for handling onmouseup for ivDiv.
ImageViewer.onMouseUp = function( pEvent ) {
	
	// some setup...
	var vThis = ImageViewer.currentInstance;
	
	// only proceed if we're not helping...
	if( ! vThis.showingHelp ) {
		
		// get the event, IE or not...
		if( ! pEvent ) pEvent = window.event;
		
		// figure out what just happened...
		var vLeftUp = false;
		var vIEButt = pEvent.button;
		if( pEvent.which ) { // if we're non-IE...
			if( pEvent.which == 1 ) {
				vLeftUp = true;
			}
		}
		else if( vIEButt ) { // if we're IE...
			if( (vIEButt & 1) == 1 ) {
				vLeftUp = true;
			}
		}
		
		// if the left button just went up...
		if( vLeftUp ) {
			
			// record our upness...
			vThis.mouseDown = false;
			var vMouseDownLoc = vThis.ivDivEx.getMouseLocation( pEvent );
			var vDelta = vMouseDownLoc.getDelta( vThis.mouseDownLoc );
			vThis.mouseDownLoc = vMouseDownLoc;
				
			// if it was just a click, and not a drag...
			var vDownTime = new Date().getTime() - vThis.mouseDownTime;
			if( (vDownTime < ImageViewer.CLICK_MS) && (vThis.mouseDownLoc.getDistance( vThis.mouseOrigDownLoc ) < 3) ) {
				
				// do the right thing, based on our mode...
				switch( Number( $F( vThis.ivClickDD )) ) {
					
					case ImageViewer.CLICK_CENTER_ZOOM:
						vThis.centerImg = vThis.ivImgDivEx.getEquivParentLocation( vThis.mouseDownLoc ).getContentPoint().mul( 1/vThis.zoom );
						vThis.setMagnification( vThis.magnification * 2 );
						vThis.placeImage();
						break;
					
					case ImageViewer.CLICK_CENTER:
						vThis.centerImg = vThis.ivImgDivEx.getEquivParentLocation( vThis.mouseDownLoc ).getContentPoint().mul( 1/vThis.zoom );
						vThis.placeImage();
						break;
					
					case ImageViewer.CLICK_MEASURE:
						var vImgLoc = vThis.ivImgDivEx.getEquivParentLocation( vThis.mouseDownLoc ).getContentPoint().mul( 1/vThis.zoom );
						vThis.marks.push( vImgLoc );
						while( vThis.marks.length >= 3 ) vThis.marks.shift();
						if( vThis.marks.length == 2 ) {
							var vDist = vThis.marks[0].getDistance( vThis.marks[1] );
							if( vThis.dpi ) {
								var vInches = vDist / vThis.dpi;
								var vCms = vInches * 2.54;
								vThis.ivMsrText.innerHTML = "" + vInches.toFixed( 2 ) + " in / " + vCms.toFixed( 2 ) + " cm";
							} else {
								vThis.ivMsrText.innerHTML = "" + vDist.toFixed() + " pixels";
							}
							if( vDist < 5 ) {
								vThis.marks = [];
							}
						}
						if( vThis.marks.length != 2 ) {
							vThis.ivMsrText.innerHTML = "";
						}
						vThis.placeImage();
						break;
				}
			} 
			
			// else if it was a drag...
			else {
				switch( Number( $F( vThis.ivDragDD )) ) {
					
					case ImageViewer.DRAG_ZOOM_BOX:
						vThis.fitBox( vThis.zoomBox );
						break;
				}
			}
			
			// lose the zoom box, if it's there...
			if( vThis.ivZoomBoxEx.isVisible() ) {
				vThis.ivZoomBoxEx.setVisibility( false );
			}
			
			// lose the magnifier, if it's there...
			if( vThis.ivMagDivEx.isVisible() ) {
				vThis.ivMagDivEx.setVisibility( false );
			}
			
			// put the mouse cursor back to rights...
			vThis.ivDivEx.setCursor( "url(target.cur), crosshair" );

		}
		
		// lose our event listeners, if the left button just went up...
		if( vLeftUp ) {
			Event.stopObserving( vThis.ivDiv, "mouseup", ImageViewer.onMouseUp, false );
			Event.stopObserving( vThis.ivDiv, "mousemove", ImageViewer.onMouseMove, false );
			Event.stopObserving( vThis.ivDiv, "mouseout", ImageViewer.onMouseOut, false );
			Event.stopObserving( vThis.ivDiv, "mouseover", ImageViewer.onMouseOver, false );
		}
		Event.stop( pEvent );
	}
}

// class method for handling onmousemove for ivDiv.
ImageViewer.onMouseMove = function( pEvent ) {
	
	// some setup...
	var vThis = ImageViewer.currentInstance;
	
	// proceed only if we're not helping...
	if( ! vThis.showingHelp ) {
		
		// grab the event, IE or not...
		if( ! pEvent ) pEvent = window.event;
		
		// do the right thing, based on our drag mode...
		var vThis = ImageViewer.currentInstance;
		var vMouseDownLoc = vThis.ivDivEx.getMouseLocation( pEvent );
		var vDelta = vMouseDownLoc.getDelta( vThis.mouseDownLoc );
		vThis.mouseDownLoc = vMouseDownLoc;
		
		switch( Number( $F( vThis.ivDragDD )) ) {
			
			case ImageViewer.DRAG_GRAB:
				
				vThis.centerImg = vThis.centerImg.sub( vDelta.mul( 1/vThis.zoom) );
				vThis.placeImage();
				break;
				
			case ImageViewer.DRAG_ZOOM_BOX:
			
				vThis.zoomBox = vThis.ivDivEx.getRectangleFromCorners( vThis.zoomOrigin, vThis.mouseDownLoc );
				vThis.ivZoomBoxEx.setContentBox( vThis.zoomBox );
				vThis.ivZoomBoxEx.setVisibility( true );	
				break;
				
			case ImageViewer.DRAG_MAG:
			
				ImageViewer.showMag();
				break;
		}
		
	}
	Event.stop( pEvent );
}

// class method for handling onmouseover for ivDiv.
ImageViewer.onMouseOver = function( pEvent ) {
	ImageViewer.currentInstance.mouseOver = true;
}

// class method for handling onmouseout for ivDiv.
ImageViewer.onMouseOut = function( pEvent ) {
	ImageViewer.currentInstance.mouseOver = false;
}

// class method for absorbing unwanted events.
ImageViewer.onAbsorber = function( pEvent ) {
	if( ! pEvent ) pEvent = window.event;
	Event.stop( pEvent );
}

// shows help for the viewer, overlaying the current image...
ImageViewer.prototype.showHelp = function() {
	var vText = [];
	// 
	vText.push( "<div id='ivHelp' style='position:absolute;background-color:#fff0f0;right:0px;left:0px;top:0px;bottom:0px;padding:25px;cursor:auto;'>" );
	vText.push( "<iframe id='ivHelpIF' src='viewerhelp.html' style='position:static;background-color:white;width:100%;'></iframe>" );
	vText.push( "<div style='position:absolute;bottom:25px;left:25px;height:25px;'>" );
	vText.push( "<button onclick='return ImageViewer.onHelpClose();'>Close</button>" );
	vText.push( "</div></div>" );
	new Insertion.Bottom( this.ivDiv, vText.join( "" ) );
	var vHt = $( "ivHelp" ).offsetHeight;
	var vIF = $( "ivHelpIF" );
	vIF.style.height = "" + (vHt - 100) + "px";
	this.showingHelp = true;
}

// class function to close the open help screen...
ImageViewer.onHelpClose = function() {
	Element.remove( "ivHelp" );
	ImageViewer.currentInstance.showingHelp = false;
	return false;
}

// sets the HTML for the viewer in the document, if it's not already there.
ImageViewer.prototype.setHTML = function() {
	this.bodyRef = document.getElementsByTagName( "body" )[0];
	if( ! $( "ivDiv" ) ) {
		var vIvMagDiv = new Tag( "div" )
			.addAttribute(
				"style", "position:absolute;overflow:hidden;visibility:hidden;border:2px double red;",
				"id", "ivMagDiv"
			);
		var vIvMagImgDiv = new Tag( "div" )
			.addAttribute(
				"style", "position:absolute;",
				"id", "ivMagImgDiv"
			);
		var vIvMagImg = new Tag( "img" )
			.addAttribute(
				"id", "ivMagImg"
			);
		vIvMagImgDiv.addContent( vIvMagImg );
		vIvMagDiv.addContent( vIvMagImgDiv );
		var vIvTitle = new Tag( "div" )
			.addContent( this.title )
			.addAttribute(
			  "id", "ivTitleDiv",
				"style", "position:absolute;left:20px;top:20px;color:black;background-color:white;border:1px solid black;" +
							   "font-size:1.1em;font-weight:bold;padding-left:10px;padding-right:10px;padding-top:3px;padding-bottom:3px;"
			);
		var vIvDiv = new Tag( "div" )
			.addAttribute( 
				"id", "ivDiv",
				"style", "visibility: hidden;" 
			);
		var vIvImgDiv = new Tag( "div" )
			.addAttribute( 
				"id", "ivImgDiv",
				"style", "position:absolute;"
			);
		var vIvImg = new Tag( "img" )
			.addAttribute(
				"id", "ivImg" 
			);
		vIvImgDiv.addContent( vIvImg );
		
		var vIvToolsDiv = new Tag( "div" )
			.addAttribute( 
				"style", "position:absolute;right:20px;top:20px;color:black;background-color:#f0fff0;border:1px solid black;" +
							   "font-size:0.85em;font-weight:bold;padding-left:10px;padding-right:10px;padding-top:5px;padding-bottom:5px;" +
							   "cursor:auto;",
				"id", "ivTools" 
			);
		var vIvZoomOutButton = new Tag( "button" )
			.addContent( "Zoom Out" )
			.addAttribute(
				"style", "font-size: 0.85em;font-weight: bold;",
				"onclick", "return ImageViewer.onZoomOut();"
			);
		var vIvZoomInButton = new Tag( "button" )
			.addContent( "Zoom In" )
			.addAttribute(
				"style", "font-size: 0.85em;font-weight: bold;",
				"onclick", "return ImageViewer.onZoomIn();"
			);
		var vIvFitLargeButton = new Tag( "button" )
			.addContent( "Fit Entire" )
			.addAttribute(
				"style", "font-size: 0.85em;font-weight: bold;",
				"onclick", "return ImageViewer.onFit( true );"
			);
		var vIvFitSmallButton = new Tag( "button" )
			.addContent( "Fit Small" )
			.addAttribute(
				"style", "font-size: 0.85em;font-weight: bold;",
				"onclick", "return ImageViewer.onFit( false );"
			);
		var vIvFullButton = new Tag( "button" )
			.addContent( "Full" )
			.addAttribute(
				"style", "font-size: 0.85em;font-weight: bold;",
				"onclick", "return ImageViewer.onFull();"
			);
		var vIvHelpButton = new Tag( "button" )
			.addContent( "Help" )
			.addAttribute(
				"style", "font-size: 0.85em;font-weight: bold;",
				"onclick", "return ImageViewer.onHelp();"
			);
		var vIvCloseButton = new Tag( "button" )
			.addContent( "Close" )
			.addAttribute(
				"style", "font-size: 0.85em;font-weight: bold;",
				"onclick", "return ImageViewer.onClose();"
			);
		var vIvClickDD = new Tag( "select" )
			.addContent( "<option value='" + ImageViewer.CLICK_CENTER + "'>Center</option>",
									 "<option value='" + ImageViewer.CLICK_CENTER_ZOOM + "'>Center & Zoom</option>",
									 "<option value='" + ImageViewer.CLICK_MEASURE + "'>Measure</option>" )
			.addAttribute( "id", "ivClickDD", "style", "font-size: 0.85em;font-weight: bold;" );
		var vIvDragDD = new Tag( "select" )
			.addContent( "<option value='" + ImageViewer.DRAG_ZOOM_BOX + "'>Zoom Box</option>", 
									 "<option value='" + ImageViewer.DRAG_GRAB + "'>Grab & Drag</option>",
									 "<option value='" + ImageViewer.DRAG_MAG + "'>Magnifier</option>" )
			.addAttribute( "id", "ivDragDD", "style", "font-size: 0.85em;font-weight: bold;" );
		var vIvTools1 = new Tag( "p" )
			.addContent( vIvFullButton, "&nbsp;", vIvFitSmallButton, "&nbsp;", vIvFitLargeButton, "&nbsp;", vIvZoomOutButton, "&nbsp;", 
									 vIvZoomInButton, "&nbsp;", vIvHelpButton, "&nbsp;", vIvCloseButton )
			.addAttribute( "style", "margin:0;padding:3px;" );
		var vIvMsrText = new Tag( "span" )
			.addAttribute( "id", "ivMsrText", "style", "" );
		var vIvTools2 = new Tag( "p" )
			.addContent( "On Click: ", vIvClickDD, "&nbsp;&nbsp;On Drag: ", vIvDragDD, "&nbsp;", vIvMsrText )
			.addAttribute( "style", "margin:0;padding:3px;" );
		vIvToolsDiv.addContent( vIvTools1, vIvTools2 );
			
		var vIvBarDiv = new Tag( "div" )
			.addAttribute(
				"style", "position:absolute;right:20px;bottom:20px;visibility:hidden;",
				"id", "ivBar"
			);
		var vIvBarTbl = new Tag( "table" )
			.addAttribute(
				"cellpadding", "2",
				"cellspacing", "0",
				"border", "0"
			);
		var vIvBarInRow = new Tag( "tr" );
		var vIvBarInBar = new Tag( "td", "<img id='ivInBar' src='InchBar.png' border='1'>" )
			.addAttribute(
				"style", "text-align:right;"
			);
		var vIvBarInLab = new Tag( "td", "<img src='InchLabel.png' border='1'>" );
		vIvBarInRow.addContent( vIvBarInBar, vIvBarInLab );
		var vIvBarCmRow = new Tag( "tr" );
		var vIvBarCmBar = new Tag( "td", "<img id='ivCmBar' src='CmBar.png' border='1'>" )
			.addAttribute(
				"style", "text-align:right;"
			);
		var vIvMsr1 = new Tag( "div" )
			.addAttribute( "id", "ivMsr1", "style", "position:absolute;visibility:hidden;padding:0;margin:0;" )
			.addContent( "<img src='MeasureMark.png' style='position:absolute;top:0;left:0;border:0;padding:0;margin:0;'>" );
		var vIvMsr2 = new Tag( "div" )
			.addAttribute( "id", "ivMsr2", "style", "position:absolute;visibility:hidden;padding:0;margin:0;" )
			.addContent( "<img src='MeasureMark.png' style='position:absolute;top:0;left:0;border:0;padding:0;margin:0;'>" );
		var vIvBarCmLab = new Tag( "td", "<img src='CmLabel.png' border='1'>" );
		vIvBarCmRow.addContent( vIvBarCmBar, vIvBarCmLab );
		vIvBarTbl.addContent( vIvBarInRow, vIvBarCmRow );
		vIvBarDiv.addContent( vIvBarTbl );
		var vZB = new Tag( "div" ).addAttribute(
			"id","ivZoomBox",
			"style","position:absolute;border:dotted 2px cyan;visibility:hidden;"
		);
		vIvDiv.addContent( vIvImgDiv, vZB, vIvMsr1, vIvMsr2, vIvMagDiv, vIvTitle, vIvToolsDiv, vIvBarDiv );
		
		new Insertion.Bottom( this.bodyRef, vIvDiv.toString() );
		this.setReferences();
		Event.observe( this.ivDiv, "mousedown", ImageViewer.onMouseDown, false );
		Event.observe( this.ivTools, "mousedown", ImageViewer.onAbsorber, false );
		Event.observe( this.ivTools, "mouseup", ImageViewer.onAbsorber, false );
		window.onresize = ImageViewer.onResize;
	}
	else {
		this.setReferences();
	}
	var vIvDivStyle = new PatternSub(
		"position: fixed;" +
		"overflow: hidden;" +
	  "top: %1%px;" +
	  "left: %1%px;" +
	  "bottom: %1%px;" +
	  "right: %1%px;" +
	  "background-color: #faf0c6;" +
	  "border: 3px solid black;"
	);
	vIvDivStyle.execute( this.margin );
	setElementStyle( this.ivDiv, vIvDivStyle );
	
	// set our image in place...
	this.ivTitleDiv.innerHTML = this.title;
	this.ivImg.src = this.image.src;
	this.ivMagImg.height = this.image.height * 2;
	this.ivMagImg.width = this.image.width * 2;
	this.ivMagImg.src = this.image.src;
	
	// set up the default target cursor for the image...
	this.ivDivEx.setCursor( "url(target.cur), crosshair" );
}

// set up references.
ImageViewer.prototype.setReferences = function() {
	this.ivDiv = $( "ivDiv" );
	this.ivDivEx = new ExElement( this.ivDiv );
	this.ivImg = $( "ivImg" );
	this.ivImgEx = new ExElement( this.ivImg );
	this.ivTools = $( "ivTools" );
	this.ivImgDiv = $( "ivImgDiv" );
	this.ivImgDivEx = new ExElement( this.ivImgDiv );
	this.ivBar = $( "ivBar" );
	this.ivInBar = $( "ivInBar" );
	this.ivCmBar = $( "ivCmBar" );
	this.ivTitleDiv = $( "ivTitleDiv" );
	this.ivClickDD = $( "ivClickDD" );
	this.ivDragDD = $( "ivDragDD" );
	this.ivMagDiv = $( "ivMagDiv" );
	this.ivMagDivEx = new ExElement( this.ivMagDiv );
	this.ivMagImgDiv = $( "ivMagImgDiv" );
	this.ivMagImgDivEx = new ExElement( this.ivMagImgDiv );
	this.ivMagImg = $( "ivMagImg" );
	this.ivMsr1 = $( "ivMsr1" );
	this.ivMsr1Ex = new ExElement( this.ivMsr1 );
	this.ivMsr2 = $( "ivMsr2" );
	this.ivMsr2Ex = new ExElement( this.ivMsr2 );
	this.ivMsrText = $( "ivMsrText" );
	this.ivZoomBox = $( "ivZoomBox" );
	this.ivZoomBoxEx = new ExElement( this.ivZoomBox );
}

// fits the entire image to the viewer.
// pLarge: true if fit to the large dimension; defaults to true.
ImageViewer.prototype.fit = function( pLarge ) {
	
	// some setup...
	if( pLarge == undefined ) pLarge = true;
	
	// compute the fitting size...
	var vTgHt = this.ivDivEx.getContentHeight();
	var vTgWd = this.ivDivEx.getContentWidth();
	var vHtRat = this.image.height / vTgHt;
	var vWdRat = this.image.width / vTgWd;
	if( (pLarge && (vHtRat > vWdRat)) || ((! pLarge) && (vHtRat < vWdRat) ) ) {
		this.setZoom( 1.0 / vHtRat );
	} else {
		this.setZoom( 1.0 / vWdRat );
	}
	
	// center the image and show it...
	this.centerImage();
	this.placeImage();
}

// fits the given box to the viewer.
// pBox: an ElementRectangle on the viewer describing a box on the scaled image being viewed
// pLarge: true if fit to the large dimension; defaults to true.
ImageViewer.prototype.fitBox = function( pBox, pLarge ) {
	if( pLarge == undefined ) pLarge = true;
	var vImgDivBox = this.ivImgDivEx.getEquivParentRectangle( pBox );
	var vImgOrig = new Point( vImgDivBox.getOrigin().getContentPoint().getX() / this.zoom,
														vImgDivBox.getOrigin().getContentPoint().getY() / this.zoom );
	var vImgRect = new Rectangle( vImgOrig, vImgDivBox.getHeight() / this.zoom, vImgDivBox.getWidth() / this.zoom );
	var vTgHt = this.ivDivEx.getContentHeight();
	var vTgWd = this.ivDivEx.getContentWidth();
	var vHtRat = vImgRect.getHeight() / vTgHt;
	var vWdRat = vImgRect.getWidth() / vTgWd;
	if( (pLarge && (vHtRat > vWdRat)) || ((! pLarge) && (vHtRat < vWdRat) ) ) {
		this.setZoom( 1.0 / vHtRat );
	} else {
		this.setZoom( 1.0 / vWdRat );
	}
	this.centerImg = vImgRect.getCenter();
	this.placeImage();
}

// set the zoom factor (also sets magnification).
ImageViewer.prototype.setZoom = function( pFactor ) {
	this.setMagnification( Math.pow( pFactor, 2 ) );
}

// set the magnification factor (also sets zoom).
ImageViewer.prototype.setMagnification = function( pFactor ) {
	this.magnification = Math.max( Math.min( pFactor, 16 ), 1 / 16);
	this.zoom = Math.pow( this.magnification, 0.5 );
}

// places the image on the viewer according to the current zoom and centering.
ImageViewer.prototype.placeImage = function() {
	
	// set the image's size (if it's not already correct)...
	var vNwHt = Math.round(this.zoom * this.image.height);
	var vNwWd = Math.round(this.zoom * this.image.width);
	if( (vNwHt != this.ivImg.height) || (vNwWd != this.ivImg.width) ) {
		this.ivImg.height = vNwHt;
		this.ivImg.width = vNwWd;
	}
	
	// set the image's position...
	var vLeft = this.ivDivEx.getCenter().getContentPoint().getX() - (this.centerImg.getX() * this.zoom);
	var vTop = this.ivDivEx.getCenter().getContentPoint().getY() - (this.centerImg.getY() * this.zoom);
	this.ivImgDivEx.setContentLocation( this.ivDivEx.fromContentPoint( new Point( vLeft, vTop ) ) );
	
	// if we know the dpi, scale our bars (if we need to)...
	if( this.dpi ) {
		var vDPIFactor = this.zoom * this.dpi / 1000;
		var vNwIn = Math.round( 1000 * vDPIFactor );
		var vNwCm = Math.round( 394 * vDPIFactor );
		if( (this.ivInBar.width != vNwIn) || (this.ivCmBar.width != vNwCm) ) {
			this.ivInBar.width = vNwIn;
			this.ivInBar.height = 10;
			this.ivCmBar.width = vNwCm;
			this.ivCmBar.height = 10;
		}
	}
	
	// show our marks, if we have any...
	this.ivMsr1Ex.setVisibility( false );
	this.ivMsr2Ex.setVisibility( false );
	for( var vI = 0; vI < this.marks.length; vI++ ) {
		var vMark = this.marks[vI];
		var vMD = (vI == 0) ? this.ivMsr1Ex : this.ivMsr2Ex;
		var vLeft = this.ivImgDiv.offsetLeft + (vMark.getX() * this.zoom) - 10;
		var vTop = this.ivImgDiv.offsetTop + (vMark.getY() * this.zoom) - 10;
		// correct IE positioning error...
		if( document.all ) {
			vLeft -= 2;
			vTop -=2;
		}
		vMD.setContentLocation( vMD.fromContentPoint( new Point( vLeft, vTop ) ) );
		vMD.setVisibility( true );
	}
}

// sets the center of the viewer to match the center of the image.
ImageViewer.prototype.centerImage = function() {
	this.centerImg = new Point( Math.floor( this.image.width / 2), Math.floor( this.image.height / 2) );
}

// shows the viewer
ImageViewer.prototype.show = function() {
	selectOption( this.ivClickDD, this.defaultClick );
	selectOption( this.ivDragDD, this.defaultDrag );
	ImageViewer.currentInstance = this;
	this.ivDiv.style.visibility = "visible";
	if( this.dpi ) {
		this.ivBar.style.visibility = "visible";
	} else {
		this.ivBar.style.visibility = "hidden";
	}
}

// hides the viewer.
ImageViewer.prototype.hide = function() {
	ImageViewer.currentInstance = null;
	this.ivBar.style.visibility = "hidden";
	this.ivDiv.style.visibility = "hidden";
	this.ivMsr1.style.visibility = "hidden";
	this.ivMsr2.style.visibility = "hidden";
}

/*
 * The Cookie object is lifted straight from O'Reilly's "Javascript: the Definitive Guide", version 5, chapter 19.2.
 */
/**
 * This is the Cookie() constructor function.
 *
 * This constructor looks for a cookie with the specified name for the
 * current document.  If one exists, it parses its value into a set of
 * name/value pairs and stores those values as properties of the newly created
 * object.
 *
 * To store new data in the cookie, simply set properties of the Cookie
 * object.  Avoid properties named "store" and "remove" since these are 
 * reserved as method names.
 * 
 * To save cookie data in the web browser's local store, call store().
 * To remove cookie data from the browser's store, call remove().
 *
 * The static method Cookie.enabled() returns true if cookies are
 * enabled and returns false otherwise.
 */
function Cookie( name ) {
    this.$name = name;  // Remember the name of this cookie

    // First, get a list of all cookies that pertain to this document
    // We do this by reading the magic Document.cookie property
    // If there are no cookies, we don't have anything to do 
    var allcookies = document.cookie;
    if (allcookies == "") return;

    // Break the string of all cookies into individual cookie strings
    // Then loop through the cookie strings, looking for our name
    var cookies = allcookies.split(';');
    var cookie = null;
    for(var i = 0; i < cookies.length; i++) {
        // Does this cookie string begin with the name we want?
        if (cookies[i].substring(0, name.length+1) == (name + "=")) {
            cookie = cookies[i];
            break;
        }
    }

    // If we didn't find a matching cookie, quit now
    if (cookie == null) return;

    // The cookie value is the part after the equals sign
    var cookieval = cookie.substring(name.length+1);

    // Now that we've extracted the value of the named cookie, we
    // must break that value down into individual state variable 
    // names and values. The name/value pairs are separated from each
    // other by ampersands, and the individual names and values are
    // separated from each other by colons. We use the split() method
    // to parse everything.
    var a = cookieval.split('&'); // Break it into an array of name/value pairs
    for(var i=0; i < a.length; i++)  // Break each pair into an array
        a[i] = a[i].split(':');

    // Now that we've parsed the cookie value, set all the names and values
    // as properties of this Cookie object. Note that we decode
    // the property value because the store() method encodes it
    for(var i = 0; i < a.length; i++) {
        this[a[i][0]] = decodeURIComponent(a[i][1]);
    }
}

/**
 * This function is the store() method of the Cookie object.
 *
 * Arguments:
 *
 *   daysToLive: the lifetime of the cookie, in days. If you set this
 *     to zero, the cookie will be deleted.  If you set it to null, or 
 *     omit this argument, the cookie will be a session cookie and will
 *     not be retained when the browser exits.  This argument is used to
 *     set the max-age attribute of the cookie.
 *   path: the value of the path attribute of the cookie
 *   domain: the value of the domain attribute of the cookie
 *   secure: if true, the secure attribute of the cookie will be set
 */
Cookie.prototype.store = function(daysToLive, path, domain, secure) {
    // First, loop through the properties of the Cookie object and
    // put together the value of the cookie. Since cookies use the
    // equals sign and semicolons as separators, we'll use colons
    // and ampersands for the individual state variables we store 
    // within a single cookie value. Note that we encode the value
    // of each property in case it contains punctuation or other
    // illegal characters.
    var cookieval = "";
    for(var prop in this) {
        // Ignore properties with names that begin with '$' and also methods
        if ((prop.charAt(0) == '$') || ((typeof this[prop]) == 'function')) 
            continue;
        if (cookieval != "") cookieval += '&';
        cookieval += prop + ':' + encodeURIComponent(this[prop]);
    }

    // Now that we have the value of the cookie, put together the 
    // complete cookie string, which includes the name and the various
    // attributes specified when the Cookie object was created
    var cookie = this.$name + '=' + cookieval;
    if (daysToLive || daysToLive == 0) { 
        cookie += "; max-age=" + (daysToLive*24*60*60);
    }

    if (path) cookie += "; path=" + path;
    if (domain) cookie += "; domain=" + domain;
    if (secure) cookie += "; secure";

    // Now store the cookie by setting the magic Document.cookie property
    document.cookie = cookie;
}

/**
 * This function is the remove() method of the Cookie object; it deletes the
 * properties of the object and removes the cookie from the browser's 
 * local store.
 * 
 * The arguments to this function are all optional, but to remove a cookie
 * you must pass the same values you passed to store().
 */
Cookie.prototype.remove = function(path, domain, secure) {
    // Delete the properties of the cookie
    for(var prop in this) {
        if (prop.charAt(0) != '$' && typeof this[prop] != 'function') 
            delete this[prop];
    }

    // Then, store the cookie with a lifetime of 0
    this.store(0, path, domain, secure);
}

/**
 * This static method attempts to determine whether cookies are enabled.
 * It returns true if they appear to be enabled and false otherwise.
 * A return value of true does not guarantee that cookies actually persist.
 * Nonpersistent session cookies may still work even if this method 
 * returns false.
 */
Cookie.enabled = function() {
    // Use navigator.cookieEnabled if this browser defines it
    if (navigator.cookieEnabled != undefined) return navigator.cookieEnabled;

    // If we've already cached a value, use that value
    if (Cookie.enabled.cache != undefined) return Cookie.enabled.cache;

    // Otherwise, create a test cookie with a lifetime
    document.cookie = "testcookie=test; max-age=10000";  // Set cookie

    // Now see if that cookie was saved
    var cookies = document.cookie;
    if (cookies.indexOf("testcookie=test") == -1) {
        // The cookie was not saved
        return Cookie.enabled.cache = false;
    }
    else {
        // Cookie was saved, so we've got to delete it before returning
        document.cookie = "testcookie=test; max-age=0";  // Delete cookie
        return Cookie.enabled.cache = true;
    }
}


/**********************************/


/*
 * Point class
 * Represents a point on a two dimensional plane.
 */

// creates a new instance of this class. Instances of this class are immutable.
function Point( pX, pY ) {
	this.x = Math.round( pX );
	this.y = Math.round( pY );
}

// getters...
Point.prototype.getX = function() { return this.x; }
Point.prototype.getY = function() { return this.y; }

// return a new Point instance with X and Y multiplied by the given value.
Point.prototype.mul = function( pFactor ) {
	return new Point( this.x * pFactor, this.y * pFactor );
}

// return a new Point instance with X and Y in this instance added to those in the given instance.
Point.prototype.add = function( pPoint ) {
	return new Point( this.x + pPoint.x, this.y + pPoint.y );
}

// return a new Point instance with X and Y in this instance less those in the given instance.
Point.prototype.sub = function( pPoint ) {
	return new Point( this.x - pPoint.x, this.y - pPoint.y );
}

// get distance between this instance and the given Point, which is assumed to be coordinate system.
Point.prototype.getDistance = function( pOtherLoc ) {
	return Math.sqrt( Math.pow(this.x-pOtherLoc.x,2) + Math.pow(this.y-pOtherLoc.y,2) );
}



/**********************************/


/*
 * Rectangle class
 * Represents a rectangle described by a Point origin, a height, and a width. A rectangle with negative height or width is automatically
 * normalized by adjusting the origin.
 */
function Rectangle( pOrigin, pHeight, pWidth ) {
	this.origin = pOrigin;
	this.height = Math.round( pHeight );
	this.width = Math.round( pWidth );
	if( pHeight < 0 ) {
		this.origin = new Point( this.origin.getX(), this.origin.getY() + pHeight );
		this.height = -1 * pHeight;
	}
	if( pWidth < 0 ) {
		this.origin = new Point( this.origin.getX() + pWidth, this.origin.getY() );
		this.width = -1 * pWidth;
	}
}

// getters...
Rectangle.prototype.getOrigin = function() { return this.origin; }
Rectangle.prototype.getHeight = function() { return this.height; }
Rectangle.prototype.getWidth = function() { return this.width; }

// returns a point describing the center of the rectangle.
Rectangle.prototype.getCenter = function() {
	return new Point( this.origin.getX() + Math.floor( this.width / 2 ),
										this.origin.getY() + Math.floor( this.height / 2 ) );
}

/**********************************/


/*
 * ElementLocation class
 * Represents a point location in a class.  Methods are provided to create this either from an element offset location or from a content
 * area location.
 */
 
// creates a new instance of this class.
function ElementLocation() {}

// class method to create an instance from a point in the content area.
// pElement: the ExElement containing the location.
// pPoint: the point within the content area.
// returns the new ElementLocation instance.
ElementLocation.fromContentPoint = function( pElement, pPoint ) {
	var vResult = new ElementLocation();
	vResult.element = pElement;
	vResult.contentPoint = pPoint;
	return vResult;
}

// class method to create an instance from a point in the element area.
// pElement: the element containing the location.
// pPoint: the point within the element area.
// returns the new ElementLocation instance.
ElementLocation.fromElementPoint = function( pElement, pPoint ) {
	var vResult = new ElementLocation();
	vResult.element = pElement;
	vResult.elementPoint = pPoint;
	return vResult;
}

// getters...
ElementLocation.prototype.getElement = function() { return this.element; }

// get the content point.
ElementLocation.prototype.getContentPoint = function() {
	if( ! this.contentPoint ) {
		this.contentPoint = new Point( this.elementPoint.getX() - this.element.getLeftInset(), this.elementPoint.getY() - this.element.getTopInset() );
	}
	return this.contentPoint;
}

// get the element point.
ElementLocation.prototype.getElementPoint = function() {
	if( ! this.elementPoint ) {
		this.elementPoint = new Point( this.contentPoint.getX() + this.element.getLeftInset(), this.contentPoint.getY() + this.element.getTopInset() );
	}
	return this.elementPoint;
}

// get delta (as a Point) between this instance and the given ElementLocation, which is assumed to be on the same element
ElementLocation.prototype.getDelta = function( pOtherLoc ) {
	var vA = this.getContentPoint();
	var vB = pOtherLoc.getContentPoint();
	return vA.sub( vB );
}

// get distance between this instance and the given ElementLocation, which is assumed to be on the same element
ElementLocation.prototype.getDistance = function( pOtherLoc ) {
	var vA = this.getContentPoint();
	var vB = pOtherLoc.getContentPoint();
	return Math.sqrt( Math.pow(vA.getX()-vB.getX(),2) + Math.pow(vA.getY()-vB.getY(),2) );
}

/**********************************/


/*
 * ElementRectangle class
 * Describes a rectangle upon an element, defined by an ElementLocation of the upper-corner, plus a height and width.
 */

function ElementRectangle( pOrigin, pHeight, pWidth ) {
	this.origin = pOrigin;
	this.height = pHeight;
	this.width = pWidth;
	var vTemp = pOrigin.getElementPoint();
	if( pHeight < 0 ) {
		vTemp = new Point( vTemp.getX(), vTemp.getY() + pHeight );
		this.height = -1 * pHeight;
	}
	if( pWidth < 0 ) {
		vTemp = new Point( vTemp.getX() + pWidth, vTemp.getY() );
		this.width = -1 * pWidth;
	}
	this.origin = ElementLocation.fromElementPoint( pOrigin.getElement(), vTemp );
}

// getters...
ElementRectangle.prototype.getOrigin = function() { return this.origin; }
ElementRectangle.prototype.getHeight = function() { return this.height; }
ElementRectangle.prototype.getWidth  = function() { return this.width; }

/**********************************/


/*
 * ExElement class 
 * Implements some extensions to the DOM Element class. It is implemented in this clumsy fashion because IE does not have an Element class.
 */

// creates a new instance of this class.
// pElement: the element being extended.
function ExElement( pElement ) {
	this.element = pElement;
}

// getters
ExElement.prototype.getElement = function() { return this.element; }

// sets the element's content box location (top/left/height/width) from the given ElementRectangle, whose element is assumed to be the 
// parent of this one's.
ExElement.prototype.setContentBox = function( pRectangle ) {
	setElementTop( this.element, pRectangle.getOrigin().getContentPoint().getY() - this.getTopInset() );
	setElementLeft( this.element, pRectangle.getOrigin().getContentPoint().getX() - this.getLeftInset() );
	setElementHeight( this.element, pRectangle.getHeight() );
	setElementWidth( this.element, pRectangle.getWidth() );
}

// sets the cursor when over the element.
ExElement.prototype.setCursor = function( pCursor ) {
	this.element.style.cursor = pCursor;
}

// sets the element's content box location (top/left) from the given ElementLocation, whose element is assumed to be the 
// parent of this one's.
ExElement.prototype.setContentLocation = function( pLocation ) {
	setElementTop( this.element, pLocation.getContentPoint().getY() - this.getTopInset() );
	setElementLeft( this.element, pLocation.getContentPoint().getX() - this.getLeftInset() );
}

// given two element locations, returns an element rectangle.  The rectangle is normalized for positive height and width.  The two locations
// must be for the same element, of course.
ExElement.prototype.getRectangleFromCorners = function( pLoc1, pLoc2 ) {
	var vHt = pLoc2.getElementPoint().getY() - pLoc1.getElementPoint().getY();
	var vWd = pLoc2.getElementPoint().getX() - pLoc1.getElementPoint().getX();
	return new ElementRectangle( pLoc1, vHt, vWd );
}

// given a point in the content area, returns an element location.
ExElement.prototype.fromContentPoint = function( pPoint ) {
	return ElementLocation.fromContentPoint( this, pPoint );
}

// given a point in the element area, returns an element location.
ExElement.prototype.fromElementPoint = function( pPoint ) {
	return ElementLocation.fromElementPoint( this, pPoint );
}

// returns the ElementLocation of the mousepointer on the element.
// pEvent: the Event object containing the mouse location information.
ExElement.prototype.getMouseLocation = function( pEvent ) {
	var vOffset = Position.cumulativeOffset( this.element );
	var vScroll = Position.realOffset( this.element );
	var vEP = new Point( Event.pointerX( pEvent) - (vOffset[0] + vScroll[0] ), 
											 Event.pointerY( pEvent) - (vOffset[1] + vScroll[1] ) );
	return new ElementLocation.fromElementPoint( this, vEP );
}

// inset getters
ExElement.prototype.getLeftInset   = function() { this.initElement(); return this.leftInset; }
ExElement.prototype.getTopInset    = function() { this.initElement(); return this.topInset; }
ExElement.prototype.getBottomInset = function() { this.initElement(); return this.bottomInset; }
ExElement.prototype.getRightInset  = function() { this.initElement(); return this.rightInset; }

// sets visibility of element.
ExElement.prototype.setVisibility = function( pMode ) {
	this.element.style.visibility = pMode ? "visible" : "hidden";
}

// returns true if element is visible.
ExElement.prototype.isVisible = function() {
	return this.element.style.visibility == "visible";
}

// returns an element location for the center of the content area.
ExElement.prototype.getCenter = function() {
	return ElementLocation.fromElementPoint( this, new Point( Math.floor(this.element.offsetWidth / 2), Math.floor(this.element.offsetHeight / 2) ) );
}

// initializes the top and left inset values...
ExElement.prototype.initElement = function() {
	
	// if we haven't already computed the insets, do so now...
	if( ! this.leftInset ) {
		
		// get the computed styles...
		var vCS;
		if( this.element.currentStyle ) {
			vCS = this.element.currentStyle;
		} else {
			vCS = getComputedStyle( this.element, null );
		}
		
		// now compute our insets...
		this.leftInset = this.mungeLengthProperty( vCS.marginLeft ) + this.mungeLengthProperty( vCS.borderLeftWidth );
		this.topInset = this.mungeLengthProperty( vCS.marginTop ) + this.mungeLengthProperty( vCS.borderTopWidth );
		this.rightInset = this.mungeLengthProperty( vCS.marginRight ) + this.mungeLengthProperty( vCS.borderRightWidth );
		this.bottomInset = this.mungeLengthProperty( vCS.marginBottom ) + this.mungeLengthProperty( vCS.borderBottomWidth );
	}
}

// extracts a number from a length property value.
ExElement.prototype.mungeLengthProperty = function( pValue ) {
	var vMat = pValue.match( /([0-9]+)/ );
	var vResult = 0;
	if( vMat ) {
		vResult = Number( vMat[1] );
	}
	return vResult;
}

// return a location on this element that is equivalent to a given location on a child element.
// pLoc: an ElementLocation on a child element
ExElement.prototype.getEquivChildLocation = function( pLoc ) {
	var vPoint = new Point( pLoc.getElement().offsetLeft + pLoc.getContentPoint().getX(), 
													pLoc.getElement().offsetTop  + pLoc.getContentPoint().getY() );
	return ElementLocation.fromContentPoint( this, vPoint );
}

// return a rectangle on this element that is equivalent to a given rectangle on a child element.
// pRect: an ElementRectangle on a child element
ExElement.prototype.getEquivChildRectangle = function( pRect ) {
	return new ElementRectangle( this.getEquivChildLocation( pRect.getOrigin() ), pRect.getHeight(), pRect.getWidth() );
}

// return a location on this element that is equivalent to a given location on the parent element.
// pLoc: an ElementLocation on the parent element
ExElement.prototype.getEquivParentLocation = function( pLoc ) {
	var vPoint = new Point( - this.getElement().offsetLeft + pLoc.getContentPoint().getX(), 
													- this.getElement().offsetTop  + pLoc.getContentPoint().getY() );
	return ElementLocation.fromContentPoint( this, vPoint );
}

// return a rectangle on this element that is equivalent to a given rectangle on the parent element.
// pRect: an ElementRectangle on the parent element
ExElement.prototype.getEquivParentRectangle = function( pRect ) {
	return new ElementRectangle( this.getEquivParentLocation( pRect.getOrigin() ), pRect.getHeight(), pRect.getWidth() );
}

// get the content area height.
ExElement.prototype.getContentHeight = function() {
	return this.element.offsetHeight - (this.getTopInset() + this.getBottomInset());
}

// get the content area width.
ExElement.prototype.getContentWidth = function() {
	return this.element.offsetWidth - (this.getLeftInset() + this.getRightInset());
}

/**********************************/


/*
 *  class
 * 
 * 
 */


/**********************************/

/* global utility functions */

// converts any special characters in the text to html entities.  This is useful for displaying any text containing
// "<", ">", etc.
function entify( pText ) {
	var vResult = pText.replace( /&/g, "&amp;" );
	vResult = vResult.replace( /</g, "&lt;" );
	vResult = vResult.replace( />/g, "&gt;" );
	return vResult;
}

// converts the given number to a string with thousands separators and an appropriate text suffix.  
// For example, "2" becomes "2nd", "355" becomes "355th", "4555" becomes "4,555th", etc.
function ordinalify( pNum ) {
	var vNumStr = "" + pNum;
	vNumStr = vNumStr.replace( /(\d)(?=(?:\d\d\d)+(?!\d))/g, "$1," );
	switch( Number( vNumStr.charAt( vNumStr.length - 1 ) ) ) {
		case 0:
		case 4:
		case 5:
		case 6:
		case 7:
		case 8:
		case 9:
			vNumStr += "th";
			break;
		case 1:
			vNumStr += "st";
			break;
		case 2:
			vNumStr += "nd";
			break;
		case 3:
			vNumStr += "rd";
			break;
	}
	return vNumStr;
}

// selects the option in the given select that has the given value.
function selectOption( pSelect, pValue ) {
	var vOpts = pSelect.options;
	for( var vI = 0; vI < vOpts.length; vI++ ) {
		vOpts[vI].selected = (vOpts[vI].value == pValue);
	}
}

// returns an array of window width and window height, from any browser in strict mode...
// stolen (and slightly modifed) from http://www.howtocreate.co.uk/tutorials/javascript/browserwindow
function windowSize() {
  var myWidth = 0, myHeight = 0;
  if( typeof( window.innerWidth ) == 'number' ) {
    //Non-IE
    myWidth = window.innerWidth;
    myHeight = window.innerHeight;
  } else if( document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight ) ) {
    //IE 6+ in 'standards compliant mode'
    myWidth = document.documentElement.clientWidth;
    myHeight = document.documentElement.clientHeight;
  } else if( document.body && ( document.body.clientWidth || document.body.clientHeight ) ) {
    //IE 4 compatible
    myWidth = document.body.clientWidth;
    myHeight = document.body.clientHeight;
  }
  var vResults = new Array();
  vResults.push( myWidth );
  vResults.push( myHeight );
  return vResults;
}

// sets the style on an element according to the given text, which must be in the format of CSS style sheet.  The style element names
// may be either CSS names or JavaScript property names; the conversion is done within this function.
function setElementStyle( pElement, pStyleText ) {
	var vStyles = ("" + pStyleText).split( ";" );
	for( var vI = 0; vI < vStyles.length; vI++) {
		var vStyle = vStyles[ vI ];
		var vParts = vStyle.split( ":" );
		var vName = vParts[0];
		if( vName.length > 0 ) {
			var vVal = vParts[1].trim();
			if( vName == "float" ) vName = "cssFloat";
			if( vName.indexOf( "-" ) >= 0 ) vName = vName.camelize();
			pElement.style[ vName ] = vVal;
		}
	}
}

// sets the left position of the element.
setElementLeft = function( pElement, pPos ) {
	pElement.style.left = Math.round( pPos ) + "px";
}
// sets the width of the element.
setElementWidth = function( pElement, pSize ) {
	pElement.style.width = Math.round( pSize ) + "px";
}
// sets the top position of the element.
setElementTop = function( pElement, pPos ) {
	pElement.style.top = Math.round( pPos ) + "px";
}
// sets the height of element.
setElementHeight = function( pElement, pSize ) {
	pElement.style.height = Math.round( pSize ) + "px";
}


// extension to String to trim leading and trailing spaces
String.prototype.trim = function() {
	return this.match( /^[ ]*(.*)[ ]*$/ )[1];
}


