@@ -1119,7 +1137,7 @@
Version 0.1
-Version 0.1.3
+Version 0.1.3
- Generate some heatmap images.
- Generate an explorer tile video.
diff --git a/search/search_index.json b/search/search_index.json
index 66c935d7..5abfac1f 100644
--- a/search/search_index.json
+++ b/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"I like to be outside and I like to record my outdoor activities with a GPX tracker. So far I have kept all my data with Strava and did some analyses there and also with Statshunters. This is already pretty nice.
There are some things which they don't offer, so I want to have a place to implement them. Also I want to support the option of using only local data. This makes this project attractive for people who have a stash of GPX or FIT files but no nice way to analyze their data.
The goal of this project is to import data either from local GPX/FIT files or the Strava API and provide some interesting insights into the data.
You can find the code on GitHub where you can also file issues. If you would like to use this yourself or contribute, feel free to reach out via the contact options from my website. I would especially appreciate improvements to the documentation. If you're familiar with Markdown and GitHub, you can also directly create a pull request.
This project is still in a early prototyping phase, I'm still changing a lot and some fundamental decisions are still not finally settled. So please keep in mind that it is all pretty brittle.
"},{"location":"#get-started","title":"Get started","text":" - Install the software using the stable version. If you want to use the latest development version, use that guide.
- Choose a data source, either activity files or the Strava API. You can only have one source per playground. But you can have as many playgrounds as you like.
"},{"location":"acknowledgments/","title":"Acknowledgments","text":"This project builds on many amazing other projects and would not be possible without them.
"},{"location":"acknowledgments/#bootstrap-css","title":"Bootstrap CSS","text":"Writing CSS is not a trivial task. For many projects I have been using the Bootstrap CSS Framework which provides sensible default values, a 12-column grid system and a lot of components. Using this I didn't have to write any CSS myself and just attach a couple of classes to HTML elements.
"},{"location":"acknowledgments/#coloredlogs","title":"coloredlogs","text":"Log messages in multiple colors are neat. Using the coloredlogs package we can get these super easily.
"},{"location":"acknowledgments/#fitdecode","title":"fitdecode","text":"For reading FIT files I use the fitdecode library which completely handles all the parsing of this file format.
"},{"location":"acknowledgments/#flask","title":"Flask","text":"The webserver is implemented with Flask which provides a really easy way to get started. It also ships with a development webserver which is enough for this project at the moment.
"},{"location":"acknowledgments/#geojson","title":"GeoJSON","text":"Transferring geographic geometry data from the Python code to Leaflet is easiest with using the GeoJSON format. The official standard RFC is a bit hard to read, rather have a look at the Wikipedia article. And there is an online viewer that you can try out.
"},{"location":"acknowledgments/#github","title":"GitHub","text":"For a smooth open source project one needs a place to share the code and collect issues. GitHub provides all of this for free.
"},{"location":"acknowledgments/#gpxpy","title":"gpxpy","text":"For reading GPX files I use the gpxpy library. This allows me to read those files without having to fiddle with the underlying XML format.
"},{"location":"acknowledgments/#leaflet","title":"Leaflet","text":"The interactive maps on the website are powered by Leaflet, a very easy to use JavaScript library for embedding interactive Open Street Map maps. It can also display GeoJSON geometries natively, of which I also make heavy use.
"},{"location":"acknowledgments/#mkdocs","title":"MkDocs","text":"Writing documentation is more fun with a nice tool, therefore I use MkDocs together with Material for MkDocs. This powers this documentation.
"},{"location":"acknowledgments/#open-street-map","title":"Open Street Map","text":"All the maps displayed use tiles from the amazing Open Street Map. This map is created by volunteers, the server hosting is for free. Without these maps this project would be quite boring.
"},{"location":"acknowledgments/#pandas","title":"Pandas","text":"Working with thousands of activities, thousands of tiles and millions of points makes it necessary to have a good library for number crunching structured data. Pandas offers this and gives good performance and many features.
"},{"location":"acknowledgments/#parquet","title":"Parquet","text":"I need to store the intermediate data frames that I generate with Pandas. Storing as JSON has disadvantages because dates are not properly encoded. Also it is a text format and quite verbose. The Parquet format is super fast and memory efficient.
"},{"location":"acknowledgments/#poetry","title":"Poetry","text":"For managing all the Python package dependencies I use Poetry which makes it very easy to have all the Python project housekeeping with one tool.
"},{"location":"acknowledgments/#python","title":"Python","text":"Almost all of the code here is written in Python, a very nice and versatile programming language with a vast ecosystem of packages.
"},{"location":"acknowledgments/#requests","title":"Requests","text":"For doing HTTP requests I use the Requests library. It provides a really easy to use interface for GET and POST requests.
"},{"location":"acknowledgments/#scikit-learn","title":"Scikit-learn","text":"Finding out which cluster is the largest one can either be formed as a graph search problem or as a data science problem. Using the Scikit-learn library I can easily use the DBSCAN algorithm to find the clusters of explorer tiles.
"},{"location":"acknowledgments/#statshunters","title":"Statshunters","text":"The Statshunters page allows to import the activities from Strava and do analysis like explorer tiles, Eddington number and many other things. This has served as inspiration for this project.
"},{"location":"acknowledgments/#strava","title":"Strava","text":"Although I have recorded some of my bike rides, I only really started to record all of them when I started to use Strava. This is a nice platform to track all activities. They also offer a social network feature, which I don't really use. They provide some analyses of the data, but they lack some analyses which I have now implemented in this project.
"},{"location":"acknowledgments/#stravalib","title":"stravalib","text":"Strava has an API, and with stravalib there exists a nice Python wrapper. This makes it much easier to interface with Strava.
"},{"location":"acknowledgments/#strava-local-heatmap","title":"Strava local heatmap","text":"https://github.com/remisalmon/Strava-local-heatmap
"},{"location":"acknowledgments/#tcxreader","title":"tcxreader","text":"https://github.com/alenrajsp/tcxreader
"},{"location":"acknowledgments/#vega-altair","title":"Vega & Altair","text":"https://altair-viz.github.io/index.html https://vega.github.io/vega/
"},{"location":"acknowledgments/#velo-viewer","title":"Velo Viewer","text":""},{"location":"features/activity-view/","title":"Activity View","text":"When you have selected a particular activity, you can view various details about it. This is what the screen looks like, we will go through the different parts in the following.
"},{"location":"features/activity-view/#metadata","title":"Metadata","text":"You have a column with metadata about the activity. The activity kind, whether it is a commute and the equipment are currently only supported via the Strava API, but we can build something to infer that from directories as well.
The calories are broken in the Strava API wrapper library that I use, therefore they don't show even if they are there.
You can also see the ID which is an internal ID. When you use Strava API as a source, it will use the IDs that Strava gives. When you use files from a directory it will be computed from a hash of the path to the activity file.
"},{"location":"features/activity-view/#map-with-track","title":"Map with track","text":"The interactive map shows a line with the activity. The speed is color-coded and peaks at 35 km/h with a yellow color.
"},{"location":"features/activity-view/#distance-speed-and-altitude","title":"Distance, speed and altitude","text":"Then there are a couple of time series plots. One is the distance vs. time. You can see how much distance you covered when and also see plateaus when you went on a break.
From this we can also compute the speed, although that might be pretty noisy:
And more interesting is the distribution of the various speed zones. This gives you an understanding how much time you spent at which speed. The buckets are set in 5 km/h intervals, but we could also change that.
If the time series data has the altitude, which isn't always the case, you can see it in the plot there. Here we can see how I did a tour and continually rode downhill. Except at the end where I had to climb in order to get the explorer tile that I wanted.
"},{"location":"features/activity-view/#heart-rate","title":"Heart rate","text":"The heart rate isn't too helpful, I feel. Still I've created the plot from the given data.
More interesting regarding the heart rate are the zones which one has spent time during this activity.
The definition of the heart rate zones is not standardized. Usually there are five zones and they have the same names. What differs is how their ranges are computed and there is some chaos around that.
All definitions that I found take the maximum heart rate as the upper limit. One can measure this as part of a professional training or just use the 220 minus age prescription which at least for me matches close enough. What they differ on is how they use a lower bound. It seems that Polar or REI basically use 0 as the lower bound. My Garmin system also uses 0 as the lower bound. But as one can see in this blog, one can also use the resting heart rate as the lower bound.
Based on the maximum and resting heart rate we will then compute the heart rate zones using certain percentages of effort. We can compute the heart rate as the following:
rate = effort \u00d7 (maximum \u2013 minimum) + minimum
The zones then take the following efforts:
Zone Effort Training 1 50 to 60 % Warmup/Recovery 2 60 to 70 % Base Fitness 3 70 to 80 % Aerobic Endurance 4 80 to 90 % Anerobic Capacity 5 90 to 100 % Speed Training You can decide how you want to do work with that. If you want to have the same definitions that say Garmin uses, you need to just enter your birth year and we can compute the rest. If you want to use a lower bound, you need to specify that.
For this create a configuration file at Playground/config.toml
and enter a stanza in there, like this:
[heart]\nbirthyear = 19..\n
If you know your maximum heart rate, you can also write maximum = 187
. If you want to use the lower bound, write resting = 48
as well. So it might look like this:
[heart]\nmaximum = 187\nresting = 48\n
You can also combine birthyear
and resting
and leave out maximum
.
If you are not happy with this prescription, please let me know.
"},{"location":"features/calendar/","title":"Calendar","text":"In order to access all the activities, there is a calendar view. It shows the years in rows and the months in columns. Each cell is a particular month and indicates the total distance traveled.
When you click on any of the months, you will see a calendar for a given month. Here is one of my months which doesn't show too personal data:
Clicking on an activity will lead you to the activity detail view.
"},{"location":"features/eddington/","title":"Eddington Number","text":"The astronomer Sir Arthur Eddington like to go on longer bike rides. Apparently he did a lot of rides and had 84 days where he rode at least 84 miles. The Eddington number for cycling was coined from this.
If you have an Eddington number E, it means that you have had E days with at least E kilometer distance. At the time of writing my number is 62, which means that I rode at least 62 km on 62 separate days. If I want to extend it to 63, I would need to have at least 63 km on 63 separate days. My bike rides that are just 62 km long will not count any more, making this challenge really hard.
In the following plot you can see in blue the number of days that exceed the given distance. You can see that I have a 998 days that exceed 1 km, that's pretty easy when one just records enough data over many years. But then there are only 259 days where I exceed 20 km as this is beyond the distance that I have when I only go for a walk or run some simple errands.
The red curve indicates how many rides one needs to get the Eddington number. As it is a semi-log plot, the straight line is curved like a log-curve.
You can see this cliff at around 80 km. That is the distance that I ride to work and back. I have many days with around 80 km, but longer rides are only on occasional bike trips. Therefore I think I will eventually make it to an Eddington number of 80, but beyond that will be super difficult.
This is a life-long challenge, so who knows what happens in the future.
"},{"location":"features/eddington/#length-unit","title":"Length unit","text":"The definition of the Eddington number depends on the length unit that one has. Eddington as a British person used the English mile as a base unit. Therefore his number of 84 is actually harder to achieve than a 84 based on kilometers because he needed to exceed 135 km (84 mi) on each ride.
Therefore using a different length unit as a base changes the meaning. The kilometer is easier, the mile (1609.344 m) is harder. One could also use nautical miles (1852 m). And while we are at arbitrary unit systems, we could also use furlongs (201.168 m).
"},{"location":"features/equipment/","title":"Equipment Overview","text":"When activities are tagged with the used equipment, one can tally the total distance traveled with each thing. This given as a table with the recently used equipment at the top:
And for each thing there is also a graph which shows how the distance accumulates over time:
Here one can see how with my old bike I only recorded the occasional bike trip and not nearly the 4000 km/a which I was doing. And then it got stolen in 2019, so I bought a new bike.
This metadata is downloaded via the Strava API, for the directory source it is not yet supported. I thought about using directories like Activities/{Activity Kind}/{Equipment Name}/{Activity Name}.gpx
. to indicate the kind and equipment. If you're interested in implementing this, let me know.
"},{"location":"features/explorer-tiles/","title":"Explorer Tiles","text":"Maps accessible via the web browser are usually served as little image tiles. The Open Street Map uses the Web Mercator coordinate system to map from latitude and longitude to pixels on the map.
Each tile is 256\u00d7256 pixels in size. The zoom levels zoom in by a factor of two. Therefore all the tiles are organized in a quad tree. As you zoom in, each tile gets split into four tiles which can then show more detail. The following prescription maps from latitude and longitude (given in degrees) to tile indices:
def compute_tile(lat: float, lon: float, zoom: int = 14) -> tuple[int, int]:\n x = np.radians(lon)\n y = np.arcsinh(np.tan(np.radians(lat)))\n x = (1 + x / np.pi) / 2\n y = (1 - y / np.pi) / 2\n n = 2**zoom\n return int(x * n), int(y * n)\n
At zoom level 14 the tiles have a side length of roughly 1.5 km in Germany. These tiles are used as the basis for explorer tiles. The basic idea is that every tile where you have at least one point in an activity is considered an explored tile.
From your activities the program will extract all the tiles that you have visited. And then it does a few things with those. One main thing is that it will display these on an interactive map. When we zoom into one area where I've been on vacation in 2023, you can see the explored tiles there:
The colored tiles are explored, I have been there. The green tiles are cluster tiles, that means that all their four neighbor tiles are also explored.
You can see here how I have explored a region and ensured that it is mostly contiguous.
There is another vacation from 2013 where I wasn't aware of the cluster tiles. I just did some bike trips and didn't look out for the tiles. There the tiles look like this:
You see all these gaps in there. And this is what the explorer tiles are about: This OCD (obsessive compulsive disorder) like craving to fill in the gaps.
Let's take a look at my main cluster of explorer tiles. Here I have explored much more than in the areas where I was on vacation.
You can see an additional feature, the blue square. This is the one largest square which can be fit into all explored tiles. In this picture it has size 21\u00b2. The idea of the square is to have a really tough challenge. Not only does one need to explore increasingly many tiles to expand the square by one unit, there must not be any gaps.
As you can see in this picture, there is a tile missing right at the top edge. I will never be able to get that because that is an off-limits area of the German air force at the airport. So I can expand my square to the south only.
You can click on each tile and get some information about that particular tile. You can see when you first explored that and with which activity. Also it shows the last activity there as well as the number of activities. If it is a local cluster, it will also show the cluster size.
There is also the option to color the tiles by first or last visit. Use one of the buttons above the map:
Then the map will show the first visit:
Or how recent your last visit is:
This uses Matplotlib's Plasma scale (see below) to color the age of a tile. Very new tiles will get a yellow color, a year old tiles a reddish color and tiles two years old or older a colder blue. This is the scale:
You can switch this with the buttons above the map.
"},{"location":"features/explorer-tiles/#squadratinhos","title":"Squadratinhos","text":"The explorer tiles at zoom level 14 are best suited for cycling and to discover the area around the city. There is a derived definition, the squadratinhos which are defined at zoom level 17 and therefore a factor 8 smaller in each direction. Each explorer tile is therefore divided into 256 squadratinhos.
These are better suited for walking and making sure that you really explored every little place in your neighborhood. Since they are so small, there are many properties which one cannot go onto, like industrial sites, airports or just a wide river.
For my home city it looks like this:
You can see how the squadratinhos are much smaller than the explorer tiles and how they lend themselves to more local exploring.
"},{"location":"features/explorer-tiles/#history","title":"History","text":"The map only shows the current state of your explorer tiles. In order to get a sense of how many new tiles you have discovered in the past, there are also plots that show you how you have extended the total number of squares, the size of your largest cluster and the size of your largest square over time:
"},{"location":"features/explorer-tiles/#missing-tile-files","title":"Missing tile files","text":"Looking at these maps you can see the gaps. And if you feel challenged to fill those, you might want to plan a \u201ctactical bike ride\u201d to explore those. Take for instance this set of tiles from a vacation:
You can see those two gaps in the south. To make identifying these gaps a bit easier there is a second map which shows you the missing tiles. It has the gaps and also a border around the explored tiles:
One can use those maps to plan the routes. You can also download these maps as a GeoJSON file and use other programs to display them. For instance with GPX See on Linux it looks like this:
I have not tested this software, but supposedly Offline Maps is able to display GeoJSON on Android.
On Android I use the OsmAnd app to display tracks and also try to visualize the missing tiles. Unfortunately GeoJSON is not supported, therefore one has to play some tricks. The missing tiles are also exported as a GPX file with a track for each missing tile. This looks strange, but it is a bit helpful with OsmAnd. This is how the file looks like in GPXSee:
And on OsmAnd it looks like this:
Unfortunately OsmAnd becomes a bit sluggish with such a huge track imported, so I don't have this displayed all the time.
"},{"location":"features/heatmaps/","title":"Heatmaps","text":"A heatmap shows where you have been more often than other places. There is an interactive and zoomable heatmap that looks like this:
Here you can see where I mostly travel between Bonn and Cologne.
The heatmap computation is based on the Strava local heatmap code. For the interactive map I needed to adjust it a lot. It might be possible that heatmaps don't look very nice if you have a different recording interval from the one that I use.
"},{"location":"features/heatmaps/#heatmap-images","title":"Heatmap images","text":"There is a second mode to generate heatmap images. Instead of using the serve
command, use the heatmaps
command. That will generate heatmap images with carefully adjusted colors using the original logic of Strava local heatmap.
We don't generate a single heatmap for all your activities as this will not look great as soon as you have done an activity away from home. Rather we use a clustering algorithm to find all disjoint geographical clusters in your activities and generate one heatmap per cluster.
For instance the heatmap generated from all my activities in the Randstad in the Netherlands:
And the activities around Nieuwvliet-Bad look like this:
These images are put into the Heatmaps
directory in your playground.
"},{"location":"features/overview/","title":"Overview page","text":"When you start the webserver, you will see the overview page. It looks like the following and shows the activities you've done in the past 30 days:
Then below that you see the latest 15 activities with their tracks on interactive maps.
Each card contains the name, activity type, distance and duration. The non-commute activities are highlighted with a blue border, the commutes just have a gray border.
Click on any of the names and you will see the details of that activity.
"},{"location":"getting-started/config-file/","title":"Configuration file","text":"The project tries to adhere to the convention over configuration mantra and therefore minimizes the amount of configuration necessary. There are still a few data points which one might need to fill out.
This page summarizes all the configuration file elements and links to the features that use them.
The configuration file is in the TOML format and contains multiple tables with options. We will go through them here one-by-one.
"},{"location":"getting-started/config-file/#strava-api","title":"Strava API","text":"If you want to use the Strava API, see the Strava API page first. There you can read how to find the values; the configuration snippet is this:
[strava]\nclient_id = \"\u2026\"\nclient_secret = \"\u2026\"\ncode = \"\u2026\"\n
"},{"location":"getting-started/config-file/#heart-rate","title":"Heart rate","text":"In order to use the heart rate zone feature, you will need to somehow specify your maximum and minimum heart rate.
In order to specify the maximum heart rate, either specify your birthyear
and it will estimate it from that. Or specify the maximum
heart rate instead. For the minimum you can optionally specify the resting
heart rate. If you leave that empty, the minimum will be taken as 0, which is how Garmin also computes the zones.
This is an example configuration:
[heart]\n# Specify either `birthyear` or `maximum`:\nbirthyear = 19..\n## maximum = 187\n\n# Optionally specify `resting`:\n## resting = 48\n
"},{"location":"getting-started/installing-git-on-linux/","title":"Installing Git Version On Linux","text":"As this project is still in development, you might want to have a peek into the development version. This is more advanced than using the stable versions, but not impossibly hard.
First you need to clone the git repository from GitHub using the following command:
git clone https://github.com/martin-ueding/geo-activity-playground.git\n
That will create a directory geo-activity-playground
in your current working directory.
Then change into that directory:
cd geo-activity-playground\n
Next we will use Poetry to install the dependencies of the project. First you need to make sure that you have Poetry available. On Ubuntu/Debian run sudo apt install python3-poetry
, on Fedora/RedHat run sudo dnf install poetry
to install it.
Then we can create the virtual environment:
poetry install\n
And next we can run the program:
poetry run geo-activity-playground --basedir path/to/your/playground --help\n
Replace the --help
with the subcommands described in the help message or the other parts described this documentation.
You will need the --basedir
option because you run the program from the source directory and not from your playground directory. If you install the stable version via PIP as described in the other page, you will not need this option.
"},{"location":"getting-started/installing-git-on-linux/#updating-to-the-latest-version","title":"Updating to the latest version","text":"Over time I will add more commits to the source control system. In order to update your clone to the latest version, execute the following:
git pull\n
This will download the missing changesets and apply them to your downloaded version. After that is done, you need to update your virtual environment with this:
poetry install\n
And then you can continue using it as before.
"},{"location":"getting-started/installing-stable-on-linux/","title":"Installing Stable On Linux","text":"In this how-to guide I will show you how you can install the latest stable version of this project on Linux.
Using PIP, you can install the latest version using this command:
pip install --user geo-activity-playground\n
If you get an error about the command pip
not found, you will need to install that first. On Ubuntu or Debian use sudo apt install python3-pip
, on Fedora or RedHat use sudo dnf install python3-pip
. After you have installed PIP, repeat the above command.
"},{"location":"getting-started/installing-stable-on-linux/#ensure-that-the-path-is-correct","title":"Ensure that the PATH is correct","text":"Next you can try to start the program by just entering the following into the terminal:
geo-activity-playground --help\n
If you get a help message, everything is fine. If you get an error about command not found, we need to adjust your PATH. Execute the following in your command line:
xdg-open ~/.profile\n
This brings up an editor with your shell profile. Add a line containing the following at the end of the file:
PATH=$PATH:$HOME/.local/bin\n
This adds the path to your shell environment. After you have done it, close your terminal window and open a new one. Try the first command in this section again, you should see the help message now.
"},{"location":"getting-started/installing-stable-on-linux/#upgrading-to-the-latest-version","title":"Upgrading to the latest version","text":"At some later point you likely want to upgrade to the latest version. For this use this command:
pip install --user --upgrade geo-activity-playground\n
"},{"location":"getting-started/starting-the-webserver/","title":"Starting The Webserver","text":"Before you start here, you should have done these things:
- You have installed the program either from a stable version or from git.
- You have set up a playground with either activity files or the Strava API.
Now we can start the webserver which provides most of the features. This is done with the serve
command. So depending on how you have installed it, the commands could look like these:
geo-activity-playground serve
if you are in the playground directory and have installed a stable version. poetry run geo-activity-playground --basedir ~/Dokumente/Karten/Playground serve
if you have it from the git checkout and want to use local files in your directory as a data source.
The webserver will start up and give you a bit of output like this:
2023-11-19 17:59:23 geo_activity_playground.importers.strava_api INFO Loading metadata file \u2026\n2023-11-19 17:59:23 stravalib.protocol.ApiV3 INFO GET 'https://www.strava.com/api/v3/athlete/activities' with params {'before': None, 'after': 1700392964, 'page': 1, 'per_page': 200}\n2023-11-19 17:59:23 geo_activity_playground.importers.strava_api INFO Checking for missing time series data \u2026\n * Serving Flask app 'geo_activity_playground.webui.app'\n * Debug mode: off\n2023-11-19 17:59:23 werkzeug INFO WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.\n * Running on http://127.0.0.1:5000\n2023-11-19 17:59:23 werkzeug INFO Press CTRL+C to quit\n
The warning about the development server is fine. We are using this only to play around, not to power a web service for other users.
Open http://127.0.0.1:5000 to open the website in your browser. There might be some more messages about downloading and parsing data. The first startup will take quite some time. When it is done you will see something like this:
Click around and explore the various features.
"},{"location":"getting-started/using-activity-files/","title":"Using Activity Files","text":"Outdoor activities are usually recorded as GPX or FIT files. Some apps like OsmAnd give you these files.
Create a directory somewhere, this will be your playground. I have mine in ~/Dokumente/Karten/Playground
, but you can put yours wherever you would like.
Inside Playground
, create another directory Activities
where your activity files will go. The program will not modify files in that directory and treat them as read-only.
"},{"location":"getting-started/using-activity-files/#directory-structure","title":"Directory structure","text":"Inside the Activities
you can dump all your files in a flat fashion. If you want to add some more metadata, use the following directory layout.
The first directory level will indicate the type of the activity. You can pick whatever make sense for you, classic options are ride, run, walk, hike. Then the second level will indicate your equipment. You can use terms like \u201crental bike\u201d, the brand and make of your shoes or whatever you find sensible. Specifying the equipment allows to track the total distance traveled with a given equipment.
At any level you can have a special directory Commute
. All activities inside of that will be marked as commutes and not highlighted as much as non-commute activities. The idea is that you can find your cool activities along potentially many commutes.
Other directory names on the third level will just be ignored, you can use those to organize your activities further in some sense.
Let us take the following directory/file structure as an example:
Activities\n\u251c\u2500\u2500 Commute\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 From the Beach.gpx\n\u251c\u2500\u2500 Ride\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 Rental Bike\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 Beach Rides\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 Breskens.gpx\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 Zwin.gpx\n\u251c\u2500\u2500 To Piazza.gpx\n\u2514\u2500\u2500 Walk\n \u251c\u2500\u2500 Commute\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 To the Beach.gpx\n \u251c\u2500\u2500 New Balance Fresh Foam 860v11\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 Beach Walk.gpx\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 Commute\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 From Piazza.gpx\n \u2514\u2500\u2500 Nieuvfliet.gpx\n
You can see that I have one file on the top level (To Piazza.gpx
), a file on the first level (Walk/Nieuvfliet.gpx
), some on the second level (Ride/Rental Bike/Zwin.gpx
), one commute (Walk/New Balance Fresh Foam 860v11/Commute/From Piazza.gpx
) and one with a group directory which isn't commuting (Ride/Rental Bike/Dunes in Sluis/Breskens.gpx
). From this the program will extract the following metadata:
Name Type Equipment Commute From the Beach None None Yes Breskens Ride Rental Bike No Zwin Ride Rental Bike No To Piazza None None No To the Beach Walk None Yes Beach Walk Walk New Balance Fresh Foam 860v11 No From Piazza Walk New Balance Fresh Foam 860v11 Yes Nieuwvliet Walk None No The file name of your activity will become the name of the activity.
"},{"location":"getting-started/using-activity-files/#supported-file-formats","title":"Supported file formats","text":"At the moment the following file formats are supported:
- FIT
- GPX
- TCX
"},{"location":"getting-started/using-activity-files/#next-steps","title":"Next steps","text":"Once you have your files put into the directory, you're all set and can proceed with the next steps.
"},{"location":"getting-started/using-strava-api/","title":"Using Strava API","text":"You might have all your data on the Strava service and would like to use this for additional analytics without moving your data. That is fine.
In order to use the Strava API, one needs to create an app. If my explanation doesn't suit you, have a look at this how-to guide as well.
Navigate to the API settings page and create an app. It only needs to have read permissions.
After you are done with that, you can see your App here:
There is a client ID that we are going to need for the next step. In general our app could be used by all sorts of people who can then access their data only. We want to access our own data, but we still need to authorize our app to use our data. In order to get this token, we need to visit the following URL, with {client_id}
replaced by your ID.
https://www.strava.com/oauth/authorize?client_id={client_id}&redirect_uri=http://localhost&response_type=code&scope=activity:read_all\n
This will prompt an OAuth2 request where you have to grant permissions to your app. After that you will be redirected to localhost and see a \u201cpage not found\u201d error. That is all okay!
Take a look at the URL. It look like this:
http://localhost/?state=&code={code}&scope=read,activity:write,activity:read_all\n
From the URL you can read off the code. Now you have all the three data points that you need. Create the file Playground/config.toml
with a text editor and fill in this template:
[strava]\nclient_id = \"\u2026\"\nclient_secret = \"\u2026\"\ncode = \"\u2026\"\n
Add the client ID and the client secret from your Strava app. Add the code that we have read off this localhost URL.
Then you are all set to download data from the Strava API. When you start one of the commands, it will automatically start to download.
"},{"location":"getting-started/using-strava-api/#rate-limiting","title":"Rate limiting","text":"When you first start this program and use the Strava API as a data source, it will download the metadata for all your activities. Then it will start to download all the time series data for each activity. Strava has a rate limiting, so after the first 200 activities it will crash and you will have to wait for 15 minutes until you can try again and it will download the next batch.
"},{"location":"reference/changelog/","title":"Changelog","text":"This is the log of high-level changes that I have done in the various versions.
"},{"location":"reference/changelog/#version-0","title":"Version 0","text":"This is the pre-release series. Things haven't settled yet, so each minor version might introduce breaking changes.
"},{"location":"reference/changelog/#version-012","title":"Version 0.12","text":" - Change coloring of clusters, have a color per cluster. Also mark the square just as an overlay.
- Fix bug with explorer tile page when the maximum cluster or square is just 1.
- Speed up the computation of the latest tiles.
"},{"location":"reference/changelog/#version-011","title":"Version 0.11","text":" - Add last activity in tile to the tooltip. GH-35
- Add explorer coloring mode by last activity. GH-45
- Actually implement
Activity/{Kind}/{Equipment}/{Name}.{Format}
directory structure. - Document configuration file.
- Interpolate tracks to find more explorer tiles. GH-27
- Fix bug that occurs when activities have no distance information.
- Show time evolution of the number of explorer tiles, the largest cluster and the square size. GH-33
- Center map view on biggest explorer cluster.
- Show speed distribution. GH-42
"},{"location":"reference/changelog/#version-010","title":"Version 0.10","text":" - Use a grayscale map for the explorer tile maps. GH-38
- Explicitly write \u201c0 km\u201d in calendar cells where there are no activities. GH-39, GH-40
"},{"location":"reference/changelog/#version-09","title":"Version 0.9","text":" - Certain exceptions are not skipped when parsing files. This way one can gather all errors at the end. GH-29
- Support TCX files. GH-8
- Fix equipment view when using the directory source. GH-25
- Fix links from the explorer tiles to the first activity that explored them. GH-30
- Fix how the API response from Strava is handled during the initial token exchange. GH-37
"},{"location":"reference/changelog/#version-08","title":"Version 0.8","text":""},{"location":"reference/changelog/#version-083","title":"Version 0.8.3","text":" - Only compute the explorer tile cluster size if there are cluster tiles. Otherwise the DBSCAN algorithm doesn't work anyway. GH-24
- Remove allocation of huge array. GH-23
"},{"location":"reference/changelog/#version-082","title":"Version 0.8.2","text":" - Some FIT files apparently have entries with explicit latitude/longitude values, but those are null. I've added a check which skips those points.
"},{"location":"reference/changelog/#version-081","title":"Version 0.8.1","text":" - Fix reading of FIT files from Wahoo hardware by reading them in binary mode. GH-20.
- Fix divide-by-zero error in speed calculation. GH-21
"},{"location":"reference/changelog/#version-080","title":"Version 0.8.0","text":" - Make heart rate zone computation a bit more flexibly by offering a lower bound for the resting heart rate.
- Open explorer map centered around median tile.
- Compute explorer cluster and square size, print that. GH-2
- Make it compatible with Python versions from 3.9 to 3.11 such that more people can use it. GH-22
"},{"location":"reference/changelog/#version-07","title":"Version 0.7","text":" - Add Squadratinhos, which are explorer tiles at zoom 17 instead of zoom 14.
- Reduce memory footprint for explorer tile computation.
"},{"location":"reference/changelog/#version-06","title":"Version 0.6","text":" - Interactive map for each activity.
- Color explorer tiles in red, green and blue. GH-2
- Directly serve GeoJSON and Vega JSON embedded in the document.
- Automatically detect which source is to be used. GH-16
- Fix the name of the script to be
geo-activity-playground
and not just geo-playground
. GH-11 - Add mini maps to the landing page. GH-9
- Add fullscreen button to the maps. GH-4
- Add favicon. GH-19
- Added some more clever caching to the explorer tiles such that loading the page with explorer tiles comes up in just a few seconds.
- Add a triplet of time series plots (distance, altitude, heart rate) for each activity.
- Show plot for heart rate zones per activity. GH-12
- Handle activities without any location points. GH-10
- Resolve Strava Gear name. GH-18
- Add page for equipment. GH-3
- Add a pop-up with some metadata about the first visit to the explorer tiles. GH-14
- Integrate missing explorer tiles into the web interface. GH-7.
- Color activity line with speed. GH-13
- Add interactive heatmap.
- Add margin to generated heatmaps. GH-1
"},{"location":"reference/changelog/#version-05","title":"Version 0.5","text":" - Add some plots for the Eddington number. GH-3
"},{"location":"reference/changelog/#version-04","title":"Version 0.4","text":""},{"location":"reference/changelog/#version-03","title":"Version 0.3","text":" - Start to build web interface with Flask.
- Remove tqdm progress bars and use colorful logging instead.
- Add interactive explorer tile map.
"},{"location":"reference/changelog/#version-02","title":"Version 0.2","text":" - Unity command line entrypoint.
- Crop heatmaps to fit.
- Export missing tiles as GeoJSON.
- Add Strava API.
- Add directory source.
"},{"location":"reference/changelog/#version-01","title":"Version 0.1","text":""},{"location":"reference/changelog/#version-013","title":"Version 0.1.3","text":" - Generate some heatmap images.
- Generate an explorer tile video.
"},{"location":"reference/directory-layout/","title":"Directory Layout","text":"There are a bunch of files that need to be given to this set of scripts, are used as intermediate files or generated as output. In order to make it clear where everything is, the following document lists all the paths which are relevant to the script.
Everything is relative to a base directory which can be passed with the --basedir
option to the scripts.
"},{"location":"reference/directory-layout/#input","title":"Input","text":"The user has to put data into the following directories in order for it to be picked up.
"},{"location":"reference/directory-layout/#output-and-cache","title":"Output and cache","text":"The following directories serve as a cache. One can inspect this but doesn't need to work with that directly.
-
Explorer
: Things related to the explorer tiles.
Per Activity
: A data frame with the tiles that have been visited within each activity. Each file is named with the activity ID like 2520340514.parquet
. The columns are time
, tile_x
, tile_y
. first_time_per_tile.parquet
: A data frame with the first visit datetime for each explorer tile. Columns time
, tile_x
, tile_y
. missing_tiles.geojson
: A GeoJSON file with square polygons for all missing tiles at the boundary of explored tiles. missing_tiles.gpx
: The same, just expressed as square tracks in the GPX format. explored.geojson
: A GeoJSON file with square polygons for all explored tiles. explored.gpx
: The same, just expressed as square tracks in the GPX format.
-
Heatmaps
: Will contain heatmap images generated from the data. They will be called like Cluster-{i}.png
with increasing numbers. When one re-generates the heatmaps, the old files will be deleted to make sure that even if the numbers of clusters has been reduced there are no old files remaining.
-
Open Street Map Tiles
: Cached tiles from the Open Street Map. The substructure is {zoom}/{x}/{y}.png
. Each image has a size of 256\u00d7256 pixels.
-
Strava API
: Everything that is downloaded via the Strava API is stored in this subtree.
Data
: The time series data for each activity as a data frame stored in the Parquet format. Filenames are {activity_id}.parquet
with the activity IDs. The column names are the following: time
, latitude
, longitude
and optionally distance
, altitude
, heartrate
. Metadata
: The activity objects from the stravalib
Python library are stored here as Python pickle objects. The file names are time stamp of the activity start, like start-{timestamp}.pickle
. strava_tokens.json
: Tokens for the Strava API. Contains the access and refresh tokens.
-
Strava Export Cache
: Caching directory for files derived from the Strava export.
Activities
: Same as Strava API/Data
.
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"I like to be outside and I like to record my outdoor activities with a GPX tracker. So far I have kept all my data with Strava and did some analyses there and also with Statshunters. This is already pretty nice.
There are some things which they don't offer, so I want to have a place to implement them. Also I want to support the option of using only local data. This makes this project attractive for people who have a stash of GPX or FIT files but no nice way to analyze their data.
The goal of this project is to import data either from local GPX/FIT files or the Strava API and provide some interesting insights into the data.
You can find the code on GitHub where you can also file issues. If you would like to use this yourself or contribute, feel free to reach out via the contact options from my website. I would especially appreciate improvements to the documentation. If you're familiar with Markdown and GitHub, you can also directly create a pull request.
This project is still in a early prototyping phase, I'm still changing a lot and some fundamental decisions are still not finally settled. So please keep in mind that it is all pretty brittle.
"},{"location":"#get-started","title":"Get started","text":" - Install the software using the stable version. If you want to use the latest development version, use that guide.
- Choose a data source, either activity files or the Strava API. You can only have one source per playground. But you can have as many playgrounds as you like.
"},{"location":"acknowledgments/","title":"Acknowledgments","text":"This project builds on many amazing other projects and would not be possible without them.
"},{"location":"acknowledgments/#bootstrap-css","title":"Bootstrap CSS","text":"Writing CSS is not a trivial task. For many projects I have been using the Bootstrap CSS Framework which provides sensible default values, a 12-column grid system and a lot of components. Using this I didn't have to write any CSS myself and just attach a couple of classes to HTML elements.
"},{"location":"acknowledgments/#coloredlogs","title":"coloredlogs","text":"Log messages in multiple colors are neat. Using the coloredlogs package we can get these super easily.
"},{"location":"acknowledgments/#fitdecode","title":"fitdecode","text":"For reading FIT files I use the fitdecode library which completely handles all the parsing of this file format.
"},{"location":"acknowledgments/#flask","title":"Flask","text":"The webserver is implemented with Flask which provides a really easy way to get started. It also ships with a development webserver which is enough for this project at the moment.
"},{"location":"acknowledgments/#geojson","title":"GeoJSON","text":"Transferring geographic geometry data from the Python code to Leaflet is easiest with using the GeoJSON format. The official standard RFC is a bit hard to read, rather have a look at the Wikipedia article. And there is an online viewer that you can try out.
"},{"location":"acknowledgments/#github","title":"GitHub","text":"For a smooth open source project one needs a place to share the code and collect issues. GitHub provides all of this for free.
"},{"location":"acknowledgments/#gpxpy","title":"gpxpy","text":"For reading GPX files I use the gpxpy library. This allows me to read those files without having to fiddle with the underlying XML format.
"},{"location":"acknowledgments/#leaflet","title":"Leaflet","text":"The interactive maps on the website are powered by Leaflet, a very easy to use JavaScript library for embedding interactive Open Street Map maps. It can also display GeoJSON geometries natively, of which I also make heavy use.
"},{"location":"acknowledgments/#mkdocs","title":"MkDocs","text":"Writing documentation is more fun with a nice tool, therefore I use MkDocs together with Material for MkDocs. This powers this documentation.
"},{"location":"acknowledgments/#open-street-map","title":"Open Street Map","text":"All the maps displayed use tiles from the amazing Open Street Map. This map is created by volunteers, the server hosting is for free. Without these maps this project would be quite boring.
"},{"location":"acknowledgments/#pandas","title":"Pandas","text":"Working with thousands of activities, thousands of tiles and millions of points makes it necessary to have a good library for number crunching structured data. Pandas offers this and gives good performance and many features.
"},{"location":"acknowledgments/#parquet","title":"Parquet","text":"I need to store the intermediate data frames that I generate with Pandas. Storing as JSON has disadvantages because dates are not properly encoded. Also it is a text format and quite verbose. The Parquet format is super fast and memory efficient.
"},{"location":"acknowledgments/#poetry","title":"Poetry","text":"For managing all the Python package dependencies I use Poetry which makes it very easy to have all the Python project housekeeping with one tool.
"},{"location":"acknowledgments/#python","title":"Python","text":"Almost all of the code here is written in Python, a very nice and versatile programming language with a vast ecosystem of packages.
"},{"location":"acknowledgments/#requests","title":"Requests","text":"For doing HTTP requests I use the Requests library. It provides a really easy to use interface for GET and POST requests.
"},{"location":"acknowledgments/#scikit-learn","title":"Scikit-learn","text":"Finding out which cluster is the largest one can either be formed as a graph search problem or as a data science problem. Using the Scikit-learn library I can easily use the DBSCAN algorithm to find the clusters of explorer tiles.
"},{"location":"acknowledgments/#statshunters","title":"Statshunters","text":"The Statshunters page allows to import the activities from Strava and do analysis like explorer tiles, Eddington number and many other things. This has served as inspiration for this project.
"},{"location":"acknowledgments/#strava","title":"Strava","text":"Although I have recorded some of my bike rides, I only really started to record all of them when I started to use Strava. This is a nice platform to track all activities. They also offer a social network feature, which I don't really use. They provide some analyses of the data, but they lack some analyses which I have now implemented in this project.
"},{"location":"acknowledgments/#stravalib","title":"stravalib","text":"Strava has an API, and with stravalib there exists a nice Python wrapper. This makes it much easier to interface with Strava.
"},{"location":"acknowledgments/#strava-local-heatmap","title":"Strava local heatmap","text":"https://github.com/remisalmon/Strava-local-heatmap
"},{"location":"acknowledgments/#tcxreader","title":"tcxreader","text":"https://github.com/alenrajsp/tcxreader
"},{"location":"acknowledgments/#vega-altair","title":"Vega & Altair","text":"https://altair-viz.github.io/index.html https://vega.github.io/vega/
"},{"location":"acknowledgments/#velo-viewer","title":"Velo Viewer","text":""},{"location":"features/activity-view/","title":"Activity View","text":"When you have selected a particular activity, you can view various details about it. This is what the screen looks like, we will go through the different parts in the following.
"},{"location":"features/activity-view/#metadata","title":"Metadata","text":"You have a column with metadata about the activity. The activity kind, whether it is a commute and the equipment are currently only supported via the Strava API, but we can build something to infer that from directories as well.
The calories are broken in the Strava API wrapper library that I use, therefore they don't show even if they are there.
You can also see the ID which is an internal ID. When you use Strava API as a source, it will use the IDs that Strava gives. When you use files from a directory it will be computed from a hash of the path to the activity file.
"},{"location":"features/activity-view/#map-with-track","title":"Map with track","text":"The interactive map shows a line with the activity. The speed is color-coded and peaks at 35 km/h with a yellow color.
"},{"location":"features/activity-view/#distance-speed-and-altitude","title":"Distance, speed and altitude","text":"Then there are a couple of time series plots. One is the distance vs. time. You can see how much distance you covered when and also see plateaus when you went on a break.
From this we can also compute the speed, although that might be pretty noisy:
And more interesting is the distribution of the various speed zones. This gives you an understanding how much time you spent at which speed. The buckets are set in 5 km/h intervals, but we could also change that.
If the time series data has the altitude, which isn't always the case, you can see it in the plot there. Here we can see how I did a tour and continually rode downhill. Except at the end where I had to climb in order to get the explorer tile that I wanted.
"},{"location":"features/activity-view/#heart-rate","title":"Heart rate","text":"The heart rate isn't too helpful, I feel. Still I've created the plot from the given data.
More interesting regarding the heart rate are the zones which one has spent time during this activity.
The definition of the heart rate zones is not standardized. Usually there are five zones and they have the same names. What differs is how their ranges are computed and there is some chaos around that.
All definitions that I found take the maximum heart rate as the upper limit. One can measure this as part of a professional training or just use the 220 minus age prescription which at least for me matches close enough. What they differ on is how they use a lower bound. It seems that Polar or REI basically use 0 as the lower bound. My Garmin system also uses 0 as the lower bound. But as one can see in this blog, one can also use the resting heart rate as the lower bound.
Based on the maximum and resting heart rate we will then compute the heart rate zones using certain percentages of effort. We can compute the heart rate as the following:
rate = effort \u00d7 (maximum \u2013 minimum) + minimum
The zones then take the following efforts:
Zone Effort Training 1 50 to 60 % Warmup/Recovery 2 60 to 70 % Base Fitness 3 70 to 80 % Aerobic Endurance 4 80 to 90 % Anerobic Capacity 5 90 to 100 % Speed Training You can decide how you want to do work with that. If you want to have the same definitions that say Garmin uses, you need to just enter your birth year and we can compute the rest. If you want to use a lower bound, you need to specify that.
For this create a configuration file at Playground/config.toml
and enter a stanza in there, like this:
[heart]\nbirthyear = 19..\n
If you know your maximum heart rate, you can also write maximum = 187
. If you want to use the lower bound, write resting = 48
as well. So it might look like this:
[heart]\nmaximum = 187\nresting = 48\n
You can also combine birthyear
and resting
and leave out maximum
.
If you are not happy with this prescription, please let me know.
"},{"location":"features/calendar/","title":"Calendar","text":"In order to access all the activities, there is a calendar view. It shows the years in rows and the months in columns. Each cell is a particular month and indicates the total distance traveled.
When you click on any of the months, you will see a calendar for a given month. Here is one of my months which doesn't show too personal data:
Clicking on an activity will lead you to the activity detail view.
"},{"location":"features/eddington/","title":"Eddington Number","text":"The astronomer Sir Arthur Eddington like to go on longer bike rides. Apparently he did a lot of rides and had 84 days where he rode at least 84 miles. The Eddington number for cycling was coined from this.
If you have an Eddington number E, it means that you have had E days with at least E kilometer distance. At the time of writing my number is 62, which means that I rode at least 62 km on 62 separate days. If I want to extend it to 63, I would need to have at least 63 km on 63 separate days. My bike rides that are just 62 km long will not count any more, making this challenge really hard.
In the following plot you can see in blue the number of days that exceed the given distance. You can see that I have a 998 days that exceed 1 km, that's pretty easy when one just records enough data over many years. But then there are only 259 days where I exceed 20 km as this is beyond the distance that I have when I only go for a walk or run some simple errands.
The red curve indicates how many rides one needs to get the Eddington number. As it is a semi-log plot, the straight line is curved like a log-curve.
You can see this cliff at around 80 km. That is the distance that I ride to work and back. I have many days with around 80 km, but longer rides are only on occasional bike trips. Therefore I think I will eventually make it to an Eddington number of 80, but beyond that will be super difficult.
This is a life-long challenge, so who knows what happens in the future.
"},{"location":"features/eddington/#length-unit","title":"Length unit","text":"The definition of the Eddington number depends on the length unit that one has. Eddington as a British person used the English mile as a base unit. Therefore his number of 84 is actually harder to achieve than a 84 based on kilometers because he needed to exceed 135 km (84 mi) on each ride.
Therefore using a different length unit as a base changes the meaning. The kilometer is easier, the mile (1609.344 m) is harder. One could also use nautical miles (1852 m). And while we are at arbitrary unit systems, we could also use furlongs (201.168 m).
"},{"location":"features/equipment/","title":"Equipment Overview","text":"When activities are tagged with the used equipment, one can tally the total distance traveled with each thing. This given as a table with the recently used equipment at the top:
And for each thing there is also a graph which shows how the distance accumulates over time:
Here one can see how with my old bike I only recorded the occasional bike trip and not nearly the 4000 km/a which I was doing. And then it got stolen in 2019, so I bought a new bike.
This metadata is downloaded via the Strava API, for the directory source it is not yet supported. I thought about using directories like Activities/{Activity Kind}/{Equipment Name}/{Activity Name}.gpx
. to indicate the kind and equipment. If you're interested in implementing this, let me know.
"},{"location":"features/explorer-tiles/","title":"Explorer Tiles","text":"Maps accessible via the web browser are usually served as little image tiles. The Open Street Map uses the Web Mercator coordinate system to map from latitude and longitude to pixels on the map.
Each tile is 256\u00d7256 pixels in size. The zoom levels zoom in by a factor of two. Therefore all the tiles are organized in a quad tree. As you zoom in, each tile gets split into four tiles which can then show more detail. The following prescription maps from latitude and longitude (given in degrees) to tile indices:
def compute_tile(lat: float, lon: float, zoom: int = 14) -> tuple[int, int]:\n x = np.radians(lon)\n y = np.arcsinh(np.tan(np.radians(lat)))\n x = (1 + x / np.pi) / 2\n y = (1 - y / np.pi) / 2\n n = 2**zoom\n return int(x * n), int(y * n)\n
At zoom level 14 the tiles have a side length of roughly 1.5 km in Germany. These tiles are used as the basis for explorer tiles. The basic idea is that every tile where you have at least one point in an activity is considered an explored tile.
From your activities the program will extract all the tiles that you have visited. And then it does a few things with those. One main thing is that it will display these on an interactive map. When we zoom into one area where I've been on vacation in 2023, you can see the explored tiles there:
The filled tiles are explored, I have been there. The colored tiles are cluster tiles, that means that all their four neighbor tiles are also explored.
You can see here how I have explored a region and ensured that it is mostly contiguous.
There is another vacation from 2013 where I wasn't aware of the cluster tiles. I just did some bike trips and didn't look out for the tiles. There the tiles look like this:
You see all these gaps in there. Also there are three different clusters which are not connected. Each unique cluster is assigned a different color such that one can see where there are gaps between the cluster tiles. And filling the gaps is what the explorer tiles are about: This OCD (obsessive compulsive disorder) like craving to fill in the gaps.
Let's take a look at my main cluster of explorer tiles. Here I have explored much more than in the areas where I was on vacation.
You can see an additional feature, the blue square. This is the one largest square which can be fit into all explored tiles. In this picture it has size 21\u00b2. The idea of the square is to have a really tough challenge. Not only does one need to explore increasingly many tiles to expand the square by one unit, there must not be any gaps.
As you can see in this picture, there is a tile missing right at the top edge. I will never be able to get that because that is an off-limits area of the German air force at the airport. So I can expand my square to the south only.
You can click on each tile and get some information about that particular tile. You can see when you first explored that and with which activity. Also it shows the last activity there as well as the number of activities. If it is a local cluster, it will also show the cluster size.
There is also the option to color the tiles by first or last visit. Use one of the buttons above the map:
Then the map will show the first visit:
Or how recent your last visit is:
This uses Matplotlib's Plasma scale (see below) to color the age of a tile. Very new tiles will get a yellow color, a year old tiles a reddish color and tiles two years old or older a colder blue. This is the scale:
You can switch this with the buttons above the map.
"},{"location":"features/explorer-tiles/#squadratinhos","title":"Squadratinhos","text":"The explorer tiles at zoom level 14 are best suited for cycling and to discover the area around the city. There is a derived definition, the squadratinhos which are defined at zoom level 17 and therefore a factor 8 smaller in each direction. Each explorer tile is therefore divided into 256 squadratinhos.
These are better suited for walking and making sure that you really explored every little place in your neighborhood. Since they are so small, there are many properties which one cannot go onto, like industrial sites, airports or just a wide river.
For my home city it looks like this:
You can see how the squadratinhos are much smaller than the explorer tiles and how they lend themselves to more local exploring.
"},{"location":"features/explorer-tiles/#history","title":"History","text":"The map only shows the current state of your explorer tiles. In order to get a sense of how many new tiles you have discovered in the past, there are also plots that show you how you have extended the total number of squares, the size of your largest cluster and the size of your largest square over time:
"},{"location":"features/explorer-tiles/#missing-tile-files","title":"Missing tile files","text":"Looking at these maps you can see the gaps. And if you feel challenged to fill those, you might want to plan a \u201ctactical bike ride\u201d to explore those. Let us take another look at my tile history in Sint Annaland:
You can see those gaps in the clusters. To make it easier to explore tiles while on the go, we can export a file with the missing tiles. Pan and zoom the map to an area which you want to export. Below the map you will find two links:
Download missing tiles in visible area as GeoJSON or GPX.
This export is available as GeoJSON or GPX such that you can open it with other applications. For instance with GPX See on Linux it looks like this when opening the GeoJSON file:
I have not tested this software, but supposedly Offline Maps is able to display GeoJSON on Android.
On Android I use the OsmAnd app to display tracks and also try to visualize the missing tiles. Unfortunately GeoJSON is not supported, therefore one has to play some tricks. The missing tiles are also exported as a GPX file with a track for each missing tile. This looks strange, but it is a bit helpful with OsmAnd. This is how the file looks like in GPXSee:
And on OsmAnd such files look like this:
Unfortunately OsmAnd becomes a bit sluggish with such a huge track imported, so make sure to only export it from rather small regions.
"},{"location":"features/heatmaps/","title":"Heatmaps","text":"A heatmap shows where you have been more often than other places. There is an interactive and zoomable heatmap that looks like this:
Here you can see where I mostly travel between Bonn and Cologne.
The heatmap computation is based on the Strava local heatmap code. For the interactive map I needed to adjust it a lot. It might be possible that heatmaps don't look very nice if you have a different recording interval from the one that I use.
"},{"location":"features/heatmaps/#heatmap-images","title":"Heatmap images","text":"There is a second mode to generate heatmap images. Instead of using the serve
command, use the heatmaps
command. That will generate heatmap images with carefully adjusted colors using the original logic of Strava local heatmap.
We don't generate a single heatmap for all your activities as this will not look great as soon as you have done an activity away from home. Rather we use a clustering algorithm to find all disjoint geographical clusters in your activities and generate one heatmap per cluster.
For instance the heatmap generated from all my activities in the Randstad in the Netherlands:
And the activities around Nieuwvliet-Bad look like this:
These images are put into the Heatmaps
directory in your playground.
"},{"location":"features/overview/","title":"Overview page","text":"When you start the webserver, you will see the overview page. It looks like the following and shows the activities you've done in the past 30 days:
Then below that you see the latest 15 activities with their tracks on interactive maps.
Each card contains the name, activity type, distance and duration. The non-commute activities are highlighted with a blue border, the commutes just have a gray border.
Click on any of the names and you will see the details of that activity.
"},{"location":"getting-started/config-file/","title":"Configuration file","text":"The project tries to adhere to the convention over configuration mantra and therefore minimizes the amount of configuration necessary. There are still a few data points which one might need to fill out.
This page summarizes all the configuration file elements and links to the features that use them.
The configuration file is in the TOML format and contains multiple tables with options. We will go through them here one-by-one.
"},{"location":"getting-started/config-file/#strava-api","title":"Strava API","text":"If you want to use the Strava API, see the Strava API page first. There you can read how to find the values; the configuration snippet is this:
[strava]\nclient_id = \"\u2026\"\nclient_secret = \"\u2026\"\ncode = \"\u2026\"\n
"},{"location":"getting-started/config-file/#heart-rate","title":"Heart rate","text":"In order to use the heart rate zone feature, you will need to somehow specify your maximum and minimum heart rate.
In order to specify the maximum heart rate, either specify your birthyear
and it will estimate it from that. Or specify the maximum
heart rate instead. For the minimum you can optionally specify the resting
heart rate. If you leave that empty, the minimum will be taken as 0, which is how Garmin also computes the zones.
This is an example configuration:
[heart]\n# Specify either `birthyear` or `maximum`:\nbirthyear = 19..\n## maximum = 187\n\n# Optionally specify `resting`:\n## resting = 48\n
"},{"location":"getting-started/installing-git-on-linux/","title":"Installing Git Version On Linux","text":"As this project is still in development, you might want to have a peek into the development version. This is more advanced than using the stable versions, but not impossibly hard.
First you need to clone the git repository from GitHub using the following command:
git clone https://github.com/martin-ueding/geo-activity-playground.git\n
That will create a directory geo-activity-playground
in your current working directory.
Then change into that directory:
cd geo-activity-playground\n
Next we will use Poetry to install the dependencies of the project. First you need to make sure that you have Poetry available. On Ubuntu/Debian run sudo apt install python3-poetry
, on Fedora/RedHat run sudo dnf install poetry
to install it.
Then we can create the virtual environment:
poetry install\n
And next we can run the program:
poetry run geo-activity-playground --basedir path/to/your/playground --help\n
Replace the --help
with the subcommands described in the help message or the other parts described this documentation.
You will need the --basedir
option because you run the program from the source directory and not from your playground directory. If you install the stable version via PIP as described in the other page, you will not need this option.
"},{"location":"getting-started/installing-git-on-linux/#updating-to-the-latest-version","title":"Updating to the latest version","text":"Over time I will add more commits to the source control system. In order to update your clone to the latest version, execute the following:
git pull\n
This will download the missing changesets and apply them to your downloaded version. After that is done, you need to update your virtual environment with this:
poetry install\n
And then you can continue using it as before.
"},{"location":"getting-started/installing-stable-on-linux/","title":"Installing Stable On Linux","text":"In this how-to guide I will show you how you can install the latest stable version of this project on Linux.
Using PIP, you can install the latest version using this command:
pip install --user geo-activity-playground\n
If you get an error about the command pip
not found, you will need to install that first. On Ubuntu or Debian use sudo apt install python3-pip
, on Fedora or RedHat use sudo dnf install python3-pip
. After you have installed PIP, repeat the above command.
"},{"location":"getting-started/installing-stable-on-linux/#ensure-that-the-path-is-correct","title":"Ensure that the PATH is correct","text":"Next you can try to start the program by just entering the following into the terminal:
geo-activity-playground --help\n
If you get a help message, everything is fine. If you get an error about command not found, we need to adjust your PATH. Execute the following in your command line:
xdg-open ~/.profile\n
This brings up an editor with your shell profile. Add a line containing the following at the end of the file:
PATH=$PATH:$HOME/.local/bin\n
This adds the path to your shell environment. After you have done it, close your terminal window and open a new one. Try the first command in this section again, you should see the help message now.
"},{"location":"getting-started/installing-stable-on-linux/#upgrading-to-the-latest-version","title":"Upgrading to the latest version","text":"At some later point you likely want to upgrade to the latest version. For this use this command:
pip install --user --upgrade geo-activity-playground\n
"},{"location":"getting-started/starting-the-webserver/","title":"Starting The Webserver","text":"Before you start here, you should have done these things:
- You have installed the program either from a stable version or from git.
- You have set up a playground with either activity files or the Strava API.
Now we can start the webserver which provides most of the features. This is done with the serve
command. So depending on how you have installed it, the commands could look like these:
geo-activity-playground serve
if you are in the playground directory and have installed a stable version. poetry run geo-activity-playground --basedir ~/Dokumente/Karten/Playground serve
if you have it from the git checkout and want to use local files in your directory as a data source.
The webserver will start up and give you a bit of output like this:
2023-11-19 17:59:23 geo_activity_playground.importers.strava_api INFO Loading metadata file \u2026\n2023-11-19 17:59:23 stravalib.protocol.ApiV3 INFO GET 'https://www.strava.com/api/v3/athlete/activities' with params {'before': None, 'after': 1700392964, 'page': 1, 'per_page': 200}\n2023-11-19 17:59:23 geo_activity_playground.importers.strava_api INFO Checking for missing time series data \u2026\n * Serving Flask app 'geo_activity_playground.webui.app'\n * Debug mode: off\n2023-11-19 17:59:23 werkzeug INFO WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.\n * Running on http://127.0.0.1:5000\n2023-11-19 17:59:23 werkzeug INFO Press CTRL+C to quit\n
The warning about the development server is fine. We are using this only to play around, not to power a web service for other users.
Open http://127.0.0.1:5000 to open the website in your browser. There might be some more messages about downloading and parsing data. The first startup will take quite some time. When it is done you will see something like this:
Click around and explore the various features.
"},{"location":"getting-started/using-activity-files/","title":"Using Activity Files","text":"Outdoor activities are usually recorded as GPX or FIT files. Some apps like OsmAnd give you these files.
Create a directory somewhere, this will be your playground. I have mine in ~/Dokumente/Karten/Playground
, but you can put yours wherever you would like.
Inside Playground
, create another directory Activities
where your activity files will go. The program will not modify files in that directory and treat them as read-only.
"},{"location":"getting-started/using-activity-files/#directory-structure","title":"Directory structure","text":"Inside the Activities
you can dump all your files in a flat fashion. If you want to add some more metadata, use the following directory layout.
The first directory level will indicate the type of the activity. You can pick whatever make sense for you, classic options are ride, run, walk, hike. Then the second level will indicate your equipment. You can use terms like \u201crental bike\u201d, the brand and make of your shoes or whatever you find sensible. Specifying the equipment allows to track the total distance traveled with a given equipment.
At any level you can have a special directory Commute
. All activities inside of that will be marked as commutes and not highlighted as much as non-commute activities. The idea is that you can find your cool activities along potentially many commutes.
Other directory names on the third level will just be ignored, you can use those to organize your activities further in some sense.
Let us take the following directory/file structure as an example:
Activities\n\u251c\u2500\u2500 Commute\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 From the Beach.gpx\n\u251c\u2500\u2500 Ride\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 Rental Bike\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 Beach Rides\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 Breskens.gpx\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 Zwin.gpx\n\u251c\u2500\u2500 To Piazza.gpx\n\u2514\u2500\u2500 Walk\n \u251c\u2500\u2500 Commute\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 To the Beach.gpx\n \u251c\u2500\u2500 New Balance Fresh Foam 860v11\n \u2502\u00a0\u00a0 \u251c\u2500\u2500 Beach Walk.gpx\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 Commute\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 From Piazza.gpx\n \u2514\u2500\u2500 Nieuvfliet.gpx\n
You can see that I have one file on the top level (To Piazza.gpx
), a file on the first level (Walk/Nieuvfliet.gpx
), some on the second level (Ride/Rental Bike/Zwin.gpx
), one commute (Walk/New Balance Fresh Foam 860v11/Commute/From Piazza.gpx
) and one with a group directory which isn't commuting (Ride/Rental Bike/Dunes in Sluis/Breskens.gpx
). From this the program will extract the following metadata:
Name Type Equipment Commute From the Beach None None Yes Breskens Ride Rental Bike No Zwin Ride Rental Bike No To Piazza None None No To the Beach Walk None Yes Beach Walk Walk New Balance Fresh Foam 860v11 No From Piazza Walk New Balance Fresh Foam 860v11 Yes Nieuwvliet Walk None No The file name of your activity will become the name of the activity.
"},{"location":"getting-started/using-activity-files/#supported-file-formats","title":"Supported file formats","text":"At the moment the following file formats are supported:
- FIT
- GPX
- TCX
"},{"location":"getting-started/using-activity-files/#next-steps","title":"Next steps","text":"Once you have your files put into the directory, you're all set and can proceed with the next steps.
"},{"location":"getting-started/using-strava-api/","title":"Using Strava API","text":"You might have all your data on the Strava service and would like to use this for additional analytics without moving your data. That is fine.
In order to use the Strava API, one needs to create an app. If my explanation doesn't suit you, have a look at this how-to guide as well.
Navigate to the API settings page and create an app. It only needs to have read permissions.
After you are done with that, you can see your App here:
There is a client ID that we are going to need for the next step. In general our app could be used by all sorts of people who can then access their data only. We want to access our own data, but we still need to authorize our app to use our data. In order to get this token, we need to visit the following URL, with {client_id}
replaced by your ID.
https://www.strava.com/oauth/authorize?client_id={client_id}&redirect_uri=http://localhost&response_type=code&scope=activity:read_all\n
This will prompt an OAuth2 request where you have to grant permissions to your app. After that you will be redirected to localhost and see a \u201cpage not found\u201d error. That is all okay!
Take a look at the URL. It look like this:
http://localhost/?state=&code={code}&scope=read,activity:write,activity:read_all\n
From the URL you can read off the code. Now you have all the three data points that you need. Create the file Playground/config.toml
with a text editor and fill in this template:
[strava]\nclient_id = \"\u2026\"\nclient_secret = \"\u2026\"\ncode = \"\u2026\"\n
Add the client ID and the client secret from your Strava app. Add the code that we have read off this localhost URL.
Then you are all set to download data from the Strava API. When you start one of the commands, it will automatically start to download.
"},{"location":"getting-started/using-strava-api/#rate-limiting","title":"Rate limiting","text":"When you first start this program and use the Strava API as a data source, it will download the metadata for all your activities. Then it will start to download all the time series data for each activity. Strava has a rate limiting, so after the first 200 activities it will crash and you will have to wait for 15 minutes until you can try again and it will download the next batch.
"},{"location":"reference/changelog/","title":"Changelog","text":"This is the log of high-level changes that I have done in the various versions.
"},{"location":"reference/changelog/#version-0","title":"Version 0","text":"This is the pre-release series. Things haven't settled yet, so each minor version might introduce breaking changes.
"},{"location":"reference/changelog/#version-013","title":"Version 0.13","text":" - Revamp heatmap, use interpolated lines to provide a good experience even at high zoom levels.
- This also fixes the gaps that were present before. GH-34
- Add cache migration functionality.
- Make sure that cache directory is created beforehand. GH-55
- Split tracks into segments based on gaps of 30 seconds in the time data. That helps with interpolation across long distances when one has paused the recording. GH-47
- Fix introduced bug. GH-56
- Add cache to heatmap such that it doesn't need to render all activities and only add new activities as needed.
- Add a footer. GH-49
- Only export missing tiles in the active viewport. GH-53
- Add missing dependency to SciKit Learn again; I was too eager to remove that. GH-59
"},{"location":"reference/changelog/#version-012","title":"Version 0.12","text":" - Change coloring of clusters, have a color per cluster. Also mark the square just as an overlay.
- Fix bug with explorer tile page when the maximum cluster or square is just 1. GH-51
- Speed up the computation of the latest tiles.
"},{"location":"reference/changelog/#version-011","title":"Version 0.11","text":" - Add last activity in tile to the tooltip. GH-35
- Add explorer coloring mode by last activity. GH-45
- Actually implement
Activity/{Kind}/{Equipment}/{Name}.{Format}
directory structure. - Document configuration file.
- Interpolate tracks to find more explorer tiles. GH-27
- Fix bug that occurs when activities have no distance information.
- Show time evolution of the number of explorer tiles, the largest cluster and the square size. GH-33
- Center map view on biggest explorer cluster.
- Show speed distribution. GH-42
"},{"location":"reference/changelog/#version-010","title":"Version 0.10","text":" - Use a grayscale map for the explorer tile maps. GH-38
- Explicitly write \u201c0 km\u201d in calendar cells where there are no activities. GH-39, GH-40
"},{"location":"reference/changelog/#version-09","title":"Version 0.9","text":" - Certain exceptions are not skipped when parsing files. This way one can gather all errors at the end. GH-29
- Support TCX files. GH-8
- Fix equipment view when using the directory source. GH-25
- Fix links from the explorer tiles to the first activity that explored them. GH-30
- Fix how the API response from Strava is handled during the initial token exchange. GH-37
"},{"location":"reference/changelog/#version-08","title":"Version 0.8","text":""},{"location":"reference/changelog/#version-083","title":"Version 0.8.3","text":" - Only compute the explorer tile cluster size if there are cluster tiles. Otherwise the DBSCAN algorithm doesn't work anyway. GH-24
- Remove allocation of huge array. GH-23
"},{"location":"reference/changelog/#version-082","title":"Version 0.8.2","text":" - Some FIT files apparently have entries with explicit latitude/longitude values, but those are null. I've added a check which skips those points.
"},{"location":"reference/changelog/#version-081","title":"Version 0.8.1","text":" - Fix reading of FIT files from Wahoo hardware by reading them in binary mode. GH-20.
- Fix divide-by-zero error in speed calculation. GH-21
"},{"location":"reference/changelog/#version-080","title":"Version 0.8.0","text":" - Make heart rate zone computation a bit more flexibly by offering a lower bound for the resting heart rate.
- Open explorer map centered around median tile.
- Compute explorer cluster and square size, print that. GH-2
- Make it compatible with Python versions from 3.9 to 3.11 such that more people can use it. GH-22
"},{"location":"reference/changelog/#version-07","title":"Version 0.7","text":" - Add Squadratinhos, which are explorer tiles at zoom 17 instead of zoom 14.
- Reduce memory footprint for explorer tile computation.
"},{"location":"reference/changelog/#version-06","title":"Version 0.6","text":" - Interactive map for each activity.
- Color explorer tiles in red, green and blue. GH-2
- Directly serve GeoJSON and Vega JSON embedded in the document.
- Automatically detect which source is to be used. GH-16
- Fix the name of the script to be
geo-activity-playground
and not just geo-playground
. GH-11 - Add mini maps to the landing page. GH-9
- Add fullscreen button to the maps. GH-4
- Add favicon. GH-19
- Added some more clever caching to the explorer tiles such that loading the page with explorer tiles comes up in just a few seconds.
- Add a triplet of time series plots (distance, altitude, heart rate) for each activity.
- Show plot for heart rate zones per activity. GH-12
- Handle activities without any location points. GH-10
- Resolve Strava Gear name. GH-18
- Add page for equipment. GH-3
- Add a pop-up with some metadata about the first visit to the explorer tiles. GH-14
- Integrate missing explorer tiles into the web interface. GH-7.
- Color activity line with speed. GH-13
- Add interactive heatmap.
- Add margin to generated heatmaps. GH-1
"},{"location":"reference/changelog/#version-05","title":"Version 0.5","text":" - Add some plots for the Eddington number. GH-3
"},{"location":"reference/changelog/#version-04","title":"Version 0.4","text":""},{"location":"reference/changelog/#version-03","title":"Version 0.3","text":" - Start to build web interface with Flask.
- Remove tqdm progress bars and use colorful logging instead.
- Add interactive explorer tile map.
"},{"location":"reference/changelog/#version-02","title":"Version 0.2","text":" - Unity command line entrypoint.
- Crop heatmaps to fit.
- Export missing tiles as GeoJSON.
- Add Strava API.
- Add directory source.
"},{"location":"reference/changelog/#version-01","title":"Version 0.1","text":""},{"location":"reference/changelog/#version-013_1","title":"Version 0.1.3","text":" - Generate some heatmap images.
- Generate an explorer tile video.
"},{"location":"reference/directory-layout/","title":"Directory Layout","text":"There are a bunch of files that need to be given to this set of scripts, are used as intermediate files or generated as output. In order to make it clear where everything is, the following document lists all the paths which are relevant to the script.
Everything is relative to a base directory which can be passed with the --basedir
option to the scripts.
"},{"location":"reference/directory-layout/#input","title":"Input","text":"The user has to put data into the following directories in order for it to be picked up.
"},{"location":"reference/directory-layout/#output-and-cache","title":"Output and cache","text":"The following directories serve as a cache. One can inspect this but doesn't need to work with that directly.
-
Explorer
: Things related to the explorer tiles.
Per Activity
: A data frame with the tiles that have been visited within each activity. Each file is named with the activity ID like 2520340514.parquet
. The columns are time
, tile_x
, tile_y
. first_time_per_tile.parquet
: A data frame with the first visit datetime for each explorer tile. Columns time
, tile_x
, tile_y
. missing_tiles.geojson
: A GeoJSON file with square polygons for all missing tiles at the boundary of explored tiles. missing_tiles.gpx
: The same, just expressed as square tracks in the GPX format. explored.geojson
: A GeoJSON file with square polygons for all explored tiles. explored.gpx
: The same, just expressed as square tracks in the GPX format.
-
Heatmaps
: Will contain heatmap images generated from the data. They will be called like Cluster-{i}.png
with increasing numbers. When one re-generates the heatmaps, the old files will be deleted to make sure that even if the numbers of clusters has been reduced there are no old files remaining.
-
Open Street Map Tiles
: Cached tiles from the Open Street Map. The substructure is {zoom}/{x}/{y}.png
. Each image has a size of 256\u00d7256 pixels.
-
Strava API
: Everything that is downloaded via the Strava API is stored in this subtree.
Data
: The time series data for each activity as a data frame stored in the Parquet format. Filenames are {activity_id}.parquet
with the activity IDs. The column names are the following: time
, latitude
, longitude
and optionally distance
, altitude
, heartrate
. Metadata
: The activity objects from the stravalib
Python library are stored here as Python pickle objects. The file names are time stamp of the activity start, like start-{timestamp}.pickle
. strava_tokens.json
: Tokens for the Strava API. Contains the access and refresh tokens.
-
Strava Export Cache
: Caching directory for files derived from the Strava export.
Activities
: Same as Strava API/Data
.
"}]}
\ No newline at end of file
diff --git a/sitemap.xml b/sitemap.xml
index 55b7303f..eedea1df 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -2,87 +2,87 @@
https://martin-ueding.github.io/geo-activity-playground/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/acknowledgments/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/features/activity-view/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/features/calendar/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/features/eddington/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/features/equipment/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/features/explorer-tiles/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/features/heatmaps/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/features/overview/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/getting-started/config-file/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/getting-started/installing-git-on-linux/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/getting-started/installing-stable-on-linux/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/getting-started/starting-the-webserver/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/getting-started/using-activity-files/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/getting-started/using-strava-api/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/reference/changelog/
- 2023-12-07
+ 2023-12-10
daily
https://martin-ueding.github.io/geo-activity-playground/reference/directory-layout/
- 2023-12-07
+ 2023-12-10
daily
\ No newline at end of file
diff --git a/sitemap.xml.gz b/sitemap.xml.gz
index 1a491207..c48378bc 100644
Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ