// Clusterer.js - marker clustering routines for Google Maps apps // // Using these routines is very easy. // // 1) Load the routines into your code: // // // // 2) Create a Clusterer object, passing it your map object: // // var clusterer = new Clusterer( map ); // // 3) Wherever you now do map.addOverlay( marker ), instead call // clusterer.AddMarker( marker, title ). The title is just a // short descriptive string to use in the cluster info-boxes. // // 4) If you are doing any map.removeOverlay( marker ) calls, change those // to clusterer.RemoveMarker( marker ). // // That's it! Everything else happens automatically. // // // The current version of this code is always available at: // http://www.acme.com/javascript/ // // // Copyright © 2005,2006 by Jef Poskanzer . // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // 1. Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS // OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. // // For commentary on this license please see http://www.acme.com/license.html // Constructor. Clusterer = function ( map ) { this.map = map; this.markers = []; this.clusters = []; this.timeout = null; this.currentZoomLevel = map.getZoom(); this.maxVisibleMarkers = Clusterer.defaultMaxVisibleMarkers; this.gridSize = Clusterer.defaultGridSize; this.minMarkersPerCluster = Clusterer.defaultMinMarkersPerCluster; this.maxLinesPerInfoBox = Clusterer.defaultMaxLinesPerInfoBox; this.icon = Clusterer.defaultIcon; this.killDisplayStat = false ; this.Infodiv = "" ; GEvent.addListener( map, 'zoomend', Clusterer.MakeCaller( Clusterer.Display, this ) ); GEvent.addListener( map, 'moveend', Clusterer.MakeCaller( Clusterer.Display, this ) ); GEvent.addListener( map, 'infowindowclose', Clusterer.MakeCaller( Clusterer.PopDown , this ) ); }; Clusterer.defaultMaxVisibleMarkers = 150; Clusterer.defaultGridSize = 5; Clusterer.defaultMinMarkersPerCluster = 5; Clusterer.defaultMaxLinesPerInfoBox = 10; Clusterer.defaultIcon = new GIcon(); Clusterer.defaultIcon.image = 'http://www.acme.com/resources/images/markers/blue_large.PNG'; Clusterer.defaultIcon.shadow = 'http://www.acme.com/resources/images/markers/shadow_large.PNG'; Clusterer.defaultIcon.iconSize = new GSize( 30, 51 ); Clusterer.defaultIcon.shadowSize = new GSize( 56, 51 ); Clusterer.defaultIcon.iconAnchor = new GPoint( 13, 34 ); Clusterer.defaultIcon.infoWindowAnchor = new GPoint( 13, 3 ); Clusterer.defaultIcon.infoShadowAnchor = new GPoint( 27, 37 ); // Clusterer.prototype.killDisplay = function (n) { this.killDisplayStat = n; } // Call this to change the cluster icon. Clusterer.prototype.SetIcon = function ( icon ) { this.icon = icon; }; // Changes the maximum number of visible markers before clustering kicks in. Clusterer.prototype.SetMaxVisibleMarkers = function ( n ) { this.maxVisibleMarkers = n; }; // Sets the minumum number of markers for a cluster. Clusterer.prototype.SetMinMarkersPerCluster = function ( n ) { this.minMarkersPerCluster = n; }; // Sets the maximum number of lines in an info box. Clusterer.prototype.SetMaxLinesPerInfoBox = function ( n ) { this.maxLinesPerInfoBox = n; }; // Call this to add a marker. Clusterer.prototype.AddMarker = function ( marker, title, id ) { if ( marker.setMap != null ) marker.setMap( this.map ); marker.title = title ; marker.onMap = false; marker.id = id; this.markers.push( marker ); this.DisplayLater(); }; // Call this to remove a marker. Clusterer.prototype.RemoveMarker = function ( marker ) { for ( var i = 0; i < this.markers.length; ++i ) if ( this.markers[i] == marker ) { if ( marker.onMap ) this.map.removeOverlay( marker ); for ( var j = 0; j < this.clusters.length; ++j ) { var cluster = this.clusters[j]; if ( cluster != null ) { for ( var k = 0; k < cluster.markers.length; ++k ) if ( cluster.markers[k] == marker ) { cluster.markers[k] = null; --cluster.markerCount; break; } if ( cluster.markerCount == 0 ) { this.ClearCluster( cluster ); this.clusters[j] = null; } else if ( cluster == this.poppedUpCluster ) Clusterer.RePop( this ); } } this.markers[i] = null; break; } this.DisplayLater(); }; /********************** fonction d'origine Clusterer.PopUp = function ( cluster ) { var clusterer = cluster.clusterer; var html = ''; var n = 0; for ( var i = 0; i < cluster.markers.length; ++i ) { var marker = cluster.markers[i]; if ( marker != null ) { ++n; html += ''; if ( n == clusterer.maxLinesPerInfoBox - 1 && cluster.markerCount > clusterer.maxLinesPerInfoBox ) { html += ''; break; } } } html += '
'; if ( marker.getIcon().smallImage != null ) html += ''; else html += ''; html += '' + marker.title + '
...and ' + ( cluster.markerCount - n ) + ' more
'; clusterer.map.closeInfoWindow(); cluster.marker.openInfoWindowHtml( html ); clusterer.poppedUpCluster = cluster; }; **********************/ // modifiée 25/06/2007 15:13:09 // zoom sur les hotels contenu dans le cluster Clusterer.PopUp = function ( cluster ) { var bm = new GLatLngBounds() ; for ( var i = 0; i < cluster.markers.length; ++i ) { var marker = cluster.markers[i]; bm.extend( marker.getPoint() ); } clusterer.map.setZoom( clusterer.map.getBoundsZoomLevel( bm )); clusterer.map.setCenter(bm.getCenter()); } Clusterer.RePop = function ( clusterer ) { if ( clusterer.poppedUpCluster != null ) Clusterer.PopUp( clusterer.poppedUpCluster ); }; Clusterer.PopDown = function ( clusterer ) { clusterer.poppedUpCluster = null; }; Clusterer.prototype.ClearCluster = function ( cluster ) { var i, marker; for ( i = 0; i < cluster.markers.length; ++i ) if ( cluster.markers[i] != null ) { cluster.markers[i].inCluster = false; cluster.markers[i] = null; } cluster.markers.length = 0; cluster.markerCount = 0; if ( cluster == this.poppedUpCluster ) this.map.closeInfoWindow(); if ( cluster.onMap ) { this.map.removeOverlay( cluster.marker ); cluster.onMap = false; } }; // This returns a function closure that calls the given routine with the // specified arg. Clusterer.MakeCaller = function ( func, arg ) { return function () { func( arg ); }; }; Clusterer.prototype.DisplayLater = function () { if ( this.timeout != null ) clearTimeout( this.timeout ); this.timeout = setTimeout( Clusterer.MakeCaller( Clusterer.Display, this ), 50 ); }; Clusterer.Display = function ( clusterer ) { if( clusterer.killDisplayStat == true ){ // on stoppe l'execution de clustering clusterer.killDisplayStat = false ; return ; } var i, j, marker, cluster; clearTimeout( clusterer.timeout ); var newZoomLevel = clusterer.map.getZoom(); if ( newZoomLevel != clusterer.currentZoomLevel ) { // When the zoom level changes, we have to remove all the clusters. for ( i = 0; i < clusterer.clusters.length; ++i ) if ( clusterer.clusters[i] != null ) { clusterer.ClearCluster( clusterer.clusters[i] ); clusterer.clusters[i] = null; } clusterer.clusters.length = 0; clusterer.currentZoomLevel = newZoomLevel; } // Get the current bounds of the visible area. var bounds = clusterer.map.getBounds(); // Expand the bounds a little, so things look smoother when scrolling // by small amounts. var sw = bounds.getSouthWest(); var ne = bounds.getNorthEast(); var dx = ne.lng() - sw.lng(); var dy = ne.lat() - sw.lat(); if ( dx < 300 && dy < 150 ) { dx *= 0.10; dy *= 0.10; bounds = new GLatLngBounds( new GLatLng( sw.lat() - dy, sw.lng() - dx ), new GLatLng( ne.lat() + dy, ne.lng() + dx ) ); } // Partition the markers into visible and non-visible lists. var visibleMarkers = []; var nonvisibleMarkers = []; for ( i = 0; i < clusterer.markers.length; ++i ) { marker = clusterer.markers[i]; if ( marker != null ) if ( bounds.contains( marker.getPoint() ) ) visibleMarkers.push( marker ); else nonvisibleMarkers.push( marker ); } // Take down the non-visible markers. for ( i = 0; i < nonvisibleMarkers.length; ++i ) { marker = nonvisibleMarkers[i]; if ( marker.onMap ) { clusterer.map.removeOverlay( marker ); marker.onMap = false; } } // Take down the non-visible clusters. for ( i = 0; i < clusterer.clusters.length; ++i ) { cluster = clusterer.clusters[i]; if ( cluster != null && ! bounds.contains( cluster.marker.getPoint() ) && cluster.onMap ) { clusterer.map.removeOverlay( cluster.marker ); cluster.onMap = false; } } // Clustering! This is some complicated stuff. We have three goals // here. One, limit the number of markers & clusters displayed, so the // maps code doesn't slow to a crawl. Two, when possible keep existing // clusters instead of replacing them with new ones, so that the app pans // better. And three, of course, be CPU and memory efficient. if ( visibleMarkers.length > clusterer.maxVisibleMarkers ) { // Add to the list of clusters by splitting up the current bounds // into a grid. var latRange = bounds.getNorthEast().lat() - bounds.getSouthWest().lat(); var latInc = latRange / clusterer.gridSize; var lngInc = latInc / Math.cos( ( bounds.getNorthEast().lat() + bounds.getSouthWest().lat() ) / 2.0 * Math.PI / 180.0 ); for ( var lat = bounds.getSouthWest().lat(); lat <= bounds.getNorthEast().lat(); lat += latInc ) for ( var lng = bounds.getSouthWest().lng(); lng <= bounds.getNorthEast().lng(); lng += lngInc ) { cluster = new Object(); cluster.clusterer = clusterer; cluster.bounds = new GLatLngBounds( new GLatLng( lat, lng ), new GLatLng( lat + latInc, lng + lngInc ) ); cluster.markers = []; cluster.markerCount = 0; cluster.onMap = false; cluster.marker = null; clusterer.clusters.push( cluster ); } // Put all the unclustered visible markers into a cluster - the first // one it fits in, which favors pre-existing clusters. for ( i = 0; i < visibleMarkers.length; ++i ) { marker = visibleMarkers[i]; if ( marker != null && ! marker.inCluster ) { for ( j = 0; j < clusterer.clusters.length; ++j ) { cluster = clusterer.clusters[j]; if ( cluster != null && cluster.bounds.contains( marker.getPoint() ) ) { cluster.markers.push( marker ); ++cluster.markerCount; marker.inCluster = true; } } } } // Get rid of any clusters containing only a few markers. for ( i = 0; i < clusterer.clusters.length; ++i ) if ( clusterer.clusters[i] != null && clusterer.clusters[i].markerCount < clusterer.minMarkersPerCluster ) { clusterer.ClearCluster( clusterer.clusters[i] ); clusterer.clusters[i] = null; } // Shrink the clusters list. for ( i = clusterer.clusters.length - 1; i >= 0; --i ) if ( clusterer.clusters[i] != null ) break; else --clusterer.clusters.length; // Ok, we have our clusters. Go through the markers in each // cluster and remove them from the map if they are currently up. for ( i = 0; i < clusterer.clusters.length; ++i ) { cluster = clusterer.clusters[i]; if ( cluster != null ) { for ( j = 0; j < cluster.markers.length; ++j ) { marker = cluster.markers[j]; if ( marker != null && marker.onMap ) { clusterer.map.removeOverlay( marker ); marker.onMap = false; } } } } // Now make cluster-markers for any clusters that need one. for ( i = 0; i < clusterer.clusters.length; ++i ) { cluster = clusterer.clusters[i]; if ( cluster != null && cluster.marker == null ) { // Figure out the average coordinates of the markers in this // cluster. var xTotal = 0.0, yTotal = 0.0; for ( j = 0; j < cluster.markers.length; ++j ) { marker = cluster.markers[j]; if ( marker != null ) { xTotal += ( + marker.getPoint().lng() ); yTotal += ( + marker.getPoint().lat() ); } } var location = new GLatLng( yTotal / cluster.markerCount, xTotal / cluster.markerCount ); marker = new GMarker( location, { icon: clusterer.icon } ); cluster.marker = marker; GEvent.addListener( marker, 'click', Clusterer.MakeCaller( Clusterer.PopUp, cluster ) ); } } } // Display the visible markers not already up and not in clusters. for ( i = 0; i < visibleMarkers.length; ++i ) { marker = visibleMarkers[i]; if ( marker != null && ! marker.onMap && ! marker.inCluster ) { clusterer.map.addOverlay( marker ); if ( marker.addedToMap != null ) marker.addedToMap(); marker.onMap = true; } } //************* ici if (clusterer.Infodiv != null ); { var DisplayHotels = new Array(); for ( i = 0; i < visibleMarkers.length; ++i ) { marker = visibleMarkers[i]; if(marker.onMap && ! marker.inCluster) { DisplayHotels[DisplayHotels.length] = marker.id ; } } new Ajax.Updater( clusterer.Infodiv , clusterer.infoPageData , { evalScripts:'true', method : 'post', parameters: { 'Hotels[]' : DisplayHotels }} ); } // Display the visible clusters not already up. for ( i = 0; i < clusterer.clusters.length; ++i ) { cluster = clusterer.clusters[i]; if ( cluster != null && ! cluster.onMap && bounds.contains( cluster.marker.getPoint() ) ) { clusterer.map.addOverlay( cluster.marker ); cluster.onMap = true; } } // In case a cluster is currently popped-up, re-pop to get any new // markers into the infobox. Clusterer.RePop( clusterer ); }; // Augment GMarker so it handles markers that have been created but // not yet addOverlayed. GMarker.prototype.setMap = function ( map ) { this.map = map; }; GMarker.prototype.addedToMap = function () { this.map = null; }; GMarker.prototype.origOpenInfoWindow = GMarker.prototype.openInfoWindow; GMarker.prototype.openInfoWindow = function ( node, opts ) { if ( this.map != null ) return this.map.openInfoWindow( this.getPoint(), node, opts ); else return this.origOpenInfoWindow( node, opts ); }; GMarker.prototype.origOpenInfoWindowHtml = GMarker.prototype.openInfoWindowHtml; GMarker.prototype.openInfoWindowHtml = function ( html, opts ) { if ( this.map != null ) return this.map.openInfoWindowHtml( this.getPoint(), html, opts ); else return this.origOpenInfoWindowHtml( html, opts ); }; GMarker.prototype.origOpenInfoWindowTabs = GMarker.prototype.openInfoWindowTabs; GMarker.prototype.openInfoWindowTabs = function ( tabNodes, opts ) { if ( this.map != null ) return this.map.openInfoWindowTabs( this.getPoint(), tabNodes, opts ); else return this.origOpenInfoWindowTabs( tabNodes, opts ); }; GMarker.prototype.origOpenInfoWindowTabsHtml = GMarker.prototype.openInfoWindowTabsHtml; GMarker.prototype.openInfoWindowTabsHtml = function ( tabHtmls, opts ) { if ( this.map != null ) return this.map.openInfoWindowTabsHtml( this.getPoint(), tabHtmls, opts ); else return this.origOpenInfoWindowTabsHtml( tabHtmls, opts ); }; GMarker.prototype.origShowMapBlowup = GMarker.prototype.showMapBlowup; GMarker.prototype.showMapBlowup = function ( opts ) { if ( this.map != null ) return this.map.showMapBlowup( this.getPoint(), opts ); else return this.origShowMapBlowup( opts ); };