/*
ClusterMarker Version 1.2.1
A marker manager for the Google Maps API
http://googlemapsapi.martinpearman.co.uk/clustermarker
Copyright Martin Pearman 2008.
Last updated 2nd February 2008
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 .
*/
function ClusterMarker($map, $options){
this._map=$map;
this._mapMarkers=[];
this._iconBounds=[];
this._clusterMarkers=[];
this._eventListeners=[];
if (typeof($options)=='undefined'){
$options={};
}
this.borderPadding=($options.borderPadding)?$options.borderPadding:256;
this.clusteringEnabled=($options.clusteringEnabled===false)?false:true;
if ($options.clusterMarkerIcon){
this._clusterMarker.icon=$options.clusterMarkerIcon;
} else {
this._clusterMarker.icon=new GIcon();
this._clusterMarker.icon.image='http://maps.google.com/mapfiles/arrow.png';
this._clusterMarker.icon.shadow='http://www.google.com/intl/en_us/mapfiles/arrowshadow.png';
this._clusterMarker.icon.shadowSize=new GSize(39, 34);
this._clusterMarker.icon.iconSize=new GSize(39, 34);
this._clusterMarker.icon.iconAnchor=new GPoint(9, 33);
}
this.clusterMarkerTitle=($options.clusterMarkerTitle)?$options.clusterMarkerTitle:'Click to zoom in and see %count markers';
if ($options.fitMapMaxZoom){
this.fitMapMaxZoom=$options.fitMapMaxZoom;
}
this.fitMapToMarkers=($options.fitMapToMarkers===false)?false:true;
this.intersectPadding=($options.intersectPadding)?$options.intersectPadding:0;
if ($options.markers){
this.addMarkers($options.markers);
}
GEvent.bind(this._map, 'moveend', this, this._moveEnd);
GEvent.bind(this._map, 'zoomend', this, this._zoomEnd);
GEvent.bind(this._map, 'maptypechanged', this, this._mapTypeChanged);
}
ClusterMarker.prototype.addMarkers=function($markers){
for (var i=$markers.length-1; i>=0; i--){
// check all these properties must be set
$markers[i]._isVisible=false;
$markers[i]._isActive=false;
$markers[i]._makeVisible=false;
}
if (this.fitMapToMarkers){
var $markersBounds=new GLatLngBounds();
for (i=$markers.length-1; i>=0; i--){
$markersBounds.extend($markers[i].getLatLng());
}
var $fitMapToMarkersZoom=this._map.getBoundsZoomLevel($markersBounds);
if (this.fitMapMaxZoom && $fitMapToMarkersZoom>this.fitMapMaxZoom){
$fitMapToMarkersZoom=this.fitMapMaxZoom;
}
this._map.setCenter($markersBounds.getCenter(), $fitMapToMarkersZoom);
}
this._mapMarkers=$markers;
this._iconBounds=[];
};
ClusterMarker.prototype._clusterMarker=function($clusterGroupIndexes){
var $clusterGroupBounds=new GLatLngBounds(), i, $clusterMarker, $map=this._map;
for (i=$clusterGroupIndexes.length-1; i>=0; i--){
$clusterGroupBounds.extend(this._mapMarkers[$clusterGroupIndexes[i]].getLatLng());
}
// var $title=this.clusterMarkerTitle.replace(/%count/gi, $clusterGroupIndexes.length);
$clusterMarker=new GMarker($clusterGroupBounds.getCenter(), {icon:this._clusterMarker.icon, title:this.clusterMarkerTitle.replace(/%count/gi, $clusterGroupIndexes.length)});
$clusterMarker._clusteredMarkers=$clusterGroupIndexes;
this._eventListeners.push(GEvent.addListener($clusterMarker, 'click', function(){
$map.setCenter($clusterGroupBounds.getCenter(), $map.getBoundsZoomLevel($clusterGroupBounds));
}));
return $clusterMarker;
};
ClusterMarker.prototype._filterActiveMapMarkers=function(){
var $borderPadding=this.borderPadding, $mapZoomLevel=this._map.getZoom(), $mapProjection=this._map.getCurrentMapType().getProjection(), $mapPointSw, $activeAreaPointSw, $activeAreaLatLngSw, $mapPointNe, $activeAreaPointNe, $activeAreaLatLngNe, $activeAreaBounds=this._map.getBounds(), i, $marker, $uncachedIconBoundsIndexes=[], $oldState;
if ($borderPadding) {
$mapPointSw=$mapProjection.fromLatLngToPixel($activeAreaBounds.getSouthWest(), $mapZoomLevel);
$activeAreaPointSw=new GPoint($mapPointSw.x-$borderPadding, $mapPointSw.y+$borderPadding);
$activeAreaLatLngSw=$mapProjection.fromPixelToLatLng($activeAreaPointSw, $mapZoomLevel);
$mapPointNe=$mapProjection.fromLatLngToPixel($activeAreaBounds.getNorthEast(), $mapZoomLevel);
$activeAreaPointNe=new GPoint($mapPointNe.x+$borderPadding, $mapPointNe.y-$borderPadding);
$activeAreaLatLngNe=$mapProjection.fromPixelToLatLng($activeAreaPointNe, $mapZoomLevel);
$activeAreaBounds.extend($activeAreaLatLngSw);
$activeAreaBounds.extend($activeAreaLatLngNe);
}
this._activeMarkersChanged=false;
if (typeof(this._iconBounds[$mapZoomLevel])=='undefined') {
// no iconBounds cached for this zoom level
// no need to check for existence of individual iconBounds elements
this._iconBounds[$mapZoomLevel]=[];
this._activeMarkersChanged=true; // force refresh(true) as zoomed to uncached zoom level
for (i=this._mapMarkers.length-1; i>=0; i--){
$marker=this._mapMarkers[i];
$marker._isActive=$activeAreaBounds.containsLatLng($marker.getLatLng())?true:false;
$marker._makeVisible=$marker._isActive;
if ($marker._isActive) {
$uncachedIconBoundsIndexes.push(i);
}
}
} else {
// icondBounds array exists for this zoom level
// check for existence of individual iconBounds elements
for (i=this._mapMarkers.length-1; i>=0; i--){
$marker=this._mapMarkers[i];
$oldState=$marker._isActive;
$marker._isActive=$activeAreaBounds.containsLatLng($marker.getLatLng())?true:false;
$marker._makeVisible=$marker._isActive;
if (!this._activeMarkersChanged && $oldState!=$marker._isActive) {
this._activeMarkersChanged=true;
}
if ($marker._isActive && typeof(this._iconBounds[$mapZoomLevel][i])=='undefined') {
$uncachedIconBoundsIndexes.push(i);
}
}
}
return $uncachedIconBoundsIndexes;
};
ClusterMarker.prototype._filterIntersectingMapMarkers=function(){
var $clusterGroup, i, j, $mapZoomLevel=this._map.getZoom();
for (i=this._mapMarkers.length-1; i>0; i--)
{
if (this._mapMarkers[i]._makeVisible){
$clusterGroup=[];
for (j=i-1; j>=0; j--){
if (this._mapMarkers[j]._makeVisible && this._iconBounds[$mapZoomLevel][i].intersects(this._iconBounds[$mapZoomLevel][j])){
$clusterGroup.push(j);
}
}
if ($clusterGroup.length!==0){
$clusterGroup.push(i);
for (j=$clusterGroup.length-1; j>=0; j--){
this._mapMarkers[$clusterGroup[j]]._makeVisible=false;
}
this._clusterMarkers.push(this._clusterMarker($clusterGroup));
}
}
}
};
ClusterMarker.prototype._mapTypeChanged=function() {
this.refresh(true);
};
ClusterMarker.prototype._moveEnd=function(){
if(!this._cancelMoveEnd){
this.refresh();
} else {
this._cancelMoveEnd=false;
}
};
ClusterMarker.prototype._preCacheIconBounds=function($indexes){
var $mapProjection=this._map.getCurrentMapType().getProjection(), $mapZoomLevel=this._map.getZoom(), i, $marker, $iconSize, $iconAnchorPoint, $iconAnchorPointOffset, $iconBoundsPointSw, $iconBoundsPointNe, $iconBoundsLatLngSw, $iconBoundsLatLngNe, $intersectPadding=this.intersectPadding;
for (i=$indexes.length-1; i>=0; i--){
$marker=this._mapMarkers[$indexes[i]];
$iconSize=$marker.getIcon().iconSize;
$iconAnchorPoint=$mapProjection.fromLatLngToPixel($marker.getLatLng(), $mapZoomLevel);
$iconAnchorPointOffset=$marker.getIcon().iconAnchor;
$iconBoundsPointSw=new GPoint($iconAnchorPoint.x-$iconAnchorPointOffset.x-$intersectPadding, $iconAnchorPoint.y-$iconAnchorPointOffset.y+$iconSize.height+$intersectPadding);
$iconBoundsPointNe=new GPoint($iconAnchorPoint.x-$iconAnchorPointOffset.x+$iconSize.width+$intersectPadding, $iconAnchorPoint.y-$iconAnchorPointOffset.y-$intersectPadding);
$iconBoundsLatLngSw=$mapProjection.fromPixelToLatLng($iconBoundsPointSw, $mapZoomLevel);
$iconBoundsLatLngNe=$mapProjection.fromPixelToLatLng($iconBoundsPointNe, $mapZoomLevel);
this._iconBounds[$mapZoomLevel][$indexes[i]]=new GLatLngBounds($iconBoundsLatLngSw, $iconBoundsLatLngNe);
}
};
ClusterMarker.prototype.refresh=function($forceFullRefresh){
var i,$marker, $uncachedIconBoundsIndexes;
$uncachedIconBoundsIndexes=this._filterActiveMapMarkers();
if (this._activeMarkersChanged || $forceFullRefresh) {
this._removeClusterMarkers();
if (this.clusteringEnabled && this._map.getZoom()0) {
this._preCacheIconBounds($uncachedIconBoundsIndexes);
}
this._filterIntersectingMapMarkers();
}
for (i=this._clusterMarkers.length-1; i>=0; i--){
this._map.addOverlay(this._clusterMarkers[i]);
}
for (i=this._mapMarkers.length-1; i>=0; i--){
$marker=this._mapMarkers[i];
if (!$marker._isVisible && $marker._makeVisible){
this._map.addOverlay($marker);
$marker._isVisible=true;
}
if ($marker._isVisible && !$marker._makeVisible){
this._map.removeOverlay($marker);
$marker._isVisible=false;
}
}
}
};
ClusterMarker.prototype._removeClusterMarkers=function(){
for (var i=this._clusterMarkers.length-1; i>=0; i--){
this._map.removeOverlay(this._clusterMarkers[i]);
}
for (i=this._eventListeners.length-1; i>=0; i--){
GEvent.removeListener(this._eventListeners[i]);
}
this._clusterMarkers=[];
this._eventListeners=[];
};
ClusterMarker.prototype.removeMarkers=function(){
for (var i=this._mapMarkers.length-1; i>=0; i--){
if (this._mapMarkers[i]. _isVisible){
this._map.removeOverlay(this._mapMarkers[i]);
}
delete this._mapMarkers[i]._isVisible;
delete this._mapMarkers[i]._isActive;
delete this._mapMarkers[i]._makeVisible;
}
this._removeClusterMarkers();
this._mapMarkers=[];
this._iconBounds=[];
};
ClusterMarker.prototype.triggerClick=function($index) {
// a new method to trigger click event on mapMarkers[$index]
var $marker=this._mapMarkers[$index];
if ($marker._isVisible) {
// $marker is visible so just trigger it's click event
GEvent.trigger($marker, 'click');
}
else if ($marker._isActive) {
// $marker is clustered, trigger it's cluster marker's click event and call triggerClick($index)
var i=0, j=0, $found=false;
while (!$found) {
if (this._clusterMarkers[i]._clusteredMarkers[j]==$index) {
$found=true;
} else {
if (j