/**
 * <p>Gordon Food Service(R) Location Locator<br />
 * Copyright (c) 2009, 2010</p>
 * <p>Declares the core classes and methods that power the Location Locator.
 * The locator consists of two halfs. One half exists within TeamSite and provides data
 * editing facilities for the locator data. It also provides the base HTML the locator
 * requires. This file (locator.js) is the other half and provides all the necessary.
 * logic for the locator to function.</p>
 * <p>When the HTML page that includes the locator has finished loading (meaning all the
 * HTML has finished downloading), the {@link _global_.DocumentReady} function is executed.</p>
 * @author Caleb Delnay
 * @version 1.1.1
 */

/**
 * Creates a new location.
 * @class LocatorLocation represents a location.
 * @param {Document} xml The XML Object containing the location's data.
 * @param {Number} index The unique index for the location.
 */
var LocatorLocation = function(xml, index){
	/**
	 * {Document} Reference to the XML object that has this location's data.
	 * The XML object is referenced directly to save on memory and time.
	 */
	this.xml = xml;
	/**
	 * {Number} The unique index of the location.
	 */
	this.index = index;
	/**
	 * {GMarker} A reference to the <a href="http://code.google.com/apis/maps/documentation/reference.html#GMarker">GMarker</a> object representing this location on the <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2">GMap2</a>.
	 */
	this.marker = null;
	/**
	 * {GLatLng} A reference to the <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLng">GLatLng</a> object for this location (its latitude and longitude).
	 */
	this.point = null;
	/**
	 * {Number} The location's most recently calculated distance.
	 */
	this.distance = null;
	/**
	 * {Object} Reference to the DIV which holds the content for this location's small info window.
	 */
	this.smallHTML = null;
	/**
	 * {Object} Reference to the DIV which holds the content for this location's large info window.
	 */
	this.largeHTML = null;
	/**
	 * {GInfoWindowOptions} Info window options for this location (<a href="http://code.google.com/apis/maps/documentation/reference.html#GInfoWindowOptions">GInfoWindowOptions</a>).
	 */
	this.infoWindowOptions = {};
};

/**
 * Returns an attribute of the location. The attribute is from the location's XML data.
 *
 * @param {String} attribute The string name of the attribute to retreive.
 * @return {String} The value of the attribute.
 */
LocatorLocation.prototype.getAttribute = function(attribute){
	return this.xml.getAttribute(attribute);
};

/**
 * Returns an attribute of the location as a Number. The attribute is from the location's XML data.
 * @param {String} attribute The name of the attribute to retreive.
 * @return {Number} The value of the attribute.
 */
LocatorLocation.prototype.getAttributeAsNumber = function(attribute){
	return Number(this.getAttribute(attribute));
};

/**
 * Returns the value of a nested child element of the location XML data.
 * @return {String} The data value of as a string.
 */
LocatorLocation.prototype.getData = function(){
	var args = Array.prototype.slice.call(arguments);
	
	return $('> ' + args.join('>'), this.xml).text();
};

/**
 * Returns the value of a nested child element of the location XML data as a Number.
 * @return {Number} The data value as a Number.
 */
LocatorLocation.prototype.getDataAsNumber = function(){
	return Number(this.getData.apply(this, arguments));
};

/**
 * Returns the attribute value of a nested child element of the location XML data.
 * @return {String} The data attribute value.
 */
LocatorLocation.prototype.getDataAttribute = function(){
	// Turn the arguments object into a normal array.
	var args = Array.prototype.slice.call(arguments);
	// The last argument is the attribute name.
	var attribute = args.pop();
	return $('> ' + args.join('>'), this.xml).attr(attribute);
};

/**
 * Creates a new state.
 * @class LocatorState represents a state.
 * @param {Object} xml Reference to the XML object that has the state's data.
 */
var LocatorState = function(xml){
	/**
	 * {Object} Reference to the XML object that has this state's data.
	 * The XML object is referenced directly to save on memory and time.
	 */
	this.xml = xml;
	/** 
	 * {GPolygon[]} An array of <a href="http://code.google.com/apis/maps/documentation/reference.html#GPolygon">GPolygon</a>s which are added to the <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2">GMap2</a>.
	 */
	this.polygons = [];
	/** 
	 * {Object} Holds the locations within this state by type. Each type is a property of the object.
	 */
	this.locationTotalsByType = {};
};
	
/**
 * Hides the state by hiding all of its polygons.
 */
LocatorState.prototype.hide = function(){
	var i = 0;
	for (i = 0; i < this.polygons.length; i++) {
		this.polygons[i].hide();
	}
};
/**
 * Shows the state by showing all of its polygons.
 */
LocatorState.prototype.show = function(){
	var i = 0;
	for (i = 0; i < this.polygons.length; i++) {
		this.polygons[i].show();
	}
};

/**
 * @class The Locator is a static object containing properties and methods to facilitate
 * the functionality of the locator.
 * @static
 */
var Locator = {
	/**
	 * {jQuery} <a href="http://docs.jquery.com/">jQuery</a> object for the sidebar.
	 */
	$sidebar: null,
	/**
	 * {String} String of HTML that is inserted into the sidebar before any searches take place.
	 */
	sidebarInitialContent: null,
	/**
	 * {jQuery} <a href="http://docs.jquery.com/">jQuery</a> object for the address form.
	 */
	$addressform: null,
	/**
	 * {jQuery} <a href="http://docs.jquery.com/">jQuery</a> object for the location type checkboxes.
	 */
	$filterboxes: null,
	/**
	 * {GMap2} <a href="http://code.google.com/apis/maps/documentation/reference.html#GMap2">GMap2</a> object, the main map object.
	 */
	map: null,
	/**
	 * {GClientGeocoder} <a href="http://code.google.com/apis/maps/documentation/reference.html#GClientGeocoder">GClientGeocoder</a> object, used to change addresses into GLatLng.
	 */
	geocoder: null,
	/**
	 * {GDirections} <a href="http://code.google.com/apis/maps/documentation/reference.html#GDirections">GDirections</a> object, used to get directions from A to B.
	 */
	directions: null,
	/**
	 * {Boolean} Boolean that is true if directions are currently being displayed.
	 */
	viewingDirections: false,
	/**
	 * {Object} JavaScript literal object that has two properties; "data" which references the data XML Document and "locations" which references the locations XML document.
	 */
	xml: {
		data: null,
		locations: null
	},
	/**
	 * {MarkerClusterer} MarkerClusterer object, used to control the display of markers on the map.
	 */
	markerClusterer: null,
	/**
	 * {LocatorLocation[]} An array of locations. Each location is a {@link LocatorLocation} object.
	 */
	locations: [],
	/**
	 * {LocatorState[]} An array of states. Each state is a {@link LocatorState} object.
	 */
	states: [],
	/**
	 * {Object} Holds the map markers organized by type. Each type name is a property that is
	 * an Arary of <a href="http://code.google.com/apis/maps/documentation/reference.html#GMarker">GMarker</a> objects.
	 */
	markers: {},
	/**
	 * {GMarker} Reference to the most recent clicked <a href="http://code.google.com/apis/maps/documentation/reference.html#GMarker">GMarker</a>.
	 */
	clickedMarker: null,
	/**
	 * {GMarker} <a href="http://code.google.com/apis/maps/documentation/reference.html#GMarker">GMarker</a> object that represents the location of the searched address.
	 */
	addressMarker: null,
	/**
	 * {SearchRecord[]} An array of {@link SearchRecord} objects created by the user when they submit a search request.
	 */
	searchRecords: [],
	/**
	 * {DirectionsRecord[]} An array of {@link DirectionsRecord} objects created by the user when they submit a directions request.
	 */
	directionsRecords: [],
	/**
	 * {ProgressbarControl} ProgressbarControl object used while loading the {@link LocatorState} and {@link LocatorLocation} objects.
	 */
	progressBar: null,
	/**
	 * {Object} Stores compiled JavaScript templates keyed by name from the data XML file.
	 */
	templates: {},
	/**
	 * {String} If the visitor reaches the locator page through another form, this variable holds the address they submitted.
	 */
	queryAddress: null,
	/**
	 * {Boolean} If true, {@link Locator.events.mapMoveEnd} does nothing the next time the map is moved but it resets this value to false.
	 */
	ignoreNextMapMoveEnd: false,
	/**
	 * {Object} Has two properties containing the zoom level (zoom) and map center (center) of the most recent "moveend" map event.
	 */
	mapMoveEndData: {
		zoom: 0,
		center: null
	},
	/**
	 * Object used for sidebar pagination. It's used to set various options for the paginator.
	 * @static
	 * @class
	 */
	pagination: {
		/**
		 * {LocatorLocation[]} Array of locations to be paginated.
		 */
		locations: null,
		/**
		 * {jQuery} <a href="http://docs.jquery.com/">jQuery</a> object that holds the currently displayed locations.
		 */
		$currentLocations: null,
		/**
		 * {Number} Current page number.
		 */
		currentPage: null,
		/**
		 * {Number} Maximum number of pages.
		 */
		maxPages: null,
		/**
		 * {Array} Array keyed by page number which holds <a href="http://docs.jquery.com/">jQuery</a> objects of built pages.
		 */
		builtPages: null,
		/**
		 * {Boolean} Set to true if the location distance should be displayed.
		 */
		showDistance: false
	},
	/**
	 * Static options used by the Locator.
	 * @static
	 * @class
	 */
	options: {
		/**
		 * {Object} Options to specify the paths of the data needed by the locator.
		 * @static
		 * @class
		 */
		urls: {
			/**
			 * The absolute path (but not the fully qualified URL) to the locator's data as an XML file.
			 */
			data: null,
			/**
			 * The absolute path (but not the fully qualified URL) to the locator's locations as an XML file.
			 */
			locations: null,
			/**
			 * The path to the current specials.
			 */
			specials: '/'
		},
		
		/**
		 * {Object} Options for the state polygons.
		 * @static
		 * @class
		 */
		states: {
			/**
			 * {Number} Number of milliseconds between each state being loaded. Used to tweak performance.
			 */
			loadInterval: 40,
			/**
			 * {Number} The zoom level at which state polygons become invisible.
			 */
			maxZoom: 6
		},
		/**
		 * {Object} Options for the location markers.
		 * @static
		 * @class
		 */
		markers: {
			/**
			 * {Number} Number of milliseconds between each set of markers being loaded. Used to tweak performance.
			 */
			loadInterval: 40,
			/**
			 * {Number} Number of markers to load per interval.
			 */
			perInterval: 10,
			/**
			 * {Number} The zoom level at which locations become visible.
			 */
			minZoom: 6,
			/**
			 * {Number} The maximum zoom level that map will automatically zoom to when searching.
			 */
			maxSearchZoom: 12,
			/**
			 * {Number} Zoom level of the mini-map displayed inside the maximized info window.
			 */
			infoWindowMiniZoom: 15
		},
		/**
		 * {Object} Options used by MarkerClusterer.
		 * @static
		 * @class
		 */
		clusters: {
			/**
			 * {Number} The zoom level at which clusters will appear. Above this zoom, nothing is visible.
			 */
			minZoom: 6,
			/**
			 * {Number} The zoom level at which clusters will no longer appear.
			 */
			maxZoom: 14,
			/**
			 * {Array} Pixel width of a cluster at different zoom levels. Index 0 should be the default.
			 * Further zoom levels can be overridden by setting their index to a Number. If a zoom level
			 * has no value defined, it will use the first level above it (smaller index) it finds with
			 * a value defined.
			 */
			gridSize: function(){
				var sizes = [45];
				sizes[8] = 15;
				return sizes;
			}(),
			/**
			 * {Array} Array of styles to be used for the clusters. Index 0 is for the smallest cluster size.
			 * Each higher index is for the next cluster size up.
			 */
			styles: [
				{ width: 30, height: 30, opt_noText: true, url: '/images/icons/cluster-magnify.png' },
				{ width: 30, height: 30, opt_noText: true, url: '/images/icons/cluster-magnify.png' },
				{ width: 30, height: 30, opt_noText: true, url: '/images/icons/cluster-magnify.png' },
				{ width: 30, height: 30, opt_noText: true, url: '/images/icons/cluster-magnify.png' },
				{ width: 30, height: 30, opt_noText: true, url: '/images/icons/cluster-magnify.png' }
			]
		},
		/**
		 * {Object} Options for pagination. This is not the same as Locator.pagination, which is modified at runtime
		 * depending on various conditions. These pagination options are static.
		 * @static
		 * @class
		 */
		pagination: {
			/**
			 * {Number} Number of locations per page.
			 */
			perPage: 4
		},
		/**
		 * {Number} Multiplier used when displaying the search directions form.
		 * The distance radius of the previous search is multiplied by this value.
		 * If the map center is further than the calculated "circle of prepopulation"
		 * then the address form of the search directions is not populated with the
		 * address of the most recent search.
		 * @see Locator.createDirectionsForm
		 */
		prepopulateDirectionsAddressDistanceMultiplier: 1.5
	},
	/**
	 * {Object} Icons used by the different types of <a href="http://code.google.com/apis/maps/documentation/reference.html#GMarker">GMarkers</a>.
	 */
	icons: {
		store: {inactive: null, active: null},
		dc: {inactive: null, active: null},
		office: {inactive: null, active: null},
		address: {inactive: null, active: null}
	},
	/**
	 * Initializes all of the necessary variables and objects (e.g. Google Maps, Geocoder, etc).
	 * Also attaches and most of the Locator's events.
	 */
	initialize: function(){		
		// Create the GMaps icons.
		var icon = new GIcon(G_DEFAULT_ICON, '/images/icons/marker-marketplace.png');
		icon.shadow = '/images/icons/marker-flag-shadow.png';
		icon.infoWindowAnchor = new GPoint(25, 0);
		icon.iconAnchor = new GPoint(24, 24);
		icon.shadowSize = new GSize(37, 25);
		icon.iconSize = new GSize(25, 25);
		icon.imageMap = [0,0 , 25,0 , 25,25 , 0,25];
		
		Locator.icons.store.inactive = new GIcon(icon, '/images/icons/marker-marketplace.png');
		Locator.icons.store.active = new GIcon(icon, '/images/icons/marker-active-marketplace.png');
		Locator.icons.dc.inactive = new GIcon(icon, '/images/icons/marker-distribution.png');
		Locator.icons.dc.active = new GIcon(icon, '/images/icons/marker-active-distribution.png');
		Locator.icons.office.inactive = new GIcon(icon, '/images/icons/marker-offices.png');
		Locator.icons.office.active = new GIcon(icon, '/images/icons/marker-active-offices.png');
		Locator.icons.address.inactive = new GIcon(G_DEFAULT_ICON, '/images/icons/marker-epicenter.png');
	
		// Grab our important containers / elements and store them. Saves on DOM queries later.
		Locator.$sidebar = $('#sidebar > .contents');
		Locator.$addressform = $('#address-form');
		Locator.$filterboxes = $('input:checkbox', Locator.$addressform);
		Locator.$directions = $('#sidebar > .directions');
		
		// Process PNG images within the address form for IE6.
		Locator.processPNGsForIE6(Locator.$addressform.find('.filter-container img[src$=.png]'));
			
		// Clicking anywhere hides open directions forms.
		// Note this is a live event so that it is affected by 'return false' in other
		// live events. If it was bound to the element directly, propagation can't be stopped.
		$('html').live('click', Locator.hideDirectionsForms);
		
		// Setup various event handlers. .live is used because these elements are all
		// dynamically generated as the map is used.
		$('a.link-address-match').live('click', Locator.events.multipleMatchesLinkClick);
		$('a.link-directions-match').live('click', Locator.events.directions.multipleMatchesLinkClick);
		$('a.directions-from-store, a.directions-from-user').live('click', Locator.events.directions.linkClick);
		$('a.link-directions-reverse').live('click', Locator.events.directions.reverseClick);
		$('a.link-directions-print').live('click', Locator.events.directions.printClick);
		$('a.link-directions-close').live('click', Locator.events.directions.closedClick);
		$('a.link-more-info, ul.results > li').live('click', Locator.events.moreInfoClick);
		$('form.directions-form').live('click', function(e){return false;});
		
		// Clicking the filter checkboxes needs to update the map.
		Locator.$filterboxes.click(Locator.events.filterCheckboxClick);
		// Clicking the new search link will reset everything.
		$('a.link-new-search').click(Locator.reset);
		// Submitting the address form performs a search.
		Locator.$addressform.submit(Locator.events.submitAddressForm);
		
		// Create the map and add the expected controls.
		Locator.map = new GMap2(document.getElementById('map'));
		Locator.map.addControl(new GLargeMapControl3D());
		Locator.map.addControl(new GScaleControl());
		Locator.map.enableScrollWheelZoom();
		
		// Objects needed for functionality
		Locator.directions = new GDirections(Locator.map, Locator.$directions[0]);
		Locator.geocoder = new GClientGeocoder();
		Locator.markerClusterer = new MarkerClusterer(Locator.map, [], Locator.options.clusters);
		Locator.progressBar = new ProgressbarControl(Locator.map, {width: 300});
		
		// Attach various events to the map.
		GEvent.addListener(Locator.map, 'zoomend', Locator.events.mapZoom);
		GEvent.addListener(Locator.map, 'moveend', Locator.events.mapMoveEnd);
		GEvent.addListener(Locator.map, 'click', Locator.events.stateClick);
		GEvent.addListener(Locator.map, 'infowindowopen', Locator.events.infoWindowOpened);
		// Events for the directions.
		GEvent.addListener(Locator.directions, 'load', Locator.events.directions.loaded);
		GEvent.addListener(Locator.directions, 'error', Locator.events.directions.failed);
	},
	
	/**
	 * Resets the locator by centering and zooming the map out, clearing the sidebar, and resetting the form.
	 *
	 * @param {Object} e Event object. Only available if called within the context of an event.
	 */
	reset: function(e){
		// If this function is being called as an event, prevent browser action (e.g. for a link click).
		if (e && e.preventDefault) e.preventDefault();
		
		// Close driving directions if they're open.
		if (Locator.viewingDirections)
			Locator.events.directions.closedClick();
		
		// Remove the address marker if it exists.
		if (Locator.addressMarker != null) {
			Locator.map.removeOverlay(Locator.addressMarker);
			Locator.addressMarker = null;
		}
					
		Locator.map.closeInfoWindow();
		Locator.map.setCenter(new google.maps.LatLng(37.43997405227057, -84.638671875), 4);
		Locator.$sidebar.html(Locator.sidebarInitialContent);
		Locator.$addressform.find('#address').val('').focus();
	},
	
	/**
	 * Performs an AJAX request to get the template and state data. When the data is retreived successfully
	 * the {@link Locator.processData} function is executed.
	 */
	loadData: function(){
		if (Locator.options.urls.data == null) {
			$('#locator').html('No URL set for locator data!');
		}
		
		if (Locator.options.urls.data[0] != '/')
			Locator.options.urls.data = '/' + Locator.options.urls.data;
			
		$('#locator').html('Loading the Store Locator... please wait.');
		
		$.ajax({
			url: Locator.options.urls.data,
			cache: false,
			error: function(){
				$('#locator').html('There was an error while loading the locator and the request could not be completed. Please try again later.');
			},
			success: Locator.processData
		});
	},
	
	/**
	 * Performs an AJAX request to get the locations. When the data is retreived successfully
	 * the {@link Locator.processLocations} function is executed.
	 */
	loadLocations: function(){
		if (Locator.options.urls.locations == null) {
			$('#locator').html('No URL set for locator locations!');
		}
		
		if (Locator.options.urls.locations[0] != '/')
			Locator.options.urls.locations = '/' + Locator.options.urls.locations;
			
		$.ajax({
			url: Locator.options.urls.locations,
			cache: false,
			error: function(){
				$('#locator').html('There was an error while loading the locator and the request could not be completed. Please try again later.');
			},
			success: Locator.processLocations
		});
	},
	
	/**
	 * Processes the locator data to create compiled templates and adds the states to the map overlay.
	 * Executes the important methods {@link Locator.initialize} and {@link Locator.reset} to get the locator
	 * in a valid state. When complete, the {@link Locator.addStatesToOverlay} function is executed with {@link Locator.loadLocations}
	 * as a parameter so it is executed afterward.
	 * @param {Document} data XML document containing the data to be processed.
	 */
	processData: function(data){
		Locator.xml.data = data;
		
		// Loop through and create the templates.
		$('data > templates > template', data).each(function(){
			Locator.templates[$(this).attr('name')] = $.createTemplate($(this).text(), null, {filter_data: false});
		});
		
		// Grab the sidebar content and remove it.
		// The sidebar is removed because the locator HTML already includes
		// a <div> with id of "sidebar".
		Locator.sidebarInitialContent = $('#sidebar').html();
		$('#sidebar').remove();
		
		// Add the locator's HTML into the main container.
		$('#locator').html($.processTemplateToText(Locator.templates['locator']));
		Forms.customControls($('#locator'));
		
		// Initialize the locator. This setups all the necessary variables and attaches events.
		Locator.initialize();
		// Reset the locator.
		Locator.reset();
	
		// Store the query address the user entered.
		Locator.queryAddress = decodeURIComponent($.url.param('place') || '').replace(/\+/g, ' ');
		if (Locator.queryAddress != null && Locator.queryAddress.length > 0)
			Locator.$addressform.find('#address').val(Locator.queryAddress);
			
		$('data > states > state', data).each(function(){
			// Create an object representing the state...
			var state = new LocatorState(this);
			Locator.states.push(state);
		});
		
		// Add the states to the overlay. When finished execute addLocationsToOverlay which
		// when finished will submit the search form if enything is entered in the address field.
		Locator.addStatesToOverlay(Locator.loadLocations);
	},
	
	/**
	 * Loops through all of the locations and creates JSON objects for them.
	 * @param {Document} data XML document containing the locations to be processed.
	 */
	processLocations: function(data){
		Locator.xml.locations = data;
		
		$('locations > location', data).each(function(index){
			// Create an object representing the location...
			var location = new LocatorLocation(this, index);
			Locator.locations.push(location);
		});
		
		Locator.addLocationsToOverlay(function(){
			if (Locator.$addressform.find('#address').val().length > 0)
					Locator.$addressform.submit();
		});
	},
		
	/**
	 * Loops through the states and creates the polygons then adds them to the map.
	 * @param {Function} callback A function to execute once all locations have been added to the overlay.
	 */
	addStatesToOverlay: function(callback){
		// Start the progress bar. The number of operations is the number of states.
		Locator.progressBar.start(Locator.states.length);
		
		var states = Locator.states.slice();
		
		// Loop through all of the states.
		var interval = null;
		// Add a certain number of markers to the map every interval.
		// This is done using an interval because otherwise it can freeze
		// the browser while the polygons are being loaded.
		interval = setInterval(function(){
			var state = states.shift();
			
			$('polygon', state.xml).each(function(){
				/*
				var xmlPoints = this.getElementsByTagName('point');
				var points = [];
				// Loop through all of the points and create a GLatLng object of the points and
				// add it to an array of points to create the polygon.
				for (var j = 0; j < xmlPoints.length; j++) {
					points.push(new GLatLng(parseFloat(xmlPoints[j].getAttribute('lat')),
						parseFloat(xmlPoints[j].getAttribute('lng'))));
				}
				
				// Create the polygon, give it to the state, and add it to the map.
				var poly = new GPolygon(points, '#aa4040', 1, 1, '#ff4040', 0.5, {clickable: true});
				*/
				
				var polygonParameters = {
					polylines: [{
						points: $(this).find('encodedPoints').text(),
						levels: $(this).find('levels').text(),
						weight: 1,
						color: '#aa4040',
						opacity: 1,
						zoomFactor: $(this).find('zoomFactor').text(),
						numLevels: $(this).find('numLevels').text()
					}],
					fill: true,
					outline: true,
					color: '#ff4040',
					opacity: 0.5
				};
				
				var poly = new GPolygon.fromEncoded(polygonParameters);
				
				poly.state = state;
				state.polygons.push(poly);
				
				var cursorTimer = null;
				GEvent.addListener(poly, 'mouseenter', function(){
					if (cursorTimer) clearTimeout(cursorTimer);
					var div = $('#map > div > div');
					div.data('cursor', div.css('cursor'));
					div.data('cursor', 'pointer');
				});
				GEvent.addListener(poly, 'mouseout', function(){
					cursorTimer = setTimeout(function(){
						var div = $('#map > div > div');
						div.css('cursor', div.data('cursor'));
					}, 10);
				});
				
				Locator.map.addOverlay(poly);
				poly.hide();
			});
			
			// Update the progress bar.
			Locator.progressBar.updateLoader(1);

			// If there aren't any more states left, remove the progress bar and stop
			// running the loading function.
			if (states.length == 0) {
				Locator.progressBar.remove()
				clearInterval(interval);
				
				// Execute the callback function if it exists.
				if (callback != null && $.isFunction(callback)) {
					callback.apply(this);
				}
			}
			
		}, Locator.options.states.loadInterval);
	},
	
	/**
	 * Loops through the locations and creates the necessary data then adds them to the map.
	 * @param {Function} callback A function to execute once all locations have been added to the overlay.
	 */
	addLocationsToOverlay: function(callback){
		// Start the progress bar. The number of operations is the number of locations.
		Locator.progressBar.start(Locator.locations.length);
		
		// Array.slice() with no arguments creates a copy of the array.
		var locations = Locator.locations.slice();
		
		var interval = null;
		// Add a certain number of markers to the map every interval. This is done
		// using an interval because otherwise it can freeze the browser while the
		// markers are being loaded.
		interval = setInterval(function(){
			var count = 0;
			
			var i = 0;
			// Loop through the remaining markers or a maximum number of markers.
			for (i = 0; i < Math.min(locations.length, Locator.options.markers.perInterval); i++) {
				// Get the first location from the array and remove it from the array.
				var location = locations.shift();
				var type = location.getAttribute('type');
				
				var smallDiv = document.createElement('div');
				smallDiv.className = 'info-window-small';
				var largeDiv = document.createElement('div');
				largeDiv.className = 'info-window-large';
				
				location.smallHTML = smallDiv;
				location.largeHTML = largeDiv;
				location.point = new GLatLng(location.getAttributeAsNumber('lat'), location.getAttributeAsNumber('lng'));
				location.marker = new GMarker(location.point, {icon: Locator.icons[type].inactive});
				location.marker.location = location;
				
				GEvent.addListener(location.marker, 'click', Locator.events.markerClick);
				
				location.infoWindowOptions = {
					maxWidth: $('#map').width() - 20,
					maxContent: location.largeHTML,
					maxTitle: '<h2>' + location.getAttribute('name') + '</h2>'
				};

				// Bind the small HTML DOM element to the marker info window.
				location.marker.bindInfoWindow(location.smallHTML, location.infoWindowOptions);
				
				// Create a new group of markers for this type if it doesn't exist.
				if (typeof Locator.markers[type] === 'undefined')
					Locator.markers[type] = [];
					
				// Store this marker so it can be added to the manager.
				Locator.markers[type].push(location.marker);
				
				for (var j in Locator.states) {
					if (Locator.states[j].xml.getAttribute('name') == location.getAttribute('state')) {
						if (typeof Locator.states[j].locationTotalsByType[type] == 'undefined')
							Locator.states[j].locationTotalsByType[type] = 1;
						else
							Locator.states[j].locationTotalsByType[type]++;
							
						break;
					}
				}
			}
			
			// Update the progress bar and the markers to the marker manager.
			Locator.progressBar.updateLoader(i);
			
			// If there aren't any more locations left, remove the progress bar and stop
			// running the loading function.
			if (locations.length == 0) {
				Locator.progressBar.remove();
				clearInterval(interval);
				
				// Display each state that has markers if the checked types.
				// Add each marker group to the clusterer.
				Locator.$filterboxes.filter(':checked').each(function(){
					for (i in Locator.states) {
						if (typeof Locator.states[i].locationTotalsByType[$(this).val()] != 'undefined' &&
							Locator.states[i].locationTotalsByType[$(this).val()] > 0)
						{
							Locator.states[i].show();
						}
					}
					Locator.markerClusterer.addMarkers(Locator.markers[$(this).val()]);
				});
				
				// Enable the location type checkboxes.
				Locator.$filterboxes.removeAttr('disabled');
				
				// Execute the callback function if it exists.
				if (callback != null && $.isFunction(callback)) {
					callback.apply(this);
				}
			}
			
		}, Locator.options.markers.loadInterval);
	},
	
	/**
	 * Generates the HTML that's used in a location's info window.
	 * @param {LocatorLocation} location An object representing the location.
	 * @param {String} [size="small"] The size of the info window to generate HTML for ("small" or "large").
	 * @return {String} A string of HTML.
	 */
	generateLocationHTML: function(location, size){
		var size = size || 'small';
		return $.processTemplateToText(Locator.templates['marker-bubble-' + size], {location: location, locator: Locator});
	},
	
	/**
	 * Searches within a distance around a point for the nearest locations.
	 * @param {Number} lat Latitude of the point to search near.
	 * @param {Number} lng Longitude of the point to search near.
	 * @param {Number} distance Maximum number of miles to search within.
	 * @return {LocatorLocation[]} An array of location objects sorted ascending by distance.
	 */
	findLocationsNearLatLng: function(latlng, distance){
		var sortedLocations = [];
		var types = Locator.getCurrentFilters();
		
		// Loop through all of the locations and find their distance to the given latitude and longitude.
		for (var i in Locator.locations) {
			var location = Locator.locations[i];
			
			var skip = true;
			for (var j in types) {
				if (types[j] == location.getAttribute('type')) {
					skip = false;
					break;
				}
			}
			
			if (skip)
				continue;
				
			location.distance = Locator.utilities.metersToMiles(latlng.distanceFrom(new GLatLng(location.getAttributeAsNumber('lat'), location.getAttributeAsNumber('lng'))));
			
			if (location.distance <= distance)
				sortedLocations.push(location);
		}
		
		// Sort the locations by distance.
		sortedLocations.sort(Locator.sortLocationsByDistance);
		
		return sortedLocations;
	},
	
	/**
	 * Returns locations with data values that match a specific value.
	 * @param {String} dataName The attribute name to use for the comparison.
	 * @param {String|Number} dataValue The attribute value to use for the comparison.
	 * @param {String} sortName The attribute name to sort by.
	 * @return {LocatorLocation[]} An array of sorted locations.
	 */
	findLocationsByData: function(dataName, dataValue, sortName){
		var sortedLocations = [];
		sortName = sortName || 'name';
		var types = [];
		Locator.$filterboxes.filter(':checked').each(function(){
			types.push($(this).val());
		});
		
		for (var i in Locator.locations) {
			var location = Locator.locations[i];
			
			var skip = true;
			for (var j in types) {
				if (types[j] == location.getAttribute('type')) {
					skip = false;
					break;
				}
			}
			
			if (skip)
				continue;
				
			if (location.getAttribute(dataName) == dataValue)
				sortedLocations.push(location);
		}
		
		sortedLocations.sort(Locator.getSortLocationsByAttributeFunction(sortName));
		
		return sortedLocations;
	},
	
	/**
	 * Sorting function to sort an array of locations by distance ascending.
	 * @param {LocatorLocation} a First location to compare.
	 * @param {LocatorLocation} b Second location to compare.
	 * @return {Number} 1 if a is further than b, -1 if b is further than a, and 0 if they're the same distance.
	 */
	sortLocationsByDistance: function(a,b) {
		return (a.distance == b.distance) ? 0 : (a.distance > b.distance ? 1 : -1);
	},
	
	/**
	 * Creates a function used by Array.sort to sort an array of {@link LocatorLocations}
	 * by an attribute of the locations.
	 * @param {String} attributeName The name of the atrribute to use in the sort function.
	 * @return {Function} A function that can be used by Array.sort.
	 */
	getSortLocationsByAttributeFunction: function(attributeName) {
		return (function(a,b){
			return (a.getAttribute(attributeName) == b.getAttribute(attributeName)) ? 0 : (a.getAttribute(attributeName) > b.getAttribute(attributeName) ? 1 : -1);
		});
	},
	
	/**
	 * Given a point and a distance, perform a search for locations and then display the results.
	 * @param {GLatLng} point The GLatLng point to search around.
	 * @param {Number} distance The distance from the point to search.
	 * @param {Object} response Response from the Google Maps geocode.
	 * @param {Boolean} [alwaysFindNearest=false] If true, the search will always find at least on location.
	 */
	performSearch: function(point, distance, response, alwaysFindNearest){
		alwaysFindNearest = alwaysFindNearest || false;

		// Create the address marker if it doesn't already exist.
		if (Locator.addressMarker == null) {
			Locator.addressMarker = new GMarker(point,
				{icon: Locator.icons.address.inactive});
				
			Locator.map.addOverlay(Locator.addressMarker);
		}
		
		Locator.addressMarker.setLatLng(point);
		if (response && typeof response.Placemark.address != 'undefined')
			Locator.addressMarker.bindInfoWindowHtml(response.Placemark.address);
		
		// Get an array of the locations sorted by distance.
		var locations = Locator.findLocationsNearLatLng(point, distance);
		// If alwaysFindNearest is true and no locations were found, then just get the nearest location. 
		if (alwaysFindNearest && locations.length == 0) {
			// Note that .slice() here creates a copy of the array so we're not sorting the original Locator.locations array.
			locations = [Locator.locations.slice().sort(Locator.sortLocationsByDistance).shift()];
		}

		// Create a record of this search and store it.
		var searchRecord = {
			point: point,
			distance: distance,
			response: response,
			locations: locations,
			isAddress: true,
			recenter: true,
			filters: Locator.getCurrentFilters()
		};
		Locator.searchRecords.push(searchRecord);
		
		// Display the search results.
		Locator.displaySearchRecord(searchRecord);
	},
	
	/**
	 * Displays a search record.
	 * @param {SearchRecord} searchRecord The search record to display.
	 */
	displaySearchRecord: function(searchRecord){
		var sidebarVariables = {
			show: true,
			text: searchRecord.response.Placemark.address,
			count: searchRecord.locations.length,
			isAddress: searchRecord.isAddress
		};
		
		// Create the sidebar results.
		Locator.$sidebar.html($.processTemplateToText(Locator.templates['sidebar-results'], sidebarVariables));
		Locator.createPager(searchRecord.locations, searchRecord.isAddress);
		
		if (searchRecord.recenter) {
			// Create a new bounds, which is used to contain all of the location points so the map
			// can be recentered and zoomed to show all of the points.
			var bounds = new GLatLngBounds();
			bounds.extend(searchRecord.point);
			
			// Loop through the locations and extend the bounds so the map can center properly.
			for (var i in searchRecord.locations) {
				var location = searchRecord.locations[i];
				bounds.extend(new GLatLng(location.getAttributeAsNumber('lat'), location.getAttributeAsNumber('lng')));
			}
			
			Locator.ignoreNextMapMoveEnd = true;
			
			// Update the map position and zoom.
			Locator.map.setCenter(bounds.getCenter(),
				Math.max(Math.min(Locator.map.getBoundsZoomLevel(bounds), Locator.options.markers.maxSearchZoom), Locator.options.markers.minZoom));
		}
	},
	
	/**
	 * Creates a new pager by reinitializing the global variables and appending the
	 * necessary HTML to the sidebar. Clicks the first page by default.
	 * @param {LocatorLocation[]} locations Array of locations that will be paged.
	 * @param {Boolean} [showDistance=false] If true, the locations will show their last calculated distance.
	 */
	createPager: function(locations, showDistance){
		// Setup the pager variables.
		Locator.pagination.$currentLocations = null;
		Locator.pagination.locations = locations;
		Locator.pagination.maxPages = Math.ceil(locations.length / Locator.options.pagination.perPage);
		Locator.pagination.builtPages = [];
		Locator.pagination.currentPage = 1;
		Locator.pagination.showDistance = showDistance || false;
		
		// Generate the pager HTML.
		Locator.$sidebar.append($.processTemplateToText(Locator.templates['pagination-pager'], Locator.pagination));
		
		// Bind event pagerPageLinkClick to the click event of the page links.
		$('ul.pager a.pager-page-link', Locator.$sidebar).click(Locator.events.pagerPageLinkClick);
		
		// Click to the first page by default.
		if (locations.length > 0)
			Locator.events.pagerPageLinkClick(null, 1);
	},
	
	/**
	 * Processes PNG files for IE6 compatibility.
	 * @param {jQuery} $PNGs The &lt;img&gt; tags to process as a jQuery set.
	 */
	processPNGsForIE6: function($PNGs){
		if ($.browser.msie && $.browser.version < 7) {
			$PNGs.each(function(){
				$(this).css('filter', "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + $(this).attr('src') + "',sizingMethod='image')");
			}).attr('src', '/images/blank.png');
		}
	},
	
	/**
	 * Creates a new directions form. Handles binding the necessary events to the form after it is created.
	 * @param {Object} templateValues The template values to use when creating the form.
	 * @return {jQuery} The directions form as a jQuery object.
	 */
	createDirectionsForm: function(templateValues){
		var $directionsForm = $($.processTemplateToText(Locator.templates['directions-form'], templateValues));
		// A bad hack to allow the form to submit properly. It's submit event is blocked because
		// of a click event on the form element itself to prevent it from beling closed.
		$directionsForm.find('input').click(function(){
			if ($(this).is('[type=submit]')) $(this.form).submit();
			return false;
		});
		// Bind the submit event to the newly constructed form.
		$directionsForm.submit(Locator.events.directions.submitForm);
		$('a.link-directions-back', $directionsForm).click(Locator.events.directions.linkBackClick);

		return $directionsForm;
	},
	
	/**
	 * Hides all directions forms except the special directions form that is part
	 * of the directions request failure response message.
	 */
	hideDirectionsForms: function(){
		$('form.directions-form:not(#directions-failure > form)').hide();
	},
	
	/**
	 * Prepare and execute the directions request.
	 * @param {DirectionsRecord} directionsRecord The directions record to execute and display.
	 */
	requestDirections: function(directionsRecord){				
		Locator.directionsRecords.push(directionsRecord);
				
		// Do a geocode on the address they entered. This will fix small typos
		// or allow the user to select from a list of matching addresses.
		Locator.geocoder.getLocations(directionsRecord[directionsRecord.userType], function(response){
			if (response.Status.code != 200) {
				// Trigger the directions failure manually.
				Locator.events.directions.failed();
			}
			else {				
				// If multiple matches were found, then update the sidebar to display the matches.
				if (typeof response.Placemark.length != 'undefined' && response.Placemark.length > 1) {
					var multipleMatchesVars = {
						address: directionsRecord[directionsRecord.userType],
						matches: response.Placemark,
						type: 'directions'
					};
					
					Locator.$directions.empty().append(
						$.processTemplateToText(Locator.templates['multiple-matches'], multipleMatchesVars)
					);
				}
				else {
					// Just use the first location if an array was given.
					if (typeof response.Placemark[0] != 'undefined') response.Placemark = response.Placemark[0];

					// Use the correct and nicely formatted address instead of what
					// the user entered. They may have made a typo that the directions
					// service won't handle gracefully.
					directionsRecord[directionsRecord.userType] = response.Placemark.address;

					// Add a loading message to the directions area.
					Locator.$directions.empty().text('Loading directions...');
					
					// Submit the request to Google. The rest is handled by Locator.events.directions.loaded or
					// Locator.events.directions.failed depending on the results of the request.
					Locator.directions.load(directionsRecord.from + ' to ' + directionsRecord.to);
				}
			}
		});
	},
	
	/**
	 * Gets the currently enabled filters as a simple array of strings.
	 */
	getCurrentFilters: function(exclude){
		exclude = exclude || [];
		if (exclude.constructor != Array) exclude = [exclude];
		var enabledFilters = [];
		
		Locator.$filterboxes.filter(':checked').each(function(){
			var filter = $(this).val();
			// Check if this filter value matches any of the values in
			// the exclude array. If so, skip this filter.
			for (var i = 0; i < exclude.length; i++) {
				if (filter == exclude[i])
					return true;
			}
			
			enabledFilters.push(filter);
		});
		
		return enabledFilters;
	},
	
	/**
	 * @class
	 * @static
	 */
	events: {
		/**
		 * Submit handler for the address form. Requests a geocode on the address entered then
		 * handles the data returned by the geocode.
		 */
		submitAddressForm: function(){
			Locator.map.closeInfoWindow();
			
			if (Locator.progressBar.current_ != Locator.progressBar.operations_) {
				//alert('Wait for the map to load!');
				return false;
			}
			
			// Close the directions if they're being viewed.
			if (Locator.viewingDirections)
				Locator.events.directions.closedClick();
			
			var address = $('input[name=address]', this).val();
			var distance = Number($('select[name=distance]', this).val());
			
			try {
				if (typeof pageTracker != 'undefined') {
					pageTracker._trackPageview('/store-locator/search/' + address);
				}
			}
			catch (e) {}
			
			Locator.geocoder.getLocations(address, function(response){
				Locator.directions.clear();
				Locator.$sidebar.empty();
				
				if (response.Status.code != 200) {
					Locator.$sidebar.append(
						'<p>We couldn\'t find the address <strong>\'' + address + '\'</strong>.</p>' +
						'<p>Please try a new search using the box above.</p>'
					);
				}
				else {
					// If multiple matches were found, then update the sidebar to display the matches.
					if (typeof response.Placemark.length != 'undefined' && response.Placemark.length > 1) {
						var multipleMatchesVars = {
							address: address,
							matches: response.Placemark,
							type: 'address'
						};
						
						Locator.$sidebar.append(
							$.processTemplateToText(Locator.templates['multiple-matches'], multipleMatchesVars)
						);
					}
					else {
						// Just use the first location if an array was given.
						if (typeof response.Placemark[0] != 'undefined') response.Placemark = response.Placemark[0];
			
						Locator.performSearch(new GLatLng(response.Placemark.Point.coordinates[1],
							response.Placemark.Point.coordinates[0]), distance, response);
					}
				}
			});
			
			return false;
		},
		
		/**
		 * Click event for the pager links. Handles both the arrows and numbers. Generates the
		 * location sidebar HTML and inserts it if doesn't exist or displays the locations if
		 * they've already been generated.
		 * @param e Event object.
		 * @param {Number} [page] The page to display. If omitted, the function tries to obtain
		 * the page from the <em>this</em> object of the function, which should be an anchor tag.
		 */
		pagerPageLinkClick: function(e, page){
			var gotoPage = page || $(this).attr('data-page');
			
			// If gotoPage is text, then convert the text representation into an actual page number.
			if (isNaN(gotoPage)) {
				switch (gotoPage) {
					case 'first': gotoPage = 1; break;
					case 'previous': gotoPage = Locator.pagination.currentPage - 1; break;
					case 'next': gotoPage = Locator.pagination.currentPage + 1; break;
					case 'last': gotoPage = Locator.pagination.maxPages; break;
				}
			}
			
			// Make sure the page value is >= 1 and <= maxPages.
			gotoPage = Math.min(Math.max(gotoPage, 1), Locator.pagination.maxPages);
			
			// Check if we're already on this page and, if so, exit.
			if (Locator.pagination.currentPage == gotoPage && Locator.pagination.$currentLocations != null)
				return false;
			
			if (Locator.pagination.$currentLocations == null)
				Locator.pagination.$currentLocations = $('ul.results li.location.page-' + Locator.pagination.currentPage, Locator.$sidebar);
			
			Locator.pagination.$currentLocations.hide();
			
			// Only show page links for 3 pages in front and behind of the destination page but always
			// show at 7 pages (e.g. 1 2 3 4 5 6).
			$('a.pager-page-link-number').parent().hide().removeClass('active');
			for (var i = Math.max(Math.min(gotoPage + 3, Locator.pagination.maxPages) - 6, 1);
				i <= Math.min(Math.max(gotoPage - 3, 1) + 6, Locator.pagination.maxPages); i++) {
				$('a.pager-page-link-number[data-page=' + i + ']').parent().show();
			}
			
			$('a.pager-page-link-number').parent().removeClass('active');
			$('a.pager-page-link-number[data-page=' + gotoPage + ']').parent().addClass('active');
			Locator.pagination.currentPage = gotoPage;	
			
			// Check to see if the locations on this page have already been generated.
			var $pageLocations = $('ul.results li.location.page-' + gotoPage, Locator.$sidebar);
			if ($pageLocations.length) {
				Locator.pagination.$currentLocations = $pageLocations;
				
				// Show the locations and exit.
				$pageLocations.show();
				return false;
			}
			
			// Calculate the start and end indexes for the location array.
			var start = (gotoPage - 1) * Locator.options.pagination.perPage;
			var end = start + Locator.options.pagination.perPage;
			
			var templateObj = {
				pagination: Locator.pagination,
				// Retreive a specific subset of locations for this page.
				locations: Locator.pagination.locations.slice(start, end),
				locator: Locator
			};
			
			// Generate the locations for the page that is being switched to and insert them where appropriate.
			$pageLocations = $($.processTemplateToText(Locator.templates['sidebar-locations'], templateObj)).filter(function(){
				// Filter out whitespace text nodes. They can cause issues otherwise.
				return this.nodeType == 1;
			});
			
			// Process PNG images for IE6.
			Locator.processPNGsForIE6($pageLocations.find('img[src$=.png]'));

			// Function for swapping images used below.
			var imageSwap = function(type){
				type = type || 'active';
				// Get the location object for the location <li> being hovered over or out of.
				var location = Locator.locations[Number($('.icon > a', this).attr('data-location-id'))];
				if (location) {
					// Do the image change.
					var imageSrc = Locator.icons[location.getAttribute('type')][type].image;
					var $image = $('.icon > a > img', this).attr('src', imageSrc);
					Locator.processPNGsForIE6($image);
					
					try {
						location.marker.setImage(imageSrc);
					} catch (ex) {}
				}
			};
			
			// Attach events for hoving to change the marker to be "active".
			// Basically it highlights the marker on the map.
			$pageLocations.hover(function(){
				imageSwap.apply(this, ['active']);
			}, function(){
				imageSwap.apply(this, ['inactive']);
			});
			
			// If there aren't any current locations then just add to the sidebar.
			if (Locator.pagination.$currentLocations.length == 0)
				$('ul.results', Locator.$sidebar).append($pageLocations);
			else {
				var after = null;
				
				// Start with the page just before the destination page and loop
				// backward until a built set of locations is found.
				for (var i = gotoPage - 1; i >= 1; i--) {
					if (typeof Locator.pagination.builtPages[i] != 'undefined') {
						after = Locator.pagination.builtPages[i];
						break;
					}
				}
				
				// If a set of locations was found, insert our new locations after those.
				if (after) {
					after.eq(after.length - 1).after($pageLocations);
				}
			}
			
			Locator.pagination.$currentLocations = $pageLocations
			Locator.pagination.builtPages[gotoPage] = $pageLocations;
			
			return false;
		},
		
		/**
		 * Event fired when a state polygon is clicked. Zooms the map in on the state.
		 * @param {GOverlay} overlay The map overlay object clicked. It will be a <a href="http://code.google.com/apis/maps/documentation/reference.html#GPolygon">GPolygon</a>.
		 * @param {GLatLng} latlng Not used.
		 * @param {GLatLng} overlaylatlng Not used.
		 */
		stateClick: function(overlay, latlng, overlaylatlng){
			// Do nothing if the markers aren't loaded yet!
			if (Locator.progressBar.current_ != Locator.progressBar.operations_)
				return;
			
			if (overlay != null && overlay.getBounds && overlay.state) {
				/*
				Locator.$sidebar.empty();
			
				var locations = Locator.findLocationsByData('state', $(overlay.state.xml).attr('name'), 'name');

				// Create a record of this search and store it.
				var searchRecord = {
					point: overlay.getBounds().getCenter(),
					distance: -1,
					response: {
						Placemark: {
							address: $(overlay.state.xml).attr('name')
						}
					},
					locations: locations,
					isAddress: false,
					recenter: false
				};
				Locator.searchRecords.push(searchRecord);
				
				Locator.displaySearchRecord(searchRecord);
				
				// Perform a search around the clicked location.
				//Locator.performSearch(overlaylatlng, 25, null, true);
				
				// Center on the clicked location.
				//Locator.map.setCenter(overlaylatlng, Locator.options.markers.minZoom);
				
				Locator.ignoreNextMapMoveEnd = true;
				*/
				// Center on the clicked state.
				Locator.map.setCenter(overlay.getBounds().getCenter(), Locator.map.getBoundsZoomLevel(overlay.getBounds()));
			}
		},
		
		/**
		 * Event fired when the map changes its zoom level. Hides or shows the states and
		 * address marker depending on the map configuration.
		 * @param {Number} oldLevel The previous zoom level of the map.
		 * @param {Number} newLevel The new zoom level of the map.
		 */
		mapZoom: function(oldLevel, newLevel){
			var i = 0, j = 0, locationsTotal = 0;
			
			// If we're viewing directions, do nothing.
			if (Locator.viewingDirections) return;
		
			// If we're now zoomed in further than the max zoom for the states,
			// then hide the states and show the address marker. The MarkerManager
			// will take care of the location markers.
			if (newLevel >= Locator.options.states.maxZoom) {
				if (Locator.addressMarker != null) {
					Locator.addressMarker.show();
				}
				
				for (i = 0; i < Locator.states.length; i++) {
					Locator.states[i].hide();
				}
			}
			// Otherwise hide the address marker and show the states.
			else {
				if (Locator.addressMarker != null) {
					Locator.addressMarker.hide();
				}
				
				var types = Locator.getCurrentFilters();
				
				for (i = 0; i < Locator.states.length; i++) {
					locationsTotal = 0;
					
					for (j in types) {
						if (typeof Locator.states[i].locationTotalsByType[types[j]] != 'undefined')
							locationsTotal += Locator.states[i].locationTotalsByType[types[j]];
					}

					if (locationsTotal > 0)
						Locator.states[i].show();
				}	
			}
		},
		
		/**
		 * Event fired when the movement of the map ends (including zooming). Updates
		 * the sidebar with a list of locations currently visible on the map, including
		 * locations that are within clusters.
		 * @param {Boolean} [force=false] Rebuild the sidebar regardless.
		 */
		mapMoveEnd: function(force) {
			force == force || false;
				
			var mapMoveEndData = {
				zoom: Locator.map.getZoom(),
				center: Locator.map.getCenter()
			};
			
			// If viewing directions or zoomed out too far, do nothing!
			// This is checked even if force is true because it doesn't make sense to
			// update the sidebar in these circumstances.
			if (Locator.viewingDirections || Locator.map.getZoom() < Locator.options.states.maxZoom)
				return;
					
			if (!force) {
				// If the previous moveend event has the same zoom and center as this moveend event, then
				// nothing has changed, so do nothing.
				if (Locator.mapMoveEndData.zoom == mapMoveEndData.zoom && Locator.mapMoveEndData.center.equals(mapMoveEndData.center))
					return;

				// Update the movend data but don't do anything else.
				// Make sure the next moveend event isn't ignored though.
				if (Locator.ignoreNextMapMoveEnd) {
					Locator.mapMoveEndData = mapMoveEndData;
					Locator.ignoreNextMapMoveEnd = false;
					return;
				}
			}
				
			// Update the moveend data.
			Locator.mapMoveEndData = mapMoveEndData;
				
			// Get all markers that are on the map whether they're in a cluster or not.
			var markers = [];
			var clusters = Locator.markerClusterer.getClustersInViewport_();
			for (var i in clusters) {
				markers = markers.concat(clusters[i].getMarkers());
			}
			
			var locations = [];
			// Get the locations out of the markers.
			for (var i in markers) {
				// Note the marker property of each array element. This is because the markers are wrapped
				// in an object by Locator.markerClusterer.
				if (typeof markers[i].marker.location != 'undefined' && markers[i].marker.location != null)
					locations.push(markers[i].marker.location);
			}
			
			// Do nothing if we have nothing.
			if (clusters == 0 || markers == 0 || locations.length == 0)
				return;
				
			locations.sort(Locator.getSortLocationsByAttributeFunction('name'));
		
			// Create a record of this search and store it.
			var searchRecord = {
				point: mapMoveEndData.center,
				distance: -1,
				response: {
					Placemark: {
						address: mapMoveEndData.center.toString()
					}
				},
				locations: locations,
				isAddress: false,
				recenter: false
			};
			Locator.displaySearchRecord(searchRecord);
		},
		
		/**
		 * Event fired when a marker is clicked. Sets up some variables used by {@link Locator.events.infoWindowOpened}.
		 * @param {GLatLng} latlng The GLatLng of the clicked marker. Not used.
		 * @param {Boolean} [maximize=false] True to maximize the info window for this marker. False to open the small info window.
		 */
		markerClick: function(latlng, maximize){
			// Close any currently open info window.
			Locator.map.closeInfoWindow();
			
			// Default to false for the maximize parameter.
			maximize = maximize || false;
			// Set the maximize setting for this markers info window. This is used by Locator.event.infoWindowOpened.
			this.maximizeInfoWindow = maximize;
			// Store this marker is the most recently clicked marker. This is used by Locator.event.infoWindowOpened.
			Locator.clickedMarker = this;
		},
		
		/**
		 * Event fired when any info window is opened on the map. It generates the HTML for the small
		 * and large sizes of the info window the first time that window is opened. It also maximizes
		 * the info window if need be. Note that this event is attached to the map, not the markers,
		 * because the markers lose the event due to removeOverlay from the MarkerClusterer.
		 */
		infoWindowOpened: function(){
			// Don't let map panning caused by the opening info window to rebuild the sidebar.
			Locator.ignoreNextMapMoveEnd = true;
			
			// Get the most recent clicked marker. This will be the marker that opened
			// our info window. A bit of a hack since we can't attach this event directly
			// to the marker that originated the call.
			var marker = Locator.clickedMarker;
			if (marker) {
				var iw = Locator.map.getInfoWindow();
				
				// If the small Div has no children, then they need to be generated.
				if ($(marker.location.smallHTML).children().length == 0) {
					// Generate the HTML for the small and maximized windows.
					$(marker.location.smallHTML).append(Locator.generateLocationHTML(marker.location, 'small'));
					$(marker.location.largeHTML).append(Locator.generateLocationHTML(marker.location, 'large'));
					
					// Unfortunately IE doesn't seem to work unless we bind the events manually
					// to the elements.
					$('a.link-more-info', marker.location.smallHTML).click(Locator.events.moreInfoClick);
					//$('a.link-current-specials', marker.location.smallHTML).click(Locator.events.moreInfoClick);
					$('a.directions-from-store', $([marker.location.smallHTML, marker.location.largeHTML]))
						.click(Locator.events.directions.linkClick);
					$('a.directions-from-user', $([marker.location.smallHTML, marker.location.largeHTML]))
						.click(Locator.events.directions.linkClick);
			  
					// Add an event for when the info window is maximized to create a mini
					// GMap to show a detailed view of the location.
					GEvent.addListener(iw, 'maximizeend', function(){
						marker.mini = {
							div: $('.mini-map', marker.location.largeHTML)[0],
							map: null,
							marker: null
						}
						marker.mini.map = new google.maps.Map2(marker.mini.div);
						//marker.mini.map.disableDragging();
						marker.mini.map.disableInfoWindow();
						//marker.mini.map.disableDoubleClickZoom();
						marker.mini.map.enableScrollWheelZoom();
						marker.mini.map.addControl(new GSmallZoomControl());
						marker.mini.marker = new GMarker(marker.location.point, {icon: Locator.icons[marker.location.getAttribute('type')].inactive, clickable: false});
						marker.mini.map.addOverlay(marker.mini.marker);
						marker.mini.map.setCenter(marker.mini.marker.getLatLng(), Locator.options.markers.infoWindowMiniZoom);
						
						// Hide the Google logo and copyright info.
						$(marker.mini.div).children(':eq(1)').hide();
						$(marker.mini.div).children(':eq(2)').hide();
					});
				}
				
				// Maximize the window if this marker has that setting as true.
				if (marker.maximizeInfoWindow) iw.maximize();
			}
		},
		
		/**
		 * Click event for an address link, which is displayed when a search is performed and
		 * the search matches multiple locations. This updates the address and submits the search
		 * again.
		 */
		multipleMatchesLinkClick: function(){
			$('#address').val($(this).text());
			Locator.$addressform.submit();
			return false;
		},
		
		/**
		 * Click event for the "More Info" links. Opens the large info window for the location.
		 * This event is also used by the location li when it is displayed in the sidebar.
		 * @param e Event object.
		 */
		moreInfoClick: function(e){
			if ($(e.target).is('a.link-current-specials'))
				return true;
				
			// Hide any open directions forms.
			Locator.hideDirectionsForms();
			
			var a = this;
			// If the list element was clicked, act as if the link was clicked for
			// the sake of the following code.
			if ($(a).is('li.location')) {
				a = $('.icon .link-more-info', a)[0];
			}
			
			var location = Locator.locations[Number($(a).attr('data-location-id'))];

			// Don't update the sidebar for the opened info window.
			Locator.ignoreNextMapMoveEnd = true;
			
			// Get the currently open info window content element.
			var $currentInfoNode = $('.info-window-small, .info-window-large');
			// Is the currently open window the same as the one we clicked?
			if ($currentInfoNode.length == 1 && ($currentInfoNode[0] == location.smallHTML || $currentInfoNode[0] == location.largeHTML)) {
				// Close the window if it is maximized.
				if ($currentInfoNode[0] == location.largeHTML)
					Locator.map.closeInfoWindow();
				// Maximize the window otherwise.
				else
					Locator.map.getInfoWindow().maximize();
			}
			// There is no info window open or the currently open info window is not this location's.
			// Open an info window and act as if this location's marker was clicked so that
			// Locator.events.infoWindowOpened can respond appropriately.
			else {
				Locator.map.setCenter(location.point, Locator.options.markers.maxSearchZoom);
				Locator.ignoreNextMapMoveEnd = true;
				Locator.map.openInfoWindow(location.point, location.smallHTML, location.infoWindowOptions);
				Locator.clickedMarker = location.marker;
				Locator.clickedMarker.maximizeInfoWindow = true;
			}
			
			return false;
		},

		/**
		 * Click event for the filter checkboxes. If the checkbox is being unchecked, it removes the
		 * locations of that type from the map. If the checkbox is being checked, the locations of that
		 * type will be added to the map. If zoomed out far enough, it also shows and hides states
		 * depending on the visible locations within that state (a state with no visible locations is
		 * not displayed on the map).
		 * @param e Event object.
		 */
		filterCheckboxClick: function(e){
			var i = 0, j = 0;
			
			// If directions are being viewed then it is not necessary to
			// add or remove markers/states to the map.
			if (Locator.viewingDirections)
				return true;
			
			// If checked, then the box was previously unchecked, so we
			// only need to add markers, not clear any.
		
			// The check for e.originalEvent is because the jQuery checkbox plugin causes some behavioral changes.
			// e.originalEvent will be set if the user clicks the checkbox label, and checked will be true.
			// If they click the placeholder fauxbox, then original event will not exist and checked is false.
			var checked = ($(this).is(':checked') && e.originalEvent) || (!$(this).is(':checked') && !e.originalEvent);
			if (checked) {
				Locator.markerClusterer.addMarkers(Locator.markers[$(this).val()]);
				
				if (Locator.map.getZoom() < Locator.options.states.maxZoom) {
					// Loop through the states and show any states that have locations of the checkbox's type.
					for (i = 0; i < Locator.states.length; i++) {
						if (typeof Locator.states[i].locationTotalsByType[$(this).val()] != 'undefined' &&
							Locator.states[i].locationTotalsByType[$(this).val()] > 0)
						{
							Locator.states[i].show();
						}
					}
				}
			}
			else {
				Locator.markerClusterer.removeMarkers(Locator.markers[$(this).val()]);
				if (Locator.map.getZoom() < Locator.options.states.maxZoom) {
					// Get the current filters, excluding the filter for this checkbox.
					var types = Locator.getCurrentFilters($(this).val());
					
					/*var types = [];
					var checkbox = this;
					Locator.$filterboxes.filter(':checked').each(function(){
						if (this == checkbox) return true;
						types.push($(this).val());
					});
					*/
					
					for (i = 0; i < Locator.states.length; i++) {
						var state = Locator.states[i];
						
						// Count how many locations are within this state by type.
						var locationsTotal = 0;
						for (j in types) {
							if (typeof state.locationTotalsByType[types[j]] != 'undefined')
								locationsTotal += state.locationTotalsByType[types[j]];
						}
						
						// No locations? Hide the state.
						if (locationsTotal == 0)
							state.hide();
					}
				}
			}
			
			try {
				if (typeof pageTracker != 'undefined') {
					pageTracker._trackPageview('/store-locator/filter/' + $(this).val() + '/' + (checked ? 'check' : 'uncheck'));
				}
			}
			catch (e) {}

			Locator.markerClusterer.resetViewport();
			
			if (Locator.getCurrentFilters().length > 0) {
				// Force the map to rebuild the sidebar since locations have changed.
				GEvent.trigger(Locator.map, 'moveend', true);
			}
			// If no filters are enabled, have the sidebar display a message indicating such.
			else
				Locator.$sidebar.html($.processTemplateToText(Locator.templates['no-filters-enabled']));
		},
		
		/**
		 * @class
		 * @static
		 */
		directions: {
			/**
			 * Submit handler for the directions form. Displays the location that is acting as a destination (or start)
			 * in the sidebar and performs the directions query to Google.
			 */
			submitForm: function(){
				if ($('input[name=directions-address1]', this).val().length && $('input[name=directions-address2]', this).val().length) {
					// Replace the sidebar with a results list so we can show the one location being searched.
					Locator.$sidebar.html($.processTemplateToText(Locator.templates['sidebar-results'], {show: false}));
					Locator.processPNGsForIE6(Locator.$sidebar.find('img[src$=.png]'));
					
					var index = Number($('input[name=directions-location-index]', this).val());
					var type = $('input[name=directions-type]', this).val();
					var userAddressType = type == 'directions-from-user' ? 'from' : 'to';
					Locator.pagination.currentPage = 1;
					
					// Add the location to the sidebar.
					$('ul.results', Locator.$sidebar).append($.processTemplateToText(Locator.templates['sidebar-locations'], {
						pagination: {showDistance: false, currentPage: 1},
						fromSearch: false,
						// Locations is expected to be an array.
						locations: [Locator.locations[index]],
						locator: Locator
					}));
					
					// Generate the directions links.
					Locator.$sidebar.append($.processTemplateToText(Locator.templates['directions-links'], {index: index}));
					
					// Close the info window so the directions can be seen on the map.
					Locator.map.closeInfoWindow();
					
					// This object defaults to the "from" type, meaning the user entered address
					// is the "from" value and the "to" value is the store.
					var points = {
						from: $.trim($('input[name=directions-address1]', this).val()),
						to: $.trim($('input[name=directions-address2]', this).val())
					};
					
					// If the user entered address is the "to" (the destination), then
					// swap the from and to addresses so that "to" is equal to the address
					// that the user entered.
					if (userAddressType == 'to')
						points = { from: points.to, to: points.from };
					
					var directionsRecord = {
						location: index,
						type: type,
						userType: userAddressType,
						from: points.from,
						to: points.to
					};
					
					// Go into viewing directions mode. This prevents events like map panning
					// from causing the sidebar to be overwritten with the locations within the
					// map viewport.
					Locator.viewingDirections = true;
				
					// Hide all of the markers so that the directions are clear.
					Locator.markerClusterer.clearMarkers();
					
					// Loop through and hide any visible states.
					for (var i in Locator.states) {
						Locator.states[i].hide();
					}
					
					// Hide the address marker if it exists.
					if (Locator.addressMarker != null)
						Locator.addressMarker.hide();					
					
					Locator.requestDirections(directionsRecord);
				}
			
				return false;
			},
		
			/**
			 * Click event for an directions link, which is displayed when directions are performed and
			 * the search matches multiple locations. This updates the address and submits the directions
			 * again.
			 */
			multipleMatchesLinkClick: function(){
				var directionsRecord = Locator.directionsRecords[Locator.directionsRecords.length - 1];
				directionsRecord[directionsRecord.userType] = $(this).text();
				Locator.requestDirections(directionsRecord);
				//Locator.directions.load([directionsRecord.from, directionsRecord.to].join(' to '));
								
				return false;
			},
		
			/**
			 * Click event for the "To here" and "From here" directions links. Opens the directions form.
			 * @param e Event object.
			 */
			linkClick: function(e){
				var templateValues = {
					index: $(this).attr('data-location-id'),
					type: $(this).is('.directions-from-store') ? 'directions-from-store' : 'directions-from-user',
					address1: '',
					// Get the location address directly from the location data and replace
					// any line breaks (<br />) with a space so that it submits properly to Google.
					address2: Locator.locations[Number($(this).attr('data-location-id'))].getAttribute('address').replace(/<br\s+?\/?>/g, ' ')
				};
				
				var mostRecentSearch = Locator.searchRecords[Locator.searchRecords.length - 1] || null;
				// Check if the map has been scrolled or moved around much since the most recent search.
				// This is done by finding the distance from the current map center to the address that
				// was most recently searched and checking if it is a certain multiplication of a distance
				// away from the radius that was specified in the search.
				// If the map hasn't moved away much, then it makes sense to prepopulate the address with
				// the address of the most recent search.
				if (mostRecentSearch && Locator.utilities.metersToMiles(mostRecentSearch.point.distanceFrom(Locator.map.getCenter())) <
					(mostRecentSearch.distance * Locator.options.prepopulateDirectionsAddressDistanceMultiplier)) {
					templateValues.address1 = mostRecentSearch.response.Placemark.address;
				}
				
				// First try to see if the directions form already exists.
				var $directionsForm = $(this).nextAll('form.directions-form');
				if ($directionsForm.length == 0) {
					$directionsForm = Locator.createDirectionsForm(templateValues);
					$directionsForm.appendTo($(this).parent()).hide()
						.css({left: ($.browser.msie && $.browser.version < 7) ? 0 : $(this).parent().children().position().left});
				}
				
				// If the type is being changed, then update the type and text.
				if (templateValues.type != $('input[name=directions-type]', $directionsForm).val()) {
					$('input[name=directions-type]', $directionsForm).val(templateValues.type);
					$('h3', $directionsForm).text(templateValues.type == 'directions-from-user' ? 'Start address (include city, state)' : 'End address (include city, state)');
				}
				else if (!$directionsForm.is(':hidden'))
					$directionsForm.hide();
					
				if ($directionsForm.is(':hidden')) {
					$directionsForm.show();
					$('input[name=directions-address1]', $directionsForm).val(templateValues.address1).focus();
					$('input[name=directions-address2]', $directionsForm).val(templateValues.address2);
				}
				
				return false;
			},
			
			/**
			 * Click event for the directions form "Back" link.
			 * @param e Event object.
			 */
			linkBackClick: function(e){
				// Hide the directions form and remove it when it vanishes.
				$(this).parents('.directions-form').hide();

				return false;
			},
			
			/**
			 * Event that fires when a directions request succeeds. Puts the map into
			 * "viewing directions" mode and hide the markers and states.
			 */
			loaded: function(){				
				// Clear out whatever is there.
				Locator.$directions.empty();
			},
			
			/**
			 * Event that fires when a directions request fails. If the directions were submitted with a to
			 * or from that wasn't using a longitude latitude pair, then the request is resubmitted using said pair.
			 * If that fails, then a nice error message is displayed.
			 */
			failed: function(){
				var directionsRecord = Locator.directionsRecords[Locator.directionsRecords.length - 1];
				
				// Send an e-mail about the failure to get directions.
				try {
					var body = 'Tried to get directions from "' + directionsRecord.from + '" to "' + directionsRecord.to + '" but was not successful!';
					
					$.ajax({
						url: '/experience/email.do',
						data: {
							'from': 'info@gfs.com',
							'to': 'Caleb.Delnay@gfs.com',
							'subject': 'GFS.com Store Locator Directions Failure',
							'body': body
						},
						method: 'POST',
						cache: false
					});
				} catch (e) {}
				
				// If the location type is not a latlng, then make the request again using the GLatLng of the location.
				if (!directionsRecord[directionsRecord.userType].match(/-?\d+(\.?\d+)?\s*,\s*-?\d+(\.?\d+)?/i)) {
					directionsRecord['previous'] = directionsRecord[directionsRecord.userType];
					directionsRecord[directionsRecord.userType] = Locator.locations[directionsRecord.location].point.toUrlValue(8);
					Locator.requestDirections(directionsRecord);
					return;
				}
				
				// Act as if the directions were successful.
				Locator.events.directions.loaded();
				
				// Create a directions form as part of the failure message so it is easy
				// for the visitor to perform another directions request.
				var directionsTemplateValues = {
					index: directionsRecord.location,
					type: directionsRecord.type,
					address1: '',
					address2: directionsRecord[directionsRecord.type]
				};
				if (typeof directionsRecord.previous != 'undefined')
					directionsTemplateValues.address = directionsRecord.previous;
				var $directionsForm = Locator.createDirectionsForm(directionsTemplateValues);
				var directionsFailureHTML = $.processTemplateToText(Locator.templates['directions-failure']);
				// Set the directions HTML to the failure template and append a directions
				// form to the bottom.
				Locator.$directions.html(directionsFailureHTML);
				Locator.$directions.find('#directions-failure').append($directionsForm);
			},
			
			/**
			 * Click event for the "Reverse directions" link. Submits a new directions query but
			 * before doing so swaps the from and to addresses.
			 * @param e Event object.
			 */
			reverseClick: function(e){				
				var directionsRecord = Locator.directionsRecords[Locator.directionsRecords.length - 1];
				var newDirectionsRecord = {
					location: directionsRecord.location,
					type: directionsRecord.type,
					userType: directionsRecord.userType == 'from' ? 'to' : 'from',
					from: directionsRecord.to,
					to: directionsRecord.from
				};
				Locator.directionsRecords.push(newDirectionsRecord);
				
				// Submit the request to Google. The rest is handled by eventDirectionsLoaded or
				// eventDirectionsFailed depending on the results of the request.
				Locator.requestDirections(newDirectionsRecord);
			
				return false;
			},
			 
			/**
			 * Click event for the "Print directions" link. Opens a popup to google for directions, since
			 * there interface is very clean with more functionality and we don't have to maintain it.
			 * @param e Event object.
			 */
			printClick: function(e){				
				var directionsRecord = Locator.directionsRecords[Locator.directionsRecords.length - 1];
				var directionsPopup = window.open('http://maps.google.com/maps?q=' + encodeURIComponent(directionsRecord.from) + ' to ' + encodeURIComponent(directionsRecord.to) + '&pw=2&z=' + Locator.map.getZoom(), 'Directions', 'menubar=1,resizable=1,scrollbars=1');
				if (directionsPopup == null) {
					alert('You must disable your popup blocker in order to view printer friendly directions.');
				}
			
				return false;
			},
			
			/**
			 * Click event for the "Close directions" link. Puts the map back into normal mode and
			 * clears out everything associated with directions. It also tries to display the most
			 * recent search if it was relevant.
			 * @param e Event object.
			 */
			closedClick: function(e){
				// Clear out the directions and get out of directions mode.
				Locator.directions.clear();
				
				if (Locator.viewingDirections) {
					Locator.viewingDirections = false;
					$('.links-directions', Locator.$sidebar).remove();
					Locator.$directions.empty();

					// Add the markers for the checked boxes.
					Locator.$filterboxes.filter(':checked').each(function(){
						Locator.markerClusterer.addMarkers(Locator.markers[$(this).val()]);
					});
				}
				
				// Act as if the map has been zoomed to refresh the markers / states.
				GEvent.trigger(Locator.map, 'zoomend', Locator.map.getZoom(), Locator.map.getZoom());
				
				var locationFound = false;
				var location = Locator.locations[Number($(this).attr('data-location-id'))];
				// See if the store we got directions for was in our most recent search results. If so, display those results.
				if (Locator.searchRecords.length) {
					var searchRecord = Locator.searchRecords[Locator.searchRecords.length - 1];
					if (Locator.utilities.arrayCompare(searchRecord.filters, Locator.getCurrentFilters()))
					{
						for (var i in searchRecord.locations) {
							if (location == searchRecord.locations[i]) {
								locationFound = true;
								Locator.displaySearchRecord(searchRecord);
								break;
							}
						}
					}
				}
				
				// If the location wasn't in the previous search result, then force a moveend event
				// to update the sidebar with the locations currently on the screen.
				if (!locationFound)
					GEvent.trigger(Locator.map, 'moveend', true);
			
				return false;
			}
		}
	},
	
	/**
	 * @class
	 * @static
	 */	
	utilities: {
		/**
		 * Converts meters into miles.
		 * @param {Number} meters The number of meters to convert to miles.
		 */
		metersToMiles: function(meters) { return meters * 0.000621371192; },
		/**
		 * Converts miles into meters.
		 * @param {Number} miles The number of miles to convert to meters.
		 */
		milesToMeters: function(miles) { return miles * 1609.344; },
		
		/**
		 * Compares array A and array B and returns true if they contain the same elements in the
		 * same order. It does not handle nested arrays.
		 * @param {Array} a The first array to compare.
		 * @param {Array} b The second array to compare.
		 */
		arrayCompare: function(a, b) {
			if (a.length != b.length) return false;
			
			for (var i = 0; i < a.length; i++) {
				if (a[i] != b[i]) return false;
			}
			
			return true;
		}
	}
};

/**
 * @name SearchRecord
 * @class Record of a submitted search request stored in {@link Locator.searchRecords}. This class is instiated as a JavaScript literal.
 */
/**
 * @lends SearchRecord#
 * @property {GLatLng} point The <a href="http://code.google.com/apis/maps/documentation/reference.html#GLatLng">GLatLng</a> object used as the center of the search.
 * @property {Number} distance The distance in miles around {@link SearchRecord.point} to search.
 * @property {Object} response Response object from {@link Locator.geocoder.getLocations}.
 * @property {LocatorLocations[]} locations Array of locations that the search found.
 * @property {Boolean} isAddress True if the search was around a specific address and not a state.
 * @property {Boolean} recenter True if the map should be recentered for the search.
 * @property {String[]} filters Array of filters that were enabled when this search was performed.
 */

/**
 * @name DirectionsRecord
 * @class Record of a submitted directions request stored in {@link Locator.directionsRecords}. This class is instiated as a JavaScript literal.
 */
/**
 * @lends DirectionsRecord#
 * @property {Number} location Index of the location this record refers to in {@link Locator.locations}.
 * @property {String} type Either "directions-from-store" or "directions-from-user" depending on the starting point for the directions.
 * @property {String} userType The name of the property that contains the user supplied address (either "to" or "from").
 * @property {String} from The start point as a string value that can be read by GDirections.load.
 * @property {String} to The end point a a string value that can be read by GDirections.load.
 */

/**
 * Document ready function executed once the DOM is ready. Simply runs {@link Locator.loadData}
 * to start the locator loading. 
 * @name DocumentReady
 * @memberOf _global_
 * @see <a href="http://docs.jquery.com/How_jQuery_Works#Launching_Code_on_Document_Ready">jQuery Document Ready Documentation</a>
 */
$(function(){
	Locator.loadData();
});

/**
 * Window unbind event which unloads Google Maps and dereferences the Locator object to
 * prevent memory leaks.
 * @name WindowUnload
 * @memberOf _global_
 */
$(window).bind('unload', function(){
	// Unload Google Maps
	GUnload();

	// Remove the locator reference to possibly free up the memory it has used.
	Locator = null;
});