//Code ©2010 Bad Math Inc. (http://www.badmath.com/)
// mapControlBase

//this is used to ensure that the z index of the last clicked
//marker is always the highest z index so that it is visible.
var maxMarkerIndexSofar = 0;
// JavaScript Document
/**
mapControlBase is the base class for the controlPanelObject
It should never be instantiated directly, but it contains the
variables and methods common to all of the control panel objects
**/
function mapControlBase(map, ControlPanelId, customTourTabId, searchTabId, JSONData, defaultStyles)
{
	this.map = map;
	this.controlPanelId = ControlPanelId;
	this.customTourTabId = customTourTabId;
	this.searchTabId = searchTabId;
	this.JSONData = JSONData;
	this.defaultStyles = defaultStyles;
	this.markersArray = new Object();
	this.markerTrails = [];
	this.highlightedTrail;
	this.highlightedTrailId = -1;
	this.infoWindow = new google.maps.InfoWindow({content: "", maxWidth:600});
	this.loadListener = null;
	this.closeListener = null;
	//if this is true, the custom tour is displayed on the map.
	this.overlayCustomTourOnMap = false;
	//this is a global variable that holds the most recent
	//filtering of the JSON data passed into the constructor
	//it is used to overlay (or remove) custom tour icons
	//without having to recalculate the last state from scratch
	this.filteredJSON;
	this.JSONData;
	this.map;
	this.customTourTabId;
	this.flusterObject = null;
	this.flowPlayerLoaded=false;



	this.extendBounds = function(bounds, value) {
		var latLng = new google.maps.LatLng(value.latitude,value.longitude, false);
		if (bounds == null) {
			bounds = new google.maps.LatLngBounds(latLng, latLng);
		} else {
			bounds.extend(latLng);
		}
		return bounds;
	};

	this.getBounds = function()
	{
		var bounds = null;
		var self = this;
		jQuery.each(self.markersArray, function(markerKey, markerValue)
		{
			if (markerValue.getVisible()) {
				if (bounds === null) {
					bounds = new google.maps.LatLngBounds(markerValue.getPosition());
				} else {
					bounds.extend(markerValue.getPosition());
				}
			}
		});
		return bounds === null ? self.map.getBounds() : bounds;
	};



	/**
	updateMap takes a JSON object as an input
	and parses it to draw the location markers onto the map.
	before drawing to the map, it clears away old markers.
	customTourCLick is the click event associated with the
	add or remove from custom tours in the info window.
	We overide the parent class here to improve efficiency when drawing large
	numbers of markers.
	**/
	this.updateMap = function(filteredData, setBounds)
	{
		if (setBounds == undefined)
		{
			setBounds = true;
		}
		var self = this;
		//close the window so that a window doesn't remain open for a location
		//that is no longer on the map   map.getZoom(
		self.infoWindow.close();
		self.clearTrailHighlighting();

		//we keep this data around in case the overlay checkbox changes.
		self.filteredJSON = filteredData;

		//if filteredData is null, we are in an empty custom tour or something is wrong.
		if(filteredData == null)
		{
			if (window.console && console.log) {
				console.log("filtered data is null");
			}

			return;
		}

		if(self.flusterObject != null && self.flusterObject.inUse)
		{
			self.flusterObject.clearMarkers();
		}

		var customTours = self.getCustomTours();
		var customTourJSON = self.getJSONForLocationIds(customTours);

		//first, we clear the map of
		//markers that are no longer in the filtered data
		jQuery.each(self.markersArray, function(markerKey, markerValue)
		{
			if(markerKey == null || markerValue == null)
			{
				return true;
			}

			if(filteredData.markers.hasOwnProperty(markerKey) == false
				|| (this.overlayCustomTourOnMap && customTourJSON.hasOwnProperty(markerKey) == false))
			{
				//remove the marker from the map by setting its
				//visible to false
				markerValue.setVisible(false);
			}

		});//end each marker in markersArray


		var bounds = null;

		//we have now already removed any stale locations from the list.  Now we see what we have to add to it.
		//we grab each marker from the JSON and create a marker for it if necessary.
		jQuery.each(filteredData.markers, function(key, value)
		{

			if(value == null)
			{
				//if there are no custom tours, and empty object gets passed in.
				//this protects against this case
				return true;
			}
			//bounds = self.extendBounds(bounds, value);

			//get the category for the icon that we need to display
			//since there are multiple categories now.
			var markerCategoryId = self.getMarkerCategoryId("id" + value.id);

			//check to see if we can just "reactivate" an existing marker
			if(self.markersArray.hasOwnProperty(key))
			{
				//check to see if we need to change its icon
				//because it was just added to or removed from a custom
				//tour.
				if(self.inCustomTour(value.id))
				{

					self.markersArray[key].setIcon(self.getMarkerIcon('custom', value, markerCategoryId));

					//since it's already there, we don't need to draw it
					//as part of the custom tour overlay.
					customTourJSON.markers["id" + value.id] = null;
				}
				else
				{
					self.markersArray[key].setIcon(self.getMarkerIcon('regular', value, markerCategoryId));
				}
				self.markersArray[key].setVisible(true);


			}//end if the key is in the markers array
			else
			{
				//if we are here, we need to create a new marker and add it to the Array

				//we need to have a different icon if the marker is in a custom tour
				var markerIcon;
				if(self.inCustomTour(value.id) == true)
				{
					markerIcon = self.getMarkerIcon('custom', value, markerCategoryId);
					//we remove the location from the list of cutom tours.
					//It has already been drawn.
					customTourJSON.markers["id" + value.id] = null;
				}
				else
				{
					markerIcon = self.getMarkerIcon('regular', value, markerCategoryId);
				}
				//create the marker and its click events

				self.newMarker(markerIcon, value, true);
			}

		});

		//finally, we add in the markers that are in the custom tour and were not in the
		//original filtered data iff this.overlayCustomTourOnMap is true.
		if(self.overlayCustomTourOnMap)
		{
			jQuery.each(customTourJSON.markers, function(key, value)
			{


				//check to see that we didn't remove a marker by setting it to
				//null
				if(value != null)
				{
					//Extend teh map boundaries arround this point
					bounds = self.extendBounds(bounds, value);

					//get the category for the icon that we need to display
					//since there are multiple categories now.
					var markerCategoryId = self.getMarkerCategoryId("id" + value.id);
					if(self.markersArray.hasOwnProperty(key))
					{
						//set the marker in the JSON.
						self.markersArray[key].setIcon(self.getMarkerIcon('customSemi', value, markerCategoryId));
						self.markersArray[key].setVisible(true);

					}
					else
					{
						var markerIcon = self.getMarkerIcon('customSemi', value, markerCategoryId);
						//create the marker and its click events
						self.newMarker(markerIcon, value, true);
					}

				}//end if value != null

			});

		}//end if overlay
		if(setBounds)
		{
			if (!jQuery.browser.msie || jQuery.browser.version >= 8) {
				self.map.fitBounds(self.getBounds());
			} else {
				window.setTimeout(function() { self.map.fitBounds(self.getBounds()) }, 300);
			}

		}

	}; //end updateMap


	this.addMarkerToCluster =function(key, marker, topCategoryId) {
		var self = this;
		//only add it to the fluster if it is not already there.
		if(!self.flusterObject.isMarkerInACluster(key))
		{
			marker.setVisible(false);

			if(self.flusterObject.inUse)
			{
				self.flusterObject.addMarkerAfterDraw(marker, topCategoryId, key);
			}
			else
			{
				self.flusterObject.addMarker(marker, topCategoryId, key);

			}
		}
		else //update the category
		{
			self.flusterObject.updateCategoryId(marker, topCategoryId, key);
		}
	};

	/*
	This updates the map using the fluster marker manager to group markers by
	category ID.
	*/
	this.updateMapWithClusters = function(filteredData, setBounds, initialBounds)
	{
		/*
		google.maps.event.addListener(self.map, 'tilesloaded', function()
		{
			alert("tiles loaded");
		});
		*/

		if (setBounds == undefined)
		{
			setBounds = true;
		}

		if (this.flusterObject == null) {
			this.flusterObject = new Fluster3(map, defaultStyles, initialBounds);
		}
		var self = this;
		var bounds = null;
		//close the window so that a window doesn't remain open for a location
		//that is no longer on the map
		self.infoWindow.close();

		self.clearTrailHighlighting();

		//we keep this data around in case the overlay checkbox changes.
		self.filteredJSON = filteredData;

		//if filteredData is null, we are in an empty custom tour or something is wrong.
		if(filteredData == null)
		{
			if (window.console && console.log) {
				console.log("filtered data is null");
			}
			return;
		}

		var customTours = self.getCustomTours();

		var customTourJSON = self.getJSONForLocationIds(customTours);

		//disable all markers that are not in our current JSON.
		jQuery.each(self.markersArray, function(key, value)
		{
			if(key == null || value == null)
			{
				return true;
			}
			if(!filteredData.markers.hasOwnProperty(key) && !(self.overlayCustomTourOnMap && customTourJSON.hasOwnProperty(key)))
			{
				value.setVisible(false);

				self.flusterObject.removeMarker(key);

			}
		});


		jQuery.each(filteredData.markers, function(key, value)
		{

			if(key == null || value == null)
			{
				return true;
			}

			//extend the boundaries of the map to contain this pint
			//bounds = self.extendBounds(bounds, value);

			//get the category for the icon that we need to display
			//since there are multiple categories now.
			var markerCategoryId = self.getMarkerCategoryId("id" + value.id);
			var topCategoryId = self.JSONData.markerIcons["id" + markerCategoryId].topCategoryId;

			var markerIcon;
			var marker;
			//we get our marker
			if(self.markersArray.hasOwnProperty(key))
			{
				marker = self.markersArray[key];
			}
			else
			{
				marker = self.newMarker(markerIcon, value, true);
			}


			//if this entry is in the custom tour...
			if(customTourJSON.markers.hasOwnProperty(key))
			{
				markerIcon = self.getMarkerIcon('custom', value, markerCategoryId);
				marker.setIcon(markerIcon);
				//if we are overlaying custom tours on map, we don't group them
				if(self.overlayCustomTourOnMap)
				{
					//remove it from the fluster in case it was addeed in a previous draw
					self.flusterObject.removeMarker(key);
					marker.setMap(map);
					marker.setVisible(true);

					//remove this location from our list if custom tours so that the remainder can
					//be assigned the half greyed marker (once we have them)
					customTourJSON.markers["id" + value.id] = null;

				}
				//if we are not overlaying, we do group them
				else
				{
					self.addMarkerToCluster(key, marker, topCategoryId);
				}
			}//end we're in the custom tour.
			else
			{
				//we're not in a custom tour - we group no matter what.
				markerIcon = self.getMarkerIcon('regular', value, markerCategoryId);
				marker.setIcon(markerIcon);
				self.addMarkerToCluster(key, marker, topCategoryId);
			}
		});

		//consider whether the overlay custom tours is checked
		if(self.overlayCustomTourOnMap)
		{
			jQuery.each(customTourJSON.markers, function(key, value)
			{

				if(key == null || value == null)
				{
					return true;
				}


				//get the category for the icon that we need to display
				//since there are multiple categories now.  We always display the top
				//category for markers not in the current JSON.
				var markerCategoryId = self.JSONData.markers[key].categories[0];
				//check to see that we didn't remove a marker by setting it to
				//null

				var marker;
				var markerIcon = self.getMarkerIcon('customSemi', value, markerCategoryId);
				if(self.markersArray.hasOwnProperty(key))
				{
					try
					{
						marker = self.markersArray[key];
					}
					catch(message)
					{
						if (window.console && console.log) {
							console.log(message);
						}
					}
				}
				else
				{
					//create the marker and its click events
					marker = self.newMarker(markerIcon, value, true);
				}
				//remove it from the fluster in case it was addeed in a previous draw

				self.flusterObject.removeMarker(key);
				marker.setIcon(markerIcon);
				marker.setVisible(true);
				marker.setMap(self.map);

				//extend the boundaries of the map to contain this pint
				bounds = self.extendBounds(bounds, value);


			});

		}//end if overlay

		//delay the call to fitbounds in IE7 to fix redraw issues
		if (setBounds) {
			if (!jQuery.browser.msie || jQuery.browser.version >= 8) {
				self.map.fitBounds(self.getBounds());
			} else {
				window.setTimeout(function() { self.map.fitBounds(self.getBounds()) }, 500);
			}
		}


		//if this is the first draw, we need to create the clusters
		//otherwise, we just redraw.
		if(!self.flusterObject.inUse)
		{

			self.flusterObject.initialize();
			self.draw();
		}

		

	};




	this.hideTrail = function(location) {
		if (this.markerTrails.hasOwnProperty(location.id)) {
			for (var i =0; i< this.markerTrails[location.id].length; i++) {
				this.markerTrails[location.id][i].setMap(null);
			}
		}
	};

	this.clearTrailHighlighting = function() {
		var self = this;
		if (self.highlightedTrail) {
			for (var i =0; i< self.highlightedTrail.length; i++) {
				self.highlightedTrail[i].setMap(null);
				this.markerTrails[highligtedTrailId][i].setOptions({strokeOpacity: 0.5});
			}
		}
		self.highlightedTrail = [];
	}


	this.highlightTrail = function(location) {
		var self = this;
		self.clearTrailHighlighting();
		if (self.markerTrails.hasOwnProperty(location.id)) {
			highligtedTrailId = location.id;

			for (var i =0; i< self.markerTrails[location.id].length; i++) {
				this.markerTrails[location.id][i].setMap(null);
				var newTrail = new google.maps.Polyline({
					path: self.markerTrails[location.id][i].getPath(),
					strokeColor: "#FFFFFF",
					strokeOpacity: 0.5,
					strokeWeight:6,
					zIndex: 1,
					map: self.map
				});

				self.highlightedTrail.push(newTrail);
				this.markerTrails[location.id][i].setMap(self.map);
				this.markerTrails[location.id][i].setOptions({strokeOpacity: 0.75});
			}

		}



	};

	this.drawTrail = function(location) {
		var self = this;
		var markerCategoryId = self.getMarkerCategoryId("id" + location.id);
		var trailColor = this.JSONData.markerIcons["id" + markerCategoryId].trailColor;

		function createTrailGPX(trail) {

			var pointArray = [];
			trail.find("trkseg").each(function() {
				var segment = jQuery(this);
				segment.find("trkpt").each(function() {
					var point = jQuery(this);
					var lat = parseFloat(point.attr("lat"));
					var lng = parseFloat(point.attr("lon"));
					pointArray.push(new google.maps.LatLng(lat, lng, false));
				});
			});
			var newTrail = new google.maps.Polyline({
				path: pointArray,
				strokeColor: trailColor,
				strokeOpacity: 0.5,
				strokeWeight:2,
				zIndex: 2,
				map: self.map
			});
			google.maps.event.addListener(newTrail, 'click', function() {
					self.markerClickFunction(self.markersArray["id" + location.id], location,
						"customTourButtonForInfoWindow" + location.id);
			});
			return newTrail;
		}

		if (self.markerTrails.hasOwnProperty(location.id)) {
			for (var i =0; i< self.markerTrails[location.id].length; i++) {
				self.markerTrails[location.id][i].setMap(self.map);
				this.markerTrails[location.id][i].setOptions({strokeColor: trailColor});
			}
		}else {

			self.markerTrails[location.id] = [];
			jQuery.get(MEDIA_DIRECTORY +location.trail, {},
				function(data, responseCode) {
					//var xml = GXml.parse(data);
					//var trails = xml.documentElement.getElementsByTagName("trk");

					jQuery(data).find("trk").each(function() {

						self.markerTrails[location.id].push(createTrailGPX(jQuery(this)));
					});
				});
		}
	};


	/**
	markerClickFunction carries out
	the needed activities to open the info window, including
	setting the content and enabling click events for buttons within the info
	window
	**/
	this.markerClickFunction = function(marker, markerJSON, customMapButtonId)
	{
		var self = this;
		self.infoWindow.close();

		if (self.loadListener != null) {
			google.maps.event.clearListeners(self.infoWindow, 'domready');
			self.loadListener = null;
		//	$f("*").each(function () { this.unload(); });

		}

		self.highlightTrail(markerJSON);

		self.infoWindow.setContent(self.getInfoWindowMarkup(markerJSON, customMapButtonId));

		jQuery(".details").css('height', (jQuery(".details").parent().height() - 40) + "px");
		//bring the marker to the foreground in case it is buried.
		marker.setZIndex(++maxMarkerIndexSofar);
		self.infoWindow.open(self.map, marker);


		self.drawDownloadSelectBox("infoWindowDownloadLI", null,  markerJSON.id, null);

		if (typeof(_gaq) != "undefined" && _gaq.push) {
			_gaq.push(['_trackEvent', 'POIs', 'View', markerJSON.title]);
		}
	}//end markerClickFunction
	/*
	This takes a type parameter and some JSON
	and returns a google maps icon with the
	anchor point set according to the type parameter.
	Type can be either regular, custom, or customSemi
	markerCategoryIndex is the index in the categories array that represents
	the current display category for the location/marker.
	*/
	this.getMarkerIcon = function(type, JSONForMarker, markerCategoryId)
	{
		var image;
		var imageSize;
		var anchorPoint;
		var anchorX;
		var anchorY;
		var height;
		var width;

		if(!this.JSONData.markerIcons.hasOwnProperty("id" +  markerCategoryId))
		{
			if (window.console && console.log) {
				console.log("getMarkerIcon: JSONData.markerIcons has no icons for category ID " + markerCategoryId);
			}

			return;
		}

		switch(type)
		{
			case 'regular':
				image = IMAGE_DIRECTORY + this.JSONData.markerIcons["id" +  markerCategoryId].icon;
				height = this.JSONData.markerIcons["id" +  markerCategoryId].iconHeight;
				width = this.JSONData.markerIcons["id" +  markerCategoryId].iconWidth;
				imageSize = new google.maps.Size(width, height);
				anchorX = this.JSONData.markerIcons["id" +  markerCategoryId].iconAnchorX;
				anchorY = this.JSONData.markerIcons["id" +  markerCategoryId].iconAnchorY;
				anchorPoint = new google.maps.Point(anchorX, anchorY);
				break;
			case 'custom':
				image = IMAGE_DIRECTORY + this.JSONData.markerIcons["id" +  markerCategoryId].iconCustom;
				height = this.JSONData.markerIcons["id" +  markerCategoryId].iconCustomHeight;
				width = this.JSONData.markerIcons["id" +  markerCategoryId].iconCustomWidth;
				imageSize = new google.maps.Size(width, height);
				anchorX = this.JSONData.markerIcons["id" +  markerCategoryId].iconCustomAnchorX;
				anchorY = this.JSONData.markerIcons["id" +  markerCategoryId].iconCustomAnchorY;
				anchorPoint = new google.maps.Point(anchorX, anchorY);
				break;
			case 'customSemi':
				image = IMAGE_DIRECTORY + this.JSONData.markerIcons["id" +  markerCategoryId].iconCustomSemi;
				height = this.JSONData.markerIcons["id" +  markerCategoryId].iconCustomSemiHeight;
				width = this.JSONData.markerIcons["id" +  markerCategoryId].iconCustomSemiWidth;
				imageSize = new google.maps.Size(width, height);
				anchorX = this.JSONData.markerIcons["id" +  markerCategoryId].iconCustomSemiAnchorX;
				anchorY = this.JSONData.markerIcons["id" +  markerCategoryId].iconCustomSemiAnchorY;
				anchorPoint = new google.maps.Point(anchorX, anchorY);
			break;
			default:
				if (window.console && console.log) {
					console.log("invalid marker type passed to getMarkerIcon");
				}
		}
		return  newIcon = new google.maps.MarkerImage(image, imageSize, new google.maps.Point(0,0), anchorPoint);
	}

	/*
	This gets the index in JSONData.markers.categories[] that represents the category
	that this marker/location is currently being displayed as.
	In suggested tours, searches, and my custom tour, this is always index 0 (top category)
	In the all points class, this is overrided to give the higest category index that
	is currently displayed.
	*/
	this.getMarkerCategoryId = function(locationId)
	{
		if(this.JSONData.markers.hasOwnProperty(locationId)
			&& this.JSONData.markers[locationId].categories[0] != null)
		{
			return this.JSONData.markers[locationId].categories[0];
		}
		else
		{
			if (window.console && console.log) {
					console.log("getMarkerCategoryId was asked to get the category for id "
				  + locationId + ", which is not in JSONData");
			}

		}
	}

	/**
	this creates a new marker, assigns it to the map,
	and adds it to the passed in markers array
	and creates its click events
	addToMap is a boolean value used to create a marker that is
	going to start life in a cluster.
	This returns the new marker.
	**/
	this.newMarker = function(markerIcon, locationObject, addToMap)
	{
		var self = this;
		var point = new google.maps.LatLng(locationObject.latitude,
											   locationObject.longitude, true);
		//set our marker options
		var markerMap = null;
		if(addToMap == true)
		{
			markerMap = self.map;
			if (locationObject.trail != "") {
				self.drawTrail(locationObject);
			}
		}
		//this is used to remove the html entity encoding
		var titleDiv =  jQuery("<div/>").append(locationObject.title);

		var shadowImage = new google.maps.MarkerImage(SHADOW_IMAGE,
			// This marker is 20 pixels wide by 32 pixels tall.
		  new google.maps.Size(54, 31),
		  // The origin for this image is 0,0.
		  new google.maps.Point(0,0),
		  // The anchor for this image is the base of the flagpole at 0,32.
		  new google.maps.Point(14, 31));
		var markerOptions = {"clickable":true, "icon":markerIcon,
			"map":markerMap, "position":point , "visible":true, "title":titleDiv.text(), "zIndex": 0};

		//we need to create the map variable here in order for the closure
		//to keep it alive as a dinstinct variable in the event listener.
		var marker = new google.maps.Marker(markerOptions);
		try{
		marker.setShadow(shadowImage);
		}
		catch(message)
		{
			if (window.console && console.log) {
					console.log(message);
			}
		}
		//we need to keep an array of markers in order to clear the map.
		self.markersArray["id" + locationObject.id] = marker;

		//this is the id of the add to custmom map icon.
		//the click function needs this.
		var customMapButtonId = "customTourButtonForInfoWindow" + locationObject.id;

		google.maps.event.addListener(marker, 'visible_changed', function() {
			//self.setTrailVisible(locationObject);
			if (locationObject.trail) {
			if (this.getVisible()) {
				self.drawTrail(locationObject);
			} else {
				self.hideTrail(locationObject);
			}
		}
		});
		google.maps.event.addListener(marker, 'icon_changed', function() {
			//self.setTrailVisible(locationObject);
			if (locationObject.trail) {
				if (this.getVisible()) {
					self.drawTrail(locationObject);
				} else {
					self.hideTrail(locationObject);
				}
			}
		});



		google.maps.event.addListener(marker, 'click', function()
		{
			 self.markerClickFunction(marker, locationObject, customMapButtonId);
		});


		return marker;

	}//end newMarker()

	/**
	getDownloadSelectBox creates the html for the select box
	that allows downloading of garmin data, etc.
	This html ends up in the info window, as well as the control
	panel for certain pages.
	the idString is passed in by the info window click event
	if the idSring is null, this download box was created by the
	location list
	idString is just the Id of the location.
	**/
	this.drawDownloadSelectBox = function(targetId, locationListId, idString, sortableId)
	{
		var self = this;
		//we need a unique id for this download box so that we can use live click events
		var ourId;
		var target;
		var downloadtext;
		var headerText =  "Download this tour:";
		var hasAudio =false;
		var target = jQuery("#" + targetId);
		var tourName ="";
		var tourSize=0;

		if(idString != null)
		{
			ourId = "downloadBoxFor" + idString;
			downloadtext = "Download...";
			headerText =  "Download:";
			hasAudio =(self.JSONData.markers["id" + idString].audio !="");
		}
		else if (locationListId != null)
		{
			ourId = "downloadBoxFor" + 	locationListId;
			downloadtext= "Download this tour...";
			headerText =  "Download this tour:";
			if (jQuery("#" + locationListId).parent().attr('id') == 'controlPanel') {
				tourName = "Custom Tour";
			} else {
				tourName =jQuery("#" + locationListId).parent().prev().text();
			}
			tourSize = jQuery("#" + locationListId + " ul").children().length;
			tmpList = self.getLocationsFromSortableList(sortableId).split(",");
			for (var i=0; i< tmpList.length; i++) {
				var locationId = tmpList[i];
				if (self.JSONData.markers["id" + locationId].audio !="") {
					hasAudio = true;
					break;
				}
			}
		}
		else
		{
			if (window.console && console.log) {
				console.log("drawDownloadSelectBox error: both LocationListId and idString are null!!!");
			}

			return;
		}

		var downloadImage = jQuery("<img/>")
			.attr("src", "template/buttonDropDown.png")
			.attr("alt", "Download")
			.addClass("downloadButton");

		target.append(downloadImage)
			.append(downloadtext);


		//remove any existing dropdownboxes
		jQuery("#" + ourId).remove();

		//create the dropdown box.
		var headerImage = jQuery("<img/>").attr("src", "template/dropDownHeader.gif");
		var headerListItem = jQuery("<li/>").addClass("header")
			.append(headerImage).append(headerText);

		var gpxImage = jQuery("<img/>")
			.attr("src", "template/buttonGPX.png")
			.attr("alt", "GPX file");
		var gpxLabel = jQuery("<label/>")
			.append(gpxImage)
			.append("GPX file for GPS import")
			.attr("id", "gpxImage" + ourId);
		var gpxListItem = jQuery("<li/>").append(gpxLabel);

		var textFileImage = jQuery("<img/>").attr("src", "template/buttonTextFile.png")
			.attr("alt", "Text file");
		var textFileLabel = jQuery("<label/>").append(textFileImage)
			.attr("id", "textImage" + ourId);
			if (idString != null) {
				textFileLabel.append("Text description");
			} else {
				textFileLabel.append("Text descriptions");
			}

		var textListItem = jQuery("<li/>").append(textFileLabel);

		if(hasAudio) {

			var mp3Image = jQuery("<img/>").attr("src", "template/buttonMP3.png")
			.attr("alt", "Audio file");
			var mp3Label = jQuery("<label/>").append(mp3Image)
			.attr("id", "mp3Image" + ourId);

			if (idString != null) {
				mp3Label.append("MP3 audio description");
			} else {
				mp3Label.append("MP3 audio descriptions");
			}
			var mpsListItem = jQuery("<li/>").append(mp3Label);
		}

		var syncImage = jQuery("<img/>").attr("src", "template/buttonGarminComm.png")
			.attr("alt", "Garmin Communicator");
		var syncLabel = jQuery("<label/>")
			.append(syncImage)
			.append("Sync to a Garmin GPS unit")
			.attr("id", "syncImage" + ourId);
		var syncListItem = jQuery("<li/>")
			.addClass("garminSync")
			.append(syncLabel);



		var list = jQuery("<ul/>")
			.attr('id', ourId)
			.addClass("dropDownMenu").append(headerListItem)
			.append(gpxListItem)
			.append(textListItem);
			if (mpsListItem){
				list.append(mpsListItem);
			}
			list.append(syncListItem);

		jQuery('body').append(list);
		list.hide();
		list.hover(function() {
				jQuery(this).show();
		}, function() { jQuery(this).hide() });

		//set the hover event that displays our download list
		jQuery("#" + targetId).click(function()
		{
			var newOffset = jQuery(this).offset();
			var padding = jQuery(this).css("padding-left");
			list.css("top", newOffset.top);
			var newLeft = newOffset.left + parseInt(padding);
			list.css("left", newLeft + "px");
			list.show();
	/*	})
		 .mouseout(function()
		{
			list.hide(); */
		});


		//set the click events
		syncListItem.bind('click', function()
		{

			//if we have no suggested tour ID, we were called from the location list
			//we need
			//to get a list of the ids in the sortable list
			//if we are in the info window, there is no list to search, and
			//we just pass in the idString parameter which contains a single
			//id
			if(idString == null)
			{
				if (typeof(_gaq) != "undefined" && _gaq.push) {
					_gaq.push(['_trackEvent', 'Tours', 'Garmin Sync', tourName, tourSize]);
				}
				idString = self.getLocationsFromSortableList(sortableId);
			}
			if(idString == "")
			{
				//we have no ids (an empty custom tour)
				return;
			}

			if (typeof(_gaq) != "undefined" && _gaq.push) {

				var ids = idString.toString().split(",");
				var pois = self.getJSONForLocationIds(ids);
				jQuery.each(pois.markers, function(key, value) {
					_gaq.push(['_trackEvent', 'POIs', 'Garmin Sync', value.title]);
				});
			}

			//it can take some time to find the GPS, so we need to let the user know
			//we are doing something.
			if (confirm("The site will now attempt to connect to your Garmin GPS.  This may take a moment.")) {
				try
				{
					control = new Garmin.DeviceControl();
					//alert(control.isPluginInstalled());
					control.register(new controlPanel.garminListener(idString));
				}
				catch(message)
				{
					if (confirm("We have not detected the required Garmin Communicator plug-in.  Would you like to open a window to the download page?" ))
					{
						open("http://www8.garmin.com/products/communicator/", "garmin");
					}
				}
				var unlocked = control.unlock([SERVER_NAME, GARMIN_KEY]	);
				//findDevices fires a callback when it is finished that is in the garminListener Object.
				//program execution continues there.
				if (unlocked) {
					control.findDevices();
				} else {
					alert('Error: Failed to unlock the Garmin Communicator plug-in.');
				}
			}
		});

		gpxListItem.bind('click', function()
		{
			//if we have a suggested tour ID, we need
			//to get a list of the ids in the sortable list
			//if Id string is null, we're being called from the
			//location list, not the info window
			if(idString == null)
			{

				idString = self.getLocationsFromSortableList(sortableId);
			}
			if(idString == "")
			{
				//we have no ids (an empty custom tour)
				return;
			}
			self.buildAndSubmitHiddenDownloadForm("GPX", idString, tourName,tourSize);
		});

		if (mpsListItem) {
			mpsListItem.bind('click', function()
			{
				if(idString == null)
				{

					idString = self.getLocationsFromSortableList(sortableId);
				}
				if(idString == "")
				{
					//we have no ids (an empty custom tour)
					return;
				}
				self.buildAndSubmitHiddenDownloadForm("MP3", idString, tourName, tourSize);

			});
		}

		textListItem.bind('click', function()
		{
			//if we have a suggested tour ID, we need
			//to get a list of the ids in the sortable list
			//if we are called from the location list, the
			//idString is null.
			if(idString == null)
			{
				idString = self.getLocationsFromSortableList(sortableId);
			}
			if(idString == "")
			{
				//we have no ids (an empty custom tour)
				return;
			}
			self.buildAndSubmitHiddenDownloadForm("Text", idString, tourName, tourSize);
		});

	};//end getDownloadSelectBox

	//this draws an add all/remove all button on the location list
	this.drawAddOrRemoveAllButton = function(newJSON, targetId, locationListId, sortableId)
	{
		var self = this;
		jQuery("#" + targetId).empty();
		var customTourArray = this.getCustomTours();
		var allInCustomTour = true;

		//first, we see if all our loactions are in the custom tours array or not
		jQuery.each(newJSON.markers, function(key, value)
		{
			//IE is saying we've got a null entry.
			//Firefox seems to be OK...weird
			if(key == null || value == null)
			{
				//alert("Key: " + key + " value: " + value);
				return true;
			}
			var inCustomTour = false;
			for(var index = 0; index < customTourArray.length; index++)
			{

				if(value.id == customTourArray[index])
				{
					inCustomTour = true;
					break;
				}
			} //end for
			if(inCustomTour == false)
			{
				allInCustomTour = false;
				return false;
			}
		});
		var buttonId = "addRemoveAllButtonFor" + sortableId;
		var button = jQuery("<img/>").attr("id", buttonId);
		var message = '';
		var spanId = "addRemoveSpanFor" + sortableId;
		var addRemoveSpan = jQuery("<span/>").attr("id", spanId);

		if(allInCustomTour == true)
		{
			button
			.attr("src", REMOVE_FROM_CUSTOM_TOUR_ICON)
			.attr("alt", "Remove");
			message = "Remove All from My Custom Tour";
		}
		else
		{
			button
			.attr("src", ADD_TO_CUSTOM_TOUR_ICON)
			.attr("alt", "Add");
			message = "Add All to My Custom Tour";
		}
		addRemoveSpan.empty().html(message);

		jQuery("#" + targetId).append(button).append(addRemoveSpan);

		//set the event
		//clear any previous event
		jQuery("#" + spanId + ", #" + buttonId).die("click").live("click", function()
		{

			var idArray = new Array();
			idArray = self.getLocationsFromSortableList(sortableId).split(',');
			if(idArray.length == 0)
			{
				//remove this button - there's nothing to add or remove
				//jQuery("#" + buttonId).parent().empty();
				return;
			}

			if(allInCustomTour)
			{
				for (var index = 0; index < idArray.length; index++)
				{
					self.removeFromCustomTour(idArray[index], true);
				}

				//change the add all to remove all
				jQuery("#" + buttonId).attr("src", ADD_TO_CUSTOM_TOUR_ICON).attr("alt", "Add");
				jQuery("#" + spanId).empty().html("Add All to My Custom Tour");
				jQuery("#" + sortableId).find(".tour").attr("src", ADD_TO_CUSTOM_TOUR_ICON);
				allInCustomTour = false;
			}
			else
			{
				if (typeof(_gaq) != "undefined" && _gaq.push) {
					var tourName =jQuery("#" + locationListId).parent().prev().text();
					_gaq.push(['_trackEvent', 'Tours', 'Add All to Custom Tour', tourName]);
				}
				self.addToCustomTour(idArray, true);
				jQuery("#" + buttonId).attr("src", REMOVE_FROM_CUSTOM_TOUR_ICON).attr("alt", "Remove");
				jQuery("#" + spanId).empty().html("Remove All from My Custom Tour");
				jQuery("#" + sortableId).find(".tour").attr("src", REMOVE_FROM_CUSTOM_TOUR_ICON);
				allInCustomTour = true;
			}

				self.updateMap(self.filteredJSON, false);
		});

	}
	/*
	this draws the options at the bottom of the location list
	*/
	this.drawToolBox = function(newJSON, locationListId, sortableId)
	{
		var self = this;


		var addRemoveListItem = jQuery("<li/>")
			.attr("id", "addRemoveFor" + locationListId);
		var printImage = jQuery("<img/>")
			.attr("src", "template/buttonPrint.png")
			.attr("alt", "Print");
		var printMyItinerary = jQuery("<li/>")
			.attr("id", "printItineraryFor" + locationListId)
			.addClass("printButton")
			.append(printImage).append("Print Itinerary");
		var download = jQuery("<li/>")
			.attr("id", "downloadFor" + locationListId);

		var list = jQuery("<ul/>")
			.addClass("toolList");
		list.append(addRemoveListItem)
			.append(printMyItinerary)
			.append(download);

		jQuery("#" + locationListId).append(list);

		//now that they're in the dom...
		this.drawAddOrRemoveAllButton(newJSON, "addRemoveFor" + locationListId, locationListId, sortableId);
		this.drawDownloadSelectBox("downloadFor" + locationListId, locationListId, null, sortableId);


		printMyItinerary.click(function()
		{
			open("./print.php?points=" + self.getLocationsFromSortableList(sortableId), "printItinerary");
		});
	}

	/**
	this checks the sortable list of locations and
	returns a string of comma delimited of location Ids.
	It returns an empty string if there are no locations
	**/
	this.getLocationsFromSortableList = function(listID)
	{
		var idString = "";
		if(jQuery("#" + listID).length != 0)
		{
			tempArray = new Array();
			jQuery("#" + listID).find("li").each(function()
			{
				tempArray[tempArray.length] = jQuery(this).attr("id").substring((listID + "_").length);
			});
			//replace the passed in string
			idString = tempArray.toString();
		}

		return idString;
	} //end getLocationsFromSortableList

	/**
	This builds a hidden form and submits the request
	for the download.
	**/
	this.buildAndSubmitHiddenDownloadForm = function(type, data, tourName, tourSize)
	{
		var self = this;

		//submit to an iframe so the google analytics
		//tracking requests are not interrupted.
		jQuery("#downloadIframe").remove();
		var iframe = jQuery("<iframe />")
			.css({border:'none',width:0,height:0})

			.attr('name', 'downloadIframe')
			.attr('id', 'downloadIframe');
		jQuery("body").append(iframe);
		jQuery("#downloadForm").remove();
		var newForm = jQuery("<form/>");
		newForm.attr("method", "get")
			.attr("action", DOWNLOAD)
			.attr('id', 'downloadForm');
		var inputType = jQuery("<input/>");
		jQuery(inputType).attr("type", "hidden").attr("name", "type").attr("value", type);
		jQuery(newForm).append(inputType);
		newForm.attr('target', 'downloadIframe');
		var inputData = jQuery("<input/>");
		jQuery(inputData).attr("type", "hidden").attr("name", "data").attr("value", data);
		jQuery(newForm).append(inputData);


		jQuery("body").append(newForm);

		if (typeof(_gaq) != "undefined" && _gaq.push) {
			var commands =[];
			if (tourName.length && tourSize > 0) {
				_gaq.push(['_trackEvent', 'Tours', 'Download ' + type, tourName, tourSize]);
			}

			var ids = data.toString().split(",");
			var pois = self.getJSONForLocationIds(ids);
			jQuery.each(pois.markers, function(key, value) {
				_gaq.push(['_trackEvent', 'POIs', 'Download ' + type, value.title]);
			});

		}
		 //setTimeout('jQuery("#downloadForm").submit()' , 200)
		newForm.trigger('submit');

	} //end buildAndSubmitHiddenDownloadForm


	this.getInfoWindowAudioButton = function(locationInfo)
	{
		var self = this;
		var audioIcon = jQuery("<img/>").attr("src", AUDIO_ICON).attr("alt", "Play audio");
		var audioDivId = "audioDiv" + locationInfo.id;
		var audioDiv = jQuery("<div/>").addClass("audioPlayer")
			.attr("href", locationInfo.audio)
			.append(audioIcon)
			.append("Play audio")
			.attr("id", audioDivId);

		jQuery("#" + audioDivId).die().live('click', function() {
			if (typeof(_gaq) != "undefined" && _gaq.push) {
				_gaq.push(['_trackEvent', 'POIs', 'Play Audio', locationInfo.title]);
			}
		});

		//the domready event fires twice for some reason so explicitly check whether or not tthe
		//player has been loaded yet.
		var loadedFlowPlayer = false;
		self.loadListener = google.maps.event.addListener(self.infoWindow, 'domready', function()
		{
			if (!loadedFlowPlayer) {
				self.setUpFlowPlayer("audioDiv" + locationInfo.id, jQuery.trim(locationInfo.audio));
				loadedFlowPlayer = true;
			}
		});

		return audioDiv;
	};

	/**
	this function returns the formatted
	html that is placed in the info window.
	locationInfo is an object with info about the location
	**/
	this.getInfoWindowMarkup = function(locationInfo, customTourButtonId)
	{
		var self = this;
		//we need a dummy wrapper because there is no
		//function to turn a DOM element into text, just innerhtml,
		//which sheds the outer tag.
		var wrapper = jQuery("<div/>");

		google.maps.event.clearListeners(self.infoWindow, 'domready');
		var markerIconsDiv = jQuery("<div/>").addClass("markerIcons")

		for(var index = 0; index < locationInfo.categories.length; index++)
		{
			var categoryId = locationInfo.categories[index];
			var markerIcon = jQuery("<img/>").attr("src", IMAGE_DIRECTORY
							+ self.JSONData.markerIcons["id" + categoryId].icon)
			.attr("title", self.JSONData.markerIcons["id" + categoryId].name)
			.attr("alt", self.JSONData.markerIcons["id" + categoryId].name)
			.attr("width", self.JSONData.markerIcons["id" + categoryId].iconWidth)
			.attr("height", self.JSONData.markerIcons["id" + categoryId].iconHeight);

			markerIconsDiv.append(markerIcon);
		}

		var title = jQuery("<h3/>").append(locationInfo.title);

		var infoWindowDiv = jQuery("<div/>").addClass("infowindow")
			.append("<input type=\"hidden\" id=\"infowindowLocationId\" value=\"" +  locationInfo.id + "\"/>")
			.append(markerIconsDiv)
			.append(title);

		var addressContainer = jQuery("<h4 />");
		var address = jQuery("<a />");
		addressContainer.append(address);


		address.append(locationInfo.streetAddress);
		if (address.text().length) {
			address.append(", ");
		}
		address.append(locationInfo.city);


		if(locationInfo.postalCode != "")
		{
			var postalSpan = jQuery("<span/>").append(locationInfo.postalCode).addClass("postalCode");
			if (address.text().length) {
				address.append(", ");
			}
			address.append(postalSpan);
		}
		if(locationInfo.phone != "")
		{
			var phoneSpan = jQuery("<span/>").append(locationInfo.phone).addClass("telephone");
			if (address.text().length) {
				addressContainer.append(", ");
			}
			addressContainer.append(phoneSpan);
		}



		if(locationInfo.streetAddress || locationInfo.phone)
		{
			infoWindowDiv.append(addressContainer);
			address.addClass("address");
			address.attr('href', 'http://maps.google.ca/maps?q=' +
			encodeURIComponent(address.text()) + "+(" + encodeURIComponent(title.text()) + ")+@" + locationInfo.latitude + ",+" +
				locationInfo.longitude).attr('target', '_blank');

		}


		var mediaDiv = jQuery("<div/>").addClass("poiMedia")
		if(locationInfo.photo != "")
		{
			var photo = jQuery("<img/>").attr("src", MEDIA_DIRECTORY + locationInfo.photo)
				.attr("alt", locationInfo.photoTitle)
				.attr("title", locationInfo.photoTitle)
				.addClass("photo");
			mediaDiv.append(photo);
		}
		if(locationInfo.audio != "")
		{
			mediaDiv.append(self.getInfoWindowAudioButton(locationInfo));
		}
		if (self.closeListener == null) {
			self.closeListener = google.maps.event.addListener(self.infoWindow, 'closeclick', function() {
				self.clearTrailHighlighting();
			});
		}
		var description = locationInfo.description;
		var websiteAnchor = jQuery("<a/>").attr("href", "http://" + locationInfo.website)
		.attr("target", "_blank").append(locationInfo.website);
		var websiteParagraph = jQuery("<p/>").append(websiteAnchor);
		var coordinates = jQuery("<p/>").append("GPS Coordinates: ")
		.append(locationInfo.latitude).append(", ").append(locationInfo.longitude);
		var detailsDiv = jQuery("<div/>").addClass("details");
		if (mediaDiv.html().length) {
			detailsDiv.append(mediaDiv);
		}

		detailsDiv.append(description);
		if (locationInfo.descriptionSource) {
			if (window.location.href.indexOf("/cms") != -1) {
				detailsDiv.append('<p class="source">Source: <a href="../pages/sources" target="_blank">' + locationInfo.descriptionSource + '</a></p>');
			} else {
				detailsDiv.append('<p class="source">Source: <a href="pages/sources">' + locationInfo.descriptionSource + '</a></p>');
			}
		}
		if(locationInfo.website != "")
		{
			detailsDiv.append(websiteParagraph);
		}
		detailsDiv.append(coordinates);

		var customTourLi = self.getCustomListItemForInfoWindow(locationInfo.id, customTourButtonId);

		var printImage = jQuery("<img/>").attr("src", "template/buttonPrint.png")
		.attr("alt", "Print");
		var printLi = jQuery("<li/>").append(printImage).append("Print").attr("id", "printFor" + locationInfo.id).addClass("printButton");

		jQuery("#printFor" + locationInfo.id).die("click").live("click", function()
		{
			open("./print.php?points=" + locationInfo.id, "printItinerary");
		});

		var downloadLi = jQuery("<li/>").attr("id", "infoWindowDownloadLI").addClass("downloadButton");

		var buttonBar = jQuery("<ul/>").addClass("buttonBar").append(customTourLi).append(printLi).append(downloadLi);



		infoWindowDiv.append(detailsDiv).append(buttonBar);

		wrapper.append(infoWindowDiv);

		return wrapper.html();

	}; //end getInfoWindowMarkup

	/*
	This is seperated out to allow easier inheritance in the CMS
	*/
	this.getCustomListItemForInfoWindow = function(locationId, customTourButtonId)
	{
		var self = this;
		var customTourIcon = jQuery("<img/>").attr("id", customTourButtonId);
		var textSpan = jQuery("<span/>").attr("id", "spanFor" + customTourButtonId);
		var customTourText;
		if(this.inCustomTour(locationId))
		{
			customTourIcon.attr("src", REMOVE_FROM_CUSTOM_TOUR_ICON)
			.attr("alt", "Remove from My Custom Tour");
			customTourText = "Remove from My Custom Tour";
		}
		else
		{
			customTourIcon.attr("src", ADD_TO_CUSTOM_TOUR_ICON)
			.attr("alt", "Add to My Custom Tour");
			customTourText = "Add to My Custom Tour";
		}
		textSpan.append(customTourText);
		var customTourLi = jQuery("<li/>").append(customTourIcon).append(textSpan)
		.attr("id", "liFor" + customTourButtonId);

		jQuery("#liFor" + customTourButtonId).die().live('click', function()
		{
			self.infoWindowCustomTourClick(locationId, customTourButtonId);
		});

		return customTourLi;
	}

	/**
	this is used for the click event in the info window that
	adds or removes a location for a custom tour.
	For suggested tours, it has to update the graphic in the
	info window and update the locations list, too.
	**/
	this.infoWindowCustomTourClick = function(id, targetId)
	{
		var self = this;
		var markerCategoryId = self.getMarkerCategoryId("id" + id);
		if (this.inCustomTour(id))
		{

			jQuery("#" + targetId).attr("src", ADD_TO_CUSTOM_TOUR_ICON).attr("alt", "Add");
			jQuery("#" + targetId).siblings().html("Add to My Custom Tour");
			//update the graphic in the location list
			jQuery("#customTourButton" + id).attr("src", ADD_TO_CUSTOM_TOUR_ICON).attr("alt", "Add");
			//if we have overlays, we need to remove the marker if it is removed from a custom tour that is not in
			//the filtered JSON (not in a suggested tour
			if(jQuery("#customTourTabCheckBox").is(':checked')
												   && !self.filteredJSON.markers.hasOwnProperty("id" + id))
			{
				self.markersArray["id" + id].setMap(null);
				self.infoWindow.close();
			}
			else
			{
				var icon = self.getMarkerIcon("regular", self.JSONData.markers["id" + id], markerCategoryId);
				self.markersArray["id" + id].setIcon(icon);
			}
			self.removeFromCustomTour(id, true);
		}
		else
		{
			self.addToCustomTour([id], true);
			jQuery("#" + targetId).attr("src", REMOVE_FROM_CUSTOM_TOUR_ICON).attr("alt", "Remove");
			jQuery("#" + targetId).siblings().html("Remove from My Custom Tour");
			jQuery("#customTourButton" + id).attr("src", REMOVE_FROM_CUSTOM_TOUR_ICON).attr("alt", "Remove");;
			var icon = self.getMarkerIcon("custom", self.JSONData.markers["id" + id], markerCategoryId);
			self.markersArray["id" + id].setIcon(icon);
		}

	}

	this.setUpFlowPlayer = function(audioDivId, audioFile)
	{

		if(audioFile == "")
		{
			return;
		}

		//set up the flow player
		return flowplayer("div.audioPlayer", { src:FLOW_PLAYER }, {
			key: FLOW_PLAYER_KEY,
			plugins:
			{
				controls:
					{
						url: 'template/flowplayer.controls-3.1.5.swf',
						fullscreen: false,
						background: '#eeeeee url(template/listHeaderBG.gif) repeat 0 0',
						buttonColor: '#494a4c',
						buttonOverColor: '#000000',
						mute: false,
						time: false,
						tooltipColor: '#4a8ac7',
						progressColor: '#91bce6',
						bufferColor: '#4a8ac7',
						sliderColor: '#494a4c',
						volumeSliderColor: '#494a4c',
						height: 24
					}
			},
			audio: {
			url: 'template/flowplayer.audio-3.1.2.swf'
			},
			clip: {
				autoPlay: true,
				autoBuffering:true,
				image:false,
				url: MEDIA_DIRECTORY + audioFile
				// optional: when playback starts close the first audio playback
				//onBeforeBegin: function() {
				//	jQueryf("player").close();
				//}
			}  //end clip
			//playlist: [{url: MEDIA_DIRECTORY + audioFile, image: false}]
		});
	}

	/**
	addToCustomTour
	takes an array of location Ids and adds them to the visitor's cookie
	of locations in their custom tour
	**/
	this.addToCustomTour = function(locationArray, updateCustomTabList)
	{
		if(this.getCustomTours().length == 0)
		{
			jQuery("#customTourTab").show();
		}

		var cookieArray;
		var cookieString;
		if (typeof(_gaq) != "undefined" && _gaq.push) {
			var pois = this.getJSONForLocationIds(locationArray);
			jQuery.each(pois.markers, function(key, value) {
				_gaq.push(['_trackEvent', 'POIs', 'Add to Custom Tour', value.title]);
			});
		}

		for(var index = 0; index < locationArray.length; index++)
		{
			cookieString = jQuery.cookie("customTours");

			if(cookieString == null || cookieString.length == 0)
			{
				//jQuery.cookie("customTours", locationArray[index], {expires: DAYS_TO_COOKIE_EXPIRE});
				cookieArray = locationArray[index];
			}
			else
			{
				cookieArray = cookieString.split(',');
				//splitting an empty string returns an array of length 1 with
				//an empty string in the first location, at least on firefox.
				if(cookieArray.length == 1 && cookieArray[0] == "")
				{
					cookieArray.length = 0;
				}

				//check to see if it is already in the custom tour
				var alreadyInTour = false;
				for(var i = 0; i < cookieArray.length; i++)
				{
					if(	cookieArray[i] == locationArray[index])
					{
						alreadyInTour = true;
					}
				}

				if(alreadyInTour == false)
				{
					cookieArray[cookieArray.length] = locationArray[index];

					//if the length is 1, it was 0, and we need to draw the custom tour tab
				}//end if already in tour is false
			} //end else
			//update the cookie.
			jQuery.cookie("customTours", cookieArray.toString(), {expires: DAYS_TO_COOKIE_EXPIRE});
			//update the icon
			this.updateMarkerFromLocationId(locationArray[index]);



		}//end for
		//update the tab list
		if(updateCustomTabList)
		{
			this.addToCustomTourTabList(locationArray);
		}



	}; //end addToCustomTour
	/**
	removeFromCustomTour removes a location for a
	visitor's custom tour list.
	**/
	this.removeFromCustomTour = function(locationId, updateCustomTourTabList)
	{
		var cookieString = jQuery.cookie("customTours");
		if(cookieString == null)
		{
			return;
		}
		var oldCookieArray = cookieString.split(',');
		var newCookieArray = new Array();

		 var newIndex = 0;
		//only add the locations if they are not the location we are removing
		for(var index = 0; index < oldCookieArray.length; index++)
		{
			if(oldCookieArray[index] != locationId && oldCookieArray[index] != null)
			{
				newCookieArray[newIndex] = oldCookieArray[index];
				newIndex++;
			}
		}
		jQuery.cookie("customTours", newCookieArray.toString(), {expires: DAYS_TO_COOKIE_EXPIRE});

		//update the map
		locationId = locationId.toString().replace(/\D/g, '');
		this.updateMarkerFromLocationId(locationId);
		//remove from list on tab
		if(updateCustomTourTabList)
		{
			this.removeFromCustomTourTab(locationId);


		}


	};//end removeFromCustomTour


	/**
	getCustomTours looks through the cookie to get a list of
	location IDs that are in a visitor's custom tours.
	returns an array.
	**/
	this.getCustomTours = function()
	{
		var self = this;
		try
		{
			if(jQuery.cookie("customTours") != null && jQuery.cookie("customTours") != "" )
			{
				var cookieArray = jQuery.cookie("customTours").split(',');
				for(var index = 0; index < cookieArray.length; index ++)
				{
					if(cookieArray[index] == '' ||
						!self.JSONData.markers.hasOwnProperty("id" + cookieArray[index]))
					{
						//this resolves an issue with firefox returning and empty first cookie
						cookieArray.splice(index, 1);
					}

				}
				return cookieArray;
			}
			else
			{
				return new Array();
			}
		}
		catch(message)
		{
			if (window.console && console.log) {
				console.log(message);
			}
		}

	};//end getCustomTours

	/**
	this function answers the question:
	am I in a custom tour?
	**/
	this.inCustomTour = function(locationId)
	{

		var tourArray = this.getCustomTours();
		for(var index = 0; index < tourArray.length; index ++)
		{
			if(tourArray[index] == locationId)
			{
				return true;
			}
		}

	}


	/**
	This creates the html for the location list in the control panel
	This takes a JSON object as its input and the id of the div
	in which we will draw the list
	the type allows us to customize the classes and attributes of the list for
	the seach list, custom tour, and suggested tour windows.  The markup is almost
	identical, so rather than having a lot of duplication in three different functions,
	we pass in a type.
	**/
	this.drawLocationList = function(JSONData, targetId, tourId, listId, type)
	{
		var self = this;
		//clear the target Id
		//jQuery("#" + targetId).empty();

		//create an unordered list so that we can sort it using
		//jquery's 'sortable'
		var ulClass;
		switch(type)
		{
			case "custom":
			case "search":
				ulClass = "searchResults";
				break;
			case "suggested":
				ulClass = "tourList";
			break;

			default:
				if (window.console && console.log) {
					console.log("unknown type sent into drawLocationList!");
				}

				return;
		}

		if(type == "search")
		{
			var emTag = jQuery("<em/>").append("Search results:");
			var strongTag = jQuery("<strong/>").append(emTag);
			var paragraph = jQuery("<p/>").append(strongTag);
			jQuery("#" + targetId).append(paragraph);
		}

		var sortableList = jQuery("<ul/>").addClass(ulClass);
		jQuery(sortableList).attr("id", listId);
			//append our list to the div
		jQuery('#' + targetId).append(sortableList);
		var odd = true;

		jQuery.each(JSONData.markers, function(key, value)
		{

			if (value == null)
			{
				//this is the same as continue;
				return true;
			}
			var rowClass;
			if(odd == true)
			{
				rowClass = "odd";
				odd = false;
			}
			else
			{
				rowClass = "even";
				odd = true;
			}
			var id = value.id;
			var customTourButtonId = "customTourButton" + id;
			var tourStop = jQuery("<li/>").addClass(rowClass);
			jQuery(tourStop).attr("id",listId + "_" + id);

			var markerCategoryId = self.getMarkerCategoryId("id" + value.id);

			var icon = jQuery("<img/>");
			jQuery(icon).attr("src", IMAGE_DIRECTORY
							  + self.JSONData.markerIcons["id" + markerCategoryId].icon);
			jQuery(icon)
				.attr("id", "locationIcon" + id)
				.attr("alt", "icon");
			var nameSpan = jQuery("<span/>").append(icon);
			var textLabel = jQuery("<label/>")
				.attr("for", "locationIcon" + id)
				.append(value.title);
			jQuery(nameSpan).append(textLabel)
				.attr("title", "View on map")
				.attr("id", "nameSpan" + id);


			var customTourIcon = jQuery("<img/>").addClass("tour");
			if(self.inCustomTour(id))
			{
				//alert("we're in a custom tour. id: " + id);
				customTourIcon.attr("src", REMOVE_FROM_CUSTOM_TOUR_ICON)
					.attr("title", "Remove from My Custom Tour")
					.attr("alt", "Remove");
			}
			else
			{
				//alert("were not in a custom tour. id: " + id);
				customTourIcon.attr("src", ADD_TO_CUSTOM_TOUR_ICON)
					.attr("title", "Add to My Custom Tour")
					.attr("alt", "Add");
			}
			customTourIcon.attr("id", customTourButtonId)
				.attr("width", SMALL_ICON_WIDTH)
				.attr("height", SMALL_ICON_HEIGHT);

			tourStop.append(customTourIcon)
				.append(nameSpan);
			sortableList.append(tourStop);

			//add the click event to the add to custom tour icon
			//jQuery('#' + customTourButtonId).die("click").live("click", function()
			customTourIcon.click(function()
			{
				self.locationListCustomTourClick(id, targetId, tourId, listId, JSONData);

			}); //end add to click

			//add the click event to the location icon on the side of the screen (if it exists)
			//jQuery("#nameSpan" + id).die('click').live('click', function()
			nameSpan.click(function()
			{
				//bring the marker to the foreground in case it is buried.
				self.markersArray["id" + id].setZIndex(++maxMarkerIndexSofar);
				self.markerClickFunction(self.markersArray["id" + id], value, "customTourButtonForInfoWindow" + id);
			});

		}); //end loop through markera



		//set our list sortable
		if(type == "custom")
		{
			jQuery('#' + listId).sortable(
			{
				update: function(event, ui)
				{
					self.sortEvent(event, ui, jQuery(this).attr('id'), self, listId);
				}
			});
		}


	} //drawLocationList

	/*
	this is passed in to drawLocationList
	so that the function can have different
	click events for the add/remove from custom tour buttons
	in the suggested and custom tours objects.
	*/
	this.locationListCustomTourClick = function(locationId, locationListDivId, tourId, ULId, JSONData)
	{
		if(this.inCustomTour(locationId))
		{
			this.removeFromCustomTour(locationId, true);
			jQuery("#customTourButton" + locationId + ", #customTourButtonForInfoWindow" + locationId).attr("src", ADD_TO_CUSTOM_TOUR_ICON)
			.attr("title", "Add to My Custom Tour").attr("alt", "Add");
			//update the text in the window, too.
			jQuery("#spanForcustomTourButtonForInfoWindow" + locationId).empty().append("Add to My Custom Tour");
			//update the add/remove all button if we're in the suggested tours page
			if(tourId != null)
			{
				this.drawAddOrRemoveAllButton(JSONData, "addRemoveFor" + locationListDivId, "suggested" + tourId, ULId)
			}
		}
		else
		{
			this.addToCustomTour([locationId], true);
			jQuery("#customTourButton" + locationId + ", #customTourButtonForInfoWindow" + locationId).attr("src", REMOVE_FROM_CUSTOM_TOUR_ICON)
			.attr("title", "Remove from My Custom Tour").attr("alt", "Remove");
			jQuery("#spanForcustomTourButtonForInfoWindow" + locationId).empty().append("Remove From My Custom Tour");
			//update the add/remove all button
			if(tourId != null)
			{
				this.drawAddOrRemoveAllButton(JSONData, "addRemoveFor" + locationListDivId, "suggested" + tourId, ULId)
			}
		}

	}

	/**
	cleanUp() needs to be called to remove
	all markers from the map
	before destroying the onject
	**/
	this.cleanUp = function()
	{
		var self = this;
		jQuery.each(self.markersArray, function(key, value)
		{
			self.markersArray[key].setMap(null);
			self.hideTrail(self.JSONData.markers[key]);
		});

		this.infoWindow.close();
		//clear an clusters from the map
		if(this.flusterObject != null)
		{
			this.flusterObject.clearMarkers();
		}




		jQuery("#" + self.controlPanelId).empty();
		jQuery(".dropDownMenu").remove();
	}

	/*
	instantiate this object and pass it into the
	garmin communicator register function.
	It manages all the callbacks that the garmin control
	calls when it finishes steps along the way
	*/
	this.garminListener = function(locationsString)
	{
		//these have to be global to the object
		//because the callback from the communicator plugin
		//needs access to this info to know if another
		//set of data needs to be transfered and
		//what that data is.
		this.currentIndex = 0;
		this.returnedJSON;
		this.json;
		var self = this;

		//this is called after instantiating the
		//garmin communicator object and it has found
		//its devices.  It may have found zero devices.
		this.onFinishFindDevices = function(json)
		{
			self.json = json;
			if(json.controller.getDevicesCount() == 0)
			{
				alert("No Garmin devices found!");
				return;
			}
			var devices = json.controller.getDevices();

			//we call ajax to get our gpi file created
			jQuery.post(AJAX_FILE, {"request":"getGPIFileName", "text":locationsString}, function(gpxJSON)
			{
				try
				{
					self.returnedJSON = eval("(" + gpxJSON + ")");
				}
				catch(message)
				{
					if (window.console && console.log) {
						console.log(message);
					}
					return;
				}
				/**
				By looping through all the devics, this code allows us to to transfer
				the data to all GPS devices attached to the computer.
				This is unlikely, but it is there.
				**/

				for (var i = 0; i < devices.length; i++)
				{
					var response = confirm("Are you sure that you want to copy your tour to the "
										   + devices[i].getDisplayName() + "?");
					if (response == true)
					{
						//check to see if the device supports writes
						if(json.controller.checkDeviceWriteSupport(Garmin.DeviceControl.FILE_TYPES.binary) == false
							|| json.controller.bytesAvailable("Garmin/DATA/") == -1)
						{
							var response = confirm("Your Garmin device reports that it does not support writing binary files."
												   + " Would you like to transfer the waypoint information"
												   + " without the images and audio?");
							if(response == true)
							{
								self.transferJustGPXData(locationsString);
								continue;

							}
							else
							{
								continue;
							}

						}
						//this array consists of pairs of sources (http://...) and destinations ("Garmin/....)
						//there can be as many pairs as we have files to transfer.  I.e. array index 1, 3, 5, 7, etc
						//must be sources and 2, 4, 6, 8, etc.. must be destinations.  Except, anything more than
						//three entries seems to crash it.

						//****note****: the communicator plugin crashes if it is passed a query string.
						//we used mod_rewrite to forward the url here to garmin.php with the locationString
						//changed to garmin.php?ids = locationString
						//add the gpx file to the transfer

						var infoArray = new Array();
						infoArray = [POI_FOLDER + locationsString, "Garmin/POI/" + self.returnedJSON.fileName];
						var xmlDescription = Garmin.GpiUtil.buildMultipleDeviceDownloadsXML(infoArray);
						try
						{
							self.json.controller.downloadToDevice(xmlDescription);
						}
						catch(message)
						{
							alert("we failed to download to device: " + message);
						}
					} //end if true
				} //end for

			});
		}
		/*
		The garmin plugin crashes if there are more than three entries in the
		array, se we send the data in chunck of 2 or 3.
		As the callback fires at the completion of a transfer, this is called again
		if there is more data to send.
		*/
		this.sendNextChunk = function()
		{
			var infoArray = new Array();
			if(self.currentIndex < self.returnedJSON.idImageAndAudio.length)
			{
				//this array consists of pairs of sources (http://...) and destinations ("Garmin/....)
				//there can be as many pairs as we have files to transfer.  I.e. array index 1, 3, 5, 7, etc
				//must be sources and 2, 4, 6, 8, etc.. must be destinations.  Except, anything more than
				//three entries seems to crash it. Whoever wrote this thing should be ashamed.


				//****note****: the communicator plugin crashes if it is passed a query string.
				//we used mod_rewrite to forward the url here to garmin.php with the locationString
				//changed to garmin.php?ids = locationString
				//add the gpx file to the transfer
				//alert(window.location.hostname);

				//we copy the gpx file on the first loop.
				if(this.currentIndex == 0)
				{
					infoArray = [GARMIN_FOLDER + locationsString, "Garmin/GPX/" + self.returnedJSON.fileName];
				}

				//we copy over all the images that are referenced by the gpx file, too
				//note: if any of the filenames are the same in a single transfer, the transfer fails.  gpxJSON.idImageAndAudio.length

				//copy the image over
				if(self.returnedJSON.idImageAndAudio[self.currentIndex].image != null
					&& jQuery.trim(self.returnedJSON.idImageAndAudio[self.currentIndex].image) != "")
				{
					infoArray[infoArray.length] = MEDIA_DIRECTORY + jQuery.trim(self.returnedJSON.idImageAndAudio[self.currentIndex].image);
					infoArray[infoArray.length] = "Garmin/DATA/" + self.returnedJSON.idImageAndAudio[self.currentIndex].id
												+ '/' + jQuery.trim(self.returnedJSON.idImageAndAudio[self.currentIndex].image);
				}
				if(self.returnedJSON.idImageAndAudio[self.currentIndex].audio != null
					&& jQuery.trim(self.returnedJSON.idImageAndAudio[self.currentIndex].audio) != "")
				{
					//copy the audio file over
					infoArray[infoArray.length] = MEDIA_DIRECTORY + jQuery.trim(self.returnedJSON.idImageAndAudio[self.currentIndex].audio);
					infoArray[infoArray.length] = "Garmin/DATA/" + self.returnedJSON.idImageAndAudio[self.currentIndex].id
													+ '/TourGuide_' + jQuery.trim(self.returnedJSON.idImageAndAudio[self.currentIndex].audio);
				}
				self.currentIndex++;
			}


			//build the xml that tells the "download to device" function what to send and where to put it.
			var xmlDescription = Garmin.GpiUtil.buildMultipleDeviceDownloadsXML(infoArray);
			//send the files!
			try
			{
				this.json.controller.downloadToDevice(xmlDescription);
			}
			catch(message)
			{
				alert("we failed to download to device: " + message);
			}
		} //end send next chunck

		this.onFinishReadFromDevice = function(json)
		{
			//alert("onFinishedReadFromDevice");
			//alert(json.controller.gpsDataString);
		}
		this.onStartReadFromDevice = function()
		{
			//alert("onStartReadFromDevice: " + self.json.controller.getDeviceStatus());
		}
		this.onStartWriteToDevice = function()
		{
			//alert("onStartWriteToDevice: " + self.json.controller.getDeviceStatus());
		}
		this.onFinishWriteToDevice = function(json)
		{

			if(json.controller.getDeviceStatus() == "Transfer Complete")
			{
				alert(json.controller.getDeviceStatus());
				//delete the POI from the web server with an ajax call.
				/*

				jQuery.post(AJAX_FILE, {"request":"deleteGPI", "text":this.returnedJSON.URL}, function(gpxJSON)
				{
					//we've cleaned up.
				});
				*/
			}
			else
			{
				alert("transfer failed!");
			}

		}

		this.onWaitingWriteToDevice = function()
		{
			//alert("still Waiting");

			alert("waiting: " + self.json.controller.getDeviceStatus());
		}

		
		/*
		this.onException = function(data) {
		
			alert("An error occured while syncing to the device. " + data.msg);
		}
		*/
		
		/*
		This function transfers just the GPX data to the Garmin device
		when the device does not support binary transfers (not a mass storage unit)
		*/
		this.transferJustGPXData = function(locationsString)
		{

			jQuery.post(AJAX_FILE, {"request":"gpx", "text":locationsString}, function(gpxString)
			{
				try
				{//self.returnedJSON.fileName
					self.json.controller.writeDataToDevice(Garmin.DeviceControl.FILE_TYPES.gpx, gpxString, self.returnedJSON.fileName + ".gpx");
				}
				catch(message)
				{
					alert("write data to device failed: " + message);
				}
			});


		}
	} //end Garmin listener

	/**
	This takes a boolean value, sets the overlay
	flag, and updates each of the locations in the custom tour
	**/
	this.setOverlayCustomTourOnMap = function(bool)
	{
		if(typeof(bool) != "boolean")
		{
			if (window.console && console.log) {
					console.log("bad input to setOverlayCustomTourOnMap!");
			}

			return;
		}
		this.overlayCustomTourOnMap = bool;
		var customToursArray = this.getCustomTours();
		for(var index = 0; index < customToursArray.length; index++)
		{
			this.updateMarkerFromLocationId(customToursArray[index]);
		}//end for
	} //end setOverlayCustomTourOnMap

		/**
	Takes an array of location IDs and some JSON and
	returns a JSON structure that includes only the
	JSON for the location IDs tour
	*/
	this.getJSONForLocationIds = function(locationIDs)
	{
		var newJSON = new Object();
		newJSON.markers = new Object();
		for(var index = 0; index < locationIDs.length; index++)
		{
			//check for ids that aren't in the database anymore, but might
			//still be in the cookie (or just being asked for because of a mistake somewhere)
			if(this.JSONData.markers.hasOwnProperty("id" + locationIDs[index]))
			{
				newJSON.markers["id" + locationIDs[index]] = this.JSONData.markers["id" + locationIDs[index]];
			}

		}
		return newJSON;
	};
	/*
	This method updates the tab that controls the custom tours.
	*/
	this.addToCustomTourTabList = function(locationArray)
	{
		var customToursArray = this.getCustomTours();
		var customToursJSON = this.getJSONForLocationIds(customToursArray);

		var newLocations = this.getJSONForLocationIds(locationArray);


		//remove the new look from all those alreay in the list
		jQuery("#customTourTabLocationsList").find("li").each(function(index, element)
		{
			jQuery(element).removeClass("new");

		});

		var newTop;
		var currentTop =parseInt(jQuery("#customTourTabLocationsList").css("top"));
		var changePerTimeout = 1;//Math.abs(currentTop - newTop) / 10;
		var timerFunction = function()
		{
			if(currentTop > newTop)
			{
				currentTop -= changePerTimeout;
			}
			else if(currentTop < newTop)
			{
				currentTop += changePerTimeout;
			}
			if(Math.abs(currentTop - newTop) < changePerTimeout)
			{
				currentTop = newTop;
			}
			if(currentTop == newTop)
			{
				jQuery("#customTourTabLocationsList").css("top", currentTop + "px");
				jQuery("#customTourTabLocationsList").find("li:hidden").eq(0).fadeIn("slow");
				if(jQuery("#customTourTabLocationsList").find("li:hidden").size() != 0)
				{
					newTop = -1 * (jQuery("#customTourTabLocationsList").find("li:visible").eq(-1).position().top
					+ jQuery("#customTourTabLocationsList").find("li:visible").eq(-1).height()
					-  (jQuery("#customTourTabLocationsDiv").height() / 2)
					+ parseInt(jQuery("#customTourTabLocationsDiv").css('line-height')));
					window.setTimeout(timerFunction, 15);
				}


			} //end if currentTop == newTop
			else
			{
				jQuery("#customTourTabLocationsList").css("top", currentTop + "px");
				window.setTimeout(timerFunction, 15);
			}

		} //end timer function


		//add the new markers with height and width zero so they don't display
		//if you just hide them, you can't get their position.
		jQuery.each(newLocations.markers, function(key, value)
		{
			//make sure that the entry isn't already there.
			if(jQuery("#customTourListItem" + value.id).size() != 0)
			{
				return true; //continue
			}

			var listItem = jQuery("<li/>")
			.attr("id", "customTourListItem" + value.id).hide().append(value.title).addClass("new"); //.css('display', 'none')
			jQuery("#customTourTabLocationsList").append(listItem);
			//get the new top position of the list
		});

		if(jQuery("#customTourTabLocationsList").find("li").size() != 1 || jQuery.browser.msie == false)
		{
			//jQuery("#customTourTabLocationsList").css("top", newTop + "px");
			//jQuery("#customTourTabLocationsList").find("li").fadeIn();

			//we have to calculate the new top based on the position of the last visible li because there is no way
			//to set the visibility to none and also get an li's  position.
			if(jQuery("#customTourTabLocationsList").find("li:visible").size() != 0)
			{
				newTop = -1 * (jQuery("#customTourTabLocationsList").find("li:visible").eq(-1).position().top
				+ jQuery("#customTourTabLocationsList").find("li:visible").eq(-1).height()
				-  (jQuery("#customTourTabLocationsDiv").height() / 2)
				+ parseInt(jQuery("#customTourTabLocationsDiv").css('line-height')));
				currentTop = parseInt(jQuery("#customTourTabLocationsList").css("top"));

			}//end if there are list elements
			else //we have no previous list element, so we place it in the middle of the div
			{
				newTop = -1 *(jQuery("#customTourTabLocationsDiv").height() / 2)
				+ parseInt(jQuery("#customTourTabLocationsDiv").css('line-height'));
			}

			window.setTimeout(timerFunction, 15);

		}
		else
		{
			jQuery("#customTourTabLocationsList").find("li").fadeIn();
		}
	}//end addToCustomTourTabList()

	this.removeFromCustomTourTab = function(locationId)
	{
	var self = this;
		var position = jQuery("#customTourListItem" + locationId).position();

		if(position == null)
		{
			//this can happen if a user gets click happy (whan I get click happy?)
			return;
		}

		//remove the new look from all those alreay in the list
		jQuery("#customTourTabLocationsList").find("li").each(function(index, element)
		{
			jQuery(element).removeClass("new");

		});


		//new top is the position that places the item to be removed in the centre of the screen.
		var newTop = -1 * (position.top
			-  (jQuery("#customTourTabLocationsDiv").height() / 2)
			+ parseInt(jQuery("#customTourTabLocationsDiv").css('line-height')));


		var currentTop =parseInt(jQuery("#customTourTabLocationsList").css("top"));
		var changePerTimeout = 1;//Math.abs(currentTop - newTop) / 10;
		//alert("newTop:" + newTop + " currentTop:" + currentTop);
		var remove = true;
		var timer;

		var timerFunction = function()
		{
			if(currentTop > newTop)
			{
				currentTop -= changePerTimeout;
			}
			else if(currentTop < newTop)
			{
				currentTop += changePerTimeout;
			}
			if(Math.abs(currentTop - newTop) < changePerTimeout)
			{
				currentTop = newTop;
			}
			if(currentTop == newTop)
			{

				if(remove == true)
				{
					jQuery("#customTourListItem" + locationId).hide(800, function()
					{
						jQuery("#customTourListItem" + locationId).remove();
						if(jQuery("#customTourTabLocationsList").find("li").size() == 0)
						{
							newTop = 0;
							jQuery("#customTourTabLocationsList").css("top", newTop + "px");
						}
						else
						{
							jQuery("#customTourTabLocationsList").css("top", newTop + "px");
							if(jQuery("#customTourTabLocationsList").find("li:last").position().top <
								Math.abs(parseInt(jQuery("#customTourTabLocationsList").css("top"))))
							{

								//alert("moving.  oldNewTop = " + newTop);
								remove = false;
								currentTop = parseInt(jQuery("#customTourTabLocationsList").css("top"));
								newTop = -1 * (jQuery("#customTourTabLocationsList").find("li:last").position().top
								- (jQuery("#customTourTabLocationsDiv").height() / 2)
								+ parseInt(jQuery("#customTourTabLocationsDiv").css('line-height')));
								//changePerTimeout = Math.abs(currentTop - newTop) / 10;
								//alert("newTop is now" + newTop);
								window.setTimeout(timerFunction, 15);


							}

						}//end else
						if (self.getCustomTours().length ==0) {
							jQuery("#customTourTab").hide();
						}
					}); //end callback function
				}//end if remove
				else
				{
					jQuery("#customTourTabLocationsList").css("top", newTop + "px");
				}

			} //end if currentTop == newTop
			else
			{
				jQuery("#customTourTabLocationsList").css("top", currentTop + "px");
				window.setTimeout(timerFunction, 15);
			}
		}
		remove = true;
	    timer = window.setTimeout(timerFunction, 15);

	}

	/*
	This method draws the tab that controls the custom tours.
	It is called from the draw function of the suggested tours object
	and from the updateControlPanelTab

	*/
	this.initCustomTourTab = function()
	{
		var self = this;
		var customToursArray = self.getCustomTours();
		var customToursJSON = self.getJSONForLocationIds(customToursArray);

		//set the click event on the list
		jQuery("#customTourTabLocationsDiv").unbind('click').click(function()
		{
			switchToCustom();
		});

		if(customToursArray.length != 0)
		{
			var checkBox = jQuery("<input/>")
				.attr("type", "checkbox")
				.attr("id", "customTourTabCheckBox");
			if(jQuery("#customTourTabCheckBox").is(":checked"))
			{
					self.overlayCustomTourOnMap = true;
			}

			jQuery("#customTourTabLocationsList").empty();
			//populate the locations in the list if it is empty (the first time we're run.
			//	if(jQuery("#customTourTabLocationsList").find("li").eq(0).text() == "uninitialized")
			if(jQuery("#customTourTabLocationsList").find("li").children().length == 0)
			{
				jQuery("#customTourTabLocationsList").find("li").remove();
				jQuery.each(customToursJSON.markers, function(key, value)
				{
					if(key == null || value == null)
					{
						return true;
					}
					var listItem = jQuery("<li/>").removeClass("new")
					.attr("id", "customTourListItem" + value.id)
					.append(value.title);
					jQuery("#customTourTabLocationsList").append(listItem);
				});
			}

		}
		else
		{
			//hide it
		}
		//we have to use a click event because a change event isn't fired on the
		//first click in IE
		jQuery("#customTourTabCheckBox").unbind('click').bind('click', function()
		{
			if(jQuery("#customTourTabCheckBox").is(':checked'))
			{
				self.setOverlayCustomTourOnMap(true);
			}
			else
			{
				self.setOverlayCustomTourOnMap(false);
			}
		});

	} //end initCustomTourTab


	this.setSearchEvents = function(searchTabId, locationsDivId)
	{
		var self = this;
		//clear the search text
		jQuery("#searchBox").attr("value", "");
		jQuery("#clearSearch").removeClass("show");

		//we need to keep track of how many
		//characters have been pressed in order to
		//populate the results div every 3 characters

		jQuery("#searchBox").unbind();
		jQuery("#searchBox").keyup(function(eventObject)
		{
			self.searchKeyUpHandler(jQuery(this).attr("value"), eventObject, locationsDivId);
		});

		jQuery("#searchBox").one('keydown', function()
		{
			jQuery("#clearSearch").addClass("show");
		});

		jQuery("#clearSearch").unbind().click(function()
		{
			jQuery("#searchBox").attr("value", "");
			var newJSON = new Object();
			newJSON.markers = new Object();
			self.applySearch(newJSON, locationsDivId);
			jQuery("#clearSearch").removeClass("show");

			jQuery("#searchBox").one('keydown', function()
			{
				jQuery("#clearSearch").addClass("show");
			});

		});

		jQuery("#searchBox").focus(function()
		{
			if(jQuery(this).attr("value") == "search...")
			{
				jQuery("#searchBox").attr("value", "");
			}
			jQuery(this).select();
		});

	} //end setSearchEvents
	/*
	This is the search keyup event handler.  It needs to have a name so that we
	can keep track of how many times each key has been pressed
	*/
	this.searchKeyUpHandler = function(searchText, eventObject, locationsDivId)
	{

		if(this.searchKeyUpHandler.charactersTyped == null)
		{
			this.searchKeyUpHandler.charactersTyped = 0;
		}
		this.searchKeyUpHandler.charactersTyped= searchText.length;
		var self = this;
		var ENTER_KEY_CODE = 13;

		if(searchText != null
		   && searchText != ""
		   && eventObject != null
		   && eventObject.which != null
		   && eventObject.which != ENTER_KEY_CODE)
		{
			//charactersTyped %= 3;
			if(this.searchKeyUpHandler.charactersTyped > 2)
			{
				this.performSearch(searchText, locationsDivId);
				//alert("executing search");
				//self.executeSearch(jQuery(this).attr("value"), locationsDivId);
			}
		}//end if value is not nothing and not enter
		//if the enter key is pressed, we search.
		else if(searchText == "")
		{
			this.searchKeyUpHandler.charactersTyped = 0;
			var newJSON = new Object();
			newJSON.markers = new Object();
			self.applySearch(newJSON, locationsDivId);
		}
		else if(eventObject.which == ENTER_KEY_CODE)
		{
			this.performSearch(searchText,locationsDivId);
		//self.executeSearch(jQuery(this).attr("value"), locationsDivId);
		}



	}

	this.performSearch = function (searchText, locationsDivId) {
		if(searchText == null)
		{
			return;
		}
		var self =this;
		jQuery.post(
			AJAX_FILE,
				{
					"request":"search",
					"text":searchText},
				function(newJSON)
				{

					try
					{
						newJSON = eval("(" + newJSON + ")");
						self.applySearch(newJSON, locationsDivId);
					}
					catch(message)
					{
						if (window.console && console.log) {
							console.log(message);
						}
						//alert(message);
						//alert("eval or apply failed");
					}
		});
	}

	/**
	This function actually executes the search and
	updates the location list and map
	*/
	this.applySearch = function(newJSON, locationsDivId)
	{		//set up the tabs

		jQuery(".tab").parent().removeClass("selected");
		jQuery("#searchTab").addClass("selected");

		jQuery('#documentation').css('display','none');

		//set the title
		document.title = "Trip-Click: Search the Map";
		//update the instructions
		jQuery("#instructions")
			.empty()
			.append("Search the map to find specific points of interest.");

		if(jQuery("#searchBox").attr("value") == "")
		{
			newJSON = new Object();
			newJSON.markers = new Object();
		}

		jQuery("#" +this.controlPanelId).empty();
		this.drawLocationList(newJSON, locationsDivId, null, "Search", "search");
		this.updateMap(newJSON,true);



	}

	this.getType = function()
	{
		return "mapControlBase";
	}
		/**
	This function actually executes the search and
	updates the location list and map
	*/
/*	this.executeSearch = function(searchtext, locationsDivId)
	{
			alert("executeSearch");
			//set up the tabs
			jQuery("#suggestedToursTab").removeClass("selected");
			jQuery("#allPOIsTab").removeClass("selected");
			jQuery("#searchTab").addClass("selected");
			jQuery("#customTourTab").removeClass("selected");
			//set the title
			document.title = "Trip-Click: Search the Map";
			//update the instructions
			jQuery("#instructions").empty()
			.append("Search the map to find specific points of interest.");

			//add the text "Search results:" to the location list


			var newJSON;
			newJSON = this.filterJSONForSearch(searchtext);
			this.drawLocationList(newJSON, locationsDivId, null, "Search", "search");
			this.updateMap(newJSON);
			//clear the suggestions box
			//jQuery("#" + suggestionsId).addClass("hidden");
	} */
		/*
	This takes  a string and returns
	a filtered version of the json that only
	contains those markers that have the search
	term in their title or description.
	*/
	this.filterJSONForSearch = function(filterString)
	{
		var self = this;
		var newJSON = new Object()
		newJSON.markers = new Object();
		//var expressionString = "\\b(" + filterString + ")\\b";
		var expressionString = "(" + filterString + ")";
	 	var regularExpression = new RegExp(expressionString, "im");

	 	jQuery.each(self.JSONData.markers, function(key, value)
		{
			if(regularExpression.test(value.title))
			{
				newJSON.markers[key] = self.JSONData.markers[key];
			}
			else if(regularExpression.test(value.description))
			{
				newJSON.markers[key] = self.JSONData.markers[key];
			}
		});

		return newJSON;


	} //end filterJSON

	/*
	This updates a marker based on its location Id
	I created this to minimize the need to redraw the map.
	This improves the user experience because there are fewer map redraws, and
	It also reduces code complexity because we now have two "update map" functions
	and we would need to keep track of which one we need to call. It also encapsulates a lot
	of complexity, keeping it out of the rest of the code.
	*/
	this.updateMarkerFromLocationId = function(locationId)
	{
		//ie adds a null entry to the end of arrays
		if(locationId == null)
		{
			return;
		}
		var self = this;
		var markerCategoryId = self.getMarkerCategoryId("id" + locationId);
		//get the JSON for this location
		var JSONForId = self.getJSONForLocationIds([locationId])
		if(!JSONForId.markers.hasOwnProperty("id" + locationId))
		{
			if (window.console && console.log) {
				console.log("updateMarkerFromLocationId has been asked for a non-existent ID!");
			}

			return;
		}
		if(jQuery(JSONForId.markers).size() != 1)
		{
			if (window.console && console.log) {
				console.log("more than one marker!");
			}

		}

		//next, get our marker from the array of markers
		var marker;
		if(self.markersArray.hasOwnProperty("id" + locationId))
		{
			marker = self.markersArray["id" + locationId];
		}
		else
		{
			var markerJSON = JSONForId.markers["id" + locationId];
			//if we're here, we are adding a marker via the overlay custom tours
			//when the tour is not in the filtered list

			marker = self.newMarker(self.getMarkerIcon('customSemi', markerJSON, markerCategoryId),
								 markerJSON, true);
		}

		//we figure out what this location's icon should be
		var icon;
		if(self.inCustomTour(locationId)
			&& self.filteredJSON.markers.hasOwnProperty("id" + locationId))
		{
			icon = self.getMarkerIcon('custom', self.filteredJSON.markers["id" + locationId], markerCategoryId);
		}
		else if(self.inCustomTour(locationId)
			&& self.filteredJSON.markers.hasOwnProperty("id" + locationId) == false)
		{
			icon = self.getMarkerIcon('customSemi', self.JSONData.markers["id" + locationId], markerCategoryId);
		}
		else
		{
			icon = self.getMarkerIcon('regular', self.JSONData.markers["id" + locationId], markerCategoryId);
		}
		//set the marker's icon
		marker.setIcon(icon);

		//check to see if this should now be or not be on the map
		//beacause of the custom tour overlay.  This is more complext than it sounds.

		//if the overlay is on and we're in a custom tour,
		//we need to show the marker.
		if(self.overlayCustomTourOnMap && self.inCustomTour(locationId))
		{
			//we need to remove the marker from its cluister or
			//the cluster will remove the marker on a redraw.
			if(self.flusterObject != null)
			{
				self.flusterObject.removeMarker("id" + locationId);
			}
			marker.setMap(self.map);
			marker.setVisible(true);

		}
		else if (!self.overlayCustomTourOnMap)
		{

			//we need to see if we're in the filteredJSON or not
			//if we are, we should be displayed...maybe.
			if(self.filteredJSON.markers.hasOwnProperty("id" + locationId))
			{
				marker.setVisible(true);

				//we need to consider clustering...
				if(self.flusterObject != null && self.flusterObject.inUse)
				{
					if(!self.flusterObject.isMarkerInACluster("id" + locationId))
					{
						//let the cluster decide if/how it will be drawn
						self.flusterObject.
							addMarkerAfterDraw(marker,
							self.JSONData.markerIcons["id" + markerCategoryId].topCategoryId,
							"id" + locationId);

					}
				}//end if we have a fluster object
				else//there is no fluster object and we're in the filteredJSON,
				//so the marker needs to be displayed
				{
					marker.setMap(self.map);
				}


			}//end if we're in the filtered list
			else //we're not in the filtered list
			{
				//since we're not in the filtered list and
				//there is no overlay on
				marker.setVisible(false);

			}
		}//end else we're not overlaying
	} //end updateMarkerFromLocationId

}; //end mapControlBase


