It's about time to make a revision of my old running map - something that I threw together in February with a few scripts and an über-early version of TileMill.
I run with a GPS watch that records heartrate, position, and elevation, and then can send it up to Garmin Connect, which used to be awesome until the original team left. Right now, it's perfectly fine for my purposes, though it severely lacks good export functionality - I'll address that later.
The main lack with the old map was that it represented my heart rate with dots at each GPS trackpoint. That's easy to pull off, since it doesn't require any postprocessing of the data, and the style in TileMill is as easy to make - just scale dots.
Ideally I'd have something that more accurately represented what's happening - I'm not running from circle to circle, but continually running, with smoothly shifting variables of heartrate, speed, etc. Any info-nerd will know the reference - the map of Napoleon's Journey, as drawn by Charles Minard, uses multi-width lines to represent troop numbers. Wikipedia claims that these are flow maps, though the term isn't quite precise or established.
I couldn't find many tools to do what I wanted to do, so I had to build quite a bit. This time around I polished up my Garmin exporter tool, which I'll call disconnect.rb - a Ruby script that uses mechanize to simulate a browser - so that private runs can be downloaded.
So, after downloading runs (never enough - 63 totalling 174 miles), it's time to start thinking about making this crazy kind of map. I'm kind of starting from scratch, so it will require some of the more difficult things in the world - maths.
note: I'm not a real mathematician, I can only pretend to be one with a computer handy and lots of chances to get it right. If you've got corrections or improvements to this technique, note them in the comments!
Okay! So, step one: we have all of the trace points, and can put them on a map. This is the most normalized form of the data that will be - geographic libraries tend to think of geographic features as basic building blocks - combining 10 points with lots of nice data into one line feature will give you only one place to put data - in the line, not in its 'point components'. It's a reasonable assumption, but makes some things difficult.
OGR is the bread-and-butter library for doing anything with vector data, and Shapely gives you nice higher abstractions and a few geographical operations that are super useful. Both have great bindings in Python, so that'll be the language of choice.
So the geometry challenge is to create a variable-width polygonal
line from a series of dots and values? Imagining that the points are connected
by an invisible line, the challenge is to find an angle that's perpendicular
to the path at each point. Luckily, finding the angle between two points is
much easier with the
that not only figures out the angle, but puts it in the right quadrant. Just
watch out that it takes
(y, x) parameters.
from math import atan2 def angle(a, b): return atan2( a.coords - b.coords, a.coords - b.coords)
So now the idea is to project line segments out perpendicular to this vertex,
which is as simple as getting 90° angles on both sides, and then figuring out
the unit vector in those directions. To get the x and y coordinates of a
unit vector in a specific angle, you can just use trusty old
# now project outwards. x = cos(e) y = sin(e)
def average_smooth_n(x, n=5): weight = 0.7 for i in range(n, len(x) - 1): before = x[i - n:i - 1] def get_x(m): return m.coords def get_y(m): return m.coords before_x = sum(map(get_x, before)) / (n - 1) before_y = sum(map(get_y, before)) / (n - 1) x[i].coords = [( x[i].coords * weight + before_x * (1 - weight), x[i].coords * weight + before_y * (1 - weight) )] return x