You are on page 1of 11

/*

A marker manager for the Google Maps API


http://googlemapsapi.martinpearman.co.uk/clustermarker
Copyright Martin Pearman 2009

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 Softw
are Foundation, either version 3 of the License, or (at your option) any later v
ersion.
This program is distributed in the hope that it will be useful, but WITH
OUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNES
S 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, $options){
var $this=this;
$this.map=$map;
$this.markers=[];
$this.clusterMarkers=[];
var $options={clusteringEnabled:true, borderPadding:100};
if(typeof($options)!=='undefined'){
// process options
$this.clusteringEnabled=$options.clusteringEnabled?$options.clus
teringEnabled:true;
$this.borderPadding=$options.borderPadding?$options.borderPaddin
g/100:0.5; // decide a default border padding from 1% to 100%
$this.iconPadding=$options.iconPadding?$options.iconPadding:0;
if($options.markers){
if($options.areOverlaid){
$areOverlaid=$options.areOverlaid;
} else {
$areOverlaid=true;
}
$this.addMarkers($options.markers, $areOverlaid);
}
}
GEvent.bind($map, 'moveend', this, $this.moveEnd);
GEvent.bind($map, 'zoomend', this, $this.zoomEnd);
GEvent.bind($map, 'maptypechanged', $this, $this.mapTypeChanged);
}
ClusterMarker.prototype.addMarkers=function($markers, $areOverlaid){ //
auto inc markers array from last time markers were added
var $this=this, $indexOffset=$this.markers.length, $length=$markers.leng
th;
if(typeof($areOverlaid)==='undefined'){
var $areOverlaid=true; // default is expect markers to be
already added to the map
}
while($length--){
$markers[$length]._ClusterMarker_={index:$length+$indexOffset, i
sActive:true, doNotCluster:false, anchorPoint:[], intersectTable:[], isOverlaid:
$areOverlaid};
}
$this.markers=$this.markers.concat($markers);
};
ClusterMarker.prototype.refresh=function(){
var $this=this;
// helper functions for cluster marker add/remove
function compareArrays($array1, $array2){
if($array1.length!==$array2.length){
return false;
}
var i, $length=$array1.length<$array2.length?$array1.length:$arr
ay2.length;
for (i=0; i<$length; i++){
if($array1[i]!==$array2[i]){
return false;
}
}
return true;
}
function createClusterMarker($indexes){
var $icon=new GIcon(G_DEFAULT_ICON, '/greenmarker/top/red.png');
var $clusterBounds=new GLatLngBounds(), $length=$indexes.length,
i, $markers=$this.markers;
while($length--){
$clusterBounds.extend($markers[$indexes[$length]].getLat
Lng());
}
// anchor cluster marker to lat/lng of marker nearest to cl
uster center
// either an error in my code or distanceFrom() is not accu
rate enough to be useable
/* $length=$indexes.length;
var $distance, $minDistance, $index, $nearestIndex, $clusterCent
er=$clusterBounds.getCenter();
while($length--){
$index=$indexes[$length];
$distance=$markers[$index].getLatLng().distanceFrom($clu
sterCenter);
if($length===$indexes.length-1 || $distance<$minDistance
){
$minDistance=$distance;
$nearestIndex=$index;
}
}
var $clusterMarker=new GMarker($markers[$nearestIndex].getLatLng
(), {icon:$icon, title:$indexes.length+' markers in this cluster'}); */
// old behaviour to center cluster marker at cluster center
// var $clusterMarker=new GMarker($clusterBounds.getCenter(
), {icon:$icon, title:$indexes.length+' markers in this cluster'});
// use (1st) clustered marker anchor as cluster marker anch
or
var $clusterMarker=new GMarker($markers[$indexes[0]].getLatLng()
, {icon:$icon, title:$indexes.length+' markers in this cluster'});

GEvent.addListener($clusterMarker, 'click', function(){


var $options={};
$this.clusterMarkerClick($clusterMarker, $options);
});
$clusterMarker._markerIndexes=$indexes; // would be more us
eful as an array of markers
$clusterMarker._clusterBounds=$clusterBounds; // save as
property or discard?
$length=$indexes.length;
while($length--){
$markers[$indexes[$length]]._clusterMarker=$clusterMarke
r;
}
return $clusterMarker;
}
// filter active markers
var $map=$this.map, $zoom=$map.getZoom(), $bounds=$map.getBounds(), $mar
kers=$this.markers, $length=$markers.length, $marker, $isActive;
// variable border padding as a % of screen size
var $mapSize=$map.getSize(), $borderPadding=1,/*$this.borderPadding,*/ $
borderPaddingX=$mapSize.width*$borderPadding, $borderPaddingY=$mapSize.height*$b
orderPadding;
// to do: compare new method to previous version
var $projection=$map.getCurrentMapType().getProjection();
var $mapSwPoint=$projection.fromLatLngToPixel($bounds.getSouthWest(), $z
oom);
var $mapNePoint=$projection.fromLatLngToPixel($bounds.getNorthEast(), $z
oom);
var $swX=$mapSwPoint.x-$borderPaddingX, $swY=$mapSwPoint.y+$borderPaddin
gY, $neX=$mapNePoint.x+$borderPaddingX, $neY=$mapNePoint.y-$borderPaddingY;
var $activeSwLatLng=$projection.fromPixelToLatLng(new GPoint($swX, $swY)
, $zoom); // is unbounded option required here?
var $activeNeLatLng=$projection.fromPixelToLatLng(new GPoint($neX, $neY)
, $zoom);
$bounds.extend($activeSwLatLng); // could create new GLatLng
Bounds() instead of extending existing $bounds
$bounds.extend($activeNeLatLng);
while($length--){
$marker=$markers[$length];
$isActive=false;
if(!$marker.isHidden() && $bounds.containsLatLng($marker.getLatL
ng())){
$isActive=true;
}
$marker._ClusterMarker_.isActive=$isActive; // not requ
ired - or keep so active markers can be quickly enumerated?
$marker._makeVisible=$isActive;
}
// filter clustered markers
var $newClusterIndexes=[];
if($this.clusteringEnabled){
var i=$markers.length, j, $indexes, $cancelCluster;
while(i--){
if($markers[i]._makeVisible){
$indexes=[i];
j=i;
while(j--){
if($markers[j]._makeVisible && $this.mar
kerIconsIntersect($markers[i], $markers[j])){
$indexes.push(j);
}
}
if($indexes.length>1){
$cancelCluster=false;
j=$indexes.length;
while(j--){
if($markers[$indexes[j]]._Cluste
rMarker_.doNotCluster){ // instead of marker doNotCluster property have an
array of markers which should not be clustered - more functional for more than o
ne marker
$cancelCluster=true;
} else {
$markers[$indexes[j]]._m
akeVisible=false;
}
}
// array $newClusterIndexes is arra
y of array of indexes of clustered markers;
if(!$cancelCluster){
$newClusterIndexes.push($indexes
);
}
}
}
}
}
// update map
// add or remove cluster markers
var $clusterMarkers=$this.clusterMarkers, $clusterMarkersLength=$cluster
Markers.length, $clusterMarker, $newClusterMarkers=[], k;
var $newClusterIndexesLength=$newClusterIndexes.length, $clusterIndex;
// remove cluster markers who markerIndexes no longer exist and fla
g the relates 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])){
// cluster marker markerIndexes still exist
so do not remove cluster marker
$newClusterMarkers.push($clusterMarker);
$newClusterIndexes[j]=false;
break; // break out of j loop
}
}
if($newClusterIndexes[j]!==false){
$indexes=$clusterMarker._markerIndexes;
$length=$indexes.length;
while($length--){
delete $this.markers[$indexes[$length]]._cluster
Marker;
}
$map.removeOverlay($clusterMarker);
}
}
// loop thru $newClusterIndexes creating cluster markers for any in
dex which is not FALSE
while($newClusterIndexesLength--){
$indexes=$newClusterIndexes[$newClusterIndexesLength];
if($indexes!==false){
$clusterMarker=createClusterMarker($newClusterIndexes[$n
ewClusterIndexesLength]);
$newClusterMarkers.push($clusterMarker);
$map.addOverlay($clusterMarker);
}
}
$this.clusterMarkers=$newClusterMarkers; // quicker to use a
rray splice?
// add or remove active markers
$length=$markers.length;
while($length--){
$marker=$markers[$length];
if($marker._makeVisible && !$marker._ClusterMarker_.isOverlaid){
$map.addOverlay($marker);
$marker._ClusterMarker_.isOverlaid=true;
} else if(!$marker._makeVisible && $marker._ClusterMarker_.isOve
rlaid){
$map.removeOverlay($marker);
$marker._ClusterMarker_.isOverlaid=false;
}
}
};
ClusterMarker.prototype.getMarkerAnchorPoint=function($marker, $zoom){
if(typeof($marker._ClusterMarker_.anchorPoint[$zoom])!=='undefined'){
return $marker._ClusterMarker_.anchorPoint[$zoom];
} else {
var $projection=this.map.getCurrentMapType().getProjection();
var $anchorPoint=$projection.fromLatLngToPixel($marker.getLatLng
(), $zoom);
$marker._ClusterMarker_.anchorPoint[$zoom]=$anchorPoint;
return $anchorPoint;
}
};
ClusterMarker.prototype.clusterMarkerClick=function($clusterMarker, $options){
// problems possible with inaccurate value returned by .getMaximumR
esolution()
// see link in bookmarks toolbar
// maybe zoomIn() or setZoom() and check if getZoom() has increased
to establish if map can zoom in any further
// but that might cause performance issues..?
// http://code.google.com/apis/maps/documentation/reference.html#GM
apType
// getMaxZoomAtLatLng()
var $this=this, $map=$this.map, $indexes=$clusterMarker._markerIndexes;
// what should a click on a cluster marker do?
// 1: old behaviour to fit map to clustered markers
// $map.setCenter($clusterMarker._clusterBounds.getCenter(), $map.g
etBoundsZoomLevel($clusterMarker._clusterBounds));
// 2: center map on cluster group center and zoom in a level
// $map.setCenter($clusterMarker._clusterBounds.getCenter());
// $map.zoomIn();
// 3: display infowindow of links to all markers in cluster and use
the doNotCluster property of any selected marker to uncluster it
// this method will become the default cluster marker click for eve
ry map type's maximum zoom level
// (ie unclusterable markers)
// update this method to use uncluster if map not fully zoomed in a
nd donotcluster if map fully zoomed in
function sortByMarkerTitle(a, b){
var title1=a.getTitle(), title2=b.getTitle();
if(title1<title2){
return -1;
} else if (title1>title2){
return 1;
} else {
return 0;
}
}
function unclusterAndClick($marker){
return function(){
$map.setZoom($this.getMinUnclusterLevel($marker));
$map.setCenter($marker.getLatLng());
GEvent.trigger($marker, 'click');
}
}
function doNotClusterAndClick($marker){
return function(){
$marker._ClusterMarker_.doNotCluster=true;
$this.refresh();
GEvent.trigger($marker, 'click');
var i=GEvent.addListener($this.map, 'infowindowclose', f
unction(){
$marker._ClusterMarker_.doNotCluster=false;
$this.refresh();
GEvent.removeListener(i);
});
}
}
var i, $length=$indexes.length, $maxContent=document.createElement('div'
), $minContent=document.createElement('div'), $link, $marker, $markers=[];
i=$length;
while(i--){
$markers.push($this.markers[$indexes[i]]);
}
$markers.sort(sortByMarkerTitle);
for(i=0; i<$length; i++){
$marker=$markers[i];
$link=document.createElement('a');
$link.href='javascript:return void()';
// this does not handle cluster markers at less than max zo
om that persist to max zoom - their click action is wrong
// so link click must detect if map fully zoomed in and the
n execute appropriate function
if($map.getZoom()<$map.getCurrentMapType().getMaximumResolution(
)){
$link.onclick=unclusterAndClick($marker);
} else {
$link.onclick=doNotClusterAndClick($marker);
}

$link.appendChild(document.createTextNode($marker.getTitle().rep
lace(/ /g, '_')));
$maxContent.appendChild($link);
if(i<$length-1){
$maxContent.appendChild(document.createTextNode(' | '));
}
}
$minContent.appendChild(document.createElement('br'));
$link=document.createElement('a');
$link.href='javascript:return void()';
$link.onclick=function(){
$this.map.getInfoWindow().maximize();
};
$link.appendChild(document.createTextNode('Show links'));
$minContent.appendChild($link);
$minContent.appendChild(document.createTextNode(' | '));
// if map fully zoomed in then no need for zoom in links and might
as well show links by default
$link=document.createElement('a');
$link.href='javascript:return void()';
$link.onclick=function(){
$map.setCenter($clusterMarker._clusterBounds.getCenter(), $map.g
etBoundsZoomLevel($clusterMarker._clusterBounds));
};
$link.appendChild(document.createTextNode('Fit map to cluster'));
$minContent.appendChild($link);
$minContent.appendChild(document.createTextNode(' | '));
$link=document.createElement('a');
$link.href='javascript:return void()';
$link.onclick=function(){
// $map.panTo($clusterMarker._clusterBounds.getCenter());
$map.zoomIn();
};
$link.appendChild(document.createTextNode('Zoom in'));
$minContent.appendChild($link);
$clusterMarker.openInfoWindow($minContent, {maxContent:$maxContent, maxT
itle:"Markers within this cluster"});
// debug option: just display a simple infowindow
// $clusterMarker.openInfoWindowHtml($indexes.length+' markers in t
his cluster');
};
// make these three event handlers into anon functions in constructor?
ClusterMarker.prototype.zoomEnd=function(){
this._cancelMoveEnd=true;
this.refresh();
};
ClusterMarker.prototype.moveEnd=function(){
if(this._cancelMoveEnd){
this._cancelMoveEnd=false;
} else {
this.refresh();
}
};
ClusterMarker.prototype.mapTypeChanged=function(){
// refresh can be assigned directly to listener if no other functio
ns are required
this.refresh();
};

ClusterMarker.prototype.markerIconsIntersect=function($marker1, $marker2, $zoom)


{ // optional zoom parameter so triggerClick can be implemented
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 $iconPadding=$this.iconPadding;
var $swIconAnchorPoint=new GPoint($markerAnchorPoint.x-$iconAnch
orPoint.x-$iconPadding, $markerAnchorPoint.y-$iconAnchorPoint.y+$iconSize.height
+$iconPadding);
var $neIconAnchorPoint=new GPoint($markerAnchorPoint.x-$iconAnch
orPoint.x+$iconSize.width+$iconPadding, $markerAnchorPoint.y-$iconAnchorPoint.y-
$iconPadding);
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
_.index])!=='undefined'){
return $marker1._ClusterMarker_.intersectTable[$zoom][$marker2._
ClusterMarker_.index];
}
/* if($marker2._ClusterMarker_.intersectTable[$zoom] && $marker2._C
lusterMarker_.intersectTable[$zoom][$marker1._ClusterMarker_.index]){
return $marker2._ClusterMarker_.intersectTable[$zoom][$marker1._
ClusterMarker_.index];
} */
if(typeof($marker1)==='undefined' || typeof($marker2)==='undefined'){
GLog.write('Undefined markers');
}
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_.
index]=$intersects;
/* if(!$marker2._ClusterMarker_.intersectTable[$zoom]){
$marker2._ClusterMarker_.intersectTable[$zoom]=[];
}
$marker2._ClusterMarker_.intersectTable[$zoom][$marker1._ClusterMarker_.
index]=$intersects; */
return $intersects;
};
ClusterMarker.prototype.removeMarkers=function($markers){
if(!$markers){ // <<< looks bugggy
for(var i=0; i<this.markers.length; i++){
this.map.removeOverlay(this.markers[i]); //
marker properties to delete
}
this.markers=[];
for(i=0; i<this.clusterMarkers.length; i++){
this.map.removeOverlay(this.clusterMarkers[i]); //
event listener property to implement
}
this.clusterMarkers=[];
}
};

ClusterMarker.prototype.fitMapToMarkers=function($markers, $maxZoom){
var $this=this, $bounds=new GLatLngBounds(), $refresh=false;
if(typeof($markers)==='undefined' || $markers===null){
var $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);
}
};

ClusterMarker.prototype.getMinUnclusterLevel=function($marker){
var $this=this, $map=$this.map, $maxZoomLevel=$map.getCurrentMapType().g
etMaximumResolution(), $isClustered, $markers=$this.markers, $length=$markers.le
ngth, $indexes=[], i, $zoomLevel;
while($length--){
if($marker!==$markers[$length]){
$indexes.push($markers[$length]._ClusterMarker_.index);
// will need updating when remove single (or more) markers is implemented t
o avoid undefined elements in this.markers array
}
}
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)){ // add support for hidden markers?
$isClustered=true;
}
}
if(!$isClustered){
break;
}
$zoomLevel++;
}
return $zoomLevel; // should return $zoomLevel-1 ??
/*
if($isUnclustered){
return $zoomLevel; // -1 ?
} else {
// marker is unclusterable with the current map type
return false;
}
*/
};

ClusterMarker.prototype.setDoNotCluster=function($marker, $state){
$marker._ClusterMarker_.doNotCluster=$state;
this.refresh();
};
ClusterMarker.prototype.newMethod=function(){
};

You might also like