tete du loic

 Loïc YON [KIUX]

  • Enseignant-chercheur
  • Référent Formation Continue
  • Responsable des contrats pros ingénieur
  • Référent entrepreneuriat
  • Responsable de la filière F2 ingénieur
  • Secouriste Sauveteur du Travail
mail
loic.yon@isima.fr
phone
(+33 / 0) 4 73 40 50 42
location_on
ISIMA
  • twitter
  • linkedin
  • viadeo

eco Régularité train

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 :

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 :


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 :