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

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

from tkinter import *

def test_dessin(taille):
    """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())

    # début du code de dessin
    ...
    ...
    # fin du code de dessin

    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. Visualiser la carte

Les cartes OpenStreetMap sont données par deux entités importantes :

Dans un premier temps, nous allons utiliser le fichier donnees_OpenStreetMap.py qui contient des données récupérées sur OpenStreetMap. Ce fichier définit deux constantes :

Nous verrons dans la deuxième partie du TP comment récupérer ces données directement depuis le site OpenStreetMap...

2.1. Les points

La constante points est un dictionnaire de points. Chaque point a un identifiant unique donné par une chaine de caractères (par exemple : '1265651281'). Cet identifiant sert de clé pour identifier le point dans le dictionnaire. Pour chaque clé, le dictionnaire point contient la longitude et la latitude du point en question. Par exemple, le dictionnaire commence par

points = {
    '1265651281': (5.870229, 45.641839),
    '1265650638': (5.870642, 45.643893),
    '341949246': (5.8722016, 45.6391549),
    ...

Il y a donc un point identifié par '1265651281' qui a pour longitude 5.870229 et pour latitude 45.641839.

Pour pouvoir afficher les points, il faut traduire leurs coordonnées longitude/latitude en pixels. Pour simplifier, nous allons supposer que toutes les cartes que nous allons dessiner sont carrées, autrement dit, le morceau du globe terrestre est que l'on regarde est spécifié par un centre (donné par sa longitude et sa latitude), et un rayon (donnée en degrés). Le morceau du globe terrestre considéré est donc délimité par les quatre coins de coordonnées

Si on suppose que le globe est localement plat, on peut transformer les coordonnées longitude / latitude d'un point en coordonnées x/y en pixels avec une règle de trois. On trouve :

taille est la taille de la fenêtre qui contient la carte que l'on dessine (en pixels).

Écrivez une procédure dessine_points(points, longitude_centre, latitude_centre, rayon, taille=400) qui prend en arguments :

  1. Pour obtenir la liste des clés d'un dictionnaire d, on peut utiliser d.keys(), on peut donc parcourir toutes les cases d'un dictionnaire dic avec :

        ...
        for k in dic.keys():
            ...
            ... dic[k]...
            ...
    
  2. Pour dessiner un pixel, vous pouvez dessiner un polygone de taille 2 avec dessin.create_polygon([x-1,y-1, x+1,y-1, x+1,y+1, x-1,y+1])...

Votre fonction devra dessiner tous les points du dictionnaire dans la fenêtre et elle aura la forme suivante, pour pouvoir créer la fenêtre graphique comme pour la question 1 :

def dessine_points(points, longitude_centre, latitude_centre, rayon, taille=400):

    # initialisation de la fenêtre graphique
    root = Tk()
    root.title("OpenStreetMap - nodes")
    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()

Lorsque vous appelez votre fonction avec les données du fichier donnees_OpenStreetMap.py et les bonnes coordonnées :

>>> from donnees_OpenStreetMap import *
>>> dessine_points(points, longitude_centre, latitude_centre, rayon, taille=600)

vous devriez obtenir

2.2. Les chemins

Les points d'OpenStreetMap ne sont pas très utiles tout seuls. L'entité fondamentale pour les cartes OpenStreeMap est en fait le chemin. Un chemin est une courbe qui relie plusieurs points entre eux. Un chemin a un (ou plusieurs) type(s), comme par exemple :

La constante chemins définit dans le fichier donnees_OpenStreetMap.py est un tableau qui contient des chemins. Chaque chemin est donné par un dictionnaire avec :

Par exemple, le premier chemin du fichier donnees_OpenStreetMap.py est

    {'points': ['359195671', '359195672', '359195675', '359195677', '359195671'],
     'types': ['amenity', 'building']}

Il s'agit donc d'un bâtiment rectangulaire (le dernier point est égal au premier point) et si on regarde dans la constance points, on peut trouver leurs coordonnées :

Pour afficher ce bâtiment, il suffit donc d'utiliser la méthode create_polygon avec les coordonnées (en pixels) de ces 4 points...

Écrivez un procédure dessine_chemins(points, chemins, longitude_centre, latitude_centre, rayon, taille=400) qui dessine tous les bâtiments (types "building") dans une fenêtre.

  1. Il ne faut plus dessiner les points, mais uniquement les bâtiments.
  2. Il ne faut dessiner que les chemins de type "building".
  3. Attention, comme les chemins peuvent avoir plusieurs types, il faut vérifier si "building" est dans le tableau des types : si c est un chemin, on peut faire

                   if "building" in c["types"]:
                       ...
    
  4. Pour dessiner un polygone, il faut au préalable créer une liste [x1,y1, x2,y2, x3,y3, ...] avec les coordonnées en pixels des points du bâtiments. Il faut donc ré-utiliser la formule pour transformer des coordonnées longitude/latitude en coordonnées x/y de la partie précédente.
  5. pour obtenir les coordonnées d'un point à partir de son identifiant id, il faut chercher dans le dictionnaire points avec points[id].

Votre fonction devra ressembler à

def dessine_chemins(points, chemins, longitude_centre, latitude_centre, rayon, taille=400):

    # initialisation de la fenêtre graphique
    root = Tk()
    root.title("OpenStreetMap")
    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()

Testez votre fonction avec

>>> from donnees_OpenStreetMap import *
>>> dessine_chemins(points, chemins, longitude_centre, latitude_centre, rayon, taille=600)

2.3. Quelques ajouts

En plus des bâtiments, il peut être intéressant de tracer les routes (type "highway")...

Modifiez votre procédure dessine_chemins pour qu'elle dessine également les routes.

Une route ne doit pas être tracée par la méthode create_polygon mais avec la méthode create_line.

Modifiez ensuite votre procédure pour qu'elle affiche (en bleu !) les cours d'eau (type "waterway").

3. Récupération des données depuis OpenStreetMap

Les constantes points et chemins utilisées jusqu'à présent ont été récupérées sur OpenStreetMap.

3.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/1516/info224/tp4.html")

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 chaine. Pour convertir un tableau d'octets en chaine, on peut utiliser :

bs = f.readline()			# bs est un tableau d'octets
s = bs.decode("UTF-8", "ignore")    	# s est une chaine 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 chaine 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.

3.2. API OpenStreetMap

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.

Faites attention à l'ordre des arguments : les deux valeurs minimales (longitude et lattitude) suivies des deux valeurs maximales (longitude et lattitude).

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

def url_openstreetmap(longitude, latitude, rayon):
    """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
      - rayon, de type flottant : écartement (en degrés) par rapport au centre de la carte

    retour de type chaine 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).

3.3. Extraction des points du fichier XML

Les fichiers carte OpenStreetMap sont au format XML et sont constitués de points (node) et de chemin (way). 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 point avec ces coordonnées (lat et lon) et son 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"/>
 </way>

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

Python possède une bibliothèque qui permet de lire et parcourir les documents XML. On peut par exemple récupérer facilement tous les "node" avec une méthode spécifique : si carte correspond à un document XML, carte.findall("node") donnera la liste de tous ces "nodes".

On peut également extraire la liste de tous les identifiants ou la liste des types à partir d'un chemin grâce aux fonctions suivantes : Les fonctions suivantes permettent de récupérer les points (node) d'un document, les types d'un chemin et la liste des chemins:

import xml.etree.ElementTree as XML
def dictionnaire_points(carte):
    """récupère le dictionnaire des points depuis un objet XML
    [OpenStreetMap http://www.openstreetmap.org/] valide.
      - argument carte : de type XML
      - résultat de type dictionnaire : dictionnaire de tous les points avec :
          . leur identifiant comme clé
          . leurs coordonnées longitude/latitude comme valeur
    """
    dico = {}
    for n in carte.findall("node"):
        dico[n.attrib["id"]] = (float(n.attrib["lon"]), float(n.attrib["lat"]))
    return dico

def liste_points(chemin):
    """récupère la liste des identifiants de points dans un chemin
      - argument chemin : de type XML
      - résultat de type liste de chaines de caractères : liste des identifiants
    """
    l = []
    for n in chemin.findall("nd"):
        l.append(n.attrib["ref"])
    return l

def liste_types(chemin):
    """récupère la liste des types d'un chemin
      - argument chemin : de type XML
      - résultat de type liste de chaines de caractères
    """
    ty = []
    for t in chemin.findall("tag"):
        ty.append(t.attrib['k'])
    return ty

def liste_chemins(carte):
    """récupère la liste des chemins d'un objet XML"""
      chemins = []
      for chemin in carte.findall("way"):
          c = {}
          c["points"] = liste_points(chemin)
          c["types"] = liste_types(chemin)
          chemins.append(c)
      return chemins

Écrivez la procédure dessine_carte(longitude_centre, latitude_centre, rayon=0.002, taille=800) qui permet de dessiner une carte à partir de coordonnées.

Pour ceci, vous devrez :

  1. créer l'url OpenStreetMap correspondante avec url_openstreetmap,
  2. récupérer les données avec get_url,
  3. créer un objet XML carte avec la fonction XML.fromstring qui prend en argument une chaine de caractère XML (le résultat de la fonction get_url donc),
  4. récupérer le dictionnaire des points avec la fonction dictionnaire_points,
  5. récupérer la liste des chemins à dessiner avec liste_chemins,
  6. appeler la fonction dessine_chemins écrite précédemment.

Dessinez les cartes correspondant à

ou

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

3.4. Minimiser la déformation de la carte

Il est 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 rayon différents pour la longitude et la latitude : on peut par exemple calculer la latitude minimale et maximale avec latitude-rayon/cos(longitude*pi/180)) et latitude+rayon/cos(longitude*pi/180)) au lieu de latitude-rayon et latitude+rayon.

Pour pouvoir accéder aux fonctions trigonométriques ou la valeur de pi, il faut ajouter

from math import *

au début de votre fichier.

Modifiez vos fonctions pour permettre :

3.5. Améliorations

Ajoutez la gestion des voies ferrées (type "railway") et vérifiez qu'elles sont bien affichées en affichant la carte du centre de Chambéry.

Pour donner un peu de vie aux cartes que vous dessinez, essayez d'utiliser :

Voici par exemple la carte du centre de Chambéry obtenue de cette façon :

Vous pouvez parfois récupérer le nom d'un chemin :

def nom(chemin):
    """récupère le nom d'un chemin
      - argument chemin : de type XML
      - résultat de type chaines de caractères, éventuellement vide s'il n'y a pas de nom
    """
    nom = ""
    for t in chemin.findall("tag"):
        if t.attrib['k'] == "name":
            nom = nom + t.attrib['v']
    return nom

Chercher le nom de la fonction qui permet d'afficher du texte dans une fenêtre (en allant par exemple voir la page http://effbot.org/tkinterbook/canvas.htm) et ajouter la possibilité de mettre des légendes (rue, bâtiments, etc.).

Vous pourriez par exemple obtenir la carte suivante :