Affichage de polylines dans application Android avec Google Maps

Vous avez besoin d'afficher des routes ou des itinéraires dans une application Android?

Ce billet présente comment afficher des polylines dans une application Android, en utilisant l'API Google Maps.

La polyline Google Maps...

Oui, la première étape est bien : comment définir le tracé?

Pour cela, on peut s'appuyer sur le service de calcul d'itinéraire de l'API Google Maps. On pourra se référer au billet Itinéraires Google Maps et Encoded Polyline Algorithm Format .

On précise les points de départ et d'arrivée et si nécessaire également des points de passage obligé. On peut même préciser de retourner au point de départ, ci-dessus on obtient, pour le coup, le tracé de la rocade de Bordeaux.

bordeaux

L'image ci-dessus est issue d'une intégration JavaScript, démo en ouvrant : bordeaux.html. Autre exemple avec le périphérique de Paris : paris-periph.html

A titre d'exemple, l'ensemble de la rocade a été obtenu en utilisant l'API web services :

http://maps.google.com/maps/api/directions/xml?sensor=false&origin=44.88792,-0.57966&destination=44.88743,-0.57318&waypoints=44.85494,-0.66433|44.789994,-0.61213|44.8383,-0.501

Le service Directions de l'API Googole Maps propose, en retour, une description textuelle de l'itinéraire. Et aussi, en bonus, une description graphique de celui-ci sous forme de polyline, dans un format encodé par soucis de bande passante.

A la fin de la réponse on note :

<overview_polyline>
<points>
od~pGzepB_MpnEJxK`@pEz@hFjAzDxAlDzBtDtB~BrB~AxC~A|LfEvB`AnDjCdDbExIzPbiAp`CnQt^vJlQpKhObI|IhFzElJnHrI|FbyA``AjIxDjJfChHx@`EJ~EG`Iq@bsBsWzCm@`F}AxC{A|CwBnDiDrEoGnl@{jAfEaHzDoExDcDdb@yZzDuDhB{BjDaGnCwGlByGxAyI`A}M@aMsAu^mJgxBQoMPwKXwFhAiLhCoN`]}iAtFsU`FoVbDwRzDqY`J}z@bAuLZmGF_Jk@cMuBoQsHmj@oAuH{@}CkA_DkCwEqD{DcDuBsB{@gE}@yi@wHkGmAsHqBcKoE{JyGaV_SeJsFmCiCmDkFm[cj@mD_FoDcDoDoBkEmAoCYaBCcKt@yBEoCa@gA_@cE_CkAgAwBuCmB_Ek@kBoDqPaCqG_DqFqAaBkIqIgDsFsCuHuC}LeCoGsCaE}AwAgBmAeF{BuFkBaGmAuDMmBH}E~@qK|D_F|@sCT{MMaEPiC^ek@nPcH|AqCVkEHqDOs^wEkJi@qHA_GPsE\aJfA_HxAmMdEcj@tVsPtGuAV{AGkE_CqAUaBf@c@f@c@fAOfBDjAj@`CHpCLTY~GDrFXrE~@zFbA`EdChG`Sz\tBfEx@rCv@xEZdEH`FIrBcArJoYzgCkDjWqCdPiF|WiBzLqApLmAnS
</points>
</overview_polyline>

Décodage de la polyline

Le billet mentionné précédemment indique comment réaliser le décodage en Java.

On pourra notamment télécharger le code source Java : polyline-decoder .

On se ramène alors à la manipulation d'une liste de points en latitude et longitude.

Dans la démo html/JavaScript ci-dessus, on a réalisé une manipulation simple en créant des markers relatifs à ces positions GPS.

L'API JavaScript propose l'objet Polyline : in fine, une liste de points GPS. On obtient le tracé parfait de notre itinéraire, sous forme de polyline, simplement en ajoutant la polyline à la Map.

Ensuite, lorsqu'on déplace la carte, les markers ou la polyline suivent le mouvement en restant bien ancrés à la carte.

Est-ce aussi simple dans le cas d'un application Android?

Affichage dans une MapView Android

bordeaux-android

L'API pour une application Android est de plus bas niveau. Il n'y a pas de notion de polyline ou de markers géographiques.

Il convient alors de recourir à un tracé brutal dans le canvas. Et pour déduire la position (i,j) dans le canvas, il convient d'appliquer une projection...

Projection pj = map.getProjection();
	Point p = new Point();
	pj.toPixels(f, p);

Tracer une polyline parallèlement à une autre?

bordeaux-android-offset

Imaginons qu'on veuille visualiser le trafic routier le long d'un itinéraire, par exemple avec un code couleur vert/orange/rouge. Les conditions de circulation ne sont pas les mêmes dans un sens ou dans l'autre.

Pour faire propre, il convient alors de faire le tracé "traficolor" légèrement décalé de l'axe de la route. Ce qui permet de tracer les deux sens de part et d'autre de la route.

Comment faire? Ce n'est pas trivial. En quelque sorte c'est : 'décaler d'une distance fixe perpendiculairement au tracé local'.

Et aussi, quelque soit le zoom appliqué à la carte, la distance est constante.

bordeaux-android-zoom

Décalage local par géométrie euclidienne

Un peu de geométrie euclidienne permet de résoudre le problème. Notre polyline est une liste de points entre lesquels on trace des segments.

Considérons 2 points, à la suite, dans la liste, appelons les from et to. Ces deux points sont bien dans l'axe de la route. On cherche le point décalé de la distance fixe d, perpandiculairement au segment (from,to).

Considérons justement ce vecteur (from,to). Dans le plan, ce vecteur à la composante :

double a = to.x-from.x;
double b = to.y-from.y;

Si on chercher un vecteur normal à (a,b), on peut prendre (-b,a). Aussi, la droite qui passe par "to" et qui est perpendiculaire au segment (from,to) peut avoir une représentation paramétrique simple comme suit : x = -b*t; y=+a*t.

Pour toute valeur du paramètre t, la distance entre le point correspondant et "to" est simplement x²+y², soit aussi : b²*t² + a²*t².

Bref, on cherche le paramètre t, tel que cette distance vaut exactement la distance fixe d.

double t = Math.sqrt( d*d/(a*a+b*b));

Du coup, le point que l'on cherche est bien :

Point p = new Point();
p.x = -new Double(b*t).intValue() ;
p.y = +new Double(a*t).intValue() ;

D'accord, on voit maintenant comment déterminer chaque point localement décalé de façon perpendiculaire.

Performances?

Avec la MapView Android, à chaque fois qu'on effectue un panning du doigt, on a le redraw(.) qui est invoqué. Ce qui veut dire qu'on a systématiquement les calculs de projection et de trigonométrie qui s'applique sur tous les points de la polyline.

A-t-on bien une réactivité temps réel? Il s'avère qu'on a bien une réacivité parfaite. Pour preuve la démonstration suivante à ouvrir avec un téléphone Android : bordeaux.apk .

On prend tout de même le soin d'écarter du traitement les points qui sont en dehors de la zone visible de la carte.

Pour ne pas qu'il y ait de phénomène de bord pour les segments, on considère la zone d'exclusion plus grande que la zone visible.

/**
 * @return Rect corresponding to twice the @param mapView viewport
 */
public static Rect toRectx2(MapView mapView) {
	int w = mapView.getLongitudeSpan();
	int h = mapView.getLatitudeSpan();
	int cx = mapView.getMapCenter().getLongitudeE6();
	int cy = mapView.getMapCenter().getLatitudeE6();
	return new Rect(cx-w, cy-h, cx+w, cy+h);
}

Avec un test du genre :

Rect viewRect = toRectx2(map);
boolean visible = viewRect.contains(f.getLongitudeE6(), f.getLatitudeE6());

Trafic Futé, application Android de trafic routier

Ces divers recettes on permis de réaliser trafic-futé ' une application Android qui présente le trafic routier sur une Google Maps. Avec pour source de données Bison Futé, une garantie de données pertinentes aux abords des villes, là où on retrouve la plupart de bouchons.

tf_lyon

Trafic Futé disponible dans l'Android Market, voir la page coutant .

Je la distribue en open-source GPL v3, le code source est disponible ici : github.com/scoutant/tf

Approche naturelle de Drag and Drop en Android

Ce billet présente une approche naturelle pour faire du Drag and Drop de widgets dans un conteneur d'une application Android.

En introduction, voyons les solutions qu'on retrouve en grand nombre sur le net.

Nombreuses solutions de Drag and Drop - solutions orientées canvas

En recherchant sur le net on trouve pleins de solutions différentes de DnD de Views avec Android. Un peu déstabilisant.

Par exemple le billet suivant. Ici l'approche consiste a avoir un conteneur qui implémente une méthode onDraw() et qui prend en charge le ré-affichage de tous les bitmaps.

Ce même conteneur capture la position et le mouvement du doigt. Déjà, lors de l'événement DOWN, on regarde si ya un des items qui correspond a la position du doigt. Lors du MOVE, on actualise des propriétés X,Y du bitmap. On appelle invalidate(), qui invoque le onDraw() qui re-dessine tout. Notamment notre bitmap avec une position actualisée.

Ci-dessous un exemple avec une classe Draggable, type data-objet qui contient simplement le bitmap et les coordonnées x et y.

@Override protected void onDraw(Canvas canvas) {
    	for (Draggable ball : draggables) {
            canvas.drawBitmap(ball.img, ball.x, ball.y, null);
          }
    }

Après, il y a d'autres variantes : on met le listener non pas sur le conteneur mais sur chacun des widgets qui encapsule le bitmap. Mais toujours avec une approche de onDraw(.) globale.

Je dirais que ces d'approche sont orienté canvas.

Approche naturelle - orientée layout

Plus naturel, est simplement de déplacer le seul item sous le doigt. Sans recourt au moindre onDraw(.). Si on a des centaines d'items, cette approche est aussi plus soucieuse des performances.

Pour cela il suffit d'actualiser unitairement le layout du widget en drag. Android 2.2 a bani l'AbsolutLayout; mais on a à disposition le FrameLayout.

Une sous-classe direct de ViewGroup, qui supporte donc un addView(widget) pour ajouter des widgets avec la possibilité d'une gestion de la position (x,y), via le layout.

public class BallView extends ImageView {
	public BallView(Context context, int X, int Y) { 
	this.setImageDrawable( context.getResources().getDrawable(R.drawable.bol_rood));
	setOnTouchListener(this);
    	FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(WIDTH, HEIGHT, Gravity.TOP);
    	layout.leftMargin = X - layout.width/2;
    	layout.topMargin = Y - layout.height/2;
    	this.setLayoutParams(layout);
	}

Ci-dessus, notre widget ImageView affiche un drawable, qui sera centrée à la position X,Y si le conteneur parent sait gérer un layout absolu.

On a construit un LayoutParams avec les dimensions WIDTH et HEIGTH souhaitées pour le widget ; ces constantes sont définies plus haute.

On portera une attention particulière au fait qu'on a opté pour le constructeur à 3 paramètres. Sans ce paramètre, avec par exemple la valeur Gravity.TOP, on n'a pas le positionnement effectif, pourquoi?

Maintenant il suffit que le widget s'abonne lui-même aux événements MotionEvent. Pour simplement actualiser les propriétés leftMargin et topMargin pour induire son propre repositionement au sein de son conteneur.

Dans le principe tout simplement,ceci !

public class BallView extends ImageView implements OnTouchListener {
	public boolean onTouch(View v, MotionEvent event) {
	FrameLayout.LayoutParams layout = (FrameLayout.LayoutParams) v.getLayoutParams();
    	if (event.getAction()==MotionEvent.ACTION_MOVE) {
		layout.leftMargin = (int) event.getRawX();    		
		layout.topMargin = (int) event.getRawY();
               }
	v.setLayoutParams(layout);
	return true;
    	}

L'approche tient à la méthode d'instance .setLayoutParams(.) qui repositionne effectivement l'instance au sein de son conteneur.

Cette approche je la qualifierais orientée layout.

On notera qu'on a casté en FrameLayout.LayoutParams le layout obtenu à partir de la View cible du MotionEvent. C'est précisément ainsi qu'on accède aux propriétés leftMargin et topMargin sur lesquelles repose notre approche.

Ceci fonctionne très bien.

Avec un petit désagrément toutefois : un petit décalage opère suivant l'endroit où on clic réellement dans le widget. Pour compenser il suffit de stocker cette valeur lors du DOWN, pour la retrancher lors du MOVE :

private int localX=0;
	private int localY=0;
	public boolean onTouch(View v, MotionEvent event) {
		FrameLayout.LayoutParams layout = (LayoutParams) v.getLayoutParams();
    	if (event.getAction()==MotionEvent.ACTION_DOWN) {
    		localX = (int)event.getX();
    		localY = (int)event.getY();
    		v.bringToFront();
    		return true;
    	}
    	if (event.getAction()==MotionEvent.ACTION_MOVE) {
			layout.leftMargin = (int) event.getRawX() - localX;    		
			layout.topMargin = (int) event.getRawY() - v.getHeight()/2 - localY;
    	}
		v.setLayoutParams(layout);
		return true;
	}

Voilà, un DnD des plus naturelle.

On peut aussi en profiter pour mettre au premier plan le widget cible du DnD, avec simplement bringToFront().

Démo et code source

On pourra télécharger le code source de l'exemple suivant. Il est légèrement plus complet : si on clic dans le conteneur, mais pas sur un widget, alors on ajoute un widget, avec un rayon en proportion à la pression du doigt.

http://blog.scoutant.org/public/layout-dnd.zip

Au final on a plein de billes rouges comme dans l'image. Que l'on peut déplacer unitairement. La réactivité est maintenue même avec des centaines de billes.

On pourra aussi tester directement en accédant à l'application Android : layout-dnd.apk.

layout-dnd.png

Jusqu'ici on déplace seulement une bille à fois.

Mais des doigts, en a plusieurs! Si on veut déplacer plusieurs items à la fois c'est tout à fait possible... prochain post!

Skin Samsung Galaxy Tab

Vous pouvez facilement customiser le skin de votre émulateur Android

Le billet précédent expliquait comment créer ou adapter un skin pour votre émulateur Android.

Galaxy-Tab

Après les smartphones, voici un skin pour la toute nouvelle tablette tablette Samsung Galaxy Tab : Galaxy-Tab.zip.

HTC Desire skin without keyboard for emulator

Vous pouvez facilement customiser le skin de votre émulateur Android

On trouve nombre de skins ici et là. Mais en générale c'est toujours avec le fameux clavier d'émulateur, si moche. Pas adapté pour des présentations.

Skin anatomy

Techniquement, un skin est un simple répertoire contenant des images et un fichier layout précisant la dispositions de ceux-ci. Par exempel un extrait :

display {
	width  480
	height 800
	x 79
	y 136
}
background {
	image background.png
	x 0
	y 0
}
button {
	home {
		image small_button.png
		x 90
		y 1036
	}
. . .
}

On trouve facilement des skins pour l'émulateur représentatnt le HTC Desire

Installing the skin HTC-Desire-no-keyboard

En partant d'un tel skin, il est facile d'éliminer le clavier. Il suffit de :

  • découper le backgroup.png,
  • retirer du layout tous les boutons du clavier.

Ce skin pourra être téléchargé : HTC-Desire-no-keyboard.zip.

Une fois dézippé, on peut lancer directement l'émulateur :

emulator -avd <avd-name> -skin HTC-Desire-no-keyboard

Cette approche est toutefois déclarée deprecated.

L'approche plus usuelle sera de placer le répertoire HTC-Desire-no-keyboard dans le répertoire platforms/android-8/skins/ de votre installation Android. Et lorsque vous utiliser le wizard de création d'AVD, le nouveau skin aparait dans la liste déroulante.

AVD-wizard

Itinéraires Google Maps et Encoded Polyline Algorithm Format

On a tous utilisé les services web en ligne de calcul d'itinéraire avec maps.google.com...

On peut aussi utiliser la Google Maps API si on souhaite faire une intégration dans son propre site web ou blog. Et on peut aussi utiliser les serices web REST de la Google Maps API pour une intégration coté server.

L'itinéraire est explimé de façon textuelle et aussi à travers un tracé founi de façon encodé via l'Encoded Polyline Algorithm Format. C'est ce que nous alons voir ici. Et notamment une solution de décodage en Java.

Calcul d'itinéraire avec Google Maps API

L'API Google Maps est là depuis longtemps. On notera simplement que depuis le V3, il n'est plus nécessaire d'avoir à manipuler de clé. Pour ce qui est de l'accès gratuit, on peut invoquer 2500 fois les services web Google Maps par tranche de 24h.

Pour ce qui est de la techno client, on peut opter classiquement pour du JavaScript. Mais on a aussi la Google Maps API for Flash.

On a aussi aussi la possibilité d'invoquer les services web de cartographie de façon agnostique : par simples requêtes Http en style REST pour obtennir un résultat textuel en Xml ou Json avec Google Maps API Web services .

Par exemple pour un calcul d'itinéraire, ici de Concorde au Trocadero :

concorde-trocadero

L'url pour obtenir une version purement textuel de cet itinéraire : http://maps.google.com/maps/api/directions/json?sensor=false&origin=trocadero,paris&destination=concorde,paris&mode=walking

On notera dans la réponse l'élément overview_polyline :

{
  "status": "OK",
. . .
      "duration": {
        "value": 1902,
        "text": "32 minutes"
      },
      "distance": {
        "value": 2579,
        "text": "2,6 km"
      },
    "overview_polyline": {
      "points": "isfiH{t}L@aBf@{@eHma@i@uFw@o_@LmAx@aDRqCMyE}@}|ABa@Vw@@_DMm@H[?mCCi@e@MqA{@",

On a une description textuelle de l'iténéraire avec aussi une description graphique sous forme de polyline : ensemble de segments ou liste de points GPS.

Cette liste est verbeuse et Google en propose une version encodé au format Encoded Polyline Algorithm Format : un encodage base64 de la latitude et de la longitude.

Polyline et encodage Encoded Polyline Algorithm Format

Le mathématicien McClure propose sur son site les algorithmes d'encodage et de décodage. Par exemple on peut utiliser le décodeur en ligne pour obtenir les 20 points GPS correspondant à l'itinéraire ci-dessus.

Avec le décodeur ci-dessus, la polyline Concorde-Trocadero encodée en isfiH{t}L@aBf@{@eHma@i@uFw@o_@LmAx@aDRqCMyE}@}|ABa@Vw@@_DMm@H[?mCCi@e@MqA{@ donne :

48.86341, 2.28702
48.86340, 2.28751
48.86320, 2.28781
48.86467, 2.29332
. . .
48.86516, 2.32041
48.86557, 2.32071

markers-trocadero-concorde

Un click dans l'image ci-dessus, permet d'accéder à la version interactive...

Ce décodeur est écrit en Javascript, voir le code source : decode.js.

Décodage de la polyline en Java

Cas de figure : on souhaite développer un serveur en Java, s'appuyant sur les services web REST de calcul d'itinéraire Google Maps. Notre serveur pourra décoder la polyline représentant l'itinéraire pour, par exemple, insérer l'insérer dans une base de données géographique : typiquement dans PostGIS.

Sur son site web, McClure mentionne un portage Java de son algorythme d'encodage : JavaPolylineEncoder. Mais pas de lien pour l'opération inverse : le décodage.

Le code javascript decode.js, vu ci-dessus, peut facilement être porté en Java. C'est ce que j'ai été amené à faire. github Si vous en avez l'utilité, vous pouvez télécharger le code de GitHub : polyline-decode. C'est un petit projet complet avec build Maven et tests unitaires. Vous pouvez utiliser uniquement la classe PolylineDecoder.java.