<- Back to work

shade-engine

Shipped

Is this parking spot in the shade right now - and until when?

shade-engine is an open-source urban shade engine. Given a point in a covered city and a moment in time it answers whether that spot is in the shade, what casts the shade (a building or a tree canopy) and how the verdict evolves across the day. It was born to find street parking in the shade in a city that spends its summers above 40°C, but nothing in it is city-specific: a city is a config file, a public LiDAR archive and one pipeline run. Everything below - the console, the per-point timeline, the parking layer - runs on precomputed artifacts served by a public API and a static file server; the console covers the two deployed cities, Cordoba and Montilla.

738M
LiDAR points
64
horizon sectors
1 m
per pixel
7
dates cover the year
184
tests
~ms
per query
The live app, embedded. Switch between Cordoba and Montilla; pick one of the seven declination rungs - the whole year has only seven distinct shadow geometries, and the app says out loud which date it swapped yours for; drag the hour along a track painted with that day’s solar elevation, so a December noon stays dim and a June one glows; search an address or click any street to ask the API whether that spot is in the shade and what casts it; switch to route mode for a walking route that trades detour for shade, coloured edge by edge.Open full screen →

One precomputation instead of a combinatorial explosion

Precomputing shadow maps per date and hour explodes: every day of the year times every quarter hour is thousands of rasters per city. shade-engine stores something better: for every square meter of the city, the elevation angle that blocks the sky in each of 64 compass directions - the pixel’s skyline fingerprint, swept once from the LiDAR surface model with the observer standing at street level. Any shade query then reduces to one comparison: is the sun, computed for the requested instant with an astronomical library, below that pixel’s horizon toward its azimuth? One offline build per city, millisecond queries, valid for any date - past or future.

From a national LiDAR archive to five rasters

The input is PNOA, Spain’s open aerial LiDAR program: for the largest city deployed so far, 90 tiles and 738 million classified points. The pipeline bins them into a surface model (buildings and canopies included), a terrain model and a landcover raster, then sweeps the horizon per pixel and sector. The artifacts ship as Cloud Optimized GeoTIFFs: internally tiled rasters whose layout lets the API answer a one-pixel question by decompressing kilobytes, not gigabytes. Points under a tree canopy are handled explicitly - the horizon computed from street level cannot see the canopy overhead, so a dedicated canopy mask (vegetation taller than 2.5 m, speckle-filtered) overrides it.

The sweep, up close

The horizon build is a ray march. For each of the 64 compass sectors, the sweep walks outward from every pixel up to 500 m, carrying the running maximum of atan((blocker height - observer height) / distance) with the observer at 1.6 m; whatever survives is the angle that blocks the sky in that direction. Vectorized properly, the walk is a handful of array slices per direction instead of a loop per pixel, and a 56 km² city - 56 million pixels times 64 sectors - builds in one overnight run. Angles are stored quantized to 8 bits (steps of 0.35°), which keeps a whole city’s horizon cube in a few gigabytes.

Shade tiles with no tile server

The map above does not render shade on demand. A pipeline step compares the whole city raster against the sun position of 83 preset instants and packs each result into two PMTiles files - building shade and tree shade, so either can be toggled alone - plus two static layers for the tree canopy and the LiDAR building footprint. Each file is a complete tile pyramid, indexed so a browser fetches any tile with plain HTTP range requests. Building interiors are left transparent: the overlay shows street-level shade only. Shade for a fixed instant is immutable, so the files are cacheable forever and the web server just serves bytes; the engine is never consulted at view time. The basemap is a self-hosted OpenStreetMap extract in the same format - the whole demo runs with no external tile service and no API keys.

The whole year in seven dates

83 instants sounds like a lot until you notice they span only seven dates. The sun’s path over a city repeats wherever its declination repeats, and declination is symmetric around the solstices: the sun’s path in mid-August retraces early May’s almost exactly. So the pipeline renders seven canonical dates - the solstices, an equinox and four steps between - and ships a ladder that maps every day of the year onto its declination twin, with about 4° of error on the worst day. The date slider in the console above is that ladder in action: pick any day and it resolves to its rendered twin, tiles and API agreeing pixel for pixel.

The parking layer: geometry meets shade

Street parking zones live in PostGIS as line geometries along the curb. The nearby endpoint finds zones within a radius (measured in meters in the local projected CRS, never in degrees), then samples each zone every 10 meters against the horizon raster and aggregates: the fraction of the zone in shade now, and - walking the sun forward - until when it stays shaded. That is the number a driver actually wants, and it is what colors the lines on the map from amber (sunny) to green (shaded).

When I would not use it

shade-engine computes geometry, not weather: on an overcast day there is no shade line to chase, and the engine says nothing about temperature or radiation. It needs a public aerial LiDAR campaign that still resembles the city - a new tower or a felled row of trees is invisible until the next flight. And its precision budget (1 m in plan, street-level questions) is the wrong tool for facade engineering or PV yield on a specific roof: those want a solar CAD, not a city-scale horizon raster. Where it shines is the question it was born for: which side of the street to park on, in any covered city, for any date, in milliseconds.

An honest public API

The public API ships with rate limiting, CORS scoped to this site and cache headers that tell the truth: a query for an explicit instant is immutable and cacheable for a day, a query for "now" is never cached. The repository doubles as a learning log - every geospatial concept the engine touches (CRS choices, LiDAR classes, COG internals, solar geometry, Web Mercator distortion) has a short note explaining what it is and which trap it hides.