Blog technique sur mes expériences de développeur.
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.
L’application que nous allons produire sera constituée de deux écrans:
<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);
}
}
});
}
}
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
}
}
Nous allons maintenant créer une tâche asynchrone qui a deux objectifs :
Dans cette tâche asynchrone, plusieurs attributs seront nécessaires :
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);
}
}
Il ne nous reste plus qu’à appeler notre méthode asynchrone dans MapActivity.
new ItineraireTask(this, gMap, editDepart, editArrivee).execute();
Comme d’habitude, voici le code complet :
<?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>
<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>
<?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" />
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);
}
}
});
}
}
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();
}
}
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);
}
}
}