]> projects.mako.cc - selectricity/blob - public/javascripts/clusterer.js
Expand coverage of IE regex to cover all versions 5-6
[selectricity] / public / javascripts / clusterer.js
1 // Clusterer.js - marker clustering routines for Google Maps apps
2 //
3 // The original version of this code is available at:
4 // http://www.acme.com/javascript/
5 //
6 // Copyright © 2005,2006 by Jef Poskanzer <jef@mail.acme.com>.
7 // All rights reserved.
8 //
9 // Modified for inclusion into the YM4R library in accordance with the 
10 // following license:
11 //
12 // Redistribution and use in source and binary forms, with or without
13 // modification, are permitted provided that the following conditions
14 // are met:
15 // 1. Redistributions of source code must retain the above copyright
16 //    notice, this list of conditions and the following disclaimer.
17 // 2. Redistributions in binary form must reproduce the above copyright
18 //    notice, this list of conditions and the following disclaimer in the
19 //    documentation and/or other materials provided with the distribution.
20 //
21 // THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
22 // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 // ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
25 // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
27 // OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28 // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30 // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31 // SUCH DAMAGE.
32 //
33 // For commentary on this license please see http://www.acme.com/license.html
34
35
36 // Constructor.
37 Clusterer = function(markers,icon,maxVisibleMarkers,gridSize,minMarkersPerCluster,maxLinesPerInfoBox) { 
38     this.markers = [];
39     if(markers){
40         for(var i =0 ; i< markers.length ; i++){
41             this.addMarker(markers[i]);
42         }
43     }    
44     this.clusters = [];
45     this.timeout = null;
46         
47     this.maxVisibleMarkers = maxVisibleMarkers || 150;
48     this.gridSize = gridSize || 5;
49     this.minMarkersPerCluster = minMarkersPerCluster || 5;
50     this.maxLinesPerInfoBox = maxLinesPerInfoBox || 10;
51     
52     this.icon = icon || G_DEFAULT_ICON;
53 }
54
55 Clusterer.prototype = new GOverlay();
56
57 Clusterer.prototype.initialize = function ( map ){
58     this.map = map;
59     this.currentZoomLevel = map.getZoom();
60    
61     GEvent.addListener( map, 'zoomend', Clusterer.makeCaller( Clusterer.display, this ) );
62     GEvent.addListener( map, 'moveend', Clusterer.makeCaller( Clusterer.display, this ) );
63     GEvent.addListener( map, 'infowindowclose', Clusterer.makeCaller( Clusterer.popDown, this ) );
64     //Set map for each marker
65     for(var i = 0,len = this.markers.length ; i < len ; i++){
66         this.markers[i].setMap( map );
67     }
68     this.displayLater();
69 }
70
71 Clusterer.prototype.remove = function(){
72      for ( var i = 0; i < this.markers.length; ++i ){
73          this.removeMarker(this.markers[i]);
74      }
75 }
76
77 Clusterer.prototype.copy = function(){
78     return new Clusterer(this.markers,this.icon,this.maxVisibleMarkers,this.gridSize,this.minMarkersPerCluster,this.maxLinesPerInfoBox);
79 }
80
81 Clusterer.prototype.redraw = function(force){
82     this.displayLater();
83 }
84
85 // Call this to change the cluster icon.
86 Clusterer.prototype.setIcon = function ( icon ){
87     this.icon = icon;
88 }
89
90 // Call this to add a marker.
91 Clusterer.prototype.addMarker = function ( marker, description){
92     marker.onMap = false;
93     this.markers.push( marker );
94     marker.description = marker.description || description;
95     if(this.map != null){
96         marker.setMap(this.map);
97         this.displayLater();
98     }
99 };
100
101
102 // Call this to remove a marker.
103 Clusterer.prototype.removeMarker = function ( marker ){
104     for ( var i = 0; i < this.markers.length; ++i )
105         if ( this.markers[i] == marker ){
106             if ( marker.onMap )
107                 this.map.removeOverlay( marker );
108             for ( var j = 0; j < this.clusters.length; ++j ){
109                 var cluster = this.clusters[j];
110                 if ( cluster != null ){
111                     for ( var k = 0; k < cluster.markers.length; ++k )
112                         if ( cluster.markers[k] == marker ){
113                             cluster.markers[k] = null;
114                             --cluster.markerCount;
115                             break;
116                         }
117                     if ( cluster.markerCount == 0 ){
118                         this.clearCluster( cluster );
119                         this.clusters[j] = null;
120                         }
121                     else if ( cluster == this.poppedUpCluster )
122                         Clusterer.rePop( this );
123                     }
124                 }
125             this.markers[i] = null;
126             break;
127             }
128     this.displayLater();
129 };
130
131 Clusterer.prototype.displayLater = function (){
132     if ( this.timeout != null )
133         clearTimeout( this.timeout );
134     this.timeout = setTimeout( Clusterer.makeCaller( Clusterer.display, this ), 50 );
135 };
136
137 Clusterer.display = function ( clusterer ){
138     var i, j, marker, cluster, len, len2;
139
140     clearTimeout( clusterer.timeout );
141
142     var newZoomLevel = clusterer.map.getZoom();
143     if ( newZoomLevel != clusterer.currentZoomLevel ){
144         // When the zoom level changes, we have to remove all the clusters.
145         for ( i = 0 , len = clusterer.clusters.length; i < len; ++i ){
146             if ( clusterer.clusters[i] != null ){
147                 clusterer.clearCluster( clusterer.clusters[i] );
148                 clusterer.clusters[i] = null;
149             }
150         }
151         clusterer.clusters.length = 0;
152         clusterer.currentZoomLevel = newZoomLevel;
153     }
154
155     // Get the current bounds of the visible area.
156     var bounds = clusterer.map.getBounds();
157
158     // Expand the bounds a little, so things look smoother when scrolling
159     // by small amounts.
160     var sw = bounds.getSouthWest();
161     var ne = bounds.getNorthEast();
162     var dx = ne.lng() - sw.lng();
163     var dy = ne.lat() - sw.lat();
164     dx *= 0.10;
165     dy *= 0.10;
166     bounds = new GLatLngBounds(
167       new GLatLng( sw.lat() - dy, sw.lng() - dx ),
168       new GLatLng( ne.lat() + dy, ne.lng() + dx ) 
169     );
170
171     // Partition the markers into visible and non-visible lists.
172     var visibleMarkers = [];
173     var nonvisibleMarkers = [];
174     for ( i = 0, len = clusterer.markers.length ; i < len; ++i ){
175         marker = clusterer.markers[i];
176         if ( marker != null )
177             if ( bounds.contains( marker.getPoint() ) )
178                 visibleMarkers.push( marker );
179             else
180                 nonvisibleMarkers.push( marker );
181     }
182
183     // Take down the non-visible markers.
184     for ( i = 0, len = nonvisibleMarkers.length ; i < len; ++i ){
185         marker = nonvisibleMarkers[i];
186         if ( marker.onMap ){
187             clusterer.map.removeOverlay( marker );
188             marker.onMap = false;
189         }
190     }
191
192     // Take down the non-visible clusters.
193     for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i ){
194         cluster = clusterer.clusters[i];
195         if ( cluster != null && ! bounds.contains( cluster.marker.getPoint() ) && cluster.onMap ){
196             clusterer.map.removeOverlay( cluster.marker );
197             cluster.onMap = false;
198         }
199     }
200
201     // Clustering!  This is some complicated stuff.  We have three goals
202     // here.  One, limit the number of markers & clusters displayed, so the
203     // maps code doesn't slow to a crawl.  Two, when possible keep existing
204     // clusters instead of replacing them with new ones, so that the app pans
205     // better.  And three, of course, be CPU and memory efficient.
206     if ( visibleMarkers.length > clusterer.maxVisibleMarkers ){
207         // Add to the list of clusters by splitting up the current bounds
208         // into a grid.
209         var latRange = bounds.getNorthEast().lat() - bounds.getSouthWest().lat();
210         var latInc = latRange / clusterer.gridSize;
211         var lngInc = latInc / Math.cos( ( bounds.getNorthEast().lat() + bounds.getSouthWest().lat() ) / 2.0 * Math.PI / 180.0 );
212         for ( var lat = bounds.getSouthWest().lat(); lat <= bounds.getNorthEast().lat(); lat += latInc )
213             for ( var lng = bounds.getSouthWest().lng(); lng <= bounds.getNorthEast().lng(); lng += lngInc ){
214                 cluster = new Object();
215                 cluster.clusterer = clusterer;
216                 cluster.bounds = new GLatLngBounds( new GLatLng( lat, lng ), new GLatLng( lat + latInc, lng + lngInc ) );
217                 cluster.markers = [];
218                 cluster.markerCount = 0;
219                 cluster.onMap = false;
220                 cluster.marker = null;
221                 clusterer.clusters.push( cluster );
222             }
223
224         // Put all the unclustered visible markers into a cluster - the first
225         // one it fits in, which favors pre-existing clusters.
226         for ( i = 0, len = visibleMarkers.length ; i < len; ++i ){
227             marker = visibleMarkers[i];
228             if ( marker != null && ! marker.inCluster ){
229                 for ( j = 0, len2 = clusterer.clusters.length ; j < len2 ; ++j ){
230                     cluster = clusterer.clusters[j];
231                     if ( cluster != null && cluster.bounds.contains( marker.getPoint() ) ){
232                         cluster.markers.push( marker );
233                         ++cluster.markerCount;
234                         marker.inCluster = true;
235                     }
236                 }
237             }
238         }
239
240         // Get rid of any clusters containing only a few markers.
241         for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i )
242             if ( clusterer.clusters[i] != null && clusterer.clusters[i].markerCount < clusterer.minMarkersPerCluster ){
243                 clusterer.clearCluster( clusterer.clusters[i] );
244                 clusterer.clusters[i] = null;
245             }
246
247         // Shrink the clusters list.
248         for ( i = clusterer.clusters.length - 1; i >= 0; --i )
249             if ( clusterer.clusters[i] != null )
250                 break;
251             else
252                 --clusterer.clusters.length;
253
254         // Ok, we have our clusters.  Go through the markers in each
255         // cluster and remove them from the map if they are currently up.
256         for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){
257             cluster = clusterer.clusters[i];
258             if ( cluster != null ){
259                 for ( j = 0 , len2 = cluster.markers.length ; j < len2; ++j ){
260                     marker = cluster.markers[j];
261                     if ( marker != null && marker.onMap ){
262                         clusterer.map.removeOverlay( marker );
263                         marker.onMap = false;
264                     }
265                 }
266             }
267         }
268         
269         // Now make cluster-markers for any clusters that need one.
270         for ( i = 0, len = clusterer.clusters.length; i < len; ++i ){
271             cluster = clusterer.clusters[i];
272             if ( cluster != null && cluster.marker == null ){
273                 // Figure out the average coordinates of the markers in this
274                 // cluster.
275                 var xTotal = 0.0, yTotal = 0.0;
276                 for ( j = 0, len2 = cluster.markers.length; j < len2 ; ++j ){
277                     marker = cluster.markers[j];
278                     if ( marker != null ){
279                         xTotal += ( + marker.getPoint().lng() );
280                         yTotal += ( + marker.getPoint().lat() );
281                     }
282                 }
283                 var location = new GLatLng( yTotal / cluster.markerCount, xTotal / cluster.markerCount );
284                 marker = new GMarker( location, { icon: clusterer.icon } );
285                 cluster.marker = marker;
286                 GEvent.addListener( marker, 'click', Clusterer.makeCaller( Clusterer.popUp, cluster ) );
287             }
288         }
289     }
290
291     // Display the visible markers not already up and not in clusters.
292     for ( i = 0, len = visibleMarkers.length; i < len; ++i ){
293         marker = visibleMarkers[i];
294         if ( marker != null && ! marker.onMap && ! marker.inCluster )
295         {
296             clusterer.map.addOverlay( marker );
297             marker.addedToMap();
298             marker.onMap = true;
299         }
300     }
301
302     // Display the visible clusters not already up.
303     for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){
304         cluster = clusterer.clusters[i];
305         if ( cluster != null && ! cluster.onMap && bounds.contains( cluster.marker.getPoint() )){
306             clusterer.map.addOverlay( cluster.marker );
307             cluster.onMap = true;
308         }
309     }
310
311     // In case a cluster is currently popped-up, re-pop to get any new
312     // markers into the infobox.
313     Clusterer.rePop( clusterer );
314 };
315
316
317 Clusterer.popUp = function ( cluster ){
318     var clusterer = cluster.clusterer;
319     var html = '<table width="300">';
320     var n = 0;
321     for ( var i = 0 , len = cluster.markers.length; i < len; ++i )
322         {
323         var marker = cluster.markers[i];
324         if ( marker != null )
325             {
326             ++n;
327             html += '<tr><td>';
328             if ( marker.getIcon().smallImage != null )
329                 html += '<img src="' + marker.getIcon().smallImage + '">';
330             else
331                 html += '<img src="' + marker.getIcon().image + '" width="' + ( marker.getIcon().iconSize.width / 2 ) + '" height="' + ( marker.getIcon().iconSize.height / 2 ) + '">';
332             html += '</td><td>' + marker.description + '</td></tr>';
333             if ( n == clusterer.maxLinesPerInfoBox - 1 && cluster.markerCount > clusterer.maxLinesPerInfoBox  )
334                 {
335                 html += '<tr><td colspan="2">...and ' + ( cluster.markerCount - n ) + ' more</td></tr>';
336                 break;
337                 }
338             }
339         }
340     html += '</table>';
341     clusterer.map.closeInfoWindow();
342     cluster.marker.openInfoWindowHtml( html );
343     clusterer.poppedUpCluster = cluster;
344 };
345
346 Clusterer.rePop = function ( clusterer ){
347     if ( clusterer.poppedUpCluster != null )
348         Clusterer.popUp( clusterer.poppedUpCluster );
349 };
350
351 Clusterer.popDown = function ( clusterer ){
352     clusterer.poppedUpCluster = null;
353 };
354
355 Clusterer.prototype.clearCluster = function ( cluster ){
356     var i, marker;
357
358     for ( i = 0; i < cluster.markers.length; ++i ){
359         if ( cluster.markers[i] != null ){
360             cluster.markers[i].inCluster = false;
361             cluster.markers[i] = null;
362         }
363     }
364     
365     cluster.markers.length = 0;
366     cluster.markerCount = 0;
367     
368     if ( cluster == this.poppedUpCluster )
369         this.map.closeInfoWindow();
370     
371     if ( cluster.onMap )
372     {
373         this.map.removeOverlay( cluster.marker );
374         cluster.onMap = false;
375     }
376 };
377
378 // This returns a function closure that calls the given routine with the
379 // specified arg.
380 Clusterer.makeCaller = function ( func, arg ){
381     return function () { func( arg ); };
382 };
383
384
385 // Augment GMarker so it handles markers that have been created but
386 // not yet addOverlayed.
387 GMarker.prototype.setMap = function ( map ){
388     this.map = map;
389 };
390
391 GMarker.prototype.getMap = function (){
392     return this.map;
393 }
394
395 GMarker.prototype.addedToMap = function (){
396     this.map = null;
397 };
398
399
400 GMarker.prototype.origOpenInfoWindow = GMarker.prototype.openInfoWindow;
401 GMarker.prototype.openInfoWindow = function ( node, opts ){
402     if ( this.map != null )
403         return this.map.openInfoWindow( this.getPoint(), node, opts );
404     else
405         return this.origOpenInfoWindow( node, opts );
406 };
407
408 GMarker.prototype.origOpenInfoWindowHtml = GMarker.prototype.openInfoWindowHtml;
409 GMarker.prototype.openInfoWindowHtml = function ( html, opts ){
410     if ( this.map != null )
411         return this.map.openInfoWindowHtml( this.getPoint(), html, opts );
412     else
413         return this.origOpenInfoWindowHtml( html, opts );
414 };
415
416 GMarker.prototype.origOpenInfoWindowTabs = GMarker.prototype.openInfoWindowTabs;
417 GMarker.prototype.openInfoWindowTabs = function ( tabNodes, opts ){
418     if ( this.map != null )
419         return this.map.openInfoWindowTabs( this.getPoint(), tabNodes, opts );
420     else
421         return this.origOpenInfoWindowTabs( tabNodes, opts );
422 };
423
424 GMarker.prototype.origOpenInfoWindowTabsHtml = GMarker.prototype.openInfoWindowTabsHtml;
425 GMarker.prototype.openInfoWindowTabsHtml = function ( tabHtmls, opts ){
426     if ( this.map != null )
427        return this.map.openInfoWindowTabsHtml( this.getPoint(), tabHtmls, opts );
428     else
429        return this.origOpenInfoWindowTabsHtml( tabHtmls, opts );
430 };
431
432 GMarker.prototype.origShowMapBlowup = GMarker.prototype.showMapBlowup;
433 GMarker.prototype.showMapBlowup = function ( opts ){
434     if ( this.map != null )
435         return this.map.showMapBlowup( this.getPoint(), opts );
436     else
437         return this.origShowMapBlowup( opts );
438 };
439
440
441 function addDescriptionToMarker(marker, description){
442     marker.description = description;
443     return marker;
444 }

Benjamin Mako Hill || Want to submit a patch?