Tracer la route avec Mapbox

Pourquoi choisir Mapbox ?

La lecture de l’article de Michael Introduction à Mapbox publié en septembre 2020 sur ce même blog, m’a donné envie de revoir l’approche que j’avais exposée dans mon propre post d’avril 2018, OSRM : le chemin le plus court pour tracer la route !

L’idée de base reste la même : proposer un composant web permettant de dessiner interactivement un tracé sur une carte, en asservissant ce tracé au réseau routier. A l’époque, je passais par la mise en place préalable d’un server OSRM et par l’utilisation du framework Leaflet pour obtenir un fond de carte basée sur OpenStreetMap. La partie OSRM était particulièrement lourde à mettre en place (même si simplifiée grâce à Docker), puisqu’elle obligeait à se construire son propre serveur de recherche d'itinéraire.

Mapbox offre une panoplie complète d’outils pour couvrir l’intégralité des services attendus pour la gestion cartographique, avec une politique commerciale basée sur un ‘fair use’ plutôt généreuse. Cela signifie que le service reste gratuit dès lors que l’utilisation reste mesurée. J’adore !

Je vais donc à nouveau construire un composant JavaScript permettant de tracer un chemin sur un fond cartographique tout en suivant le réseau routier disponible. Et comme d’habitude, je vais utiliser un des frameworks front le plus sous-estimé du moment : Ember.js.

Deux étapes pour deux types d’utilisation

Afin d’avoir la démarche la plus progressive possible, deux étapes de mise en place du projet seront considérées :

  1. Dans la première, nous nous intéresserons principalement à l’affichage de la carte et à la possibilité de tracer une polyligne, sans s’occuper du réseau routier sous-jacent.
  2. Nous ferons ensuite appel au service de MapBox pour recoller ce tracé à la route la plus proche.

Pour ceux qui veulent aller directement au code, chacune de ces étapes est disponible dans un projet Gitlab sur https://gitlab.ippon.fr/bpinel/mapbox-patheditor avec deux branches :

Etape 1 : Tracer des segments sur un fond cartographique

Création du projet et mise en place de Mapbox GL JS

Il faut bien sûr commencer par créer un projet web, et cela passe par Ember CLI et sa commande magique :

ember new --no-welcome mapbox-patheditor
cd mapbox-patheditor

(Bien entendu, il faut avoir installé Ember CLI au préalable)

L’installation de la bibliothèque Mapbox GL JS  se fait via l’add-on Ember à l’aide de la commande :

ember install ember-mapbox-gl

La configuration des paramètres d’initialisation de cette bibliothèque s’appuie sur le fichier config/environment.js en y ajoutant les lignes :

    mapboxToken: process.env.MAPBOX_TOKEN,
    'mapbox-gl': {
        accessToken: process.env.MAPBOX_TOKEN,
        map: {
            style: 'mapbox://styles/mapbox/basic-v9',
            zoom: 16,
            center: [ 2.3488, 48.8534 ]
        }
    },

A part fixer la valeur du token d’identification, on définit aussi au passage le style de carte à afficher, la coordonnée centrale (ici, au coeur de Paris) et le facteur de zoom.

Vous remarquerez que le token d’identification n’est pas directement intégré au code, mais passé par l’intermédiaire d’une variable d’environnement que vous devrez fixer, une fois votre compte Mapbox créé :

export MAPBOX_TOKEN=pk.xxxxxxxyyxyxyyxyxyxyxiuuuuuuuu

Création du composant de cartographie

Le setup du projet étant fait, on peut passer à la création d’un composant qui comportera un contrôleur JavaScript. En s’appuyant toujours sur Ember CLI, cela donne :

ember g component path-editor
ember g component-class path-editor

Dans la foulée, on va afficher ce nouveau composant dans la page principale de notre application en éditant le fichier application.hbs qui devient simplement :

<h2 id="title">Simple Path Editor</h2>
<PathEditor>Path Editor</PathEditor>
{{outlet}}

Reste à implémenter le composant !

Pour cela, on va commencer par ajouter dans la vue du composant la carte à l’aide de la balise <MapboxGl/> dans le fichier components/path-editor.hbs :

<div>{{yield}}</div>
{{! Second div to display the current latitude and longitude with binding attributes }}
<div>
    <p>Latitude: <code>{{lat}}</code> / Longitude: <code>{{lng}}</code></p>
</div>
{{! Right column to contain the map}}
<div class="flex">
    <MapboxGl class='map-container' initOptions=(hash pitch=30) as |map|>
        {{map.on 'click' (action 'mapClicked')}}
    </MapboxGl>
</div>

On remarque deux points importants :

  • Deux variables lat et lng sont affichées et devront être présentes dans le fichier JavaScript du composant,
  • le clic sur la carte est mappé sur une action nommée mapClicked qu’il s’agira d’implémenter dans la partie JavaScript.

On ajoute également dans le fichier css de l’application, situé dans styles/app.css les caractéristiques d’affichage de la page : dans le cas présent, on ne s’embarasse pas et on prend 100% du viewport !

.mapboxgl-map {  height: 100vh;  width: 100vw;}

Reste à coder le tracé du segment, en séparant le premier clic, qui génère un Marker côté MapBox des suivants qui décrivent la polyligne :

// components/path-editor.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import mapboxgl from 'mapbox-gl';

export default class PathEditorComponent extends Component {

    @tracked lat = 0
    @tracked lng = 0
    @tracked polyline = []
    startPoint
    geoJsonPolyline = {
        "geometry": {
        "coordinates": this.polyline,
        "type": "LineString"
        },
        "type": "Feature",
        "properties": {
            "name": "Cyclo path"
        }
    }
    initLayer = false

    @action
    async mapClicked({ target:map, point}) {
        let clickCoord = map.unproject(point);
        this.lat = clickCoord.lat;
        this.lng = clickCoord.lng;
        let newCoord = [this.lng, this.lat];
        this.polyline.push(newCoord);

        if (this.polyline.length == 1) {
            // First point special processing --> add a marker as starting point
            this.startPoint = new mapboxgl.Marker().setLngLat([this.lng, this.lat]).addTo(map);
        } else {
            if (!this.initLayer) {
                map.addSource('cycling-path', 
                    { 'type': 'geojson', 'data': this.geoJsonPolyline });
                map.addLayer({
                    'id': 'Drawn_cycling_path',
                    'type': 'line',
                    'source': 'cycling-path',
                    'layout': {
                        'line-cap': 'round'
                    },
                    'paint': {
                        'line-dasharray': [1, 2],
                        'line-color': '#f77',
                        'line-width': 3
                    }
                });
                this.initLayer=true;
            } else {
                map.getSource('cycling-path').setData(this.geoJsonPolyline);
            }
        }
    }
}

On retrouve les variables lat et lng, bindées sur la vue par l’annotation @tracked, ainsi que l’action mapClicked dont l’implémentation n’appelle que peu de commentaires hormis les suivants :

  • La polyligne suit le format GeoJSON,
  • Il est nécessaire de rafraîchir le tracé de la polyligne via un appel à la méthode setData() sur la source de la carte.

Il n’y a plus qu’à démarrer le serveur via un ember serve et se rendre sur http://localhost:4200 pour constater le résultat :

C'est fini pour cette première étape, mais le trajet est peu réaliste si l'on veut pouvoir le suivre !

 

Etape 2 : Suivre les routes !

Reste maintenant à asservir le tracé au réseau routier (ou plutôt cycliste dans notre cas !).

Pour être plus didactique, cet article va passer par les Web services exposés par Mapbox et plus particulièrement le service ‘Directions’.

Pour cela, le package axios est utilisé au travers d’un Service (au sens Ember du terme) encapsulant directement l’appel au Web Service.

Il convient donc dans un premier temps d’installer le package axios et de créer le service via les commandes :

npm install axios
ember g service mapbox-ws

L’implémentation du service s’appuie sur une surcharge du client axios, en configurant directement l’URL de base des services de Mapbox et déclarant une méthode asynchrone directions encapsulant le Web service du même nom :

// services/mapbox-ws.js
import Service from '@ember/service';
import axios from 'axios';
import config from '../config/environment';

export default class MapboxWsService extends Service {
    baseUrl = 'https://api.mapbox.com';
    token = config.mapboxToken;
    
    init() {
        super.init(...arguments);
        this.axios = axios.create(this.config());
        this.load(this.axios);
        this.request = this.axios.get;
        this.get = this.axios.get;
        this.delete = this.axios.delete;
        this.head = this.axios.head;
        this.options = this.axios.options;
        this.post = this.axios.post;
        this.put = this.axios.put;
        this.patch = this.axios.patch;
    }

    client() {
        return this.axios;
    }
    
    async direction(profile, coord1, coord2) {
        const coords = coord1[0]+','+coord1[1]+';'+coord2[0]+','+coord2[1]
        const urlOptimize = this.baseUrl+'/directions/v5/mapbox/'+profile+'/'+coords+'?geometries=geojson&access_token='+this.token;
        let response = await axios({
            method: 'get',
            url: urlOptimize,
            mode: 'no-cors',
            headers: {
                "Accept": "*/*",
                "content-type": "text/plain",
                "Access-Control-Allow-Origin": "*"
            }
        });
        if (response.status !== 200 && response.data.code !== 'Ok') {
            return [];
        }
        return response.data.routes[0].geometry.coordinates;
    }
    
    headers() {
        return {};
    }

    config() {
        return {
            baseURL: this.baseUrl,
            headers: this.headers()
        };
    }

    load() {
        return true;
    }
}

On peut alors bénéficier directement de la puissance de ce service dans le composant path-editor en modifiant le fichier path-editors.js via trois modifications mineures :

  • Ajout de l’import du service en tête du fichier et de :
import { inject as service } from '@ember/service';
  • Ajout de la variable d’accès au service dans le corps de la classe
@service mapboxWs;
  • Ajout du code situé entre les commentaires /**** Addition for following roads */et /***** End of addition *****/dans le corps du else pour appeler le service ‘Directions’ de Mapbox et insérer dans la polyligne tous les points intermédiaires correspondant aux segments de routes retournés (le reste du code de l'étape 1 reste inchangé) :
    if (this.polyline.length == 1) { // First point special processing --> add a marker as starting point
        this.startPoint = new mapboxgl.Marker().setLngLat([this.lng, this.lat]).addTo(map);
    } else {
        /**** Addition for following roads */
        // complete polyline by first retrieving new path though mapbox service
        let previousCoord = this.polyline[this.polyline.length - 2];
        let path = await this.mapboxWs.direction("cycling", previousCoord, newCoord);
        if (path.coords.length > 0) { // Insert intermediate coord in the polyline
            let last = this.polyline.pop();
            for (let i = 0; i < path.coords.length; i++) {
                this.polyline.push(path.coords[i]);
            }
        }
        // Don't put back last point that could be outside the road
        /***** End of addition : remaining code not changed *****/
        if (!this.initLayer) {

Le résultat est alors celui attendu en introduction à cet article :

La ligne suit maintenant le réseau routier mais également ses contraintes de circulation !

 

Mapbox : la boîte à outils cartographiques magique

Mapbox aurait pu s'appeler MagicMapBox tant elle met à la disposition du développeur une grande partie de l’outillage nécessaire aux traitements géographiques et cartographiques les plus ambitieux. Son approche commerciale est particulièrement habile et agréable puisque le ‘fair use’ est généreux. On peut par exemple passer 100 000 appels par mois au service ‘Directions’ ! Dépasser ces seuils semble donc réservé à des entreprises ayant des business solides et les moyens de payer pour plus d’appels.

Le risque à considérer est par contre de se lier fortement à une plateforme commerciale fermée dont on ne maîtrise ni la Roadmap, ni l’éventuelle évolution de politique de prix.

Mais pour le confort d’utilisation, l’ouverture aux développeurs et le large éventail de services fournis, je vote pour Mapbox et ne suis pas prêt dans l’immédiat à ré-installer un serveur OSRM sur ma machine !