// Panorama script
// By Andy Baxter, May 2006

// This code may be used and copied according to the terms of the GNU General Public License
// http://www.gnu.org/licenses/gpl.html 

function showMessage(str) {
	var rep=document.getElementById("message1");
	dojo.dom.textContent(rep,str);
	}

function showMessage2(str) {
	var rep=document.getElementById("message2");
	dojo.dom.textContent(rep,str);
	}

// represents a link in a panorama.
function Link(xmlNode) {
	this.id=xmlNode.getAttribute("id");
	this.type=xmlNode.getAttribute("type");
	// array of arrays of points on the image map. Each point is an object with pan and height properties.
	this.areas=new Array();
	//alert("found link: "+this.id);
	// Construct the link object from its xml nodes.
	for (i=0; i<xmlNode.childNodes.length;i++) {
		var key=xmlNode.childNodes[i];
		switch (key.nodeName) {
			case "area":
 				//alert("found area node");
				var area=new Array()
				for (var j=0; j<key.childNodes.length;j++) {
					var point=key.childNodes[j];
					switch (point.nodeName) {
						case "point":
							var pointObj={pan: point.getAttribute("pan"), height: point.getAttribute("height")};
// 							alert("found point node with pan:"+pointObj.pan+", height:"+pointObj.height);
							area.push(pointObj);
							break;
						case "#text":
						case "#comment":
							break
						default:
							alert("xml parse error - invalid name for point node");
							return;
						}
					}
				this.areas.push(area);
			case "#text":
			case "#comment":
				break;
			default:
				alert("xml parse error - bad link parameter");
			};
		};
	};
	
// represents a panorama view.
function View(node) {
	//alert("found view node.");
	this.id=node.getAttribute("id");
	this.defaultPan=+node.getAttribute("defaultPan");
	this.links=new Array();
	this.imgTag=null;
	// add it to the lookup table.
	//alert("adding view:"+this.id+" to lookup table");
	xml.lookup.views[this.id]=this;
	// now read the rest of the child nodes.
	for (var i=0; i<node.childNodes.length; i++) {
		var key=node.childNodes[i];
		switch (key.nodeName) {
			case "shortname":
			case "description":
			case "imageset":
				this[key.nodeName]=dojo.dom.textContent(key);
				break;
			case "mappoint":
				this.mapX= +key.getAttribute("x");
				this.mapY= +key.getAttribute("y");
				//alert("added map point- mapX:"+this.mapX+", mapY:"+this.mapY);
				break;
			case "links":
				for (var j=0; j<key.childNodes.length; j++) {
					var linkNode=key.childNodes[j];
					switch (linkNode.nodeName) {
						case "link":
							var linkObj=new Link(linkNode);
							this.links.push(linkObj);
							break;
						case "#text":
						case "#comment":
							break
						default:
							alert("xml parse error - found node where link node should be. nodeName="+linkNode.nodeName);
						};
					}
				break;
			case "#text":
			case "#comment":
				break;
			default:
				alert("xml parse error - bad view parameter");
			}
		}
	}
	
// return a set of DOM nodes which say 'what you can do from here'.
View.prototype.linkInfo=function(hyperlink,introStr) {
	// hyperlink is true if the links should be hyperlinked to open an info page when you click them.
	var div=document.createElement("div");
	div.appendChild(document.createTextNode(introStr+" "+this.description+"."));
	div.appendChild(document.createElement("br"));
	div.appendChild(document.createElement("br"));
	div.appendChild(document.createTextNode("From here you can:"));
	var ul=document.createElement("ul");
	var pages={}; // hash of all the info pages which can be opened from here.
	// add something for each link.
	for (var j=0; j<this.links.length; j++) {
		var link=this.links[j];
		var linkObj=xml.lookup[link.type+"s"][link.id];
		//alert("found link:"+link.id+" of type:"+link.type);
		var linkId="a--"+link.type+"--"+link.id+"--0";
		//var linkref="<a style=\"color:#ffff80;\" href=\"javascript:tour.clickLink(\'"+linkId+"\')\" id=\""+linkId+"\" \">";
		if (link.type=="info") {
			if (linkObj.page!=undefined && pages[linkObj.page]==undefined) {
				// record any unique info pages.
				pages[linkObj.page]={
					page: xml.lookup.infoPages[linkObj.page],
					linkId: linkId
					};
				}
			}
		else {
			var li=document.createElement("li");
			var view=xml.lookup.views[linkObj.destinationId];
			li.appendChild(document.createTextNode("Walk to "));
			var p=document.createTextNode(view.shortname);
			if (hyperlink) {
				var a=document.createElement("a");
				a.href="javascript:tour.clickLink(\'"+linkId+"\')";
				a.id=linkId;
				a.appendChild(p);
				li.appendChild(a);
				}
			else {
				li.appendChild(p);
				}
			ul.appendChild(li);
			}
		}
	// now add links for all the unique info pages.
	for (var page in pages) {
		//alert("adding an info page:"+page);
		var li=document.createElement("li");
		li.appendChild(document.createTextNode("Find out more about "));
		var p=document.createTextNode(pages[page].page.shortname);
		if (hyperlink) {
			var a=document.createElement("a");
			var linkId=pages[page].linkId;
			a.href="javascript:tour.clickLink(\'"+linkId+"\')";
			a.id=linkId;
			a.appendChild(p);
			li.appendChild(a);
			}
		else {
			li.appendChild(p);
			}
		ul.appendChild(li);
		}
	div.appendChild(ul);
	return div;
	}
	
function Info(node) {
	// represents an 'info' link in the image.
	this.id=node.getAttribute("id");
	this.page=undefined;
	// add it to the lookup table.
	//alert("adding view:"+this.id+" to lookup table");
	xml.lookup.infos[this.id]=this;
	// now read the rest of the child nodes.
	for (var i=0; i<node.childNodes.length; i++) {
		var key=node.childNodes[i];
		switch (key.nodeName) {
			case "description":
			case "page":
				this[key.nodeName]=dojo.dom.textContent(key);
				break;
			case "#text":
			case "#comment":
				break;
			default:
				alert("xml parse error - bad info parameter");
			}
		}
	};
	
Info.prototype.mouseOver=function() {
	var descr=this.description;
	if (this.page!=undefined) descr+=" Click the image for more information about "+xml.lookup.infoPages[this.page].shortname+".";
	dojo.dom.textContent(tour.panoInfo,descr);
	tour.showLinkInfo(descr);
	};
	
Info.prototype.click=function() {
	//alert("clicked info: "+this.id);
	// add a new information tab about the link which was just clicked.
	// create a ContentPane widget.
	//alert("loading the info page:"+infoObj.link);
	var infoPage= xml.lookup.infoPages[this.page];
	if (infoPage != undefined) infoPage.load();
	};
	
function InfoPage(node) {
	// represents one of the information pages which comes up when you click on a link or hotspot.
	this.id=node.getAttribute("id");
	this.link=undefined;
	this.shortname=undefined;
	// add it to the lookup table.
	//alert("adding view:"+this.id+" to lookup table");
	xml.lookup.infoPages[this.id]=this;
	// now read the rest of the child nodes.
	for (var i=0; i<node.childNodes.length; i++) {
		var key=node.childNodes[i];
		switch (key.nodeName) {
			case "shortname":
			case "link":
				this[key.nodeName]=dojo.dom.textContent(key);
				break;
			case "#text":
			case "#comment":
				break;
			default:
				alert("xml parse error - bad infopage parameter");
			}
		}
	};
	
InfoPage.prototype.load = function () {
	// load the html file.
	//alert("loading page file: "+this.link);
	var bindArgs = {
		url: xml.config.infoPagePath+this.link,
		mimetype: "text/html",
		error: dojo.lang.hitch(this,"loadError"),
		load: dojo.lang.hitch(this,"loaded")
		};
	var requestObj = dojo.io.bind(bindArgs);
	};
	
InfoPage.prototype.loaded=function(type,data,evt) {
	// handle successful response here
	//alert("loaded data:"+data);
	var newNodes=dojo.html.createNodesFromText(data,true);
	this.infoNode=newNodes[0];
	// actually show the tab.
	var tabPane=dojo.widget.createWidget("ContentPane",{label: "About "+this.shortname,closable: 'true'});
	var div=document.createElement("div");
	div.className="infoPage";
	div.appendChild(this.infoNode);
	tabPane.domNode.appendChild(div);
	// add the ContentPane to the TabContainer
	var tabContainer=dojo.widget.byId("tourTabContainer");
	tabContainer.addChild(tabPane);
	tabContainer.selectChild(tabPane);
	};
	
InfoPage.prototype.loadError= function(type, errObj){
	// handle error here
	this.fail=true;
	alert("Failed to load page");
	dojo.debugDeep(errObj);
	}

function Walk(node) {
	// represents an 'walk' link in the image.
	this.id=node.getAttribute("id");
	// add it to the lookup table.
	xml.lookup.walks[this.id]=this;
	// now read the rest of the child nodes.
	for (var i=0; i<node.childNodes.length; i++) {
		var key=node.childNodes[i];
		switch (key.nodeName) {
			case "destination":
				this.destinationId=key.getAttribute("id");
				this.destinationPan=+key.getAttribute("pan");
				break;
			case "start":
				this.startPan=+key.getAttribute("pan");
				break;
			case "images":
				this.imagesRootName=key.getAttribute("rootname");
				this.imagesNumber=+key.getAttribute("number");
			case "#text":
			case "#comment":
				break;
			default:
				alert("xml parse error - bad walk parameter");
				return;
			}
		}
	//alert("found walk with id:"+this.id+" destinationId:"+this.destinationId+" destinationPan:"+this.destinationPan);
	};
	
Walk.prototype.mouseOver=function() {
	tour.showLinkInfo("This path will take you to "+xml.lookup.views[this.destinationId].shortname+". Click the path to go there.");
	}
	
Walk.prototype.click=function() {
	//alert("clicked walk: "+this.id);
	pano.startSwing(this);
	//show.startWalk(this);
	}

// structure for holding map data.
function MapData(node) {
	// array of data about each zoom level.
	// each zoom level is an object like this:
	// {
	// x:, y: - size of map image at this zoom level.
	// xtiles:, ytiles - number of tiles in each direction.
	// }
	this.levels=new Array();
	// number of zoom levels (powers of 2 to zoom out by)
	this.numLevels=undefined;
	this.curlevel=0;
	//alert("parsing map data");
	// get tile dimensions.
	this.tileWidth=node.getAttribute("tileWidth");
	this.tileHeight=node.getAttribute("tileHeight");
	for (var i=0; i<node.childNodes.length; i++) {
		var key=node.childNodes[i];
		switch (key.nodeName) {
			case "zooms":
			for (var i=0; i<key.childNodes.length; i++) {
				var key2=key.childNodes[i];
				switch (key2.nodeName) {
					case "zoom":
						this.levels[this.curlevel]={xtiles: key2.getAttribute("xtiles"), ytiles: key2.getAttribute("ytiles"), width: key2.getAttribute("width"), height: key2.getAttribute("height")};
						//alert("added zoom - level:"+this.curlevel+", xtiles:"+this.levels[this.curlevel].xtiles+", ytiles:"+this.levels[this.curlevel].ytiles+", x:"+this.levels[this.curlevel].x+", y:"+this.levels[this.curlevel].y);
						this.curlevel++;
						break;
					case "#text":
					case "#comment":
						break;
					default:
						alert("xml parse error - bad zoom parameter");
						return;
					}
				}
				this.numLevels=this.curlevel;
				break;
			case "#text":
			case "#comment":
				break;
			default:
				alert("xml parse error - bad map parameter");
				return;
			}
		}
	}

var xml = {
	// object to handle the xml data file.
	// is the file loaded and parsed?
	ready: 0,
	// did the loading fail?
	fail: 0,
	// a lookup table for getting quickly to views, infos, and walks.
    lookup: {
        views: {},
		walks: {},
		infos: {},
		infoPages: {}
        },
    // tooltip data. array of objects like this: {id: number in array, attach: node to attach to, text: tooltip text,
	// 	group: "" or group name, widget: dojo widget object, hidden: true or false}
	tooltips: [],
	// key-value configuration data.
	config: {
		},
	load: function() {
		// load the data file.
		//alert("loading XML");
		var bindArgs = {
			url: "setup.xml",
			mimetype: "text/xml",
			error: dojo.lang.hitch(xml,"loadError"),
			load: dojo.lang.hitch(xml,"loaded")
			};
		var requestObj = dojo.io.bind(bindArgs);
		},
	loaded: function(type, data, evt){
		// handle successful response here
		// parse the XML DOM into a javascript structure
		xml.intros=new Array();
		var tourNode=data.childNodes[0];
		//alert("parsing XML");
		for (var i=0; i<tourNode.childNodes.length; i++) {
			var key=tourNode.childNodes[i];
			//alert("found top level node - name:"+key.nodeName+", content:"+dojo.dom.textContent(key));
			switch (key.nodeName) {
				case "configs":
					for (var j=0; j<key.childNodes.length; j++) {
						var configNode=key.childNodes[j];
						switch (configNode.nodeName) {
							case "config":
								//alert("found config: "+configNode.getAttribute("id")+"->"+dojo.dom.textContent(configNode));
								// add a new config item
								xml.config[configNode.getAttribute("id")]=dojo.dom.textContent(configNode);
								break;
							case "#text":
							case "#comment":
								break;
							default:
								alert("xml parse error - found node where config node should be. nodeName="+configNode.nodeName);
								return;
							}
						}
					break;
				case "map":
					// create a new mapdata object using this node.
					xml.mapdata=new MapData(key);
					break;
				case "views":
					for (var j=0; j<key.childNodes.length; j++) {
						var viewNode=key.childNodes[j];
						switch (viewNode.nodeName) {
							case "view":
								// N.B. the new view adds itself to the lookup table, so it won't just go in garbage.
								var viewObj=new View(viewNode);
								break;
							case "#text":
							case "#comment":
								break;
							default:
								alert("xml parse error - found node where view node should be. nodeName="+viewNode.nodeName);
								return;
							}
						}
					break;
				case "infos":
					for (var j=0; j<key.childNodes.length; j++) {
						var infoNode=key.childNodes[j];
						switch (infoNode.nodeName) {
							case "info":
								// N.B. the new info adds itself to the lookup table, so it won't just go in garbage.
								var infoObj=new Info(infoNode);
								break;
							case "#comment":
							case "#text":
								break;
							default:
								alert("xml parse error - found node where info node should be. nodeName="+infoNode.nodeName);
								return;
							}
						}
					break;
				case "infopages":
					for (var j=0; j<key.childNodes.length; j++) {
						var infoPageNode=key.childNodes[j];
						switch (infoPageNode.nodeName) {
							case "infopage":
								// N.B. the new infpage adds itself to the lookup table, so it won't just go in garbage.
								var infoPageObj=new InfoPage(infoPageNode);
								break;
							case "#comment":
							case "#text":
								break;
							default:
								alert("xml parse error - found node where infopage node should be. nodeName="+infoNode.nodeName);
								return;
							}
						}
					break;
				case "walks":
					for (var j=0; j<key.childNodes.length; j++) {
						var walkNode=key.childNodes[j];
						switch (walkNode.nodeName) {
							case "walk":
								// N.B. the new walk adds itself to the lookup table, so it won't just go in garbage.
								var walkObj=new Walk(walkNode);
								break;
							case "#text":
							case "#comment":
								break;
							default:
								alert("xml parse error - found node where info node should be. nodeName="+walkNode.nodeName);
								return;
							}
						}
					break;
				case "tooltips":
					for (var j=0; j<key.childNodes.length; j++) {
						var tipNode=key.childNodes[j];
						switch (tipNode.nodeName) {
							case "tooltip":
								// found an intro item
                                //alert("found tooltip. content:"+dojo.dom.textContent(tipNode));
								var tipText=dojo.dom.textContent(tipNode);
 								var tipAttach=tipNode.getAttribute("attach");
								var tipGroup;
                                //alert("here");
                                //alert("attr:"+tipNode.hasAttribute("group"));
                                tipGroup=tipNode.getAttribute("group");
                                //alert("group:"+tipGroup);
                                var tipObj={id:xml.tooltips.length, attach:tipAttach, group:tipGroup, text:tipText, hidden:true};
								xml.tooltips.push(tipObj);
								//alert("found tooltip - attach:"+tipAttach+" group:"+tipGroup+" text:"+tipText);
								break;
							case "#text":
							case "#comment":
								break;
							default:
								alert("xml parse error - found node where tooltip node should be. nodeName="+walkNode.nodeName);
								return;
							}
						}
					break;
				case "#text":
				case "#comment":
					break;
				default:
					alert("xml parse error - bad top level node. nodeName="+key.nodeName);
					return;
				}
			}
		//alert("finished parsing");
		xml.ready=true;
		// initialise the rest of the system.
		tour.init();
		//alert("finished initalising system");
		//map.init();
    	},
	loadError: function(type, data, evt){
		// handle error here
		xml.fail=true;
		dojo.debugDeep(data);
		//alert("error loading setup file: "+data);
		}
	}

var tooltips={
	// reference to dom node which holds all the tooltips.
	tipContainer: null,
	widgetList: [],
	alertShown:false,
	// set things up.
	init: function() {
		//alert("initialising tooltips");
		tooltips.tipContainer=document.getElementById("tooltips");
		//dojo.debugDeep(xml.tooltipGroups);
		// find the tab labels and give them ids. (So they get picked up later by tooltips.addTip)
		var tabWidget=dojo.widget.byId("tourTabContainer");
		var buttons=tabWidget.tablist.children;
		for (var n=0; n<buttons.length; n++) {
			var button=buttons[n];
			var node=button.domNode;
			node.id="tabLabel"+dojo.dom.textContent(node);
			}
		// now go through all the tooltips and attach them to DOM nodes.
		for (var id=0; id<xml.tooltips.length; id++) {
			var tipObj=xml.tooltips[id];
			tooltips.addTip(tipObj);
			}
		// set up the 'show tooltips' button.
		tooltips.showTooltipsButton=document.getElementById("showTooltips");
		dojo.event.connect(tooltips.showTooltipsButton,"onclick",tooltips,"redisplay");
		},
	addTip: function(tipObj) {
		//alert("attaching tooltip to:"+connectId+" text:"+text);
		var tipWidget=dojo.widget.createWidget("Tooltip",{
			connectId:tipObj.attach,
			toggle:"fade",
			toggleDuration:300,
			hideDelay:500});
		// now add the caption.
		var lines=tipObj.text.split("\\n"); // split into lines on '\n'
		while (lines.length>0) {
			var line=lines.shift();
			var space=(lines.length>0)?"":" ";
			var textNode=document.createTextNode(line+space);
			tipWidget.domNode.appendChild(textNode);
			if (lines.length>0) {
				space=" ";
				var breakNode=document.createElement("br");
				tipWidget.domNode.appendChild(breakNode);
				}
			}
		// add the 'hide tooltip' link.
		var linkNode=document.createElement("a");
		linkNode.href="javascript:tooltips.removeById("+tipObj.id+")";
		var linkText="Hide this tooltip.";
		if (tipObj.group!=null) {
			linkText="Hide all "+tipObj.group+" tooltips.";
			}
		var linkTextNode=document.createTextNode(linkText);
		linkNode.appendChild(linkTextNode);
		tipWidget.domNode.appendChild(linkNode);
		tipObj.widget=tipWidget; // remember the widget to remove later.
		tipObj.hidden=false;
		},
	removeAlert: function() {
		alert("You can show all tooltips again by going to the 'Setup' tab.");
		tooltips.removeAll();
		},
	// remove a tooltip by Id (and its group members).
	removeById: function(id) {
		var tipObj=xml.tooltips[id+0];
		if (!tooltips.alertShown) {
			alert("You can show all tooltips again by going to the 'Setup' tab");
			tooltips.alertShown=true;
			}
		if (tipObj.group != null) {
			// remove all in the group.
			//dojo.debugDeep(xml.tooltipGroups[tipObj.group]);
			for (var id=0; id<xml.tooltips.length; id++) {
				var tip2=xml.tooltips[id];
				//alert("removing tip:"+tip);
				if (tip2.group == tipObj.group) {
					tooltips.removeByRef(tip2);
					}
				}
			}
		else {
			tooltips.removeByRef(tipObj);
			}
		},
	// remove a tooltip by reference.
	removeByRef: function(tipObj) {
		if (tipObj.hidden) {return;}
		//dojo.debugShallow(tipObj);
		tipObj.widget.uninitialize(); // stop events
		dojo.dom.removeNode(tipObj.widget.domNode); // remove from document.
		tipObj.widget=null;
		tipObj.hidden=true;
		},
	// redisplay all the tooltips.
	redisplay: function() {
		// first remove them all.
		for (var id=0; id<xml.tooltips.length; id++) {
			tip=xml.tooltips[id];
			tooltips.removeByRef(tip);
			}
		// and then add them back.
		for (var id=0; id<xml.tooltips.length; id++) {
			tip=xml.tooltips[id]
			tooltips.addTip(tip);
			}
		}
	}

// object to control the whole tour.
var tour={
	// current view object for the panorama
	curView: null,
	// startup message
	panoMsg: null,
	// div to show information about the current link.
	panoInfo: null,
	// div to show details about current view, with text links.
	panoControl: null,
	// whether to print location of mouse clicks on the panorama or the map.
	showClicks: false,
	// a string containing the results so far of any mouse clicks.
	clickStr: "",
	// the span to display mouse clicks in.
	clickSpan: null,
	// the checkbox which turns mouse cliks on and off.
	showClicksBox: null,
	// delay between mouse click and movement (needed to allow double clicks).
	mouseDelay: 500,
	// 1000ths of a sec to wait between moving panorama.
	scrollRepeatTime: 100,
	// the select box which sets the frame rate
	rateSelect: null,
	// initialise the tour.
	init: function() {
		// set up DOM objects.
		//alert("initialising tour");
		tour.panoMsg = document.getElementById("panoMsg");
		tour.panoInfo= document.getElementById("panoInfo");
		tour.panoControl=document.getElementById("panoControl");
		tour.showClicksBox=document.getElementById("showClicks");
		tour.clickSpan=document.getElementById("clickSpan");
		tour.rateSelect=document.getElementById("rateSelect");
		dojo.event.connect(tour.rateSelect, "onchange", tour, "rateChange");
		// initialise the tooltips, panorama, viewport, and map.
		port.init();
		pano.init();
		tour.curView=xml.lookup.views[xml.config.firstView];
		map.init();
		tooltips.init();
		//alert("initial pan:"+xml.config.initialPan);
		if (tour.showClicksBox!=undefined) {
			dojo.event.connect(tour.showClicksBox, "onchange", tour, "onShowClicks");
			}
		tour.setView(tour.curView.id,+xml.config.initialPan);
        //alert("finished initialising tour");
		},
	// a new frame rate has been selected.
	rateChange: function(evt) {
		tour.scrollRepeatTime=evt.target.value;
		clearInterval(pano.scrollIntervalEvt);
		pano.scrollIntervalEvt=setInterval(pano.mover, tour.scrollRepeatTime);
		clearInterval(map.scrollIntervalEvt);
		map.scrollIntervalEvt=setInterval(map.mover, tour.scrollRepeatTime);
		//alert("new repeat:"+pano.scrollRepeatTime);
		},
	// called when 'show mouse clicks' box is changed.
	onShowClicks: function(evt) {
		tour.showClicks=evt.target.checked;
		pano.ready=! evt.target.checked;
		tour.clickStr="";
		},
	// display info about a link.
	showLinkInfo: function(infoStr) {
		dojo.dom.textContent(tour.panoInfo,infoStr);
		},
	// set the current view.
	setView: function(viewid,pan) {
		//alert("setting view to id:"+viewid+", pan:"+pan);
		tour.curView=xml.lookup.views[viewid];
		tour.panoLoading();
		pano.loadPano(pan);
		map.hideMarkers();
		map.drawMarkers();
		},
	// display information about the current view
	showViewInfo: function() {
		var infoDiv=tour.curView.linkInfo(true,"You are now standing");
		dojo.html.removeChildren(tour.panoControl);
		dojo.dom.insertAtPosition(infoDiv,tour.panoControl,"last");
		// do the same(ish) for the map.
		map.showViewInfo();
		},
	// show a 'pano loading' message in the tour and the map.
	panoLoading: function() {
		var div=document.createElement("div");
		div.appendChild(document.createTextNode("Please wait while the panorama image loads."));
		dojo.html.removeChildren(tour.panoControl);
		dojo.dom.insertAtPosition(div,tour.panoControl,"last");
		dojo.dom.textContent(map.viewInfoSpan,"Please wait while the panorama image loads.");
		},
	// a link has been clicked, so call its click function.
	clickLink: function(id) {
		//if (! pano.ready) return;
		var linkObj=tour.getAreaId(id);
		//alert("id: "+linkObj.id+" type:"+linkObj.type);
		var link=xml.lookup[linkObj.type+"s"][linkObj.id];
		link.click();
		},
	// find the id of an imagemap area.
	getAreaId: function(str) {
		var dash1=str.indexOf("--",0);
		var dash2=str.indexOf("--",dash1+1);
		var dash3=str.indexOf("--",dash2+1);
		var id=str.substring(dash2+2,dash3);
		var type=str.substring(dash1+2,dash2);
		//alert("str:"+str+" dash1:"+dash1+" dash2:"+dash2+" id:"+id);
		return {id: id,type: type};
		}
	}

// object to handle the scrollable map.
var map={
	// where to look for map tiles.
	tileDirectory: "maptiles/",
	// width & height of the viewport.
	portWidth: 533,
	portHeight: 400,
	// zoom level - power of 2 to zoom out by.
	zoomLevel: undefined,
	// real zoom to apply to the map tiles.
	zoom: undefined,
	// maximum zoom level
	maxZoomLevel: 3.3,
	// range of zoom slider
	zoomRange: 20,
	// size of zoomed tiles
	zoomedTileSize: null,
	// 2D array of tile objects.
	tiles: null,
	// image coordinates (as proportion of image height / width)
	X: 0.5,
	Y: 0.5,
	// size of marker bitmap.
	markerSize: 18,
	// has mouse been pressed over a marker?
	markerHit: false,
	// object to store temporary data about which tiles are needed and where to put them relative to the viewport.
	bounds: null,
	// is the mouse down?
	mouseDown: false,
	// mouse X and Y coords.
	mouseX: 0,
	mouseY: 0,
	// change in mouse X and Y during a drag.
	mouseDx: 0,
	mouseDy: 0,
	// array of img tags for the map markers.
	markers: null,
	// has the map been changed since the last time it was displayed?
	mapChanged: true,
	// timer event for image scroller.
	scrollIntervalEvt: undefined,
	// offset of viewport from document.
	portOffset: null,
	// document element objects. (assigned in 'init').
	viewInfoSpan: null,
	hoverInfoSpan: null,
	portDiv: null,
	portInnerDiv: null,
	img: null,
	tableNode: null,
	// initialise the map...
	init: function() {
		// assign the document objects.
		//alert("initialising map");
		map.portDiv=document.getElementById("mapDiv");
		map.hoverInfoSpan=document.getElementById("mapHoverInfo");
		map.viewInfoSpan=document.getElementById("mapViewInfo");
		map.portInnerDiv=document.getElementById("mapInnerDiv");
		// set the map viewport dimensions.
		var box=dojo.html.getViewport();
		//alert("box.height="+box.height+" width="+box.width);
		map.portHeight=(box.height*0.80);
		map.portWidth=(box.width*0.6);
		map.portDiv.style.height=map.portHeight.toFixed(0)+"px";
		map.portDiv.style.width=map.portWidth.toFixed(0)+"px";
		// set up misc variables.
		map.zoomLevel=map.maxZoomLevel;
		map.portOffset=dojo.html.getAbsolutePosition(map.portDiv,true);
		// connect events
		dojo.event.connect(map.portDiv, "onmousedown", map, "onMouseDown");
		dojo.event.connect(map.portDiv, "onmouseup", map, "onMouseUp");
		dojo.event.connect(map.portDiv, "onmousemove", map, "onMouseMove");
		dojo.event.connect(map.portDiv, "onmouseout", map, "onMouseOut");
		// start off the image mover event loop.
		map.scrollIntervalEvt=setInterval(map.mover ,tour.scrollRepeatTime);
		// update the map image.
		map.showZoom();
        //alert("finished initialising map");
		},
	// mouse has been pressed down.
	onMouseDown: function(evt) {
		if (tour.showClicks) {
			// print out the location of the cursor in image relative coordinates.
			var cursorX=map.X+((map.mouseX-(map.portWidth/2))/(map.level.width*map.zoom));
			var cursorY=map.Y+((map.mouseY-(map.portHeight/2))/(map.level.height*map.zoom));
			tour.clickStr+="<mappoint x=\""+cursorX.toFixed(4)+"\" y=\""+cursorY.toFixed(4)+"\" />";
			dojo.dom.textContent(tour.clickSpan,tour.clickStr);
			}
		else {
			map.mouseDown=true;
			}
		map.portOffset=dojo.html.getAbsolutePosition(map.portDiv,true);
		evt.stopPropagation();
		evt.preventDefault();
		//map.showInfo();
		},
	// mouse has been released.
	onMouseUp: function(evt) {
		map.mouseDown=false;
		evt.stopPropagation();
		evt.preventDefault();
		//map.showInfo();
		},
	// mouse has left the div.
	onMouseOut: function(evt) {
		// only lift the mouse if the event is from the real portDiv, not propagated from one of the tile images.
		if (evt.target == map.portDiv) map.mouseDown=false;
		evt.stopPropagation();
		evt.preventDefault();
		//map.showInfo();
		},
	// mouse has moved inside the div
	onMouseMove: function(evt) {
		mouseX=evt.clientX-map.portOffset.x;
		mouseY=evt.clientY-map.portOffset.y;		
		if (map.mouseDown) {
			// we're dragging inside the viewport, so count how much by.
			map.mouseDx+=mouseX-map.mouseX;
			map.mouseDy+=mouseY-map.mouseY;
			}
		map.mouseX=mouseX;
		map.mouseY=mouseY;
		evt.stopPropagation();
		evt.preventDefault();
		//map.showInfo();
		},
	// redraw the map image for this zoom level.
	showZoom: function() {
		// find which image set to use (which set of tiles).
		map.imageZoomLevel=Math.floor(map.zoomLevel);
		// remainder is the amount of real zoom to apply to those tiles.
		map.zoom=1/Math.pow(2,(map.zoomLevel-map.imageZoomLevel));
		// adjust the zoom to the size of the tiles (to prevent gaps).
		map.zoomedTileSize={
			width: Math.round(xml.mapdata.tileWidth*map.zoom),
			height: Math.round(xml.mapdata.tileHeight*map.zoom)
			};
		// data about current zoom level. {x,y= image size; xtiles, ytiles= tiles in each dimension}
		map.level=xml.mapdata.levels[map.imageZoomLevel];
		//alert("drawing map with image zoom level:"+map.imageZoomLevel+", zoom:"+map.zoom+", X:"+map.X+", Y:"+map.Y+", image width:"+map.level.width+", image height:"+map.level.height+", X tiles:"+map.level.xtiles+", Y tiles:"+map.level.ytiles);
		// delete any existing tiles
		if (map.bounds != null) {
			for (var x=map.bounds.tileLeft; x<=map.bounds.tileRight; x++) {
				for (var y=map.bounds.tileTop; y<=map.bounds.tileBottom; y++) {
					// delete the tile
					map.portInnerDiv.removeChild(map.tiles[x][y]);
					}
				}
			}
		// find which tiles are needed to cover the viewport:
		map.bounds=map.findBounds();
		// now create an empty 2D array with an entry for every tile in the image.
		// (this makes it easier to refer to the actual IMG tags and add /delete tiles later).
		map.tiles=new Array();
		for (var x=0; x<map.level.xtiles; x++) {
			map.tiles[x]=new Array();
			for (var y=0; y<map.level.ytiles; y++) {
				map.tiles[x][y]=null;
				}
			}
		//alert("drawing tiles from x="+tileLeft+"-"+tileRight+", y="+tileTop+"-"+tileBottom);
		// now we want tiles from tileLeft ... tileRight and tileTop ... tileBottom.
		for (var x=map.bounds.tileLeft; x<=map.bounds.tileRight; x++) {
			for (var y=map.bounds.tileTop; y<=map.bounds.tileBottom; y++) {
				// create a tile object and add it to the array.
				map.tiles[x][y]=map.makeTile(x,y);
				map.moveTile(x,y);
				}
			}
		map.hideMarkers();
		map.drawMarkers();
		//alert("finished drawing map");
		},
	// find which tiles are needed to cover the viewport, and set up some variables.
	findBounds: function() {
		// create an object to store the results in.
		var bounds={};
		// first translate the coordinates into real pixels. These are pixels on the zoomed map image. I.e. viewport pixels.
		bounds.imgX=Math.round(map.X*map.level.width*map.zoom);
		bounds.imgY=Math.round(map.Y*map.level.height*map.zoom);
		// these are the coordinates of the point on the map which is at the centre of the image, in viewport pixels.
		// now the left edge is at bounds.imgX-map.portWidth/2 and similarly for the right edge, top and bottom.
		// work out which tiles overlap the viewport from left to right.
		// first find out where the viewport edges are on the map. (in viewport coords)
		var portLeft=Math.floor(bounds.imgX-map.portWidth/2);
		portLeft=(portLeft<0)?0:portLeft; // if the left edge is inside the viewport...
		var portRight=Math.ceil(bounds.imgX+map.portWidth/2);
		portRight=(portRight>map.level.width)?map.level.width:portRight;
		var portTop=Math.floor(bounds.imgY-map.portHeight/2);
		portTop=(portTop<0)?0:portTop;
		var portBottom=Math.ceil(bounds.imgY+map.portHeight/2);
		portBottom=(portBottom>map.level.Height)?map.level.Height:portBottom;
		// then divide by the scaled tile size to find out which tiles make the edges.
		var tileLeft=Math.floor(portLeft/map.zoomedTileSize.width);
		bounds.tileLeft=(tileLeft<0)?0:tileLeft;
		var tileRight=Math.ceil(portRight/map.zoomedTileSize.width);
		bounds.tileRight=(tileRight>=map.level.xtiles)?map.level.xtiles-1:tileRight;
		var tileTop=Math.floor(portTop/map.zoomedTileSize.height);
		bounds.tileTop=(tileTop<0)?0:tileTop;
		var tileBottom=Math.ceil(portBottom/map.zoomedTileSize.height);
		bounds.tileBottom=(tileBottom>=map.level.ytiles)?map.level.ytiles-1:tileBottom;
		return bounds
		},
	makeTile: function(x,y) {
		// make an image object for this tile.
		var imgTag=document.createElement("img");
		// set the image parameters.
		imgTag.style.visibility="hidden";
		imgTag.style.border="0px solid";
		imgTag.style.position="absolute";
		imgTag.border="0";
		imgTag.src=xml.config.imagePath+map.tileDirectory+"map-"+map.imageZoomLevel+"-"+x+"-"+y+".jpg";
		imgTag.width=map.zoomedTileSize.width;
		imgTag.height=map.zoomedTileSize.height;
		dojo.dom.insertAtPosition(imgTag,map.portInnerDiv,"last");
		return imgTag;
		},
	// move all the tiles.
	moveTiles: function() {
		// assumes map.bounds.imgX, map.bounds.imgY have already been set right.
		for (var x=map.bounds.tileLeft; x<=map.bounds.tileRight; x++) {
			for (var y=map.bounds.tileTop; y<=map.bounds.tileBottom; y++) {
				map.moveTile(x,y);
				}
			}
		},
	// move a single tile.
	moveTile: function(x,y) {
		// assumes map.bounds.imgX and map.bounds.imgY have been set right.
		var imgTag=map.tiles[x][y];
		imgTag.style.left=(Math.round(map.portWidth/2-map.bounds.imgX)+(x*map.zoomedTileSize.width))+"px";
		imgTag.style.top=(Math.round(map.portHeight/2-map.bounds.imgY)+(y*map.zoomedTileSize.height))+"px";
		imgTag.style.visibility="visible";
		},
	// remove all the markers.
	hideMarkers: function() {
		for (var viewid in xml.lookup.views) {
			var view=xml.lookup.views[viewid];
			if (view.imgTag!=null) {
				map.portInnerDiv.removeChild(view.imgTag);
				view.imgTag=null;
				}
			}
		},
	// dra w the view markers.
	drawMarkers: function() {
		//alert("drawing markers");
		//if (map.markers!=null) {
			// remove the old tags.
			//alert("removing old markers. length="+map.markers.length);
		//	for (var m in map.markers) {
				//alert("removing old marker: "+m);
		//		map.portInnerDiv.removeChild(map.markers[m]);
		//		}
		//	}
		for (var viewid in xml.lookup.views) {
			// go through each of the views and draw it on the map.
			var view=xml.lookup.views[viewid];
			var X=Math.round(((view.mapX-map.X)*map.level.width*map.zoom)+(map.portWidth/2));
			var Y=Math.round(((view.mapY-map.Y)*map.level.height*map.zoom)+(map.portHeight/2));
			//alert("view:"+view.id+", mapX:"+view.mapX+", mapY:"+view.mapY+", markerX:"+X+", markerY:"+Y);
			if (X > -map.markerSize/2 && X < map.portWidth+map.markerSize/2 && Y > -map.markerSize/2 && Y < map.portHeight+map.markerSize/2) {
				// it's inside the viewport, so show it.
				var img=null;
				if (view.imgTag==null) {
					// there isn't one already so create it.
					img=document.createElement("img");
					if (view==tour.curView) {
						img.src="images/crosshair-selected.gif";
						img.style.zIndex="2";
						}
					else {
						img.src="images/crosshair.gif";
						img.style.zIndex="1";
						}
					img.style.position="absolute";
					img.style.border="0px";
					img.style.left=(X-map.markerSize/2)+"px";
					img.style.top=(Y-map.markerSize/2)+"px";
					img.id="viewmarker--"+viewid;
					dojo.dom.insertAtPosition(img,map.portInnerDiv,"last");
					dojo.event.connect(img,"onmouseover",map,"mouseOverMarker");
					dojo.event.connect(img,"onmouseout",map,"mouseOutMarker");
					dojo.event.connect(img,"onmousedown",map,"mouseDownMarker");
					dojo.event.connect(img,"onmouseup",map,"mouseUpMarker");
					view.imgTag=img;
					}
				else {
					// just move it.
					view.imgTag.style.left=(X-map.markerSize/2)+"px";
					view.imgTag.style.top=(Y-map.markerSize/2)+"px";
					}
				}
			else {
				// the marker may have just moved out of the viewport.
				if (view.imgTag!=null) {
					map.portInnerDiv.removeChild(view.imgTag);
					view.imgTag=null;
					}
				}
			}
		},
	// mouse is over a given marker.
	mouseOverMarker: function(evt) {
		var view=map.parseMarkerId(evt.target.id);
		//alert("mouse over marker"+view.id);
		var infoNode=document.createElement("div");
		if (view!=tour.curView) {
			infoNode.appendChild(view.linkInfo(false,"The mouse is over the viewpoint"));
			infoNode.appendChild(document.createTextNode("Click the circle to move here."));
			}
		else {
			infoNode.appendChild(document.createTextNode("This is where you are now standing."));
			}
		dojo.html.removeChildren(map.hoverInfoSpan);
		map.hoverInfoSpan.appendChild(infoNode);
		},
	// mouse has just left a given marker.
	mouseOutMarker: function(evt) {
		dojo.dom.textContent(map.hoverInfoSpan," ");
		map.markerHit=false;
		},
	mouseDownMarker: function(evt) {
		map.markerHit=true;
		setTimeout(function() {map.markerHit=false; },tour.mouseDelay);
		},
	mouseUpMarker: function(evt) {
		var view=map.parseMarkerId(evt.target.id);
		if (map.markerHit==true && view != tour.curView ) {
			// mouse released within click period.
			map.mouseDown=false;
			tour.setView(view.id,view.defaultPan);
			var tabContainer=dojo.widget.byId("tourTabContainer");
			var tourPane=dojo.widget.byId("tourPane");
			tabContainer.selectChild(tourPane);
			}
		},
	// find the view object from the marker id.
	parseMarkerId: function(marker) {
		var cut=marker.indexOf("--");
		var viewid=marker.substr(cut+2);
		var view=xml.lookup.views[viewid];
		return view;
		},
	showViewInfo: function() {
		dojo.dom.textContent(map.viewInfoSpan,"You are now standing "+tour.curView.description+".");
		},
	// called on a timer to actually move / resize the map.
	mover: function() {
		// first check if anything has changed...
		if (map.zoomChanged) {
			// the zoom level has been changed.
			map.mouseDx=0;
			map.mouseDy=0;
			}
		else if (map.mouseDx != 0 || map.mouseDy !=0) {
			// the map has been moved.
			// first move the relative coordinates proportional to Dx,Dy
			map.X-=map.mouseDx/(map.level.width*map.zoom);
			map.Y-=map.mouseDy/(map.level.height*map.zoom);
			map.mouseDx=0;
			map.mouseDy=0;
			if (map.X<0) map.X=0;
			if (map.X>1) map.X=1;
			if (map.Y<0) map.Y=0;
			if (map.Y>1) map.Y=1;
			// remember the old tile numbers.
			var oldbounds=map.bounds;
			// find out which tiles are needed now and where the tiled image should be relative to the viewport.
			map.bounds=map.findBounds();
			if (oldbounds.tileLeft != map.bounds.tileLeft || oldbounds.tileRight != map.bounds.tileRight || oldbounds.tileTop != map.bounds.tileTop || oldbounds.tileBottom != map.bounds.tileBottom ) {
				// the bounds have changed, so...
				// remove any unwanted tiles.
				for (x=oldbounds.tileLeft; x<=oldbounds.tileRight; x++) {
					for (y=oldbounds.tileTop; y<=oldbounds.tileBottom; y++) {
						if ( x<map.bounds.tileLeft || x>map.bounds.tileRight || y<map.bounds.tileTop || y> map.bounds.tileBottom) {
							// the old tile isn't in the new bounds, so remove it.
							// TODO - maybe get rid of the tile object altogether and just use the array.
							//if (map.tiles[x][y]==null) alert("x:"+x+", y:"+y);
							map.portInnerDiv.removeChild(map.tiles[x][y]);
							map.tiles[x][y]=null;
							}
						}
					}
				// add any new tiles
				for (x=map.bounds.tileLeft; x<=map.bounds.tileRight; x++) {
					for (y=map.bounds.tileTop; y<=map.bounds.tileBottom; y++) {
						if ( map.tiles[x][y]==null) {
							// there's no tile here so add one.
							map.tiles[x][y]=map.makeTile(x,y);
							}
						}
					}
				}
			// move the tiles.
			map.moveTiles();
			map.drawMarkers();
			//map.drawMarkers();
			}
		//map.showInfo();
		},
	// called by the slider widget to set the zoom level.
	setZoom: function(zoom) {
		// do some error checking (because of an occasional bug in the slider widget)
		zoom=(zoom<0)?0:zoom;
		zoom=(zoom>map.zoomRange)?map.zoomRange:zoom;
		// scale the slider value to the right range.
		map.zoomLevel=(1-zoom/map.zoomRange)*map.maxZoomLevel;
		// draw the map at the new zoom level.
		map.showZoom();
		//map.showInfo();
		},
	// display debugging info.
	showInfo: function() {
		dojo.dom.textContent(map.infoSpan,"X:"+map.imgX+" Y:"+map.imgY+" mouseX:"+map.mouseX+" mouseY:"+map.mouseY+" mouseDown:"+map.mouseDown+" mouseDx:"+map.mouseDx+" mouseDy:"+map.mouseDy+" zoom level:"+map.zoomLevel);
		}
	}

// object to represent the viewport (used by pano and show)
var port = {
	// the width/height ratio of the viewport
	ratio: 1.33333,
	// the outer div of the viewport
	mainDiv: null,
	// the inner div.
	innerDiv: null,
	// the table cell containing the divs
	tableCell: null,
	// the arrows.
	arrows: null,
	// arrow visibility:
	arrowsVisible: true,
	// the list of viewport heights.
	resolutions: undefined,
	// the current viewport height.
	resolution: undefined,
	// callback for when the port is resized.
	callResize: null,
	// the resolution selection control
	resSelect: null,
	// the initialisation function
	init: function() {
		//alert("Initialising viewport");
		// set up the DOM references.
		port.mainDiv = document.getElementById("panoDiv");
		port.innerDiv = document.getElementById("panoInnerDiv");
		port.resSelect = document.getElementById("resSelect");
		port.arrows=Array();
		for (var n=1; n<3; n++) {
			port.arrows[n]=document.getElementById("panoArrow"+n);
			}
		// find the available resolutions.
		port.resolutions=xml.config.resolutions.split(","); // N.B. these are strings not numbers
		// set the port size (no callback on initialisation).
		port.callResize=function() {return;};
		port.resize(+xml.config.initialResolution);
		// set the resolution selection control to the right resolution.
		var selIndex=undefined;
		for (var res=0;res<port.resolutions.length;res++) {
			if (+port.resolutions[res]==port.resolution) selIndex=res;
			};
		if (selIndex == undefined) {
			alert("error - initial resolution in config file doesn't match any available resolution");
			return;
			}
		port.resSelect.selectedIndex=selIndex;
		dojo.event.connect(port.resSelect, "onchange", port, "resChange");
		},
	// sets callback function to call when the port is resized. (Can change depending on whether the panorama or the slideshow is active)
	setCallback: function(callback) {
		port.callResize=callback;
		},
	// resize the panorama viewport.
	resize: function(newsize) {
		port.mainDiv.style.height=(+newsize).toFixed(0)+"px";
		port.mainDiv.style.width=(newsize*port.ratio).toFixed(0)+"px";
		port.mainDiv.style.clip="rect(0px,"+(newsize*port.viewportRatio).toFixed(0)+"px,"+newsize+"px,0px)";
		port.resolution=newsize;
		port.showArrows(port.arrowsVisible);
		// run the callback.
		port.callResize();
		},
	// a new resolution has been selected.
	resChange: function(evt) {
		//alert("select:"+pano.resolutions[evt.target.selectedIndex]);
		port.resize(+port.resolutions[evt.target.selectedIndex]);
		},
	// show or hide arrows.
	showArrows: function(visible) {
		for (var n=1; n<3; n++) {
			//port.arrows[n].style.top=(port.resolution/2-16).toFixed(0)+"px";
			port.arrows[n].style.visibility=visible?"visible":"hidden";
			}
		port.arrowsVisible=visible;
		}
	}

// handles the slideshows between panoramas.
var show={
	// the current walk.
	curWalk: null,
	// the current image in the walk.
	curImage: null,
	// the DOM tag for the image.
	imgTag: null,
	// an array of images.
	images: null,
	// delay in 1/1000ths of a second between slides.
	slideDelay: 2000,
	// slide timer object.
	slideTimer: null,
	// time to wait before checking whether image has loaded again.
	imageLoadWait: 200,
	// time to wait before removing the last image from the slideshow.
	slideRemoveWait: 100,
	// path to walk images.
	imgPath: "walks/",
	// start the walk.
	startWalk: function(walkObj) {
		// start the walk referenced by walkObj.
		if (! pano.ready) return;
		show.curWalk=walkObj;
		show.curImage=0;
		show.ImgTag=undefined;
		pano.ready=false;
		//alert("starting walk with root name:"+walkObj.imagesRootName+" number of images:"+walkObj.imagesNumber);
		if (walkObj.imagesNumber >0 ) {
			// do the slideshow if there are some images.
			show.images=new Array();
			// preload the images.
			// TODO - preload the panorama image.
			for (var i=0; i<walkObj.imagesNumber; i++) {
				// TODO - what happens if you resize the viewport during a slideshow?
				var imgName=xml.config.imagePath+show.imgPath+walkObj.imagesRootName+"-"+(i+1)+"-"+port.resolution+".jpg";
				var image=new Image();
				image.src=imgName;
				show.images.push(image);
				}
			dojo.dom.removeChildren(tour.panoControl);
			var destView=xml.lookup.views[walkObj.destinationId];
			dojo.dom.textContent(tour.panoControl,"You are now walking to "+destView.shortname+". Please wait for the slideshow to finish.");
			dojo.dom.textContent(tour.panoInfo," ");
			// now kick off a series of timed events.
			show.slideTimer=setTimeout(show.showSlide,show.slideDelay);
			}
		port.showArrows(false);
		},
	showSlide: function() {
		// called repeatedly to actually run the slideshow.
		// tour.curWalk is the walk being run. tour.curWalk.curImage is the current image.
		var walkObj=show.curWalk;
		//alert("loading slide:"+show.curImage+" src:"+show.images[show.curImage].src);
		if (! show.images[show.curImage].complete) {
			// image hasn't loaded yet, so call myself back later.
			show.slideTimer=setTimeout(show.showSlide,show.imageLoadWait);
			return;
			}
		// load the image into the viewport, and swap it.
		var imgTag=document.createElement("img");
		imgTag.src=show.images[show.curImage].src;
		imgTag.style.width=(port.resolution*port.ratio).toFixed(0)+"px";
		imgTag.style.height=port.resolution.toFixed(0)+"px";
		imgTag.style.position="absolute";
		imgTag.style.top="0px";
		imgTag.style.left="0px";
		imgTag.style.border="0px";
		imgTag.zIndex="1";
		dojo.dom.insertAtPosition(imgTag,port.innerDiv,"last");
		if (show.curImage==0) {
			// hide the panorama.
			pano.hidePano();
			}
		else {
			// remove the last slideshow image.
			port.innerDiv.removeChild(show.imgTag);
			}
		show.imgTag=imgTag;
		show.curImage++;
		if (show.curImage < walkObj.imagesNumber) {
			// set up another slide.
			show.slideTimer=setTimeout(show.showSlide,show.slideDelay);
			}
		else {
			// set up the next panorama.
			show.slideTimer=null;
			show.slideTimer=setTimeout(function() {tour.setView(walkObj.destinationId,walkObj.destinationPan)},show.slideDelay);
			}
		},
	// remove the last slide from the slideshow.
	removeSlide: function() {
		if (show.imgTag != null) {
			port.innerDiv.removeChild(show.imgTag);
			show.imgTag=null;
			}
		if (show.slideTimer != null) {
			clearTimeout(show.slideTimer);
			show.slideTimer=null;
			}
		dojo.dom.textContent(tour.panoInfo," ");
		}
	}

// object to handle the panorama.
var pano = {
	// initialisation for the panorama.
	// fraction of viewport to move when mouse is at full left / right per 1000th of a second.
	scale: 0.03/100,
	// path to panorama images.
	panoImgPath: "panoramas/",
	// x/y ratio of the current panorama.
	imgRatio: 0,
	// whether the panorama is ready to look at.
	ready: false,
	// mouse status.
	mouseDown: false,
	mouseX: 0,
	mouseY: 0,
	// whether to move the panorama.
	move: false,
	// ratio that panorama is panned to left.
	pan: 0,
	// velocity of swing
	dpan: 0,
	// value of swing acceleration:
	ddpan: 0,
	// are we doing a swing?
	swinging: false,
	// is the swing going negative?
	swingNegative:0,
	// start value of swing acceleration per 1000th of a sec.
	swingAccel: 0.001/100,
	// has swing been reversed;
	swingReversed: 0,
	// value of pan for the prupose of the swing.
	swingPan: 0,
	// pan to start slowing swing.
	midwayPan: 0,
	// the walk to swing to.
	swingWalk: null,
	// timer for the swing.
	swingTimer: null,
	// available resolutions:
	resolutions: undefined,
	// currently selected link (or null if none);
	liveLink: null,
	// scroll interval event
	scrollIntervalEvt: undefined,
	// image object for the panorama image.
	panoImg: undefined,
	// image offset from document top corner.
	imageOffset: null,
	// references to DOM objects.
	imageMap: 0,
	panoImgTags: undefined,
	panoMsgSpan: 0,
	imgLoadImg: 0,
	resSelect: 0,
	imageMap: undefined,
	// initialise the panorama.
	init: function() {
		//alert("initialising panorama");
		pano.panoImgTags=new Array(undefined,undefined);
		if (! document.getElementById) return;
		pano.panoMsgSpan = document.getElementById("panoMsg");
		pano.imageMap=document.getElementById("panoMapTag");
		pano.imageOffset=dojo.html.getAbsolutePosition(port.mainDiv,true); // put here to speed up scrolling.
		//alert("first view is: "+pano.curView.id);
		//pano.panoDiv.style.="crosshair";
		// connect events.
		dojo.event.connect(port.mainDiv, "onmousedown", pano, "onMouseDown");
		dojo.event.connect(port.mainDiv, "onmouseup", pano, "onMouseUp");
		dojo.event.connect(port.mainDiv, "onmousemove", pano, "onMouseMove");
		dojo.event.connect(port.mainDiv, "onmouseover", pano, "onMouseOver");
		dojo.event.connect(port.mainDiv, "onmouseout", pano, "onMouseOut");
		dojo.event.connect(port.mainDiv, "onmouseclick", pano, "stopDefault");
		// call the function which actually scrolls the panorama at regular intervals.
		pano.scrollIntervalEvt=setInterval(pano.mover ,tour.scrollRepeatTime);
		// set a callback for when the resolution changes.
		port.setCallback(pano.resCallback);
		},
	// creates an HTML nodeset which is an imagemap for the current view.
	makeImageMap: function() {
		//alert("making image map");
		// remove the old imagemap if there is one.
		dojo.dom.removeChildren(pano.imageMap);
		//pano.imageMap.style.zIndex="3";
		for (var i=0; i<tour.curView.links.length; i++) {
			var link=tour.curView.links[i];
 			//alert("found link:"+link.id);
			for (var j=0; j<link.areas.length; j++) {
 				//alert("found area");
				var mapArea=document.createElement("area");
				var area=link.areas[j]; // area is an array of point objects.
				var pointStr="";
				for (var k=0; k<area.length; k++) {
					var point=area[k];
					if (pointStr!="") { pointStr += ","; };
					var mapX=port.resolution*pano.imgRatio*point.pan;
					var mapY=port.resolution*point.height;
					pointStr+=mapX.toFixed(0)+","+mapY.toFixed(0);
 					//alert("adding point with pan:"+point.pan+",height:"+point.height);
					}
				mapArea.setAttribute("shape","poly");
				mapArea.setAttribute("nohref","");
				mapArea.setAttribute("coords",pointStr);
				mapArea.style.cursor="pointer";
				//alert("pointStr:"+mapArea.coords);
				mapArea.setAttribute("id","area--"+link.type+"--"+link.id+"--"+j);
				//var id=pano.getAreaId(mapArea);
				dojo.dom.insertAtPosition(mapArea,pano.imageMap,"last");
				dojo.event.connect(mapArea,"onmouseover",pano,"mouseOverArea");
				dojo.event.connect(mapArea,"onmouseout",pano,"mouseOutArea");
				}
			}
		},
	// the mouse is over an imagemap area.
	mouseOverArea: function(evt) {
		//pano.setCursor("pointer");
		//alert("over link.");
		var linkInfo=tour.getAreaId(evt.target.id);
		var link=xml.lookup[linkInfo.type+"s"][linkInfo.id];
		pano.liveLink=link;
		link.mouseOver();
		},
	// mouse has left an imagemap area.
	mouseOutArea: function(evt) {
		//pano.setCursor("crosshair");
		//alert("mouse out of area");
		tour.showLinkInfo(" ");
		pano.liveLink=null;
		//evt.preventDefault();
		},
	// called back when resolution changes.
	resCallback: function() {
		pano.loadPano(pano.pan); // TODO - make this work for the slidehow as well.
		},
	// load a panorama.
	loadPano: function(newpan) {
		// image name or size has changed, so reload the image
		//alert("loading image");
		pano.pan=newpan;
		pano.ready=false; // disable the panorama code while the image loads.
		// load a new image. showPano will be called by an event once the image has loaded.
		pano.panoImg=new Image();
		// call showPano when the image has loaded.
		pano.panoImg.onload=pano.showPano;
		pano.panoImg.src=xml.config.imagePath+pano.panoImgPath+tour.curView.imageset+"-"+port.resolution+".jpg";
		//alert("return:"+pano.panoImg.name);
		},
	// the image has loaded, so show the panorama.
	showPano: function() {
		// shows the images in the panorama, once they have been loaded.
		//alert("image loaded");
		pano.imgRatio=pano.panoImg.width/pano.panoImg.height;
		// create the image map.
		pano.makeImageMap();
		// set up and move the images.
		for (var i=0;i<2;i++) {
			var imgTag=pano.panoImgTags[i];
			if (imgTag!=undefined) {
				port.innerDiv.removeChild(imgTag);
				}
			imgTag=document.createElement("img");
			pano.panoImgTags[i]=imgTag;
			imgTag.id="panoImage"+i;
			imgTag.src=pano.panoImg.src;
			imgTag.style.width=((port.resolution*pano.imgRatio)+1).toFixed(0)+"px";
			imgTag.style.height=port.resolution+"px";
			imgTag.zIndex="0";
			imgTag.setAttribute("usemap","#panoMap",0);
			imgTag.style.position="absolute";
			imgTag.style.visibility="hidden"
			dojo.dom.insertAtPosition(imgTag,port.innerDiv,"first")
			};
		pano.moveImages();
		// show the images.
		for (var i=0;i<2;i++) {
			pano.panoImgTags[i].style.visibility="visible";
			}
		// remove the last slide after a slight delay (to allow pano image to display)
		var t=setTimeout(show.removeSlide,show.slideRemoveWait);
		// enable the panorama scrolling code.
		pano.ready=true;
		// update the 'you are now standing' messages.
		tour.showViewInfo();
		port.showArrows(true);
		pano.swinging=false;
		//alert("panorama ready");
		},
	// hide the panorama.
	hidePano: function() {
		for (var i=0; i<2; i++) {	
			var panoImgTag=pano.panoImgTags[i];
			if (panoImgTag!=undefined) {
				panoImgTag.style.visibility="hidden";
				}
			}
		},
	// called every so often to move the panorama.
	mover: function() {
		// don't do anything unless the mouse is down and the panorama is enabled.
		if (! pano.move || ! pano.ready) return;
		var width=port.resolution*port.ratio;
		// work out how much to move based on position in the viewport.
		// fraction you've moved from the viewport centre.
		var swing=2*(pano.mouseX-width/2)/width;
		var sign=(swing>0)?1:-1;
		// square it to improve sensitivity in the centre.
		swing*=swing;
		var offset=pano.scale*tour.scrollRepeatTime*swing*sign;
		pano.pan += offset;
		if (pano.pan <0) {
			pano.pan +=1;
			}
		else if (pano.pan >1) {
			pano.pan -=1;
			};
		showMessage2(" pan:"+pano.pan+" pic x:"+pano.panoImgTags[0].style.left);
		pano.moveImages();
		},
	// start off the swing from one pan to another.
	startSwing: function(walk) {
		// first work out whether we are going clockwise or anti.
		if (walk == undefined || walk.startPan ==undefined) {
			alert("Error in setup.xml - walk pan not defined - see pano.startSwing");
			return;
			}
		if (walk.startPan < pano.pan) {
			if (pano.pan-walk.startPan<0.5) {
				pano.ddpan=-(tour.scrollRepeatTime*pano.swingAccel);
				pano.midwayPan=(walk.startPan+pano.pan)/2;
				pano.swingPan=pano.pan;
				pano.swingNegative=true;
				}
			else
				{
				pano.ddpan=(tour.scrollRepeatTime*pano.swingAccel);
				pano.midwayPan=(walk.startPan-1+pano.pan)/2
				pano.swingPan=pano.pan-1;
				pano.swingNegative=false;
				}
			}
		else {
			if (pano.pan+1-walk.startPan < 0.5) {
				pano.ddpan=-(tour.scrollRepeatTime*pano.swingAccel);
				pano.midwayPan=(walk.startPan+1+pano.pan)/2
				pano.swingPan=pano.pan+1;
				pano.swingNegative=true;
				}
			else
				{
				pano.ddpan=(tour.scrollRepeatTime*pano.swingAccel);
				pano.midwayPan=(walk.startPan+pano.pan)/2
				pano.swingPan=pano.pan;
				pano.swingNegative=false;
				}
			}
		pano.dpan=0;
		pano.swingReversed=false;
		pano.swingWalk=walk;
		pano.swingTimer=setTimeout(pano.doSwing,tour.scrollRepeatTime);
		pano.swinging=true;
		},
	// do the swing
	doSwing: function() {
		pano.dpan+=pano.ddpan;
		pano.swingPan+=pano.dpan;
		var endSwing=false;
		if (pano.swingNegative) {
			if (pano.swingPan<pano.midwayPan && ! pano.swingReversed) {
				pano.ddpan=-pano.ddpan;
				pano.swingReversed=true;
				}
			if (pano.swingReversed && pano.dpan>0) {
				endSwing=true;
				}
			}
		else {
			if (pano.swingPan>pano.midwayPan && ! pano.swingReversed) {
				pano.ddpan=-pano.ddpan;
				pano.swingReversed=true;
				}
			if (pano.swingReversed && pano.dpan<0) {
				endSwing=true;
				}
			}
		pano.pan=pano.swingPan;
		if (pano.pan <0) {
			pano.pan +=1;
			}
		if (pano.pan >1) {
			pano.pan -=1;
			}
		if (!endSwing) {
			// call myself again.
			pano.moveImages();
			pano.swingTimer=setTimeout(pano.doSwing,tour.scrollRepeatTime);
			}
		else {
			pano.swinging=false;
			show.startWalk(pano.swingWalk);
			}
		},
	// move images to a position depending on the pan value.
	moveImages: function() {
		for (var i=0; i<2 ; i++) {
			pano.panoImgTags[i].style.left=((i-pano.pan)*port.resolution*pano.imgRatio).toFixed(0)+"px";
			}
		},
	// set cursor style
	setCursor: function(cursorStyle) {
		pano.panoDiv.style.cursor=cursorStyle;
		for (var i=0;i<2;i++) {
			pano.panoImgTags[i].style.cursor=cursorStyle;
			}
		pano.imageMap.style.cursor=cursorStyle;
		},
	stopDefault: function(evt) {
		evt.preventDefault();
		},
	// called when mouse is pressed
	onMouseDown: function(evt) {
		var index=-1;
		pano.imageOffset=dojo.html.getAbsolutePosition(port.mainDiv,true); // put here to speed up scrolling.
		if (tour.showClicks) {
			// This code is just to be used when creating image maps. It shouldn't normally run.
			// record the click and add it to the click string. Then display it to be copied.
			var mousePan=pano.pan+(pano.mouseX/(port.resolution*pano.imgRatio));
			var mouseHeight=pano.mouseY/port.resolution;
			//alert("mousePan:"+mousePan+" mouseHeight:"+mouseHeight);
			tour.clickStr+="<point pan=\""+mousePan.toFixed(4)+"\" height=\""+mouseHeight.toFixed(4)+"\" />\n";
			dojo.dom.textContent(tour.clickSpan,tour.clickStr);
			}
		else {
			// otherwise record the button press.
			if (! pano.swinging) {
				pano.mouseDown=true;
				window.setTimeout(pano.mouseTimeout,tour.mouseDelay);
				}
			}
		evt.stopPropagation();
		evt.preventDefault();
		},
	// mouse has been held down for long enough to start moving.
	mouseTimeout: function(evt) {
		if (pano.mouseDown) {
			pano.move=true;
			}
		},
	// called when mouse is released.
	onMouseUp: function(evt) {
		pano.mouseDown=false;
		if (! pano.move) {
			// released inside click period.
			if (pano.liveLink != null) {
				pano.mouseDown=false;
				pano.liveLink.click();
				}
			}
		pano.move=false;
		evt.stopPropagation();
		evt.preventDefault();
		},
	// the mouse has moved.	
	onMouseMove: function(evt) {
		// calculate mouse pan.
		pano.mouseX=evt.clientX-pano.imageOffset.x;
		pano.mouseY=evt.clientY-pano.imageOffset.y;
		evt.stopPropagation();
		evt.preventDefault();
		},
	// the mouse has entered the div.
	onMouseOver: function(evt) {
		evt.preventDefault();
		},
	// the mouse has left the div.
	onMouseOut: function(evt) {
		//dojo.debugShallow(evt);
		//dojo.debug("currentTarget: "+evt.currentTarget.id+" originalTarget:"+evt.originalTarget.id+" target:"+evt.target.id+" relatedTarget:"+evt.relatedTarget.id);
		relTarg=evt.relatedTarget || evt.toElement;
		if (!dojo.dom.isDescendantOf(relTarg,port.mainDiv)) {
			pano.mouseDown=false;
			pano.move=false;
			}
		//evt.stopPropagation();
		//evt.preventDefault();
		}
	};


function init() {
	// main initialisation function.
	// load the data file.
	//alert("initialising tour");
	xml.load();
	// set up the tour.
	//tour.init();
	}

//alert("starting");
dojo.addOnLoad(init);
