Statamic Peak

Article

Améliorez vos cartes avec Vuejs et Leaflet

Kévin Cluzel

0 min

Les problèmes arrivent quand on affiche trop de marqueurs au même endroit. Comment y remédier? Par l’utilisation d’un cluster.

Bonjour à tous. Cet article est la suite de notre premier article sur les cartes interactives que vous pourrez trouver ici:  https://www.conciergerie.dev/blog/creez-vos-cartes-interactives-avec-vuejs-et-leaflet/

Dans l'article précédent, nous avons appris à configurer notre carte et faire apparaitre des marqueurs personnalisés dessus. Les problèmes arrivent quand on a trop de marqueurs au même endroit: Imaginons tous les restaurants du monde qui apparaissent d’un coup où alors trop de restaurants à la même adresse (comme un centre commercial). Comment y remédier? Par l’utilisation d’un cluster.

Un cluster désigne un regroupement d'éléments au même endroit. Dans notre programme, il va se matérialiser par une icône spécifique qui, en cas de clic dessus, s’ouvrira pour révéler les éléments présents dedans.

Comment procéder? Il s’avère qu’il existe également une librairie pour vue2-leaflet permettant de faire ces clusters. Elle est cependant assez peu documentée et demande de se creuser un peu les méninges pour être utilisée efficacement: vue2-leaflet-markercluster.

npm install --save vue2-leaflet-markercluster

Une fois l'installation effectuée, on va importer la librairie dans notre fichier map.vue, l'enregistrer dans les components, et importer le style (obligatoire).

import Vue2LeafletMarkerCluster from 'vue2-leaflet-markercluster';
components: {
    LMap,
    LTileLayer,
    Restaurant,
    'v-marker-cluster': Vue2LeafletMarkerCluster
  },
<style>
  @import "~leaflet.markercluster/dist/MarkerCluster.css";
  .map {
    position: absolute;
    width: 100%;
    height: 100%;
    overflow :hidden
  }
  .cluster {
    position: absolute;
    margin-left: -20px;
    margin-top: -20px;
  }
</style>

Pour l'intégration dans le code, c'est très simple: On fait en sorte que le component v-marker-cluster englobe le component de nos icônes, soit <restaurant>, comme ceci:

<v-marker-cluster
      ref="cluster"
      :averageCenter="true"
      :ignoreHidden="true"
    >
      <restaurant
        v-for="marker in markers"
        :key="marker.id"
        :marker="marker"
      >
      </restaurant>
</v-marker-cluster>

On se retrouve alors avec un système de cluster. Fonctionnel certes, mais très honnêtement, avec un design qui laisse complètement à désirer. Il est heureusement possible de le modifier afin de coller au design général, ce que nous allons voir tout de suite.

Nous allons d'abord rajouter une propriété "options" à notre component v-marker-cluster

<v-marker-cluster
      ref="cluster"
      :averageCenter="true"
      :ignoreHidden="true"
      :options="clusterOptions"
    >

clusterOptions se trouve dans data (), et voici son contenu:

clusterOptions: {
          spiderfyDistanceMultiplier: 3,
          iconCreateFunction: cluster => {
              let clusterUsers = cluster.getAllChildMarkers().map(marker => marker.id)
              let clusterIconEl = new EnhancedClusterIcon({propsData: { clusterUsers }}).$mount().$el
              return divIcon({
                  html: clusterIconEl.outerHTML,
                  className: 'cluster',
                  iconSize: null
              })
          }
      }

spiderfyDistanceMultiplier est un paramètre servant à gérer la distance pris en compte pour faire rentrer les icônes dans le cluster.

Disclaimer: La partie qui arrive peut sembler un peu lourde ou complexe. J'essaye d'expliquer le processus de manière simple, mais pas de panique si tout ne semble pas clair: la customisation ne se fait pas à ce niveau, mais plus tard, dans une partie bien plus facile à appréhender.

iconCreateFunction se révèle être le nerf de la guerre: Cette méthode un peu compliquée à appréhender va nous permettre d'importer un composant Vue et de le transformer en icône de cluster. En lisant le code, on comprend ceci: On récupère les identifiants des markers inclus dans le cluster dans une variable clusterUsers.

On crée ensuite une variable clusterIconEl à partir d'un EnhancedClusterIcon qui est mount, auquel on passe en propriété le clusterUsers. clusterIconEl sera ensuite utilisé pour la création de l'Icon qui sera utilisée pour le cluster.

Mais qu'est ce que le EnhancedClusterIcon? Tout simplement l'extension à Vue d'un composant spécifique, qui contient le code de l'icône du cluster. On l'intègre comme ceci:

import ClusterIcon from './cluster-icon'

const EnhancedClusterIcon = Vue.extend(ClusterIcon);

delete Icon.Default.prototype._getIconUrl;
Icon.Default.mergeOptions({
    iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
    iconUrl: require('leaflet/dist/images/marker-icon.png'),
    shadowUrl: require('leaflet/dist/images/marker-shadow.png')
})

On notera la nécessité de supprimer l'icône de base manuellement dans notre code, comme montré ci-dessus

La partie explicative ardue se termine, nous pouvons maintenant voir notre fichier Vue cluster-icon, qui contiendra le code de notre icône:

<template>
  <div>
    <img src="https://img.icons8.com/doodle/48/000000/building--v1.png" class="cluster-img"/>
    <p class="cluster-text">noparse_0658db04e93cee3003cb2f02dc79321a</p>
  </div>
</template>

<script>
export default {
  props: {
    clusterUsers: {
      type: Array,
      required: true
    },
  },
  methods: {
    getNumber () {
      return this.$props.clusterUsers.length
    },
  }
}
</script>

<style>
  .cluster-img {
    float: left;
    width: 60px;
    height: 60px;
  }
  .cluster-text {
    margin:12px;
    width:40px;
    height:40px;
    border-radius:100%;
    background-color: #FEBF34;
    color:#fff;
    text-align:center;
    font-size:14px;
    overflow:hidden;
    line-height:40px;
  }
</style>

Plutôt simple non? L'image de notre choix, une fonction qui calcule le nombre de markers passés en paramètres pour l'afficher sous l'icône du cluster, un peu de CSS et le tour est joué!

En cas de clic sur l'icône, un zoom s'effectue, le cluster s'ouvre et libère les icônes, et si nécéssaire reste en place en mettant à jour le nombre d'éléments présents dedans. Voilà, nous avons réussi!

Petit bonus pour la route: Pourquoi ne pas ajouter de petites bulles explicatives en cas de clic sur une icône de restaurant? Allez c'est plutôt simple et assez pratique, je vous laisse le code ci dessous.

Pour le fichier restaurant.vue, il suffit d'importer LPopup de vue2-leaflet et de s'en servir pour par exemple afficher le nom du restaurant ainsi qu'un avis:

<template>
  <l-marker
    :key="marker.id"
    :lat-lng="marker.coordinates"
  >
    <l-icon ref="icon">
      <img class="restaurant-icon" :src="marker.imageUrl"/>
    </l-icon>
    <l-popup>
      <h3>noparse_f2da21fad6983e0817f27d1e28bea0bb</h3>
      <p>noparse_d56bef5e18558dd9265fee86e2d16a4c</p>
    </l-popup>
  </l-marker>
</template>

<script>
import { LIcon, LMarker, LPopup } from 'vue2-leaflet'
export default {
  components: { LIcon, LMarker, LPopup },
  props: {
    marker: {
      type: Object,
      required: true
    }
  }
}

</script>

<style>
  .restaurant-icon {
    height: 50px;
    margin-left: -15px;
    width: auto;
  }
</style>

Evidemment, ne pas oublier dans les data de map.vue de mettre à jour les markers avec les nouveaux grade:

markers: [
        {id: 1, name: 'Restaurant de Poisson', imageUrl: 'https://img.icons8.com/doodle/48/000000/fish-food--v1.png', coordinates: [ 49.114910, 6.178810 ], grade: 'Simpa mais sans plus'},
        {id: 2, name: 'Pizzeria', imageUrl: 'https://img.icons8.com/doodle/48/000000/pizza--v1.png' ,coordinates: [ 49.133290, 6.154370 ], grade: 'Le meilleur en ville'},
        {id: 3, name: 'Boulangerie', imageUrl: 'https://img.icons8.com/doodle/48/000000/croissant--v1.png', coordinates: [ 49.102160, 6.158850 ], grade: 'Super découverte'},
        {id: 4, name: 'Brunch', imageUrl: 'https://img.icons8.com/doodle/48/000000/the-toast--v2.png', coordinates: [ 49.136010, 6.199630 ], grade: 'J\'ai mis un logo champagne pour faire élégant'},
        {id: 5, name: 'Fast Food', imageUrl: 'https://img.icons8.com/doodle/48/000000/hamburger.png', coordinates: [ 49.105563, 6.182234 ], grade: 'Je pourrais donner un avis mais j\'ai un peu la flemme tout bien réfléchi'},
      ],

Je vous laisse découvrir le resultat pour vous même...

Nous en avons fini avec la cartographie sur Vue.js, je vous remercie d'avoir suivi ce tutorial, en espérant vous avoir donné envie d'utiliser ce super outil pour tous vos projets!

Vous trouverez l'ensemble du code source de ce projet sur mon Github.