/***********************
 * CLASSES
 ***********************/
/**
 * Class
 * PhotoManager - Manages and allows nagivation of a list of Photo objects
 * @param photo_elm		object	Image element where photos will be loaded.
 * @param caption_elm	object	Element that will hold the caption text.
 * 		Note: Photo and caption elements may be passed to constructor seperately,
 *			  in an array, or in an object that has 'photo' and 'caption' keys.
 *
 * Public Member Variables
 * photos		array	List of Photo objects
 * currentPhoto	int		Index of current photo being viewed
 * elements		object	Container for the photo and caption elements
 */
function PhotoManager(photo_elm, caption_elm){
	this.photos = [];
	this.currentPhoto = 0;
	this.elements={
		'photo': (typeof photo_elm=='object' ? photo_elm: $(photo_elm)),
		'caption': (typeof caption_elm=='object' ? caption_elm: $(caption_elm))
	};
	/*var args = ((arguments.length > 1) ? arguments: arguments[0]), arkey = false,i=0,item=null;
	if(args instanceof Array){ args=slice(args); arkey=true; }
	for(var key in this.elements){
		item = args[(arkey?i:key)];
		this.elements[key] = (typeof(item)=='object'? item: $(item)); i++;
	}*/
}
/**
 * Public Methods
 */
PhotoManager.prototype = {
	/**
	 * get - Retrieves a member variable and returns it.
	 * @param mvar	string	Name of the member variable to retrieve
	 * @return 		mixed	Contents of the member variable or null if not found.
	 */
	'get':function(mvar){
		if(mvar in this.photos){ return this.photos[mvar]; }
		else if(mvar in this){ return this[mvar]; }
		return null;
	},
	/**
	 * set - Sets the contents of a member variable
	 * @param mvar	string			Name of the member variable to store the data in
	 * @param value	mixed			Data to store in the member variable
	 * @return		PhotoManager	Instance that called the method.
	 */
	'set':function(mvar,value){
		switch(mvar){
			case 'photos':break;
			default: this[mvar]=value;break;
		}
		return this;
	},
	/**
	 * add - Creates a new Photo object, adds it to the list, and returns it
	 * @param id		int		ID for the photo.
	 * @param options	object	Options for creating the new Photo.
	 * @return			Photo	New Photo object that was created.
	 */
	'add':function(id,options){
		this.photos.push(new Photo((id || this.photos.length),options));
		return this.photos[this.photos.length-1];
	},
	/**
	 * remove - Removes a Photo from the manager.
	 * @param photo		mixed	(Photo object)Removes the matching object. (Int)Removes the Photo object at the specified index
	 * @return 			mixed	The removed Photo object if one was found otherwise null.
	 */
	'remove':function(photo){
		var idx = (typeof(photo)=='object') ? this.photos.indexOf(photo): ((idx in this.photos)?idx:-1);
		if(idx != -1){ return this.photos.splice(idx,1); }
		return null;
	},
	/**
	 * addNav - Adds an element as trigger to navigate through the photos
	 * @param elm	mixed		String ID or element object to trigger the navigation
	 * @param fn	function	Function that will be called when event occurs
	 * @return		mixed		PhotoManager instance if successfull otherwise null
	 */
	'addNav':function(elm,fn){
		elm = (typeof(elm)=='object' ? elm :$(elm));
		if(!elm || (typeof(fn)!='function')){ return null; }
		var self = this, args=(slice(arguments,2)[0] || []);
		registerEvent(elm,'click',function(event){
			event=fixEvent(event); event.preventDefault();
			var nvargs = args.concat([self.currentPhoto,event]);
			fn.apply(self,nvargs);
		});
		return this;
	},
	/**
	 * gotoPhoto - Navigates to a specific photo
	 * @param idx	int		Index of photo to navigate to
	 */
	'gotoPhoto':function(idx){
		if(idx in this.photos){
			this.currentPhoto = idx;
			if(this.photos[this.currentPhoto]){
				this.photos[this.currentPhoto].load(this.elements['photo'],this.elements['caption']);
			}
		}		
	},
	/**
	 * prevPhoto - Navigates to the previous photo in the list
	 * @return	PhotoManager	Instance that called the method
	 */
	'prevPhoto':function(){
		this.currentPhoto = (this.currentPhoto <= 0)? this.photos.length-1: this.currentPhoto-1;
		if(this.photos[this.currentPhoto]){
			this.photos[this.currentPhoto].load(this.elements['photo'],this.elements['caption']);
		}
		return this;
	},
	/**
	 * nextPhoto - Navigates to the next photo in the list
	 * @return	PhotoManager	Instance that called the method
	 */
	'nextPhoto':function(){
		this.currentPhoto = (this.currentPhoto >= this.photos.length-1) ? 0 : this.currentPhoto+1;
		if(this.photos[this.currentPhoto]){
			this.photos[this.currentPhoto].load(this.elements['photo'],this.elements['caption']);
		}
		return this;
	}
};

/**
 * Class
 * Photo - Defines a photo and it's caption
 * @param id		int		Numeric id for the photo
 * @param options	object	Options for the photo
 *
 * Public Member Variables
 * id		int		Numeric id of the photo
 * name		string	Name of the photo
 * caption	string	Caption for the photo
 * sizes	object	JSON object hold image paths and sizes
 */
function Photo(id,options) {
	this.id = id;
	opts = options || {};
	this.name = opts.name || '';
	this.caption = opts.caption || '';
	this.sizes = {
		'thumb':{'path':'','size':''},
		'zoom':{'path':'','size':''}
	};
	if(!opts.sizes){ return; }
	for(var key in opts.sizes){
		this.sizes[key]=opts.sizes[key];
	}
}
/**
 * Public Methods
 */
Photo.prototype={
	/**
	 * load - Loads the photo into the given image element and caption into the caption element.
	 * @param photo_elm		object	Image element in which to load the photo
	 * @param caption_elm	object	Element in which the caption will be loaded
	 * @param size			string	Name of the size image to load.(Default: 'zoom')
	 */
	'load':function(photo_elm, caption_elm, size){
		size = size || 'zoom';
		if(photo_elm){ photo_elm.src = this.sizes[size]['path']; }
		if(caption_elm){ caption_elm.innerHTML = this.caption; }
	}
};


/**
 * Class
 * TabManager - Manages a collection of Tab objects
 * 
 * Public Member Variables
 * tabs				array	Tab objects
 * lastTab			int		Index of tab viewed before the current one
 * currentTab		int		Index of the currently visible tab
 * currentTabClass	string	CSS class name to apply to the tab when it becomes current
 */
function TabManager(){
	this.tabs=[];
	this.lastTab = 0;
	this.currentTab = 0;
	this.currentTabClass = 'current';
}
/**
 * Public Methods
 */
TabManager.prototype = {
	/**
	 * get - Retrieves the contents of a member variable and returns it
	 * @param mvar	mixed	(String)Name of the variable to retrieve, (Int)Index of tab to retrieve
	 * @return		mixed	Contents of the member variable or the Tab object
	 */
	'get':function(mvar){
		if(mvar in this.tabs){
			return this.tabs[mvar];
		}else if(mvar in this){ return this[mvar]; }
		return null;
	},
	/**
	 * set - Sets the contents of a member variable
	 * @param mvar	string		Name of the member variable to set.
	 * @param value	mixed		Data to store in the member variable
	 * @return		TabManager	Instance that called the method
	 */
	'set':function(mvar,value){
		switch(mvar){
			case 'tabs': break;
			default:
				if(mvar in this){ this[mvar]=value; }
		}
		return this;
	},
	/**
	 * add - Creates a new tab and adds it to the manager
	 * @param name	string	Name of the tab.
	 * @param tab	mixed	String ID or element object that acts as the tab
	 * @param page	mixed	String ID or element object that acts as the page for the tab
	 * @return		Tab		Newly created Tab object 
	 */
	'add':function(name,tab,page){
		var self = this;
		var len=this.tabs.push(new Tab(name,tab,page).addListen('show',function(){self.changeTab(this);}));
		return this.tabs[len-1];
	},
	/**
	 * changeTab - Changes the current tab's and new tab's className and hide/show their respective pages
	 * @param tab	Tab		Tab object to become the current tab
	 * @return		int		Index of the new current tab
	 */
	'changeTab':function(tab){
		var idx = this.tabs.indexOf(tab);
		if(idx == -1 || idx == this.currentTab){ return this.currentTab; }
		var curTab = this.get(this.currentTab);
		curTab.set('tabClass', curTab.get('tabClass').replace(this.currentTabClass,''));
		curTab.hide();
		tab.set('tabClass', ' '+this.currentTabClass);
		this.lastTab = this.currentTab;
		this.currentTab = idx;
		return this.currentTab;
	}
};

/**
 * Class
 * Tab - Defines a tab and it's corresponding page
 * @param name	string	Name of the tab. (default is '')
 * @param tab	mixed	String ID or element object that acts as the tab
 * @param page	mixed	String ID or element object that acts as the page for the tab
 *
 * Public Member Variables
 * name		string	Name of the tab
 * tab		object	Element object acting as a tab
 * page		object	Element object acting as the page
 * events	array	List of events and the list of their corresponding callbacks
 */
function Tab(name,tab,page){
	this.name = name || '';
	this.tab = (typeof tab=='object') ? tab:$(tab);
	this.page = (typeof page=='object') ? page:$(page);
	this.events = {'show':[],'hide':[]};
	this.type = null;
	this.contact_form = '';
	var self = this;
	/* Used internally for events */
	this.activate = function(event){ event=fixEvent(event); self.trigger(event.type,event); }
	/* Automatically have the page show when the tab is clicked (Prevent navigating away from anchor click) */
	this.addListen('click',function(tab,page,event){ event.preventDefault(); self.show(); });
}
/**
 * Public Methods
 */
Tab.prototype = {
	/**
	 * get - Gets the contents of a member variable and returns it
	 * @param mvar	string	Name of the member variable to retrieve
	 * @return		mixed	Contents of the member variable
	 * 		Note: 'tabClass' and 'pageClass' can be used as shortcuts for the 'className' property of each
	 */
	'get':function(mvar){
		switch(mvar){
			case 'tabClass':
				return this.tab.className;break;
			case 'pageClass':
				return this.page.className;break;
			default:
				if(mvar in this){ return this[mvar]; }break;
		}
		return null;
	},
	/**
	 * set - Sets the contents of a member variable.
	 * @param mvar	string	Name of the member variable to set
	 * @param value	mixed	Data to store in the member variable
	 * @return		Tab		Instance that called the method
	 * 		Note: 'tabClass' and 'pageClass' can be used as shortcuts for the 'className' property of each
	 */
	'set':function(mvar,value){
		switch(mvar){
			case 'tabClass':
				this.tab.className = value;break;
			case 'pageClass':
				this.page.className = value;break;
			default:
				if(mvar in this){ this[mvar]=value; }break;
		}
		return this;	
	},
	/**
	 * addListen - Adds a callback to the list for an event
	 * @param tabEvent	string		Name of the event to listen for
	 * @param fn		function	Function to call when the event is triggered
	 * @return			Tab			Instance that called the method
	 */
	'addListen':function(tabEvent,fn){
		tabEvent = (tabEvent) ? tabEvent.toLowerCase() : '';
		if(tabEvent in this.events){
			if(this.events[tabEvent].indexOf(fn) == -1){ this.events[tabEvent].push(fn); }
		}else{
			this.events[tabEvent] = [fn];
			registerEvent(this.tab,tabEvent,this.activate);
		}
		return this;
	},
	/**
	 * removeListen - Removes a callback from the list of functions to be called for an event
	 * @param tabEvent	string		Name of the event to listen remove the callback for
	 * @param fn		function	Function to remove from the list
	 * @return			mixed		If a matching callback is found, the removed callback is return, otherwise null 
	 */
	'removeListen':function(tabEvent,fn){
		tabEvent = (tabEvent) ? tabEvent.toLowerCase() : '';
		var idx = -1;
		if((tabEvent in this.events) && ((idx = this[tabEvent].indexOf(fn)) != -1)){
			return this[tabEvent].splice(idx,1);
		}
		return null;
	},
	/**
	 * show - Sets the page's style.display=='', and triggers the 'show' event
	 * @return	Tab		Instance that called the method
	 */
	'show':function(){
		this.page.style.display = '';
		this.trigger('show');
		if (this.contact_form) { 
			showID('contact_form');
			if (did('tab_id')) {
				did('tab_id').value = this.tab.id;
			}
		} else {
			hideID('contact_form');
		} 
		return this;
	},
	/**
	 * hide - Sets the page's style.display=='none' and triggers the 'hide' event
	 * @return	Tab		Instance that called the method
	 */
	'hide':function(){ 
		this.page.style.display = 'none';
		this.trigger('hide');
		return this;
	},
	/**
	 * trigger - Calls all the callback functions in order for a specified event
	 * @param tabEvent	string	Name of the event to have it's callbacks activated
	 * 		Note: Any parameters beyond the first will be passed as parameters to the callback.
	 */
	'trigger':function(tabEvent){
		if(!(tabEvent in this.events)){ return false; }
		var cbs = this.events[tabEvent];
		var args = slice(arguments,1);
		cbs.forEach(function(fn){
			if(fn instanceof Function){ fn.apply(this,[this.tab,this.page].concat(args)); }
		},this);
	}
};

/**
 * Class
 * GoogleMap - Wraps common functionality of Google's map API
 * @param elm		mixed		String ID or element object to place the map in.
 * @param options	object		optional;GMapOptions object for map options.
 * @param onLoad	function	optional;Callback for when the map is finished loading.
 *
 * Public Member Variables
 * compatible		boolean		True is browser is compatible, false otherwise
 * container		object		Element the GMap2 is placed into.
 * listeners		object		List of events and the corresponding callback
 * currentAddress	object		JSON version of the last geocoded address
 * georesponse		object		JSON geocoding response object
 * coords			GLatLng		Current center coordinates of the map
 * markers			array		List of GMarker objects on the map
 * map				GMap2		Instance of GMap2 object
 */
function GoogleMap(elm,options,onLoad){
	this.compatible = GBrowserIsCompatible();
	elm = (typeof elm=='object') ? elm: $(elm);
	if(!this.compatible || !elm){ return null; }
	onLoad = (typeof(onLoad)=='function') ? onLoad : function(){};
	this.container = elm;
	
	this.listeners = {}; /* List of events: {type:event_handle,... } */
	this.currentAddress = '';
	this.georesponse = {}; /* JSON geocoding response object */
	this.coords = new GLatLng(0,0); /* sync'd with map center point */
	/* Initialize the geocoder if it's not already done */
	this.geocoder = new GClientGeocoder();
	/* Associative list of GMarkers */
	this.markers = {};
	this.map = new GMap2(elm,options);
	//Add event listener to trigger callback for when map is done loading.
	this.addListen(null,'load',onLoad);
	//Map will be finished loading after this call
	this.map.setCenter(this.coords,1);
	//Remove callback listener since we're done loading
	this.removeListen('load');
	//Setup a listener to always update the internal 'coords' member to the value of the center of the map
	this.addListen(null,'moveend', function(map){ this.coords = map.getCenter(); });
}
/**
 * Static Properties
 * geocoder		GClientGeocoder		Instance of GClientGeocoder
 * responses	object				Associative list of geocoding response codes to human readable messages
 */
GoogleMap.responses = {
	'200': 'Success',
	'400': 'The directions request could not be parsed.',
	'500': 'Server error. Try again later.',
	'601': 'No address specified.',
	'602': 'Could not locate address. Please check the information and try again.',
	'603': 'Address is unavailable to map for legal or contractual reasons.',
	'610': 'Geocoder key is invalid or does not match this domain.',
	'620': 'You have exceeded the limit of address geocoding. Please try again tomorrow.'
};
/**
 * Public Methods
 */
GoogleMap.prototype = {
	/**
	 * get - Retrieves a member variable and returns it.
	 * @param mvar	string	Name of member variable to retrieve
	 * @return		mixed	Value of the member variable or null when member var does not exist.
	 */
	'get':function(mvar){
		switch(mvar){
			case 'center': return this.map.getCenter();
			case 'zoom': return this.map.getZoom();
			case 'address': return this.currentAddress;
			default:
				var key = mvar.toString().toLowerCase();
				if(key in this.markers){ return this.markers[key]; }
				else if(mvar in this){ return this[mvar]; }
		}
		return null;
	},
	/**
	 * set - Sets the contents of a member variable
	 * @param mvar		string		Name of member variable to save data to.
	 * @param value		mixed		Data to store in the variable
	 * @param option	mixed		optional;Extra option when setting some member variables.
	 * @return			GoogleMap	Instance that called the method
	 * 		Note: The following mvars have the ability to use the 3rd parameter
	 *			coords	(option: zoom	integer	Zoom level to be set at the same time(Default: Current Zoom))
	 *			address	(option: delim	string	Delimiter for splitting the address when passed as a string.(Default: ','))
	 */
	'set':function(mvar,value /*, option */){
		switch(mvar){
			case 'map': case 'container': break;
			case 'coords': case 'center':
				if(typeof(value)=='string' || (value instanceof String)){
					var tmp = value.split(',');
					if(tmp.length == 2){ this.coords = new GLatLng(parseFloat(tmp[0]),parseFloat(tmp[1])); }
					else{ break; }
				}else if(value instanceof Array){
					this.coords = new GLatLng(parseFloat(value[0]),parseFloat(value[1]));
				}else if(value instanceof GLatLng){
					this.coords = value;
				}else if(typeof(value) == 'object' && ('lat' in value) && ('long' in value)){	
					this.coords = new GLatLng(parseFloat(value['lat']),parseFloat(value['long']));
				}else{ break; }
				//auto center the map on the new coords and change zoom if hidden 3rd arg used.
				this.map.setCenter(this.coords,(arguments[2] || this.map.getZoom()));
				break;
			case 'zoom':
				this.map.setZoom(value);break;
			case 'address': case 'location':
				function trimit(item){ return (item||this).toString().trim().replace(/\s{2,}/g,' '); }
				if(value instanceof Array){
					//remove extra whitespace from each, then join with commas
					value = value.map(trimit).join(', ');
				}else if(typeof(value)=='string' || (value instanceof String)){
					//split on delimeter, remove extra white space from each, then rejoin with commas
					value = value.split((arguments[2]||',')).map(trimit).join(', ');
				}else if(typeof(value)=='object'){
					var str='';
					for(var key in value){ str+=trimit(value[key])+', '; }
					value = str.replace(/,\s$/,'');
				}
				this.currentAddress = value;
				break;
		}
		return this;
	},
	/**
	 * mark - Places a GMarker on the map
	 * @param loc	GLatLng			optional;Location to place the marker.(default is center point of map)
	 * @param opts	GMarkerOptions	optional;Options object for the marker. ('title' in options also used as marker key if specified)
	 * @return		GoogleMap		Instance that called the method
	 */
	'mark':function(loc, opts){
		loc = ((loc instanceof GLatLng)? loc: false) || this.get('center');
		opts = opts || {};
		var mark = new GMarker(loc,opts);
		this.map.addOverlay(mark);
		var name = (opts.title ? opts.title.toLowerCase():false) || getLength.call(this.markers);
		this.markers[name]=mark;
		return mark;
	},
	/**
	 * locate - Performs geocoding of currently stored address
	 * @param title		string		optional;Title to refer to this address (ex: "My Office" or "McDonald's" or "BP Gas Station")
	 * @param address	string		optional;Address to locate. If unset it will look for a match in the cache, then it will try and use
	 *								the currentAddress, then, if one still is not found, it will fail
	 * @return			GoogleMap	Instance that called the method or false on error
	 */
	'locate':function(title,address){
		if(!address && !title){ return false; }
		if(!address){
			var c = this.get('geocoder').getCache(), resp = c.get(title);
			if(resp){
				address = (resp.Placemark && resp.Placemark.length > 0) ? resp.Placemark[0].address : title;
			} /* if we found a matching item in cache, just use title */
			else if(!empty.test(this.get('address'))){ address = this.get('address'); } /* otherwise, if currentAddres isn't empty, use that */
			else{ return false; } /* otherwise, just fail */
		}else if(!title){ title=address; } /* if we have an address, but not a title, use the address as the title */
		var self = this, address = this.set('address',address).get('address'); /* update currentAddress set 'self' for scope purposes */
		this.get('geocoder').getLocations(address,function(response){ self.geocodeHandler(response,title); });
		return this;
	},
	/**
	 * geocodeHandler - Determines success/failure of geocoding and adds a message describing the results.
	 * @param response	object		JSON response object received from geocoding an address.
	 * @return			GoogleMap	Instance that called the method
	 */
	'geocodeHandler':function(response,title){
		if(!response){ return null; }
		var statusCode = response.Status.code;
		/* If successful: Get GLatLng object from coordinates */
		var coords = new GLatLng(0,0);
		if(statusCode == G_GEO_SUCCESS && response.Placemark.length > 0){/*todo:figure out what to do with multiple placemarks... */
			var place = response.Placemark[0].Point.coordinates;
			coords = new GLatLng(place[1],place[0]);
		}
		/* Get appropriate message */
		var msg= (statusCode in GoogleMap.responses) ? GoogleMap.responses[statusCode]:'Unknown Error.';
		/* Store response object and embed title and message into it */
		response.title = title;
		response.Status.message = msg;
		this.georesponse = response;
		/* Trigger the 'geocode' event */
		GEvent.trigger(this,'geocode',coords,this.georesponse);
		return this;
	},
	/**
	 * addListen - Adds a callback for the specified event on the GMap2 or GoogleMap objects
	 * @param mapEvent	string		Name of the event to listen for
	 * @param fn		function	Callback to trigger on the event
	 * @return			GoogleMap	Instance that called the method
	 */
	'addListen':function(obj, mapEvent,fn){
		if(typeof(fn) != 'function' || (mapEvent in this.listeners)){ return null; }
		var args = [this].concat(slice(arguments,2));
		var self = this, obj = (obj || this.map);
		this.listeners[mapEvent]= GEvent.addListener(obj, mapEvent,function(){
			var evargs = [self.map].concat(slice(arguments,(arguments[0]?0:1)), args);
			fn.apply(self,evargs);
		});
		return this;
	},
	/**
	 * removeListen - Removes the callback for a specified event on the GMap2 or GoogleMap objects
	 * @param mapEvent	string		Name of the event to remove the callback for
	 * @return			GoogleMap	Instance that called the method
	 */
	'removeListen':function(mapEvent){
		if(!(mapEvent in this.listeners)){ return null; }
		GEvent.removeListener(this.listeners[mapEvent]);
		delete this.listeners[mapEvent];
		return this;
	},
	/* Fix the constructor property for all instances */
	'constructor': GoogleMap
};

/* Setup proper inheritance for the GoogleMapCache object */
GoogleMapCache.prototype = (typeof GGeocodeCache != 'undefined' ? new GGeocodeCache() : new Object());
GoogleMapCache.prototype.constructor = GoogleMapCache;
/**
 * Class
 * GoogleMapCache - Geocode response cache object that can be preloaded with responses.
 * @param obj	mixed	optional;List of predefined response objects to load
 * 		Note: see loadCache method for accepted obj formats
 *
 * Public Member Variables
 * defaults		mixed	List of predefined response objects.
 */
function GoogleMapCache(obj){
	this.defaults = obj;
	GGeocodeCache.apply(this);
}
/**
 * Public Methods
 */
/**
 * reset - Overloaded reset that will preload responses if we specified any.
 */
GoogleMapCache.prototype.reset=function(){
	GGeocodeCache.prototype.reset.call(this);
	this.loadCache();
};
/**
 * get - Overloaded get that will try standard way first then, if not found,
 * 		 it will try to match against the title and nickname.
 * @param nat	string	Name, Address, or Title of the item to get from the cache
 */
GoogleMapCache.prototype.get=function(nat){
	nat = nat.toLowerCase();
	var response = GGeocodeCache.prototype.get.call(this,nat);
	if(!response){
		var c = {};
		for(var key in this){
			if(key != 'defaults' && typeof this[key] == 'object'){
				c = this[key];
			}
		}
		for(var key in c){
			if(nat == c[key].title.toLowerCase() || nat == c[key].name.toLowerCase()){ return c[key]; }
			else if(c[key].Placemark){
				for(var i=0;i < c[key].Placemark.length;i++){
					if(nat == c[key].Placemark[i].address.toLowerCase()){ return c[key]; }
				}
			}
		}
		
	}
	return response;
};
/**
 * loadCache - Preloads one or more response objects into the cache
 * @param obj	mixed	optional;Response object(s) to load (will override those defined when calling constructor).
 * 		Note: obj can be formatted in the following ways...
 *			Array of response objects -- [{..},{..}]
 *			Object whose keys hold an array of response objects -- {'key1':[{...},{...}], 'key2':[{...},{...}]}
 */
GoogleMapCache.prototype.loadCache=function(obj){
	obj = (obj ? (this.defaults=obj) : this.defaults);
	for(var section in obj){
		for(var title in obj[section]){
			this.put(title, obj[section][title]);
		}
	}
};



/***********************
 * CALLBACK FUNCTIONS 
 ***********************/

/**
 * startMap - Sets up initial map controls. (callback fired on map load)
 * @param map	GMap2	Google map object
 */
function startMap(map){
	//Add controls to the map
	map.addControl(new GSmallMapControl());
	map.addMapType(G_PHYSICAL_MAP);
	map.addControl(new GHierarchicalMapTypeControl());
	//Set map to be showing Ireland
	this.set('center', new GLatLng(53.6,-7.6), 6);
}

/**
 * display - Places a mark on the google map. (callback fired when geocoding has completed)
 * @param map		GMap2		Google map object
 * @param coords	GLatLng		Geocoded coordinates for the address
 * @param resp		object		JSON geocoding response object
 */
function display(map,coords,resp){
	if(resp.Status && resp.Status.code == G_GEO_SUCCESS && resp.Placemark.length > 0){
		var address = resp.Placemark[0].address, title=resp.title;
		var html='<span><b>'+title+'</b><br/>'+address.replace(/,/,"<br/>")+'</span>';
		var mark = this.mark(coords,{'title':title});
		mark.bindInfoWindowHtml(html);
	}
}

/**
 * toggleTypeMarks - toggles the visibility of marks on the map
 * @param type		string	Key in the cache defaults property
 * @param force		string	optional;Force hiding or showing of all markers of that type ('show' or 'hide')
 * @return			array	List of marks that were affected.
 */
function toggleTypeMarks(map, type, force){
	var c = map.get('geocoder').getCache(), force=force || '';
	var fn=(/hide|show/i.test(force)) ? force:null;
	if(!type || !c.defaults || !(type in c.defaults)){ return []; }
	var ret=[], items=c.defaults[type];
	var mark, func;
	for(var title in items){
		mark = map.get(title);
		if(!mark){ continue; }
		func = fn || (mark.isHidden() ? 'show' : 'hide');
		mark.closeInfoWindow();
		mark[func]();
		ret.push(mark);
	}
	return ret;
}

/**
 * toggleType - Determine the type based on the link clicked and toggle the marks
 * @param event	object	Event object passed automatically
 */
function toggleType(event){
	event=fixEvent(event);
	event.preventDefault();
	var targ = event.target, type = targ.firstChild.nodeValue;
	//  targ(type link).parent(li).parent(ul)   prev() then gets: div gmap_#
	var idx = prev(targ.parentNode.parentNode).id.split('_')[1];
	toggleTypeMarks(gmaps[idx], type);
}

/**
 * fixMap - Fixes the google map being displayed off center. (callback fired on a tab being shown)
 */
function fixMap(tab,page){
	var idx = tab.id.split('_')[1] || 0;
	//checkResize fixes map view, but results in map being off center
	gmaps[idx].get('map').checkResize();
	//reset the center of the map to where it's supposed to be
	gmaps[idx].set('center',gmaps[idx].get('coords'),gmaps[idx].get('zoom'));
}

/**
 * 			Globals Vars
 * tabMgr		TabManager		Instance of TabManager
 * photoMgrs	array			List of PhotoManager instances
 * gmaps		array			List of GoogleMap instances
 * photo_cache	array			Array of arrays containing objects to load into each PhotoManager
 * map_cache	array			
 */
var tabMgr = new TabManager(), photoMgrs = [], gmaps = [];
var photo_cache = [], map_cache = [];

/**
 * Window 'load' event - conducts initial setup after DOM has loaded
 */
registerEvent(window, 'load', function(){
	//Get the tour_nav and tour_content divs. If we don't have both of these, then skip everything
	var tnav = $('tour_nav'), tcontent = $('tour_content'); 
	if (!tnav || !tcontent) {
		return; 
	}

	//Get collection of anchors(tabs) in tour_nav, and divs with class tab_content(pages) in tour_content
	var tourTabs = slice($t.call(tnav,'a'));
	var pages = slice($t.call(tcontent,'div')).filter(function(div){ return /\s?tab_content\s?/i.test(div.className); });
	tourTabs.forEach(function(tab,idx){
		//tab=anchor, idx=tab index, type=tab type, name=text of anchor
		var type = tab.id.split('_')[0], name = (tab.firstChild) ? tab.firstChild.nodeValue: idx;
		if (!pages[idx]) { 
			return; //make sure we have a page for this tab
		}
		//create a tab object and set it's type
 		var otab = tabMgr.add(name,tab,pages[idx]).set('type',type);
 		try { 
 			otab.set('contact_form', contact_form[tab.id]); 
 		} catch (err) { }
		
 		switch(type){
 			case 'map':
 				//add a handler to fix the map when this tab is shown
 				otab.addListen('show',fixMap,idx);
 				//Get the map div in this tab
 				var mapelm = $('gmap_'+idx);
				//Get the anchors for each of the accommodation types (nextSibling.nextSibling = ul containing anchors)
				var mapnav = slice($t.call(next(mapelm),'a'));
				//add click handler for type links
 				mapnav.forEach(function(nav){
 					registerEvent(nav,'click',toggleType);
 				});
				//Create the google map
				gmaps[idx] = new GoogleMap(mapelm,null,startMap);
				//Add handler to the 'geocode' event to display the markers
				gmaps[idx].addListen(gmaps[idx],'geocode',display);
				//Get the geocoder
 				var gc = gmaps[idx].get('geocoder');
 				//Set the cache
 				gc.setCache(new GoogleMapCache());
 				//If this tab doesn't have a cache then skip
 				if(!map_cache[idx]){ return; }
 				//Tab has a cache - load it
 				gc.getCache().loadCache(map_cache[idx]);
 				var obj={},name='',addr='';
 				//For each cache->accommodation type->location map the address (or just use the name if the location was previously mapped)
				for(var type in map_cache[idx]){
					for(var title in map_cache[idx][type]){
						obj = map_cache[idx][type][title];
						name = (obj.title || obj.name || ''), addr = (obj.Placemark ? obj.Placemark[0].address : '');
						if(obj.Status && obj.Status.code == G_GEO_SUCCESS){ gmaps[idx].locate(name); }
						else{ gmaps[idx].locate(name,addr); }  
					}
	 			}
 				
 				break;
 			case 'photos':
 				//If there are no photos in the cache for this tab then skip
 				if(!photo_cache[idx] || photo_cache[idx].length < 1){
 					if(pages[idx]){
 						var navs = slice($t.call(pages[idx],'a'));
 						navs.forEach(function(nav){ nav.style.display='none'; },this);
 					}
 					break;
 				}
 				//Get the 'zoom' img element and the caption element
 				var photo_elm = $t.call(pages[idx],'img')[0];
 				var caption_elems = $t.call(pages[idx],'p');
 				var caption_elm;
 				for ( i in caption_elems ) {
 					caption_elm = caption_elems[i];
 					if (caption_elm && caption_elm.nodeType == 1 && caption_elm.className.indexOf('caption') > -1) {
 						break;
 					}
 				}
 				delete caption_elems;
 				//Create a new PhotoManager at the current tab index in photoMgrs array
 				photoMgrs[idx]=new PhotoManager(photo_elm, caption_elm);
 				photo_cache[idx].forEach(function(photo,i){
 					this.add(i,photo); //Create a new Photo object
 				},photoMgrs[idx]);
 				//Get all nav elements for this tab
 				var navs = slice($t.call(pages[idx],'a'));
 				navs.forEach(function(nav,i){
 					var fn=null, args=null;
 					switch(nav.className){ //Get the correct function to use
 						case 'previous': fn = photoMgrs[idx].prevPhoto; break;
 						case 'next': fn = photoMgrs[idx].nextPhoto; break;
 						default: fn = photoMgrs[idx].gotoPhoto; args=[i-2]; break;
 					}
 					//add the element as a navigation link that will call the function when clicked
 					photoMgrs[idx].addNav(nav,fn,args);
				});
 				break;
 		}
	});
	contact_form_show();
});
/**
 * Window 'unload' event - conducts cleanup
 */
registerEvent(window,'unload',function(){
	if(gmaps.length < 1){ return; }
	gmaps.forEach(function(gm){ GEvent.clearInstanceListeners(gm); });
	GUnload();
});