<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: TopoLines</title>
    <description>The latest articles on DEV Community by TopoLines (@topolines).</description>
    <link>https://dev.to/topolines</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4038960%2F5e9809ca-eef2-461e-b4ea-37a397e1c583.png</url>
      <title>DEV Community: TopoLines</title>
      <link>https://dev.to/topolines</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/topolines"/>
    <language>en</language>
    <item>
      <title>Reading 9 national lidar terrain models over HTTP, without downloading a country</title>
      <dc:creator>TopoLines</dc:creator>
      <pubDate>Mon, 19 Oct 2026 10:42:52 +0000</pubDate>
      <link>https://dev.to/topolines/reading-9-national-lidar-terrain-models-over-http-without-downloading-a-country-11k7</link>
      <guid>https://dev.to/topolines/reading-9-national-lidar-terrain-models-over-http-without-downloading-a-country-11k7</guid>
      <description>&lt;p&gt;Most countries in Europe now publish a lidar-derived terrain model (DTM) at 1–5 m as open data. On paper that's amazing. In practice, each one sits behind a different web service with its own idea of what a "bounding box" is, what "no data" looks like, and how long you're allowed to wait.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://www.topolines.app/" rel="noopener noreferrer"&gt;TopoLines&lt;/a&gt;, a web tool that traces contour lines for a zone you draw on a map. To trace a zone in France on 5 m lidar instead of a 30 m global model, I need &lt;em&gt;that&lt;/em&gt; zone's elevations, in a few seconds, without downloading a département. So I wired nine national models plus two global ones, all read over HTTP, all in plain Python (rasterio + numpy, no GDAL CLI, no Docker).&lt;/p&gt;

&lt;p&gt;This post is the field notes. The full list — endpoints, native resolution, licence, native CRS — is open on GitHub as &lt;a href="https://github.com/alexisoules/topolines-docs/blob/main/docs/national-elevation-models.md" rel="noopener noreferrer"&gt;Markdown&lt;/a&gt; and &lt;a href="https://github.com/alexisoules/topolines-docs/blob/main/data/national-elevation-models.csv" rel="noopener noreferrer"&gt;CSV&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four shapes of "give me elevations for this box"
&lt;/h2&gt;

&lt;p&gt;Every provider I've met falls into one of four access patterns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Who&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;One big Cloud-Optimized GeoTIFF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GEDTM30 (worldwide)&lt;/td&gt;
&lt;td&gt;Read a window with HTTP range requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WMS / ImageServer that returns raw floats&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IGN France, USGS 3DEP&lt;/td&gt;
&lt;td&gt;One request, one GeoTIFF for the whole box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WCS 2.0.1 GetCoverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spain, England, Italy, North Rhine-Westphalia&lt;/td&gt;
&lt;td&gt;One request, but each server has opinions about axes and CRS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tiles (COG or plain GeoTIFF)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Switzerland, Austria, Bavaria&lt;/td&gt;
&lt;td&gt;Find the tiles, read each, mosaic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. A global COG: read just a window
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://doi.org/10.7717/peerj.19673" rel="noopener noreferrer"&gt;GEDTM30&lt;/a&gt; (OpenGeoHub, CC BY 4.0) is a 30 m bare-earth model of the whole planet, published as a single Cloud-Optimized GeoTIFF. You never download it: rasterio opens it through &lt;code&gt;/vsicurl/&lt;/code&gt; and only fetches the byte ranges for your window.&lt;/p&gt;

&lt;p&gt;The GDAL settings matter more than you'd think. Without them GDAL tries to list the bucket directory before opening the file, which is slow and pointless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;rasterio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rasterio.windows&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;from_bounds&lt;/span&gt;

&lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://opentopography.s3.sdsc.edu/raster/GEDTM30/GEDTM30_be/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gedtm_rf_m_30m_s_20060101_20151231_go_epsg.4326.3855_v1.2.tif&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GDAL_DISABLE_READDIR_ON_OPEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EMPTY_DIR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# don't list the bucket
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CPL_VSIL_CURL_ALLOWED_EXTENSIONS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.tif&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# don't probe sidecar files
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GDAL_HTTP_TIMEOUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VSI_CACHE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TRUE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gedtm_window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;west&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;south&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;east&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;north&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;rasterio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rasterio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/vsicurl/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;from_bounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;west&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;south&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;east&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;north&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round_offsets&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;round_lengths&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;float32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;3.4e38&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# float32-max = sea / void
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;window_transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same file is mirrored on OpenGeoHub's own S3, so I try one host and fall through to the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. WMS and ImageServer that return numbers, not pictures
&lt;/h2&gt;

&lt;p&gt;People think of WMS as "the thing that returns map images". But &lt;strong&gt;IGN's Géoplateforme&lt;/strong&gt; will hand you &lt;strong&gt;raw float32 elevations&lt;/strong&gt; as a GeoTIFF if you ask for the right layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERVICE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WMS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VERSION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.3.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REQUEST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GetMap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LAYERS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STYLES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CRS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EPSG:4326&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BBOX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;south&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;west&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;north&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;east&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# WMS 1.3.0 + 4326 = lat,lon order!
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WIDTH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HEIGHT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FORMAT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/geotiff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://data.geopf.fr/wms-r/wms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things bit me here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Axis order.&lt;/strong&gt; WMS 1.3.0 with EPSG:4326 wants &lt;code&gt;lat,lon&lt;/code&gt;. Get it wrong and you get a valid GeoTIFF of somewhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"200 OK" isn't success.&lt;/strong&gt; WMS can answer 200 with an XML &lt;code&gt;ServiceException&lt;/code&gt; in the body. Check the &lt;code&gt;Content-Type&lt;/code&gt; before you write anything to disk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also measured how big one GetMap can be: 0.8 Mpx answered in 4.7 s, 2.4 Mpx in 7.8 s, and 4.2 Mpx returned a 504 no matter how long I waited. Splitting into tiles works (the seams are invisible), but it multiplies the failure surface. So I cap a request at 3 Mpx — roughly 75 km² at 5 m — and above that I use a coarser source rather than promise 5 m and time out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;USGS 3DEP&lt;/strong&gt; works the same way through an ArcGIS ImageServer &lt;code&gt;exportImage&lt;/code&gt;. The nice surprise: the mosaic picks the best source &lt;em&gt;per pixel&lt;/em&gt;, so a 5 m request returns lidar-derived heights wherever lidar has been flown and the 1/3″ (~10 m) product elsewhere. Force your own &lt;code&gt;noData&lt;/code&gt; so you know what a void looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bbox&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;west&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;south&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;east&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;north&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bboxSR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4326&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;imageSR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4326&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tiff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pixelType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;F32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noData&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noDataInterpretation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;esriNoDataMatchAny&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;interpolation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RSP_BilinearInterpolation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. WCS 2.0.1: same standard, four dialects
&lt;/h2&gt;

&lt;p&gt;WCS is the "proper" way to get coverage data, and four of my providers use it. They do not agree on much.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spain (IGN España)&lt;/strong&gt; is the polite one: you subset in lat/long, &lt;code&gt;SCALESIZE&lt;/code&gt; pins the output grid, the server reprojects for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERVICE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WCS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VERSION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REQUEST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GetCoverage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COVERAGEID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Elevacion4258_5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUBSET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;long(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;west&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;east&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUBSET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lat(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;south&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;north&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCALESIZE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;long(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;),lat(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FORMAT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/tiff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch: &lt;strong&gt;there is no void value&lt;/strong&gt;. The sea and the land outside Spain (Portugal, France) both come back as a flat &lt;code&gt;0&lt;/code&gt;. If you trace contours on that, you get a 0 m contour drawn along the Portuguese border. I remap &lt;code&gt;0&lt;/code&gt; to a void sentinel before anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;England (Environment Agency)&lt;/strong&gt;, &lt;strong&gt;Italy (INGV TINITALY)&lt;/strong&gt; and &lt;strong&gt;North Rhine-Westphalia&lt;/strong&gt; only behave in their &lt;strong&gt;native projected CRS&lt;/strong&gt;. On the EA's GeoServer, a lon/lat subset errors or hangs for minutes. Asking in British National Grid eastings/northings with &lt;code&gt;SCALEFACTOR&lt;/code&gt; returns a 5 m grid for a 6 × 6 km box in about a second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rasterio.warp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_bounds&lt;/span&gt;

&lt;span class="n"&gt;e0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_bounds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EPSG:4326&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EPSG:27700&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;west&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;south&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;east&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;north&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;  &lt;span class="c1"&gt;# metres, so the warp back to lon/lat has data at the edges
&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERVICE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WCS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VERSION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REQUEST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GetCoverage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COVERAGEID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COVERAGE_ID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUBSET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;E(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pad&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pad&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUBSET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pad&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pad&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCALEFACTOR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# native 1 m → 5 m
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FORMAT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/tiff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I warp the result onto my lon/lat grid myself. The same function serves England (EPSG:27700), Italy (EPSG:32632 — for the &lt;em&gt;whole&lt;/em&gt; country, eastern regions included) and NRW (EPSG:25832).&lt;/p&gt;

&lt;p&gt;One more English trap: a subset entirely off the data (Wales, Scotland) also comes back as a flat &lt;code&gt;0&lt;/code&gt; with no nodata tag. Real lidar ground in England is never exactly 0.0 — even the Fens read −11.42 m — so there, &lt;code&gt;0&lt;/code&gt; means "no data".&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Tiles: find them, read them, mosaic them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Switzerland (swisstopo swissALTI3D)&lt;/strong&gt; publishes 1 km Cloud-Optimized GeoTIFFs behind a STAC API. Ask for items in your bbox, and you'll often get &lt;em&gt;several survey years for the same tile&lt;/em&gt;, so keep the newest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://data.geo.admin.ch/api/stac/v0.9/collections/ch.swisstopo.swissalti3d/items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
       &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?bbox=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;west&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;south&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;east&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;north&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;limit=100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;newest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;feat&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;features&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# swissalti3d_&amp;lt;year&amp;gt;_&amp;lt;E&amp;gt;-&amp;lt;N&amp;gt;
&lt;/span&gt;        &lt;span class="n"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;feat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_2_2056_5728.tif&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# the 2 m asset
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;newest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;newest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tile&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;links&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tiles are kilometre-aligned in LV95, so they mosaic without resampling at the seams. Then I warp once to lon/lat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Austria (BEV)&lt;/strong&gt; publishes 50 km COG tiles in EPSG:3035 with internal overviews. Asking rasterio for a window at a coarser &lt;code&gt;out_shape&lt;/code&gt; makes GDAL pick the matching overview, so a 5–20 m read only pulls a few MB. There are exactly 55 published tiles; anything else 404s, so I keep the list in code instead of guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bavaria&lt;/strong&gt; is the hard one: no open WCS (the INSPIRE one needs a login), only 1 km GeoTIFF tiles that are &lt;strong&gt;not&lt;/strong&gt; cloud-optimized, about 2.5 MB each. Every tile is a full download, so I read them in parallel and cap the count.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;Germany as a whole&lt;/strong&gt; has no free nationwide DGM1 — the federal one is licensed — so each Land is its own provider with its own service. Baden-Württemberg's INSPIRE WCS answers, but over part of the Land it returns whole-metre integers with no CRS, which terraces every contour. It's not wired yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody warns you about: is that void the sea, or France?
&lt;/h2&gt;

&lt;p&gt;National models stop at the border &lt;em&gt;and&lt;/em&gt; at the coast, and both show up as "no data". For contours, these mean opposite things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Void = sea&lt;/strong&gt; → keep it, it's your coastline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Void = a neighbouring country&lt;/strong&gt; → this zone isn't fully covered; use another source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My heuristic: look at the ring of valid pixels around each void. If the median elevation of that ring is below 60 m, it's a shoreline. If it's higher, the void is foreign land the national model doesn't cover, and the zone falls back to a worldwide source. Above 0.5 % of void pixels inside the box, I don't trust the national model for that zone at all.&lt;/p&gt;

&lt;p&gt;This also leads to a rule I'd recommend to anyone mixing sources: &lt;strong&gt;one dataset per map&lt;/strong&gt;. A national model is only used when the zone lies entirely inside its coverage. Stitching a 1 m lidar model to a 30 m global one at a border gives you a visible step in the contours exactly where you least want it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself a year ago
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check the Content-Type, always.&lt;/strong&gt; Every OGC server here returns errors as XML or JSON with a 200 status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask in the native CRS.&lt;/strong&gt; Reprojection on the server is where WCS implementations break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know what "no data" looks like for each source.&lt;/strong&gt; It's &lt;code&gt;NaN&lt;/code&gt;, &lt;code&gt;-9999&lt;/code&gt;, &lt;code&gt;-99999&lt;/code&gt;, float32-max, or &lt;code&gt;0&lt;/code&gt;, depending on who you ask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the server, then cap below it.&lt;/strong&gt; Falling back fast is better than a 30 s timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licences differ, attribution doesn't.&lt;/strong&gt; CC BY 4.0, OGL v3.0, Etalab 2.0 and swisstopo's terms all require a credit; I write it into the metadata of every export.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full list, with endpoints, native resolution, licence and CRS for each model, is on GitHub: &lt;a href="https://github.com/alexisoules/topolines-docs/blob/main/docs/national-elevation-models.md" rel="noopener noreferrer"&gt;open national elevation models&lt;/a&gt; (&lt;a href="https://github.com/alexisoules/topolines-docs/blob/main/data/national-elevation-models.csv" rel="noopener noreferrer"&gt;CSV&lt;/a&gt;). If you know an open national DTM with a usable web service that isn't there, open an issue and I'll test it.&lt;/p&gt;

&lt;p&gt;And if you just want the contours: &lt;a href="https://www.topolines.app/" rel="noopener noreferrer"&gt;TopoLines&lt;/a&gt; traces them for any zone on Earth, on whichever of these models covers it, and exports GeoJSON or DXF in your CRS.&lt;/p&gt;

</description>
      <category>gis</category>
      <category>python</category>
      <category>opendata</category>
      <category>geospacial</category>
    </item>
    <item>
      <title>I built a web tool that traces contour lines from real elevation data (GeoJSON, DXF, SVG, PNG)</title>
      <dc:creator>TopoLines</dc:creator>
      <pubDate>Sat, 25 Jul 2026 14:50:45 +0000</pubDate>
      <link>https://dev.to/topolines/i-built-a-web-tool-that-turns-real-elevation-data-into-topographic-contour-line-art-svg-png-132m</link>
      <guid>https://dev.to/topolines/i-built-a-web-tool-that-turns-real-elevation-data-into-topographic-contour-line-art-svg-png-132m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update — October 2026:&lt;/strong&gt; TopoLines now traces on &lt;strong&gt;GEDTM30&lt;/strong&gt; (30 m bare-earth) worldwide and on &lt;strong&gt;national lidar terrain models at 5–10 m&lt;/strong&gt; in France, the US, Spain, England, Austria, Switzerland, Italy and parts of Germany, and exports &lt;strong&gt;GeoJSON and DXF in your CRS&lt;/strong&gt; alongside SVG and PNG. Current docs: &lt;a href="https://www.topolines.app/docs" rel="noopener noreferrer"&gt;topolines.app/docs&lt;/a&gt; · &lt;a href="https://github.com/alexisoules/topolines-docs" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. This post has been updated to match.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Hey DEV Community 👋&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the past few months I've been building &lt;a href="https://www.topolines.app/" rel="noopener noreferrer"&gt;TopoLines&lt;/a&gt; — a browser-based &lt;strong&gt;contour line generator&lt;/strong&gt; that traces &lt;strong&gt;topographic contours&lt;/strong&gt; from &lt;strong&gt;real, citable elevation data&lt;/strong&gt; anywhere on Earth. You draw a box on a map, pick your settings, and it traces the contours of that exact area — exported as &lt;strong&gt;GeoJSON&lt;/strong&gt; or &lt;strong&gt;DXF&lt;/strong&gt; in the coordinate system of your choice, or as &lt;strong&gt;SVG&lt;/strong&gt; and &lt;strong&gt;PNG&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is a follow-up to my &lt;a href="https://topolines.app/" rel="noopener noreferrer"&gt;first post&lt;/a&gt; about the designer workflow. This time I want to go deeper on &lt;strong&gt;how it actually works under the hood&lt;/strong&gt;, show a gallery of &lt;strong&gt;real places&lt;/strong&gt;, and talk about the elevation data pipeline — because a lot of you asked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjltgauscz4k04fbtzlej.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjltgauscz4k04fbtzlej.png" alt="TopoLines editor tracing contour lines along the Brittany coastline in France from IGN RGE ALTI 5 m elevation data, with the zone drawn on the map and the settings panel open" width="800" height="438"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The TopoLines editor — tracing the Brittany coastline, France, from IGN RGE ALTI 5 m elevation data.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why contour lines are surprisingly hard to get
&lt;/h2&gt;

&lt;p&gt;If you've ever needed &lt;strong&gt;contour lines&lt;/strong&gt; for a real place — a site study, a GIS project, a CAD drawing, a laser-cut terrain model, a hiking map, a data visualisation or a design layout — you've probably hit the same wall I did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full GIS software&lt;/strong&gt; like &lt;strong&gt;QGIS&lt;/strong&gt;, &lt;strong&gt;ArcGIS&lt;/strong&gt; or &lt;strong&gt;GRASS&lt;/strong&gt; can do it, but first you have to find the right &lt;strong&gt;DEM&lt;/strong&gt; (Digital Elevation Model), download and clip it, reproject it, run &lt;code&gt;gdal_contour&lt;/code&gt;, clean the noise, and style the output. That's a multi-hour job if you don't do it every day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static asset packs&lt;/strong&gt; give you pre-made contour patterns — you can't change the location, the &lt;strong&gt;contour interval&lt;/strong&gt;, or the resolution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generative noise tools&lt;/strong&gt; produce topographic-looking squiggles that don't correspond to any &lt;strong&gt;real terrain&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted the contour step of a GIS workflow, with the data sourcing already done, and without installing anything. So I built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What TopoLines does
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Search any place on Earth&lt;/strong&gt; — cities, mountains, coastlines, islands, valleys — using the built-in geocoder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draw a bounding box&lt;/strong&gt; on the map to define your area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The best elevation model covering that zone is picked for you&lt;/strong&gt; — and named in the editor:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GEDTM30 30 m&lt;/strong&gt; (free) — a worldwide &lt;strong&gt;bare-earth&lt;/strong&gt; model: it follows the ground under forest, not the canopy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;National terrain models at 5–10 m&lt;/strong&gt;, mostly lidar — IGN RGE ALTI (France), USGS 3DEP (US), IGN España MDT, Environment Agency LIDAR (England), BEV (Austria), swissALTI3D (Switzerland), TINITALY (Italy), DGM1 (Bavaria, North Rhine-Westphalia).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your own GeoTIFF&lt;/strong&gt; if you have survey data.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set the trace:&lt;/strong&gt; contour interval (down to 5 m), index contours, smoothing, a minimum contour length to drop DEM-noise loops, auxiliary contours on flat ground, depression contours with inward hachures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Style it:&lt;/strong&gt; elevation gradients, stroke width and taper, contour labels cut into the line, scale bar, north arrow and legend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GeoJSON&lt;/strong&gt; — each feature carries its elevation and contour type; written in WGS 84, automatic UTM, Lambert-93, Web Mercator, British National Grid or ETRS89 UTM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DXF&lt;/strong&gt; — one layer per contour type, same CRS options, for CAD and laser cutting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG&lt;/strong&gt; — editable paths grouped by elevation, opens cleanly in &lt;strong&gt;Figma&lt;/strong&gt;, &lt;strong&gt;Illustrator&lt;/strong&gt; or &lt;strong&gt;Inkscape&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG&lt;/strong&gt; — up to 3200 px wide.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every export carries the name of the dataset it was traced from and its licence credit in its metadata.&lt;/p&gt;

&lt;p&gt;It runs in the browser, with &lt;strong&gt;no software to install&lt;/strong&gt;. The free tier needs a Google or GitHub sign-in (no payment) and exports a real PNG.&lt;/p&gt;

&lt;h2&gt;
  
  
  The elevation data pipeline (for the curious)
&lt;/h2&gt;

&lt;p&gt;The terrain comes from &lt;strong&gt;open elevation datasets&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GEDTM30 v1.2&lt;/strong&gt; (OpenGeoHub, CC BY 4.0) is a single global &lt;strong&gt;Cloud-Optimized GeoTIFF&lt;/strong&gt;. I read just the window I need over HTTP range requests (&lt;code&gt;/vsicurl&lt;/code&gt;), so there's nothing to download or stitch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;National models&lt;/strong&gt; are read from each agency's own web service — WMS, WCS, ArcGIS ImageServer, STAC + COG tiles — each with its own quirks (I wrote them up &lt;a href="https://github.com/alexisoules/topolines-docs/blob/main/docs/national-elevation-models.md" rel="noopener noreferrer"&gt;here&lt;/a&gt;). A national model is only used when the zone lies entirely inside its coverage, so a map is never stitched from two datasets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Terrain Tiles&lt;/strong&gt; (&lt;code&gt;elevation-tiles-prod&lt;/code&gt;) is the fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenStreetMap coastlines&lt;/strong&gt; (&lt;code&gt;natural=coastline&lt;/code&gt;) clip contours at the shore, because many elevation models report non-zero heights over the sea.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The processing chain is pure Python — &lt;strong&gt;rasterio&lt;/strong&gt; to read and warp the DEM, &lt;strong&gt;scikit-image&lt;/strong&gt;'s marching squares to trace the &lt;strong&gt;iso-elevation contours&lt;/strong&gt;, &lt;strong&gt;shapely&lt;/strong&gt; for geometry and simplification, &lt;strong&gt;Pillow&lt;/strong&gt; for the raster output, and rasterio's warp machinery again to reproject vector exports into your CRS. Each &lt;strong&gt;contour line&lt;/strong&gt; is a true iso-line connecting points of equal &lt;strong&gt;altitude&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Everything is generated &lt;strong&gt;server-side&lt;/strong&gt;, so what you download is a genuine vector file with real paths — not a traced bitmap.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gallery of real places
&lt;/h2&gt;

&lt;p&gt;All of these were generated straight from real &lt;strong&gt;elevation data&lt;/strong&gt;, no manual editing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6nkahfon6myu8xa3i68.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6nkahfon6myu8xa3i68.jpg" alt="Contour lines of Sarajevo, Bosnia and Herzegovina, traced from real elevation data, showing the city's valley and the surrounding mountains" width="800" height="565"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Sarajevo, Bosnia and Herzegovina&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8lssjp79fy3qt08215l.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8lssjp79fy3qt08215l.jpg" alt="Contour lines of Funchal on Madeira, Portugal, traced from real elevation data, from the coastline up the island's steep slopes" width="800" height="565"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Funchal, Madeira, Portugal&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsxbo6biqduhjhjsgi2o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzsxbo6biqduhjhjsgi2o.jpg" alt="Contour lines of a mountain range in Montenegro, traced from real elevation data, with tightly packed lines on the steep slopes" width="800" height="565"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Montenegro mountains&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it's for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GIS and survey users&lt;/strong&gt; who need contours for an area quickly, with a named source and CRS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architects, landscape architects and engineers&lt;/strong&gt; doing site studies — DXF in a projected CRS, on 5 m lidar where available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cartographers and designers&lt;/strong&gt; who want editable, correctly sourced vectors in Figma or Illustrator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Makers&lt;/strong&gt; cutting layered terrain models, one DXF/SVG layer per elevation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hikers and outdoor folks&lt;/strong&gt; looking at the real terrain of a trail or a peak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data visualisation&lt;/strong&gt; people who want iso-lines grounded in real data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it with a place you know
&lt;/h2&gt;

&lt;p&gt;The best way to judge it is to trace a place you actually know — your city, the mountain you last climbed, the coastline you grew up on.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://www.topolines.app/" rel="noopener noreferrer"&gt;topolines.app&lt;/a&gt;&lt;/strong&gt; — free with a Google or GitHub account. Docs: &lt;a href="https://www.topolines.app/docs" rel="noopener noreferrer"&gt;topolines.app/docs&lt;/a&gt; · feedback and roadmap on &lt;a href="https://github.com/alexisoules/topolines-docs" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'd genuinely love feedback from this community: feature requests, edge cases, places that break it, formats or CRS you'd want. Drop a comment with the place you traced — I read every one. 🙏&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>design</category>
      <category>dataviz</category>
      <category>maps</category>
    </item>
    <item>
      <title>I built a vector topographic contour map generator for designers (SVG export)</title>
      <dc:creator>TopoLines</dc:creator>
      <pubDate>Tue, 21 Jul 2026 00:40:34 +0000</pubDate>
      <link>https://dev.to/topolines/i-built-a-vector-topographic-contour-map-generator-for-designers-svg-export-5hj</link>
      <guid>https://dev.to/topolines/i-built-a-vector-topographic-contour-map-generator-for-designers-svg-export-5hj</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwu76o2w4pr437z1wbfa9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwu76o2w4pr437z1wbfa9.jpg" alt=" " width="800" height="565"&gt;&lt;/a&gt;&lt;br&gt;
Hey everyone! 👋&lt;/p&gt;

&lt;p&gt;As a designer, I constantly needed high-quality vector topographic contour lines and generative patterns for branding, UI hero backgrounds, and print projects. Most existing tools were either heavy GIS software (like QGIS) or required static file purchases.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://topolines.app" rel="noopener noreferrer"&gt;Topolines&lt;/a&gt;&lt;/strong&gt;—a fast, web-based vector topographic generator.&lt;/p&gt;

&lt;h3&gt;
  
  
  🎛️ Key Features:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parametric Control:&lt;/strong&gt; Fine-tune elevation density, noise scale, detail, and line weights in real-time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Styling:&lt;/strong&gt; Full visual control over color palettes, gradients, contrast, and backgrounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean Vector Export:&lt;/strong&gt; Instant SVG exports (perfect for Figma, Illustrator, or web code) and HD PNGs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frictionless Free Tier:&lt;/strong&gt; Direct PNG exports without even needing an account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love for you to try it out at &lt;strong&gt;&lt;a href="https://topolines.app" rel="noopener noreferrer"&gt;topolines.app&lt;/a&gt;&lt;/strong&gt; and let me know your feedback or feature requests!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>design</category>
      <category>webdev</category>
      <category>svg</category>
    </item>
  </channel>
</rss>
