<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muhammed Anas K P</title>
    <description>The latest articles on DEV Community by Muhammed Anas K P (@anas_kp).</description>
    <link>https://dev.to/anas_kp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3940831%2Fc69a5616-4ef4-495e-933d-8b58f3140a00.jpg</url>
      <title>DEV Community: Muhammed Anas K P</title>
      <link>https://dev.to/anas_kp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anas_kp"/>
    <language>en</language>
    <item>
      <title>I Built a Web App That Finds Shady Parking Spots Using Sun Physics</title>
      <dc:creator>Muhammed Anas K P</dc:creator>
      <pubDate>Mon, 22 Jun 2026 05:10:53 +0000</pubDate>
      <link>https://dev.to/anas_kp/i-built-a-web-app-that-finds-shady-parking-spots-using-sun-physics-3dkk</link>
      <guid>https://dev.to/anas_kp/i-built-a-web-app-that-finds-shady-parking-spots-using-sun-physics-3dkk</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;strong&gt;&lt;a href="https://shadepark.vercel.app/" rel="noopener noreferrer"&gt;Try the live demo&lt;/a&gt;&lt;/strong&gt; · &lt;a href="https://github.com/Anaskp/shadepark" rel="noopener noreferrer"&gt;Source on GitHub&lt;/a&gt; — 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.&lt;/p&gt;




&lt;p&gt;Parking in direct sun can turn a car into an oven.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Could we estimate which parking spots will stay in shade?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That curiosity became &lt;strong&gt;ShadePark&lt;/strong&gt; — a small web app that estimates how much of a parking area is in sun or shade over a selected time range. You can &lt;strong&gt;&lt;a href="https://shadepark.vercel.app/" rel="noopener noreferrer"&gt;try it live&lt;/a&gt;&lt;/strong&gt; or browse the &lt;strong&gt;&lt;a href="https://github.com/Anaskp/shadepark" rel="noopener noreferrer"&gt;source on GitHub&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Demo workflow&lt;/li&gt;
&lt;li&gt;The core idea&lt;/li&gt;
&lt;li&gt;System architecture&lt;/li&gt;
&lt;li&gt;Step 1: Getting building data (GOBS)&lt;/li&gt;
&lt;li&gt;Step 2: Loading data into PostGIS&lt;/li&gt;
&lt;li&gt;Step 3: Fetching nearby buildings&lt;/li&gt;
&lt;li&gt;Step 4: Computing shadows in the browser&lt;/li&gt;
&lt;li&gt;From building footprints to shadows&lt;/li&gt;
&lt;li&gt;Step 5: Shade scoring with a parking grid&lt;/li&gt;
&lt;li&gt;Step 6: Map visualization&lt;/li&gt;
&lt;li&gt;Data quality limitations&lt;/li&gt;
&lt;li&gt;Design decisions&lt;/li&gt;
&lt;li&gt;Current limitations&lt;/li&gt;
&lt;li&gt;Future improvements&lt;/li&gt;
&lt;li&gt;Lessons learned&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;li&gt;Try it yourself&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Demo workflow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://shadepark.vercel.app/" rel="noopener noreferrer"&gt;shadepark.vercel.app&lt;/a&gt; (Kozhikode only — zoom past level 17 to draw a parking area)&lt;/p&gt;

&lt;p&gt;The user flow is intentionally simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zoom in&lt;/strong&gt; to a location (zoom level must be greater than 17).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draw&lt;/strong&gt; a parking polygon by clicking vertices on the map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose&lt;/strong&gt; a time range (for example, 09:00–14:00).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View&lt;/strong&gt; a heatmap showing sun vs. shade coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change time&lt;/strong&gt; (optional) — pick a new window for the same parking area without redrawing; buildings stay cached in memory and only shade is recomputed.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Green&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mostly shaded during the selected window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Yellow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mixed sun and shade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Red&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mostly in direct sun&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xs0vs6a1s5wchcles8z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xs0vs6a1s5wchcles8z.png" alt="App desktop view" width="800" height="431"&gt;&lt;/a&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdx0uy0p94cbcfaxm7az.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhdx0uy0p94cbcfaxm7az.png" alt="ShadePark heatmap over a drawn parking polygon in Kozhikode — green areas indicate more time in shade." width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zi2xyc9m2b7fou09wmh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zi2xyc9m2b7fou09wmh.png" alt="Drawing a parking polygon and selecting time range" width="800" height="859"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;Shade happens when something blocks sunlight. For urban parking, the main blockers are &lt;strong&gt;buildings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To estimate shade over time, you need three inputs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Building footprints&lt;/strong&gt; (2D geometry)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building heights&lt;/strong&gt; (to scale shadow length)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sun position&lt;/strong&gt; (azimuth and altitude) at each timestep&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From there you can project shadows and score how often each part of the parking area sits inside them.&lt;/p&gt;

&lt;p&gt;The hard part is not the physics — it is making this &lt;strong&gt;interactive in the browser&lt;/strong&gt; without melting your API server.&lt;/p&gt;




&lt;h2&gt;
  
  
  System architecture
&lt;/h2&gt;

&lt;p&gt;ShadePark splits cleanly into a thin backend and a geometry-heavy frontend.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React 19, Vite, MapLibre GL, SunCalc, polygon-clipping, Tailwind CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Node.js (Express 5), PostgreSQL + PostGIS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Map tiles&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ESRI World Imagery (satellite)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key design decision: client-side shadow math
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;All shadow and shade scoring runs in the browser.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The API only answers one question: &lt;em&gt;which buildings are near this parking polygon?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That keeps the server small, avoids heavy geospatial processing on every time-range change, and makes iteration fast during development.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Getting building data (GOBS)
&lt;/h2&gt;

&lt;p&gt;Shadows need footprints and heights. I used &lt;strong&gt;&lt;a href="https://gobs.aeee.in/" rel="noopener noreferrer"&gt;GOBS (Geospatial Open Building Data)&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source file:&lt;/strong&gt; &lt;code&gt;KERALA.csv&lt;/code&gt; (from &lt;code&gt;KERALA.csv.gz&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope for the prototype:&lt;/strong&gt; Kozhikode district only (&lt;code&gt;district_name = Kozhikode_further&lt;/code&gt; in the source CSV)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Data prep pipeline (offline)
&lt;/h3&gt;

&lt;p&gt;Tools: &lt;a href="https://github.com/BurntSushi/xsv" rel="noopener noreferrer"&gt;xsv&lt;/a&gt; for CSV work, &lt;a href="https://gdal.org/" rel="noopener noreferrer"&gt;GDAL&lt;/a&gt; (&lt;code&gt;ogr2ogr&lt;/code&gt;) for GeoJSON and DB import.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Inspect columns&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xsv headers KERALA.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Keep only what we need&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xsv &lt;span class="k"&gt;select &lt;/span&gt;latitude,longitude,geometry,building_height,district_name KERALA.csv &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; KERALA_REDUCED.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Filter to Kozhikode&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xsv search &lt;span class="nt"&gt;-s&lt;/span&gt; district_name Kozhikode_further KERALA_REDUCED.csv &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; KERALA_KOZHIKODE_FURTHER.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Drop the district column&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xsv &lt;span class="k"&gt;select &lt;/span&gt;latitude,longitude,geometry,building_height KERALA_KOZHIKODE_FURTHER.csv &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; KOZHIKODE_REDUCED.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Convert to GeoJSON (WGS84)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ogr2ogr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; GeoJSON &lt;span class="se"&gt;\&lt;/span&gt;
  kozhikode.geojson &lt;span class="se"&gt;\&lt;/span&gt;
  KOZHIKODE_REDUCED.csv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-oo&lt;/span&gt; &lt;span class="nv"&gt;GEOM_POSSIBLE_NAMES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;geometry &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-oo&lt;/span&gt; &lt;span class="nv"&gt;KEEP_GEOM_COLUMNS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;NO &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-a_srs&lt;/span&gt; EPSG:4326
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output: &lt;code&gt;kozhikode.geojson&lt;/code&gt;, ready for PostGIS import.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Loading data into PostGIS
&lt;/h2&gt;

&lt;p&gt;Import with &lt;code&gt;ogr2ogr&lt;/code&gt; into a &lt;code&gt;buildings&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ogr2ogr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; PostgreSQL &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"PG:host=HOST port=PORT dbname=DB user=USER password=PASSWORD sslmode=require"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  kozhikode.geojson &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-nln&lt;/span&gt; buildings &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-nlt&lt;/span&gt; MULTIPOLYGON &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-lco&lt;/span&gt; &lt;span class="nv"&gt;GEOMETRY_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;geom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important columns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;geom&lt;/code&gt; — &lt;code&gt;MULTIPOLYGON&lt;/code&gt; footprint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;building_height&lt;/code&gt; — used for shadow length&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add a spatial index so intersection queries stay fast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;buildings_geom_idx&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;buildings&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having the index is not enough — the query must filter on &lt;code&gt;geom&lt;/code&gt; (geometry) so Postgres can use it. More on that in Step 3.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Fetching nearby buildings
&lt;/h2&gt;

&lt;p&gt;When the user finishes drawing, the client sends a GeoJSON parking polygon to the API.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;search&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ST_Buffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ST_GeomFromGeoJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;geography&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;area&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;building_height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ST_AsGeoJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;geometry&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;buildings&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;search&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;area&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;ST_Intersects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; operator checks bounding-box overlap first (fast, index-friendly). &lt;code&gt;ST_Intersects&lt;/code&gt; refines to the buffered area.&lt;/p&gt;

&lt;h3&gt;
  
  
  A performance trap I hit in production
&lt;/h3&gt;

&lt;p&gt;My first version looked reasonable but was &lt;strong&gt;disastrously slow&lt;/strong&gt; (~32 seconds on ~250k rows):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Do NOT do this if you have a GIST index on geom&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ST_Intersects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;geography&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ST_Buffer&lt;/span&gt;&lt;span class="p"&gt;(...::&lt;/span&gt;&lt;span class="n"&gt;geography&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;Why buffer?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shadows extend beyond the parking outline.&lt;/li&gt;
&lt;li&gt;A small buffer pulls in adjacent buildings whose shadows may still reach the lot.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;LIMIT&lt;/code&gt; keeps client-side union work bounded.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 4: Computing shadows in the browser
&lt;/h2&gt;

&lt;p&gt;The interesting work happens after the API responds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sun position with SunCalc
&lt;/h3&gt;

&lt;p&gt;I use &lt;strong&gt;&lt;a href="https://github.com/mourner/suncalc" rel="noopener noreferrer"&gt;SunCalc&lt;/a&gt;&lt;/strong&gt; to get sun &lt;strong&gt;altitude&lt;/strong&gt; and &lt;strong&gt;azimuth&lt;/strong&gt; for a lat/lng and timestamp.&lt;/p&gt;

&lt;p&gt;When the sun is above the horizon, shadow length follows basic trigonometry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shadow_length = building_height / tan(sun_altitude)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each building vertex is projected opposite the sun direction to form a shadow polygon.&lt;/p&gt;

&lt;h3&gt;
  
  
  Swept volumes, not just shadow outlines
&lt;/h3&gt;

&lt;p&gt;A shadow polygon alone is not enough for “was this point shaded at time &lt;em&gt;t&lt;/em&gt;?”&lt;/p&gt;

&lt;p&gt;For each building and timestamp, ShadePark builds a &lt;strong&gt;swept area&lt;/strong&gt;: the union of&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the building footprint,&lt;/li&gt;
&lt;li&gt;the shadow footprint, and&lt;/li&gt;
&lt;li&gt;quadrilateral “side faces” connecting corresponding vertices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That swept polygon represents the full region the building occludes as the sun moves between those two silhouettes for that instant.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/mfogel/polygon-clipping" rel="noopener noreferrer"&gt;&lt;code&gt;polygon-clipping&lt;/code&gt;&lt;/a&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  From building footprints to shadows
&lt;/h2&gt;

&lt;p&gt;Here is the mental model:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inputs:&lt;/strong&gt; 2D footprint + scalar height + sun vector&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Output:&lt;/strong&gt; per-cell &lt;strong&gt;% of timesteps in shade&lt;/strong&gt; over your selected window&lt;/p&gt;

&lt;p&gt;This is a deliberate simplification: we treat each building as a vertical extrusion with a flat roof — no roof pitch, no courtyards, no trees.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 5: Shade scoring with a parking grid
&lt;/h2&gt;

&lt;p&gt;Instead of testing shade continuously, the app samples the parking polygon on a &lt;strong&gt;grid&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target cell size: about &lt;strong&gt;3 meters&lt;/strong&gt; (~one car width).&lt;/li&gt;
&lt;li&gt;Resolution scales with polygon size (clamped between 8 and 80 cells per axis).&lt;/li&gt;
&lt;li&gt;Timesteps: &lt;strong&gt;every 15 minutes&lt;/strong&gt; between &lt;code&gt;fromTime&lt;/code&gt; and &lt;code&gt;toTime&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each cell center:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;score = (number of timesteps in shade) / (total timesteps)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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




&lt;h2&gt;
  
  
  Step 6: Map visualization
&lt;/h2&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;A small legend explains the score: &lt;strong&gt;percentage of the selected time range spent in shade&lt;/strong&gt;.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Data quality limitations
&lt;/h2&gt;

&lt;p&gt;GOBS is &lt;strong&gt;automatically generated&lt;/strong&gt;, not hand-surveyed. That matters for shadow apps, because shadow shape follows footprint shape.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdwk6ca0jd4zoasgptqkb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdwk6ca0jd4zoasgptqkb.png" alt="Triangular building on satellite imagery compared to rectangular Google Maps footprints" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means for ShadePark:&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Possible improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blend &lt;strong&gt;OpenStreetMap&lt;/strong&gt; building footprints with GOBS heights&lt;/li&gt;
&lt;li&gt;Cross-check multiple datasets and drop outliers&lt;/li&gt;
&lt;li&gt;Manual correction for high-traffic POIs&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Design decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why client-side computation?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Interaction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Users change time ranges frequently; the client caches buildings in memory after the first fetch so “Change Time” only recomputes shade.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hundreds of buildings × dozens of timesteps is workable in modern browsers with spatial filtering.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simplicity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The API stays a thin PostGIS lookup service.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why spatial filtering + hard limits?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ST_Buffer(..., 10)&lt;/code&gt; on geography keeps the search area accurate in meters.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;geom &amp;amp;&amp;amp; area&lt;/code&gt; ensures the GIST index is used before precise intersection.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LIMIT 500&lt;/code&gt; prevents pathological payloads in dense urban cores.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why a grid instead of continuous geometry?
&lt;/h3&gt;

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




&lt;h2&gt;
  
  
  Current limitations
&lt;/h2&gt;

&lt;p&gt;Being honest about scope helps readers trust the prototype:&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Future improvements
&lt;/h2&gt;

&lt;p&gt;Ideas on the roadmap:&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;This project was a practical tour of:&lt;/p&gt;

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

&lt;p&gt;The biggest takeaway: &lt;strong&gt;simple physics + decent open data + smart architecture&lt;/strong&gt; can produce a surprisingly useful map tool without a heavy GIS backend.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does ShadePark work outside Kozhikode?
&lt;/h3&gt;

&lt;p&gt;Not yet. The deployed dataset is filtered to Kozhikode. The same GOBS pipeline can be repeated for other districts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the server calculate shadows?
&lt;/h3&gt;

&lt;p&gt;No. The server returns nearby building geometry. The React client runs SunCalc and polygon unions.&lt;/p&gt;

&lt;h3&gt;
  
  
  How accurate is the shade map?
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  Why 15-minute intervals?
&lt;/h3&gt;

&lt;p&gt;A balance between smooth time coverage and client performance. The interval is easy to change in code.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when I change the time range?
&lt;/h3&gt;

&lt;p&gt;After the first analysis, tap &lt;strong&gt;Change Time&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run it locally?
&lt;/h3&gt;

&lt;p&gt;Yes. Clone the repo, import &lt;code&gt;kozhikode.geojson&lt;/code&gt; into PostGIS, configure &lt;code&gt;server/.env&lt;/code&gt;, then run the Express API and Vite client. See the &lt;a href="https://github.com/Anaskp/shadepark/blob/main/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://shadepark.vercel.app/" rel="noopener noreferrer"&gt;shadepark.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/Anaskp/shadepark" rel="noopener noreferrer"&gt;github.com/Anaskp/shadepark&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Keywords for search: parking shade map, sun shadow calculator, PostGIS building query, MapLibre heatmap, GOBS Kerala buildings, client-side geospatial, ShadePark.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>gis</category>
    </item>
  </channel>
</rss>
