Ludovic ROLAND

Blog technique sur mes expériences de développeur.

Android : Des itinéraires dans vos applications grâce à l’API Google Direction !

15 mars 2013

Dans mon dernier article, je vous montrais comment intégrer des itinéraires dans vos applications Windows Phone 7 grâce aux services Microsoft. Cette semaine, je vous propose de faire la même chose sur la plate-forme de Google.

Ce que nous allons faire

L’application que nous allons produire sera constituée de deux écrans:

  • un écran permettant de saisir les lieux de départ et d’arrivée ;
  • un écran affichant l’itinéraire sur la carte ;

L’écran de saisie des lieux de départ de d’arrivée

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="10dp" >

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="3"
        android:orientation="horizontal" >

        <TextView 
            android:text="Départ"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:textStyle="bold" />

        <EditText
            android:id="@+id/editDepart"
            android:layout_weight="2"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:maxLines="1"
            android:inputType="text"
            android:lines="1" />
     </LinearLayout>

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="3"
        android:orientation="horizontal" >

        <TextView 
            android:text="Arrivée"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:textStyle="bold" />

        <EditText
            android:id="@+id/editArrivee"
            android:layout_weight="2"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:maxLines="1"
            android:inputType="text"
            android:lines="1" />
    </LinearLayout>

    <Button 
        android:id="@+id/btnSearch"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:text="Rechercher"/>
</LinearLayout>

Dans notre Activity, nous allons simplement vérifier que les champs ne sont pas vide afin de les passer à l’activité qui affichera la carte :

public class MainActivity extends Activity {
    private EditText editDepart;
    private EditText editArrivee;
    private Button btnRechercher;

    /**
    * {@inheritDoc}
    */
    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //On récupère les composants graphiques
        editDepart = (EditText) findViewById(R.id.editDepart);
        editArrivee = (EditText) findViewById(R.id.editArrivee);
        btnRechercher = (Button) findViewById(R.id.btnSearch);

        btnRechercher.setOnClickListener(new OnClickListener() {
            /**
            * {@inheritDoc}
            */
            @Override
            public void onClick(final View v) {
                if("".equals(editDepart.getText().toString().trim())) {
                    Toast.makeText(MainActivity.this, "Merci de saisir un lieu de départ", Toast.LENGTH_SHORT).show();
                }
                else if("".equals(editArrivee.getText().toString().trim())) {
                    Toast.makeText(MainActivity.this, "Merci de saisir un lieu d'arrivée", Toast.LENGTH_SHORT).show();
                }
                else {
                    //On transmet les données à l'activité suivante
                    final Intent intent = new Intent(MainActivity.this, MapActivity.class);
                    intent.putExtra("DEPART", editDepart.getText().toString().trim());
                    intent.putExtra("ARRIVEE", editArrivee.getText().toString().trim());

                    MainActivity.this.startActivity(intent);
                }				
            }
        });
    }
}

L’affichage de la carte et calcul de l’itinéraire

L’affichage de la carte

Avant de faire la recherche, je vous propose de mettre en place l’écran qui accueillera la carte sur laquelle l’itinéraire sera tracée. Il s’agit d’une simple carte Google. Pour plus d’information sur la carte et comment récupérer une clef, vous pouvez lire cet article.

Votre activité doit alors ressembler à ça :

public class MapActivity extends Activity {
    private GoogleMap gMap;

    /**
    * {@inheritDoc}
    */
    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_map);

        //On récupère les composants graphiques
        gMap = ((MapFragment)getFragmentManager().findFragmentById(R.id.map)).getMap();

        //On récupère le départ et l'arrivée
        final String editDepart = getIntent().getStringExtra("DEPART");
        final String editArrivee = getIntent().getStringExtra("ARRIVEE");

        //TODO: Appeler une tâche asynchrone qui affiche l'itinéraire sur la carte
    }
}

Le calcul de l’itinéraire

Nous allons maintenant créer une tâche asynchrone qui a deux objectifs :

  • appeler l’API Google Direction pour récupérer l’itinéraire ;
  • mettre à jour la carte.

Dans cette tâche asynchrone, plusieurs attributs seront nécessaires :

  • le contexte ;
  • la carte afin de la mettre à jour ;
  • le lieu de départ ;
  • le lieu d’arrivée.

Nous allons également avoir besoin d’un tableau qui contiendra les latitudes et longitudes des différents points à afficher sur la carte.

Voici alors la structure de notre tâche asynchrone :

public class ItineraireTask extends AsyncTask<Void, Integer, Boolean> {
    private static final String TOAST_MSG = "Calcul de l'itinéraire en cours";
    private static final String TOAST_ERR_MAJ = "Impossible de trouver un itinéraire";

    private Context context;
    private GoogleMap gMap;
    private String editDepart;
    private String editArrivee;
    private final ArrayList<LatLng> lstLatLng = new ArrayList<LatLng>();

    /**
    * Constructeur.
    * @param context
    * @param gMap
    * @param editDepart
    * @param editArrivee
    */
    public ItineraireTask(final Context context, final GoogleMap gMap, final String editDepart, final String editArrivee) {
        this.context = context;
        this.gMap= gMap;
        this.editDepart = editDepart;
        this.editArrivee = editArrivee;
    }

    /**
    * {@inheritDoc}
    */
    @Override
    protected void onPreExecute() {
        Toast.makeText(context, TOAST_MSG, Toast.LENGTH_LONG).show();
    }

    /***
    * {@inheritDoc}
    */
    @Override
    protected Boolean doInBackground(Void... params) {
        // TODO Auto-generated method stub
        return null;
    }

    /**
    * {@inheritDoc}
    */
    @Override
    protected void onPostExecute(final Boolean result) {	
        // TODO Auto-generated method stub
    }
}

Nous allons maintenant implémenter la méthode doInBackground dont le but est d’appeler le service Google et d’alimenter notre tableau de latitudes et longitudes.

Voici alors ce que ça donne :

/**
* {@inheritDoc}
*/
@Override
protected Boolean doInBackground(Void... params) {
    try {
        //Construction de l'url à appeler			
        final StringBuilder url = new StringBuilder("http://maps.googleapis.com/maps/api/directions/xml?sensor=false&language=fr");
        url.append("&origin=");
        url.append(editDepart.replace(' ', '+'));
        url.append("&destination=");
        url.append(editArrivee.replace(' ', '+'));

        //Appel du web service
        final InputStream stream = new URL(url.toString()).openStream();

        //Traitement des données
        final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setIgnoringComments(true);

        final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();

        final Document document = documentBuilder.parse(stream);
        document.getDocumentElement().normalize();

        //On récupère d'abord le status de la requête
        final String status = document.getElementsByTagName("status").item(0).getTextContent();
        if(!"OK".equals(status)) {
            return false;
        }

        //On récupère les steps
        final Element elementLeg = (Element) document.getElementsByTagName("leg").item(0);
        final NodeList nodeListStep = elementLeg.getElementsByTagName("step");
        final int length = nodeListStep.getLength();

        for(int i=0; i<length; i++) {		
            final Node nodeStep = nodeListStep.item(i);

            if(nodeStep.getNodeType() == Node.ELEMENT_NODE) {
                final Element elementStep = (Element) nodeStep;

                //On décode les points du XML
                decodePolylines(elementStep.getElementsByTagName("points").item(0).getTextContent());
            }
        }

        return true;			
    }
    catch(final Exception e) {
        return false;
    }
}

/**
* Méthode qui décode les points en latitude et longitudes
* @param points
*/
private void decodePolylines(final String encodedPoints) {
    int index = 0;
    int lat = 0, lng = 0;

    while (index < encodedPoints.length()) {
        int b, shift = 0, result = 0;

        do {
            b = encodedPoints.charAt(index++) - 63;
            result |= (b & 0x1f) << shift;
            shift += 5;
        } while (b >= 0x20);

        int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
        lat += dlat;
        shift = 0;
        result = 0;

        do {
            b = encodedPoints.charAt(index++) - 63;
            result |= (b & 0x1f) << shift;
            shift += 5;
        } while (b >= 0x20);

        int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
        lng += dlng;

        lstLatLng.add(new LatLng((double)lat/1E5, (double)lng/1E5));
    }
}

Comme vous pouvez le constater, il est nécessaire de décoder les points renvoyer par Google qui sont de la forme }leiHgmjM[xBKx@. A noter que je ne suis pas l’auteur de cette fonction que j’ai trouvé sur ce site.

Maintenant que nous avons toutes les données nécessaires, nous allons pouvoir mettre à jour notre carte. C’est dans la méthode onPostExecute qui tout se passe :

/**
* {@inheritDoc}
*/
@Override
protected void onPostExecute(final Boolean result) {	
    if(!result) {
        Toast.makeText(context, TOAST_ERR_MAJ, Toast.LENGTH_SHORT).show();
    }
    else {
        //On déclare le polyline, c'est-à-dire le trait (ici bleu) que l'on ajoute sur la carte pour tracer l'itinéraire
        final PolylineOptions polylines = new PolylineOptions();
        polylines.color(Color.BLUE);

        //On construit le polyline
        for(final LatLng latLng : lstLatLng) {
            polylines.add(latLng);
        }

        //On déclare un marker vert que l'on placera sur le départ
        final MarkerOptions markerA = new MarkerOptions();
        markerA.position(lstLatLng.get(0));
        markerA.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN));

        //On déclare un marker rouge que l'on mettra sur l'arrivée
        final MarkerOptions markerB = new MarkerOptions();
        markerB.position(lstLatLng.get(lstLatLng.size()-1));
        markerB.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED));

        //On met à jour la carte
        gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(lstLatLng.get(0), 10));
        gMap.addMarker(markerA);
        gMap.addPolyline(polylines);
        gMap.addMarker(markerB);
    }
}

L’appel de la méthode asynchrone

Il ne nous reste plus qu’à appeler notre méthode asynchrone dans MapActivity.

new ItineraireTask(this, gMap, editDepart, editArrivee).execute();

Le code complet

Comme d’habitude, voici le code complet :

Le fichier AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="fr.rolandl.blog_itineraire"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="11"
        android:targetSdkVersion="17" />

    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true"/>

    <permission
        android:name="fr.rolandl.blog_itineraire.permission.MAPS_RECEIVE"
        android:protectionLevel="signature"/>

    <uses-permission android:name="fr.rolandl.blog_itineraire.permission.MAPS_RECEIVE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >

        <meta-data 
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="A..."/>

        <activity
            android:name="fr.rolandl.blog_itineraire.MainActivity"
            android:configChanges="keyboardHidden|orientation|screenSize"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name="fr.rolandl.blog_itineraire.MapActivity"
            android:label="CARTE"
            android:configChanges="keyboardHidden|orientation|screenSize" />
    </application>
</manifest>

Le fichier activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  		android:layout_width="fill_parent"
  		android:layout_height="wrap_content"
  		android:orientation="vertical"
  		android:padding="10dp" >

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  		android:layout_width="fill_parent"
  		android:layout_height="0dp"
  		android:layout_weight="3"
  		android:orientation="horizontal" >

		<TextView 
     	android:text="Départ"
     	android:layout_width="0dp"
     	android:layout_weight="1"
      android:layout_height="wrap_content"
			android:textStyle="bold" />

		<EditText
		    android:id="@+id/editDepart"
			  android:layout_weight="2"
     		android:layout_width="0dp"
     		android:layout_height="wrap_content"
     		android:maxLines="1"
			  android:inputType="text"
			  android:lines="1" />
 		</LinearLayout>

	     <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	  		android:layout_width="fill_parent"
	  		android:layout_height="0dp"
	  		android:layout_weight="3"
	  		android:orientation="horizontal" >

			<TextView 
	     		android:text="Arrivée"
	     		android:layout_width="0dp"
	     		android:layout_weight="1"
	     		android:layout_height="wrap_content"
				android:textStyle="bold" />

			<EditText
			    android:id="@+id/editArrivee"
				android:layout_weight="2"
	     		android:layout_width="0dp"
	     		android:layout_height="wrap_content"
	     		android:maxLines="1"
				android:inputType="text"
				android:lines="1" />
 		</LinearLayout>

	    <Button 
	        android:id="@+id/btnSearch"
	        android:layout_width="fill_parent"
	        android:layout_height="fill_parent"
	        android:text="Rechercher"/>
</LinearLayout>

Le fichier activity_map.xml

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/map"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    class="com.google.android.gms.maps.MapFragment" />

Le fichier MainActivity.java

package fr.rolandl.blog_itineraire;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import fr.rolandl.blog_itineraire.R;

/**
 * MainActivity
 * @author Ludovic
 */
public class MainActivity extends Activity {
	private EditText editDepart;
	private EditText editArrivee;
	private Button btnRechercher;

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void onCreate(final Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		//On récupère les composants graphiques
		editDepart = (EditText) findViewById(R.id.editDepart);
		editArrivee = (EditText) findViewById(R.id.editArrivee);
		btnRechercher = (Button) findViewById(R.id.btnSearch);

		btnRechercher.setOnClickListener(new OnClickListener() {
			/**
			 * {@inheritDoc}
			 */
			@Override
			public void onClick(final View v) {
				if("".equals(editDepart.getText().toString().trim())) {
					Toast.makeText(MainActivity.this, "Merci de saisir un lieu de départ", Toast.LENGTH_SHORT).show();
				}
				else if("".equals(editArrivee.getText().toString().trim())) {
					Toast.makeText(MainActivity.this, "Merci de saisir un lieu d'arrivée", Toast.LENGTH_SHORT).show();
				}
				else {
					//On transmet les données à l'activité suivante
					final Intent intent = new Intent(MainActivity.this, MapActivity.class);
					intent.putExtra("DEPART", editDepart.getText().toString().trim());
					intent.putExtra("ARRIVEE", editArrivee.getText().toString().trim());

					MainActivity.this.startActivity(intent);
				}				
			}
		});
	}
}

Le fichier MapActivity.java

package fr.rolandl.blog_itineraire;

import android.app.Activity;
import android.os.Bundle;

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapFragment;

/**
 * MapActivity
 * @author Ludovic
 */
public class MapActivity extends Activity {
	private GoogleMap gMap;

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void onCreate(final Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_map);

		//On récupère les composants graphiques
		gMap = ((MapFragment)getFragmentManager().findFragmentById(R.id.map)).getMap();

		//On récupère le départ et l'arrivée
		final String editDepart = getIntent().getStringExtra("DEPART");
		final String editArrivee = getIntent().getStringExtra("ARRIVEE");

		//Appel de la méthode asynchrone
		new ItineraireTask(this, gMap, editDepart, editArrivee).execute();
	}
}

Le fichier ItineraireTask.java

package fr.rolandl.blog_itineraire;

import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolylineOptions;

import android.content.Context;
import android.graphics.Color;
import android.os.AsyncTask;
import android.widget.Toast;

/**
 * ItineraireTask
 * @author Ludovic
 */
public class ItineraireTask extends AsyncTask<Void, Integer, Boolean> {
	/*******************************************************/
	/** CONSTANTES.
	/*******************************************************/
	private static final String TOAST_MSG = "Calcul de l'itinéraire en cours";
	private static final String TOAST_ERR_MAJ = "Impossible de trouver un itinéraire";

	/*******************************************************/
	/** ATTRIBUTS.
	/*******************************************************/
	private Context context;
	private GoogleMap gMap;
	private String editDepart;
	private String editArrivee;
	private final ArrayList<LatLng> lstLatLng = new ArrayList<LatLng>();

	/*******************************************************/
	/** METHODES / FONCTIONS.
	/*******************************************************/
	/**
	 * Constructeur.
	 * @param context
	 * @param gMap
	 * @param editDepart
	 * @param editArrivee
	 */
	public ItineraireTask(final Context context, final GoogleMap gMap, final String editDepart, final String editArrivee) {
		this.context = context;
		this.gMap= gMap;
		this.editDepart = editDepart;
		this.editArrivee = editArrivee;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
    protected void onPreExecute() {
		Toast.makeText(context, TOAST_MSG, Toast.LENGTH_LONG).show();
	}

	/***
	 * {@inheritDoc}
	 */
	@Override
	protected Boolean doInBackground(Void... params) {
		try {
			//Construction de l'url à appeler			
			final StringBuilder url = new StringBuilder("http://maps.googleapis.com/maps/api/directions/xml?sensor=false&language=fr");
			url.append("&origin=");
			url.append(editDepart.replace(' ', '+'));
			url.append("&destination=");
			url.append(editArrivee.replace(' ', '+'));

			//Appel du web service
			final InputStream stream = new URL(url.toString()).openStream();

			//Traitement des données
			final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
			documentBuilderFactory.setIgnoringComments(true);

			final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();

			final Document document = documentBuilder.parse(stream);
			document.getDocumentElement().normalize();

			//On récupère d'abord le status de la requête
			final String status = document.getElementsByTagName("status").item(0).getTextContent();
			if(!"OK".equals(status)) {
				return false;
			}

			//On récupère les steps
			final Element elementLeg = (Element) document.getElementsByTagName("leg").item(0);
			final NodeList nodeListStep = elementLeg.getElementsByTagName("step");
			final int length = nodeListStep.getLength();

			for(int i=0; i<length; i++) {		
				final Node nodeStep = nodeListStep.item(i);

				if(nodeStep.getNodeType() == Node.ELEMENT_NODE) {
					final Element elementStep = (Element) nodeStep;

					//On décode les points du XML
					decodePolylines(elementStep.getElementsByTagName("points").item(0).getTextContent());
				}
			}

			return true;			
		}
		catch(final Exception e) {
			return false;
		}
	}

	/**
	 * Méthode qui décode les points en latitudes et longitudes
	 * @param points
	 */
	private void decodePolylines(final String encodedPoints) {
		int index = 0;
		int lat = 0, lng = 0;

		while (index < encodedPoints.length()) {
			int b, shift = 0, result = 0;

			do {
				b = encodedPoints.charAt(index++) - 63;
				result |= (b & 0x1f) << shift;
				shift += 5;
			} while (b >= 0x20);

			int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
			lat += dlat;
			shift = 0;
			result = 0;

			do {
				b = encodedPoints.charAt(index++) - 63;
				result |= (b & 0x1f) << shift;
				shift += 5;
			} while (b >= 0x20);

			int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
			lng += dlng;

			lstLatLng.add(new LatLng((double)lat/1E5, (double)lng/1E5));
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void onPostExecute(final Boolean result) {	
		if(!result) {
			Toast.makeText(context, TOAST_ERR_MAJ, Toast.LENGTH_SHORT).show();
		}
		else {
			//On déclare le polyline, c'est-à-dire le trait (ici bleu) que l'on ajoute sur la carte pour tracer l'itinéraire
			final PolylineOptions polylines = new PolylineOptions();
			polylines.color(Color.BLUE);

			//On construit le polyline
			for(final LatLng latLng : lstLatLng) {
					polylines.add(latLng);
			}

			//On déclare un marker vert que l'on placera sur le départ
			final MarkerOptions markerA = new MarkerOptions();
			markerA.position(lstLatLng.get(0));
			markerA.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN));

			//On déclare un marker rouge que l'on mettra sur l'arrivée
			final MarkerOptions markerB = new MarkerOptions();
			markerB.position(lstLatLng.get(lstLatLng.size()-1));
			markerB.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED));

			//On met à jour la carte
			gMap.moveCamera(CameraUpdateFactory.newLatLngZoom(lstLatLng.get(0), 10));
			gMap.addMarker(markerA);
			gMap.addPolyline(polylines);
			gMap.addMarker(markerB);
		}
	}
}

Commentaires