DEV Community

Cover image for I Built a Web App That Finds Shady Parking Spots Using Sun Physics
Muhammed Anas K P
Muhammed Anas K P

Posted on Edited on

I Built a Web App That Finds Shady Parking Spots Using Sun Physics

TL;DR: Try the live demo · Source on GitHub — draw a parking area on a map, pick a time window, and see which spots stay in shade. Powered by open building data, basic sun physics, and shadow math that runs entirely in the browser.


Parking in direct sun can turn a car into an oven.

In hot regions like Kerala, India, even 30 minutes of sunlight on a parked car can make the interior uncomfortably hot. I kept asking a simple question:

Could we estimate which parking spots will stay in shade?

That curiosity became ShadePark — a small web app that estimates how much of a parking area is in sun or shade over a selected time range. You can try it live or browse the source on GitHub.

You draw a parking polygon on a satellite map, choose a time window, and the app visualizes shade coverage as a heatmap over your selection.


Table of contents


Demo workflow

Live demo: shadepark.vercel.app (Kozhikode only — zoom past level 17 to draw a parking area)

The user flow is intentionally simple:

  1. Zoom in to a location (zoom level must be greater than 17).
  2. Draw a parking polygon by clicking vertices on the map.
  3. Choose a time range (for example, 09:00–14:00).
  4. View a heatmap showing sun vs. shade coverage.
  5. Change time (optional) — pick a new window for the same parking area without redrawing; buildings stay cached in memory and only shade is recomputed.
Color Meaning
Green Mostly shaded during the selected window
Yellow Mixed sun and shade
Red Mostly in direct sun

App desktop viewShadePark heatmap over a drawn parking polygon in Kozhikode — green areas indicate more time in shade.

Drawing a parking polygon and selecting time range

The core idea

Shade happens when something blocks sunlight. For urban parking, the main blockers are buildings.

To estimate shade over time, you need three inputs:

  1. Building footprints (2D geometry)
  2. Building heights (to scale shadow length)
  3. Sun position (azimuth and altitude) at each timestep

From there you can project shadows and score how often each part of the parking area sits inside them.

The hard part is not the physics — it is making this interactive in the browser without melting your API server.


System architecture

ShadePark splits cleanly into a thin backend and a geometry-heavy frontend.

Layer Stack
Frontend React 19, Vite, MapLibre GL, SunCalc, polygon-clipping, Tailwind CSS
Backend Node.js (Express 5), PostgreSQL + PostGIS
Map tiles ESRI World Imagery (satellite)
User draws parking polygon
        ↓
Client POSTs polygon → /api/buildings
        ↓
PostGIS returns nearby buildings (≤ 500, 10 m buffer)
        ↓
Client computes swept shadow volumes every 15 minutes
        ↓
Grid cells scored by % time in shade
        ↓
MapLibre heatmap (red → yellow → green)
Enter fullscreen mode Exit fullscreen mode

Key design decision: client-side shadow math

All shadow and shade scoring runs in the browser.

The API only answers one question: which buildings are near this parking polygon?

That keeps the server small, avoids heavy geospatial processing on every time-range change, and makes iteration fast during development.


Step 1: Getting building data (GOBS)

Shadows need footprints and heights. I used GOBS (Geospatial Open Building Data).

  • Source file: KERALA.csv (from KERALA.csv.gz)
  • Scope for the prototype: Kozhikode district only (district_name = Kozhikode_further in the source CSV)

Data prep pipeline (offline)

Tools: xsv for CSV work, GDAL (ogr2ogr) for GeoJSON and DB import.

1. Inspect columns

xsv headers KERALA.csv
Enter fullscreen mode Exit fullscreen mode

2. Keep only what we need

xsv select latitude,longitude,geometry,building_height,district_name KERALA.csv > KERALA_REDUCED.csv
Enter fullscreen mode Exit fullscreen mode

3. Filter to Kozhikode

xsv search -s district_name Kozhikode_further KERALA_REDUCED.csv > KERALA_KOZHIKODE_FURTHER.csv
Enter fullscreen mode Exit fullscreen mode

4. Drop the district column

xsv select latitude,longitude,geometry,building_height KERALA_KOZHIKODE_FURTHER.csv > KOZHIKODE_REDUCED.csv
Enter fullscreen mode Exit fullscreen mode

5. Convert to GeoJSON (WGS84)

ogr2ogr \
  -f GeoJSON \
  kozhikode.geojson \
  KOZHIKODE_REDUCED.csv \
  -oo GEOM_POSSIBLE_NAMES=geometry \
  -oo KEEP_GEOM_COLUMNS=NO \
  -a_srs EPSG:4326
Enter fullscreen mode Exit fullscreen mode

Output: kozhikode.geojson, ready for PostGIS import.


Step 2: Loading data into PostGIS

Import with ogr2ogr into a buildings table:

ogr2ogr \
  -f PostgreSQL \
  "PG:host=HOST port=PORT dbname=DB user=USER password=PASSWORD sslmode=require" \
  kozhikode.geojson \
  -nln buildings \
  -nlt MULTIPOLYGON \
  -lco GEOMETRY_NAME=geom
Enter fullscreen mode Exit fullscreen mode

Important columns:

  • geomMULTIPOLYGON footprint
  • building_height — used for shadow length

Add a spatial index so intersection queries stay fast:

CREATE INDEX IF NOT EXISTS buildings_geom_idx
ON buildings
USING GIST (geom);
Enter fullscreen mode Exit fullscreen mode

Having the index is not enough — the query must filter on geom (geometry) so Postgres can use it. More on that in Step 3.


Step 3: Fetching nearby buildings

When the user finishes drawing, the client sends a GeoJSON parking polygon to the API.

The server query uses a 10 m geography buffer (for accurate meters), then filters on geometry so the GIST index applies. Results are capped at 500 buildings:

WITH search AS (
    SELECT ST_Buffer(
        ST_GeomFromGeoJSON($1)::geography,
        10
    )::geometry AS area
)
SELECT
    b.id,
    b.building_height,
    ST_AsGeoJSON(b.geom) AS geometry
FROM buildings b
CROSS JOIN search s
WHERE b.geom && s.area
  AND ST_Intersects(b.geom, s.area)
LIMIT 500;
Enter fullscreen mode Exit fullscreen mode

The && operator checks bounding-box overlap first (fast, index-friendly). ST_Intersects refines to the buffered area.

A performance trap I hit in production

My first version looked reasonable but was disastrously slow (~32 seconds on ~250k rows):

-- Do NOT do this if you have a GIST index on geom
WHERE ST_Intersects(geom::geography, ST_Buffer(...::geography, 10))
Enter fullscreen mode Exit fullscreen mode

EXPLAIN ANALYZE showed a parallel sequential scan — the cast geom::geography on the column prevented Postgres from using the GIST index on geom. The index existed; the query just could not use it.

After switching to geom && area + ST_Intersects(geom, area), the same query dropped to milliseconds. If you build on PostGIS + Supabase, always check the query plan, not just whether an index exists.

Why buffer?

  • Shadows extend beyond the parking outline.
  • A small buffer pulls in adjacent buildings whose shadows may still reach the lot.
  • The LIMIT keeps client-side union work bounded.

Step 4: Computing shadows in the browser

The interesting work happens after the API responds.

Sun position with SunCalc

I use SunCalc to get sun altitude and azimuth for a lat/lng and timestamp.

When the sun is above the horizon, shadow length follows basic trigonometry:

shadow_length = building_height / tan(sun_altitude)
Enter fullscreen mode Exit fullscreen mode

Each building vertex is projected opposite the sun direction to form a shadow polygon.

Swept volumes, not just shadow outlines

A shadow polygon alone is not enough for “was this point shaded at time t?”

For each building and timestamp, ShadePark builds a swept area: the union of

  • the building footprint,
  • the shadow footprint, and
  • quadrilateral “side faces” connecting corresponding vertices.

That swept polygon represents the full region the building occludes as the sun moves between those two silhouettes for that instant.

The polygon-clipping library unions these pieces in the browser. At each 15-minute step, swept areas from all nearby buildings are unioned again into one multipolygon per timestamp.


From building footprints to shadows

Here is the mental model:

Inputs: 2D footprint + scalar height + sun vector

Output: per-cell % of timesteps in shade over your selected window

This is a deliberate simplification: we treat each building as a vertical extrusion with a flat roof — no roof pitch, no courtyards, no trees.


Step 5: Shade scoring with a parking grid

Instead of testing shade continuously, the app samples the parking polygon on a grid.

  • Target cell size: about 3 meters (~one car width).
  • Resolution scales with polygon size (clamped between 8 and 80 cells per axis).
  • Timesteps: every 15 minutes between fromTime and toTime.

For each cell center:

score = (number of timesteps in shade) / (total timesteps)
Enter fullscreen mode Exit fullscreen mode

Example: 09:00–14:00 → 21 timestamps (15-minute steps). A cell shaded at 12 of them scores ~57% (displayed as 0–100% in the legend).

Night/low-sun edge case: when the sun is below the horizon, timesteps count as fully shaded — reasonable for “no direct sun.”


Step 6: Map visualization

Results render directly on the MapLibre map as a fill layer over the parking polygon, interpolated from red (more sun) through yellow to green (more shade).

A small legend explains the score: percentage of the selected time range spent in shade.

For debugging, URL flags like ?debug=all can show building fills, outlines, and swept-area layers — useful when validating geometry against satellite imagery.


Data quality limitations

GOBS is automatically generated, not hand-surveyed. That matters for shadow apps, because shadow shape follows footprint shape.

In practice I saw simplified footprints that do not match real structures. One example: a building that is effectively triangular on the ground, but GOBS (and Google Maps) show a rectangular box — likely from the same class of automated extraction pipelines many map products share.

Triangular building on satellite imagery compared to rectangular Google Maps footprints

What this means for ShadePark:

  • Shadow direction and length can still be reasonable if height is in the right ballpark.
  • Footprint shape errors skew the shadow outline — especially for irregular buildings.
  • For parking-lot-scale decisions (“north side vs. south side of the lot”), the approximation is often still useful.
  • For spot-level precision (e.g. one motorcycle bay), treat results as indicative, not ground truth.

Possible improvements:

  • Blend OpenStreetMap building footprints with GOBS heights
  • Cross-check multiple datasets and drop outliers
  • Manual correction for high-traffic POIs

Design decisions

Why client-side computation?

Reason Detail
Interaction Users change time ranges frequently; the client caches buildings in memory after the first fetch so “Change Time” only recomputes shade.
Scale Hundreds of buildings × dozens of timesteps is workable in modern browsers with spatial filtering.
Simplicity The API stays a thin PostGIS lookup service.

Why spatial filtering + hard limits?

  • ST_Buffer(..., 10) on geography keeps the search area accurate in meters.
  • geom && area ensures the GIST index is used before precise intersection.
  • LIMIT 500 prevents pathological payloads in dense urban cores.

Why a grid instead of continuous geometry?

  • Easy to explain (% time in shade per ~3 m cell).
  • Fast to render as a choropleth-style heatmap.
  • Good enough for “pick a row / pick a corner” parking decisions.

Current limitations

Being honest about scope helps readers trust the prototype:

  • Single region — building data covers Kozhikode only (for now).
  • Single day — analysis uses today’s date; there is no date picker yet.
  • Buildings only — no trees, awnings, or temporary structures.
  • No persistence — refresh clears drawn polygons and results (buildings are kept in memory only for “Change Time” within the same session).
  • Flat extrusion model — no roof pitch or complex 3D forms.
  • GOBS imperfections — see Data quality limitations.

Future improvements

Ideas on the roadmap:

  • Date selection (seasonal sun angles change everything in Kerala).
  • Expand the GOBS pipeline to more districts/regions.
  • Integrate vegetation / OSM layers where available.
  • Cache frequent building queries server-side.
  • Parking recommendation mode (“best cell in this lot for 12:00–15:00”).
  • Optional save/export of analyses.

Lessons learned

This project was a practical tour of:

  • Open geospatial pipelines — CSV → GeoJSON → PostGIS
  • Spatial indexing — why GIST on geom matters, and why geom::geography in a WHERE clause can silently force a full table scan
  • Sun modelling — azimuth, altitude, and shadow length
  • Browser geometry — unions, swept areas, and scoring grids
  • Product honesty — automated building data is useful but imperfect

The biggest takeaway: simple physics + decent open data + smart architecture can produce a surprisingly useful map tool without a heavy GIS backend.


FAQ

Does ShadePark work outside Kozhikode?

Not yet. The deployed dataset is filtered to Kozhikode. The same GOBS pipeline can be repeated for other districts.

Does the server calculate shadows?

No. The server returns nearby building geometry. The React client runs SunCalc and polygon unions.

How accurate is the shade map?

It is a heuristic based on 2D footprints, estimated heights, and simplified shadow geometry — useful for comparison within a lot, not for engineering sign-off.

Why 15-minute intervals?

A balance between smooth time coverage and client performance. The interval is easy to change in code.

What happens when I change the time range?

After the first analysis, tap Change Time on the results screen. The app reuses the buildings already fetched for that polygon and only reruns the client-side shade simulation — no second trip to PostGIS.

Can I run it locally?

Yes. Clone the repo, import kozhikode.geojson into PostGIS, configure server/.env, then run the Express API and Vite client. See the README.


Try it yourself

If you build something similar — or extend ShadePark to your city — I would love to hear what you changed (especially around data sources and accuracy).


Keywords for search: parking shade map, sun shadow calculator, PostGIS building query, MapLibre heatmap, GOBS Kerala buildings, client-side geospatial, ShadePark.

Top comments (0)