
/**
 * Utility.js
 */

/**
 * Constructor.
 *
 * Instances of the Utility should not be constructed.
 * A constructor is provided as a "namespace" for class methods only.
 */
function Utility()
{
}

/**
 * Loads a stylesheet in
 */
Utility.loadStylesheet = function(url)
{
	document.write("<lin"+"k type=\"text/css\" rel=\"stylesheet\" href=\""+url+"\">");
}

/**
 * Loads a script in using document.write().
 * This should be called before onLoad fires and the script will be executed synchronously in the next tag.
 */
Utility.loadScript = function(scriptPath)
{
	document.write("<sc"+"ript language=\"JavaScript\" type=\"text/javascript\" src=\""+scriptPath+"\"></s"+"cript>");
}

/**
 * Returns the value of the style currently identified by the specified
 * element and style name. The style name must be specified in both
 * IE and W3C DOM formats.
 *
 * eg. getStyle('headingLayer', 'backgroundColor', 'background-color')
 * might return 'blue'.
 */
Utility.getStyle = function(elementId, ieStyleName, cssStyleName)
{
	return Utility.getStyleFromTag( document.getElementById(elementId), ieStyleName, cssStyleName );
}

/**
 * Gets the style from the tag element
 */
Utility.getStyleFromTag = function(element, ieStyleName, cssStyleName)
{
	if (element.currentStyle)
	{
		// Internet Explorer.
		return element.currentStyle[ieStyleName];
	}
	if (window.getComputedStyle)
	{
		// W3C DOM compliant browser.
		var style = window.getComputedStyle(element, '');
		return style.getPropertyValue(cssStyleName);
	}
	if (document.defaultView && document.defaultView.getComputedStyle)
	{
		return document.defaultView.getComputedStyle(element,"").getPropertyValue(cssStyleName);
  	}
	return '';
}

Utility.getImageWidth = function( name )
{
		//First, get the width of the image now (this is so downloading is ok)
	var img = document.images[name];
	var width;
	if( theBrochure.isOpera() )
	{
		var style = window.getComputedStyle(img, '');
		width = style.getPropertyValue("width");
	}
	else
	{
		width = img.width;
	}
	if( typeof( width ) == "string" && width.substring( width.length-2 ) == "px" )
	{
		width = width.substring( 0, width.length-2 );
	}
	return width*1;
}

/**
 * Returns the path to the directory holding the path of a file.
 */
Utility.getDirectory = function(path)
{
	var pos = path.lastIndexOf('/');
	if( pos == -1 )
	{
		return '';
	}
	return path.substr(0, pos);
}

/**
 * If path is relative, make it absolute based on the given absolute path.
 * This is in URL directories, so there could very well be a protocol in absolute path.
 * We can tell if path is relative by looking for a protocol and then if it begins in /.
 * @param absolutePath - an absolute path that ends in /.
 * @param path - a potentially relative path to a resource.
 * TODO ensure that all calls to this function really do pass an absolute path with a protocol
 */
Utility.makeAbsolute = function( absolutePath, path )
{
	var pos;
	if( path.indexOf( '://' ) >= 0 )
	{		//Path has a protocol - so we ignore totally the absolutePath and return the already absoluted path.
		return path;
	}
	if( absolutePath.charAt( absolutePath.length-1 ) != '/' )
	{		//Ensure that absolutePath ends in '/'
		absolutePath+='/';
	}
	if( path.charAt( 0 ) == '/' )
	{		//The path that is potentially relative is in fact an absolute path without a protocol and host.
			//We copy the protocol and host from the absolutePath and pre-pend them to the real path.
		pos = absolutePath.indexOf('://');
		return absolutePath.substring(0,absolutePath.substring(pos+3).indexOf('/')+pos+3)+path;
	}
		//Walk the ..'s removing a directory each time
	do
	{
		pos = path.indexOf('/');
		if( pos != 2 || path.substring( 0, 2 ) != ".." )
		{		//Don't have a ..
			break;
		}
			//Find a directory to take off
		pos = absolutePath.substring( 0, absolutePath.length-1 ).lastIndexOf( '/' );
		if( pos == -1 )
		{		//No directory to take off - not good!!!
			break;
		}
			//Do it
		path = path.substring( 3 );
		absolutePath = absolutePath.substring( 0, pos+1 );
	}while( true );
	return absolutePath+path;
}

/**
 * Given a URL, return the position of the / marking the end of the host name part.
 */
Utility.getPathPos = function( url )
{
	var pos = url.indexOf('://');
	if( pos != -1 )
	{
		var pos2=url.substring(pos+3).indexOf('/');
		if( pos2 == -1 )
		{
			return -1;
		}
		return pos2+pos+3;
	}
	return url.indexOf( '/' );
}

/**
 * Given two absolute URLs, try to make a relative url that points to the first one based on the second.
 * If not possible (due to different hosts or protocols) return the first one.
 * @param url - The url to find a relative path to.
 * @param baseUrl - The url to find a relative path from.
 */
Utility.getBestPath = function( url, baseUrl )
{
	if( baseUrl.charAt(baseUrl.length-1) != '/' )
	{		//Must end in a / because it is to a directory
		baseUrl+='/';
	}
		//The / that begins the path is at what position?
	var pos = Utility.getPathPos( url );
	var pos2 = Utility.getPathPos( baseUrl );
	var path,basePath;
	if( pos == -1 )
	{		//The url is not even a complete protocol and host.
			//So lets see if it is the same host
		if( url != baseUrl.substring( 0, pos2 ) )
		{		//Not same protocols and hosts
			return url;
		}
			//Actually the same host
		if( baseUrl.length == pos2+1 )
		{		//baseUrl is just the protocol and host, return ""
			return "";
		}
			//Work out how to get from basePath to path relatively
		path = "";
		basePath = baseUrl.substring( pos2+1 );
	}
	else
	{		//Both have protocols and hosts
		if( pos != pos2 || url.substring( 0, pos ) != baseUrl.substring( 0, pos ) )
		{		//But they're not equal
			return url;
		}
			//Work out how to get from basePath to path relatively
		basePath = baseUrl.substring( pos+1 );
		path = url.substring( pos+1 );
	}

	while( true )
	{		//We loop directories in common
		pos = path.indexOf( '/' );
		if( pos == -1 )
		{		//Finished with the /'s in the path.
			if( basePath.length > path.length && basePath.substring( 0, path.length ) == path && basePath.charAt( path.length ) == '/' )
			{		//We have a last directory to end on
				basePath = basePath.substring( path.length+1 );
				path = "";
			}
			break;
		}
			//Still a / in path
		var pos2 = basePath.indexOf( '/' );
		if( pos2 != pos || path.substring( 0, pos ) != basePath.substring( 0, pos ) )
		{
			break;
		}
			//Same directory - remove it and continue
		path = path.substring( pos+1 );
		basePath = basePath.substring( pos+1 );
	}
		//Now loop adding in the ..s for going up directories
	while( (pos = basePath.indexOf( '/' )) != -1 )
	{
		if( path == "" )
		{
			path = "..";
		}
		else
		{
			path="../"+path;
		}
		if( basePath.length == pos+1 )
		{
			basePath = "";
			break;
		}
		basePath = basePath.substring( pos+1 );
	}
	if( basePath.length > 0 )
	{
		if( path == "" )
		{
			path = "..";
		}
		else
		{
			path="../"+path;
		}
	}
	return path;
}

/**
 * Returns the className component of the specified path.
 * In other words, this function returns a filename with its directory
 * component and its filename suffix removed.
 */
Utility.getClassName = function(path)
{
	var fileName = Utility.getFileName(path);
	return fileName.substr(0, fileName.lastIndexOf('.js'));
}

/**
 * Returns the fileName component of the specified path.
 */
Utility.getFileName = function(path)
{
	return path.substr(path.lastIndexOf('/') + 1);
}

/**
 * Outputs a string that is, except for functions, a valid javascript text that will re-create the non-cyclic node.
 * @param node - The javascript object to output.
 * @param indent - The indent text for each line before outputting the specific text.
 */
Utility.printObjectNetwork = function( node, indent )
{
	var ans = "";
	var needsComma = false;
	for( var i in node )
	{
		if( needsComma )
		{
			ans+=",\n";
		}
		else
		{
			needsComma = true;
		}
		var obj = node[i];
		ans+=indent+i+":";
		switch( typeof( obj ) )
		{
		case "object":
			if( obj == null )
			{
				ans+="null";
			}
			else
			{
				ans+="{\n";
				ans+=Utility.printObjectNetwork( obj, indent+"    " );
				ans+=indent+"}";
			}
			break;
		case "string":
			ans+="\""+Utility.escape(obj)+"\"";
			break;
		case "function":
			ans+="FUNCTION";
			break;
		default:
			ans+=obj;
		}
	}
	ans+="\n";
	return ans;
}

Utility.escape = function( str )
{
	str = Utility.replace( str, "\\", "\\\\" );
	str = Utility.replace( str, "\"", "\\\"" );
	str = Utility.replace( str, "\'", "\\\'" );
	str = Utility.replace( str, "\r", "\\r" );
	str = Utility.replace( str, "\n", "\\n" );
	str = Utility.replace( str, "\t", "\\t" );
	return str;
}

Utility.replace = function( str, token, rep )
{
	var pos;
	var ans = "";
	while( (pos = str.indexOf( token )) >= 0 )
	{
		ans += str.substring( 0, pos )+rep;
		str = str.substring( pos+token.length );
	}
	return ans+str;
}

/**
 * Due to resources using relative paths,
 * when we load an object network in, we got it from a path.
 * Based on that we need to locateResources it so the resource objects all
 * get their path information correctly.
 * The rule is as follows:
 * Make an absolute URL based on the given url sub attribute and the path parameter.
 * Then convert it to a relative path if possible, relative to docPath.
 */
Utility.locateResources=function( container, path, docPath, listener )
{
	if( container == null )
	{
		return;
	}
	if( typeof(listener) == "undefined" ) {
		listener = null;
	}
		//Pre-process the path to make it absolute based on docPath.
	Utility.locateResourcesImpl( container, Utility.makeAbsolute( docPath, path ), docPath, path, docPath, listener );
}

/**
 * A small speed improvement - we call this inside the first call to do the recursive calls.
 * The container must be a non null object.
 * It itself is not a resource or image.
 * This routine cycles its sub objects recursively, looking for Resource or Image nodes.
 * This is identified by the nodeType (Resource or Image).
 * In this case we also have an "url" for which we can resolve the correct path.
 * Images are also converted to Image objects thereby loading the resource.
 */
Utility.locateResourcesImpl = function( container, path, docPath, listener )
{
	for( var nodeName in container )
	{		//Loop all the nodes in the object
		var data = container[nodeName];
		if( typeof( data ) != "object" || data == null )
		{		//Not an object or null - ignore
			continue;
		}
		if( typeof( data.nodeType ) == "string" )
		{		//We have an object so lets look up the nodeType.
				//If it exists, lets see what it is
			if( data.nodeType == "Resource" )
			{
					//Its nodeType is "Resource" - something to convert
				data.url = Utility.getBestPath( Utility.makeAbsolute( path, data.url ), docPath );
					//Only url exists, we can replace it
				container[nodeName] = data.url;
				continue;
			}
		} else if (
		    typeof(data.nodeType) == "object"
		    && data.nodeType != null
		    && typeof(data.nodeType.name) == "string"
		    && typeof(data.nodeType.type) == "string") {
				//Its an external project node
			data.nodeType.url = Utility.getBestPath( Utility.makeAbsolute( path, "../../objects/"+data.nodeType.type+"/"+data.nodeType.name ), docPath );
			Utility.registerProjectUsage( container, nodeName, listener );
			continue;
		}
			//If either no nodeType or nodeType is not known, recursively enter
			//An object that isn't a resource - go into it
		Utility.locateResourcesImpl( data, path, docPath, listener );
	}
}

/**
 * static object that maps IDs to condition objects that have the form:
 *  condition - function that evaluates to a boolean.  True means we're done.
 *  whenDone - function to call when done.
 */
Utility.conditions = {};
/**
 * Counts how many conditions have been added.
 */
Utility.numConditions = 0;

/**
 * Waits for a condition to become true and then calls whenDone.
 * Internally it creates an object representing this call and adds it to Utility.conditions.
 * It then calls doWaitForCondition that calls back on itself.
 * @param obj - Either null for no holding object or an object that has the condition and whenDone functions in it.
 * @param condition - If obj is null then this is a function, if obj is an object, then this is the name of a function in obj.
 * @param whenDone - If obj is null then this is a function, if obj is an object, then this is the name of a function in obj.
 */
Utility.waitForCondition = function( obj, condition, whenDone )
{
	var cobj = {};
	cobj.obj = obj;
	cobj.condition = condition;
	cobj.whenDone = whenDone;
	var id = Utility.numConditions++;
	Utility.conditions[id] = cobj;
	Utility.doWaitForCondition( id );
}

/**
 * Calls back on itself until the condition is met
 */
Utility.doWaitForCondition = function( id )
{
	var cobj = Utility.conditions[id];
	if( cobj.obj == null ? cobj.condition() : cobj.obj[cobj.condition]() )
	{
		delete Utility.conditions[id];
		cobj.obj == null ? cobj.whenDone() : cobj.obj[cobj.whenDone]();
	}
	else
	{
		setTimeout( "Utility.doWaitForCondition( "+id+" )", 100 );
	}
}

/**
 * This is a very flexible and highly recursive method.
 * Given a node object and a stack of object networks, it merges the object networks into the node.
 * Eg we have 2 networks:
 * {
 *     a:1
 * },
 * {
 *     b:2
 * }
 *
 * The result is:
 * {
 *     a:1,
 *     b:2
 * }
 *
 * ----
 * Slightly more complex:
 * {
 *     a:{
 *         a2:1
 *     }
 * },
 * {
 *     a:{
 *         a3:3
 *     },
 *     b:2
 * }
 *
 * Becomes
 * {
 *     a:{
 *         a2:1,
 *         a3:3
 *     },
 *     b:2
 * }
 *
 * ----
 * The order of the data networks is important because if there is a collision the first one will take precedence:
 * {
 *     a:1
 * },
 * {
 *     a:2
 * }
 *
 * Becomes
 * {
 *     a:1
 * }
 *
 * ----
 * If an object in the network has a nodeType or listType, then the subsequent networks are ignored.
 * The nodeType is notably, used later in Applet publishing to write out the value at the header and not as a sub node:
 * <PARAM name="a.b.c" value="MyType" />
 * and not
 * <PARAM name="a.b.c.nodeType" value="MyType" />
 *
 * The nodeType of "Callback" is special and the sub object that is built is of type Callback.
 * A Callback represents a Java/Flash to Javascript callback and is both published somehow (eg in the <PARAM> tags)
 * and into the window script.  It has to ensure the namespace uniqueness of the function callback names.
 * This is handled at publish time by collaboration between the Applet/Flash viewer javascript object and Callback types.
 * The only sub nodes in a callback node are:
 * nodeType:"Callback",
 * code:"function({PARAMS}){...}"
 * The code is run through the replacements system described below making it very powerful and expressive.
 * No subsequent object network processing is performed.
 * TODO - maybe change later if we put parameters for callbacks into the Applet.
 *
 * The listType is special because it signifies that we have a list of sub nodes and the order is important.
 * We might want to suppress one or more nodes or add others in between.
 * If we find a listType it represents the default type for the list.  We then call addIntoListN()
 * to fill a DelayedSortList, and not an ordinary object.  See that method for more details.
 *
 * ----
 * The replacements parameter is important for more advanced usages.
 * Any string value gets run through Utility.doReplacements() passing the replacements array.
 * These are effectively parameter values treating this network building as a method.
 * These can then be used through the syntax $( Javascript expression ).
 *
 * For example, if the replacements has the following structure:
 * {
 *     greeting:"Hi",
 *     name:{
 *         first:"Michael",
 *         surname:"Bienstein"
 *     }
 * }
 *
 * And we have a string: "$(greeting) $(name.surname), $(name.first)" then this would resolve to:
 * "Hi Bienstein, Michael"
 *
 * If the replacements do affect the string, then caching is not possible and this will be returned as false.
 *
 * ----
 * The exclude parameter is either null, a string or an object.
 * Typically as an object it contains keys whose values are all the boolean value "true".
 * The system checks to see if the sub node's key is also a key in the exclude array and if so it doesn't treat it.
 * This does NOT cascade to lower levels.
 * For example, with a data network stack of:
 * {
 *     a:{
 *          subA:1
 *     },
 *     b:2
 * },
 * {
 *     b:3,
 *     c:4
 * }
 *
 * And either an exclusion string of "c" or an exclusion array of {c:true}, the result is
 * {
 *     a:{
 *         subA:1
 *     },
 *     b:2
 * }
 *
 * ----
 * Nulls in the object network stack are legal and will be ignored totally.
 * Nulls inside an object network are legal too, but are treated as objects without data (same as {}).
 * EXCEPT: If all the data networks either do not have the relevant node or the value is null for all,
 * then the result will be null. A nodeType of null removes the node completely.
 * ----
 * As mentioned above it returns true if the node can be cached (ie doesn't depend on parameters), false otherwise.
 *
 * @param node - A container to put information.  It is normally empty, but doesn't have to be. TODO
 * @param datas - An array of similarly structured object networks.  These MUST NOT HAVE CYCLIC REFERENCES.
 * @param replacements - Either null or a map of named objects used to resolve replacements of the type $(expression).
 * @param exclude - Either null if no exclusions, or a string if one exclusion or a map whose keys are to be excluded and
 *  whose values resolve to false in !exclude[key].  This is used for objects within the networks that are named.  If the
 *  name is the same as the exclusion name or if the name is a key in the exclude map then the node will be done if
 *  !exclude[name] is true.
 * @param mode - Boolean value for the mode - true means that Callback and DelayedSortLists should be created and left in the output.
 * @returns true if there was no replacements done making this cachable.
 */
Utility.addIntoNodeN = function( node, datas, replacements, exclude, mode, ctxt ) {
	var cachable = true;
	if( exclude != null && typeof( exclude ) != "object" )
	{
		var temp = {};
		temp[exclude] = true;
		exclude = temp;
	}
		//If there are null nodeTypes, we have to remember what was suppressed.  That goes here
	var localExclude = null;
	for( var i = 0; i < datas.length; i++ )
	{		//Loop each context adding in parallel from there down
		var data = datas[i];
		for( var j in data )
		{		//Loop the data networks in order
			if( (exclude == null || !exclude[j]) && (localExclude == null || !localExclude[j]) && typeof( node[j] ) == "undefined" )
			{
				if( Utility.doReplacements( replacements, data[j], node, j ) )
				{		//Found a replacement, so use it and we are uncachable
					cachable = false;
				}
				else if( typeof( data[j] ) == "object" )
				{		//We have an object so we have to go recursively into it somehow
					var listType = null;
					var nodeType = null;
					var subDatas = new Array();
					var found = false;
					var numSubDatas;
					if( data[j] != null )
					{
						numSubDatas = 1;
						subDatas[0] = data[j];
						if( typeof( data[j]["listType"] ) == "string" )
						{
							listType = data[j]["listType"];
							found = true;
						}
						else if( typeof( data[j]["nodeType"] ) != "undefined" )
						{
							nodeType = data[j]["nodeType"];
							found = true;
						}
					}
					else
					{
						numSubDatas = 0;
					}
					if( !found )
					{
						for( var k = i+1; k < datas.length; k++ )
						{		//Go through remaining datas excluding ourselves
							if( datas[k] == null )
							{
								continue;
							}
							if( typeof( datas[k][j] ) == "object" && datas[k][j] != null )
							{		//Will walk it in parallel
									//Put it into the sub datas array
								subDatas[numSubDatas] = datas[k][j];
								if( typeof( subDatas[numSubDatas]["listType"] ) == "string" )
								{		//We found a list - the first one
									listType = subDatas[numSubDatas]["listType"];
									found = true;
									numSubDatas++;
									break;
								}
								if( typeof( subDatas[numSubDatas]["nodeType"] ) != "undefined" )
								{
									nodeType = subDatas[numSubDatas]["nodeType"];
									found = true;
									numSubDatas++;
									break;
								}
								numSubDatas++;
							}
						}
					}
					if( listType == null )
					{		//No list
						if( found && nodeType == null )
						{		//Told to delete
							if( localExclude == null )
							{
								localExclude = {};
							}
							localExclude[j] = true;
						}
						else if( mode && nodeType == "Callback" )
						{		//It's a callback - do replacements specially and at publish time we do something special
							node[j] = new Callback( subDatas, replacements, ctxt );
						}
						else if( numSubDatas > 0 )
						{
							cachable &= Utility.addIntoNodeN( node[j] = new Object(), subDatas, replacements, null, mode, ctxt );
						}
						else
						{
							node[j] = null;
						}
					}
					else
					{		//Found a list
						cachable &= Utility.addIntoListN( node, j, listType, subDatas, replacements, mode, ctxt );
					}
				}
				else
				{		//Normal copy
					node[j] = data[j];
				}
			}
		}
	}

	return cachable;
}

/**
 * This is more complicated for the following two reasons:
 * 1)
 * a) In each data object network in the stack, each node in the list either exists or does not.
 * b) At some level it must exist.  We must therefore find a nodeType.
 * c) But later overrides might want to modify it or even DELETE it - which does not exist for those above.
 * This is done by setting the nodeType to null.
 *
 * 2) The order of the nodes is important.
 * For example, the skin defines 3 effects in a series effect, fade in, pause, fade out.
 * The data adds another and wants it to go between pause and fade out.
 * How can we do this?
 *
 * We have to create by NAME and do the sort afterwards based on constraints and the priority of the data levels.
 * Each node has a constraint to be after or before another node in its data or lower data.
 * If it wants to be at the start it adds a constraint for that too.
 *
 * We either add a new DelayedSortList(listType) to the node at listID (ie node[listID] = new DelayedSortList(listType))
 * OR we add a normal object rebuilding as if to merge.
 * The choice depends on the mode.
 */
Utility.addIntoListN = function( node, listID, listType, datas, replacements, mode, ctxt )
{			//We always make one just in case, but we don't add it unless we have to
	var list = new DelayedSortList(listType, ctxt);
	var cachable = true;
	for( var i = 0; i < datas.length; i++ )
	{		//Loop each context adding in parallel from there down
		var data = datas[i];
		for( var j in data )
		{		//Loop every node in the context ensuring not already added, it does exist (totally ignore nulls)
			if( j != "listType" && !list.exists( j ) && data[j] != null )
			{		//Loop each context from us down looking for the nodeType while building subDatas.
				var subDatas = [];
				var numSubDatas = 1;
				subDatas[0] = data[j];
				var nodeType = null;
				for( var k = i; k < datas.length; k++ )
				{
					if( datas[k] == null )
					{
						continue;
					}
					if( typeof( datas[k][j] ) == "object" && datas[k][j] != null )
					{		//Will walk it in parallel
							//Put it into the sub datas array
						subDatas[numSubDatas] = datas[k][j];
						if( typeof( subDatas[numSubDatas]["nodeType"] ) == "object" )
						{
							nodeType = subDatas[numSubDatas]["nodeType"];
							if( nodeType == null )
							{		//Delete - we need to keep walking, but only find the original position info
								for( var l = k+1; l < datas.length; l++ )
								{
									if( datas[l] != null && typeof( datas[l][j] ) == "object" && typeof( datas[l][j]["nodeType"] ) == "object" && datas[l][j]["nodeType"] != null )
									{		//Found it
										var oldNodeType = datas[l][j]["nodeType"];
										nodeType = [];
										for( var m in oldNodeType )
										{
											nodeType[m] = oldNodeType[m];
										}
											//Tell it to delete
										nodeType.nodeType = null;
									}
								}
							}
							numSubDatas++;
							break;
						}
						numSubDatas++;
					}
				}
					//So now we should have a nodeType
				if( nodeType == null )
				{
					continue;
				}
				if( nodeType.nodeType == null )
				{
							//Always has top priority
					list.addDelNode( nodeType, j, 0, ctxt );
				}
				else
				{
					cachable &= Utility.addIntoNodeN( list.addNode( nodeType, j, i, ctxt ), subDatas, replacements, "nodeType", mode, ctxt );
				}
			}
		}
	}
		//So now we check the mode to see if we're done, or if we have to rebuild something
	if( mode === true )
	{		//For publishing
		node[listID] = list;
	}
	else
	{		//Need to rebuild
		node[listID] = list.rebuild();
	}
	return cachable;
}

/**
 * Evaluates expressions in the str parameter based on javascript objects in replacements.
 * The syntax is $(eval expression).
 * eval expression is evaluated after replacing all occurances of the various replacements.
 * If none were performed we return false.
 * If we did perform a replacement then we put it in node[j] and return true.
 */
Utility.doReplacements = function(replacements, str, node, j )
{
	if( replacements == null || typeof( str ) != "string" )
	{		//If no replacements - we can leave straight away
//TODO -why did we do this?		node[j] = str;
		return false;
	}
		//Have we changed the contents?
	var changed = false;
		//The beginning of this replacement text
	var pos;
	while( (pos = str.indexOf("$(")) != -1 )
	{		//We found one
		pos+=2;
			//The end of the replacement text
		var closePos = pos;
			//Number of open brackets
		var numOpen = 1;
		while( numOpen > 0 )
		{		//TODO escape these in strings
			openB = str.indexOf("(",closePos);
			closeB = str.indexOf(")",closePos);
			if( openB == -1 )
			{		//No more open brackets
				if( closeB == -1 )
				{		//Left open - finish up without changing it
					if( changed )
					{
						node[j] = str;
						return true;
					}
					return false;
				}
				closePos = closeB+1;
				numOpen--;
			}
			else if( closeB != -1 && closeB < openB )
			{		//Both open and close bracket exist, but close is before open
				closePos = closeB+1;
				numOpen--;
			}
			else
			{
				closePos = openB+1;
				numOpen++;
			}
		}
			//Here's the code we will evaluate
		var repStr = str.substring(pos,closePos-1);
		with( replacements )
		{
			repStr = eval( repStr );
		}
		if( pos == 2 && closePos == str.length )
		{		//Entire replacement text is our text, so don't force to be a string
			node[j] = repStr;
			return true;
		}
		str=str.substring(0,pos-2)+repStr+str.substring( closePos );
		changed = true;
	}
		//TODO remove escaped $( - later!!!
	node[j] = str;
	return changed;
}

/**
 * This is used to dynamically merge Lists.
 *
 * For example consider the case of a Template defining two action types A and B.
 * The brochureData overrides them too.
 *
 * In the template, A has a list with 3 sub objects X, Y and Z.
 * B delegates to A.
 *
 * In the brochureData, B deletes X and adds Z', A adds Y' after Y.
 *
 * ie:
 * brochureData			template
 * A
 * add Y' after Y		X
 * 						Y after X
 * 						Z after Y
 *
 * B
 * delete X				Delegate to A
 * add Z' after Z
 *
 * Then the actions resolve as:
 * A:
 *  X
 *  Y
 *  Y'
 *  Z
 *
 * B:
 *  Y
 *  Y'
 *  Z
 *  Z'
 *
 * This is simple for A - we have the template's A as the first priority layer and then the brochureData's A as the second.
 * We loop brochureData first adding in Y'
 * Then we loop the template data adding in X, Y and Z
 * process the list and publish.
 *
 * For B though it is a little more complicated.
 * First we add a record for X saying "delete" in priority 0
 * Then we add Z' in priority 0.
 * Then we go to the template and this tells us to delegate to A which loops
 * brochureData's A as priority 1 and template A as priority 2
 * Add Y' as priority 1
 * Don't add X because it is already there
 * Add Y as priority 2
 * Add Z as priority 2
 *
 * process the list and publish.
 *
 */
function DelayedSortList(listType, ctxt)
{
	if (ctxt != null && typeof(ctxt.delayedSortListPublish) == "function") {
		this.publish=ctxt.delayedSortListPublish;
	} else {
		alert("DelayedSortList with ctxt="+ctxt+" codes="+codes+" replacements="+replacements);
	}
		//What is the default type
	this.listType = listType;
		//Name to node map
	this.nodes = {};
		//linked list of priority objects that have a nodes array and next reference
		//This is always the highest priority (highest number) which corresponds to the layer closest to the template
	this.headPriority = null;
		//Lookup priority objects by priority
	this.priorities = {};
		//The head of the list
	this.head = null;
		//The tail of the list
	this.tail = null;
}

/**
 * Returns null if none exists
 */
DelayedSortList.prototype.exists = function( name )
{
	return typeof( this.nodes[name] ) != "undefined";
}

DelayedSortList.prototype.addDelNode=function( nodeType, name, priority, ctxt )
{
		//Make the AppletNode
	var node = new DelayedSortNode( nodeType, name, ctxt );
		//Tell it its deleted
	node.nodeType = null;
		//Search for a priority object to add it to
	var pr;
	if( typeof( this.priorities[priority] ) == "undefined" )
	{		//None existing for that priority, lets go looking
		pr = {pr:priority,nodes:{name:node}};
		if( this.headPriority == null || this.headPriority.pr < priority )
		{		//Replace head so highest priority is always first
			pr.next = this.headPriority;
			this.headPriority = pr;
		}
		else
		{		//Find the previous priority
			var p;
			for( p = this.headPriority; p.next != null && p.next.pr > pr; p = p.next );
			pr.next = p.next;
			p.next = pr;
		}
	}
	else
	{		//Found it already
		pr = this.priorities[priority];
		pr.nodes[name]=node;
	}

		//We don't return the node, but its properties
	return node.props;
}

/**
 * Add a new node and return the object for its properties.
 * nodeType - The type of object or null if we suppress.
 * name - The name with which to identify it.
 * prevName - must be after this (can be null).
 *
 * NB Be careful in defining templates because for example: A wants to be just before B and B wants to be just before C and C wants to be just before A.
 *
 *
 */
DelayedSortList.prototype.addNode=function( nodeType, name, priority, ctxt )
{
		//Make the AppletNode
	var node = new DelayedSortNode( nodeType, name, ctxt );
	this.nodes[name] = node;
		//Search for a priority object to add it to
	var pr;
	if( typeof( this.priorities[priority] ) == "undefined" )
	{		//None existing for that priority, lets go looking
		pr = {pr:priority,nodes:{}};
		this.priorities[priority] = pr;
		pr.nodes[name]=node;
		if( this.headPriority == null || this.headPriority.pr < priority )
		{		//Replace head so highest priority is always first
			pr.next = this.headPriority;
			this.headPriority = pr;
		}
		else
		{		//Find the previous priority
			var p;
			for( p = this.headPriority; p.next != null && p.next.pr > pr; p = p.next );
			pr.next = p.next;
			p.next = pr;
		}
	}
	else
	{		//Found it already
		pr = this.priorities[priority];
		pr.nodes[name]=node;
	}

		//We don't return the node, but its properties
	return node.props;
}

/**
 * Returns an object network that represents the fusion of all the data network layers (priorities) into this list.
 */
DelayedSortList.prototype.rebuild = function()
{
		//Clear the state information
	this.head = null;
	this.tail = null;
		//Loop the priority objects in order
	for( var p = this.headPriority; p != null; p = p.next )
	{		//Now we loop the nodes in the priority in any order
		for( var name in p.nodes )
		{
			p.nodes[name].notifyLink( this, "" );
		}
	}
	var ans = {};
	ans.listType = this.listType;
		//Now loop them
	for( var n = this.head; n != null; n = n.next )
	{
		var newNode = ans[n.name] = {};
		newNode.nodeType = {
			type:n.nodeType,
			after: (n.prev == null ? null : n.prev.name)
					//We don't add before here
		}
		for( var i in n.props )
		{
			newNode[i] = n.props[i];
		}
	}
	return ans;
}

/**
 * Can be a placeholder if type is null.
 */
function DelayedSortNode( type, name, ctxt )
{
	if (ctxt != null && typeof(ctxt.callbackPublish) == "function") {
		this.publish=ctxt.callbackPublish;
	}
	if( type != null && (typeof( type.nodeType ) == "undefined" || (type.nodeType != null && typeof( type.nodeType ) != "string" ) ) )
	{
		alert("Missing type information on DelayedSortNode '"+name+"' "+type+" "+type.nodeType);
	}
	this.nodeType = type.nodeType;
	if( typeof( type.before ) != "undefined" )
	{
		this.before = type.before;
	}
	if( typeof( type.after ) != "undefined" )
	{
		this.after = type.after;
	}
		//Assume these two items are there
	this.name = name;
	this.next = null;
	this.prev = null;
	this.props = [];
}

/**
 * Called by DelayedSortList.publish() to help build the final list
 */
DelayedSortNode.prototype.notifyLink = function( list,stack )
{
	if( this.notifiedLink )
	{
		return;
	}
	if( this.notifyingLink )
	{
		alert("Cyclic reference "+stack);
		return;
	}
	this.notifyingLink = true;
	if( typeof( this.after ) != "undefined" )
	{
		if( this.after == null )
		{		//To the head
			if( list.head == null )
			{		//First thing
				list.head = this;
				list.tail = this;
				//list.nodes[this.name] = this; //DS 20060328
			}
			else
			{		//Replace the existing head moving it further in
				this.next = list.head;
				this.prev = null;
				list.head = this;
				this.next.prev = this;
			}
		}
		else
		{		//Insert after the existing node
			var other = list.nodes[this.after];
			other.notifyLink( list,stack+" "+this.name+".after->"+this.after );
			this.next = other.next;
			this.prev = other;
			if( other.next == null )
			{		//Was tail
				list.tail = this;
			}
			else
			{
				other.next.prev = this;
			}
			other.next = this;
		}
	}
	if( typeof( this.before ) != "undefined" )
	{
		if( this.before == null )
		{		//To the tail
			if( list.tail == null )
			{
				list.head = this;
				list.tail = this;
			}
			else
			{		//Replace the tail by us and move it back one
				this.prev = list.tail;
				this.next = null;
				list.tail = this;
				this.prev.next = this;
			}
		}
		else
		{		//Have another object
			var other = list.nodes[this.before];
			other.notifyLink( list, stack+" "+this.name+".before->"+this.before );
				//Attach my next to the link's prev
			this.prev = other.prev;
			this.next = other;
			if( other.prev == null )
			{		//Was first
				list.head = this;
			}
			else
			{		//Not the head
				other.prev.next = this;
			}
			other.prev = this;
		}
	}
	this.notifyingLink = false;
	this.notifiedLink = true;
}

/**
 * Used to callback to the javascript from the java
 */
function Callback( codes, replacements, ctxt )
{
	if (ctxt != null && typeof(ctxt.callbackPublish) == "function") {
		this.publish=ctxt.callbackPublish;
	} else {
		alert("Callback with ctxt="+ctxt+" codes="+codes+" replacements="+replacements);
	}
	this.code = null;
	for( var i in codes )
	{
		if( codes[i] != null && typeof( codes[i].code ) == "string" )
		{
			this.code = codes[i].code;
			break;
		}
	}
	if( this.code == null )
	{
		return;
	}
	Utility.doReplacements( replacements, this.code, this, "code" );
}

/**
 * Button
 * A button has two image objects it gets from data for normal and rollover.
 * It has a type and an id.  It calls theBrochure.buttonClicked() passing these two parameters.
 */

/**
 * Constructor.
 */
function Button(type,buttonData)
{
	if (arguments.length > 0)
		this.init(type,buttonData);
}

/**
 * Initialization method. Used by the constructor,
 * descendant class constructor and nobody else.
 */
Button.prototype.init = function(type,buttonData)
{
	this.type = type;
	this.id = buttonData['id'];
	if( buttonData['alt'] )
	{
		this.alt = buttonData['alt'];
	}
	else
	{
		this.alt = null;
	}
	this.image = buttonData['image'];
	this.rolloverImage = buttonData['rolloverImage'];
	this.width = buttonData['width'];
	this.height = buttonData['height'];
	this.enabled = true;
		//Preload images
	this.imageObj = new Image();
	this.imageObj.src=this.image;
	this.rolloverImageObj = new Image();
	this.rolloverImageObj.src=this.rolloverImage;
}

/**
 * Returns a string containing the HTML necessary to render a button.
 */
Button.prototype.format = function()
{
	var str ='<img name="' + this.id + '" ';
/*
	if (this.height) {
		str+='height="' + this.height + '" ';
	}
	if (this.width) {
		str+='width="' + this.width + '" ';
	}
*/
	if( this.alt != null )
	{
		str += 'alt="' +this.alt + '" ';
	}
	str+=  'src="' + this.getCurrentImage().src + '" '
		+ 'onmouseover="return theBrochure.mouseOverHandler(\'' + this.type + '\',\'' + this.id + '\')" '
		+ 'onmouseout="return theBrochure.mouseOutHandler(\'' + this.type + '\',\'' + this.id + '\')" '
		+ 'onclick="theBrochure.buttonClicked(\'' + this.type + '\',\'' + this.id + '\')">';
	return str;
}

/**
 * This method is called when the user mouses over the button.
 * References the global associative button array "theButtons".
 * May be overridden in descendant classes.
 */
Button.prototype.mouseOverHandler = function()
{
	if( this.enabled && document.images && document.images[this.id])
	{
		document.images[this.id].src = this.getCurrentRolloverImage().src;
		return true;
	}
	else
		return false;
}

/**
 * This method is called when the user move the mouse out of the button.
 * References the global associative button array "theButtons".
 * May be overridden in descendant classes.
 */
Button.prototype.mouseOutHandler = function()
{
	if( document.images && document.images[this.id])
	{
		document.images[this.id].src = this.getCurrentImage().src;
		return true;
	}
	else
		return false;
}

Button.prototype.setEnabled = function( isEnabled )
{
	this.enabled = isEnabled;
}

Button.prototype.getCurrentImage = function()
{
	return this.imageObj;
}

Button.prototype.getCurrentRolloverImage = function()
{
	return this.rolloverImageObj;
}

/**
 * RemoteButton
 * Extends the button class adding support for remote rollover images.
 */

RemoteButton.prototype = new Button();
RemoteButton.prototype.constructor = RemoteButton;
RemoteButton.superclass = Button.prototype;

function RemoteButton(type,buttonData)
{
	if (arguments.length > 0)
		this.init(type,buttonData);
}

/**
 * Initialization method. Used by the constructor,
 * descendant class constructor and nobody else.
 */
RemoteButton.prototype.init = function(type,buttonData)
{
	RemoteButton.superclass.init.call(this, type,buttonData);

	this.over = false;
	this.remoteImage	= buttonData['remoteImage'];
	this.remoteRolloverImage= buttonData['remoteRolloverImage'];
	this.remoteWidth	= buttonData['remoteWidth'];
	this.remoteHeight	= buttonData['remoteHeight'];
	this.remoteId		= buttonData['remoteId'];
	this.enabled		= true;
		//Preload images
	this.remoteImageObj = new Image();
	this.remoteImageObj.src = this.remoteImage;
	this.remoteRolloverImageObj = new Image();
	this.remoteRolloverImageObj.src = this.remoteRolloverImage;
}

/**
 * Path is path to page directory
 */
RemoteButton.prototype.format = function()
{
	var str='<img name="' + this.id + '" ';
	/*
	if (this.height) {
		str+='height="' + this.height + '" ';
	}
	if (this.width) {
		str+='width="' + this.width + '" ';
	}
	*/

	str+='src="' + this.getCurrentImage().src + '" '
		+ 'onmouseover="return theBrochure.mouseOverHandler(\'' + this.type + '\',\'' + this.id + '\')" '
		+ 'onmouseout="return theBrochure.mouseOutHandler(\'' + this.type + '\',\'' + this.id + '\')" '
		+ 'onclick="theBrochure.buttonClicked(\'' + this.type + '\',\'' + this.id + '\')">';
	return str;
}

RemoteButton.prototype.raw = function()
{
	return this.getCurrentImage().src;
}

RemoteButton.prototype.mouseOverHandler = function()
{
	this.over = true;
	if( this.enabled && document.images && document.images[this.id] && document.images[this.remoteId])
	{
		document.images[this.id].src = this.getCurrentRolloverImage().src;
		document.images[this.remoteId].src = this.remoteRolloverImageObj.src;
		return true;
	}
	else
		return false;
}

RemoteButton.prototype.mouseOutHandler = function()
{
	this.over = false;
	if( document.images && document.images[this.id] && document.images[this.remoteId])
	{
		document.images[this.id].src = this.getCurrentImage().src;
		document.images[this.remoteId].src = this.remoteImageObj.src;
		return true;
	}
	else
		return false;
}

RemoteButton.prototype.setEnabled = function( isEnabled )
{
	this.enabled = isEnabled;
}

MuteButton.prototype = new RemoteButton();
MuteButton.prototype.constructor = MuteButton;
MuteButton.superclass = RemoteButton.prototype;

function MuteButton(type,buttonData)
{
	if (arguments.length > 0)
		this.init(type,buttonData);
}

/**
 * Initialization method. Used by the constructor,
 * descendant class constructor and nobody else.
 */
MuteButton.prototype.init = function(type,buttonData)
{
	MuteButton.superclass.init.call(this, type,buttonData);
	this.mutedImage = buttonData.muteImage;
	this.mutedImageObj = new Image();
	this.mutedImageObj.src = this.mutedImage;
	this.mutedRolloverImage = buttonData.rolloverMuteImage;
	this.mutedRolloverImageObj = new Image();
	this.mutedRolloverImageObj.src = this.mutedRolloverImage;
	this.activeImage = buttonData.activeImage;
	this.activeImageObj = new Image();
	this.activeImageObj.src = this.activeImage;
	this.activeRolloverImage = buttonData.rolloverActiveImage;
	this.activeRolloverImageObj = new Image();
	this.activeRolloverImageObj.src = this.activeRolloverImage;

	var v = theBrochure.getViewer();
	if( typeof( v.registerMuteStateListener ) == "function" )
	{
		v.registerMuteStateListener(this);
	}
}

MuteButton.prototype.onMuteStateChanged = function()
{
	if( this.over )
	{
		this.mouseOverHandler();
	}
	else
	{
		this.mouseOutHandler();
	}
}

MuteButton.prototype.getCurrentImage = function()
{
	var v = theBrochure.getViewer();
	if( typeof( v.getMuteState ) == "function" )
	{
		switch( v.getMuteState() )
		{
		case "nosound":
			return this.imageObj;
		case "mute":
			return this.mutedImageObj;
		default: //case "active":
			return this.activeImageObj;
		}
	}
}

MuteButton.prototype.getCurrentRolloverImage = function()
{
	var v = theBrochure.getViewer();
	if( typeof( v.getMuteState ) == "function" )
	{
		switch( v.getMuteState() )
		{
		case "nosound":
			return this.rolloverImageObj;
		case "mute":
			return this.mutedRolloverImageObj;
		default: //case "active":
			return this.activeRolloverImageObj;
		}
	}
}

MuteButton.prototype.setEnabled=function(b)
{
}

/**
 * Thumbnail
 *
 * Base thumbnail class.
 *
 * A thumbnail consists of the following parts:
 * A thumbnail image.
 * A description.
 * An animated icon and a normal icon.
 * The width and height for the icon and the width and height for the overall area
 */

/**
 * Constructor.
 */
function Thumbnail(thumbnailData, id, viewable )
{
	if (arguments.length > 0)
		this.init( thumbnailData, id, viewable );
}

/**
 * Initialization method. Used by the constructor,
 * descendant class constructor and nobody else.
 */
Thumbnail.prototype.init = function(thumbnailData,id,viewable)
{
	this.id				= id;
	this.thmbURL		= viewable.thumbnailURL;
	this.desc = viewable.short_desc;
	if ((this.desc == "") || (this.desc == null)) this.desc = "";
	this.icon = thumbnailData.icon;
	this.iconAni = thumbnailData.iconAni;
	this.outerWidth = thumbnailData.outerWidth;
	this.outerHeight = thumbnailData.outerHeight;
	this.titleWidth = thumbnailData.titleWidth;
	this.titleHeight = thumbnailData.titleHeight;
	this.photoWidth = this.outerWidth;
	this.photoHeight = this.outerHeight;

		//Preload images
	this.iconObj = new Image();
	this.iconObj.src = thumbnailData.icon;
	this.iconAniObj = new Image();
	this.iconAniObj.src = thumbnailData.iconAni;
}

/**
 * TODO I want to get rid of the path
 */
Thumbnail.prototype.format = function(highlight)
{
	var str="";
	var width = this.photoWidth > this.titleWidth ? this.photoWidth : this.titleWidth;
	if(highlight) str += "<table border=\"0\" style=\"border:1px solid #FF0000;display:inline\" width=\""+width+"\" cellspacing=\"0\" cellpadding=\"0\">";
	else str += "<table border=\"0\" style=\"display:inline\" width=\""+width+"\" cellspacing=\"0\" cellpadding=\"0\">";
	str+="<tr><td><a href=\"javascript:theBrochure.buttonClicked('thumbnail','"+this.id+"')\" onMouseOut=\"theBrochure.mouseOutHandler('thumbnail','"+this.id+"')\" onMouseOver=\"theBrochure.mouseOverHandler('thumbnail','"+this.id+"')\">";
	str+='<img name="icon_thmb_ani'+this.id+'" border="0" src="'+this.iconObj.src+'" width="'+this.titleWidth+'" height="'+this.titleHeight+'" alt="'+this.desc+'" />';
	str+='</a></td></tr>';
	str+="<tr><td>";
	str+='<a href="javascript:theBrochure.buttonClicked(\'thumbnail\',\''+this.id+'\')" onMouseOut="theBrochure.mouseOutHandler(\'thumbnail\',\''+this.id+'\')" onMouseOver="theBrochure.mouseOverHandler(\'thumbnail\',\''+this.id+'\')">';
	//str+='<img src="'+this.thmbURL+'" border="0" alt="'+this.desc+'" width='+this.photoWidth+' height='+this.photoHeight+'></a></td></tr>';
	str+='<img src="'+this.thmbURL+'" border="0" alt="'+this.desc+'"></a></td></tr>';
	str+="<tr><td>"+this.desc+"</td></tr>";
	str+="</table>";

	return str;
}

Thumbnail.prototype.testMeKate = function()
{
	var str="";
	str+='<a href="javascript:theBrochure.buttonClicked(\'thumbnail\',\''+this.id+'\')" onMouseOut="theBrochure.mouseOutHandler(\'thumbnail\',\''+this.id+'\')" onMouseOver="theBrochure.mouseOverHandler(\'thumbnail\',\''+this.id+'\')">';
	str+='<img name="icon_thmb_ani'+this.id+'" border="0" src="'+this.iconObj.src+'" width="'+this.titleWidth+'" height="'+this.titleHeight+'" alt="'+this.desc+'" />';
	str+='</a><br />';
	str+='<a href="javascript:theBrochure.buttonClicked(\'thumbnail\',\''+this.id+'\')" onMouseOut="theBrochure.mouseOutHandler(\'thumbnail\',\''+this.id+'\')" onMouseOver="theBrochure.mouseOverHandler(\'thumbnail\',\''+this.id+'\')">';
	str+='<img src="'+this.thmbURL+'" border="0" alt="'+this.desc+'" width='+this.photoWidth+' height='+this.photoHeight+'></a><br>';
	str+=this.desc;

	return str;
}

Thumbnail.prototype.mouseOutHandler = function()
{
	document.images['icon_thmb_ani'+this.id].src=this.iconObj.src;
}

Thumbnail.prototype.mouseOverHandler = function()
{
	document.images['icon_thmb_ani'+this.id].src=this.iconAniObj.src;
}

/**
 * PanoramaThumbnail
 *
 * Descendant of the Thumbnail class. Adds scrolling functionality to the thumbnail.
 */

/**
 * Constructor.
 */
function PanoramaThumbnail(thumbnailData,id,viewable)
{
	if (arguments.length > 0)
		this.init(thumbnailData,id,viewable);
}

/**
 * Initialization method. Used by the constructor,
 * descendant class constructor and nobody else.
 */
PanoramaThumbnail.prototype.init = function(thumbnailData,id,viewable)
{
	this.id				= id;
		//Is it a partial pano or not?
	this.partial		= typeof( viewable.partial ) != "undefined";
	if( this.partial )
	{		//In a partial pano we go from left to right to left to right.
			//We have a direction.
			//Start with the left (false)
		this.direction = false;
	}
	this.thmbURL		= viewable.thumbnailURL;
	this.desc = viewable.short_desc;
	if ((this.desc == "") || (this.desc == null)) this.desc = "";
	this.icon = thumbnailData.icon;
	this.iconAni = thumbnailData.iconAni;
	this.outerWidth = thumbnailData.outerWidth;
	this.outerHeight = thumbnailData.outerHeight;
	this.titleWidth = thumbnailData.titleWidth;
	this.titleHeight = thumbnailData.titleHeight;
	this.photoWidth = this.outerWidth;
	this.photoHeight = this.outerHeight;
	this.imageWidth = null;		//Will be set by callback
	this.imgPos = 0;
		//Preload images
	this.iconObj = new Image();
	this.iconObj.src = thumbnailData.icon;
	this.iconAniObj = new Image();
	this.iconAniObj.src = thumbnailData.iconAni;
}

/**
 * superDiv - the containing div tag for it all.
 *	iconDiv - the normal icon layer on top as in the normal thumbnail
 *  descDiv - the description text
 *  parentDiv - the parent of the two child image layers
 *   child1Div - an image of the pano
 *   child2Div - the other image of the pano
 *
 * The positioning of the images and their clipping organise the
 *
 * path is the path to the current page directory
 */
PanoramaThumbnail.prototype.format = function(highlight)
{
	var overAction = 'theBrochure.mouseOverHandler(\'thumbnail\',\''+this.id+'\')';
	var outAction = 'theBrochure.mouseOutHandler(\'thumbnail\',\''+this.id+'\')';
	var clickAction = 'theBrochure.buttonClicked(\'thumbnail\',\''+this.id+'\')';
	var text = '';
	var width = this.photoWidth > this.titleWidth ? this.photoWidth : this.titleWidth;
	if(highlight) text += "<table border=\"0\" style=\"border:1px solid #FF0000;display:inline\" width=\""+width+"\" cellspacing=\"0\" cellpadding=\"0\">";
	else text += "<table border=\"0\" style=\"display:inline\" width=\""+width+"\" cellspacing=\"0\" cellpadding=\"0\">";
	text += "<tr><td><a href=\"javascript:"+clickAction+"\" onMouseOut=\""+outAction+"\" onMouseOver=\""+overAction+"\">";
	text +=  "<img name=\"icon_thmb_ani"+this.id+"\" border=\"0\" src=\""+this.iconObj.src+"\" width=\""+this.titleWidth+"\" height=\""+this.titleHeight+"\" alt=\""+this.desc+"\" />";
	text += "</a></td></tr>";
	text += "<tr><td><div style=\"position:relative;left:0px;top:0px;cursor:hand;width:"+this.photoWidth+"px;height:"+this.photoHeight+"px;\" alt=\""+this.desc+"\">";
	text +=   "<div id=\"PanoramaThumbnail_parentDiv"+this.id+"\" style=\"position:absolute; left:0; top:0; width:"+this.photoWidth+"px; height:"+this.photoHeight+"px; clip:rect(0px, "+this.photoWidth+"px, "+this.photoHeight+"px, 0px);\" />";
	if( this.partial )
	{
		text +=    "<div id=\"PanoramaThumbnail_child1Div"+this.id+"\" style=\"position:absolute; left:0; top:0; height:"+this.photoHeight+"px;\">";
		text +=     "<a href=\"javascript:"+clickAction+"\" onMouseOut=\""+outAction+"\" onMouseOver=\""+overAction+"\">";
		text +=      "<img src=\""+this.thmbURL+"\" name=\"PanoramaThumbnail_"+this.id+"_1\" height=\""+this.photoHeight+"\" border=\"0\" style=\"pixelHeight:"+this.photoHeight+"\">";
		text +=     "</a>";
		text +=    "</div>";
	}
	else
	{
			//Since we use these in a number of places, work them out ahead of time
		text +=    "<div id=\"PanoramaThumbnail_child1Div"+this.id+"\" style=\"position:absolute; left:0; top:0; height:"+this.photoHeight+"px;\">";
		text +=     "<a href=\"javascript:"+clickAction+"\" onMouseOut=\""+outAction+"\" onMouseOver=\""+overAction+"\">";
		text +=      "<img src=\""+this.thmbURL+"\" name=\"PanoramaThumbnail_"+this.id+"_1\" height=\""+this.photoHeight+"\" border=\"0\" style=\"pixelHeight:"+this.photoHeight+"\">";
		text +=     "</a>";
		text +=    "</div>";
		text +=    "<div id=\"PanoramaThumbnail_child2Div"+this.id+"\" style=\"position:absolute; left:0; top:0; height:"+this.photoHeight+"px;\">";
		text +=     "<a href=\"javascript:"+clickAction+"\" onMouseOut=\""+outAction+"\" onMouseOver=\""+overAction+"\">";
		text +=      "<img src=\""+this.thmbURL+"\" name=\"PanoramaThumbnail_"+this.id+"_2\" height=\""+this.photoHeight+"\" border=\"0\" style=\"pixelHeight:"+this.photoHeight+"\">";
		text +=     "</a>";
		text +=    "</div>";
	}
	text +=   "</div>";
	text +=  "</div>";
	text += "</td></tr>";
	text += "<tr><td>"+this.desc+"</td></tr>";
	text += "</table>";

	return text;
}

function testMeKate()
{
	var text = '';
	text += '<a href="javascript:'+clickAction+'" onMouseOut="'+outAction+'" onMouseOver="'+overAction+'">';
	text +=  '<img name="icon_thmb_ani'+this.id+'" border="0" src="'+this.iconObj.src+'" width="'+this.titleWidth+'" height="'+this.titleHeight+'" alt="'+this.desc+'" />';
	text += '</a>';
	text +=  '<div style="cursor:hand; position:relative; left:0; top:0; width:'+this.photoWidth+'; height:'+this.photoHeight+';" alt="'+this.desc+'">';
	text +=   '<div id="PanoramaThumbnail_parentDiv'+this.id+'" style="position:absolute; left:0; top:0; width:'+this.photoWidth+'; height:'+this.photoHeight+'; clip:rect(0px, '+this.photoWidth+'px, '+this.photoHeight+'px, 0px);" />';
	if( this.partial )
	{
		text +=    '<div id="PanoramaThumbnail_child1Div'+this.id+'" style="position:absolute; left:0; top:0; height:'+this.photoHeight+';">';
		text +=     '<a href="javascript:'+clickAction+'" onMouseOut="'+outAction+'" onMouseOver="'+overAction+'">';
		text +=      '<img src="'+this.thmbURL+'" name="PanoramaThumbnail_'+this.id+'_1" height="'+this.photoHeight+'" border="0" style="pixelHeight:'+this.photoHeight+'">';
		text +=     '</a>';
		text +=    '</div>';
	}
	else
	{
			//Since we use these in a number of places, work them out ahead of time
		text +=    '<div id="PanoramaThumbnail_child1Div'+this.id+'" style="position:absolute; left:0; top:0; height:'+this.photoHeight+';">';
		text +=     '<a href="javascript:'+clickAction+'" onMouseOut="'+outAction+'" onMouseOver="'+overAction+'">';
		text +=      '<img src="'+this.thmbURL+'" name="PanoramaThumbnail_'+this.id+'_1" height="'+this.photoHeight+'" border="0" style="pixelHeight:'+this.photoHeight+'">';
		text +=     '</a>';
		text +=    '</div>';
		text +=    '<div id="PanoramaThumbnail_child2Div'+this.id+'" style="position:absolute; left:0; top:0; height:'+this.photoHeight+';">';
		text +=     '<a href="javascript:'+clickAction+'" onMouseOut="'+outAction+'" onMouseOver="'+overAction+'">';
		text +=      '<img src="'+this.thmbURL+'" name="PanoramaThumbnail_'+this.id+'_2" height="'+this.photoHeight+'" border="0" style="pixelHeight:'+this.photoHeight+'">';
		text +=     '</a>';
		text +=    '</div>';
	}
	text +=   '</div>';
	text +=  '</div>';
	text +=  this.desc;

	return text;
}

/**
 */
PanoramaThumbnail.prototype.mouseOverHandler = function()
{
	document.images['icon_thmb_ani'+this.id].src=this.iconAniObj.src;
	PanoramaThumbnail.stop();
	if( typeof( this.imageWidth ) == "undefined" )
	{
		return;
	}
	PanoramaThumbnail.activeID = this.id;
	var func;
	if( this.partial )
	{
		func = "rotatePartial";
	}
	else
	{
		func = "rotate";
	}
	PanoramaThumbnail.rotateID = window.setInterval("PanoramaThumbnail."+func+"()", 50);
}

/**
 */
PanoramaThumbnail.prototype.mouseOutHandler = function()
{
	document.images['icon_thmb_ani'+this.id].src=this.iconObj.src;
	PanoramaThumbnail.stop();
}

	//Which thumbnail id is actively scrolling?
PanoramaThumbnail.activeID = null;
PanoramaThumbnail.rotateID = null;
PanoramaThumbnail.rotateInc = -1;

PanoramaThumbnail.stop = function()
{
	PanoramaThumbnail.activeID = null;
	window.clearInterval(PanoramaThumbnail.rotateID);
}

PanoramaThumbnail.rotate = function(func)
{
	var aid = PanoramaThumbnail.activeID;
	if( aid == null )
	{
		return;
	}
	theBrochure.template.getButton('thumbnail',aid).rotateImages();
}

PanoramaThumbnail.rotatePartial = function(func)
{
	var aid = PanoramaThumbnail.activeID;
	if( aid == null )
	{
		return;
	}
	theBrochure.template.getButton('thumbnail',aid).rotatePartialImage();
}

PanoramaThumbnail.prototype.rotatePartialImage = function()
{		//Rotate one way and back with one image only
	var left;
	if( this.direction )
	{		//Rightwards - ends with left=0
		left = this.imgPos-PanoramaThumbnail.rotateInc;
		if( left > 0 )
		{
			left = -left;
			this.direction = false;
		}
		this.imgPos = left;
	}
	else
	{		//Leftwards - ends when right=photoWidth
		left = this.imgPos+PanoramaThumbnail.rotateInc;
		var width = Utility.getImageWidth( 'PanoramaThumbnail_'+this.id+'_1' );
		var right = left+width;
		if( right < this.photoWidth )
		{
			left = 2*this.photoWidth-right-width;
			this.direction = true;
		}
		this.imgPos = left;
	}
	document.getElementById('PanoramaThumbnail_child1Div'+this.id).style.left = left+"px";
}

PanoramaThumbnail.prototype.rotateImages = function()
{		//Rotate with two images always in the same direction and swap them
	var left = this.imgPos+PanoramaThumbnail.rotateInc;
	var width = Utility.getImageWidth( 'PanoramaThumbnail_'+this.id+'_1' );
	var right = left+width;
	if( right < 0 )
	{
		left = right;
		right = left+width;
	}
	this.imgPos = left;
	document.getElementById('PanoramaThumbnail_child1Div'+this.id).style.left = left+"px";
	document.getElementById('PanoramaThumbnail_child2Div'+this.id).style.left = right+"px";
}

/**
 * Description of Project registering and resolving.
 * -------------------------------------------------
 * Project objects such as Template, VRBrochure, Effect etc are stored in the objects sub directory.
 * A VRBrochure named xyz is stored so that a js file can be found at objects/VRBrochure/xyz/xyz.js.
 * For the projects that are registered and loaded using the system below, this js file should contain
 * a call to Utility.registerProject( type, name, project ) where the type is for example "VRBrochure",
 * the name is "xyz" and the project is a data structure that can be referenced in this way.
 *
 * When a data structure is being processed by locateResources and a node has a nodeType that is an
 * object with name and type TODO and url END TODO then a call to
 * registerProjectUsage( holder, name, listener ) is made.
 * The holder is the object, name is the key in the holder to the node that has the nodeType.
 * listener is optional and is simply passed through.  TODO
 *
 * The project might already be loaded in which case the node is replaced with the project data.
 * The project might be loaded but its dependant projects are not in which case the information is stored
 * and later, when the project is fully loaded it is fille in.
 * The project might not be loaded in which case the details are recorded and later, when the project is ready
 * it is filled in.
 *
 * The point of the listener is as a callback for when this happened.
 * This allows projects that are loaded but who's dependencies aren't yet, to get informed of when they are
 * so that they can in turn tell their listeners etc.
 * null is valid and otherwise it is an object that this system created, because the project data is
 * passed through locateResources.
 *
 * So in brief the structure that is matched by locateResource() is as follows:
 * holder
 * |
 * +----[name]
 *      |
 *      +----nodeType		//Typically a string for the type of node
 *      |    |
 *      |    +----type		//The type of object.  Eg Effect, VRBrochure etc
 *      |    |
 *      |    +----name		//The name of the project.  Eg MyTemplate
 *      |
 *      +----Other options to customise loaded object.
 *
 * Algorithm
 * ---------
 * The project data is stored in a two-level deep map called Utility.projects.
 * This maps the type name to a map.  These second maps map the project name an object
 * that keeps track of data loaded, number of dependencies etc.
 * It has:
 *  data - (If found): The data that was loaded (Else null).
 *  usages - (If not resolved): Array of {holder,name,listener} (Else null).
 *  numDependencies - : Number that says how many dependencies it needs.
 *
 * A call to registerProjectUsage(holder,name,listener) will look up the relevant object.
 *
 * If not found, one will be created and a SCRIPT tag written out to load the project in later.
 * The initial values are: data=null, usages has one entry, numDependencies=0
 *
 * If however an object already exists, someone else has asked for it already.
 * If usages is null then it is resolved, in which case we copy the details into the caller.
 * This is done by resolveProject().
 *
 * Otherwise we add a usage to the array of usages and increment the listener's numDependencies.
 *
 * registerProject(
 */

Utility.projects = {};

/*
 * @param holder - The holder object
 * @param name - The key within the holder that maps to the object to replace when resolved.
 * @param listener - An optional object that keeps track of loading of dependencies.
 */
Utility.registerProjectUsage = function( holder, name, listener ) {
		//Remember the information that defines how to load the project.
    var info = holder[name].nodeType;
    	//Lookup the existing record for it
    var typeArr = Utility.projects[info.type];
    if (typeArr) {
    		//Already exists
    	var obj = typeArr[info.name];
    	if (obj) {
    			//Exact record already exists
    		if (obj.usages == null) {
					//Already resolved.  Just tell the caller in this thread.
				Utility.resolveProjectUsage(obj,holder,name,listener);
			} else {
					//Not yet resolved, add a usage
				obj.usages[obj.usages.length] = {holder:holder, name:name, listener:listener};
				if (listener != null) {
					listener.numDependencies++;
				}
			}
			return;
    	}
    } else {	//Make it
    	typeArr = Utility.projects[info.type] = {};
    }
    var obj = typeArr[info.name] = {};
    obj.data = null;
    obj.usages = [{holder:holder,name:name,listener:listener}];
    if (listener != null) {
    	listener.numDependencies++;
    }
    obj.numDependencies = 0;
	Utility.loadScript( "objects/"+info.type+"/"+info.name+"/"+info.name+".js" );
}

Utility.resolveProjectUsage = function( obj, holder, name, listener ) {
	var overlay = holder[name];
	delete overlay.nodeType;
	Utility.addIntoNodeN(holder[name] = {}, [overlay, obj.data], null, null, false, null );
	if (listener != null && 0 == --listener.numDependencies) {
		Utility.resolveProject(listener);
	}
}

Utility.resolveProject = function(obj) {
	for (var i = obj.usages.length-1; i >= 0; i--) {
		var usage = obj.usages[i];
		Utility.resolveProjectUsage(obj,usage.holder,usage.name,usage.listener);
	}
	obj.usages = null;
}

/**
 * Register a project that is now loaded.
 * Called by the Effect.js code or similar project code.
 * The project MUST have been expected.
 */
Utility.registerProject = function( type, name, data ) {
		//We get object - it must exist
    var obj = Utility.projects[type][name];
    	//Store the data for later
    obj.data = data;

		//Use the first usage to build the URL to ourselves
    var usage = obj.usages[0];
    var nodeType = usage.holder[usage.name].nodeType;
    	//Pass the object as the listener
    Utility.locateResources( data, "../../"+nodeType.type+"/"+nodeType.name+".js", document.location.href, obj );

		//We can resolve the project
    if (obj.numDependencies == 0) {
            //No more dependencies to load.
        Utility.resolveProject(obj);
    }
}

