/*
	ClusterMarker Version 1.3.2
	
	A marker manager for the Google Maps API
	http://googlemapsapi.martinpearman.co.uk/clustermarker
	
	Copyright Martin Pearman 2008
	Last updated 29th September 2008

	This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

	You should have received a copy of the GNU General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.
	
*/

function ClusterMarker($map, $options){
	this._map=$map;
	this._mapMarkers=[];
	this._iconBounds=[];
	this._clusterMarkers=[];
	this._eventListeners=[];
	if(typeof($options)==='undefined'){
		$options={};
	}
	this.borderPadding=($options.borderPadding)?$options.borderPadding:256;
	this.clusteringEnabled=($options.clusteringEnabled===false)?false:true;
	if($options.clusterMarkerClick){
		this.clusterMarkerClick=$options.clusterMarkerClick;
	}
	if($options.clusterMarkerIcon){
		this.clusterMarkerIcon=$options.clusterMarkerIcon;
	}else{
		this.clusterMarkerIcon=new GIcon();
		this.clusterMarkerIcon.image='http://maps.google.com/mapfiles/arrow.png';
		this.clusterMarkerIcon.iconSize=new GSize(39, 34);
		this.clusterMarkerIcon.iconAnchor=new GPoint(9, 31);
		this.clusterMarkerIcon.infoWindowAnchor=new GPoint(9, 31);
		this.clusterMarkerIcon.shadow='http://www.google.com/intl/en_us/mapfiles/arrowshadow.png';
		this.clusterMarkerIcon.shadowSize=new GSize(39, 34);
	}
	this.clusterMarkerTitle=($options.clusterMarkerTitle)?$options.clusterMarkerTitle:'Click to zoom in and see %count markers';
	if($options.fitMapMaxZoom){
		this.fitMapMaxZoom=$options.fitMapMaxZoom;
	}
	this.intersectPadding=($options.intersectPadding)?$options.intersectPadding:0;
	if($options.markers){
		this.addMarkers($options.markers);
	}
	GEvent.bind(this._map, 'moveend', this, this._moveEnd);
	GEvent.bind(this._map, 'zoomend', this, this._zoomEnd);
	GEvent.bind(this._map, 'maptypechanged', this, this._mapTypeChanged);
}

ClusterMarker.prototype.addMarkers=function($markers){
	var i;
	/*if(!$markers[0]){
		//	assume $markers is an associative array and convert to a numerically indexed array
		var $numArray=[];
		for(i in $markers){
			$numArray.push($markers[i]);
		}
		$markers=$numArray;
	}*/
	for(i=$markers.length-1; i>=0; i--){
		$markers[i]._isVisible=false;
		$markers[i]._isActive=false;
		$markers[i]._makeVisible=false;
	}
	this._mapMarkers=this._mapMarkers.concat($markers);
};

ClusterMarker.prototype._clusterMarker=function($clusterGroupIndexes){
	function $newClusterMarker($location, $icon, $title){
		return new GMarker($location, {icon:$icon, title:$title});
	}
	var $clusterGroupBounds=new GLatLngBounds(), i, $clusterMarker, $clusteredMarkers=[], $marker, $this=this, $mapMarkers=this._mapMarkers;
	for(i=$clusterGroupIndexes.length-1; i>=0; i--){
		$marker=$mapMarkers[$clusterGroupIndexes[i]];
		$marker.index=$clusterGroupIndexes[i];
		$clusterGroupBounds.extend($marker.getLatLng());
		$clusteredMarkers.push($marker);
	}
	var myIcon=new GIcon(this.clusterMarkerIcon);
	var markerCount=$clusteredMarkers.length;
	myIcon.image = "/images/gmaps/"+markerCount+".png"
	$clusterMarker=$newClusterMarker($clusterGroupBounds.getCenter(), myIcon, this.clusterMarkerTitle.replace(/%count/gi, $clusterGroupIndexes.length));
	$clusterMarker.clusterGroupBounds=$clusterGroupBounds;	//	only req'd for default cluster marker click action
	this._eventListeners.push(GEvent.addListener($clusterMarker, 'click', function(){
		$this.clusterMarkerClick({clusterMarker:$clusterMarker, clusteredMarkers:$clusteredMarkers });
	}));
	$clusterMarker._childIndexes=$clusterGroupIndexes;
	for(i=$clusterGroupIndexes.length-1; i>=0; i--){
		$mapMarkers[$clusterGroupIndexes[i]]._parentCluster=$clusterMarker;
	}
	return $clusterMarker;
};

ClusterMarker.prototype.clusterMarkerClick=function($args){
	this._map.setCenter($args.clusterMarker.getLatLng(), this._map.getBoundsZoomLevel($args.clusterMarker.clusterGroupBounds));
};

ClusterMarker.prototype._filterActiveMapMarkers=function(){
	var $borderPadding=this.borderPadding, $mapZoomLevel=this._map.getZoom(), $mapProjection=this._map.getCurrentMapType().getProjection(), $mapPointSw, $activeAreaPointSw, $activeAreaLatLngSw, $mapPointNe, $activeAreaPointNe, $activeAreaLatLngNe, $activeAreaBounds=this._map.getBounds(), i, $marker, $uncachedIconBoundsIndexes=[], $oldState, $mapMarkers=this._mapMarkers, $iconBounds=this._iconBounds;
	if($borderPadding){
		$mapPointSw=$mapProjection.fromLatLngToPixel($activeAreaBounds.getSouthWest(), $mapZoomLevel);
		$activeAreaPointSw=new GPoint($mapPointSw.x-$borderPadding, $mapPointSw.y+$borderPadding);
		$activeAreaLatLngSw=$mapProjection.fromPixelToLatLng($activeAreaPointSw, $mapZoomLevel);
		$mapPointNe=$mapProjection.fromLatLngToPixel($activeAreaBounds.getNorthEast(), $mapZoomLevel);
		$activeAreaPointNe=new GPoint($mapPointNe.x+$borderPadding, $mapPointNe.y-$borderPadding);
		$activeAreaLatLngNe=$mapProjection.fromPixelToLatLng($activeAreaPointNe, $mapZoomLevel);
		$activeAreaBounds.extend($activeAreaLatLngSw);
		$activeAreaBounds.extend($activeAreaLatLngNe);
	}
	this._activeMarkersChanged=false;
	if(typeof($iconBounds[$mapZoomLevel])==='undefined'){
		//	no iconBounds cached for this zoom level
		//	no need to check for existence of individual iconBounds elements
		this._iconBounds[$mapZoomLevel]=[];
		this._activeMarkersChanged=true;	//	force refresh(true) as zoomed to uncached zoom level
		for(i=$mapMarkers.length-1; i>=0; i--){
			$marker=$mapMarkers[i];
			$marker._isActive=$activeAreaBounds.containsLatLng($marker.getLatLng())?true:false;
			$marker._makeVisible=$marker._isActive;
			if($marker._isActive){
				$uncachedIconBoundsIndexes.push(i);
			}
		}
	}else{
		//	icondBounds array exists for this zoom level
		//	check for existence of individual iconBounds elements
		for(i=$mapMarkers.length-1; i>=0; i--){
			$marker=$mapMarkers[i];
			$oldState=$marker._isActive;
			$marker._isActive=$activeAreaBounds.containsLatLng($marker.getLatLng())?true:false;
			$marker._makeVisible=$marker._isActive;
			if(!this._activeMarkersChanged && $oldState!==$marker._isActive){
				this._activeMarkersChanged=true;
			}
			if($marker._isActive && typeof($iconBounds[$mapZoomLevel][i])==='undefined'){
				$uncachedIconBoundsIndexes.push(i);
			}
		}
	}
	return $uncachedIconBoundsIndexes;
};

ClusterMarker.prototype._filterIntersectingMapMarkers=function(){
	var $clusterGroup, i, j, $mapZoomLevel=this._map.getZoom(), $mapMarkers=this._mapMarkers, $iconBounds=this._iconBounds;
	for(i=$mapMarkers.length-1; i>0; i--)
	{
		if($mapMarkers[i]._makeVisible){
			$clusterGroup=[];
			for(j=i-1; j>=0; j--){
				if($mapMarkers[j]._makeVisible && $iconBounds[$mapZoomLevel][i].intersects($iconBounds[$mapZoomLevel][j])){
					$clusterGroup.push(j);
				}
			}
			if($clusterGroup.length!==0){
				$clusterGroup.push(i);
				for(j=$clusterGroup.length-1; j>=0; j--){
					$mapMarkers[$clusterGroup[j]]._makeVisible=false;
				}
				this._clusterMarkers.push(this._clusterMarker($clusterGroup));
			}
		}
	}
};

ClusterMarker.prototype.fitMapToMarkers=function(){
	var $mapMarkers=this._mapMarkers, $markersBounds=new GLatLngBounds(), i;
	if($mapMarkers.length === 0){return;}
	for(i=$mapMarkers.length-1; i>=0; i--){
		$markersBounds.extend($mapMarkers[i].getLatLng());
	}
	var $fitMapToMarkersZoom=this._map.getBoundsZoomLevel($markersBounds);
		
	if(this.fitMapMaxZoom && $fitMapToMarkersZoom>this.fitMapMaxZoom){
		$fitMapToMarkersZoom=this.fitMapMaxZoom;
	}
	this._map.setCenter($markersBounds.getCenter(), $fitMapToMarkersZoom);
	this.refresh();
};

ClusterMarker.prototype._mapTypeChanged=function(){
	this.refresh(true);
};

ClusterMarker.prototype._moveEnd=function(){
	if(!this._cancelMoveEnd){
		this.refresh();
	}else{
		this._cancelMoveEnd=false;
	}
};

ClusterMarker.prototype._preCacheIconBounds=function($indexes, $mapZoomLevel){
	var $mapProjection=this._map.getCurrentMapType().getProjection(), i, $marker, $iconSize, $iconAnchorPoint, $iconAnchorPointOffset, $iconBoundsPointSw, $iconBoundsPointNe, $iconBoundsLatLngSw, $iconBoundsLatLngNe, $intersectPadding=this.intersectPadding, $mapMarkers=this._mapMarkers;
	for(i=$indexes.length-1; i>=0; i--){
		$marker=$mapMarkers[$indexes[i]];
		$iconSize=$marker.getIcon().iconSize;
		$iconAnchorPoint=$mapProjection.fromLatLngToPixel($marker.getLatLng(), $mapZoomLevel);
		$iconAnchorPointOffset=$marker.getIcon().iconAnchor;
		$iconBoundsPointSw=new GPoint($iconAnchorPoint.x-$iconAnchorPointOffset.x-$intersectPadding, $iconAnchorPoint.y-$iconAnchorPointOffset.y+$iconSize.height+$intersectPadding);
		$iconBoundsPointNe=new GPoint($iconAnchorPoint.x-$iconAnchorPointOffset.x+$iconSize.width+$intersectPadding, $iconAnchorPoint.y-$iconAnchorPointOffset.y-$intersectPadding);
		$iconBoundsLatLngSw=$mapProjection.fromPixelToLatLng($iconBoundsPointSw, $mapZoomLevel);
		$iconBoundsLatLngNe=$mapProjection.fromPixelToLatLng($iconBoundsPointNe, $mapZoomLevel);
		this._iconBounds[$mapZoomLevel][$indexes[i]]=new GLatLngBounds($iconBoundsLatLngSw, $iconBoundsLatLngNe);
	}
};

ClusterMarker.prototype.refresh=function($forceFullRefresh){
	var i, $marker, $zoomLevel=this._map.getZoom(), $uncachedIconBoundsIndexes=this._filterActiveMapMarkers();
	if(this._activeMarkersChanged || $forceFullRefresh){
		this._removeClusterMarkers();
		if(this.clusteringEnabled && $zoomLevel<this._map.getCurrentMapType().getMaximumResolution()){
			if($uncachedIconBoundsIndexes.length>0){
				this._preCacheIconBounds($uncachedIconBoundsIndexes, $zoomLevel);
			}
			this._filterIntersectingMapMarkers();
		}
		for(i=this._clusterMarkers.length-1; i>=0; i--){
			this._map.addOverlay(this._clusterMarkers[i]);
		}
		for(i=this._mapMarkers.length-1; i>=0; i--){
			$marker=this._mapMarkers[i];
			if(!$marker._isVisible && $marker._makeVisible){
				this._map.addOverlay($marker);
				$marker._isVisible=true;
				if($marker.data && $marker.data.length && $marker.data.length > 1){
					$marker.setImage('/images/gmaps/'+$marker.data.length+'.png');
				}
			}
			if($marker._isVisible && !$marker._makeVisible){
				this._map.removeOverlay($marker);
				$marker._isVisible=false;
			}
		}
	}
};

ClusterMarker.prototype._removeClusterMarkers=function(){
	var i, j, $map=this._map, $eventListeners=this._eventListeners, $clusterMarkers=this._clusterMarkers, $childIndexes, $mapMarkers=this._mapMarkers;
	for(i=$clusterMarkers.length-1; i>=0; i--){
		$childIndexes=$clusterMarkers[i]._childIndexes;
		for(j=$childIndexes.length-1; j>=0; j--){
			delete $mapMarkers[$childIndexes[j]]._parentCluster;
		}
		$map.removeOverlay($clusterMarkers[i]);
	}
	for(i=$eventListeners.length-1; i>=0; i--){
		GEvent.removeListener($eventListeners[i]);
	}
	this._clusterMarkers=[];
	this._eventListeners=[];
};

ClusterMarker.prototype.removeMarkers=function(){
	var i, $mapMarkers=this._mapMarkers, $map=this._map;
	for(i=$mapMarkers.length-1; i>=0; i--){
		if($mapMarkers[i]._isVisible){
			$map.removeOverlay($mapMarkers[i]);
		}
		delete $mapMarkers[i]._isVisible;
		delete $mapMarkers[i]._isActive;
		delete $mapMarkers[i]._makeVisible;
	}
	this._removeClusterMarkers();
	this._mapMarkers=[];
	this._iconBounds=[];
};

ClusterMarker.prototype.triggerClick=function($index){
	var $marker=this._mapMarkers[$index];
	if($marker._isVisible){
		//	$marker is visible
		GEvent.trigger($marker, 'click');
	}
	else if($marker._isActive){
		//	$marker is clustered
		var $clusteredMarkersIndexes=$marker._parentCluster._childIndexes, $intersectDetected=true, $uncachedIconBoundsIndexes, i, $mapZoomLevel=this._map.getZoom(), $clusteredMarkerIndex, $iconBounds=this._iconBounds, $mapMaxZoomLevel=this._map.getCurrentMapType().getMaximumResolution();
		while($intersectDetected && $mapZoomLevel<$mapMaxZoomLevel){
			$intersectDetected=false;
			$mapZoomLevel++;
			if(typeof($iconBounds[$mapZoomLevel])==='undefined'){
				//	no iconBounds cached for this zoom level
				//	no need to check for existence of individual iconBounds elements
				$iconBounds[$mapZoomLevel]=[];
				// need to create cache for all clustered markers at $mapZoomLevel
				this._preCacheIconBounds($clusteredMarkersIndexes, $mapZoomLevel);
			}else{
				//	iconBounds array exists for this zoom level
				//	check for existence of individual iconBounds elements
				$uncachedIconBoundsIndexes=[];
				for(i=$clusteredMarkersIndexes.length-1; i>=0; i--){
					if(typeof($iconBounds[$mapZoomLevel][$clusteredMarkersIndexes[i]])==='undefined'){
						$uncachedIconBoundsIndexes.push($clusteredMarkersIndexes[i]);
					}
				}
				if($uncachedIconBoundsIndexes.length>=1){
					this._preCacheIconBounds($uncachedIconBoundsIndexes, $mapZoomLevel);
				}
			}
			for(i=$clusteredMarkersIndexes.length-1; i>=0; i--){
				$clusteredMarkerIndex=$clusteredMarkersIndexes[i];
				if($clusteredMarkerIndex!==$index && $iconBounds[$mapZoomLevel][$clusteredMarkerIndex].intersects($iconBounds[$mapZoomLevel][$index])){	
					$intersectDetected=true;
					break;
				}
			}
			
		};
		this._map.setCenter($marker.getLatLng(), $mapZoomLevel);
		this.triggerClick($index);
	}else{
		// $marker is not within active area (map bounds + border padding)
		this._map.setCenter($marker.getLatLng());
		this.triggerClick($index);
	}
};

ClusterMarker.prototype._zoomEnd=function(){
	this._cancelMoveEnd=true;
	this.refresh(true);
};


var baseIcon = false;
var markerPoints = 0;
var selected_marker = null;
var latlngbounds = null;
var map_markers = {};

function drawAndSetListMapEvents(properties){
	var map_top = $('map_area').positionedOffset().top;
	$$('tr.item').each( function(elem){ addListMapObservers(elem) })
	plotSearchListMap($('list_map'), properties);
	if(!Prototype.Browser.IE6 ){
		Event.observe(window,'scroll', function(){
			if ( document.viewport.getScrollOffsets().top > map_top){
				$('map_area').addClassName('fixed');
			} else {
				$('map_area').removeClassName('fixed');
			}
		})
	}
}
function plotSearchListMap(container, properties){
	if (GBrowserIsCompatible()){
		map = new GMap2(container);
		latlngbounds = new GLatLngBounds()
		properties.each(function(p){
			var marker = createSearchListMarker(p);
			map.addOverlay(marker);
		});
		map.setCenter( latlngbounds.getCenter( ), map.getBoundsZoomLevel( latlngbounds ) );
		map.addControl( new GSmallZoomControl3D() );
		map.addControl( new GMenuMapTypeControl() );
		return map;
	}
}
function createSearchListMarker(data){
	if (! baseIcon ){
		baseIcon = new GIcon();
		baseIcon.image = "/images/gmaps/pin-propiedades.gif";
		baseIcon.iconSize = new GSize(30, 45);
		baseIcon.iconAnchor = new GPoint(15,45);
		baseIcon.infoWindowAnchor = new GPoint(9, 2);
	}
	var latlng = new GLatLng(data.lat, data.lon);
	var marker = new GMarker(latlng, {icon: baseIcon});
	marker.hash_id = 'itp'+data.id;
	GEvent.addListener(marker, "click", function(){ toggleSearchMapIcons(marker) });
	
	map_markers[marker.hash_id] = marker;
	
	latlngbounds.extend( latlng );
	
	return marker;

}
function toggleSearchMapIcons(marker){
	resetSearchMapIcons();
	window.location.hash = marker.hash_id;
	$(marker.hash_id).addClassName('highlighted');
	setMarker(marker);
}
function setMarker(marker){
	marker.setImage('/images/gmaps/pin-prop-selected.gif');
	selected_marker = marker;
}
function resetSearchMapIcons(){
	$$('.item.highlighted').each(function(e){e.removeClassName('highlighted')});
	selected_marker && selected_marker.setImage('/images/gmaps/pin-propiedades.gif');
	selected_marker = null;
}

function addListMapObservers(elem){
	elem.observe('mouseover', function(e){
		resetSearchMapIcons();
		this.addClassName('highlighted');
		var mark = map_markers[elem.id];
		if(mark){
			setMarker(mark);
		}
	});
	elem.observe('mouseout', function(e){
		resetSearchMapIcons();
	})
}

function createMarker( data, previousMarkers ){
	if (! baseIcon ){
		baseIcon = new GIcon();
		baseIcon.image = '/images/gmaps/pin-propiedades.gif';
		baseIcon.iconSize = new GSize(30, 45);
		baseIcon.iconAnchor = new GPoint(15,45);
		baseIcon.infoWindowAnchor = new GPoint(9, 2);
	}
	var letteredIcon = new GIcon(baseIcon);
	var lat = data.lat, lon = data.lon;
	var point = new GLatLng( lat, lon );
	markerOptions = { icon:letteredIcon };
	var marker = null;
	if ( previousMarkers && previousMarkers[point.toString()] ){
		marker = previousMarkers[point.toString()];
		if ( data.featured_rank ){
			marker.data.unshift(data);
		}
		else {
			marker.data.push(data);
		}
	}
	else {  
		marker = new GMarker(point, markerOptions);
		marker.data = [data];
		marker.SAInfoWindow = function() {
			var form = $('property_sideform');
			if(form){
				form.select('.prop_ids').invoke('remove');
				form.appendChild( new Element('input', {'type':'hidden', 'name':'search[prop_ids]', 'value':this.data.map(function(e){ return e.id}).join(','), 'class':'prop_ids'}) );
				searchControl.hashSerializeForm();
			}
			
			var title = '', window_height = 150;
			if (this.data.length > 1){
				window_height = 170;
				title = "<h4>Se han encontrado "+this.data.length+" propiedades</h4>";
			}
			
			var listing = new Element('div', {'class':'map_listing','style':'width:490px;height:150px;overflow:auto;'}).update("<img src='/images/spinner.gif' />");

			var window_info = new Element('div', {'style':'width:500px;height:'+window_height+'px;'}).update(title).insert(listing);
			this.openInfoWindow(window_info);
			
			new Ajax.Request('/data/realestate', {
				method:'get',
				parameters:this.data.map(function(e){return 'ids[]='+e.id}).join('&'),
				onSuccess:function(transport){
					listing.update();
					var response = transport.responseJSON;
					for(var j = 0; j < response.length; ++j){
						var data = response[j];
						var link_url = data.get_url;
						var html = "<div class='map_info_window" + (data.featured_rank ? (' featured_' + j) : '') +"' style='overflow:auto;'><div class='image slideshow'></div><div class='details'><h1><a href='"+(link_url)+"'>"+data.title+"</a></h1>";
						if (data.price && data.priceUnit){html += "<div class='price'>"+data.priceUnit+" "+number_with_delimiter(data.price)+"</div>";}
						if (data.address_html){html += "<address>"+data.address_html+"</address>";}
						if (data.features){html += "<p>"+data.features.truncate(80)+"</p>"; }
						if (data.description){html += "<p>"+data.description.truncate(80)+"</p>";}
						html += "<a class='more' href='"+(link_url)+"'>Ver +</a>";
						html += "<a class='source'>Publicado en "+data.source_name+" el "+data.publish_date+"</a>";
						html += "</div><br class='clearfix' /></div>";
				
						var o = new Element('div', {'class' : 'map_listing_item'}).update(html);
						var images_container = o.select("div.image")[0];
						if(data.images){
							images_to_slideshow(data.images.map(function(i){return i.url}), images_container);
						}else if (data.first_image){
							images_container.update("<img src='"+data.first_image.url+"' />");
							images_container.removeClassName("slideshow");
						}else{
							images_container.removeClassName("slideshow");
							images_container.addClassName("default");
							images_container.update("<img src='http://a0.sumavisos.com/images/realestate100px.png' />");
						}
				
						listing.appendChild(o);
					}
				}
			})
		}
		GEvent.addListener(marker, "click", marker.SAInfoWindow);
		if (previousMarkers) {
			previousMarkers[point.toString()] = marker;
		}
	}
	return marker;

}
//global circle_radius, for the map
var circle_radius = 500;
var map=null;
function plotMap(container, center, properties, no_search, translations_text, fixed_size) {
	if (GBrowserIsCompatible()) {
		//global map
		var map_options = {}
		if(fixed_size){
			map_options['size'] = fixed_size
		}
		map = new GMap2(container, map_options);
		var mapCenter = (center) ? center : {"lat":-34.600433,"lon":-58.466492} ;
		map.setCenter(new GLatLng(mapCenter.lat, mapCenter.lon), 12);

		map.addControl(new GLargeMapControl3D(map));
		map.addControl(new GMapTypeControl(map));
		map.addControl(new GScaleControl());
		map.addControl(new GOverviewMapControl());
		map.addControl(new PanoMapTypeControl(map));
		var markers = [];
		var previousMarkers = {};
		for(var i=0;i < properties.length; ++i){
			var marker = createMarker( properties[i], previousMarkers );
			markers.push( marker );
		}
		
		var numericIcon = new GIcon();
		numericIcon.iconSize = new GSize(37, 51);
		numericIcon.iconAnchor = new GPoint(19,51);
		numericIcon.infoWindowAnchor = new GPoint(9, 2);

		var cluster=new ClusterMarker(map, { markers:markers , clusterMarkerIcon:numericIcon, intersectPadding:8 } );
		cluster.fitMapToMarkers();
		
		if(!no_search){
			searchControl = new SearchControl(map);
			searchControl.translations = translations_text.search_control;
			searchControl._clusterMarker = cluster;

			var statusControl = new StatusControl(map);
			statusControl.translations = translations_text.status_control;
			searchControl._statusControl = statusControl;

			var howToControl = new HowToControl(map);
			howToControl.translations = translations_text.how_to_control;
			searchControl._howToControl = howToControl;
				
			map.addControl(statusControl);
			map.addControl(searchControl);
			map.addControl(howToControl);
					
			GEvent.addListener(map, "click", function(overlay, point) {
			  if (point) {	    
				singleClick = !singleClick;
				setTimeout(function(){if (singleClick) createCircle(new GLatLng(point.lat(), point.lng() ), circle_radius )}, 300);
			  }
			});
		}
	}
	return map;
}
function drawPOI(poi){
	if(poi){
		opts = {title : poi.title};
		if(poi.icon){
			opts.icon = new GIcon(G_DEFAULT_ICON, poi.icon);
			opts.icon.iconSize = new GSize(32,32);
		}
		var poi_marker = new GMarker(new GLatLng(poi.lat, poi.lon), opts);
		
		map.addOverlay(poi_marker);
	}
}


var singleClick = false;
var searchCenterOptions = new Object();
var searchBorderOptions = new Object();

searchCenterOptions.icon = new GIcon();
searchCenterOptions.icon.image = "/images/gmaps/centerArrow.png";
searchCenterOptions.icon.iconSize = new GSize(16,16);
searchCenterOptions.icon.iconAnchor = new GPoint(8, 8);
searchCenterOptions.draggable = true;
searchCenterOptions.bouncy = true;
searchCenterOptions.zIndexProcess = function(){return 250000000;};

searchBorderOptions.icon = new GIcon();
searchBorderOptions.icon.image = "/images/gmaps/resizeArrow.png";
searchBorderOptions.icon.iconSize = new GSize(25,20);
searchBorderOptions.icon.iconAnchor = new GPoint(12, 10);
searchBorderOptions.draggable = true;
searchBorderOptions.bouncy = true;
searchBorderOptions.zIndexProcess = function(){return 250000000;};


function createCircle(point, radius, fit_map, only_render) {
  singleClick = false;
  searchArea = new SearchArea();
  searchArea.initializeCircle(radius, point, map);
  searchControl.addSearchArea(searchArea);
  searchArea.render(fit_map, only_render);
}

function destination(orig, hdng, dist) {
  var R = 6371; // earth's mean radius in km
  var oX, oY;
  var x, y;
  var d = dist/R;  // d = angular distance covered on earth's surface
  hdng = hdng * Math.PI / 180; // degrees to radians
  oX = orig.x * Math.PI / 180;
  oY = orig.y * Math.PI / 180;

  y = Math.asin( Math.sin(oY)*Math.cos(d) + Math.cos(oY)*Math.sin(d)*Math.cos(hdng) );
  x = oX + Math.atan2(Math.sin(hdng)*Math.sin(d)*Math.cos(oY), Math.cos(d)-Math.sin(oY)*Math.sin(y));

  y = y * 180 / Math.PI;
  x = x * 180 / Math.PI;
  return new GLatLng(y, x);
}

function distance(point1, point2) {
  var R = 6371; // earth's mean radius in km
  var lon1 = point1.lng() * Math.PI / 180;
  var lat1 = point1.lat() * Math.PI / 180;
  var lon2 = point2.lng() * Math.PI / 180;
  var lat2 = point2.lat() * Math.PI / 180;

  var deltaLat = lat1 - lat2;
  var deltaLon = lon1 - lon2;

  var step1 = Math.pow(Math.sin(deltaLat/2), 2) + Math.cos(lat2) * Math.cos(lat1) * Math.pow(Math.sin(deltaLon/2), 2);
  var step2 = 2 * Math.atan2(Math.sqrt(step1), Math.sqrt(1 - step1));
  return step2 * R;
}

function SearchArea() {}

SearchArea.prototype.COLOR="#FF0000";
SearchArea.prototype._map;
SearchArea.prototype._radius;
SearchArea.prototype._dragHandle;
SearchArea.prototype._centerHandle;
SearchArea.prototype._polygon;
SearchArea.prototype._control;
SearchArea.prototype._points;
SearchArea.prototype._dragHandlePosition;
SearchArea.prototype._centerHandlePosition;
SearchArea.prototype._centerField;
SearchArea.prototype._radiusField;

SearchArea.prototype.initializeCircle = function(radius, point, map) {
    this._radius = radius;
    this._map = map;
    this._points = [];
    this._dragHandlePosition = destination(point, 90, this._radius/1000);
    this._dragHandle = new GMarker(this._dragHandlePosition, searchBorderOptions);
    this._centerHandlePosition = point;
    this._centerHandle = new GMarker(this._centerHandlePosition, searchCenterOptions);
    this._map.addOverlay(this._dragHandle);
    this._map.addOverlay(this._centerHandle);
    this._centerField = $('search_point');
    this._radiusField = $('search_radius');
    var that = this;
    GEvent.addListener (this._dragHandle, "dragend", function() {that.updateCircle('border');});
    GEvent.addListener (this._centerHandle, "dragend", function() {that.updateCircle('center');});
}

SearchArea.prototype.updateCircle = function (type) {
    this._map.removeOverlay(this._polygon);
    if (type=='border') {
      this._dragHandlePosition = this._dragHandle.getPoint();
      this._radius = distance(this._centerHandlePosition, this._dragHandlePosition) * 1000;
      this.render();
    } else if (type=='center') {
      this._centerHandlePosition = this._centerHandle.getPoint();
      this.render();
      this._dragHandle.setPoint(this.getEast());
    }
}

SearchArea.prototype.updateFields = function() {
    this._centerField.value = this.getCenterLatLng();
    this._radiusField.value = this.getKMRadius();
}
SearchArea.prototype.clearFields = function() {
    this._centerField.value="";
    this._radiusField.value="";
    window.location.hash = "#.";
}

SearchArea.prototype.render = function(fit_map, only_render) {
	this.updateFields();
	circle_radius = this._radius;
	if(!only_render){
		this._control.updateMarkers(fit_map);
	}
	
	this._points.clear();
   	var distance = this._radius/1000;
   	for (var i = 0; i < 72; ++i) {
   	  this._points.push(destination(this._centerHandlePosition, i * 360/72, distance) );
   	}
   	this._points.push(destination(this._centerHandlePosition, 0, distance) );
   	this._polygon = new GPolygon(this._points, this.COLOR, 2, 1, this.COLOR, 0.1);
   	this._control.render();
   	this._map.addOverlay(this._polygon);
}

SearchArea.prototype.remove = function() {
  this.clearFields();
  this._map.removeOverlay(this._polygon);
  GEvent.clearListeners(this._dragHandle);
  GEvent.clearListeners(this._centerHandle);
  this._map.removeOverlay(this._dragHandle);
  this._map.removeOverlay(this._centerHandle);
  this._searchPoint = null;
  this._searchRadius = null;
  this._control = null;
}

SearchArea.prototype.getKMRadius = function() {
    return (this._radius/1000).toFixed(2);
}

SearchArea.prototype.getCenterLatLng = function() {
    return this._centerHandle.getPoint().toUrlValue();
}

SearchArea.prototype.getHTML = function() {
  result = "<a title='"+this._control.translations.remove+"' onClick='searchControl.removeSearchArea(true); return false;' ><img src='/images/transparent.gif' class='sprite sprite-close'/></a>"+this._control.translations.searching+"<span class='radius'> "+this._control.translations.radius+" ";
  if (this._radius < 1000) {
    result += this._radius.toFixed(1) + 'm';
  } else {
    result += (this._radius / 1000).toFixed(1) + 'km';
  }
  result += "</span>";
  if (this._polygon){
    result += "<span class='area'>" +this._control.translations.area + " " + (this._polygon.getArea()/1000000).toFixed(1) + "km&sup2;</span>";
  }
  return result;   
}
SearchArea.prototype.getNorth = function() {
  return this._points[0];
}

SearchArea.prototype.getSouth = function() {
  return this._points[(72/2)];
}

SearchArea.prototype.getEast = function() {
  return this._points[(72/4)];
}

SearchArea.prototype.getWest = function() {
  return this._points[(72/4*3)];
}

function SearchControl () {}
SearchControl.prototype = new GControl();
SearchControl.prototype._searchArea;
SearchControl.prototype._searchDiv;
SearchControl.prototype._searchForm;
SearchControl.prototype._statusControl;
SearchControl.prototype.preloaded_markers = [];
SearchControl.prototype.translations;
SearchControl.prototype.show_info_window = true;

SearchControl.prototype.initialize = function(map) {
  this._map = map;
  this._searchDiv = new Element("div", { 'id' : "searchControl", 'class' : 'invisible map_control'});
  this._map.getContainer().appendChild(this._searchDiv);
  this._searchForm = $('property_sideform');
  this._results_counter = $('map_results_count');
  this._results_title = $('map_results_title');
  this.title_suffix = document.title.replace(/.+?\|/, ' |');
  
  this._poi_markers = [];
  
  var preloaded_data = window.location.hash.replace(/#/, '').toQueryParams();
  if ( preloaded_data.prop_ids ){
  	this.preloaded_markers = preloaded_data.prop_ids.split(',');
  }
  
  var that = this;
  $('property_search_operation').observe('change', function(){
  	that.togglePriceOperationFilter(this.value);
  	that.filterBy({operation:this.value});
  })
  
  $('property_search_property_type').observe('change', function(){
  	that.filterBy({property_type:this.value});
  })

  return this._searchDiv;
}

SearchControl.prototype.getDefaultPosition = function() {
  return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(70, 10));
}

SearchControl.prototype.addSearchArea = function(searchArea) {
  if(this._searchArea){
  	this.removeSearchArea();
  }
  this._searchArea = searchArea;
  
  searchArea._control = this;
  this._searchDiv.innerHTML = searchArea.getHTML();
}

SearchControl.prototype.render = function() {
  if (this._searchArea){
	this._searchDiv.removeClassName('invisible');
  	this._searchDiv.innerHTML =  this._searchArea.getHTML();
  } else {
  	this._searchDiv.innerHTML = '';
  	this._searchDiv.addClassName('invisible');
  }
}

SearchControl.prototype.togglePriceOperationFilter = function(operation) {
	$('price_sell_menu').toggle();
	$('search_sell_currency').disabled = !$('search_sell_currency').disabled
	$('price_sell_currency').toggle();
	$('price_rent_menu').toggle();
	$('search_rent_currency').disabled = !$('search_rent_currency').disabled
	$('price_rent_currency').toggle();
	$('search_price_from').value = ''
	$('search_price_to').value = ''
}
SearchControl.prototype.updatePlaces = function(places_arr) {
	var place_ids = places_arr.map(function(e){
		return e.select('select').map(function(f){
			return f.value
		}).reject(function(f){return !f || f.empty()}).last()
	}).compact().uniq()
	
	this._searchForm.select('.place_ids').each(function(e){e.remove()});
	
	for(var i=0; i< place_ids.length; ++i){
		this._searchForm.appendChild( new Element('input', {'type':'hidden', 'name':'search[place_ids]['+i+']', 'value':place_ids[i], 'class':'place_ids'}) );
	}
	this.updateMarkers();
}

SearchControl.prototype.filterBy = function(filters, clicked, parent_id, fit_map) {
	if (clicked){
		$$('#'+parent_id+' a').each(function(e){e.removeClassName('current')});
		clicked.addClassName('current');
		clicked.blur();
	}
	$H(filters).each(function(e){
		var filter = $('search_'+e.key)
		if (filter){
			filter.value = e.value;
		}
	});
	this.updateMarkers(fit_map);
}
SearchControl.prototype.hashSerializeForm = function(){
	var values = [];
	$H(Form.serialize(this._searchForm, true)).each(function(h){
		if (!h.value.toString().empty()){
			values.push( h.key.replace(/search\[(.+?)\]/, '$1' ) + '=' + h.value );
		}
	});
	window.location.hash = values.join('&');
}
var request_counter = 0;
SearchControl.prototype.updateMarkers = function(fit_map) {
	this.hashSerializeForm();
	if (update_markers){
		var that = this;
		this.setLoading();
		var this_request = ++request_counter;
		new Ajax.Request(this._searchForm.action, { 
			parameters:Form.serialize(this._searchForm)+'&ajax=1',
			method:'get',
			onSuccess:function(transport){
				if(this_request === request_counter){
					var new_properties = transport.responseJSON.properties;
					var new_title = transport.responseJSON.title;
					var new_win_title = transport.responseJSON.win_title
					var new_markers = [];
					var previousMarkers = {};
					var click = false;
					for(var i=0; i < new_properties.length; ++i){
						new_markers.push( createMarker(new_properties[i], previousMarkers) );
						if( click === false && that.show_info_window && that.preloaded_markers.include( new_properties[i].id ) ){
							click = i;
						}
					}
					if (that._results_counter){ that._results_counter.update( new_properties.length.toString() ); }
					if (that._results_title && new_title){ 
						that._results_title.update( new_title );
					}
					if (new_win_title){
						document.title = new_win_title + that.title_suffix;
					}
					that._clusterMarker.removeMarkers();
					that._clusterMarker.addMarkers(new_markers);
					that._clusterMarker.refresh();
					if(that.show_info_window && click !== false){
						that._clusterMarker.triggerClick(click);
						that.show_info_window = false;
					}else if(fit_map){
						that._clusterMarker.fitMapToMarkers();
					}
					that.drawPOIs(transport.responseJSON.pois);
				}
			},
			onComplete:function(transport){ if(this_request === request_counter) that.endLoading();}
		});
	}else{update_markers=true;}
}
SearchControl.prototype.drawPOIs = function(pois) {
	if(this._poi_markers.length > 0){
		this._poi_markers.each(function(e){
			this._map.removeOverlay(e);
		});
		this._poi_markers.clear();
	}
	if(pois){
		var that = this;
		pois.each(function(poi){
			var opts = {title: poi.title};
			if(poi.icon){
				opts.icon = new GIcon(G_DEFAULT_ICON, poi.icon);
				opts.icon.iconSize = new GSize(32,32);
			}
			var marker = new GMarker(new GLatLng(poi.lat, poi.lon), opts);
			GEvent.addListener(marker, "click", function(){
				this.openInfoWindow("<h3>"+ poi.title + "</h3><p>" + 
					poi.description + "</p>"
				)
			});
			that._map.addOverlay(marker);
			that._poi_markers.push(marker);
		})
	}
}
SearchControl.prototype.setLoading = function() {
	this._statusControl.show();
}
SearchControl.prototype.endLoading = function() {
	this._statusControl.hide();
}

SearchControl.prototype.removeSearchArea = function(update_markers) {
  this._searchArea.remove();
  this._searchArea = null;
  this.render();
  if(update_markers){
	  this.updateMarkers();
  }
}
SearchControl.prototype.closeHowTo = function() {
	this._howToControl.hide();
}

function StatusControl () {}
StatusControl.prototype = new GControl();
StatusControl.prototype._statusDiv;
StatusControl.prototype.translations;

StatusControl.prototype.initialize = function(map) {
  this._map = map;
  this._statusDiv = new Element("div", { 'id' : "statusControl", 'class':'map_control'}).update("<img src='/images/spinner.gif' class='spinner'/>"+this.translations.loading_text);
  this._statusDiv.hide();
  this._map.getContainer().appendChild(this._statusDiv);
  
  return this._statusDiv;
}

StatusControl.prototype.getDefaultPosition = function() {
  var left_distance = (this._map.getSize().width/3).round();
  return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(left_distance, 30));
}

StatusControl.prototype.show = function() {
	this._statusDiv.show();
}

StatusControl.prototype.hide = function() {
	this._statusDiv.hide();
}
function followIfCompatible(text , button_text){
  if (GBrowserIsCompatible() && navigator.geolocation) {
	var b = new Element("button",{"id":"follow"}).update(button_text)
	b.observe("click", follow );
	$$(".map_search_instructions").first().insert(text).appendChild(b)
  }
}
function follow() {
    searchControl.setLoading()
    positionId = navigator.geolocation.watchPosition(function(position){
		  currentLocation = new GLatLng(position.coords.latitude, position.coords.longitude);
		  createCircle( currentLocation ,  1000 , true );
	});
}


function HowToControl () {}
HowToControl.prototype = new GControl();
HowToControl.prototype._statusDiv;
HowToControl.prototype.translations;

HowToControl.prototype.initialize = function(map) {
  this._map = map;
  this._statusDiv = new Element('div', { 'id' : 'howToControl', 'class':'map_control map_search_instructions'}).update("<img src='/images/transparent.gif' class='sprite sprite-close close' onclick='searchControl.closeHowTo();return false;'/>" + (this.translations));
  this._map.getContainer().appendChild(this._statusDiv);
  
  return this._statusDiv;
}

HowToControl.prototype.getDefaultPosition = function() {
  return new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(20, 50));
}
HowToControl.prototype.hide = function() {
	this._statusDiv.hide();
}


/**
 * Copyright (c) 2008 Google Inc.
 *
 * You are free to copy and use this sample.
 * License can be found here: http://code.google.com/apis/ajaxsearch/faq/#license
*/

/**
 * @fileoverview A slideshow control based on the AJAX Feed API.
 * @author dcollison@google.com (Derek Collison)
 */

/**
 * GFslideshow
 * @param {String} photoFeed The feed URL.
 * @param {String|Object} container Either the id string or the element itself.
 * @param {Object} options Options map.
 * @constructor
 */

function GFslideShow(photoFeed, container, options) {
  this.feedUrl = null;
  this.directEntries = null;
  if (typeof photoFeed == 'string') {
    this.feedUrl = photoFeed;
  } else if (photoFeed && photoFeed.length && photoFeed.length > 1) {
    this.directEntries = photoFeed;
  } else {
    throw "invalid argument: photoFeed";
  }
  if (typeof container == "string") {
    container = document.getElementById(container);
  }
  this.container = container;
  this.parseOptions(options);
  this.setup();
}

// Thumbnail size preferences.

GFslideShow.THUMBNAILS_SMALL = "small";
GFslideShow.THUMBNAILS_MEDIUM = "medium";
GFslideShow.THUMBNAILS_LARGE = "large";

// Thumbnail tag names and namespaces.

// MediaRSS.
GFslideShow.MRSS_THUMBNAIL_TAG = "thumbnail";
GFslideShow.MRSS_THUMBNAIL_NS  = "http://search.yahoo.com/mrss/";

// iTunes.
GFslideShow.ITMS_THUMBNAIL_TAG = "coverArt";
GFslideShow.ITMS_THUMBNAIL_NS  = "http://phobos.apple.com/rss/1.0/modules/itms/";

// MediaRSS is default.
GFslideShow.DEFAULT_THUMBNAIL_TAG = GFslideShow.MRSS_THUMBNAIL_TAG;
GFslideShow.DEFAULT_THUMBNAIL_NS  = GFslideShow.MRSS_THUMBNAIL_NS;

// Default display timings, all in milliseconds.
GFslideShow.DEFAULT_DISPLAY_TIME = 3000;
GFslideShow.DEFAULT_TRANSISTION_TIME = 1000;
GFslideShow.DEFAULT_TRANSISTION_STEP = 40;

GFslideShow.DEFAULT_PAUSE_PNG = google.loader.ServiceBase +
                                "/solutions/slideshow/pause.png";
GFslideShow.DEFAULT_PLAY_PNG = google.loader.ServiceBase +
                               "/solutions/slideshow/play.png";

// Full Control Setting
GFslideShow.FC_PAUSE_PNG = {
  small : google.loader.ServiceBase + "/solutions/slideshow/btn_pause_small.png",
  big   : google.loader.ServiceBase + "/solutions/slideshow/btn_pause.png"
};
GFslideShow.FC_PLAY_PNG = {
  small : google.loader.ServiceBase + "/solutions/slideshow/btn_play_small.png",
  big   : google.loader.ServiceBase + "/solutions/slideshow/btn_play.png"
};
GFslideShow.FC_PREV_PNG = {
  small : google.loader.ServiceBase + "/solutions/slideshow/btn_prev_small.png",
  big   : google.loader.ServiceBase + "/solutions/slideshow/btn_prev.png"
};
GFslideShow.FC_NEXT_PNG = {
  small : google.loader.ServiceBase + "/solutions/slideshow/btn_next_small.png",
  big   : google.loader.ServiceBase + "/solutions/slideshow/btn_next.png"
};

GFslideShow.DEFAULT_FC_FADEOUT_TIME = 5000;
GFslideShow.DEFAULT_FC_OPACITY = 0.65;

/**
 * Setup default option map and apply overrides from constructor.
 * @param {Object} options Options map.
 * @private
 */
GFslideShow.prototype.parseOptions = function(options) {
  var maxEntries;
  if (google != undefined && google.feeds != undefined) {
    maxEntries = google.feeds.Feed.MAX_ENTRIES;
  } else {
    maxEntries = 20;
  }
  // Default Options
  this.options = {
    numResults : maxEntries,
    scaleImages : false,
    thumbnailTag : GFslideShow.DEFAULT_THUMBNAIL_TAG,
    thumbnailNamespace : GFslideShow.DEFAULT_THUMBNAIL_NS,
    thumbnailSize : GFslideShow.THUMBNAILS_LARGE,
    linkTarget : null,
    displayTime : GFslideShow.DEFAULT_DISPLAY_TIME,
    transitionTime : GFslideShow.DEFAULT_TRANSISTION_TIME,
    transitionStep : GFslideShow.DEFAULT_TRANSISTION_STEP,
    pauseOnHover : true,
    pauseImage : GFslideShow.DEFAULT_PAUSE_PNG,
    pauseStateCallback : null,
    scalePauseImage : true,
    autoCleanup : true,
    thumbnailUrlResolver : null,
    transitionCallback : null,
    transitionAnimationCallback : null,
    feedLoadCallback : null,
    feedProcessedCallback : null,
    imageClickCallback : null,
    centerBias : { topBias : 0, leftBias : 0 },
    pauseCenterBias : { topBias : 0, leftBias : 0 },
    fullControlPanel : false,
    fullControlPanelCursor : false,
    fullControlPanelFadeOutTime : GFslideShow.DEFAULT_FC_FADEOUT_TIME,
    fullControlPanelPlayCallback : null,
    fullControlPanelSmallIcons : false,
    maintainAspectRatio : true
  };

  if (options) {
    for (o in this.options) {
      if (typeof options[o] != "undefined") {
        this.options[o] = options[o];
      }
    }
  }
  // Override strange options
  if (this.options.displayTime < 100) {
    this.options.displayTime = 100;
  }
  // Calculated
  var ts = (this.options.transitionTime / this.options.transitionStep);
  this.delta = Math.min(1, (1.0/ts));

  // Flag to start
  this.started = false;
};

/**
 * Basic setup.
 * @private
 */
GFslideShow.prototype.setup = function() {
  if (this.container == null) return;

  // Browser fun.
  if (window.ActiveXObject) {
    this.ie = this[window.XMLHttpRequest ? 'ie7' : 'ie6'] = true;
  } else if (window.opera) {
    this.opera = true;
  } else if (document.childNodes && !document.all && !navigator.taintEnabled) {
    this.safari = true;
    if (navigator.userAgent.indexOf('iPhone') > 0) {
      this.iphone = true;
    }
  } else if (document.getBoxObjectFor != null) {
    this.gecko = true;
  }

  // Feeds..
  if (this.feedUrl) {
    this.feed = new google.feeds.Feed(this.feedUrl);
    this.feed.setResultFormat(google.feeds.Feed.MIXED_FORMAT);
    this.feed.setNumEntries(this.options.numResults);
    this.feed.load(this.bind(this.feedLoaded));
  } else if (this.directEntries) {
    this.feedLoaded(this.directEntries);
  }
};

/**
 * Add new entries to the existing ones. Only useful in direct entry mode.
 * @param {Object} newEntries the additional entries Array.
 */
GFslideShow.prototype.addEntries = function(newEntries) {
  this.processEntries(newEntries);
  if (!this.thumb_timer) {
    this.processThumbs();
  }
};


/**
 * Helper method to bind this instance correctly.
 * @param {Object} method function/method to bind.
 * @return {Function}
 * @private
 */
GFslideShow.prototype.bind = function(method) {
  var self = this;
  var opt_args = [].slice.call(arguments, 1);
  return function() {
    var args = opt_args.concat([].slice.call(arguments));
    return method.apply(self, args);
  }
};

/**
 * Process mouseover event.
 * @param {Event} e Optional passed in event.
 * @private
 */
GFslideShow.prototype.mouseOver = function(e) {
  var event = e || window.event;
  var relatedTarget = event.relatedTarget || event.fromElement;

  while(relatedTarget != null) {
    if (relatedTarget == this.container) {
      return;
    }
    relatedTarget = relatedTarget.parentNode;
  }

  if (this.options.fullControlPanel) {
    if (this.options.pauseOnHover && !this.display_paused) {
      this.pauseOrPlayFullControl();
    }
    this.fadeInFullControl();
  } else {
    this.display_paused = true;
    if (this.pauseImage) {
      this.pauseImage.style.visibility = "visible";
    }
  }

  if (this.options.pauseStateCallback) {
    this.options.pauseStateCallback(this.display_paused);
  }
};

/**
 * Process mouseout event.
 * @param {Event} e Optional passed in event.
 * @private
 */
GFslideShow.prototype.mouseOut = function(e) {
  var event = e || window.event;
  var relatedTarget = event.relatedTarget || event.toElement;

  while(relatedTarget != null) {
    if (relatedTarget == this.container) {
      return;
    }
    relatedTarget = relatedTarget.parentNode;
  }

  if (this.options.fullControlPanel) {
    this.fadeOutFullControl();
    this.container.onmousemove = null;
    if (this.options.pauseOnHover && this.display_paused) {
      this.pauseOrPlayFullControl();
    }
  } else {
    this.display_paused = false;
    if (this.pauseImage) {
      this.pauseImage.style.visibility = "hidden";
    }
  }

  if (this.options.pauseStateCallback) {
    this.options.pauseStateCallback(this.display_paused);
  }

  if (this.display_timer == null && this.transition_timer == null) {
    // restart.
    this.displayNextPhoto();
  }
};

GFslideShow.prototype.operaClickAndCallout = function() {
  var entry = this.entries[this.photo_index];
  var tmpLink = this.createLink(entry.link);
  tmpLink.click();
};

/**
 * Programatic pause action.
 */
GFslideShow.prototype.pause = function(opt_suppressPauseImage) {
  var pi = this.pauseImage;
  if (opt_suppressPauseImage) {
    this.pauseImage = null;
  }
  this.pauseAndCallout();
  this.pauseImage = pi;
};

/**
 * Programatic resume action.
 */
GFslideShow.prototype.resume = function() {
  this.resumeSlideShow();
};

/**
 * Process pause action and associated user callout.
 * @private
 */
GFslideShow.prototype.pauseAndCallout = function() {
  this.display_paused = true;
  if (this.pauseImage) {
    this.pauseImage.style.visibility = "visible";
  }

  // for some reason a mouseout happens
  // when we click and swap divs...
  this.container.onmouseout = null;
  if (this.options.imageClickCallback) {
    this.options.imageClickCallback(this.entries[this.photo_index]);
  }
};

/**
 * Resume the slideshow after a pause action.
 */
GFslideShow.prototype.resumeSlideShow = function() {
  if (this.options.pauseOnHover || this.options.fullControlPanel) {
    this.container.onmouseover = this.bind(this.mouseOver);
    this.container.onmouseout = this.bind(this.mouseOut);
  }
  this.display_paused = false;
  if (this.pauseImage) {
    this.pauseImage.style.visibility = "hidden";
  }
  if (this.display_timer == null && this.transition_timer == null) {
    // restart.
    this.displayNextPhoto();
  }
};

/**
 * Helper method to properly clear a node and its children.
 * @param {Object} node Node to clear.
 * @private
 */
GFslideShow.prototype.clearNode = function(node) {
  if (node == null) return;
  var child;
  while (child = node.firstChild) {
    node.removeChild(child);
  }
};

/**
 * Setup our own subcontainer to the user supplied container.
 * @private
 */
GFslideShow.prototype.createSubContainer = function() {
  var div = document.createElement("div");
  div.style.width = "100%";
  div.style.height = "100%";
  div.style.position = "relative";
  div.style.overflow = "hidden";
  this.clearNode(this.container);
  this.container.appendChild(div);
  // Hold onto our sub-container.
  this.container = div;
};

/**
 * Select the appropriate thumbnail url from the array of thumbnails provided
 * based on options.
 * @param {Array} thumbNodes Array of thumbnails urls.
 * @private
 */
GFslideShow.prototype.grabThumb = function(thumbNodes) {
  var ti = 0;
  if (thumbNodes.length > 1) {
    // Use size hint.
    if (this.options.thumbnailSize == GFslideShow.THUMBNAILS_LARGE) {
      ti = thumbNodes.length - 1;
    } else if (this.options.thumbnailSize == GFslideShow.THUMBNAILS_MEDIUM) {
      ti = Math.floor(thumbNodes.length / 2);
    }
  }
  var node = thumbNodes[ti];
  var thumb = null;
  var thumb = node.getAttribute("url");
  if (!thumb) {
    thumb = node.firstChild.nodeValue;
  }
  return thumb;
};

/**
 * Process the thumbs and create appropriate images. These can be done in
 * chunks.
 * @param {Number} opt_chunk optional chunk size to process.
 * @param {Number} opt_timeout optional timeout for next chunk.
 * @private
 */
GFslideShow.prototype.processThumbs = function(opt_chunk, opt_timeout) {
  this.thumb_timer = null;
  var start = this.thumbs_index;
  var num = this.entries.length;
  var chunk = opt_chunk || 4;
  if (num > (start + chunk)) {
    num = (start + chunk);
    // schedule next batch.
    var cb = this.bind(this.processThumbs);
    var to = opt_timeout || Math.round(this.options.displayTime / 4);
    this.thumb_timer = window.setTimeout(cb, to);
  }
  for (var i = start; i < num; i++) {
    var thumbUrl = this.entries[i].thumbUrl;
    var image = this.createImage(thumbUrl);
    this.images.push(image);
    if (this.options.linkTarget) {
      if (!this.opera) {
        var link = this.createLink(this.entries[i].link);
        link.appendChild(image);
        this.container.appendChild(link);
      } else { // Opera Hack
        image.onclick = this.bind(this.operaClickAndCallout);
        image.style.cursor = 'pointer';
        this.container.appendChild(image);
      }
    } else {
      this.container.appendChild(image);
    }
    if (image.complete) {
      // We are already loaded and we have size dimensions.
      this.imageLoaded(image);
    } else {
      // We need to wait for the image to load..
      image.onerror = this.bind(this.imageError, image);
      image.onload = this.bind(this.imageLoaded, image);
    }
    this.thumbs_index++;
  }
};


/**
 * Process and setup the entries
 * @param {Object} entries Entries array.
 * @private
 */

GFslideShow.prototype.processEntries = function(entries) {
  for (var i = 0; i < entries.length; i++) {
    var thumbUrl = null;
    if (this.options.thumbnailUrlResolver) {
      thumbUrl = this.options.thumbnailUrlResolver(entries[i]);
    } else {
      var thumbNodes = google.feeds.getElementsByTagNameNS(
                           entries[i].xmlNode,
                           this.options.thumbnailNamespace,
                           this.options.thumbnailTag);
      if (thumbNodes && thumbNodes.length > 0) {
        thumbUrl = this.grabThumb(thumbNodes);
      }
    }
    if (thumbUrl) {
      entries[i].thumbUrl = thumbUrl;
      this.entries.push(entries[i]);
    }
  }

};

/**
 * Callback associated with the AJAX Feed api after load.
 * @param {Object} result Loaded result.
 * @private
 */
GFslideShow.prototype.feedLoaded = function(result) {
  if (this.options.feedLoadCallback) {
    this.options.feedLoadCallback(result);
  }

  if ((this.feedUrl && result.error) ||
      (this.directEntries && this.directEntries.length == 0) ) {
    if (!this.options.feedLoadCallback) {
      this.container.innerHTML = "<center>feed could not be loaded.</center>";
    }
    return;
  }

  this.createSubContainer();
  if (this.container.offsetWidth) {
    // snapshot.
    this.width = this.container.offsetWidth;
    this.height = this.container.offsetHeight;
  }
  this.createPauseImage();
  this.images = [];
  this.entries = [];
  this.thumbs_index = 0;
  var entries;
  if (this.feedUrl) {
    entries = result.feed.entries;
  } else {
    entries = this.directEntries;
  }

  // Process the entries.
  this.processEntries(entries);

  if (this.options.feedProcessedCallback) {
    this.options.feedProcessedCallback(result);
  }

  // Enable full panel control mode.
  if (this.options.fullControlPanel && this.entries.length > 0) {
    this.createFullControlPanel();
  }

  // Attach mouse handlers if applicable for pausing.
  if ((this.options.pauseOnHover || this.options.fullControlPanel) &&
      this.entries.length > 0) {
    this.container.onmouseover = this.bind(this.mouseOver);
    this.container.onmouseout = this.bind(this.mouseOut);
  }

  if (this.options.imageClickCallback) {
    this.container.onclick = this.bind(this.pauseAndCallout);
  }

  // Seed with first image and quick timeout for next chunk.
  this.processThumbs(1, 100+(Math.random()*100));
};

/**
 * Callback asscoiated with an image load.
 * @param {Element} image Image instance that was loaded.
 * @private
 */
GFslideShow.prototype.imageLoaded = function(image) {
  image.__gfloaded = true;
  this.adjustImage(image);

  // Once the first image is loaded, begin the slideshow..
  if (!this.started) {
    for (var i = 0; i < this.images.length; i++) {
      if (image == this.images[i]) {
        this.beginSlideShow(i);
      }
      break;
    }
  }
};

/**
 * Callback asscoiated with an image load error.
 * @param {Element} image Image instance that was loaded.
 * @private
 */
GFslideShow.prototype.imageError = function(image) {
  image.__gferror = true;
};

/**
 * Adjust the image to the container after load. Will scale and center.
 * @param {Element} image Image instance that needs adjusting.
 * @private
 */
GFslideShow.prototype.adjustImage = function(image) {
  // Scale if requested.
  if (this.options.scaleImages) {
    if (this.options.maintainAspectRatio) {
      this.scaleImage(image);
    } else {
      image.style.height = this.height + "px";
      image.style.width = this.width + "px";
    }
  }
  // Center the image.
  this.centerImage(image);
};

/**
 * Scale the image appropriately to fit in the container.
 * @param {Element} image Image instance that needs scaling.
 * @private
 */

GFslideShow.prototype.scaleImage = function(image, opt_width, opt_height) {
  // These change when the first one is set, so we need to remember them.
  var width = opt_width || this.width;
  var height = opt_height || this.height;
  var imgW = image.offsetWidth;
  var imgH = image.offsetHeight;
  if (imgW <= 0 || imgH <= 0) return;

  var scaleH = height / imgH;
  var scaleW = width / imgW;

  if (scaleH < scaleW) {
    image.style.height = height + "px";
    image.style.width = Math.round(imgW * scaleH) + "px";
  } else {
    image.style.width = width + "px";
    image.style.height = Math.round(imgH * scaleW) + "px";
  }
};

/**
 * Center the image appropriately within the container.
 * @param {Element} image Image instance.
 * @private
 */
GFslideShow.prototype.centerImage = function(image) {
  var oh = this.height - image.offsetHeight;
  var ow = this.width - image.offsetWidth;

  // Don't assume these are zero..
  image.style.top = "0px";
  image.style.left = "0px";

  // center the image
  if (oh > 0) {
    var ah = Math.round(oh/2);
    image.style.top = image.offsetTop + ah +
                      this.options.centerBias.topBias + "px";
  }
  if (ow > 0) {
    var aw = Math.round(ow/2);
    image.style.left = image.offsetLeft + aw +
                       this.options.centerBias.leftBias + "px";
  }
};

/**
 * Create a link element.
 * @param {String} href Href attribute for the element.
 * @return {Element} Link element.
 * @private
 */
GFslideShow.prototype.createLink = function(href) {
  var link = document.createElement('a');
  link.setAttribute('href', href);
  if (this.options.linkTarget) {
    link.setAttribute('target', this.options.linkTarget);
  }
  return link;
};

/**
 * Create an image element.
 * @param {String} src Source attribute for the image element.
 * @private
 */
GFslideShow.prototype.createImage = function(src) {
  var image = document.createElement("img");
  image.style.position = "absolute";
  image.setAttribute("src", src);
  this.setOpacity(image, 0);
  return image;
};

/**
 * Properly adjust the pause image if need be.
 * @param {Element} image Image representing pause state.
 * @private
 */
GFslideShow.prototype.adjustPauseImage = function(image) {
  if (this.options.scalePauseImage) {
    var height = Math.round(this.height * 0.33);
    var width = Math.round(this.width * 0.33);
    this.scaleImage(image, width, height);
  }
  this.placePauseImage(image);
};

/**
 * Properly place the pause image for overlay on a pause state.
 * @param {Element} image Image representing pause state.
 * @private
 */
GFslideShow.prototype.placePauseImage = function(image) {
  var oh = this.height - image.offsetHeight;
  var ow = this.width - image.offsetWidth;

  // Don't assume these are zero..
  image.style.top = "0px";
  image.style.left = "0px";

  // center the image
  if (oh > 0) {
    var off = Math.round(this.height * 0.10);
    if (off < 15) off = 10;
    var ah = this.height - (image.offsetHeight + off);
    if (ah < 0) ah = 0;
    image.style.top = image.offsetTop + ah +
                      this.options.pauseCenterBias.topBias + "px";
  }
  if (ow > 0) {
    var aw = Math.round(ow/2);
    image.style.left = image.offsetLeft + aw +
                       this.options.pauseCenterBias.leftBias + "px";
  }
};

/**
 * Properly create the alpha transparent version of the pause image.
 * @param {Element} image Image representing pause state.
 * @private
 */
GFslideShow.prototype.createAlphaPauseImage = function(image) {
  // Work with offscreen version first to get the correct sizes and offsets..
  this.adjustPauseImage(image);

  var imgW = image.offsetWidth;
  var imgH = image.offsetHeight;
  var imgT = image.style.top;
  var imgL = image.style.left;

  // Now create real one.
  var element = null;
  if (this.ie) {
    var src = this.options.pauseImage;
    element = document.createElement("div");
    element.style.filter = "progid:DXImageTransform.Microsoft." +
        "AlphaImageLoader(src='" + src + "', sizingMethod='scale')";
    element.style.position = "absolute";
    element.style.width = imgW + "px";
    element.style.height = imgH + "px";
    element.style.left = imgL;
    element.style.top = imgT;
  } else {
    element = image;
    element.style.opacity = "";
  }

  element.style.visibility = "hidden";
  element.style.zIndex = 222;

  if (element != image) {
    this.container.appendChild(element);
    this.container.removeChild(image);
  }
  this.pauseImage = element;
};

/**
 * Callback asscoiated with the pause image load.
 * @param {Element} image Pause image instance that was loaded.
 * @private
 */
GFslideShow.prototype.pauseImageLoaded = function(image) {
  this.createAlphaPauseImage(image);
};

/**
 * Create the pause image element.
 * @param {String} src Source attribute for the pause image element.
 * @private
 */
GFslideShow.prototype.createPauseImage = function(src) {
  if (!this.options.pauseOnHover) return;
  var pauseOff = this.createImage(this.options.pauseImage);
  this.container.appendChild(pauseOff);
  if (pauseOff.complete) {
    this.createAlphaPauseImage(pauseOff);
  } else {
    pauseOff.onload = this.bind(this.pauseImageLoaded, pauseOff);
  }
};

/**
 * Create the fullControlPanel setup.
 * @private
 */
GFslideShow.prototype.createFullControlPanel = function() {
  var h = (this.options.fullControlPanelSmallIcons?25:45);
  if (this.options.fullControlPanelCursor) h += 10;
  var padTop = (this.options.fullControlPanelSmallIcons?5:10);
  var padBottom = 5;
  var div = document.createElement('div');
  div.style.backgroundColor = '#000000';
  div.style.height = h + 'px';
  div.style.top = (this.height - (h+padBottom+padTop)) + 'px';
  div.style.width = '100%';
  div.style.zIndex = '222';
  div.style.position = 'relative';
  div.style.textAlign = 'center';
  div.style.direction = 'ltr';
  div.style.paddingTop = padTop + 'px';
  div.style.paddingBottom = padBottom + 'px';

  var iconSize = this.options.fullControlPanelSmallIcons?'small':'big';
  var handCursor = this.ie?'hand':'pointer';

  var pause = document.createElement("img");
  pause.src = GFslideShow.FC_PAUSE_PNG[iconSize];
  pause.style.cursor = handCursor;

  var next = document.createElement("img");
  next.src = GFslideShow.FC_NEXT_PNG[iconSize];
  next.style.cursor = handCursor;

  var prev = document.createElement("img");
  prev.src = GFslideShow.FC_PREV_PNG[iconSize];
  prev.style.cursor = handCursor;

  pause.style.marginLeft = '5px';
  pause.style.marginRight = '5px';

  div.appendChild(prev);
  div.appendChild(pause);
  div.appendChild(next);

  var cursor = null;
  if (this.options.fullControlPanelCursor) {
    cursor = document.createElement('div');
    cursor.style.height = '1.3em';
    cursor.style.fontSize = '11px';
    cursor.style.color = '#bbbbbb';
    div.appendChild(cursor);
  }

  // Hold onto the ui elements..
  this.fc = {};
  this.fc.container = div;
  this.fc.pause = pause;
  this.fc.next = next;
  this.fc.prev = prev;
  this.fc.cursor = cursor;

  next.onclick = this.bind(this.goForward);
  prev.onclick = this.bind(this.goBackward);
  pause.onclick = this.bind(this.pauseOrPlayClick);

  this.fc.container.style.visibility = "hidden";
  this.container.appendChild(div);
};

/**
 * Clear the transition timer. Used to prevent leaks.
 * @private
 */
GFslideShow.prototype.clearTransitionTimer = function() {
  if (this.transition_timer) {
    clearInterval(this.transition_timer);
    this.transition_timer = null;
  }
};

/**
 * Sets the transition timer for fadeout.
 * @private
 */
GFslideShow.prototype.setTransitionTimer = function() {
  this.clearTransitionTimer();
  this.lastTick = GFslideShow.timeNow();
  var cb = this.bind(this.transitionAnimation);
  this.transition_timer = window.setInterval(cb, this.options.transitionStep);
};

/**
 * Clear the display timer. Used to prevent leaks.
 * @private
 */
GFslideShow.prototype.clearDisplayTimer = function() {
  if (this.display_timer) {
    clearTimeout(this.display_timer);
    this.display_timer = null;
  }
};

/**
 * Sets the display timer.
 * @private
 */
GFslideShow.prototype.setDisplayTimer = function() {
  if (this.display_timer) return;
  var cb = this.bind(this.displayNextPhoto);
  this.display_timer = window.setTimeout(cb, this.options.displayTime);
};

/**
 * Clear the thumb timer. Used to prevent leaks.
 * @private
 */
GFslideShow.prototype.clearThumbTimer = function() {
  if (this.thumb_timer) {
    clearTimeout(this.thumb_timer);
    this.thumb_timer = null;
  }
};

/**
 * Displays the slideshow, starting at the corresponding index.
 * @param {Number} index Index of image to start with.
 * @private
 */
GFslideShow.prototype.beginSlideShow = function(index) {
  this.photo_index = index;
  this.next = this.images[this.photo_index];
  this.snapToNextPhoto();
  this.started = true;
};

/**
 * Class helper method for the time now in milliseconds
 * @private
 */
GFslideShow.timeNow = function() {
  var d = new Date();
  return d.getTime();
};

/**
 * Move to the next photo.
 */
GFslideShow.prototype.goForward = function() {
  this.finishTransition();
  this.setNextPhoto();
  this.snapToNextPhoto();
  this.clearFullControlTimeoutTimer();
};

/**
 * Move to the previous photo.
 */
GFslideShow.prototype.goBackward = function() {
  this.finishTransition();
  this.setPreviousPhoto();
  this.snapToNextPhoto();
  this.clearFullControlTimeoutTimer();
};

/**
 * Goto the specified indexed photo.
 */
GFslideShow.prototype.gotoIndex = function(index) {
  if (index == this.photo_index) {
    return;
  }
  this.clearTransitionTimer();
  this.setPhotoIndex(index);
  this.snapToNextPhoto();
  this.clearFullControlTimeoutTimer();
}

/**
 * Handle mouse clicks on the pause or play button.
 */
GFslideShow.prototype.pauseOrPlayClick = function() {
  // Trap a play click if we have a callout registered.
  if (this.options.fullControlPanelPlayCallback && this.display_paused) {
    // for some reason a mouseout happens
    // when we click and swap divs...
    this.container.onmouseover = null;
    this.container.onmouseout = null;
    this.options.fullControlPanelPlayCallback(this.entries[this.photo_index]);
    this.fadeOutFullControl();
  } else {
    this.pauseOrPlayFullControl();
  }
}

/**
 * Toggle Pause or Play in FullControl Mode.
 */
GFslideShow.prototype.pauseOrPlayFullControl = function() {
  // todo, pause callout?
  var iconSize = this.options.fullControlPanelSmallIcons?'small':'big';
  if (this.display_paused) {
    this.display_paused = false;
    this.fc.pause.src = GFslideShow.FC_PAUSE_PNG[iconSize];
    if (this.display_timer == null && this.transition_timer == null) {
      // restart.
      this.displayNextPhoto();
    }
  } else {
    this.display_paused = true;
    this.fc.pause.src = GFslideShow.FC_PLAY_PNG[iconSize];
  }
};

/**
 * monitors mouse motion inside the container while fullcontrol panel is
 * active.
 * @private
 */
GFslideShow.prototype.fullControlMotion = function() {
  var op = this.fc.container.opacity;
  if (op < GFslideShow.DEFAULT_FC_OPACITY) {
    this.container.onmousemove = null;
    this.clearFullControlTimeoutTimer();
    this.fadeInFullControl();
  } else {
    this.setFullControlTimeoutTimer();
  }
}

/**
 * Clears the timeout timer itself.
 * @private
 */
GFslideShow.prototype.clearFullControlTimeoutTimer = function() {
  if (!this.fc) {
    return;
  }
  if (this.fc.timeout) {
    clearTimeout(this.fc.timeout);
    this.fc.timeout = null;
  }
}

/**
 * Set up the timeout timer itself.
 * @private
 */
GFslideShow.prototype.setFullControlTimeoutTimer = function() {
  if (this.fc.timeout) {
    clearTimeout(this.fc.timeout);
    this.fc.timeout = null;
  }
  if (this.options.fullControlPanelFadeOutTime > 0) {
    var cb = this.bind(this.fadeOutFullControl);
    this.fc.timeout = setTimeout(cb, this.options.fullControlPanelFadeOutTime);
  }
}

/**
 * Set up a timeout to fadeout the control if no mouse activity.
 * @private
 */
GFslideShow.prototype.setFullControlTimeout = function() {
  this.container.onmousemove = this.bind(this.fullControlMotion);
  this.setFullControlTimeoutTimer();
}

/**
 * Begin fading in of the FullControl.
 */
GFslideShow.prototype.fadeInFullControl = function() {
  this.setOpacity(this.fc.container, 0);
  var cb = this.bind(this.fadeInFullControlAnimation);
  this.setFullControlFadeTimer(cb);
}

/**
 * Fade in animation for the FullControl
 */
GFslideShow.prototype.fadeInFullControlAnimation = function() {
  var op = this.fc.container.opacity;
  op += 0.075; // Approximation
  op = Math.min(GFslideShow.DEFAULT_FC_OPACITY, op);
  this.setOpacity(this.fc.container, op);
  if (op >= GFslideShow.DEFAULT_FC_OPACITY) {
    this.setFullControlFadeTimer();
    this.setFullControlTimeout();
  }
}

/**
 * Begin fading out of the FullControl.
 */
GFslideShow.prototype.fadeOutFullControl = function() {
  var cb = this.bind(this.fadeOutFullControlAnimation);
  this.setFullControlFadeTimer(cb);
}

/**
 * Fade out animation for the FullControl
 */
GFslideShow.prototype.fadeOutFullControlAnimation = function() {
  var op = this.fc.container.opacity;
  op -= 0.075;
  this.setOpacity(this.fc.container, op);
  if (op <= 0) {
    this.setFullControlFadeTimer();
  }
}

/**
 * Set the fade timer, clearing previous ones.
 * @param {Object} opt_callback function/method to bind timer to.
 * @private
 */
GFslideShow.prototype.setFullControlFadeTimer = function(opt_callback) {
  if (this.fc.fade_timer) {
    clearInterval(this.fc.fade_timer);
    this.fc.fade_timer = null;
  }
  if (opt_callback) {
    this.fc.fade_timer = window.setInterval(opt_callback, 40);
  }
}

/**
 * Transition animation to next photo. Cleanup when finished.
 * @private
 */
GFslideShow.prototype.transitionAnimation = function() {
  if (this.current && this.next) {
    var delta = this.delta;
    var ts = this.options.transitionStep;
    var now = GFslideShow.timeNow();
    var tick = now - this.lastTick;
    this.lastTick = now;
    delta *= (tick/ts);
    if (delta < 0) return;

    var cur_op = this.current.opacity - delta;
    var next_op = this.next.opacity + delta;

    this.setOpacity(this.current, cur_op);
    this.setOpacity(this.next, next_op);

    if (this.options.transitionAnimationCallback) {
      this.options.transitionAnimationCallback(this.next.opacity);
    }

    // Still more to go?
    if (cur_op > 0) {
      return;
    }
  }

  // Finished transition.
  this.finishTransition();
};

/**
 * Select the next photo to display. This takes into account different load
 * times for images and bad links, etc.
 * @private
 */
GFslideShow.prototype.setNextPhoto = function() {
  if (this.images.length == 0) {
    return;
  }
  var ci = this.photo_index;
  var done = false;
  while (!done) {
    // wrap
    if (++this.photo_index >= this.images.length) {
      this.photo_index = 0;
    }
    var image = this.images[this.photo_index];
    if (image && image.__gfloaded) {
      this.next = image;
      done = true;
    } else {
      // Image not loaded for some reason, skip it. But don't loop forever.
      if (this.photo_index == ci) {
        this.next = this.images[0];
        done = true;
      }
    }
  }
};


/**
 * Select the previous photo to display. This takes into account different load
 * times for images and bad links, etc.
 * @private
 */
GFslideShow.prototype.setPreviousPhoto = function() {
  var ci = this.photo_index;
  var done = false;
  while (!done && this.images.length != 0) {
    if (--this.photo_index < 0) {
      this.photo_index = this.images.length-1;
    }
    var image = this.images[this.photo_index];
    if (image && image.__gfloaded) {
      this.next = image;
      done = true;
    } else {
      // Image not loaded for some reason, skip it. But don't loop forever.
      if (this.photo_index == ci) {
        this.next = this.images[0];
        done = true;
      }
    }
  }
};

/**
 * Select the photo index to display.
 * @private
 */
GFslideShow.prototype.setPhotoIndex = function(index) {
  if (index < 0 || index >= this.images.length) {
    return;
  }
  var image = this.images[index];
  if (image && image.__gfloaded) {
    this.next = image;
    this.photo_index = index;
  }
};

/**
 * Clears the pause events, prevents leaks.
 * @private
 */
GFslideShow.prototype.clearPauseEvents = function() {
  this.container.onmouseover = null;
  this.container.onmouseout = null;
};

/**
 * Cleanup when we notice we have been removed or replaced. Try to be
 * as GC friendly as possible.
 * @private
 */
GFslideShow.prototype.cleanup = function() {
  // Try to be gc friendly.
  this.clearTransitionTimer();
  this.clearDisplayTimer();
  this.clearThumbTimer();
  this.clearPauseEvents();
  this.clearNode(this.container);
  this.container = null;
};

/**
 * Display the next appropriate photo if we can. Also determine if we
 * need to start transition animation if applicable.
 * @private
 */
GFslideShow.prototype.displayNextPhoto = function() {
  this.display_timer = null;

  if (!this.started) {
    return false;
  }

  // Just return if we are in a paused state.
  if (this.display_paused) return;

  // See if we have been orphaned and cleanup if needed.
  if ((!this.container || !this.container.parentNode) &&
      this.options.autoCleanup) {
    this.cleanup();
    return;
  }

  this.setNextPhoto();
  this.beginTransition();
};

/**
 * Helper method to snap to next photo
 * @private
 */
GFslideShow.prototype.snapToNextPhoto = function() {
  this.setOpacity(this.next, 1);
  this.setOpacity(this.current, 0);
  this.current = this.next;
  this.setDisplayTimer();
  if (this.options.transitionCallback) {
    this.options.transitionCallback(this.entries[this.photo_index],
                                    this.options.transitionTime);
  }
  if (this.options.fullControlPanel && this.options.fullControlPanelCursor) {
    var index = (this.photo_index+1) + ' / ' + this.images.length;
    this.fc.cursor.innerHTML = index;
  }
}

/**
 * Helper method to start transition to next selected photo.
 * Takes into account transition parameters.
 * @private
 */
GFslideShow.prototype.beginTransition = function() {
  if (!this.current || !this.next || (this.current == this.next)) {
    // Skip if we are missing something or trying to transition to
    // ourselves.
    this.setDisplayTimer();
    return;
  }
  if (this.options.transitionTime >= this.options.transitionStep) {
    this.setTransitionTimer();
  } else {
    this.snapToNextPhoto();
  }
};

/**
 * Helper method to finish the transition to next selected photo.
 * @private
 */
GFslideShow.prototype.finishTransition = function() {
  this.clearTransitionTimer();
  this.snapToNextPhoto();
};

/**
 * Helper method to set opacity for images.. Also takes into account
 * visibility in general.
 * @param {Element} image Image element.
 * @param {Number} opacity alpha level.
 * @private
 */
GFslideShow.prototype.setOpacity = function(image, opacity) {
   if (image == null) return;
   opacity = Math.max(0, Math.min(1, opacity));
   if (opacity == 0) {
     if (image.style.visibility != "hidden") {
       image.style.visibility = "hidden";
     }
   } else {
     if (image.style.visibility != "visible") {
       image.style.visibility = "visible";
     }
   }
   if (this.ie) image.style.filter = "alpha(opacity=" + opacity*100 + ")";
   image.style.opacity = image.opacity = opacity;
};


/* 
 * MarkerManager, v1.0
 * Copyright (c) 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 *
 *
 * Author: Doug Ricket, others
 * 
 * Marker manager is an interface between the map and the user, designed
 * to manage adding and removing many points when the viewport changes.
 *
 *
 * Algorithm: The MM places its markers onto a grid, similar to the map tiles.
 * When the user moves the viewport, the MM computes which grid cells have
 * entered or left the viewport, and shows or hides all the markers in those
 * cells.
 * (If the users scrolls the viewport beyond the markers that are loaded,
 * no markers will be visible until the EVENT_moveend triggers an update.)
 *
 * In practical consequences, this allows 10,000 markers to be distributed over
 * a large area, and as long as only 100-200 are visible in any given viewport,
 * the user will see good performance corresponding to the 100 visible markers,
 * rather than poor performance corresponding to the total 10,000 markers.
 *
 * Note that some code is optimized for speed over space,
 * with the goal of accommodating thousands of markers.
 *
 */



/**
 * Creates a new MarkerManager that will show/hide markers on a map.
 *
 * @constructor
 * @param {Map} map The map to manage.
 * @param {Object} opt_opts A container for optional arguments:
 *   {Number} maxZoom The maximum zoom level for which to create tiles.
 *   {Number} borderPadding The width in pixels beyond the map border,
 *                   where markers should be display.
 *   {Boolean} trackMarkers Whether or not this manager should track marker
 *                   movements.
 */
function MarkerManager(map, opt_opts) {
  var me = this;
  me.map_ = map;
  me.mapZoom_ = map.getZoom();
  me.projection_ = map.getCurrentMapType().getProjection();

  opt_opts = opt_opts || {};
  me.tileSize_ = MarkerManager.DEFAULT_TILE_SIZE_;
  
  var maxZoom = MarkerManager.DEFAULT_MAX_ZOOM_;
  if(opt_opts.maxZoom != undefined) {
    maxZoom = opt_opts.maxZoom;
  }
  me.maxZoom_ = maxZoom;

  me.trackMarkers_ = opt_opts.trackMarkers;

  var padding;
  if (typeof opt_opts.borderPadding == "number") {
    padding = opt_opts.borderPadding;
  } else {
    padding = MarkerManager.DEFAULT_BORDER_PADDING_;
  }
  // The padding in pixels beyond the viewport, where we will pre-load markers.
  me.swPadding_ = new GSize(-padding, padding);
  me.nePadding_ = new GSize(padding, -padding);
  me.borderPadding_ = padding;

  me.gridWidth_ = [];

  me.grid_ = [];
  me.grid_[maxZoom] = [];
  me.numMarkers_ = [];
  me.numMarkers_[maxZoom] = 0;

  GEvent.bind(map, "moveend", me, me.onMapMoveEnd_);

  // NOTE: These two closures provide easy access to the map.
  // They are used as callbacks, not as methods.
  me.removeOverlay_ = function(marker) {
    map.removeOverlay(marker);
    me.shownMarkers_--;
  };
  me.addOverlay_ = function(marker) {
    map.addOverlay(marker);
    me.shownMarkers_++;
  };

  me.resetManager_();
  me.shownMarkers_ = 0;

  me.shownBounds_ = me.getMapGridBounds_();
};

// Static constants:
MarkerManager.DEFAULT_TILE_SIZE_ = 1024;
MarkerManager.DEFAULT_MAX_ZOOM_ = 17;
MarkerManager.DEFAULT_BORDER_PADDING_ = 100;
MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE = 256;


/**
 * Initializes MarkerManager arrays for all zoom levels
 * Called by constructor and by clearAllMarkers
 */ 
MarkerManager.prototype.resetManager_ = function() {
  var me = this;
  var mapWidth = MarkerManager.MERCATOR_ZOOM_LEVEL_ZERO_RANGE;
  for (var zoom = 0; zoom <= me.maxZoom_; ++zoom) {
    me.grid_[zoom] = [];
    me.numMarkers_[zoom] = 0;
    me.gridWidth_[zoom] = Math.ceil(mapWidth/me.tileSize_);
    mapWidth <<= 1;
  }
};

/**
 * Removes all currently displayed markers
 * and calls resetManager to clear arrays
 */
MarkerManager.prototype.clearMarkers = function() {
  var me = this;
  me.processAll_(me.shownBounds_, me.removeOverlay_);
  me.resetManager_();
};


/**
 * Gets the tile coordinate for a given latlng point.
 *
 * @param {LatLng} latlng The geographical point.
 * @param {Number} zoom The zoom level.
 * @param {GSize} padding The padding used to shift the pixel coordinate.
 *               Used for expanding a bounds to include an extra padding
 *               of pixels surrounding the bounds.
 * @return {GPoint} The point in tile coordinates.
 *
 */
MarkerManager.prototype.getTilePoint_ = function(latlng, zoom, padding) {
  var pixelPoint = this.projection_.fromLatLngToPixel(latlng, zoom);
  return new GPoint(
      Math.floor((pixelPoint.x + padding.width) / this.tileSize_),
      Math.floor((pixelPoint.y + padding.height) / this.tileSize_));
};


/**
 * Finds the appropriate place to add the marker to the grid.
 * Optimized for speed; does not actually add the marker to the map.
 * Designed for batch-processing thousands of markers.
 *
 * @param {Marker} marker The marker to add.
 * @param {Number} minZoom The minimum zoom for displaying the marker.
 * @param {Number} maxZoom The maximum zoom for displaying the marker.
 */
MarkerManager.prototype.addMarkerBatch_ = function(marker, minZoom, maxZoom) {
  var mPoint = marker.getPoint();
  // Tracking markers is expensive, so we do this only if the
  // user explicitly requested it when creating marker manager.
  if (this.trackMarkers_) {
    GEvent.bind(marker, "changed", this, this.onMarkerMoved_);
  }

  var gridPoint = this.getTilePoint_(mPoint, maxZoom, GSize.ZERO);

  for (var zoom = maxZoom; zoom >= minZoom; zoom--) {
    var cell = this.getGridCellCreate_(gridPoint.x, gridPoint.y, zoom);
    cell.push(marker);

    gridPoint.x = gridPoint.x >> 1;
    gridPoint.y = gridPoint.y >> 1;
  }
};


/**
 * Returns whether or not the given point is visible in the shown bounds. This
 * is a helper method that takes care of the corner case, when shownBounds have
 * negative minX value.
 *
 * @param {Point} point a point on a grid.
 * @return {Boolean} Whether or not the given point is visible in the currently
 * shown bounds.
 */
MarkerManager.prototype.isGridPointVisible_ = function(point) {
  var me = this;
  var vertical = me.shownBounds_.minY <= point.y &&
      point.y <= me.shownBounds_.maxY;
  var minX = me.shownBounds_.minX;
  var horizontal = minX <= point.x && point.x <= me.shownBounds_.maxX;
  if (!horizontal && minX < 0) {
    // Shifts the negative part of the rectangle. As point.x is always less
    // than grid width, only test shifted minX .. 0 part of the shown bounds.
    var width = me.gridWidth_[me.shownBounds_.z];
    horizontal = minX + width <= point.x && point.x <= width - 1;
  }
  return vertical && horizontal;
}


/**
 * Reacts to a notification from a marker that it has moved to a new location.
 * It scans the grid all all zoom levels and moves the marker from the old grid
 * location to a new grid location.
 *
 * @param {Marker} marker The marker that moved.
 * @param {LatLng} oldPoint The old position of the marker.
 * @param {LatLng} newPoint The new position of the marker.
 */
MarkerManager.prototype.onMarkerMoved_ = function(marker, oldPoint, newPoint) {
  // NOTE: We do not know the minimum or maximum zoom the marker was
  // added at, so we start at the absolute maximum. Whenever we successfully
  // remove a marker at a given zoom, we add it at the new grid coordinates.
  var me = this;
  var zoom = me.maxZoom_;
  var changed = false;
  var oldGrid = me.getTilePoint_(oldPoint, zoom, GSize.ZERO);
  var newGrid = me.getTilePoint_(newPoint, zoom, GSize.ZERO);
  while (zoom >= 0 && (oldGrid.x != newGrid.x || oldGrid.y != newGrid.y)) {
    var cell = me.getGridCellNoCreate_(oldGrid.x, oldGrid.y, zoom);
    if (cell) {
      if (me.removeFromArray(cell, marker)) {
        me.getGridCellCreate_(newGrid.x, newGrid.y, zoom).push(marker);
      }
    }
    // For the current zoom we also need to update the map. Markers that no
    // longer are visible are removed from the map. Markers that moved into
    // the shown bounds are added to the map. This also lets us keep the count
    // of visible markers up to date.
    if (zoom == me.mapZoom_) {
      if (me.isGridPointVisible_(oldGrid)) {
        if (!me.isGridPointVisible_(newGrid)) {
          me.removeOverlay_(marker);
          changed = true;
        }
      } else {
        if (me.isGridPointVisible_(newGrid)) {
          me.addOverlay_(marker);
          changed = true;
        }
      }
    }
    oldGrid.x = oldGrid.x >> 1;
    oldGrid.y = oldGrid.y >> 1;
    newGrid.x = newGrid.x >> 1;
    newGrid.y = newGrid.y >> 1;
    --zoom;
  }
  if (changed) {
    me.notifyListeners_();
  }
};


/**
 * Searches at every zoom level to find grid cell
 * that marker would be in, removes from that array if found.
 * Also removes marker with removeOverlay if visible.
 * @param {GMarker} marker The marker to delete.
 */
MarkerManager.prototype.removeMarker = function(marker) {
  var me = this;
  var zoom = me.maxZoom_;
  var changed = false;
  var point = marker.getPoint();
  var grid = me.getTilePoint_(point, zoom, GSize.ZERO);
  while (zoom >= 0) {
    var cell = me.getGridCellNoCreate_(grid.x, grid.y, zoom);

    if (cell) {
      me.removeFromArray(cell, marker);
    }
    // For the current zoom we also need to update the map. Markers that no
    // longer are visible are removed from the map. This also lets us keep the count
    // of visible markers up to date.
    if (zoom == me.mapZoom_) {
      if (me.isGridPointVisible_(grid)) {
          me.removeOverlay_(marker);
          changed = true;
      } 
    }
    grid.x = grid.x >> 1;
    grid.y = grid.y >> 1;
    --zoom;
  }
  if (changed) {
    me.notifyListeners_();
  }
};


/**
 * Add many markers at once.
 * Does not actually update the map, just the internal grid.
 *
 * @param {Array of Marker} markers The markers to add.
 * @param {Number} minZoom The minimum zoom level to display the markers.
 * @param {Number} opt_maxZoom The maximum zoom level to display the markers.
 */
MarkerManager.prototype.addMarkers = function(markers, minZoom, opt_maxZoom) {
  var maxZoom = this.getOptMaxZoom_(opt_maxZoom);
  for (var i = markers.length - 1; i >= 0; i--) {
    this.addMarkerBatch_(markers[i], minZoom, maxZoom);
  }

  this.numMarkers_[minZoom] += markers.length;
};


/**
 * Returns the value of the optional maximum zoom. This method is defined so
 * that we have just one place where optional maximum zoom is calculated.
 *
 * @param {Number} opt_maxZoom The optinal maximum zoom.
 * @return The maximum zoom.
 */
MarkerManager.prototype.getOptMaxZoom_ = function(opt_maxZoom) {
  return opt_maxZoom != undefined ? opt_maxZoom : this.maxZoom_;
}


/**
 * Calculates the total number of markers potentially visible at a given
 * zoom level.
 *
 * @param {Number} zoom The zoom level to check.
 */
MarkerManager.prototype.getMarkerCount = function(zoom) {
  var total = 0;
  for (var z = 0; z <= zoom; z++) {
    total += this.numMarkers_[z];
  }
  return total;
};


/**
 * Add a single marker to the map.
 *
 * @param {Marker} marker The marker to add.
 * @param {Number} minZoom The minimum zoom level to display the marker.
 * @param {Number} opt_maxZoom The maximum zoom level to display the marker.
 */
MarkerManager.prototype.addMarker = function(marker, minZoom, opt_maxZoom) {
  var me = this;
  var maxZoom = this.getOptMaxZoom_(opt_maxZoom);
  me.addMarkerBatch_(marker, minZoom, maxZoom);
  var gridPoint = me.getTilePoint_(marker.getPoint(), me.mapZoom_, GSize.ZERO);
  if(me.isGridPointVisible_(gridPoint) && 
     minZoom <= me.shownBounds_.z &&
     me.shownBounds_.z <= maxZoom ) {
    me.addOverlay_(marker);
    me.notifyListeners_();
  }
  this.numMarkers_[minZoom]++;
};

/**
 * Returns true if this bounds (inclusively) contains the given point.
 * @param {Point} point  The point to test.
 * @return {Boolean} This Bounds contains the given Point.
 */
GBounds.prototype.containsPoint = function(point) {
  var outer = this;
  return (outer.minX <= point.x &&
          outer.maxX >= point.x &&
          outer.minY <= point.y &&
          outer.maxY >= point.y);
}

/**
 * Get a cell in the grid, creating it first if necessary.
 *
 * Optimization candidate
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @return {Array} The cell in the array.
 */
MarkerManager.prototype.getGridCellCreate_ = function(x, y, z) {
  var grid = this.grid_[z];
  if (x < 0) {
    x += this.gridWidth_[z];
  }
  var gridCol = grid[x];
  if (!gridCol) {
    gridCol = grid[x] = [];
    return gridCol[y] = [];
  }
  var gridCell = gridCol[y];
  if (!gridCell) {
    return gridCol[y] = [];
  }
  return gridCell;
};


/**
 * Get a cell in the grid, returning undefined if it does not exist.
 *
 * NOTE: Optimized for speed -- otherwise could combine with getGridCellCreate_.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @return {Array} The cell in the array.
 */
MarkerManager.prototype.getGridCellNoCreate_ = function(x, y, z) {
  var grid = this.grid_[z];
  if (x < 0) {
    x += this.gridWidth_[z];
  }
  var gridCol = grid[x];
  return gridCol ? gridCol[y] : undefined;
};


/**
 * Turns at geographical bounds into a grid-space bounds.
 *
 * @param {LatLngBounds} bounds The geographical bounds.
 * @param {Number} zoom The zoom level of the bounds.
 * @param {GSize} swPadding The padding in pixels to extend beyond the
 * given bounds.
 * @param {GSize} nePadding The padding in pixels to extend beyond the
 * given bounds.
 * @return {GBounds} The bounds in grid space.
 */
MarkerManager.prototype.getGridBounds_ = function(bounds, zoom, swPadding,
                                                  nePadding) {
  zoom = Math.min(zoom, this.maxZoom_);
  
  var bl = bounds.getSouthWest();
  var tr = bounds.getNorthEast();
  var sw = this.getTilePoint_(bl, zoom, swPadding);
  var ne = this.getTilePoint_(tr, zoom, nePadding);
  var gw = this.gridWidth_[zoom];
  
  // Crossing the prime meridian requires correction of bounds.
  if (tr.lng() < bl.lng() || ne.x < sw.x) {
    sw.x -= gw;
  }
  if (ne.x - sw.x  + 1 >= gw) {
    // Computed grid bounds are larger than the world; truncate.
    sw.x = 0;
    ne.x = gw - 1;
  }
  var gridBounds = new GBounds([sw, ne]);
  gridBounds.z = zoom;
  return gridBounds;
};


/**
 * Gets the grid-space bounds for the current map viewport.
 *
 * @return {Bounds} The bounds in grid space.
 */
MarkerManager.prototype.getMapGridBounds_ = function() {
  var me = this;
  return me.getGridBounds_(me.map_.getBounds(), me.mapZoom_,
                           me.swPadding_, me.nePadding_);
};


/**
 * Event listener for map:movend.
 * NOTE: Use a timeout so that the user is not blocked
 * from moving the map.
 *
 */
MarkerManager.prototype.onMapMoveEnd_ = function() {
  var me = this;
  me.objectSetTimeout_(this, this.updateMarkers_, 0);
};


/**
 * Call a function or evaluate an expression after a specified number of
 * milliseconds.
 *
 * Equivalent to the standard window.setTimeout function, but the given
 * function executes as a method of this instance. So the function passed to
 * objectSetTimeout can contain references to this.
 *    objectSetTimeout(this, function() { alert(this.x) }, 1000);
 *
 * @param {Object} object  The target object.
 * @param {Function} command  The command to run.
 * @param {Number} milliseconds  The delay.
 * @return {Boolean}  Success.
 */
MarkerManager.prototype.objectSetTimeout_ = function(object, command, milliseconds) {
  return window.setTimeout(function() {
    command.call(object);
  }, milliseconds);
};


/**
 * Refresh forces the marker-manager into a good state.
 * <ol>
 *   <li>If never before initialized, shows all the markers.</li>
 *   <li>If previously initialized, removes and re-adds all markers.</li>
 * </ol>
 */
MarkerManager.prototype.refresh = function() {
  var me = this;
  if (me.shownMarkers_ > 0) {
    me.processAll_(me.shownBounds_, me.removeOverlay_);
  }
  me.processAll_(me.shownBounds_, me.addOverlay_);
  me.notifyListeners_();
};


/**
 * After the viewport may have changed, add or remove markers as needed.
 */
MarkerManager.prototype.updateMarkers_ = function() {
  var me = this;
  me.mapZoom_ = this.map_.getZoom();
  var newBounds = me.getMapGridBounds_();
  
  // If the move does not include new grid sections,
  // we have no work to do:
  if (newBounds.equals(me.shownBounds_) && newBounds.z == me.shownBounds_.z) {
    return;
  }

  if (newBounds.z != me.shownBounds_.z) {
    me.processAll_(me.shownBounds_, me.removeOverlay_);
    me.processAll_(newBounds, me.addOverlay_);
  } else {
    // Remove markers:
    me.rectangleDiff_(me.shownBounds_, newBounds, me.removeCellMarkers_);

    // Add markers:
    me.rectangleDiff_(newBounds, me.shownBounds_, me.addCellMarkers_);
  }
  me.shownBounds_ = newBounds;

  me.notifyListeners_();
};


/**
 * Notify listeners when the state of what is displayed changes.
 */
MarkerManager.prototype.notifyListeners_ = function() {
  GEvent.trigger(this, "changed", this.shownBounds_, this.shownMarkers_);
};


/**
 * Process all markers in the bounds provided, using a callback.
 *
 * @param {Bounds} bounds The bounds in grid space.
 * @param {Function} callback The function to call for each marker.
 */
MarkerManager.prototype.processAll_ = function(bounds, callback) {
  for (var x = bounds.minX; x <= bounds.maxX; x++) {
    for (var y = bounds.minY; y <= bounds.maxY; y++) {
      this.processCellMarkers_(x, y,  bounds.z, callback);
    }
  }
};


/**
 * Process all markers in the grid cell, using a callback.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 * @param {Function} callback The function to call for each marker.
 */
MarkerManager.prototype.processCellMarkers_ = function(x, y, z, callback) {
  var cell = this.getGridCellNoCreate_(x, y, z);
  if (cell) {
    for (var i = cell.length - 1; i >= 0; i--) {
      callback(cell[i]);
    }
  }
};


/**
 * Remove all markers in a grid cell.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 */
MarkerManager.prototype.removeCellMarkers_ = function(x, y, z) {
  this.processCellMarkers_(x, y, z, this.removeOverlay_);
};


/**
 * Add all markers in a grid cell.
 *
 * @param {Number} x The x coordinate of the cell.
 * @param {Number} y The y coordinate of the cell.
 * @param {Number} z The z coordinate of the cell.
 */
MarkerManager.prototype.addCellMarkers_ = function(x, y, z) {
  this.processCellMarkers_(x, y, z, this.addOverlay_);
};


/**
 * Use the rectangleDiffCoords function to process all grid cells
 * that are in bounds1 but not bounds2, using a callback, and using
 * the current MarkerManager object as the instance.
 *
 * Pass the z parameter to the callback in addition to x and y.
 *
 * @param {Bounds} bounds1 The bounds of all points we may process.
 * @param {Bounds} bounds2 The bounds of points to exclude.
 * @param {Function} callback The callback function to call
 *                   for each grid coordinate (x, y, z).
 */
MarkerManager.prototype.rectangleDiff_ = function(bounds1, bounds2, callback) {
  var me = this;
  me.rectangleDiffCoords(bounds1, bounds2, function(x, y) {
    callback.apply(me, [x, y, bounds1.z]);
  });
};


/**
 * Calls the function for all points in bounds1, not in bounds2
 *
 * @param {Bounds} bounds1 The bounds of all points we may process.
 * @param {Bounds} bounds2 The bounds of points to exclude.
 * @param {Function} callback The callback function to call
 *                   for each grid coordinate.
 */
MarkerManager.prototype.rectangleDiffCoords = function(bounds1, bounds2, callback) {
  var minX1 = bounds1.minX;
  var minY1 = bounds1.minY;
  var maxX1 = bounds1.maxX;
  var maxY1 = bounds1.maxY;
  var minX2 = bounds2.minX;
  var minY2 = bounds2.minY;
  var maxX2 = bounds2.maxX;
  var maxY2 = bounds2.maxY;

  for (var x = minX1; x <= maxX1; x++) {  // All x in R1
    // All above:
    for (var y = minY1; y <= maxY1 && y < minY2; y++) {  // y in R1 above R2
      callback(x, y);
    }
    // All below:
    for (var y = Math.max(maxY2 + 1, minY1);  // y in R1 below R2
         y <= maxY1; y++) {
      callback(x, y);
    }
  }

  for (var y = Math.max(minY1, minY2);
       y <= Math.min(maxY1, maxY2); y++) {  // All y in R2 and in R1
    // Strictly left:
    for (var x = Math.min(maxX1 + 1, minX2) - 1;
         x >= minX1; x--) {  // x in R1 left of R2
      callback(x, y);
    }
    // Strictly right:
    for (var x = Math.max(minX1, maxX2 + 1);  // x in R1 right of R2
         x <= maxX1; x++) {
      callback(x, y);
    }
  }
};


/**
 * Removes value from array. O(N).
 *
 * @param {Array} array  The array to modify.
 * @param {any} value  The value to remove.
 * @param {Boolean} opt_notype  Flag to disable type checking in equality.
 * @return {Number}  The number of instances of value that were removed.
 */
MarkerManager.prototype.removeFromArray = function(array, value, opt_notype) {
  var shift = 0;
  for (var i = 0; i < array.length; ++i) {
    if (array[i] === value || (opt_notype && array[i] == value)) {
      array.splice(i--, 1);
      shift++;
    }
  }
  return shift;
};


function PanoramioLayerCallback(json, panoLayer) {
  this.panoLayer = panoLayer;

  for (var i = 0; i < json.photos.length; i++) {
    var photo = json.photos[i];
    if (!panoLayer.ids[photo.photo_id]) {
      var marker = this.createMarker(photo, panoLayer.markerIcon);
      panoLayer.mgr.addMarker(marker, 0);
      panoLayer.ids[photo.photo_id] = "exists";
      panoLayer.mgr.addMarker(marker, panoLayer.map.getZoom());
    }
  }
}

PanoramioLayerCallback.prototype.formImgUrl = function(photoId, imgType) {
  return 'http://www.panoramio.com/photos/' + imgType + '/' + photoId + '.jpg';
}
 
PanoramioLayerCallback.prototype.formPageUrl = function(photoId) {
  return 'http://www.panoramio.com/photo/' + photoId;
}

PanoramioLayerCallback.prototype.createMarker = function(photo, baseIcon) {
  var me = this;
  var markerIcon = new GIcon(baseIcon);
  markerIcon.image = this.formImgUrl(photo.photo_id, "mini_square");
  var marker = new GMarker(new GLatLng(photo.latitude, photo.longitude), {icon: markerIcon, title: photo.photo_title});

  if (photo.photo_title.length > 33) {
    photo.photo_title = photo.photo_title.substring(0, 33) + "&#8230;";
  }
  var html = "<div id='infowin' style='height:320px; width:240px;'>" +
            "<p><a href='http://www.panoramio.com/' target='_blank'>" + 
             "<img src='http://www.panoramio.com/img/logo-small.gif' border='0' width='119px' height='25px' alt='Panoramio logo' /><\/a></p>" +
             "<a id='photo_infowin' target='_blank' href='" + photo.photo_url + "'>" +                
             "<img border='0' width='" + photo.width + "' height='" + photo.height + "' src='" + photo.photo_file_url + "'/><\/a>" +
             "<div style='overflow: hidden; width: 240px;'>" +
             "<p><a target='_blank' class='photo_title' href='" + photo.photo_url +
             "'><strong>" + photo.photo_title + "<\/strong><\/a></p>" +
             "<p>Posted by <a target='_blank' href='" + photo.owner_url + "'>" +
             photo.owner_name + "<\/a></p><\/div>" +
             "<\/div>";

  marker.html = html;

  GEvent.addListener(marker, "click", function() {
    me.panoLayer.map.openInfoWindow(marker.getLatLng(), marker.html, {noCloseOnClick: true});
  });
 
  return marker;
}


function PanoramioLayer(map, opt_opts) {
  var me = this;
  me.map = map;
  me.ids = {};
  me.mgr = new MarkerManager(map, {maxZoom: 19});

  var icon = new GIcon();
  icon.image = "http://www.panoramio.com/img/panoramio-marker.png"; 
  icon.shadow = "";  
  icon.iconSize = new GSize(24, 24); 
  icon.shadowSize = new GSize(22, 22); 
  icon.iconAnchor = new GPoint(9, 9);  
  icon.infoWindowAnchor = new GPoint(9, 0); 

  me.markerIcon = icon;
  me.enabled = false;

  GEvent.addListener(map, "moveend", function() {
    if (me.enabled) {
      var bounds = map.getBounds();
      var southWest = bounds.getSouthWest();
      var northEast = bounds.getNorthEast();
      me.load(me, {maxy: northEast.lat(), miny: southWest.lat(), maxx: northEast.lng(), minx: southWest.lng()});
    }
  });
}

PanoramioLayer.prototype.enable = function() {
  this.enabled = true;
  GEvent.trigger(this.map, "moveend");
}

PanoramioLayer.prototype.disable = function() {
  this.enabled = false;
  this.mgr.clearMarkers();
  this.ids = {};
}

PanoramioLayer.prototype.getEnabled = function() {
  return this.enabled;
}

PanoramioLayer.prototype.load = function(panoLayer, userOptions) {
  var options = {
    order: "popularity",
    set: "public",
    from: "0",
    to: "10",
    minx: "-180",
    miny: "-90",
    maxx: "180",
    maxy: "90",
    size: "small"
  };
 
  for (optionName in userOptions) {
    if (userOptions.hasOwnProperty(optionName)) {
      options[optionName] = userOptions[optionName];
    }
  }
 
  var url = "http://www.panoramio.com/map/get_panoramas.php?";
  var uniqueID = "";
 
  for (optionName in options) {
    if (options.hasOwnProperty(optionName)) {
      var optionVal = "" + options[optionName] + "";
      url += optionName + "=" + optionVal + "&";
      uniqueID += optionVal.replace(/[^\w]+/g,"");
    }
  }
  var callbackName = "PanoramioLayerCallback.loader" + uniqueID; //ask dion
  eval(callbackName + " = function(json) { var pa = new PanoramioLayerCallback(json, panoLayer);}");
 
  var script = document.createElement('script');
  script.setAttribute('src', url + 'callback=' + callbackName);
  script.setAttribute('id', 'jsonScript');
  script.setAttribute('type', 'text/javascript');
  document.documentElement.firstChild.appendChild(script);
}



/*
* PanoMapTypeControl Class 
*  Copyright (c) 2007, Google 
*  Author: Pamela Fox, others
* 
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* 
*       http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This class lets you add a control to the map which mimics GMapTypeControl
*  and allows for the addition of a traffic button/traffic key.
*/

/*
 * Constructor for PanoMapTypeControl
 */
function PanoMapTypeControl(opt_opts) {
  this.options = opt_opts || {};
}


PanoMapTypeControl.prototype = new GControl();

/**
 * Is called by GMap2's addOverlay method. Creates the button 
 *  and appends to the map div.
 * @param {GMap2} map The map that has had this PanoMapTypeControl added to it.
 * @return {DOM Object} Div that holds the control
 */ 
PanoMapTypeControl.prototype.initialize = function(map) {
  var container = document.createElement("div");
  var me = this;

  var panoDiv = me.createButton_("Panoramio");
  panoDiv.style.marginRight = "8px";
  GEvent.addDomListener(panoDiv, "click", function() {
    if (me.panoLayer) {
      if (me.panoLayer.getEnabled()) {
        me.panoLayer.disable();
      } else {
        me.panoLayer.enable();
      }
    } else {
      me.panoLayer = new PanoramioLayer(map);
      me.panoLayer.enable();
    }
    me.toggleButton_(panoDiv.firstChild, me.panoLayer.getEnabled());
  });
  me.toggleButton_(panoDiv.firstChild, false);

  /*var mapDiv = me.createButton_("Mapa");
  var satDiv = me.createButton_("Satelite");
  var hybDiv = me.createButton_("Hibrido");
 
  me.assignButtonEvent_(mapDiv, map, G_NORMAL_MAP, [satDiv, hybDiv]);
  me.assignButtonEvent_(satDiv, map, G_SATELLITE_MAP, [mapDiv, hybDiv]);
  me.assignButtonEvent_(hybDiv, map, G_HYBRID_MAP, [satDiv, mapDiv]);
  GEvent.addListener(map, "maptypechanged", function() {
    if (map.getCurrentMapType() == G_NORMAL_MAP) {
      GEvent.trigger(mapDiv, "click"); 
    } else if (map.getCurrentMapType() == G_SATELLITE_MAP) {
      GEvent.trigger(satDiv, "click");
    } else if (map.getCurrentMapType() == G_HYBRID_MAP) {
      GEvent.trigger(hybDiv, "click");
    }
  });*/

  container.appendChild(panoDiv);
  /*container.appendChild(mapDiv);
  container.appendChild(satDiv);
  container.appendChild(hybDiv);*/

  map.getContainer().appendChild(container);

  GEvent.trigger(map, "maptypechanged");
  return container;
}

/*
 * Creates simple buttons with text nodes. 
 * @param {String} text Text to display in button
 * @return {DOM Object} The div for the button.
 */
PanoMapTypeControl.prototype.createButton_ = function(text) {
  var buttonDiv = document.createElement("div");
  this.setButtonStyle_(buttonDiv);
  buttonDiv.style.cssFloat = "left";
  buttonDiv.style.styleFloat = "left";
  var textDiv = document.createElement("div");
  textDiv.appendChild(document.createTextNode(text));
  textDiv.style.width = "6em";
  buttonDiv.appendChild(textDiv);
  return buttonDiv;
}

/*
 * Assigns events to MapType buttons to change maptype
 *  and toggle button styles correctly for all buttons
 *  when button is clicked.
 *  @param {DOM Object} div Button's div to assign click to
 *  @param {GMap2} Map object to change maptype of.
 *  @param {Object} mapType GMapType to change map to when clicked
 *  @param {Array} otherDivs Array of other button divs to toggle off
 */  
PanoMapTypeControl.prototype.assignButtonEvent_ = function(div, map, mapType, otherDivs) {
  var me = this;

  GEvent.addDomListener(div, "click", function() {
    for (var i = 0; i < otherDivs.length; i++) {
      me.toggleButton_(otherDivs[i].firstChild, false);
    }
    me.toggleButton_(div.firstChild, true);
    map.setMapType(mapType);
  });
}

/*
 * Changes style of button to appear on/off depending on boolean passed in.
 * @param {DOM Object} div  Button div to change style of
 * @param {Boolean} boolCheck Used to decide to use on style or off style
 */
PanoMapTypeControl.prototype.toggleButton_ = function(div, boolCheck) {
   div.style.fontWeight = boolCheck ? "bold" : "";
   div.style.border = "1px solid white";
   var shadows = boolCheck ? ["Top", "Left"] : ["Bottom", "Right"];
   for (var j = 0; j < shadows.length; j++) {
     div.style["border" + shadows[j]] = "1px solid #b0b0b0";
  } 
}

/*
 * Required by GMaps API for controls. 
 * @return {GControlPosition} Default location for control
 */
PanoMapTypeControl.prototype.getDefaultPosition = function() {
  return new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(150, 7));
}

/*
 * Sets the proper CSS for the given button element.
 * @param {DOM Object} button Button div to set style for
 */
PanoMapTypeControl.prototype.setButtonStyle_ = function(button) {
  button.style.color = "#000000";
  button.style.backgroundColor = "white";
  button.style.font = "small Arial";
  button.style.border = "1px solid black";
  button.style.padding = "0px";
  button.style.margin= "0px";
  button.style.textAlign = "center";
  button.style.fontSize = "12px"; 
  button.style.cursor = "pointer";
}
