Exportation GeoJSON, recherche d'arrêts
ReCHor – étape 8
1. Introduction
Cette étape a deux buts : d'une part, permettre l'exportation du tracé d'un voyage au format GeoJSON, afin de pouvoir le visualiser sur une carte ; et d'autre part, offrir un moyen de rechercher, de manière aussi agréable et efficace que possible, un arrêt de transport public par nom.
2. Concepts
2.1. Voyages au format GeoJSON
Une des fonctionnalités offertes par ReCHor est la visualisation du tracé d'un voyage sur une carte. Cette visualisation est faite sur un site Web externe, auquel le tracé est transmis dans un format nommé GeoJSON, qu'il convient donc de décrire rapidement, de même que le format JSON sur lequel il se base.
2.1.1. Format JSON
JSON (pour JavaScript Object Notation) est un format textuel relativement simple permettant de représenter des données organisées en hiérarchie. Ce format est très utilisé sur le Web pour échanger des données entre ordinateurs.
Un document JSON peut être composé de valeurs atomiques — c.-à-d. qui ne sont pas composées d'autres valeurs plus petites — et de valeurs composites.
Les deux seuls types de valeurs atomiques que nous utiliserons dans ce projet sont :
- les nombres, dont la syntaxe est, à quelques détails près, la même que celle des valeurs de type
double
en Java, - les chaînes, dont la syntaxe est, à quelques détails près, la même que celle des chaînes en Java.
En plus de ces types de valeurs atomiques, nous utiliserons également les deux types de valeurs composites de JSON, qui sont :
- les tableaux, dont les éléments sont entourés par des crochets (
[
et]
) et séparés par des virgules (,
), - les « objets », similaires à des tables associatives, dont les paires clef/valeur sont entourées par des accolades (
{
et}
) et séparées par des virgules (,
) ; la clef et la valeur d'une paire sont séparées par un deux-points (:
).
Les espaces entre les différents éléments sont ignorées.
Par exemple, le document JSON suivant représente un tableau contenant 4 éléments, chacun d'entre eux étant un objet constitué de deux paires clef/valeur, la première ayant pour clef name
, la seconde ayant pour clef radius
.
2.1.2. Format GeoJSON
GeoJSON (pour Geographic JSON) est une version de JSON permettant de représenter des données géospatiales, c.-à-d. des données géométriques — points, lignes, polygones, etc. — à la surface de la Terre.
Dans le cadre de ce projet, nous utiliserons le format GeoJSON pour exporter le tracé (approximatif) d'un voyage afin de le visualiser sur un site Web externe. Le seul type de donnée que nous utiliserons pour cela est la ligne brisée (polyline ou line string) et c'est donc le seul qui sera décrit ici.
Dans le format GeoJSON, une ligne brisée est représentée par un objet ayant deux paires clef/valeur :
- la première a pour clef
type
et pour valeurLineString
, et identifie l'objet comme étant une ligne brisée, - la seconde a pour clef
coordinates
et pour valeur un tableau de tableaux de nombres, chaque sous-tableau contenant deux nombres représentant les coordonnées géographiques (longitude et latitude, en degrés) d'un point de la ligne brisée.
Par exemple, le document GeoJSON suivant représente une ligne brisée reliant, dans cet ordre, les gares de Lausanne, Palézieux, Romont et Fribourg:
Ce document GeoJSON peut être validé et visualisé sur différents sites Web comme GeoJSONLint, geojson.io ou uMap.
2.2. Recherche d'arrêts
Dans la version finale de ReCHor, un utilisateur pourra rechercher un voyage en choisissant un arrêt de départ, un arrêt d'arrivée, et une date/heure de voyage. Pour que le choix des arrêts puisse être fait de manière aussi agréable et efficace que possible, il est capital que l'utilisateur ne doive pas entrer exactement le nom de l'arrêt désiré, mais qu'il puisse le rechercher en écrivant une partie de son nom.
Par exemple, il devrait être facile de trouver l'arrêt Renens VD sans devoir écrire le R initial en majuscule, ou en omettant le VD à la fin. De même, il devrait être possible de trouver l'arrêt Mézières FR, village sans utiliser d'accents ou en omettant la virgule, par exemple en entrant quelque chose comme mez vil. Finalement, il devrait être possible de trouver certains arrêts en utilisant des noms équivalents au nom officiel, par exemple Losanna au lieu de Lausanne.
Dans ce but, le texte entré par l'utilisateur sera traité comme une requête (query) qui sera utilisée pour rechercher tous les arrêts auxquels elle pourrait correspondre. Ces arrêts seront ensuite triés afin que ceux qui semblent les plus pertinents apparaissent en premier, puis ils seront présentés à l'utilisateur pour qu'il choisisse l'un d'eux.
2.2.1. Recherche
Pour trouver les arrêts dont le nom correspond à une requête, celle-ci est tout d'abord découpée en sous-requêtes (sub-queries), séparées les unes des autres par des espaces. Par exemple, la requête mez vil est composée de deux sous-requêtes, mez et vil. L'ordre des sous-requêtes n'est pas important, donc la requête vil mez est totalement équivalente à mez vil.
Les noms d'arrêts correspondant à une requête sont ceux qui contiennent chacune des sous-requêtes. Toutefois, le test pour déterminer si un nom d'arrêt contient une sous-requête n'est pas effectué de manière stricte, dans la mesure où :
- une lettre non accentuée est considérée comme équivalente à n'importe laquelle de ses variantes accentuées,
- si la sous-requête ne comporte aucune majuscule, alors une lettre minuscule est considérée comme équivalente à sa variante majuscule.
La table ci-dessous précise la première de ces règles en montrant les 6 lettres qui sont considérées comme équivalentes à d'autres lettres — généralement leurs variantes accentuées — ainsi que ces équivalences :
lettre | équivalences |
---|---|
c |
c , ç |
a |
a , á , à , â , ä |
e |
e , é , è , ê , ë |
i |
i , í , ì , î , ï |
o |
o , ó , ò , ô , ö |
u |
u , ú , ù , û , ü |
Par exemple, le nom d'arrêt Mézières FR, village correspond à la requête mez vil car :
- il contient — au sens non strict décrit ci-dessus — la sous-requête mez, puisque Méz est considéré comme équivalent à mez,
- il contient la sous-requête vil.
Ce même nom d'arrêt ne correspond toutefois pas à la requête meZ vil, car le Z majuscule de la première sous-requête force le test d'appartenance à respecter les majuscules et minuscules. De même, il ne correspond pas à la requête mèz vil, car le è (e grave) de la première sous-requête n'est pas équivalent au é (e aigu) présent dans le nom.
2.2.2. Tri
Une fois que les noms des arrêts correspondant à la requête ont été obtenus, ils doivent encore être triés par pertinence, de la plus élevée à la plus basse.
La pertinence d'un nom d'arrêt par rapport à une requête est déterminée en attribuant un « score de pertinence » à chaque nom d'arrêt. Ce score est déterminé comme suit.
Le score de base d'une sous-requête est le pourcentage — arrondi vers le bas à l'entier le plus proche — du nom de l'arrêt correspondant à la sous-requête. Par exemple, le score de base de la sous-requête mez pour le nom d'arrêt Mézières FR, village vaut 15, car 15% des caractères du nom — 3 sur 20 — correspondent à la sous-requête.
Ce score de base est encore éventuellement multiplié par :
- un facteur 4 si la sous-requête correspond au début d'un mot, c.-à-d. si elle se trouve tout au début du nom de l'arrêt, ou si le caractère la précédant dans ce nom n'est pas une lettre,
- un facteur 2 si la sous-requête correspond à la fin d'un mot, c.-à-d. si elle se trouve tout à la fin du nom, ou si le caractère la suivant n'est pas une lettre.
Lorsqu'une sous-requête correspond à la fois au début et à la fin d'un mot, son score de base est multiplié par les deux facteurs, donc 8 au total.
Si une sous-requête apparaît plusieurs fois dans un nom d'arrêt, alors seule sa première occurrence est utilisée pour déterminer son score.
Finalement, les scores de toutes les sous-requêtes sont additionnés pour obtenir le score de la requête complète.
2.2.3. Exemple
La table ci-dessous donne les scores de la requête mez vil pour les 4 arrêts de l'horaire officiel auxquels elle correspond.
Nom d'arrêt | Score |
---|---|
Mézières FR, village |
120 |
Mézières VD, village |
120 |
Mézery-près-Donneloye, village |
80 |
Charleville-Mézières |
75 |
Pour les deux premiers arrêts, le score (120) est déterminé ainsi : les deux sous-requêtes ont la même longueur, donc le même score de base, qui vaut 15 ; les deux sont de plus au début d'un mot, mais pas à la fin, donc ces scores de base sont multipliés par 4 pour donner 60. Finalement, ces deux scores individuels des sous-requêtes sont additionnés pour obtenir celui de la requête, qui vaut donc 120.
2.2.4. Recherche par expressions régulières
À première vue, la recherche d'une sous-requête dans un nom d'arrêt paraît difficile à mettre en œuvre, car elle doit se faire de la manière non stricte décrite plus haut — en considérant une lettre non accentuée comme équivalente à n'importe laquelle de ses variantes accentuées, et en ignorant généralement la distinction entre les majuscules et les minuscules. Toutefois, grâce au concept d'expression régulière (regular expression) et aux classes de la bibliothèque Java permettant de les manipuler, il est possible de la mettre en œuvre relativement aisément.
Brièvement, une expression régulière — ER dans ce qui suit — est une notation compacte pour décrire un ensemble de chaînes de caractères. Au moyen d'une ER, il est donc possible de décrire facilement toutes les variantes d'une sous-requête à rechercher.
Par exemple, la sous-requête Mez correspond à un total de 5 chaînes différentes (Mez, Méz, Mèz, Mêz et Mëz) car le e non accentué est équivalent à n'importe laquelle de ses variantes accentuées. Cet ensemble de 5 chaînes s'écrit facilement sous la forme d'une ER, ainsi : M[eéèêë]z
. En effet, dans une ER, un ensemble de caractères placé entre crochets représente n'importe lequel de ces caractères. (Les crochets ont été mis en rouge ici pour qu'il soit bien clair qu'ils ne font pas partie des chaînes décrites par l'ER, mais indiquent simplement un choix entre les caractères qu'ils entourent.)
Il découle de ce qui précède que la recherche d'une sous-requête peut se faire en :
- la transformant en une ER, puis
- recherchant cette ER dans les noms d'arrêts.
La transformation d'une sous-requête en une ER consiste simplement à remplacer chaque caractère de la première colonne de la table de la §2.2.1 par l'ensemble des caractères de la seconde colonne. Par exemple, la sous-requête Mezier est transformée en l'ER M[eéèêë]z[iíìîï][eéèêë]r
.
La recherche de l'ER dans les noms d'arrêts peut se faire facilement en Java, dont la bibliothèque offre des classes permettant de représenter des ER et de les rechercher dans des chaînes. Cette recherche peut de plus se faire sans tenir compte des différences entre majuscules et minuscules, ce qui permet de facilement mettre en œuvre la seconde équivalence décrite plus haut.
3. Mise en œuvre Java
3.1. Interface Json
L'interface Json
du paquetage principal, publique et scellée, représente un document JSON. Elle est implémentée par 4 enregistrements imbriqués dans elle, qui représentent les types de données JSON utiles à ce projet et qui sont :
- JArray
- qui représente un tableau JSON et possède comme unique attribut une liste de valeurs de type
Json
, qui sont les éléments du tableau, - JObject
- qui représente un objet JSON et possède comme unique attribut une table associative qui associe des valeurs de type
Json
à des chaînes Java (de typeString
), - JString
- qui représente une chaîne JSON et possède comme unique attribut une chaîne Java dont le contenu est celui de la chaîne JSON,
- JNumber
- qui représente un nombre JSON et possède comme unique attribut une valeur de type
double
dont la valeur est celle du nombre JSON.
Ces différents enregistrements ne possèdent rien d'autre qu'une redéfinition de la méthode toString
qui retourne la représentation JSON de l'objet auquel on l'applique. (On aurait pu vouloir introduire une méthode séparée et nommée p. ex. toJsonString
pour cela, mais dans un soucis de simplicité, nous avons décidé de réutiliser la méthode toString
.)
Toujours dans un soucis de simplicité, ces enregistrements ne font pas les (nombreuses) validations d'arguments qu'ils pourraient faire. Par exemple, JString
ne vérifie pas que la chaîne passée à son constructeur ne possède pas de caractères illégaux comme des caractères de contrôle, et elle fait l'hypothèse qu'elle ne contient pas non plus de guillemets.
3.2. Classe JourneyGeoJsonConverter
La classe JourneyGeoJsonConverter
du sous-paquetage journey
, publique et non instanciable, offre une méthode, nommée p. ex. toGeoJson
, permettant de convertir un voyage en un document GeoJSON représentant son tracé.
Le document GeoJson retourné par toGeoJson
doit être constitué d'une unique ligne brisée dont les points sont tous les arrêts du voyage — aussi bien ceux de départ et d'arrivée des étapes que les arrêts intermédiaires. Les longitudes et latitudes des différents points doivent être arrondis à 5 décimales, ce qui offre une précision de l'ordre du mètre, qui suffit largement à nos besoins. Finalement, deux points successifs de la ligne brisée doivent avoir des coordonnées différentes.
De plus, toGeoJson
doit produire le document le plus court possible, car il a pour but d'être transmis par Internet à un site qui se charge de l'afficher. Il ne doit donc contenir aucun « caractère blanc » : ni espace, ni tabulation, ni retour à la ligne.
3.3. Classe StopIndex
La classe StopIndex
du paquetage principal, publique et immuable, représente un index de nom d'arrêts dans lequel il est possible d'effectuer des recherches.
Le constructeur de StopIndex
prend en arguments :
- la liste des noms d'arrêts à indexer, de type
List<String>
, - une table associant les noms alternatifs des arrêts à leur nom principal, de type
Map<String, String>
.
En plus de ce constructeur, StopIndex
offre une unique méthode publique, nommée p. ex. stopsMatching
, qui prend en argument une requête et un nombre maximum de noms d'arrêts à retourner, et retourne la liste des noms d'arrêts correspondant à la requête, triés par pertinence décroissante et sans doublons. Bien entendu, la taille de cette liste ne dépasse pas la limite donnée en argument.
La méthode stopsMatching
doit gérer les noms alternatifs des arrêts exactement comme les noms principaux, si ce n'est que seuls les noms principaux apparaissent dans la liste retournée. Par exemple, si la requête Losanna
est passée en argument, la liste retournée contient Lausanne
en première position.
3.3.1. Conseils de programmation
La classe Pattern
de la bibliothèque Java représente une expression régulière. Sa méthode compile
permet de transformer une chaîne contenant une expression régulière en l'instance de Pattern
correspondante. Par exemple, l'appel suivant :
Pattern vowels = Pattern.compile("[aeiou]");
permet d'obtenir une expression régulière décrivant l'ensemble des chaînes constituées d'une seule voyelle.
Il existe également une variante de la méthode compile
qui prend en second argument des « fanions » (flags) permettant d'activer différentes options. Ces fanions sont passés sous la forme d'un entier de type int
dont les différents bits représentent chacun une option. Ainsi, pour obtenir une expression régulière pour laquelle la différence entre les majuscules et les minuscules est ignorée, on peut appeler la méthode ainsi :
int flags = Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE; Pattern noCaseVowels = Pattern.compile("[aeiou]", flags);
Une fois une expression régulière obtenue, on peut l'utiliser de différentes manières. Par exemple, les méthodes split
et splitWithDelimiters
permettent de découper une chaîne à chaque occurrence d'une expression régulière. Cela s'avère très utile pour découper la requête en sous-requêtes, et pour séparer chaque sous-requête en les parties à transformer en ensembles de caractères — les voyelles et la lettre c
— et les parties « constantes », à préserver telles quelles.
Lors de la construction de l'ER correspondant à une sous-requête il faut prendre garde à appeler la méthode quote
sur toutes les parties constantes avant de les ajouter à la chaîne à passer à compile
. Cela garantit que même si ces parties contiennent des caractères ayant une signification particulière dans les ER, comme les crochets, elles sont interprétées de manière littérale. Ainsi, l'ER pour la sous-requête mez doit être construite d'une manière équivalente à celle-ci :
Pattern mez = Pattern.compile(Pattern.quote("m") + "[eéèêë]" + Pattern.quote("z"));
Pour rechercher une expression régulière dans une chaîne, il faut tout d'abord obtenir un matcher correspondant, au moyen de la méthode matcher
. Cela fait, on peut appeler la méthode find
de cet objet pour lui faire rechercher la prochaine occurrence de l'expression régulière dans la chaîne. S'il y en a une, les méthodes start
et end
permettent d'obtenir ses positions de début et de fin dans la chaîne.
Notez pour terminer que la programmation par flots permet d'écrire le code de StopIndex
de manière particulièrement concise et élégante, et il vaut donc la peine d'essayer de l'utiliser.
4. Résumé
Pour cette étape, vous devez :
- écrire les classes et interfaces
Json
,Json.JArray
,Json.JObject
,Json.JString
,Json.JNumber
,JourneyGeoJsonConverter
etStopIndex
selon les indications plus haut, - tester votre code,
- documenter la totalité des entités publiques que vous avez définies.
Même s'il n'est pas obligatoire de rendre cette étape avant le rendu final (30 mai à 18h00), nous vous conseillons néanmoins fortement de le faire dès que vous l'aurez terminée. Nous conserverons ainsi une copie de sauvegarde de votre travail, que nous pourrons vous fournir en cas de problème.