/* 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