Fetching and storing activities from Garmin Connect with Strapi and visualizing them with NextJS

Fetching and storing activities from Garmin Connect with Strapi and visualizing them with NextJS

With getting into the IndieWeb i started to reflect about myself and how i can actually own my data instead of giving them to so called silos.

Due to the fact I am a passionate (mountain) bike rider i was thinking about how i could use tracking/activity apps in a way to get my data back, because obviously when i am going for a ride with my bike and i am tracking the route with strava and/or komoot the data is then saved by them. Considering that every route is tracked on a Garmin device anyway and then synchronised to the apps i decided to have a look at the Garmin Connect/Activity API.

Unfortunately the official Garmin Activity API is only available for approved business developer.

But after some searching i found the npm-package garmin-connect which allows you to connect to Garmin Connect for sending and receiving activity data.

Fetching and storing activities from Garmin Connect with Strapi

You can install the package with

js
Copy code
npm install garmin-connect

or

js
Copy code
yarn add garmin-connect

and use it like

js
Copy code
const { GarminConnect } = require('garmin-connect');
// Create a new Garmin Connect Client
const GCClient = new GarminConnect();
// Uses credentials from garmin.config.json or uses supplied params
await GCClient.login('my.email@example.com', 'MySecretPassword');
const userInfo = await GCClient.getActivities());

I stored the email and the password for the login in environment variables and used them with

js
Copy code
const { GarminConnect } = require('garmin-connect');
const GCClient = new GarminConnect();
await GCClient.login(process.env.GARMIN_EMAIL ,process.env.GARMIN_PWD);
const userInfo = await GCClient.getActivities());

Afterwards experimented a bit with the Garmin Connect and found out there are some very low limits. Approximately after ~50 requests in one minute couldn't get anymore any data and had to wait for some (maybe one?; i am not sure) hours until the request was successful again.

In general you can probably do way more with Garmin Connect as you will need, like for example:

  • Get user info
  • Get social user info
  • Get heart rate
  • Set body weight
  • Get list of workouts
  • Add new workouts
  • Add workouts to you calendar
  • Remove previously added workouts
  • Get list of activities
  • Get details about one specific activity
  • Get the step count

I used only GCClient.getActivities(); to get all activities and and GCClient.getActivity({ activityId: id }); to get the details of the activity (like spatial data representing the route, start-point and end-point).

To be able to store the data in Strapi i created a new content type collection activities with the following fields/attributes:

Sorry, somehow the image is not available :(

Afterwards new entrys for activities can be created.

Strapi has a documention which explains how to fetch external data and create entries with it: Fetching external data.

To get the data from Garmin Connect into Strapi i created a function getGarminConnectActivities.js for Strapi (https://gist.github.com/dietrichmax/306b36abd5a9d1ac0c938adcd15f2f69)

The function will take care of:

  • getting existing activities in Strapi,
  • get recent activites from Garmin Connect,
  • checking if an activity already exists in Strapi,
  • creating the activity in Strapi if it doesn't exist yet and also get the details for it.

and basically looks like this:

js
Copy code
module.exports = async () => {
  await GCClient.login(process.env.GARMIN_USERNAME, process.env.GARMIN_PWD)
  const activities = await GCClient.getActivities()
  const exisitingActivities = await getExistingActivities()
  activities ? activities.map((activity) => {
    const isExisting = exisitingActivities.includes(activity.activityId)
    isExisting ? console.log(activity.activityId + " already exists") : createEntry(activity)
  })
  : console.log("no activities found")
}

After all activities from Garmin are fetched, i am mapping through them to

  • check if they already exist in my cms
  • and create them if needed

The exisiting activities in my CMS are fetched with

js
Copy code
const getExistingActivities = async () => {
  const existingActivityIds = []
  const activities = await axios.get(`https://strapi.url/activities`)
  
  activities.data.map((activity) => {
    existingActivityIds.push(activity.activityID)
  })
  return existingActivityIds
}

and the activityIds (originally from Garmin Connect) are returned to be able to check if an entry already exists. If the entry doesn't exist, details for the missing activity are fetched and a new entry is created with:

js
Copy code
const createEntry = async (activity) => {
  const details = await GCClient.getActivity({ activityId: activity.activityId });
  await strapi.query('activity').create({
    activityID: activity.activityId,
    activityName: activity.activityName,
    beginTimestamp: activity.beginTimestamp,
    activityType: activity.activityType,
    distance: activity.distance,
    duration: activity.duration,
    elapsedDuration:  activity.elapsedDuration,
    movingDuration: activity.movingDuration,
    elevationGain: activity.elevationGain,
    elevationLoss: activity.elevationLoss,
    minElevation: activity.minElevation,
    maxElevation: activity.minElevation,
    sportTypeId: activity.sportTypeId,
    averageSpeed: activity.averageSpeed * 3.6 //(m/s -> km/h),
    maxSpeed: activity.maxSpeed * 3.6 //(m/s -> km/h),,
    startLatitude: activity.startLatitude,
    startLongitude: activity.startLongitude,
    endLatitude: activity.endLatitude,
    endLongitude: activity.endLongitude,
    details: details
  })
}

You can save way more but i tried to cut it down to the ones i really need or eventually will need.

Only thing missing is some automatic triggering. For this you can use cron jobs in Strapi (/config/functions/cron.js).

js
Copy code
module.exports = {
// Add your own logic here (e.g. send a queue of email, create a database backup, etc.).

    '0 0 18 * * *': () => {
      strapi.config.functions.getGarminConnectActivities();
    },
};

I decied to trigger the function everyday at 6 pm so i can have a look at my activites in the evening. 😎

Thats the fun from the 'backend part'.

Next step is to visualize the data in NextJS.

Visualizing the data with NextJS

I am really liking the embeddable tours of komoot optic-wise, so i decided to create a similiar looking option for the preview of my activities in the posts-feed and activites-feed.

So the preview should consist of

  • an title of the activitie (+datetime),
  • an symbol displaying the activity type (cycling, running, etc.)
  • some basic metrics like distance, duration and average speed,
  • a map displaying the line of the route with osm-data and and aerial-view,
  • and optionally a graph displaying the elevation along the track (still in work).

The component looks like this at the moment:

Sorry, somehow the image is not available :(

Showing some Metrics

In the activityType object you can find typeId which correlates to the type of the activity, e.g. cycling, running etc. I created a small function which will return a icon from react-icons visualizing the activity type.

js
Copy code
import { FaRunning, FaBiking } from 'react-icons/fa';

const getTypeIcon = activity => {
    if (activity.activityType.typeId == 5) {
        return <FaBiking/>
    } else if (activity.activityType.typeId == 15) {
      return <FaRunning/>
 }

getTypeIcon(activity)

Due to the fact the duration is given in seconds and i wanted it to display like 1h 10m 12s there is also a need for a workaround which looks like the following:

js
Copy code
const secondsToHms = (s) => {
    const hours = (((s - s % 3600) / 3600) % 60)
    const minutes = (((s - s % 60) / 60) % 60)  
    const seconds = (s % 60)  
    return (`${hours}h ${minutes}min ${seconds}s`)
 }
secondsToHms(activity.duration)

Displaying a Map with "react-leaflet"

Then i created a small map with react-leaflet displaying

  • the track as polyline,
  • the start point and
  • the end point.

Therefore i created a new map-component:

js
Copy code
import React, { useEffect, useState } from "react"
import { Marker, MapContainer, TileLayer, LayersControl, Polyline } from "react-leaflet";

const Map = (data) => {
  const geo = data.data
  const style= { 
    color: '#11a9ed',
    weight: "5"
  }

  const bounds = [[geo.maxLat, geo.maxLon], [geo.minLat, geo.minLon]]
  return (
    <MapContainer
      style={{ height: "500px", width: "100%" }}
      bounds={bounds}       
      scrollWheelZoom={false}
    >
    <LayersControl position="topright">
      <LayersControl.BaseLayer checked name="OpenStreetMap.Mapnik">
        <TileLayer 
          url='https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png'
          attribution ='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'

        />
        </LayersControl.BaseLayer>
        <LayersControl.BaseLayer name="Esri World Imagery">
          <TileLayer
            attribution='Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP'
            url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
          />
        </LayersControl.BaseLayer>

        <Marker id="start" position={geo.startPoint}/>
        <Polyline pathOptions={style} positions={geo.polyline} />
        <Marker id="end" position={geo.endPoint}/>

      </LayersControl>
    </MapContainer>
  );
};

export default Map;

Luckily the data from the Garmin Connect has already exactly the structure we need to create the map, which are the coordinates for the polyline and the two points.

The coordinates can be found in geoPolylineDTO in the activitydetails

Sorry, somehow the image is not available :(

With maxLat, maxLon, minLat and minLon i created the bounds which will set the default view for the map when passed to the MapContainer.

js
Copy code
const bounds = [[geo.maxLat, geo.maxLon], [geo.minLat, geo.minLon]]

<MapContainer
      style={{ height: "200px", width: "100%" }}
      bounds={bounds}       
      scrollWheelZoom={false}
 >
.
.

Then i added the LayerControl to be able to toggle between the two Tilelayers

  • OSM (Carto) and
  • Aerial Image (Esri)

After that i just created two markers and a polyline with the exisiting objects startPoint, endPoint and polyline.

js
Copy code
.
.
        <Marker id="start" position={geo.startPoint}/>
        <Polyline pathOptions={style} positions={geo.polyline} />
        <Marker id="end" position={geo.endPoint}/>
.
.

You can find several other tilelayers in leaflet-providers-preview.

For an example of the preview head over to /activities. You can find the code for the activity-preview in my github-repositiory.

For the actual post of the activity i made the map a bit larger and added some more metrics for now.

(osm) Sorry, somehow the image is not available :(

(Aerial view) Sorry, somehow the image is not available :(

First published March 21, 2021

0 Webmentions

Have you published a response to this? Send me a webmention by letting me know the URL.

Found no Webmentions yet. Be the first!

Write a comment

About The Author

Max
Max

Geospatial Developer

Hi, I'm Max (he/him). I am a geospatial developer, author and cyclist from Rosenheim, Germany. Support me

0 Virtual Thanks Sent.

Continue Reading

  1. A Guide to Location Tracking and Visualization with OwnTracks, Node.js, PostgreSQL, GeoServer, MapProxy, Nginx and OpenLayers

    Inspired by Aaron Parecki and who he has been tracking his location since 2008 with an iPhone app and a server side tracking API i decided to go for a similar approach. I wanted to track my position constantly with my Android smartphone and use the data to display a map with all locations i have ever been to.

    Continue reading...

  2. How to create a custom cookie banner for your React application

    Recently I implemented a custom cookie banner solution on my Next.js site which you probably have seen a few seconds before. There are a lot of prebuilt cookie banners you can use for React or Next js sites but i wanted to create a custom cookie banner which also has some personal touch and keeps the design with the website in line.

    Continue reading...

  3. Dockerizing a Next.js Application with GitHub Actions

    In this article, we'll explore how to Dockerize a Next.js application and automate its deployment using GitHub Actions, thereby simplifying the deployment workflow and enhancing development productivity.

    Continue reading...