shade-engine
ShippedIs 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
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.