Clusterer=function(map)
{
  this.map=map;
  this.markers=[];
  this.clusters=[];
  this.timeout=null;
  this.currentZoomLevel=map.getZoom();
  this.maxVisibleMarkers=Clusterer.defaultMaxVisibleMarkers;
  this.gridSize=Clusterer.defaultGridSize;
  this.minMarkersPerCluster=Clusterer.defaultMinMarkersPerCluster;
  this.maxLinesPerInfoBox=Clusterer.defaultMaxLinesPerInfoBox;
  this.icon=Clusterer.defaultIcon;
  GEvent.addListener(map,'zoomend',Clusterer.MakeCaller(Clusterer.Display,this));
  GEvent.addListener(map,'moveend',Clusterer.MakeCaller(Clusterer.Display,this));
  GEvent.addListener(map,'infowindowclose',Clusterer.MakeCaller(Clusterer.PopDown,this));
};

Clusterer.defaultMaxVisibleMarkers=150;
Clusterer.defaultGridSize=5;
Clusterer.defaultMinMarkersPerCluster=5;
Clusterer.defaultMaxLinesPerInfoBox=10;
Clusterer.defaultIcon=new GIcon();
//Clusterer.defaultIcon.image='http://www.acme.com/resources/images/markers/blue_large.PNG';
//Clusterer.defaultIcon.shadow='http://www.acme.com/resources/images/markers/shadow_large.PNG';
Clusterer.defaultIcon.image='http://maps.google.com/mapfiles/ms/icons/purple-dot.png';
Clusterer.defaultIcon.shadow='http://maps.google.com/mapfiles/shadow50.png';
Clusterer.defaultIcon.iconSize=new GSize(40,40);
Clusterer.defaultIcon.shadowSize=new GSize(60,40);
Clusterer.defaultIcon.iconAnchor=new GPoint(20,40);
Clusterer.defaultIcon.infoWindowAnchor=new GPoint(20,3);
Clusterer.defaultIcon.infoShadowAnchor=new GPoint(0,40);

Clusterer.prototype.SetIcon=function(icon)
{this.icon=icon;};

Clusterer.prototype.SetMaxVisibleMarkers=function(n)
{this.maxVisibleMarkers=n;};

Clusterer.prototype.SetMinMarkersPerCluster=function(n)
{this.minMarkersPerCluster=n;};

Clusterer.prototype.SetMaxLinesPerInfoBox=function(n)
{this.maxLinesPerInfoBox=n;};

Clusterer.prototype.AddMarker=function(marker,title)
{
  if(marker.setMap!=null)
    marker.setMap(this.map);
  marker.title=title;
  marker.onMap=false;
  this.markers.push(marker);
  this.DisplayLater();
};

Clusterer.prototype.RemoveMarker=function(marker)
{
  for(var i=0; i<this.markers.length; ++i)
    if(this.markers[i]==marker)
    {
      if(marker.onMap)
        this.map.removeOverlay(marker);
      for(var j=0; j<this.clusters.length; ++j)
      {
        var cluster=this.clusters[j];
        if(cluster!=null)
        {
          for(var k=0; k<cluster.markers.length; ++k)
            if(cluster.markers[k]==marker)
            {
	      cluster.markers[k]=null;
              --cluster.markerCount;
              break;
           }
          if(cluster.markerCount==0)
          {
	    this.ClearCluster(cluster);
            this.clusters[j]=null;
          }
          else if(cluster==this.poppedUpCluster)
            Clusterer.RePop(this);
        }
      }
      this.markers[i]=null;
      break;
    }
  this.DisplayLater();
};

Clusterer.prototype.DisplayLater=function()
{
  if(this.timeout!=null)
    clearTimeout(this.timeout);
  this.timeout=setTimeout(Clusterer.MakeCaller(Clusterer.Display,this),50);
};

Clusterer.Display=function(clusterer)
{
  var i,j,marker,cluster;
  clearTimeout(clusterer.timeout);
  var newZoomLevel=clusterer.map.getZoom();
  if(newZoomLevel!=clusterer.currentZoomLevel)
  {
    for(i=0; i<clusterer.clusters.length; ++i)
      if(clusterer.clusters[i]!=null)
      {
        clusterer.ClearCluster(clusterer.clusters[i]);
        clusterer.clusters[i]=null;
      }
    clusterer.clusters.length=0;
    clusterer.currentZoomLevel=newZoomLevel;
  }
  var bounds=clusterer.map.getBounds();
  var sw=bounds.getSouthWest();
  var ne=bounds.getNorthEast();
  var dx=ne.lng()-sw.lng();
  var dy=ne.lat()-sw.lat();
  if(dx<300&&dy<150)
  {
    dx*=0.10;
    dy*=0.10;
    bounds=new GLatLngBounds(new GLatLng(sw.lat()-dy,sw.lng()-dx),new GLatLng(ne.lat()+dy,ne.lng()+dx));
  }
  var visibleMarkers=[];
  var nonvisibleMarkers=[];
  for(i=0; i<clusterer.markers.length; ++i)
  {
    marker=clusterer.markers[i];
    if(marker!=null)
      if(bounds.contains(marker.getPoint()))
        visibleMarkers.push(marker);
    else
      nonvisibleMarkers.push(marker);
  }
  for(i=0; i<nonvisibleMarkers.length; ++i)
  {
    marker=nonvisibleMarkers[i];
    if(marker.onMap)
    {
      clusterer.map.removeOverlay(marker);
      marker.onMap=false;
    }
  }
  for(i=0; i<clusterer.clusters.length; ++i)
  {
    cluster=clusterer.clusters[i];
    if(cluster!=null&&!bounds.contains(cluster.marker.getPoint())&&cluster.onMap)
    {
      clusterer.map.removeOverlay(cluster.marker);
      cluster.onMap=false;
    }
  }
  if(visibleMarkers.length>clusterer.maxVisibleMarkers)
  {
    var latRange=bounds.getNorthEast().lat()-bounds.getSouthWest().lat();
    var latInc=latRange/clusterer.gridSize;
    var lngInc=latInc/Math.cos((bounds.getNorthEast().lat()+bounds.getSouthWest().lat())/2.0*Math.PI/180.0);
    for(var lat=bounds.getSouthWest().lat(); lat<=bounds.getNorthEast().lat(); lat+=latInc)
      for(var lng=bounds.getSouthWest().lng(); lng<=bounds.getNorthEast().lng(); lng+=lngInc)
      {
        cluster=new Object();
        cluster.clusterer=clusterer;
        cluster.bounds=new GLatLngBounds(new GLatLng(lat,lng),new GLatLng(lat+latInc,lng+lngInc));
        cluster.markers=[];
        cluster.markerCount=0;
        cluster.onMap=false;
        cluster.marker=null;
        clusterer.clusters.push(cluster);
      }
    for(i=0; i<visibleMarkers.length; ++i)
    {
      marker=visibleMarkers[i];
      if(marker!=null&&!marker.inCluster)
      {
        for(j=0; j<clusterer.clusters.length; ++j)
        {
          cluster=clusterer.clusters[j];
          if(cluster!=null&&cluster.bounds.contains(marker.getPoint()))
          {
            cluster.markers.push(marker);
            ++cluster.markerCount;
            marker.inCluster=true;
          }
        }
      }
    }
    for(i=0; i<clusterer.clusters.length; ++i)
      if(clusterer.clusters[i]!=null&&clusterer.clusters[i].markerCount<clusterer.minMarkersPerCluster)
      {
        clusterer.ClearCluster(clusterer.clusters[i]);
        clusterer.clusters[i]=null;
      }
    for(i=clusterer.clusters.length-1; i>=0; --i)
      if(clusterer.clusters[i]!=null)
        break;
      else
        --clusterer.clusters.length;
    for(i=0; i<clusterer.clusters.length; ++i)
    {
      cluster=clusterer.clusters[i];
      if(cluster!=null)
      {
        for(j=0; j<cluster.markers.length; ++j)
        {
          marker=cluster.markers[j];
          if(marker!=null&&marker.onMap)
          {
            clusterer.map.removeOverlay(marker);
            marker.onMap=false;
          }
        }
      }
    }
    for(i=0; i<clusterer.clusters.length; ++i)
    {
      cluster=clusterer.clusters[i];
      if(cluster!=null&&cluster.marker==null)
      {
        var xTotal=0.0,yTotal=0.0;
        for(j=0; j<cluster.markers.length; ++j)
        {
          marker=cluster.markers[j];
          if(marker!=null)
          {
            xTotal+=(+marker.getPoint().lng());
            yTotal+=(+marker.getPoint().lat());
          }
        }
        var location=new GLatLng(yTotal/cluster.markerCount,xTotal/cluster.markerCount);
         var info = "This cluster represents " + cluster.markers.length + " sites; click on the purple pin to zoom.";
 
       //marker=new GMarker(location,{icon:clusterer.icon});
        marker=new GMarker(location,{icon:clusterer.icon, title:info});

        cluster.marker=marker;

        //cluster marker event listeners
        GEvent.addListener(marker, "click", function(latlng) {
          map.setCenter(latlng);
          map.zoomIn();
        });    
        //GEvent.addListener(marker, 'click', Clusterer.MakeCaller( Clusterer.PopUp, cluster ));
        //GEvent.addListener( marker, 'mouseover', Clusterer.MakeCaller(Clusterer.PopUp,cluster) );
        //GEvent.addListener(marker,'mousemove',Clusterer.MakeCaller(Clusterer.PopDown,cluster));
      }
    }
  }
  for(i=0; i<visibleMarkers.length; ++i)
  {
    marker=visibleMarkers[i];
    if(marker!=null&&!marker.onMap&&!marker.inCluster)
    {
      clusterer.map.addOverlay(marker);
      if(marker.addedToMap!=null)
      marker.addedToMap();
      marker.onMap=true;
    }
  }
  for(i=0; i<clusterer.clusters.length; ++i)
  {
    cluster=clusterer.clusters[i];
    if(cluster!=null&&!cluster.onMap&&bounds.contains(cluster.marker.getPoint()))
    {
      clusterer.map.addOverlay(cluster.marker);
      cluster.onMap=true;
    }
  }
  Clusterer.RePop(clusterer);
};

Clusterer.PopUp=function(cluster)
{
  var clusterer=cluster.clusterer;
  /*var html='<table width="300">';
  var n=0;
  for(var i=0; i<cluster.markers.length; ++i)
  {
    var marker=cluster.markers[i];
    if(marker!=null)
    {
      ++n;
      html+='<tr><td>';
      if(marker.getIcon().smallImage!=null)
        html+='<img src="'+marker.getIcon().smallImage+'">';
      else
        html+='<img src="'+marker.getIcon().image+'" width="'+(marker.getIcon().iconSize.width/2)+'" height="'+(marker.getIcon().iconSize.height/2)+'">';
      html+='</td><td>'+marker.title+'</td></tr>';
      if(n==clusterer.maxLinesPerInfoBox-1&&cluster.markerCount>clusterer.maxLinesPerInfoBox)
      {
        html+='<tr><td colspan="2">...and '+(cluster.markerCount-n)+' more</td></tr>';
        break;
      }
    }
  }
  html+='</table>';
  */
  var html = '<br>This cluster represents ' + cluster.markers.length + ' sites; click on the pin to zoom.';
  clusterer.map.closeInfoWindow();
  cluster.marker.openInfoWindowHtml(html);
  clusterer.poppedUpCluster=cluster;
};

Clusterer.RePop=function(clusterer)
{
  if(clusterer.poppedUpCluster!=null)
  Clusterer.PopUp(clusterer.poppedUpCluster);
};

Clusterer.PopDown=function(clusterer)
{clusterer.poppedUpCluster=null;};

Clusterer.prototype.ClearCluster=function(cluster)
{
  var i,marker;
  for(i=0; i<cluster.markers.length; ++i)
    if(cluster.markers[i]!=null)
    {
      cluster.markers[i].inCluster=false;
      cluster.markers[i]=null;
    }
  cluster.markers.length=0;
  cluster.markerCount=0;
  if(cluster==this.poppedUpCluster)
    this.map.closeInfoWindow();
  if(cluster.onMap)
  {
    this.map.removeOverlay(cluster.marker);
    cluster.onMap=false;
  }
};

Clusterer.MakeCaller=function(func,arg)
{
  return function(){func(arg);};};

GMarker.prototype.setMap=function(map)
{this.map=map;};

GMarker.prototype.addedToMap=function()
{this.map=null;};

GMarker.prototype.origOpenInfoWindow=GMarker.prototype.openInfoWindow;

GMarker.prototype.openInfoWindow=function(node,opts)
{
  if(this.map!=null)
    return this.map.openInfoWindow(this.getPoint(),node,opts);
  else
    return this.origOpenInfoWindow(node,opts);
};

GMarker.prototype.origOpenInfoWindowHtml=GMarker.prototype.openInfoWindowHtml;

GMarker.prototype.openInfoWindowHtml=function(html,opts)
{
  if(this.map!=null)
    return this.map.openInfoWindowHtml(this.getPoint(),html,opts);
  else
    return this.origOpenInfoWindowHtml(html,opts);
};

GMarker.prototype.origOpenInfoWindowTabs=GMarker.prototype.openInfoWindowTabs;

GMarker.prototype.openInfoWindowTabs=function(tabNodes,opts)
{
  if(this.map!=null)
    return this.map.openInfoWindowTabs(this.getPoint(),tabNodes,opts);
  else
    return this.origOpenInfoWindowTabs(tabNodes,opts);
};

GMarker.prototype.origOpenInfoWindowTabsHtml=GMarker.prototype.openInfoWindowTabsHtml;

GMarker.prototype.openInfoWindowTabsHtml=function(tabHtmls,opts)
{
  if(this.map!=null)
    return this.map.openInfoWindowTabsHtml(this.getPoint(),tabHtmls,opts);
  else
    return this.origOpenInfoWindowTabsHtml(tabHtmls,opts);
};

GMarker.prototype.origShowMapBlowup=GMarker.prototype.showMapBlowup;

GMarker.prototype.showMapBlowup=function(opts)
{
  if(this.map!=null)
    return this.map.showMapBlowup(this.getPoint(),opts);
  else
    return this.origShowMapBlowup(opts);
};
