Consignes

La partie obligatoire de ce TP sera notée par votre intervenant pendant la séance de TP. Vous devrez donc appeler votre intervenant à chaque fois que vous avez terminé une question.

Les questions « bonus » peuvent être envoyée par email à votre intervenant pendant la semaine (7 jours) qui suivent votre séance de TP. N'oubliez pas de mettre votre nom ainsi que votre filière en commentaire dans les fichiers que vous nous envoyez...

Attention : pour que les questions bonus soient prises en compte, il faut que les questions obligatoires aient été traitées. Si vous n'avez pas fini les questions obligatoires en TP, il faut donc les finir chez vous avant de traiter les questions bonus...

La note ne valide pas seulement le résultat de votre programme, mais également son style :

Vérifiez ces points avant de demander à votre intervenant de valider votre code.

Liens utiles

1. GoogleMap, graphe des distances

Dans ce TP, nous allons utiliser l'API ("Application Programming Interface") GoogleMap qui permet à un utilisateur d'interroger le serveur et récupérer des information utilisables par un programme :

  1. on envoie une requête http particulière,
  2. au lieu d'une page web (au format html), on obtient un fichier JSON qui contient les informations demandées,
  3. on convertit très facilement les données JSON en données Python (ou Java, ou n'importe quel autre langage de programmation...

1.1. Distances entres les villes

1.1.1. Distance entre deux villes

Pour demander la distance (à vol d'oiseau) entre New-York et Los-Angeles, il « suffit » d'aller sur la page web http://maps.googleapis.com/maps/api/distancematrix/json?origins=New-York&destinations=los-angeles&sensor=false . Le résultat sera une page-web spéciale dans un format texte appellé "JSON" :

{
   "destination_addresses" : [ "Los Angeles, Californie, États-Unis" ],
   "origin_addresses" : [ "New York, État de New York, États-Unis" ],
   "rows" : [
      {
         "elements" : [
            {
               "distance" : {
                  "text" : "4 469 km",
                  "value" : 4469340
               },
               "duration" : {
                  "text" : "1 jour 21 heures",
                  "value" : 161474
               },
               "status" : "OK"
            }
         ]
      }
   ],
   "status" : "OK"
}

On voit bien la distance entre les deux villes dans le champs "value", du dictionnaire "distance" qui est l'unique élément d'une liste etc.

Pour récupérer cette page web, le mieux est d'utiliser la fonction "get_url" suivante, qui gardera une copie locale afin d'éviter que des requêtes successives pour les même informations repassent par le réseau.

import urllib.request
import os
import hashlib

def get_url(url, local=True, binaire=False):
    """charge une url et crée une version dans un cache local.
    Lors des requêtes suivantes, la verstion en cache est chargée
    si local=True.
    Pour des fichiers binaires (images), utiliser "binaire=True"."""
    if not os.path.exists("url_cache"):
        os.makedirs("url_cache/")
    nom_fichier = "url_cache/"+hashlib.md5(url.encode("utf8")).hexdigest() + ".tmp"

    if os.path.exists(nom_fichier) and local:
        text = open(nom_fichier, "rb" if binaire else "r").read()
    else:
        fichier = open(nom_fichier, "wb" if binaire else "w")
        text = urllib.request.urlopen(url).read()
        if not binaire:
            text = text.decode("utf8","ignore") + '\n'
        fichier.write(text)
        fichier.close()
    return(text)

Écrivez une fonction "distance_deux_villes(ville1, ville2)" qui prend deux noms de ville en arguments et interroge GoogleMap pour obtenir la distance.

L'url est construite de la maniere suivante :

  1. une partie fixe "http://maps.googleapis.com/maps/api/distancematrix/json?"
  2. la ville de départ "origins=XXXX" (notez le "s" dans origins)
  3. la ville d'arrivée "&destinations=XXXX" (notez le "s" dans destinations)
  4. une partie fixe "&sensor=false".

Dans un premier temps, votre fonction devra simplement renvoyer la chaîne de caractères retournée par "get_url". Il suffit donc de contruire l'url et de la passer à "get_url".

Attention, il faut que toutes les villes soient sur le même continent !

1.1.2. Distances entres plusieurs villes

Il est possible de récupérer les distances entre une liste de ville et une autre liste de villes :

Une requête sur l'url http://maps.googleapis.com/maps/api/distancematrix/json?origins=tokyo|hokkaido&destinations=tokyo|osaka|kyoto&sensor=false donne le résultat

{
   "destination_addresses" : [
      "Tokyo, Japon",
      "Ōsaka, Préfecture d'Ōsaka, Japon",
      "Kyōto, Préfecture de Kyōto, Japon"
   ],
   "origin_addresses" : [ "Tokyo, Japon", "Hokkaido Prefecture, Japon" ],
   "rows" : [
      {
         "elements" : [
            {
               "distance" : {
                  "text" : "1 m",
                  "value" : 0
               },
               "duration" : {
                  "text" : "1 minute",
                  "value" : 0
               },
               "status" : "OK"
            },
            {
               "distance" : {
                  "text" : "512 km",
                  "value" : 512076
               },
               "duration" : {
                  "text" : "6 heures 41 minutes",
                  "value" : 24033
               },
               "status" : "OK"
            },
            {
               "distance" : {
                  "text" : "465 km",
                  "value" : 464930
               },
               "duration" : {
                  "text" : "6 heures 9 minutes",
                  "value" : 22112
               },
               "status" : "OK"
            }
         ]
      },
      {
         "elements" : [
            {
               "distance" : {
                  "text" : "1 134 km",
                  "value" : 1134088
               },
               "duration" : {
                  "text" : "17 heures 52 minutes",
                  "value" : 64337
               },
               "status" : "OK"
            },
            {
               "distance" : {
                  "text" : "1 493 km",
                  "value" : 1493375
               },
               "duration" : {
                  "text" : "23 heures 49 minutes",
                  "value" : 85751
               },
               "status" : "OK"
            },
            {
               "distance" : {
                  "text" : "1 446 km",
                  "value" : 1446230
               },
               "duration" : {
                  "text" : "23 heures 17 minutes",
                  "value" : 83831
               },
               "status" : "OK"
            }
         ]
      }
   ],
   "status" : "OK"
}

Le résultat s'interprète de la manière suivante :

  1. les villes de départ ("origin_addresses") sont
    • Tokyo
    • Hokkaido
    dans cet ordre,
  2. les villes d'arrivée ("destination_addresses") sont
    • Tokyo
    • Ōsaka
    • Kyōto
    dans cet ordre,
  3. la suite est une liste de "lignes" ("rows") dans l'ordre des villes de départ
  4. chaque ligne contient une liste d'éléments ("elements") dans l'ordre des villes de d'arrivée
  5. chaque élément contient la distance et la durée du trajet entre la ville de départ et la ville d'arrivée correspondante.

Par exemple, le résultat si dessus peut se résumer en un tableau :

Tokyo Ōsaka Kyōto
Tokyo 0 512076 464930
Hokkaido 1134088 1493375 1446230

Écrivez une fonction "distances_villes(l)" qui prend une liste de villes en argument et récupère les distances entres ces villes. (C'est à dire que les villes d'arrivée et de départs sont identiques.)

L'url est construite de la manière suivante :

  1. une partie fixe "http://maps.googleapis.com/maps/api/distancematrix/json?"
  2. les villes de départ "origins=ville1|ville2|..."
  3. les villes d'arrivée "&destinations=ville1|ville2|..."
  4. une partie fixe "&sensor=false".

Pour éviter que les accents posent des problèmes dans les url, il est fortement conseillé de transformer les chaînes de caractères représentant les villes en utilisant la fonction "urllib.parse.quote".

Par exemple

>>> import urllib.parse
>>> urllib.parse.quote("Chambéry")
'Chamb%C3%A9ry'

Le résultat de la fonction "distances_villes" est une grosse chaîne de caractères. Vous pouvez transformer cette chaîne en données structurées avec le code Python suivant :

import json

    ... # la variable "resultat" contient la chaîne produite par une url
    u = json.loads(resultat)
    distances = {}
    for d in range(len(u["origin_addresses"])):
        ville_depart = u["origin_addresses"][d]
        distances[ville_depart] = {}
        for a in range(len(u["destination_addresses"])):
            ville_arrivee = u["destination_addresses"][a]
            value = int(u["rows"][d]["elements"][a]["distance"]["value"])
            distances[ville_depart][ville_arrivee] = value

Ajoutez ce morceau de Python à la fin de votre fonction distances_villes() et regardez ce que le résultat devient.

Écrivez une procédure "affiche_distances" qui affiche les distances dans un format lisible pour un humain :

>>> affiche_distances(distances)
Distance de Tokyo, Japon à Tokyo, Japon  =  0 mètres.
Distance de Tokyo, Japon à Ōsaka, Préfecture d'Ōsaka, Japon  =  512076 mètres.
Distance de Tokyo, Japon à Kyōto, Préfecture de Kyōto, Japon  =  464930 mètres.
Distance de Hokkaido Prefecture, Japon à Tokyo, Japon  =  1134088 mètres.
Distance de Hokkaido Prefecture, Japon à Ōsaka, Préfecture d'Ōsaka, Japon  =  1493375 mètres.
Distance de Hokkaido Prefecture, Japon à Kyōto, Préfecture de Kyōto, Japon  =  1446230 mètres.

1.1.3. Bonus

Un fichier JSON contient une description au format texte d'une valeur Python. On peut transformer une chaîne JSON en véritable valeur avec la fonction "json.loads(chaîne)".

>>> json.loads('{ "a": [1,2,3], "b": { "c": "toto" } }')
{'a': [1, 2, 3], 'b': {'c': 'toto'}}

Bonus

Modifier le code ajouté à votre fonction distances_villes pour prendre en compte le champs "status" : s'il est à une valeur différente de "OK", il ne faudra pas mettre la distance correspondante dans le dictionnaire.

D'autre part, profitez en pour ne pas ajouter les distances entre une ville et elle même dans la matrice des distances.

En cas d'erreur (status à autre chose que "OK"), vous pouvez provoquer une erreur spéciale avec la ligne suivante

   raise DistanceError(chaîne)

Ceci provoquera une erreur et affichera le contenu de chaîne. Il est conseillé de choisir chaîne de manière pertinente pour que l'utilisateur puisse avoir une idée de la provenance de l'erreur...

Pour ceci, il faut avoir déclaré l'erreur "DistanceError" avec les lignes suivantes :

class DistanceError(Exception):
    def __init__(self,msg):
        self.msg=msg
    def __str__(self):
        return(self.msg)

Bonus

En utilisant la fonction json.loads(chaîne) directement sur le fichier JSON obtenu, cherchez les deux villes pour lesquelles le temps de trajet est le plus long.

Vous afficherez le nom des deux villes ainsi que la durée du trajet dans un format lisible :

>>> eloignement(["Paris", "Lyon", "Marseille"])
Il faut 7 hours 47 mins pour aller de
  - Marseille, France
à
  - Paris, France.

Vous pouvez améliorer encore votre procédure pour que le temps soit affiché en français.

1.2. Calcul du plus court chemin pour aller dans plusieurs villes

Étant donné une liste de ville, nous allons essayer de résoudre le problème du voyageur de commerce : comment se rendre dans toutes les villes en minimisant la distance totale parcourue. Ce problème est très complexe lorsque le nombre de villes atteint l'ordre d'une centaine. Nous nous restreindrons à un petit nombre de villes (une dizaine) pour pouvoir utiliser un algorithme naïf.

L'idée est simplement de tester toutes les possibilités et de garder la meilleure !

Écrivez une fonction "distance_parcourue(dist,l)" qui prend en argument :

et qui calcule la distance totale parcourue si on se rend dans les villes dans l'ordre indiqué par "l", en revenant à son point départ.

Pour générer toutes les possibilités, on utilisera ensuite la fonction "permutations" de la bibliothèque "itertools" :

>>> from itertools import permutations
>>> for l in permutations([1,2,3]):
...         print(list(l))
...
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]

Écrivez une fonction "meilleure_distance(dist)" qui prend une matrice de distances entre villes comme calculée par la fonction "distances_villes", et qui renvoie l'ordre optimal pour se rendre dans toutes les villes.

Cette fonction utilisera la fonction précédente...

Important

Regroupez toutes vos fonctions dans une fonction principale "voyageur_de_commerce(l)" qui prend en argument une liste de villes ("["Paris","Lyon","Marseille","Chambéry"]" par exemple) et qui calcule l'ordre de visite optimal, en affichant la distance parcourue.

1.2.1. Un algorithme plus rapide (BONUS)

Le problème quand on teste toutes les permutations possibles, c'est que ça prend du temps. (Pour n villes, cela fait n! permutations à regarder...)

On peut essayer d'optimiser la distance localement, on choisissant, à chaque fois, la ville la plus proche de là où on se trouve. De cette manière, on n'a pas à explorer toutes les permutations.

Bonus

Cette manière de calculer un trajet ne donne pas toujours le résultat optimal. Donnez un contre-exemple...

Bonus

Écrivez une fonction "meilleure_distance_glouton(dist)" qui prend une matrice de distances entre villes comme calculée par la fonction "distances_villes", et qui renvoie l'ordre de parcours qui choisit la ville la plus proche à chaque fois.

La ville de départ pourra être quelconque...

Vous pouvez supprimer un champs dans dictionnaire avec l'instruction "del" :

>>> d = { "a" : 0 , "b" : 12742 , "c" : 111 }
>>> print(d)
{'a': 0, 'c': 111, 'b': 12742}
>>> del d["b"]
>>> print(d)
{'a': 0, 'c': 111}

Attention toutefois, cela va modifier le dictionnaire. N'oubliez pas d'en faire une copie si vous voulez le garder...

2. GoogleMap, image du résultat (BONUS)

2.1. Récupérer une carte du trajet

Bonus

Écrivez une fonction "url_image(l)" qui prend une liste de villes en arguments et construit l'url de la carte GoogleMap qui montre l'itinéraire entre ces villes.

L'url est construite de la manière suivante :

  1. une partie fixe "http://maps.googleapis.com/maps/api/staticmap?"
  2. les villes du chemin à parcourir "path=ville1|ville2|..."
  3. la description du format d'image "&format=gif&size=600x600"
  4. une partie fixe "&sensor=false".

Attention : les villes que vous avez dans la structure de données peuvent comporter des caractères spéciaux ("Ōsaka, Préfecture d'Ōsaka, Japon" par exemple).

Vous pouvez transformer un nom de ville en chaîne de caractères équivalente spéciale url avec la fonction "urllib.parse.quote(v)".

Vous pouvez maintenant afficher l'image correspondante avec la fonction suivante :

from tkinter import *
import binascii

def affiche_carte(url):
    """affiche la carte correspondant à l'url donnée en argument (q pour fermer la fenêtre)"""

    img = get_url(url,binaire=True)
    img = binascii.b2a_base64(img)

    root = Tk()
    photo=PhotoImage(data=img)
    labl = Label(root, image=photo)
    labl.pack() ;
    root.bind("q", lambda _: root.destroy())
    root.mainloop()

Remarque : on utilise encore "get_url" en précisant bien que l'url ne pointe pas sur un fichier au format texte.

2.2. Interface graphique

Bonus (difficile)

En vous inspirant des fichiers du TP1 celui ci et celui là, créez une petite interface graphique en utilisant la bibliothèque "Tk".

Vous pourrez par exemple :