Pedestrian navigation app that finds the sunniest and shadiest walking routes between two addresses, based on real-time sun position and building shadows.
- Geocoding: addresses are resolved via Nominatim (OSM) with autocomplete
- Sun position: altitude and azimuth computed with SunCalc for the chosen date, time and midpoint coordinates
- Route generation: OSRM foot-routing API generates up to ~10 route variants by shifting intermediate waypoints perpendicularly to the direct line (77-200 m offsets at pedestrian street scale)
- Building data: Overpass API fetches all building footprints in the route bounding box, with real heights from OSM tags (
height,building:levels) - Shadow scoring: for each route segment, every building's shadow is evaluated with a geometrically exact model: a segment midpoint
qis in shadow if the reverse-projectionq - shadow_vec(shiftingqtoward the sun byheight / tan(altitude)) falls inside the building's ground footprint (ray-casting point-in-polygon) - Ranking: routes are sorted by sun fraction; the sunniest and shadiest are highlighted
- Display: both routes are drawn with a per-segment orange-to-blue gradient matching the actual sun/shade pattern; the recommended tab (shade at high sun, sun at low sun) is pre-selected
- Elevation: once results are displayed, a background request to OpenTopoData (SRTM 30 m) fetches elevation along both routes (25 sampled points each, combined into a single API call). D+ and D− are computed from the elevation profile and shown in the results drawer. Falls back to Open-Elevation if OpenTopoData is unavailable; both APIs use exponential-backoff retry (1.5 s → 4 s) on 429/503/504.
| Layer | Technology |
|---|---|
| Framework | Astro v6 |
| Map | Leaflet + CartoDB Voyager tiles |
| Pedestrian routing | OSRM foot profile |
| Geocoding / autocomplete | Nominatim (OSM) |
| Sun position | SunCalc |
| Building footprints | Overpass API (OSM) |
| Elevation | OpenTopoData (SRTM 30 m) · fallback: Open-Elevation |
All external APIs are free and require no API key.
sunpath-app/
├── src/
│ ├── pages/
│ │ └── index.astro # single page: HTML, CSS, script orchestration
│ └── lib/
│ ├── autocomplete.js # address dropdown (Nominatim suggest, debounced)
│ ├── buildings.js # Overpass query + building polygon parsing
│ ├── compass.js # canvas compass showing sun direction
│ ├── geocode.js # address → {lat, lng} via Nominatim
│ ├── helpers.js # haversine, bearing, fmtDist, fmtDur
│ ├── map.js # Leaflet init, gradient route drawing, opacity control
│ ├── elevation.js # D+/D− fetch via OpenTopoData/Open-Elevation with retry
│ ├── routing.js # OSRM foot routing + via-point variant generation
│ ├── shadow.js # per-segment shadow scoring (point-in-polygon)
│ ├── sun.js # SunCalc wrapper → {azDeg, altDeg}
│ └── ui.js # status toast, tabs, swipeable results drawer
└── package.json
Requires Node ≥ 22 (Astro 6 constraint). If you use nvm:
nvm install 22 && nvm use 22cd sunpath-app
npm install
npm run dev # http://localhost:4321npm run build # production build → dist/
npm run preview # preview the build locallyThe shadow cast by a building of height h at solar altitude α extends in the direction opposite to the sun (azimuth + 180°) by sLen = h / tan(α) metres.
A route point q is in shadow if and only if the point q - shadow_vec (the reverse-projection of q toward the sun) falls inside the building's ground-floor polygon, tested with a standard ray-casting algorithm. This is geometrically exact for flat-roofed buildings and avoids the angular-tolerance heuristics of earlier approaches.
A conservative bounding-radius pre-filter (|q - centroid| > sLen + building_radius) skips buildings that cannot possibly cast shadow on q, keeping the per-route scoring fast even with hundreds of buildings.
The app uses OSM-based services everywhere (Nominatim, OSRM, Overpass), so it works for any city in the world, not just Switzerland. The core shadow model, sun position, and routing logic are fully geography-agnostic.
The main variable across regions is building height data quality: OSM coverage is excellent in dense European cities but sparse elsewhere. The table below shows the main limitations and how they could be improved.
After a route search, a bottom drawer slides up from the map edge. The drawer has two states:
- Peeked (default): the active tab and its sun/shade ratio bar are visible at a glance without covering the map.
- Expanded: drag the handle upward (or tap it) to reveal the full detail panel — distance, walking duration, elevation D+/D−, and the shaded vs. sunny distance split. Drag down or tap to collapse.
The drawer is driven by touch/mouse drag events and snaps to either state with a CSS transform transition (no layout reflow).
| Limitation | Potential improvement |
|---|---|
| OSM building heights are incomplete in many areas | Integrate authoritative 3D building datasets per country (e.g. swisstopo swissBUILDINGS3D for Switzerland, IGN BD TOPO for France, OS Building Height Attribute for the UK) |
| OSRM via-point variants don't always find both sidewalks of the same street | Use micro-offsets (~11 m) or OSRM snapping to highway=footway ways |
| Route deduplication by distance ±3% may miss genuinely different same-length routes | Compare coordinate-level geometry overlap |
| No vegetation (trees, parks) | Integrate OSM natural=tree and landuse=forest |
| Flat-roof assumption | Extend to pitched roofs using OSM roof:shape |
GraphHopper algorithm=alternative_route would produce cleaner non-backtracking variants |
Needs an API key (free or 💰) |
| SRTM 30 m elevation has ~15–30 m horizontal resolution | Use higher-res DEM (e.g. swisstopo DHM25 for Switzerland) for accurate urban D+/D− |