Aller au contenu
Sylvain Joffraud l4w
Retour

Leaflet : `L.geoJSON()` n’est pas un layer mais un groupe

Pattern classique dans une carte Leaflet avec gestion de zones polygonales rechargées depuis une API : on charge, on dessine, on rafraîchit périodiquement, on supprime.

const zonesLayer = new L.FeatureGroup().addTo(map);
const zoneById = new Map();

async function loadZones() {
  // Cleanup avant rechargement
  for (const { layer } of zoneById.values()) zonesLayer.removeLayer(layer);
  zoneById.clear();

  for (const z of zones) {
    const layer = L.geoJSON(z.geometry, { style: ... });
    layer.eachLayer(l => {
      zonesLayer.addLayer(l);              // ← on ajoute les ENFANTS
      l.on('click', () => focus(z.id));
    });
    zoneById.set(z.id, { layer, data: z }); // ← on garde le PARENT en référence
  }
}

Logique apparente cohérente. L’utilisateur supprime une zone → loadZones() est rappelée → la zone devrait disparaître de la carte.

Sauf qu’elle ne disparaît pas.

Symptôme

Les polygones des zones supprimées restent affichés indéfiniment. Aucune erreur console. L’API renvoie bien la nouvelle liste sans la zone. loadZones() est bien rappelée, zoneById.clear() exécuté. Et pourtant la couche reste à l’écran.

Cause

L.geoJSON() ne retourne pas un layer atomique — c’est une L.FeatureGroup (sous-classe de L.LayerGroup) qui contient les couches individuelles (un layer par feature du GeoJSON).

Quand on fait layer.eachLayer(l => zonesLayer.addLayer(l)), on ajoute les enfants à zonesLayer. Le parent L.geoJSON n’est jamais ajouté à zonesLayer.

Donc au cleanup : zonesLayer.removeLayer(layer) cherche layer (le parent) dans zonesLayer.getLayers(), ne le trouve pas, et return silencieusement sans rien faire. Les enfants ajoutés directement restent en place.

C’est asymétrique : on a ajouté A et B (les enfants), on tente de retirer C (le parent). C n’a jamais été ajouté, retrait = no-op.

Solution

Cohérence : si on référence le parent dans le tracking, ajouter le parent à zonesLayer (la FeatureGroup propage addTo(map) à ses enfants automatiquement) :

for (const z of zones) {
  const layer = L.geoJSON(z.geometry, { style: ... });
  layer.eachLayer(l => l.on('click', () => focus(z.id))); // handlers seulement
  zonesLayer.addLayer(layer);                              // ← parent dans zonesLayer
  zoneById.set(z.id, { layer, data: z });
}

// cleanup OK : removeLayer(parent) retire tous les enfants en cascade
for (const { layer } of zoneById.values()) zonesLayer.removeLayer(layer);

Lessons learned

Questions fréquentes

Pourquoi mes polygones Leaflet ne disparaissent pas malgré removeLayer ?
Parce que removeLayer cible probablement un objet jamais ajouté au container. Si vous avez ajouté les enfants d’un GeoJSON avec eachLayer mais que vous tentez de retirer le parent L.geoJSON, Leaflet ne trouve pas le parent dans le groupe et retourne en silence sans rien faire. Les couches restent donc affichées indéfiniment, sans erreur console.
Est-ce que L.geoJSON() retourne un layer simple dans Leaflet ?
Non. L.geoJSON() retourne une L.FeatureGroup (sous-classe de L.LayerGroup) qui contient un layer individuel par feature du GeoJSON. Ce n’est pas une couche atomique : c’est un groupe avec des enfants, ce qui change la façon de l’ajouter et de le retirer de la carte.
Comment supprimer proprement un calque GeoJSON ajouté à une FeatureGroup ?
Restez cohérent entre l’ajout et le retrait sur le MÊME objet. Ajoutez le parent avec zonesLayer.addLayer(L.geoJSON(...)) (la FeatureGroup propage l’affichage à ses enfants), puis nettoyez avec zonesLayer.removeLayer(parent) qui retire tous les enfants en cascade. Réservez eachLayer uniquement à l’attachement de handlers ou de styles.
Pourquoi removeLayer ne renvoie aucune erreur quand le bug est là ?
Parce que removeLayer sur un layer absent du container est un no-op silencieux : pas d’exception, pas de warning. Le bug reste invisible jusqu’au test fonctionnel. Pour diagnostiquer, comparez zonesLayer.getLayers().length avant et après le cleanup : si le compteur ne descend pas, votre removeLayer cible le mauvais objet.

Partager cet article :

Article précédent
Traccar `position.attributes` : bien plus que lat/lon
Article suivant
Free Mobile SMS API : un canal de notif gratuit