Consignes

Vous devrez utiliser l'interface TPLab pour envoyer vos TP aux encadrants de TP.

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. Préliminaires : mini interface graphique (30 minutes)

La procédure suivante initialise une petite fenêtre de dessin :

from tkinter import *

def test_dessin(taille=400):
    """procédure pour tester l'interface de dessin
paramètre : taille, de type entier : taille en pixel de la zone de dessin"""
    # initialisation de la fenêtre graphique
    root = Tk()
    root.title("test")
    root.resizable(width=False, height=False)
    root.bind('q', lambda _: root.destroy())
    dessin = Canvas(root, width=taille, height=taille, background='white')
    dessin.pack()
    dessin.bind('q', lambda _: root.destroy())

    ...

    mainloop()

La variable dessin est un objet particulier avec de nombreuses méthode de dessin associées :

Attention : l'origine de la fenêtre est le coin en haut à gauche, et l'axe vertical est orienté vers le bas.

Les méthodes create_line et create_polygon ont en commun des arguments optionnels :

Complétez la fonction de test pour dessiner un carré plein rouge avec ses diagonales en noir au milieu de la fenêtre de dessin.

2. Récupération des données OpenStreetMap (1 heure)

2.1. Récupérer des fichiers par leur url

Nous avons vu comment ouvrir un fichier sur le disque dur : on utilise la fonction open de Python. Il est également possible d'ouvrir (en lecture seulement) un fichier sur le réseau : on utilise la fonction urlopen :

from urllib.request import urlopen
f = urlopen("http://lama.univ-savoie.fr/~hyvernat/Enseignement/1213/info113/td1.txt")

Le résultat d'un urlopen(url) est très similaire au résultat d'un open(fichier) : on peut accéder au contenu grace aux méthodes read ou readline.

Les fichiers réseaux sont ouverts en mode binaire. Le résultat d'un read ou d'un readline est donc un tableau d'octets au lieu d'une chaîne. Pour convertir un tableau d'octets en chaîne, on peut utiliser :

bs = f.readline()			# bs est un tableau d'octets
s = bs.decode("UTF-8", "ignore")    	# s est une chaîne de caractères en ASCII

Attention : les octets qu'on ne peut pas décoder en caractère UTF-8 seront ignorés. si le fichier n'est pas un document texte en UTF-8, la variable s contiendra seulement les caractère non accentués du document.

Pour simplifier la gestion des documents téléchargés, nous utiliserons la fonction suivante :

import os
import hashlib
from urllib.request import urlopen

  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 version 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("UTF-8")).hexdigest() + ".tmp"

      if os.path.exists(nom_fichier) and local:
          if binaire:
              text = open(nom_fichier, "rb").read()
          else:
              text = open(nom_fichier, "r", encoding="UTF-8").read()
      else:
          text = urlopen(url).read()
          if len(text) > 0:
              if binaire:
                  fichier = open(nom_fichier, "wb")
              else:
                  fichier = open(nom_fichier, "w", encoding="UTF-8")

              if not binaire:
                  text = text.decode("UTF-8", "ignore") + '\n'
              fichier.write(text)
              fichier.close()
      return(text)

Cette fonction télécharge le fichier à l'url demandé et le convertit en grosse chaîne de caractères. Chaque document téléchargé est sauvegardé dans un répertoire temporaire ("url_cache") et lorsqu'on redemande un document déjà téléchargé, la fonction le récupère sur le disque.

2.2. API OpenStreetMap

Dans ce TP, nous allons utiliser l'API ("Application Programming Interface") OpenStreetMap 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 XML ("eXtensible Markup Language") qui contient les informations demandées,
  3. on utilise les données XML grâce à des bibliothèques Python (ou Java, ou n'importe quel autre langage de programmation)...

L'API OpenStreetMap permet de télécharger les données d'une carte rectangulaire à partir de ses coordonnées :

Par exemple, les coordonnées de la Tour Eiffel sont :

On peut donc obtenir les informations avec 2.2946±0.002 comme longitudes et 48.8584±0.002 comme latitudes.

Lorsque l'on a les coordonnées, il suffit de récupérer l'url http://api.openstreetmap.org/api/0.6/map?bbox=lon_min,lat_min,lon_max,lat_max. Par exemple : http://api.openstreetmap.org/api/0.6/map?bbox=2.2926,48.8564,2.2966,48.8604

Écrivez une fonction qui permet de construire une url pour un morceau de carte :

def url_openstreetmap(longitude, latitude, delta):
    """construit l'url de téléchargement des données d'une carte sur OpenStreetMap.

paramètres :
  - longitude, de type flottant : longitude du point central de la carte
  - latitude, de type flottant : latitude du point central de la carte
  - delta, de type flottant : écartement (en degrés) par rapport au centre de la carte

retour de type chaîne de caractères : url du morceau de carte correspondant """

Par exemple, l'url ci dessus a été obtenue avec url_openstreetmap(2.2946, 48.8584, 0.002).

Il est théoriquement impossible de plaquer un morceau de sphère sur un carré sans déformation. Pour corriger un peu cette déformation, on peut considérer des delta différents pour la longitude et la latitude : on peut par exemple calculer la latitude minimale et maximale avec latitude-delta/cos(longitude*pi/180)) et latitude+delta/cos(longitude*pi/180)) au lieu de latitude-delta et latitude+delta.

2.3. Fichers XML

Les fichiers carte OpenStreetMap sont au format XML et sont constités de noeuds (node) et de chemin (way). Chaque chemin est lui même composé de noeuds. Par exemple, le fichier correspondant à l'url donnée ci dessus contient

<node id="21378260" lat="48.8566913" lon="2.2935429"/>
<node id="24909428" lat="48.8600801" lon="2.2952792"/>
<node id="33388329" lat="48.8576384" lon="2.2947551"/>

Chacune de ces lignes identifie un noeud avec ces coordonnées (lat et lon) et un identifiant (id). Le chemin

<way id="69034494">
  <nd ref="828545285"/>
  <nd ref="828545086"/>
  <nd ref="828543774"/>
  <nd ref="828543773"/>
  <nd ref="828545285"/>
  <tag k="building" v="yes"/>
  <tag k="source" v="cadastre-dgi-fr source : Direction Générale des Impôts - Cadastre. Mise à jour : 2010"/>
 </way>

contient 5 noeuds, identifiés par leur ... identifiant.

La bibliothèque xml permet de lire et parcourir les documents XML en python. On peut en particulier transformer un fichier ou une chaîne de caractères en document XML. (À condition que le fichier ou la chaîne contienne un description XML valide.)

>>> import xml.etree.ElementTree as XML
...
>>> carte = XML.fromstring(s)

L'avantage est que la variable carte devient un objet de type "document XML", avec des méthodes spécifiques. Par exemple, on peut retrouver la liste des noeuds (les éléments node) avec

>>> liste_noeuds = carte.findall("node")

On peut ensuite accéder aux attributs (identifiant, longitude, latitude) de chaque noeud avec attrib :

>>> premier_noeud = liste_noeuds[0]
>>> print("Le premier noeud a l'identifiant", premier_noeud.attrib["id"])
Le premier noeud a l'identifiant 368288

premier_noeud.attrib est donc simplement un dictionnaire contenant tous les attributs.

Écrivez une fonction pour récupérer les coordonnées de chaque noeud en flottant :

def dictionnaire_noeuds(carte):
    """créé un dictionnaire de noeuds à partir des données brutes d'une carte

Paramètre : carte, de type document XML : carte

retour de type dictionnaire : associe à chaque identifiant de noeud,
ses coordonnées (càd une paire de flottants : longitude et latitude)"""

Pour tester :

>>> carte = XML.fromstring(get_url(url_openstreetmap(2.2946, 48.8584, 0.002)))
>>> dico_noeuds = dictionnaire_noeuds(carte)
>>> print(len(dico))
2345
>>> i = "368288"
>>> print("coordonnées du noeud", i, dico_noeuds[i])
coordonnées du noeud 368288 (2.2954514, 48.8579822)

3. Visualiser la carte (1 heure)

3.1. Les chemins

Les éléments de carte OpenStreetMap sont contenu dans des "chemin" : des élément appelés way dans les données XML. Par exemple :

<way id="69034494">
  <nd ref="828545285"/>
  <nd ref="828545086"/>
  <nd ref="828543774"/>
  <nd ref="828543773"/>
  <nd ref="828545285"/>
  <tag k="building" v="yes"/>
  <tag k="source" v="cadastre-dgi-fr source : Direction Générale des Impôts - Cadastre. Mise à jour : 2010"/>
 </way>

représente un bâtiment (<tag k="building">) donné par cinq noeud (nodes <nd .../>). Seuls les identifiants des noeuds sont donnés. On peut donc retrouver leurs coordonnées dans le dictionnaire renvoyé par la fonction dictionnaire_noeuds.

Comme pour les noeuds, on peut récupérer la liste des chemins avec

>>> liste_chemins = carte.findall("way")

Chaque chemin est également un morceau de document XML, et on peut donc récupérer les éléments nd avec findall :

>>> chemin = liste_chemins[0]
>>> ln = chemin.findall("nd")

Écrivez une fonction

def liste_noeuds(chemin):
    """récupère la liste des identifiants de noeuds dans un chemin

Paramètre : chemin, de type XML

retour de type liste de chaînes de caractères (liste des identifiants de noeuds)"""

Pour ceci, la première étape est de récupérer la liste des éléments nd du chemin avec chemin.findall("nd"). Il faudra ensuite parcourir cette liste pour récupérer les identifiants (attribut "ref").

3.2. Types des chemins

Chaque chemin a un type. Il y a par exemple

Le type d'un chemin est stocké dans dans l'attribut "k" d'un élément tag. Malheureusement, un chemin peut avoir de nombreux éléments tag. On peut tester si un chemin est d'un certain type en :

Écrivez la fonction

def chemin_de_type(chemin, type):
    """teste si un chemin OpenStreeMap a un certain type

Paramètre :
  chemin, de type XML : chemin de la carte
  type, de type chaîne de caractères : type du chemin que l'on cherche

retour de type booléen"""

3.3. Un premier dessin

Étant donnés les coordonnées longitude / latitude d'un point, on peut calculer (une approximation) de ses coordonnées en pixel dans carré de taille de la manière suivante

    x = (longitude - longitude_min) * taille/(2*delta_longitude)
    y = taille - (latitude - latitude_min) * taille/(2*delta_latitude)

Écrivez la procédure suivante :

def dessine_routes(longitude, latitude, delta = 0.002):
    """dessine les routes dans la zone donnée

paramètres :
  - longitude, de type flottant : longitude du point central de la carte
  - latitude, de type flottant : latitude du point central de la carte
  - delta, de type flottant : écartement (en degrés) par rapport au centre de la carte"""

Pour les coordonnées de la tour Eiffel, vous devriez obtenir quelque chose comme

Pour cela, vous devrez :

  1. créer l'url appropriée (fonction url_openstreetmap)
  2. récupérer les données brutes sous forme de chaine de caractères (fonction get_url)
  3. transformer les données brute en objet XML (avec XML.fromstring)
  4. récupérer le dictionnaire de tous les noeuds avec leurs coordonnées (fonction dictionnaire_noeuds)
  5. récupérer la liste de tous les chemins (avec .findall("way"))
  6. en extraire la liste de toutes les routes (en utilisant la fonction chemin_de_type)
  7. dessiner chaque route de la liste c'est à dire :
    • récupérer la liste des identifiants des éléments nd du chemin (avec la fonction liste_noeuds)
    • mettre les coordonnées correspondant à chaque nd dans une liste
    • dessiner la route avec un create_line

Bien sûr, il faudra pour pouvoir dessiner initialiser une fenêtre graphique comme dans la partie préliminaires.

Si vous avez utilisé la formule latitude_max = latitude+delta/cos(longitude*pi/180)) dans votre fonction url_openstreetmap, il faudra la réutiliser pour transformer les coordonnées longitude / latitude en pixels.

4. Améliorations

En partant de votre procédure dessine_route, écrivez une procédure dessine_route_batiment qui dessine les routes et les bâtiments (type building).

Attention : pour les bâtiments, il faut utiliser create_polygon au lieu de create_line.

Votre carte pourrait ressembler à

Dessinez les cartes correspondant à

ou

(Attention, le dernier exemple est long...)

Transformez votre procédure précédente en procédure dessine_carte qui dessine d'autres éléments...

Pour donner un peu de vie aux cartes que vous dessinez, vous pouvez utiliser :

Bonus

Complétez votre programmes avec, par exemple :