Le but de ce tutorial est d’apprendre à regrouper ses markers en clusters (groupe de markers) afin d’alléger une application Google Map.
Ici, nous verrons un exemple de clustering utilisant une grille fictive sur notre carte représentée par des cellules rectangulaires.
MarkerClusterer, ou comment regrouper des markers
Problématique :
vous disposez d’une application Google Map qui doit afficher un jeu de données conséquent.
Au-delà de 100 markers à afficher sur une carte, on peut considérer qu’il est préférable, avantageux et même conseillé de regrouper ses markers en clusters.
Le tutorial qui suit va tenter d’expliquer pas à pas la procédure à suivre afin de mettre en place ce clustering.
Regrouper vos markers sur la map (cluster)
Présentation du tutorial :
dans notre exemple, nous allons nous focaliser sur Paris. Nous disposons d’environ 700 adresses représentant des restaurant, hôtels et autres situés dans la capitale.
récupération des points stockés en bdd en php avec retour au format JSON
déclaration des variables utilisées au sein de la map
fonction d’initialisation de la map
fonction de clustering
initialisation de la map
fonctions liées aux markers
création d’un objet Rectangle pour les tests d’affichage sur la map
Récupération des points stockés en bdd en php avec retour au format JSON
Ce script PHP retourne un objet JSON contenant l’ensemble des points ainsi que leurs informations (adresse, logo, nom …). Il est à placer en début de page, avant les appels javascript.
<?php ## Coordonnées des coins du rectangle rouge (limite de notre zone de d'affichage) $nelng = 2.44171142578125; $swlng = 2.22198486328125; $nelat = 48.90625412315376; $swlat = 48.80686346108517; ## Connexion à la Base de Données $db = mysql_connect(serveur, utilisateur, motdepasse); $select = mysql_select_db(basededonnees, $db); ## Récupération des points inclus dans notre limite (rectangle rouge) $result = mysql_query(" SELECT lng,lat, Titre, Image, Adresse FROM table WHERE (lng > $swlng AND lng < $nelng) AND (lat < $nelat AND lat > $swlat) ORDER BY RAND() "); $list = array(); $i = 0; $row = mysql_fetch_assoc($result); while($row) { $i++; extract($row); ## Construction de l'objet javascript au format JSON $list[] = "p{$i}:{ lat:{$lat}, lng:{$lng}, Titre:'".addslashes(utf8_encode($Titre))."', Image: '".$Image."', Adresse: '".addslashes(utf8_encode($Adresse))."' }"; $row = mysql_fetch_assoc($result); } //Retourne un objet javascript formaté (JSON) header('content-type:text/plain;'); echo "var points = {\n\t".join(",\n\t",$list)."\n}"; ?>
Déclaration des variables utilisées dans notre application
<script type="text/javascript"> // Variables utilisées sur la map var map; var centerLatitude = 48.8566667; var centerLongitude = 2.3299871; var startZoom = 11; // déclaration de l'icône pour les clusters var iconCluster = new GIcon(); iconCluster.shadow = "images/cluster_shadow.png"; iconCluster.shadowSize = new GSize(22, 20); iconCluster.iconAnchor = new GPoint(13, 25); iconCluster.infoWindowAnchor = new GPoint(13, 1); iconCluster.infoShadowAnchor = new GPoint(26, 13); // déclaration de l'icône pour un marker simple var iconSingle = new GIcon(); iconSingle.image = "images/single.png"; iconSingle.shadow = "images/single_shadow.png"; iconSingle.iconSize = new GSize(12, 20); iconSingle.shadowSize = new GSize(22, 20); iconSingle.iconAnchor = new GPoint(6, 20); iconSingle.infoWindowAnchor = new GPoint(6, 1); iconSingle.infoShadowAnchor = new GPoint(13, 13); </script>
Création de l’objet de type GMap2 (notre carte)
<script type="text/javascript"> function init(){ // Déclaration de la map map = new GMap2(document.getElementById("map"),{mapTypes: [G_HYBRID_MAP]}); // Chargement de la map map.setCenter(new GLatLng(centerLatitude, centerLongitude), startZoom); // Ajout des outils de contrôle par défaut (zoom, ...) map.setUIToDefault(); // Désactivation du zoom possible avec la molette de la souris map.disableScrollWheelZoom(); // Mise à jour de la map updateMarkers(); // Recharge la carte après zoom (+ ou -) GEvent.addListener(map,'zoomend',function() { updateMarkers(); }); // Décommenter l'écouteur suivant pour recharger la carte après un drag / drop // GEvent.addListener(map,'moveend',function() { // updateMarkers(); // }); } </script>
Fonction chargée de regrouper les markers (clusters)
<script type="text/javascript"> function updateMarkers() { // Mise à zéro de la map (on efface tout ce qui s'y trouve) map.clearOverlays(); // Création de l'aire délimitant les données à afficher (rectangle rouge) // utile en phase de test var allsw = new GLatLng(48.80686346108517, 2.22198486328125); var allne = new GLatLng(48.90625412315376, 2.44171142578125); var allmapBounds = new GLatLngBounds(allsw,allne); // affichage de l'aire délimitant l'affichage des données (rectangle rouge) // commenter cette ligne pour ne pas l'afficher map.addOverlay(new Rectangle(allmapBounds,3,"#F00",'')); // Délimitation de la map var mapBounds = map.getBounds(); // coordonnées SW de la map (sur la taille du div id = "map") var sw = mapBounds.getSouthWest(); // coordonnées NE de la map (sur la taille du div id = "map") var ne = mapBounds.getNorthEast(); // Size est une aire représentant les dimensions du rectangle de la map en degrés var size = mapBounds.toSpan(); //retourne un objet GLatLng // créé une cellule de 10x10 pour constituer notre "grille" // les cellules de cette grille sont ici affichées en bleu var gridSize = 10; var gridCellSizeLat = size.lat()/gridSize; var gridCellSizeLng = size.lng()/gridSize; // cellGrid représente un tableau qui contiendra l'ensemble // des cellules dans lesquelles se trouveront des points // - dans notre exemple, nous aurons 19 cellules (rectangles bleus) var cellGrid = []; //Parcourt l'ensemble des points et les assigne à la cellule concernée for (k in points) { var latlng = new GLatLng(points[k].lat,points[k].lng); // on vérifie si le point appartient à notre zone // la zone correspond à la map totale affichée // si le point n'appartient pas la map en cours on passe au point siuvant if(!mapBounds.contains(latlng)) continue; // On créé un rectangle temporaire en fonction des coordonnées // du point et de celles de la map afin d'obtenir notre cellule (cell) var testBounds = new GLatLngBounds(sw,latlng); var testSize = testBounds.toSpan(); var i = Math.ceil(testSize.lat()/gridCellSizeLat); var j = Math.ceil(testSize.lng()/gridCellSizeLng); // cell peut être comparée à une case d'échiquier var cell = new Array(i,j); // Si cette case (cellule) n'a pas encore été créée (undefined) // on l'ajoute à notre grille ( = tableau de cellules = échiquier) if(typeof cellGrid[cell] == 'undefined') { var lat_cellSW = sw.lat()+((i-1)*gridCellSizeLat); var lng_cellSW = sw.lng()+((j-1)*gridCellSizeLng); // coordonnées sud-ouest de notre cellule var cellSW = new GLatLng(lat_cellSW, lng_cellSW); var lat_cellNE = cellSW.lat()+gridCellSizeLat var lng_cellNE = cellSW.lng()+gridCellSizeLng; // coordonnées nord-est de notre cellule var cellNE = new GLatLng(lat_cellNE, lng_cellNE); // Déclaration de la cellule et de ses propriétés (cluster ou non, points ...) cellGrid[cell] = { GLatLngBounds : new GLatLngBounds(cellSW,cellNE), cluster : false, markers:[], lt:[], lg:[], titre:[], adresse:[], image:[], length: 0 }; // Ajoute la cellule (rectangle bleu) à la carte // utile en phase de test map.addOverlay(new Rectangle(cellGrid[cell].GLatLngBounds,1,"#00F",'')); } // augmentation du nombre de cellules sur la grille ( = 1 cellule en plus) cellGrid[cell].length++; // Si la cellule contient au moins 2 points, nous décidons ici // que les markers seront clustérisés pour cette cellule if(cellGrid[cell].markers.length > 1) // On passe alors à true la propriété cluster de la cellule cellGrid[cell].cluster = true; // On lui renseigne ensuite les propriétés du point concerné cellGrid[cell].lt.push(points[k].lat); cellGrid[cell].lg.push(points[k].lng); cellGrid[cell].markers.push(latlng); cellGrid[cell].titre.push(points[k].Titre); cellGrid[cell].adresse.push(points[k].Adresse); cellGrid[cell].image.push(points[k].Image); } // On parcourt l'ensemble des cellules de notre grille (cases de l'échiquier) for (k in cellGrid) { // Si les markers de la cellule doivent apparaître sous forme de cluster if(cellGrid[k].cluster == true) { // création d'un marker au centre de la cellule var span = cellGrid[k].GLatLngBounds.toSpan(); var sw = cellGrid[k].GLatLngBounds.getSouthWest(); var swLAT_span = sw.lat()+(span.lat()/2); var swLNG_span = sw.lng()+(span.lng()/2); var marker = createMarker(new GLatLng(swLAT_span,swLNG_span),'c',cellGrid[k]); } else { // Sinon, création d'un marker simple for(i in cellGrid[k].markers) var marker = createMarker(cellGrid[k].markers[i],'p',cellGrid[k]); } } } </script>
Initialisation de la map
<script type="text/javascript"> // Chargement de la map une fois la page chargée window.onload = init; </script>
Fonctions liées aux markers
À noter que les fonctions ci-dessous ont été simplifiées par rapport à l’exemple de l’application. La différence se trouve dans l’affichage de l’infobulle.
La fonction createMarker n’affichera ici que les noms des résultats trouvés et non l’image (cliquable pour zoom sur un marker) ni l’adresse.
<script type="text/javascript"> function createMarker(point, type,infoMarker) { // Si le marker représente plusieurs points (cluster) if(type=='c') { // Pour définir le marker du cluster, nous faisons appel à un script PHP, // non détaillé ici, chargé de récupérer le nombre de markers concernés par ce cluster, // infoMarker.length. // Nous créons alors un cercle à l'aide la librairie GD2 du php // qui aura une couleur et une taille définies en fonction du nombre de markers. iconCluster.image = 'https://www.weboblog.fr/icone-marker.php?nb='+infoMarker.length; var marker = new GMarker(point,iconCluster); } else // Sinon, il s'agit d'un marker isolé (simple) var marker = new GMarker(point,iconSingle); // On ajoute le marker à la map map.addOverlay(marker); // avec son écouteur d'événement clickMarker(marker,infoMarker); } // fonction écouteur d'événement pour un marker function clickMarker(marker,infoMarker){ var info = (infoMarker.length < 2) ? '' : infoMarker.length+' résultats <br />'; GEvent.addListener(marker, "click", function() { // ici, contenu de l'infobulle = listing des résultats for(var n = 0; n < infoMarker.length; n++) info += infoMarker.titre[n]+'<br />'; marker.openInfoWindowHtml(info); }); } </script>
création d’un objet Rectangle pour les tests d’affichage sur la map
<script type="text/javascript"> // Definition de l'objet RECTANGLE pour les phases de développement / debugging ... function Rectangle(bounds, opt_weight, opt_color, opt_html) { this.bounds_ = bounds; this.weight_ = opt_weight || 1; this.html_ = opt_html || ""; this.color_ = opt_color || "#888888"; } Rectangle.prototype = new GOverlay(); Rectangle.prototype.initialize = function(map) { var div = document.createElement("div"); div.innerHTML = this.html_; div.style.border = this.weight_ + "px solid " + this.color_; div.style.position = "absolute"; map.getPane(G_MAP_MAP_PANE).appendChild(div); this.map_ = map; this.div_ = div; } Rectangle.prototype.remove = function() { this.div_.parentNode.removeChild(this.div_); } Rectangle.prototype.copy = function() { return new Rectangle( this.bounds_, this.weight_, this.color_, this.backgroundColor_, this.opacity_ ); } Rectangle.prototype.redraw = function(force) { if (!force) return; var c1 = this.map_.fromLatLngToDivPixel(this.bounds_.getSouthWest()); var c2 = this.map_.fromLatLngToDivPixel(this.bounds_.getNorthEast()); this.div_.style.width = Math.abs(c2.x - c1.x) + "px"; this.div_.style.height = Math.abs(c2.y - c1.y) + "px"; this.div_.style.left = (Math.min(c2.x, c1.x) - this.weight_) + "px"; this.div_.style.top = (Math.min(c2.y, c1.y) - this.weight_) + "px"; } </script>
2 juin 2010 à 11 h 36 min
Bonjour,
Les images que vous utilisez sont-elles disponibles dans un fichier ou un lien de téléchargement?
En vous remerciant d’avance,
Apprenti
10 juin 2010 à 7 h 40 min
Bonjour Apprenti,
Non actuellement aucune image n’est disponible en téléchargement. Mais de quelles images parles-tu ?
4 juillet 2011 à 14 h 52 min
bonjour webmaster, les fonctions sont appelées à quel niveau
20 septembre 2011 à 17 h 26 min
Désolé de ce déterrage de dossier mais à mon avis, apprenti parlait soit des images dont tu récupères le nom dans ta BDD, soit de celles que tu appelles la :
iconSingle.image = \"images/single.png\";
iconSingle.shadow = \"images/single_shadow.png\";
20 septembre 2011 à 17 h 27 min
Bonjour,
Désolé de ce déterrage de dossier mais à mon avis, apprenti parlait soit des images dont tu récupères le nom dans ta BDD, soit de celles que tu appelles la :
iconSingle.image = \"images/single.png\";
iconSingle.shadow = \"images/single_shadow.png\";
Bonne journée