// 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.

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 = 50;

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.iconSize = new GSize( 30, 51 );

Clusterer.defaultIcon.shadowSize = new GSize( 56, 51 );

Clusterer.defaultIcon.iconAnchor = new GPoint( 13, 34 );

Clusterer.defaultIcon.infoWindowAnchor = new GPoint( 13, 3 );

Clusterer.defaultIcon.infoShadowAnchor = new GPoint( 27, 37 );

// Call this to change the cluster icon.

Clusterer.prototype.SetIcon = function ( icon )

    {

    this.icon = icon;

    };

// 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 )

    {

    if ( marker.setMap != null )

	marker.setMap( this.map );

    marker.title = title;

    marker.onMap = false;

    this.markers.push( marker );

    this.DisplayLater();

    };

// Call this to remove a marker.

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 )

	{

	// When the zoom level changes, we have to remove all the clusters.

	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;

	}

    // Get the current bounds of the visible area.

    var bounds = clusterer.map.getBounds();

    // 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 GLatLngBounds(

	  new GLatLng( sw.lat() - dy, sw.lng() - dx ),

	  new GLatLng( 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.getPoint() ) )

		visibleMarkers.push( marker );

	    else

		nonvisibleMarkers.push( marker );

	}

    // Take down the non-visible markers.

    for ( i = 0; i < nonvisibleMarkers.length; ++i )

	{

	marker = nonvisibleMarkers[i];

	if ( marker.onMap )

	    {

	    clusterer.map.removeOverlay( marker );

	    marker.onMap = false;

	    }

	}

    // 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.getPoint() ) && cluster.onMap )

	    {

	    clusterer.map.removeOverlay( cluster.marker );

	    cluster.onMap = false;

	    }

	}

    // 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.

    if ( visibleMarkers.length > clusterer.maxVisibleMarkers )

	{

	// Add to the list of clusters by splitting up the current bounds

	// into a grid.

	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 );

		}

	// 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];

	    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;

			}

		    }

		}

	    }

	// Get rid of any clusters containing only a few markers.

	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;

		}

	// Shrink the clusters list.

	for ( i = clusterer.clusters.length - 1; i >= 0; --i )

	    if ( clusterer.clusters[i] != null )

		break;

	    else

		--clusterer.clusters.length;

	// 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.onMap )

			{

			clusterer.map.removeOverlay( marker );

			marker.onMap = false;

			}

		    }

		}

	    }

	// Now make cluster-markers for any clusters that need one.

	for ( i = 0; i < clusterer.clusters.length; ++i )

	    {

	    cluster = clusterer.clusters[i];

	    if ( cluster != null && cluster.marker == null )

		{

		// Figure out the average coordinates of the markers in this

		// cluster.

		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 );

		marker = new GMarker( location, { icon: clusterer.icon } );

		cluster.marker = marker;

		GEvent.addListener( marker, 'click', Clusterer.MakeCaller( Clusterer.PopUp, cluster ) );

		}

	    }

	}

    // Display the visible markers not already up and not in clusters.

    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;

	    }

	}

    // Display the visible clusters not already up.

    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;

	    }

	}

    // In case a cluster is currently popped-up, re-pop to get any new

    // markers into the infobox.

    Clusterer.RePop( clusterer );

    };

Clusterer.PopUp = function ( cluster )

    {

    	

    // marker list layout changed, made scrollable to display all dealers and clickable, hknauer

    

    var clusterer = cluster.clusterer;

    var container = document.createElement('div');

    container.style.overflow = 'auto';

    container.style.height = '180px';

    container.style.width= '250px'; 

    container.style.marginTop ='15px';

    

    var marker;

    for ( var i = 0; i < cluster.markers.length; i++ )

	{

	if ( cluster.markers[i] != null )

	    {

	    marker = cluster.markers[i];

	    

	    var markerNode = document.createElement('a');

	    var markerNodeAttrib = document.createAttribute('href');

      	markerNodeAttrib.nodeValue = 'javascript:void(0);';

      	markerNode.setAttributeNode(markerNodeAttrib);

      	

	    markerNode.appendChild(document.createTextNode(marker.getTitle()));

	    markerNode.appendChild(document.createElement('br'));

      	container.appendChild(markerNode);

      	markerNode.marker = marker;

      	

      	GEvent.addDomListener(markerNode, 'click', function() {

	        map.setCenter(this.marker.getLatLng() ,10);

        	GEvent.trigger(this.marker, 'click');

      	});

	   

	   }

	   

	}

    

    clusterer.map.closeInfoWindow();

    cluster.marker.openInfoWindow( container );

    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;

	}

    };

// This returns a function closure that calls the given routine with the

// specified arg.

Clusterer.MakeCaller = function ( func, arg )

    {

    return function () { func( arg ); };

    };

// Augment GMarker so it handles markers that have been created but

// not yet addOverlayed.

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 );

    };