# research question
Where does your shadow take you?

# 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
- For a given day, start location, and speed, identify the path that would be traced were you follow your shadow[3] from sunrise to sunset
- Modify the path for start times after sunrise, and finish times before sunset
- Learn a thing or two about javascript, and web-mapping
- For bonus points: snap route to road/path network
# 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.
- to view this on its own page: go here
- modifed 22:51 29/03/2025
# 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:
- date wrangling:
_today() msToTime() getDate() getTimes() riseSet()
- handling changes to inputs:
onChange() onMapClick() adjustTimeSelector() slider() getRideTimes()
and theEventListener
- plotting path on map:
getNextPoint() makePath() updateMap() getLineLength()
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 push
ed 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.

# 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
- leaflet.js (inc. fullscreen plugin) for the mapping
- OSRM for routing
- suncalc for getting solar azimuths and sunrise/set times
- spacetime for timezone magic[13]
- MaxShuty's accessible web components for the double range slider
- and iFrame resizer for embedding the iFrame on this page
# 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
- it's very easy to write very messy code[14]
- should probably read something about how to structure these kinds of project
- feel a bit more confident in js and leaflet
- javascript feels very different to python
- handling timezones is
generallyalways fiddly - oh my styling range sliders is a can of worms
- and maybe I could have used these
- this is similar to the chase the sun family of organised rides, only it's like being chased by the sun. The naming of the chase the sun rides has always bugged me, because you're not chasing it. I could visualise the route that chase the sun should take by just using the azimuth as input into
getNextPoint()
. I'll add a radio button at some point that will let you toggle do you want to follow your shadow or the sun[15]
# todo
at some point...maybe
-
add toggle for ride/walk/drive routing[did, but unless I swap to a different routing engine it's doesn't make a difference, so I removed it 2025-03-29T22:51] -
add toggle for chase shadow / chase sun[done 2025-03-29T22:51] -
think about the moon, maybe [started this, then quickly stopped when realising that on some days the moon rises after it sets... 2025-03-29T22:51]
-
add export functionality
-
construct some tortured & tenuous metric that compares the ideal path and the network route to give a score for how faithfully you can follow your shadow
# footnotes
(a) plotted in-advance; (b) printed on a route-sheet; (c) made-up on in the moment ↩︎
a far-away train station; a friend's; that fish & chip shop ↩︎
assuming cloud-free skies and nothing too pesky in the way, like all these buildings and mountains[16] ↩︎
and another line, which we'll get to shortly ↩︎
which is a bit fiddly, and tricky to style ↩︎
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. ↩︎
and entirely predictably ↩︎
which is 180° away from the azimuth (with some modular arithmetic to get it back in the 0-360° range) ↩︎
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) ↩︎getNextPoint()
is borrowed from Chris Veness' geodesy library, specificailly therhumbDestinationPoint()
which when given a point (lat lon), a distance and a bearing returns a new point.[17] ↩︎should, i think, i hope ↩︎
turns out, I have already signed up. can't remember when ↩︎
there is, there must be a "better" way ↩︎
knew that already, but it is good to cement that knowledge ↩︎
or the moon? ↩︎
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' ↩︎