My Runing Map using python and folium
Tags: python, beginners
Published: 2023-09-07 20:27:13

_A map of race events I took part in, with gpx traces and pop-up race information, entirely generated from a google spreadsheet._

[![race_map](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8z6tud27udf1ajls1byq.png)](https://alexdjulin.ovh/run/run_map/run_map.html)_Click on map to display the interactive one._

[Link to the Github project](https://github.com/alexdjulin/running-events-map)

I'm a huge fan of maps, probably since I played around with my dad's roadmaps as a kid (he hated it, as he could never fold them back properly!). I cannot spend a day without fiddling
with Google Maps. Although my trail running hobby and my GPS-watch addiction plays a big part in that!

I have a running [blog](https://run.alexdjulin.ovh/p/events.html) where I share reviews of events I took part in 🏃 But over the years, I kind of lost track of them. How many was there? Where did I run? How was the route? I need some kind of maps to visualise them. What if I could make it so simple, that I would only need to update a google spreadsheet with race information and the map on my blog would update accordingly? Let's see what we can do...

---

**[FOLIUM](https://python-visualization.github.io/folium/latest/)**. That's our code-word for today. Folium is a python module that is going to provide exactly what we need: Manipulate data in Python, then visualize it in a Leaflet map. 

Let's start with a simple example: Creating a map from one race event logged in this [spreadsheet](https://docs.google.com/spreadsheets/d/1WghWJbxdCeKpbi3-H_6FmBAv_jILD_1_woRhuKGJ190/edit?usp=sharing). 

[![spreadsheet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/229nw47uhsrv0k0xtr8j.png)](https://docs.google.com/spreadsheets/d/1WghWJbxdCeKpbi3-H_6FmBAv_jILD_1_woRhuKGJ190/edit?usp=sharing)

See [tutorial.py](https://github.com/alexdjulin/running-events-map/blob/main/tutorial/tutorial.py) on my git repo.

---

## Download the spreadsheet
Let's first download the google spreadsheet as a csv file, it will be easier to work with the data. I shared the document so anyone with the link can access it. The following command will download the spreadsheet.

```python
import os
sheet_url = 'https://docs.google.com/spreadsheets/d/1WghWJbxdCeKpbi3-H_6FmBAv_jILD_1_woRhuKGJ190/export?exportFormat=csv'
current_folder = os.path.dirname(os.path.abspath(__file__))
csv_filepath = os.path.join(current_folder, 'run_events.csv')
command = f'curl -L {sheet_url} -o {csv_filepath}'
os.system(command)
```

You should now have a _run_events.csv_ file next to your script.

---

## Load CSV data
Who doesn't love [Pandas](https://pandas.pydata.org/) 🐼? Load the contents of the csv file into a python dictionnary.

```python
import pandas as pd
data = pd.read_csv(csv_filepath).to_dict(orient='records')[0]
print(data)

# Output:
# {'Date': '28.09.2014', 'Race': '41. Berlin Marathon', 'Latitude': 52.51625499, 'Longitude': 13.37757535, 'Time': '4:17:35', 'Link': 'https://www.bmw-berlin-marathon.com/'}
```

---

## Create the html map
Now we can start having fun! You will find Folium tutorials everywhere and the documentation itself is pretty straightforward. Let's generate a simple map and populate it with our data.

```python
import folium

# create map object and center it on our event
import folium
run_map = folium.Map(location=[data['Latitude'], data['Longitude']], tiles=None, zoom_start=12)

# add Openstreetmap layer
folium.TileLayer('openstreetmap', name='OpenStreet Map').add_to(run_map)

# save and open map
run_map.save('run_map.html')
import webbrowser
webbrowser.open('run_map.html')
```

Run the code, the map should be generated, saved and opened in your default web browser. As you can see, it's empty and just centered on the race location (Berlin). Click images to see html maps.

[![map_empty](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w9a0vgarq1et17y5bx7d.png)](https://alexdjulin.ovh/dev/run_map/map_empty.html)

---

## Populate the map
Let's add a marker point for our race to the map. We want it to be part of a 'Marathons' feature group, display the race name as tooltip, assign a color to it and display a short legend in the corner.

```python
# add feature group for Marathons
fg_marathons = folium.FeatureGroup(name='Marathons').add_to(run_map)

# create marker and add it to marathon feature group
folium_marker = folium.Marker(location=[data['Latitude'], data['Longitude']], tooltip=data['Race'], icon=folium.Icon(color='red'))
folium_marker.add_to(fg_marathons)

# add legend in top right corner
run_map.add_child(folium.LayerControl(position='topright', collapsed=False, autoZIndex=True))
```

Our marker now shows up at the given coordinates. It displays the name of the race if we hover over it and we can display or hide it from the corner legend.

[![map_marker](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8yqk62zjtj8tfa1fond7.png)](https://alexdjulin.ovh/dev/run_map/map_marker.html)

---

## Add pop-up windows
It would be nice to make that marker clickable and offer more information about the race. Let's create an html iframe that will pop-up when the user clicks on the marker. You will need some basic html knowledge for that, but nothing fancy at this point.

```python
# create an iframe pop-up for the marker
popup_html = f"<b>Date:</b> {data['Date']}<br/>"
popup_html += f"<b>Race:</b> {data['Race']}<br/>"
popup_html += f"<b>Time:</b> {data['Time']}<br/>"
popup_html += '<b><a href="{}" target="_blank">Event Page</a></b>'.format(data['Link'])
popup_iframe = folium.IFrame(width=200, height=110, html=popup_html)

# modify the marker object to display the pop-up
folium_marker = folium.Marker(location=[data['Latitude'], data['Longitude']], tooltip=data['Race'], popup=folium.Popup(popup_iframe), icon=folium.Icon(color='red'))
folium_marker.add_to(fg_marathons)
```

There it is. We even created a link on the url, opening the event page in another tab. And since it's an html iframe, we can now basically display anything we want in it (pictures, videos, links, css stying, and so on).

[![map_popup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kfm0qe3aokx4cpl6kti3.png)](https://alexdjulin.ovh/dev/run_map/map_popup.html)

---

## Add GPX trace
Cherry on top, it would be amazing to display the route, like you usually see on the even't website.

![map_gpx](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s80q7kkx6btlcva4sgv3.png)

This was actually easier than I thought. GPX files recorded by gps-watches or phones are XML-documents easy to read and parse. I used the [gpxpy](https://github.com/tkrajina/gpxpy) library for that, which is doing exactly what we need: read the gpx file and extract points/segments from it to display on our map. You can find GPX files everywhere, on [Strava](https://www.strava.com/clubs/257389/group_events/680829) or [Komoot](https://www.komoot.com/tour/46519986) for instance.

Here is how I opened and extracted segments from my own GPX file recorded during the race. I am using a _step_ value when slicing all the points, to smooth out the curve (loading 1 every 10 coordinate points).

```python
# parse gpx file
import gpxpy
gpx_file = 'berlin_marathon_2014.gpx'
gpx = gpxpy.parse(open(gpx_file))
track = gpx.tracks[0]
segment = track.segments[0]

# load coordinate points
points = []
for track in gpx.tracks:
    for segment in track.segments:
        step = 10
        for point in segment.points[::step]:
            points.append(tuple([point.latitude, point.longitude]))

# add segments to the map
folium_gpx = folium.PolyLine(points, color='red', weight=5, opacity=0.85).add_to(run_map)

# add the gpx trace to our marathon group
folium_gpx.add_to(fg_marathons)
```

Wonderful, the trace is showing up as expected on the map. Notice that, as we added it to the Marathons feature group, it inherits the red color and is affected by the legend checkbox too. You can now play around with the segments weight, opacity, color and with the step value to fine-tune it.

[![map_gpx](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ttsolgc88jk6st65qpf3.png)](https://alexdjulin.ovh/dev/run_map/map_gpx.html)

That's it, you have all you need to build a kick-ass map. Just add multiple lines to the spreadsheet and modify your dataframe to load all the data into lists.

---

## Improve the map
If you browse through my [run_map.py](https://github.com/alexdjulin/running-events-map/blob/main/run_map.py) script, you will notice that it is a bit more advanced. Here are some improvements I added to my map and to the project itself, to make it more appealing and to fit my needs:

- Add additional tile layers (ArcGIS)
- Group my events by type (halfs, marathons, ultras) and assign to each one a different color, also affecting the pop-up title and the gpx trace
- Customize the pop-up window with a picture of the race, a nice font, links, etc
- Move all paths and map settings to a json file, so there are no hard-coded values in the code and it is easier to change a setting (gpx trace weight or opacity for instance)
- Put all race events info into the google spreadsheet
- Add an ftp upload method at the end to send the html, jpg and gpx files onto the online storage my blog is using


[![run_map_final](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8p46pf25dafs1gsrpbu2.png)](https://alexdjulin.ovh/run/run_map/run_map.html)

You can see the final result in the [Events](https://run.alexdjulin.ovh/p/events.html) tab of my running blog.

Finally, you may notice that my script does not only update the map but also a table of events information displayed below it, as well as a little event-o-meter gadget in the sidebar. Both are generated when I run the script and updated according to the updated google spreadsheet information.

![event_table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0z9yc5bzydg8px25v6an.png)

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zn5yams3l1cbigt2iusr.png" alt="eventometer" width="100" height="100"/>

---

## Conclusion

I therefore succeeded in my holy quest to put all my running events on a pretty nice-looking map. All I need to do now, after completing a new event, is to fill in the information in the google doc and provide a jpg thumbnail and the gpx trace of my run, then run the script to generate a new map and update the table and gadget. This last step could be automated of course, if we had our script running in the cloud and checking any update done on the spreadsheet.

Have fun playing with folium and don't forget to share your maps!
Take care and see you on the trail 🏔️🏃‍♂️