An app to generate flat running routes in San Francisco.
A blog post on the development process can be found here.
I started my pathfinding algorithm by implementing A* in its classic incarnation, so that it could simply find a path from A to B. In A*, a node that may be along a path is scored using
f(n) = g(n) + h(n)
where g(n)
is the cost to arrive at a node from the start, and h(n)
is the
estimated heuristic cost from a node to the end.
Once I understood that, I added elevation data so that elevation changes were proportionally more costly, so that whereas before, for example, a route from Van Ness and Lombard to Green Street at the Embarcadero was going over Russian Hill and Telegraph Hill, now it was avoiding the hills and routing north around them (Bay Street). Essentially at this point my A* score function was
f(n) = g(n) + h(n) + elevation_weight
My problem then was that A* works to find routes from A to B, but I wanted to find routes from A to A with an X-mile detour.
I realized I could modify A* heavily so that, given no end point, I could invert the geodesic distance from the start to a given node (and give it a significant multiplier) so that it was more costly to stay closer to the start, and therefore the pathfinding would just find somewhere in the general direction of "away." This became a key part of my "explore" score function, which was essentially
f(n) = elevation_weight + α * 1/distance_from_start
Then after covering ~40% of the intended distance, the pathfinder should turn around and find its way back. At that point the score function changes to be more like a traditional A* algorithm, with elevation factored in. The one issue I had here was that I wanted to sometimes make nice running loops, not retrace my own steps, so I added another weight into the scoring function that calculated the distance from any node to the nearest node on the outbound path, inverted that, and multiplied it by the distance to the end point (which at this point is the start point). In doing so it makes it costlier to stay near the path already taken until it gets closer to the end, when it gradually gets less expensive to approach and finish the loop. Here, then, the score function looks like
f(n) = elevation_weight + g(n) + h(n) + (h(n) * 1/distance_to_previous_path)
…with lots of α multipliers throughout. I'm still tweaking the coefficients and cost factors here, but the app is generating nice, relatively flat loops at this point.
Having been, in my past life, a UX designer, I wanted to choose a project where, given my time constraints and inability to really do independent user research, I really understood my user. The convenient thing about building this app is that the user I had in mind was, in fact, myself. Some highlights of my design process on this project are as follows:
One challenge I did run into in design was that I kept finding these strange little loops. At first I thought it may be a problem with my algorithm, but then I realized that it was actually Google's DirectionsService API, which sends a user (even in walking directions) to make U-turns around the block if given coordinates slightly off an expected path. At that point I reseeded my database to include a PostgreSQL array of all nodes between each intersection so that in generating a list of coordinates from a list of intersection objects, a route object would also query the database for all nodes between intersections. In this way I was able to draw much nicer polylines without using the DirectionsService API.
Starting screen
Loader
Rendered route
New route generation without page reload
I also plan to continue tweaking the score functions to perfect the wayfinding algorithm.