Leaflet: Mapping Strava runs/polylines on Open Street Map
I’m a big Strava user and spent a bit of time last weekend playing around with their API to work out how to map all my runs.
Strava API and polylines
This is a two step process:
- Call the /athlete/activities/ endpoint to get a list of all my activities
- For each of those activities call /activities/[activityId] endpoint to get more detailed information for each activity
That second API returns a ‘polyline’ property which the documentation describes as follows:
Activity and segment API requests may include summary polylines of their respective routes. The values are string encodings of the latitude and longitude points using the Google encoded polyline algorithm format.
If we navigate to that page we get the following explanation:
Polyline encoding is a lossy compression algorithm that allows you to store a series of coordinates as a single string.
I tried out a couple of my polylines using the interactive polyline encoder utility which worked well once I realised that I needed to escape backslashes (“\”) in the polyline before pasting it into the tool.
Now that I’d figured out how to map one run it was time to automate the process.
Leaflet and OpenStreetMap
I’ve previously had a good experience using Leaflet so I was keen to use that and luckily came across a Stack Overflow answer showing how to do what I wanted.
I created a HTML file and manually pasted in a couple of my runs (not forgetting to escape those backslashes!) to check that they worked:
blog.html
<html> <head> <title>Mapping my runs</title> </head> <body> <script src="http://cdn.leafletjs.com/leaflet-0.7/leaflet.js"></script> <script type="text/javascript" src="https://rawgit.com/jieter/Leaflet.encoded/master/Polyline.encoded.js"></script> <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7/leaflet.css" /> <div id="map" style="width: 100%; height: 100%"></div> <script> var map = L.map('map').setView([55.609818, 13.003286], 13); L.tileLayer( 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, }).addTo(map); var encodedRoutes = [ "{zkrIm`inANPD?BDXGPKLATHNRBRFtAR~AFjAHl@D|ALtATj@HHJBL?`@EZ?NQ\\Y^MZURGJKR]RMXYh@QdAWf@[~@aAFGb@?j@YJKBU@m@FKZ[NSPKTCRJD?`@Wf@Wb@g@HCp@Qh@]z@SRMRE^EHJZnDHbBGPHb@NfBTxBN|DVbCBdA^lBFl@Lz@HbBDl@Lr@Bb@ApCAp@Ez@g@bEMl@g@`B_AvAq@l@ QF]Rs@Nq@CmAVKCK?_@Nw@h@UJIHOZa@xA]~@UfASn@U`@_@~@[d@Sn@s@rAs@dAGN?NVhAB\\Ox@@b@S|A?Tl@jBZpAt@vBJhATfGJn@b@fARp@H^Hx@ARGNSTIFWHe@AGBOTAP@^\\zBMpACjEWlEIrCKl@i@nAk@}@}@yBOWSg@kAgBUk@Mu@[mC?QLIEUAuAS_E?uCKyCA{BH{DDgF`AaEr@uAb@oA~@{AE}AKw@ g@qAU[_@w@[gAYm@]qAEa@FOXg@JGJ@j@o@bAy@NW?Qe@oCCc@SaBEOIIEQGaAe@kC_@{De@cE?KD[H[P]NcAJ_@DGd@Gh@UHI@Ua@}Bg@yBa@uDSo@i@UIICQUkCi@sCKe@]aAa@oBG{@G[CMOIKMQe@IIM@KB]Tg@Nw@^QL]NMPMn@@\\Lb@P~@XT", "u}krIq_inA_@y@My@Yu@OqAUsA]mAQc@CS@o@FSHSp@e@n@Wl@]ZCFEBK?OC_@Qw@?m@CSK[]]EMBeAA_@m@qEAg@UoCAaAMs@IkBMoACq@SwAGOYa@IYIyA_@kEMkC]{DEaAScC@yEHkGA_ALsCBiA@mCD{CCuAZcANOH@HDZl@Z`@RFh@\\TDT@ZVJBPMVGLM\\Mz@c@NCPMXERO|@a@^Ut@s@p@KJAJ Bd@EHEXi@f@a@\\g@b@[HUD_B@uADg@DQLCLD~@l@`@J^TF?JANQ\\UbAyABEZIFG`@o@RAJEl@_@ZENDDIA[Ki@BURQZaARODKVs@LSdAiAz@G`BU^A^GT@PRp@zARXRn@`BlDHt@ZlAFh@^`BX|@HHHEf@i@FAHHp@bBd@v@DRAVMl@i@v@SROXm@tBILOTOLs@NON_@t@KX]h@Un@k@\\c@h@Ud@]ZGNKp@Sj@KJo@ b@W`@UPOX]XWd@UF]b@WPOAIBSf@QVi@j@_@V[b@Uj@YtAEFCCELARBn@`@lBjAzD^vB^hB?LENURkAv@[Ze@Xg@Py@p@QHONMA[HGAWE_@Em@Hg@AMCG@QHq@Cm@M[Jy@?UJIA{@Ae@KI@GFKNIX[QGAcAT[JK?OVMFK@IAIUKAYJI?QKUCGFIZCXDtAHl@@p@LjBCZS^ERAn@Fj@Br@Hn@HzAHh@RfD?j@TnCTlA NjANb@\\z@TtARr@P`AFnAGfBG`@CFE?" ] for (let encoded of encodedRoutes) { var coordinates = L.Polyline.fromEncoded(encoded).getLatLngs(); L.polyline( coordinates, { color: 'blue', weight: 2, opacity: .7, lineJoin: 'round' } ).addTo(map); } </script> </body> </html>
We can spin up a Python web server over that HTML file to see how it renders:
$ python -m http.server Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
And below we can see both runs plotted on the map.
Automating Strava API to Open Street Map
The final step is to automate the whole thing so that I can see all of my runs.
I wrote the following script to call the Strava API and save the polyline for every run to a CSV file:
import requests import os import sys import csv token = os.environ["TOKEN"] headers = {'Authorization': "Bearer {0}".format(token)} with open("runs.csv", "w") as runs_file: writer = csv.writer(runs_file, delimiter=",") writer.writerow(["id", "polyline"]) page = 1 while True: r = requests.get("https://www.strava.com/api/v3/athlete/activities?page={0}".format(page), headers = headers) response = r.json() if len(response) == 0: break else: for activity in response: r = requests.get("https://www.strava.com/api/v3/activities/{0}?include_all_efforts=true".format(activity["id"]), headers = headers) polyline = r.json()["map"]["polyline"] writer.writerow([activity["id"], polyline]) page += 1
I then wrote a simple script using Flask to parse the CSV files and send a JSON representation of my runs to a slightly modified version of the HTML page that I described above:
from flask import Flask from flask import render_template import csv import json app = Flask(__name__) @app.route('/') def my_runs(): runs = [] with open("runs.csv", "r") as runs_file: reader = csv.DictReader(runs_file) for row in reader: runs.append(row["polyline"]) return render_template("leaflet.html", runs = json.dumps(runs)) if __name__ == "__main__": app.run(port = 5001)
I changed the following line in the HTML file:
var encodedRoutes = {{ runs|safe }};
Now we can launch our Flask web server:
$ python app.py * Running on http://127.0.0.1:5001/ (Press CTRL+C to quit)
And if we navigate to http://127.0.0.1:5001/ we can see all my runs that went near Westminster:
The full code for all the files I’ve described in this post are available on github. If you give it a try you’ll need to provide your Strava Token in the ‘TOKEN’ environment variable before running extract_runs.py.
Hope this was helpful and if you have any questions ask me in the comments.
Reference: | Leaflet: Mapping Strava runs/polylines on Open Street Map from our WCG partner Mark Needham at the Mark Needham Blog blog. |