function ClusterMarker($map, $options){
	this._map=$map;
	this._mapMarkers=[];
	this._mapMarkersIconBounds=[];
	this._clusterMarkers=[];
	this._clusterMarkersEventListeners=[];
	if(typeof($options)=='undefined'){
		$options={};
	}
	this.fitMapToMarkers=($options.fitMapToMarkers===false)?false:true;
	if ($options.markers){
		this.addMarkers($options.markers);
	}
	this.borderPadding=($options.borderPadding)?$options.borderPadding:256;
	this.intersectPadding=($options.intersectPadding)?$options.intersectPadding:5;
	this.clusteringEnabled=($options.clusteringEnabled===false)?false:true;
	if ($options.clusterMarkerIcon){
		this._clusterMarker.icon=$options.clusterMarkerIcon;
	} else {
		this._clusterMarker.icon=new GIcon();
		this._clusterMarker.icon.image=defaultPropertyClusterIcon;
		this._clusterMarker.icon.shadow=defaultPropertyClusterIconShadow;
		this._clusterMarker.icon.iconSize=new GSize(defaultPropertyClusterIconWidth, defaultPropertyClusterIconHeight);
		this._clusterMarker.icon.shadowSize=new GSize(defaultPropertyClusterIconShadowWidth, defaultPropertyClusterIconShadowHeight);
		this._clusterMarker.icon.iconAnchor=new GPoint(6, 20);
	}
	this._cancelMoveEnd=false;
	GEvent.bind(this._map, 'moveend', this, this._moveEnd);
	GEvent.bind(this._map, 'zoomend', this, this._zoomEnd);
	GEvent.bind(this._map, 'maptypechanged', this, this.refresh);
};

ClusterMarker.prototype.addMarkers=function($markers){
	for (var i=$markers.length-1; i>=0; i--){
		$markers[i]._isVisible=false;
	}
	if (this.fitMapToMarkers){
		var $markersBounds=new GLatLngBounds();
		for (i=$markers.length-1; i>=0; i--){
			$markersBounds.extend($markers[i].getLatLng());
		}
		this._map.setCenter($markersBounds.getCenter(), this._map.getBoundsZoomLevel($markersBounds));
	}
	this._mapMarkers=$markers;
	this._mapMarkersIconBounds=[];
};

ClusterMarker.prototype._clusterMarker=function($clusterGroupIndexes){
	var $clusterGroupBounds, i, $clusterMarker, $map;
	$clusterGroupBounds=new GLatLngBounds();
	for (i=$clusterGroupIndexes.length-1; i>=0; i--){
		$clusterGroupBounds.extend(this._mapMarkers[$clusterGroupIndexes[i]].getLatLng());
	}
	$clusterMarker=new GMarker($clusterGroupBounds.getCenter(), {icon:this._clusterMarker.icon, title:'Click to zoom in and see '+$clusterGroupIndexes.length+' markers.'});
	//	copy of this._map needed to obtain function closure in click event
	$map=this._map;
	this._clusterMarkersEventListeners.push(GEvent.addListener($clusterMarker, 'click', function(){
		//	on cluster marker click center and zoom map to contain all map markers in cluster group
		//	expose cluster marker click event to enable over-riding the default function (to do)
		$map.setCenter($clusterGroupBounds.getCenter(), $map.getBoundsZoomLevel($clusterGroupBounds));
	}));
	return $clusterMarker;
};

ClusterMarker.prototype._filterContainedMapMarkers=function(){
	var $borderPadding, $mapZoomLevel, $mapProjection, $mapBounds, $mapPointSw, $activeAreaPointSw, $activeAreaLatLngSw, $mapPointNe, $activeAreaPointNe, $activeAreaLatLngNe, $activeAreaBounds, i;
	//	iterate thru markers array
	//	set makeVisible to FALSE if marker not within map bounds + borderPadding
	$borderPadding=this.borderPadding;
	
	$mapZoomLevel=this._map.getZoom();
	$mapProjection=this._map.getCurrentMapType().getProjection();
	$mapBounds=this._map.getBounds();
	
	$mapPointSw=$mapProjection.fromLatLngToPixel($mapBounds.getSouthWest(), $mapZoomLevel);
	$activeAreaPointSw=new GPoint($mapPointSw.x-$borderPadding, $mapPointSw.y+$borderPadding);
	$activeAreaLatLngSw=$mapProjection.fromPixelToLatLng($activeAreaPointSw, $mapZoomLevel, true);
	
	$mapPointNe=$mapProjection.fromLatLngToPixel($mapBounds.getNorthEast(), $mapZoomLevel);
	$activeAreaPointNe=new GPoint($mapPointNe.x+$borderPadding, $mapPointNe.y-$borderPadding);
	$activeAreaLatLngNe=$mapProjection.fromPixelToLatLng($activeAreaPointNe, $mapZoomLevel, true);
	
	$activeAreaBounds=new GLatLngBounds($activeAreaLatLngSw, $activeAreaLatLngNe);
	
	for (i=this._mapMarkers.length-1; i>=0; i--){
		this._mapMarkers[i]._makeVisible=$activeAreaBounds.containsLatLng(this._mapMarkers[i].getLatLng())?true:false;
	}
};

ClusterMarker.prototype._filterIntersectingMapMarkers=function(){
	var $clusterGroup, i, j, $mapZoomLevel=this._map.getZoom();
	if (typeof(this._mapMarkersIconBounds[$mapZoomLevel])=='undefined'){
		this._mapMarkersIconBounds[$mapZoomLevel]=this._mapMarkersBoundsArray();
	}
	for (i=this._mapMarkers.length-1; i>0; i--)
	{
		if (this._mapMarkers[i]._makeVisible){
			$clusterGroup=[];
			for (j=i-1; j>=0; j--){
				if (this._mapMarkers[j]._makeVisible && this._mapMarkersIconBounds[$mapZoomLevel][i].intersects(this._mapMarkersIconBounds[$mapZoomLevel][j])){
					$clusterGroup.push(j);
				}
			}
			if ($clusterGroup.length!==0){
				$clusterGroup.push(i);
				for (j=$clusterGroup.length-1; j>=0; j--){
					this._mapMarkers[$clusterGroup[j]]._makeVisible=false;
				}
				this._clusterMarkers.push(this._clusterMarker($clusterGroup));
			}
		}
	}
};

ClusterMarker.prototype._mapMarkersBoundsArray=function(){
	//	returns single dimension array of icon bounds for map's current zoom level
	var $mapProjection, $mapZoomLevel, i, $marker, $iconSize, $iconWidth, $iconHeight, $markerPoint, $markerPointX, $markerPointY, $iconAnchorPointOffset, $iconAnchorPointOffsetX, $iconAnchorPointOffsetY, $iconBoundsPointSw, $iconBoundsPointNe, $iconBoundsLatLngSw, $iconBoundsLatLngNe, $iconBounds=[], $intersectPadding;
	$intersectPadding=this.intersectPadding;
	$mapProjection=this._map.getCurrentMapType().getProjection();
	$mapZoomLevel=this._map.getZoom();
	for (i=this._mapMarkers.length-1; i>=0; i--){
		$marker=this._mapMarkers[i];
		
		$iconSize=$marker.getIcon().iconSize;
		$iconWidth=$iconSize.width;
		$iconHeight=$iconSize.height;
		
		$markerPoint=$mapProjection.fromLatLngToPixel($marker.getLatLng(), $mapZoomLevel);
		$markerPointX=$markerPoint.x;
		$markerPointY=$markerPoint.y;
		
		$iconAnchorPointOffset=$marker.getIcon().iconAnchor;
		$iconAnchorPointOffsetX=$iconAnchorPointOffset.x;
		$iconAnchorPointOffsetY=$iconAnchorPointOffset.y;
		
		$iconBoundsPointSw=new GPoint($markerPointX-$iconAnchorPointOffsetX-$intersectPadding, $markerPointY-$iconAnchorPointOffsetY+$iconHeight+$intersectPadding);
		$iconBoundsPointNe=new GPoint($markerPointX-$iconAnchorPointOffsetX+$iconWidth+$intersectPadding, $markerPointY-$iconAnchorPointOffsetY-$intersectPadding);
		$iconBoundsLatLngSw=$mapProjection.fromPixelToLatLng($iconBoundsPointSw, $mapZoomLevel, true);
		$iconBoundsLatLngNe=$mapProjection.fromPixelToLatLng($iconBoundsPointNe, $mapZoomLevel, true);
		
		$iconBounds[i]=new GLatLngBounds($iconBoundsLatLngSw, $iconBoundsLatLngNe);
	}
	return $iconBounds;
};

ClusterMarker.prototype._moveEnd=function(){
	switch (this._cancelMoveEnd){
	case true:this._cancelMoveEnd=false; break;
	default:this.refresh();
	}
};

ClusterMarker.prototype.refresh=function(){
	//	remove any cluster markers and event listeners then clear both cluster marker and event arrays
	//	use array pop method instead if quicker
	var i,j;
	
	this._removeClusterMarkers();
	
	this._filterContainedMapMarkers();
	if (this.clusteringEnabled && this._map.getZoom()<this._map.getCurrentMapType().getMaximumResolution()){
		this._filterIntersectingMapMarkers();
	}
	
	//	add any cluster markers
	for (i=this._clusterMarkers.length-1; i>=0; i--){
		this._map.addOverlay(this._clusterMarkers[i]);
	}
	//	add or remove map markers
	for (i=this._mapMarkers.length-1; i>=0; i--){
		j=this._mapMarkers[i];
		if (!j._isVisible && j._makeVisible){
			this._map.addOverlay(j);
			j._isVisible=true;
		}
		if (j._isVisible && !j._makeVisible){
			this._map.removeOverlay(j);
			j._isVisible=false;
		}
	}
};

ClusterMarker.prototype._removeClusterMarkers=function(){
	for (var i=this._clusterMarkers.length-1; i>=0; i--){
		this._map.removeOverlay(this._clusterMarkers[i]);
	}
	for (i=this._clusterMarkersEventListeners.length-1; i>=0; i--){
		GEvent.removeListener(this._clusterMarkersEventListeners[i]);
	}
	this._clusterMarkers=[];
	this._clusterMarkersEventListeners=[];
};

ClusterMarker.prototype.removeMarkers=function(){
	for (var i=this._mapMarkers.length-1; i>=0; i--){
		if (this._mapMarkers[i]. _isVisible){
			this._map.removeOverlay(this._mapMarkers[i]);
		}
		delete this._mapMarkers[i]. _isVisible;
		delete this._mapMarkers[i]._makeVisible;
	}
	this._removeClusterMarkers();
	this._mapMarkers=[];
	this._mapMarkersIconBounds=[];
};

ClusterMarker.prototype._zoomEnd=function(){
	this._cancelMoveEnd=true;
	this.refresh();
};
