How to plot your own bike/running route using Python and Google Maps API

Apart from being a data scientist, I also spend a lot of time on my bike. It is therefore no surprise that I am a huge fan of all kinds of wearable devices. Lots of the times though, I get quite frustrated with the data processing and data visualization software that major providers of wearable devices offer. That’s why I have been trying to take things to my own hands. Recently I have started to play around with plotting my bike route from Python using Google Maps API. My novice’s guide to all this follows in the post.

strava_map

Recently I was playing with my sports data and wanted to create a Google map with my bike ride like Garmin Connect or Strava is doing.

That let me to Google Map API, specifically to their JavaScript API. And since I was playing with my data in Python we’ll be creating the map from there.

But first things first. To get the positional data for some of my recent bike rides I downloaded a TCX file from Garmin Connect. Parsing the TCX is easy but more about that some other time. For now let me just show a basic Python 3.x snippet that parses latitude and longitude from my TCX file and stores them in a pandas data frame.

from lxml import objectify
import pandas as pd

# helper function to handle missing data in my file
def add_trackpoint(element, subelement, namespaces, default=None):
in_str = './/' + subelement
try:
return float(element.find(in_str, namespaces=namespaces).text)
except AttributeError:
return default

# activity file and namespace of the schema
tcx_file = 'activity_1485936178.tcx'
namespaces={'ns': 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2'}

# get activity tree
tree = objectify.parse(tcx_file)
root = tree.getroot()
activity = root.Activities.Activity

# run through all the trackpoints and store lat and lon data
trackpoints = []
for trackpoint in activity.xpath('.//ns:Trackpoint', namespaces=namespaces):
latitude_degrees = add_trackpoint(trackpoint, 'ns:Position/ns:LatitudeDegrees', namespaces)
longitude_degrees = add_trackpoint(trackpoint, 'ns:Position/ns:LongitudeDegrees', namespaces)
trackpoints.append((latitude_degrees,
longitude_degrees))

# store as dataframe
activity_data = pd.DataFrame(trackpoints, columns=['latitude_degrees', 'longitude_degrees'])

Now we can focus on the Google Map JavaScript. The documentation is really great so there is no point in rewriting it myself. This tutorial got me started. In a nutshell, I was about to create a html file that would source Google Map JavaScript API and use its syntax to create a map and plot the route on it.

Following javascript code initializes a new map.

var map;
function show_map() {{
map = new google.maps.Map(document.getElementById("map-canvas"), {{
zoom: {zoom},
center: new google.maps.LatLng({center_lat}, {center_lon}),
mapTypeId: 'terrain'
}});

What we need to solve is where to centre the map and what should be the zoom. The first task is easy as you can simply take an average of minimal and maximal latitude and longitude. Zoom is where things get a bit tricky.

Zoom is documented here plus I found an extremely useful answer on stackoverflow. The trick is to get the extreme coordinates of the route and deal with the Mercator projection Google Maps is using to get the zoom needed to show the whole route on one screen. This is done by functions _get_zoom and _lat_rad as shown further down in a code with Map class I used.

Once we have a map that is correctly centered and zoomed we can start plotting the route. This step is done by using simple polylines. Such polyline is initialised by following javascript code.

var activity_route = new google.maps.Polyline({{
path: activity_coordinates,
geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 1.0,
strokeWeight: 2
}});

Where activity_coordinates contains the coordinates of my route.

I wrapped all this into a Python class called Map that looks as follows

from __future__ import print_function
import math

class Map(object):

def __init__(self):
self._points = []

def add_point(self, coordinates):
"""
Adds coordinates to map
:param coordinates: latitude, longitude
:return:
"""

# add only points with existing coordinates
if not ((math.isnan(coordinates[0])) or (math.isnan(coordinates[1]))):
self._points.append(coordinates)

@staticmethod
def _lat_rad(lat):
"""
Helper function for _get_zoom()
:param lat:
:return:
"""
sinus = math.sin(math.radians(lat + math.pi / 180))
rad_2 = math.log((1 + sinus) / (1 - sinus)) / 2
return max(min(rad_2, math.pi), -math.pi) / 2

def _get_zoom(self, map_height_pix=900, map_width_pix=1900, zoom_max=21):
"""
Algorithm to derive zoom from the activity route. For details please see
- https://developers.google.com/maps/documentation/javascript/maptypes#WorldCoordinates
- http://stackoverflow.com/questions/6048975/google-maps-v3-how-to-calculate-the-zoom-level-for-a-given-bounds
:param zoom_max: maximal zoom level based on Google Map API
:return:
"""

# at zoom level 0 the entire world can be displayed in an area that is 256 x 256 pixels
world_heigth_pix = 256
world_width_pix = 256

# get boundaries of the activity route
max_lat = max(x[0] for x in self._points)
min_lat = min(x[0] for x in self._points)
max_lon = max(x[1] for x in self._points)
min_lon = min(x[1] for x in self._points)

# calculate longitude fraction
diff_lon = max_lon - min_lon
if diff_lon < 0:
fraction_lon = (diff_lon + 360) / 360
else:
fraction_lon = diff_lon / 360

# calculate latitude fraction
fraction_lat = (self._lat_rad(max_lat) - self._lat_rad(min_lat)) / math.pi

# get zoom for both latitude and longitude
zoom_lat = math.floor(math.log(map_height_pix / world_heigth_pix / fraction_lat) / math.log(2))
zoom_lon = math.floor(math.log(map_width_pix / world_width_pix / fraction_lon) / math.log(2))

return min(zoom_lat, zoom_lon, zoom_max)

def __str__(self):
"""
A Python wrapper around Google Map Api v3; see
- https://developers.google.com/maps/documentation/javascript/
- https://developers.google.com/maps/documentation/javascript/examples/polyline-simple
- http://stackoverflow.com/questions/22342097/is-it-possible-to-create-a-google-map-from-python
:return: string to be stored as html and opened in a web browser
"""
# center of the activity route
center_lat = (max((x[0] for x in self._points)) + min((x[0] for x in self._points))) / 2
center_lon = (max((x[1] for x in self._points)) + min((x[1] for x in self._points))) / 2

# get zoom needed for the route
zoom = self._get_zoom()

# string with points for the google.maps.Polyline
activity_coordinates = ",\n".join(
["{{lat: {lat}, lng: {lon}}}".format(lat=x[0], lon=x[1]) for x in self._points])

return """


<div id="map-canvas" style="height: 100%; width: 100%;"></div>


var map;
function show_map() {{
map = new google.maps.Map(document.getElementById("map-canvas"), {{
zoom: {zoom},
center: new google.maps.LatLng({center_lat}, {center_lon}),
mapTypeId: 'terrain'
}});

var activity_coordinates = [{activity_coordinates}]

var activity_route = new google.maps.Polyline({{
path: activity_coordinates,
geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 1.0,
strokeWeight: 2
}});

activity_route.setMap(map);
}}
google.maps.event.addDomListener(window, 'load', show_map);

""".format(zoom=zoom, center_lat=center_lat, center_lon=center_lon, activity_coordinates=activity_coordinates)

Using this to plot my route, I simply start with object initialization:

from activity_map import Map
import webbrowser

# init map
loc_map = Map()

The next step is to add my route coordinates to the Map object.

# add coordinates
activity_data.apply(lambda row: loc_map.add_point((row['latitude_degrees'], row['longitude_degrees'])), axis=1)

Finally, I can print my Map object into some html file and open it in a browser (which is when the Google Maps API is called).

# save as html
with open('activity_map.html', "w") as out:
print(loc_map, file=out)

# open in a web browser
webbrowser.open_new_tab('activity_map.html')

And voilà here is my route! Below is only a picture, but in reality it is a JavaScript of course.

activity_googlemap

Please mind that if you want to embed such a map in your page you need to use an API key. One can apply for it here.

Did you like it?

Let us know so we can improve

+1 Vote DownVote Up