Cluster marker (regrouper des markers)

Le but de ce tutorial est d’apprendre à regrouper ses markers en clusters (groupe de markers) afin d’alléger une application Google Map.

Avant de commencer, il est bon de préciser qu’il existe plusieurs techniques de clustering. Chaque application peut avoir ses propres besoins.
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
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>

5 Réponses à “Cluster marker (regrouper des markers)”

  1. Apprenti :

    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

  2. webmaster :

    Bonjour Apprenti,

    Non actuellement aucune image n’est disponible en téléchargement. Mais de quelles images parles-tu ?

  3. frank :

    bonjour webmaster, les fonctions sont appelées à quel niveau

  4. Clemchan :

    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\";

  5. Clemchan :

    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 ;)

Laisser une réponse

Security Code: