// Metroseeq namespace
var mesq = mesq || {};

// temporary object to enable method outline in JSEclipse
mesq._Map = {
	
	// objects
	container: null,
	dealTypeSelection: null,
	markers: null,
	gmap: null, // GMap object
	list: null, // mesq.List
	gmapZoomControl: null,
	gmapTypeControl: null,
	geocoder: null, // GClientCoder object
	
	// state
	markers: null, // holds GMarker objects
	selectedItem: null, // set when infowindow is shown in either marker or list link
	
	// config
	minZoom: 14,
	origCenterPoint: null, // point determined by "where" query and geolocation
	searchCenterPoint: null, // dynamic point (equals origCenterPoint when "Unlock center point" is not enabled)
	
	// overlay
	overlay_black: null,
	overlay_loader: null,
	overlayVisible: false,
	
	initialize: function(container) {
		this.markers = [];
		this.container = $(container);
		
		// initialise geocoder
 		this.geocoder = new GClientGeocoder();
		
		// initialize maps panel
  		this.gmap = new GMap2(this.container);
		this.gmapZoomControl = new GLargeMapControl();
		this.gmap.addControl(this.gmapZoomControl);
		this.gmapTypeControl = new GMapTypeControl();
		this.gmap.addControl(this.gmapTypeControl);
		// temporarily disabled until "freezing" map is resolved (enableDrag not called for some fricking weird reason)
		this.gmap.disableDragging(); // re-enabled in last updateResults() call 
		
		this.gmap.setCenter(new GLatLng(0,0));
		
		this.createOverlay();
		
		// add red center icon for search center
		var centericon = new GIcon();
		centericon.image = MINIMARKER_IMG;
		centericon.iconSize = new GSize(16, 16);
		centericon.iconAnchor = new GPoint(1, 1);
		centericon.infoWindowAnchor = new GPoint(5, 1);
		
		this.centerMarker = new GMarker(new GLatLng(0,0), centericon);
		
		// disable auto-closing of extinfowindows
		this.gmap.ClickListener_ = GEvent.addListener(this.gmap, 'click', function(e) {return true;});
		
	},
	
	createOverlay: function(){
		this.overlay_black = new GScreenOverlay(
			'mesq/images/map_overlay.png', 
	    	new GScreenPoint(0, 0, 'pixels', 'fraction'),  // screenXY 
	    	new GScreenPoint(0, 0),  // overlayXY 
	    	new GScreenSize(1, 1, 'fraction', 'fraction')	 // size on screen
    	); 
    	var x=(this.gmap.getSize().width/2)-22;
		var y=(this.gmap.getSize().height/2)-6; 
	    this.gmap.addOverlay(this.overlay_black);
	    	
    	/*this.overlay_loader = new GScreenOverlay(
			'mesq/images/map_loader.gif',
			new GScreenPoint(x, y, 'pixels', 'fraction'),  // screenXY 
	    	new GScreenPoint(0, 0),  // overlayXY 
	    	new GScreenSize(43, 11, 'pixels', 'pixels')	 // size on screen
    	); 
    	
    	this.gmap.addOverlay(this.overlay_loader);*/
    	
    	if(!document.getElementById('LoaderIcon')){
	    	var loader_icon=document.createElement('div');
	    	loader_icon.className='loader_icon';
	    	loader_icon.id='LoaderIcon';
	    	
	    	map=document.getElementById('PlaceMap');
	    	if(!map)
	    		map=document.getElementById('DealMap');
	    	if(!map)
	    		map=document.getElementById('MiniMap');
	    		
	    	loader_icon.style.display='block';	
	    	loader_icon.style.top='-' + (map.offsetHeight/2) -6 + 'px';
	    	loader_icon.style.left=(map.offsetWidth/2) -22 + 'px';
	    		    	
	    	map.appendChild(loader_icon);
	    	this.overlay_loader=loader_icon;
    	}
    	this.hideOverlay();
    	
	},
	
	showOverlay: function(){
		// add an overlay
		if(this.gmap.overlayVisible!=true){
			if(this.overlay_black!=null){
				this.overlay_black.show();
				this.overlay_loader.style.display='block';
				this.gmap.overlayVisible=true;
			}
		}
	},
	
	hideOverlay: function(){
		//this.gmap.removeOverlay(this.overlay);
		this.overlay_black.hide();
		this.overlay_loader.style.display='none';
		this.gmap.overlayVisible=false;
	},
	
	markCenter: function(centerPoint) {
		// only mark center if it wasn't previously locked	
   		//if($F('MapLockCenter') && !centerPoint) return false;
		this.searchCenterPoint = centerPoint ? centerPoint : this.getCenterPoint();
		this.centerMarker.setPoint(this.searchCenterPoint);
		// only add marker if no clustering is applied
		// @todo implement marker excludes for cluster
		if(!this.clusterMarker) {
			this.gmap.addOverlay(this.centerMarker);
		}
	},
	
	/**
	 * Always set center marker to absolute center of the map,
	 * rather than sticking to the original search location
	 */
	lockCenter: function(noRefresh) {
		// reset to original
		this.searchCenterPoint = this.gmap.getCenter();
		this.origCenterPoint = this.gmap.getCenter();
		$('MapLockCenterContainer').addClassName('enabled');
		$('MapLockCenterContainer').down('label').update(this.strUnlockCenter);
		$('MapLockCenter').checked = true;
		
		if(!noRefresh) this.refresh();
	},
	
	/**
	 * Stick center marker to original search location
	 * (e.g. a city center point), rather than following around
	 * (default setting).
	 */
	unlockCenter: function() {
 		// if setting is unticked, refresh current map-area
		this.searchCenterPoint = this.gmap.getCenter();
		$('MapLockCenterContainer').removeClassName('enabled');
		$('MapLockCenterContainer').down('label').update(this.strLockCenter);
		$('MapLockCenter').checked = false;
		
		this.markCenter(this.searchCenterPoint);
		
		this.refresh();
	},
	
	getCenterPoint: function() {
		return this.searchCenterPoint;
		return ($F('MapLockCenter')) ? this.origCenterPoint : this.searchCenterPoint; 
	},
	
	refreshMarkers: function() {
		for(var i=0; i<this.markers.length; i++) {
			this.gmap.addOverlay(this.markers[i]);
		}
	},
	
	addMarkerFromData: function(data) {
		var marker = this.getMarkerFromData(data);
		
		this.markers.push(marker);
		this.gmap.addOverlay(marker);
		
		return marker;
	},
	
	getMarkerFromData: function(data) {
		var latLng = new GLatLng(parseFloat(data.lat), parseFloat(data.lng));
		
		/**
		 * Set the custom icons for the map
		 */
		if(window.app.isPrint()) {
			var markerprint = new GIcon();
			// TODO Doesn't work for more than 23 matches
			markerprint.image = this.numberedMarkerImgTemplate.interpolate(
				{'number': (this.list) ? this.list.items.length+1 : 1}
			);
			markerprint.iconSize = new GSize(20, 32);
			markerprint.shadow = MARKER_SHADOW;
			markerprint.shadowSize = new GSize(31, 32);
			markerprint.iconAnchor = new GPoint(6, 5);
			markerprint.infoWindowAnchor = new GPoint(5, 1);
			markerprint.clickable = false;
			var marker = new GMarker(latLng, markerprint);
		} else if((data.Deals && data.Deals.length) || (data.DealCount && data.DealCount > 0)) {						
			var markerdeals = new GIcon();
			markerdeals.image = MARKER_DEALS;
			markerdeals.iconSize = new GSize(20, 32);
			markerdeals.shadow = MARKER_SHADOW;
			markerdeals.shadowSize = new GSize(31, 32);
			markerdeals.iconAnchor = new GPoint(6, 20);
			markerdeals.infoWindowAnchor = new GPoint(5, 1);
				
			var marker = new GMarker(latLng, markerdeals);
		} else {
			var markernormal = new GIcon();
			markernormal.image = MARKER_NORMAL;
			markernormal.iconSize = new GSize(20, 32);
			markernormal.shadow = MARKER_SHADOW;
			markernormal.shadowSize = new GSize(31, 32);
			markernormal.iconAnchor = new GPoint(6, 5);
			markernormal.infoWindowAnchor = new GPoint(5, 1);
			
			var marker = new GMarker(latLng, markernormal);
		}
		
		return marker;
	},
	
	zoomByBounds: function(bounds) {
		var boundsZoom = this.gmap.getBoundsZoomLevel(bounds);
		this.gmap.setZoom(boundsZoom);
		this.gmap.setCenter(bounds.getCenter());
	},
	
	onDrag: function(e) {
		return true;
	},
	
	setList: function(list) {
		this.list = list;
	},
	
	clear: function() {
		this.searchCenterPoint = null;
		this.markerList = new Array();
		this.gmap.clearOverlays();
	}
};
mesq.Map = Class.create(mesq._Map);



/**
 * Displays on the restaurant detail page in the righthand navigation.
 * Works on a single place/marker, so doesn't interact
 * with a mesq.List. 
 */
mesq._MiniMap = {
	
	minZoom: 15,
	
	initialize: function($super, container, placeData) {
		$super(container);
		
		this.gmap.setCenter(new GLatLng(placeData.lat, placeData.lng));
		this.gmap.setZoom(this.minZoom);
		this.gmap.removeControl(this.gmapZoomControl);
		this.gmap.removeControl(this.gmapTypeControl);
		this.gmapZoomControl = new GSmallMapControl();
		this.gmap.addControl(this.gmapZoomControl);
		
		this.addMarkerFromData(placeData);
		
		this.gmap.enableDragging();
	}
	
}
mesq.MiniMap = Class.create(mesq.Map, mesq._MiniMap);



/**
 * This map displays distinct places,
 * similiar to mesq.PlaceMap, but doesn't have searching
 * capabilities - it will display static 
 */
mesq._DealMap = {
	dealTypeSelection: null,
	dealPlacesJsonData: null,
	clusterMarker: null, // ClusterMarker object
	minZoom: 13, // covering a large area, independent of marker distribution#
	baseAccuracyZoom: 7, // zooming based on google geocoder accuracy (added on top of this base value)
	
	initialize: function($super, container) {
		$super(container);
		
		if($('ResultsArea')) $('ResultsArea').hide();
		
		this.dealPlacesJsonData = $("DealPlacesJsonData").innerHTML.evalJSON();

		if(window.app.debugprofile) console.time('mesq.Map.initialize(): Parse Markers');
		// @todo Move this functionality into a DealTypeSelection class
		var tmpMarkers = [];
		for(partnerID in this.dealPlacesJsonData) {
			for(dealCount=0; dealCount<this.dealPlacesJsonData[partnerID].length; dealCount++) {
				var data = this.dealPlacesJsonData[partnerID][dealCount];
				var tmpMarker = this.getMarkerFromData(data);
				tmpMarker.id = Number(data.id);
				tmpMarker.data = data;
				GEvent.addListener(tmpMarker, 'click', this.createListItemForMarker.bind(this, tmpMarker));
				tmpMarkers.push(tmpMarker);
			}
			
			// add count to deal type selection
			var dealCountForPartner = this.dealPlacesJsonData[partnerID].length;
			var partnerNode = $('partner' + partnerID);
			if(partnerNode) partnerNode.down('span.count').innerHTML = dealCountForPartner;
		}
		if(window.app.debugprofile) console.timeEnd('mesq.Map.initialize(): Parse Markers');
		
		// update deal type totals
		// @todo Move this functionality into a DealTypeSelection class
		$$('#DealTypes li.type').each(function(el) {
			var count = 0;
			var counts = $(el).select('.partner .count').pluck('innerHTML');
			for(var i=0; i<counts.length; i++) count += Number(counts[i]); 
			el.down('.count').innerHTML = count;
		});
		
		this.createOverlay();
		
		var clusterMarkerIcon = new GIcon();
		clusterMarkerIcon.image = window.app.baseURL() + MARKER_CLUSTER;
		clusterMarkerIcon.shadow = window.app.baseURL() + MARKER_CLUSTER_SHADOW;
		clusterMarkerIcon.iconSize = new GSize(32, 36);
		//clusterMarkerIcon.shadowSize = new GSize(48, 48);
		clusterMarkerIcon.iconAnchor = new GPoint(10, 10);
		clusterMarkerIcon.infoWindowAnchor = new GPoint(16, 1);
		
		
		// create cluster manager
		this.clusterMarker = new ClusterMarker(this.gmap, {
			markers: tmpMarkers,
			clusterMarkerIcon: clusterMarkerIcon,
			intersectPadding: 10
		});
		this.clusterMarker.refresh();
		
		this.gmap.enableDragging();
		
	},
	
	/**
	 * Execution chain: search -> getGeocode -> getGeocode_onComplete -> localSearch_onComplete
	 */	
	search: function() {
		this.searchParams = window.location.href.toQueryParams();
		if($F($('UserSearch').down('input[name=where]'))) this.searchParams.where = $F($('UserSearch').down('input[name=where]')).stripTags();
 		if(!this.searchParams.where) {
			$('where').addClassName('error');
 			return false;
 		}

 		this.getGeocode(this.searchParams.where);
	},
	
	getGeocode: function(placename) {
		this.geocoder.getLocations(placename, this.getGeocode_onComplete.bind(this));
	},
	
	getGeocode_onComplete: function(response) {
		if(!response.Placemark || response.Status.code != 200) {
			mesq.App.errorMessage("I couldn't recognize the location you entered: '" + this.searchParams.where.urldecode().stripTags() + "'.\nPlease try again.");
			return false;
		}
		var placemark = response.Placemark[0];
		var point = new GLatLng(placemark.Point.coordinates[1], placemark.Point.coordinates[0]);
		var zoom = this.baseAccuracyZoom + placemark.AddressDetails.Accuracy;
		
		this.searchCenterPoint = point;
		this.origCenterPoint = point;
		this.gmap.setCenter(this.searchCenterPoint, zoom);
		this.markCenter(this.searchCenterPoint);
	},
	
	zoomByBounds: function(bounds) {
		var boundsZoom = this.gmap.getBoundsZoomLevel(bounds);
		this.gmap.setZoom((boundsZoom <= this.minZoom) ? boundsZoom : this.minZoom);
		this.gmap.setCenter(bounds.getCenter());
	},
	
	/**
	 * @todo Needs refactoring into a Place class thats not directly related
	 * to list behaviour.
	 */
	createListItemForMarker: function(marker) {
		if(typeof(marker.listItem) != 'undefined') {
			// listitem caching
			marker.listItem.openInfoWindow();
		} else {
			// scaffold new item for quick response in showing infowindow
			// @todo closure?
			marker.listItem = this.list.addItem(marker.data, false, marker);
			// fires off ajax request to get full data and templates (will override the previously set scaffold data)
			marker.listItem.openInfoWindow();
		}
		this.selectedItem = marker.listItem;
	},
	
	setSearching: function(isSearching) {
		
	},

	searchEnabled: function() {
		return true;
		//return this.container.hasClassName('enabled');
	}
};
mesq.DealMap = Class.create(mesq.Map, mesq._DealMap);



/**
 * Wrapper for GMap2 and result-list.
 * Requests and manages server-data about
 * restaurants and deals and dynamically produces
 * the results.
 */
mesq._PlaceMap = {

	// configuration
	numExtendedSearches: 4,
	numExtendedSearchesShowEverything: 10,
	maxDistanceKm: 50, // max distance of the search results from center
  	queryAddition: "Restaurants", 
	queryDefaultWhat: 'Restaurants',
  	extendedSearchDistMultiplier: 1.3,
  	printMarkerLabels: $R('A','Z'),
  	printMarkerImgTemplate: 'http://www.google.com/mapfiles/marker#{number}.png',
  	numberedMarkerImgTemplate: 'themes/mesq/images/markers/marker#{number}.png',

	// state  	
	searchParams: null,
	firstLocalSearch: null,
  	isFirstSearch: true, // no search has been conducted to google local
	searchMode: 'default', // influences the amount of results delivered (Options: 'default'|'showeverything')
  	hasPinned: null, // determines if pinned items from cookie are already added (to prevent continous re-adding)
  	isZoomBlocked: false, // determines if the zoom block is in place, which keeps markers from showing up on too high zoom levels
  	_runningSearches: $A([]), // stores all pending requests to google API and ajax requests to metroseeq server (used to determine isSearching())
  	
  	// strings
  	strNoResultsFound: 'No results found. Please try to refine your search terms.',
	strLoadingFailed: 'Failed to load more information.',
	strLockCenter: 'Lock Current Location',
	strUnlockCenter: 'Unlock Current Location',
	
	// ressources
	zoomWarningImgURL: 'themes/mesq/images/zoomWarningOverlay.png',
	
	// deprecated/unused
	googleBaseResultCount: 8,
	maxResults: 40, // Allows for multiple searches
  	autoZoomDecrease: 0, // decrease zoom on first result to approximate area of subsequent searches
	
	
	initialize: function($super, container) {
  		$super(container);
		
		// don't allow search-features for static lists without search parameters
		if(!this.searchEnabled()) {
			this.setSearching(false);
			$('ResultsStopSearch').disable();
			$('ResultsStopSearchContainer').addClassName('disabled');
			//$('MapLockCenter').disable();
			//$('MapLockCenterContainer').addClassName('disabled');
			$('ResultsStopSearchContainer').removeClassName('enabled');
		}
  		
		GEvent.addListener(this.gmap, "dragend", this.onDrag.bind(this));
				
		 // initiate helpers
		 if(this.searchEnabled() && $('ResultsStopSearch')) {
		 	if(window.app.getPref('ResultsStopSearch')) this.enableStopProgressiveSearch();
		 	
		 	Event.observe($('ResultsStopSearch'), 'click', function(e) {
		 		window.app.setPref('ResultsStopSearch', ($F(Event.element(e))));

		 		// if setting is unticket, refresh current map-area
		 		($F(Event.element(e)) && $F(Event.element(e)) != 'null') ? this.enableStopProgressiveSearch() : this.disableStopProgressiveSearch();
	 		}.bind(this));
		 }
		 
		 /**
		 * If checkbox is ticked, set the computed center to current map center
		 * (and refresh automatically on each drag). Otherwise set back to original
		 * center point (but stay within the same map space).
		 */
		 if(this.searchEnabled() && $('MapLockCenter')) {
		 	// temporarily disabled (don't react to legacy cookie settings)
		 	//if(window.app.getPref('MapLockCenter')) this.lockCenter(true);
		 	
		 	Event.observe($('MapLockCenter'), 'change', function(e) {
		 		//if(this.isSearching) return false;
		 		window.app.setPref('MapLockCenter', ($F(Event.element(e))));
		 		
		 		($F(Event.element(e)) && $F(Event.element(e)) != 'null') ? this.lockCenter() : this.unlockCenter();
		 	}.bind(this));
		 }
	},
	
	/**
	 * Execution chain: search -> getGeocode -> getGeocode_onComplete -> localSearch_onComplete
	 */	
	search: function() {
		this.searchParams = window.location.href.toQueryParams();
		if($F($('UserSearch').down('input[name=where]'))) this.searchParams.where = $F($('UserSearch').down('input[name=where]')).stripTags();
		if($F($('UserSearch').down('input[name=what]'))) this.searchParams.what = $F($('UserSearch').down('input[name=what]')).stripTags();
 		if(!this.searchParams.where) {
			$('where').addClassName('error');
 			return false;
 		}

		// if no what was defined, switch to search for default keywords
		// and trigger more google queries to avoid hidden results
		// (we're naturally dealing with a larger result-set)
 		if(!this.searchParams.what) {
 			this.searchParams.what = this.queryDefaultWhat;
 			this.searchMode = 'showeverything';
 		} else {
 			this.searchMode = 'default';
 		}
 		
 		this.getGeocode(this.searchParams.where);
 		
 		// this.list.showMainLoader();
	},
	
	setSearching: function(isSearching) {
		if(isSearching) {
			$('ResultsLoaded').hide();
			$('ResultsLoadingIndicator').show();
			this.showOverlay();
		} else {
			$('ResultsLoadingIndicator').hide();
			$('ResultsLoaded').show();
			this.hideOverlay();
		}
	},
	
	isSearching: function() {
		return (this._runningSearches.length > 0);
	},
	
	getGeocode: function(placename) {
		this.geocoder.getLatLng(placename, this.getGeocode_onComplete.bind(this));
	},
	
	getGeocode_onComplete: function(point) {
		if(!point) {
			mesq.App.errorMessage("I couldn't recognize the location you entered: '" + this.searchParams.where.urldecode().stripTags() + "'.\nPlease try again.");
			return false;
		}
		
		this.searchCenterPoint = point;
		this.origCenterPoint = point;
		this.gmap.setCenter(this.searchCenterPoint, this.minZoom);
		this.markCenter(this.searchCenterPoint);
		
		var localSearch = this.createLocalSearchObject(this.searchCenterPoint);
		localSearch.setSearchCompleteCallback(this, this.firstLocalSearch_onComplete.bind(this,localSearch));
		
		var searchString = this.getWhereQuery();
		//console.log('getGeocode_onComplete: searching for: "%s"', searchString);
		
		this.setSearching(true);
		this._runningSearches.push(localSearch);
		localSearch.execute(searchString);
		
		// need to remember first search as a starting point for subsequent searches in the area
		if(this.isFirstSearch) this.firstLocalSearch = localSearch;
		
		// augument rss feed
		if($('DealRSSLink')) {
			// update with real location data
			$('DealRSSLink').href = $('DealRSSLink').href.interpolate({'lat': point.lat(), 'lng': point.lng()});
		}
		
		//set cookie to save location search
		var days = 365; //number of days until expires
		var date = new Date();
		date.setTime(date.getTime()+(days*24*60*60*1000));
		document.cookie = 'last_loc='+this.searchParams.where.replace(/;/,'')+'; expires='+date.toGMTString()+'; path=/';
	},
	
	/**
	 * Only triggered after first google query comes back,
	 * which in turn triggers a number of followup queries
	 * (with a different callback).
	 */
	firstLocalSearch_onComplete: function(searcher) {
		this._runningSearches = this._runningSearches.without(searcher);
		
		// if we have local search results, put them on the map
		if(!searcher.results || !searcher.results.length > 0) {
			if(this.isFirstSearch) {
				this.list.setMessage(this.strNoResultsFound,'error');
				this.gmap.enableDragging();
				this.setSearching(false);
				
			}
			return false;
		}
		
		var bounds = new GLatLngBounds();
		for(var i=0; i<searcher.results.length; i++) {
   			// compute bounds (automatic zoomlevel)
			var latLng = new GLatLng(parseFloat(searcher.results[i].lat), parseFloat(searcher.results[i].lng));
			bounds.extend(latLng);
		}
		
		// automatically set zoom based on first result set
		// if zoom is too small (e.g. large area with few matches),
		// automatically focus on first result with a minimal zoom
		bounds.extend(this.searchCenterPoint);
		this.zoomByBounds(bounds);
		
		// process results
		this.localSearch_onComplete(searcher, false);
		
		// monitor zooming
		GEvent.addListener(this.gmap, 'zoomend', this.gmap_onZoomEnd.bind(this));
		
		// trigger more searches if necessary
		this.extendedSearches(bounds);
	},
	
	localSearch_onComplete: function(searcher) {
		this._runningSearches = this._runningSearches.without(searcher);
		
		// if we have local search results, put them on the map
		if(!searcher.results || !searcher.results.length > 0) {
			if(this.isFirstSearch) {
				this.list.setMessage(this.strNoResultsFound,'error');
				this.gmap.enableDragging();
				this.setSearching(false);
				
			}
			return false;
		}
		
		if(!this.isSearching()) {
			this.container.fire('Map:resultsloaded');
		}
		
		var request = new Ajax.Request(
			'place/batchAugumentOrCreate',
			{
				method:'post',
				parameters: {
					jsonResults: searcher.results.toJSON(),
					lat: this.searchCenterPoint.lat(),
					lng: this.searchCenterPoint.lng()
				},
				onSuccess: this.updateResults.bind(this),
				onFailure: function(e) {this.list.setMessage(this.strLoadingFailed,'error');}.bind(this)
			}
		);
		this._runningSearches.push(request.transport);
		
		searcher = null;
	},
	
	/**
	 * Work around limitation of 8 results on each API call by
	 * looking at the first result boundaries and triggering
	 * more searches (based on this.maxResults).
	 * 
	 * Search area looks like this ('c' is the bounds center, numbers are different increments):
	 *       [6]
	 *    [1]   [2]
	 * [5]   [c]   [7]
	 *    [4]   [3]
	 *       [8]
	 * We've got two different types of loop ("rectangle-corners" and "diamond-corners")
	 * than can theoretically be followed infinetly, but with decreasing population
	 * in the spaces between the corners as their distance for each other gets larger).
	 */
	extendedSearches: function(bounds) {
		// already did one search, so substract one
		var southWestPoint = bounds.getSouthWest();
		var northEastPoint = bounds.getNorthEast();
		var southEastPoint = new GLatLng(northEastPoint.lat(),southWestPoint.lng());
		var northWestPoint = new GLatLng(southWestPoint.lat(),northEastPoint.lng());
		var boundsCenter = bounds.getCenter();
		
		var times = (this.searchMode == 'showeverything') ? this.numExtendedSearchesShowEverything : this.numExtendedSearches;
		for(var i=1; i<times+1; i++) {
			var loopRun = Math.ceil(i/4);
			var loopType = ((loopRun % 2) == 0) ? 'diamond' : 'rect'; // modulo is either 0 or 1
			var loopCorner = (i % 4 == 0) ? 4 : i % 4;
			var distLatFromCenter = (southWestPoint.lat() - boundsCenter.lat()) * this.extendedSearchDistMultiplier * loopRun;
			var distLngFromCenter = (southWestPoint.lng() - boundsCenter.lng()) * this.extendedSearchDistMultiplier * loopRun;
			
			// choose bigger of two distances to have uniform distribution
			if(distLatFromCenter > distLngFromCenter) distLngFromCenter = distLatFromCenter;
			if(distLngFromCenter > distLatFromCenter) distLatFromCenter = distLngFromCenter;
			
			// TODO replace with a bit smarter sine/cosine computation
			if(loopType == 'diamond') {
				// choose four corners of a diamond
				switch(loopCorner) {
					case 1: // or 5,13,...
						var areaPoint = new GLatLng(
							northWestPoint.lat() - distLatFromCenter, 
							northWestPoint.lng() - distLngFromCenter
						);
						break;
					case 2: // or 6,14,...
						var areaPoint = new GLatLng(
							northEastPoint.lat() + distLatFromCenter, 
							northEastPoint.lng() - distLngFromCenter
						);
						break;
					case 3:
						var areaPoint = new GLatLng(
							southEastPoint.lat() + distLatFromCenter, 
							southEastPoint.lng() + distLngFromCenter
						);
						break;
					case 4:
						var areaPoint = new GLatLng(
							southWestPoint.lat() - distLatFromCenter, 
							southWestPoint.lng() + distLngFromCenter
						);
						break;
				}
			} else if(loopType == 'rect') {
				// choose four corners of a rect
				switch(loopCorner) {
					case 1: // or 1,9,...
						var areaPoint = new GLatLng(
							northWestPoint.lat() - distLatFromCenter, 
							northWestPoint.lng() - distLngFromCenter
						);
						break;
					case 2: // or 2,10, ...
						var areaPoint = new GLatLng(
							northEastPoint.lat() + distLatFromCenter, 
							northEastPoint.lng() - distLngFromCenter
						);
						break;
					case 3:
						var areaPoint = new GLatLng(
							southEastPoint.lat() + distLatFromCenter, 
							southEastPoint.lng() + distLngFromCenter
						);
						break;
					case 4:
						var areaPoint = new GLatLng(
							southWestPoint.lat() - distLatFromCenter, 
							southWestPoint.lng() + distLngFromCenter
						);
						break;
				}
			}
			var localSearch = this.createLocalSearchObject(areaPoint);
			localSearch.setSearchCompleteCallback(this, this.localSearch_onComplete.bind(this,localSearch));
			
			// add center markers
			if(window.app.debug) {
				var icon = new GIcon();
				icon.image = window.app.baseURL() + this.numberedMarkerImgTemplate.interpolate({'number':i});
				icon.iconSize = new GSize(20, 32);
				icon.iconAnchor = new GPoint(6, 5);
				icon.clickable = false;
				var marker = new GMarker(areaPoint, icon);
				this.gmap.addOverlay(marker, icon);
			}
			
			// performance: delay calls for a bit as they cause serious computing in their callbacks
			// (mainly DOM manipulation and list sorting)
			setTimeout(function(_localSearch) {
				this.setSearching(true);
				this._runningSearches.push(_localSearch);
				_localSearch.execute(this.getWhereQuery());
			}.bind(this, localSearch), i*500);
		}
		
		// add center markers
		if(window.app.debug) {
			$A([boundsCenter, northWestPoint, southWestPoint, northEastPoint, southEastPoint]).each(function(p) {
				var marker = new GMarker(p);
				this.gmap.addOverlay(marker);
			}.bind(this));
		}
	},
	
	refresh: function() {
		if(window.app.debug) console.debug("PlaceMap.refresh() called");
		
		//then move center point of search to center point on map and re-run the search
		this.searchCenterPoint = this.gmap.getCenter();
		
		// has to be searchCenterPoint, unrelated to UnlockCenter flag
		// TODO resolve closure
		var localSearch = this.createLocalSearchObject(this.searchCenterPoint);
		localSearch.setSearchCompleteCallback(this, this.localSearch_onComplete.bind(this,localSearch));
		
		this.setSearching(true);
		this._runningSearches.push(localSearch);
		localSearch.execute(this.getWhereQuery());
	},
	
	updateResults:function(response) {
		this._runningSearches = this._runningSearches.without(response.transport);
		
		var filteredResults = response.responseText.evalJSON();
		
		// hide loaders
		// this.list.hideMainLoader();
	
		if(window.app.debugprofile) console.time('mesq.PlaceMap.updateResults(): Add ListItems');
		// TODO Merge filtered and original results
		for(var i=0; i<filteredResults.length; i++) {
			this.list.addItem(filteredResults[i]);
   		}
		if(window.app.debugprofile) console.timeEnd('mesq.PlaceMap.updateResults(): Add ListItems');
		
   		// only needs to be executed once (not on every refresh)
   		if(!this.isSearching() && !this.hasPinned) {
   			this.list.addPinnedFromCookie();
   			this.hasPinned = true;
   		}

   		this.list.refresh();
   		
		this.setSearching(this.isSearching());

   		if(this.isSearching() || !$F('MapLockCenter')) {
   			this.markCenter();
   		}
   		
   		// don't allow dragging during initial searches (only re-enable after last search is completed)
   		if(!this.isSearching()) this.gmap.enableDragging();
		
	},
	
	showPlaceByID: function(id) {
		this.openInfoWindowByID(id);
	},
	
	getWhereQuery: function() {
		var query = this.searchParams.what;
		query += (this.showRestaurantsOnly()) ? ' ' + this.queryAddition : '';
		return query;
	},
	
	showRestaurantsOnly: function() {
		return document.location.href.toQueryParams().foodonly;
	},
	
	openInfoWindowByID: function(id) {
		var item = this.list.getItemByID(id);
		if(!item) return false;
		
		item.openInfoWindow();
	},
	
	onDrag: function(e){
		if(this.gmap.getZoom() < this.minZoom) return false;
		
		// markcenter can either be done directly, or after refresh callbacks
		if($F('ResultsStopSearch')) {
			// default to center of map as we can't get the boundary boxes from a new search result
			if(!$F('MapLockCenter')) {
				this.markCenter(this.gmap.getCenter());
				this.list.refresh();
			}
		} else {
			this.refresh();
		}
	},
	
	gmap_onZoomEnd: function(oldLevel, newLevel) {
		// don't apply zoom restriction, see http://helpdesk.silverstripe.com/tickets/327
		//return true;

		// if the requested zoom level is lower (=showing more) than the minimum
		// level, AND there's no hidden markers outside of the current zoom bounds
		// that would require the map to display at a higher zoomlevel
		var bounds = this.getBoundsFromMarkers();
		if(newLevel < this.minZoom && this.gmap.getBoundsZoomLevel(bounds) >= this.minZoom) {
			// remove all overlays
			this.gmap.clearOverlays();
			this.zoomWarningOverlay = new GScreenOverlay(
				window.app.baseURL() + this.zoomWarningImgURL,
				new GScreenPoint(70, 385),  // screenXY
        		new GScreenPoint(0, 0),  // overlayXY
        		new GScreenSize(238, 83)  // size on screen
			);
			this.gmap.addOverlay(this.zoomWarningOverlay);
			this.isZoomBlocked = false;
		} else if(oldLevel >= this.minZoom){
			// coming from cleared map into new showing
			this.refreshMarkers();
			this.markCenter();
			this.isZoomBlocked = true;
			if(typeof this.zoomWarningOverlay != 'undefined') this.gmap.removeOverlay(this.zoomWarningOverlay);
		}
	},
	
	zoomByBounds: function(bounds) {
		// make sure the center point is still in view
		if(this.searchCenterPoint) bounds.extend(this.searchCenterPoint);
		var mapBoundsZoom = this.gmap.getBoundsZoomLevel(bounds);
		this.gmap.setZoom(mapBoundsZoom);
		/*
		// set to minimum possible zoom
		this.gmap.setZoom(this.minZoom);
		this.gmap.setCenter(bounds.getCenter());
		// if no markers are visible in this zoom, fall back to zoom computed by bounds
		if(!this.hasVisibleMarkers()) {
			var mapBoundsZoom = this.gmap.getBoundsZoomLevel(bounds);
			this.gmap.setZoom(mapBoundsZoom);
		}
		*/
	},
	
	getBoundsFromMarkers: function() {
		var bounds = new GLatLngBounds();
		for(var i=0; i<this.markers.length; i++) bounds.extend(this.markers[i].getPoint());
		return bounds;
	},
	
	hasVisibleMarkers: function() {
		return false;
	},
	
	createLocalSearchObject: function(centerPoint) {
 		var localSearch = new GlocalSearch();
		localSearch.setNoHtmlGeneration(); // Don't need HTML generation, outputting custom list
		localSearch.setResultSetSize(GSearch.LARGE_RESULTSET);
		// @see http://groups.google.com/group/Google-AJAX-Search-API/browse_frm/thread/4e40258efa05b8d1
		//localSearch.setAddressLookupMode(GlocalSearch.ADDRESS_LOOKUP_DISABLED); // Only search names, not addresses
		localSearch.setCenterPoint(centerPoint);
		
		return localSearch;
	},
	
	/**
	 * We need to disable searching for seeqlists,
	 * they don't have a center-point or search-terms
	 */
	searchEnabled: function() {
		return this.container.hasClassName('enabled');
	},
	
	enableStopProgressiveSearch: function() {
		$('ResultsStopSearch').checked = true;
		$('ResultsStopSearchContainer').addClassName('enabled');
	},
	
	disableStopProgressiveSearch: function() {
		$('ResultsStopSearch').checked = false;
		$('ResultsStopSearchContainer').removeClassName('enabled');
	},
	
	clear: function() {
		superclass.clear();
		
		this.searchParams = null;
		this.localSearch.clearResults();
	}
};
mesq.PlaceMap = Class.create(mesq.Map, mesq._PlaceMap); 