// 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 = '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 newicon = mo.gov.clone(clusterer.icon);
				newicon.image = newicon.image.replace(/chl=.*?&/i,'chl=' + cluster.markers.length + '&');
				
				var location = new GLatLng(yTotal / cluster.markerCount, xTotal / cluster.markerCount);
				marker = new GMarker(location, {icon : newicon	});
				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;
		}
	}
	var layerID = clusterer.layerID;
//	if (mo.gov.fbconsole) {
//		//mo.gov.GeoDataPageRepository.dataSets.dorLicenseOffices.mapLayer
//		mo.gov.fbconsole.dir(mo.gov.GeoDataPageRepository.dataSets['dorLicenseOffices'].mapLayer);
//		mo.gov.fbconsole.log(mo.gov.GeoDataPageRepository.dataSets['dorLicenseOffices'].mapLayer.sideBarFn);
//	}
	if(mo.gov.GeoDataPageRepository.dataSets[layerID].mapLayer.sideBarFn != null ) {
		mo.gov.GeoDataPageRepository.dataSets[layerID].mapLayer.sideBarFn.apply(visibleMarkers,[layerID]);
	}


	// 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="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><a href="#" onclick="GEvent.trigger('+ marker.path + ',\'click\');return false;">' + marker.title + '</a></td></tr>';
			html += '</td><td><a href="#" onclick="Clusterer.showMarker(' + marker.path + ');return false;">' + marker.title + '</a></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>';
	clusterer.map.closeInfoWindow();
	cluster.marker.openInfoWindowHtml(html);
	clusterer.poppedUpCluster = cluster;
};

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

Clusterer.showMarker = function(marker) {
	marker.show();
	mo.gov.GeoDataDisplay.geoDataMap.map.setCenter(marker.getLatLng(), 13);
	GEvent.trigger(marker,'click');
};

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

