Which side of the bus should I sit on so the sun isn't in my face?
Give it a start, a destination and a departure time. It fetches the real road route, walks along it minute by minute, works out where the sun is at each point, and tells you which side of the vehicle stays in the shade, plus an interactive map showing exactly where and when the sun swaps sides.
routesun Liberec "Hradec Kralove" --depart 07:30Hue is the side: blue is your left window, red is your right, grey is no direct sun. Depth of colour is how strong it is. Short rays point at where the sun actually is at the moment you pass each spot. Hovering any stretch of road gives you the time, the sun's altitude and bearing, and the exposure on each window; the panel's timeline does the same over time, and there is a plain table view for anything colour can't carry.
git clone https://github.com/jholaj/RouteSunVisualizer.git
cd RouteSunVisualizer
python -m venv .venv && source .venv/bin/activate
pip install -e .Routing and place-name lookup use OpenRouteService, which needs a free API key:
cp .env_example .env # then paste your key into .envWithout a key you can still use --offline, which draws a straight
great-circle line at a fixed speed. Good enough for a route that runs roughly
straight, useless for one that doesn't.
routesun ORIGIN DESTINATION [options]
ORIGIN, DESTINATION a place name, or "lat,lon"
-d, --depart TIME 'now', 'HH:MM', or 'YYYY-MM-DD HH:MM' (default: now)
--tz ZONE IANA zone for --depart, e.g. Europe/Prague
-p, --profile P driving-car, driving-hgv, cycling-regular,
cycling-road, foot-walking, wheelchair
-o, --output FILE where to write the report (default: route_sun.html)
--open open the report in a browser when it is ready
--no-html print the summary only
--offline skip the API, use a great-circle line (needs "lat,lon")
--speed KMH average speed for --offline (default: 70)
--legs N how finely to sample the route (default: 240)
--api-key KEY overrides $OPENROUTESERVICE_API_KEY
-v, --verbose
python main.py ... works identically if you'd rather not install the package.
# Morning commute, opened straight away
routesun Liberec "Hradec Kralove" --depart 07:30 --open
# A specific day, in a specific timezone
routesun Praha Brno --depart '2026-08-01 06:00' --tz Europe/Prague
# No API key, coordinates only
routesun "50.767,15.056" "50.210,15.825" --offline --depart 17:00import datetime as dt
from zoneinfo import ZoneInfo
from routesun import analysis, routing, visualize
provider = routing.OpenRouteServiceProvider(api_key)
route = provider.route(
provider.geocode("Liberec"), provider.geocode("Hradec Kralove"), "driving-car"
)
trip = analysis.analyse(
route, dt.datetime(2026, 8, 1, 7, 30, tzinfo=ZoneInfo("Europe/Prague"))
)
print(trip.recommended) # Side.RIGHT
print(trip.explanation) # "The sun stays on your left the whole way."
visualize.save_report(trip, "route_sun.html")The route is cut into ~240 legs. Each leg knows its compass heading and, from OpenRouteService's per-step durations, the clock time you actually reach it, so the sun moves across the sky as you travel rather than being frozen at the departure time.
For each leg, PySolar gives the sun's
altitude h and azimuth A. With the heading B, the sun's bearing relative
to the direction of travel is θ = (A - B) mod 360, and the direct beam
landing on a vertical side window is
exposure = I(h) · cos(h) · |sin θ| sun on the right when sin θ > 0
cos(h): a vertical window only catches the horizontal part of the beam. A sun overhead lands on the roof, not on you.|sin θ|: the sun has to be off to the side. Straight ahead or behind, it goes through the windscreen instead.I(h): clear-sky beam strength, from the Kasten-Young air mass with a 0.7 zenith transmittance, square-rooted so a low sun stays perceptually significant rather than rounding to nothing.
Exposure is integrated over each leg's duration; the side with the smaller total is the one to sit on. Below the horizon, exposure is zero.
What it does not know: the weather, tinted or curtained windows, tunnels, cuttings, tree cover, terrain shadowing, or which way the seats face. It assumes a clear sky and a vehicle pointing along the road.
pip install -e '.[dev]'
python -m pytest -qThe map's colours are a validated diverging palette (blue and red poles, neutral grey midpoint) with separately stepped light and dark sets, checked for lightness monotonicity and for separation under simulated protanopia and deuteranopia. If you change them, re-check rather than eyeball.
| File | What lives there |
|---|---|
routesun/geo.py |
coordinates, bearings, spherical offsets |
routesun/sun.py |
solar position and the side-exposure model |
routesun/routing.py |
OpenRouteService and offline route providers, trip timing |
routesun/analysis.py |
legs, per-side totals, the recommendation |
routesun/chart.py |
the diverging timeline SVG |
routesun/palette.py |
colour tokens for both themes |
routesun/visualize.py |
the Folium map and the report page |
routesun/report.py |
the terminal summary |
routesun/cli.py |
argument parsing and wiring |
- OpenRouteService for routing and geocoding
- PySolar for solar position
- Folium and Leaflet for the map
- OpenStreetMap contributors and CARTO for tiles

