// 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.changed = false;
	this.markers = new Object();
	this.clusters = new Object();
	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.Move, this ) );
	GEvent.addListener( map, 'moveend',
		Clusterer.MakeCaller( Clusterer.Move, this ) );
	GEvent.addListener( map, 'infowindowclose',
		Clusterer.MakeCaller( Clusterer.PopDown, this ) );
};


Clusterer.defaultMaxVisibleMarkers = 70;
Clusterer.defaultGridSize = 6;
Clusterer.defaultMinMarkersPerCluster = 5;
Clusterer.defaultMaxLinesPerInfoBox = 10;

Clusterer.defaultIcon = new GIcon();
Clusterer.defaultIcon.image = '/images/smallglass.png';
Clusterer.defaultIcon.iconSize =	new GSize( 59, 57 );
Clusterer.defaultIcon.iconAnchor =	new GPoint( 21, 0 );
Clusterer.defaultIcon.infoWindowAnchor = new GPoint( 13, 3 );

Clusterer.imageMap = new Array(21,0,59,0,59,10,21,10);

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


//
// Fetch new unique clusterID
//
Clusterer.prototype.GetNewID = function()
{
	if (!this.lastID)
		this.lastID =0;
	return this.lastID++;
}

// 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.AddMarkerNoRedraw = function(marker,title)
{
	if (marker.setMap != null)
		marker.setMap(this.map);

	marker.title = title;
	marker.onMap = false;
	this.changed = true;
	this.markers[marker.name] = marker;
};

// Call this to add a marker.
Clusterer.prototype.AddMarker = function(marker,title)
{
	this.AddMarkerNoRedraw(marker,title);
	this.DisplayLater();
};


// Call this to remove a marker.
Clusterer.prototype.RemoveMarker = function (marker)
{
	this.RemoveMarkerNoRedraw(marker);
	this.DisplayLater();
};

// Call this to remove a marker.
Clusterer.prototype.RemoveMarkerNoRedraw = function ( marker )
{
	if (this.markers[marker.name] == marker) {
		if (marker.onMap)
			this.map.removeOverlay(marker);
	}
	this.changed = true;
	delete this.markers[marker.name];
};


Clusterer.prototype.DisplayLater = function () {
	if (this.omSet)
		this.omSet('<h2>Plotting properties</h2>Please be patient...');
	if (this.timeout != null)
		clearTimeout( this.timeout );
	this.timeout = setTimeout( Clusterer.MakeCaller( Clusterer.Display, this ), 50 );
};

Clusterer.prototype.ReDisplay = function() {
	if (this.omSet)
		this.omSet('<h2>Plotting properties</h2>Please be patient...');
	if (this.timeout!=null)
		clearTimeout(this.timeout);
	this.timeout = setTimeout(Clusterer.MakeCaller(Clusterer.Display,this ),50);
};

//
//
//
Clusterer.Move = function( clusterer ) {

	// Get the current bounds of the visible area.
	var map = clusterer.map;

        //
        // If we are in range then display and quit.
        //
	var C = map.getCenter();
        if (!cluster.allowedBounds.contains(C)) {
                // It`s not OK, so find the nearest allowed point and move there
                var X = C.lng();
                var Y = C.lat();

                var AmaxX = allowedBounds.getNorthEast().lng();
                var AmaxY = allowedBounds.getNorthEast().lat();
                var AminX = allowedBounds.getSouthWest().lng();
                var AminY = allowedBounds.getSouthWest().lat();

                if (X < AminX)
                        X = AminX;
                if (X > AmaxX)
                        X = AmaxX;
                if (Y < AminY)
                        Y = AminY;
                if (Y > AmaxY)
                        Y = AmaxY;
		clusterer.changed=true;
                map.setCenter(new GLatLng(Y,X));
        }

	// Expand the bounds a little, so things look smoother when scrolling
	// by small amounts.
	var bounds = map.getBounds();
	var sw = bounds.getSouthWest();
	var ne = bounds.getNorthEast();

	//
	// When the zoom level changes, we have to remove all the clusters.
	//
	var newZoomLevel = map.getZoom();
	if (clusterer.currentZoomLevel != newZoomLevel) {
		clusterer.currentZoomLevel = newZoomLevel;
	}
	else {
		if (!clusterer.changed && clusterer.expandedBounds) {
			if (clusterer.expandedBounds.contains(sw) &&
				clusterer.expandedBounds.contains(ne))
				return;
		}
	}

	clusterer.changed = true;

	//
	// Calculate expanded bounds...
	//
	Clusterer.SetExpandedBounds(clusterer,sw,ne);  

	//
	// Call the callback function to draw properties
	//
	clusterer.drawProperties();
}

Clusterer.SetExpandedBounds = function( clust,sw,ne ) {

	var nelat = ne.lat();
	var nelng = ne.lng();

	var swlat = sw.lat();
	var swlng = sw.lng();

	var dx = nelng - swlng;
	var dy = nelat - swlat;
	dx *= 0.20;
	dy *= 0.20;

	clust.expandedBounds = new GLatLngBounds(
		new GLatLng( swlat - dy, swlng - dx ),
		new GLatLng( nelat + dy, nelng + dx ) );
}


Clusterer.Display = function ( clusterer ) {

	clearTimeout(clusterer.timeout);


	var map = clusterer.map;
	//
	// Expand the bounds a little, so things look smoother when scrolling
	// by small amounts.
	var bounds = map.getBounds();
	var sw = bounds.getSouthWest();
	var ne = bounds.getNorthEast();

	if (!clusterer.expandedBounds)
		Clusterer.SetExpandedBounds(clusterer,sw,ne);

	var i, j, marker, cluster;

	if (!clusterer.changed)
		return;

	clusterer.changed = false;

	var nelat = ne.lat();
	var nelng = ne.lng();

	var swlat = sw.lat();
	var swlng = sw.lng();

	var dx = nelng - swlng;
	var dy = nelat - swlat;

	bounds = clusterer.expandedBounds;

	//
	// Add cluster markers
	//
	for (i in clusterer.clusters) {
		var cluster = clusterer.clusters[i];
		if (cluster == this.poppedUpCluster)
			clusterer.omClear();
		if (cluster.onMap)
			map.removeOverlay(cluster.marker);
	}
	clusterer.clusters = new Object();

	var markerCount = 0;
	for (i in clusterer.markers) {
		markerCount++;
		var marker = clusterer.markers[i];
		if (marker.onMap) {
			map.removeOverlay(marker);
			marker.onMap = false;
		}
		delete marker.inCluster;
	}	

	//
	// 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 (markerCount > clusterer.maxVisibleMarkers) {
		// Add to the list of clusters by splitting up the current bounds
		// into a grid.
		var latRange = nelat - swlat;
		var latInc = latRange / clusterer.gridSize;
		var lngInc = latInc / Math.cos( ( nelat + swlat ) / 2.0 * Math.PI / 180.0 );
		var lngX = 0;
		for (var lng=swlng;lng<=nelng;lng+=lngInc) {
			for (var col=0;col<clusterer.gridSize;col++) {
				var lat = swlat + (col * latInc);
				cluster = new Object();
				cluster.clusterer = clusterer;

				var tl = new GLatLng( lat, lng );
				var br = new GLatLng( lat+latInc, lng+lngInc );
				cluster.bounds = new GLatLngBounds( tl,br );
				cluster.markers = new Object();
				cluster.markerCount = 0;
				cluster.onMap = false;
				cluster.marker = null;
				//
				// This for debug
				//
				var foo = (lngX * clusterer.gridSize) + col;
				cluster.ID = foo;
				clusterer.clusters[foo] = cluster;
	
				//
				// These are used to calc average position
				//
				cluster.totLng = 0.0;
				cluster.totLat = 0.0;
			}
			lngX++;
		}

		//
		// Put all the unclustered visible markers into a cluster - the first
		// one it fits in, which favors pre-existing clusters.
		//
		var haveAlerted = 0;
		for (i in clusterer.markers) {
			marker = clusterer.markers[i];
			if (!marker.inCluster) {
				var p = marker.getPoint();
				var x = Math.floor((p.lat() - swlat) / latInc);
				if ((x >= 0) && (x<clusterer.gridSize)) {
				var y = Math.floor((p.lng() - swlng) / lngInc);
				if (y >= 0) {

				var index = (y * clusterer.gridSize) + x;

				var cluster = clusterer.clusters[index];
				if (cluster != null) {
					cluster.markers[marker.name]=marker;
					cluster.markerCount++;
					marker.inCluster = cluster;

					cluster.totLng += p.lng();
					cluster.totLat += p.lat();
				}
				}
				}
			}
		}

		// Get rid of any clusters containing only a few markers.
		for (i in clusterer.clusters)
			if (clusterer.clusters[i].markerCount < clusterer.minMarkersPerCluster) {
				clusterer.ClearCluster(clusterer.clusters[i]);
				delete clusterer.clusters[i];
			}

		//
		// 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 in clusterer.clusters) {
			cluster = clusterer.clusters[i];
			for (j in cluster.markers) {
				marker = cluster.markers[j];
				if (marker.onMap) {
					map.removeOverlay(marker);
					marker.onMap = false;
				}
			}
		}

		// Now make cluster-markers for any clusters that need one.
		for (i in clusterer.clusters) {
			cluster = clusterer.clusters[i];
			if (cluster.marker == null) {
				// Figure out the average coordinates of
				// the markers in this
				// cluster.
				var xTotal = cluster.totLng;
				var yTotal = cluster.totLat;
				//for (j in cluster.markers) {
				//	marker = cluster.markers[j];
				//	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 ) );
				GEvent.addListener( marker, 'dblclick', Clusterer.MakeCaller( Clusterer.ZoomIn, cluster ) );
			}
		}
	}

	// Display the visible markers not already up and not in clusters.
	for (i in clusterer.markers) {
		marker = clusterer.markers[i];
		if (!marker.onMap && !marker.inCluster) {
			map.addOverlay(marker);
			if (marker.addedToMap != null)
				marker.addedToMap();
			marker.onMap = true;
		}
	}

	// Display the visible clusters not already up.
	for (i in clusterer.clusters) {
		cluster = clusterer.clusters[i];
		if (!cluster.onMap) {
			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);

	if (clusterer.omClear)
		clusterer.omClear();

};

Clusterer.prototype.SetBusyOverlay=function(omSet,omClear) {
	this.omSet = omSet;
	this.omClear = omClear;
}

Clusterer.ZoomIn = function(cluster)
{
	var clusterer = cluster.clusterer;
	clusterer.map.setCenter(cluster.marker.getPoint(),clusterer.currentZoomLevel + 1);
}
	
Clusterer.PopUp = function(cluster)
{
	var clusterer = cluster.clusterer;
	var html = "<table>\n" +
		"<tr><td>\n" +
		"<table class=\"empty\" width=\"100%\"><tr><td><h2>" +
                "Zoom for details" +
                "</h2></td>" +
                "<td align=\"right\"><a href=\"javascript:clearOverlayPane();\"><img src=\"/images/closeWindow.gif\"" +
                " height=\"20\" align=\"right\" width=\"20\" /></a> "  +
                "</td></tr></table>" +
		"</td></tr>\n" +
		"<tr><td>";

	var schools = 0;
	var properties = 0;
	var stations = 0;
	var supermarkets = 0;
	for (var i in cluster.markers) {
		var type = cluster.markers[i].type;
		if (type == "School")
			schools++;
		else if (type == "Property")
			properties++;
		else if (type == "Station")
			stations++;
		else if (type == "Supermarket")
			supermarkets++;
	}
	if (supermarkets)
		html += supermarkets + " supermarkets<br />";
	if (stations)
		html += stations + " stations<br />";
	if (schools)
		html += schools + " schools<br />";
	if (properties)
		html += properties + " properties<br />";
	var point = cluster.marker.getPoint();
	html += "<a href=\"javascript: clearOverlayPane();map.setCenter(new GLatLng(" +
		point.lat() + "," +
		point.lng() + ")," + (clusterer.currentZoomLevel + 1) +
		");\">Zoom</a> - or double click marker";

	html += "</td></tr></table>";

	clusterer.omSet(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 )
{
	for (var i in cluster.markers) {
		delete cluster.markers[i].inCluster;
		delete cluster.markers[i];
	}
	cluster.markerCount = 0;
	if (cluster == this.poppedUpCluster)
		clusterer.omClear();
	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);
};

