De nombreux services en ligne permettent de récupérer facilement des données en ligne via des interfaces HTTP. Il suffit d'envoyer une requête bien formée au bon serveur pour avoir accès à une grande quantité d'information. Aujourd'hui nous récupérons des données sur OpenStreetMap. Cela va nous demander d'écrire une requête en Xml, d'envoyer la requête à un serveur, de parser la réponse (au format json), d'utiliser une représentation adaptée pour les informations récupérées et de faire un petit calcul avec (en l'occurence, on va calculer la longueur du réseau routier de Marseille).
Nous aurons besoin des librairies suivantes :
Voici la requête pour récupérer l'ensemble des voies de Marseille (de l'autoroute au chemin de randonnée) :
Le langage de requête est spécifié ici, mais comme il ne s'agit pas du sujet du TP de l'apprendre, on va se contenter de cette requête.
On pourrait simplement mettre cette requête dans une chaîne de caractères, mais il est plus intéressant d'apprendre à générer des requêtes bien construites directement en OCaml. Pour cela il faudrait utiliser une librairie permettant de travailler avec le format Xml, telle que Xmlm ou Tyxml. On va utiliser un petit module qui est en fait une version très simplifiée de Tyxml, que vous pouvez récupérer ici.
Un fichier Xml décrit une structure arborescente. Les nœuds (par exemple : osm-script, union, ...) sont décrits par des balises de la forme <nom attributs> suivi d'une liste des enfants puis une balise fermante </nom>. Les attributs (par exemple output, type,...) de chaque nœud sont des informations associées au nœud, sous la forme de paires (identifiant, chaînes de caractère). Il peut aussi y avoir des données entre les balises. Certains balises n'ont pas d'enfant et s'écrivent alors <nom/> ou <nom attributs/>.
Le module Xml définit :
Lisez l'interface et comprenez comment construire un arbre Xml avec. Puis créez un fichier fetchMarseilleStreets.ml dans lequel vous définissez la requête pour OpenStreetMap, de type Xml.t.
Vous pouvez compiler xml.cmo et le charger dans l'interpréteur. La valeur Xml.pp est un formatteur, vous pouvez donc faire dans l'interpréteur :
Ainsi, lors de l'évaluation d'une valeur de type Xml.t, la chaîne de caractères correspondant au Xml s'affichera. Vous pouvez ainsi vérifier que la requête est bien formée.
OpenStreetMap propose un service d'accès en lecture aux données grâce au serveur se trouvant à l'adresse http://overpass-api.de/api/, et qui est nommé l'API overpass. Le principe est de faire une requête HTTP POST sur ce serveur avec comme corps de la requête le Xml spécifiant ce que nous voulons récupérer.
Attention, l'usage de ce serveur est limité : nous ne voulons pas nous faire bannir du serveur, donc :
Du coup, nous allons récupérer la réponse du serveur et la mettre dans un fichier. Nous relirons ensuite le fichier dès que nous en aurons besoin, sans que cela contribue à notre quota de requêtes sur overpass-api.
Cohttp est une librairie permettant de créer très simplement des clients ou des serveurs HTTP. Pas besoin d'avoir suivi le cours de réseau ! Nous allons spécifiquement utiliser les fonctions de Cohttp fonctionnant avec Lwt, une librairie de promesses que nous avons déjà croisée. Une valeur de type 'value Lwt.t est un calcul qui fournira éventuellement (un jour) une valeur de type 'value.
Créez un fichier fetchPostXml.ml. Commencez le fichier par :
Client contient les fonctions nécessaires pour envoyer des requêtes HTTP à un serveur distant et traiter les réponses. Body contient les fonctions de manipulations du corps d'une requête HTTP. Spécifiquement, nous aurons besoin des fonctions suivantes :
La documentation complète est ici.
Souvenons-nous que les 'value Lwt.t s'interprêtent comme des calculs qui produiront éventuellement une valeur de type 'value. Techniquement, on s'en sert ici pour encoder les fonctions bloquantes (open_file, write, post par exemple). On peut ensuite séquencer plusieurs opérations bloquantes avec >>=.
Écrivez la fonction create_query_body qui étant donné un argument de type Xml.t, retourne le corps de type Body.t correspondant.
Supposons que nous ayons la réponse, sous la forme d'une en-tête response et d'un corps body. On souhaite écrire une fonction permettant d'extraire uniquement le corps. Il suffirait donc de retourner simplement body. Mais on veut aussi vérifier que la réponse du serveur est positive. Pour cela, on observe d'abord Cohttp.Response.(response.status) : Cohttp.Code.status_code. Si c'est `OK (en majuscules), on retourne effectivement le corps. Sinon, on provoque un échec avec Lwt.fail, qui prend en argument une exception.
On définit donc une exception :
Écrivez la fonction :
Utilisez les fonctions de Lwt_io pour, étant donné un argument de type Body.t et un nom de fichier, écrire le contenu du corps dans un fichier de ce nom. Il faut bien sûr ouvrir le fichier, écrire le corps (avec Body.write_body) et fermer le fichier.
On peut maintenant écrire la fonction qui prend une adresse, un corps et un nom de fichier, envoie la requête POST au serveur, reçoit la réponse et l'écrit dans un fichier.
Notez comme toutes les fonctions que nous avons utilisée ont un type de la forme 'arg -> 'result Lwt.t. C'est le type idéal pour faire des pipes avec Lwt.(>>=). Au final, le programme devrait donc être aussi simple que si nous n'avions pas utilisé Lwt.
Ajoutez une interface fetchPostXml.mli (exporter l'exception, create_body_query et fetch).
Revenez ensuite au fichier fetchMarseilleStreets.ml.
Créez l'adresse du serveur, de type Uri.t. Il suffit d'utiliser la fonction Uri.of_string avec comme argument la chaîne de caractères contenant l'URL du serveur : "http://overpass-api.de/api/interpreter" .
Créez une valeur main, qui utilise fetch pour créer un calcul (de type unit Lwt.t) effectuant la requête, qui produira le fichier marseille.json. Idéalement, il faudrait aussi utiliser Lwt.catch pour gérer les éventuelles erreurs (faites-le si vous avez une erreur, en ajoutant l'affichage d'un message détaillant la réponse du serveur). Il nous reste à dire à l'ordonnanceur de traiter ce calcul avec :
Compilez fetchMarseilleStreets.native et exécutez-le. Vérifiez que cela a bien créé un fichier contenant la réponse du serveur (le fichier doit faire de l'ordre de 25Mo ± 5Mo).
Nous allons maintenant charger le fichier et représenter les données avec des types adaptés en OCaml.
Créez un fichier osm.ml. Dans ce fichier, créez deux modules, Node pour représenter les nœuds d'OpenStreetMap (des points), Way pour représenter les routes d'OpenStreetMap (des listes de nœuds). Au début du fichier ajoutez :
Chaque nœud possède :
Ajoutez dans Node un type t pour représenter les nœuds.
On veut maintenant ajouter une fonction convertissant une valeur de type Yojson.Basic.json en un nœud. Pour rappel, voici ce type :
Un nœud est représentée par une liste d'association `Assoc, possédant les éléments ayant les clés "type" , "id" , "lat" , "lon" et "tags" , et tel que "type" est associé à "node" . Par exemple :
Créez une fonction prenant une valeur de type json et retournant un nœud si elle représente un nœud, rien sinon :
Comme le champ tags est optionnel, on utilisera Json.to_option Json.to_assoc pour traiter le contenu de ce champ (ce qui nous fournit une option sur le contenu du champ).
Enfin, ajoutez la fonction suivante qui associe à chaque nœud une position (adaptez-la selon votre type des nœuds) :
Chaque route possède :
Ajoutez un type pour représenter les routes, et une fonction de comparaison.
Pour créer la liste des nœuds d'une route, il faut pouvoir retrouver un nœud à partir d'un identifiant entier. Pour cela, on utilise une structure de dictionnaire. Le module IdMap que nous avons défini en haut du fichier est un dictionnaire sur des clés entières. On peut donc utiliser les fonctions :
Mieux vaut redéfinir find avec des options :
Créez une fonction convertissant une liste json d'entiers en une liste de nœuds. La fonction prend en argument le dictionnaire de tous les nœuds. On pourra utiliser List.filter_opt : 'elt option list -> 'elt list.
Les routes ressemblent à ceci en json :
Créez une fonction de conversion d'une valeur de type json vers Way.t option.
On peut lire le fichier json avec la fonction :
Le json contient une liste d'association, ayant un membre "elements" associé à une liste. Chaque élément de la liste est un nœud ou un route (ou une relation).
Créez le dictionnaire des nœuds définis dans le fichier. Créez la liste des routes définies dans le fichier.
Créez une fonction calculant la longueur d'une route :
Puis calculez la longueur totale des routes de Marseille.