GPX Route Generator

Evgeny Arbatov
3 min readJun 18, 2024

--

Tired of running the same routes?

Here is how you can build a GPX file generator. You can upload the GPX file to your watch and discover new places in your neighborhood.

A bonus point is that the solution is entirely self-hosted and you don’t need a Strava subscription to generate routes!

Let’s start with what you need on a high level:

  • Open Source Routing Machine (OSRM) for generating routes
  • Streamlit for rendering simple UI and downloading GPX files
  • Locations you want to include in the routes

First, OSRM. There are two things you need for OSRM to work:

  • PBF file with the map of your city / area. Get one here.
  • OSRM profile to customize route . Use foot.lua for a start.

Create docker-compose.yaml to launch OSRM with Docker:

services:
osrm:
image: osrm/osrm-backend
hostname: osrm
volumes:
- ~/Documents/osm:/data
- ./profiles:/profiles
networks:
- default
ports:
- 6000:5000
restart: unless-stopped
command: >
bash -c "
osrm-extract -p /profiles/foot.lua /data/singapore.osm.pbf &&
osrm-partition /data/singapore.osrm &&
osrm-customize /data/singapore.osrm &&
osrm-routed --algorithm mld /data/singapore.osrm
"

You can start generating route recommendations once OSRM is up and running by picking random coordinates on the map!

Since OSRM prioritizes shortest routes, with default settings you are likely to end up with a route that is valid but you would not normally take:

Let’s make a change in foot.lua profile to prefer ways which have ‘Park Connector’ in them:

function setup()   
park_connector_bonus = 10
walking_speed = 5

....

function handle_running_tags(profile ,way, result, data)
-- Prefer ways with 'Park Connector' in the name
local name = way:get_value_by_key('name')
if name and name:find('Park Connector') then
result.forward_speed = walking_speed * park_connector_bonus
result.backward_speed = walking_speed * park_connector_bonus
end
...
end

function process_way(profile, way, result)
...
local handlers = Sequence {
...
handle_running_tags,
...
}

This is the route we get get with the park connector preference:

We can further optimize this by preferring designated foot paths:

function setup()
designated_foot_bonus = 10

function handle_running_tags(profile ,way, result, data)
...
-- Prefer ways with '<tag k="foot" v="designated"/>' tag
local foot_designated = way:get_value_by_key("foot")
if foot_designated == "designated" then
result.forward_speed = walking_speed * designated_foot_bonus
result.backward_speed = walking_speed * designated_foot_bonus
end
...

The single route works fine. Let’s test few different locations:

The results look reasonable and importantly running friendly.

Let’s scale this by having multiple user-defined destinations. We are going to use Google My Maps to create a list of destinations and export it as KML.

Streamlit will take the KML file and generate routes to the destinations with a given start point:

See the source code https://github.com/evgeniyarbatov/gpx-route-generator

--

--