# the why
Not that long ago, I wrote about walking in, through, up, and amongst, the nearby(ish) brown hills. In that piece I expressed the desire to do a bit more.
And I have done so. At the beginning of the month, a friend (of more than half a lifetime[1]) and I went marching. We managed to stay on the cheerful side of the line[2], blessed by blue skies, plentiful food[3], and 5 star peg campsites. It was a few days work and in doing so we bagged some Munros.
Like many other [adjective][4] people, the idea of becoming a compleator appeals[5]. Before moving up here, I (naively) assumed I'd head to the highlands every weekend. But, well, trains are expensive[6], and the highlands aren't that close. And work. And another excuse. And the weather. Nonetheless, I have ticked off a few, and, I have been[7] keeping track.
In addition to providing routes[8], the excellent walk highlands website, lets users keep track of which hills they have/haven't bagged. This is a nice feature, but somewhat incompleat[9]. And so, armed with
- a folder of
.gpx
and.fit
files - the desire to learn a bit more
javascript
and web-mapping[10] withleaflet.js
- a vision
I made a thing.
# a disclaimer
Just so I have said it: Whilst I would like to get 'em all, the most likely outcome is that I don't. I am not blind to that. Maybe making this serves as some sort of motivation. I don't know. It was fun either way.
# aims
- a slippy map showing all Munros, coloured by climbed / not climbed
- ability to show routes taken to those that i have climbed
- view route profiles and info (distance, ascent, moving time...)
- a few graphs / data-vizzy things (e.g. timeseries of cumulative total of summits bagged)
- table of summary statistics (total ascent, average number of summits per day, most summits in a day...)
# stretch aim
- ensure any code written in the pursuit of the above is sufficiently general, such that with two inputs (1. a
geojson
with some points and, 2. a folder with some.gpx
files in), the same (/similar) output map and stats can be generated
skip to the result, i won't be offended.
# method
# input data
# munros
For the summits themselves, I combined two sources. This table from Wikipedia and some data from OpenStreetMap, gained from a very short overpass turbo query: node[munro=yes]; out;
.
The table on wikipedia has all the good stuff, in fact, it has everything I need, like the classification, and the prominence, and a grid reference.
The osgb
(grid-banger) library was used to convert the grid refereneces to lat-long, and this is why the OSM data was needed. The background tiles will be based on OpenStreetMap data. When plotting the converted[11] coordinates from wikipedia on top of an OpenStreetMap tile, the plotted points and the mapped summits were not colocated[12]. On the scale of zero to trivial, this discrepency scores a commendable trifling, and therefore cannot be ignored. A quick sjoin_nearest()
for pairing the summits meant I could use the geometry from OSM, and the attributes from wikipedia. 🤌
# my data
I tend to record my running/cycling/hiking activities, using a handheld eTrex #something, or a Forerunner #somethingelse, and have done so since ~2011, with a few breaks[13]. Before the laptop I'm writing this on got linux'd I was using RubiTrack to store all of these. Many of these logs are on strava, some are on garmin connect, and some are stored locally[14]. [ed. stop rambling] I have .gpx
and .fit
files for the Munro hikes, which I manually picked[15] from my collection[16], but the code I wrote could handle the whole collection - but it doesn't need to.
# processing
gpsbabel
for converting the.fit
to.gpx
.- for each track get: start date, duration, distance, ascent, descent
- take the track geometry ((
Mutli
)LineString
) and combine it with the elevation data to make aLineString Z
- flip to working in a projected coordinate system (UTM)
- create a 100 m buffer around each summit[17]
- spatial join between the tracks and buffers, with
intersection
as thepredicate
.- this allows for one track passing through multiple summits
- the summits that have been climbed, are now associated with a track and its stats. bingo.
- for each climbed summit, buffer by 100 m (again), get bounding box of buffered area, and pass to the
bbox
paramater ofgpd.read_file()
to re-open the corresponding track, and extract only the track points that fall within that area- the timestamps on these points allows for an estimate of time spent at the summit[18]. add that data to the data frame
- export two
.geojson
:- points (geometry: point. properties: name, height, prominence, climbed (true/false), date climbed...)
- includes all munros
- tracks (geometry: linestring z. properties: name, height, prominence, climbed, date climbed...)
- tracks has one row for each summit climbed so the track geometry is repeated if a track passed multiple summits
- points (geometry: point. properties: name, height, prominence, climbed (true/false), date climbed...)
- generate summary statistics
- pandas wrangling is my happy place
- use
dataframe.to_html()
, saving it as anjk
partial, along with a little bit ofcss
.
- use
- pandas wrangling is my happy place
# mapping
leaflet.js
does the heavy lifting, and Heightgraph
sprinkles some magic on top.
There is a gratuitous number of basemaps included - all useful.
The two geojson
s are read in. The munros are added to the map and styled according to whether or not they have been climbed. An onEachFeature
function does a few things:
- adds a popup to each marker for displaying the name, and if applicable, the date climbed and time spent spent at the summit
- finds the corresponding route, displays it, adds it to
Heightgraph
, scales the map the route
# the result
The map and stats are shown below. Additionally, the output lives on its own page.
The way this site gets built means the map and table below will update whenever I add a new gpx track to the bucket of gpx tracks[19]. At the time of writing, I am at 6.03% (17/282).
# stats
The headline here, is that I have probably spent more time producing this, than walking up hills, and I have certainly spent more time making this than I have on top of the summits. Remind me, what's the point? Oh well. I enjoy both.
last updated:16/04/2025
% compleat | 6.03% (17/282) | |
---|---|---|
highest | 1131.4 m | Ben Lui (Beinn Laoigh) |
lowest | 916.3 m | Beinn a' Chleibh |
most prominent | 876.0 m | Ben Lui (Beinn Laoigh) |
least prominent | 90.0 m | Stob Coire Sgriodain |
most west | 4.8356° W | Beinn a' Chleibh |
most east | 3.7297° W | Beinn a' Ghlò - Bràigh Coire Chruinn-bhalgain |
most south | 56.3893° N | Ben Oss |
most north | 56.8772° N | Beinn Dearg |
most in a day | 4 | Ben Oss, Beinn Dubhchraig, Beinn a' Chleibh, Ben Lui (Beinn Laoigh) |
number of tracks | 8 | |
distance | 159.0 km | |
ascent | 12806 m | |
time spent | 2 days 16:30:51 | |
time spent on summits | 0 days 02:59:21 | |
most time spent on summit | 0 days 00:27:59 | Stob Coire Easain |
# the map
# stretching
With a little bit of extra up-front work, I was able to also make this if you prefer the Lakes.
Stretch goal - tick.
# next?
# outside
Unfortunately, this exercise reduced my tally by one. I thought I'd ticked Beinn a' Ghló - Càrn nan Gabhar (1121 m) I would like to be able to have a magic link here that could be clicked and would take you up to the map and show what I'm talking about. But I'll save that for another day.[20].
On reaching a cairn in the cloud, we stopped, before then descending the same way. That cairn was about ~150 m away from, and a few metres below, the true summit[21]. So, I suppose I should go there.
Or anywhere really.
# online
I have a few ideas for additional statistics to display. I also haven't included any figures here. I might.
None of my code handles the case where one summit is visited my multiple tracks. I tried. If I ditch the elevation profile widget on the map, and just dump all the tracks - it works.
right, that's quite enough.
i've just spotted a mistake. if you find it. you win.
# footnotes
tick-tock-tick-tock ↩︎
ordeal being on the other side of the line ↩︎
I was carrying three days' worth of food ↩︎
obsessive, ambitious, naive, arrogant ↩︎
i wonder what the ratio of checklists:compleators is. tangent alarm maybe it's like the sophie the giraffe factoid where the french make more of those in a year than they do children. ↩︎
cars, more so ↩︎
no surprises here ↩︎
and a forum full of lovely hill-nerds and good advice ↩︎
sorry ↩︎
can i call it that? i think so. ↩︎
or reprojected ↩︎
i did make a histogram showing the discrepencies, which I can't be bothered to remake. the difference was never more than 300 m. but often ~50 m (ish). ↩︎
that i'm not remotely upset/annoyed about ↩︎
and i have totally got myself into a bit of a mess, and really want to have a system ↩︎
because, there aren't that many, this was fine ↩︎
goal is to have my collection in some database of some description (DuckDB, PostGIS, Postgresql, SpatialLite? HELP?) ↩︎
an allowance for poor signal ↩︎
at the summit meaning within ~100 m of the summit (horizontal, not vertical distance, in case you were wondering). and yes, i could be a bit more strict here. but whatever. maybe i had my summit snack just out off the shoulder and out of the wind. ↩︎
and run a script ↩︎
I made the link, and it wasn't too fiddly. ↩︎
bugger. On the route description (stage 6), it mentions this. oops. ↩︎