<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Data Dave's Blog</title><link>https://davidwhittingham.com/</link><description>Data Dave's Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 10 May 2025 09:00:00 +0200</lastBuildDate><atom:link href="https://davidwhittingham.com/index.xml" rel="self" type="application/rss+xml"/><item><title>High grade geospatial processing (feat. crazy fast ducks) 🦆🔥</title><link>https://davidwhittingham.com/posts/performance/</link><pubDate>Sat, 10 May 2025 09:00:00 +0200</pubDate><author><name>Author</name></author><guid>https://davidwhittingham.com/posts/performance/</guid><description><![CDATA[<div class="featured-image">
                <img src="/media/performance/kreuzungen-benchmark.avif" referrerpolicy="no-referrer">
            </div><h2 id="introduction-" class="headerLink">
    <a href="#introduction-" class="header-mark"></a>Introduction 👋</h2><p>So last year, I created a web app called <a href="https://kreuzungen.world" target="_blank" rel="noopener noreferrer">kreuzungen.world</a> that calculates the number of waterways crossed by a gpx route. It was a fun little project to build and led to some interesting encounters.</p>
<figure><a class="lightgallery" href="/media/performance/kreuzungen-screenrecording.avif" title="/media/performance/kreuzungen-screenrecording.avif" data-thumbnail="/media/performance/kreuzungen-screenrecording.avif" data-sub-html="<h2>Kreuzungen - the app that started it all</h2>">
        <img
            
            loading="lazy"
            src="/media/performance/kreuzungen-screenrecording.avif"
            srcset="/media/performance/kreuzungen-screenrecording.avif, /media/performance/kreuzungen-screenrecording.avif 1.5x, /media/performance/kreuzungen-screenrecording.avif 2x"
            sizes="auto"
            alt="/media/performance/kreuzungen-screenrecording.avif">
    </a><figcaption class="image-caption">Kreuzungen - the app that started it all</figcaption>
    </figure>
<p>The app was built using javascript in a way that required no backend.. Thats right, all the data fetching and processing was done in the browser. (No server costs 💸).</p>
<p>It uses the <code>Overpass API</code> to fetch the waterways data from OpenStreetMap and then uses <code>turf.js</code> to calculate the intersections between the route and the waterways. Pretty simple, right?</p>
<p>What&rsquo;s been surprisingly rewarding is seeing people actually use it! From Japan to Argentina to New Zealand to the USA (where one kayaker uses it to track river crossings). It&rsquo;s humbling to see something I made for fun, being used by people across the globe.</p>
<link href="https://unpkg.com/maplibre-gl@5.5.0/dist/maplibre-gl.css" rel="stylesheet" />
<style>
  #kreuzungen-map {
    width: 100%;
    height: 500px;
    margin-bottom: 1.5em;
    border-radius: 8px;
  }
  .maplibregl-popup {
    max-width: 300px;
    font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
  }
</style>

<div id="kreuzungen-map" class="maplibregl-map"></div>

<script src="https://unpkg.com/maplibre-gl@5.5.0/dist/maplibre-gl.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6.5.0/turf.min.js"></script>
<script>
  
  function getFlagEmoji(countryCode) {
    return countryCode.toUpperCase().replace(/./g, char =>
      String.fromCodePoint(127397 + char.charCodeAt())
    );
  }

  
  function addGlobeProjection(styleJson) {
    return {
      ...styleJson,
      projection: { type: 'globe' }
    };
  }

  
  function updateStyleJson(styleJson, countriesWaterwayData) {
    const updatedStyle = addGlobeProjection(JSON.parse(JSON.stringify(styleJson)));

    const countryColors = {};
    for (const [countryCode, data] of Object.entries(countriesWaterwayData)) {
      if (data.waterways_crossed === 0) {
        countryColors[countryCode] = 'rgba(230, 230, 250, 1)';
      } else {
        const originalLayer = styleJson.layers.find(layer => layer.id === 'countries-fill' && layer.type === 'fill');
        if (originalLayer) {
          const originalColor = originalLayer.paint['fill-color'];
          countryColors[countryCode] = originalColor;
        }
      }
    }

    updatedStyle.layers = updatedStyle.layers.map(layer => {
      if (layer.id === 'countries-fill' && layer.type === 'fill') {
        layer.paint['fill-color'] = [
          'match',
          ['get', 'ADM0_A3'],
          ...Object.entries(countryColors).flat(),
          '#EAB38F'
        ];
      }
      return layer;
    });

    return updatedStyle;
  }
  
  document.addEventListener('DOMContentLoaded', function() {
    const map = new maplibregl.Map({
      container: 'kreuzungen-map',
      center: [0, 20], 
      zoom: 1.2, 
    });
    
    
    function adjustMapForScreenSize() {
      const width = window.innerWidth;
      if (width < 480) {
        map.setZoom(0.8); 
      } else if (width < 768) {
        map.setZoom(1.0); 
      } else {
        map.setZoom(1.2); 
      }
    }
    
    
    map.on('load', adjustMapForScreenSize);
    
    
    window.addEventListener('resize', adjustMapForScreenSize);
    
    
    let isActive = false;
    let rotationInterval;
    
    
    function startRotation() {
      stopRotation(); 
      isActive = false;
      rotationInterval = setInterval(() => {
        if (!isActive) {
          const currentCenter = map.getCenter();
          map.easeTo({
            center: [currentCenter.lng + 2, currentCenter.lat],
            duration: 1000,
            easing: t => t
          });
        }
      }, 300);
    }
    
    
    function stopRotation() {
      isActive = true;
      if (rotationInterval) {
        clearInterval(rotationInterval);
        rotationInterval = null;
      }
    }
    
    
    function resetViewAndRotate() {
      adjustMapForScreenSize(); 
      startRotation();
    }
    
    
    map.on('mousedown', stopRotation);
    map.on('touchstart', stopRotation);
    map.on('dragstart', stopRotation);
    
    
    document.addEventListener('click', (e) => {
      if (!document.getElementById('kreuzungen-map').contains(e.target)) {
        resetViewAndRotate();
      }
    });
    
    
    function checkVisibility() {
      const mapElement = document.getElementById('kreuzungen-map');
      const rect = mapElement.getBoundingClientRect();
      const isVisible = (
        rect.top >= -rect.height &&
        rect.left >= -rect.width &&
        rect.bottom <= (window.innerHeight + rect.height) &&
        rect.right <= (window.innerWidth + rect.width)
      );
      
      if (!isVisible && !isActive) {
        resetViewAndRotate();
      }
    }
    
    
    document.addEventListener('scroll', checkVisibility);
    
    
    map.on('load', startRotation);

    fetch('https://demotiles.maplibre.org/style.json')
      .then(response => response.json())
      .then(customStyle => {
        fetch('https://fly.storage.tigris.dev/hydro-xpid/modelled/country.json')
          .then(response => response.text())
          .then(text => {
            try {
              const data = text.split('\n').filter(line => line.trim() !== '').map(line => JSON.parse(line));
              const waterwaysDict = data.reduce((acc, country) => {
                acc[country.country_code_3] = {
                  waterways_crossed: country.waterway_realtions_crossed,
                  unique_waterways_crossed: country.unique_waterway_realtions_crossed,
                  country_name: country.country,
                  most_popular_waterway: country.most_popular_waterway || 'N/A',
                  country_code_2: country.country_code_2
                };
                return acc;
              }, {});

              updatedStyle = updateStyleJson(customStyle, waterwaysDict);
              map.setStyle(updatedStyle);

              map.on('load', () => {
                let hoveredCountryId = null;
                const popup = new maplibregl.Popup({
                  closeButton: false,
                  closeOnClick: false
                });

                map.on('mousemove', 'countries-fill', (e) => {
                  if (e.features.length > 0) {
                    if (hoveredCountryId) {
                      map.setFeatureState(
                        { source: 'maplibre', sourceLayer: 'countries', id: hoveredCountryId },
                        { hover: false }
                      );
                    }
                    hoveredCountryId = e.features[0].id;
                    map.setFeatureState(
                      { source: 'maplibre', sourceLayer: 'countries', id: hoveredCountryId },
                      { hover: true }
                    );

                    const countryCode = e.features[0].properties.ADM0_A3;
                    const countryData = waterwaysDict[countryCode];
                    const countryName = e.features[0].properties.ADMIN;

                    if (countryData) {
                      popup.setLngLat(e.lngLat)
                        .setHTML(`
                          <strong>${getFlagEmoji(countryData.country_code_2)} ${countryData.country_name}</strong><br>
                          Waterways Crossed: ${countryData.waterways_crossed}<br>
                          Unique Waterways: ${countryData.unique_waterways_crossed}<br>
                          Popular: ${countryData.most_popular_waterway}
                        `)
                        .addTo(map);
                    } else {
                      popup.setLngLat(e.lngLat)
                        .setHTML(`
                          <strong>${countryName}</strong><br>
                          No waterway data available
                        `)
                        .addTo(map);
                    }
                  }
                });

                map.on('mouseleave', 'countries-fill', () => {
                  if (hoveredCountryId) {
                    map.setFeatureState(
                      { source: 'maplibre', sourceLayer: 'countries', id: hoveredCountryId },
                      { hover: false }
                    );
                  }
                  hoveredCountryId = null;
                  popup.remove();
                });

                map.on('click', 'countries-fill', (e) => {
                  if (e.features.length > 0) {
                    const country = e.features[0];
                    const bbox = turf.bbox(country);
                    map.fitBounds(bbox, {
                      padding: 40,
                      duration: 2000
                    });
                  }
                });
              });
            } catch (error) {
              console.error('Error parsing JSON:', error);
            }
          })
          .catch(error => console.error('Error fetching waterways data:', error));
      })
      .catch(error => console.error('Error fetching style JSON:', error));
  });
</script>
<p><a href="https://kreuzungen.world" target="_blank" rel="noopener noreferrer">kreuzungen.world</a> isn&rsquo;t changing the world, but it is a great example of how open source software and open data can be used to create something (crossing rivers is not going to save lives, but my shred-buddies find it interesting, so that&rsquo;s something).</p>
<h3 id="the-powerful-client-" class="headerLink">
    <a href="#the-powerful-client-" class="header-mark"></a>The powerful client 💪</h3><p>I want to point out my genuine surprise at how fast the browser does the geospatial processing&hellip; It just works, even on old devices, the performance is good enough. We are talking a couple of seconds for most routes. Including fetching the waterway data via OSM, render the calculated intersecting waterways on a vector map. Waterway fans are happy, I am happy too 😁</p>
<figure><a class="lightgallery" href="/media/performance/kreuzungen-loading.avif" title="/media/performance/kreuzungen-loading.avif" data-thumbnail="/media/performance/kreuzungen-loading.avif" data-sub-html="<h2>Demonstrating the performance of the waterway detection</h2>">
        <img
            
            loading="lazy"
            src="/media/performance/kreuzungen-loading.avif"
            srcset="/media/performance/kreuzungen-loading.avif, /media/performance/kreuzungen-loading.avif 1.5x, /media/performance/kreuzungen-loading.avif 2x"
            sizes="auto"
            alt="/media/performance/kreuzungen-loading.avif">
    </a><figcaption class="image-caption">Demonstrating the performance of the waterway detection</figcaption>
    </figure>
<p>But, I am not completely satisfied&hellip; Recently I have had a craving for speed in my life, don&rsquo;t know why, but I feel it. And fittingly I decided to revisit this problem and see how fast I could push this thing 🚴</p>
<h2 id="i-had-an-idea-" class="headerLink">
    <a href="#i-had-an-idea-" class="header-mark"></a>I had an Idea 💡</h2><p>River data doesn&rsquo;t change that often, so by pre-processing the data and using a storage format optimized for querying <em><strong>it should be much faster</strong></em> My train of thought was something like this 🤔</p>
<ul>
<li>Pre-download waterways instead of waiting for the Overpass API 💾</li>
<li>Use duckdb alone for fast geospatial queries 🌍🦆</li>
<li>Implement R-Tree indexing to minimize intersection search space 🎯</li>
<li>Apply two-step filtering: bounding box, then precise intersection 🔍</li>
<li>Lightweight API Layer with endpoint for GPX upload 💻</li>
</ul>
<p>Well folks, buckle up, because I took that idea and ran with it and it turned out to be a sprint! 🏃</p>
<h2 id="the-setup-" class="headerLink">
    <a href="#the-setup-" class="header-mark"></a>The Setup 🛠</h2><p>The entire solution is available on <a href="https://github.com/01100100/wasserwege" target="_blank" rel="noopener noreferrer">GitHub</a>. Feel free to adapt it for your own needs.</p>
<p>The solution has two parts:</p>
<ul>
<li>Preparing the database</li>
<li>Wrapping it in an API</li>
</ul>
<p>Actually thats a lie, there is another part&hellip;</p>
<ul>
<li>The benchmarking! ⏳</li>
</ul>
<p>I also included a benchmark with some different gpx files, because, well timing things makes sense (when you&rsquo;re obsessed with speed 🎽).</p>
<p>The entire solution is built using open source tools and libraries and is available on <a href="https://github.com/01100100/wasserwege" target="_blank" rel="noopener noreferrer">GitHub</a>. Feel free to explore and adapt it for your own projects! If you have any suggestions or feedback, please reach out. I&rsquo;m always eager to learn and improve.</p>
<p>The project structure looks like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">├── README.md
</span></span><span class="line"><span class="cl">├── prepare_waterways_data.py   -- data pipeline to prepare the database
</span></span><span class="line"><span class="cl">├── server.py                   -- fastapi wrapper
</span></span><span class="line"><span class="cl">├── benchmark.py                -- benchmark script
</span></span><span class="line"><span class="cl">├── benchmark_logs              -- benchmark logs
</span></span><span class="line"><span class="cl">├── data                        -- data directory
</span></span><span class="line"><span class="cl">│   ├── filtered/               -- filtered waterways <span class="o">(</span>.pbf<span class="o">)</span>
</span></span><span class="line"><span class="cl">│   ├── parquet/                -- filtered Waterways <span class="o">(</span>.parquet<span class="o">)</span>    
</span></span><span class="line"><span class="cl">│   ├── pond.duckdb             -- <span class="nb">local</span> duckdb database
</span></span><span class="line"><span class="cl">│   └── raw/                    -- raw data osm <span class="o">(</span>.pbf<span class="o">)</span>
</span></span><span class="line"><span class="cl">├── quelle                      -- dbt project
</span></span><span class="line"><span class="cl">│   ├── dbt_project.yml
</span></span><span class="line"><span class="cl">│   ├── models                  -- transformation logic
</span></span><span class="line"><span class="cl">│   └── profiles.yml            -- duckdb setup
</span></span><span class="line"><span class="cl">└── test_data
</span></span><span class="line"><span class="cl">    └── gpx                     -- benchmarking gpx files
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="the-big-data-preprocessing-pipeline-" class="headerLink">
    <a href="#the-big-data-preprocessing-pipeline-" class="header-mark"></a>The &ldquo;Big Data&rdquo; Preprocessing Pipeline 🚰</h3><p>Let me walk you through the pipeline, which is at the heart of the solution, that transforms raw OpenStreetMap data into the DuckDB table optimized for spatial querying. This is the source of the Wasserwege API 🌊</p>
<ol>
<li>
<p><strong>Download OSM Data</strong>: Using extracts from <a href="https://download.geofabrik.de/" target="_blank" rel="noopener noreferrer">Geofabrik</a> rather than processing the entire planet file.</p>
</li>
<li>
<p><strong>Filter Waterway Features</strong>: Using <a href="https://github.com/openstreetmap/osmosis" target="_blank" rel="noopener noreferrer">Osmosis</a>, filter for waterway features from the OSM data. This dramatically reduces the data size.</p>
</li>
<li>
<p><strong>Convert to GeoParquet</strong>: Using <a href="https://github.com/GIScience/ohsome-planet" target="_blank" rel="noopener noreferrer">ohsome-planet</a>. The format is columnar and much more efficient for analytical queries. This Java tool is amazing, it does a great job at converting the OSM data into a format that is easy to work with.</p>
</li>
<li>
<p><strong>Build database for querying</strong>: I leverage <a href="https://github.com/duckdb/dbt-duckdb" target="_blank" rel="noopener noreferrer">dbt</a> with the DuckDB adapter. This framework brings structure to data transformation workflows. It nicely separates the data transformation logic from the data engineering configuration. Makes a setup that&rsquo;s easy to maintain and extend and minimizes boilerplate code.</p>
</li>
</ol>
<figure><a class="lightgallery" href="/media/performance/datapipeline.avif" title="/media/performance/datapipeline.avif" data-thumbnail="/media/performance/datapipeline.avif" data-sub-html="<h2>The whole of Andorra in downloaded, filtered and optimized to serve in &lt;10 seconds</h2>">
        <img
            
            loading="lazy"
            src="/media/performance/datapipeline.avif"
            srcset="/media/performance/datapipeline.avif, /media/performance/datapipeline.avif 1.5x, /media/performance/datapipeline.avif 2x"
            sizes="auto"
            alt="/media/performance/datapipeline.avif">
    </a><figcaption class="image-caption">The whole of Andorra in downloaded, filtered and optimized to serve in &lt;10 seconds</figcaption>
    </figure>
<h4 id="r-tree-indexing-" class="headerLink">
    <a href="#r-tree-indexing-" class="header-mark"></a>R-Tree Indexing 🌳</h4><p>The most critical optimization is the creation of an R-Tree spatial index to organize the geometries in a hierarchical tree structure.</p>
<p>When a query like &ldquo;find all waterways that intersect with this route&rdquo; is executed, the R-Tree allows the database to quickly eliminate vast portions of the dataset without checking each waterway individually. This reduces the time complexity from <code>O(n)</code> to something closer to <code>O(log n)</code>.</p>
<p>An index can be created with a simple SQL statement, thanks to the duckdb <a href="https://duckdb.org/docs/stable/extensions/spatial/overview.html" target="_blank" rel="noopener noreferrer">SPATIAL</a> extension:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">waterways_geom_idx</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">waterways</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">RTREE</span><span class="w"> </span><span class="p">(</span><span class="n">geom</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>This single line of code provides dramatic performance improvements for spatial queries.</p>
<p><figure><a class="lightgallery" href="/media/performance/stree.webp" title="R-tree use wikipedia" data-thumbnail="/media/performance/stree.webp">
        <img
            
            loading="lazy"
            src="/media/performance/stree.webp"
            srcset="/media/performance/stree.webp, /media/performance/stree.webp 1.5x, /media/performance/stree.webp 2x"
            sizes="auto"
            alt="R-tree use wikipedia">
    </a></figure></p>
<p>Imagine trying to find a group of friends at a festival&hellip; good luck if you have to search through the entire crowd. But if you know they will be at the beach stage, you can skip out the masses and search through only the people in the near vicinity. The R-Tree index is an ordering of data such that you quickly narrow down the search space to just the relevant geometries.</p>
<h4 id="dbt-ing-it-all-together-" class="headerLink">
    <a href="#dbt-ing-it-all-together-" class="header-mark"></a>DBT-ing it all together 🧩</h4><p>The <code>waterways.sql</code> model combines the transformations and creation of the R-Tree index.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="err">{{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">config</span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">materialized</span><span class="o">=</span><span class="s2">&#34;table&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">file_format</span><span class="o">=</span><span class="s2">&#34;parquet&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">location_root</span><span class="o">=</span><span class="s2">&#34;../data/processed/&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">pre_hook</span><span class="o">=</span><span class="s2">&#34;DROP INDEX IF EXISTS waterways_geom_idx;&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">post_hook</span><span class="o">=</span><span class="s2">&#34;CREATE INDEX waterways_geom_idx ON {{ this }} USING RTREE (geom);&#34;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="err">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">with</span><span class="w"> </span><span class="n">waterway_features</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">select</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">osm_id</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">st_geomfromwkb</span><span class="p">(</span><span class="n">geometry</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">geom</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">tags</span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">]</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">waterway_name</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">tags</span><span class="p">[</span><span class="s1">&#39;waterway&#39;</span><span class="p">]</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">waterway_type</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">from</span><span class="w"> </span><span class="err">{{</span><span class="w"> </span><span class="k">source</span><span class="p">(</span><span class="s2">&#34;osm&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;waterways&#34;</span><span class="p">)</span><span class="w"> </span><span class="err">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">where</span><span class="w"> </span><span class="n">tags</span><span class="p">[</span><span class="s1">&#39;name&#39;</span><span class="p">]</span><span class="w"> </span><span class="k">is</span><span class="w"> </span><span class="k">not</span><span class="w"> </span><span class="k">null</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">select</span><span class="w"> </span><span class="o">*</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">from</span><span class="w"> </span><span class="n">waterway_features</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>The output of this all is a local duckdb file <code>pond.duckdb</code> with a table <code>waterways</code> that contains all the waterways, configured ready for fast querying.</p>
<h3 id="the-api-" class="headerLink">
    <a href="#the-api-" class="header-mark"></a>The API 🎮</h3><p>The API is built using FastAPI, a modern web framework for building APIs with Python. My aim was to keep this lightweight and hand off the heavy lifting to the database. I added two endpoints to the API:</p>
<ul>
<li><code>/healthcheck</code>: A simple endpoint to check if the API is running and healthy and reports the number of waterways in the database.</li>
<li><code>/process_gpx</code>: This endpoint accepts a GPX file, parses it, and finds the waterways that intersect with the route. It returns the results in a JSON format.</li>
</ul>
<div class="details admonition tip">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-lightbulb fa-fw"></i>Parsing Gpx in Python?<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content">Parsing the GPX file is done using thanks to the <code>gpxpy</code> library, which is a simple and efficient library for parsing GPX files. The GPX file is converted to a LineString geometry using the <code>gpx_to_linestring</code> function, which extracts the coordinates from the GPX file and creates a LineString object, this is then passed to the <code>find_waterway_crossings</code> function.</div>
        </div>
    </div>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@app.post</span><span class="p">(</span><span class="s2">&#34;/process_gpx&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">process_gpx</span><span class="p">(</span><span class="n">file</span><span class="p">:</span> <span class="n">UploadFile</span> <span class="o">=</span> <span class="n">File</span><span class="p">(</span><span class="o">...</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Process GPX file and find waterway intersections&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">overall_start_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Read and parse the uploaded GPX file</span>
</span></span><span class="line"><span class="cl">    <span class="n">contents</span> <span class="o">=</span> <span class="k">await</span> <span class="n">file</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">gpx</span> <span class="o">=</span> <span class="n">gpxpy</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">contents</span><span class="o">.</span><span class="n">decode</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Convert to linestring and find intersections</span>
</span></span><span class="line"><span class="cl">    <span class="n">linestring</span> <span class="o">=</span> <span class="n">gpx_to_linestring</span><span class="p">(</span><span class="n">gpx</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">crossings</span> <span class="o">=</span> <span class="n">find_waterway_crossings</span><span class="p">(</span><span class="n">linestring</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Return results with timing information</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;processing_times_ms&#34;</span><span class="p">:</span> <span class="p">{</span><span class="o">...</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;total_crossings&#34;</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="n">crossings</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;crossings&#34;</span><span class="p">:</span> <span class="n">crossings</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="the-query-" class="headerLink">
    <a href="#the-query-" class="header-mark"></a>The Query ✍️</h4><p>The core query that powers the intersection detection is remarkably simple (thanks to the R-Tree index and DuckDB&rsquo;s spatial functions):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">WITH</span><span class="w"> </span><span class="n">route_geom_cte</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">SELECT</span><span class="w"> </span><span class="n">ST_GeomFromText</span><span class="p">(</span><span class="err">$</span><span class="mi">1</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">geom</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">           </span><span class="n">ST_Envelope</span><span class="p">(</span><span class="n">ST_GeomFromText</span><span class="p">(</span><span class="err">$</span><span class="mi">1</span><span class="p">))</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">bbox</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">w</span><span class="p">.</span><span class="n">id</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">w</span><span class="p">.</span><span class="n">waterway_name</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">w</span><span class="p">.</span><span class="n">waterway_type</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ST_AsGeoJSON</span><span class="p">(</span><span class="n">ST_Intersection</span><span class="p">(</span><span class="n">w</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="p">.</span><span class="n">geom</span><span class="p">))</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">intersection_geojson</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">waterways</span><span class="w"> </span><span class="n">w</span><span class="p">,</span><span class="w"> </span><span class="n">route_geom_cte</span><span class="w"> </span><span class="n">r</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">ST_Intersects</span><span class="p">(</span><span class="n">w</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="p">.</span><span class="n">bbox</span><span class="p">)</span><span class="w"> </span><span class="c1">-- First fast filter with bounding box
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">ST_Intersects</span><span class="p">(</span><span class="n">w</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span><span class="w"> </span><span class="n">r</span><span class="p">.</span><span class="n">geom</span><span class="p">)</span><span class="w"> </span><span class="c1">-- Then precise intersection
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>This query uses the created index to quickly identify potential intersections, then it performs the exact spatial operation only on those candidates.</p>
<h2 id="quantifying-the-gains-" class="headerLink">
    <a href="#quantifying-the-gains-" class="header-mark"></a>Quantifying the Gains 📈</h2><p>To measure the performance gains, I created a benchmarking script (<code>benchmark.py</code>) that tests different GPX files against the API. The results showed a drastic improvement:</p>
<figure><a class="lightgallery" href="/media/performance/kreuzungen-benchmark.avif" title="/media/performance/kreuzungen-benchmark.avif" data-thumbnail="/media/performance/kreuzungen-benchmark.avif" data-sub-html="<h2>Seeing the performance increase 🧑‍💻</h2>">
        <img
            
            loading="lazy"
            src="/media/performance/kreuzungen-benchmark.avif"
            srcset="/media/performance/kreuzungen-benchmark.avif, /media/performance/kreuzungen-benchmark.avif 1.5x, /media/performance/kreuzungen-benchmark.avif 2x"
            sizes="auto"
            alt="/media/performance/kreuzungen-benchmark.avif">
    </a><figcaption class="image-caption">Seeing the performance increase 🧑‍💻</figcaption>
    </figure>
<p>You can see the performance of the API in action. The benchmark script runs 10 different GPX files, each with varying lengths and complexities, and measures the time taken to process each file, and it runs before the old browser-based solution even finishes a single route.</p>
<p>This represents a <strong>biggggg</strong> in processing speed compared to the browser-based solution!</p>
<h2 id="conclusion-performance-is-a-journey-not-a-destination-" class="headerLink">
    <a href="#conclusion-performance-is-a-journey-not-a-destination-" class="header-mark"></a>Conclusion: Performance is a Journey, Not a Destination 🧘</h2><p>This experiment shows that with different tools and techniques, we can dramatically improve the performance of applications. What was already &ldquo;pretty fast&rdquo; in the browser is now much faster because of a change in architecture 🦆</p>
<p>I&rsquo;ll be honest, I don&rsquo;t even have a feeling of what a &ldquo;fast&rdquo; optimized response to this problem should be 😲</p>
<p>There are so many ways this problem can be solved using a computer, and each approach has its own characteristics.</p>
<p>I didn&rsquo;t explore other optimizations, like using a different database or alternative indexing strategies. I could have written low-level code in Rust, but that would take far more time than I had available for this project.</p>
<p>We are lucky to have such fast tools available to us today to use for free, and can build on top of really well made software. This saves time ❤️</p>
<div class="details admonition tip open">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-lightbulb fa-fw"></i>The power of open source collaboration<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content"><p>Seriously, a lot of the reason why someone like me can solve these problems is thanks to gluing together libraries and tools that are openly available on GitHub.</p>
<p>There are many layers of abstraction in this solution, a lot of code I&rsquo;m not even aware of. Every tool I use is built on top of other tools, which are built on top of others, and so on. Many developers have spent countless hours pondering optimizations at each level, and it&rsquo;s thanks to this combined effort that a single developer like me can build something this efficient. A solution that&rsquo;s more than fast enough for my needs 🙇</p>
<p>Standing on the shoulders of giants isn&rsquo;t just a saying, it&rsquo;s literally how modern software development works. The R-Tree implementation I&rsquo;m using might have taken years to perfect by dedicated algorithm specialists inspired by centuries of mathematicians thinking about geometry, and here I am, the tech-bro activating it with a single line of SQL in a database named after a duck! This ability to reuse the mental work of others, whether you are aware of it or not, makes this field so incredible. ✨</p>
</div>
        </div>
    </div>
<h2 id="takeaways-performance-is-a-journey-not-a-destination-" class="headerLink">
    <a href="#takeaways-performance-is-a-journey-not-a-destination-" class="header-mark"></a>Takeaways: Performance is a Journey, Not a Destination 🧘</h2><p>It&rsquo;s deeply satisfying to see abstract ideas transform into real performance gains. What started as a theoretical hunch about spatial indexing and data processing became a real and measurable improvement. It&rsquo;s a humble reminder that studying these concepts can actually lead to practical benefits when applied to the right problem.</p>
<p>Not everything in life needs to be fast, but in situations where it matters, a well-chosen index and thoughtful data pipeline might just be the solution you need! 🏁</p>
<h2 id="standing-on-the-shoulders-of-open-giants-" class="headerLink">
    <a href="#standing-on-the-shoulders-of-open-giants-" class="header-mark"></a>Standing on the Shoulders of (Open) Giants 🙇</h2><p>None of this would be possible without the incredible open-source tools and libraries that are available today.</p>
<p><a href="https://www.openstreetmap.org" target="_blank" rel="noopener noreferrer">OpenStreetMap Contributors</a>: Every river and stream was manually mapped by volunteers in one of humanity&rsquo;s most impressive collaborative projects 💚</p>
<p><a href="https://www.geofabrik.de/" target="_blank" rel="noopener noreferrer">Geofabrik</a>: Provides free daily OSM extracts, saving countless hours of redundant processing. Thank you! 🙇</p>
<p><a href="https://osmcode.org/osmium-tool/" target="_blank" rel="noopener noreferrer">Osmosis</a>: When OSM data is like IKEA (an overwhelming maze with too many options when all you need is cup), this tool helps filter just what you need 🕵️‍♀️</p>
<p><a href="https://github.com/GIScience/ohsome-planet" target="_blank" rel="noopener noreferrer">ohsome-planet</a>: Transforms OSM&rsquo;s simply-elegant-but-awkward data model (just nodes with tags and pointers) into the GOAT GeoParquet format 📦</p>
<p><a href="https://duckdb.org" target="_blank" rel="noopener noreferrer">DuckDB</a>: Turns your laptop into a high-performance geospatial data warehouse 🦆</p>
<p><a href="https://www.getdbt.com/" target="_blank" rel="noopener noreferrer">dbt</a>, <a href="https://github.com/Maproom/gpxpy" target="_blank" rel="noopener noreferrer">gpxpy</a>, <a href="https://fastapi.tiangolo.com/" target="_blank" rel="noopener noreferrer">FastAPI</a>: The building blocks that tied everything together into a coherent system 🧰</p>]]></description></item><item><title>Traveling to State of the Map Europe 2024 🚲</title><link>https://davidwhittingham.com/posts/sotm/</link><pubDate>Thu, 01 Aug 2024 01:46:42 +0200</pubDate><author><name>Author</name></author><guid>https://davidwhittingham.com/posts/sotm/</guid><description><![CDATA[<div class="featured-image">
                <img src="/media/sotm/ride-poland.avif" referrerpolicy="no-referrer">
            </div><h2 id="intro-" class="headerLink">
    <a href="#intro-" class="header-mark"></a>Intro 🪐</h2><p>I found myself at the <a href="https://stateofthemap.eu" target="_blank" rel="noopener noreferrer">State of the Map Europe</a> event! An annual gathering of OpenStreetMap enthusiasts. I am relatively new to the OSM world and saw it as a fantastic chance to get involved and learn more about the community and the projects happening in the space. Oh my, what a big space it turns out to be! OSM is everywhere.</p>
<p>Some special talks, for no specific reasons:</p>
<ul>
<li><a href="https://cfp.openstreetmap.org.pl/state-of-the-map-europe-2024/talk/PQ8VKC/" target="_blank" rel="noopener noreferrer">AI-assisted mapping by the Humanitarian OSM Team (HOT🔥)</a></li>
<li><a href="https://cfp.openstreetmap.org.pl/state-of-the-map-europe-2024/talk/K8LF7U/" target="_blank" rel="noopener noreferrer">Flowing Connections: Mapping rivers &amp; streams with WaterwayMap.org</a></li>
<li><a href="https://cfp.openstreetmap.org.pl/state-of-the-map-europe-2024/talk/V8ZB3U/" target="_blank" rel="noopener noreferrer">Supporting Mobility Transitions: OpenStreetMap&rsquo;s Contribution to Public Administration Data Needs</a></li>
<li><a href="https://cfp.openstreetmap.org.pl/state-of-the-map-europe-2024/talk/M8HTSJ/" target="_blank" rel="noopener noreferrer">Minutely vector tiles for the community</a></li>
</ul>
<p>It was a great event! Kudos to the organizing team! I met a ton of cool people, soaked up some serious knowledge, and all while enjoying the vibrant city of Łódź.</p>
<p>A lot of folks I met were curious about my journey and had many questions about how I managed to transport myself to the event.</p>
<figure><a class="lightgallery" href="/media/sotm/ride-poland-wide.avif" title="/media/sotm/ride-poland-wide.avif" data-thumbnail="/media/sotm/ride-poland-wide.avif" data-sub-html="<h2>My ride to Łódź</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/ride-poland-wide.avif"
            srcset="/media/sotm/ride-poland-wide.avif, /media/sotm/ride-poland-wide.avif 1.5x, /media/sotm/ride-poland-wide.avif 2x"
            sizes="auto"
            alt="/media/sotm/ride-poland-wide.avif">
    </a><figcaption class="image-caption">My ride to Łódź</figcaption>
    </figure>
<p>This blog post spills the beans on how I prepared to ride my bike from Berlin to SOTM, held in Łódź, Poland. 500km<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> in 2 days 🚴‍♀️🌕🚴‍♂️.</p>
<h2 id="the-bike-setup-" class="headerLink">
    <a href="#the-bike-setup-" class="header-mark"></a>The Bike Setup ⚙️</h2><div class="details admonition tip">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-lightbulb fa-fw"></i>What is the best bike?<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content"><p>The best bike is the one you have &mdash; just get out and ride it!</p>
<p>&mdash; The Buddha 🧘 (Probably)</p>
</div>
        </div>
    </div>
<p>I&rsquo;m a firm believer that you can enjoy a bike tour on pretty much any bike with the right mindset.</p>
<p>For this journey, I aimed to go fast and efficiently, stay comfy, and not be totally done by the time I arrived in Łódź. The way would be mostly flat, so weight wasn&rsquo;t a concern. I would mostly ride on the road to save time.</p>
<p>I chose to break the ride into two days, with an overnight in a cheap hotel in Poznan. This meant I could leave the tent, sleeping setup, and cooking gear behind. It would just be me, my bike, and the essentials.</p>
<p>Here are the key areas I focused on to optimize my setup for the journey:</p>
<ul>
<li>Comfort</li>
<li>Aerodynamics</li>
<li>Rolling resistance</li>
</ul>
<h3 id="comfort-" class="headerLink">
    <a href="#comfort-" class="header-mark"></a>Comfort 😌</h3><p>Meet Trustini, my trusty long-distance steed.</p>
<figure><a class="lightgallery" href="/media/sotm/trustini.webp" title="/media/sotm/trustini.webp" data-thumbnail="/media/sotm/trustini.webp" data-sub-html="<h2>Trustini</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/trustini.webp"
            srcset="/media/sotm/trustini.webp, /media/sotm/trustini.webp 1.5x, /media/sotm/trustini.webp 2x"
            sizes="auto"
            alt="/media/sotm/trustini.webp">
    </a><figcaption class="image-caption">Trustini</figcaption>
    </figure>
<p>I have done many long rides on this bike and know it fits me well. A gravel bike with a relaxed geometry, built around a buttery smooth titanium frame, and a lot of history. We have a good relationship built on years of trust and going places together.</p>
<div class="details admonition info">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-info-circle fa-fw"></i>Bike fit<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content"><p>Bike fitting is essentially moving the different parts of the bike around to fit the rider. You want to be in a position when cycling that optimizes comfort, performance, and efficiency.</p>
<p>There are many resources online to help you get a good fit:</p>
<ul>
<li>The classic &ldquo;Bicycling and Pain&rdquo; article by <a href="https://www.sheldonbrown.com/pain.html" target="_blank" rel="noopener noreferrer">Sheldon Brown</a>.</li>
<li>GCN on Bike Fit on <a href="https://www.youtube.com/watch?v=1VYhyppWTDc" target="_blank" rel="noopener noreferrer">youtube.com</a>.</li>
<li>Steve Hogg&rsquo;s <a href="https://www.stevehoggbikefitting.com/" target="_blank" rel="noopener noreferrer">Bike Fitting</a> website.</li>
<li>Physio-Pedia&rsquo;s <a href="https://www.physio-pedia.com/Bike_Fit" target="_blank" rel="noopener noreferrer">Bike Fit</a> article.</li>
</ul>
<p>There are also professional bike fitting services.</p>
</div>
        </div>
    </div>
<p>For long distances comfort, the contact points are where the magic happens: saddle, handlebars, and pedals. Discomfort tends to accumulate here, potentially causing trouble if not addressed. Everyone&rsquo;s body is different, and it is best to experiment with different setups and find out what works for you personally.</p>
<p>I use a narrow <a href="https://www.brooksengland.com/en_eu/c13.html" target="_blank" rel="noopener noreferrer">Brooks C13 Cambium</a> saddle with a bit of flex and team it up with my most comfortable bib shorts from Santini that include a butt-saving &ldquo;C3 seat pad with anti-shock gel inserts&rdquo;. This combination has proven itself on many long rides.</p>
<figure><a class="lightgallery" href="/media/sotm/santini-pad.webp" title="/media/sotm/santini-pad.webp" data-thumbnail="/media/sotm/santini-pad.webp" data-sub-html="<h2>Santini C3 seat pad</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/santini-pad.webp"
            srcset="/media/sotm/santini-pad.webp, /media/sotm/santini-pad.webp 1.5x, /media/sotm/santini-pad.webp 2x"
            sizes="auto"
            alt="/media/sotm/santini-pad.webp">
    </a><figcaption class="image-caption">Santini C3 seat pad</figcaption>
    </figure>
<p>For the handlebars, I use the thickest bar tape I can find. More cushion absorbs the vibrations that come through the bars better. I also wear padded gloves.</p>
<figure><a class="lightgallery" href="/media/sotm/bartape.webp" title="/media/sotm/bartape.webp" data-thumbnail="/media/sotm/bartape.webp" data-sub-html="<h2>Different width bar tapes are available.</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/bartape.webp"
            srcset="/media/sotm/bartape.webp, /media/sotm/bartape.webp 1.5x, /media/sotm/bartape.webp 2x"
            sizes="auto"
            alt="/media/sotm/bartape.webp">
    </a><figcaption class="image-caption">Different width bar tapes are available.</figcaption>
    </figure>
<p>My friend Michael gave me some aero bars to try out. This gives me more hand positions to switch between, helping avoid any nerve damage<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> from the long days in the saddle.</p>
<figure><a class="lightgallery" href="/media/sotm/ironman.webp" title="/media/sotm/ironman.webp" data-thumbnail="/media/sotm/ironman.webp" data-sub-html="<h2>Ironman tri bars</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/ironman.webp"
            srcset="/media/sotm/ironman.webp, /media/sotm/ironman.webp 1.5x, /media/sotm/ironman.webp 2x"
            sizes="auto"
            alt="/media/sotm/ironman.webp">
    </a><figcaption class="image-caption">Ironman tri bars</figcaption>
    </figure>
<p>I use clip-in pedals with stiff shoes that fit me well. This means I can pedal more efficiently and have the force evenly distributed around my whole foot.</p>
<!-- markdownlint-disable-next-line MD036 -->
<p><strong>The combination of these things hopefully means I can ride for hours without any discomfort 🤞</strong></p>
<h3 id="aerodynamics-" class="headerLink">
    <a href="#aerodynamics-" class="header-mark"></a>Aerodynamics 💨</h3><p>When cycling above 15km/h, the biggest thing slowing you down is air resistance or drag.</p>
<p>Considering I am much bigger than the bike, the biggest source of drag is me. Changing your position on the bike can make a big difference.</p>
<figure><a class="lightgallery" href="/media/sotm/aero-positions.webp" title="/media/sotm/aero-positions.webp" data-thumbnail="/media/sotm/aero-positions.webp" data-sub-html="<h2>Aerodynamic study of different riding positions</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/aero-positions.webp"
            srcset="/media/sotm/aero-positions.webp, /media/sotm/aero-positions.webp 1.5x, /media/sotm/aero-positions.webp 2x"
            sizes="auto"
            alt="/media/sotm/aero-positions.webp">
    </a><figcaption class="image-caption">Aerodynamic study of different riding positions</figcaption>
    </figure>
<p>While I will try to stay as aero as possible, a comfortable position is key. So I opted for a tight-fitting jersey. Not because it looks cool, but because it&rsquo;s practicool.The reason the pros wear them is that it’s faster to have skin-fitting clothes than things flapping around.</p>
<p>Wearing a helmet actually reduces drag and makes you faster, as proven by Greg LeMond in the 1989 Tour de France<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>. People actually compare helmets to see which one is fastest<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>.</p>
<p>I wear an <a href="https://www.oakley.com/en-eu/product/FOS901302?variant=193517780012" target="_blank" rel="noopener noreferrer">Oakley Ar05 Race</a> helmet which does a good job. I also use sun glasses with a big lens to protect my eyes from the sun, wind, and bugs. The frame of the glasses matches up with the helmet and reduces drag even further. Marginal gains, but hey, I will take every saving I can!</p>
<p>The rest of my stuff was stashed in my bike bags. Top tube bag + frame bag + so-called &ldquo;Arse Rocket&rdquo; saddle bag. The bags are positioned to avoid increasing the frontal area and avoid any extra turbulence. According to some sources, the bags might even help me stay aero<sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>.</p>
<figure><a class="lightgallery" href="/media/sotm/aero-trustini.webp" title="/media/sotm/aero-trustini.webp" data-thumbnail="/media/sotm/aero-trustini.webp" data-sub-html="<h2>My bike</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/aero-trustini.webp"
            srcset="/media/sotm/aero-trustini.webp, /media/sotm/aero-trustini.webp 1.5x, /media/sotm/aero-trustini.webp 2x"
            sizes="auto"
            alt="/media/sotm/aero-trustini.webp">
    </a><figcaption class="image-caption">My bike</figcaption>
    </figure>
<p>It was my first time using aero bars, and wow, it was a game changer! My intention was to give myself different positions to rest my hands. But oh boy, when I was in the aero bar position, I felt like I was piercing through the air. It was also super comfortable to be able to switch between different positions. I will definitely be using them again on long rides.</p>
<p>Plus, I could mount my phone on the aero bars in a chiller position, optimal for navigation and changing up tunes on the go.</p>
<figure><a class="lightgallery" href="/media/sotm/phone-mount.webp" title="/media/sotm/phone-mount.webp" data-thumbnail="/media/sotm/phone-mount.webp" data-sub-html="<h2>Phone &#43; aero bar setup</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/phone-mount.webp"
            srcset="/media/sotm/phone-mount.webp, /media/sotm/phone-mount.webp 1.5x, /media/sotm/phone-mount.webp 2x"
            sizes="auto"
            alt="/media/sotm/phone-mount.webp">
    </a><figcaption class="image-caption">Phone + aero bar setup</figcaption>
    </figure>
<h3 id="rolling-resistance-" class="headerLink">
    <a href="#rolling-resistance-" class="header-mark"></a>Rolling Resistance 🛞</h3><p>Another crucial contact point is the tires. Tires are the only part of the bike touching the ground and are the biggest source of rolling resistance.</p>
<p>Traditionally, the fastest (aka the least rolling resistance) tire is slick and narrow, inflated to high pressure.</p>
<p>This idea is evolving with new perspectives, lower pressures, and the industry producing tubeless tires and wider rims. According to new data, a wider tire at a lower pressure can be faster than a narrow tire at a high pressure, especially on rougher surfaces.</p>
<p>I like Continental GP5000 tires, and according to the rolling resistance data<sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>, the 32mm version, which I am using, is not much worse than the skinny 25mm version, and is one of the faster tires available.</p>
<figure><a class="lightgallery" href="/media/sotm/width_rolling_resistance.webp" title="/media/sotm/width_rolling_resistance.webp" data-thumbnail="/media/sotm/width_rolling_resistance.webp" data-sub-html="<h2>Grand Prix 5000 Different Width Rolling Resistance Test</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/width_rolling_resistance.webp"
            srcset="/media/sotm/width_rolling_resistance.webp, /media/sotm/width_rolling_resistance.webp 1.5x, /media/sotm/width_rolling_resistance.webp 2x"
            sizes="auto"
            alt="/media/sotm/width_rolling_resistance.webp">
    </a><figcaption class="image-caption">Grand Prix 5000 Different Width Rolling Resistance Test</figcaption>
    </figure>
<p>Using 32mm, fatter and softer tires also means more comfort, especially on the rough stuff, which is important for long rides.</p>
<!-- markdownlint-disable-next-line MD036 -->
<p><strong>With all this setup, I should be rolling with ease. The only thing left to do was to pack my bags and hit the road 🚴</strong></p>
<h3 id="packing-list-" class="headerLink">
    <a href="#packing-list-" class="header-mark"></a>Packing List 🧳</h3><div class="details admonition abstract">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-list-ul fa-fw"></i>Full Equipment List<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content"><p>Here is a complete list of what I took with me:</p>
<ul>
<li>Essentials
<ul>
<li>Passport</li>
<li>Money</li>
</ul>
</li>
<li>Bike</li>
<li>Bike bags
<ul>
<li>Frame bag</li>
<li>Saddle bag</li>
<li>Top tube bag</li>
</ul>
</li>
<li>Helmet</li>
<li>Bike lights</li>
<li>Gloves</li>
<li>Electronics
<ul>
<li>Phone</li>
<li>Camera</li>
<li>Wireless headphones</li>
<li>Powerbank</li>
<li>Headtorch</li>
</ul>
</li>
<li>Bottles
<ul>
<li>Stay hydrated</li>
</ul>
</li>
<li>Bike tools
<ul>
<li>Pump</li>
<li>Multitool</li>
<li>Spare tube</li>
<li>Patch kit</li>
<li>Tire levers</li>
<li>Chain lube</li>
</ul>
</li>
<li>Clothes</li>
<li>Shoes
<ul>
<li>Bike</li>
<li>Casual</li>
</ul>
</li>
<li>Hygiene
<ul>
<li>Toothbrush</li>
<li>Toothpaste</li>
<li>Sunscreen</li>
</ul>
</li>
</ul></div>
        </div>
    </div>
<h2 id="the-route-" class="headerLink">
    <a href="#the-route-" class="header-mark"></a>The Route 🗺️</h2><p>I used <a href="https://www.komoot.com" target="_blank" rel="noopener noreferrer">Komoot</a> to plan the route. I put in my start and end locations and set Poznan as a waypoint 📍 to break up the journey. I tweaked the route by adding a waypoint in a national park to avoid the main road, and that was it.</p>
<p><figure><a class="lightgallery" href="/media/sotm/komoot-route-planner.webp" title="Komoot route planner" data-thumbnail="/media/sotm/komoot-route-planner.webp">
        <img
            
            loading="lazy"
            src="/media/sotm/komoot-route-planner.webp"
            srcset="/media/sotm/komoot-route-planner.webp, /media/sotm/komoot-route-planner.webp 1.5x, /media/sotm/komoot-route-planner.webp 2x"
            sizes="auto"
            alt="Komoot route planner">
    </a></figure></p>
<p>Komoot provides the surface type and elevation expected along the route to give you a good idea of what to expect and helping you pace yourself.</p>
<p>Komoot Premium includes a feature that shows the expected weather along the route at the time you are expected to be there. It also displays the sunrise and sunset times, which are super useful for deciding what to take with you on a tour.</p>
<!-- markdownlint-disable-next-line MD033 -->
<iframe src="https://www.komoot.com/tour/1962295602/embed?share_token=atDy5joOv8kjA98sxIcyRWnLEUDfAIG2bWD4hF8aW0gaVZJi6g&profile=1" width="100%" height="900" frameborder="0" scrolling="no"></iframe>
<!-- markdownlint-disable-next-line MD036 -->
<p><strong>I saved the route offline on my phone, strapped it to my handlebars, and was ready 🛫</strong></p>
<h2 id="the-wind-" class="headerLink">
    <a href="#the-wind-" class="header-mark"></a>The Wind 🌬️</h2><p>Nature plays a huge factor in cycling, but it&rsquo;s beyond your control. Sometimes you get unlucky and have a rainy headwind all day; sometimes you get blown off your bike by a gust of wind, but that is all part of the fun.</p>
<p>I used <a href="https://www.windy.com" target="_blank" rel="noopener noreferrer">windy.com</a> to check the wind forecast, and it looked like the wind gods would be blowing in my favor for once!</p>
<p><figure><a class="lightgallery" href="/media/sotm/windy.avif" title="Windy forecast" data-thumbnail="/media/sotm/windy.avif">
        <img
            
            loading="lazy"
            src="/media/sotm/windy.avif"
            srcset="/media/sotm/windy.avif, /media/sotm/windy.avif 1.5x, /media/sotm/windy.avif 2x"
            sizes="auto"
            alt="Windy forecast">
    </a></figure></p>
<p>Get in! I would have a tailwind all the way to Łódź.</p>
<p>It was forecasted to be dry and hot, which means a lower air density and even less drag. I got lucky.</p>
<p><figure><a class="lightgallery" href="/media/sotm/heat.webp" title="Temperature Forecast" data-thumbnail="/media/sotm/heat.webp">
        <img
            
            loading="lazy"
            src="/media/sotm/heat.webp"
            srcset="/media/sotm/heat.webp, /media/sotm/heat.webp 1.5x, /media/sotm/heat.webp 2x"
            sizes="auto"
            alt="Temperature Forecast">
    </a></figure></p>
<h2 id="the-journey-" class="headerLink">
    <a href="#the-journey-" class="header-mark"></a>The Journey 🚴‍♂️</h2><p>I left Berlin around 10:00 AM. I had a laptop with me, which I dropped off at Dominik&rsquo;s place on the outskirts of Berlin (big thanks for taking care of it!). From then on, I was heading east!</p>
<p>The first 100km were super smooth. I was flying! The sun was shining, and I was moving through the forests of Brandenburg with a tailwind all the way to the border. I was cruising on the aero bars at around 30km/h without much effort.</p>
<p>I went over the Oder/Odra River, which marked the international border between Germany and Poland. I do love a good Schengen border crossing. You just glide through without being stopped or checked. I probably wouldn&rsquo;t have even noticed if I didn&rsquo;t know I was crossing a border. Especially as someone who used to be an EU citizen, I truly appreciate the freedom of movement.</p>
<p>The road quality changed as soon as I crossed the border. The roads became rougher and bumpier. Such is life. It would be interesting to analyze the road quality data from OSM to see if there is a difference between countries. I imagine that the surface_type completeness in Polish street data in OSM is lower than in Germany, but that is a topic for another time.</p>
<p>I went along a Park narodowy (aka a national park) and saw some beautiful birds flying around, many storks. I also saw a few deer and a big frog.</p>
<p>One remarkable thing I noticed was how clean Poland was. There was no trash anywhere. I didn&rsquo;t see a single piece of litter on the road, and the fields and forests were well-kept.</p>
<figure><a class="lightgallery" href="/media/sotm/polish-road.webp" title="/media/sotm/polish-road.webp" data-thumbnail="/media/sotm/polish-road.webp" data-sub-html="<h2>A beautiful polish road</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/polish-road.webp"
            srcset="/media/sotm/polish-road.webp, /media/sotm/polish-road.webp 1.5x, /media/sotm/polish-road.webp 2x"
            sizes="auto"
            alt="/media/sotm/polish-road.webp">
    </a><figcaption class="image-caption">A beautiful polish road</figcaption>
    </figure>
<p>A lake provided the perfect spot to cool off and eat some food. Poland is beautiful in the summer.</p>
<p>The afternoon heat was intense, and I was cooking, my skin slick with a slimy mix of salty sweat and sunscreen lotion. But whatever, I was moving fast.</p>
<p>I spent the night in Poznan, a beautiful city with a lot of young people and a lot of history. I arrived, had a shower, and headed out to explore the city and have something to eat. The city was alive with people and music. I almost got swindled in a strip club, but that is a story for another time.</p>
<p>After a big breakfast, I hit the road again. The second day was again super smooth. I had a tailwind all the way to Łódź. I was flying!</p>
<p>A sudden pothole caused a snakebite<sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup>. No biggie, I had a spare tube and fixed it in no time and was back on the road cruising again.</p>
<p>With about 50km left, I encountered a group of Polish gravel riders. They were, let&rsquo;s just say, a lively bunch. They invited me to join their ride. We raced into Łódź together, they showed me a few sights, and we finished the journey sharing stories and beers. A perfect ending to an epic ride.</p>
<figure><a class="lightgallery" href="/media/sotm/unicorn.webp" title="/media/sotm/unicorn.webp" data-thumbnail="/media/sotm/unicorn.webp" data-sub-html="<h2>The Polish Gravel Gang</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/unicorn.webp"
            srcset="/media/sotm/unicorn.webp, /media/sotm/unicorn.webp 1.5x, /media/sotm/unicorn.webp 2x"
            sizes="auto"
            alt="/media/sotm/unicorn.webp">
    </a><figcaption class="image-caption">The Polish Gravel Gang</figcaption>
    </figure>
<h2 id="the-food-" class="headerLink">
    <a href="#the-food-" class="header-mark"></a>The Food 🍲</h2><p>A few people asked me how I would eat on the road.</p>
<p>A key to long-distance cycling is fueling your body with enough calories to sustain the effort you&rsquo;re putting out. If you don’t, you might find yourself in an unfortunate situation called &ldquo;bonking&rdquo;<sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup>.</p>
<p>I like to take things like Bananas, muesli bars, croissants with me and keep many stashed in my top tube bag. I eat these regularly while riding to maintain my energy levels. You can&rsquo;t eat too much on a long ride, so keep snacking!</p>
<p>In Poland, you find &ldquo;Sklep Polski&rdquo; everywhere, which are small local shops selling everything you need and can resupply. The owners were always nice and refilled my water bottles. I also wanted to taste some traditional Polish food, so I stopped at a couple of restaurants along the way and enjoyed some delicious pierogi.</p>
<figure><a class="lightgallery" href="/media/sotm/sklep.webp" title="/media/sotm/sklep.webp" data-thumbnail="/media/sotm/sklep.webp" data-sub-html="<h2>Yet another Sklep Polski</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/sklep.webp"
            srcset="/media/sotm/sklep.webp, /media/sotm/sklep.webp 1.5x, /media/sotm/sklep.webp 2x"
            sizes="auto"
            alt="/media/sotm/sklep.webp">
    </a><figcaption class="image-caption">Yet another Sklep Polski</figcaption>
    </figure>
<p>Since it was so hot, I needed to drink a lot of water on the ride, like 6 litres a day. However it was never a problem to find clean water en route. I took some isotonic tablets with me to stay hydrated.</p>
<h2 id="takeaways-for-long-distance-cycling-" class="headerLink">
    <a href="#takeaways-for-long-distance-cycling-" class="header-mark"></a>Takeaways for Long-Distance Cycling 📝</h2><ul>
<li><strong>Bike Fit</strong>: Ensure your bike fits you well. Small adjustments can make a huge difference in comfort and performance.</li>
<li><strong>Fueling</strong>: Eat, eat, eat! Keep your energy levels up by eating regularly. Stock up regularly on the road and always have some snacks on hand. Stay hydrated!</li>
<li><strong>Gear</strong>: Invest in gear that works for you and prioritizes comfort and efficiency. Tight fitting clothing and good tires can significantly enhance your ride.</li>
<li><strong>Route Planning</strong>: Use tools like Komoot to plan your route. Knowing the terrain and expected weather conditions helps you prepare better.</li>
</ul>
<h2 id="conclusion-" class="headerLink">
    <a href="#conclusion-" class="header-mark"></a>Conclusion 🏁</h2><p>State of the Map 2024 was fantastic! I had a great time and learned a lot, which could fill another blog post.</p>
<p>The journey itself was a highlight and went incredibly well.</p>
<p>Traveling across Poland by bike gave me an opportunity to see the country evolve. I saw the small villages, the fields, the forests, the rivers, and the big cities. I had a lot of time to see it and take it all in.</p>
<p>Cycling has many uncountable benefits, but for me, it&rsquo;s the best way to travel and straight-up fun. I recommend for anyone interested to get out there and give it a go!</p>
<figure><a class="lightgallery" href="/media/sotm/sunshine.webp" title="/media/sotm/sunshine.webp" data-thumbnail="/media/sotm/sunshine.webp" data-sub-html="<h2>Me on my bike!]</h2>">
        <img
            
            loading="lazy"
            src="/media/sotm/sunshine.webp"
            srcset="/media/sotm/sunshine.webp, /media/sotm/sunshine.webp 1.5x, /media/sotm/sunshine.webp 2x"
            sizes="auto"
            alt="/media/sotm/sunshine.webp">
    </a><figcaption class="image-caption">Me on my bike!]</figcaption>
    </figure>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>510km according to <a href="https://www.komoot.com/tour/1700459745" target="_blank" rel="noopener noreferrer">Komoot.com</a>.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>How Nerve Damage During Ultras is a thing from <a href="https://dotwatcher.cc/feature/nerve-damage-during-ultras" target="_blank" rel="noopener noreferrer">dotwatcher.cc</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>A interesting article on bike helmets from <a href="https://www.nasa.gov/directorates/stmd/tech-transfer/spinoffs/tech-today-a-nasa-inspired-bike-helmet-with-aerodynamics-of-a-jet/" target="_blank" rel="noopener noreferrer">nasa.gov</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Guy tests out helmet in a wind tunnel on <a href="https://www.youtube.com/watch?v=7d7DznkY3Jo" target="_blank" rel="noopener noreferrer">youtube.com</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>According to the manufacturer, bike bags can make you faster&hellip; <a href="https://www.apidura.com/journal/apidura-aero-pack-system-packs-that-make-you-faster/" target="_blank" rel="noopener noreferrer">apidura.com</a>&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>Grand Prix 5000 comparison on <a href="https://www.bicyclerollingresistance.com/specials/grand-prix-5000-s-tr-comparison" target="_blank" rel="noopener noreferrer">bicyclerollingresistance.com</a>&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p>Sheldon Brown on <a href="https://www.sheldonbrown.com/brandt/snakebites.html" target="_blank" rel="noopener noreferrer">Snakebite Punctures</a>&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p>Wikipedia on bonking <a href="https://en.wikipedia.org/wiki/Hitting_the_wall" target="_blank" rel="noopener noreferrer">aka Hitting the Wall</a>&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></description></item><item><title>Map Hopping Made Easy! 🗺️🦘🗺️</title><link>https://davidwhittingham.com/posts/maphopping/</link><pubDate>Tue, 09 Jul 2024 23:28:25 +0200</pubDate><author><name>Author</name></author><guid>https://davidwhittingham.com/posts/maphopping/</guid><description><![CDATA[<div class="featured-image">
                <img src="/media/maphopping/all_maps_greenwich.avif" referrerpolicy="no-referrer">
            </div><h2 id="the-problem" class="headerLink">
    <a href="#the-problem" class="header-mark"></a>The Problem</h2><p>I use a lot of different maps applications and often end up &ldquo;map hopping&rdquo;. That involves finding a place on one mapping app and then trying to locate the same place on another app for &ldquo;reasons&rdquo;.</p>
<p>Sometimes this is a smooth and quick process, but other times it can be a real pain fumbling around to digitally triangulate<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> and locate the same place on both maps.</p>
<figure><a class="lightgallery" href="/media/maphopping/maphopping_skiguru_komoot.avif" title="Trying to line up Komoot with SkiGuru" data-thumbnail="/media/maphopping/maphopping_skiguru_komoot.avif" data-sub-html="<h2>Trying to line up Komoot with SkiGuru</h2>">
        <img
            
            loading="lazy"
            src="/media/maphopping/maphopping_skiguru_komoot.avif"
            srcset="/media/maphopping/maphopping_skiguru_komoot.avif, /media/maphopping/maphopping_skiguru_komoot.avif 1.5x, /media/maphopping/maphopping_skiguru_komoot.avif 2x"
            sizes="auto"
            alt="Trying to line up Komoot with SkiGuru">
    </a><figcaption class="image-caption">Trying to line up Komoot with SkiGuru</figcaption>
    </figure>
<p>You might encounter two very different looking basemaps, which often leads to squinting and searching for a uniquely-shaped river bend on both maps, using that to line things up and then inevitably finding another bend in a river and doubting if you&rsquo;re even looking at the same thing.</p>
<h2 id="the-solution" class="headerLink">
    <a href="#the-solution" class="header-mark"></a>The Solution</h2><p><a href="https://mapswap.trailsta.sh/" target="_blank" rel="noopener noreferrer">MapSwap</a>, is a  simple yet ingenious tool I recently discovered through the <a href="https://slack.openstreetmap.us/" target="_blank" rel="noopener noreferrer">OSM US Slack community</a>. It provides a way of switching between mapping application whilst preserving the viewport. This means no more time spent manually aligning things and is a game changer 🔥.</p>
<figure><a class="lightgallery" href="/media/maphopping/mapswap.avif" title="Screen capture of MapSwap Demo" data-thumbnail="/media/maphopping/mapswap.avif" data-sub-html="<h2>MapSwap Demo</h2>">
        <img
            
            loading="lazy"
            src="/media/maphopping/mapswap.avif"
            srcset="/media/maphopping/mapswap.avif, /media/maphopping/mapswap.avif 1.5x, /media/maphopping/mapswap.avif 2x"
            sizes="auto"
            alt="Screen capture of MapSwap Demo">
    </a><figcaption class="image-caption">MapSwap Demo</figcaption>
    </figure>
<h2 id="encoding-the-viewport-in-the-url" class="headerLink">
    <a href="#encoding-the-viewport-in-the-url" class="header-mark"></a>Encoding the viewport in the URL</h2><p>Many mapping services use a common pattern of encoding the viewport settings (latitude, longitude and zoom level) into the URL using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/hash" target="_blank" rel="noopener noreferrer">hash property</a>.</p>
<p>Consider for the sake of some examples, the undisputed arbitrarily-chosen center of the World<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>, The Royal Observatory in Greenwich, London with co-ordinates <code>51.4769° N, 0.0000° E</code> and a arbitrary zoom level of <code>13</code>.</p>
<p>Here’s how different mapping services encode this information into the URL:</p>
<ul>
<li>OpenStreetMap: <a href="https://www.openstreetmap.org/#map=13/51.4769/0.0000" target="_blank" rel="noopener noreferrer"><code>https://www.openstreetmap.org/#map=13/51.4769/0.0000</code></a></li>
<li>Komoot: <a href="https://www.komoot.com/plan/@51.4769000,0.0000000,12.000z" target="_blank" rel="noopener noreferrer"><code>https://www.komoot.com/plan/@51.4769000,0.0000000,12.000z</code></a></li>
<li>Windy: <a href="https://www.windy.com/?51.476,0.000,13" target="_blank" rel="noopener noreferrer"><code>https://www.windy.com/?51.477,0.000,13</code></a></li>
<li>Google Maps: <a href="https://www.google.com/maps/@51.4769,0.0000,14z" target="_blank" rel="noopener noreferrer"><code>https://www.google.com/maps/@51.4769,0.0000,13z</code></a></li>
</ul>
<p>Most mapping libraries have built-in support for this feature out of the box.</p>
<div class="details admonition tip open">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-lightbulb fa-fw"></i>Tip<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content"><p>If you&rsquo;re a developer using <a href="https://maplibre.org/maplibre-gl-js/docs" target="_blank" rel="noopener noreferrer">MapLibre</a> or <a href="https://www.mapbox.com/mapbox-gljs" target="_blank" rel="noopener noreferrer">Mapbox</a>, you can simply set the <a href="https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/MapOptions/#hash" target="_blank" rel="noopener noreferrer"><code>hash</code> mapOption</a> to <code>true</code> when initializing the map, and the library will automatically update the URL hash when the map is moved or zoomed.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">map</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Map</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">container</span><span class="o">:</span> <span class="s1">&#39;map&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">center</span><span class="o">:</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mf">51.4769</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nx">zoom</span><span class="o">:</span> <span class="mi">13</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">hash</span><span class="o">:</span> <span class="kc">true</span> <span class="c1">// SET THIS TO TRUE!
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">})</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>If you&rsquo;re using <a href="https://leafletjs.com" target="_blank" rel="noopener noreferrer">Leaflet</a>, there is a popular plugin that does this <a href="https://github.com/mlevans/leaflet-hash" target="_blank" rel="noopener noreferrer">https://github.com/mlevans/leaflet-hash</a>.</p>
</div>
        </div>
    </div>
<h2 id="how-mapswap-works" class="headerLink">
    <a href="#how-mapswap-works" class="header-mark"></a>How MapSwap works</h2><p>MapSwap is a little utility that you can add to your web browser, a Bookmarklet<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>. When you click the &ldquo;MapSwap&rdquo; bookmark, the browser executes a bit of javascript that parses the {x} and {y} coordinates and {z} zoom level from a map via the URL, using the <code>window.location</code><sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> object. It then opens another tab, allowing you to select from various mapping applications. The selected application will open configured with the same coordinates and zoom level, essentially preserving the original viewport.</p>
<p>The file <a href="https://gitlab.com/trailstash/mapswap/-/blob/main/swap/maps.js?ref_type=heads" target="_blank" rel="noopener noreferrer"><code>map.js</code></a> houses a JSON array of mapping application entries.</p>
<p>Each entry contains the name of the application, a URL template string, an icon URL, and an array of tags.</p>
<p>A entry for Komoot would look something like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;Komoot&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">template</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;https://www.komoot.com/plan/@{y},{x},{z}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">icon</span><span class="o">:</span> <span class="s2">&#34;https://www.komoot.com/favicon.ico&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">tags</span><span class="o">:</span> <span class="p">[</span><span class="s2">&#34;commercial&#34;</span><span class="p">,</span> <span class="s2">&#34;outdoor&#34;</span><span class="p">,</span> <span class="s2">&#34;osm&#34;</span><span class="p">,</span> <span class="s2">&#34;bike&#34;</span><span class="p">,</span> <span class="s2">&#34;routing&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The template string uses the placeholders <code>{x}</code>, <code>{y}</code> and <code>{z}</code> to insert the coordinates and zoom level into the URL. The tags are used to filter the applications in the UI.</p>
<p>The full list of parameters is documented in the <a href="https://gitlab.com/trailstash/mapswap/-/blob/main/README.md" target="_blank" rel="noopener noreferrer">MapSwap README</a>.</p>
<ul>
<li><code>{x}</code> - Longitude</li>
<li><code>{y}</code> - Latitude</li>
<li><code>{z}</code> - Mapbox &amp; MapLibre style Floating-point Zoom level</li>
<li><code>{Z}</code> - Google style Floating-point Zoom level (one greater than Mapbox/MapLibre style zoom)</li>
<li><code>{Zr}</code> - Leaflet style integer Zoom level (one greater than Mapbox/Maplibre style zoom)</li>
<li><code>{zr}</code> - Integer Zoom level (don&rsquo;t know if any maps use this)</li>
</ul>
<h2 id="the-mapping-community" class="headerLink">
    <a href="#the-mapping-community" class="header-mark"></a>The Mapping community</h2><p>The online mapping community is incredibly welcoming and friendly. I am repeatedly impressed by the quality of the tools and the willingness of developers to help each other out.</p>
<p>I noticed that <a href="https://www.komoot.com" target="_blank" rel="noopener noreferrer">Komoot</a> wasn&rsquo;t supported in MapSwap, so I messaged the creator <a href="https://schep.me" target="_blank" rel="noopener noreferrer">Daniel Schep</a> and put in a <a href="https://gitlab.com/trailstash/mapswap/-/merge_requests/2" target="_blank" rel="noopener noreferrer">PR</a> to add Komoot to the tool. It was a pleasure to chat with Daniel, and the PR was merged in no time.</p>
<h2 id="conclusion" class="headerLink">
    <a href="#conclusion" class="header-mark"></a>Conclusion</h2><p>There are many different mapping applications out there, each having pros and cons. Combining them together is incredibly useful, and MapSwap makes this easy. It means less fumbling between different maps trying to pinpoint the same location and that&rsquo;s a win in my book.</p>
<figure><a class="lightgallery" href="/media/maphopping/all_maps_greenwich.avif" title="Screencapture of map hopping in Greenwich, London" data-thumbnail="/media/maphopping/all_maps_greenwich.avif" data-sub-html="<h2>Map hopping in Greenwich, London</h2>">
        <img
            
            loading="lazy"
            src="/media/maphopping/all_maps_greenwich.avif"
            srcset="/media/maphopping/all_maps_greenwich.avif, /media/maphopping/all_maps_greenwich.avif 1.5x, /media/maphopping/all_maps_greenwich.avif 2x"
            sizes="auto"
            alt="Screencapture of map hopping in Greenwich, London">
    </a><figcaption class="image-caption">Map hopping in Greenwich, London</figcaption>
    </figure>
<p>The next level to this would be syncing different maps viewports in real-time.</p>
<p>Imagine planning a big winter ski-touring-bike-packing trip using Komoot on the left side of the screen and then both snow conditions and ski touring routes on the right side, and everything moving in sync.</p>
<p>Sounds awesome, however browser security restrictions make this a bit tricky&hellip; a topic for another post.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://en.wikipedia.org/wiki/Triangulation" target="_blank" rel="noopener noreferrer">https://en.wikipedia.org/wiki/Triangulation</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://en.wikipedia.org/wiki/Prime_meridian#Prime_meridian_at_Greenwich" target="_blank" rel="noopener noreferrer">https://en.wikipedia.org/wiki/Prime_meridian#Prime_meridian_at_Greenwich</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://en.wikipedia.org/wiki/Bookmarklet" target="_blank" rel="noopener noreferrer">https://en.wikipedia.org/wiki/Bookmarklet</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/location" target="_blank" rel="noopener noreferrer">https://developer.mozilla.org/en-US/docs/Web/API/Window/location</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>]]></description></item><item><title>Map Loading... 🗺️📏🎨</title><link>https://davidwhittingham.com/posts/maploading/</link><pubDate>Mon, 25 Mar 2024 11:42:41 +0100</pubDate><author><name>Author</name></author><guid>https://davidwhittingham.com/posts/maploading/</guid><description><![CDATA[<div class="featured-image">
                <img src="/media/maploading/map_race.webp" referrerpolicy="no-referrer">
            </div><h2 id="introduction" class="headerLink">
    <a href="#introduction" class="header-mark"></a>Introduction</h2><p>When using a map on a digital device, it is important that it is fast to load and the map interactions are seamlessly smooth. A fast loading map will go by unnoticed as &ldquo;everything works&rdquo; leading to a natural feeling experience. A slow loading map will get in the way of a good user experience, leading to frustration, grief and a bad taste in the mouth.</p>
<figure><a class="lightgallery" href="/media/maploading/slowloading.avif" title="GIF of a slow loading map" data-thumbnail="/media/maploading/slowloading.avif" data-sub-html="<h2>A slow loading map</h2>">
        <img
            
            loading="lazy"
            src="/media/maploading/slowloading.avif"
            srcset="/media/maploading/slowloading.avif, /media/maploading/slowloading.avif 1.5x, /media/maploading/slowloading.avif 2x"
            sizes="auto"
            alt="GIF of a slow loading map">
    </a><figcaption class="image-caption">A slow loading map</figcaption>
    </figure>
<p>Vector maps are being used more and more on the web. They offer a faster and more interactive experience and more ways of being styled.</p>
<div class="details admonition question open">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-question-circle fa-fw"></i>What is a vector map?<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content"><p>Vector maps use vector data, made up of points, lines and polygons with accompanying meta-data. This is downloaded to the device and the map you end up seeing on the screen, is as a result of rendering on the client side and certain styling instructions.</p>
<p>Vector maps are the opposite of raster maps, which are made up of pixel data pre-rendered on a server. Raster maps are harder to manipulate and style.</p>
<p>The best resource out there to to have a refresh on web maps is <a href="https://mapschool.io/" target="_blank" rel="noopener noreferrer">mapschool.io</a>. It explains the difference between raster and vector maps and much more.</p>
</div>
        </div>
    </div>
<p>I am using <a href="https://maplibre.org/maplibre-gl-js/docs/" target="_blank" rel="noopener noreferrer"><code>maplibre-gl</code></a>, which is a vector map library in a hobby project and  interested in finding out which map style is the fastest to load.</p>
<h3 id="map-drawing-101" class="headerLink">
    <a href="#map-drawing-101" class="header-mark"></a>Map drawing 101</h3><p>A vector map library needs a <em>recipe</em> to draw a map on the screen. The <em>recipe</em> will have instructions telling the mapping library <strong>where to request</strong> the vector data from and <strong>how to render</strong> that data to the screen. Rendering instructions include what parts of the data to draw, what colors to use and what order to draw the layers of data in.</p>
<p>When you pan and zoom to a specific part of a vector map, the library will know based on this <em>recipe</em>, where to request the body of data that should fill up the screen and how to style it.</p>
<div class="details admonition tip open">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-lightbulb fa-fw"></i>What else is it called?<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content">This <em>recipe</em> is often called a <code>style</code> document, which is usually a json file conforming to a specification (in my case the <a href="https://maplibre.org/maplibre-style-spec/" target="_blank" rel="noopener noreferrer">Maplibre style spec</a>).</div>
        </div>
    </div>
<p>A good designer aka a cartograpaher, will design a style in a way that it shows off the best parts of the data with certain balance of space and color, producing something that looks appealing and allows easy reading of the data.</p>
<figure><a class="lightgallery" href="/media/maploading/painting_map.webp" title="A artist painting a map" data-thumbnail="/media/maploading/painting_map.webp" data-sub-html="<h2>A artist painting a map</h2>">
        <img
            
            loading="lazy"
            src="/media/maploading/painting_map.webp"
            srcset="/media/maploading/painting_map.webp, /media/maploading/painting_map.webp 1.5x, /media/maploading/painting_map.webp 2x"
            sizes="auto"
            alt="A artist painting a map">
    </a><figcaption class="image-caption">A artist painting a map</figcaption>
    </figure>
<h3 id="what-makes-things-fast-or-slow" class="headerLink">
    <a href="#what-makes-things-fast-or-slow" class="header-mark"></a>What makes things fast or slow?</h3><p>The time taken to display a map on the screen will depend on where the data comes from and how complex the rendering instructions are.</p>
<p>It makes sense that a style that is simple and requests a little data from somewhere close to the user will load faster than a style that is complex and requests a lot of data from somewhere far away from the user.</p>
<h4 id="a-peek-inside-a-style-document" class="headerLink">
    <a href="#a-peek-inside-a-style-document" class="header-mark"></a>A Peek inside a Style document</h4><p>Here is a taste of a style à la MapTiler Basic Light**</p>
<p>(note: I have omitted most parts to simplify the example)</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;version&#34;</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;basic&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Basic Light**&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;sources&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;openmaptiles&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;url&#34;</span><span class="p">:</span> <span class="s2">&#34;https://api.maptiler.com/tiles/v3/tiles.json?key={YOUR_API_KEY}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;vector&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;maptiler_attribution&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;attribution&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;a href=\&#34;https://www.maptiler.com/copyright/\&#34; target=\&#34;_blank\&#34;&gt;&amp;copy; MapTiler&lt;/a&gt; &lt;a href=\&#34;https://www.openstreetmap.org/copyright\&#34; target=\&#34;_blank\&#34;&gt;&amp;copy; OpenStreetMap contributors&lt;/a&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;vector&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;layers&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;background&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;background&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;paint&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;background-color&#34;</span><span class="p">:</span> <span class="s2">&#34;rgba(224, 224, 208, 1)&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;landcover_grass&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;fill&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;openmaptiles&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;source-layer&#34;</span><span class="p">:</span> <span class="s2">&#34;landcover&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;paint&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;fill-color&#34;</span><span class="p">:</span> <span class="s2">&#34;rgba(192, 213, 169, 1)&#34;</span><span class="p">,</span> <span class="nt">&#34;fill-opacity&#34;</span><span class="p">:</span> <span class="mf">0.4</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;filter&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;==&#34;</span><span class="p">,</span> <span class="s2">&#34;class&#34;</span><span class="p">,</span> <span class="s2">&#34;grass&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;landcover_wood&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;fill&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;openmaptiles&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;source-layer&#34;</span><span class="p">:</span> <span class="s2">&#34;landcover&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;paint&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;fill-color&#34;</span><span class="p">:</span> <span class="s2">&#34;hsl(82, 46%, 72%)&#34;</span><span class="p">,</span> <span class="nt">&#34;fill-opacity&#34;</span><span class="p">:</span> <span class="mf">0.8</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;filter&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;==&#34;</span><span class="p">,</span> <span class="s2">&#34;class&#34;</span><span class="p">,</span> <span class="s2">&#34;wood&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;water&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;fill&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;openmaptiles&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;source-layer&#34;</span><span class="p">:</span> <span class="s2">&#34;water&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;layout&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;visibility&#34;</span><span class="p">:</span> <span class="s2">&#34;visible&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;paint&#34;</span><span class="p">:</span> <span class="p">{</span> <span class="nt">&#34;fill-color&#34;</span><span class="p">:</span> <span class="s2">&#34;hsl(205, 56%, 73%)&#34;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;filter&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;all&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;!=&#34;</span><span class="p">,</span> <span class="s2">&#34;intermittent&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">],</span> <span class="p">[</span><span class="s2">&#34;!=&#34;</span><span class="p">,</span> <span class="s2">&#34;brunnel&#34;</span><span class="p">,</span> <span class="s2">&#34;tunnel&#34;</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;building&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;fill&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;openmaptiles&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;source-layer&#34;</span><span class="p">:</span> <span class="s2">&#34;building&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;paint&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;fill-color&#34;</span><span class="p">:</span> <span class="s2">&#34;rgba(212, 204, 176, 1)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;fill-opacity&#34;</span><span class="p">:</span> <span class="mf">0.6</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;fill-antialias&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;glyphs&#34;</span><span class="p">:</span> <span class="s2">&#34;https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key={YOUR_API_KEY}&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;bearing&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;pitch&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;center&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;zoom&#34;</span><span class="p">:</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>It a very simple style that only shows a few of the features you would expect of a map. It has a background color, grass and wood areas, water, buildings and some text.</p>
<p>There are two sources of data, one for the map data and one for the attribution. This informs the library where to request data from the tile server with address <code>https://api.maptiler.com/tiles/v3/tiles.json?key={YOUR_API_KEY}</code>.</p>
<p>The layers key is where the rendering instructions are. Each layer has an id, a type, a source (in our case it is always the single <code>openmaptiles</code> source) and some paint instructions.</p>
<p>The background gets loaded, polygons of grass gets drawn with a opaque green, water gets drawn in blue, but not when it goes through a tunnel, building footprints rise up.</p>
<p>Paint instructions can get quite complex, and contain many conditional rules and transformations. Things that can be controlled are the size lines, opacity of fills and the font of the text.</p>
<div class="details admonition info open">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-info-circle fa-fw"></i>The combination of vector and raster<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content">Vector maps can also contain raster data layers. The raster data is delivered pre-rendered and is ready to be drawn on the screen. This is useful for things like satellite imagery, where the data is too complex to be drawn on the fly.</div>
        </div>
    </div>
<h4 id="where-does-the-actual-vector-data-get-served-from" class="headerLink">
    <a href="#where-does-the-actual-vector-data-get-served-from" class="header-mark"></a>Where does the actual vector data get served from?</h4><p>See the <code>sources</code> key in the above style document. This is where the vector data is served from. The data is served in a format called vector tiles. These are small chunks of data that are ready to be drawn on the screen. The data is normally served in a format that is easy to draw and easy to style.</p>
<div class="details admonition info open">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-info-circle fa-fw"></i>What format is the data in?<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content"><p>Mapbox created the <a href="https://github.com/mapbox/vector-tile-spec" target="_blank" rel="noopener noreferrer">Mapbox Vector Tile Specification</a>.</p>
<p>The data is encoded in a format called <a href="https://developers.google.com/protocol-buffers" target="_blank" rel="noopener noreferrer">Protocol Buffers</a>. This is a fast to parse and is easy to compress binary format. This is why it is used for vector tiles.</p>
<p>There are many tools out there to generate and serve vector tiles. The <a href="https://github.com/mapbox/awesome-vector-tiles" target="_blank" rel="noopener noreferrer">Awesome vector tiles</a></p>
</div>
        </div>
    </div>
<p>The ideal place to get the data is the clients device storage. This is fast as the data is ready to be used straight away without requesting and downloading it over a internet connection. But this is not practical for most use cases.</p>
<p>The next best thing is a server vector tile server. This server can be hosted by a tile service provider, or it can be hosted by yourself. There are lots of tools out there to roll your own solution. The <a href="https://github.com/mapbox/awesome-vector-tiles" target="_blank" rel="noopener noreferrer">Awesome vector tiles</a> repo on github is a good place to start.</p>
<p>Most people will use a third party provider for ease and other reasons instead of DIYing. These providers will have servers all over the world that will serve the data to the user close to where they are.</p>
<h3 id="where-can-you-get-one-of-these-style-recipe-from" class="headerLink">
    <a href="#where-can-you-get-one-of-these-style-recipe-from" class="header-mark"></a>Where can you get one of these style recipe from?</h3><p>There are many different places to get vector map styles. Providers offer them. Some of them are free, some of them are paid. Some of them are open source, some of them are closed source. Some of them result in a fast loading experience, some do not.</p>
<p>Different providers offer different styles and it is not always clear which one is the fastest, the Usain Bolt of map styles.</p>
<p>It is also possible to write a custom style, and there are many tools out there that can help you. Maplibre-gl has a <a href="https://github.com/maplibre/maplibre-style-spec" target="_blank" rel="noopener noreferrer">style spec</a> that you can use to write your own style. MapTiler has a <a href="https://www.maptiler.com/cloud/customize/" target="_blank" rel="noopener noreferrer">online style editor</a> that you can use to create your own style.</p>
<h2 id="the-experiment" class="headerLink">
    <a href="#the-experiment" class="header-mark"></a>The Experiment</h2><p>I used playwright, a browser automation library to launch a chromium browser, initialize a map with a style and time how long it took for the <a href="https://maplibre.org/maplibre-gl-js/docs/API/types/MapEventType/" target="_blank" rel="noopener noreferrer">relevant loaded event</a> to fire. This was done for a number of different styles.</p>
<p>The experiment was run on my local machine (A 10 year old HP Pavilion) with a 50mbps internet connection somewhere in Germany.</p>
<p>All <a href="#code-used-to-profile-the-styles" rel="">the code</a> is available in <a href="https://github.com/01100100/mapStyleProfile" target="_blank" rel="noopener noreferrer">this repo</a> if you want to replicate the experiment.</p>
<p>Note: Your mileage may vary depending on your hardware and internet connection.</p>
<h3 id="styles-tested" class="headerLink">
    <a href="#styles-tested" class="header-mark"></a>Styles tested</h3><p>The <a href="https://wiki.openstreetmap.org/wiki/Vector_tiles#Providers" target="_blank" rel="noopener noreferrer">Open Street Map Wiki</a> lists different vector tile providers. I tested the following:</p>
<ul>
<li><a href="https://www.maptiler.com/" target="_blank" rel="noopener noreferrer">MapTiler</a></li>
<li><a href="https://stadiamaps.com/" target="_blank" rel="noopener noreferrer">Stadia Maps</a></li>
</ul>
<div class="details admonition abstract">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-list-ul fa-fw"></i>The styles tested<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content"><table>
  <thead>
      <tr>
          <th>Provider</th>
          <th>Style</th>
          <th>URL</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>MapTiler</td>
          <td>Backdrop</td>
          <td><a href="https://api.maptiler.com/maps/backdrop/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/backdrop/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>Basic</td>
          <td><a href="https://api.maptiler.com/maps/basic/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/basic/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>Bright</td>
          <td><a href="https://api.maptiler.com/maps/bright/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/bright/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>Dataviz</td>
          <td><a href="https://api.maptiler.com/maps/dataviz/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/dataviz/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>Landscape</td>
          <td><a href="https://api.maptiler.com/maps/landscape/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/landscape/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>Ocean</td>
          <td><a href="https://api.maptiler.com/maps/ocean/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/ocean/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>OpenStreetMap</td>
          <td><a href="https://api.maptiler.com/maps/openstreetmap/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/openstreetmap/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>Outdoor</td>
          <td><a href="https://api.maptiler.com/maps/outdoor/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/outdoor/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>Satellite</td>
          <td><a href="https://api.maptiler.com/maps/satellite/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/satellite/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>Streets</td>
          <td><a href="https://api.maptiler.com/maps/streets/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/streets/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>Toner</td>
          <td><a href="https://api.maptiler.com/maps/toner/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/toner/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>Topo</td>
          <td><a href="https://api.maptiler.com/maps/topo/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/topo/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>MapTiler</td>
          <td>Winter</td>
          <td><a href="https://api.maptiler.com/maps/winter/style.json?key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://api.maptiler.com/maps/winter/style.json?key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>StadiaMaps</td>
          <td>Alidade Smooth</td>
          <td><a href="https://tiles.stadiamaps.com/styles/alidade_smooth.json?api_key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://tiles.stadiamaps.com/styles/alidade_smooth.json?api_key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>StadiaMaps</td>
          <td>Alidade Smooth Dark</td>
          <td><a href="https://tiles.stadiamaps.com/styles/alidade_smooth_dark.json?api_key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://tiles.stadiamaps.com/styles/alidade_smooth_dark.json?api_key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>StadiaMaps</td>
          <td>Alidade Satellite</td>
          <td><a href="https://tiles.stadiamaps.com/styles/alidade_satellite.json?api_key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://tiles.stadiamaps.com/styles/alidade_satellite.json?api_key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>StadiaMaps</td>
          <td>Stadia Outdoors</td>
          <td><a href="https://tiles.stadiamaps.com/styles/stadia_outdoors.json?api_key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://tiles.stadiamaps.com/styles/stadia_outdoors.json?api_key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>StadiaMaps</td>
          <td>Stamen Toner</td>
          <td><a href="https://tiles.stadiamaps.com/styles/stamen_toner.json?api_key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://tiles.stadiamaps.com/styles/stamen_toner.json?api_key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>StadiaMaps</td>
          <td>Stamen Terrain</td>
          <td><a href="https://tiles.stadiamaps.com/styles/stamen_terrain.json?api_key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://tiles.stadiamaps.com/styles/stamen_terrain.json?api_key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>StadiaMaps</td>
          <td>Stamen Watercolor</td>
          <td><a href="https://tiles.stadiamaps.com/styles/stamen_watercolor.json?api_key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://tiles.stadiamaps.com/styles/stamen_watercolor.json?api_key={API_KEY}</a></td>
      </tr>
      <tr>
          <td>StadiaMaps</td>
          <td>OSM Bright</td>
          <td><a href="https://tiles.stadiamaps.com/styles/osm_bright.json?api_key=%7bAPI_KEY%7d" target="_blank" rel="noopener noreferrer">https://tiles.stadiamaps.com/styles/osm_bright.json?api_key={API_KEY}</a></td>
      </tr>
  </tbody>
</table>
</div>
        </div>
    </div>
<h3 id="code-used-to-profile-the-styles" class="headerLink">
    <a href="#code-used-to-profile-the-styles" class="header-mark"></a>Code used to profile the styles</h3><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">  1
</span><span class="lnt">  2
</span><span class="lnt">  3
</span><span class="lnt">  4
</span><span class="lnt">  5
</span><span class="lnt">  6
</span><span class="lnt">  7
</span><span class="lnt">  8
</span><span class="lnt">  9
</span><span class="lnt"> 10
</span><span class="lnt"> 11
</span><span class="lnt"> 12
</span><span class="lnt"> 13
</span><span class="lnt"> 14
</span><span class="lnt"> 15
</span><span class="lnt"> 16
</span><span class="lnt"> 17
</span><span class="lnt"> 18
</span><span class="lnt"> 19
</span><span class="lnt"> 20
</span><span class="lnt"> 21
</span><span class="lnt"> 22
</span><span class="lnt"> 23
</span><span class="lnt"> 24
</span><span class="lnt"> 25
</span><span class="lnt"> 26
</span><span class="lnt"> 27
</span><span class="lnt"> 28
</span><span class="lnt"> 29
</span><span class="lnt"> 30
</span><span class="lnt"> 31
</span><span class="lnt"> 32
</span><span class="lnt"> 33
</span><span class="lnt"> 34
</span><span class="lnt"> 35
</span><span class="lnt"> 36
</span><span class="lnt"> 37
</span><span class="lnt"> 38
</span><span class="lnt"> 39
</span><span class="lnt"> 40
</span><span class="lnt"> 41
</span><span class="lnt"> 42
</span><span class="lnt"> 43
</span><span class="lnt"> 44
</span><span class="lnt"> 45
</span><span class="lnt"> 46
</span><span class="lnt"> 47
</span><span class="lnt"> 48
</span><span class="lnt"> 49
</span><span class="lnt"> 50
</span><span class="lnt"> 51
</span><span class="lnt"> 52
</span><span class="lnt"> 53
</span><span class="lnt"> 54
</span><span class="lnt"> 55
</span><span class="lnt"> 56
</span><span class="lnt"> 57
</span><span class="lnt"> 58
</span><span class="lnt"> 59
</span><span class="lnt"> 60
</span><span class="lnt"> 61
</span><span class="lnt"> 62
</span><span class="lnt"> 63
</span><span class="lnt"> 64
</span><span class="lnt"> 65
</span><span class="lnt"> 66
</span><span class="lnt"> 67
</span><span class="lnt"> 68
</span><span class="lnt"> 69
</span><span class="lnt"> 70
</span><span class="lnt"> 71
</span><span class="lnt"> 72
</span><span class="lnt"> 73
</span><span class="lnt"> 74
</span><span class="lnt"> 75
</span><span class="lnt"> 76
</span><span class="lnt"> 77
</span><span class="lnt"> 78
</span><span class="lnt"> 79
</span><span class="lnt"> 80
</span><span class="lnt"> 81
</span><span class="lnt"> 82
</span><span class="lnt"> 83
</span><span class="lnt"> 84
</span><span class="lnt"> 85
</span><span class="lnt"> 86
</span><span class="lnt"> 87
</span><span class="lnt"> 88
</span><span class="lnt"> 89
</span><span class="lnt"> 90
</span><span class="lnt"> 91
</span><span class="lnt"> 92
</span><span class="lnt"> 93
</span><span class="lnt"> 94
</span><span class="lnt"> 95
</span><span class="lnt"> 96
</span><span class="lnt"> 97
</span><span class="lnt"> 98
</span><span class="lnt"> 99
</span><span class="lnt">100
</span><span class="lnt">101
</span><span class="lnt">102
</span><span class="lnt">103
</span><span class="lnt">104
</span><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">asyncio</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">playwright.async_api</span> <span class="kn">import</span> <span class="n">async_playwright</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">MAPTILER_API_KEY</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;MAPTILER_API_KEY&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">STADIA_API_KEY</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;STADIA_API_KEY&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">MAPTILER_API_KEY</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">or</span> <span class="n">STADIA_API_KEY</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;MAPTILER_API_KEY and STADIA_API_KEY environment variables must be set&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">STYLES</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;MapTiler - Backdrop&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/backdrop/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;MapTiler - Basic&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/basic/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Maptiler - Bright&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/bright/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Maptiler - Dataviz&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/dataviz/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Maptiler - Landscape&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/landscape/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Maptiler - Ocean&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/ocean/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Maptiler - OpenStreetMap&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/openstreetmap/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Maptiler - Outdoor&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/outdoor/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Maptiler - Satellite&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/satellite/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Maptiler - Streets&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/streets/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Maptiler - Toner&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/toner/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Maptiler - Topo&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/topo/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Maptiler - Winter&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://api.maptiler.com/maps/winter/style.json?key=</span><span class="si">{</span><span class="n">MAPTILER_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;StadiaMaps - Alidade Smooth&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://tiles.stadiamaps.com/styles/alidade_smooth.json?api_key=</span><span class="si">{</span><span class="n">STADIA_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;StadiaMaps - Alidade Smooth Dark&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://tiles.stadiamaps.com/styles/alidade_smooth_dark.json?api_key=</span><span class="si">{</span><span class="n">STADIA_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;StadiaMaps - Alidade Satellite&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://tiles.stadiamaps.com/styles/alidade_satellite.json?api_key=</span><span class="si">{</span><span class="n">STADIA_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;StadiaMaps - Stadia Outdoors&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://tiles.stadiamaps.com/styles/outdoors.json?api_key=</span><span class="si">{</span><span class="n">STADIA_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;StadiaMaps - Stamen Toner&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://tiles.stadiamaps.com/styles/stamen_toner.json?api_key=</span><span class="si">{</span><span class="n">STADIA_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;StadiaMaps - Stamen Terrain&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://tiles.stadiamaps.com/styles/stamen_terrain.json?api_key=</span><span class="si">{</span><span class="n">STADIA_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;StadiaMaps - Stamen Watercolor&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://tiles.stadiamaps.com/styles/stamen_watercolor.json?api_key=</span><span class="si">{</span><span class="n">STADIA_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;StadiaMaps - OSM Bright&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;https://tiles.stadiamaps.com/styles/osm_bright.json?api_key=</span><span class="si">{</span><span class="n">STADIA_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">time_style</span><span class="p">(</span><span class="n">style_name</span><span class="p">,</span> <span class="n">style_url</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">html_content</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;!DOCTYPE html&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;html&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;head&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">        &lt;title&gt;Vector Map Style Profiler&lt;/title&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">        &lt;script src=&#34;https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js&#34;&gt;&lt;/script&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">        &lt;link href=&#34;https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css&#34; rel=&#34;stylesheet&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;/head&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;style&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">        html </span><span class="se">{{</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">            height: 100%;
</span></span></span><span class="line"><span class="cl"><span class="s2">        </span><span class="se">}}</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">        body </span><span class="se">{{</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">            height: 100%;
</span></span></span><span class="line"><span class="cl"><span class="s2">            align-items: stretch;
</span></span></span><span class="line"><span class="cl"><span class="s2">            margin: 0;
</span></span></span><span class="line"><span class="cl"><span class="s2">            padding: 0;
</span></span></span><span class="line"><span class="cl"><span class="s2">        </span><span class="se">}}</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">        #map </span><span class="se">{{</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">            flex-grow: 1;
</span></span></span><span class="line"><span class="cl"><span class="s2">            min-height: 100%;
</span></span></span><span class="line"><span class="cl"><span class="s2">            max-height: 100%;
</span></span></span><span class="line"><span class="cl"><span class="s2">        </span><span class="se">}}</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;/style&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;body&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">        &lt;div id=&#34;map&#34; style=&#34;width: 100%; height: 100%;&#34;&gt;&lt;/div&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">        &lt;script&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">            const startTime = performance.now();
</span></span></span><span class="line"><span class="cl"><span class="s2">            const map = new maplibregl.Map(</span><span class="se">{{</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">                container: &#34;map&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">                style: &#34;</span><span class="si">{</span><span class="n">style_url</span><span class="si">}</span><span class="s2">&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">                center: [0, 51.4769], // Greenwich meridian
</span></span></span><span class="line"><span class="cl"><span class="s2">                zoom: 10,
</span></span></span><span class="line"><span class="cl"><span class="s2">                maxZoom: 18,
</span></span></span><span class="line"><span class="cl"><span class="s2">                minZoom: 5,
</span></span></span><span class="line"><span class="cl"><span class="s2">            </span><span class="se">}}</span><span class="s2">);
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">            map.on(&#39;load&#39;, (e) =&gt; </span><span class="se">{{</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">                const endLoadTime = performance.now();
</span></span></span><span class="line"><span class="cl"><span class="s2">                loadTime = endLoadTime - startTime;
</span></span></span><span class="line"><span class="cl"><span class="s2">                window.loadTime = loadTime;
</span></span></span><span class="line"><span class="cl"><span class="s2">            </span><span class="se">}}</span><span class="s2">);
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">        &lt;/script&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;/body&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;/html&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">with</span> <span class="n">async_playwright</span><span class="p">()</span> <span class="k">as</span> <span class="n">p</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">browser_type</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">chromium</span>
</span></span><span class="line"><span class="cl">        <span class="n">browser</span> <span class="o">=</span> <span class="k">await</span> <span class="n">browser_type</span><span class="o">.</span><span class="n">launch</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span> <span class="o">=</span> <span class="k">await</span> <span class="n">browser</span><span class="o">.</span><span class="n">new_page</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">await</span> <span class="n">page</span><span class="o">.</span><span class="n">set_content</span><span class="p">(</span><span class="n">html_content</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">await</span> <span class="n">page</span><span class="o">.</span><span class="n">wait_for_function</span><span class="p">(</span><span class="s2">&#34;window.loadTime&#34;</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">30000</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">load_time</span> <span class="o">=</span> <span class="k">await</span> <span class="n">page</span><span class="o">.</span><span class="n">evaluate</span><span class="p">(</span><span class="s2">&#34;() =&gt; { return window.loadTime; }&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">style_name</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">load_time</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">except</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">TimeoutError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Timeout occurred for </span><span class="si">{</span><span class="n">style_name</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;An error occurred for </span><span class="si">{</span><span class="n">style_name</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">finally</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">await</span> <span class="n">browser</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">STYLES</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">time_style</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>All code is available in the <a href="https://github.com/01100100/mapStyleProfile" target="_blank" rel="noopener noreferrer">mapStyleProfile github repository</a>.</p>
<h2 id="the-results" class="headerLink">
    <a href="#the-results" class="header-mark"></a>The Results</h2><div class="echarts" id="id-1" style="width: 100%; height: 30rem;"></div>
<div class="details admonition abstract">
        <div class="details-summary admonition-title">
            <i class="icon fas fa-list-ul fa-fw"></i>The Results Table<i class="details-icon fas fa-angle-right fa-fw"></i>
        </div>
        <div class="details-content">
            <div class="admonition-content"><table>
  <thead>
      <tr>
          <th>Provider - Style</th>
          <th>Time to load (ms)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>StadiaMaps - Stamen Watercolor</td>
          <td>709</td>
      </tr>
      <tr>
          <td>Maptiler - Dataviz</td>
          <td>1702</td>
      </tr>
      <tr>
          <td>Maptiler - Toner</td>
          <td>2156</td>
      </tr>
      <tr>
          <td>StadiaMaps - Alidade Smooth</td>
          <td>2181</td>
      </tr>
      <tr>
          <td>MapTiler - Basic</td>
          <td>2324</td>
      </tr>
      <tr>
          <td>Maptiler - Satellite</td>
          <td>2368</td>
      </tr>
      <tr>
          <td>StadiaMaps - Stamen Toner</td>
          <td>2573</td>
      </tr>
      <tr>
          <td>MapTiler - Backdrop</td>
          <td>2692</td>
      </tr>
      <tr>
          <td>Maptiler - Streets</td>
          <td>2694</td>
      </tr>
      <tr>
          <td>StadiaMaps - OSM Bright</td>
          <td>2848</td>
      </tr>
      <tr>
          <td>Maptiler - OpenStreetMap</td>
          <td>2954</td>
      </tr>
      <tr>
          <td>StadiaMaps - Stadia Outdoors</td>
          <td>2990</td>
      </tr>
      <tr>
          <td>Maptiler - Bright</td>
          <td>3005</td>
      </tr>
      <tr>
          <td>StadiaMaps - Alidade Smooth Dark</td>
          <td>3169</td>
      </tr>
      <tr>
          <td>Maptiler - Ocean</td>
          <td>3679</td>
      </tr>
      <tr>
          <td>Maptiler - Outdoor</td>
          <td>4030</td>
      </tr>
      <tr>
          <td>Maptiler - Landscape</td>
          <td>4513</td>
      </tr>
      <tr>
          <td>Maptiler - Topo</td>
          <td>4629</td>
      </tr>
      <tr>
          <td>StadiaMaps - Stamen Terrain</td>
          <td>5670</td>
      </tr>
      <tr>
          <td>Maptiler - Winter</td>
          <td>5827</td>
      </tr>
  </tbody>
</table>
</div>
        </div>
    </div>
<h2 id="conclusion" class="headerLink">
    <a href="#conclusion" class="header-mark"></a>Conclusion</h2><p>Now there are some numbers to quantify the different map styles speed.</p>
<p>Remember speed isn&rsquo;t everything, and a good map experience is a combination of many things. A fast loading map is just one part of the puzzle, along with space and color, compromises may have to be made to get the best overall experience.</p>
<h2 id="dont-just-take-my-word-for-it-test-styles-out-yourself-online-now" class="headerLink">
    <a href="#dont-just-take-my-word-for-it-test-styles-out-yourself-online-now" class="header-mark"></a>Don&rsquo;t just take my word for it, test styles out yourself online now</h2><p>I made a online tool that lets you paste a style url into it and it will time how long it takes to load the map.</p>
<div style="text-align: center;">
<p><a href="https://01100100.github.io/mapStyleProfile/" target="_blank" rel="noopener noreferrer">https://01100100.github.io/mapStyleProfile/</a></p>
</div>
<a class="lightgallery" href="/media/maploading/screenshot_frame.webp" title="Screenshot of the online tool" data-thumbnail="/media/maploading/screenshot_frame.webp">
        <img
            
            loading="lazy"
            src="/media/maploading/screenshot_frame.webp"
            srcset="/media/maploading/screenshot_frame.webp, /media/maploading/screenshot_frame.webp 1.5x, /media/maploading/screenshot_frame.webp 2x"
            sizes="auto"
            alt="Screenshot of the online tool">
    </a>
<p>All source code is available in the same <a href="https://github.com/01100100/mapStyleProfile" target="_blank" rel="noopener noreferrer">github repository</a>.</p>
<h2 id="future-ideas" class="headerLink">
    <a href="#future-ideas" class="header-mark"></a>Future ideas</h2><p>This was a simple experiment to get some numbers on the different map styles, only looking at two providers.</p>
<p>There are a few things that could be done to improve the experiment:</p>
<ul>
<li><i class="far fa-square fa-fw"></i> Add more styles from different providers to get a better idea of the landscape.</li>
<li><i class="far fa-square fa-fw"></i> Profile the different parts of the map loading process to break down ingload time</li>
<li><i class="far fa-square fa-fw"></i> Add more &ldquo;real world&rdquo; interactions to the experiment and see how the  styles perform under different conditions.</li>
<li><i class="far fa-square fa-fw"></i> Set up a github action to run the experiment on pull requests and provided a central place to see the results.</li>
</ul>]]></description></item><item><title/><link>https://davidwhittingham.com/posts/rtree/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author><name>Author</name></author><guid>https://davidwhittingham.com/posts/rtree/</guid><description></description></item></channel></rss>