In how many German districts did I cycle?
In the previous post I described how I build the YAML file with all German districts. Now I wanted to use this data for something else. Wandrer.earth already tracks which streets I cycled, but not which district (Landkreis) they are in. So: a new score -- how many of the 401 districts and how many of the 16 states (Bundesländer) did I cycle in?
The input is my Wahoo export: 3288 FIT files, from 2019 to today.
The tours from before the Wahoo ELEMNT BOLTs, so before February 2019, I checked in VeloViewer.
Almost all of them were in Baden-Württemberg, so they would not change the statistics much.
I did longer tours before that too, but without Strava and a bike computer I never recorded them.
Reading the Wahoo FIT files is the easy part, fitdecode does that, and with StandardUnitsDataProcessor the positions come out as degrees instead of semicircles.
The district file I used for the kfz.cress.space page has an osm_id per district -- but that is a reference, not a shape.
I first started looking for a ready-made GeoJSON of German district boundaries, and then realized I don't need one:
the ids I already have can fetch exactly the 401 relations from my selfhosted Overpass in a single query.
What Overpass returns for out geom is not a polygon, it is the relation's member ways with their coordinates, in arbitrary order and direction.
Shapely turns that into polygons in two steps: linemerge glues the ways into closed rings, polygonize makes areas out of them.
The member roles matter here.
Ways with role inner are holes, and for district boundaries these holes are mostly the enclaved kreisfreie Städte, e.g. Ansbach the city sits inside Landkreis Ansbach.
In total 88 of the 401 districts have at least one such hole.
Without subtracting them a ride through the city would count for the city and the surrounding Landkreis:
outer = close(rings["outer"]) inner = close(rings["inner"]) if inner is not None and not inner.is_empty: outer = outer.difference(inner)
For the lookup itself all polygons go into a shapely.STRtree, and the whole track of one ride is queried at once:
_, found = tree.query(shapely.points(lons, lats), predicate="intersects") hits = {keys[i] for i in found}
That vectorized query is fast enough (~0.1s for a ride with 2000 points against all 401 districts) and it needs no shapely.prepare:
I added a prepare call assuming it would speed things up, benchmarked it, and it changed exactly nothing -- STRtree.query with a predicate prepares the tree geometries itself.
Before the lookup I round every position to 4 decimals (~11m) and put them in a set, which keeps 58% of the recorded points. Less than I expected, because rounding is a grid and not a clustering: two points a few meters apart still end up in different cells when they straddle a boundary.
The first full run took 11 minutes. The results are cached, because a tour that is already calculated will never change. One entry per FIT file, keyed by filename with the file size as a change check:
"2019-05-19-061304-ELEMNT BOLT 8284-98-0.fit": { "districts": ["08111", "08115", "08118"], "date": "2019-05-19", "size": 570271 }
The districts are the Kreisschlüssel again, so Stuttgart, Böblingen and Ludwigsburg for this tour.
And the answer:
3288 rides districts: 191 / 401 (47.6%) federal states: 14 / 16 covered: 143,208 km² (40.1% of Germany) per federal state Baden-Württemberg 41 / 44 █████████ Bayern 32 / 96 ███ Berlin 1 / 1 ██████████ Brandenburg 9 / 18 █████ Bremen 0 / 2 Hamburg 1 / 1 ██████████ Hessen 21 / 26 ████████ Mecklenburg-Vorpommern 2 / 8 ██ Niedersachsen 17 / 45 ████ Nordrhein-Westfalen 28 / 53 █████ Rheinland-Pfalz 24 / 36 ███████ Saarland 2 / 6 ███ Sachsen 1 / 13 █ Sachsen-Anhalt 7 / 14 █████ Schleswig-Holstein 0 / 15 Thüringen 5 / 23 ██ new districts per year 2019 + 29 → 29 2020 + 3 → 32 2021 + 13 → 45 2022 + 43 → 88 2023 + 27 → 115 2024 + 10 → 125 2025 + 30 → 155 2026 + 36 → 191
Just under half of the districts, and 14 of 16 states. Missing completely are Bremen and Schleswig-Holstein. I was in Bremen, Kiel and Lübeck in the past, but not cycling there.
The per-year column is basically a travel log. 2020 with +3 is the lockdown year of riding the same routes around Stuttgart, and since 2022 I cycle a lot of distance with my Brompton, mainly along rivers where a single tour crosses a handful of districts.
In the next years I will monitor this and see if I get even more coverage of districts, and obviously "catch" the last 2 states.
