Skip to content

PhilippeGuyard/cycling_brighton

Repository files navigation

Cycling Brighton

Track progress cycling every bikeable street in Brighton & Hove.

  • Fetch the OpenStreetMap bike network into PostGIS.
  • Map-match recorded GPX rides to the network with HMM + Viterbi (via leuvenmapmatching), globally consistent, avoids parallel-road hopping.
  • Visualise cumulative coverage (done / partial / untouched).
  • Auto-generate ~20 km Chinese-Postman loops over the streets you still haven't ridden.

Current real network: ~13,179 edges / ~988 km of bikeable ways (after excluding trunk roads, bicycle=no segments, and small dead-end impasses).

Inspired by Vinayak Mehta's talk "Running every street in Paris with Python and PostGIS" (running-every-street-talk). This is an independent rewrite for cycling in Brighton and Hove.

Architecture

  • src/cycling_brighton/ (all logic, importable + unit-tested): db (engine/schema/PostGIS), network (osmnx fetch to DB rows), gpx (parse/clean), matching (HMM+Viterbi), coverage (per-edge % + stats), routing (CPP loop generation), viz (folium maps), cli (Makefile-driven commands).
  • notebooks/ (thin orchestration + visual output only).
  • Storage: PostGIS. Matching + geometry in EPSG:27700 (British National Grid, metres); a WGS84 copy is kept for folium.

Setup

1. PostgreSQL + PostGIS

Uses Postgres.app (PostgreSQL 15), which bundles PostGIS 3.3 (no separate PostGIS install needed). It runs on the default port 5432.

-- once, against your server
CREATE DATABASE cycling_brighton;
\c cycling_brighton
CREATE EXTENSION postgis;

Postgres.app binaries live at: /Applications/Postgres.app/Contents/Versions/latest/bin

Port 5432 gotcha: Homebrew postgresql@14 / @16 will fight Postgres.app for 5432. Free it with:

brew services stop postgresql@14 postgresql@16

2. .env (gitignored)

DATABASE_URL=postgresql://<user>@localhost:5432/cycling_brighton

# plan-ride defaults (home start point + target uncovered km per loop)
HOME_LAT=50.8225
HOME_LON=-0.1372
TARGET_UNCOVERED_KM=14

Config keys:

  • DATABASE_URL: PostGIS connection string.
  • HOME_LAT / HOME_LON: default loop start (your home), used by plan-ride when --start is not given.
  • TARGET_UNCOVERED_KM: default amount of uncovered street to include per loop, used by plan-ride when --km is not given (~14 km uncovered rides ~20 km total after CPP deadheading).

3. Python env

uv venv .venv --python 3.11
uv pip install -e ".[dev]"

Usage / workflow

The make targets wrap the CLI (.venv/bin/python -m cycling_brighton.cli) so you never need to open Jupyter for the day-to-day loop. They read .env for the DB and defaults.

Target What it does
make add-ride Import GPX ride(s), map-match, update coverage, record the ride.
make plan-ride Grow an uncovered subgraph near home, solve the CPP, export a loop GPX.
make status Print overall coverage (km done / total, streets done) and ride stats.
make map Render the coverage map to coverage.html and open it.
make test Run the test suite with .env loaded.

Importing rides

Drop one or more ride GPX files into data/gpx/, then:

make add-ride

It imports every *.gpx in data/gpx/, skipping any already imported (rides are deduped by content hash in the rides table, so re-running is safe and prints skip (already imported)). For a one-off file outside that folder:

make add-ride file=/path/to/ride.gpx

After importing, it prints the new overall coverage %.

Planning a ride

make plan-ride

By default plan-ride is interactive: it prompts for start (lat,lon), target uncovered km, and output filename, each seeded with a default from .env (press Enter to accept). Supplying --start, --km, --filename, or --yes skips the matching prompts:

.venv/bin/python -m cycling_brighton.cli plan-ride \
  --start 50.8225,-0.1372 --km 14 --yes --filename loop_14km.gpx

The loop GPX is written to data/routes/ (load it into your bike computer, Komoot, or Strava). If your start point sits in a disconnected pocket of the network, plan-ride prints a note that the ride starts some distance away on the main network.

Notebooks (optional)

The notebooks mirror the CLI for interactive/visual work:

  1. 01_build_network.ipynb: geocode Brighton & Hove, fetch the osmnx network_type='bike' graph, write nodes + edges to PostGIS. Caches the graph to data/network.graphml so you don't re-fetch OSM every session. One-time / refresh.
  2. 02_track_coverage.ipynb: drop a ride GPX into data/gpx/, parse it, map-match, apply matches to the coverage table, then see overall progress and the coverage map (same path as make add-ride + make map).
  3. 03_plan_ride.ipynb: set home + target_km, grow an uncovered subgraph near home, solve the CPP, and export a loop GPX (same as make plan-ride).

Tuning knobs

  • match_track(max_dist=30): max GPS-to-edge candidate distance (m); raise for noisier tracks.
  • match_track(obs_noise=10): GPS emission noise (m) in the HMM.
  • cpp_route(target) via target_km: desired loop length (~20 km). Covering ~13 to 15 km of uncovered street means riding ~20 km total (CPP deadheading).
  • cpp_route(max_odd_nodes=400): safety cap on the O(k squared) odd-node matching; exceeding it raises a clear error asking you to shrink the target/subgraph.

Testing

make test        # runs the suite with .env loaded (includes DB tests)
.venv/bin/pytest # unit tests only; DB integration tests auto-skip

The DB integration tests auto-skip when DATABASE_URL is unset (no Postgres needed in CI; no live OSM fetch in tests).

Data layout

All gitignored (except .gitkeep):

  • data/gpx/: your recorded rides (input).
  • data/routes/: generated loop GPX (output).
  • data/network.graphml: osmnx graph cache.

About

Track progress cycling every bikeable street in Brighton & Hove (OSM + PostGIS, HMM+Viterbi map matching, Chinese-Postman route planning)

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors