Urban shade analysis from OpenStreetMap + solar geometry.
Compute which sidewalks are in shade, score shade potential across a neighborhood, or query a single point in real time. Read a blog version here at https://korawichmawinkavee.medium.com/i-built-a-web-app-that-tells-you-how-much-shade-a-street-has-and-it-was-harder-than-i-expected-52b81879467d?postPublishedType=repub
🌐 Live Demo →
📡 API →
SalusShadow casts shadows from buildings (and optionally trees) onto sidewalks using real sun angles, then measures how much of each street segment is shaded.
Shadow geometry is computed from OSM building footprints + estimated heights, solar position from pvlib, and street networks from OSMnx.
- Open cmu-salus-lab.github.io/salusshadow
- Click anywhere on the map or drag the pin to your location of interest
- Pick a date and time using the datetime picker
- Hit "Check shade" — results appear in ~5–15 seconds
- See whether the spot is ☀️ in sun or 🌑 in shade, the street shadow ratio, sun azimuth/elevation, and street-level imagery from Mapillary
Note: The first request after a period of inactivity may take ~30 seconds (Render free tier cold start). Subsequent requests are fast.
salusshadow/
├── salusshadow.py # Core library — shadow geometry, pipeline, point query
├── main.py # FastAPI wrapper (deployed to Render)
├── requirements.txt # Python dependencies
└── index.html # Web app (served via GitHub Pages)
Drop a pin on the map (or type lat/lon), pick a date and time, and instantly see:
- ☀️ / 🌑 In sun or in shade
- Shadow ratio of the nearest street segment
- Sun azimuth + elevation
- Street-level imagery from Mapillary
The app calls the FastAPI backend on Render. The first request after a period of inactivity may take ~30 seconds (free tier cold start).
pip install osmnx pvlib geopandas shapely rasterio numpy pandas tqdmimport salusshadow
result = salusshadow.query_point_shade(
lat=42.3601,
lon=-71.0589,
timestamp="2025-07-29T14:00:00-04:00",
search_radius_m=200,
include_trees=True,
)
# {
# 'in_shade': False,
# 'street_name': 'Court Street',
# 'street_osm_id': '8646962',
# 'shadow_ratio': 0.0,
# 'sun_azimuth': 216.98,
# 'sun_elevation': 62.03
# }Score every sidewalk in an area and export to GeoPackage:
python salusshadow.py \
--place "Downtown Boston, Massachusetts, USA" \
--dt "2025-07-29T14:00:00-04:00" \
--include-trees \
--save-shadows \
--out boston_shade.gpkgOr with a bounding box:
python salusshadow.py \
--bbox "42.365,42.355,-71.055,-71.068" \
--dt "2025-07-29T14:00:00-04:00" \
--footway-source both \
--alpha 3.0 \
--out output.gpkg| Flag | Default | Description |
|---|---|---|
--place or --bbox |
— | Area to analyze (required) |
--dt |
— | ISO 8601 datetime with timezone (required) |
--include-trees |
off | Add OSM tree + tree_row shadows |
--footway-source |
osm |
osm, synth (synthesized from roads), or both |
--alpha |
3.0 |
Sun-aversion weight for experienced length scoring |
--save-shadows |
off | Export shadow polygons as extra layers (useful in QGIS) |
--default-bldg-h |
12.0 |
Fallback building height in metres |
--default-tree-h |
10.0 |
Default tree height in metres |
--sidewalk-offset |
7.0 |
Offset (m) for synthesized sidewalk lines |
The FastAPI backend exposes a single endpoint:
GET https://salusshadow-api.onrender.com/shade
Parameters
| Param | Required | Example |
|---|---|---|
lat |
✅ | 42.3601 |
lon |
✅ | -71.0589 |
timestamp |
✅ | 2025-07-29T14:00:00-04:00 |
include_trees |
optional | true |
search_radius_m |
optional | 200 |
Example
curl "https://salusshadow-api.onrender.com/shade?lat=42.3601&lon=-71.0589×tamp=2025-07-29T14:00:00-04:00"
Response
{
"in_shade": false,
"street_name": "Court Street",
"street_osm_id": "8646962",
"shadow_ratio": 0.0,
"sun_azimuth": 216.98,
"sun_elevation": 62.03
}Interactive docs: https://salusshadow-api.onrender.com/docs
# Clone
git clone https://github.com/CMU-SALUS-Lab/salusshadow.git
cd salusshadow
# Install
pip install -r requirements.txt
# Start API
uvicorn main:app --reload --port 8000
# Open app — set the sidebar API URL to http://localhost:8000
open index.htmlSalusShadow supports tree canopy segmentation via DetecTree using NAIP (1 m) or Sentinel-2 (10 m) orthophotos from Microsoft Planetary Computer:
python salusshadow.py \
--place "Pittsburgh, PA" \
--dt "2025-08-01T12:00:00-04:00" \
--auto-ortho \
--ortho-source naip \
--out pittsburgh_shade.gpkgRequires additional packages: detectree pystac-client stackstac planetary-computer scipy.
| Field | Description |
|---|---|
shade_frac |
Fraction of sidewalk segment in shadow (0–1) |
experienced_len |
Thermally-weighted length: α × sun_length + shade_length |
length_m |
Segment length in metres |
source |
osm or synth |
- Sun azimuth and elevation are computed with
pvlibfor the given datetime and location - Each building polygon is extruded along the sun vector to produce a shadow polygon; the building footprint itself is subtracted so only the cast shadow remains
- Tree shadows are approximated as buffered projections (point trees → circles, tree rows → buffered lines)
- All shadow polygons are unioned into a single geometry
- Each sidewalk segment is intersected with the shadow union to compute
shade_frac
- OSMnx — street networks and building footprints from OpenStreetMap
- pvlib — solar position calculations
- GeoPandas + Shapely — spatial operations
- FastAPI — REST API
- Leaflet.js — interactive map
- Mapillary JS SDK — street-level imagery
Apache 2.0 — CMU SALUS Lab
