sometimes a map and a few lines of code are all you need
Experiments
Software Engineering
Published
March 10, 2025
Recently I was trying to figure out sun exposure for a specific location, except I was a couple of thousand kilometers away, and needed to get a sense of the sun’s path relative to a rooftop. There are quite a few tools available online (like SunCalc), and some mobile apps which pretty much can do the job. But, I really wanted to find out if I could do it myself from first principles.
The problem here is that although I vaguely remember the area, I don’t have a clear picture of the surroundings. So, an overlay on a map of the sun’s path at different times of the day and year would be really helpful and give an intuition of the kind of sun exposure the area gets.
To solve this we would need a few steps - we need to map the area, and for that we have Python’s folium library which is a wrapper around leaflet.js. We also need to calculate the sun’s position at different times of the day and year, for which we can use pysolar library. Finally we need to figure out how to overlay the sun’s course on the map, and for that we need to do some basic geometry.
Let us start by visualising the area. I know the coordinates of a polygon which covers the geometry I want to check, which we can then overlay on a map.
Show the code
import foliumlon =-8.649779901717114lat =41.14898866596278base = (lat, lon)shape = [ [41.14897506901333, -8.649853359565837], [41.14893442240481, -8.6497568000413], [41.14898264291275, -8.64971958439122], [41.1489942562235, -8.649717237458331], [41.14908867741279, -8.64980273287068], [41.149035155258005, -8.649911027059657], [41.14897506901333, -8.649853359565837],]# Set the zoom levelzoom_start =19# Create the mapmap_osm = folium.Map(location=base, zoom_start=zoom_start)# Add a polygon to the mapfolium.Polygon( shape, color="blue", fill=True, weight=1, fill_color="blue", fill_opacity=0.2).add_to(map_osm)# Save the map to an HTML filemap_osm.save("osm_map.html")
Now that we have the area mapped, we can start adding the sun’s path at different times of the day. Let us create a function that will add lines indicating the position and altitude of the sun at different times of the day. We need to calculate the :link azimuth and :link altitude of the sun at a given location and time, and then draw a line from the base to the calculated position. pysolar does this job tremendously well for us - we can give the function a specific date, and it will then calculate the sun’s position at different times of the day and draw the relevant lines. We will compute distinct lengths for each depending on the sun’s altitude (the shorter, the higher the sun), which should give us a sense of how strongly the sun shines on different parts of the area at different times of the day.
Show the code
from pysolar.solar import get_azimuth, get_altitudefrom geopy.distance import distancefrom folium.features import DivIconimport foliumimport datetimedef add_sun_lines( map_obj, month, day, line_color, line_weight, label_pos_end=False, line_style="solid",):# Define the hours (7am to 7pm, every 2 hours) hours = [7, 9, 11, 13, 15, 17, 19] year =2025 base_distance =50# maximum distance when sun is at horizonfor hour in hours:# Create a UTC datetime for the given hour dt = datetime.datetime( year, month, day, hour, 0, 0, tzinfo=datetime.timezone.utc )# Compute the sun's azimuth and altitude at your location azimuth = get_azimuth(lat, lon, dt) altitude = get_altitude(lat, lon, dt)# Ensure altitude is non-negative for the calculation altitude_for_calc =max(0, altitude)# The higher the sun, the shorter the line effective_distance = base_distance * (1- altitude_for_calc /90)# Calculate a destination point using the effective distance dest = distance(meters=effective_distance).destination( point=base, bearing=azimuth )# Setup polyline options based on line_style polyline_options = {}if line_style.lower() =="dashed": polyline_options["dash_array"] ="5, 5"# Draw the line (polyline) from the base to the destination folium.PolyLine( locations=[[lat, lon], [dest.latitude, dest.longitude]], color=line_color, weight=line_weight, popup=f"{dt.strftime('%b %d, %H:%M')} - Azimuth: {azimuth:.1f}°, Altitude: {altitude:.1f}°",**polyline_options, ).add_to(map_obj) pos_lat = dest.latitude if label_pos_end else (lat + dest.latitude) /2 pos_lon = dest.longitude if label_pos_end else (lon + dest.longitude) /2# Add a label and star icon only for specific hours (7, 13, 19)if hour in (7, 13, 19):# Add time label marker folium.Marker( [pos_lat, pos_lon], icon=DivIcon( html=f'<div style="font-size: 11pt; color: black;">{hour}:00</div>' ), ).add_to(map_obj)# Add a star icon (UTF star character) at the destination, shifted with CSS folium.Marker( [dest.latitude, dest.longitude], icon=DivIcon( html='<div style="font-size: 14pt; color: orange; transform: translate(10px, -10px);">★</div>' ), ).add_to(map_obj)
With that, let us add the directional sun lines to the map for August 1 (summer) and January 1 (winter). We will use red lines for August 1 and blue lines for January 1.
Show the code
# Add sun lines for August 1 (bold red lines) and January 1 (thin blue lines)add_sun_lines( map_osm, month=8, day=1, line_color="red", line_weight=3, label_pos_end=True)add_sun_lines( map_osm, month=1, day=1, line_color="blue", line_weight=2, label_pos_end=True, line_style="dashed",)
folium relies on leaflet.js which produces Javascript for rendering maps, so we need to save the map to an HTML file to view it.
Show the code
# Save the map to an HTML filemap_osm.save("osm_map_with_sun_markers.html")
That worked very well! We want a map which gives the best visual sense of the area and the sun’s path, so let us add a tile layer to the map. We can use the CartoDB Positron tile layer, which is a light, minimalistic map style.
Show the code
# Add a tile layer to the mapfolium.TileLayer("CartoDB Positron", opacity=1.0).add_to(map_osm)map_osm.save("osm_map_with_sun_markers_and_tiles.html")
It would also be interesting to see the sun’s path throughout the day as an arc. We can add a continuous line to the map that represents the sun’s path from sunrise to sunset. Let us create another function which will compute the sun’s position more frequently and draw a continuous line on the map.
Show the code
def add_sun_path(map_obj, month, day, line_color="orange", line_weight=2, interval=15): year =2025 base_distance =50 sun_path_coords = []# Compute sun positions every `interval` minutes between 7:00 and 19:00 UTCfor t inrange(7*60, 19*60, interval): hour = t //60 minute = t %60 dt = datetime.datetime( year, month, day, hour, minute, 0, tzinfo=datetime.timezone.utc ) azimuth = get_azimuth(lat, lon, dt) altitude = get_altitude(lat, lon, dt)# Ensure a non-negative altitude for the calculation altitude_for_calc =max(0, altitude)# Scale the distance: the higher the sun, the shorter the line effective_distance = base_distance * (1- altitude_for_calc /90) dest = distance(meters=effective_distance).destination( point=base, bearing=azimuth ) sun_path_coords.append([dest.latitude, dest.longitude])# Draw the continuous sun path folium.PolyLine( locations=sun_path_coords, color=line_color, weight=line_weight, opacity=0.7, popup="Sun Path", ).add_to(map_obj)
This allows us to add the sun path for August 1 and January 1 as before, with different line colors and weights.
Show the code
# Add sun path for August 1add_sun_path(map_osm, month=8, day=1, line_color="darkorange", line_weight=2)add_sun_path(map_osm, month=1, day=1, line_color="orange", line_weight=1)map_osm.save("osm_map_with_sun_markers_tiles_and_path.html")
That works very well, but we are lacking a sense of depth - we can add a satellite layer to the map to give a better sense of the area, the geometry, and potential obstacles which could occlude the sun’s path.
Show the code
# Add an ESRI Satellite layer to the mapfolium.TileLayer(tiles="StadiaAlidadeSatellite", opacity=1.0).add_to(map_osm)map_osm.save("osm_map_with_sun_markers_tiles_path_and_satellite.html")
Although not perfect, it gives a really good sense of the area and the sun’s path, any potential shaded zones, and the kind of sun exposure it will get. It’s a good example as to what a few lines of code, basic geometry, and creativity can do!