<?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: Jimmy Utterström</title>
    <description>The latest articles on DEV Community by Jimmy Utterström (@jimutt).</description>
    <link>https://dev.to/jimutt</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%2F140619%2Fb304d33d-dd13-4f74-b445-e01a1f21d3b0.jpg</url>
      <title>DEV Community: Jimmy Utterström</title>
      <link>https://dev.to/jimutt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jimutt"/>
    <language>en</language>
    <item>
      <title>Segno Player: play along on the original score, on any screen</title>
      <dc:creator>Jimmy Utterström</dc:creator>
      <pubDate>Sun, 25 Oct 2026 15:50:59 +0000</pubDate>
      <link>https://dev.to/jimutt/segno-player-a-sheet-music-player-that-keeps-the-engravers-layout-9d7</link>
      <guid>https://dev.to/jimutt/segno-player-a-sheet-music-player-that-keeps-the-engravers-layout-9d7</guid>
      <description>&lt;p&gt;&lt;em&gt;Segno Player (&lt;a href="https://github.com/segnomusic/segno-player" rel="noopener noreferrer"&gt;https://github.com/segnomusic/segno-player&lt;/a&gt;) is an open source web component that plays a MuseScore score's audio on its original engraved pages, with a marker on the bar being played and a cursor that follows the notes. This post covers why I built it, how it works and where it's heading.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I have a hobby, or possibly a problem, of collecting sheet music for old public domain songs, mostly Swedish folk music and choral music. Some of it does get sung or played, and quite a lot of it doesn't. I do however intend to transcribe the works and publish them for public access on the web. I host the Swedish website &lt;a href="https://frianoter.se/" rel="noopener noreferrer"&gt;frianoter.se&lt;/a&gt; where some have been published already.&lt;/p&gt;

&lt;p&gt;To make the scores more accessible, I want them to be easy to view, play and download in any format. I'm very well aware of the immense library of digitalized sheet music on for example &lt;a href="https://musescore.com" rel="noopener noreferrer"&gt;musescore.com&lt;/a&gt;. But I don't like how almost all interactions with the community-contributed scores are blocked by paywalls nowadays. The open source engraving software (&lt;a href="https://musescore.org" rel="noopener noreferrer"&gt;musescore.org&lt;/a&gt;) is however still a brilliant piece of software, and if I were to guess, it will remain the most popular engraving software in the general community and in non-commercial environments.&lt;/p&gt;

&lt;p&gt;A number of years back I was using OpenSheetMusicDisplay (OSMD) for rendering MusicXML files (exported from MuseScore) in a couple of different side projects. It was, and seemingly still is, a really solid MusicXML rendering library. I also prototyped &lt;a href="https://github.com/jimutt/osmd-audio-player" rel="noopener noreferrer"&gt;a very rough audio player for OSMD&lt;/a&gt;, and got to chat and work a little bit with the nice guys in the core team on an early version of the official audio player for OSMD. There are a few other notable OSS libraries in the space as well, for example &lt;a href="https://www.verovio.org/index.xhtml" rel="noopener noreferrer"&gt;Verovio&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  To reflow or not to reflow
&lt;/h2&gt;

&lt;p&gt;OSMD, Verovio and many similar sheet music renderers have quite advanced layout engines that allow them to dynamically engrave the sheet music depending on your available screen size. This is a truly great feature for music practice and education apps. But how successful these strategies are tends to depend a bit on the scores being rendered. Busy piano scores and choral works sometimes become a bit cluttered, and generally speaking &lt;strong&gt;intentional layout decisions get lost&lt;/strong&gt;. It's a natural consequence and not really a flaw in the libraries, but for some scores it matters. Those are the cases where I think the built-in player on &lt;a href="https://musescore.com" rel="noopener noreferrer"&gt;musescore.com&lt;/a&gt;, which preserves the full original layout, works better.&lt;/p&gt;

&lt;p&gt;Here's an example. This is Helene Munktell's "Fjerran på enslig stig", a song for voice and piano from the &lt;a href="https://www.levandemusikarv.se/" rel="noopener noreferrer"&gt;Swedish Musical Heritage&lt;/a&gt; collection. It's nothing exotic, but it has the usual mix of triplets, hairpins, lyrics and a busy piano part. First the original print:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jimmyutterstrom.com/images/segno-player/original-print.png" rel="noopener noreferrer"&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%2F9ridlktr2yyxwf8pfy21.png" alt="Two systems from the original print of Fjerran på enslig stig" width="799" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's the same passage (from my MusicXML transcription of the print) rendered with OSMD in a 390 px wide window, roughly a phone in portrait:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jimmyutterstrom.com/images/segno-player/osmd-mobile.png" rel="noopener noreferrer"&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%2Fcly5dagw0tozzlzynd8k.png" alt="The same bars rendered by OSMD at phone width, with overlapping lyrics and crowded notes" width="780" height="1460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything is technically there, and the renderer is doing what it's supposed to do: squeezing the music into the width it was given. But "fjerran der vägen går" has turned into a smudge, and the same goes for "fram genom gröna" a few bars later. The tuplets are crammed together and the notes in the piano's right hand are practically sitting on top of each other. With a few more pixels to work with this would look just fine, and it's a trade-off I'm happy to accept in a practice app where you can zoom and scroll. But it's not what the engraver intended the music to look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Segno Player, the ambivalent sheet music audio player
&lt;/h2&gt;

&lt;p&gt;At its core, Segno Player is a layout-respecting sheet music player, allowing interactive playback of digital sheet music based on the original engraving. You press play, the audio starts, and a marker follows the current bar while a thin cursor flows from note to note. You can also click any bar to start playing from there.&lt;/p&gt;

&lt;p&gt;Here's what it looks like in action. The screenshot links to the live demo page, and the &lt;a href="https://jimmyutterstrom.com/blog/2026/10/25/segno-player/" rel="noopener noreferrer"&gt;original post&lt;/a&gt; has the player embedded right in the article, so you can press play there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://segnomusic.github.io/segno-player/" rel="noopener noreferrer"&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%2Fw9zkpyxq4jhcmtn111b1.png" alt="Segno Player showing the full score, with a bar marker and note cursor in the second system" width="800" height="655"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Engraving of När juldagsmorgon glimmar by &lt;a href="https://frianoter.se/noter/nar-juldagsmorgon-glimmar/" rel="noopener noreferrer"&gt;Fria Noter&lt;/a&gt;, &lt;a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener noreferrer"&gt;CC BY 4.0&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The reason I'm accusing my Segno Player of being ambivalent is that it &lt;strong&gt;does&lt;/strong&gt; offer some hybrid rendering modes that are meant to make scores more accessible on smaller screens. It offers these two additional display modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zoom&lt;/strong&gt; - Renders two vertical systems and a few bars, and scrolls the score as it's playing. Under the hood it crops the original engraving around pairs of systems, so it's still the original notation you're looking at, just closer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jimmyutterstrom.com/images/segno-player/zoom-mode.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjimmyutterstrom.com%2Fimages%2Fsegno-player%2Fzoom-mode.png" alt="Zoom mode on a phone-sized player" width="714" height="990"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reflow&lt;/strong&gt; - It's not entirely honest to call this mode reflow. What it really does is that it, in a somewhat primitive way, chunks up the score bar by bar and lays the pieces out on the screen. It allows more subsequent bars to be shown at once than in "zoom" mode, but it may produce less visually pleasing layouts and more artifacts from the segmentation and cropping. You can see one in the screenshot below, where the volta bracket of the second line has been cut loose from its bars.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jimmyutterstrom.com/images/segno-player/reflow-mode.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjimmyutterstrom.com%2Fimages%2Fsegno-player%2Freflow-mode.png" alt="Reflow mode on a phone-sized player" width="714" height="886"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full score is always the default, and it's also the fallback if the canvas modes fail for some reason. If you'd rather not decide yourself you can set &lt;code&gt;mode="auto"&lt;/code&gt;, which uses full score when the player is 768 px or wider and zoom below that.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;When Segno Player is used to render and play files that have been exported from MuseScore, things are quite simple, since the MuseScore CLI supports exporting &lt;code&gt;.spos&lt;/code&gt; and &lt;code&gt;.mpos&lt;/code&gt; files. What are those, you may ask? Simply speaking they are compact XML files that specify the position of bars (&lt;code&gt;.mpos&lt;/code&gt;) and notes (&lt;code&gt;.spos&lt;/code&gt;) in the score. Thankfully for us they don't only specify the visual locations, they also contain timing information, which is what allows us to sync the exported MP3 with the playback.&lt;/p&gt;

&lt;p&gt;One MuseScore conversion job writes everything the player needs from a single score: the audio, one SVG per page and the two position files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'[{"in": "song.mscz", "out": ["song.mp3", "song.svg", "song.mpos", "song.spos"]}]'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; job.json
mscore &lt;span class="nt"&gt;-j&lt;/span&gt; job.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what the start of an &lt;code&gt;.mpos&lt;/code&gt; file looks like for the example score in the screenshots (När juldagsmorgon glimmar):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;elements&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;element&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"13464.5"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"26393.2"&lt;/span&gt; &lt;span class="na"&gt;sx=&lt;/span&gt;&lt;span class="s"&gt;"16788.6"&lt;/span&gt; &lt;span class="na"&gt;sy=&lt;/span&gt;&lt;span class="s"&gt;"24486.9"&lt;/span&gt; &lt;span class="na"&gt;page=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;element&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"30253.1"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"26393.2"&lt;/span&gt; &lt;span class="na"&gt;sx=&lt;/span&gt;&lt;span class="s"&gt;"19900.1"&lt;/span&gt; &lt;span class="na"&gt;sy=&lt;/span&gt;&lt;span class="s"&gt;"24486.9"&lt;/span&gt; &lt;span class="na"&gt;page=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/elements&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;events&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;event&lt;/span&gt; &lt;span class="na"&gt;elid=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;position=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;event&lt;/span&gt; &lt;span class="na"&gt;elid=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;position=&lt;/span&gt;&lt;span class="s"&gt;"522"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;event&lt;/span&gt; &lt;span class="na"&gt;elid=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;position=&lt;/span&gt;&lt;span class="s"&gt;"2609"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/events&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;elements&lt;/code&gt; are the bars: where each one is on the page (&lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;) and how big it is (&lt;code&gt;sx&lt;/code&gt;, &lt;code&gt;sy&lt;/code&gt;). The &lt;code&gt;events&lt;/code&gt; say when a bar starts playing, in milliseconds. So in this score bar 0 starts immediately (that's the pickup bar), bar 1 after 0.5 seconds and bar 2 after 2.6 seconds. Note that the events are listed in &lt;strong&gt;playback order&lt;/strong&gt;, so a bar inside a repeat shows up more than once, and things like voltas, D.S. and codas are already unrolled for us. I was pleasantly surprised by that, as it removes a whole category of annoying problems. The &lt;code&gt;.spos&lt;/code&gt; file looks the same but with one element per note position instead of per bar, which is what drives the note cursor.&lt;/p&gt;

&lt;p&gt;Segno turns these into what I call a &lt;strong&gt;sync map&lt;/strong&gt;. It's a small JSON file (about 2 KB for a song) where all positions have been converted to fractions of the page, so the player doesn't need to care about the size of the SVG or the screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"v"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.11309&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.15675&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.14102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.14543&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;522&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1901&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.15675&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.14543&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"noteEvents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;261&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bars&lt;/code&gt; holds &lt;code&gt;[page, x, y, w, h]&lt;/code&gt; for each bar and &lt;code&gt;events&lt;/code&gt; holds &lt;code&gt;[bar, ms]&lt;/code&gt; in playback order. The format is documented in the &lt;a href="https://github.com/segnomusic/segno-player/blob/main/docs/sync-map.md" rel="noopener noreferrer"&gt;repo&lt;/a&gt;, and nothing says the sync map has to come from MuseScore. Anything that can produce bar positions and timings can write one, which turns out to matter in the next section. The sync map can be built in the browser, or ahead of time and shipped with the page.&lt;/p&gt;

&lt;p&gt;The remaining parts are fairly straightforward. The pages are shown as plain SVG images, and the marker and cursor are drawn on top of them using the percentages from the sync map. During playback the player looks up which event is current for the audio's &lt;code&gt;currentTime&lt;/code&gt;, and moves the marker accordingly. Clicking a bar goes the other way around: find the bar under the pointer and seek the audio to it (picking the pass through the repeat that's closest to where you currently are). The &lt;code&gt;&amp;lt;segno-player&amp;gt;&lt;/code&gt; element follows the &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element API as closely as possible, so &lt;code&gt;play()&lt;/code&gt;, &lt;code&gt;pause()&lt;/code&gt;, &lt;code&gt;currentTime&lt;/code&gt;, &lt;code&gt;playbackRate&lt;/code&gt; and so on work as you'd expect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/@segnomusic/player/dist/player/index.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;segno-player&lt;/span&gt;
  &lt;span class="na"&gt;pages=&lt;/span&gt;&lt;span class="s"&gt;"song-1.svg song-2.svg"&lt;/span&gt;
  &lt;span class="na"&gt;audio=&lt;/span&gt;&lt;span class="s"&gt;"song.mp3"&lt;/span&gt;
  &lt;span class="na"&gt;positions=&lt;/span&gt;&lt;span class="s"&gt;"song.mpos"&lt;/span&gt;
  &lt;span class="na"&gt;notes=&lt;/span&gt;&lt;span class="s"&gt;"song.spos"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"När juldagsmorgon glimmar"&lt;/span&gt;
  &lt;span class="na"&gt;controls&lt;/span&gt;
  &lt;span class="na"&gt;follow&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/segno-player&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the markers feel slightly early or late on your setup, you can tweak the timing with the &lt;code&gt;bar-lead&lt;/code&gt; and &lt;code&gt;note-lead&lt;/code&gt; attributes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond MuseScore files
&lt;/h2&gt;

&lt;p&gt;Most of the scores I care about don't exist as MuseScore files though. They're scans or photos of old printed pages. I'm working on an OMR (optical music recognition) project of my own that reads a scan and turns it into MusicXML, with the aim of needing as little manual work as possible. It's not public yet, so I won't go into the details here.&lt;/p&gt;

&lt;p&gt;What's relevant for Segno is that the sync map doesn't care where it came from. The MusicXML can be run through MuseScore to get audio and timings, and since the OMR knows where the bars are on the scan, the two can be matched bar by bar into a sync map. That means Segno can play along over the &lt;strong&gt;original scan&lt;/strong&gt; instead of a fresh engraving.&lt;/p&gt;

&lt;p&gt;To show that it isn't just something that works on clean scans, here's a real example. It's a photo of a page in an old hymnal, with all the usual issues: curved paper, uneven lighting and a shadow along the spine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jimmyutterstrom.com/images/segno-player/photo-original.jpg" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjimmyutterstrom.com%2Fimages%2Fsegno-player%2Fphoto-original.jpg" alt="Photo of a hymnal page" width="800" height="1171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this is the same page after going through the whole chain, playing in Segno Player (click the image for a ten second clip, with sound). The page has been straightened out, and the bar marker follows the audio that was generated from the recognized music:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jimmyutterstrom.com/images/segno-player/photo-scan-demo.mp4" rel="noopener noreferrer"&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%2Fnn7644jybbg0ep9kyuuo.jpg" alt="The straightened-out photo playing in Segno Player, with the bar marker following the audio" width="800" height="976"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This isn't something you can try out yet. It's still a spike, and it's only as good as the recognition. But support for this is coming, and the plan is to end up with an end-to-end solution: scan in, player-ready sync map out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Already in use
&lt;/h2&gt;

&lt;p&gt;Segno Player is already running on &lt;a href="https://frianoter.se/" rel="noopener noreferrer"&gt;frianoter.se&lt;/a&gt;, but &lt;strong&gt;it's not enabled for all songs&lt;/strong&gt;. It only kicks in for songs that have been engraved in MuseScore and exported with audio and position files, which at the time of writing is 23 of the 63 songs on the site. For all the others, pressing "Lyssna" falls back to the old OSMD-based playback, which re-renders and synthesizes the score. That's the reflowing behaviour described above.&lt;/p&gt;

&lt;p&gt;Here's one where it &lt;strong&gt;is&lt;/strong&gt; enabled, the same score as in the screenshots above: &lt;a href="https://frianoter.se/noter/nar-juldagsmorgon-glimmar/" rel="noopener noreferrer"&gt;När juldagsmorgon glimmar&lt;/a&gt;. Press "Lyssna" and you'll get the player. The site is built with Astro and Svelte, and the player just drops in as a web component.&lt;/p&gt;

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

&lt;p&gt;Segno Player is still pre-v1, in practice meaning there may be breaking changes to the API surface of both its web component, the sync map format and so on. It's quite unlikely that they change in ways that make it hard to migrate to future versions though, so if you find the player valuable already now, don't hesitate to try it out. There's a &lt;a href="https://segnomusic.github.io/segno-player/" rel="noopener noreferrer"&gt;demo page&lt;/a&gt; where you can play around with the example score.&lt;/p&gt;

&lt;p&gt;The minimal setup is to export your score from MuseScore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'[{"in": "song.mscz", "out": ["song.mp3", "song.svg", "song.mpos"]}]'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; job.json
mscore &lt;span class="nt"&gt;-j&lt;/span&gt; job.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;mscore&lt;/code&gt; is MuseScore's executable, and its name and path depend on your platform and install.) That gives you &lt;code&gt;song.mp3&lt;/code&gt;, &lt;code&gt;song-1.svg&lt;/code&gt; (one per page) and &lt;code&gt;song.mpos&lt;/code&gt;. Then drop the player on a page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/@segnomusic/player/dist/player/index.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;segno-player&lt;/span&gt;
  &lt;span class="na"&gt;pages=&lt;/span&gt;&lt;span class="s"&gt;"song-1.svg"&lt;/span&gt;
  &lt;span class="na"&gt;audio=&lt;/span&gt;&lt;span class="s"&gt;"song.mp3"&lt;/span&gt;
  &lt;span class="na"&gt;positions=&lt;/span&gt;&lt;span class="s"&gt;"song.mpos"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"My song"&lt;/span&gt;
  &lt;span class="na"&gt;controls&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/segno-player&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Add a &lt;code&gt;song.spos&lt;/code&gt; file and a &lt;code&gt;notes="song.spos"&lt;/code&gt; attribute if you want the note cursor too. If you're using a bundler, &lt;code&gt;npm install @segnomusic/player&lt;/code&gt; and &lt;code&gt;import '@segnomusic/player'&lt;/code&gt; does the same job as the script tag. A couple of things are worth knowing: export everything in the same job from the same MuseScore version, and make sure "Play repeats" is on, otherwise the audio and the position files won't line up. The &lt;a href="https://github.com/segnomusic/segno-player#exporting-from-musescore" rel="noopener noreferrer"&gt;README&lt;/a&gt; covers those details and a few more.&lt;/p&gt;

&lt;p&gt;And please open an issue in the &lt;a href="https://github.com/segnomusic/segno-player" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; if you run into problems or would like to submit a feature request.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>music</category>
    </item>
    <item>
      <title>The Missing Link Between AI Agents and the Code They Modify</title>
      <dc:creator>Jimmy Utterström</dc:creator>
      <pubDate>Wed, 25 Mar 2026 12:07:47 +0000</pubDate>
      <link>https://dev.to/jimutt/the-missing-link-between-ai-agents-and-the-code-they-modify-kke</link>
      <guid>https://dev.to/jimutt/the-missing-link-between-ai-agents-and-the-code-they-modify-kke</guid>
      <description>&lt;p&gt;&lt;em&gt;AI coding agents write code confidently, but they don't know why your code looks the way it does. DLD (&lt;a href="https://github.com/jimutt/dld-kit" rel="noopener noreferrer"&gt;https://github.com/jimutt/dld-kit&lt;/a&gt;) is an open source framework that links code directly to the decisions behind it, so agents read the reasoning before modifying anything. This post covers how I got there and what I've learned so far.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I've been coding since I was 11 or 12, and even though I have a bachelor's degree in embedded systems I'm self-taught in most areas. The main reason I got into this field is curiosity, a drive to understand how things work. That drive was complemented by a creative mind and a joy in building things.&lt;/p&gt;

&lt;p&gt;While I find it interesting to understand the fundamentals of how a CPU is constructed at the hardware level, I've always found most joy in creating things that act, move or interact with their environment in other ways. Be it a standard backend service shuffling data between systems, moving pixels around on the screen, or building physical robots.&lt;/p&gt;

&lt;p&gt;I suspect many engineers with a similar profile belong to the camp that follows the development of LLMs with both excitement and horror. Horror because we're not sure how the profession will shift, how our skills will be valued, whether we'll be able to adapt quickly enough. Excitement because maybe now is the time we'll actually realise a few more of those side project ideas. Even those of us, myself included, who have aged past 30 and found ourselves in a number of life contexts that consume much of our no longer endless spare time. I don't even have kids, but still find it extremely encouraging that I may now be able to build some of those projects I never thought would see the light of day.&lt;/p&gt;

&lt;p&gt;The excitement of being able to realise more ideas is obviously present in professional contexts too, but it's more complex there. It's often combined with aggressive productivity gain targets tied to increased AI use. Many are facing worries that the added complexity from AI-driven development will be difficult to maintain over time with unchanged, or even leaner, staffing. There are many aspects that make the professional adoption of AI more complicated than the personal one, both practically and mentally. And then I'm not even mentioning any of the moral aspects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching for the right way to leverage the mechanical advantage
&lt;/h2&gt;

&lt;p&gt;The core question I keep coming back to is: how do you best leverage the mechanical advantage of using AI to build complex systems faster while not &lt;strong&gt;only&lt;/strong&gt; counting on further future improvements of the performance of the LLMs themselves to guarantee a process that works also in the longer term?&lt;/p&gt;

&lt;p&gt;I've been spending the past year or so trying different approaches. In my day job at Storytel, an audiobook and ebook streaming service, I work as a staff engineer with AI adoption as part of my responsibilities, which means I both get to experiment personally and get a broader view of how different teams approach the problem. And I've tried a lot of things: from fairly unstructured "vibe coding" to more disciplined spec-driven development approaches.&lt;/p&gt;

&lt;p&gt;The unstructured approach works surprisingly well for small, isolated tasks. And as new models are released it starts working for larger and larger chunks of code. Write a prompt, get code, review it, move on. But once your project grows beyond a certain complexity and the related business context is no longer as clearly mirrored by the code, things start falling apart. The AI doesn't remember why that retry logic has a specific timeout, or why that validation step exists despite looking redundant. And honestly, after a long enough break, or a sufficiently ambitious round of layoffs, neither does anyone else.&lt;/p&gt;

&lt;p&gt;Spec-driven development (SDD) was the next thing I explored: write a specification first, then let the AI implement against it. Tools like GitHub's Spec Kit, OpenSpec and similar frameworks have emerged to support this workflow. And they often help in achieving few-prompt implementation with reasonable functional correctness. But I found some friction points. Specs tend to drift from the actual implementation over time. The bigger the project gets, the harder it becomes to keep them in sync. And there's a certain all-or-nothing quality to many SDD approaches: either you write the full spec upfront, or you're back to unstructured prompting. For someone that  often works in a hybrid mode, writing some things manually and having AI fill in other parts, that rigidity can feel like overhead rather than help. It can also be hard to combine with manual programming, where you want the flexibility to make implementation decisions as you go without first updating a spec document.&lt;/p&gt;

&lt;p&gt;There's also a more fundamental issue that kept nagging me. Many SDD approaches encourage you to describe every edge case and behavior in the spec before implementation. But human language is inherently imprecise, and trying to exhaustively specify system behavior in prose is a bit like trying to describe a painting in enough detail that someone could reproduce it pixel by pixel. You can get close, but the closer you try to get, the more verbose and ambiguous it becomes. At some point the implementation &lt;em&gt;is&lt;/em&gt; the spec, and any parallel description of it is just a lossy approximation that you now also need to maintain. It's a variation of the same problem we've seen with overly detailed UML diagrams, exhaustive requirements documents, and every other attempt to fully capture a system in a representation that isn't the system itself. &lt;/p&gt;

&lt;p&gt;When it comes to ensuring correctness on a detailed level in software systems, my personal opinion is that it should be done through automated tests and not natural language specs. But you &lt;strong&gt;can&lt;/strong&gt; of course try to combine the two by finding a good balance. Be it through Gherkin syntax and attempting to elevate the test cases so that they're accessible by more roles than only engineers, or by simply accepting that your tests are written on a level where general coding skills are still required for maintaining and validating their implementation. &lt;/p&gt;

&lt;h2&gt;
  
  
  The idea behind DLD
&lt;/h2&gt;

&lt;p&gt;These experiences eventually led me to think about what I actually needed: not a complete specification document, but a way to preserve the &lt;em&gt;reasoning&lt;/em&gt; behind implementation choices in a format that both I and AI agents could use effectively.&lt;/p&gt;

&lt;p&gt;The initial concept for what became Decision-Linked Development (DLD) was actually born from a two-hour car drive. I spent the drive talking through my frustrations and ideas into a voice recorder, then had an LLM help me turn that raw transcript into a structured concept paper. Which felt fitting, given that the whole point of DLD is about capturing knowledge before it evaporates.&lt;/p&gt;

&lt;p&gt;The core idea borrows from event sourcing: instead of maintaining a mutable specification document that you have to keep up to date, you maintain an append-only log of decisions. Each decision captures what was decided, why, and which parts of the code it relates to. Decisions can be superseded by newer decisions, but they're never edited or deleted. The full history is preserved.&lt;/p&gt;

&lt;p&gt;The key mechanism is the &lt;code&gt;@decision(DL-XXX)&lt;/code&gt; annotation in code. When an AI agent encounters this annotation while modifying code, it has an explicit signal to look up the referenced decision before proceeding. This transforms "the agent should intuitively know to check for context" from a hope into a reliable trigger. And if the planned change conflicts with an existing decision, the agent tells you about it and suggests recording a new decision instead of silently breaking things.&lt;/p&gt;

&lt;p&gt;The consolidated specification, if you want one, is generated from the decision log. It's a derived view, never manually maintained. Like a materialised view in a database: useful for reading, but not the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;The framework is used through a set of slash commands. A typical workflow for a new feature looks something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/dld-plan&lt;/code&gt; breaks down the feature into a set of decisions interactively. You describe what you're building, and the agent helps you slice it into reasonably scoped decisions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/dld-implement DL-001&lt;/code&gt; takes a proposed decision and implements it: writes the code, adds &lt;code&gt;@decision&lt;/code&gt; annotations, and marks the decision as accepted.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/dld-snapshot&lt;/code&gt; regenerates the overview documentation from the decision log.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For smaller, isolated changes like a bugfix or a single design choice, &lt;code&gt;/dld-decide&lt;/code&gt; records one decision directly without the planning step.&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.amazonaws.com%2Fuploads%2Farticles%2Fv7uxwc79xn09wwlm5yrv.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.amazonaws.com%2Fuploads%2Farticles%2Fv7uxwc79xn09wwlm5yrv.png" alt="Diagram showing the core workflow described above" width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A decision record is a markdown file with YAML frontmatter. Here's a real one from my bird recording project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DL-045&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Species/hour&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;activity&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dashboard"&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;accepted&lt;/span&gt;
&lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;web-dashboard&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;references&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/web/src/components/ActivityMatrix.svelte&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/web/src/components/Dashboard.svelte&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Context&lt;/span&gt;
The dashboard is the landing page. BirdNET-Go has a species/hour
activity matrix that shows detection counts per species per hour —
a compact way to see what's been active and when. This is the primary
visualization for the Gillerkvitter dashboard.

&lt;span class="gu"&gt;## Decision&lt;/span&gt;
Build a species/hour activity matrix as the main dashboard component:
&lt;span class="p"&gt;-&lt;/span&gt; Rows: Species (sorted by total detections or recency)
&lt;span class="p"&gt;-&lt;/span&gt; Columns: Hours of the day
&lt;span class="p"&gt;-&lt;/span&gt; Cells: Detection count, with color intensity reflecting volume
&lt;span class="p"&gt;-&lt;/span&gt; Default view: 24-hour period, scrolled to show the current 2-3 hours
&lt;span class="p"&gt;-&lt;/span&gt; Live update: Matrix refreshes via API polling (per DL-040)

&lt;span class="gu"&gt;## Rationale&lt;/span&gt;
The hour-by-species matrix gives an immediate overview of activity
patterns — which species are present and when they're most active.

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Needs a backend query that returns detections grouped by species
  and hour
&lt;span class="p"&gt;-&lt;/span&gt; Color intensity scale needs careful design to work with the nature
  palette (DL-044)
&lt;span class="p"&gt;-&lt;/span&gt; Initial scroll position should auto-focus on the current time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how it references other decisions inline (DL-040 for the polling approach, DL-044 for the color palette). That cross-referencing happens naturally as decisions accumulate, and it's one of the things that helps the AI agent understand how different parts of the system relate.&lt;/p&gt;

&lt;p&gt;And the corresponding code annotation is just a comment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// @decision(DL-008)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;retryWithBackoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an AI agent encounters that annotation, it reads DL-008 before touching the function. If the change it's about to make conflicts with the decision, it stops and tells you.&lt;/p&gt;

&lt;p&gt;The framework also includes &lt;code&gt;/dld-audit&lt;/code&gt; for detecting drift between decisions and code, and a &lt;code&gt;/dld-retrofit&lt;/code&gt; command for bootstrapping decisions from an existing codebase. You don't need to start from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes it practical for hybrid workflows
&lt;/h2&gt;

&lt;p&gt;One thing I specifically wanted was for this to work in a casual, incremental way. A single decision log means you don't need to spend your thinking effort on deciding what belongs in a user story vs. a detailed implementation plan, whether you need to merge two specs because the feature scope changed, or how to handle a change that partially supersedes an earlier decision. You just record decisions as they come up, at whatever level of abstraction is natural for that particular choice. Some decisions are high-level product choices, others are low-level implementation details driven by infrastructure constraints. They all live in the same flat, sequential log.&lt;/p&gt;

&lt;p&gt;You also don't need to use it for everything. It's perfectly fine to use DLD for the parts of your codebase where non-obvious decisions accumulate, and not bother annotating straightforward, idiomatic code that speaks for itself.&lt;/p&gt;

&lt;p&gt;There's also a passive mode for teams that want living documentation without changing how they work day to day. You run the initial bootstrap once to generate decisions from the existing codebase, then schedule the audit and snapshot commands to run automatically via CI. The audit detects unreferenced code changes, infers new decisions, and back-annotates the code. No one needs to invoke any DLD commands during their normal workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it in practice
&lt;/h2&gt;

&lt;p&gt;I've been using DLD on my personal side project, Gillerkvitter, an off-grid bird song recording station running on a solar-powered Raspberry Pi in northern Sweden. It records bird songs, runs them through BirdNET for species identification, and syncs the detections to a cloud server with a web dashboard. The system spans hardware, embedded software, a Bun.js backend API, PostgreSQL, and an Astro/Svelte frontend. Quite a few moving parts.&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.amazonaws.com%2Fuploads%2Farticles%2Fscmotiplxl1e27wipwso.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.amazonaws.com%2Fuploads%2Farticles%2Fscmotiplxl1e27wipwso.png" alt="System overview diagram of the main Gillerkvitter components" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project now has 99 accepted decisions in its log, and so far it's been a really encouraging test case for the framework. The added documentation has really helped both me and the coding agents to easily pick up from a previous session and not lose track of how things are connected. It's the kind of broad, multi-domain project where you can go weeks without touching a particular subsystem, and when you come back, the decision log tells you exactly why things look the way they do.&lt;/p&gt;

&lt;p&gt;We're also trying it at Storytel in a team setting, with promising early results. One thing that's been interesting is how reviewing the proposed decisions before implementation can shift verification earlier in the process. You catch misunderstandings at the decision level rather than in code review, which sometimes makes it more comfortable to accept larger implementation PRs. You've already agreed on the approach; the PR is "just" the execution. And nobody had to get a master's degree in PRD writing to get there, since each decision is a small, focused artifact rather than a chapter in an ever-growing requirements novel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Being honest about what this is
&lt;/h2&gt;

&lt;p&gt;I want to be upfront: DLD in its current form is a collection of Claude Code skills. In practice that means markdown-based prompts that instruct the AI agent, backed by some Bash scripts for file management and validation. I'm neither ashamed of that, nor do I want to inflate what it is. The skills follow the &lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;Agent Skills&lt;/a&gt; open standard and work with Claude Code today, and through the &lt;a href="https://tessl.io/" rel="noopener noreferrer"&gt;Tessl&lt;/a&gt; "package manager" with other AI coding tools as well.&lt;/p&gt;

&lt;p&gt;What matters to me is not the sophistication of the implementation, but whether the underlying idea holds up. And so far, based on my own experience and the early team usage at Storytel, it seems at least as promising as other approaches. The decision log is a simple concept, the annotations are just code comments, and the projections are generated markdown files. But combined, they solve a real problem: keeping decision context accessible when AI agents (and humans returning after a break) modify code they didn't write.&lt;/p&gt;

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

&lt;p&gt;If this resonates with your experience of AI-assisted development, you might find DLD useful. The project is open source at &lt;a href="//github.com/jimutt/dld-kit"&gt;github.com/jimutt/dld-kit&lt;/a&gt; and still pre-v1, so expect rough edges and potentially breaking changes to the process. If you've ever come back to a project after a month and wondered "why on earth did I do it this way?", or if you've watched an AI agent cheerfully refactor away a workaround that existed for a very good reason, you know the problem DLD is trying to solve.&lt;/p&gt;

&lt;p&gt;I started this post talking about curiosity and the joy of building things. DLD came from the same place: a genuine attempt to figure out how to keep building effectively as the tools change around us. It's not the final answer, but it's a practical step that's working for me so far, right here, right now. I'd love to hear if it works for you too.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>specdriven</category>
      <category>llm</category>
    </item>
    <item>
      <title>Removed from Twitter for my actions 12 years ago (as a 12 year old)</title>
      <dc:creator>Jimmy Utterström</dc:creator>
      <pubDate>Fri, 18 Oct 2019 18:28:21 +0000</pubDate>
      <link>https://dev.to/jimutt/removed-from-twitter-for-my-actions-12-years-ago-as-a-12-year-old-egi</link>
      <guid>https://dev.to/jimutt/removed-from-twitter-for-my-actions-12-years-ago-as-a-12-year-old-egi</guid>
      <description>&lt;p&gt;&lt;strong&gt;Update: This night I got a new mail from Twitter saying that I might actually be able to get my account, including tweets, back. And I actually got a link to a reactivation page where I only needed to confirm my name and phone number. So that's great and more like how I would have expected it to work from the beginning. I'm not sure though whether their automated process for handling these issues is poorly designed and is supposed to work like this. Or if there's some kind of manual review step that needs to be done for these cases, before they tell you it might be possible to just unlock the account.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm happy that it seems like it might get resolved, but it remains an extremely poor user experience.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Twitter is a popular platform among many web developers, myself included, which is why I want to share an important lesson I learned today: &lt;strong&gt;don't try to correct past mistakes in order to be honest, and never forget anything you did before you turned 13, you WILL be punished&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What follows is a text full of self pity where I'm whining about Twitters policy to punish and never forgive in the name of GDPR. You'll most likely want to go read an other DEV post. &lt;/p&gt;

&lt;p&gt;I've had my &lt;a class="mentioned-user" href="https://dev.to/jimutt"&gt;@jimutt&lt;/a&gt; Twitter account for a pretty long time. But I didn't actually start using it actively until the last couple of years. I've been slightly annoyed by the fact that my Twitter account's date of birth has been incorrect. I'm born 1994 but on Twitter it said 1993. &lt;/p&gt;

&lt;p&gt;So, as a victim of my own stupidity I thought "Why not change the year to be correct and honest". And so I did, thinking that I'd probably misclicked during the account creation and selected the wrong year. I changed it and didn't get any kind of warning about what would happen next; immediately after changing my birth date my account was locked and made completely inaccessible. The reason stated was the fact that Twitter is only available for people who are 13 years or older. And that they suspect I was under 13 when I created my account. I don't know if it's true or not, because I don't remember which websites I signed up for 12 years ago, but I'll trust that they hopefully at least have got my account registration date correct. &lt;/p&gt;

&lt;p&gt;What I fail to see is why locking the account of a, now 24 year old, adult is to be considered the preferred choice. Wouldn't it make a little more sense to perhaps delete the tweets (if any) I made as 12 year old? Guess not.. &lt;/p&gt;

&lt;p&gt;Now I've read that they've started to reactivate some accounts that has been locked because of an invalid age. But if there is to be any chance at all to get my account reactivated I guess I at the very least will need to trust them with a copy of my driving license. With my small amount of followers I'll probably just create a new one though, and make sure to be very careful when touching my account settings in the future. 🙄&lt;/p&gt;

&lt;p&gt;Rant over! &lt;/p&gt;

</description>
      <category>twitter</category>
      <category>gdpr</category>
    </item>
    <item>
      <title>Debugging Svelte apps with the newly released Svelte Devtools</title>
      <dc:creator>Jimmy Utterström</dc:creator>
      <pubDate>Sat, 28 Sep 2019 13:37:29 +0000</pubDate>
      <link>https://dev.to/jimutt/debugging-svelte-apps-with-the-newly-released-svelte-devtools-457k</link>
      <guid>https://dev.to/jimutt/debugging-svelte-apps-with-the-newly-released-svelte-devtools-457k</guid>
      <description>&lt;p&gt;A minor nuisance with using Svelte 3 (if you're coming from Vue or React) might be the lack of a browser plugin like Vue devtools, it offers convenient ways to inspect the app's component tree and direct access to component state. &lt;/p&gt;

&lt;p&gt;There is now a &lt;a href="https://github.com/RedHatter/svelte-devtools" rel="noopener noreferrer"&gt;community created devtools extension for Svelte&lt;/a&gt;, made by &lt;a href="https://github.com/RedHatter" rel="noopener noreferrer"&gt;Timothy Johnson&lt;/a&gt;, which gives you some of the basic functionality found in similar tools for other frameworks.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Firefox&lt;/strong&gt; - &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/svelte-devtools/" rel="noopener noreferrer"&gt;Install from the official store&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Chrome&lt;/strong&gt; - The extension is, at the time of writing, under review for Chrome so you'll have to manually download a zip package according to the instructions in the Readme: &lt;a href="https://github.com/RedHatter/svelte-devtools/blob/master/README.md" rel="noopener noreferrer"&gt;https://github.com/RedHatter/svelte-devtools/blob/master/README.md&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Svelte Devtools only work with Svelte version 3.12 or greater, so you might want to make sure that you've updated your Svelte NPM dependency.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Create a new Svelte app and compile it with development mode enabled
&lt;/h3&gt;

&lt;p&gt;If you don't have a custom Svelte 3 project to use you can just go with the basic app template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx degit sveltejs/template svelte-app
cd svelte-app
npm install

npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Open the app in Firefox or Chrome, press F12 and select the "Svelte tab"
&lt;/h3&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.amazonaws.com%2Fuploads%2Farticles%2Fcx3ht6rqjwtqhy901j1m.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.amazonaws.com%2Fuploads%2Farticles%2Fcx3ht6rqjwtqhy901j1m.PNG" alt="Open devtools" width="800" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Inspect state &amp;amp; components and filter what's shown
&lt;/h3&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.amazonaws.com%2Fuploads%2Farticles%2Fnmdvgwdttwtyht77u67h.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.amazonaws.com%2Fuploads%2Farticles%2Fnmdvgwdttwtyht77u67h.PNG" alt="Devtools window" width="800" height="172"&gt;&lt;/a&gt;&lt;br&gt;
In the main panel you can view and inspect all components and the HTML elements they contain. A component's props are shown both in the element/component view and in the state panel to the right. &lt;/p&gt;

&lt;p&gt;Props and state can be updated directly from the devtools:&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2F2jflccjkczbwo2vtk6ol.gif" 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.amazonaws.com%2Fuploads%2Farticles%2F2jflccjkczbwo2vtk6ol.gif" alt="Edit state" width="1086" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also filter which information you want to be shown, for example if you'd like to hide HTML elements and only show components.&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2Fdt08e91peawnez8e0x0i.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fdt08e91peawnez8e0x0i.gif" alt="Filter view" width="1010" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Like what you see? At least make sure to star the repo on GitHub (&lt;a href="https://github.com/RedHatter/svelte-devtools" rel="noopener noreferrer"&gt;https://github.com/RedHatter/svelte-devtools&lt;/a&gt;) and perhaps check if &lt;a href="https://github.com/RedHatter" rel="noopener noreferrer"&gt;RedHatter&lt;/a&gt; wants any help on improving the tool. &lt;/p&gt;

&lt;p&gt;Also check out my earlier post on Svelte: &lt;a href="https://dev.to/jimutt/boost-your-legacy-apps-with-svelte-3-components-2ab"&gt;Boost your legacy apps with Svelte 3 components&lt;/a&gt;&lt;/p&gt;

</description>
      <category>svelte</category>
      <category>javascript</category>
      <category>debugging</category>
    </item>
    <item>
      <title>How to write Go code and make it run on Adafruit Feather or Arduino</title>
      <dc:creator>Jimmy Utterström</dc:creator>
      <pubDate>Tue, 27 Aug 2019 10:09:50 +0000</pubDate>
      <link>https://dev.to/jimutt/how-to-write-go-code-and-make-it-run-on-adafruit-feather-or-arduino-m3c</link>
      <guid>https://dev.to/jimutt/how-to-write-go-code-and-make-it-run-on-adafruit-feather-or-arduino-m3c</guid>
      <description>&lt;p&gt;Did you know that you can use Go for embedded systems (like the Arduino Uno or Adafruit Feather)? &lt;a href="https://tinygo.org/" rel="noopener noreferrer"&gt;TinyGo&lt;/a&gt; is a light-weight Go compiler specifically created for that purpose. Though the project is still very young, so there are quirks and missing functionality that hasn't been implemented yet.&lt;/p&gt;

&lt;p&gt;TinyGo supports a subset of the Go programming language, for a more detailed description please refer to &lt;a href="https://tinygo.org/lang-support/" rel="noopener noreferrer"&gt;this page&lt;/a&gt;. Still, many parts of the language are already supported, and for example Goroutines seems to be working reasonably well. &lt;a href="https://tinygo.org/" rel="noopener noreferrer"&gt;The documentation&lt;/a&gt; is still not very extensive and the AVR compilation (for use with for example Arduino Uno) seems to be quite experimental at the time of writing. Therefore I will be using an &lt;a href="https://www.adafruit.com/product/2772" rel="noopener noreferrer"&gt;Adafruit Feather M0&lt;/a&gt; board with an ARM MCU for this post.  &lt;/p&gt;

&lt;p&gt;This guide will tell you how to setup your computer for TinyGo development and how to flash a simple blinking LED program to the board. The instructions have been tested on an &lt;strong&gt;Adafruit Feather M0&lt;/strong&gt; board, but should also work for these two boards (if the compilation &lt;code&gt;target&lt;/code&gt; is changed to the corresponding board):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adafruit ItsyBitsy M0 (&lt;code&gt;build -target=itsybitsy-m0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Adafruit Trinket M0 (&lt;code&gt;build -target=trinket-m0&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will be using the UF2 bootloader which makes the Feather appear as a USB drive when in bootloader mode; allowing easy copy-paste of the compiled binary. The Feather will then restart and load the program. &lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Very basic Go knowledge&lt;/li&gt;
&lt;li&gt;Knowing how to use the Arduino IDE together with a Feather Board (to install the UF2 bootloader)&lt;/li&gt;
&lt;li&gt;A compatible dev board&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TinyGo Installation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Linux
&lt;/h3&gt;

&lt;p&gt;Download and install TinyGo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ wget https://github.com/tinygo-org/tinygo/releases/download/v0.7.1/tinygo_0.7.1_amd64.deb

$ sudo apt-get install ./tinygo_0.7.1_amd64.deb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add TinyGo to &lt;code&gt;PATH&lt;/code&gt; (log out and in again for the changes to be applied): Open &lt;code&gt;~/.profile&lt;/code&gt; in your editor of choice and add the below line to the end of  the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export PATH=$PATH:/usr/local/tinygo/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Windows
&lt;/h3&gt;

&lt;p&gt;If you've enabled &lt;a href="https://docs.microsoft.com/en-us/windows/wsl/install-win10" rel="noopener noreferrer"&gt;Windows Subsystem for Linux&lt;/a&gt; (WSL) you should be able to follow the Linux instructions above. Otherwise I recommend looking at the Docker image provided by the TinyGo team: &lt;a href="https://tinygo.org/getting-started/using-docker/" rel="noopener noreferrer"&gt;https://tinygo.org/getting-started/using-docker/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  macOS
&lt;/h3&gt;

&lt;p&gt;I'm unfortunately not a Mac user so I won't be able to provide any help here, but installation on macOS is of course covered by the official docs: &lt;a href="https://tinygo.org/getting-started/macos/" rel="noopener noreferrer"&gt;https://tinygo.org/getting-started/macos/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure UF2 bootloader
&lt;/h2&gt;

&lt;p&gt;The UF2 bootloader removes the need of installing separate tooling for flashing (the BOSSA cli in this case). Instead, flashing is done through dropping a file onto a removable drive. &lt;/p&gt;

&lt;h3&gt;
  
  
  1. Enter Bootloader mode
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Connect the Feather M0 board to the computer. &lt;/li&gt;
&lt;li&gt;Double-press the reset button quickly - the board should now go into Bootloader mode, and the red built-in #13 LED should start fade in and out. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now check if your computer has detected a new USB drive (should be present in &lt;code&gt;/media/{your_user}/&lt;/code&gt; on Linux). If that's the case, you're already ready to go (pun intended) and can skip the next part! If not, you need to install the UF2 Bootloader first.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Install Bootloader if necessary
&lt;/h3&gt;

&lt;p&gt;Using the standard Arduino IDE, flash the correct UF2 Bootloader to your board. Please refer to the official Adafruit documentation for this step: &lt;a href="https://learn.adafruit.com/installing-circuitpython-on-samd21-boards/installing-the-uf2-bootloader" rel="noopener noreferrer"&gt;https://learn.adafruit.com/installing-circuitpython-on-samd21-boards/installing-the-uf2-bootloader&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're feeling too lazy to read the documentation: on the Feather M0 board you need to download &lt;a href="https://github.com/adafruit/uf2-samdx1/releases/download/v3.7.0/update-bootloader-feather_m0-v3.7.0.ino" rel="noopener noreferrer"&gt;this sketch file&lt;/a&gt; and upload it to the board through the Arduino IDE)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IMPORTANT! Make sure you download the correct bootloader file for your board, or you will risk bricking it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Build and flash/upload a simple program
&lt;/h2&gt;

&lt;p&gt;The classic "blink an LED" example for microcontrollers may be overused and not very fancy. As this post's primary purpose is bringing attention to TinyGo and showing how to get started, you will have to wait until later for more interesting use cases though. &lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create directory and an empty Go file
&lt;/h3&gt;

&lt;p&gt;Create a new directory called "feather-blink". Navigate to the directory and create an empty &lt;code&gt;feather-blink.go&lt;/code&gt; file. &lt;/p&gt;

&lt;h3&gt;
  
  
  2. Make the LED blink
&lt;/h3&gt;

&lt;p&gt;Add the following skeleton to the feather-blink.go file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="s"&gt;"machine"&lt;/span&gt;
     &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&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 "machine" import, which will be automatically resolved when compiling, allows access to the hardware. The available APIs in the &lt;code&gt;machine&lt;/code&gt; package differ depending on the board used. To know exactly which types, constants, and methods that are available, you can look at the source code in the &lt;a href="https://github.com/tinygo-org/tinygo/tree/master/src/machine" rel="noopener noreferrer"&gt;TinyGo repository&lt;/a&gt;. I suppose this type of information will most likely be included in written documentation later on.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generic &lt;a href="https://github.com/tinygo-org/tinygo/blob/master/src/machine/machine.go" rel="noopener noreferrer"&gt;machine.go&lt;/a&gt; with definition and methods for the "Pin" type&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tinygo-org/tinygo/blob/master/src/machine/board_feather-m0.go" rel="noopener noreferrer"&gt;board_feather-m0.go&lt;/a&gt; - Contains board-specific constants for PIN bindings, UART, SPI etc.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To make the on-board LED blink we can access it through the &lt;code&gt;machine.LED&lt;/code&gt; constant (&lt;code&gt;LED = D13&lt;/code&gt;) which has already been declared for us. We then configure it as an output. In the main loop, we use the &lt;code&gt;Sleep&lt;/code&gt; function from the &lt;code&gt;time&lt;/code&gt; package to add a delay before toggling the pin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="s"&gt;"machine"&lt;/span&gt;
     &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="n"&gt;led&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LED&lt;/span&gt;
     &lt;span class="n"&gt;led&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PinConfig&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Mode&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PinOutput&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

     &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;led&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Low&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;led&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&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;That's it! Now we just need to build the program and flash it to the board.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Build program and flash it onto the Feather
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Connect the Feather board to the computer and touble-click the reset button to put it in bootloader mode. &lt;/li&gt;
&lt;li&gt;Check the path for the new USB drive (on Linux it's most likely &lt;code&gt;/media/{your username}/FEATHERBOOT&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Build the program and point the output to the Feather device
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tinygo build -target=feather-m0 -o=/media/{your_user}/FEATHERBOOT/flash.uf2 feather-blink.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the build is successful, the Feather should now automatically restart and run the the program. You should be able to verify it by checking that the LED is blinking, as specified in the code, instead of fading in and out as it did in the bootloader mode. &lt;/p&gt;

&lt;p&gt;If you want to flash a new version to the board you need to first put it into the bootloader mode again. &lt;/p&gt;

&lt;h2&gt;
  
  
  Resources and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tinygo.org/" rel="noopener noreferrer"&gt;TinyGo Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;TinyGo has its own channel in the Gophers slack. Get your invite here: &lt;a href="https://invite.slack.golangbridge.org/" rel="noopener noreferrer"&gt;https://invite.slack.golangbridge.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Buy a Feather M0 board: &lt;a href="https://www.adafruit.com/product/2772" rel="noopener noreferrer"&gt;https://www.adafruit.com/product/2772&lt;/a&gt; (it's available in different versions, there's for example one with an Micro SD card slot as well)&lt;/li&gt;
&lt;li&gt;If you're new to Go you might want to checkout this interactive walkthrough: &lt;a href="https://tour.golang.org" rel="noopener noreferrer"&gt;https://tour.golang.org&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>arduino</category>
      <category>feather</category>
    </item>
    <item>
      <title>Boost your legacy apps with Svelte 3 components</title>
      <dc:creator>Jimmy Utterström</dc:creator>
      <pubDate>Fri, 21 Jun 2019 14:15:01 +0000</pubDate>
      <link>https://dev.to/jimutt/boost-your-legacy-apps-with-svelte-3-components-2ab</link>
      <guid>https://dev.to/jimutt/boost-your-legacy-apps-with-svelte-3-components-2ab</guid>
      <description>&lt;p&gt;There sure has been some well deserved fuzz around &lt;a href="https://svelte.dev/" rel="noopener noreferrer"&gt;Svelte 3&lt;/a&gt; lately, but maybe not enough? It's an amazing light-weight framework without a heavy runtime and with very little overhead. This makes it a suitable choice for more than just SPAs and isomorphic web apps. &lt;/p&gt;

&lt;p&gt;Did you know that you can create a Svelte component and with almost no extra steps distribute- and use it like any classic old Javascript library through a global constructor (&lt;code&gt;let myComponent = new MyComponent()&lt;/code&gt;)? &lt;/p&gt;

&lt;p&gt;Svelte components by default compiles to standard JavaScript classes so you only need to add an IIFE build with your component. I'll briefly show you how that's done with Rollup and how the component is used. It's not a big difference to the official app starter template (&lt;a href="https://github.com/sveltejs/template" rel="noopener noreferrer"&gt;https://github.com/sveltejs/template&lt;/a&gt;) but I think it can be easy to miss how convenient Svelte is for distributing and consuming individual components. &lt;/p&gt;

&lt;p&gt;I will use a simple example component of mine to demonstrate. It renders a Leaflet map and lets the user select a position. An event is emitted when the selected location changes (which can be used in order to update a form field for example) and the map allows props for configuring for example initial lat/lng and zoom level. &lt;/p&gt;

&lt;p&gt;The component can be found here: &lt;a href="https://github.com/jimutt/svelte-pick-a-place" rel="noopener noreferrer"&gt;https://github.com/jimutt/svelte-pick-a-place&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It can also be installed with npm: &lt;code&gt;npm install svelte-pick-a-place&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(the component is primarily created for experimental use and it's simple enough to argue that using Svelte to build might be overkill)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you were to use it within a Svelte app it would probably look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PickAPlace&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;svelte-pick-a-place&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;PickAPlace&lt;/span&gt; &lt;span class="na"&gt;leaflet=&lt;/span&gt;&lt;span class="s"&gt;{leaflet}&lt;/span&gt; &lt;span class="na"&gt;on:update=&lt;/span&gt;&lt;span class="s"&gt;{()&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; console.log('Update!')} on:save={() =&amp;gt;
console.log('On save!')} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff4vo3gut056h62e6knm6.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.amazonaws.com%2Fuploads%2Farticles%2Ff4vo3gut056h62e6knm6.png" alt="Screenshot of component" width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But what if we want to use it in a legacy app built with for example Bootstrap 3 and lots of jQuery where there's no sign of Node.js? No problem! We'll just use the IIFE build and instantiate the component class through its global constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/svelte-pick-a-place@latest/dist/pick-a-place.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/svelte-pick-a-place@latest/dist/pick-a-place.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pickAPlace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PickAPlace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;leaflet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Listen to events through component.$on('eventName', callback)&lt;/span&gt;
&lt;span class="nx"&gt;pickAPlace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New position: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Here we could for example populate an input field with the new value&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Update a prop with $set, or set the 'accessors' compiler option&lt;/span&gt;
&lt;span class="c1"&gt;// to true to automatically generate getters and setters for all props&lt;/span&gt;
&lt;span class="nx"&gt;pickAPlace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codepen demo with Bootstrap: &lt;a href="https://codepen.io/jimutt/pen/ZgaYBP" rel="noopener noreferrer"&gt;https://codepen.io/jimutt/pen/ZgaYBP&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see props and events can be easily accessed through the class API. Unfortunately there's not a convenient way to add slot content at the moment though. &lt;/p&gt;

&lt;h2&gt;
  
  
  Build config
&lt;/h2&gt;

&lt;p&gt;For the Pick a place component the production build entry point is the file &lt;strong&gt;src/components/components.module.js&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./PickAPlace.svelte&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It contains a default export with the PickAPlace component. If you were to export multiple components in the same package and wanted to instantiate them namespaced like &lt;code&gt;new MyPackageName.Component1()&lt;/code&gt; you could use named exports instead. &lt;/p&gt;

&lt;p&gt;The file is specified as the input for production builds in &lt;code&gt;rollup.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;//rollup.config.js&lt;/span&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;production&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/main.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/components/components.module.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;a href="https://github.com/jimutt/svelte-pick-a-place/blob/master/rollup.config.js" rel="noopener noreferrer"&gt;rollup.config.js&lt;/a&gt; we've added several outputs to support multiple ways of consuming the component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;//rollup.config.js&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist/index.min.mjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist/index.min.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;umd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dist/pick-a-place.min.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iife&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;// "name" is set to PickAPlace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you've probably already figured out it's the last output definition that should be by legacy apps. With the above output configuration we allow consuming the component both from a modern app with a Node.js-based environment and from legacy apps. &lt;/p&gt;

&lt;p&gt;The PickAPlace component was created from this project template and then slightly adapted: &lt;a href="https://github.com/YogliB/svelte-component-template" rel="noopener noreferrer"&gt;https://github.com/YogliB/svelte-component-template&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's all I had to say! Just wanted to post a quick reminder of Svelte's versatility and that it's a great choice for more than just complete web apps!&lt;/p&gt;

</description>
      <category>svelte</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Generate merged GeoTIFF imagery from web maps (xyz tile servers) with Python</title>
      <dc:creator>Jimmy Utterström</dc:creator>
      <pubDate>Wed, 05 Jun 2019 21:07:51 +0000</pubDate>
      <link>https://dev.to/jimutt/generate-merged-geotiff-imagery-from-web-maps-xyz-tile-servers-with-python-4d13</link>
      <guid>https://dev.to/jimutt/generate-merged-geotiff-imagery-from-web-maps-xyz-tile-servers-with-python-4d13</guid>
      <description>&lt;p&gt;This guide will show you how to programmatically download map imagery from a web map (often referred to as slippy maps) tile server, georeference it and save it as a GeoTIFF. The GeoTIFF image can later be used for other purposes, in my case I used it to colorize a LIDAR data point cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Very basic Python knowledge (I'm really not a Python dev myself)&lt;/li&gt;
&lt;li&gt;GDAL with its Python bindings (if you're using &lt;a href="https://docs.conda.io/en/latest/" rel="noopener noreferrer"&gt;Conda&lt;/a&gt; you can just follow along with the instructions below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're just here looking for the finished source code you'll find it in this repo: &lt;a href="https://github.com/jimutt/tiles-to-tiff" rel="noopener noreferrer"&gt;https://github.com/jimutt/tiles-to-tiff&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Web maps and the XYZ tile format
&lt;/h2&gt;

&lt;p&gt;Most online maps uses a shared way to render the map. Most maps are visualized by using multiple 256x256px raster tiles. The correct tiles are loaded by providing their x, y and z coordinates. Where z corresponds to the current zoom level. &lt;/p&gt;

&lt;p&gt;For example &lt;a href="https://osm.org" rel="noopener noreferrer"&gt;Open Street Map&lt;/a&gt; tiles can be fetched with an URL following this pattern: &lt;a href="https://a.tile.openstreetmap.org/" rel="noopener noreferrer"&gt;https://a.tile.openstreetmap.org/&lt;/a&gt;&lt;strong&gt;{z}&lt;/strong&gt;/&lt;strong&gt;{x}&lt;strong&gt;/&lt;/strong&gt;{y}&lt;/strong&gt;.png&lt;/p&gt;

&lt;p&gt;The x, y and z parameters are integers and works as described on the &lt;a href="https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames" rel="noopener noreferrer"&gt;OSM wiki&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;X goes from 0 (left edge is 180 °W) to 2^zoom − 1 (right edge is 180 °E)&lt;br&gt;
Y goes from 0 (top edge is 85.0511 °N) to 2^zoom − 1 (bottom edge is 85.0511 °S) in a Mercator projection&lt;/p&gt;

&lt;p&gt;For the curious, the number 85.0511 is the result of arctan(sinh(π)). By using this bound, the entire map becomes a (very large) square. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a zoom level of &lt;strong&gt;n&lt;/strong&gt; the number of tiles needed to cover the whole world is equal to 2^n × 2^n .&lt;/p&gt;

&lt;h2&gt;
  
  
  Project setup
&lt;/h2&gt;

&lt;p&gt;We'll be using Python together with &lt;a href="https://gdal.org/" rel="noopener noreferrer"&gt;GDAL&lt;/a&gt; which provides lots of convenient utilities for manipulation of geospatial raster data. &lt;/p&gt;

&lt;p&gt;I'm using &lt;a href="https://docs.conda.io/en/latest/" rel="noopener noreferrer"&gt;Conda&lt;/a&gt; to manage my Python environment as I find it to be very convenient for setting up isolated environments with easy installation of third party dependencies. &lt;/p&gt;

&lt;p&gt;Create and activate a new Conda environment with gdal as a dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;conda create -n env_name gdal
conda activate env_name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Convert latitude and longitude to corresponding tiles
&lt;/h2&gt;

&lt;p&gt;Create a file called &lt;code&gt;tile_convert.py&lt;/code&gt; for conversion from WGS84 coordinates to the corresponding tile at the specified zoom level (this fill will be containing various utility functions for transformation between WGS84 coordinates and tile names):&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;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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;latlon_to_xyz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tile_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pow&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="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;
    &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;sec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nf"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tile_count&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tile_count&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to derive tile name from latitude and longitude a number of operations has to be executed. Combine the below steps and you'll end up with the &lt;code&gt;latlon_to_xyz&lt;/code&gt; function:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Calculate the total number of tiles at the current zoom level:&lt;br&gt;
tile_count = 2^z&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reproject lat and lon to the Mercator projection (EPSG:3857).&lt;br&gt;
x = lon&lt;br&gt;
y = log(tan(lat) + sec(lat))&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transform x and y values to fit within the range of 0 - 1 and set origin to top left corner.&lt;br&gt;
x = (lon + 180) / 360&lt;br&gt;
y = (1 − (y / π)) / 2&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiply x and y by the number of tiles. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next we'll add a new function (to the same file) which allows us to get the range of tiles for a specified rectangular area/bounding box:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bbox_to_xyz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lon_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;x_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;latlon_to_xyz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;x_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;latlon_to_xyz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_min&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_max&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
           &lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_min&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_max&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we know how to calculate which individual tiles we need in order to cover a bounding box which corners are specified as WGS84 coordinates! &lt;/p&gt;

&lt;h2&gt;
  
  
  Tile download
&lt;/h2&gt;

&lt;p&gt;To download a map tile all you need to do is to send a GET request that matches the URL specification provided by the tile server. But make sure that you follow the user agreement/terms of service for the tile server you're accessing!&lt;/p&gt;

&lt;p&gt;Create a new Python file called &lt;code&gt;tiles_to_tiff.py&lt;/code&gt;. This will be our main script which will use download the required tiles, georeference each tile and then merge them into a single image.&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;math&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="c1"&gt;#---------- CONFIGURATION -----------#
&lt;/span&gt;&lt;span class="n"&gt;tile_server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.png?access_token=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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;MAPBOX_ACCESS_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;temp_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;temp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;output_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;zoom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
&lt;span class="n"&gt;lon_min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;21.49147&lt;/span&gt;
&lt;span class="n"&gt;lon_max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;21.5&lt;/span&gt;
&lt;span class="n"&gt;lat_min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;65.31016&lt;/span&gt;
&lt;span class="n"&gt;lat_max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;65.31688&lt;/span&gt;
&lt;span class="c1"&gt;#-----------------------------------#
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;download_tile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tile_server&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="n"&gt;tile_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{x}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{y}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{z}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&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;temp_dir&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;x&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;y&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;z&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.png&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlretrieve&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;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;download_tile&lt;/code&gt; function calls the specified tile server to download the requested tile and saves it as a PNG file in the output directory. &lt;/p&gt;

&lt;p&gt;Create an empty "temp" folder in your workspace and add the below line to &lt;code&gt;tiles_to_tiff.py&lt;/code&gt;:&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="nf"&gt;download_tile&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="mi"&gt;0&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;tile_server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it to validate that the download function is working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;python tiles_to_tiff.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're able to execute the script successfully you should now see a 256x256px PNG image showing the whole world in the temp directory.&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.amazonaws.com%2Fuploads%2Farticles%2F8gr7mrfh78qiegp0ky05.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.amazonaws.com%2Fuploads%2Farticles%2F8gr7mrfh78qiegp0ky05.png" alt="Map tile 0,0,0" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Download tiles that intersects bounding box
&lt;/h3&gt;

&lt;p&gt;Chances are high that you need a large image with higher resolution than what fits within a single tile. Therefore we'll use the &lt;code&gt;bbox_to_xyz&lt;/code&gt; function to get the range of tiles needed and then we'll loop through every tile and pass it to the download function. This could be done with multiple parallel requests to make it more efficient, but for now it's being kept as simple as possible.&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;x_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bbox_to_xyz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;lon_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&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;Downloading &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_max&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x_min&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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="n"&gt;y_max&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y_min&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tiles&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;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x_max&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_max&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&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;x&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;y&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="nf"&gt;download_tile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tile_server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Download complete&lt;/span&gt;&lt;span class="sh"&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 above code will download all required tiles as standard, non georeferenced, PNG images. Later we are going to merge all tiles to a single picture and for that we'll use the handy &lt;a href="https://gdal.org/programs/gdal_merge.html" rel="noopener noreferrer"&gt;gdal_merge.py&lt;/a&gt; script. Though it requires that we georeferenced the input imagery first. &lt;/p&gt;

&lt;h2&gt;
  
  
  Georeferencing
&lt;/h2&gt;

&lt;p&gt;In order to georeference the downloaded tiles we first need to reverse the translation we performed from latitude and longitude to the tile name format. We need to know the coordinates for each of the four corners of the tile. Then we'll use &lt;code&gt;gdal.Translate&lt;/code&gt; to save the image as a TIFF file with embedded geolocation data.&lt;/p&gt;

&lt;p&gt;If we remember the steps needed to go from WGS84 coordinates to the XYZ tile names we can also figure out how to retrieve the min and max longitude from a tile with the following function. Add the function to the &lt;code&gt;tile_convert.py&lt;/code&gt; file.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;x_to_lat_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tile_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pow&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="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;unit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tile_count&lt;/span&gt;
    &lt;span class="n"&gt;lon1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;
    &lt;span class="n"&gt;lon2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lon1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;
    &lt;span class="nf"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lon1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the latitude conversion we'll use the same approach but an additional function is added to translate back from the Mercator projection:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mercatorToLat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mercatorY&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sinh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mercatorY&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;y_to_lat_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tile_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pow&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="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;unit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tile_count&lt;/span&gt;
    &lt;span class="n"&gt;relative_y1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;
    &lt;span class="n"&gt;relative_y2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;relative_y1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;
    &lt;span class="n"&gt;lat1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mercatorToLat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;relative_y1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;lat2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mercatorToLat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;relative_y2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these functions in place we'll write a function that returns an array with the min and max longitude and latitude values in the same order as expected by the &lt;a href="https://gdal.org/python/osgeo.gdal-module.html#TranslateOptions" rel="noopener noreferrer"&gt;outputBounds parameter&lt;/a&gt; for the &lt;code&gt;gdal.Translate&lt;/code&gt; method. Add the &lt;code&gt;tile_edges&lt;/code&gt; function to &lt;code&gt;tile_Convert.py&lt;/code&gt;.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tile_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;y_to_lat_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;lon1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;x_to_lon_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lon1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally we will write a &lt;code&gt;georeference_raster_tile&lt;/code&gt; function in &lt;code&gt;tiles_to_tiff.py&lt;/code&gt; and then call the function from the download loop:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;georeference_raster_tile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;bounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tile_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;gdal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&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="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;outputSRS&lt;/span&gt;&lt;span class="o"&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="n"&gt;outputBounds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x_max&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_max&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&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;x&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;y&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="n"&gt;png_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;download_tile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tile_server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;georeference_raster_tile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;png_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Merge tiles to one large image
&lt;/h2&gt;

&lt;p&gt;As we've already included a dependency to GDAL I chose to also use a GDAL python script to merge the tile images. We'll be invoking &lt;code&gt;gdal_merge.py&lt;/code&gt; to do the work for us. You will need to add imports for &lt;code&gt;glob&lt;/code&gt; and &lt;code&gt;subprocess&lt;/code&gt; first.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;merge_tiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;merge_command&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_merge.py&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;-o&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&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;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_pattern&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;merge_command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merge_command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a folder called "output" in your workspace. Set the &lt;code&gt;lon_min&lt;/code&gt;, &lt;code&gt;lon_max&lt;/code&gt;, &lt;code&gt;lat_min&lt;/code&gt; and &lt;code&gt;lat_max&lt;/code&gt; to your liking as well as the &lt;code&gt;zoom&lt;/code&gt;. I'm using these settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;zoom = 15
lon_min = 21.49147
lon_max = 21.5
lat_min = 65.31016
lat_max = 65.31688
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a call to the &lt;code&gt;merge_tiles&lt;/code&gt; function at the bottom of &lt;code&gt;tiles_to_tiff.py&lt;/code&gt;.&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x_max&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_max&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&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;x&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;y&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="n"&gt;png_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;download_tile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tile_server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;georeference_raster_tile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;png_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Download complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Merging tiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;merge_tiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp_dir&lt;/span&gt; &lt;span class="o"&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="n"&gt;output_dir&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/merged.tif&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Merge complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Clear temp directory
&lt;/h3&gt;

&lt;p&gt;Before we run the script and take a look at the results you might want to add an automatic cleanup of the temp folder. I'm doing it by importing &lt;code&gt;shutil&lt;/code&gt; for removing the temp directory to then recreate it:&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;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rmtree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makedirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Showtime!
&lt;/h2&gt;

&lt;p&gt;Now let's run the script and inspect the output. I'm using the bounding box specified earlier and aerial imagery from Mapbox.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python tiles_to_tiff.py 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Downloading 8 tiles
36680,16917
36680,16918
36680,16919
36680,16920
36681,16917
36681,16918
36681,16919
36681,16920
Download complete
Merging tiles
0...10...20...30...40...50...60...70...80...90...100 - done.
Merge complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When inspecting the "output" folder it now contains a single stitched TIFF image of the specified area.&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2Fc32lkjh1ngm34um6jrke.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.amazonaws.com%2Fuploads%2Farticles%2Fc32lkjh1ngm34um6jrke.png" alt="Output directory containing merged.tif file" width="799" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When opened in a standard image viewer it looks just like any other raster image, but if you import it to QGIS you'll be able to visually verify that it has been georeferenced correctly:&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.amazonaws.com%2Fuploads%2Farticles%2Fpm7j75ct5slhev2p694p.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.amazonaws.com%2Fuploads%2Farticles%2Fpm7j75ct5slhev2p694p.png" alt="Merged TIFF in QGIS" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There you go! Source is available here: &lt;a href="https://github.com/jimutt/tiles-to-tiff" rel="noopener noreferrer"&gt;https://github.com/jimutt/tiles-to-tiff&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gis</category>
      <category>python</category>
      <category>maps</category>
      <category>geodata</category>
    </item>
  </channel>
</rss>
