Création d’itinéraires avec Flutter et Google Maps


Avatar de Pierre Courtois

Tracer un itinéraire peut être une fonction très important selon le but de votre application Flutter. Dans ce guide, je vous explique comment faire des tracés sur une carte Google Maps, avec le package Polyline et estimer le temps de celui-ci, selon le mode de déplacement.


Mise en place de polyline pour tracer des itinéraires

Maintenant que vous avez ajouté une carte Google maps à votre application Flutter et êtes capable de lui ajouter des marqueurs, la suite va être de tracer un itinéraire entre ceux-ci.

La première étape va être de configurer votre clé API dans Google Cloud, ajouter des autorisations nécessaires pour Android et iOS, et intégrer le package flutter_polyline_points à votre application Flutter pour générer l’itinéraire.

Configurer vote clé API dans Google Cloud

Avant de pouvoir interagir avec l’API Directions et générer un itinéraire, vous devez configurer correctement votre clé API dans la Google Cloud Console. Pour commencer, ajoutez les services Directions API et Routes API.

Puis, restreignez votre clé API à cette liste de services et copiez là :

  • Maps SDK for Android
  • Places API
  • Maps JavaScript API
  • Maps Embed API
  • Maps SDK for iOS
  • Directions API
  • Routes API
  • Roads API
  • Geocoding API
  • Geolocation API
  • Street View Static API
  • Distance Matrix API

Ces API sont essentielles pour permettre à votre application d’accéder à des fonctionnalités de cartographie, de géolocalisation et de création d’itinéraires.

Attention, votte clé ne doit pas être restreinte pour Android ou iOS, sinon la création d’un itinéraire ne fonctionnera pas. Vous aurez une erreur Exception: Unable to get route: Response —> REQUEST_DENIED.

Configurer Android

Pour Android, vous devrez ajouter quelques autorisations pour permettre l’accès à la localisation de l’utilisateur. La seule chose que vous aurez à faire est de mettre à jour le fichier AndroidManifest.xml en ajoutant ces lignes si elles ne sont pas déjà présentes :

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>

Configurer IOS

Pour IOS, vous n’aurez pas besoin de demander des permissions, mais vous devrez spécifier les raisons pour lesquelles votre application nécessite l’accès à la localisation.

Pour cela, rendez vous dans le fichier Info.plist de votre application et ajoutez les permissions qui suivent :

<key>NSLocationWhenInUseUsageDescription</key>
<string>Cet app nécessite l'accès à la localisation lorsque l'application est ouverte.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Cet app nécessite l'accès à la localisation en permanence et lorsque l'application est ouverte.</string>
<key>NSLocationUsageDescription</key>
<string>Les anciens appareils nécessitent l'accès à la localisation.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Cet app nécessite l'accès à la localisation en arrière-plan.</string>

Puis, pensez aussi à configurer correctement Google Maps dans le fichier AppDelegate.swift en ajoutant l’importation et la configuration de la clé API comme suit :

import GoogleMaps

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GMSServices.provideAPIKey("VOTRE_CLÉ_API") // Remplacez cette valeur par votre clé
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Intégration du package flutter polyline points

Enfin, la dernière étape va être d’installer et configurer le package flutter_polyline_points pour générer l’itinéraire entre deux marqueurs.

Dans votre fichier pubspec.yaml, ajoutez la dernière version (2.1.0) du package :

dependencies:
  flutter:
    sdk: flutter
  google_maps_flutter: ^2.1.0
  flutter_polyline_points: ^2.1.0

Ou utilisez cette commande dans le terminal :

flutter pub add flutter_polyline_points

Puis importez le package dans votre fichier Dart où vous souhaitez gérer l’itinéraire :

import 'package:flutter_polyline_points/flutter_polyline_points.dart';

Créer un itinéraire avec Google Maps dans Flutter

Maintenant que la mise en place est terminée, vous allez pouvoir créer des itinéraires en plaçant des marqueurs sur votre carte. Voici un code d’exemple qui reprend les fonctionnalités de base, c’est à dire :

  • Placer et supprimer des marqueurs sur la carte.
  • Trouver la route la plus rapide entre ces marqueurs.
  • Adapter le trajet, selon les marqueurs ajoutés ou supprimés.
  • Changer le mode de transport (Voiture, marche, ou transports en commun).
  • Le temps de voyage estimé.
  • Recentrer la carte autour de l’itinéraire.
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MapScreen(),
    );
  }
}

class MapScreen extends StatefulWidget {
  const MapScreen({Key? key}) : super(key: key);

  @override
  _MapScreenState createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  late GoogleMapController mapController;
  Set<Marker> _markers = {};
  Set<Polyline> _polylines = {};
  List<LatLng> _routePoints = [];
  PolylinePoints polylinePoints = PolylinePoints();
  String googleApiKey = "VOTRE_CLÉ_API";
  String _travelTime = "Calcul en cours...";
  TravelMode _selectedMode = TravelMode.driving;

  @override
  void initState() {
    super.initState();
    checkAndRequestPermission();
  }

  Future<void> checkAndRequestPermission() async {
    PermissionStatus status = await Permission.location.status;
    if (!status.isGranted) {
      await Permission.location.request();
    }
  }

  void _onMapTap(LatLng position) {
    setState(() {
      String markerId = "marker_${_markers.length}";
      _markers.add(
        Marker(
          markerId: MarkerId(markerId),
          position: position,
          infoWindow: InfoWindow(title: "Point ${_markers.length + 1}"),
          onTap: () {
            setState(() {
              _markers
                  .removeWhere((marker) => marker.markerId.value == markerId);
              _updateRoute();
            });
          },
        ),
      );
      _updateRoute();
    });
  }

  void _updateRoute() async {
    if (_markers.length < 2) return;

    _routePoints.clear();
    List<Marker> markerList = _markers.toList();

    for (int i = 0; i < markerList.length - 1; i++) {
      PolylineResult result = await polylinePoints.getRouteBetweenCoordinates(
        googleApiKey: googleApiKey,
        request: PolylineRequest(
          origin: PointLatLng(
            markerList[i].position.latitude,
            markerList[i].position.longitude,
          ),
          destination: PointLatLng(
            markerList[i + 1].position.latitude,
            markerList[i + 1].position.longitude,
          ),
          mode: _selectedMode,
        ),
      );

      if (result.points.isNotEmpty) {
        _routePoints
            .addAll(result.points.map((p) => LatLng(p.latitude, p.longitude)));
      }
    }

    setState(() {
      _polylines.clear();
      _polylines.add(
        Polyline(
          polylineId: const PolylineId("route"),
          color: Colors.blue,
          width: 5,
          points: _routePoints,
        ),
      );
      _calculateTravelTime();
      _recenterMap(); // Recentrer la carte
    });
  }

  void _calculateTravelTime() {
    setState(() {
      _travelTime = "~ ${(_routePoints.length / 5).toStringAsFixed(1)} min";
    });
  }

  // Recentrer la carte pour afficher tous les points
  void _recenterMap() {
    if (_markers.isEmpty) return;

    double minLat = _routePoints.first.latitude;
    double maxLat = _routePoints.first.latitude;
    double minLon = _routePoints.first.longitude;
    double maxLon = _routePoints.first.longitude;

    for (LatLng point in _routePoints) {
      if (point.latitude < minLat) minLat = point.latitude;
      if (point.latitude > maxLat) maxLat = point.latitude;
      if (point.longitude < minLon) minLon = point.longitude;
      if (point.longitude > maxLon) maxLon = point.longitude;
    }

    LatLngBounds bounds = LatLngBounds(
      southwest: LatLng(minLat, minLon),
      northeast: LatLng(maxLat, maxLon),
    );

    mapController.animateCamera(CameraUpdate.newLatLngBounds(bounds, 50));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Planificateur d'itinéraire"),
        actions: [
          DropdownButton<TravelMode>(
            value: _selectedMode,
            onChanged: (TravelMode? mode) {
              setState(() {
                _selectedMode = mode!;
                _updateRoute();
              });
            },
            items: const [
              DropdownMenuItem(value: TravelMode.driving, child: Text("Voiture")),
              DropdownMenuItem(value: TravelMode.walking, child: Text("Marche")),
              DropdownMenuItem(value: TravelMode.transit, child: Text("Transport")),
              DropdownMenuItem(value: TravelMode.bicycling, child: Text("Vélo")),
            ],
          ),
        ],
      ),
      body: Stack(
        children: [
          GoogleMap(
            onMapCreated: (controller) => mapController = controller,
            initialCameraPosition: const CameraPosition(
              target: LatLng(48.8566, 2.3522),
              zoom: 12.0,
            ),
            markers: _markers,
            polylines: _polylines,
            onTap: _onMapTap,
          ),
          Positioned(
            top: 20,
            left: 20,
            child: Container(
              padding: const EdgeInsets.all(10),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(8),
                boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5)],
              ),
              child: Text(
                "Temps estimé: $_travelTime",
                style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _markers.clear();
            _polylines.clear();
            _routePoints.clear();
            _travelTime = "Calcul en cours...";
          });
        },
        child: const Icon(Icons.delete),
      ),
    );
  }
}

Avec ce code d’exemple que vous pouvez adapter à vos besoins, vous pouvez facilement créer des itinéraires et estimer le temps de trajet selon le mode de déplacement.

Ajouter et supprimer des marqueurs

Ajouter des marqueurs sur la carte est la première étape pour créer un itinéraire. Pour ajouter un marqueur, on utilise la méthode onTap de GoogleMap, qui permet de récupérer la position cliquée et d’ajouter un widget de type Marker à cet endroit :

//Fonction lancée, lorsque l'utilisateur clique quelque part sur la carte 

void _onMapTap(LatLng position) {
  setState(() {
    // Génère un identifiant unique pour le marqueur
    String markerId = "marker_\${_markers.length}";
    
    // Ajoute un marqueur à l'endroit cliqué
    _markers.add(
      Marker(
        markerId: MarkerId(markerId),
        position: position,
        infoWindow: InfoWindow(title: "Point \${_markers.length + 1}"),
        
        // Ajoute une action de suppression lorsqu'on clique sur le marqueur
        onTap: () {
          setState(() {
            _markers.removeWhere((marker) => marker.markerId.value == markerId);
            _updateRoute(); // Met à jour l'itinéraire
          });
        },
      ),
    );
    
    _updateRoute(); // Appel la fonction qui met à jour l'itinéraire après ajout du marqueur
  });
}

Je vous invite à lire mon guide, si vous souhaitez en apprendre plus sur comment ajouter des marqueurs et les personnaliser selon vos besoins.

Récupérer les marqueurs et tracer un itinéraire

Une fois plusieurs marqueurs ajoutés, l’itinéraire entre eux peut être tracé à l’aide du package flutter_polyline_points, qui utilise l’API Google Directions pour calculer le meilleur chemin.

void _updateRoute() async {
  if (_markers.length < 2) return; // Pas assez de points pour tracer un itinéraire

  _routePoints.clear(); // Efface l'itinéraire actuel
  List<Marker> markerList = _markers.toList();

  for (int i = 0; i < markerList.length - 1; i++) {
    // Appel à l'API Google Directions pour obtenir l'itinéraire entre deux points. L'étape se répète jusqu'à ce que tous les points soient reliés.
    PolylineResult result = await polylinePoints.getRouteBetweenCoordinates(
      googleApiKey: googleApiKey,
      request: PolylineRequest(
        origin: PointLatLng(
          markerList[i].position.latitude,
          markerList[i].position.longitude,
        ),
        destination: PointLatLng(
          markerList[i + 1].position.latitude,
          markerList[i + 1].position.longitude,
        ),
        mode: _selectedMode, // Mode de transport sélectionné
      ),
    );

    // Ajoute les points de l'itinéraire à la liste
    if (result.points.isNotEmpty) {
      _routePoints.addAll(result.points.map((p) => LatLng(p.latitude, p.longitude)));
    }
  }

  setState(() {
    _polylines.clear(); // Efface les anciennes lignes
    _polylines.add(
      Polyline(
        polylineId: const PolylineId("route"),
        color: Colors.blue,
        width: 5,
        points: _routePoints,
      ),
    );
    _calculateTravelTime(); // Calcule le temps de trajet selon le mode de transport selectionné
  });
}

La fonction se compose de 3 étapes.

  1. Récupérer les marqueurs : L’application garde une liste des marqueurs ajoutés, stockée dans la variable _markers. Chaque marqueur représente une étape de l’itinéraire.
  2. Appel à l’API Google Directions : Pour chaque paire de points adjacents (c’est-à-dire deux marqueurs qui se suivent dans la liste _markers), l’application appelle l’API Google Directions via le package flutter_polyline_points pour obtenir la route entre ces deux points. L’API renvoie une série de points qui décrivent la trajectoire de la route entre ces deux points. Cette opération est répétée jusqu’à ce que tous les marqueurs soient reliés par une route.
  3. Tracer l’itinéraire sur la carte : Les points obtenus à chaque appel sont ajoutés à une liste (_routePoints) et utilisés pour dessiner une polyline sur la carte, qui représente visuellement l’itinéraire.
  4. L’ancienne polyligne est effacée et puis une fois toutes les paires de points tracées, est mise à jour avec celles-ci.

Ainsi, l’itinéraire est tracé entre les marqueurs, et la carte est mise à jour pour refléter visuellement le trajet entre chaque point.

Adapter le tracé lorsque des marqueurs sont retirés

Lorsque l’utilisateur supprime un marqueur, la route doit se recalculer. Ceci est fait directement dans la fonction _onMapTap qui se relance lorsqu’on supprime un point du tracé et qui utilise la fonction _updateRoute() :

onTap: () {
  setState(() {
    _markers.removeWhere((marker) => marker.markerId.value == markerId); // Supprime le marqueur
    _updateRoute(); // Recalcule l'itinéraire
  });
}

Le tracé est supprimé, pour être redessiné à partir des points restants.

Changer le mode de transport

Google Maps permet de calculer des itinéraires selon différents modes de déplacement. On peut ainsi utiliser la propriété TravelMode pour spécifier l’option souhaitée.

Dans mon exemple, j’utilise un DropdownButton pour changer dynamiquement le mode de déplacement et recalculer l’itinéraire :

DropdownButton<TravelMode>(
  value: _selectedMode, // Mode de transport actuel
  onChanged: (TravelMode? mode) {
    setState(() {
      _selectedMode = mode!; // Met à jour le mode de transport
      _updateRoute(); // Recalcule l'itinéraire avec le nouveau mode
    });
  },
  items: const [
    DropdownMenuItem(value: TravelMode.driving, child: Text("Voiture")),
    DropdownMenuItem(value: TravelMode.walking, child: Text("Marche")),
    DropdownMenuItem(value: TravelMode.transit, child: Text("Transport")),
  ],
)

Les trois modes de transports disponibles sont driving (voiture), bicycling (vélo), walking (marche) et transit (Transports en commun).

Calculer le temps de trajet

Une estimation du temps de trajet peut être calculée en fonction du nombre de points de l’itinéraire et le mode de transport.

void _calculateTravelTime() {
    setState(() {
      _travelTime = "~ ${(_routePoints.length / 5).toStringAsFixed(1)} min";
    });
  }

Ici, l’estimation du temps de trajet repose sur le nombre de points qui définissent l’itinéraire. Ces points sont générés par l’API de Google Maps lorsque vous calculez la route entre deux endroits (marqueurs).

Chaque fois que vous ajoutez des marqueurs sur la carte, l’application calcule la route entre chaque paire de marqueurs et génère une série de points (coordonnées GPS) représentant cette route. Ces points sont stockés dans la variable _routePoints.

Pour estimer le temps de trajet, l’application divise simplement le nombre de points par 5. Cette division est une approximation, car l’idée est que environ 5 points correspondent à 1 minute de trajet, en fonction de la densité de la route.

Le nombre de points générés par l’API varie en fonction du mode de transport choisi (voiture, marche, vélo, etc.). Par exemple, pour un trajet en voiture, l’itinéraire sera plus direct, avec moins de points, tandis que pour la marche, l’itinéraire sera plus sinueux et aura plus de points.

Cette méthode reste approximative, car elle ne prend pas en compte la distance réelle ou la vitesse spécifique du mode de transport.

Recentrer la carte autour de l’itinéraire

Lorsque des points sont ajoutés ou supprimés sur la carte et que l’itinéraire est recalculé, il est nécessaire de s’assurer que toute la route est visible pour l’utilisateur.

void _recenterMap() {
    if (_markers.isEmpty) return;

    double minLat = _routePoints.first.latitude;
    double maxLat = _routePoints.first.latitude;
    double minLon = _routePoints.first.longitude;
    double maxLon = _routePoints.first.longitude;

    for (LatLng point in _routePoints) {
      if (point.latitude < minLat) minLat = point.latitude;
      if (point.latitude > maxLat) maxLat = point.latitude;
      if (point.longitude < minLon) minLon = point.longitude;
      if (point.longitude > maxLon) maxLon = point.longitude;
    }

    LatLngBounds bounds = LatLngBounds(
      southwest: LatLng(minLat, minLon),
      northeast: LatLng(maxLat, maxLon),
    );

    mapController.animateCamera(CameraUpdate.newLatLngBounds(bounds, 50));
  }

Voici comment cette fonctionnalité fonctionne :

  1. Mise à jour des points de la route : Dès qu’un utilisateur ajoute ou enlève un marqueur sur la carte, cela déclenche la mise à jour de l’itinéraire. Le calcul de l’itinéraire est effectué avec la méthode _updateRoute().
  2. Calcul des coordonnées extrêmes : Une fois les points de l’itinéraire calculés, on parcoure toutes les coordonnées des points pour déterminer les limites nord, sud, est et ouest (latitude et longitude minimale et maximale).
  3. Création des limites de la carte : Avec ces coordonnées extrêmes, on peut ensuite créer un LatLngBounds qui délimite la zone géographique à afficher sur la carte.
  4. Recentrage de la carte : Enfin, la méthode animateCamera va permettre d’appliquer un zoom et centrer la carte afin qu’elle affiche l’ensemble de l’itinéraire dans la vue.

Cela permet à l’utilisateur de toujours voir l’itinéraire complet sur la carte, même s’il ajoute ou retire des points.

Personnaliser le visuel de son itinéraire

Plusieurs options peuvent vous permettre de personnaliser l’apparence de l’itinéraire sur la carte. Voici les principales options qui vous sont proposées.

Changer la couleur de la polyline

La couleur de la polyline est l’un des aspects les plus simples à personnaliser. Vous pouvez définir la couleur de votre polyline (le chemin qui relie les points) pour qu’elle corresponde à la charte graphique de votre application ou bien pour la rendre plus visible.

_polylines.add(
  Polyline(
    polylineId: PolylineId("route"),
    color: Colors.blue, // Définir la couleur de la polyline
    width: 5,
    points: _routePoints,
  ),
);

Changer l’épaisseur de la Polyline

L’épaisseur de la polyline peut être modifiée pour la rendre plus ou moins visible. Vous pouvez ajuster l’épaisseur en jouant avec la propriété width.

_polylines.add(
  Polyline(
    polylineId: PolylineId("route"),
    color: Colors.blue,
    width: 10, // Modifier l'épaisseur de la polyline
    points: _routePoints,
  ),
);

Changer le style de la Polyline (Dotted ou Dashé)

Google Maps vous permet de modifier le style des polylines, en ajoutant des segments ou des pointillés à votre itinéraire.

_polylines.add(
  Polyline(
    polylineId: PolylineId("route"),
    color: Colors.blue,
    width: 5,
    patterns: [PatternItem.dash(30.0), PatternItem.gap(20.0)], // Style dashé
    points: _routePoints,
  ),
);

Prix de l’API Google Directions

Avant de commencer à utiliser l’API Google Directions, sachez que celle-ci suit un modèle de facturation à l’usage. Il est donc important de comprendre combien celles-ci coutent, ainsi que les différentes options disponibles.

Modèle de facturation

L’API Directions utilise un modèle de facturation basé sur le nombre de requêtes effectuées. Chaque requête envoyée au service Directions (via API ou SDK) génère un coût, et ce coût dépend du type de requête effectuée. Il existe deux types principaux de SKU pour cette API : Directions et Directions Advanced.

L’API Directions ne fonctionnera pas si vous n’avez pas activé la facturation.

Tarifs pour les requêtes standard (Directions)

Pour les requêtes standard qui ne nécessitent pas d’informations supplémentaires (comme le trafic en temps réel ou plus de 10 points de cheminement), le tarif est le suivant :

  • De 0 à 100 000 requêtes par mois : 0,005 USD par requête
  • De 100 001 à 500 000 requêtes par mois : 0,004 USD par requête
  • Au-delà de 500 000 requêtes par mois : Contactez Google pour obtenir un prix sur mesure

Les requêtes standard sont généralement utilisées pour des itinéraires simples, sans demandes complexes comme les informations de trafic.

Tarifs pour les requêtes avancées (Directions Advanced)

Les requêtes avancées englobent des fonctionnalités supplémentaires telles que les informations sur le trafic en temps réel. La facturation de ce service est la suivante :

  • De 0 à 100 000 requêtes par mois : 0,01 USD par requête
  • De 100 001 à 500 000 requêtes par mois : 0,008 USD par requête
  • Au-delà de 500 000 requêtes par mois : Contactez Google pour obtenir un prix sur mesure

Les requêtes avancées incluent des fonctionnalités telles que :

  • Informations sur le trafic : Calculs d’itinéraires prenant en compte le trafic en temps réel, si le paramètre departure_time est défini.
  • Optimisation des points de cheminement : Lorsque vous définissez optimize:true pour réorganiser les points de cheminement dans l’ordre le plus efficace.
  • Modificateurs d’emplacement : Utilisation de side_of_road ou heading pour influencer l’itinéraire.
Avatar de Pierre Courtois