/**
*  MLS Search Engine Plugin
*  Copyright (c) 2008-2011, RealPageMaker.com
*  Provides all functions associated with the MLS Search Engine
*
*  Dependencies: jquery, jqueryUI, colorbox, jquery.rpm.js
*/

(function($){

	/** Main Search Engine Methods 
	  *
	  * public methods:
	  *	"init" - initializes the search engine. Must be run before anything else.
	  *	"updateResults" - runs the search again and updates the results
	  *	"listingDetails" - loads the select listing details
	  *	"busyOverlay"
	  *	"lockByListDate" - locks the search engine to only show listings listed between a (in url) list date range.
	  */
	$.fn.csIDXSearchEngine = function(method, options){

		var config = null;	// contains all information for this call.
		var searchAjaxActivity = null;
		var statsAjaxActivity = null;
		var statisticsMessage = '<div style="padding:5px;">Loading Statistics, please wait (takes up to 15 seconds)...</div>';
		
		// MLS Search Overload protection.
		var mlsSearchOP_current_delay   = 0;  // The current delay to wait until we issue the query to the server.
		var mlsSearchOP_delay_up_step   = 1;  // The delay will be increased in steps this big (seconds).
		var mlsSearchOP_delay_down_step = 2;  // The delay will be increased in steps this big (seconds).
		var mlsSearchOP_delay_max       = 2; // The maximum delay that will be implemented (seconds).
		var mlsSearchOP_timeout_id	= 0;  // The timeout id of the currently pending search (so we can cancel it if another comes in).

		// Constants
		var LISTING_DETAILS_WIDTH = 730;	// the width of the listing details colorbox overlay
		var MAP_RESULT_CONTAINER_MAX_DISTANCE_ABOVE_MAP = 100; // distance above the map that a result container is permitted.


		/**
		* Map Marker
		*
		* @param position (google.maps.LatLng) coordinate point for tooltip to anchor on
		* @param content (String) HTML content of tooltip
		* @param align (String) alignment of tooltip relative to position value - defaults to Southeast (Only works on IE >= 9)
		* @param opacity (Number) opacity level of tooltip - value should be between 0.1 and 1.0
		*/
		function AreaIdMarker(position, content, align, opacity){
			this.position_ = position;  //LatLng
			this.content_ = content;
			this.align_ = AreaIdMarker.SouthWest;
			if(align == AreaIdMarker.NorthWest 
			|| align == AreaIdMarker.NorthEast 
			|| align == AreaIdMarker.SouthWest 
			|| align == AreaIdMarker.SouthEast 
			|| align == AreaIdMarker.Center){ this.align_ = align; }
			this.opacity_ = opacity;
			
			this.showOnDraw_ = false;  //Needed in case show() gets called before draw()
			
			this.div_ = null;
		}

			/**
			* Static variables for this class
			*/
			AreaIdMarker.NorthWest = "NW";
			AreaIdMarker.NorthEast = "NE";
			AreaIdMarker.SouthWest = "SW";
			AreaIdMarker.SouthEast = "SE";
			AreaIdMarker.Center    = "C";
		
			AreaIdMarker.prototype = new google.maps.OverlayView();
			
			AreaIdMarker.prototype.onAdd = function(){
				try{
					var div = document.createElement("div");
					div.style.position = "absolute";
					div.style.filter = 'alpha(opacity=' + (this.opacity_ * 100) + ')';
					div.style.opacity = this.opacity_;
					div.innerHTML = this.content_;
					div.style.visibility = "hidden";
					
					var panes = this.getPanes();
					panes.overlayImage.appendChild(div);
						
					this.div_ = div;
				}catch(e){
					alert(e.name + " - " + e.message);
				}
			};
			
			AreaIdMarker.prototype.draw = function(){
				try{
					var projection = this.getProjection();
					var anchor = projection.fromLatLngToDivPixel(this.position_);
					var divStyle, width = 0, height = 0;
					
					if(typeof window.getComputedStyle != "undefined"){
						divStyle = window.getComputedStyle(this.div_, null);
						width = new Number(divStyle.getPropertyValue("width").substring(0, divStyle.getPropertyValue("width").indexOf("px")));
						height = new Number(divStyle.getPropertyValue("height").substring(0, divStyle.getPropertyValue("height").indexOf("px")));
					}else{
						width = this.div_.clientWidth;
						height = this.div_.clientHeight;
					}
					
					switch(this.align_){
						case AreaIdMarker.NorthWest :
							anchor.x -= width;
							anchor.y -= height;
							break;
						case AreaIdMarker.SouthWest :
							anchor.x -= width;
							anchor.y += height;
							break;
						case AreaIdMarker.NorthEast :
							anchor.y -= height;
							break;
						case AreaIdMarker.Center :
							anchor.x -= (width / 2);
							anchor.y -= (height / 2); 
							break;
						default: //SouthEast
							break;
					}
					
					this.div_.style.top = anchor.y + "px";
					this.div_.style.left = anchor.x + "px";	
					
					if(this.showOnDraw_ == true){
						this.div_.style.visibility = "visible";
						this.showOnDraw_ == false;
					}
				}catch(e){
					alert(e.name + " - " + e.message);
				}
			};
			
			AreaIdMarker.prototype.onRemove = function(){
				try{
					if(this.div){
						this.div_.style.visibility = "hidden";
					}
					this.div_.parentNode.removeChild(this.div_);
					this.div_ = null;
				}catch(e){
					alert(e.name + " - " + e.message);
				}
			};
			
			AreaIdMarker.prototype.show = function(){
				try{
					if(this.div_){ 
						this.div_.style.visibility = "visible"; 
					}else{
						this.showOnDraw_ = true;
					}
				}catch(e){
					alert(e.name + " - " + e.message);
				}
			};
			
			AreaIdMarker.prototype.hide = function(){
				try{
					if(this.div_){ 
						this.div_.style.visibility = "hidden"; 
					}
				}catch(e){
					alert(e.name + " - " + e.message);
				}
			};


		
		/**
		*  MarkerHighlight class
		*  Copyright (c) 2011, RealPageMaker.com 
		*  Simple overlay view class for displaying 
		*  Author: AA
		*/
		
		function MarkerHighlight(latlng){
			this.position_ = latlng;
			this.showOnDraw_ = false;
			this.debug = false;
			
			this.img_ = null;
			this.div_ = null;
		}
		
			MarkerHighlight.prototype = new google.maps.OverlayView();
			
			MarkerHighlight.prototype.onAdd = function(){
				try{
					var img = document.createElement("img");
					img.setAttribute("src", config.resLink + "/t/resources/rpm3.0/images/icons/searchEngine/hl.png");
					
					var div = document.createElement("div");
					div.style.position = "absolute";
					div.style.width = "26px";
					div.style.height = "36px";
					div.style.visibility = "hidden";
					div.style.zIndex = "2000";
					div.appendChild(img);
					
					this.div_ = div;
					this.getPanes().overlayShadow.appendChild(this.div_);
				}catch(e){
					alert(e.name + " - " + e.message);
				}
			};
			
			MarkerHighlight.prototype.draw = function(){
				try{
					var prj = this.getProjection();
					var pnt = prj.fromLatLngToDivPixel(this.position_);
					
					pnt.x -= 13;
					pnt.y -= 18.5;
					
					this.div_.style.top = pnt.y + "px";
					this.div_.style.left = pnt.x + "px";
					
					if(this.showOnDraw_){
						this.div_.style.visibility = "visible";
						this.showOnDraw_ = false;
					}
				}catch(e){
					alert(e.name + " - " + e.message);
				}
			};
			
			MarkerHighlight.prototype.onRemove = function(){
				try{
					this.div_.style.visibility = "hidden";
					this.div_.parentNode.removeChild(this.div_);
					this.div_ = null;
					this.img_ = null;
				}catch(e){
					alert(e.name + " - " + e.message);
				}
			};
			
			MarkerHighlight.prototype.show = function(){
				if(this.div_){
					this.div_.style.visibility = "visible";
				}else{
					this.showOnDraw_ = true;
				}
			};
			
			MarkerHighlight.prototype.hide = function(){
				if(this.div_){
					this.div_.style.visibility = "hidden";
				}
			};



		/** Listing
		  *
		  * @param listNum unique listing number for this listing
		  * @param lat the lattitude for this Listing
		  * @param lng the longitude for this Listing
		  * @param mCol the color of the marker
		  * @param mlsNumber unique MLS Number for this listing
		  * @param price the price
		  * @param labelValuePairs the information we want to display
		  * @param mainPhoto the 'img' tag for the main photo
		  * @param picCount the additional photos available for this listing
		  * @param listingAgentsString the "listed by" quote for the listing
		  */
		function Listing(listnum, lat, lng, mCol, mlsNumber, price, mainPhoto, picCount, listingAgentsString, labelValuePairs){
			this.listnum = listnum;
			this.lat = lat;
			this.lng = lng;
			this.mCol = mCol;
			this.onMap = false;
			this.highlight = null;
			this.polygon = null; //Testing, delete when done;

			this.mlsNumber = mlsNumber;
			this.price = price;
			this.labelValuePairs = labelValuePairs;
			this.mainPhoto = mainPhoto;
			this.picCount = picCount;
			if(this.picCount > 0)
				this.numberOfAdditionalPhotos = picCount - 1;
			else
				this.numberOfAdditionalPhotos = 0;
			this.listingAgentsString = listingAgentsString;
		}
	
		/** gets the ListingIcon object for this listing depending on searchType and status. Note that this function
		  * will need to be edited to decouple the listing icon from searchType, and re-couple with property type.
		  *
		  */
		Listing.prototype.getMarkerImage = function(){
			var imgUrl = config.resLink + "/t/resources/rpm3.0/images/icons/searchEngine/";
			switch(this.mCol){
				case "Blue" :
					imgUrl += "new_"+config.searchTypes[config.mlsSearchType].icon+".png"; //Blue (New) TODO: Calculate if listing is new
					break;
				case "Green" :
					imgUrl += "active_"+config.searchTypes[config.mlsSearchType].icon+".png"; //Green (Active)
					break;
				case "Yellow" :
					imgUrl += "pending_"+config.searchTypes[config.mlsSearchType].icon+".png";  //Yellow (Pending)
					break;
				default:
					imgUrl += "sold_"+config.searchTypes[config.mlsSearchType].icon+".png"; //Red (Sold)
					break;
			}
			
			return new google.maps.MarkerImage(
				imgUrl,
				new google.maps.Size(18, 29),  //Size
				new google.maps.Point(0, 0), //Origin
				new google.maps.Point(9, 29)  //Anchor
			);
		};
	
		Listing.prototype.getMarkerImageShadow = function(){	
			return new google.maps.MarkerImage(
				config.resLink + "/t/resources/rpm3.0/images/icons/searchEngine/shadow.png",
				new google.maps.Size(28, 38),  //Size
				new google.maps.Point(0, 0), //Origin
				new google.maps.Point(9, 29)  //Anchor
			);
		};
	
		Listing.prototype.getMarkerImageHighlight = function(){
			return new google.maps.MarkerImage(
				config.resLink + "/t/resources/rpm3.0/images/icons/searchEngine/glow.png",
				new google.maps.Size(50, 50),  //Size - added 4 px to all, may need to fiddle with it...
				new google.maps.Point(0, 0), //Origin
				new google.maps.Point(30, 48)  //Anchor
			);
		};
	
		Listing.prototype.getMarkerShape = function(){
			return {
				coord: [0,0,18,0,18,15,11,20,10,26,8,26,7,21,0,15],
				type: 'poly'
			};
		}
	
		Listing.prototype.setShape = function(){
			if(this.marker != null){
				this.marker.setShape(this.getMarkerShape);
			}
		}
	
		/** 
		*  Adds a Listing to the map
		*/
		Listing.prototype.addToMap = function(){
			var x = this;
	
			if(this.marker == null){
				var markerOpts = {
					map : config.map.getMap(),
					position : new google.maps.LatLng(this.lat, this.lng),
					icon : this.getMarkerImage(),
					visible : true,
					zIndex : 2,
					shadow : this.getMarkerImageShadow(),
					shape : this.getMarkerShape()
				};
				
				this.marker = new google.maps.Marker(markerOpts);
			}
			
			this.onMap = true;
			
			//Prevent zooming when double clicking or clicking on markers
			google.maps.event.addListener(this.marker, "dblclick", function(){});
			google.maps.event.addListener(this.marker, "click", function(){});
			
			this.setMOClasses();
			
			google.maps.event.addListener(this.marker, "mouseover", function(){
				x.openHTMLWindow();
			});
			
			//var shadow = this.getMarkerImageShadow();
			google.maps.event.addListener(this.marker, "mouseout", function(){});
		};

		/** 
		*  Removes this listing from the map
		*/
		Listing.prototype.removeFromMap = function(){
			if(this.polygon != null){ this.polygon.setMap(null); }
			
			if(this.marker != null){
				google.maps.event.clearInstanceListeners(this.marker);
				this.marker.setMap(null); 
			}
			
			this.polygon = null;
			this.marker = null;
			this.onMap = false;
		};
	
		/**
		* Required because the tooltip needs a new lat/lng to anchor to when zoom changed or it'll still
		* use the old one (which is incorrect)
		*/
		Listing.prototype.setMOClasses = function(){
			var bounds = this.getBounds();
			
			if(this.highlight != null){
				this.highlight.setMap(null);
				this.highlight = null;
			}
			
			this.highlight = new MarkerHighlight(bounds.getCenter());
			this.highlight.setMap(config.map.getMap());
		};
			
		/** 
		* Returns the marker
		*/
		Listing.prototype.getMarker = function(){
			return this.marker;
		};
	
		/**
		* 
		*/
		Listing.prototype.getBounds = function(){
			var zL = Math.pow(2, config.map.getMap().getZoom());
			var wCP = config.map.getMap().getProjection().fromLatLngToPoint(new google.maps.LatLng(this.lat, this.lng));
			var pCP = new google.maps.Point((wCP.x * zL), (wCP.y * zL));
			var nEC = config.map.getMap().getProjection().fromPointToLatLng(new google.maps.Point(((pCP.x + 9) / zL), ((pCP.y - 29) / zL)));
			var sWC = config.map.getMap().getProjection().fromPointToLatLng(new google.maps.Point(((pCP.x - 9) / zL), wCP.y));
	
			return new google.maps.LatLngBounds(sWC, nEC);
		};
	
		/**
		* Debug function for outlining clickable area of icon
		*/
		Listing.prototype.showBounds = function() {
			if(this.polygon != null){ this.polygon.setMap(null); }
			
			var rectOptions = {
				strokeColor: "#FF0000",
				strokeOpacity: 0.8,
				strokeWeight: 2,
				fillColor: "#FF0000",
				fillOpacity: 0.35,
				map: config.map.getMap(),
				bounds: this.getBounds()
			};
			
			this.polygon = new google.maps.Rectangle();
			this.polygon.setOptions(rectOptions);
		};

		/** 
		*  Opens a summary view for this listing
		*  NOTE: this = this.marker
		*/
		Listing.prototype.openHTMLWindow = function(){
			var x = this;

			/* IDX LISTINGS INFO WINDOW RESULTS */
			var listingData = "";
			listingData += '  <div class="cs-idx-search-info-window-container">';
			listingData += '    <div class="cs-idx-search-info-window-result-single" id="ln_'+this.listnum+'">';
			listingData +=        this.generateHTML();
			listingData += '    </div>';
			listingData += '  </div>';
			/* END IDX LISTINGS INFO WINDOW RESULTS */

			// make sure the "multiple results" window is hidden
			$('#cs-idx-search-results-info-window', config.self).html('').hide();

			// find the position of the marker
			var markerPosition = config.map.getMarkerPosition(this.marker);
			var positionX = markerPosition.x;
			var positionY = markerPosition.y - 15;

			// we need to make sure the HTML Window doesn't open-up outside the module (or we could trigger a horizontal/vertical scroll)
			if(positionX + $('#cs-idx-search-result-info-window', config.self).width() > $('.cs-idx-search-container', config.self).width()){
				$('#cs-idx-search-result-info-window', config.self).css('left', positionX - $('#cs-idx-search-result-info-window', config.self).width());
			}else{
				$('#cs-idx-search-result-info-window', config.self).css('left', positionX);
			}

			var remainingSpaceY = positionY + MAP_RESULT_CONTAINER_MAX_DISTANCE_ABOVE_MAP;
			if($('#cs-idx-search-result-info-window', config.self).height() > remainingSpaceY){
				$('#cs-idx-search-result-info-window', config.self).css('top', -MAP_RESULT_CONTAINER_MAX_DISTANCE_ABOVE_MAP);
			}else{
				$('#cs-idx-search-result-info-window', config.self).css('top', remainingSpaceY - MAP_RESULT_CONTAINER_MAX_DISTANCE_ABOVE_MAP - $('#cs-idx-search-result-info-window', config.self).height());  // display above
			}

			$('#cs-idx-search-result-info-window', config.self).html("").show();

			$('#cs-idx-search-result-info-window', config.self).html(listingData);
					
			$(".cs-idx-search-info-window-container", config.self).show('fast', function(){
				$('#cs-idx-search-result-info-window', config.self).animate({
					height: $(".cs-idx-search-info-window-container", config.self).height()
					}, 100, function() {
						// Animation complete.
					});
			});

			$('#cs-map-search-canvas', config.self).bind("mouseover", function(){
				$('#cs-idx-search-result-info-window', config.self).hide();
				$('#cs-map-search-canvas', config.self).unbind("mouseover");
			});
		};
		
		/** 
		*  Generates the HTML for this listing
		*  
		*/
		Listing.prototype.generateHTML = function(){
			function isBlank(val){
				if(val == null || val == '')
					return true;
				else
					return false;
			}

			var listingHTML = "";

			listingHTML += '      <div class="cs-idx-search-info-window-result-id">';
			if(!isBlank(this.mlsNumber)){
				listingHTML += '        <a onclick="$(\'#modletWrapper_'+config.modletId+'\').csIDXSearchEngine(\'listingDetails\', '+this.listnum+'); return false;" href="#">';
				listingHTML += '          MLS<sup>&reg;</sup>: '+this.mlsNumber;
				listingHTML += '        </a>';
			}
			/* !!MUST KEEP THE NEXT LINE INLINE OR THE LAYOUT WILL BREAK!! */
			listingHTML += '      </div><div class="cs-idx-search-info-window-result-price">';
			/* ok - now you can do whatever you want */
			listingHTML += '        '+this.price;
			listingHTML += '      </div>';
			listingHTML += '      <div class="cs-idx-search-info-window-result-left">';
			listingHTML += '        <dl class="cs-idx-search-info-window-result-tabular-info">';
			$.each(this.labelValuePairs, function(index, value){
				listingHTML += '          <dt>'+index+':</dt><dd>'+value+'</dd>';
			});
			listingHTML += '        </dl>';
			/* !!MUST KEEP THE NEXT LINE INLINE OR THE LAYOUT WILL BREAK!! */
			listingHTML += '      </div><div class="cs-idx-search-info-window-result-right">';
			/* ok - now you can do whatever you want */
			listingHTML += '        <a onclick="$(\'#modletWrapper_'+config.modletId+'\').csIDXSearchEngine(\'listingDetails\', '+this.listnum+'); return false;" href="#" class="cs-photo-container">';

			if(this.picCount > 0){
				listingHTML += '            <img src="'+this.mainPhoto+'" class="cs-photo-container-wrap"/>';
				if(this.numberOfAdditionalPhotos > 1)
					listingHTML += '            '+this.numberOfAdditionalPhotos+' more photos';
				else
					listingHTML += '          1 more photo';
			}else
				listingHTML += '            <img src="'+config.resLink+'/t/resources/rpm3.0/images/search_engine/no_photo.jpg" class="cs-photo-container-wrap"/>';
			listingHTML += '        </a>';
			listingHTML += '      </div>';
			if(!isBlank(this.listingAgentsString)){
				listingHTML += '      <div class="cs-idx-search-info-window-result-agent">Listed By '+this.listingAgentsString+'</div>';
			}

			return listingHTML;
		}


		/**
		*  represents a cluster of listings on the map.
		*/
		function ListingCluster(listings){
			this.marker = null;
			this.listings = listings; // Hashtable of Listing objects
			this.padding = 0;
			this.onMap = false;
			this.lat = null;
			this.lng = null;
			this.highlight = null;
			this.polygon = null; //For testing, delete when done
		}
		
			/**
			*  Gets the ListingIcon object for this listing depending on searchType and status. Note that this function
			*  will need to be edited to decouple the listing icon from searchType, and re-couple with property type.
			*  @return ListingIcon
			*/
			ListingCluster.prototype.getMarkerImage = function(){
				this.listings.moveFirst();
				var imgUrl = config.resLink + "/t/resources/rpm3.0/images/icons/searchEngine/";
				while(this.listings.next()){
					if(this.listings.getValue() != null){
						switch(this.listings.getValue().mCol){
							case "Blue":
								imgUrl += "new_"+config.searchTypes[config.mlsSearchType].icon+"_m.png"; //Blue (New) TODO: Calculate if listing is new
								break;
							case "Green":
								imgUrl += "active_"+config.searchTypes[config.mlsSearchType].icon+"_m.png"; //Green (Active)
								break;
							case "Yellow":
								imgUrl += "pending_"+config.searchTypes[config.mlsSearchType].icon+"_m.png";  //Yellow (Pending)
								break;
							default:
								imgUrl += "sold_"+config.searchTypes[config.mlsSearchType].icon+"_m.png"; //Red (Sold)
								break;
						}
						break;
					}
				}
							
				return new google.maps.MarkerImage(
					imgUrl,
					new google.maps.Size(18, 29),  //Size
					new google.maps.Point(0, 0), //Origin
					new google.maps.Point(9, 29)  //Anchor
				);
			};

			ListingCluster.prototype.getMarkerImageShadow = function(){
				return new google.maps.MarkerImage(
					config.resLink + "/t/resources/rpm3.0/images/icons/searchEngine/shadow.png",
					new google.maps.Size(28, 38),  //Size
					new google.maps.Point(0, 0), //Origin
					new google.maps.Point(9, 29)  //Anchor
				);
			};
		
			ListingCluster.prototype.getMarkerImageHighlight = function(){
				return new google.maps.MarkerImage(
					config.resLink + "/t/resources/rpm3.0/images/icons/searchEngine/glow.png",
					new google.maps.Size(50, 50),  //Size - added 4 px to all, may need to fiddle with it...
					new google.maps.Point(0, 0), //Origin
					new google.maps.Point(30, 48)  //Anchor
				);
			};
		
			ListingCluster.prototype.getMarkerShape = function(){
				return {
					coord: [0,0,18,0,18,26,8,26,5,21,5,18,0,17,0,11],
					type: 'poly'
				};
			}
		
			ListingCluster.prototype.markerBounds = function() {
				try{
					var zL = Math.pow(2, config.map.getMap().getZoom());
					var wCP = config.map.getMap().getProjection().fromLatLngToPoint(this.markerCenter());
					var pCP = new google.maps.Point((wCP.x * zL), (wCP.y * zL));
					var nEC = config.map.getMap().getProjection().fromPointToLatLng(new google.maps.Point(((pCP.x + 9) / zL), ((pCP.y - 29) / zL)));
					var sWC = config.map.getMap().getProjection().fromPointToLatLng(new google.maps.Point(((pCP.x - 9) / zL), wCP.y));
		
					return new google.maps.LatLngBounds(sWC, nEC);
				}catch(e){
					alert(e.name + " - " + e.message);
					return null;
				}
			};
					
			//Function used for testing purposes
			ListingCluster.prototype.showBounds = function() {
				if(this.polygon != null){ this.polygon.setMap(null); }
				
				var rectOptions = {
					strokeColor: "#FF0000",
					strokeOpacity: 0.8,
					strokeWeight: 2,
					fillColor: "#FF0000",
					fillOpacity: 0.35,
					map: config.map.getMap(),
					bounds: this.markerBounds()
				};
				
				this.polygon = new google.maps.Rectangle();
				this.polygon.setOptions(rectOptions);
			};
					
			ListingCluster.prototype.markerCenter = function() {
				var mLat = new Number(0), mLng = new Number(0);
				this.listings.moveFirst();
				while(this.listings.next()){
					mLat = mLat + new Number(this.listings.getValue().lat);
					mLng = mLng + new Number(this.listings.getValue().lng);
				}
				
				mLat = mLat / new Number(this.listings.size());
				mLng = mLng / new Number(this.listings.size());
							
				return new google.maps.LatLng(mLat, mLng);
			};
		
		
			ListingCluster.prototype.getMarkerBounds = function() {
				if(this.marker != null){
					this.marker.setPosition(this.markerCenter());
				}
				return this.markerBounds();
			};
		
			ListingCluster.prototype.resetMarkerPosition = function() {
				this.removeMarker();
				this.marker.setPosition(this.markerCenter());
				this.marker.setMap(config.map.getMap());
				this.marker.setShape(this.getMarkerShape());
				this.initMarkerEvents();
			};
		
			ListingCluster.prototype.initializeMarker = function() {
				try{
					if(this.listings.size() > 1){
						if(this.marker == null){
							this.marker = new google.maps.Marker({
								map : config.map.getMap(),
								position : this.markerCenter(), 
								visible : true,
								zIndex : 2,
								icon : this.getMarkerImage(),
								shadow : this.getMarkerImageShadow(),
								shape : this.getMarkerShape()
							});
		
							this.lat = this.marker.getPosition().lat();
							this.lng = this.marker.getPosition().lng();
						}
											
						this.marker.setMap(config.map.getMap());
						this.onMap = true;
						//this.showBounds(); //Debug
						this.initMarkerEvents();				
					}
				}catch(e){
					alert("ListingCluster.initializeMarker: " + e.name + " - " + e.message);
				}
			};
		
			ListingCluster.prototype.removeMarker = function() {
				if(this.marker != null){ 
					google.maps.event.clearInstanceListeners(this.marker);
					this.marker.setMap(null);
				}
				
				if(this.highlight != null){ this.highlight.setMap(null); }
				this.highlight = null;
			};
		
			ListingCluster.prototype.destroyMarker = function() {
				this.removeMarker();
				this.marker = null;
				this.listings = null;
				this.onMap = false;
			};
		
			ListingCluster.prototype.initMarkerEvents = function() {
				var x = this;
				if(this.marker != null){
					google.maps.event.addListener(this.marker, "dblclick", function(){});
					google.maps.event.addListener(this.marker, "click", function(){});
					
					if(this.highlight != null){
						this.highlight.setMap(null);
						this.highlight = null;
					}
					
					this.highlight = new MarkerHighlight(this.markerBounds().getCenter());
					this.highlight.setMap(config.map.getMap());
					
					google.maps.event.addListener(this.marker, "mouseover", function(){
						x.openHTMLWindow();
					});
					
					google.maps.event.addListener(this.marker, "mouseout", function(){
					});
					
				}
			};
			/**
			* TODO: Implement code & view for multiple listings
			*/
			ListingCluster.prototype.openHTMLWindow = function(selectedId){
				var x = this;
				
				var count = 0;
				var listingData = "";
				this.listings.moveFirst();
				while(this.listings.next()){
					if(this.listings.getValue() != null){
						/* IDX LISTINGS INFO WINDOW RESULTS */
						var listing = this.listings.getValue();
						listingData += '  <div class="cs-idx-search-info-window-container">';
						listingData += '    <div class="cs-idx-search-info-window-result" id="ln_'+listing.listingNumber+'">';
						listingData +=        listing.generateHTML();
						listingData += '    </div>';
						listingData += '  </div>';
						/* END IDX LISTINGS INFO WINDOW RESULTS */
						count++;
					}
				}

				// if we've got no listings, then don't do anything (this shouldn't happen)
				if(count == 0) return;
				
				// make sure the "single result" window is hidden
				$('#cs-idx-search-result-info-window', config.self).html('').hide();

				// find the position of the marker
				var markerPosition = config.map.getMarkerPosition(this.marker);
				var positionX = markerPosition.x;
				var positionY = markerPosition.y - 15;
	
				// we need to make sure the HTML Window doesn't open-up outside the module (or we could trigger a horizontal/vertical scroll)
				if(positionX + $('#cs-idx-search-results-info-window', config.self).width() > $('.cs-idx-search-container', config.self).width()){
					$('#cs-idx-search-results-info-window', config.self).css('left', positionX - $('#cs-idx-search-results-info-window', config.self).width());
				}else{
					$('#cs-idx-search-results-info-window', config.self).css('left', positionX);
				}
	
				var remainingSpaceY = positionY + MAP_RESULT_CONTAINER_MAX_DISTANCE_ABOVE_MAP;
				if($('#cs-idx-search-results-info-window', config.self).height() > remainingSpaceY){
					$('#cs-idx-search-results-info-window', config.self).css('top', -MAP_RESULT_CONTAINER_MAX_DISTANCE_ABOVE_MAP);
				}else{
					$('#cs-idx-search-results-info-window', config.self).css('top', remainingSpaceY - MAP_RESULT_CONTAINER_MAX_DISTANCE_ABOVE_MAP - $('#cs-idx-search-results-info-window', config.self).height());  // display above
				}

				$('#cs-idx-search-results-info-window', config.self).html("").show();


				$('#cs-idx-search-results-info-window', config.self).html(listingData).each(function(){
					$(".cs-idx-search-info-window-container", this).fadeIn();
				});
				$('#cs-map-search-canvas', config.self).bind("mouseover", function(){
					$('#cs-idx-search-results-info-window', config.self).hide();
					$('#cs-map-search-canvas', config.self).unbind("mouseover");
				});
		
			};
				
			ListingCluster.prototype.listingExists = function(listnum) {
				this.listings.moveFirst();
				while(this.listings.next()){
					if(this.listings.getKey() == listnum){
						return true;
					}
				}
				return false;
			};
		
			ListingCluster.prototype.removeListing = function(listnum) {
				if(this.listings.size() == 0) return false;
				var size = this.listings.size();
				this.listings.remove(listnum);
				if(this.listings.size() != size){ //Item Removed
					return true;
				}else{ //No Change
					return false;
				}
			};
		
			ListingCluster.prototype.addListing = function(listnum, listingData) {
				if(!this.listingExists(listnum)){
					this.listings.put(listnum, listingData);
				}
			};
		
			ListingCluster.prototype.isEmpty = function() {
				if(this.listings == null){
					return true;
				}else{
					if(this.listings.size() == 0){
						return true;
					}else{
						return false;
					}
				}
			};
		
			ListingCluster.prototype.size = function() {
				return this.listings.size();
			};
		
			ListingCluster.prototype.listingsArray = function() {
				var a = [];
				this.listings.moveFirst();
				while(this.listings.next()){
					a.push(this.listings.getValue());
				}
				return a;
			}
		/**
		*  Controller for IDX Search Map View
		*  
		* @param self the context where we're calling the object from
		*/
		function MarketAnalyzerPanel(self) {
			this.displaying = false;
			this.self = self;
			var x = this;
			//Configure tab events
			$(".cs-idx-view-market-analyzer", this.self).click(function(event) {
				if(config.searchTypes[$('#searchTypes', x.self).val()].showStats.toLowerCase() == "true" && config.currentSearchView != config.searchViews.stats && config.mapLoaded){
					x.load();
				}
				return false;
			});
			
			this.lastLoadedTab; // we need to store this in case a VIP request needs rolling back
			
			/**
			  * Private function that loads the market analyzer tab
			  *
			  */
			this.loadTab = function(){
				if(x.displaying){
					$("#statisticsPanelLoadingOverlay", x.self).block({message: "<div style='font-size:2em;font-weight:bold;'>Loading...</div><img src='t/resources/rpm3.0/images/search_engine/map-ajax-loader.gif' />"});
				}else{
					x.lastLoadedTab = $(".cs-tabs-tab-active", x.self).get();
					$(".cs-tabs-tab-active", x.self).addClass("cs-tabs-tab").removeClass("cs-tabs-tab-active");
					$(".cs-tabs-tab-left", x.self).addClass("cs-tabs-tab-first-loading");
					$('#cs-idx-view-market-analyzer', x.self).parent().addClass("cs-tabs-tab-active").removeClass("cs-tabs-tab");
				}
			}
			/**
			  * Private function that unloads the market analyzer tab and reloads the last-loaded tab (in case a VIPAjax request fails
			  *
			  */
			this.rollbackLoadTab = function(){
				if(x.displaying){
					
				}else{
					$(".cs-tabs-tab-active", x.self).addClass("cs-tabs-tab").removeClass("cs-tabs-tab-active");
					$(".cs-tabs-tab-left", x.self).removeClass("cs-tabs-tab-first-loading");
					$(x.lastLoadedTab).addClass("cs-tabs-tab-active").removeClass("cs-tabs-tab");
				}
			}
		}

			/**
			* Displays real estate market statistics for the given area
			*
			*/
			MarketAnalyzerPanel.prototype.load = function(){
				var x = this;
				try{
					// fetch the statistics data from the server		
					$.csVOWPanel('csVOWAjax', {
						type: "POST",
						url: config.absUrl,
						beforeSend: function(){
							x.loadTab();
						},
						data: "pathway=48&mkt=true&envPropType="+config.mlsSearchType+helpers.getSearchUrlString.call(this.self)+helpers.getSearchCoords.call(this.self),
						dataType: "html",
						async: true,
						error: function(data, error){
							//alert("Error: csIDXSearchEngineMarketStats(): " + error + " " + data);  //SE
						},
						success: function(data){
							config.currentSearchView = config.searchViews.stats;
							$("#cs-idx-search-results-market-analyzer-panel", x.self).html(data);
							x.displaying = true;
						},
						complete: function(xmlhttprequest, successtype){
							config.mapViewPanel.hide();
							config.listViewPanel.unload();
							$("#cs-idx-search-results-market-analyzer:hidden", x.self).show();
							
							// hide the search statistics panel
							config.searchStatisticsPanel.hide()

							// hide the pagination
							config.pagination.hide();

							if(x.displaying){
								$("#statisticsPanelLoadingOverlay", x.self).unblock();
							}
							
							// make the graphs
							$("#listedVersusSold", x.self).clickSoldUtils("renderListedVersusSoldGraph", listedVersusSold.MLSStatsListedVersusSold);
							$("#daysOnMarket", x.self).clickSoldUtils("renderDOMGraph", daysOnMarket.MLSStatsDOMs);

							statsAjaxActivity = null;
							x.displaying = true;
							$(".cs-tabs-tab-left", this.self).removeClass("cs-tabs-tab-first-loading");
							
							$(".cs-idx-view-map", this.self).parent("li").attr("class", "cs-tabs-tab");
							$(".cs-idx-view-list", this.self).parent("li").attr("class", "cs-tabs-tab");
							$(".cs-idx-view-market-analyzer", this.self).parent("li").attr("class", "cs-tabs-tab-active");
							
							return false;
						}, 
						failComplete: function(xmlhttprequest, successtype){
							/*
							if(initLoad){
								$("#statsView", x.self).html(statsTabLabel);
							}else{
								$("#statisticsPanelLoadingOverlay", x.self).unblock();
								config.refactorMap = true;
								$("#cs-idx-view-map", x.self).click();
							}
							statsAjaxActivity = null;
							*/
						}
					}, 
					function(){}, 
					function(){
						// rollback the operation
						x.rollbackLoadTab();						
					}, true);
				}catch(ex){
					alert("marketStats error\n" + ex.name + " - " + ex.message); 
				}
			}
			
			/**
			* Hides and depopulates the market analyzer view view
			*
			* @param after callback function that is executed once the view is loaded
			* @return the result of the callback if a callback is provided, else true
			*/
			MarketAnalyzerPanel.prototype.unload = function(after){
				this.displaying = false;
				$("#cs-idx-search-results-market-analyzer-panel", this.self).html("");
			}


		/**
		* Controller for IDX Search Map View
		*  
		* @param self the context where we're calling the object from
		*/
		function MapViewPanel(self) {
			// tells whether the listings summary view is displaying
			this.displaying = true;
			this.self = self;
			var x = this;

			//Configure tab events
			$(".cs-idx-view-map", this.self).click(function(){
				if(config.currentSearchView != config.searchViews.map){
					if(config.currentSearchView == config.searchViews.list){
						config.currentListingsSort = $("#cs-idx-search-sort:eq(0)", this.self).val();
						
						// if the last view was the list, then we need to update the "hidden" listings in the pagination
						config.pagination.insertHiddenCount();
					}
										
					config.currentSearchView = config.searchViews.map;

					// hide the currently selected tab
					$(".cs-tabs-tab-active", this.self).addClass("cs-tabs-tab").removeClass("cs-tabs-tab-active");

					// make the map tab the selected tab
					$(this).parent().addClass("cs-tabs-tab-active").removeClass("cs-tabs-tab");

					$("#cs-map-search-canvas", this.self).show();
					
					// make sure the statistics panel is shown if necessary
					config.searchStatisticsPanel.show();

					// make sure the pagination is shown
					config.pagination.show(0);
					config.pagination.hide(1);  //Hide the second pagination bar

					config.map.checkResize();
					if(config.refactorMap == true){
						config.refactorMap = false;
						helpers.runSearch.call(this.self, config.currentPage, false);
					}

					// hide the listings results page (if necessary)
					if(config.listViewPanel != null)
						config.listViewPanel.unload();

					// hide the market analyzer panel (if necessary)
					if(config.marketAnalyzerPanel != null)
						config.marketAnalyzerPanel.unload();
				}
				return false;
			});
			
			// create the map object
			config.map = new Map(
				this.self,
				config.centerCoords,
				config.maxResults,
				config.zoomLevel,
				config.absUrl,
				config.mapDiv
			);

			$(window).load(function(){
				// This event fires when the map view is on listings, and when the map is
				// moved we want to display those listings that fall within the boundaries of the map.
				// This fires on map drag
				google.maps.event.addListener(config.map.getMap(), "dragend", function(){
					if(!config.map.savedPosition){
						config.map.removeAreaLabel = false;
						helpers.runSearch.call(x.self);
					}
				});
				
				// If the map is zoomed then reload results
				google.maps.event.addListener(config.map.getMap(), "zoom_changed", function(){
					helpers.runSearch.call(x.self);
				});
			});


			/**
			* Used to execute the actual map-search. Private method, and only accessible by calling MapViewPanel.load.
			*
			* @param startNumber starting number of the search results
			* @param incremental boolean indicating whether we're stepping through results or not. Affects whether stats are processed (false=process, true=don't process).
			* @param sort the sort value from the result sorting drop-down
			*/
			this.runSearchCall = function(startNumber, incremental, sort){
				var x = this;
				try{
					$.csVOWPanel('csVOWAjax', {
						type: "POST",
						url: config.absUrl,
						data: "pathway=5&search=1&envPropType="+config.mlsSearchType+helpers.getSearchUrlString.call(this)+"&startNumber="+startNumber+helpers.getSearchCoords.call(this)+"&zoomLevel="+config.map.getZoom()+"&listingsSort="+sort+"&searchViewType="+config.currentSearchView,
						dataType: "json",
						async: true,
						error: function(data, error){
							//$.clickSoldUtils('csConsole', 'MapViewPanel.runSearchCall error:' + error + " - " + error.message);
						},
						success: function(data){
							// add listings
							config.map.addListings(data);

							// hide loading message
							config.map.hideLoadingMessage();

							// indicate that the map is loaded
							config.mapLoaded = true;

							// only set the indicator to null if the request was successful. DON'T MOVE TO THE 
							// "complete" section as anything in there gets executed when .abort() is called.
							searchAjaxActivity = null;

							// show the search stats
							if(config.searchTypes[config.mlsSearchType].showStats.toLowerCase() == "true" && !incremental){ 
								config.searchStatisticsPanel.load(); 
							}
						},
						complete: function(xmlhttprequest, successtype){}
					
					}, 
					function(data){},
					function(data){}, true);
				}catch(ex){
					$.clickSoldUtils('csConsole', 'MapViewPanel.runSearchCall error\n' + ex.name + " - " + ex.message);
					searchAjaxActivity = null;
				}
			}
		}
		
			/**
			* Loads and reloads the map results view
			*
			* @param int sNumber starting number for the results
			* @param boolean incremental whether the results are incremental or not
			* @param after callback function that is executed once the view is loaded
			* @return the result of the callback if a callback is provided, else true
			*/
			MapViewPanel.prototype.load = function(sNumber, incremental, after){
				var x = this;
				config.mapLoaded = false;

				// show the map panel if it's hidden
				$("#cs-map-search-canvas:hidden", this.self).show().each(function(){
					this.displaying = true;
				});
				
				if(incremental == null){ incremental = false; }
				
				// show the stats-loading icons if necessary
				if(config.searchTypes[config.mlsSearchType].showStats.toLowerCase() == "true" && !incremental){
					config.searchStatisticsPanel.showLoadingIcons();
				}
				
				// show saved search button
				$('#savedSearchMessage:not(:hidden)', this.self).hide();
				$('#saveSearchButton:hidden', this.self).show();
				
				// get available sorting option
				var sort;
				if($(".cs-pagination-bar-select:eq(0)", this.self).length > 0){
					sort = $(".cs-pagination-bar-select:eq(0)", this.self).val();
				}
				
				// verify the starting number
				if(sNumber == null)
					sNumber = 0;
				
				// Save global vars
				config.currentNEBounds = config.map.getNortheastBounds();
				config.currentSWBounds = config.map.getSouthwestBounds();
				config.currentPage = sNumber;
				
				// show loading message
				config.map.showLoadingMessage();
				
				// Cancel any pending search.
				clearTimeout(mlsSearchOP_timeout_id);

				// Check if a search is in progress.
				if(searchAjaxActivity != null) {
					searchAjaxActivity.abort(); // If it's in progress kill it.
				
					// Increase the delay (up to the max) -- as a penalty for running to many searches too quickly.
					mlsSearchOP_current_delay = mlsSearchOP_current_delay + mlsSearchOP_delay_up_step;
					if(mlsSearchOP_current_delay > mlsSearchOP_delay_max) { mlsSearchOP_current_delay = mlsSearchOP_delay_max; }

					// Schedule the search to be executed after the current timeout.
					mlsSearchOP_timeout_id = setTimeout(function () {
						x.runSearchCall.call(x.self, sNumber, incremental, sort); 
					}, mlsSearchOP_current_delay * 1000);
				} else {
					// Decrease the delay.
					mlsSearchOP_current_delay = mlsSearchOP_current_delay - mlsSearchOP_delay_down_step;
					if(mlsSearchOP_current_delay < 0) { mlsSearchOP_current_delay = 0; }
					
					// Schedule the search to be executed after the current timeout.
					mlsSearchOP_timeout_id = setTimeout(function () {
						x.runSearchCall.call(x.self, sNumber, incremental, sort); 
					}, mlsSearchOP_current_delay * 1000);
				}
			}
			/**
			* Hides the map panel
			*
			*/
			MapViewPanel.prototype.hide = function(){
				// hide the map panel if it's shown
				$("#cs-map-search-canvas:visible", this.self).hide().each(function(){
					this.displaying = false;
				});
				
				// hide any summary windows if necessary
				$('#cs-idx-search-results-info-window', config.self).hide();
				$('#cs-idx-search-result-info-window', config.self).hide();


			}

		/**
		*  Controller for IDX Search List View
		*  
		* @param self the context where we're calling the object from
		*/
		function ListViewPanel(self) {
			// tells whether the listings summary view is displaying
			this.displaying = false;
			this.self = self;
			var x = this;

			

			// bind the event that loads the "list view" version of the results.
			$(".cs-idx-view-list", self).click(function(){
				if(config.currentSearchView != config.searchViews.list && config.mapLoaded){
					config.currentListingsSort = $(".cs-pagination-bar-select:eq(0)", self).val();
				
					config.currentSearchView = config.searchViews.list;
					config.currentNEBounds = config.map.getNortheastBounds(); //Needs to be set here because IE9's dead code elimination will give us the value of the last search
					config.currentSWBounds = config.map.getSouthwestBounds(); //Ditto above

					// load the list view panel
					config.listViewPanel.load(config.currentPage, true);
				}
				return false;
			});

			/**
			* Used to execute the actual list-search. Private method, and only accessible by calling ListViewPanel.load.
			*
			* @param startNumber starting number of the search results
			* @param incremental boolean indicating whether we're stepping through results or not. Affects whether stats are processed (false=process, true=don't process).
			*/
			this.runSearchCall = function(startNumber, incremental){
				// initiate new search
				try{
					$.csVOWPanel('csVOWAjax', {
						type: "POST",
						url: config.absUrl,
						data: "pathway=5&list=1&modletId=" + config.modletId + "&envPropType="+config.mlsSearchType+helpers.getSearchUrlString.call(x.self)+"&startNumber="+startNumber+helpers.getSearchCoords.call(x.self)+"&listingsSort="+config.currentListingsSort+"&searchViewType="+config.currentSearchView+"&zoomLevel="+config.map.getZoom(),
						dataType: "html",
						cache: false,
						async: true,
						error: function(data, error){
							$.clickSoldUtils('csConsole', 'ListViewPanel.load error:' + error + " - " + error.message);
						},
						success: function(data){
							$("#cs-idx-search-results-listings-panel").html(data);
		
							// hide the market analyzer panel (if necessary)
							if(config.marketAnalyzerPanel != null) config.marketAnalyzerPanel.unload();
							
							// hide the map if necessary
							config.mapViewPanel.hide();
							
							// make sure the statistics panel is shown if necessary
							config.searchStatisticsPanel.show();
		
							// make sure the pagination is shown
							config.pagination.show();
		
							// show the listings results
							$("#cs-idx-search-results-listings", x.self).css("display", "inline");
		
							// hide the overlay if necessary
							if(x.displaying){ $("#listingsPanelLoadingOverlay", x.self).unblock(); }
							
							// report that current search is finished
							searchAjaxActivity = null;
							
							// set displaying flag to say that the listing results is now displaying
							x.displaying = true;
							
							// need to set mapLoaded to true in case this is the first view to show on page load
							config.mapLoaded = true;
							
							// hide the loading icon
							$(".cs-tabs-tab-left", x.self).removeClass("cs-tabs-tab-first-loading");
							
							// refresh the search stats if necessary
							if(!incremental) config.searchStatisticsPanel.load();
						}
	
						//complete: function(xmlhttprequest, successtype){
						//}
					},
					function(data){},
					function(data){}, true);
				
				}catch(ex){
					alert("marketStats error\n" + ex.name + " - " + ex.message); 
				}
			}
		}
			/**
			* Loads and reloads the listings results view
			*
			* @param int sNumber starting number for the results
			* @param boolean incremental whether the results are incremental or not
			* @param after callback function that is executed once the view is loaded
			* @return the result of the callback if a callback is provided, else true
			*/
			ListViewPanel.prototype.load = function(sNumber, incremental, after){
				var x = this;	// declare a reference to use inside callbacks

				// default to non-incremental search if none is provided
				if(incremental == null){ incremental = false; }
				
				// show the stats-loading icons if necessary
				if(config.searchTypes[config.mlsSearchType].showStats.toLowerCase() == "true" && !incremental){
					config.searchStatisticsPanel.showLoadingIcons();
				}

				// if it's currently displaying then block
				if(this.displaying){
					$("#listingsPanelLoadingOverlay", this.self).block({
						message: '<div style="font-size:2em;font-weight:bold;">Loading...</div><img src="t/resources/rpm3.0/images/search_engine/map-ajax-loader.gif" />',
						centerY: false,
						css: {
							top:'10px'
						}
					});
				}else{
					$(".cs-tabs-tab-active", this.self).addClass("cs-tabs-tab").removeClass("cs-tabs-tab-active");
					$(".cs-tabs-tab-left", this.self).addClass("cs-tabs-tab-first-loading");
					$('.cs-idx-view-list', this.self).parent().addClass("cs-tabs-tab-active").removeClass("cs-tabs-tab");
				}

				// Cancel any pending search and, if interrupting a map search, make sure to set the map to refactor on load.
				// NOTE: this code never seems to be called because the map search seems to override the tab clicks.
				/*if(mlsSearchOP_timeout_id != null && typeof mlsSearchOP_timeout_id == 'function'){
					config.refactorMap = true;
				}*/
				clearTimeout(mlsSearchOP_timeout_id);

				// Check if a search is in progress.
				if(searchAjaxActivity != null) {
					searchAjaxActivity.abort(); // If it's in progress kill it.
				
					// Increase the delay (up to the max) -- as a penalty for running to many searches too quickly.
					mlsSearchOP_current_delay = mlsSearchOP_current_delay + mlsSearchOP_delay_up_step;
					if(mlsSearchOP_current_delay > mlsSearchOP_delay_max) { mlsSearchOP_current_delay = mlsSearchOP_delay_max; }

					// Schedule the search to be executed after the current timeout.
					mlsSearchOP_timeout_id = setTimeout(function () {
						x.runSearchCall.call(x.self, sNumber, incremental); 
					}, mlsSearchOP_current_delay * 1000);
				} else {
					// Decrease the delay.
					mlsSearchOP_current_delay = mlsSearchOP_current_delay - mlsSearchOP_delay_down_step;
					if(mlsSearchOP_current_delay < 0) { mlsSearchOP_current_delay = 0; }
					
					// Schedule the search to be executed after the current timeout.
					mlsSearchOP_timeout_id = setTimeout(function () {
						x.runSearchCall.call(x.self, sNumber, incremental); 
					}, mlsSearchOP_current_delay * 1000);
				}
			}
			/**
			* Hides and depopulates the listings results view
			*
			* @param after callback function that is executed once the view is loaded
			* @return the result of the callback if a callback is provided, else true
			*/
			ListViewPanel.prototype.unload = function(after){
				this.displaying = false;
				$("#cs-idx-search-results-listings-panel", this.self).html("");
			}

		/**
		*  Object for the Search Stats (that sit at the bottom)
		*  
		* @param self the context where we're calling the object from
		*/
		function SearchStatisticsPanel(self) {
			// tells whether the listings summary view is displaying
			this.displaying = true;
			this.self = self;
			
			this.$mapStats_mostExp = $("#mapStats_mostExp", this.self);
			this.$mapStats_leastExp = $("#mapStats_leastExp", this.self);
			this.$mapStats_avg = $("#mapStats_avg", this.self);
			this.$mapStats_new = $("#mapStats_new", this.self);
		}

			/**
			* Loads and reloads the Search Statistics Panel
			*
			* @param after callback function that is executed once the view is loaded
			* @return the result of the callback if a callback is provided, else true
			*/
			SearchStatisticsPanel.prototype.load = function(after){
				var x = this;	// declare a reference to use inside callbacks
				try{
					if(statsAjaxActivity != null) statsAjaxActivity.abort();

					// if the statistics panel is hidden, show it
					x.show();
					
					statsAjaxActivity = $.ajax({
						type: "POST",
						url: config.absUrl,
						data: "pathway=48&map=true&envPropType="+config.mlsSearchType+helpers.getSearchUrlString.call(this.self)+helpers.getSearchCoords.call(this.self),
						dataType: "json",
						async: true,
						error: function(data, error){
							x.$mapStats_mostExp.html("N/A");
							x.$mapStats_leastExp.html("N/A");
							x.$mapStats_avg.html("N/A");
							x.$mapStats_new.html("N/A");
						},
						success: function(data){
							// populate the statistics
							x.$mapStats_mostExp.html(data.maxPrice);
							x.$mapStats_leastExp.html(data.minPrice);
							x.$mapStats_avg.html(data.averagePrice);
							x.$mapStats_new.html(data.listedToday);
						},
						complete: function(xmlhttprequest, successtype){
							statsAjaxActivity = null;
						}
					});
						
				}catch(ex){
					alert("SearchStatisticsPanel.load error\n" + ex.name + " - " + ex.message); 
				}
			}
			/**
			* Shows the statistics panel
			*
			*/
			SearchStatisticsPanel.prototype.show = function(){
				// show the cs-idx-quickstats panel if it's hidden
				if(config.searchTypes[$('#searchTypes', this.self).val()].showStats.toLowerCase() == "true"){
					$("#cs-idx-quickstats:hidden", this.self).show();
				}
			}
			/**
			* Hide the statistics panel
			*
			*/
			SearchStatisticsPanel.prototype.hide = function(){
				// show the cs-idx-quickstats panel if it's hidden
				$("#cs-idx-quickstats:visible", this.self).hide();
			}

			/**
			* Shows the loading icons in the statistics panel
			*
			*/
			SearchStatisticsPanel.prototype.showLoadingIcons = function(){
				this.$mapStats_mostExp.html("<img src=\"" + config.resLink + "/t/resources/rpm3.0/images/activity/se_indicator_small.gif\"  />");
				this.$mapStats_leastExp.html("<img src=\"" + config.resLink + "/t/resources/rpm3.0/images/activity/se_indicator_small.gif\"  />");
				this.$mapStats_avg.html("<img src=\"" + config.resLink + "/t/resources/rpm3.0/images/activity/se_indicator_small.gif\"  />");
				this.$mapStats_new.html("<img src=\"" + config.resLink + "/t/resources/rpm3.0/images/activity/se_indicator_small.gif\"  />");
			}

		/**
		*  Controls pagination for the search results
		*  
		* @param self the context where we're calling the object from
		*
		*/
		function Pagination(self) {
			this.self = self;
			this.$cs_idx_pagination = $('.cs-pagination-bar-paging-mls-search', this.self); // cache the pagination div
			this.$cs_idx_displaying = $('.cs-pagination-bar-displaying-mls-search', self);  // cache the display div
			this.maxResults;
			this.lastMapHiddenCount = null;


			/**
			* Generates the "hidden" text
			*
			* @param hidden integer of the number of hidden listings
			*/
			this.getHiddenCountText = function(hidden){
				if(hidden > 0)
					if(hidden == 1){
						return hidden + " listing unmapped, ";
					}else{
						return hidden + " listings unmapped, ";
					}
				else
					return "";
			}
		}

			/**
			* When a user switches back from listview the number of hidden listings needs to be re-inserted. This function takes
			* care of that.
			*
			*/
			Pagination.prototype.insertHiddenCount = function(){
				var self = this;
				if(this.lastMapHiddenCount != null){
					var displaying = this.$cs_idx_displaying.html();
					this.$cs_idx_displaying.filter(":visible").each(function(){
						$(this).html(displaying.substring(0, displaying.indexOf('(') + 1) + self.getHiddenCountText(self.lastMapHiddenCount) + displaying.substring(displaying.indexOf('(') + 1, displaying.length));
					});
				}
			}

			/**
			* Loads and reloads the pagination
			*
			* @param totalResults - the total number of results (ie. =150 in the following: 101 - 120 of 150)
			* @param maxResults - the maximum number of results displayed on one page of results
			* @param startNumber - the index of the first result (ie. the first result's startNumber would be 0)
			* @param hidden - number of hidden listings
			*/
			Pagination.prototype.load = function(totalResults, maxResults, startNumber, hidden){
				this.maxResults = maxResults;
				var x = this;

				var self = this.self;
				// create the navigation section of the Map Tools
				var listCountSummary;
				var totalPages;
				var totalPagesOutput;
				var hiddenListings = "";
				
				if(totalResults == 0){
					listCountSummary = "No Listings Found";
					$(".pagination", self).css("display", "none");
				}else{

					// calculate the number of data chunks this result set needs to be broken into {REVIEW THIS...}
					var num = Math.round(totalResults/maxResults + 0.4);
					
					// if the result is '0' then increase the result counter by 1
					// we need to do this if we have fewer than 10% of maxResults
					if(num == 0){
						num = 1;
					}else if(totalResults % maxResults != 0){
						num += 1;
					}
					
					var lowerLimit;	// the lower limit of the result window we're showing (ie. =101 in the following: 101 - 120 of 150)
					var upperLimit; // the upper limit of the result window we're showing (ie. =120 in the following: 101 - 120 of 150)
					
					// iterate through the pages to see which page we're on, then paginate
					for(var i = 0; i < num; i++){
						
						lowerLimit = i * maxResults;
						upperLimit = (i+1)*maxResults;
						if(upperLimit > totalResults){
							upperLimit = totalResults;
						}
								
						if(startNumber == lowerLimit){

							// create the text for the pagination display
							if(totalResults <= maxResults){
								totalPages = 1;
								totalPagesOutput = "1 page";
							}else if(totalResults % maxResults == 0){
								totalPages = (totalResults / maxResults);
								totalPagesOutput = totalPages + " pages";
							}else{
								totalPages = parseInt((totalResults / maxResults) + 1, 10);
								totalPagesOutput = totalPages + " pages";
							}
							if(hidden > 0){
								this.lastMapHiddenCount = hidden;
							}
							listCountSummary = "Showing " + (lowerLimit+1) + " to " + upperLimit + " of " + totalResults + " properties (" + this.getHiddenCountText(hidden) + totalPagesOutput + ")";
							
							if(startNumber == 0){
								currentPage = 1;
							}else{
								currentPage = (startNumber/maxResults) + 1;
							}
							$(".pagination", self).css("display", "block");

							// create pagination
							var paginationOpts = {
								callback: function(pageIndex, paginationContainer){
									helpers.runSearch.call(self, (pageIndex * x.maxResults), true);
									return false;
								},
								next_text: '&nbsp;&gt;&nbsp;',
								prev_text: '&nbsp;&lt;&nbsp;',
								num_display_entries: '3',
								num_edge_entries: '1',
								current_page: (currentPage-1),
								items_per_page:1 // Show only one item per page
							};
							
							this.$cs_idx_pagination.each(function(){
								$(this).pagination(totalPages, paginationOpts);
							});
							
							break;
						}
					}
				}
	
				// add the pagination display message
				this.$cs_idx_displaying.html(listCountSummary);
			}
			
			/**
			* Hides the pagination bar.
			* Note that this takes in a 0-based index - There are a possible whopping two pagination bars 
			* that can be shown.
			*/
			Pagination.prototype.hide = function(index){
				//Hide all if no index is supplied
				if(typeof index == "undefined") $('.cs-pagination-bar-idx-search', this.self).filter(":visible").hide();
				else $('.cs-pagination-bar-idx-search:eq(' + index + ')', this.self).hide();
			}
			
			/**
			* Shows the pagination bar.
			* See note for Pagination.prototype.hide
			*/
			Pagination.prototype.show = function(index){
				if(typeof index == "undefined") $('.cs-pagination-bar-idx-search', this.self).filter(":hidden").show();
				else {
					$('.cs-pagination-bar-idx-search:eq(' + index + ')', this.self).show();
				}
			}


		/** Map
		  *
		  *
		  * @param self calling DOM element
		  * @param centerCoords center coordinates for the map
		  * @param maxResults the maximum for the map
		  * @param zoomLevel the starting zoom level for the map
		  * @param absUrl the update url for the map
		  */
		function Map(self, centerCoords, maxResults, zoomLevel, absUrl, mapDiv) {
			this.self = self;
			
			var opts = this;
			
			// set the map parameters
			this.maxResults = maxResults;
			this.zoomLevel = zoomLevel;
			this.absoluteUrl = absUrl;
			this.northEastBounds = null;
			this.southWestBounds = null;
			this.mapDiv = mapDiv;
			this.loadingMessage = null;
			this.newListings = null;
			this.currentListings = null;
			this.allListings = null;
			this.currentClusters = null;
			this.savedPosition = false;
			this.openInfoWindowAgain = false;
			this.markerArray = null;
			this.clusterSensitivity = 0;
			this.areaLabel = null;
			this.removeAreaLabel = true;
			this.infoWindow = null;
			this.infoWindowClose = null;
			
			// we constantly cache the last known bounds when in map view, then rely on these in
			// listview or statsview. We need to do this because of a known GMaps bug in IE8.
			this.cachedNortheastBounds = null;
			this.cachedSouthwestBounds = null;
						
			// intialize and center the map
			this.map = new google.maps.Map(document.getElementById(this.mapDiv), {
				center: centerCoords,
				//scrollwheel: false,
				zoom: this.zoomLevel,
				//panControl: false,
				//zoomControl: false,
				//mapTypeControl: false,
				//scaleControl: false,
				//streetViewControl: false,
				//overviewMapControl: false,
				mapTypeId: google.maps.MapTypeId.ROADMAP
			});
			
			// setup loading message
			$('<div class="cs-idx-search-loading-message-container"><span class="cs-idx-search-loading-message">Searching Listings...</span></div>').appendTo('.cs-idx-search-container', this.self);
			this.loadingMessage = $('.cs-idx-search-loading-message', this.self);
		}
	
			/** 
			 * Gets the position, in pixels, of the marker relative to the map container. Useful for positioning divs near markers.
			 *
			 * @param marker Google Maps marker object
			 */
		
			Map.prototype.getMarkerPosition = function(marker){
				var scale = Math.pow(2, this.map.getZoom());
				var nw = new google.maps.LatLng(
				    this.map.getBounds().getNorthEast().lat(),
				    this.map.getBounds().getSouthWest().lng()
				);
				var worldCoordinateNW = this.map.getProjection().fromLatLngToPoint(nw);
				var worldCoordinate = this.map.getProjection().fromLatLngToPoint(marker.getPosition());
				var pixelOffset = new google.maps.Point(
				    Math.floor((worldCoordinate.x - worldCoordinateNW.x) * scale),
				    Math.floor((worldCoordinate.y - worldCoordinateNW.y) * scale)
				);

				return pixelOffset;
			}
			
			/**
			*  Function used by RPMMapTools
			*/
			Map.prototype.disableMapMoveEvent = function(disable){
				config.disableMapMoveEvent = disable;
			}
			
			/** returns the map container
			  *
			  * @param mType the map type
			  */
			Map.prototype.setMapType = function(mType){
				return this.map.setMapType(mType);
			};
		
			/** returns the map container
			  *
			  */
			Map.prototype.getContainer = function(){
				return this.map.getContainer();
			};
		
			/** 
			 *  Saves lat/lng and zoom values
			 */
			Map.prototype.setSavedPosition = function(){
				if(!this.savedPosition){
					this.sCenter = this.map.getCenter();
					//this.sZoom = this.map.getZoom();
					this.savedPosition = true;
				}else{
					this.openInfoWindowAgain = true;
				}
			};
		
			/** 
			 *  Sets map zoom and position values to ones previously saved
			 */
			Map.prototype.returnToSavedPosition = function(){		
				if(!this.openInfoWindowAgain){
					//this.map.setZoom(this.sZoom);
					this.map.panTo(this.sCenter);
					this.savedPosition = false;
				}
			};
		
			/** adds a control to the map
			  *
			  * @param control to add to the map
			  */
			Map.prototype.addControl = function(control){
				this.map.addControl(control);
			};
		
			/** returns the current zoom level of the map
			  *
			  * @return zoom level of map
			  */
			Map.prototype.getZoom = function(){
				return this.map.getZoom();
			};
		
			Map.prototype.setZoom = function(zoomValue){
				this.map.setZoom(zoomValue);
			};
		
			/** centers the map on given coordinates and zoom level
			  *
			  * @param centerCoords coordinates to center the map at
			  * @param zoomLevel zoom level to start the map at
			  */
			Map.prototype.setCenter = function(centerCoords, zoomLevel){
				this.map.setCenter(centerCoords);
				this.map.setZoom(zoomLevel);
			};
		
			/** gets the current center of the map
			  *
			  * @return coordinates indicating the center of the map
			  */
			Map.prototype.getCenter = function(){
				return this.map.getCenter();
			};
	
			/** Returns the NorthEast bounds of this map
			  * Compensate for markers and map tools overlay
			  * @return Northeast bounds
			  */
			Map.prototype.getNortheastBounds = function(){
				// only update the bounds if we're in map view or this is the first run (null); otherwise use the cached bounds
				if(config.currentSearchView == config.searchViews.map || this.cachedNortheastBounds == null){
					var zL = Math.pow(2, this.map.getZoom());
					var nePt = this.map.getProjection().fromLatLngToPoint(this.map.getBounds().getNorthEast());
					var nePx = new google.maps.Point(nePt.x * zL, nePt.y * zL);
					
					//Compensate for markers
					nePx.x -= 9;
					nePx.y += 29;
					
					nePt = new google.maps.Point(nePx.x / zL, nePx.y / zL);
					this.cachedNortheastBounds = this.map.getProjection().fromPointToLatLng(nePt);
				}
				
				return this.cachedNortheastBounds;
			};
		
			/** Returns the Southwest bounds of this map
			  * Compensate for markers 
			  * @return Southwest bounds
			  */
			Map.prototype.getSouthwestBounds = function(){
				// only update the bounds if we're in map view or this is the first run (null); otherwise use the cached bounds
				if(config.currentSearchView == config.searchViews.map || this.cachedSouthwestBounds == null){
					var zL = Math.pow(2, this.map.getZoom());
					var swPt = this.map.getProjection().fromLatLngToPoint(this.map.getBounds().getSouthWest());
					var swPx = new google.maps.Point(swPt.x * zL, swPt.y * zL);
					
					//Compensate for markers
					swPx.x += 9;
					swPx.y -= 14;
					
					swPt = new google.maps.Point(swPx.x / zL, swPx.y / zL);
					this.cachedSouthwestBounds = this.map.getProjection().fromPointToLatLng(swPt);
				}
				return this.cachedSouthwestBounds;
			};
		
			/** returns the actual GMap2 object
			  *
			  * @return GMap2 object
			  */
			Map.prototype.getMap = function(){
				return this.map;
			};
		
			/** adds an overlay to the map
			  *
			  * @param overlay
			  */
			Map.prototype.addOverlay = function(overlay){
				this.map.addOverlay(overlay);
			};
		
			/** shows the loading message
			  *
			  */
			Map.prototype.showLoadingMessage = function(){
				// show the loading message
				this.loadingMessage.show();
			};
		
			/** hides the loading message
			  *
			  */
			Map.prototype.hideLoadingMessage = function(){
				this.loadingMessage.hide();
			};
				
			/**
			*  Primary search function to populate the map with markers.
			*/
			Map.prototype.addListings = function(data){
				if(data.totalResults == 0){
					this.clearOverlays();
					config.pagination.load(data.totalResults, data.maxResults, data.startNumber, 0);
					return;
				}
		
				this.tempListings = new Hashtable();  //Temp list of all listings
				this.newListings = new Hashtable();  //List of all new listings
				this.newClusters = [];
				this.existingListings = new Hashtable();  //List of listings already residing in this.currentListings
				var hL = 0;	// hidden listings
				
				if(this.allListings == null) this.allListings = new Hashtable();  //All listing objects
				if(this.currentClusters == null) this.currentClusters = [];  //All clusters
				
				for(var x=0;x<data.listings.length;x++){
					if(data.listings[x].latitude == "" && data.listings[x].longitude == ""){
						hL++;
					}else if(this.allListings.get(data.listings[x].listingNumber) == null){
						
						this.newListings.put(data.listings[x].listingNumber, new Listing (data.listings[x].listingNumber, data.listings[x].latitude, data.listings[x].longitude, data.listings[x].markerColor, data.listings[x].mlsNumber, data.listings[x].formattedPrice, data.listings[x].mainPhoto, data.listings[x].picCount, data.listings[x].listingAgentsString, data.listings[x].labelValuePairs));
						this.tempListings.put(data.listings[x].listingNumber, new Listing (data.listings[x].listingNumber, data.listings[x].latitude, data.listings[x].longitude, data.listings[x].markerColor, data.listings[x].mlsNumber, data.listings[x].formattedPrice, data.listings[x].mainPhoto, data.listings[x].picCount, data.listings[x].listingAgentsString, data.listings[x].labelValuePairs));
					}else{
						this.existingListings.put(data.listings[x].listingNumber, this.allListings.get(data.listings[x].listingNumber));
						this.tempListings.put(data.listings[x].listingNumber, this.allListings.get(data.listings[x].listingNumber));
					}
				}
				
				this.removeInvalidListings();
				this.refactorExistingClusters();
				this.addListingsToExistingClusters();
				this.createNewClusters();
				
				this.allListings = this.tempListings;
				
				//Adding listing markers to map
				this.newListings.moveFirst();
				while(this.newListings.next()){
					this.allListings.get(this.newListings.getKey()).addToMap();
				}
		
				//Adding new clusters to map
				for(var x=0;x<this.newClusters.length;x++){
					this.newClusters[x].initializeMarker();
				}
		
				//Refreshing positions of existing clusters
				for(var x=0;x<this.currentClusters.length;x++){
					this.currentClusters[x].resetMarkerPosition();
				}
				
				//Refreshing bounds of existing markers
				this.existingListings.moveFirst();
				while(this.existingListings.next()){
					this.allListings.get(this.existingListings.getKey()).setMOClasses(true);
					//this.allListings.get(this.existingListings.getKey()).setShape();
				}
				
				this.currentClusters = this.currentClusters.concat(this.newClusters);
								
				//now paginate
				config.pagination.load(data.totalResults, data.maxResults, data.startNumber, hL);
			};
			
			/**
			*  Creates new clusters from list of new listings as well as listings that may already be on the map
			*/
			Map.prototype.createNewClusters = function(){
				var debug = false;
				var cluster = null;
				var clusteredListings = new Hashtable();
				var preClusteredListings = new Hashtable();
				var totalClusteredListings = new Hashtable();
				var listings = [];
				
				this.newListings.moveFirst();
				while(this.newListings.next()){
					listings.push(this.newListings.getValue());
				}
				
				for(var i=0;i<this.currentClusters.length;i++){
					preClusteredListings = preClusteredListings.add(this.currentClusters[i].listings);
				}
				
				//Only get unclustered listings in existing listings
				this.existingListings.moveFirst();
				while(this.existingListings.next()) {
					if(preClusteredListings.get(this.existingListings.getKey()) == null) {
						listings.push(this.existingListings.getValue());
					}
				}
				
				if(debug) alert("this.newListings: " + this.newListings.size() + "\nlistings: " + listings.length);
				
				for(var i=0;i<listings.length-1;i++){
					if(totalClusteredListings.get(listings[i]) == null){
						clusteredListings = null;
						
						for(var j=i+1;j<listings.length;j++){
							if(totalClusteredListings.get(listings[j].listnum) == null && listings[i].listnum != listings[j].listnum){
								if(listings[i].getBounds().intersects(listings[j].getBounds())){
									if(clusteredListings == null){
										clusteredListings = new Hashtable();
										clusteredListings.put(listings[i].listnum, listings[i]);
										if(debug) alert("init - adding " + listings[i].listnum);
									}
									if(debug) alert("after init - adding " + listings[j].listnum);
									clusteredListings.put(listings[j].listnum, listings[j]);
									listings.splice(j, 1);
									j--;
								}
							}
						}
										
						if(clusteredListings != null){
							clusteredListings.moveFirst();
							while(clusteredListings.next()){
								if(this.newListings.get(clusteredListings.getKey()) != null){
									this.newListings.remove(clusteredListings.getKey());
								}else if(this.existingListings.get(clusteredListings.getKey()) != null){
									this.existingListings.get(clusteredListings.getKey()).removeFromMap();
									this.existingListings.remove(clusteredListings.getKey());
								}else{
									if(debug) alert("createNewClusters - clustered listing not found in list!!!");
								}
							}
						
							cluster = new ListingCluster(clusteredListings);
							this.newClusters.push(cluster);
							
							if(debug){
								var clusteredListingsMsg = "";
								clusteredListings.moveFirst();
								while(clusteredListings.next()){
									clusteredListingsMsg += "\n" + clusteredListings.getKey();
								}
								alert("Number of listings in new cluster: " + clusteredListings.size() + clusteredListingsMsg); 
							}
							
							totalClusteredListings = totalClusteredListings.add(clusteredListings);
							listings.splice(i, 1);
							i--;
						}
					}
				}
				
				if(debug) alert("Size of this.newClusters: " + this.newClusters.length);
				
				for(var i=0;i<this.newClusters.length;i++){
					for(var j=0;j<listings.length;j++){
						if(this.newClusters[i].getMarkerBounds().intersects(listings[j].getBounds())){
							this.newClusters[i].addListing(listings[j].listnum, listings[j])
							if(this.newListings.get(listings[j].listnum) != null){
								this.newListings.remove(listings[j].listnum);
							}else if(this.existingListings.get(listings[j].listnum) != null){
								if(this.existingListings.get(listings[j].listnum).onMap) this.existingListings.get(listings[j].listnum).removeFromMap();
								this.existingListings.remove(listings[j].listnum);
							}
						}
					}
				}
				
				//Ensure new clusters don't overlap each other
				for(var i=0;i<this.newClusters.length-1;i++){
					for(var j=i+1;j<this.newClusters.length;j++){
						if(this.newClusters[j].getMarkerBounds().intersects(this.newClusters[i].getMarkerBounds())){
							this.newClusters[i].listings.add(this.newClusters[j].listings);
							this.newClusters.splice(j, 1);
							j--;
						}
					}
				}
				
				//Ensure the new clusters aren't overlapping the old ones, if so then join them
				for(var i=0;i<this.currentClusters.length;i++){
					for(var j=0;j<this.newClusters.length;j++){
						if(this.newClusters[j].getMarkerBounds().intersects(this.currentClusters[i].getMarkerBounds())){
							this.currentClusters[i].listings.add(this.newClusters[j].listings);
							this.newClusters.splice(j, 1);
							j--;
						}
					}
				}
			};
	
			/**
			*  Adds any single listings to existing clusters that overlap each other
			*/
			Map.prototype.addListingsToExistingClusters = function(){
				var removeKeyList = null;
				for(var i=0;i<this.currentClusters.length;i++){
					removeKeyList = [];
					this.newListings.moveFirst();
					while(this.newListings.next()){				
						if(this.newListings.getValue().getBounds().intersects(this.currentClusters[i].getMarkerBounds())){
							this.currentClusters[i].addListing(this.newListings.getValue().listnum, this.newListings.getValue());
							removeKeyList.push(this.newListings.getValue().listnum);
						}
					}
					
					for(var j=0;j<removeKeyList.length;j++){
						this.newListings.remove(removeKeyList[j]);
					}
					
					removeKeyList = [];
					this.existingListings.moveFirst();
					while(this.existingListings.next()){				
						if(this.existingListings.getValue().onMap && this.existingListings.getValue().getBounds().intersects(this.currentClusters[i].getMarkerBounds())){
							this.currentClusters[i].addListing(this.existingListings.getValue().listnum, this.existingListings.getValue());
							removeKeyList.push(this.existingListings.getValue().listnum);
						}
					}
					
					for(var j=0;j<removeKeyList.length;j++){
						this.existingListings.get(removeKeyList[j]).removeFromMap();
						this.existingListings.remove(removeKeyList[j]);
					}
				}
			};

			/**
			*  Checks validity of listings in existing clusters
			*/
			Map.prototype.refactorExistingClusters = function(){
				var debug = false;
				
				for(var i=0;i<this.currentClusters.length;i++){
					var cListings = this.currentClusters[i].listingsArray();
					var removeList = [];
						
					if(debug) this.currentClusters[i].showBounds();	
						
					//Check each listing in cluster to see if they still overlap								
					for(var j=0;j<cListings.length-1;j++){
						if(debug) cListings[j].showBounds();
						for(var k=j+1;k<cListings.length;k++){
							if(!cListings[j].getBounds().intersects(cListings[k].getBounds())) {
								removeList.push(cListings[k]);
								cListings.splice(k, 1);
								k--;
							}
						}
					}
									
					if(debug && removeList.length > 0){
						var msg = "";
						for(var s=0;s<removeList.length;s++) msg += "\n" + removeList[s].listnum;
						alert("Cluster " + i + ":" + msg);
					}
					
					//Remove any found invalid listings from cluster
					if(removeList.length > 0){
						for(var j=0;j<removeList.length;j++){
							if(!this.currentClusters[i].removeListing(removeList[j].listnum)){
								if(debug){
									this.currentClusters[i].listings.moveFirst();
									var msg = "\n---";
									while(this.currentClusters[i].listings.next()){
										msg += "\n" + this.currentClusters[i].listings.getValue().listnum;
									}
									msg += "\n---";
									alert("refactorExistingClusters - remove listing " + removeList[j].listnum + " failed on cluster " + i + msg);
								}
							}
							this.newListings.put(removeList[j].listnum, removeList[j]);
							this.existingListings.remove(removeList[j].listnum);
						}
		
						if(this.currentClusters[i].size() < 2){
							if(this.currentClusters[i].size() == 1){
								this.newListings.add(this.currentClusters[i].listings);
							}
							this.currentClusters[i].destroyMarker();
							this.currentClusters.splice(i, 1);
							i--;
						}
					}
				}
				
				//Merge any overlapping clusters
				for(var i=0;i<this.currentClusters.length-1;i++){
					for(var j=i+1;j<this.currentClusters.length;j++){
						if(this.currentClusters[i].getMarkerBounds().intersects(this.currentClusters[j].getMarkerBounds())){
							this.currentClusters[i].listings = this.currentClusters[i].listings.add(this.currentClusters[j].listings);
							this.currentClusters[j].destroyMarker();
							this.currentClusters.splice(j, 1);
							j--;
						}
					}
				}
			};
	
			/**
			* Removes listings currently on map or in clusters that are not in the list of new listings given
			*/
			Map.prototype.removeInvalidListings = function(){
				var debug = false;
				this.allListings.moveFirst();
				while(this.allListings.next()){
					if(this.tempListings.get(this.allListings.getKey()) == null){
						if(!this.allListings.getValue().onMap){
							//Find listing in clusters and remove
							for(var t=0;t<this.currentClusters.length;t++) {
								if(this.currentClusters[t].listingExists(this.allListings.getKey())){
									if(!debug){
										this.currentClusters[t].removeListing(this.allListings.getKey());
									}else{
										if(!this.currentClusters[t].removeListing(this.allListings.getKey())) alert("removeInvalidListings - remove from cluster failed!");
									}
									break;
								}
							}
						}
						this.allListings.getValue().removeFromMap();
						this.allListings.remove(this.allListings.getKey());
						this.allListings.moveFirst();
					}
				}
				
				//Process invalid clusters
				for(var t=0;t<this.currentClusters.length;t++){
					if(this.currentClusters[t].size() < 2){
						if(this.currentClusters[t].size() == 1) this.newListings.add(this.currentClusters[t].listings);
						this.currentClusters[t].destroyMarker();
						this.currentClusters.splice(t, 1);
						t--;
					}
				}
			};
		
			/** 
			*  Removes all overlays from map
			*/
			Map.prototype.clearOverlays = function(){
				if(this.allListings != null && typeof this.allListings != "undefined"){
					this.allListings.moveFirst();
					while(this.allListings.next()){
						this.allListings.getValue().removeFromMap();
					}
					this.allListings = null;
				}
				
				if(this.currentClusters != null && this.currentClusters != "undefined"){
					for(var i=0;i<this.currentClusters.length;i++){
						this.currentClusters[i].destroyMarker();
					}
					this.currentClusters = null;
				}
			};
	
			/** 
			*  Modifies the map in case the container size has changed
			*/
			//TODO: Check to see if resizing the map works for v3 cuz this method doesn't exist
			Map.prototype.checkResize = function(){
				google.maps.event.trigger(this.map, "resize");
			};

		/** Private methods
		  *
		  * For further description, see the function headers below
		  *
		  * - runQuickSearch(value)
		  * - getSearchUrlString()
		  * - loadConfig($this, options)
		  * - runSearch(sNumber, incremental){
		  * - loadSearchOptions(mlsSearchType, initialize, executeSearch)
		  *
		  */
		var helpers = {
			
			/**
			  * runQuickSearch - runs the search on the value provided in the quicksearch field
			  *
			  * @param value the value to process the search on
			  */
			runQuickSearch:function(value){
				
				var self = this;  // initialize a pointer to the context for the plugin
				var $this = $(this);  // create a quick-reference jQuery object with this DOM element
				
				self.searchAjaxActivity = $.ajax({
					type: "GET",
					url: config.absUrl,
					data: "pathway=5&textSearch=true&queryText="+encodeURIComponent(value),
					dataType: "json",
					error: function(data, error){
						setTimeout(function(){$('#searchEngineSearchOptionsLoadingOverlay', self).unblock();}, 1000);
					},
					success: function(data){
						if(data.searchResultType == "Listing"){
							methods.listingDetails.call($this, data.listingNumber);
						}else if(data.searchResultType == "Map"){
							config.map.setCenter(new google.maps.LatLng(data.lat, data.lng), parseInt(data.zoom, 10));
							config.currentNEBounds = config.map.getNortheastBounds();
							config.currentSWBounds = config.map.getSouthwestBounds();
															
							$(function(){
								google.maps.event.addListenerOnce(config.map.getMap(), "tilesloaded", function(){
									config.map.areaLabel = new AreaIdMarker(new google.maps.LatLng(data.lat, data.lng), '<div class="cs-idx-areaid-marker">' + data.queryText + '</div>', AreaIdMarker.Center, 0.8, data.queryText);  //0.8
									config.map.areaLabel.setMap(config.map.getMap());
									config.map.areaLabel.show();
								});
							});
						}else{
							// This is here for the quick search. After the search button is pressed there needs to be feedback that something happened but that records were not found. NOTE: this does not show up during a regular search if there is no records found.
							alert("No Records Found");
						}
					}
				});	
			},
			
			/**
			* Returns the latitude/longitude coordinates of the map.
			* If sending to plugin (PHP) then the square brackets are required to prevent duplicate properties from being parsed out of the
			* post request.
			*/
			getSearchCoords:function(){
				var arrBr = "";
				if(config.plugin) arrBr = "[]";
				return "&latitude"+arrBr+"="+config.map.getSouthwestBounds().lat()+"&latitude"+arrBr+"="+config.map.getNortheastBounds().lat()+"&longitude"+arrBr+"="+config.map.getSouthwestBounds().lng()+"&longitude"+arrBr+"="+config.map.getNortheastBounds().lng()
			},

			/**
			  * getSearchUrlString - runs the search on the value provided in the quicksearch field
			  *
			  */
			getSearchUrlString:function(value){
				var self = this;  // initialize a pointer to the context for the plugin
				var $this = $(this);  // create a quick-reference jQuery object with this DOM element
				var arrBr = ""; // Will be populated with "[]" if is plugin (required for php post requests to allow duplicate params);
				
				if(config.plugin) arrBr = "[]";
				
				var urlString = "";
				// get slider values
				$(".cs-slider", self).each(function(){
					var $id = $(this).attr("id");
					urlString += '&'+$id+arrBr+'='+encodeURIComponent($(".cs-slider-inner-label-value-left", this).html())+'&'+$id+arrBr+'='+encodeURIComponent($(".cs-slider-inner-label-value-right", this).html());
				});
				
				// get drop-down values
				$(".cs-dd-range", self).each(function(){
					var $id = $(this).attr("id");
					urlString += '&'+$id+arrBr+'='+encodeURIComponent($(".cs-dd-range-left", this).val())+'&'+$id+arrBr+'='+encodeURIComponent($(".cs-dd-range-right", this).val());
				});
				
				// get tree values
				$(".cs-treeview", self).each(function(){
					var $id = $(this).attr("id");
					$(this).find("input:checked").each(function(){
						urlString += '&'+$id+arrBr+'='+$(this).attr("id");
					});
				});
				// get hidden date/time restriction for emailed search
				$('.datetime_range', self).each(function(){
					var $id = $(this).attr("id");
					urlString += '&'+$id+arrBr+'='+encodeURIComponent($("#"+$id+"Start", this).val())+'&'+$id+arrBr+'='+encodeURIComponent($("#"+$id+"End", this).val());
				});

				// get all other hidden fields
				$('#hiddenSearchOptions', self).find('input').each(function(){
					if($(this).parent().attr('class') == 'datetime_range') { // We skip any who'se parents are datetime_range typed as these are handled above by themselves.
						return;
					}
					var $id = $(this).attr("id");
					urlString += '&'+$id+arrBr+'='+encodeURIComponent($('#'+$id+'').val());
				});

				// get the checkbox options
				$('.cs-idx-search-vip-options', self).find('input:checked').each(function(){
					var $id = $(this).attr("id");
					urlString += '&'+$id+arrBr+'=true';
				});

				// return the completed url string
				if(urlString == "")
					urlString = "&";
				return urlString;
			},
			/**
			  * loadConfig - loads the config. if there's no config available then attempts to load from the options
			  *		 given to the plugin
			  *
			  * @param $this - options sent from running the plugin (if available)
			  * @param options - options sent from running the plugin (if available)
			  */
			loadConfig:function(options){  // throws exception
				var self = this;  // initialize a pointer to the context for the plugin
				var $this = $(this);  // create a quick-reference jQuery object with this DOM element

				if(config == null){
					config = $this.data('config'); // attempt to extract any stored configs for this DOM element
					
					// if config is still null, continue.
					if(config == null){
						if(options != null){
							config = {};
							if(!options) return false;
							$.extend(config, options);
						}else{
							var ex = {"name":"loadConfig", "message":"No config available"};
							throw ex;
						}
					}
				}
			},
			
			/**
			* Wrapper for either invoking a map search or a list view search
			*
			* @param sNumber the starting number of the results
			* @param incremental boolean indicating whether we're stepping through results or not. Affects whether stats are processed (false=process, true=don't process).
			*/
			runSearch : function(sNumber, incremental){
				var self = this;  // initialize a pointer to the context for the plugin
				var $this = $(this);  // create a quick-reference jQuery object with this DOM element

				// initialize values
				if(typeof sNumber == "undefined"){
					sNumber = 0;
				}
				if(typeof incremental == "incremental"){
					incremental = false;
				}

				if(!config.disableMapMoveEvent){
					if(config.map.removeAreaLabel && config.map.areaLabel != null){
						config.map.areaLabel.hide();
						config.map.areaLabel.setMap(null);
						config.map.areaLabel = null;				
						$("#cs-idx-quicksearch", this).val("");
					}else if(!config.map.removeAreaLabel){
						config.map.removeAreaLabel = true;
					}
					
					if(config.currentNEBounds == null || config.currentSWBounds == null){
						config.currentNEBounds = config.map.getNortheastBounds();
						config.currentSWBounds = config.map.getSouthwestBounds();
					}
					
					// figure out which view to load
					if(config.currentSearchView == config.searchViews.list){	// list view
						config.refactorMap = true;

						// get the current sorting
						config.currentListingsSort = parseInt($(".cs-pagination-bar-select:eq(0)", this).val(), 10);

						// load the listings results view
						config.listViewPanel.load(sNumber, incremental);

					}else if(config.currentSearchView == config.searchViews.stats){ // stats view
						config.refactorMap = true;
						
						// load the market analyzer panel
						config.marketAnalyzer.load(false);

					}else{	// map view
						// load the map results view
						config.mapViewPanel.load(sNumber, incremental);
					}
				}
			},
			/**
			* Loads the search options from the server based on the given search type
			*
			* @param mlsSearchType - integer specifying the type of MLS search
			* @param initialize - boolean indicating whether this is initialize function
			* @param executeSearch - boolean runs the search once it's finished
			*/
			loadSearchOptions : function(mlsSearchType, initialize, executeSearch){
				try{
					var self = this;  // initialize a pointer to the context for the plugin
					var $this = $(this);  // create a quick-reference jQuery object with this DOM element

					// clean up search - unloads/destroys any JS components used in the prior MLS search type
					config.mlsSearchType = mlsSearchType;
	
					if(config.currentSearchView == config.searchViews.stats && config.searchTypes[config.mlsSearchType].showStats.toLowerCase() == "true"){
						$("#mapView", this).click();
					}
					
					// if this isn't an initial load, get the new search criteria from the server
					if(!initialize){
						// fetch the listing/areaID data from the server
						if(searchAjaxActivity != null)searchAjaxActivity.abort();
						
						searchAjaxActivity = $.ajax({
							type: "GET",
							url: config.absUrl,
							data: "pathway=5&options=true&envPropType="+mlsSearchType,
							dataType: "html",
							beforeSend: function(){
								// methods["busyOverlay"].call(self, 'mlsSearchType', true); // 2011-09-14 EZ - busyOverlay is implemented in terms of the old search engine, this does nothing.
							},
							error: function(data, error){
								setTimeout(function(){$('#searchEngineSearchOptionsLoadingOverlay', self).unblock();}, 1000);
							},
							success: function(data){
								$('#cs-idx-search-options', self).html(data);
							},
							complete: function(xmlhttprequest, successtype){
//								setTimeout(function(){
//									// methods["busyOverlay"].call(self, "loading", false); // 2011-09-14 EZ - busyOverlay is implemented in terms of the old search engine, this does nothing.
//								}, 1000);
								// initialize the search options
								helpers.initSearchOptions.call(self);

								// run the search
								if(executeSearch) { 
									helpers.runSearch.call(self);
								}
							}
						});
					}else{
						// initialize the search options
						helpers.initSearchOptions.call(self);

						// hide the statistics panel if necessary
						if(config.searchTypes[config.mlsSearchType].showStats.toLowerCase() == "false"){ 
							config.searchStatisticsPanel.hide(); 
						}

						// run the search - projection object null check & listener is there because of IE9
						if(config.map.getMap().getProjection() == null){
							//IE browsers usually get here on first load
							google.maps.event.addListenerOnce(config.map.getMap(), "idle", function(){ helpers.runSearch.call(self); });
						}else{
							helpers.runSearch.call(self);
						}
					}
				}catch(ex){
					alert("helpers.loadSearchOptions error\n" + ex.name + " - " + ex.message); 
				}
			},
			/**
			* Binds to all search options in the search engine and assigns appropriate events, etc.
			*
			*/
			initSearchOptions : function(){
				try{
					var self = this;  // initialize a pointer to the context for the plugin
					var $this = $(this);  // create a quick-reference jQuery object with this DOM element
					
					$('#cs-id-save-search-tab', self).unbind("click").bind("click", function(){
						var startNumber = 0;

						$.csVOWPanel('csVOWAjax', {
							type: "POST",
							url: config.absUrl,
							beforeSend: function(){},
							data: "pathway=5&startNumber="+startNumber+"&saveSearch=true&envPropType="+config.mlsSearchType+helpers.getSearchUrlString.call(self)+helpers.getSearchCoords.call(self)+"&zoomLevel="+config.map.getZoom()+"&listingsSort="+$this.find(".cs-pagination-bar-select:eq(0)").val(),
							dataType: "html",
							success: function(data){
								// load the listing overlay
								$(self).block({message: 'Saving...'});
								$('#saveSearchButton', self).hide();
								$('#savedSearchMessage', self).show();
							},
							complete: function(){
								setTimeout(function(){$(self).unblock();}, 500);
							},
							failComplete: function() {
								setTimeout(function(){$(self).unblock();}, 500);
							}
						}, function(){
							setTimeout(function(){$(self).unblock();}, 500);
						}, function(){}, true);
						return false;
					});
					
					$('#saveSendOptions', self).mouseover(function(){
						$('#saveSendOptions', self).toggleClass("saveSendOptions_on");
					});
					
					$('#saveSendOptions', self).mouseout(function(){
						$('#saveSendOptions', self).toggleClass("saveSendOptions_on");
					});
					
					// Wire up the "select all" and "clear all" controls.
					$('.cs-treeview', self).find('.cs-treeview-check-all, .cs-treeview-check-none').each(function(){
						var control = this;
						$(this).click(function(){
							
							// For each element of this tree item.
							$(this).siblings(".cs-treeview-boxtree-container").find('li').each(function(){
								// set or clear the value of the checkbox depending on the control that was clicked.
								if($(control).attr("class") == 'cs-treeview-check-all') {
									$(this).find('input:checkbox').attr('checked', true);
								} else {
									$(this).find('input:checkbox').attr('checked', false);
								}
							});
							
							// Trigger the search as checking / un-checking all of these options will almost certainly change the search results.
							helpers.runSearch.call(self);
							
							// So the state of the checkbox that is used for this control does NOT change.
							return false;
						});
					});
	
					// attach events to the foreclosure/estate sale checkboxes, etc.
					$('#foreclosure, #estateSale', self).click(function(){
						var x = this;

						// make sure this feature can be accessed
						$.csVOWPanel('csVOWAjax', {
							type: "GET",
							dataType: "html",
							url: config.absUrl,
							data: "pathway=5&vipAccessCheck=true",
							error: function(data, error){},
							success: function(data){
								// make sure the other isn't checked
								if(x.id == 'estateSale'){
									$('#foreclosure:checked', self).each(function(){
										this.checked=false;
									});
								}else{
									$('#estateSale:checked', self).each(function(){
										this.checked=false;
									});
								}
								
								// run search
								helpers.runSearch.call(x);
							},
							failComplete:function(){}
						}, function(){}, function(){
							// rollback the operation
							$('#estateSale:checked, #foreclosure:checked', self).each(function(){
								this.checked=false;
							});
						}, true);

					});
					
					//Lock out the searching if we have a date time restricion in effect.
					if($('#listDateRangeStart', self).val() != '' || $('#listDateRangeEnd', self).val() != '') {
						methods["lockByListDate"].call(self, true);
					}				
				}catch(ex){
					alert("initSearchOptions error\n" + ex.name + " - " + ex.message); 
				}
			}
		}


		/** Public methods
		  *
		  * used to keep the $.fn namespace uncluttered, collect all of the plugin's methods in an object literal and call
		  * called as: $(selector).csIDXSearchEngine('methodName', arg1, arg2, ... argn) or, from within the plugin itself, 
		  * as methods.methodName(arg1, arg2, ... argn)
		  *
		  * - init(options)
		  * - updateResults(startNumber, incremental)
		  *
		  */
		var methods = {
			/**
			* Initialize plugin
			*
			* @param options options used to configure the plygin
			*/
			init : function(options){
				return this.each(function() {
					try{
						var self = this;  // initialize a pointer to the context for the plugin
						var $this = $(this);  // create a quick-reference jQuery object with this DOM element
						
						helpers.loadConfig.call(this, options); // load the config.
						
						// create a mouse position tracker for html window popups on the map
						$(document.body).mousemove(function(e){
							//config.pageX = e.pageX;
							//config.pageY = e.pageY;
						});
						
						if( typeof config.zoomLevel == "undefined"
						 || typeof config.absUrl == "undefined"
						 || typeof config.firstSearch == "undefined"
						 || typeof config.mapDiv == "undefined"
						 || typeof config.mlsSearchType == "undefined"
						 || typeof config.listingsSort == "undefined" 
						 || typeof config.startSearchViewType == "undefined"
						){
							alert("Required fields missing, exiting..."); return false;
						}
						
						config.modletId = $this.attr("id").substring($this.attr("id").lastIndexOf("_") + 1);
						config.searchViews = {map:0, list:1, stats:2};
						config.currentSearchView = config.startSearchViewType;
						config.rangeTypes = {slide: 0, drop: 1};
						config.currentRangeType = null;
						config.currentNEBounds = null;
						config.currentSWBounds = null;
						config.currentPage = 0;
						config.refactorMap = false;
						config.disableMapMoveEvent = false;
						
						//Set url for use with image resources (plugin)
						if(config.resLink == null){
							config.resLink = "";
						}
						
						config.self = self;	// set this for global reference
						
						var lSort = parseInt(config.listingsSort);
						if(!isNaN(lSort)){
							config.currentListingsSort = lSort;
							$(".cs-pagination-bar-select:eq(0)", self).val(lSort);
						}else{
							config.currentListingsSort = 0;
						}
						
						// create the pagination object
						config.pagination = new Pagination(this);

						// create the map view object
						config.mapViewPanel = new MapViewPanel(this);
						
						// create the list view object
						config.listViewPanel = new ListViewPanel(this);

						// create the market analyzer panel
						config.marketAnalyzerPanel = new MarketAnalyzerPanel(this);
						
						// create the Search Statistics panel (at the bottom)
						config.searchStatisticsPanel = new SearchStatisticsPanel(this);
												
						// create an onchange event in the searchtypes drop-down that configures which parts of the view are visible
						$('#searchTypes', this).change(function(){
							if(config.searchTypes[$(this).val()].showStats.toLowerCase() == "true"){
								$("#cs-idx-view-market-analyzer", self).parent(":hidden").show("slide", {direction:"left"}); // show the market analyzer tab if it's hidden
								$("#cs-idx-quickstats:hidden", self).show("slide", {direction:"left"});  // show the cs-idx-quickstats panel if it's hidden
							}else{
								$("#cs-idx-view-market-analyzer", self).parent(":visible").hide("slide", {direction:"left"}); // hide the market analyzer tab if it's shown
								$("#cs-idx-quickstats:visible", self).hide("slide", {direction:"left"});  // hide the cs-idx-quickstats panel if it's shown
							}
							helpers.loadSearchOptions.call(self, config.searchTypes[$(this).val()].searchTypeId, false, true);
						});

						// add the event to slide down (or up) the search options panel
						$("#cs-id-search-options-tab", this).click(function(){
							if($('#cs-idx-search-options', self).css('display') == 'none'){
								$('#cs-idx-search-options', self).slideDown();
								$(this).addClass('cs-idx-search-options-tab-active');
							}else{
								$('#cs-idx-search-options', self).slideUp();
								$(this).removeClass('cs-idx-search-options-tab-active');
							}
							return false;
						});
		
		
						// add the onChange event to the 'sort-by' field in the pagination bar(s)
						$(this).delegate('.cs-pagination-bar-select', 'change', function(){
							// run the search if the search sort has a value (ie. the first entry is not an acceptable value)
							if($(this).val() && $(this).val() != ''){
								// if there are two pagination bars (list view), change the value of the drop downs to
								// reflect the recently changed one
								if( $('.cs-pagination-bar-select', self).length > 1 ){
									var val = $(this).val();
									$('.cs-pagination-bar-select', self).each(function(){
										if($(this).val() != val) $(this).val(val); 
									});
								}
								helpers.runSearch.call(self, 0, true);
							}
						});

						// setup the quicksearch autocomplete field
						config.quickSearchLoading = null;
						
						$("#term", this).bind( "click", function(){
							$(this).val('');
						}).autocomplete({
							autoFocus: true,	// Automagically focus the first item.
							disabled: false,
							appendTo: "#" + $this.attr("id"),
							source: config.absUrl + "?pathway=5&autoComp=true",
							minLength: 2,
							delay:400,
							search: function(){
								var x = this;
								$(this).addClass("cs-idx-quick-search-loading");
								clearTimeout(config.quickSearchLoading);
								config.quickSearchLoading = setTimeout(function(){
									$(x).removeClass("cs-idx-quick-search-loading");
								}, 3000);
							},
							open: function(){
								var x = this;
								clearTimeout(config.quickSearchLoading);
								config.quickSearchLoading = setTimeout(function(){
									$(x).removeClass("cs-idx-quick-search-loading");
								}, 1000);
							},
							select: function(event, ui){
								// autoFocus is set to true, meaning that we will always have some value, if it's "no results" we don't run the search.
								if(ui.item.value == "no results") {
									return false; // false so it does not update the search term with the string "no results".
								} else { // Serch is cool and will likely do something useful.
									helpers.runQuickSearch.call(self, ui.item.value);
								}
							},
							close: function(){
							}
						}).css("color", $(".cs-form-simple", this).css("color"));

						$(window).load(function(){
							
							// If we have had a quick search term sent to us we trigger the quick search, otherwise we run the regular search.
							if($("#term", self).val() == null || $("#term", self).val() == '' || $("#term", self).val() == 'City, Neighborhood, Address, MLS, etc.') { // Regular search.
								// load the search with the default mlsSearchType
								helpers.loadSearchOptions.call(self, config.mlsSearchType, true, true);
							} else { // Run the quick search.
								// Need to load the search but specify false for autoexecute.
								helpers.loadSearchOptions.call(self, config.mlsSearchType, true, false);
		
								// Now run the search
								helpers.runQuickSearch.call(self, $('#term', self).val());
							}
						});
												
						// store the config for external calls
						$this.data('config', config);
					}catch(e){
						alert("init error\n" + e.name + " - " + e.message); 
					}
				});
			},
			/**
			* Updates the search results. Will only run properly if the plugin has been initialized.
			*
			* @param startNumber starting number of the search results
			* @param incremental boolean indicating whether we're stepping through results or not. Affects whether stats are processed (false=process, true=don't process).
			*
			*/
			updateResults : function(startNumber, incremental){
				return this.each(function() {
					try{
						helpers.loadConfig.call(this, options); // load the config.
						helpers.runSearch.call(this, startNumber, incremental);
					}catch(ex){
						alert("updateResults error\n" + ex.name + " - " + ex.message); 
					}
				});
			},
			/**
			* Updates the pagination. Will only run properly if the plugin has been initialized.
			*
			* @param totalResults - the total number of results (ie. =150 in the following: 101 - 120 of 150)
			* @param maxResults - the maximum number of results displayed on one page of results
			* @param startNumber - the index of the first result (ie. the first result's startNumber would be 0)
			* @param hidden - number of hidden listings
			*
			*/
			updatePagination : function(totalResults, maxResults, startNumber, hidden){
				return this.each(function() {
					try{
						helpers.loadConfig.call(this, options); // load the config.
						config.pagination.load(totalResults, maxResults, startNumber, hidden);
					}catch(ex){
						alert("updatePagination error\n" + ex.name + " - " + ex.message); 
					}
				});
			},
			/**
			* Shows / hides block overlay on options - if show is true, overlay is added; removed if false.
			*
			* NOTE // 2011-09-14 EZ - busyOverlay is implemented in terms of the old search engine, this does nothing.
			*/
			busyOverlay : function(busyType, show){

				try{
					var me = this;
					var $this = $(this);
					
					if(show) {
						//Show Options Overlay
						if(busyType == 'listingDetails') { //Locks out search options and listings table.
							$('#searchEngineLeftColumnBusyOverlay, #searchEngineLeftColumnBusyCloseListingDetails', me).fadeIn();
						} else { //Locks out search options and listings table.
							$('#searchEngineLeftColumnBusyOverlay, #searchEngineLeftColumnBusyLoadingMessage', me).fadeIn();
						}
						
					} else {
						//Hide Options Overlay
						if(busyType == 'listingDetails') {
							$('#searchEngineLeftColumnBusyOverlay, #searchEngineLeftColumnBusyCloseListingDetails', me).fadeOut();
						} else { //Locks out search options and listings table.
							$('#searchEngineLeftColumnBusyOverlay, #searchEngineLeftColumnBusyLoadingMessage', me).fadeOut();
						}
					}
					
					return false;
				}catch(e){
					alert("busyOverlay error\n" + e.name + " - " + e.message); 
				}
			},
			/**
			* Shows listing details view for provided listing number, hides view if null is provided instead
			*
			* @param listnum
			*/
			listingDetails : function(listnum){
				return this.each(function() {
					try{
						var self = this;  // initialize a pointer to the context for the plugin
						var $this = $(this);  // create a quick-reference jQuery object with this DOM element
						
						helpers.loadConfig.call(this, options); // load the config.
						
						if(listnum != null){
							//Show the listing
							var options = {
								href:config.absUrl,
								cb_his_control: "log",
								top: "30px",
								scrolling: false,
								data:"pathway=6&type=mls&envPropType="+config.mlsSearchType+"&listingNumber="+listnum+"&viewType=mls&modletId=" + $.clickSoldUtils('getNextAvailableModuleId'),
								innerWidth:LISTING_DETAILS_WIDTH,
								initialWidth:100,
								initialHeight:100,
								opacity:0.5,
								onComplete: function(){
									$.clickSoldUtils('infoBoxResize');
								}
							};
							$.clickSoldUtils('infoBoxCreate', options);
						}
	
					}catch(e){
						alert("listingDetails error\n" + e.name + " - " + e.message); 
					}
				});
			},
			/**
			* Displays the locked by list date message and hides search engine options that can't be used with this restriction.
			*/
			lockByListDate : function(lock){
				
				try{
					var me = this;
					if(lock == true){
						
						// Slide down the locked message.
						$('#cs-idx-search-list-date-restriction-msg', me).slideDown();
					
						// Hide the save search and quick search options as they can't be used with this restriction.
						$('#cs-id-save-search-tab').fadeOut();
						$('#cs-id-quick-search-tab').fadeOut();
						
						//Register the onclick function that hides this thing when the user wishes to be done.
						$('#cs-idx-search-list-date-restriction-msg', me).click(function(){
							$(this).unbind("click");
							return methods["lockByListDate"].call(me, false);
						});
					}else{

						// Slide up the locked message.
						$('#cs-idx-search-list-date-restriction-msg', me).slideUp();

						// Un=Hide the save search and quick search options.
						$('#cs-id-save-search-tab').fadeIn();
						$('#cs-id-quick-search-tab').fadeIn();

						//Set the list date range variables to blank to clear the list date range restriction.
						$('#listDateRangeStart', me).val('');
						$('#listDateRangeEnd', me).val('');
	
						//Re-Run the search as it's results will now be different.
						helpers.runSearch.call(me);
					}
				}catch(e){
					alert("lockByListDate error\n" + e.name + " - " + e.message); 
				}finally{
					return false;
				}
			}			
		};

		// The meat of the matter - where everything is called
		try{
			if( methods[method] ){
				return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
			}else if(typeof method === "object"){
				return methods.init.apply(this, arguments);
			}else{
				alert("Error! Method does not exist: " + method);
				$.error("Method " + method + " doesn't exist on jQuery.csIDXSearchEngine");
			}
		}catch(ex){
			alert("$.fn.csIDXSearchEngine - error\n" + ex.name + " - " + ex.message);
		}
	}
})(jQuery);

// Simple JS object to contain the definition of a "search type"
function searchType(searchType, icon, showStats){	
	this.searchTypeId = searchType;
	this.icon = icon;
	this.showStats = showStats;
};
