<?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: Frédéric Junod</title>
    <description>The latest articles on DEV Community by Frédéric Junod (@fredjunod).</description>
    <link>https://dev.to/fredjunod</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%2Fuser%2Fprofile_image%2F3003003%2Fd95bd105-b2f5-42a0-ad12-37a3413fef91.jpg</url>
      <title>DEV Community: Frédéric Junod</title>
      <link>https://dev.to/fredjunod</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fredjunod"/>
    <language>en</language>
    <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>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>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>
