Date de première publication : 2024/03/05
On vous propose d'étudier et d'afficher la régularité des trains express régionaux (TER) en France avec les données ouvertes de la SNCF
Les données sont disponibles sur le site de la sncf dédié. On peut récupérer les données avec une API ou en téléchargeant des fichiers aux formats CSV et JSON. Voici le contenu des données :
- Date (année-mois)
- Région
- Nombre de trains programmés
- Nombre de trains ayant circulé
- Nombre de trains annulés
- Nombre de trains en retard à l'arrivée
- Taux de régularité
- Nombre de trains à l'heure pour un train en retard à l'arrivée
- Commentaires. Ce champ est un peu pénible. S'il prend plus d'une ligne, il est entouré de ""
Les données proviendront d'une base de type h2.
Mise en place du TP
Structure générale
Voici un petit lien pour l' initializer préconfiguré
Pour les dépendances, nous développons un site web avec le moteur de template Thymeleaf et une base de données (JPA) h2.>
Il ne faudra pas oublier de préciser un nom pour la base de données même si on ne l'utilise pas tout de suite dans le fichier application.propoerties
, ainsi qu'un numéro de port si nécessaire
Nous n'utiliserons pas de tests unitaires (c'est une erreur), vous pouvez commenter ou effacer les lignes correspondantes dans le fichier build.gradle
Un contrôleur
La page à afficher (index.html
) est pour l'instant statique mais nous allons la placer dans le répertoires templates
donc nous avons besoin d'un contrôleur même basique
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
// le nom de la méthode n'est pas important, mais les annotations si !
@GetMapping("/")
public String index(Model model) {
// model.addAttribute("infos", ma_magnifique_liste);
return "index";
}
}
Une page web
C'est la page index.html
qui va afficher les données. Placez-la dans
le répertoire templates
même si pour l'instant tout est "statique" (la page n'est pas encore générée par le backend)
Pour l'affichage des données, on va utiliser une bibliothèque javascript connue : chart.js. L'exemple ci-dessous est inspiré de la documentation :
<div>
<canvas id="myChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx = document.getElementById('myChart');
var abscisses = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin'];
var ordonnees = [3, 1, 2, 2, 4, 1];
new Chart(ctx, {
type: 'line',
data: {
labels: abscisses,
datasets: [{
label: 'kilos de bonbons mangés par le loic',
data: ordonnees,
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
</script>
La page devrait s'afficher avec des données "statiques". Celles-ci sont placées dans deux tableaux
javascript : abscisses
et ordonnees
.
Pour que ce soit plus propre, il faut ajouter l'entête de fichier (doctype) ainsi que les balises manquantes et l'espace de nommage thymeleaf.
Vif du sujet
On va maintenant injecter des données dans la page.
Récupération des données
On peut facilement lire des fichiers CSV en Java mais ici, c'est la notion de CSV par la SNCF qui pose problème notamment à cause du champ commentaire. On va donc s'intéresser à la lecture d'un fichier contenant des données au format JSON (à placer à la racine du répertoire de travail).
La lecture d'un fichier JSON est assez simple et le mécanisme à mettre en oeuvre est similaire au mapping base de données / modèle objet. Le fichier contient un tableau d'informations JSON (vues comme des objets javascript ou un tableau associatif, c'est la même chose en JS).
Chaque objet JSON est converti en objet JAVA avec la classe ObjectMapper
. Dans le code ci-dessous, chaque objet JSON
est placé dans un objet de classe Info
que nous allons créer juste après.
// import com.fasterxml.jackson.databind.ObjectMapper;
// import java.io.File;
ObjectMapper mapper = new ObjectMapper();
// lecture d'un élement - POUR INFORMATION
Info obj = mapper.readValue(new File("regularite-mensuelle-ter.json"), Info.class);
// lecture d'un tableau - A UTILISER
List<Info> objs = Arrays.asList(mapper.readValue(new File("regularite-mensuelle-ter.json"), Info[].class));
System.out.println("Nb lu : "+ objs.size()); // ou un log.info
Le code diffère en fonction de ce que l'on veut lire : un élément ou un tableau/liste d'éléments. Pour la traduction, l'introspection est utilisée (d'où le point .class).
Proposition (indécente) : objs
est un attribut du contrôleur et est initialisé dans le constructeur du contrôleur
La lecture du fichier nécessite de gérer les exceptions : un simple try/catch fera l'affaire. Affichez les erreurs potentielles
dans un log ou en utilisant la méthode printStackTrace()
d'Exception
try {
} catch (Exception e) {
// Décommentez ce que vous voulez
// e.printStackTrace();
// log.info(e.getMessage());
}
La classe Info
Il est toutefois nécessaire de créer une classe Info
qui va "mapper" les clés/valeurs d'un objet JSON en un objet JAVA.
La classe Info
est assez fastidieuse à écrire mais vous pouvez vous contenter d'écrire les champs avec leur nom et leur type et de générer le code pour le
constructeur, les getters et les setters et les éventuelles méthodes equals()/hasCode()
avec votre éditeur (même VS Code/Codium)
ou avec le projet LOMBOK qui est dans les dépendances du projet.
import lombok.*;
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
class Classique {
@Getter
@Setter
Double monAttribut;
}
Le nom de la clé à lire doit correspondre au nom de l'attribut sinon il faut paramétrer cela avec une annotation.
// import com.fasterxml.jackson.annotation.JsonProperty;
class Info {
// Annotation supplémentaire pas nécessaire
String date;
@JsonProperty("nombre_de_trains_programmes")
Integer plannedTrains;
// les autres attributs de type Double ou Integer
// les getters / setters / constructeurs / ...
// éventuellement générés avec lombok
}
Il peut être de bon ton que la classe Info
implémente l'interface Serializable
Filtrage des données
Pour l'instant, la liste objs
contient toutes les données. On peut les trier ou les filtrer en fonction de nos besoins. Cela permettra au programme d'être plus efficace en limitant la transmission de données.
Nous allons faire cela avec les Streams vus en ZZ2 :
List<Info> filteredObjects = objs
.stream()
.filter(o -> o.getRegion().startsWith("Auvergne") && o.getDate().startsWith("2023"))
.collect(Collectors.toList());
Dans le bout de code présenté, le filtre se fait en utilisant une lambda et seuls les objets dont la région commence par "Auvergne" et dont la date est "2023" sont retenus.
Si nous voulions trier les données, on pourrait utiliser l'action sorted()
sur des objets Comparable<O>
(à implémenter)
ou bien en donnant un comparateur (par exemple, une lambda)
Affichage des données
La suite est un peu barbare. Les données vont venir du tableau/liste d'Info
. Il faut donc donner cette liste au modèle dans la méthode idoine du contrôleur.
Pour générer du javscript avec Thymeleaf, il faut ajouter l'attribut suivant à la balise script
(celle sans l'attribut src
):
<script th:inline="javascript">
On va utiliser une syntaxe BARBARE (pour SpringBoot 3 uniqument) pour définir les abscisses, en créant un tableau vide, puis en insérant les données avec une boucle Thymeleaf :
var abscisses = [];
/*[# th:each="n : ${infos}"]*/
abscisses.push("[(${n.getDate()})]");
/*[/]*/
Les commentaires JS sont importants car ils sont évalués par le moteur de template.
Si la date n'est pas une chaine de caractères, on peut la modifier comme suit :
[${#dates.format(n.date, 'yyyy-MM')}]]
Faites la même chose pour afficher le nombre de trains programmés et le tableau ordonnees
. Vous pouvez ensuite
afficher le nombre de trains annulés par exemple. Pour ce faire, il faut ajouter un champ à datasets
.
datasets: [{
label: 'premier jeu',
data: ordonnnes1,
borderWidth: 1
},{
label: 'deuxieme jeu',
data: ordonnnes2,
borderWidth: 1
}
]
Si les données ne sont pas triées sur la date, les abscisses peuvent ne pas êtres ordonnées ;-)
Utilisation d'une base de données
Ces données doivent être placées en base de données. On propose de construire deux tables : une des régions et une des données.
La création et la mise à jour de la base se font dans l'application principale avec une option à l'exécution (comme "install" par exemple).
Le contrôleur et l'application
possèdent maintenant des attributs sur les Repository
.
@Autowired
InfoRepository infoRepository;
@Autowired
RegionRepository regionRepository;
Il faut essayer de choisir les bons types de données (par exemple le champ de type Date
Il y a plusieurs manières de convertir une chaine en date. En voici deux :
- Configurer
ObjectMapper
- Utiliser une annotation
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM");
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(df);
// import com.fasterxml.jackson.annotation.JsonFormat;
class Info {
@JsonFormat
(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM")
Date date;
}
Et pour finir, on pourrait dire que sans la base de données, tout ce que l'on a fait ne sert à rien car on aurait pu directement écrire la page web qui demande les données à l'API SNCF.
Aller plus loin
On peut maintenant améliorer ce que l'on a fait :
- Changer de région ou d'année
- On peut maintenant choisir d'afficher des données consolidées : par exemple, le nombre de trains sur la France, ...