// 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 = 150;
Clusterer.defaultGridSize = 5;
Clusterer.defaultMinMarkersPerCluster = 5;
Clusterer.defaultMaxLinesPerInfoBox = 10;

Clusterer.defaultIcon = new GIcon();
Clusterer.defaultIcon.image = '/images/map-multiple.png';
Clusterer.defaultIcon.shadow = '/images/map-multiple-shadow.png';
Clusterer.defaultIcon.iconSize = new GSize(35, 28);
Clusterer.defaultIcon.shadowSize = new GSize(48, 30);
Clusterer.defaultIcon.iconAnchor = new GPoint(10, 0);
Clusterer.defaultIcon.infoWindowAnchor = new GPoint(13, 3);
Clusterer.defaultIcon.infoShadowAnchor = new GPoint(11, 34);


// 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, url_title)
{
	if (marker.setMap != null)
		marker.setMap(this.map);
	
	marker.title = title;
	marker.url_title = url_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 (var 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 (var 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 (var 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 (var 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 (var i=0; i<visibleMarkers.length; ++i)
		{
			marker = visibleMarkers[i];
			if (marker != null && ! marker.inCluster)
			{
				for (var 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 (var 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 (var 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 (var i=0; i<clusterer.clusters.length; ++i)
		{
			cluster = clusterer.clusters[i];
			if (cluster != null)
			{
				for (var 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 (var 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 (var 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 (var 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 (var 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)
{
	var clusterer = cluster.clusterer;
	var html = '<table width="250">';
	var n = 0;
	for (var i=0; i<cluster.markers.length; ++i)
	{
		var marker = cluster.markers[i];
		if (marker != null)
		{
			++n;
			html += '<tr><td style="width:20px;">';
			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><a href="http://www.nationalfamilyweek.co.uk/events/' + marker.url_title + '/">' + marker.title + '</a></td></tr>';
			if (n == clusterer.maxLinesPerInfoBox - 1 && cluster.markerCount > clusterer.maxLinesPerInfoBox )
			{
				html += '<tr><td colspan="2" style="padding-top:10px;">...and ' + (cluster.markerCount - n) + ' more (<a href="#" onclick="zoomIn();">zoom in</a> for more detail)</td></tr>';
				break;
			}
		}
	}
	html += '</table><br />';
	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 (var 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;
	}
};

// Call this to remove all markers.
// Added by LDB
Clusterer.prototype.RemoveAllMarkers = function()
{
	for (var i=0; i<this.markers.length; ++i)
		if (this.markers[i] != null && this.markers[i].onMap)
			this.map.removeOverlay(this.markers[i]);
	
	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)
			{
				cluster.markers[k] = null;
				--cluster.markerCount;
			}
			if (cluster.markerCount == 0)
			{
				this.ClearCluster(cluster);
				this.clusters[j] = null;
			}
			else if (cluster == this.poppedUpCluster)
				Clusterer.RePop(this);
		}
	}
	this.markers[i] = null;

	this.DisplayLater();
};

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