<?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: Camptocamp Geospatial Solutions</title>
    <description>The latest articles on DEV Community by Camptocamp Geospatial Solutions (@camptocamp-geo).</description>
    <link>https://dev.to/camptocamp-geo</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.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2284%2F6c6bb436-f7d7-4838-8dff-5d90b4fad6ab.png</url>
      <title>DEV Community: Camptocamp Geospatial Solutions</title>
      <link>https://dev.to/camptocamp-geo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/camptocamp-geo"/>
    <language>en</language>
    <item>
      <title>Incremental Backup with PostgreSQL 17</title>
      <dc:creator>Marion Baumgartner</dc:creator>
      <pubDate>Fri, 20 Mar 2026 08:09:17 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/incremental-backup-with-postgresql-17-47e8</link>
      <guid>https://dev.to/camptocamp-geo/incremental-backup-with-postgresql-17-47e8</guid>
      <description>&lt;p&gt;With PostgreSQL 17 incremental backups wer introduced in a built in way. This article describes the result of a workshop dedicated to study this new feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&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%2Ft8gidwv9cny7m8amd19u.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%2Ft8gidwv9cny7m8amd19u.png" alt="ContainerVolumeSchema" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We used the following docker composition to simulate the a cluster with traffic that we want backed up. For this we created the following &lt;code&gt;docker-compose.yml&lt;/code&gt; with several containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_main&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testdb&lt;/span&gt;
      &lt;span class="na"&gt;PGDATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testdb&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pg_data:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;wal_archive:/mnt/wal_archive&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;full_backup:/mnt/full_backup&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;incremental_backup:/mnt/incremental_backup&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;postgres -c archive_mode=on&lt;/span&gt;
               &lt;span class="s"&gt;-c archive_command='cp %p /mnt/wal_archive/%f'&lt;/span&gt;
               &lt;span class="s"&gt;-c summarize_wal=on&lt;/span&gt;

  &lt;span class="na"&gt;postgres_restore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;restore&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testdb&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pg_data_restore:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;wal_archive:/mnt/wal_archive&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;full_backup:/mnt/full_backup&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;incremental_backup:/mnt/incremental_backup&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;postgres -c restore_command='cp /mnt/wal_archive/%f %p'&lt;/span&gt;

  &lt;span class="na"&gt;cli&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cli&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;stop_grace_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1s&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;PGUSER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;PGDATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;testdb&lt;/span&gt;
      &lt;span class="na"&gt;PGHOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres_main&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pg_data_restore:/mnt/data/restore&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pg_data:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;wal_archive:/mnt/wal_archive&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;full_backup:/mnt/full_backup&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;incremental_backup:/mnt/incremental_backup&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./checksum.py:/usr/local/bin/checksum.py&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bin/sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;chown -R 999 /mnt/wal_archive&lt;/span&gt;
        &lt;span class="s"&gt;chown -R 999 /mnt/full_backup&lt;/span&gt;
        &lt;span class="s"&gt;chown -R 999 /mnt/incremental_backup&lt;/span&gt;
        &lt;span class="s"&gt;chown -R 999 /mnt/data/restore&lt;/span&gt;
        &lt;span class="s"&gt;sleep infinity&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pg_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pg_data_restore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;wal_archive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;full_backup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;incremental_backup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes about the docker compose file&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;postgres_main&lt;/code&gt; contains the database we wish to backup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;postgres_restore&lt;/code&gt; is the database where we want to restore the database&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cli&lt;/code&gt; is to prepare the data for restoration, it enables us to connect to all clusters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ Make sure to use PG17!&lt;/p&gt;

&lt;h2&gt;
  
  
  The main idea:
&lt;/h2&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%2F5033dkn8gyfer9pgh9pr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5033dkn8gyfer9pgh9pr.jpg" alt=" " width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take a full backup as a starting point&lt;/li&gt;
&lt;li&gt;Take an incremental backup&lt;/li&gt;
&lt;li&gt;Take another incremental backup&lt;/li&gt;
&lt;li&gt;Repeat step 3&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Initial full Backup
&lt;/h2&gt;

&lt;p&gt;The first step, consists of creating an initial full backup with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_basebackup &lt;span class="nt"&gt;--pgdata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/full_backup 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Some info about the &lt;code&gt;pg_basebackup&lt;/code&gt; command
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;The target directory is a mount point for the full backup, it’s also shared between containers (via the volumes).&lt;/li&gt;
&lt;li&gt;By default pgdata specifies the target directory where the backup will be stored. In this case the output will be written to &lt;code&gt;/mnt/full_backup&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Output of the command
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;pg_basebackup&lt;/code&gt; will create a bunch of files, we will focus on the 2 important ones:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;backup_label&lt;/code&gt;:&lt;/strong&gt; this is a legacy description of the backup and here is an extract&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;START WAL LOCATION: 0/4000028 (file 000000010000000000000004)
CHECKPOINT LOCATION: 0/4000080
BACKUP METHOD: streamed
BACKUP FROM: primary
START TIME: 2025-04-24 09:08:31 UTC
LABEL: pg_basebackup base backup
START TIMELINE: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file indicates the location in the WAL where the backup starts, and so is the checkpoint location. The other information speak of themselves. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;backup_manifest&lt;/code&gt;:&lt;/strong&gt; (available since pg 13) is linked to the feature we are currently talking about - the incremental backup.&lt;/p&gt;

&lt;p&gt;This file will serve as a reference to determiner which files should be included in the incremental backup. Don’t hesitate to order some training and/or feel free to checkout the documentation [&lt;a href="https://www.postgresql.org/docs/current/backup-manifest-files.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/backup-manifest-files.html&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Here is an extract of this new &lt;code&gt;backup_manifest&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ 
 "Path": "base/16384/3766",
 "Size": 16384,
 "Last-Modified": "2025-04-24 08:56:45 GMT", 
 "Checksum-Algorithm": "CRC32C",
 "Checksum": "3c0ea625"
},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this specific “extract” of the file ‘base/16384/3766’ we have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The checksum, that is the fingerprint 3c0ea625&lt;/li&gt;
&lt;li&gt;The last modification date 2025-04-24 08:56:45 GMT&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Understanding the Checksum
&lt;/h4&gt;

&lt;p&gt;We can check this fingerprint by ourselves with a short snippet:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;python3&lt;/li&gt;
&lt;li&gt;pip install crc32c (you may need –fix-broken-packages)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;crc32c&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="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;Usage: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt;filename&amp;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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;Failed to read file: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;checksum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crc32c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crc32c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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;CRC32C (normal) : 0x&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;checksum&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="n"&gt;x&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;le_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checksum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;byteorder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;big&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[::&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;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="s"&gt;CRC32C (little-endian) : 0x&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;le_bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While no traffic is recorded on the pg cluster, the data will remain the same, as shown here.&lt;/p&gt;

&lt;p&gt;Execute the checksum python script (file is in the path) with the parameter (file)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; You can find the file where the data of a specific table is stored with the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'base/'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;pg_database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;relfilenode&lt;/span&gt; &lt;span class="k"&gt;AS&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;pg_database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;db_oid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;pg_database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;nspname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_namespace&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;pg_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relnamespace&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_database&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;pg_database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pg_database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;relfilenode&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'pgbench%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which in this case returns the output:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;relname&lt;/th&gt;
&lt;th&gt;filename&lt;/th&gt;
&lt;th&gt;database&lt;/th&gt;
&lt;th&gt;schema&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pgbench_accounts&lt;/td&gt;
&lt;td&gt;base/16384/16397&lt;/td&gt;
&lt;td&gt;testdb&lt;/td&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgbench_accounts_pkey&lt;/td&gt;
&lt;td&gt;base/16384/16405&lt;/td&gt;
&lt;td&gt;testdb&lt;/td&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgbench_branches&lt;/td&gt;
&lt;td&gt;base/16384/16398&lt;/td&gt;
&lt;td&gt;testdb&lt;/td&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgbench_branches_pkey&lt;/td&gt;
&lt;td&gt;base/16384/16401&lt;/td&gt;
&lt;td&gt;testdb&lt;/td&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgbench_history&lt;/td&gt;
&lt;td&gt;base/16384/16399&lt;/td&gt;
&lt;td&gt;testdb&lt;/td&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgbench_tellers&lt;/td&gt;
&lt;td&gt;base/16384/16400&lt;/td&gt;
&lt;td&gt;testdb&lt;/td&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgbench_tellers_pkey&lt;/td&gt;
&lt;td&gt;base/16384/16403&lt;/td&gt;
&lt;td&gt;testdb&lt;/td&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So based on this we need to take a look at the file &lt;code&gt;base/16384/16397&lt;/code&gt; to determine checksum for the &lt;code&gt;pgbench_accounts&lt;/code&gt; relation&lt;/p&gt;

&lt;p&gt;Now execute the python script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./checksum.py /var/lib/docker/volumes/demo-postgres-backup-incremental_pg_data/_data/base/16384/16397

&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; 0x57679e47  &lt;span class="c"&gt;# CRC32C &lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Path"&lt;/span&gt;: &lt;span class="s2"&gt;"base/16384/16397"&lt;/span&gt;, &lt;span class="s2"&gt;"Size"&lt;/span&gt;: 536870912, &lt;span class="s2"&gt;"Last-Modified"&lt;/span&gt;: &lt;span class="s2"&gt;"2025-04-24 11:44:14 GMT"&lt;/span&gt;, &lt;span class="s2"&gt;"Checksum-Algorithm"&lt;/span&gt;: &lt;span class="s2"&gt;"CRC32C"&lt;/span&gt;, &lt;span class="s2"&gt;"Checksum"&lt;/span&gt;: &lt;span class="s2"&gt;"57679e47"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To se a change on the checksum let’s make some changes: We update a column in the &lt;code&gt;pgbench_account&lt;/code&gt; table&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;pgbench_accounts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;abalance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;abalance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lets, check the fingerprint again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./checksum.py /var/lib/docker/volumes/demo-postgres-backup-incremental_pg_data/_data/base/16384/16397

&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; 0x06cc374f  &lt;span class="c"&gt;# CRC32C&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Path"&lt;/span&gt;: &lt;span class="s2"&gt;"base/16384/16397"&lt;/span&gt;, &lt;span class="s2"&gt;"Size"&lt;/span&gt;: 1073741824, &lt;span class="s2"&gt;"Last-Modified"&lt;/span&gt;: &lt;span class="s2"&gt;"2025-04-24 12:37:28 GMT"&lt;/span&gt;, &lt;span class="s2"&gt;"Checksum-Algorithm"&lt;/span&gt;: &lt;span class="s2"&gt;"CRC32C"&lt;/span&gt;, &lt;span class="s2"&gt;"Checksum"&lt;/span&gt;: &lt;span class="s2"&gt;"06cc374f"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;‼️&lt;strong&gt;When using a long update query, we can inspect the datadir and notice that the file is modified (checksum will be different), even if the transaction is not yet committed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If we cancel the query, even if the data is not modified from the logical point (transaction rollback), the data on the disk will contain the uncommitted modifications. Due to the &lt;a href="https://www.postgresql.org/docs/current/storage-vm.html" rel="noopener noreferrer"&gt;visibility map&lt;/a&gt; those modifications are not visible by any transaction but the file is modified, and the checksum will be different&lt;/p&gt;

&lt;p&gt;⚠️ Wal summarize need to be activated (see our docker-compose)&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Now Let’s Increment!
&lt;/h2&gt;

&lt;p&gt;The first increment contains the diff from the full backup. Then the second increment references the previous one and should only contain the diff from the first incremental.&lt;/p&gt;

&lt;p&gt;To set this up we will use the following command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_basebackup &lt;span class="nt"&gt;--checkpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fast &lt;span class="nt"&gt;--incremental&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/full_backup/backup_manifest &lt;span class="nt"&gt;--pgdata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/incremental_backup/0/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--checkpoint=fast&lt;/code&gt; is set in order not wait for the next checkpoint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--incremental&lt;/code&gt; is where the magic appears, it must point to the backup_manifest of the last increment or full backup and is the origin of the diff&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-- pgdata&lt;/code&gt; specifies the destination directory where the incremental backup will be stored&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The creation of a new increment can be repeated multiple times. Where each increment contains only a copy of the blocks/pages that have changed since the last given increment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating Incremental Backups for Restoration
&lt;/h2&gt;

&lt;p&gt;Now let’s put the incremental backup back together with the full backup and begin the database restoration process.&lt;br&gt;
In order to do this we will use &lt;code&gt;pg_combinebackup&lt;/code&gt; to merge the full backup with the incremental backups.&lt;br&gt;
This command accepts multiple arguments, allowing us to specify as many incremental backups as we need. The first argument must be a full backup followed by the increments in a chronological order. If this order is not respected we will run into issues.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_combinebackup &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /mnt/data/restore /mnt/full_backup /mnt/incremental_backup/0 /mnt/incremental_backup/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will give us a full backup in &lt;code&gt;/mnt/data/restore&lt;/code&gt;. So now we can start a restored database from the combined backup.&lt;/p&gt;

&lt;p&gt;In terms of the size of the back up we can see, that the full backup is much larger than the the increment: &lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.6G&lt;/td&gt;
&lt;td&gt;full_backup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25M&lt;/td&gt;
&lt;td&gt;incremental_backup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the more changes there are in the database the larger is the incremental backup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Changes&lt;/th&gt;
&lt;th&gt;Comment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;25M&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25M&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;change of 2 records in a partitioned table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;28M&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;change of 1000 records in a partitioned table&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  RTO (Recovery Time Objective) and RPO (Recovery Point Objective):
&lt;/h2&gt;

&lt;p&gt;Lets talk about the impact of the recovery time objective (RTO). This is the time that is needed to rebuild a full backup with all the increments and recreate a recovery database.&lt;/p&gt;

&lt;p&gt;It is possible to to have different scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One full backup once a week and one incremental backup every day.&lt;/li&gt;
&lt;li&gt;One nightly full backup and incremental backups on an hourly rate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The second scenery is only relevant for a database with a lot of traffic.&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%2F4y8sy1yxepfnqvz74fhh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4y8sy1yxepfnqvz74fhh.jpg" alt="PTORTO" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most consuming part is the reply of the WAL file, this means that we need to reduce the number of WAL to replay. One way to do this is to make the last incremental backup as close as possible to the recovery target. The second scenario is a good candidate to cover this. With incremental backups it is possible to handle this in a way so that not all of the data in the Database is transferred every hour but just the tables that contain modification in comparison to the last incremental backup.&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%2Fv9q8k5i5v9kwzriirlba.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%2Fv9q8k5i5v9kwzriirlba.png" alt="backupStrategy" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another independent scenery that could be imagined is that we make use of the increments and the full backup to create a new füll backup once in a while. Then on the next increment will be based on this new full backup. In this way backup traffic on the cluster can be reduced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary:
&lt;/h2&gt;

&lt;p&gt;In the blog pst we have analysed how to use incremental backups and how to set them back together to create a new folder so that we can start a restoration of the original database.&lt;/p&gt;

&lt;p&gt;On the side the checksum and the manifest files that are used for incremental backups are explained and analysed.&lt;/p&gt;

&lt;p&gt;As an out come we see is that by using incremental backups the subsequent backup gets smaller since only what changed since the last incremental run is saved.&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>tutorial</category>
      <category>postgressql</category>
    </item>
    <item>
      <title>FOSDEM 2026</title>
      <dc:creator>Emilien Devos</dc:creator>
      <pubDate>Tue, 10 Mar 2026 12:52:59 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/fosdem-2026-2e8p</link>
      <guid>https://dev.to/camptocamp-geo/fosdem-2026-2e8p</guid>
      <description>&lt;p&gt;Feedback from FOSDEM in Brussels (Belgium) in 2026: &lt;a href="https://fosdem.org/2026/" rel="noopener noreferrer"&gt;https://fosdem.org/2026/&lt;/a&gt; and &lt;a href="https://fosdem.org/2026/about/" rel="noopener noreferrer"&gt;https://fosdem.org/2026/about/&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vibe
&lt;/h2&gt;

&lt;p&gt;Like &lt;a href="https://dev.to/camptocamp-geo/fosdem-2025-less-walking-but-more-talking-2354"&gt;last year&lt;/a&gt;, I focused more on the social aspect and discussions than trying to cram as many presentations as possible into two days.&lt;/p&gt;

&lt;p&gt;There were more people than last year. FOSDEM is becoming increasingly popular. This year was the first time I was unable to attend the closing presentation of FOSDEM because there were too many participants in a room that can accommodate 1,500 people!&lt;/p&gt;

&lt;p&gt;In particular, gyptazy, a member of the community, shared the feeling that this year has been a turning point, with a sharp increase in attendance:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;But in 2026, it felt like something had shifted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read more here: &lt;a href="https://gyptazy.com/blog/fosdem-2026-opensource-conference-brussels/" rel="noopener noreferrer"&gt;https://gyptazy.com/blog/fosdem-2026-opensource-conference-brussels/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Interesting discussions
&lt;/h2&gt;

&lt;p&gt;This year, I wasn't able to participate in a &lt;a href="https://fosdem.org/2026/schedule/#bofs" rel="noopener noreferrer"&gt;BoF (Birds Of a Feather)&lt;/a&gt; session, and unfortunately there weren't any organized around geospatial topics.&lt;/p&gt;

&lt;p&gt;But I did get to chat with a few people from the community, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://vates.tech/en/" rel="noopener noreferrer"&gt;Vates&lt;/a&gt;, a virtualization solution that positions itself as an open source alternative to Proxmox and VMWare but based on Xen technology.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linkedin.com/in/lacombef/" rel="noopener noreferrer"&gt;François Lacombe&lt;/a&gt;, who shared his Podoma project with me, which I will discuss in the next section.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Talks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Geospatial
&lt;/h3&gt;

&lt;p&gt;This is the second year that the Geospatial track has returned to FOSDEM. And the Geospatial community at FOSDEM seems to be growing, as this year the room could accommodate 190 people compared to 75 last year!&lt;/p&gt;

&lt;p&gt;The only presentation I managed to attend was &lt;a href="https://fosdem.org/2026/schedule/event/TJ8TQS-openstreetmap-podoma-monitoring-worldwide/" rel="noopener noreferrer"&gt;Podoma&lt;/a&gt;, by François Lacombe, who presented a tool for visualizing OpenStreetMap contributions. This tool was created out of a need to better understand the evolution of contributions to OSM. It includes a podium system to highlight the biggest contributors in specific areas.&lt;/p&gt;

&lt;p&gt;Other Geospatial presentations are available here: &lt;a href="https://fosdem.org/2026/schedule/track/geospatial/" rel="noopener noreferrer"&gt;https://fosdem.org/2026/schedule/track/geospatial/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Infrastructure
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://fosdem.org/2026/schedule/event/HVNAHG-garage_object_storage_2_0_update_and_best_practices/" rel="noopener noreferrer"&gt;Garage Object Storage: 2.0 update and best practices&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;The Garage team presents its second major release. Garage is software for hosting a self-hosted S3 API.&lt;/p&gt;

&lt;p&gt;The enthusiasm for the project is much greater than when I attended its launch presentation two years ago. This is mainly because Minio is no longer open source, so the community has turned to Garage.&lt;/p&gt;

&lt;p&gt;I noted quite a few tips on how to use Garage, particularly regarding hosting. This could be very useful for an S3 deployment on GeoServer.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://fosdem.org/2026/schedule/event/JWX9UM-postgres-mysql-two-databases-three-perspectives/" rel="noopener noreferrer"&gt;PostgreSQL and MySQL, Two Databases, Three Perspectives&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;A great talk that clearly explains the internal differences between PostgreSQL and MySQL. It covers areas such as replication, the database engine, client connections, and more.&lt;/p&gt;

&lt;p&gt;This talk highlights the value of drawing inspiration from MySQL's strengths in order to improve PostgreSQL accordingly.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://fosdem.org/2026/schedule/event/8MUFCA-building_isos_from_oci_containers/" rel="noopener noreferrer"&gt;Building ISOs from OCI containers&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;I was intrigued by this talk because at Camptocamp, many of us write Dockerfiles, but to deploy an application in a container.&lt;/p&gt;

&lt;p&gt;The idea of being able to deploy this container as a virtual machine or even physically via a USB key is interesting!&lt;/p&gt;

&lt;p&gt;This talk discussed the Containerfile format, a variant of Dockerfile, for generating an ISO file from a syntax familiar to that of a Dockerfile.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other very interesting talks
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://fosdem.org/2026/schedule/event/LXFKS9-servo-project-impact/" rel="noopener noreferrer"&gt;The Servo project and its impact on the web platform ecosystem&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Servo is a rendering engine for web pages. This talk presents the impact that Servo has on the web ecosystem.&lt;/p&gt;

&lt;p&gt;It was accompanied by a demo where the presenter showed the rendering of complex web pages such as GitHub, Wikipedia, playing on Chess.com, and even his presentation was in Servo!&lt;/p&gt;

&lt;p&gt;This is super exciting, because only three browser rendering engines dominate the web, and one more is really welcome!&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://fosdem.org/2026/schedule/event/AUFR8F-who-pays-your-bills/" rel="noopener noreferrer"&gt;Who Pays Your Bills? Sustainability, Community, and Business: The Open Source Triangle&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Closely related to geospatial technology because the presenter is part of the QGIS team.&lt;/p&gt;

&lt;p&gt;This presentation explains how to find the financial balance between the three pillars of an open source project: community, product, and business.&lt;/p&gt;

&lt;h3&gt;
  
  
  To view the other 1,073 talks
&lt;/h3&gt;

&lt;p&gt;All talks have been recorded and can be viewed by selecting the desired theme and then the specific talk: &lt;a href="https://fosdem.org/2026/schedule/rooms/" rel="noopener noreferrer"&gt;https://fosdem.org/2026/schedule/rooms/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>fosdem</category>
      <category>opensource</category>
      <category>camptocamp</category>
      <category>techtalks</category>
    </item>
    <item>
      <title>Adding SonarQube to an AI-Assisted Development Workflow</title>
      <dc:creator>Florent Gravin</dc:creator>
      <pubDate>Mon, 02 Mar 2026 11:11:40 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/adding-sonarqube-to-an-ai-assisted-development-workflow-3k0h</link>
      <guid>https://dev.to/camptocamp-geo/adding-sonarqube-to-an-ai-assisted-development-workflow-3k0h</guid>
      <description>&lt;p&gt;We use AI agents as coding assistants (here Claude Code) in our daily work. Not in a "vibe coding" way where you throw a prompt and hope for the best. We practice &lt;strong&gt;spec-driven development&lt;/strong&gt;: detailed specifications, structured plans, context engineering through &lt;code&gt;CLAUDE.md&lt;/code&gt; files, custom instructions, and carefully scoped tasks. The goal is to give the AI enough context to produce good code from the start.&lt;/p&gt;

&lt;p&gt;And it mostly works. The code we get is solid. But "mostly" isn't enough when you're shipping to production. Even with good context and clear specs, an AI assistant can still produce code with security issues, excessive complexity, or subtle smells that you'd catch in a thorough review — if you had the time.&lt;/p&gt;

&lt;p&gt;So we added &lt;strong&gt;SonarQube&lt;/strong&gt; to the loop, and we let the AI fix what Sonar finds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to rely on CI based Sonar scans and reports, you need to have non-free options :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sonar cloud subscription&lt;/li&gt;
&lt;li&gt;SonarQube sever, developer edition (the community edition does not help much).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;We wanted something that runs locally, requires no manual steps, and integrates directly into the AI agent's workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A local SonarQube instance&lt;/strong&gt; via Docker Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sonarqube&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sonarqube:lts-community&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9000:9000"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sonar_data:/opt/sonarqube/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A scan script&lt;/strong&gt; (&lt;code&gt;scripts/sonar-scan.sh&lt;/code&gt;) that handles the full lifecycle: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checks that SonarQube is up&lt;/li&gt;
&lt;li&gt;auto-provisions an auth token on first run&lt;/li&gt;
&lt;li&gt;creates the project if needed&lt;/li&gt;
&lt;li&gt;runs the scanner via Docker&lt;/li&gt;
&lt;li&gt;waits for analysis to finish&lt;/li&gt;
&lt;li&gt;fetches all issues&lt;/li&gt;
&lt;li&gt;and writes them to a structured &lt;code&gt;sonar-report.json&lt;/code&gt; file. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each issue comes with a file path, line number, severity, rule ID, and a description. This structured output is what makes the automation possible — the AI agent can parse it and act on each issue directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Claude Code slash command&lt;/strong&gt; (&lt;code&gt;.claude/commands/sonar.md&lt;/code&gt;) that ties it together. When you run &lt;code&gt;/sonar&lt;/code&gt;, the agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runs &lt;code&gt;bash scripts/sonar-scan.sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reads the report — issues sorted by severity (BLOCKER, CRITICAL, MAJOR...)&lt;/li&gt;
&lt;li&gt;For each issue: reads the file, understands the rule violation, applies a minimal fix&lt;/li&gt;
&lt;li&gt;Skips generated code (API clients, etc.)&lt;/li&gt;
&lt;li&gt;Runs &lt;code&gt;npm run type-check&lt;/code&gt; to make sure fixes don't break anything&lt;/li&gt;
&lt;li&gt;Re-scans. If new issues appeared, fixes those too.&lt;/li&gt;
&lt;li&gt;Reports what it changed and how many iterations it took.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;CI enforcement&lt;/strong&gt; via GitHub Actions on every PR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SonarQube Scan&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SonarSource/sonarqube-scan-action@v5&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;SONAR_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SONAR_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;SONAR_HOST_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SONAR_HOST_URL }}&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Quality Gate&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SonarSource/sonarqube-quality-gate-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How the loop works in practice
&lt;/h2&gt;

&lt;p&gt;We recently built a feature with several Lit web components, services, and types — version history with diffs, draft management, conflict resolution dialogs. The AI generated it from detailed specs. The code worked, tests passed, TypeScript was happy.&lt;/p&gt;

&lt;p&gt;Then we ran &lt;code&gt;/sonar&lt;/code&gt;. Sonar found things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cognitive complexity too high in some diff rendering logic&lt;/li&gt;
&lt;li&gt;Unused imports left over from refactoring iterations&lt;/li&gt;
&lt;li&gt;Duplicated string literals across components&lt;/li&gt;
&lt;li&gt;Missing type narrowing where null dereferences were possible&lt;/li&gt;
&lt;li&gt;Missing ARIA attributes on interactive elements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these were show-stoppers. All of them were things you'd want fixed before merging. The agent resolved them in 2-3 scan cycles without intervention.&lt;/p&gt;

&lt;p&gt;The point isn't that the AI wrote bad code. The point is that static analysis catches a class of issues that are easy to miss during generation — and the AI is perfectly capable of fixing them when told what's wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Sonar and not just a linter
&lt;/h2&gt;

&lt;p&gt;ESLint catches a lot, but SonarQube covers ground that linters don't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security vulnerability detection (injections, hardcoded secrets, insecure patterns)&lt;/li&gt;
&lt;li&gt;Cross-file duplication detection&lt;/li&gt;
&lt;li&gt;Cognitive complexity scoring&lt;/li&gt;
&lt;li&gt;A quality gate — a binary pass/fail, not just a list of warnings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combined with TypeScript's type checker and your test suite, you get three independent verification layers, each catching different categories of problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like day-to-day
&lt;/h2&gt;

&lt;p&gt;Our workflow before a PR:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write specs and plan the feature&lt;/li&gt;
&lt;li&gt;AI implements it with full context (CLAUDE.md, specs, existing code)&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;/sonar&lt;/code&gt; — the agent scans, fixes, re-scans until clean&lt;/li&gt;
&lt;li&gt;Verify that typecheck and tests still pass&lt;/li&gt;
&lt;li&gt;Review the diff and open the PR&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Sonar step typically adds a few minutes. It's not something we run after every edit — just before we consider a feature done. Think of it as an automated pre-review pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;If you use AI assistants for coding, you already have good reasons to invest in context engineering — giving the AI the right specs, the right constraints, the right examples. SonarQube is another form of that same idea: giving the AI structured feedback about what's not right, and letting it iterate.&lt;/p&gt;

&lt;p&gt;The scan script, the slash command, and the CI workflow are all straightforward to set up. SonarQube Community Edition is free. The scripts are plain bash. This works with any AI coding tool that can run shell commands and read structured output.&lt;/p&gt;

&lt;p&gt;The code is on GitHub if you want to look at the implementation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/fgravin/ai-agent-sonar-loop" rel="noopener noreferrer"&gt;Link to the repository&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sonarqube</category>
      <category>aiagents</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>🧠 Lessons Learned: Multi-Platform Intelligence Suite</title>
      <dc:creator>Emmanuel Belo</dc:creator>
      <pubDate>Sat, 21 Feb 2026 10:31:48 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/lessons-learned-xtwitter-daily-summary-project-ip5</link>
      <guid>https://dev.to/camptocamp-geo/lessons-learned-xtwitter-daily-summary-project-ip5</guid>
      <description>&lt;p&gt;This retrospective documents the development of a Multi-Platform Intelligence Suite — an autonomous OSINT python tool designed to transform high-noise social feeds into structured, geopolitical-style briefings, released on &lt;a href="https://github.com/ebelo/x-daily-summary?tab=readme-ov-file#x-daily-summary-tool" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The application currently aggregates timelines from X (Twitter) and Bluesky, standardizing engagement metrics via Z-score normalization to ensure high-signal content is identified regardless of the host platform's user density.&lt;/p&gt;

&lt;p&gt;The tool supports two AI backends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini (Google Cloud):&lt;/strong&gt; Fast, high-quality, but costs API credits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama (Local):&lt;/strong&gt; Runs entirely on your own hardware, free to use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This document captures the real challenges I ran into, the decisions I made, and the insights I'd apply if starting over. It is written for someone who wants to build something similar.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. API Costs: Discover, Measure, Then Mitigate
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What happened:&lt;/strong&gt; The X API is priced pay-per-request. $25 was loaded as credits and the tool was run for the first time on a real 24-hour timeline. The result: fetching ~800 posts cost approximately $4 in a single run.&lt;/p&gt;

&lt;p&gt;That's a concrete, measurable signal. At that rate, iterating on the AI pipeline — running it 5-10 times to test different prompts, models, and architectures — would have burned through the entire $25 budget in API calls alone, producing nothing useful.&lt;/p&gt;

&lt;p&gt;The immediate response was to ask the agent to implement a skip flag: &lt;code&gt;--from-summary&lt;/code&gt;. Once a summary is fetched and saved to disk, all subsequent runs reuse it. The X API is never called again until you explicitly want fresh data.&lt;/p&gt;

&lt;p&gt;A second flag, &lt;code&gt;--intel-limit N&lt;/code&gt;, was added to cap the number of posts sent to the LLM for fast, cheap iteration on the AI components.&lt;/p&gt;

&lt;p&gt;The lesson is not "plan for costs upfront." It's: measure your first real run, notice where money is being burned, and immediately build the escape hatch. On a pay-per-request API, the fetch layer and the intelligence layer should be independently executable from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Two-stage pipelines (fetch → process) should always support running each stage independently. The moment you see a real cost, you can respond in minutes — if the architecture allows it.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Value of Agent-Delegated Complexity
&lt;/h2&gt;

&lt;p&gt;Several genuinely hard technical problems appeared during this project: OAuth 1.0a authentication (a 4-key scheme used by the X API), environment variable loading bugs, and API credential validation errors that produced opaque error messages. These are exactly the kinds of problems that would traditionally stop a non-developer cold.&lt;/p&gt;

&lt;p&gt;They didn't stop the project. The agent diagnosed each issue, reprompted itself, tried fixes, validated them, and moved on — autonomously. From the user's perspective, these problems &lt;em&gt;happened and were resolved&lt;/em&gt; without any manual intervention. No terminal commands to write. No &lt;code&gt;.env&lt;/code&gt; syntax to understand. No OAuth documentation to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real lesson here is not technical — it is about trust and scope.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Working with an agentic AI partner means you can &lt;strong&gt;delegate an entire category of problem&lt;/strong&gt; — low-level debugging, configuration, environment management — and direct your own attention toward higher-level decisions: &lt;em&gt;what&lt;/em&gt; to build, &lt;em&gt;which&lt;/em&gt; model to use, &lt;em&gt;what&lt;/em&gt; the output should look like.&lt;/p&gt;

&lt;p&gt;This is a different kind of collaboration than using a search engine or a tutorial. The agent doesn't just give you the answer — it implements it, tests it, and moves on. The release of being able to trust that process was significant: it meant the pace of the project was set by &lt;em&gt;decisions&lt;/em&gt;, not by debugging capacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; The ceiling of what a non-developer can build is dramatically higher with an agentic AI partner — not because the technical problems go away, but because they no longer have to stop you.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Architecture Decision: Map-Reduce for Local Models
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Context:&lt;/strong&gt; A "context window" is the maximum amount of text a language model can process at once. A typical LLM context window holds ~8,000–32,000 tokens. An 800-post timeline easily exceeds that limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Feeding all 800+ posts to a local model in one prompt caused hallucinations, dropped posts, and incoherent summaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution — Map-Reduce:&lt;/strong&gt;&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Map:&lt;/strong&gt; Split posts into small batches of 10. Ask the model to classify each post into one of 6 predefined categories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select:&lt;/strong&gt; From each category, take only the top 15 posts by engagement score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft:&lt;/strong&gt; Generate a section summary for each category (90 posts total, well within any context window).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduce (Reflective Pass):&lt;/strong&gt; Feed all 6 drafted sections back to the model and ask it to synthesize a single-paragraph &lt;strong&gt;Executive Summary&lt;/strong&gt; — the model reflecting on its own work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The "reflective pass" is a powerful general pattern: generate drafts, then ask the model to step back and synthesize. It consistently produces better summaries than one-shot attempts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; For large datasets, never attempt a single prompt. Classify first, then synthesize progressively.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Cloud-to-Local: Architectures Don't Scale Down
&lt;/h2&gt;

&lt;p&gt;This is perhaps the most counterintuitive lesson of the entire project.&lt;/p&gt;

&lt;p&gt;The natural assumption when adding local model support is: &lt;em&gt;"I already have a Gemini prompt that works well — I'll reuse it with Ollama and swap the endpoint."&lt;/em&gt; This is wrong, and the failure is not gradual — it is qualitative.&lt;/p&gt;

&lt;p&gt;In naval engineering, this problem has a name: &lt;strong&gt;similitude failure&lt;/strong&gt;. When you build a scale model of a ship to test in a tank, the forces that dominate behavior are &lt;em&gt;different&lt;/em&gt; at small scale. A full-size ship is dominated by wave drag and inertia. A small model in a tub is dominated by surface tension and viscosity. The same equations, the same shape — completely different governing physics. You can't just shrink the design.&lt;/p&gt;

&lt;p&gt;The same principle applies to LLMs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure Mode&lt;/th&gt;
&lt;th&gt;Large Cloud Model (Gemini)&lt;/th&gt;
&lt;th&gt;Small Local Model (Llama 3.2)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context window&lt;/td&gt;
&lt;td&gt;Effectively unlimited (1M+ tokens)&lt;/td&gt;
&lt;td&gt;Very limited (~8K tokens)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instruction fidelity&lt;/td&gt;
&lt;td&gt;Reliably follows complex prompts&lt;/td&gt;
&lt;td&gt;Drifts, invents, simplifies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;Stable with a well-written prompt&lt;/td&gt;
&lt;td&gt;Needs strict enumeration of every valid output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucination&lt;/td&gt;
&lt;td&gt;Rare in classification tasks&lt;/td&gt;
&lt;td&gt;Common if categories are not explicitly constrained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-pass summary&lt;/td&gt;
&lt;td&gt;Works for 800+ posts&lt;/td&gt;
&lt;td&gt;Fails; context is truncated or ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;dominant failure modes are qualitatively different&lt;/strong&gt;, not just quantitatively worse. A single Gemini prompt breaks in a completely different way on Llama 3.2 than you'd expect from "a smaller, dumber version."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means in practice:&lt;/strong&gt; Local model support is not a feature — it is a separate architecture. It requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Breaking large prompts into small, constrained steps (Map-Reduce)&lt;/li&gt;
&lt;li&gt;Strict enumeration of all valid outputs in the prompt&lt;/li&gt;
&lt;li&gt;A reflective synthesis pass instead of one-shot generation&lt;/li&gt;
&lt;li&gt;Validation and fallback logic at every stage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Corollary — Define the Output Schema First:&lt;/strong&gt; This lesson has an immediate practical implication. With two backends (Gemini and Ollama), each had its own system prompt written independently. The result: different category names, different heading structures, and incompatible report formats. The Gemini prompt used free-form topics; the Ollama pipeline used a fixed list of 6 categories. They couldn't be compared or combined.&lt;/p&gt;

&lt;p&gt;The fix was to define the 6 categories centrally and update both prompts to reference them. In a multi-backend system, the output schema is the contract — treat it like an API. Writing prompts before agreeing on the schema causes messy retrofits.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Model Selection: VRAM is the Real Constraint
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Context:&lt;/strong&gt; VRAM (Video RAM) is the dedicated memory on your GPU. If a model fits entirely in VRAM, inference is extremely fast. If it doesn't, the system "spills" to regular RAM, which is 10-20x slower.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Fits in 4GB VRAM?&lt;/th&gt;
&lt;th&gt;Time for 839 posts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mistral 7B&lt;/td&gt;
&lt;td&gt;4.4 GB&lt;/td&gt;
&lt;td&gt;❌ (RAM spill)&lt;/td&gt;
&lt;td&gt;~6.5 hours (estimated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.2 3B&lt;/td&gt;
&lt;td&gt;2.0 GB&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~25 minutes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini Flash&lt;/td&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;~45 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hardware used for this benchmark: &lt;strong&gt;Intel 11th Gen i7, 16GB RAM, NVIDIA T500 (4GB VRAM)&lt;/strong&gt; — a mobile workstation laptop several years old. Despite its age, Llama 3.2 ran entirely in its VRAM and processed the full 839-post dataset comfortably.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Trap:&lt;/strong&gt; "Mistral-Small" sounds like a small model — it is actually 24B parameters (~14GB). The "Small" in Mistral's naming refers to it being small relative to their 123B flagship, not relative to a 7B base model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; For batch-heavy local pipelines, the largest model that fits &lt;em&gt;completely&lt;/em&gt; in VRAM will outperform a "smarter" model that doesn't — often by an order of magnitude.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Prompt Engineering: Strictness Over Creativity
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Challenge:&lt;/strong&gt; Local models are prone to "category hallucination" — inventing category names not in the provided list (e.g. returning "Finance &amp;amp; Economy" instead of the defined "Economics &amp;amp; Markets"). This corrupts the Map-Reduce grouping step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What worked:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Providing the exact list of 6 category strings in the prompt with explicit instruction to use &lt;em&gt;only&lt;/em&gt; these strings.&lt;/li&gt;
&lt;li&gt;One-shot formatting: showing the model one example of the expected input/output pair.&lt;/li&gt;
&lt;li&gt;Strict matching in the classification parser with a fallback to "unrecognised" rather than accepting approximate matches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; For structured classification tasks, suppress model creativity. Enumerate all valid outputs explicitly. Any ambiguity will be exploited.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Code Quality: SonarCloud as Your External Auditor
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;As a non-developer, you cannot assess the security or quality of code you didn't write.&lt;/strong&gt; You can't spot a deeply nested function that will become impossible to maintain, or know when exception handling is done in a way that suppresses critical signals. You have to trust that the code is correct.&lt;/p&gt;

&lt;p&gt;SonarCloud provides an independent, automated answer to that trust problem. Every time code was pushed to GitHub, SonarCloud ran a quality pass and flagged any issues — from structural complexity to unsafe exception handling patterns. No developer judgment required; the findings are plain-language, specific, and actionable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The workflow that emerged was trivially simple:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the SonarCloud dashboard after a push.&lt;/li&gt;
&lt;li&gt;Copy the flagged issue description.&lt;/li&gt;
&lt;li&gt;Hand it directly to the coding agent: &lt;em&gt;"Fix this SonarCloud warning."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;The agent implements the fix, explains what changed, and pushes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No major security issues were found. The warnings were structural — functions too complex to maintain easily, commented-out dividers mistaken for dead code, a &lt;code&gt;try/except&lt;/code&gt; pattern that could suppress system signals. All were resolved quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; For non-developer-led projects, a CI/CD quality gate like SonarCloud is not optional — it is your independent code reviewer. It closes the gap between "the agent wrote code" and "the code is trustworthy."&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Git Strategy: Branches for Architectural Changes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Challenge:&lt;/strong&gt; During the shift to Map-Reduce, the codebase changed significantly — new modules, new data flow, new prompts. The temptation is to keep going until it "works" before committing. The result: a sprawling unstaged delta that's hard to reason about or partially revert.&lt;br&gt;
&lt;strong&gt;The psychological trap:&lt;/strong&gt; "I don't want to commit broken code." This causes delayed commits, which causes large messy commits, which causes reverting damage to be much worse.&lt;br&gt;
&lt;strong&gt;What to do instead:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;git checkout -b feature/map-reduce&lt;/code&gt; — protect &lt;code&gt;main&lt;/code&gt; from the work-in-progress.&lt;/li&gt;
&lt;li&gt;Commit each stable sub-component individually: the classifier function, the category selector, the section generator, then the assembler.&lt;/li&gt;
&lt;li&gt;End-to-end doesn't need to work for each commit — only the piece you just wrote needs to be internally correct.&lt;/li&gt;
&lt;li&gt;Merge to &lt;code&gt;main&lt;/code&gt; only when the full pipeline is tested green.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; A broken commit on a branch isn't broken code — it's a checkpoint. Use branches to make partial commits feel safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Beyond Vibe Coding: Agentic Technical Direction
&lt;/h2&gt;

&lt;p&gt;When you build software entirely through conversational AI, the popular term is "vibe coding" — a process often characterized by low-context requests ("make an app that does X") and the blind acceptance of AI-generated code. This frequently results in unmaintainable architecture because the human doesn't actually understand the system.&lt;br&gt;
This project demonstrated something fundamentally different: &lt;strong&gt;Agentic Technical Direction&lt;/strong&gt;. &lt;br&gt;
In this model, I acted as a Tech Lead or Technical Product Manager, not a typist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Defining Architecture:&lt;/strong&gt; I made the hard structural decisions ("Use Ollama for local inference," "Rank by engagement instead of X's proprietary algorithm," "Implement Map-Reduce to handle context windows").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditing &amp;amp; Verifying:&lt;/strong&gt; I didn't accept the first draft. When presented with failure modes (hallucinations, VRAM spills, API costs) by the agentic AI, I defined the mitigation strategy (e.g., "Implement a &lt;code&gt;--from-summary&lt;/code&gt; skip flag").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforcing Standards:&lt;/strong&gt; I demanded external quality gates (SonarCloud), insisted on 90%+ test coverage, and enforced open-source standards (MIT licenses, &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documenting the &lt;em&gt;Why&lt;/em&gt;:&lt;/strong&gt; I framed the project as a cohesive product with a distinct point of view (Local Privacy, Deep Reading), rather than just a script.
The AI agent wrote the code, but I designed the system. That is the future of software engineering.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  10. Leveling Up: Skills for Agentic Technical Direction
&lt;/h2&gt;

&lt;p&gt;Acting as a Technical Director for an AI agent requires a specific skill set that differs from traditional coding. Based on my workflow in this project, here are the key areas I need to develop for future agentic projects:&lt;/p&gt;

&lt;h3&gt;
  
  
  What to Learn: Git and Version Control
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Challenge:&lt;/strong&gt; The fastest way out of bad AI-generated code is not asking the AI to fix it, but simply reverting to the last known good state. If I don't control the version timeline, the AI controls me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Skill:&lt;/strong&gt; Master the difference between the working directory, staging area, and commits. Learn to ruthlessly use feature branches (&lt;code&gt;git checkout -b new-experiment&lt;/code&gt;) so the main branch stays pristine until I explicitly approve the agent's work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to Improve: The Skill of "Scaffolding"
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Challenge:&lt;/strong&gt; AI models struggle with "big bang" integration where everything is built at once, but they are incredible at filling in the blanks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Skill:&lt;/strong&gt; Practice "Data-First" building. Before asking the agent to write a processing pipeline, manually define exactly what the input data and desired output data look like. Write empty function signatures (stubs) with clear docstrings, and let the AI fill in the logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to Study: Fundamentals of Software Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Challenge:&lt;/strong&gt; Without structural guidance, an AI will take the path of least resistance, resulting in giant, monolithic files holding thousands of lines of spaghetti code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Skill:&lt;/strong&gt; Learn the vocabulary of system design to direct the agent on &lt;em&gt;how&lt;/em&gt; files should interact. Understand "Separation of Concerns" (e.g., splitting fetching, ranking, and rendering) and "Dependency Injection" (which allowed us to easily swap between Gemini and Ollama).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Superpower to Cultivate: The "Stop and Inspect" Reflex
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Strength:&lt;/strong&gt; My best moments in this project occurred when development was paused to inspect failures. When SonarCloud flagged complexity, I had the agent refactor it. When a $4 API cost was observed, I had the agent build a bypass flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Lesson:&lt;/strong&gt; Many developers let AI tools generate code until the system collapses under its own weight. The reflex to inspect output, measure costs, and refactor messes before adding the next feature is exactly what makes a great Technical Director.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  11. Conclusion: Learning by Doing with an AI Partner
&lt;/h2&gt;

&lt;p&gt;This project started as a focused utility and became a hands-on education in local AI orchestration, Python engineering, API management, CI/CD quality gates, and system design.&lt;/p&gt;

&lt;p&gt;The most important meta-lesson: &lt;strong&gt;complex, real work is the best teacher.&lt;/strong&gt; Passively reading about Map-Reduce, VRAM constraints, and OAuth authentication doesn't produce the same understanding as hitting those walls directly and solving through them.&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%2F5ayagszorokn3j2zgkbj.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%2F5ayagszorokn3j2zgkbj.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Working with an agentic AI partner compressed that learning dramatically — not by removing the problems, but by making it possible to actually encounter and solve them faster than you could alone.&lt;/p&gt;

&lt;p&gt;The barriers to running your own local AI pipeline, writing production-quality Python, and shipping to GitHub with automated tests — are lower than they appear. You just need to start.&lt;/p&gt;

&lt;p&gt;And the trend is clear: the models that ran efficiently today on older laptop hardware would have required a high-end server just a few years ago. As quantization and architecture efficiency improve, the "VRAM constraint" will keep relaxing. A 3B model in 2026 already does things a 13B model from 2023 struggled with. The intelligence-per-gigabyte ratio is rising fast — and it will keep rising.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>python</category>
    </item>
    <item>
      <title>Multi-Line Editing in edittrack: Work with Multiple Routes Simultaneously</title>
      <dc:creator>Frédéric Junod</dc:creator>
      <pubDate>Mon, 09 Feb 2026 09:05:10 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/multi-line-editing-in-edittrack-work-with-multiple-routes-simultaneously-5hk2</link>
      <guid>https://dev.to/camptocamp-geo/multi-line-editing-in-edittrack-work-with-multiple-routes-simultaneously-5hk2</guid>
      <description>&lt;p&gt;Planning complex routes often requires more than a single track. Whether you're mapping a multi-day trek, comparing alternative paths, or managing a network of interconnected trails, keeping all your routes in one workspace lets you see the full picture and maintain context while editing each part independently.&lt;/p&gt;

&lt;p&gt;Starting with version 2.0-beta, edittrack introduces multi-line editing—the ability to create, edit, and manage multiple independent tracks within the same TrackManager. This post walks through the feature, its real-world applications, and how to work with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What multi-line editing offers
&lt;/h2&gt;

&lt;p&gt;Multi-line editing lets you work with multiple independent line strings—called "parts"—in a single edittrack workspace. Each part is a complete track with its own control points, segments, POIs, and routing configuration. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create as many parts as you need&lt;/li&gt;
&lt;li&gt;Switch between parts to edit them individually&lt;/li&gt;
&lt;li&gt;See all parts simultaneously for context&lt;/li&gt;
&lt;li&gt;Style active and inactive parts differently for visual clarity&lt;/li&gt;
&lt;li&gt;Delete parts you no longer need&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you draw, move points, or add POIs, only the currently active part is affected. Other parts remain untouched, letting you refine one route without disrupting others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-world use cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Multi-day trips
&lt;/h3&gt;

&lt;p&gt;Break a long journey into daily segments. Each day becomes its own part, so you can edit day 3 without accidentally moving points from day 1. All days remain visible on the map, giving you a sense of the full itinerary while you tweak individual stages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alternative routes
&lt;/h3&gt;

&lt;p&gt;Present options to clients or collaborators by drawing multiple variations side-by-side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scenic route vs. fastest route&lt;/li&gt;
&lt;li&gt;Technical mountain bike trail vs. beginner-friendly path&lt;/li&gt;
&lt;li&gt;Main highway vs. scenic backroads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compare distances, elevation profiles, and surface types across alternatives before committing to one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route networks
&lt;/h3&gt;

&lt;p&gt;Build interconnected trail systems where a main route has optional side trips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A primary hiking trail with viewpoint spurs&lt;/li&gt;
&lt;li&gt;Different starting points converging at a common destination&lt;/li&gt;
&lt;li&gt;Loop routes with shortcut options&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each branch is its own part, making it easy to modify without tangling the entire network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Activity segments
&lt;/h3&gt;

&lt;p&gt;Separate parts based on how you'll travel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Walk to the trailhead (part 0), bike the main trail (part 1), drive home (part 2)&lt;/li&gt;
&lt;li&gt;Approach routes vs. main climbing routes&lt;/li&gt;
&lt;li&gt;Paved access roads vs. off-road trails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each part can use different routing profiles or snapping configurations if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version control and experimentation
&lt;/h3&gt;

&lt;p&gt;Keep your original route as part 0, then create part 1 for an optimized version. Compare them directly on the map. If the experiment doesn't work, simply delete the new part or switch back to the original.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working with multiple parts: the API
&lt;/h2&gt;

&lt;p&gt;If you're familiar with edittrack from the &lt;a href="https://dev.to/camptocamp-geo/draw-and-edit-network-snapped-tracks-in-openlayers-with-edittrack-pbl"&gt;first blog post&lt;/a&gt;, working with multiple parts builds naturally on what you already know. The key difference: instead of managing one track, you now manage multiple parts and switch between them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating new parts
&lt;/h3&gt;

&lt;p&gt;Start with an existing TrackManager instance. Call &lt;code&gt;createNewPart()&lt;/code&gt; to add a new empty part:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;partIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createNewPart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Returns the index (0, 1, 2, etc.) and automatically switches to that part&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new part becomes the active part, and you can immediately start drawing on it. The first part you create is always part 0.&lt;/p&gt;

&lt;h3&gt;
  
  
  Switching between parts
&lt;/h3&gt;

&lt;p&gt;To edit a different part, use &lt;code&gt;workOnPart()&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="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;workOnPart&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="c1"&gt;// Switch to part 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After switching, all drawing, point manipulation, and POI operations affect only that part. To check which part is currently active:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentPart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;activePart&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="s2"&gt;`Editing part &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;currentPart&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Managing parts
&lt;/h3&gt;

&lt;p&gt;Find out how many parts you have:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partsCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete a part you no longer need:&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="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deletePart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Remove part 2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deleting a part removes all its features—control points, segments, and POIs—and renumbers subsequent parts to maintain a sequential index. If you delete the active part, edittrack automatically switches to another available part.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic switching on drag
&lt;/h3&gt;

&lt;p&gt;By default, edittrack lets you grab features from any part and automatically switches to that part when you start dragging. This makes quick edits intuitive: just grab a point from part 2, and edittrack switches context so you can move it.&lt;/p&gt;

&lt;p&gt;To disable this behavior (e.g., to prevent accidental edits to inactive parts):&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="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;switchPartOnDrag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this set to &lt;code&gt;false&lt;/code&gt;, dragging features from inactive parts is blocked entirely, enforcing strict isolation between parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting data from all parts
&lt;/h3&gt;

&lt;p&gt;The familiar methods—&lt;code&gt;getSegments()&lt;/code&gt;, &lt;code&gt;getControlPoints()&lt;/code&gt;, and &lt;code&gt;getPOIs()&lt;/code&gt;—return data only from the currently active part. If you need data from all parts at once, use the "all" variants:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allSegments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAllSegments&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Returns Feature[][]&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allControlPoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAllControlPoints&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allPOIs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAllPOIs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of these returns a nested array: one sub-array per part. For example, &lt;code&gt;allSegments[0]&lt;/code&gt; contains segments from part 0, &lt;code&gt;allSegments[1]&lt;/code&gt; contains segments from part 1, and so on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Iterating through parts
&lt;/h3&gt;

&lt;p&gt;To process each part individually without manually switching back and forth, use the &lt;code&gt;partsGenerator()&lt;/code&gt; method:&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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trackData&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partsGenerator&lt;/span&gt;&lt;span class="p"&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="s2"&gt;`Part &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trackData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; segments`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Process trackData.segments, trackData.controlPoints, trackData.pois&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator automatically restores the original active part when the loop completes, so you don't have to track state manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  History management
&lt;/h3&gt;

&lt;p&gt;Undo and redo now track both features and the active part. When you undo, edittrack restores not just the previous geometry and points, but also which part you were editing:&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;await&lt;/span&gt; &lt;span class="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;undo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trackManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means you can switch parts, make edits, switch again, and undo your way back through the entire sequence—edittrack remembers which part was active at each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visual feedback and user experience
&lt;/h2&gt;

&lt;p&gt;To help users distinguish between parts, edittrack sets an &lt;code&gt;active&lt;/code&gt; property on every feature. This boolean indicates whether the feature belongs to the currently active part. You can use it in your styles to make active parts stand out:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trackLineStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stroke-width&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stroke-color&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;case&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;==&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get&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="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#00883c80&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Semi-transparent green for inactive parts&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#00883cff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;// Opaque green for active parts&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-value&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;concat&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="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get&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="s2"&gt;part&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="c1"&gt;// Display part number as text&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-fill-color&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="s2"&gt;#fff&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, control points can use conditional styling:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controlPointStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;circle-fill-color&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;case&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;==&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get&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="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#0071ec80&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Semi-transparent blue for inactive&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#0071ecff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;// Opaque blue for active&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 &lt;code&gt;part&lt;/code&gt; property stores the numeric index (0, 1, 2, etc.) of each feature's part. Displaying this number as a label on the map helps users keep track of which part is which, especially when working with many parts.&lt;/p&gt;

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

&lt;p&gt;The &lt;a href="https://geoblocks.github.io/edittrack/simple.html" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; now includes three new controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a new line string&lt;/strong&gt;: creates a new empty part&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change active line string&lt;/strong&gt;: cycles through existing parts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete active line string&lt;/strong&gt;: removes the currently active part&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To experiment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Draw a route on part 0 by clicking points on the map&lt;/li&gt;
&lt;li&gt;Click "Add a new line string" to create part 1&lt;/li&gt;
&lt;li&gt;Draw a second route—notice the first route becomes semi-transparent&lt;/li&gt;
&lt;li&gt;Click "Change active line string" to switch back to part 0 and modify it&lt;/li&gt;
&lt;li&gt;Try dragging points from inactive parts to see automatic switching in action&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The demo uses the conditional styling shown above, so you'll see clear visual distinction between active and inactive parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docs: &lt;a href="https://geoblocks.github.io/edittrack/api/" rel="noopener noreferrer"&gt;geoblocks.github.io/edittrack/api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demos: &lt;a href="https://geoblocks.github.io/edittrack/simple.html" rel="noopener noreferrer"&gt;simple&lt;/a&gt;, &lt;a href="https://geoblocks.github.io/edittrack/schm.html" rel="noopener noreferrer"&gt;schm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/geoblocks/edittrack" rel="noopener noreferrer"&gt;github.com/geoblocks/edittrack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/@geoblocks/edittrack" rel="noopener noreferrer"&gt;@geoblocks/edittrack&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  License
&lt;/h2&gt;

&lt;p&gt;BSD-3-Clause&lt;/p&gt;

</description>
      <category>openlayers</category>
      <category>graphhopper</category>
    </item>
    <item>
      <title>[PostGIS/PgRouting] With french routable data</title>
      <dc:creator>jwaddle</dc:creator>
      <pubDate>Tue, 27 Jan 2026 12:10:14 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/postgispgrouting-with-french-routable-data-5dpb</link>
      <guid>https://dev.to/camptocamp-geo/postgispgrouting-with-french-routable-data-5dpb</guid>
      <description>&lt;h1&gt;
  
  
  Road-network isochrones with PostGIS &amp;amp; pgRouting (Docker)
&lt;/h1&gt;

&lt;p&gt;This post documents a complete workflow to compute &lt;strong&gt;road-network isochrones&lt;/strong&gt; using &lt;strong&gt;PostGIS&lt;/strong&gt; and &lt;strong&gt;pgRouting&lt;/strong&gt;, running inside &lt;strong&gt;Docker&lt;/strong&gt;.&lt;br&gt;
It used Data (BDTopo Dataset) from the IGN, France’s national mapping agency.&lt;br&gt;
It simulates the time taken by fire trucks to reach different locations from a set of origin points (fire stations).&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Important disclaimer (read first or don't)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;speed information in &lt;strong&gt;BD TOPO&lt;/strong&gt; is not always correct, and optimistic, work is being done to make it more accurate.&lt;/li&gt;
&lt;li&gt;intersections are &lt;strong&gt;not always modelled correctly&lt;/strong&gt; (see &lt;code&gt;pgr_separateTouching&lt;/code&gt; and/or use &lt;code&gt;pgr_separateCrossing&lt;/code&gt;)

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.pgrouting.org/latest/en/pgRouting-concepts.html#id65" rel="noopener noreferrer"&gt;https://docs.pgrouting.org/latest/en/pgRouting-concepts.html#id65&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;snapping is approximate (For the starting point, we get the closest point on the graph, it can be far away, or not truly be accessible in reality)&lt;/li&gt;
&lt;li&gt;roads are sometimes included entirely even if only partially reachable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result should be understood as a &lt;strong&gt;network reach envelope&lt;/strong&gt;, not a real travel-time model.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. PostgreSQL + pgRouting in Docker
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; pgrouting &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 5432:5432 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gis &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gis &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gis &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/pgdata:/var/lib/postgresql &lt;span class="se"&gt;\&lt;/span&gt;
  pgrouting/pgrouting:18-3.6-4.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;postgis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pgrouting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  2. Preparing the Road Network
&lt;/h2&gt;

&lt;p&gt;Upload road network to the database (gdal, qgis). The data used here was downloaded from the french IGN &lt;a href="https://geoservices.ign.fr/bdtopo" rel="noopener noreferrer"&gt;https://geoservices.ign.fr/bdtopo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ST_Force2D&lt;/code&gt; is used to drop Z/M values if present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ST_Force2D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LineString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2154&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;vitesse_moyenne_vl&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;troncon_de_route&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;geom&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always add an index on the geometry column for performance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;edges_gix&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add required columns for pgRouting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;reverse_cost&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Travel Time (Length × Speed)
&lt;/h2&gt;

&lt;p&gt;Replace NULL speeds with 0 (unusable edges).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;vitesse_moyenne_vl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;vitesse_moyenne_vl&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compute costs based on length and speed (in km/h).&lt;br&gt;
With BD TOPO, I &lt;strong&gt;exaggerated&lt;/strong&gt; the cost by 15% to get more optimistic results.&lt;br&gt;
Nullif is used to avoid division by zero; &lt;code&gt;1000.0 / 3600.0&lt;/code&gt; converts km/h to m/s, I hope&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;cost&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="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ST_Length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vitesse_moyenne_vl&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&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="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;reverse_cost&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="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ST_Length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vitesse_moyenne_vl&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Costs are expressed in &lt;strong&gt;seconds&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Intersections are free (if a node exists ofc), speeds are approximate, results are optimistic.&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%2Frl4lt0ijfhr9anyu6ozt.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%2Frl4lt0ijfhr9anyu6ozt.png" alt=" " width="270" height="187"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Graph Vertices
&lt;/h2&gt;

&lt;p&gt;This is the point layer representing all start/end points of edges.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges_vertices&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgr_extractVertices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;geom&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always add indexes for performance, well not always but when it makes sense.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;edges_vertices_gix&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges_vertices&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;edges_vertices_id&lt;/span&gt;  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges_vertices&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each edge (line), find the start and end point and link to the corresponding vertex id (calculated previously).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges_vertices&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ST_StartPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges_vertices&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ST_EndPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is visible in QGIS.&lt;br&gt;&lt;br&gt;
&lt;code&gt;cost&lt;/code&gt; and &lt;code&gt;reverse_cost&lt;/code&gt; are in seconds.&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%2Fovfy06afbxlhvo17ap0r.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%2Fovfy06afbxlhvo17ap0r.png" alt=" " width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Snapping Origin Points
&lt;/h2&gt;

&lt;p&gt;In the example we use fire stations as origin points (these are not the real locations).&lt;br&gt;
You only need a table with points and an id column.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fire_stations&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;start_vid&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;snap_dist_m&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to snap them to the closest vertex in the road network.&lt;br&gt;
This is an approximation; ideally we would snap to the closest point on the closest edge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fire_stations&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;start_vid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges_vertices&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;snap_dist_m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ST_Distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges_vertices&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we can see, the closest vertex is not very representative of the actual road network location.&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%2Fh4zgvypb2xi27x7icyqf.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%2Fh4zgvypb2xi27x7icyqf.png" alt=" " width="712" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Isochrone Computation
&lt;/h2&gt;

&lt;p&gt;The aim here is to compute polygons representing areas reachable within a set of time thresholds (5, 10, 15, 20 minutes).&lt;/p&gt;

&lt;p&gt;First create the output table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isochrones&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;fire_stations_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;minutes&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;geom&lt;/span&gt; &lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Polygon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2154&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;And here is the complete query to compute the isochrones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isochrones&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;dd20&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agg_cost&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sis&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
     &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="n"&gt;pgr_drivingDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse_cost&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_vid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                              &lt;span class="n"&gt;directed&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_vid&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="c1"&gt;-- and p.id = 7 TEST ONLY&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;bucketed&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dd20&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;reach_pts&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bucketed&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges_vertices&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isochrones&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ST_Buffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ST_ConcaveHull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ST_Collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Polygon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2154&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;geom&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reach_pts&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Quick tip: As polygons are not donuts.&lt;br&gt;&lt;br&gt;
You can still use QGIS to make sure the minutes taken are visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;click on layer properties → Symbology → Categorized&lt;/li&gt;
&lt;li&gt;check &lt;strong&gt;Control feature rendering order&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is what it looks like in QGIS :)&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%2Fmgapdsetcph0avrelk8w.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%2Fmgapdsetcph0avrelk8w.png" alt=" " width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And vs OSM isochrone tool (ORS tools), which handles a lot more (turns, roundabouts, etc.)&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%2Fva7vuun2pt9a1k5s8fxs.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%2Fva7vuun2pt9a1k5s8fxs.png" alt=" " width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For 5 and 10 minutes both sets of data are quite close, but this gets worse the further the distance.&lt;/p&gt;
&lt;h1&gt;
  
  
  Some explanation about the above query
&lt;/h1&gt;

&lt;p&gt;For each point in &lt;code&gt;bdtopo.fire_stations&lt;/code&gt;, compute &lt;strong&gt;travel-time isochrones&lt;/strong&gt; for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5 minutes&lt;/li&gt;
&lt;li&gt;10 minutes&lt;/li&gt;
&lt;li&gt;15 minutes&lt;/li&gt;
&lt;li&gt;20 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using a road network stored in &lt;code&gt;bdtopo.edges&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Costs are expressed in &lt;strong&gt;seconds&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  SQL Workflow
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- TRUNCATE bdtopo.isochrones;&lt;/span&gt;

&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;dd20&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agg_cost&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sis&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
     &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="n"&gt;pgr_drivingDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse_cost&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_vid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;directed&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_vid&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;bucketed&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;CASE&lt;/span&gt;
            &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
            &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
            &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
            &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dd20&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;reach_pts&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bucketed&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges_vertices&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isochrones&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ST_Buffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ST_ConcaveHull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ST_Collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Polygon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2154&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;geom&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;reach_pts&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step-by-Step Explanation
&lt;/h3&gt;
&lt;h4&gt;
  
  
  1. Compute reachable vertices within 20 minutes (&lt;code&gt;dd20&lt;/code&gt;)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pgr_drivingDistance&lt;/code&gt; is executed once per SIS point&lt;/li&gt;
&lt;li&gt;Maximum distance: &lt;strong&gt;20 minutes (1200 seconds)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Output nodes are filtered to &lt;code&gt;agg_cost &amp;lt;= 20*60&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(fire_station_id, node_id, travel_time_seconds)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Assign vertices to time bands (&lt;code&gt;bucketed&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Vertices are classified into &lt;strong&gt;exclusive time ranges&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Travel time&lt;/th&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0–4:59&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5–9:59&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10–14:59&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15–20:00&lt;/td&gt;
&lt;td&gt;20 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  3. Convert vertices to points (&lt;code&gt;reach_pts&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;Each reachable node is joined to &lt;code&gt;bdtopo.edges_vertices&lt;/code&gt;&lt;br&gt;
to retrieve its POINT geometry.&lt;/p&gt;
&lt;h4&gt;
  
  
  4. Build polygons from points
&lt;/h4&gt;

&lt;p&gt;For each &lt;code&gt;(fire_station_id, minutes)&lt;/code&gt; group:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Points are collected&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;tight concave hull&lt;/strong&gt; is computed using alpha &lt;code&gt;0.10&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A small buffer (5 m) smooths the geometry&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Interpretation
&lt;/h2&gt;

&lt;p&gt;These polygons represent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Approximate envelopes of reachable network vertices&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They are suitable for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exploratory analysis&lt;/li&gt;
&lt;li&gt;accessibility screening&lt;/li&gt;
&lt;li&gt;visual communication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are &lt;strong&gt;not&lt;/strong&gt; precise travel-time surfaces.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tip
&lt;/h2&gt;

&lt;p&gt;If you want &lt;strong&gt;cumulative isochrones&lt;/strong&gt; (0–5, 0–10, 0–15, 0–20),&lt;br&gt;
And make sur the smaller isochrones are drawn on top of the larger ones.&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%2F2u4pgtlxb1vpvrgcquji.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%2F2u4pgtlxb1vpvrgcquji.png" alt=" " width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  7. Visualization with points
&lt;/h1&gt;

&lt;p&gt;Here is a small snippet to extract isochrone points for visualization,&lt;br&gt;
and see for each point which category (5, 10, 15, 20 minutes) it belongs to.&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%2Fyt3hiiulrxxi715unwas.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%2Fyt3hiiulrxxi715unwas.png" alt=" " width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isochrone_points&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isochrone_points&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;fire_station_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;minutes&lt;/span&gt;  &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;vid&lt;/span&gt;      &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;geom&lt;/span&gt;     &lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2154&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;isochrone_points_gix&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isochrone_points&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;isochrone_points_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isochrone_points&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isochrone_points&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agg_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;dd20&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agg_cost&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sis&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="n"&gt;pgr_drivingDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse_cost&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_vid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;directed&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_vid&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="c1"&gt;-- and p.id = 7 TEST ONLY&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;bucketed&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;agg_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;  &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;  &lt;span class="mi"&gt;5&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
      &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;agg_cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
      &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dd20&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fire_station_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;vid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agg_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bucketed&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;bdtopo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edges_vertices&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can visualize the result in QGIS.&lt;br&gt;
This helps you understand what can be wrong/explained with/by the network data.&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%2F3tmxn2f2apv4yvf56qdm.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%2F3tmxn2f2apv4yvf56qdm.png" alt=" " width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;These isochrones are &lt;strong&gt;approximations&lt;/strong&gt;.&lt;br&gt;
They are useful for exploration, not for precision routing—unless you have a very high-quality road network. :)&lt;/p&gt;

&lt;p&gt;I did not work on the base data itself, it would need to be preprocessed for better results (e.g. filtering bike lanes, stairs, etc.).&lt;/p&gt;

&lt;p&gt;In order to be used for real routing / isochrone computation, we need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;improve snapping to edges (closest point, not vertex)&lt;/li&gt;
&lt;li&gt;split the edges at intersections (&lt;code&gt;pgr_separateTouching&lt;/code&gt; / &lt;code&gt;pgr_separateCrossing&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;handle turn restrictions&lt;/li&gt;
&lt;li&gt;split the road into smaller segments to have better speed representation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All can be achieved with pgRouting/PostGIS functions, a good dataset and time.&lt;/p&gt;

&lt;h1&gt;
  
  
  links
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://docs.pgrouting.org/3.8/en" rel="noopener noreferrer"&gt;https://docs.pgrouting.org/3.8/en&lt;/a&gt;&lt;br&gt;
&lt;a href="https://qgis.org/" rel="noopener noreferrer"&gt;https://qgis.org/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>qgis</category>
      <category>pgrouting</category>
      <category>isochrones</category>
    </item>
    <item>
      <title>GreHack 2025</title>
      <dc:creator>Emilien Devos</dc:creator>
      <pubDate>Mon, 01 Dec 2025 14:37:09 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/grehack-2025-je0</link>
      <guid>https://dev.to/camptocamp-geo/grehack-2025-je0</guid>
      <description>&lt;h2&gt;
  
  
  Conference presentation
&lt;/h2&gt;

&lt;p&gt;GreHack is a conference on IT security held in Grenoble. The first day is dedicated to talks in English by various speakers, as well as workshops at the end of the day. The second day is specifically devoted to CTF, which brings together several hundred people.&lt;/p&gt;

&lt;p&gt;This year, the following topics were presented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network security / Hardware security&lt;/li&gt;
&lt;li&gt;Enterprise application security&lt;/li&gt;
&lt;li&gt;Physical security / Red team&lt;/li&gt;
&lt;li&gt;Reverse engineering / Program analysis&lt;/li&gt;
&lt;li&gt;IoT security&lt;/li&gt;
&lt;li&gt;Web security / Browsers&lt;/li&gt;
&lt;li&gt;Protocol security / Databases / Authentication&lt;/li&gt;
&lt;li&gt;DevOps security / CI-CD / Supply chain&lt;/li&gt;
&lt;li&gt;Advanced network analysis / Network forensics&lt;/li&gt;
&lt;li&gt;Radio security / SDR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of the talks are available for replay here: &lt;a href="https://www.youtube.com/live/X-ZJH4d2tuE" rel="noopener noreferrer"&gt;https://www.youtube.com/live/X-ZJH4d2tuE&lt;/a&gt; and the talk schedule is here: &lt;a href="https://grehack.fr/program/" rel="noopener noreferrer"&gt;https://grehack.fr/program/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Interesting presentations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One does not simply walk into a building... or do they?
&lt;/h3&gt;

&lt;p&gt;PDF of the presentation: &lt;a href="https://blog.volkercarstein.com/grehack_2025_one_does_not_simply_walk_into_a_building.pdf" rel="noopener noreferrer"&gt;https://blog.volkercarstein.com/grehack_2025_one_does_not_simply_walk_into_a_building.pdf&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The presenter recounts a week spent physically infiltrating a company's premises without being caught or seen. The aim was to test the physical security of the company's premises.&lt;/p&gt;

&lt;p&gt;This is quite interesting, as it highlights the fact that physical security depends heavily on the conditioning of employees, who are often the weakest link.&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%2Fjhhled8yuu35cbnhfyd0.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%2Fjhhled8yuu35cbnhfyd0.png" alt=" " width="460" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Exploring Browser Permissions and Exploiting Permission Hijacking
&lt;/h3&gt;

&lt;p&gt;PDF of the presentation: &lt;a href="https://albertofdr.github.io/web-security-class/browser/browser.permissions" rel="noopener noreferrer"&gt;https://albertofdr.github.io/web-security-class/browser/browser.permissions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Presentation on managing permissions (e.g. camera) in the browser, which also apply to malicious components that may be found on the site, such as an iframe. This could be the case if the iframe was injected following a website hack or a poorly configured website.&lt;/p&gt;

&lt;p&gt;The presentation highlights the importance of defining HTTP headers in order to more finely manage the browser permissions authorised on a website. Similar to a Content Security Policy (CSP) but for browser permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  From YAML to Root: CI/CD Pipeline Attacks and Countermeasures
&lt;/h3&gt;

&lt;p&gt;Video of the presentation: &lt;a href="https://www.youtube.com/watch?v=YUbN6MuiuFM" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=YUbN6MuiuFM&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The presenter explains the potential exploitation and recovery of secrets via compromised CI/CD. With different types of access, how it is possible to exfiltrate secrets each time. Focused mainly on Azure DevOps but also applies to GitHub actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workshops and CTF
&lt;/h2&gt;

&lt;p&gt;I was unable to participate in the workshops or the CTF because I bought my tickets at the last minute.&lt;/p&gt;

&lt;p&gt;There were 12 workshops where participants had to solve exercises while being guided by the organiser. The exercises ranged from application exploitation to hardware.&lt;/p&gt;

&lt;p&gt;List here: &lt;a href="https://grehack.fr/workshops/" rel="noopener noreferrer"&gt;https://grehack.fr/workshops/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The CTF took place in a large room at ENSIMAG, where participants formed groups of up to eight people. The groups had to solve challenges in the following categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cryptography&lt;/li&gt;
&lt;li&gt;Reverse Engineering&lt;/li&gt;
&lt;li&gt;Exploit&lt;/li&gt;
&lt;li&gt;Web&lt;/li&gt;
&lt;li&gt;Forensics&lt;/li&gt;
&lt;li&gt;Hardware&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mood
&lt;/h2&gt;

&lt;p&gt;The atmosphere was very student-oriented, and it was nice to discuss security issues with students and young workers.&lt;/p&gt;

&lt;p&gt;Many French companies were present, including Synacktiv (a branch in Lyon), Orange, DGSE, EDF and many others: &lt;a href="https://grehack.fr/sponsors/" rel="noopener noreferrer"&gt;https://grehack.fr/sponsors/&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I was personally very pleased to have participated in this conference.&lt;/p&gt;

&lt;p&gt;Although many of the talks did not apply to Camptocamp's field of activity, some of them still provided me with additional knowledge that I can apply in my work (see the list of presentations above).&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;p&gt;At the end of the day, participants in the audience can give short presentations, in the style of ‘Lightning talks’. It was almost exclusively students, and the atmosphere was rather light-hearted, with everyone laughing about the intrusions they had each managed to pull off.&lt;/p&gt;

&lt;p&gt;These talks are called ‘Rump session’.&lt;/p&gt;

&lt;p&gt;The one that made me laugh the most was a student who began his presentation with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I need to eat and I don't have any money. So it's either work at Burger King or hack Burger King.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He then explained how he abused the Burger King app's coupon system to get free burgers (in very large quantities).&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%2F53booykcdmhux1wxes88.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%2F53booykcdmhux1wxes88.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>grehack</category>
      <category>security</category>
      <category>grenoble</category>
      <category>techtalks</category>
    </item>
    <item>
      <title>Securing AI in Software Development: the Lethal Trifecta</title>
      <dc:creator>Emmanuel Belo</dc:creator>
      <pubDate>Wed, 05 Nov 2025 09:46:05 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/securing-ai-in-software-development-the-lethal-trifecta-3m07</link>
      <guid>https://dev.to/camptocamp-geo/securing-ai-in-software-development-the-lethal-trifecta-3m07</guid>
      <description>&lt;p&gt;AI assistants are transforming how software is developed. They help automate tasks, accelerate coding, and improve quality, but they also introduce new security challenges. Understanding and managing these risks is essential to ensure AI systems operate safely. Secure AI development involves identifying where vulnerabilities arise, limiting exposure to sensitive data, and ensuring AI tools cannot be misused or manipulated.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Lethal Trifecta?
&lt;/h3&gt;

&lt;p&gt;A concept developed by &lt;a href="https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/" rel="noopener noreferrer"&gt;Simon Willison&lt;/a&gt; that identifies three conditions that make AI systems particularly risky when they occur together. When all three conditions are present, an AI system becomes significantly more dangerous.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Three Conditions
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exposure to untrusted content&lt;/strong&gt;: Including hidden malicious instructions or incorrect information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access to private data&lt;/strong&gt;: Including secret keys, tokens, passwords, and proprietary information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ability to externally communicate&lt;/strong&gt;: The capability to send data outside a closed system.&lt;/li&gt;
&lt;/ol&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%2Fsuuhs4v3ys66bxfwq5gp.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%2Fsuuhs4v3ys66bxfwq5gp.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;br&gt;
(c) &lt;a href="https://trainingportal.linuxfoundation.org/courses/secure-aiml-driven-software-development-lfel1012" rel="noopener noreferrer"&gt;https://trainingportal.linuxfoundation.org/courses/secure-aiml-driven-software-development-lfel1012&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Risk
&lt;/h3&gt;

&lt;p&gt;When all three conditions exist simultaneously, an AI assistant can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Receive malicious commands from untrusted content&lt;/li&gt;
&lt;li&gt;Extract or use private data&lt;/li&gt;
&lt;li&gt;Send that data elsewhere or use it to attack systems&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Risk Mitigation Strategies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The primary recommendation is to remove at least one of the three conditions.&lt;/li&gt;
&lt;li&gt;If removal isn't possible, constrain what you can control.&lt;/li&gt;
&lt;li&gt;Implement additional risk reduction measures when the trifecta cannot be fully broken.&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Draw and edit network‑snapped tracks in OpenLayers with edittrack</title>
      <dc:creator>Frédéric Junod</dc:creator>
      <pubDate>Fri, 17 Oct 2025 12:11:29 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/draw-and-edit-network-snapped-tracks-in-openlayers-with-edittrack-pbl</link>
      <guid>https://dev.to/camptocamp-geo/draw-and-edit-network-snapped-tracks-in-openlayers-with-edittrack-pbl</guid>
      <description>&lt;p&gt;Planning a hike, a bike ride, or any route that should follow a real‑world network? edittrack lets users draw and edit tracks that snap to routing services , enrich them with elevation profiles, and manage POIs—all directly on an OpenLayers map.&lt;/p&gt;

&lt;p&gt;This post gives you a quick tour, shows how to wire it up, and highlights tips from the built‑in demos.&lt;/p&gt;

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

&lt;p&gt;edittrack is a lightweight UI library focused on interactive track editing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snapped segments: draw lines that follow a network via routers (GraphHopper or OSRM)&lt;/li&gt;
&lt;li&gt;Control points: add/move/delete points that define and modify segments&lt;/li&gt;
&lt;li&gt;POIs: add metadata‑bearing points anywhere along your track&lt;/li&gt;
&lt;li&gt;Elevation profiles: compute per‑segment XYZM profiles via profilers (Swisstopo, extract from geometry, or a fallback chain)&lt;/li&gt;
&lt;li&gt;History: undo/redo across all edits&lt;/li&gt;
&lt;li&gt;Densification: optional point insertion to improve geometry quality between router samples&lt;/li&gt;
&lt;li&gt;Shadow track and mask: visualize original track state while editing and constrain drawing to an extent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core class you’ll interact with is &lt;code&gt;TrackManager&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API docs: &lt;a href="https://geoblocks.github.io/edittrack/api/" rel="noopener noreferrer"&gt;geoblocks.github.io/edittrack/api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live demos: &lt;a href="https://geoblocks.github.io/edittrack/simple.html" rel="noopener noreferrer"&gt;simple&lt;/a&gt; and &lt;a href="https://geoblocks.github.io/edittrack/schm.html" rel="noopener noreferrer"&gt;schm&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @geoblocks/edittrack ol
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Edittrack is ESM and ships TypeScript types&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ol&lt;/code&gt; (OpenLayers) is a peer dependency&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quickstart
&lt;/h2&gt;

&lt;p&gt;Below is a minimal setup using OSRM for routing and a profiler chain that tries to reuse segment Z values first, then falls back to Swisstopo for high‑quality elevation (if you work in Switzerland and have EPSG:2056 registered).&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="k"&gt;import&lt;/span&gt; &lt;span class="nb"&gt;Map&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;ol/Map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;View&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;ol/View&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;TileLayer&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;ol/layer/Tile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OSM&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;ol/source/OSM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;VectorLayer&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;ol/layer/Vector&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;VectorSource&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;ol/source/Vector&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Stroke&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Circle&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CircleStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Fill&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;ol/style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;TrackManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;OSRMRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ExtractFromSegmentProfiler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;FallbackProfiler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SwisstopoProfiler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SnappedDensifier&lt;/span&gt;&lt;span class="p"&gt;,&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;@geoblocks/edittrack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Basic map and layers&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trackLayer&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;VectorLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VectorSource&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shadowTrackLayer&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;VectorLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VectorSource&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;map&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;Map&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="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;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;zoom&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="na"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TileLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OSM&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;shadowTrackLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;trackLayer&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Router (provide your own OSRM/GraphHopper URL)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&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;OSRMRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://router.project-osrm.org/route/v1/driving&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// optional:&lt;/span&gt;
  &lt;span class="c1"&gt;// extraParams: 'annotations=true',&lt;/span&gt;
  &lt;span class="c1"&gt;// radius: 10000,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Profiler chain (first try existing Z, then Swisstopo)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profiler&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;FallbackProfiler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;profilers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExtractFromSegmentProfiler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;projection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getView&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getProjection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SwisstopoProfiler&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;projection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getView&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getProjection&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Optional densifier to insert points between router samples&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;densifier&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;SnappedDensifier&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;optimalPointDistance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// meters&lt;/span&gt;
  &lt;span class="na"&gt;maxPointDistance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Style (use your own StyleLike/FlatStyleLike)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Style&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stroke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#2563eb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Style&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CircleStyle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Fill&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#111827&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="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Track manager&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tm&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;TrackManager&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;trackLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;shadowTrackLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;profiler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;densifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hitTolerance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// px&lt;/span&gt;
  &lt;span class="c1"&gt;// deleteCondition, addLastPointCondition, addControlPointCondition are optional&lt;/span&gt;
  &lt;span class="c1"&gt;// drawExtent, drawMaskColor are optional&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Start editing&lt;/span&gt;
&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;edit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Listen to changes (update a profile or UI)&lt;/span&gt;
&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addTrackChangeEventListener&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="c1"&gt;// e.g., recompute a merged elevation profile using tm.getSegments()&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Add an initial point by letting users click on the map (TrackInteraction handles it)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Working with the track
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Add/move points: in edit mode, click to add control points; drag points or segments to update routing&lt;/li&gt;
&lt;li&gt;Toggle snapping: &lt;code&gt;tm.snapping = true | false&lt;/code&gt; (if false, segments become straight lines)&lt;/li&gt;
&lt;li&gt;Undo/Redo: &lt;code&gt;await tm.undo()&lt;/code&gt; / &lt;code&gt;await tm.redo()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reverse: &lt;code&gt;await tm.reverse(true)&lt;/code&gt; to re‑route, or &lt;code&gt;await tm.reverse(false)&lt;/code&gt; to just flip geometry and refresh profiles&lt;/li&gt;
&lt;li&gt;POIs:

&lt;ul&gt;
&lt;li&gt;Add: &lt;code&gt;tm.addPOI([x, y], {name: 'Café'})&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Update meta: &lt;code&gt;tm.updatePOIMeta(index, meta)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Delete: &lt;code&gt;tm.deletePOI(index)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Save/restore state:
&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getControlPoints&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSegments&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPOIs&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restoreFeatures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Hover feedback: subscribe and map distance to your UI (e.g., highlight point on a chart)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="nx"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addTrackHoverEventListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;distanceFromStart&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="c1"&gt;// distanceFromStart in meters along the full track (if available)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Constraining drawing and visual aids
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Draw extent and mask: pass &lt;code&gt;drawExtent&lt;/code&gt; and optionally &lt;code&gt;drawMaskColor&lt;/code&gt; to restrict editing to a region and render a mask overlay&lt;/li&gt;
&lt;li&gt;Shadow track: when entering edit mode, the current track is cloned into &lt;code&gt;shadowTrackLayer&lt;/code&gt; so you can see what changed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Routers and profilers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Routers:

&lt;ul&gt;
&lt;li&gt;GraphHopper (&lt;code&gt;GraphHopperRouter&lt;/code&gt;): snaps segments to a GraphHopper backend&lt;/li&gt;
&lt;li&gt;OSRM (&lt;code&gt;OSRMRouter&lt;/code&gt;): snaps via OSRM; supports &lt;code&gt;radius&lt;/code&gt;, &lt;code&gt;extraParams&lt;/code&gt;, and pixel‑based &lt;code&gt;maxRoutingTolerance&lt;/code&gt; inherited from &lt;code&gt;RouterBase&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Profilers:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SwisstopoProfiler&lt;/code&gt;: high‑quality elevation profile for CH (register EPSG:2056)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExtractFromSegmentProfiler&lt;/code&gt;: reuse geometry Z values if present&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FallbackProfiler&lt;/code&gt;: try multiple profilers in order and use the first that succeeds&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Each routed segment stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;segment.get('snapped')&lt;/code&gt;: boolean&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;segment.get('profile')&lt;/code&gt;: &lt;code&gt;Coordinate[]&lt;/code&gt; with [x, y, z, m], where m is cumulative distance from the segment start&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;segment.get('surfaces')&lt;/code&gt;: optional array of surface ranges (when supported by the router)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tips from the demos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Densification improves elevation smoothness and downstream analytics by adding intermediate points while keeping a hard cap (&lt;code&gt;maxPoints&lt;/code&gt;) to avoid oversized geometries&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;hitTolerance&lt;/code&gt; between 10–20 px for easier selection on touch devices&lt;/li&gt;
&lt;li&gt;If you want to require a modifier key to delete, pass a &lt;code&gt;deleteCondition&lt;/code&gt; that checks the event (see &lt;code&gt;demos/simple/demo.js&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TypeScript and modules
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Full type definitions are published under &lt;code&gt;lib/types&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Package is ESM‑only; use modern bundlers (Vite, Parcel, Webpack 5+, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docs: &lt;a href="https://geoblocks.github.io/edittrack/api/" rel="noopener noreferrer"&gt;geoblocks.github.io/edittrack/api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demos: &lt;a href="https://geoblocks.github.io/edittrack/simple.html" rel="noopener noreferrer"&gt;simple&lt;/a&gt;, &lt;a href="https://geoblocks.github.io/edittrack/schm.html" rel="noopener noreferrer"&gt;schm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/geoblocks/edittrack" rel="noopener noreferrer"&gt;github.com/geoblocks/edittrack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/@geoblocks/edittrack" rel="noopener noreferrer"&gt;@geoblocks/edittrack&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  License
&lt;/h2&gt;

&lt;p&gt;BSD‑3‑Clause&lt;/p&gt;

</description>
      <category>openlayers</category>
      <category>graphhopper</category>
    </item>
    <item>
      <title>Postgres psql on Windows: SSL SYSCALL error: EOF detected expected authentication request from server, but received H</title>
      <dc:creator>Oliver Christen</dc:creator>
      <pubDate>Wed, 30 Jul 2025 13:24:33 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/postgres-psql-on-windows-sl-syscall-error-eof-detected-expected-authentication-request-from-3b4p</link>
      <guid>https://dev.to/camptocamp-geo/postgres-psql-on-windows-sl-syscall-error-eof-detected-expected-authentication-request-from-3b4p</guid>
      <description>&lt;p&gt;Some recent (as of july 2025) Windows update has apparently broken old version of Postgres psql binaries, when trying to connect to an external database with SSL enabled, you may get the following kind of error:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;pg_restore: error: connection to database "xxx" failed: SSL SYSCALL error: EOF detected &lt;br&gt;
expected authentication request from server, but received H"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The solution is "simply" to use the latest version of the psql binary.&lt;br&gt;
Version 17 has been tested ok.&lt;/p&gt;

&lt;p&gt;This of course also apply to pg_dump/pg_restore.&lt;/p&gt;

</description>
      <category>postgressql</category>
      <category>windows</category>
      <category>psql</category>
    </item>
    <item>
      <title>Querying Overture Maps GeoParquet Directly in the Browser with DuckDB WASM</title>
      <dc:creator>Florent Gravin</dc:creator>
      <pubDate>Fri, 16 May 2025 12:32:04 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/querying-overture-maps-geoparquet-directly-in-the-browser-with-duckdb-wasm-4jn4</link>
      <guid>https://dev.to/camptocamp-geo/querying-overture-maps-geoparquet-directly-in-the-browser-with-duckdb-wasm-4jn4</guid>
      <description>&lt;p&gt;&lt;a href="https://overturemaps.org/" rel="noopener noreferrer"&gt;Overture Maps&lt;/a&gt; provides a rich, open and collaborative dataset of geospatial features that’s designed to power everything from routing to visualization. These datasets are distributed in &lt;a href="https://geoparquet.org/" rel="noopener noreferrer"&gt;GeoParquet&lt;/a&gt; format : &lt;strong&gt;cloud-native&lt;/strong&gt;, efficient, columnar, and increasingly becoming a standard for geospatial data at scale.&lt;/p&gt;

&lt;p&gt;But what if you could explore and query these massive datasets directly in your browser - &lt;strong&gt;without any server-side processing or backend setup&lt;/strong&gt;? Thanks to &lt;a href="https://duckdb.org/docs/stable/clients/wasm/overview.html" rel="noopener noreferrer"&gt;DuckDB-WASM&lt;/a&gt;, you can.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://fgravin.github.io/overture-duckdb-wasm/" rel="noopener noreferrer"&gt;Demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fgravin/overture-duckdb-wasm" rel="noopener noreferrer"&gt;Code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, we’ll walk through how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load an Overture Maps GeoParquet file into the browser&lt;/li&gt;
&lt;li&gt;Query it using SQL, right from the front-end&lt;/li&gt;
&lt;li&gt;Visualize features on a map&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s dive in. 🦆&lt;/p&gt;




&lt;h2&gt;
  
  
  Tools and Technologies
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DuckDB-WASM&lt;/strong&gt;: A full DuckDB engine compiled to WebAssembly, enabling in-browser analytics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GeoParquet&lt;/strong&gt;: Parquet format with additional metadata to support geometry and spatial operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overture Maps&lt;/strong&gt;: A collaborative open map dataset from companies like Meta, Microsoft, Amazon, and TomTom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MapLibre GL JS&lt;/strong&gt;: For rendering geospatial features.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Load DuckDB WASM
&lt;/h2&gt;

&lt;p&gt;Include DuckDB-WASM in your HTML or install it via npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @duckdb/duckdb-wasm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And initialize it:&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="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;duckdb&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;@duckdb/duckdb-wasm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;duckdb_wasm&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;@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mvp_worker&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;@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;duckdb_wasm_next&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;@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;eh_worker&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;@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MANUAL_BUNDLES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;mvp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;mainModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;duckdb_wasm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mainWorker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mvp_worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;eh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;mainModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;duckdb_wasm_next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mainWorker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eh_worker&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectBundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MANUAL_BUNDLES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&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;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mainWorker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncDuckDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instantiate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mainModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pthreadWorker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Fetch and Load the GeoParquet File
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt; : As DuckDB WASM &lt;a href="https://duckdb.org/docs/stable/clients/wasm/extensions.html#httpfs" rel="noopener noreferrer"&gt;does not support HTTPFS extension&lt;/a&gt; you can't directly hint the official Overture Maps GeoParquet releases, which provide root level S3 file system entry.&lt;br&gt;
You should extract a subset of the datasets and host them in your web application, you could use the &lt;a href="https://docs.overturemaps.org/getting-data/overturemaps-py/" rel="noopener noreferrer"&gt;Python CLI&lt;/a&gt; for that.&lt;/p&gt;

&lt;p&gt;Then, load the spatial extension to process the GeoParquet, and init your DuckDB connection.&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="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSTALL spatial;LOAD spatial;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSTALL h3 FROM community;LOAD h3;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Query Your Data
&lt;/h2&gt;

&lt;p&gt;Run SQL queries directly in the browser:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
WITH areas AS (
  SELECT names.primary as name,
         geometry as area_geom
  FROM read_parquet('https://your.parquet.path/division_area.parquet', filename=true, hive_partitioning=1) 
  WHERE subtype = 'locality'
    AND region = 'US-UT'
),
schools AS (SELECT geometry as place_geom
  FROM read_parquet('https://your.parquet.path/places.parquet', filename=true, hive_partitioning=1) 
  WHERE categories.main = 'hotel'
)
SELECT name, ST_AsGeoJSON(area_geom) as geometry,
       CAST(count(place_geom) as INT) as count
FROM areas
       LEFT JOIN schools ON ST_Contains(area_geom, place_geom)
GROUP BY area_geom, name
`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query fetches Overture division areas of Utah, and performs a spatial aggregation, it counts every hotel within each locality.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Visualize on a Map
&lt;/h2&gt;

&lt;p&gt;Convert geometries to GeoJSON and render them:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FeatureCollection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DuckResponseObject&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Feature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Polygon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;properties&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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;places-area-sources&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;geojson&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;places-area&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;places-area-sources&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;paint&lt;/span&gt;&lt;span class="p"&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;fill-color&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;interpolate&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linear&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&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;count&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;colorGradient&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fill-outline-color&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;grey&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;fill-opacity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&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;Where &lt;code&gt;colorGradient&lt;/code&gt; is a computed classification depending on count distribution.&lt;/p&gt;

&lt;p&gt;Et voila ! You have a beautiful choropleth map, from Overture Maps datasets, with no backend implied 👏&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%2Fvlcezwowkym3ddzukqni.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%2Fvlcezwowkym3ddzukqni.png" alt="Hotel distribution per county in Utah, from Overture Maps division areas and places datasets" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>geoparquet</category>
      <category>duckdb</category>
      <category>cloudnative</category>
      <category>overturemaps</category>
    </item>
    <item>
      <title>Cesium Guess</title>
      <dc:creator>Frédéric Junod</dc:creator>
      <pubDate>Mon, 28 Apr 2025 11:39:07 +0000</pubDate>
      <link>https://dev.to/camptocamp-geo/cesium-guess-334l</link>
      <guid>https://dev.to/camptocamp-geo/cesium-guess-334l</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Welcome to &lt;strong&gt;Cesium Guess&lt;/strong&gt;, an interactive web application that challenges you to guess locations on a map. This project leverages powerful mapping technologies to provide an engaging and educational experience. In this blog post, we'll explore what Cesium Guess does, the technologies it uses, and how you can get started with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Cesium Guess?
&lt;/h2&gt;

&lt;p&gt;Cesium Guess is a web-based game where players are presented with a random location in Switzerland and must guess where it is on the map. The game provides a fun way to test and improve your geographical knowledge of Switzerland. After making a guess, the game shows the actual location and the distance between your guess and the correct location.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Random Location Generation&lt;/strong&gt;: The game uses a random position generator to place the camera at a random location within Switzerland.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive Map&lt;/strong&gt;: Players can interact with the map to make their guesses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distance Calculation&lt;/strong&gt;: After guessing, the game calculates and displays the distance between the guessed location and the actual location.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive Design&lt;/strong&gt;: The game is designed to work seamlessly on various devices, including desktops and mobile devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technologies Used
&lt;/h2&gt;

&lt;p&gt;Cesium Guess is built using a combination of modern web technologies and powerful mapping libraries. Here are the key technologies used in this project:&lt;/p&gt;

&lt;h3&gt;
  
  
  CesiumJS
&lt;/h3&gt;

&lt;p&gt;CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin. It provides the core functionality for rendering the 3D globe and handling interactions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CesiumWidget&lt;/strong&gt;: The main component for rendering the 3D globe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ImageryLayer&lt;/strong&gt;: Used to display map imagery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CesiumTerrainProvider&lt;/strong&gt;: Provides terrain data for the globe.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  OpenLayers
&lt;/h3&gt;

&lt;p&gt;OpenLayers is a high-performance, feature-packed library for creating interactive maps. It is used in this project to handle the 2D map interactions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Map&lt;/strong&gt;: The main component for rendering the 2D map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TileLayer&lt;/strong&gt;: Used to display map tiles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VectorLayer&lt;/strong&gt;: Used to display vector data, such as points and lines.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Turf.js
&lt;/h3&gt;

&lt;p&gt;Turf.js is a JavaScript library for spatial analysis. It is used in this project to generate random positions and check if they are within Switzerland's boundaries.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;randomPosition&lt;/strong&gt;: Generates random geographical positions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;booleanPointInPolygon&lt;/strong&gt;: Checks if a point is within a polygon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bbox&lt;/strong&gt;: Calculates the bounding box of a polygon.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Vite
&lt;/h3&gt;

&lt;p&gt;Vite is a build tool that provides a fast development environment and optimized build process. It is used to bundle and serve the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web Components
&lt;/h3&gt;

&lt;p&gt;The project uses Web Components for creating reusable UI elements, such as the compass bar and dialog boxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;To get started with Cesium Guess, follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clone the Repository&lt;/strong&gt;: Clone the project repository from GitHub.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git clone https://github.com/fredj/cesium-guess.git
   &lt;span class="nb"&gt;cd &lt;/span&gt;cesium-guess
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install Dependencies&lt;/strong&gt;: Install the required dependencies using npm.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the Development Server&lt;/strong&gt;: Start the development server using Vite.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   npm run start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open the Application&lt;/strong&gt;: Open your web browser and navigate to &lt;code&gt;http://localhost:3000&lt;/code&gt; to start playing the game.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Cesium Guess is a fun and educational project that combines modern web technologies with powerful mapping libraries. Whether you're a geography enthusiast or just looking for a fun way to pass the time, Cesium Guess offers an engaging experience. We hope you enjoy playing the game and exploring the beautiful landscapes of Switzerland!&lt;/p&gt;

&lt;p&gt;For more information and to try the online demo, visit the &lt;a href="https://fredj.github.io/cesium-guess/" rel="noopener noreferrer"&gt;Cesium Guess GitHub page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy guessing!&lt;/p&gt;

</description>
      <category>cesium</category>
      <category>geoguessr</category>
      <category>3d</category>
    </item>
  </channel>
</rss>
