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.
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()) # 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 :
dessin.create_line
, qui prend en argument des coordonnées (en pixels) de points. Par exemple
dessin.create_line([0, 0, 100, 100, 100, 200])
dessin.create_polygon
, qui prend aussi des coordonnées (en pixels) de points. Par exemple
dessin.create_polygon([0, 0, 100, 100, 100, 200])
x
et la coordonnée y
à chaque fois.
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 :
fill=
pour spécifier la couleur. (La couleur est une chaîne de caractères contenant le nom de la couleur en anglais : "red"
' "black"
, etc. ou bien une chaîne donnant la décomposition RGB en hexadécimal sur 6 caractères : "#00ff80"
.)
width=
pour spécifier l'épaisseur des traits (en pixels).
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.
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 :
points
, qui contient un dictionnaire de points,
chemins
, qui contient un tableau de chemins.
Nous verrons dans la deuxième partie du TP comment récupérer ces données directement depuis le site OpenStreetMap...
La constante points
est un dictionnaire de points. Chaque point a un identifiant unique donné par une chaîne 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
longitude_centre - rayon , latitude_centre - rayon
,
longitude_centre - rayon , latitude_centre + rayon
,
longitude_centre + rayon , latitude_centre + rayon
,
longitude_centre + rayon , latitude_centre - rayon
.
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 :
x = (longitude - (longitude_centre - rayon)) * taille/(2*rayon)
,
y = taille - (latitude - (latitude_centre - rayon)) * taille/(2*rayon)
.
où 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 :
points
: un dictionnaire des points,
longitude_centre
et latitude_centre
: les coordonnées du centre de la carte,
rayon
: le rayon de la carte (en degré),
taille
: la taille de la fenêtre de dessin.
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]... ...
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
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 :
"building"
: les bâtiments,
"highway"
: les routes,
"railway"
: les voies ferrées,
"waterway"
: les voies fluviales,
"shop"
, "sport"
, "tourism"
, ...
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 :
"types"
qui contient les types du chemin (par exemple, "highway
"),
"points"
qui contient la liste des identifiants des points du chemin.
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 :
(5.8702717, 45.6406124)
,
(5.8704311, 45.6402286)
,
(5.8705966, 45.6402608)
,
(5.8704354, 45.6406447)
.
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.
"building"
.
"building"
est dans le tableau des types : si c
est un chemin, on peut faire
if "building" in c["types"]: ...
[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.
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)
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"
).
Les constantes points
et chemins
utilisées jusqu'à présent ont été récupérées sur OpenStreetMap.
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/info224/td4.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.
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 :
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, 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 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)
.
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.
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.)
On peut extraire le dictionnaire des points depuis le fichier XML avec la fonction suivante :
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
On peut également extraire la liste de tous les identifiants ou la liste des types à partir d'un chemin grâce aux fonctions suivantes :
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 chaînes 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 chaînes de caractères """ ty = [] for t in chemin.findall("tag"): ty.append(t.attrib['k']) return ty
Écrivez la fonction liste_chemins(carte)
qui permet de récupérer la liste des chemins à partir d'un document XML.
Vous devrez créer une liste de dictionnaires, où chaque dictionnaire comporte :
points
de type liste d'identifiants,
types
de type liste de chaîne de caractères.
Vous devrez pour ceci utiliser les deux fonctions données ci dessus, ainsi qu'un appel à méthode carte.findall("way")
qui vous donnera la liste des chemins comme liste d'objets XML.
É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 :
get_url
,
carte
avec la fonction XML.fromstring
qui prend en argument une chaîne de caractère XML (le résultat de la fonction get_url
donc),
dictionnaire_points
,
liste_chemins
écrite précédemment,
dessine_chemins
écrite précédemment.
Dessinez les cartes correspondant à
ou
(Attention, le dernier exemple est long...)
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 :
fill=
)
dash=[4,2]
pour la procédure create_line
smooth=1
)
outline=
)
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 chaînes 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 ty
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 :
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 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
.
Modifiez vos fonctions pour permettre :