shadow chasing

23:03 28/03/2025 1944 words
22:51 29/03/2025 (modified)
contents

research question

Where does your shadow take you?

a grassy field with the silhouette of someone astride their bike - taken from the perspective of that person
Fig 0: follow that shadow, over that hill

motivation

It was an idle thought I first had a good while ago. And one I've since had on many other occasions. It invariably comes to mind when cycle touring; audaxing or dun-runing, and I notice my shadow isn't where it was earlier in the ride, despite a ~similar heading.

I realise I've become a gnomon on wheels.

When touring or randonneuring I am typically following a route[1], this requires some - often not much, but nonetheless some - attention. Generally, the destination[2] governs a route. Sometimes, however, the terrain is the lodestar: going around that hill in order to get to that lake; heading over that pass to get into the valley. But, what if, what if the route was a function of wherever you happen to be, whenever you happen to be there? Or what if you don't have a casquette and can't bear the sun being in your eyes? If you want to free yourself from the responsibility of navigating, well, you could just follow your shadow. But, where would it take you?

aims

the widget results

Inputting a date from a date picker, selecting a speed from a slider, and clicking on the map wherever you choose to start from should automatically do the thing. A green marker for start, red for finish and a smooth blue curve joining the two[4].

Adjusting the speed slider means you go further - the path gets longer. And tweaking the lower start/stop slider[5] modifies the start/stop times to be after/before sunrise/sunset, respectively.[6] Around the sumer solstice, in the mid-latitudes the path is generally 'C' shaped. At high latitudes in the summer it's circular, and in the tropics around the equinox it is reduced to an out'n'back.

Modifying the end time to be before sunset simply cuts the line at an earlier point. Starting later, rather pleasingly[7], sends you off somewhere else.

method

Feel free to skip to the end. This section is quite dry. As usual, the content warning applies.


14 functions and an EventListener. These can be roughly grouped into a few categories:

Constructing the workflow for determining and drawing the path was, reasonably, straightforward. Where my inexperience shows is in handling all the inputs, and handling changes to the inputs.

In the first instance, the inputted date and location are used to create an array of UTC times that span midnight to midnight (in the local timezone), at one-minute spacing.

Starting with the clicked coordinate and the first element of the time array, the solar azimuth and altitude are calculated. If the sun is above the horizon (altitude > 0): the direction of the shadow is calculated[8] and the input speed[9] are used to determine the next point[10]. This point is pushed to an array, and the process repeats with this point and the next time step. That is the nuts and bolts of makePath(). Checking the solar elevation at each step in this process allows for the fact that sunset time changes with latitude, so if you happen to be travelling fast, this[11] accounts for that.

The polyline's coordinates are updated with the array of points. The start and end markers are similarly updated (updateMap()), and to getLineLength() the distance between each point is summed.

At the same time as the path is drawn, the start/stop slider is populated with the sunrise/set times. Modifying these forces makePath() to go again. And in addition to checking the sun is above the horizon at each time step, it checks that the time is within the two slider buttons before determining the next point.

The start/stop slider was fiddly. As you can see, it doesn't have any labels, because I couldn't make it so. Under the hood the slider initially goes from 0 to 1440 (the number of minutes in a day), and its limits update whenever the location or date changes. There are quite a few conversion back and forth to account for this.

a view across a valley, with a cyclist on a track, roughly following their shadow
Fig 1: jc trying to follow his shadow on the 2017 torino-nice rally

bonus point

No bonus points today. Earlier, I thought I'd call it job done and not worry about trying to snap the shadow path to the road/path network. I had a quick look at GraphHopper and having to sign-up was reason enough for me to forgo the bonus points[12]. Then I had a quick look for other routing engines and came across OSRM which seemed simple enough to use...

This required writing four more functions (that could quite easily have been two): formatCoords(), because OSRM's API wants coordinates to be longitude-latitude strings, and leaflet polylines are latitude-longitude arrays, this also allows me to only sample n points from the path - for the sake of simplifying the query; makeUrl() to construct the request URL from the coordinates and mode of transport (bike or foot); fetchAsync() which does some magic and returns the result (a geojson); and finally getRoute() for doing all of the above and then assigning the coordinates to the route polyline (shown in cyan) that was created and added to the map on load.

Make a call to getRoute() at the bottom of updateMap(), calculate the route distance and display that - and there we go. I can claim a bonus point. Singular. The routes are, a bit messy, and would probably benefit from being cleaned and duplicate points removed etc... but you get the idea.

Adding an export route/path button would have earned me a few extra points. And a button for choosing bike/foot.

dependenices

conclusion takeaway

This was a fun little project that's been knocking around in my head for a fair while. I implemented a version of this in python a few years ago, and it was slow, and not interactive. I have progressed. That's nice to know.

Will I ever follow one of these routes? I might. If/when I will be sure to report back. If you ever do, please do contact me. And I accept no responsibility for the quality of the route.

misc other thoughts

todo

at some point...maybe

footnotes


  1. (a) plotted in-advance; (b) printed on a route-sheet; (c) made-up on in the moment ↩︎

  2. a far-away train station; a friend's; that fish & chip shop ↩︎

  3. assuming cloud-free skies and nothing too pesky in the way, like all these buildings and mountains[16] ↩︎

  4. and another line, which we'll get to shortly ↩︎

  5. which is a bit fiddly, and tricky to style ↩︎

  6. this needed a bit of work. it used to raise an alert if you slide before sunrise and after sunset, rather than not allowing it in the first instance - now the slider limits are fixed at sunrise and sunset. ↩︎

  7. and entirely predictably ↩︎

  8. which is 180° away from the azimuth (with some modular arithmetic to get it back in the 0-360° range) ↩︎

  9. in km/h divided by 60 to get km/min; getNextPoint() uses angular distance, so this is then divided by the Earth's radius (in km) ↩︎

  10. getNextPoint() is borrowed from Chris Veness' geodesy library, specificailly the rhumbDestinationPoint() which when given a point (lat lon), a distance and a bearing returns a new point.[17] ↩︎

  11. should, i think, i hope ↩︎

  12. turns out, I have already signed up. can't remember when ↩︎

  13. there is, there must be a "better" way ↩︎

  14. knew that already, but it is good to cement that knowledge ↩︎

  15. or the moon? ↩︎

  16. 🎶 slowly they will rise, before our eyes 🎶 ↩︎

  17. this function includes the following charming comment, which I have duly copied into my code: '// check for some daft bugger going past the pole, normalise latitude if so' ↩︎


#maps #dataviz #js