Journal Openstreetmap et GPS garmin

Posté par  . Licence CC By‑SA.
Étiquettes :
49
13
juin
2022

Sommaire

Hello

En passant, je vous présente un petit projet sur lequel je bosse quand j'ai un peu de temps. Bonne lecture.

Contexte

En 2013, une moule avait posté un chouette post sur l'utilisation d'un montre garmin sous linux : garmin-forerunner-110.

Comme je suis un peu un mouton qui adore réinventer la roue, je me suis dis que j'allais faire pareil, avec une montre similaire : la garmin forerunner 10.

Pour faire court, quand on va courir, la montre enregistre un fichier .FIT récupérable simplement par connexion USB (le système de fichier monté est du FAT32).

Objectif

Avec ce projet, je voulais un truc simple: une liste de sorties sur lesquelles je peux cliquer, et l'affichage de la course sélectionnée sur un fond de carte.

Comme souvent, sur ce genre de projet, je tente d'apprendre des trucs, comme ça fait longtemps que je commencé, j'ai eu le temps de tester plein de trucs :

  • awk
  • bash
  • python
  • automake
  • gtk
  • openstreetmap

Mais j'y reviendrai, peut être.

Après avoir cherché et testé différents concepts, je me suis arrété sur le suivant, à savoir utiliser leaflet.js qui permet d'afficher simplement des données json sur une carte openstreetmap

Pour cela, il faut

  • convertir certaines informations des enregistrements en json (yaka)
  • afficher le tout sur le fond de carte (fokon)

Fokon : utiliser OSM à la maison

Pour gribouiller sur des cartes, on utilise donc leaflet.js.

C'est pas trop compliqué, tout tiens dans un fichier html (dont beaucoup de javascript) :

  • On crée un div qui contiendra la carte, on l'appelle 'map' (on force sa taille à 100%)
  • On crée une carte L.map qu'on place dans le div précédent
  • On crée un chemin L.geoJSON qu'on place sur la carte. Le format geojson est documenté là : [geojson.org].

Un petit truc qui m'a géné : les paramètres de latitude et longitude semblent inversé entre L.map et L.geoJSON, dans le premier, on donne latitude, longitude, dans le second, longitude, latitude…

Voici donc un petit fichier HTML qui fait ça, on notera que j'ai mis en local les fichier leaflet.[js,css] pour simplifier les tests, mais on peut les remplacer par un cdn.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
  <script src="./leaflet.js"></script>
  <link rel="stylesheet" href="./leaflet.css" />
<style> 
html, body { height: 100%; padding: 0; margin: 0; }
#map { width: 100%; height: 100%; }
</style>
</head>
<body onload="init()">
<div id="map"></div>
<script type="text/javascript">

// fonction appelée au démarrage
function init() {
    // deux variables, pour contenir la carte et le chemin qu'on trace
    var map;
    var path;

    // la petite ligne de copyright
    var szAttr = '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors<\/a>';

    // On choisi les tuiles osm par défaut
    var layer_osm = L.tileLayer(
        'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: szAttr
    });

    // Création de la carte avec les attributs qui vont bien:
    // * dans le div "map"
    // * ou on regarde
    // * zoom
    // * tuiles à utiliser
    map = L.map('map', {
        center: [-49.35, 70.3],
        zoom: 13,
        layers: [layer_osm]
    });

    // Création du chemin proprement dit
    path = L.geoJSON([{
                "type": "LineString", 
                "coordinates": [[70.265, -49.354], [70.25,-49.354], [70.25,-49.35], [70.265, -49.35], [70.265, -49.354]]
    }]);

    // Et on affiche le chemin sur la carte
    path.addTo(map);

    console.log('done');
}

</script>
</body>
</html>

Si le fichier est ouvert sur un navigateur, il devrait faire un petit rectangle autour d'une station sur une petite ile australe…

Tadaa

Taadaa !

On sait maintenant à quoi doit ressembler notre yaka : quelque chose qui transforme les .FIT en geojson.

Yaka : de fit à geojson.

Ici, on a deux solutions, on peut utiliser fitdump et décoder le contenu avec awk (comme dans le premier journal). L'inconvénient de cette solution et qu'il faut l'adapter à chaque montre : le code des messages change en fonction des paramètres que peut stocquer la montre. La modification n'est pas énorme, c'est simplement le code du Local message type qui est à revoir, mais bon…

On on peut s'amuser à faire une bibliotèque pour travailler avec les fichiers .FIT et un programme que l'utilise pour générer du json.

Le format .FIT est bien décrit par garmin, leur SDK propose pas mal de choses là dessus.
Un .FIT, c'est une succession d'enregistrements. On en a de deux types:

  • Les définitions,
  • Les données.

Les définitions décrivent comment sont organisées les données, elles contiennent :

  • le type de données (parmis un choix donné)
  • les champs contenus dans l'enregistrement.

Ok, comme ça c'est pas clair… Disons que dans un fichier, on peut avoir un message disant :

"À partir de maintenant, les données de type FIT_GLOBAL_RECORD contiennent un champ latitude et un champ longitude"

Les données sont variables, on sait ce qu'elle contiennent grace aux définitions.

libfit: lire les .FIT

Implémenter une specification, c'est pas très marrant à décrire, je vous passe donc les détails, mais on peut tout trouver ici: libfit

C'est une petite bibliothèque qui propose quelques fonctions qui font ce qu'on veut:

  • FIT_open: ouvre le fichier .FIT (en vérifiant au passage sa cohérence)
  • FIT_readRecord: lit le prochain enregistrement disponible
  • FIT_decodeValues: décode les valeurs contenues dans un enregistrement.

Et générer du geojson

Couplée à libjson-c, on a tout ce qu'il faut pour créer un convertisseur qui tient en 64 lignes:

#include <json-c/json.h>
#include <stdio.h>
#include "libfit.h"
#include "libfit-messages.h"

#define die(...) do { fprintf(stderr, __VA_ARGS__); exit(1); } while(0)

int main(int argc, char *argv[]) {
    if (argc != 3) {
        die("usage %s <fit file to read> <json file to create>", *argv);
    }

    /* open the fit file */
    fit_file_t *f;
    fit_record_t *rec = NULL;
    f = FIT_open(argv[1]);
    if (NULL == f)
        die("cannot dump %s\n", argv[1]);

    /* prepare the json file */
    struct json_object *obj, *path, *coordinates;
    obj = json_object_new_object();
    path = json_object_new_object();
    json_object_object_add(path, "type", json_object_new_string("LineString"));
    coordinates = json_object_new_array();

    /* convert */
    for (;;) {
        double lat, lon;
        /* read next record */
        fit_record_t *rc = FIT_readRecord(f, rec);
        if (!rc) {
            FIT_free(rec);
            break;
        }
        rec = rc;
        /* only kee global record */
        if (FIT_GLOBAL_RECORD != FIT_getRecordType(rec))
            continue;

        /* decode position and speed */
        if (0 == FIT_decodeValues(rec, FIT_GLOBAL_RECORD,
                FIT_RECORD_POSITION_LONG_S32, &lon,
                FIT_RECORD_POSITION_LAT_S32, &lat,
                -1)) {

            struct json_object * xy = json_object_new_array();
            json_object_array_add(xy, json_object_new_double(lon));
            json_object_array_add(xy, json_object_new_double(lat));
            json_object_array_add(coordinates, xy);
        }
    }

    FIT_close(f);

    /* finalize */
    json_object_object_add(path, "coordinates", coordinates);
    json_object_object_add(obj, "path", path);

    /* save json file */
    json_object_to_file(argv[2], obj);
}

Un peu de mise en forme

On se retrouve avec un fichier json de 11 ko (pour un fit de 8), pas génial de mettre ça directement dans le fichier index.html. Il est assez facile de créer une fonction qui récupère le contenu d'un json via fetch, mais le problème de cette méthode est qu'elle impose la mise en place d'un serveur http, car firefox interdit les COR, comme j'ai envie de rester simple pour cette note, on va s'en passer. (Notez que c'est facile de mettre en place un serveur qui fait le job: python -m http.server suffit)

On commence par transformer notre json en js. Sachant que le fichier json tient sur une seule ligne, on peut se contenter d'inserer un peu de code en tête:

sed 's/^/var data = /' test.json > test.js

Voilà, on peut alors charger notre fichier via une balise <script>

  <script src="./test.js"></script>

Au lieu de donner des coordonnées en dur, cette nouvelle variable est utilisée pour centrer le plan et dessiner le chemin :

    map = L.map('map', {
        center: [data.path.coordinates[0][1], data.path.coordinates[0][0]],
        zoom: 13,
        layers: [layer_osm]
    });

    path = L.geoJSON(data.path),

Et voilà

Taadaa !

Les sources

Toutes les sources ici : libfit

Et après ?

Dans un autre repo, j'ai codé un truc plus complet, dans lesquels on retrouve :

  • Le calcul de la hauteur à partir de la position GPS (merci elevation)
  • L'affichage de la vitesse à l'aide de Chart.js
  • Afficher plusieurs trace en même temps, pour faire comme une heatmap…

P'tet qu'un jour, je documenterai ça

  • # KerguelenKerguelen !!!

    Posté par  . Évalué à 9.

    Un journal qui commence par une carto centrée sur port aux français est forcement un bon journal :)

    Ker58 for ever

  • # Problème de lien

    Posté par  (Mastodon) . Évalué à 2.

    Toutes les sources ici : https://framagit.org/mabu/libfit

    Il y a un problème dans la redirection du lien

  • # BasicFitConvert & GPX2Video

    Posté par  (site web personnel) . Évalué à 1.

    Je viens de lire l'article, et de découvrir l'outil BasicFitConvert qui permet de générer une image depuis une trace GPS.

    On peut faire l'équivalent avec l'outil GPX2Video :

    $ ./gpx2video -g ACTIVITY.gpx -o map.png --map-source=1 --map-zoom=11 --map-factor 2.0 track

    A partir du fichier GPX et d'un type de carte (map-source), gpx2video construit la carte, puis affiche la trace GPS.

    Prochainement, on pourra personnaliser la couleur de la trace.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.