// Clusterer.js - marker clustering routines for Google Maps apps
//
// Using these routines is very easy.
//
// 1) Load the routines into your code:
//
//        <script src="http://www.acme.com/javascript/Clusterer.js" type="text/javascript"></script>
//
// 2) Create a Clusterer object, passing it your map object:
//
//        var clusterer = new Clusterer( map );
//
// 3) Wherever you now do map.addOverlay( marker ), instead call
//    clusterer.AddMarker( marker, title ).  The title is just a
//    short descriptive string to use in the cluster info-boxes.
//
// 4) If you are doing any map.removeOverlay( marker ) calls, change those
//    to clusterer.RemoveMarker( marker ).
//
// That's it!  Everything else happens automatically.
//
//
// The current version of this code is always available at:
// http://www.acme.com/javascript/
//
//
// Copyright � 2005,2006 by Jef Poskanzer <jef@mail.acme.com>.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// For commentary on this license please see http://www.acme.com/license.html

/**
	Constructor
	
	@param map	Map to render on
	@param prePanFunction	optopnal - 'pan' or 'drag' function to call
	@param preZoomFunction	optopnal - 'zoom' function to call
 */
Clusterer = function( map, prePanFunction, preZoomFunction, postLoadFunction )
{
    this.map = map;
    
    this.markers = [];
    this.visibleMarkerCount = 0;
    this.clusters = [];
    this.timeout = null;
    this.currentZoomLevel = map.getZoom();
    this.currrInfoWindow = null;
    this.maxVisibleMarkers = Clusterer.defaultMaxVisibleMarkers;
    this.gridSize = Clusterer.defaultGridSize;
    this.minMarkersPerCluster = Clusterer.defaultMinMarkersPerCluster;
    this.maxLinesPerInfoBox = Clusterer.defaultMaxLinesPerInfoBox;
	map.loadedFully = false;

	this.bounds = null;
	this.prePanFunction = prePanFunction;
	this.preZoomFunction = preZoomFunction;
	this.postLoadFunction = postLoadFunction;
	
	// NOTE: This must be added before the zoom/drag - otherwise, the bounds not being defined issue will still be present (until the map is reloaded)
	// handle known Google Maps issue where the first time in the bounds of the map is not preloaded (see discussion at http://code.google.com/p/gmaps-api-issues/issues/detail?id=1692#makechanges)
	google.maps.event.addListener(map, 'tilesloaded', function() {
	  // Called on initial map load.
	  var bounds = map.getBounds();
	  map.loadedFully = true;
  	 google.maps.event.clearListeners(map, 'tilesloaded');
	});
	
    google.maps.event.addListener( map, 'zoom_changed', Clusterer.MakeCaller( Clusterer.HandleZoom, this ) );
    google.maps.event.addListener( map, 'dragend', Clusterer.MakeCaller( Clusterer.HandlePan, this ) );

}

Clusterer.defaultMaxVisibleMarkers = 150;
Clusterer.defaultGridSize = 5;
Clusterer.defaultMinMarkersPerCluster = 5;
Clusterer.defaultMaxLinesPerInfoBox = 10;

Clusterer.prototype.handlePostLoad = function()
{
	if (this.postLoadFunction)
	{
		this.postLoadFunction(this);
	}
	
	this.postLoadFunction = null;
}

// display the map - call this after adding markers to the map
Clusterer.prototype.RaiseMap = function()
{
	//this.DisplayLater();
	Clusterer.Display(this);
}

// Call this to change the cluster icon.
Clusterer.prototype.SetIcon = function( icon )
{
    this.icon = icon;
}

Clusterer.prototype.getBounds = function( )
{
	if (this.bounds)
		return this.bounds;
	else
		return maps.getBounds();
}

// Changes the maximum number of visible markers before clustering kicks in.
Clusterer.prototype.SetMaxVisibleMarkers = function( n )
{
    this.maxVisibleMarkers = n;
}

// Sets the minumum number of markers for a cluster.
Clusterer.prototype.SetMinMarkersPerCluster = function( n )
{
    this.minMarkersPerCluster = n;
}

// Sets the maximum number of lines in an info box.
Clusterer.prototype.SetMaxLinesPerInfoBox = function( n )
{
    this.maxLinesPerInfoBox = n;
}

// Call this to add a marker.
Clusterer.prototype.AddMarker = function( marker, title, infoWindowContent )
{
	if (marker.clickListener)
	{
		google.maps.event.removeListener(marker.clickListener);
		marker.clickListener = null;
	}
    marker.title = title;
    marker.id = this.markers.length;
    marker.infoWindowContent = infoWindowContent;
    this.markers.push( marker );

    //this.DisplayLater();
}

// Call this to remove a marker.
Clusterer.prototype.RemoveMarker = function( marker )
{
	var thisMarkerIdx = [];
	var removeMarkerIds = [];
	var removeClusterIds = [];
	
    for ( var i = 0; i < this.markers.length; i++ )
    {
		if ( this.markers[i] == marker )
		{
			if (marker.currInfoWindow == this.currInfoWindow)
			{
				Clusterer.PopDown(clusterer);
			}
			marker.currInfoWindow = null;
			
			thisMarkerIdx.push(i);
			
		    if ( marker.getMap() )
				marker.setMap(null);
	
			if (marker.clickListener)
			{
				google.maps.event.removeListener(marker.clickListener);
				marker.clickListener = null;
			}

			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 )
						{
							removeMarkerIds.push(k);
							break;
						}
	
						if ( cluster.markers.length == 0 )
						{
							removeClusterIds.push(j);
							this.ClearCluster( cluster );
						}
						
					}

					// Cleanup
					if (removeMarkerIds)
					{
						for (var r = 0; r < removeMarkerIds.length; r++)
						{
							removeMarkerId = removeMarkerIds[r];
							cluster.markers.splice(removeMarkerId, 1);
						}
					}
					
					if (removeClusterIds)
					{
						for (var c = 0; c < removeClusterIds.length; c++)
						{
							clusterId = removeClusterIds[c];
							this.clusters.splice(clusterId, 1);
						}
					}
				}
		    }   
		}
    }

    // Perform actual cleanup outside of loops
    if (thisMarkerIdx.length)
    {
    	for (var i = 0; i < thisMarkerIdx.length; i++)
    	{
    		this.markers.splice(thisMarkerIdx[i], 1);
    	}
    }
    
    //this.DisplayLater();
}

// Call this to remove ALL markers.
Clusterer.prototype.RemoveAllMarkers = function() {
	var allMarkers = this.markers;
	
	for (var i = allMarkers.length - 1; i >= 0; i--)
	{
		this.RemoveMarker(allMarkers[i]);
	}
	
	this.markers.length = 0;
	this.clusters.length = 0;
	
	Clusterer.PopDown(this);
}

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


/**
	Handle change in zoom
*/
Clusterer.HandleZoom = function( clusterer )
{
	// Call any pre-processing code first
	if (clusterer.preZoomFunction) { clusterer.preZoomFunction(clusterer); }

	// When the zoom level changes, we have to remove all the clusters.
	if (clusterer.clusters)
	{
		for ( i = (clusterer.clusters.length - 1); i >= 0 ; i-- )
		{
			if ( clusterer.clusters[i] != null )
			{
				clusterer.ClearCluster( clusterer.clusters[i] );
				clusterer.clusters[i] = null;
			}
		}
	}
	
	clusterer.clusters.length = 0;

	Clusterer.Display( clusterer );
}

/**
	Handle change in pan
*/
Clusterer.HandlePan = function( clusterer )
{
	// Call any pre-processing code first
	if (clusterer.prePanFunction) { clusterer.prePanFunction(clusterer); }
	Clusterer.Display( clusterer );
}

/**
	Perform actual display of pins and clustering
*/
Clusterer.Display = function( clusterer )
{
    var i = 0, j = 0, marker = null, cluster = null;

    clearTimeout( clusterer.timeout );

    // Get the current bounds of the visible area.
    var bounds = clusterer.getBounds();

    if (!bounds)
    {
    	return;
    }
    
    // Expand the bounds a little, so things look smoother when scrolling
    // by small amounts.
    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 google.maps.LatLngBounds(
		  new google.maps.LatLng( sw.lat() - dy, sw.lng() - dx ),
		  new google.maps.LatLng( ne.lat() + dy, ne.lng() + dx ) );
	}
	
    // Partition the markers into visible and non-visible lists.
    var visibleMarkers = [];
    var nonvisibleMarkers = [];
    for ( i = 0; i < clusterer.markers.length; ++i )
	{
		marker = clusterer.markers[i];

		if ( marker != null )
		{
		    	if ( bounds.contains( marker.getPosition() ) )
			{
				visibleMarkers.push( marker );
			}
		    else
				nonvisibleMarkers.push( marker );
		}
	}//next cluster.markers
	
    // Take down the non-visible markers.
    for ( i = 0; i < nonvisibleMarkers.length; ++i )
	{
	marker = nonvisibleMarkers[i];
	if ( marker.getMap() )
	    {
	    marker.setMap(null);
	    }
	}
	
    // Take down the non-visible clusters.
    for ( i = 0; i < clusterer.clusters.length; ++i )
	{
	cluster = clusterer.clusters[i];
	if ( cluster != null && ! bounds.contains( cluster.marker.getPosition() ) && cluster.marker.getMap() )
	    {
	    cluster.marker.setMap(null);
	    }
	}
//maxVisibleMarkers
    // Clustering!  This is some complicated stuff.  We have three goals
    // here.  One, limit the number of markers & clusters displayed, so the
    // maps code doesn't slow to a crawl.  Two, when possible keep existing
    // clusters instead of replacing them with new ones, so that the app pans
    // better.  And three, of course, be CPU and memory efficient.
	// Add to the list of clusters by splitting up the current bounds
	// into a grid.
	if ( visibleMarkers.length > clusterer.maxVisibleMarkers )
	{
		var latRange = bounds.getNorthEast().lat() - bounds.getSouthWest().lat();
		var lngRange = bounds.getNorthEast().lng() - bounds.getSouthWest().lng();
		var latInc = latRange / clusterer.gridSize;
		var lngInc = latInc / Math.cos( ( bounds.getNorthEast().lat() + bounds.getSouthWest().lat() ) / 2.0 * Math.PI / 180.0 );
		var clusterId = 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.id = clusterId++;
				cluster.bounds = new google.maps.LatLngBounds( new google.maps.LatLng( lat, lng ), new google.maps.LatLng( lat + latInc, lng + lngInc ) );
				cluster.markers = [];
				cluster.marker = null;
				cluster.onMap = false;
				clusterer.clusters.push( cluster );
			}
	
		// Put all the unclustered visible markers into a cluster - the first
		// one it fits in, which favors pre-existing clusters.
		for ( i = 0; i < visibleMarkers.length; ++i )
		{
		    marker = visibleMarkers[i];
		    for ( j = 0; j < clusterer.clusters.length; ++j )
		    {
			    cluster = clusterer.clusters[j];
			    if ( cluster != null && 
				 cluster.bounds.contains( marker.getPosition() ) && 
				 !marker.cluster )
				{
					cluster.markers.push( marker );
					marker.cluster = cluster;
				}
		    }
		}
	
		removeIds = [];
	
		// Get rid of any clusters less than the 'minMarkersPerCluster' value defined
		for ( i = 0; i < clusterer.clusters.length; ++i )
		    if ( clusterer.clusters[i] != null && clusterer.clusters[i].markers.length < clusterer.minMarkersPerCluster )
			{
				removeIds.push(i);
			}
	
		for (ri = removeIds.length - 1; ri >= 0 ; ri--)
		{
			var removeId = removeIds[ri];
			cluster = clusterer.clusters[removeId];
			clusterer.ClearCluster( cluster );
			clusterer.clusters.splice(removeId, 1);
		}
	
		// Ok, we have our clusters.  Go through the markers in each
		// cluster and remove them from the map if they are currently up.
		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.getMap() )
					{
						if (marker.clickListener)
						{
							google.maps.event.removeListener(marker.clickListener);
							marker.clickListener = null;
						}
						marker.setMap(null);			
					}
			    }
			}
		}
	
		// Now make cluster-markers for any clusters that need one.
		for ( i = 0; i < clusterer.clusters.length; ++i )
		{
			cluster = clusterer.clusters[i];
			// Figure out the average coordinates of the markers in this
			// cluster.
			var xTotal = 0.0, yTotal = 0.0;
			var firstMarker = cluster.markers[0];
			cluster.marker = firstMarker;
			for ( j = 0; j < cluster.markers.length; ++j )
			{
				marker = cluster.markers[j];
	
				if ( marker )
				{
					xTotal += ( + marker.getPosition().lng() );
					yTotal += ( + marker.getPosition().lat() );
	
				}
			}
	
			if (firstMarker)
			{
				// determine where in the grid the marker should render
				var location = new google.maps.LatLng( yTotal / cluster.markers.length, xTotal / cluster.markers.length );
	
				var clickListener = google.maps.event.addListener( firstMarker, 'click', Clusterer.MakeCaller( Clusterer.PopUp, cluster ) );
				firstMarker.clickListener = clickListener;
				firstMarker.setMap(clusterer.map);
			}
		}
	}//end clustering	
    
	// Display the visible markers not already up and not in clusters.
	clusterer.visibleMarkerCount = visibleMarkers.length;
    	for ( i = 0; i < visibleMarkers.length; ++i )
	{
		marker = visibleMarkers[i];
		if ( marker && !marker.getMap() && !marker.cluster )
		{
	    	    Clusterer.SetupSingleMarkerEvent(clusterer, marker);
	    	    marker.setMap(clusterer.map);
	    	}
	}
    	
    	// done displaying the map and pins - anything else we need to do?
    	if (clusterer.postLoadFunction)
    	{
    		// have the post load function fire a second later due to some weird timing issues in the google api and custom marker icons
    		setTimeout("clusterer.handlePostLoad()", 1000);
    	}
}

// InfoWindow/Flyout functionality
Clusterer.SetupSingleMarkerEvent = function( clusterer, marker )
{
	var clickListener = marker.clickListener;
	if (clickListener != null)
	{
		// remove any existing 'click' listener on this marker
		google.maps.event.removeListener(clickListener);
		marker.clickListener = null;
	}
	
	google.maps.event.addListener(marker, "click", function() {
			if (clusterer.currInfoWindow)
			{
				clusterer.currInfoWindow.close();
				clusterer.currInfoWindow = null;
			}
			
	clusterer.currInfoWindow = new google.maps.InfoWindow({
		content: marker.infoWindowContent
	}); // end info window declare
	clusterer.currInfoWindow.open(marker.getMap(), marker);
	marker.currInfoWindow=clusterer.currInfoWindow;
	});
}

Clusterer.PopUp = function( cluster )
{
    var clusterer = cluster.clusterer;
    var html = '<table>';
    var n = 0;

    if (clusterer.currInfoWindow)
    {
    	    // take down any currently displayed info window
    	    clusterer.currInfoWindow.close();
    	    clusterer.currInfoWindow = null;
    }
    
	if (!cluster.markers.length) return;
	
    for ( var i = 0; i < cluster.markers.length; ++i )
	{
		var marker = cluster.markers[i];
		
		if ( marker && marker.infoWindowContent)
		{
		    ++n;
		    html += '<tr><td>';

		html += marker.infoWindowContent;

		    html += '</td></tr>';
		}
	}
	html += '</table>';

	Clusterer.PopDown( clusterer );
	
	clusterer.currInfoWindow = new google.maps.InfoWindow({
			content: html,
			position: cluster.marker.getPosition()
			});
	clusterer.currInfoWindow.open(cluster.marker.getMap());
	cluster.marker.currInfoWindow=clusterer.currInfoWindow;
}

Clusterer.PopDown = function( clusterer )
{
	if (clusterer.currInfoWindow)
	{
		clusterer.currInfoWindow.close();
	}
	clusterer.currInfoWindow = null;
}

Clusterer.prototype.ClearCluster = function( cluster )
{
    var i;

	if (!cluster.markers) return;

    for ( i = 0; i < cluster.markers.length; ++i )
	if ( cluster.markers[i] != null )
	{
	    cluster.markers[i].inCluster = false;
	    cluster.markers[i].cluster = null;
	    cluster.markers[i] = null;
	}
    cluster.markers.length = 0;
    if ( cluster.marker && cluster.marker.getMap() )
	{
		if (cluster.marker.clickListener)
		{
			google.maps.event.removeListener(cluster.marker.clickListener);
			cluster.marker.clickListener = null;
		}
		cluster.marker.setMap(null);
		cluster.marker.cluster = null;
	}
}

// This returns a function closure that calls the given routine with the
// specified arg.
Clusterer.MakeCaller = function( func, arg )
{
    return function() { func( arg ); };
}


// Augment google.maps.Marker so it handles markers that have been created but
// not yet addOverlayed.

/*
   google.maps.Marker.prototype.setMap = function( map )
    {
    this.map = map;
    };
*/
google.maps.Marker.prototype.addedToMap = function()
{
    this.map = null;
}

google.maps.Marker.prototype.origOpenInfoWindow = google.maps.Marker.prototype.openInfoWindow;
google.maps.Marker.prototype.openInfoWindow = function( node, opts )
{
    if ( this.map != null )
		return this.map.openInfoWindow( this.getPosition(), node, opts );
    else
		return this.origOpenInfoWindow( node, opts );
}

google.maps.Marker.prototype.origOpenInfoWindowHtml = google.maps.Marker.prototype.openInfoWindowHtml;
google.maps.Marker.prototype.openInfoWindowHtml = function( html, opts )
{
    if ( this.map != null )
		return this.map.openInfoWindowHtml( this.getPosition(), html, opts );
    else
		return this.origOpenInfoWindowHtml( html, opts );
}

google.maps.Marker.prototype.origOpenInfoWindowTabs = google.maps.Marker.prototype.openInfoWindowTabs;
google.maps.Marker.prototype.openInfoWindowTabs = function( tabNodes, opts )
{
    if ( this.map != null )
		return this.map.openInfoWindowTabs( this.getPosition(), tabNodes, opts );
    else
		return this.origOpenInfoWindowTabs( tabNodes, opts );
}

google.maps.Marker.prototype.origOpenInfoWindowTabsHtml = google.maps.Marker.prototype.openInfoWindowTabsHtml;
google.maps.Marker.prototype.openInfoWindowTabsHtml = function( tabHtmls, opts )
{
    if ( this.map != null )
		return this.map.openInfoWindowTabsHtml( this.getPosition(), tabHtmls, opts );
    else
		return this.origOpenInfoWindowTabsHtml( tabHtmls, opts );
}

google.maps.Marker.prototype.origShowMapBlowup = google.maps.Marker.prototype.showMapBlowup;
google.maps.Marker.prototype.showMapBlowup = function( opts )
{
    if ( this.map != null )
		return this.map.showMapBlowup( this.getPosition(), opts );
    else
		return this.origShowMapBlowup( opts );
}

