Google Maps sous Android : garantir de bonnes performances

Standard

Article publié dans le magazine Programmez ! – Juillet 2014 – N° 176

La présence d’une carte est souvent nécessaire dans les applications Android, notamment des applications liées au transport, ou encore à certains réseaux sociaux indiquant où se trouvent des contacts. L’utilisateur est alors amené à parcourir la carte et à jouer avec le zoom pour situer des repères qui l’intéressent : un ami, un restaurant, un itinéraire… Dès que de nombreux repères sont à placer sur la carte, celle-ci risque d’être ralentie. Mais si la carte n’est pas fluide et se dessine par saccades, l’utilisateur peut vite être découragé et chercher une autre application. Quelles règles élémentaires respecter alors afin de garantir une bonne réactivité de la carte Google ?

Depuis la deuxième version de l’API Google Maps, fonctionnant avec les services Google Play, il est beaucoup plus simple de gérer les repères sur la carte grâce aux événements disponibles sur la classe centrale com.google.android.gms.maps.GoogleMap.

1/ Charger uniquement ce qui est visible à la caméra

Lorsque de nombreux repères sont à placer sur la carte, par exemple, des milliers de repères sur toute la France, une première règle fondamentale consiste à ne placer que ce que l’utilisateur est amené  à voir. Placer d’emblée tous les repères sur la carte  alourdira celle-ci qui se redessinera lentement et par saccades.  Il s’agit donc de placer les marqueurs en mode lazy en fonction des déplacements et des modifications de zoom de l’utilisateur.

lazy

 

1/1/ Déterminer les bornes géographiques affichées à l’écran

Afin de permettre déplacement et zooms, l’API Google utilise une caméra qui se focalise sur telle ou telle partie du monde. En sachant précisément sur quelle zone de la carte la caméra est focalisée, le développeur peut déterminer si le marqueur doit être ajouté à la carte, ou au contraire, si celui-ci n’est pas visible dans l’angle de la caméra et n’a donc pas besoin d’être ajouté. Pour cela, il dispose de la très utile classe Projection, disponible au sein de  l’API Google Maps V2 et qui agit comme une passerelle entre des coordonnées x et y à l’écran et une latitude et une longitude sur terre. Sans cette classe, le développeur aurait autrement à écrire de nombreux calculs permettant des conversions d’un système de coordonnées x et y (pixels) vers un système de projection de la surface terrestre. Tandis que via l’API, pour connaître les coordonnées géographiques visibles à l’écran, il suffit d’écrire :

GoogleMap map = getMap(); //obtient la carte via l’activité courante
LatLngBounds bounds = _map.getProjection().getVisibleRegion().latLngBounds;

 

Puis il peut tester si un point géographique est contenu dans la zone visible à l’écran par un simple appel :

boolean isVisible = bounds.contains(friend.getPosition());

if(isVisible){

MarkerOptions markerOptions = new MarkerOptions();
Marker addedMarker = _map.addMarker(markerOptions);

}

1/2/ Retirer les points qui ne sont plus visibles

Bien entendu, il faut également retirer les points auparavant ajoutés qui deviennent invisibles lors de déplacements. La classe GoogleMap n’expose pas de collection de marqueurs afin que celle-ci ne soit pas modifiée de l’extérieur, il convient donc de garder une référence vers la liste des marqueurs affichés sur la carte, à synchroniser dès qu’un élément est enlevé ou ajouté à la carte. Le développeur peut donc conserver deux listes en mémoire, une liste de l’ensemble des points à afficher (_allMarkers) et une liste des points qui sont sur la carte actuellement (_markersOnMap). Lorsque la caméra de la carte change de point de vue, il pourrait exécuter ce code :


LatLngBounds mapBounds = _map.getProjection().getVisibleRegion().latLngBounds;
           for (Marker marker : _allMarkers) {

                if (mapBounds.contains(marker.getPosition())) {
                      if (!_markersOnMap.contains(marker)) {

          Marker addedMarker = _map.addMarker(new MarkerOptions());
                                _markersOnMap.add(addedMarker);                          
                      }

                } else {

                     if (_markersOnMap.contains(marker)) {                                   
                           marker.remove();                                                   
                           _markersOnMap.remove(marker);
                     }
                 }
           }

D’aucuns diraient qu’il est plus simple d’effectuer à chaque fois un appel à _map.clear()pour ensuite rajouter les marqueurs visibles à l’écran.  Cette manière de faire entraînerait un effet de clignotement intempestif désagréable au moindre déplacement de l’utilisateur. C’est pourquoi il est préférable de procéder point par point.

1/3/ Conserver un certain naturel en élargissant les bornes

Cependant, l’utilisateur risque de se déplacer rapidement d’un point à l’autre, ou encore de revenir sur ses pas. Les repères vont alors s’afficher puis disparaître pour s’afficher une nouvelle fois, y compris lors de menus déplacements. Pour éviter cet effet d’apparition, il serait élégant de charger toujours un peu plus que ce qui est strictement visible à l’écran :

lazyelargi

Des stations qui seraient visibles à quelques pixels de déplacement près pourraient être ajoutées à la carte afin que l’utilisateur ne perçoive pas ces perpétuels ajouts et suppressions. De plus, cela permettrait d’améliorer quelque peu les performances en réduisant la fréquence des ajouts et des suppressions. Ainsi, simplement avant de rentrer dans la boucle du code ci-dessus du rendu des marqueurs, le développeur pourrait agrandir les bornes visibles à la caméra qui permettent de déterminer l’ajout ou non d’un marqueur :


 LatLngBounds mapBounds = _map.getProjection().getVisibleRegion().latLngBounds;
 LatLngBounds visibleBounds = GeoHelper.extendLimits(bounds, 3);
 for (Marker marker : _allMarkers) { etc…</span>

Le code de la fonction extendLimits pourrait simplement consister en une extension du rectangle de projection géographique :


public static LatLngBounds extendLimits(LatLngBounds bounds, int ratio) {

double extendedLongitude = Math.abs(bounds.northeast.longitude

                     - bounds.southwest.longitude)/ ratio;

double extendedLatitude = Math.abs(bounds.northeast.latitude

                     - bounds.southwest.latitude)/ ratio;

LatLng topRight = null;
 LatLng bottomLeft = null;

// Longitudes
 double topRightLongitude = bounds.northeast.longitude                    + extendedLongitude;

double bottomLeftLongitude = bounds.southwest.longitude                  - extendedLongitude;

// latitudes
 double topRightLatitude = bounds.northeast.latitude +   extendedLatitude;

double bottomLeftLatitude = bounds.southwest.latitude - extendedLatitude;

topRight = new LatLng(topRightLatitude, topRightLongitude);

bottomLeft = new LatLng(bottomLeftLatitude, bottomLeftLongitude);

return new LatLngBounds(bottomLeft, topRight);

}

2/ Regrouper les points lors de dézooms

Charger les points en mode lazy est une première étape. Mais si des milliers de points relativement proches sont à placer sur la carte, même si le rendu est circonscrit à la zone visible, une multitude de points se chevauchant risquent d’apparaître et de nuire à la lisibilité de la carte. C’est pour de tels scénarios que la technique du clustering est intéressante : au lieu d’afficher chaque point sur la carte, le développeur les regroupe en paquets plus ou moins importants selon le niveau de zoom. Ces paquets peuvent par exemple se représenter par des cercles, avec des chiffres, indiquant le nombre de repères contenus dans un paquet :

clusters

 

Or, depuis la version 2 de l’API Google Maps, des librairies open source ont émergé proposant des solutions de clustering toutes faites, et cependant extensibles. Deux librairies retiennent notamment l’attention, Google Maps Android API Utility et Android Maps Extension.

Des librairies de clustering extensibles

La première de ces deux librairies, Google Maps Android API Utility, embarque deux algorithmes différents de clustering, détaillés plus avant sur https://developers.google.com/maps/articles/toomanymarkers. Accessoirement, elle possède aussi des classes de rendu capables de lancer de jolies animations sur les clusters lorsque ceux-ci se multiplient ou se rejoignent.

  • Le premier algorithme, exprimé par la classe GridBasedAlgorithm, est le plus simple : les points sont placés dans une grille, dont la taille des cases varie en fonction du zoom. En fonction de la case dans laquelle les points se trouvent, ils sont regroupés entre eux et ne forment plus qu’un point au centre de la case. S’il est performant, cet algorithme peut s’avérer plutôt approximatif quant à la position des clusters. En effet, si deux points sont très rapprochés mais ne se situent pas dans la même case, ils seront chacun dispersés dans un cluster différent. De plus, lorsque les clusters sont nombreux, l’utilisateur aperçoit clairement une grille de points, parfaitement alignés, ce qui n’est pas toujours du meilleur effet. Ainsi, cet algorithme peut être mis en œuvre lorsque les performances sont cruciales et lorsque les points ne sont pas trop nombreux aux mêmes endroits.

gridalgo

  • Le second, exprimé par la classe NonHierarchicalDistanceBasedAlgorithm est plus complexe mais aussi plus intelligent. Chaque point est considéré comme un centre de gravité et un cluster potentiel. Ensuite, chaque point est évalué et ajouté à un cluster si sa distance est inférieure à une certaine valeur et sinon, il reste lui-même un cluster. Un ajustement a ensuite lieu si un point a été rattaché à un cluster alors qu’une plus petite distance encore le sépare d’un autre cluster. Cet algorithme est intéressant lorsque de nombreux points coexistent de manière rapprochée sur une carte.

distancealgo

Ainsi, on préfèrera utiliser le second algorithme, sans doute moins rapide à mettre en place, mais beaucoup plus précis et plus agréable à observer. Mais mettre en place un mécanisme de clusters ne dispense pas le développeur, qui possède beaucoup de points à placer, de charger ceux-ci en mode lazy comme indiqué précédemment. Malheureusement, Google Maps Android API Utility ne permet pas nativement de chargement des points en mode lazy. Cependant, la librairie est hautement extensible, elle fonctionne notamment par contrats. Il suffit alors d’étendre une interface chargée du rendu des points pour ajouter un fonctionnement lazy. L’API contient par défaut une classe DefaultClusterRenderer, il serait alors possible de créer un LazyClusterRenderer, qui se chargerait d’ajouter les points, comme vu précédemment, en fonction de leur visibilité à l’écran :

mapsUtilityRenderer

 

C’est la méthode onClustersChanged qui est surtout intéressante de surcharger car elle est automatiquement appelée par le gestionnaire de clusters de l’API, la classe ClusterManager, lorsque la caméra change de niveau de zoom. La librairie n’étant pas prévue pour un chargement lazy des points, il peut être nécessaire d’étendre ClusterManager,  notamment à cause de l’implémentation de cette méthode :


@Override
     public void onCameraChange(CameraPosition cameraPosition) {

if (mRenderer instanceof GoogleMap.OnCameraChangeListener) { ((GoogleMap.OnCameraChangeListener)mRenderer).onCameraChange(cameraPosition);

        }

        // Don't re-compute clusters if the map has just been panned/tilted/rotated.

        CameraPosition position = mMap.getCameraPosition();

        if (mPreviousCameraPosition != null && mPreviousCameraPosition.zoom == position.zoom) {

            return;

        }

        mPreviousCameraPosition = mMap.getCameraPosition();

        cluster();

    }

Comme le montre ce code et comme l’indique le commentaire, le ClusterManager ne provoquera pas de clustering si la caméra est déplacée. Or, si l’on veut charger les points en mode lazy, il faut appeler la méthode cluster quoi qu’il arrive. Cela ne signifie pas que l’algorithme sera exécuté à chaque fois, car, en coulisse, la librairie Google Maps Android Utility, suivant le pattern décorateur, conserve un cache des clusters rangés selon un niveau de zoom sous forme de dictionnaire clé-valeur, et ce, grâce à une classe décorateur, PreCachingAlgorithmDecorator. Cela signifie donc simplement qu’il faut, à chaque déplacement ou changement de zoom, passer les bons clusters au ClusterRenderer pour que les points se mettent à jour à l’écran.

mapsUtilityAlgoDecorator

 

La seconde librairie, Android Maps Extension, ne propose que le premier algorithme qui est basé sur le système de grilles. Elle a cependant l’avantage de proposer nativement un chargement lazy. Une application de test, Android Maps Extensions Demo, est à télécharger sur le store de Google. Si l’utilisateur se déplace vite, il peut alors voir les points se redessiner sur la carte, ce qui témoigne d’un chargement dynamique. L’effet de grille que l’on pourrait chercher à éviter est attenué dans cette démo, du fait de la relative dispersion des points sur le globe. Cette librairie est également extensible et le développeur peut très bien proposer sa propre implémentation de l’interface ClusteringStrategy, et implémenter l’algorithme basé sur les distances. Ainsi, à l’inverse de la librairie précédente, il n’aurait pas à implémenter de renderer car le chargement est déjà lazy, par contre, il aurait à fournir un autre algorithme pour éviter un effet de grille en cas de nombreux points proches.

Conclusion

Ainsi, on pourra retenir plusieurs bonnes pratiques lorsque de trop nombreux éléments sont à ajouter sur une carte Google Maps, laquelle risque d’être souvent manipulée par l’utilisateur. Le développeur favorisera le chargement des points en mode lazy pour éviter une carte surchargée, lente et peu réactive, surtout sur des téléphones d’entrée de gamme. Il convient aussi de garder en tête qu’il s’agit d’être le plus économe possible : de ne supprimer de la carte que ce qui n’y apparaîtra plus et d’éviter de tout supprimer pour tout ajouter à chaque fois.

Ce chargement dynamique ne dispense pas de mettre en place un mécanisme de clustering, notamment lorsque les points sont nombreux et rapprochés. Pour cela, depuis la version 2 de Google Maps pour Android, des librairies de clustering ont vu le jour et proposent différents mécanismes de clustering extensibles et personnalisables.

Advertisements

2 responses »

    • Merci pour votre commentaire. Le but ici était de dire ” je veux charger par exemple 1/3 (d’où le 3 en paramètre lors de l’appel) de mon rectangle en plus”, hors des limites de l’écran. Je ne voulais pas charger 3 fois le rectangle mais bien 1/3 de largeur et de la hauteur en plus de chaque côté. Après, si vous désirez charger davantage de surface à l’avance pourquoi pas réduire le paramètre ratio, ou bien multiplier si vous en avez envie.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s