<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://eduardheijkoop.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://eduardheijkoop.github.io/" rel="alternate" type="text/html" /><updated>2026-05-13T20:23:32+02:00</updated><id>https://eduardheijkoop.github.io/feed.xml</id><title type="html">Eduard Heijkoop</title><subtitle>Researcher specializing in high-resolution digital elevation models for sea level rise impact studies. Using satellite imagery and remote sensing to understand coastal vulnerability.</subtitle><author><name>Eduard Heijkoop</name></author><entry><title type="html">Getting Started with ICESat-2 ATL03 Photon Data in Python</title><link href="https://eduardheijkoop.github.io/tutorial/icesat2-getting-started/" rel="alternate" type="text/html" title="Getting Started with ICESat-2 ATL03 Photon Data in Python" /><published>2025-03-15T00:00:00+01:00</published><updated>2025-03-15T00:00:00+01:00</updated><id>https://eduardheijkoop.github.io/tutorial/icesat2-getting-started</id><content type="html" xml:base="https://eduardheijkoop.github.io/tutorial/icesat2-getting-started/"><![CDATA[<p>DISCLAIMER: THE TEXT ON THIS PAGE IS LIKELY LARGELY INCORRECT AND IS JUST A PLACEHOLDER GENERATED BY CLAUDE. THIS STATEMENT WILL BE REMOVED WHEN THIS PAGE HAS BEEN EDITED FOR ACCURACY.</p>

<p>ICESat-2 has revolutionized our ability to measure surface elevation from space. Its photon-counting LiDAR achieves decimeter-level vertical accuracy at a dense along-track sampling (~70 cm), making it ideal for mapping low-lying coastal terrain where traditional radar altimetry fails.</p>

<p>In this post, I’ll walk through the full workflow from data download to cleaned elevation profiles.</p>

<h2 id="1-data-access">1. Data Access</h2>

<p>ICESat-2 data is freely available through NASA’s <a href="https://nsidc.org/data/icesat-2">NSIDC DAAC</a>. I recommend using <code class="language-plaintext highlighter-rouge">icepyx</code> for programmatic access:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>icepyx h5py numpy pandas geopandas matplotlib
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">icepyx</span> <span class="k">as</span> <span class="n">ipx</span>

<span class="c1"># Define your region of interest and time range
</span><span class="n">region</span> <span class="o">=</span> <span class="n">ipx</span><span class="p">.</span><span class="n">Query</span><span class="p">(</span>
    <span class="n">dataset</span>      <span class="o">=</span> <span class="s">'ATL03'</span><span class="p">,</span>
    <span class="n">spatial_extent</span> <span class="o">=</span> <span class="p">[</span><span class="o">-</span><span class="mf">91.5</span><span class="p">,</span> <span class="mf">29.0</span><span class="p">,</span> <span class="o">-</span><span class="mf">89.0</span><span class="p">,</span> <span class="mf">30.5</span><span class="p">],</span>  <span class="c1"># Louisiana coast
</span>    <span class="n">date_range</span>   <span class="o">=</span> <span class="p">[</span><span class="s">'2023-01-01'</span><span class="p">,</span> <span class="s">'2023-12-31'</span><span class="p">],</span>
    <span class="n">start_time</span>   <span class="o">=</span> <span class="s">'00:00:00'</span><span class="p">,</span>
    <span class="n">end_time</span>     <span class="o">=</span> <span class="s">'23:59:59'</span>
<span class="p">)</span>

<span class="n">region</span><span class="p">.</span><span class="n">avail_granules</span><span class="p">()</span>
<span class="n">region</span><span class="p">.</span><span class="n">order_granules</span><span class="p">()</span>
<span class="n">region</span><span class="p">.</span><span class="n">download_granules</span><span class="p">(</span><span class="s">'/data/icesat2/'</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="2-reading-atl03-hdf5-files">2. Reading ATL03 HDF5 Files</h2>

<p>ATL03 stores data in a beam-organized HDF5 hierarchy. ICESat-2 has 3 pairs of beams (gt1, gt2, gt3), each with a left (l) and right (r) beam. Strong beams carry ~4× more photons than weak beams.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">h5py</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="k">def</span> <span class="nf">read_atl03_beam</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">beam</span><span class="o">=</span><span class="s">'gt1l'</span><span class="p">):</span>
    <span class="s">"""Read photon data from a single ATL03 beam."""</span>
    <span class="k">with</span> <span class="n">h5py</span><span class="p">.</span><span class="n">File</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="s">'r'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="c1"># Check if beam exists
</span>        <span class="k">if</span> <span class="n">beam</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">f</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">None</span>
        
        <span class="n">grp</span> <span class="o">=</span> <span class="n">f</span><span class="p">[</span><span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">beam</span><span class="si">}</span><span class="s">/heights'</span><span class="p">]</span>
        
        <span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">DataFrame</span><span class="p">({</span>
            <span class="s">'lon'</span><span class="p">:</span>  <span class="n">grp</span><span class="p">[</span><span class="s">'lon_ph'</span><span class="p">][:],</span>
            <span class="s">'lat'</span><span class="p">:</span>  <span class="n">grp</span><span class="p">[</span><span class="s">'lat_ph'</span><span class="p">][:],</span>
            <span class="s">'h'</span><span class="p">:</span>    <span class="n">grp</span><span class="p">[</span><span class="s">'h_ph'</span><span class="p">][:],</span>       <span class="c1"># WGS84 ellipsoidal height
</span>            <span class="s">'conf'</span><span class="p">:</span> <span class="n">grp</span><span class="p">[</span><span class="s">'signal_conf_ph'</span><span class="p">][:,</span> <span class="mi">0</span><span class="p">],</span>  <span class="c1"># land conf
</span>            <span class="s">'dist'</span><span class="p">:</span> <span class="n">grp</span><span class="p">[</span><span class="s">'dist_ph_along'</span><span class="p">][:]</span>
        <span class="p">})</span>
        
        <span class="c1"># Beam strength (needed to identify strong vs weak)
</span>        <span class="n">sc_orient</span> <span class="o">=</span> <span class="n">f</span><span class="p">[</span><span class="s">'orbit_info/sc_orient'</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span>
        <span class="n">beam_type</span> <span class="o">=</span> <span class="n">f</span><span class="p">[</span><span class="sa">f</span><span class="s">'</span><span class="si">{</span><span class="n">beam</span><span class="si">}</span><span class="s">/geolocation/beam_type'</span><span class="p">][</span><span class="mi">0</span><span class="p">].</span><span class="n">decode</span><span class="p">()</span>
        <span class="n">df</span><span class="p">[</span><span class="s">'beam'</span><span class="p">]</span> <span class="o">=</span> <span class="n">beam</span>
        <span class="n">df</span><span class="p">[</span><span class="s">'beam_type'</span><span class="p">]</span> <span class="o">=</span> <span class="n">beam_type</span>
        
    <span class="k">return</span> <span class="n">df</span>
</code></pre></div></div>

<h2 id="3-filtering-signal-photons">3. Filtering Signal Photons</h2>

<p>ATL03 provides a confidence flag (0–4) for land surface photons. For bare coastal terrain, <code class="language-plaintext highlighter-rouge">conf &gt;= 3</code> works well. In vegetated areas, you may want to use ATL08 land/veg classification.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">filter_signal_photons</span><span class="p">(</span><span class="n">df</span><span class="p">,</span> <span class="n">conf_min</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">h_min</span><span class="o">=-</span><span class="mi">10</span><span class="p">,</span> <span class="n">h_max</span><span class="o">=</span><span class="mi">50</span><span class="p">):</span>
    <span class="s">"""
    Filter to likely ground photons.
    
    conf_min : minimum confidence (3 = medium, 4 = high)
    h_min/h_max : rough elevation range to exclude noise
    """</span>
    <span class="n">mask</span> <span class="o">=</span> <span class="p">(</span>
        <span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="s">'conf'</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="n">conf_min</span><span class="p">)</span> <span class="o">&amp;</span>
        <span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="s">'h'</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">h_min</span><span class="p">)</span> <span class="o">&amp;</span>
        <span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="s">'h'</span><span class="p">]</span> <span class="o">&lt;</span> <span class="n">h_max</span><span class="p">)</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">df</span><span class="p">[</span><span class="n">mask</span><span class="p">].</span><span class="n">copy</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="4-converting-to-mean-sea-level">4. Converting to Mean Sea Level</h2>

<p>ATL03 heights are referenced to the <strong>WGS84 ellipsoid</strong>. For coastal studies, you need heights above a geoid (e.g., EGM2008 or GEOID18).</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pyproj</span> <span class="kn">import</span> <span class="n">Transformer</span><span class="p">,</span> <span class="n">CRS</span>

<span class="k">def</span> <span class="nf">ellipsoid_to_msl</span><span class="p">(</span><span class="n">df</span><span class="p">,</span> <span class="n">geoid_grid_path</span><span class="p">):</span>
    <span class="s">"""Subtract geoid undulation from ellipsoidal heights."""</span>
    <span class="kn">import</span> <span class="nn">rasterio</span>
    <span class="kn">from</span> <span class="nn">rasterio.sample</span> <span class="kn">import</span> <span class="n">sample_gen</span>
    
    <span class="k">with</span> <span class="n">rasterio</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">geoid_grid_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">src</span><span class="p">:</span>
        <span class="n">coords</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="s">'lon'</span><span class="p">],</span> <span class="n">df</span><span class="p">[</span><span class="s">'lat'</span><span class="p">]))</span>
        <span class="n">geoid_N</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">val</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">src</span><span class="p">.</span><span class="n">sample</span><span class="p">(</span><span class="n">coords</span><span class="p">)])</span>
    
    <span class="n">df</span><span class="p">[</span><span class="s">'h_msl'</span><span class="p">]</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s">'h'</span><span class="p">]</span> <span class="o">-</span> <span class="n">geoid_N</span>
    <span class="k">return</span> <span class="n">df</span>
</code></pre></div></div>

<h2 id="5-visualizing-the-profile">5. Visualizing the Profile</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>

<span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">14</span><span class="p">,</span> <span class="mi">7</span><span class="p">),</span> <span class="n">sharex</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="k">for</span> <span class="n">beam</span><span class="p">,</span> <span class="n">color</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">([</span><span class="s">'gt1l'</span><span class="p">,</span><span class="s">'gt1r'</span><span class="p">],</span> <span class="p">[</span><span class="s">'#2ecc71'</span><span class="p">,</span><span class="s">'#3498db'</span><span class="p">]):</span>
    <span class="n">df_beam</span> <span class="o">=</span> <span class="n">read_atl03_beam</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">beam</span><span class="p">)</span>
    <span class="n">df_filt</span> <span class="o">=</span> <span class="n">filter_signal_photons</span><span class="p">(</span><span class="n">df_beam</span><span class="p">)</span>
    
    <span class="c1"># All photons (noise)
</span>    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">scatter</span><span class="p">(</span><span class="n">df_beam</span><span class="p">[</span><span class="s">'dist'</span><span class="p">],</span> <span class="n">df_beam</span><span class="p">[</span><span class="s">'h'</span><span class="p">],</span> 
                    <span class="n">s</span><span class="o">=</span><span class="mf">0.3</span><span class="p">,</span> <span class="n">c</span><span class="o">=</span><span class="s">'gray'</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>
    <span class="c1"># Signal photons
</span>    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">scatter</span><span class="p">(</span><span class="n">df_filt</span><span class="p">[</span><span class="s">'dist'</span><span class="p">],</span> <span class="n">df_filt</span><span class="p">[</span><span class="s">'h'</span><span class="p">],</span>
                    <span class="n">s</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">c</span><span class="o">=</span><span class="n">color</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="n">beam</span><span class="p">)</span>

<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">set_ylabel</span><span class="p">(</span><span class="s">'Height (m, WGS84)'</span><span class="p">)</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">legend</span><span class="p">()</span>
<span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">set_title</span><span class="p">(</span><span class="s">'ICESat-2 ATL03 — Louisiana Coast'</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">tight_layout</span><span class="p">()</span>
<span class="n">plt</span><span class="p">.</span><span class="n">savefig</span><span class="p">(</span><span class="s">'icesat2_profile.png'</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="next-steps">Next Steps</h2>

<p>In the <a href="/blog/dem-correction-from-icesat2/">next post</a>, I’ll show how to use these filtered photon profiles to correct biases in existing DEMs (TanDEM-X, Copernicus) over coastal lowlands.</p>

<hr />

<p><strong>Code:</strong> All scripts from this post are available in the <a href="https://github.com/EduardHeijkoop"><code class="language-plaintext highlighter-rouge">icesat2-tools</code></a> repository on GitHub.</p>]]></content><author><name>Eduard Heijkoop</name></author><category term="Tutorial" /><category term="ICESat-2" /><category term="Python" /><category term="LiDAR" /><category term="HDF5" /><summary type="html"><![CDATA[A step-by-step guide to downloading, reading, and filtering ICESat-2 ATL03 photon data for coastal elevation studies.]]></summary></entry><entry><title type="html">Why Your Coastal DEM Is Wrong (And How to Fix It)</title><link href="https://eduardheijkoop.github.io/research/dem-bias-coastal/" rel="alternate" type="text/html" title="Why Your Coastal DEM Is Wrong (And How to Fix It)" /><published>2025-01-20T00:00:00+01:00</published><updated>2025-01-20T00:00:00+01:00</updated><id>https://eduardheijkoop.github.io/research/dem-bias-coastal</id><content type="html" xml:base="https://eduardheijkoop.github.io/research/dem-bias-coastal/"><![CDATA[<p>DISCLAIMER: THE TEXT ON THIS PAGE IS LIKELY LARGELY INCORRECT AND IS JUST A PLACEHOLDER GENERATED BY CLAUDE. THIS STATEMENT WILL BE REMOVED WHEN THIS PAGE HAS BEEN EDITED FOR ACCURACY.</p>

<p>If you’ve ever run a sea level rise inundation model using SRTM or even the Copernicus DEM, you’ve almost certainly overestimated how many people live in flood zones. Here’s why — and what to do about it.</p>

<h2 id="the-problem-dems-measure-canopy-not-ground">The Problem: DEMs Measure Canopy, Not Ground</h2>

<p>Radar-based DEMs like SRTM (C-band, 2000) and TanDEM-X (X-band, 2010–2015) measure the <strong>first radar return</strong> — which over vegetated terrain is the <strong>top of the canopy</strong>, not the ground.</p>

<p>In flat coastal lowlands dominated by mangroves, rice paddies, and coastal scrub, this canopy bias can reach <strong>+0.5 to +2.5 m</strong>. Since we’re trying to map areas below 2 m elevation, a 1 m positive bias means:</p>

<blockquote>
  <p>We think a field is 1.8 m above sea level.<br />
It’s actually 0.8 m above sea level.<br />
Under a 1 m SLR scenario, we say it’s safe. It floods.</p>
</blockquote>

<p>This isn’t a small rounding error — it changes how many people fall in the flood zone by <strong>20–40%</strong> in heavily vegetated deltas.</p>

<h2 id="where-the-bias-is-largest">Where the Bias Is Largest</h2>

<p>We can quantify this bias by comparing DEMs to ICESat-2 ground photons (which measure actual ground elevation):</p>

<table>
  <thead>
    <tr>
      <th>Land Cover</th>
      <th>SRTM Bias</th>
      <th>Copernicus Bias</th>
      <th>TanDEM-X Bias</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bare soil / beach</td>
      <td>+0.1 m</td>
      <td>+0.05 m</td>
      <td>+0.08 m</td>
    </tr>
    <tr>
      <td>Low crops</td>
      <td>+0.4 m</td>
      <td>+0.3 m</td>
      <td>+0.2 m</td>
    </tr>
    <tr>
      <td>Mangrove</td>
      <td>+2.1 m</td>
      <td>+1.8 m</td>
      <td>+1.4 m</td>
    </tr>
    <tr>
      <td>Rice paddy</td>
      <td>+0.7 m</td>
      <td>+0.5 m</td>
      <td>+0.35 m</td>
    </tr>
    <tr>
      <td>Urban (low-rise)</td>
      <td>+0.3 m</td>
      <td>+0.2 m</td>
      <td>+0.15 m</td>
    </tr>
  </tbody>
</table>

<p>X-band (TanDEM-X) penetrates vegetation better than C-band (SRTM), hence lower bias — but it’s still significant.</p>

<h2 id="the-fix-icesat-2-based-bias-correction">The Fix: ICESat-2-Based Bias Correction</h2>

<p>The cleanest solution is to use <strong>ICESat-2 ATL08</strong> ground photons as a reference surface and correct the DEM locally.</p>

<h3 id="step-1-extract-dem-values-at-icesat-2-locations">Step 1: Extract DEM values at ICESat-2 locations</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">rasterio</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>
<span class="kn">from</span> <span class="nn">rasterio.sample</span> <span class="kn">import</span> <span class="n">sample_gen</span>

<span class="k">def</span> <span class="nf">sample_dem_at_icesat2</span><span class="p">(</span><span class="n">df_icesat2</span><span class="p">,</span> <span class="n">dem_path</span><span class="p">):</span>
    <span class="s">"""Sample DEM elevation at ICESat-2 photon locations."""</span>
    <span class="k">with</span> <span class="n">rasterio</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">dem_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">src</span><span class="p">:</span>
        <span class="n">coords</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">df_icesat2</span><span class="p">[</span><span class="s">'lon'</span><span class="p">],</span> <span class="n">df_icesat2</span><span class="p">[</span><span class="s">'lat'</span><span class="p">]))</span>
        <span class="n">dem_vals</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">v</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">sample_gen</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">coords</span><span class="p">)])</span>
    
    <span class="n">df_icesat2</span><span class="p">[</span><span class="s">'h_dem'</span><span class="p">]</span> <span class="o">=</span> <span class="n">dem_vals</span>
    <span class="n">df_icesat2</span><span class="p">[</span><span class="s">'residual'</span><span class="p">]</span> <span class="o">=</span> <span class="n">df_icesat2</span><span class="p">[</span><span class="s">'h_msl'</span><span class="p">]</span> <span class="o">-</span> <span class="n">dem_vals</span>  <span class="c1"># ICESat-2 minus DEM
</span>    <span class="k">return</span> <span class="n">df_icesat2</span>
</code></pre></div></div>

<h3 id="step-2-stratify-by-land-cover">Step 2: Stratify by land cover</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">geopandas</span> <span class="k">as</span> <span class="n">gpd</span>

<span class="k">def</span> <span class="nf">stratify_by_landcover</span><span class="p">(</span><span class="n">df</span><span class="p">,</span> <span class="n">landcover_path</span><span class="p">):</span>
    <span class="s">"""Assign ESA WorldCover class to each ICESat-2 point."""</span>
    <span class="k">with</span> <span class="n">rasterio</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">landcover_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">src</span><span class="p">:</span>
        <span class="n">coords</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">zip</span><span class="p">(</span><span class="n">df</span><span class="p">[</span><span class="s">'lon'</span><span class="p">],</span> <span class="n">df</span><span class="p">[</span><span class="s">'lat'</span><span class="p">]))</span>
        <span class="n">lc</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="n">v</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">sample_gen</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">coords</span><span class="p">)])</span>
    <span class="n">df</span><span class="p">[</span><span class="s">'landcover'</span><span class="p">]</span> <span class="o">=</span> <span class="n">lc</span>
    <span class="k">return</span> <span class="n">df</span>

<span class="c1"># Compute bias per land cover class
</span><span class="n">bias_by_lc</span> <span class="o">=</span> <span class="p">(</span>
    <span class="n">df</span><span class="p">.</span><span class="n">groupby</span><span class="p">(</span><span class="s">'landcover'</span><span class="p">)[</span><span class="s">'residual'</span><span class="p">]</span>
    <span class="p">.</span><span class="n">agg</span><span class="p">([</span><span class="s">'mean'</span><span class="p">,</span> <span class="s">'std'</span><span class="p">,</span> <span class="s">'count'</span><span class="p">])</span>
    <span class="p">.</span><span class="n">rename</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">{</span><span class="s">'mean'</span><span class="p">:</span> <span class="s">'bias'</span><span class="p">})</span>
<span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">bias_by_lc</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="step-3-apply-spatially-varying-correction">Step 3: Apply spatially varying correction</h3>

<p>Rather than a single global offset, we apply a <strong>kriged correction surface</strong> — essentially spatial interpolation of the residuals:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pykrige.ok</span> <span class="kn">import</span> <span class="n">OrdinaryKriging</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="k">def</span> <span class="nf">kriging_correction</span><span class="p">(</span><span class="n">df_residuals</span><span class="p">,</span> <span class="n">target_lon</span><span class="p">,</span> <span class="n">target_lat</span><span class="p">):</span>
    <span class="s">"""
    Spatially interpolate DEM bias using ordinary kriging.
    """</span>
    <span class="n">OK</span> <span class="o">=</span> <span class="n">OrdinaryKriging</span><span class="p">(</span>
        <span class="n">df_residuals</span><span class="p">[</span><span class="s">'lon'</span><span class="p">].</span><span class="n">values</span><span class="p">,</span>
        <span class="n">df_residuals</span><span class="p">[</span><span class="s">'lat'</span><span class="p">].</span><span class="n">values</span><span class="p">,</span>
        <span class="n">df_residuals</span><span class="p">[</span><span class="s">'residual'</span><span class="p">].</span><span class="n">values</span><span class="p">,</span>
        <span class="n">variogram_model</span><span class="o">=</span><span class="s">'spherical'</span><span class="p">,</span>
        <span class="n">verbose</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
        <span class="n">enable_plotting</span><span class="o">=</span><span class="bp">False</span>
    <span class="p">)</span>
    
    <span class="n">z_correction</span><span class="p">,</span> <span class="n">ss</span> <span class="o">=</span> <span class="n">OK</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span>
        <span class="s">'grid'</span><span class="p">,</span> <span class="n">target_lon</span><span class="p">,</span> <span class="n">target_lat</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">z_correction</span>
</code></pre></div></div>

<h3 id="step-4-apply-to-raster">Step 4: Apply to raster</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">rasterio</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>

<span class="k">def</span> <span class="nf">apply_dem_correction</span><span class="p">(</span><span class="n">dem_path</span><span class="p">,</span> <span class="n">correction_grid</span><span class="p">,</span> <span class="n">output_path</span><span class="p">):</span>
    <span class="s">"""Add correction surface to DEM raster."""</span>
    <span class="k">with</span> <span class="n">rasterio</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">dem_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">src</span><span class="p">:</span>
        <span class="n">dem</span> <span class="o">=</span> <span class="n">src</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="mi">1</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="nb">float</span><span class="p">)</span>
        <span class="n">profile</span> <span class="o">=</span> <span class="n">src</span><span class="p">.</span><span class="n">profile</span>
        <span class="n">nodata</span> <span class="o">=</span> <span class="n">src</span><span class="p">.</span><span class="n">nodata</span>
    
    <span class="n">dem_corrected</span> <span class="o">=</span> <span class="n">dem</span> <span class="o">+</span> <span class="n">correction_grid</span>
    <span class="n">dem_corrected</span><span class="p">[</span><span class="n">dem</span> <span class="o">==</span> <span class="n">nodata</span><span class="p">]</span> <span class="o">=</span> <span class="n">nodata</span>
    
    <span class="n">profile</span><span class="p">.</span><span class="n">update</span><span class="p">(</span><span class="n">dtype</span><span class="o">=</span><span class="s">'float32'</span><span class="p">)</span>
    <span class="k">with</span> <span class="n">rasterio</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">output_path</span><span class="p">,</span> <span class="s">'w'</span><span class="p">,</span> <span class="o">**</span><span class="n">profile</span><span class="p">)</span> <span class="k">as</span> <span class="n">dst</span><span class="p">:</span>
        <span class="n">dst</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">dem_corrected</span><span class="p">.</span><span class="n">astype</span><span class="p">(</span><span class="s">'float32'</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="validation-results">Validation Results</h2>

<p>After correction, RMSE against independent airborne LiDAR drops from:</p>

<ul>
  <li><strong>SRTM</strong>: 0.89 m → 0.21 m RMSE</li>
  <li><strong>Copernicus DEM</strong>: 0.63 m → 0.17 m RMSE</li>
</ul>

<p>And flood exposure changes substantially:</p>

<blockquote>
  <p>In the Mekong Delta at 1 m SLR:</p>
  <ul>
    <li>Uncorrected Copernicus: 14.2 million exposed</li>
    <li>Corrected DEM: 9.8 million exposed</li>
    <li><strong>−31% difference</strong></li>
  </ul>
</blockquote>

<h2 id="takeaway">Takeaway</h2>

<p>If you’re using off-the-shelf DEMs for coastal flood modeling without vegetation bias correction, your results are likely significantly overestimating exposure in vegetated delta regions. The good news: with ICESat-2 data (free, global coverage) and a few hundred lines of Python, you can substantially improve accuracy.</p>

<p><strong>Next post:</strong> I’ll show how to build a production pipeline that processes all ICESat-2 tracks over a region automatically and generates a corrected DEM tile mosaic.</p>

<hr />

<p><em>Questions? Open an issue or discussion on the <a href="https://github.com/EduardHeijkoop">GitHub repo</a>.</em></p>]]></content><author><name>Eduard Heijkoop</name></author><category term="Research" /><category term="DEM" /><category term="Coastal" /><category term="Bias Correction" /><category term="Sea Level Rise" /><summary type="html"><![CDATA[Exploring the systematic elevation biases in SRTM and Copernicus DEMs over coastal lowlands — and why they cause us to overestimate flood exposure.]]></summary></entry><entry><title type="html">Interactive Coastal Maps with Python Folium</title><link href="https://eduardheijkoop.github.io/tutorial/folium-coastal-maps/" rel="alternate" type="text/html" title="Interactive Coastal Maps with Python Folium" /><published>2024-11-05T00:00:00+01:00</published><updated>2024-11-05T00:00:00+01:00</updated><id>https://eduardheijkoop.github.io/tutorial/folium-coastal-maps</id><content type="html" xml:base="https://eduardheijkoop.github.io/tutorial/folium-coastal-maps/"><![CDATA[<p>DISCLAIMER: THE TEXT ON THIS PAGE IS LIKELY LARGELY INCORRECT AND IS JUST A PLACEHOLDER GENERATED BY CLAUDE. THIS STATEMENT WILL BE REMOVED WHEN THIS PAGE HAS BEEN EDITED FOR ACCURACY.</p>

<p>Static maps are great for publications, but when you’re exploring coastal flood extents or satellite coverage, <strong>interactive maps</strong> are a game-changer. In this post I’ll share my Folium workflow for creating the kinds of embedded maps you see throughout this portfolio.</p>

<h2 id="why-folium">Why Folium?</h2>

<ul>
  <li>Pure Python (no JavaScript knowledge needed)</li>
  <li>Outputs self-contained HTML — easy to embed anywhere</li>
  <li>Built on Leaflet.js — smooth, professional maps</li>
  <li>Works seamlessly with GeoPandas / Shapely geometries</li>
  <li>Great tile options: OpenStreetMap, CartoDB, Stamen, Esri</li>
</ul>

<h2 id="basic-setup">Basic Setup</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pip</span> <span class="n">install</span> <span class="n">folium</span> <span class="n">geopandas</span> <span class="n">branca</span>
</code></pre></div></div>

<h2 id="1-a-simple-dem-coverage-map">1. A Simple DEM Coverage Map</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">folium</span>
<span class="kn">from</span> <span class="nn">folium</span> <span class="kn">import</span> <span class="n">plugins</span>

<span class="c1"># Base map — dark tiles look great for scientific data
</span><span class="n">m</span> <span class="o">=</span> <span class="n">folium</span><span class="p">.</span><span class="n">Map</span><span class="p">(</span>
    <span class="n">location</span><span class="o">=</span><span class="p">[</span><span class="mf">10.5</span><span class="p">,</span> <span class="mf">106.0</span><span class="p">],</span>   <span class="c1"># Mekong Delta center
</span>    <span class="n">zoom_start</span><span class="o">=</span><span class="mi">8</span><span class="p">,</span>
    <span class="n">tiles</span><span class="o">=</span><span class="s">'CartoDB dark_matter'</span><span class="p">,</span>
    <span class="n">width</span><span class="o">=</span><span class="s">'100%'</span><span class="p">,</span>
    <span class="n">height</span><span class="o">=</span><span class="s">'100%'</span>
<span class="p">)</span>

<span class="c1"># Add a scale bar
</span><span class="n">plugins</span><span class="p">.</span><span class="n">MeasureControl</span><span class="p">(</span><span class="n">position</span><span class="o">=</span><span class="s">'bottomleft'</span><span class="p">).</span><span class="n">add_to</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>

<span class="c1"># Add fullscreen button
</span><span class="n">plugins</span><span class="p">.</span><span class="n">Fullscreen</span><span class="p">().</span><span class="n">add_to</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>

<span class="n">m</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="s">'mekong_base.html'</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="2-adding-icesat-2-ground-tracks">2. Adding ICESat-2 Ground Tracks</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">geopandas</span> <span class="k">as</span> <span class="n">gpd</span>
<span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="n">pd</span>

<span class="k">def</span> <span class="nf">add_icesat2_tracks</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">tracks_gdf</span><span class="p">,</span> <span class="n">colormap</span><span class="o">=</span><span class="s">'YlOrRd'</span><span class="p">):</span>
    <span class="s">"""Add ICESat-2 ground tracks colored by acquisition date."""</span>
    <span class="kn">from</span> <span class="nn">branca.colormap</span> <span class="kn">import</span> <span class="n">linear</span>
    
    <span class="c1"># Create date colormap
</span>    <span class="n">dates</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">to_datetime</span><span class="p">(</span><span class="n">tracks_gdf</span><span class="p">[</span><span class="s">'date'</span><span class="p">])</span>
    <span class="n">date_num</span> <span class="o">=</span> <span class="p">(</span><span class="n">dates</span> <span class="o">-</span> <span class="n">dates</span><span class="p">.</span><span class="nb">min</span><span class="p">()).</span><span class="n">dt</span><span class="p">.</span><span class="n">days</span>
    
    <span class="n">cmap</span> <span class="o">=</span> <span class="n">linear</span><span class="p">.</span><span class="n">YlOrRd_09</span><span class="p">.</span><span class="n">scale</span><span class="p">(</span><span class="n">date_num</span><span class="p">.</span><span class="nb">min</span><span class="p">(),</span> <span class="n">date_num</span><span class="p">.</span><span class="nb">max</span><span class="p">())</span>
    <span class="n">cmap</span><span class="p">.</span><span class="n">caption</span> <span class="o">=</span> <span class="s">'ICESat-2 Acquisition Date'</span>
    
    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">tracks_gdf</span><span class="p">.</span><span class="n">iterrows</span><span class="p">():</span>
        <span class="n">d_num</span> <span class="o">=</span> <span class="p">(</span><span class="n">pd</span><span class="p">.</span><span class="n">to_datetime</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="s">'date'</span><span class="p">])</span> <span class="o">-</span> <span class="n">dates</span><span class="p">.</span><span class="nb">min</span><span class="p">()).</span><span class="n">days</span>
        <span class="n">color</span> <span class="o">=</span> <span class="n">cmap</span><span class="p">(</span><span class="n">d_num</span><span class="p">)</span>
        
        <span class="n">folium</span><span class="p">.</span><span class="n">GeoJson</span><span class="p">(</span>
            <span class="n">row</span><span class="p">.</span><span class="n">geometry</span><span class="p">,</span>
            <span class="n">style_function</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">,</span> <span class="n">c</span><span class="o">=</span><span class="n">color</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">'color'</span><span class="p">:</span> <span class="n">c</span><span class="p">,</span>
                <span class="s">'weight'</span><span class="p">:</span> <span class="mf">1.5</span><span class="p">,</span>
                <span class="s">'opacity'</span><span class="p">:</span> <span class="mf">0.8</span>
            <span class="p">},</span>
            <span class="n">tooltip</span><span class="o">=</span><span class="sa">f</span><span class="s">"Date: </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="s">'date'</span><span class="p">]</span><span class="si">}</span><span class="s">&lt;br&gt;Beam: </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="s">'beam'</span><span class="p">]</span><span class="si">}</span><span class="s">"</span>
        <span class="p">).</span><span class="n">add_to</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
    
    <span class="n">cmap</span><span class="p">.</span><span class="n">add_to</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">m</span>
</code></pre></div></div>

<h2 id="3-choropleth-exposed-population-by-province">3. Choropleth: Exposed Population by Province</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">json</span>

<span class="k">def</span> <span class="nf">add_population_choropleth</span><span class="p">(</span><span class="n">m</span><span class="p">,</span> <span class="n">provinces_gdf</span><span class="p">,</span> <span class="n">pop_column</span><span class="p">):</span>
    <span class="s">"""Add population exposure choropleth."""</span>
    <span class="kn">import</span> <span class="nn">branca.colormap</span> <span class="k">as</span> <span class="n">cm</span>
    
    <span class="n">colormap</span> <span class="o">=</span> <span class="n">cm</span><span class="p">.</span><span class="n">LinearColormap</span><span class="p">(</span>
        <span class="n">colors</span><span class="o">=</span><span class="p">[</span><span class="s">'#ffffcc'</span><span class="p">,</span> <span class="s">'#fd8d3c'</span><span class="p">,</span> <span class="s">'#800026'</span><span class="p">],</span>
        <span class="n">vmin</span><span class="o">=</span><span class="n">provinces_gdf</span><span class="p">[</span><span class="n">pop_column</span><span class="p">].</span><span class="nb">min</span><span class="p">(),</span>
        <span class="n">vmax</span><span class="o">=</span><span class="n">provinces_gdf</span><span class="p">[</span><span class="n">pop_column</span><span class="p">].</span><span class="nb">max</span><span class="p">(),</span>
        <span class="n">caption</span><span class="o">=</span><span class="sa">f</span><span class="s">'Exposed Population (</span><span class="si">{</span><span class="n">pop_column</span><span class="si">}</span><span class="s">)'</span>
    <span class="p">)</span>
    
    <span class="n">folium</span><span class="p">.</span><span class="n">GeoJson</span><span class="p">(</span>
        <span class="n">provinces_gdf</span><span class="p">,</span>
        <span class="n">style_function</span><span class="o">=</span><span class="k">lambda</span> <span class="n">feature</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">'fillColor'</span><span class="p">:</span> <span class="n">colormap</span><span class="p">(</span>
                <span class="n">feature</span><span class="p">[</span><span class="s">'properties'</span><span class="p">][</span><span class="n">pop_column</span><span class="p">]</span> <span class="ow">or</span> <span class="mi">0</span>
            <span class="p">),</span>
            <span class="s">'color'</span><span class="p">:</span> <span class="s">'white'</span><span class="p">,</span>
            <span class="s">'weight'</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">,</span>
            <span class="s">'fillOpacity'</span><span class="p">:</span> <span class="mf">0.7</span>
        <span class="p">},</span>
        <span class="n">tooltip</span><span class="o">=</span><span class="n">folium</span><span class="p">.</span><span class="n">GeoJsonTooltip</span><span class="p">(</span>
            <span class="n">fields</span><span class="o">=</span><span class="p">[</span><span class="s">'province'</span><span class="p">,</span> <span class="n">pop_column</span><span class="p">],</span>
            <span class="n">aliases</span><span class="o">=</span><span class="p">[</span><span class="s">'Province'</span><span class="p">,</span> <span class="s">'Exposed Population'</span><span class="p">],</span>
            <span class="n">localize</span><span class="o">=</span><span class="bp">True</span>
        <span class="p">)</span>
    <span class="p">).</span><span class="n">add_to</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
    
    <span class="n">colormap</span><span class="p">.</span><span class="n">add_to</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">m</span>
</code></pre></div></div>

<h2 id="4-layer-control-for-slr-scenarios">4. Layer Control for SLR Scenarios</h2>

<p>The key to a good SLR comparison map is <code class="language-plaintext highlighter-rouge">FeatureGroup</code> + <code class="language-plaintext highlighter-rouge">LayerControl</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">folium</span>
<span class="kn">from</span> <span class="nn">folium</span> <span class="kn">import</span> <span class="n">FeatureGroup</span><span class="p">,</span> <span class="n">LayerControl</span>

<span class="k">def</span> <span class="nf">create_slr_comparison_map</span><span class="p">(</span><span class="n">flood_polygons_dict</span><span class="p">,</span> <span class="n">center</span><span class="p">):</span>
    <span class="s">"""
    flood_polygons_dict: {'0.5m': GeoDataFrame, '1.0m': GDF, '2.0m': GDF}
    """</span>
    <span class="n">m</span> <span class="o">=</span> <span class="n">folium</span><span class="p">.</span><span class="n">Map</span><span class="p">(</span><span class="n">location</span><span class="o">=</span><span class="n">center</span><span class="p">,</span> <span class="n">zoom_start</span><span class="o">=</span><span class="mi">9</span><span class="p">,</span>
                   <span class="n">tiles</span><span class="o">=</span><span class="s">'CartoDB positron'</span><span class="p">)</span>
    
    <span class="n">scenario_colors</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">'0.5m'</span><span class="p">:</span> <span class="p">{</span><span class="s">'fill'</span><span class="p">:</span> <span class="s">'#3498db'</span><span class="p">,</span> <span class="s">'label'</span><span class="p">:</span> <span class="s">'SLR +0.5 m'</span><span class="p">},</span>
        <span class="s">'1.0m'</span><span class="p">:</span> <span class="p">{</span><span class="s">'fill'</span><span class="p">:</span> <span class="s">'#e67e22'</span><span class="p">,</span> <span class="s">'label'</span><span class="p">:</span> <span class="s">'SLR +1.0 m'</span><span class="p">},</span>
        <span class="s">'2.0m'</span><span class="p">:</span> <span class="p">{</span><span class="s">'fill'</span><span class="p">:</span> <span class="s">'#e74c3c'</span><span class="p">,</span> <span class="s">'label'</span><span class="p">:</span> <span class="s">'SLR +2.0 m'</span><span class="p">},</span>
    <span class="p">}</span>
    
    <span class="k">for</span> <span class="n">scenario</span><span class="p">,</span> <span class="n">gdf</span> <span class="ow">in</span> <span class="n">flood_polygons_dict</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="n">cfg</span> <span class="o">=</span> <span class="n">scenario_colors</span><span class="p">[</span><span class="n">scenario</span><span class="p">]</span>
        <span class="n">fg</span> <span class="o">=</span> <span class="n">FeatureGroup</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="n">cfg</span><span class="p">[</span><span class="s">'label'</span><span class="p">],</span> <span class="n">show</span><span class="o">=</span><span class="p">(</span><span class="n">scenario</span> <span class="o">==</span> <span class="s">'1.0m'</span><span class="p">))</span>
        
        <span class="n">folium</span><span class="p">.</span><span class="n">GeoJson</span><span class="p">(</span>
            <span class="n">gdf</span><span class="p">.</span><span class="n">__geo_interface__</span><span class="p">,</span>
            <span class="n">style_function</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">,</span> <span class="n">c</span><span class="o">=</span><span class="n">cfg</span><span class="p">[</span><span class="s">'fill'</span><span class="p">]:</span> <span class="p">{</span>
                <span class="s">'fillColor'</span><span class="p">:</span> <span class="n">c</span><span class="p">,</span>
                <span class="s">'color'</span><span class="p">:</span> <span class="n">c</span><span class="p">,</span>
                <span class="s">'fillOpacity'</span><span class="p">:</span> <span class="mf">0.45</span><span class="p">,</span>
                <span class="s">'weight'</span><span class="p">:</span> <span class="mf">0.3</span>
            <span class="p">}</span>
        <span class="p">).</span><span class="n">add_to</span><span class="p">(</span><span class="n">fg</span><span class="p">)</span>
        
        <span class="n">fg</span><span class="p">.</span><span class="n">add_to</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
    
    <span class="n">LayerControl</span><span class="p">(</span><span class="n">collapsed</span><span class="o">=</span><span class="bp">False</span><span class="p">).</span><span class="n">add_to</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">m</span>
</code></pre></div></div>

<h2 id="5-embedding-in-jekyll--github-pages">5. Embedding in Jekyll / GitHub Pages</h2>

<p>Save the Folium map as HTML and put it in <code class="language-plaintext highlighter-rouge">assets/maps/</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>m.save<span class="o">(</span><span class="s1">'assets/maps/slr_impact.html'</span><span class="o">)</span>
</code></pre></div></div>

<p>Then in your Markdown:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;iframe</span> 
  <span class="na">src=</span><span class="s">"/assets/maps/slr_impact.html"</span> 
  <span class="na">width=</span><span class="s">"100%"</span> 
  <span class="na">height=</span><span class="s">"500"</span> 
  <span class="na">frameborder=</span><span class="s">"0"</span><span class="nt">&gt;</span>
<span class="nt">&lt;/iframe&gt;</span>
</code></pre></div></div>

<p>One gotcha: Folium generates full HTML pages with <code class="language-plaintext highlighter-rouge">&lt;html&gt;</code> and <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> tags. This works fine in iframes, but if you want inline embedding you’ll need <code class="language-plaintext highlighter-rouge">branca.element.Figure</code> to generate just the map div. For most use cases, iframes are the simplest approach.</p>

<h2 id="pro-tips">Pro Tips</h2>

<ul>
  <li>Use <code class="language-plaintext highlighter-rouge">prefer_canvas=True</code> in <code class="language-plaintext highlighter-rouge">folium.Map()</code> for large point datasets (&gt;10k points)</li>
  <li>Convert large GeoDataFrames to GeoJSON strings first — it’s faster than passing the GDF directly</li>
  <li>Use <code class="language-plaintext highlighter-rouge">folium.plugins.MarkerCluster</code> for many point markers</li>
  <li><code class="language-plaintext highlighter-rouge">CartoDB dark_matter</code> and <code class="language-plaintext highlighter-rouge">CartoDB positron</code> are ideal for scientific maps — clean, no clutter</li>
</ul>

<hr />

<p>All map generation scripts for this portfolio are available on <a href="https://github.com/EduardHeijkoop">GitHub</a>. Feel free to fork and adapt for your own coastal research.</p>]]></content><author><name>Eduard Heijkoop</name></author><category term="Tutorial" /><category term="Folium" /><category term="Python" /><category term="Visualization" /><category term="GIS" /><summary type="html"><![CDATA[How I use Python's Folium library to create interactive geospatial visualizations for sea level rise and DEM analysis — and embed them directly in GitHub Pages.]]></summary></entry></feed>