Les articles se font rare ici… On va essayer de corriger cela.

Au début de l’année je vous avais parlé de la nouvelle génération de web-carto qui utilise le WebGL pour le rendu. En ce moment, je travaille sur un petit projet mobile (hybride) qui doit être en mesure d’afficher pas mal de markers sur une carte et j’en profite donc pour partager mon expérience. Avec Leaflet, à partir d’un certain nombre d’éléments, l’affichage de la carte perd de sa fluidité (trop de DOM à gérer pour le petit processeur). Je me suis donc tourné vers l’excellent Mapbox GL JS qui utilise le Web GL et le GPU en lieu et place du DOM et du CPU.

Ma problématique était de pouvoir afficher des markers personnalisés sur la carte. Chose relativement simple sur Leaflet, notamment avec les extensions Leaflet.awesome-markers ou Leaflet.ExtraMarkers mais plus complexe avec MapboxGL. Il existe deux approches que je vais essayer de détailler.

Utiliser le DOM

Comme sur Leaflet on peut quand même utiliser les DOM pour afficher des markers personnalisés. C’est la solution la plus simple si vous n’avez pas trop d’éléments à afficher. Dans le cas contraire, la carte aura là aussi beaucoup de DOM à gérer et la fluidité va en prendre un coup.

Pour la démo, nous allons utiliser le CSS de l’extension pour Leaflet ExtraMarkers. Les données proviennent d’OSM, ce sont les 44 supermarchés autour de Grenoble (uniquement les éléments de type ‘node’). On crée une ‘div’ que l’on va superposer à la carte. C’est aussi simple que ça.

$.ajax({
    url: 'data/supermarket.geojson',
    dataType: 'JSON',
    success: function (geojson) {
        geojson.features.forEach(function (marker) {
            // create a DOM element for the marker
            let el = document.createElement('div');
            el.className = 'extra-marker extra-marker-circle-red';
            let icon = document.createElement('i');
            icon.className = 'fa fa-circle';
            icon.style.color = 'white';
            icon.className = 'fa fa-shopping-cart';
            el.appendChild(icon);
           
            var currentMarker = new mapboxgl.Marker(el, { offset: [-17, -42] });
             markersLayer.push(currentMarker);
                currentMarker.setLngLat(marker.geometry.coordinates)
                .addTo(map);
        });
    }
});

Dessiner les marker en WebGL

Maintenant on veut afficher les 5447 arbres du périmètre que l’on a exporté d’Openstreetmap avec l’overpassApi. Aucune chance de pouvoir faire cela avec la méthode précédente… Il va falloir les « dessiner » en WebGL.

Pour personnaliser les markers il va falloir créer notre propre « sprite ». Un « sprite » est une image contenant une multitude d’icônes, il est accompagné d’une fichier .json décrivant l’id et la position de l’icône.

Voici un exemple du sprite de maki et un extrait de son json qui lui est associé

  "aerialway-15": {
    "height": 15,
    "pixelRatio": 1,
    "width": 15,
    "x": 0,
    "y": 0
  },
  "airfield-11": {
    "height": 11,
    "pixelRatio": 1,
    "width": 11,
    "x": 191,
    "y": 105
  },
...

Pour créer notre sprite, on utilise spritezero-cli de Mapbox. Chaque fichier représentant le symbole doit être au format SVG et doit posséder un « id » qui servira de référence. Dans l’exemple, on utilise uniquement un SVG représentant un arbre qui va servir d’icône et un marker vert qui servira de fond.

Une fois nos 2 fichiers stockés dans un même dossier il suffit d’exécuter la commande :

spritezero sprites le_dossier_de_svg

Cela nous génère 2 fichiers : sprites.png et sprites.json. Mais malheureusement, à ma connaissance il n’est pas possible d’utiliser plusieurs sprites pour un même style. Il va donc falloir intégrer les autres icônes du ‘fond de carte’ que l’on veut utiliser. Dans l’exemple on va utiliser le style Basic-v9 de mapbox. On va donc ajouter dans notre dossier tous les SVG qui se trouvent dans le répertoire sprites/basic-v9/_svg  du dépôt GitHub mapbox/mapbox-gl-styles.

On relance la commande de spritezero  et une deuxième fois pour les écrans en haute définition:

spritezero sprites markers-demo/
spritezero --retina sprites@2x markers-demo/

À noter qu’il est possible d’ajouter dans le .json pour chaque symbole : « sdf »:true . Ceci permet de modifier la couleur du symbole, mais d’après mon expérience, cela provoque de l’alliasing pas très joli… Dommage

Il ne reste plus qu’a indiquer à Mapbox les sprites à utiliser. Pour cela on récupère le style basic-v9.json du dépôt Github de Mapbox puis on modifie la référence de notre sprite.

"sprite": "sprites",
"glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf",

On initialise la carte avec le style basic-v9.json situé à la racine:

var map = new mapboxgl.Map({
    container: 'map',
    style: 'basic-v9.json',
    center: [5.73551, 45.186189],
    zoom: 13
});

On définit la source des données (id: trees) et les deux layers (le marker et l’icône) qui vont utiliser cette même « source »

map.addSource("trees", {
    "type": "geojson",
    "data": {
        "type": "FeatureCollection",
        "features": []
    }
});

map.addLayer({
    "id": "marker",
    "type": "symbol",
    "source": "trees",
    "layout": {
        "icon-image": "marker-circle-green",
        "icon-allow-overlap": true,
        "icon-offset": [0, -12]
    }

});

map.addLayer({
    "id": "icon",
    "type": "symbol",
    "source": "trees",
    "layout": {
        "icon-image": "tree-white",
        "icon-ignore-placement": true,
        "icon-offset": [0, -18]
    }
});

Il ne reste plus qu’à charger les données (ici en utilisant jQuery)

$.ajax({
    url: 'data/trees.geojson',
    dataType: 'JSON',
    success: function (geojson) {
        map.getSource('trees').setData(geojson);
    }
});

Et voilà, nos 5500 arbres peuvent être affichés en toute fluidité.

Vous pouvez accéder à la démo ici et aux sources ici