

/**

 * 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);

    }

}



