Dans cet exemple, j’exploite deux cartes, rangées dans deux tabs. Elle dispose d’un système de filtres personnalisés, d’un panneau latéral dynamique qui permet d’afficher toutes les données GeoJSON au clic sur les marqueurs.
L’autre particularité ici c’est que les clusters ne se subdivisent pas au zoom maximal. La fonction clusters.on(‘clusterclick’, function() nous permet d’agir directement sur le cluster pour afficher les données sans avoir à afficher les marqueurs. Cela est utile lorsque plusieurs de vos marqueurs possèdent les mêmes coordonnées et qu’on veut éviter la spirale de marqueurs qui peut vite perdre l’utilisateur.
Les données récupérées dans les clusters sont insérées dans une fonction onEachFeature dans laquelle je supprime les données doublons. Cela me permet d’afficher les « features » en accordéon, en affichant une seule fois le nom de l’établissement par exemple.
La coloration des filtres et pictos sont liés au type de « features » sélectionnées.
Une version mobile améliorée
Dans la version mobile, la carte est centrée, les filtres ainsi que le panneau de données dynamiques apparaissent avec un effet de slide au clic sur les marqueurs ou le bouton de filtrage.
Pour réaliser cette carte, je me suis largement inspiré de ce tutoriel en anglais.
La création des données GeoJSON
Dans mon fichier JS, je charge toutes mes données en les structurant de la manière suivante :
//et plus bas dans le code var map = L.map('map')// ici l'ID du conteneur .addLayer(cartoDb) .setView([46.85, 2.3518], 6); var map2 = L.map('map2')// ici l'ID du deuxième conteneur .addLayer(cartoDb2) .setView([46.85, 2.3518], 6);
Création des marqueurs
Comme j’utilise ici le plugin awesome-marker.js, je vais attribuer une couleur à chaque « catégorie » de marqueurs. Mes catégories sont définies par la propriété « Examen » dans mes données GeoJSON.
Ce plugin utilise fontawesome pour l’apparence des icônes, ce qui explique la présence du préfixe. Voici un exemple : on choisit le préfixe de l’icône fontawesome, la couleur de l’icône, celle du marqueur et le type d’icône fontawesome.
On va ensuite récupérer nos données GeoJSON afin d’afficher nos marqueurs sur la carte, avec la fonction var L.geoJson(examenData, {}). Pour rappel, examenData est ici le nom de ma variable contenant toutes mes données GeoJSON.
A l’intérieur, on créer notre fonctions filter: elle va nous permettre de cibler précisément la « catégorie » de marqueurs souhaités. Ici par exemple, j’appelle mes marqueurs dont la « properties » est « examen » et dont la valeur (catégorie) est ccl18.
On appelle ensuite notre fonction onEachFeature: celle-ci agit comme une boucle nous permettant d’aller chercher le contenu de nos marqueurs.
Enfin on créer la fonction pointToLayerqui va placer nos marqueurs sur la carte et grâce à laquelle on choisit l’icône définie plus haut.
/* related filters and makers */ var ccl18 = L.geoJson(examenData, { filter: function(feature, layer) { return feature.properties.Examen == "ccl18"; }, onEachFeature: onEachFeature, pointToLayer: function(feature, latlng) {
return L.marker(latlng, { icon: ccl18Icon }) } })
L’affichage du html dans une div externe
Dans mon projet, plutôt que d’appeler une popup au clic sur le marqueur, je souhaite tout afficher dans une div externe.
Ma fonction onEachFeaturecontient donc tout le html que je souhaite afficher. En fait il reprend une à une chacune des propertiescorrespondant au marqueur ciblé. Dans cette boucle, le code suivant:
me permet d’afficher tout mon html grâce à la fonction jquery .html(). J’ai nommé ma variable contenant tout mon html « html » pour faire simple. Voici toute la fonction
function onEachFeature(feature, layer) {
var html = ''; if (feature.properties.Chu) { html += '<p class="chu">' + feature.properties.Chu + '</p>'; } if (feature.properties.Adresse) { html += '<p class="adress">' + feature.properties.Adresse + '</p>'; } if (feature.properties.Cp) { html += '<p class="cp">' + feature.properties.Cp + '</p>'; } if (feature.properties.Laboratoire) { html += '<p class="labo">' + feature.properties.Laboratoire + '</p>'; } if (feature.properties.Professeur) { html += '<p class="prof">' + feature.properties.Professeur + '</p>'; } if (feature.properties.Tel) { html += '<p class="tel">' + feature.properties.Tel + '</p>'; } if (feature.properties.Fax) { html += '<p class="fax">' + feature.properties.Fax + '</p>'; } if (feature.properties.Mail) { html += '<p class="mail"><a href="mailto:' + feature.properties.Mail + '">' + feature.properties.Mail + '</a></p>'; } if (feature.properties.Professeur2) { html += '<p class="prof2">' + feature.properties.Professeur2 + '</p>'; } if (feature.properties.Tel2) { html += '<p class="tel">' + feature.properties.Tel2 + '</p>'; } if (feature.properties.Fax2) { html += '<p class="fax">' + feature.properties.Fax2 + '</p>'; } if (feature.properties.Mail2) { html += '<p class="mail"><a href="mailto:' + feature.properties.Mail2 + '">' + feature.properties.Mail2 + '</a></p>'; } html += '<div class="pictos">'; if (feature.properties.Examen && feature.properties.Mode =="Tube") { html += '<span class="tube tube-' + feature.properties.Examen + '"></span>'; html += '<span class="' + feature.properties.Examen + '">' + feature.properties.Examen + '</span>'; }else if(feature.properties.Examen && feature.properties.Mode =="Buvard") { html += '<span class="buvard buvard-' + feature.properties.Examen + '"></span>'; html += '<span class="' + feature.properties.Examen + '">' + feature.properties.Examen + '</span>'; }else if(feature.properties.Examen){
html += '<span class="circle circle-' + feature.properties.Examen + '"></span>'; html += '<span class="' + feature.properties.Examen + '">' + feature.properties.Examen + '</span>'; } html += '</div>'; if (feature.properties.Prelevement) { html += '<p>' + feature.properties.Prelevement + '</p>'; } if (feature.properties.Envoi) { html += '<p>' + feature.properties.Envoi + '</p>'; } if (feature.properties.Renseignement) { html += '<p class="rt">' + feature.properties.Renseignement + '</p>'; } if (feature.properties.Url) { html += '<p class="url"><a href="' + feature.properties.Url + '">' + feature.properties.Url + '</a></p>'; } if (feature.properties.Doc) { html += '<p class="doc"><a href="' + feature.properties.Doc + '"><i class="fa fa-file-pdf-o" aria-hidden="true"></i> Télécharger la fiche</a></p>'; } layer.on('click', function() { $('#layer_infos .fill').html(html); if (L.Browser.mobile) { $('#infos').addClass("slide"); $('#filters').removeClass('slide'); $('.hamburger').text('Sélectionner un examen').fadeIn(); } }) }
Dans le code final du codePen, vous remarquerez que j’ai volontairement doublé cette fonction pour ma deuxième carte. Je suis obligé de cibler deux ID de div différentes, sinon dans l’inspecteur d’élément, toutes les actions sur la première carte se répercute sur ma deuxième carte. On peut certainement tout condenser, mais au moins comme ça c’est plus clair !
Créer les clusters
Les clusters sont particulièrement utiles lorsque dans une carte, plusieurs marqueurs affichent les mêmes coordonnées. Sans les marqueurs, les marqueurs vont se chevaucher, voir n’afficher que le dernier de la boucle. Grâce aux clusters, on va pouvoir afficher quelque chose de plus lisible avec notamment le nombre de marqueurs contenus à l’intérieur.
/*cluster for the first map */ var clusters = L.markerClusterGroup({ spiderfyOnMaxZoom: false, showCoverageOnHover: false, zoomToBoundsOnClick: true });
Le plugin markerCluster.js permet d’afficher ce cluster. Au zoom maximal, le cluster se déploie et affiche sous forme de spirale tous les marqueurs contenus à l’intérieur. Le rendu est le suivant :
Cette solution est largement suffisante. Mais si par exemple dans les données GeoJSON la plupart des informations sont communes d’un marqueurs à l’autre, le fait d’avoir deux marqueurs peut vite perdre l’utilisateur. Dans mon projet, mes marqueurs désignent des établissements de soins. J’aurais donc là la même adresse, nom d’hôpital, nom de médecin dans mes deux marqueurs. L’idée est donc de réussir à les regrouper, ou à rendre le cluster directement cliquables, et de n’afficher les données qu’une seule fois.
Rendre les clusters cliquables
Pour rendre le cluster directement cliquable et faire en sorte qu’il ne se subdivise pas en marqueurs, la première chose à faire est d’ajouter les paramètres :
spiderfyOnMaxZoom: false, : ici on annule l’effet de toile d’araignée et d’affichage des marqueurs showCoverageOnHover: false, ici on annule le surlignement en bleu par défaut qui indique la zone de couverture du cluster zoomToBoundsOnClick: true, ici on garde quand même le zoom automatique au clic sur le cluster
Ensuite, on va utiliser la fonction
clusters.on('clusterclick', function() {})
A l’intérieur on va remettre une fonction onEachFeature, qui va là encore servir de boucle pour remplir tout notre html dans notre div dynamique. Avec
if (a.layer._zoom == 18) {}
je précise qu’une fois le zoom maximal atteint et pas avant, j’affiche mes données.
Comment faire pour ne cibler que les marqueurs contenus dans mon cluster
Au clic sur le cluster, on souhaite n’afficher que les données des marqueurs contenus à l’intérieur du cluster. Vous rencontrerez peut-être le même problème que moi, c’est à dire qu’en cliquant sur le cluster, cela vous affiche les données de tous les marqueurs des la ou les carte. Pour limiter l’affichage aux seuls marqueurs concernés, j’ai procédé de la manière suivante :
On commence par ajouter un paramètre à notre fonction qui va cibler précisément notre cluster
clusters.on('clusterclick', function(a) {})
Ensuite on crée une boucle grâce à :
for (feat in a.layer._markers) {
On précise que pour chaque marqueur (feat in…_marker) contenu dans la couche (.layer) de notre cluster (a), j’appelle une à une mes properties. Voici la fonction en entier. Je commente encore une fois le code pour expliquer chaque étape.
clusters.on('clusterclick', function(a) { if (a.layer._zoom == 18) {
var html = ''; for (feat in a.layer._markers) {
if (a.layer._markers[feat].feature.properties['Chu']) { var chu = a.layer._markers[feat].feature.properties['Chu']; if (html.indexOf(chu) == -1) // ici j'enlève les doublons pour n'afficher le nom de l'établissement qu'une seule fois html += '<p class="chu">' + a.layer._markers[feat].feature.properties['Chu'] + '</p>'; }
if (a.layer._markers[feat].feature.properties['Adresse']) { var adresse = a.layer._markers[feat].feature.properties['Adresse']; if (html.indexOf(adresse) == -1)// ici j'enlève les doublons pour n'afficher l'adresse de l'établissement qu'une seule fois html += '<p class="adress">' + a.layer._markers[feat].feature.properties['Adresse'] + '</p>'; } if (a.layer._markers[feat].feature.properties['Cp']) { var cp = a.layer._markers[feat].feature.properties['Cp']; if (html.indexOf(cp) == -1)// ici j'enlève les doublons pour n'afficher le code postal de l'établissement qu'une seule fois html += '<p class="cp">' + a.layer._markers[feat].feature.properties['Cp'] + '</p>'; }
if (a.layer._markers[feat].feature.properties['Examen'] && a.layer._markers[feat].feature.properties['Mode'] =="Tube") { // ici j'utilise des conditions pour afficher un icône différent en fonction du type d'examen html += '<div class="pictos"><span class="tube tube-' + a.layer._markers[feat].feature.properties['Examen'] + '"></span><span class="' + a.layer._markers[feat].feature.properties['Examen'] + '">' + a.layer._markers[feat].feature.properties['Examen'] + '</span><i class="fas fa-chevron-down"></i></div>'; }else if (a.layer._markers[feat].feature.properties['Examen'] && a.layer._markers[feat].feature.properties['Mode'] =="Buvard") { html += '<div class="pictos"><span class="buvard buvard-' + a.layer._markers[feat].feature.properties['Examen'] + '"></span><span class="' + a.layer._markers[feat].feature.properties['Examen'] + '">' + a.layer._markers[feat].feature.properties['Examen'] + '</span><i class="fas fa-chevron-down"></i></div>'; }else if(a.layer._markers[feat].feature.properties['Examen']){ html += '<div class="pictos"><span class="circle-' + a.layer._markers[feat].feature.properties['Examen'] + '"></span><span class="' + a.layer._markers[feat].feature.properties['Examen'] + '">' + a.layer._markers[feat].feature.properties['Examen'] + '</span><i class="fas fa-chevron-down"></i></div>'; } html += '<div class="pictos-tabs picto-' + a.layer._markers[feat].feature.properties['Examen'] + '">';
// ensuite j'affiche toutes mes données qui peuvent différer d'un marqueur (établissement) à l'autre
if (a.layer._markers[feat].feature.properties['Laboratoire']) { html += '<p class="labo">' + a.layer._markers[feat].feature.properties['Laboratoire'] + '</p>'; } if (a.layer._markers[feat].feature.properties['Professeur']) { html += '<p class="prof">' + a.layer._markers[feat].feature.properties['Professeur'] + '</p>'; } if (a.layer._markers[feat].feature.properties['Tel']) { html += '<p class="tel">' + a.layer._markers[feat].feature.properties['Tel'] + '</p>'; } if (a.layer._markers[feat].feature.properties['Fax']) { html += '<p class="fax">' + a.layer._markers[feat].feature.properties['Fax'] + '</p>'; } if (a.layer._markers[feat].feature.properties['Mail']) { html += '<p class="mail"><a href="mailto:' + a.layer._markers[feat].feature.properties['Mail'] + '">' + a.layer._markers[feat].feature.properties['Mail'] + '</a></p>'; } if (a.layer._markers[feat].feature.properties['Professeur2']) { html += '<p class="prof2">' + a.layer._markers[feat].feature.properties['Professeur2'] + '</p>'; } if (a.layer._markers[feat].feature.properties['Tel2']) {
html += '<p class="tel">' + a.layer._markers[feat].feature.properties['Tel2'] + '</p>'; } if (a.layer._markers[feat].feature.properties['Fax2']) { html += '<p class="fax">' + a.layer._markers[feat].feature.properties['Fax2'] + '</p>'; } if (a.layer._markers[feat].feature.properties['Mail2']) { html += '<p class="mail"><a href="mailto:' + a.layer._markers[feat].feature.properties['Mail2'] + '">' + a.layer._markers[feat].feature.properties['Mail2'] + '</a></p>'; } if (a.layer._markers[feat].feature.properties['Prelevement']) { html += '<p>' + a.layer._markers[feat].feature.properties['Prelevement'] + '</p>'; } if (a.layer._markers[feat].feature.properties['Envoi']) { html += '<p>' + a.layer._markers[feat].feature.properties['Envoi'] + '</p>'; }
if (a.layer._markers[feat].feature.properties['Url']) { html += '<p class="url"><a href="' + a.layer._markers[feat].feature.properties['Url'] + '">'+ a.layer._markers[feat].feature.properties['Url'] + '</a></p>'; } if (a.layer._markers[feat].feature.properties['Doc']) { html += '<p class="doc"><a target="_blank" href="' + a.layer._markers[feat].feature.properties['Doc'] + '"><i class="fa fa-file-pdf-o" aria-hidden="true"></i> Télécharger la fiche</a></p>'; } html += '</div>'; }
$('#layer_infos .fill').html(html); if (L.Browser.mobile) { $('#infos').addClass("slide"); $('#filters').removeClass('slide'); $('.hamburger').text('Sélectionner un examen').fadeIn(); } } })
Créer ses propres fonctions de filtrage
Il me reste enfin à utiliser mes checkbox, qui me servent de filtre pour l’affichage des marqueurs sur la carte.
$("#ccl18").click(function() { //au clic sur la checkbox dont l'ID EST CCL18 if (this.checked) { // si c'est bien celui là qui est cliqué
ccl18.addTo(clusters); // ajoute le sur la carte : on utilise ici addTo(clusters) et non addTo(map) pour que les clusters agissent pour tous les marqueurs. map.fitBounds(allexamens.getBounds(), { // ici une fonction propre à mon projet : lorsqu'un autre filtre est coché, si je coche celui là, rezoom la carte pour l'afficher en entier padding: [50, 50] });
} else { clusters.removeLayer(ccl18); // si c'est pas cette checkbox ou si je clique sur une autre checkbox, retire ces marqueurs map.fitBounds(allexamens.getBounds(), { // pareil que précédemment padding: [50, 50] });
} });
Vu le nombre de données et de filtres, ajouté au fait que mon projet comprend deux cartes, le code final est extrêmement long ! Prenez ce qui vous semble utile pour créer votre propre carte personnalisée !
Voici le rendu final. Pour une meilleure lisibilité, ouvrez le Codepen en grand !