Using the Strava API, we can retrieve all kinds of data of a given activity, including geographical information. We can use this data to render the route associated with the activity using a mapping platform like Mapbox. The following post builds up a simple example, displaying a bike ride in and around San Francisco on a map.

First, let’s load the activity data

Once we have obtained the activity data from the API in JSON format (stored locally as mt_tam.json), our main focus is on the map.polyline, which contains latitude and longitude information about the route path.

There is one problem, though. This property’s value seems to contain a string of random characters and symbols, not GPS data. The reason for this is that this info is encoded using Google’s Encoded Polyline Algorithm. To transform this string into coordinates, we will have to run them through a decoding function. Luckily, we find what we need right inside Mapbox’s GitHub repo.

For simplicity’s sake, we will be using jQuery to retrieve the activity data.

$.getJSON('mt_tam.json', function(activity) {
  // the rest of our code will go here... 
});

Sources and Layers

In order to display additional information on a Mapbox map, the framework supports the concept of Layers, which describes how said additional information should be displayed, i.e. coloring, the thickness of lines, etc. Layers can be connected to Sources, which contain the actual geographical data that should be added to the map. In our case, we will be feeding the activity data from the route into Mapbox in GeoJSON format. We will start with the basic structure of a GeoJSON object.

let geoJSON = {
  "type": "Feature",
  "geometry": {
    "type": "LineString",
    "coordinates": []
  }
};

We will populate coordinates with the GPS data stored in our activity. In order to do this, we first need to decode the polyline data, stored in activity.map.polyline.

coordinates = polyline.decode(activity.map.polyline);

Data Massaging

Unfortunately, polyline.decode returns each coordinate in [ Lat, Long ] format. Mapbox, however, needs the data in reverse, so [ Long, Lat ]. A simple loop is required to order the data correctly and then add it to our GeoJSON object.

for (let i = 0; i < coordinates.length; i++) {
  coordinates[i] = [
    coordinates[i][1],
    coordinates[i][0]
  ];
}

geoJSON.geometry.coordinates = coordinates;

Add Activity Layer

We can now create a new map instance and, upon loading, add the new layer to the map.

map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/outdoors-v12'
});

map.on('load', function () {
  map.addSource('route', { type: 'geojson', data: geoJSON });
  map.addLayer({
    "id": "route",
    "type": "line",
    "source": "route",
    "paint": {
      "line-color": "#F7455D",
      "line-opacity": 0.75,
      "line-width": 5
    }
  });
});

Tweaking

When loading the code we have so far, we will immediately notice one issue. Since we did not provide a center and zoom property when instantiating the map, Mapbox will automatically center the map on Null Island at zoom level 0. What we want instead is to adjust center and zoom level of the map to the bounds of our polyline. Thankfully one of the API examples provides a solution to this.

// Pass the first coordinates in the LineString to `lngLatBounds` &
// wrap each coordinate pair in `extend` to include them in the bounds
// result.
bounds = coordinates.reduce(function(bounds, coord) {
  return bounds.extend(coord);
}, new mapboxgl.LngLatBounds(coordinates[0], coordinates[0]));

We can then use the fitBounds function to calculate zoom level and center based on bounds.

map.fitBounds(bounds, {
  // add some spacing around the coordinates
  padding: 80, 

  // disable the animation to zoom to the bounding box
  animate: false 
});

With those changes, we now have a rendering of our activity route on a Mapbox map.