function ClusterMarker($map){
	var $this=this;
	$this.map=$map;
	$this.markers=[];
	$this.clusterMarkers=[];
	$this.clusteringEnabled=true;
	GEvent.bind($map, 'moveend', this, $this.moveEnd);
	GEvent.bind($map, 'zoomend', this, $this.zoomEnd);
	GEvent.bind($map, 'maptypechanged', $this, $this.mapTypeChanged);
}

ClusterMarker.prototype.addMarkers=function($markers, $areOverlaid){
	var $this=this, $indexOffset=$this.markers.length, $length=$markers.length;
	if(typeof($areOverlaid)==='undefined'){
		$areOverlaid=false;
	}
	while($length--){
		$markers[$length]._ClusterMarker_={index:$length+$indexOffset, isActive:true, doNotCluster:false, anchorPoint:[], intersectTable:[], isOverlaid:$areOverlaid};
	}
	$this.markers=$this.markers.concat($markers);
};

ClusterMarker.prototype.refresh=function(){
	var $this=this;
	function compareArrays($array1, $array2){
		if($array1.length!==$array2.length){
			return false;
		}
		var i, $length=$array1.length<$array2.length?$array1.length:$array2.length;
		for (i=0; i<$length; i++){
			if($array1[i]!==$array2[i]){
				return false;
			}
		}
		return true;
	}
	function createClusterMarker($indexes){
		var $clusterBounds=new GLatLngBounds(), $length=$indexes.length, i, $markers=$this.markers, $title='';
		while($length--){
			$clusterBounds.extend($markers[$indexes[$length]].getLatLng());
			//$title+='"'+$markers[$indexes[$length]].getTitle()+'"';
			//if($length){
			//	$title+=', ';
			//}
		}
		var $clusterMarker=new GMarker($markers[$indexes[0]].getLatLng(), {icon:$this.clusterMarkerIcon, title:$title});
		GEvent.addListener($clusterMarker, 'click', function(){
			$this.clusterMarkerClickHandler($clusterMarker);
		});
		$clusterMarker._markerIndexes=$indexes;
		$clusterMarker._clusterBounds=$clusterBounds;
		$length=$indexes.length;
		while($length--){
			$markers[$indexes[$length]]._clusterMarker=$clusterMarker;
		}
		return $clusterMarker;
	}
	
	//	filter active markers
	var $map=$this.map, $zoom=$map.getZoom(), $bounds=$map.getBounds(), $markers=$this.markers, $length=$markers.length, $marker, $isActive;
	var $mapSize=$map.getSize(), $borderPaddingX=$mapSize.width, $borderPaddingY=$mapSize.height;
	var $projection=$map.getCurrentMapType().getProjection();
	var $mapSwPoint=$projection.fromLatLngToPixel($bounds.getSouthWest(), $zoom);
	var $mapNePoint=$projection.fromLatLngToPixel($bounds.getNorthEast(), $zoom);
	var $swX=$mapSwPoint.x-$borderPaddingX, $swY=$mapSwPoint.y+$borderPaddingY, $neX=$mapNePoint.x+$borderPaddingX, $neY=$mapNePoint.y-$borderPaddingY;
	var $activeSwLatLng=$projection.fromPixelToLatLng(new GPoint($swX, $swY), $zoom);
	var $activeNeLatLng=$projection.fromPixelToLatLng(new GPoint($neX, $neY), $zoom);
	
	$bounds.extend($activeSwLatLng);
	$bounds.extend($activeNeLatLng);
	
	while($length--){
		$marker=$markers[$length];
		$isActive=false;
		if(!$marker.isHidden() && $bounds.containsLatLng($marker.getLatLng())){
			$isActive=true;
		}
		$marker._ClusterMarker_.isActive=$isActive;
		$marker._makeVisible=$isActive;
	}
	
		//	filter clustered markers
	var $newClusterIndexes=[];
	if($this.clusteringEnabled){
		var i=$markers.length, j, $indexes, $cancelCluster;
		while(i--){
			if($markers[i]._makeVisible){
				$indexes=[i];
				j=i;
				while(j--){
					if($markers[j]._makeVisible && $markers[i].getLatLng().distanceFrom($markers[j].getLatLng())<10000 && $this.markerIconsIntersect($markers[i], $markers[j])){
						$indexes.push(j);
					}
				}
				if($indexes.length>1){
					$cancelCluster=false;
					j=$indexes.length;
					while(j--){
						if($markers[$indexes[j]]._ClusterMarker_.doNotCluster){
							$cancelCluster=true;
						} else {
							$markers[$indexes[j]]._makeVisible=false;
						}
					}
					//	array $newClusterIndexes is array of array of indexes of clustered markers;
					if(!$cancelCluster){
						$newClusterIndexes.push($indexes);
					}
				}
			}
		}
	}
	
	//	update map
	
	//	add or remove cluster markers
	var $clusterMarkers=$this.clusterMarkers, $clusterMarkersLength=$clusterMarkers.length, $clusterMarker, $newClusterMarkers=[], i, j, k, $indexes;
	var $newClusterIndexesLength=$newClusterIndexes.length, $clusterIndex;
	
	//	remove cluster markers who markerIndexes no longer exist and flag the relates index array not to be created
	for(i=0; i<$clusterMarkersLength; i++){
		$clusterMarker=$clusterMarkers[i];
		$clusterIndex=$clusterMarker._markerIndexes;
		for(j=0; j<$newClusterIndexesLength; j++){
			if(compareArrays($clusterIndex, $newClusterIndexes[j])){
				//	cluster marker markerIndexes still exist so do not remove cluster marker
				$newClusterMarkers.push($clusterMarker);
				$newClusterIndexes[j]=false;
				break;	//	break out of j loop
			}
		}
		if($newClusterIndexes[j]!==false){
			$indexes=$clusterMarker._markerIndexes;
			$length=$indexes.length;
			while($length--){
				delete $this.markers[$indexes[$length]]._clusterMarker;
			}
			$map.removeOverlay($clusterMarker);
		}
		
	}
	//	loop thru $newClusterIndexes creating cluster markers for any index which is not FALSE
	while($newClusterIndexesLength--){
		$indexes=$newClusterIndexes[$newClusterIndexesLength];
		if($indexes!==false){
			$clusterMarker=createClusterMarker($newClusterIndexes[$newClusterIndexesLength]);
			$newClusterMarkers.push($clusterMarker);
			$map.addOverlay($clusterMarker);
		}
	}
	$this.clusterMarkers=$newClusterMarkers;
	//	add or remove active markers
	$length=$markers.length;
	while($length--){
		$marker=$markers[$length];
		if($marker._makeVisible && !$marker._ClusterMarker_.isOverlaid){
			$map.addOverlay($marker);
			$marker._ClusterMarker_.isOverlaid=true;
		} else if(!$marker._makeVisible && $marker._ClusterMarker_.isOverlaid){
			$map.removeOverlay($marker);
			$marker._ClusterMarker_.isOverlaid=false;
		}
	}
};

ClusterMarker.prototype.clusterMarkerClickHandler=function($clusterMarker){
	var $this=this, $map=$this.map, $indexes=$clusterMarker._markerIndexes;
	function $sortByMarkerTitle(a, b){
		var title1=a.getTitle(), title2=b.getTitle();
		if(title1<title2){
			return -1;
		} else if (title1>title2){
			return 1;
		} else {
			return 0;
		}
	}
	function $clusterMarkerInfoWindowClickHandler($marker){
		return function(){
			var $desiredZoom=$this.getMinUnclusterLevel($marker);
			if($desiredZoom>=$map.getCurrentMapType().getMaximumResolution()){
				//	marker cannot be unclustered
				$map.panTo($marker.getLatLng());
				GEvent.trigger($marker, 'click');
			} else {
				//	marker can be unclustered
				$map.setZoom($this.getMinUnclusterLevel($marker));
				$map.panTo($marker.getLatLng());
				GEvent.trigger($marker, 'click');
			}
		};
	}
	
	var $length=$indexes.length, $html='', $markers=[], i;
	for(i=0; i<$length; i++){
		$markers.push($this.markers[$indexes[i]]);
	}
	$markers.sort($sortByMarkerTitle);
	for(i=0; i<$length; i++){
		$html+=$markers[i]._html+'';
		if(i!==$length-1){
			$html+='';
		}
	}
	$clusterMarker.openInfoWindowHtml($html);
};

ClusterMarker.prototype.getMarkerAnchorPoint=function($marker, $zoom){
	if(typeof($marker._ClusterMarker_.anchorPoint[$zoom])!=='undefined'){
		return $marker._ClusterMarker_.anchorPoint[$zoom];
	} else {
		var $anchorPoint=this.map.getCurrentMapType().getProjection().fromLatLngToPixel($marker.getLatLng(), $zoom);
		$marker._ClusterMarker_.anchorPoint[$zoom]=$anchorPoint;
		return $anchorPoint;
	}
};

ClusterMarker.prototype.zoomEnd=function(){
	this._cancelMoveEnd=true;
	this.refresh();
};

ClusterMarker.prototype.moveEnd=function(){
	if(this._cancelMoveEnd){
		this._cancelMoveEnd=false;
	} else {
		this.refresh();
	}
};

ClusterMarker.prototype.mapTypeChanged=function(){
	this.refresh();
};


ClusterMarker.prototype.markerIconsIntersect=function($marker1, $marker2, $zoom){	//	optional zoom parameter so triggerClick can be implemented
	var $this=this, $map=$this.map;
	function getIconPointBounds($marker){
		var $icon=$marker.getIcon();
		var $iconSize=$icon.iconSize;
		var $iconAnchorPoint=$icon.iconAnchor;
		var $markerAnchorPoint=$this.getMarkerAnchorPoint($marker, $zoom);
		
		var $swIconAnchorPoint=new GPoint($markerAnchorPoint.x-$iconAnchorPoint.x, $markerAnchorPoint.y-$iconAnchorPoint.y+$iconSize.height);
		var $neIconAnchorPoint=new GPoint($markerAnchorPoint.x-$iconAnchorPoint.x+$iconSize.width, $markerAnchorPoint.y-$iconAnchorPoint.y);
		return {sw:$swIconAnchorPoint, ne:$neIconAnchorPoint};
	}
	if(typeof($zoom)==='undefined'){
		$zoom=$map.getZoom();
	}
	if(typeof($marker1._ClusterMarker_.intersectTable[$zoom])!=='undefined' && typeof($marker1._ClusterMarker_.intersectTable[$zoom][$marker2._ClusterMarker_.index])!=='undefined'){
		return $marker1._ClusterMarker_.intersectTable[$zoom][$marker2._ClusterMarker_.index];
	}
	var $bounds1=getIconPointBounds($marker1), $bounds2=getIconPointBounds($marker2);
	var $intersects=!($bounds2.sw.x>$bounds1.ne.x || $bounds2.ne.x<$bounds1.sw.x || $bounds2.ne.y>$bounds1.sw.y || $bounds2.sw.y<$bounds1.ne.y);
	if(typeof($marker1._ClusterMarker_.intersectTable[$zoom])==='undefined'){
		$marker1._ClusterMarker_.intersectTable[$zoom]=[];
	}
	$marker1._ClusterMarker_.intersectTable[$zoom][$marker2._ClusterMarker_.index]=$intersects;
	return $intersects;
};

ClusterMarker.prototype.fitMapToMarkers=function($markers, $maxZoom){
	var $this=this, $bounds=new GLatLngBounds(), $refresh=false; 
	if(typeof($markers)==='undefined' || $markers===null){
		$markers=this.markers;
	}
	var $length=$markers.length;
	while($length--){
		if(!$markers[$length].isHidden()){
			$bounds.extend($markers[$length].getLatLng());
			$refresh=true;
		}
	}
	if($refresh){
		var $zoom=$this.map.getBoundsZoomLevel($bounds);
		if(typeof($maxZoom)!=='undefined'){
			$zoom=$zoom>$maxZoom?$maxZoom:$zoom;
		}
		$this.map.setCenter($bounds.getCenter(), $zoom);	
	}
};

ClusterMarker.prototype.getMinUnclusterLevel=function($marker){
	var $this=this, $map=$this.map, $maxZoomLevel=$map.getCurrentMapType().getMaximumResolution(), $isClustered, $markers=$this.markers, $length=$markers.length, $indexes=[], i, $zoomLevel;
	while($length--){
		if($marker!==$markers[$length]){
			$indexes.push($markers[$length]._ClusterMarker_.index);	//	will need updating when remove single (or more) markers is implemented to avoid undefined elements in this.markers array
		}
	}
	if($marker._clusterMarker){
		$zoomLevel=$map.getZoom()+1;
	} else {
		$zoomLevel=0;
	}
	$length=$indexes.length;
	while($zoomLevel<=$maxZoomLevel){
		$isClustered=false;
		i=$length;
		while(!$isClustered && i--){
			if($this.markerIconsIntersect($marker, $markers[$indexes[i]], $zoomLevel)){
				$isClustered=true;
			}
		}
		if(!$isClustered){
			break;
		}
		$zoomLevel++;
	}
	return $zoomLevel;
};


ClusterMarker.prototype.setDoNotCluster=function($marker, $state){
	$marker._ClusterMarker_.doNotCluster=$state;
	this.refresh();
	if($state){
		var $this=this, i=GEvent.addListener($this.map, 'infowindowclose', function(){
			$marker._ClusterMarker_.doNotCluster=false;
			$this.refresh();
			GEvent.removeListener(i);
		});
	}
};

