﻿/*
	ClusterMarker Version 2
	
	A marker manager for the Google Maps API
	http://googlemapsapi.martinpearman.co.uk/clustermarker
	
	Copyright Martin Pearman 2009
	Last updated 17th April 2010 for www.auto-stop.cz

	This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

	You should have received a copy of the GNU General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.
	
*/

function ClusterMarker($map){
	var $this=this;
	$this.map=$map;
	$this.markers=[];
	$this.clusterMarkers=[];
	$this.clusteringEnabled=true;
	//	using the plain green icon for cluster marker icons
	$this.clusterMarkerIcon=new GIcon(G_DEFAULT_ICON, 'http://maps.gstatic.com/intl/en_ALL/mapfiles/marker_green.png');
	GEvent.bind($map, 'moveend', this, $this.moveEnd);
	GEvent.bind($map, 'zoomend', this, $this.zoomEnd);
	GEvent.bind($map, 'maptypechanged', $this, $this.mapTypeChanged);
}

//	add markers and initialise some custome properties for the markers
//	this method adds markers but does not delete/remove any existing markers
ClusterMarker.prototype.addMarkers=function($markers, $areOverlaid){
	var $this=this, $indexOffset=$this.markers.length, $length=$markers.length, $marker;
	if(typeof($areOverlaid)==='undefined'){
		$areOverlaid=false;
	}
	while($length--){
		$marker=$markers[$length];
		$marker._ClusterMarker_={id:$length+$indexOffset, isActive:true, doNotCluster:false, anchorPoint:[], intersectTable:[], isOverlaid:$areOverlaid, instance:$this};
	}
	$this.markers=$this.markers.concat($markers);
};

//	the refresh() method is the method that does all the updating
ClusterMarker.prototype.refresh=function(){
	var $this=this;
	function compareArrays($array1, $array2){
		if($array1.length!==$array2.length){
			return false;
		}
		var i, $length=$array1.length<$array2.length?$array1.length:$array2.length;
		for (i=0; i<$length; i++){
			if($array1[i]!==$array2[i]){
				return false;
			}
		}
		return true;
	}
	function createClusterMarker($indexes){
		var $clusterBounds=new GLatLngBounds(), $length=$indexes.length, $markers=$this.markers;
		while($length--){
			$clusterBounds.extend($markers[$indexes[$length]].getLatLng());										   
		}
		var $clusterMarker=new GMarker($markers[$indexes[0]].getLatLng(), {icon:$this.clusterMarkerIcon, title:$indexes.length+' members located here'});
		GEvent.addListener($clusterMarker, 'click', function(){
			$this.clusterMarkerClickHandler($clusterMarker);
		});
		$clusterMarker._markerIndexes=$indexes;
		$clusterMarker._clusterBounds=$clusterBounds;
		$length=$indexes.length;
		while($length--){
			$markers[$indexes[$length]]._clusterMarker=$clusterMarker;
		}
		return $clusterMarker;
	}
	
	//	filter active markers
	var $map=$this.map, $zoom=$map.getZoom(), $bounds=$map.getBounds(), $markers=$this.markers, $length=$markers.length, $marker, $isActive;
	var $mapSize=$map.getSize(), $borderPaddingX=$mapSize.width/2, $borderPaddingY=$mapSize.height/2;
	
	var $projection=$map.getCurrentMapType().getProjection();
	var $mapSwPoint=$projection.fromLatLngToPixel($bounds.getSouthWest(), $zoom);
	var $mapNePoint=$projection.fromLatLngToPixel($bounds.getNorthEast(), $zoom);
	var $swX=$mapSwPoint.x-$borderPaddingX, $swY=$mapSwPoint.y+$borderPaddingY, $neX=$mapNePoint.x+$borderPaddingX, $neY=$mapNePoint.y-$borderPaddingY;
	var $activeSwLatLng=$projection.fromPixelToLatLng(new GPoint($swX, $swY), $zoom);
	var $activeNeLatLng=$projection.fromPixelToLatLng(new GPoint($neX, $neY), $zoom);
	
	$bounds.extend($activeSwLatLng);
	$bounds.extend($activeNeLatLng);
	
	//	if a marker is within the map bounds plus the border padding then it's isActive and makeVisible properties are set to true
	while($length--){
		$marker=$markers[$length];
		$isActive=false;
		if(!$marker.isHidden() && $bounds.containsLatLng($marker.getLatLng())){
			$isActive=true;
		}
		$marker._ClusterMarker_.isActive=$isActive;
		$marker._ClusterMarker_.makeVisible=$isActive;
	}
	
		//	filter clustered markers
	var $newClusterIndexes=[], i=$markers.length, j, $indexes;
	if($this.clusteringEnabled){
		var $cancelCluster;
		//	loop thru all markers, if a marker is visible and it's icon intersects the icon of the marker it is being compared to then push it to the $indexes array
		while(i--){
			if($markers[i]._ClusterMarker_.makeVisible){
				$indexes=[i];
				j=i;
				while(j--){
					if($markers[j]._ClusterMarker_.makeVisible && $this.markerIconsIntersect($markers[i], $markers[j])){
						$indexes.push(j);
					}
				}
				//	if the marker icon doesn't intersect any other marker icons then $index will have a single element
				//	so if $index.length is greater than 1 then a cluster of 2 or more markers with intersecting markers has been found
				if($indexes.length>1){
					$cancelCluster=false;
					j=$indexes.length;
					//	loop thru the cluster of markers whose icons intersect
					//	if any marker in that cluster has it's doNotCluster property set to true then we do not want to create a cluster marker
					//	instead the marker whose doNotCluster property is true will be displayed and any other markers that are in this cluster are not displayed
					while(j--){
						if($markers[$indexes[j]]._ClusterMarker_.doNotCluster){
							$cancelCluster=true;
						} else {
							$markers[$indexes[j]]._ClusterMarker_.makeVisible=false;
						}
					}
					//	$indexes is an array of markers whose icons intersect
					//	$newClusterIndexes is an array of arrays
					if(!$cancelCluster){
						$newClusterIndexes.push($indexes);
					}
				}
			}
		}
	}
	
	//	update map
	
	//	add or remove cluster markers
	var $clusterMarkers=$this.clusterMarkers, $clusterMarkersLength=$clusterMarkers.length, $clusterMarker, $newClusterMarkers=[];
	var $newClusterIndexesLength=$newClusterIndexes.length, $clusterIndex;
	
	//	remove cluster markers whose markerIndexes no longer exist and flag the related index array not to be created
	for(i=0; i<$clusterMarkersLength; i++){
		$clusterMarker=$clusterMarkers[i];
		$clusterIndex=$clusterMarker._markerIndexes;
		for(j=0; j<$newClusterIndexesLength; j++){
			if(compareArrays($clusterIndex, $newClusterIndexes[j])){
				//	no need to create a cluster marker if a cluster marker already exists and that cluster marker contains the same markers that we want to create a cluster marker for
				$newClusterMarkers.push($clusterMarker);
				//	to flag this array of markers as belonging to a cluster marker that already exists change it's value from the array to false
				$newClusterIndexes[j]=false;
				break;	//	break out of j loop
			}
		}
		if($newClusterIndexes[j]!==false){
			//	this cluster marker is not going to be reused/recycled - it is going to be removed from the map
			//	so delete the _clusterMarker property of each marker within that cluster
			$indexes=$clusterMarker._markerIndexes;
			$length=$indexes.length;
			while($length--){
				delete $this.markers[$indexes[$length]]._clusterMarker;
			}
			$map.removeOverlay($clusterMarker);
		}
		
	}
	//	loop thru $newClusterIndexes creating cluster markers for any index which is not FALSE
	while($newClusterIndexesLength--){
		$indexes=$newClusterIndexes[$newClusterIndexesLength];
		if($indexes!==false){
			$clusterMarker=createClusterMarker($newClusterIndexes[$newClusterIndexesLength]);
			$newClusterMarkers.push($clusterMarker);
			$map.addOverlay($clusterMarker);
		}
	}
	$this.clusterMarkers=$newClusterMarkers;
		
	//	add or remove active markers
	$length=$markers.length;
	while($length--){
		$marker=$markers[$length];
		if($marker._ClusterMarker_.makeVisible && !$marker._ClusterMarker_.isOverlaid){
			//	if a marker's makeVisible property is true and it's isOverlaid property is false then add it to the map
			$map.addOverlay($marker);
			$marker._ClusterMarker_.isOverlaid=true;
		} else if(!$marker._ClusterMarker_.makeVisible && $marker._ClusterMarker_.isOverlaid){
			//	if a marker's makeVisible property is false and it's isOverlaid property is true then remove it from the map
			$map.removeOverlay($marker);
			$marker._ClusterMarker_.isOverlaid=false;
		}
	}
	//	trigger a custom event 'refreshed' each time ClusterMarker's refresh() method has finished executing
	//	this is commented out - it is intended to be used when a dynamic sidebar is attached to the map
	//	the sidebar would listen for the 'refreshed' event and then update itself to reflect the state of the map markers
	//	GEvent.trigger($this, 'refreshed');
};

//	this method returns pixel coordinates of a marker's lat/lng position on the map
//	if this calculation has already been done then it's value will have been cached and the cached value is returned
//	otherwise the value is calculated and cached before being returned
ClusterMarker.prototype.getMarkerAnchorPoint=function($marker, $zoom){
	if(typeof($marker._ClusterMarker_.anchorPoint[$zoom])!=='undefined'){
		return $marker._ClusterMarker_.anchorPoint[$zoom];
	} else {
		var $anchorPoint=this.map.getCurrentMapType().getProjection().fromLatLngToPixel($marker.getLatLng(), $zoom);
		$marker._ClusterMarker_.anchorPoint[$zoom]=$anchorPoint;
		return $anchorPoint;
	}
};

//	this method handles any clicks on a cluster marker
ClusterMarker.prototype.clusterMarkerClickHandler=function($clusterMarker){
	var $this=this, $map=$this.map, $indexes=$clusterMarker._markerIndexes;
	function $sortByMarkerTitle(a, b){
		var title1=a.getTitle().toLowerCase(), title2=b.getTitle().toLowerCase();
		if(title1<title2){
			return -1;
		} else if (title1>title2){
			return 1;
		} else {
			return 0;
		}
	}
	//	this function handles clicks on links to markers in a cluster marker infowindow
	function $clusterMarkerInfoWindowClickHandler($marker){
		return function(){
			GEvent.trigger($marker, 'click');
			$marker._ClusterMarker_.openClusterMarkerOnClose=true;
		};
	}
	var i, $length=$indexes.length, $iwCtn=document.createElement('div'), $div1=document.createElement('div'), $div2=document.createElement('div'), $link, $marker, $markers=[];
	i=$length;
	while(i--){
		$markers.push($this.markers[$indexes[i]]);
	}
	$markers.sort($sortByMarkerTitle);

	//	$minContent.appendChild(document.createElement('br'));
	
	//	if map fully zoomed in then no need for 'fit map to locations' or 'zoom in' links
	if($map.getZoom()<$map.getCurrentMapType().getMaximumResolution()){
		$link=document.createElement('a');
		$link.href='javascript:void(0)';
		$link.onclick=function(){
			$map.setCenter($clusterMarker._clusterBounds.getCenter(), $map.getBoundsZoomLevel($clusterMarker._clusterBounds));
		};
		$link.appendChild(document.createTextNode('Zobrazit uživatele'));
		//	$div1.appendChild(document.createElement('br'));
		$div1.appendChild($link);
		$div1.appendChild(document.createTextNode(' | '));
		$link=document.createElement('a');
		$link.href='javascript:void(0)';
		$link.onclick=function(){
			$map.zoomIn();
		};
		$link.appendChild(document.createTextNode('Přiblížit'));
		$div1.appendChild($link);
		$div1.appendChild(document.createElement('br'));	
	}
	$div1.appendChild(document.createTextNode('Tady se nacházejí uživatelé:'));
	$iwCtn.appendChild($div1);
	$div2.style.maxHeight='11em';
	$div2.style.overflow='auto';
	for(i=0; i<$length; i++){
		$marker=$markers[i];
		$link=document.createElement('a');
		$link.href='javascript:void(0)';
		$link.onclick=$clusterMarkerInfoWindowClickHandler($marker);
		$link.appendChild(document.createTextNode($marker.getTitle()));
		$div2.appendChild($link);
		if(i<$length-1){
			$div2.appendChild(document.createTextNode(', '));
		} else {
			$div2.appendChild(document.createTextNode('.'));
		}
	}
	$iwCtn.appendChild($div2);
	$clusterMarker.openInfoWindow($iwCtn, {maxWidth:($map.getSize().width/2)});
};

//	zoomEnd, moveEnd and mapTypeChanged are all GMap2 events that ClusterMarker needs to listen for and update itself when any of these events occurs
ClusterMarker.prototype.zoomEnd=function(){
	this._cancelMoveEnd=true;
	this.refresh();
};

//	a map zoom will trigger both a zoomEnd and moveEnd event/
//	no need to execute ClusterMarker's refresh() event twice so the cancelMoveEnd property is used as a flag to avoid executing refresh() twice
ClusterMarker.prototype.moveEnd=function(){
	if(this._cancelMoveEnd){
		delete this._cancelMoveEnd;
	} else {
		this.refresh();
	}
};

ClusterMarker.prototype.mapTypeChanged=function(){
	this.refresh();
};

//	here we build a pair of pixel coordinates for each marker's icon
//	the pair representing the marker's icon's south-west and north-east points on the map
//	we can then calculate whether or not two marker icons intersect
//	all calculated values are cached so that they do not need to be re-caculated if required again
ClusterMarker.prototype.markerIconsIntersect=function($marker1, $marker2, $zoom){
	var $this=this, $map=$this.map;
	function getIconPointBounds($marker){
		var $icon=$marker.getIcon();
		var $iconSize=$icon.iconSize;
		var $iconAnchorPoint=$icon.iconAnchor;
		var $markerAnchorPoint=$this.getMarkerAnchorPoint($marker, $zoom);
		
		var $swIconAnchorPoint=new GPoint($markerAnchorPoint.x-$iconAnchorPoint.x, $markerAnchorPoint.y-$iconAnchorPoint.y+$iconSize.height);
		var $neIconAnchorPoint=new GPoint($markerAnchorPoint.x-$iconAnchorPoint.x+$iconSize.width, $markerAnchorPoint.y-$iconAnchorPoint.y);
		return {sw:$swIconAnchorPoint, ne:$neIconAnchorPoint};
	}
	
	if(typeof($zoom)==='undefined'){
		$zoom=$map.getZoom();
	}
	if(typeof($marker1._ClusterMarker_.intersectTable[$zoom])!=='undefined' && typeof($marker1._ClusterMarker_.intersectTable[$zoom][$marker2._ClusterMarker_.id])!=='undefined'){
		return $marker1._ClusterMarker_.intersectTable[$zoom][$marker2._ClusterMarker_.id];
	}
	var $bounds1=getIconPointBounds($marker1), $bounds2=getIconPointBounds($marker2);
	var $intersects=!($bounds2.sw.x>$bounds1.ne.x || $bounds2.ne.x<$bounds1.sw.x || $bounds2.ne.y>$bounds1.sw.y || $bounds2.sw.y<$bounds1.ne.y);
	if(typeof($marker1._ClusterMarker_.intersectTable[$zoom])==='undefined'){
		$marker1._ClusterMarker_.intersectTable[$zoom]=[];
	}
	$marker1._ClusterMarker_.intersectTable[$zoom][$marker2._ClusterMarker_.id]=$intersects;
	return $intersects;
};

//	this method is not complete - if you require to remove markers from ClusterMarker then i shall finish this method for you
//	custom marker properties ought to be deleted and cluster marker event listeners removed
ClusterMarker.prototype.removeMarkers=function($markers){
	if(!$markers){
		for(var i=0; i<this.markers.length; i++){
			this.map.removeOverlay(this.markers[i]);
		}
		this.markers=[];
		for(i=0; i<this.clusterMarkers.length; i++){
			this.map.removeOverlay(this.clusterMarkers[i]);
		}
		this.clusterMarkers=[];
	}
};

//	this method will zoom and pan the map to show all markers in the $markers array that is passed to it
//	if no array of markers is passed to this method then the map will zoom and pan to fit all markers added to ClusterMarker
//	if a value is passed for $maxZoom then the map will not be zoomed in any more than this zoom level
ClusterMarker.prototype.fitMapToMarkers=function($markers, $maxZoom){
	var $this=this, $bounds=new GLatLngBounds(), $refresh=false; 
	if(typeof($markers)==='undefined' || $markers===null){
		$markers=this.markers;
	}
	var $length=$markers.length;
	while($length--){
		if(!$markers[$length].isHidden()){
			$bounds.extend($markers[$length].getLatLng());
			$refresh=true;
		}
	}
	if($refresh){
		var $zoom=$this.map.getBoundsZoomLevel($bounds);
		if(typeof($maxZoom)!=='undefined'){
			$zoom=$zoom>$maxZoom?$maxZoom:$zoom;
		}
		$this.map.setCenter($bounds.getCenter(), $zoom);	
	}
};

//	this method will return the minimum zoom level at which a marker is not clustered
//	if the marker is currently part of a cluster then start searching for the minimum unclustered zoom level from the current map zoom level plus one
//	(no need to search for the zoom level at lower zoom levels as we know the marker is clustered at this zoom level)
//	otherwise search for the minimum unclustered zoom level from map zoom level zero
ClusterMarker.prototype.getMinUnclusterLevel=function($marker){
	var $this=this, $map=$this.map, $maxZoomLevel=$map.getCurrentMapType().getMaximumResolution(), $isClustered, $markers=$this.markers, $length=$markers.length, $indexes=[], i, $zoomLevel;
	while($length--){
		if($marker!==$markers[$length]){
			$indexes.push($markers[$length]._ClusterMarker_.id);
		}
	}
	if($marker._clusterMarker){
		$zoomLevel=$map.getZoom()+1;
	} else {
		$zoomLevel=0;
	}
	$length=$indexes.length;
	while($zoomLevel<=$maxZoomLevel){
		$isClustered=false;
		i=$length;
		while(!$isClustered && i--){
			if($this.markerIconsIntersect($marker, $markers[$indexes[i]], $zoomLevel)){
				$isClustered=true;
			}
		}
		if(!$isClustered){
			break;
		}
		$zoomLevel++;
	}
	return $zoomLevel;
};

//	set a marker's doNotCluster property to true or false
//	if the doNotCluster property is set to true then listen for the GMap2 'infowindowclose' event so we can then set doNotCluster to false
ClusterMarker.prototype.setDoNotCluster=function($marker, $state){
	var $this=this;
	if($state && !$marker._ClusterMarker_.doNotCluster){
		$marker._ClusterMarker_.doNotCluster=true;
		$marker._ClusterMarker_.infowindowcloseListener=GEvent.addListener($this.map, 'infowindowclose', function(){
			GEvent.removeListener($marker._ClusterMarker_.infowindowcloseListener);
			$marker._ClusterMarker_.doNotCluster=false;
			$this.refresh();
			if($marker._clusterMarker && $marker._ClusterMarker_.openClusterMarkerOnClose){
				delete $marker._ClusterMarker_.openClusterMarkerOnClose;
				var $f=function(){
					GEvent.trigger($marker._clusterMarker, 'click');
				};
				setTimeout($f, 10);
			}
		});
	} else if (!$state && $marker._ClusterMarker_.doNotCluster){
		$marker._ClusterMarker_.doNotCluster=false;
		$this.refresh();
		GEvent.removeListener($marker._ClusterMarker_.infowindowcloseListener);
	}
};

