<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://fastgis.eu/guide/blog</id>
    <title>FastGIS Blog</title>
    <updated>2026-09-03T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://fastgis.eu/guide/blog"/>
    <subtitle>FastGIS Blog</subtitle>
    <icon>https://fastgis.eu/guide/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[Talking to the bike]]></title>
        <id>https://fastgis.eu/guide/blog/talking-to-the-bike</id>
        <link href="https://fastgis.eu/guide/blog/talking-to-the-bike"/>
        <updated>2026-09-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[This was the week the app stopped inferring what the motorcycle was doing and]]></summary>
        <content type="html"><![CDATA[<p>This was the week the app stopped inferring what the motorcycle was doing and
started asking it. A Bluetooth OBD-II adapter, two garage sessions, one
surprise hiding in monitor mode — and by the end, live engine data on the
phone and a gear indicator nobody sold us.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-three-day-arc">The three-day arc<a href="https://fastgis.eu/guide/blog/talking-to-the-bike#the-three-day-arc" class="hash-link" aria-label="Direct link to The three-day arc" title="Direct link to The three-day arc" translate="no">​</a></h2>
<p>The OBD-II work had been parked for months as a deliberately unmerged spike:
a pure ELM327 protocol layer with tests, a BLE transport, and a diagnostic
screen — all waiting on one question no amount of code review could answer:
<em>does the bike actually respond?</em> Motorcycles are notorious for implementing
a thin, inconsistent slice of the OBD-II standard, or none at all.</p>
<p>Then the adapter arrived (a Vgate iCar Pro 2S), and the question got answered
in stages.</p>
<p><strong>Bench day.</strong> Ignition on, scan, connect — and the first quiet surprise: the
adapter advertises the <code>18F0</code> BLE service, not the <code>FFF0</code> layout its own
documentation suggests. The transport's autodiscovery fallback (any service
with one notifiable and one writable characteristic) carried the day, and
<code>18F0</code> is now first in the known-layouts list. Then the second surprise: the
KTM 890 <strong>ignores <code>0100</code></strong>, the standard "which PIDs do you support?" query.
A capability-first design would have concluded the bike supports nothing.
Polling PIDs directly told a different story: RPM, speed, coolant, throttle,
load and battery voltage, all live, all plausible — 12.5 V with the engine
off, 14.2 V charging the moment it fired up.</p>
<p><strong>Probe day.</strong> The owner's real asks were odometer and fuel. So the screen
grew an <em>extended probe</em>: ask each interesting PID once per connection and
report answered / no-answer, honestly, per line. The verdict on the 890:
fuel level, true odometer, fuel rate, mass-air-flow and gear — silent.
But <strong>distance-since-codes-cleared (<code>31</code>) answers</strong>, and that is enough:
anchor it once against the dash and the app can track total distance from
then on. Maintenance tracking gets ECU-exact kilometres; fuel planning stays
on the learned-consumption path that never needed the ECU at all.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-hunt">The hunt<a href="https://fastgis.eu/guide/blog/talking-to-the-bike#the-hunt" class="hash-link" aria-label="Direct link to The hunt" title="Direct link to The hunt" translate="no">​</a></h2>
<p>The best part of the week came from the signals OBD-II <em>doesn't</em> hand over.
The ELM327 has a monitor mode that eavesdrops on the bike's broadcast CAN
bus — received wisdom says cheap clones drop frames and give up. Filtered
tightly, this one doesn't. Twenty-five broadcast CAN IDs reach the diagnostic
connector, and among them:</p>
<ul>
<li class=""><strong>Gear position</strong>, plainly broadcast — decoded and confirmed through all
six gears against prediction,</li>
<li class="">the <strong>IMU cluster</strong>, identified by a sign flip while riding circles,</li>
<li class="">a <strong>speed frame</strong> whose fields go to zero at a standstill — raw material
for a wheel-speed odometer with far finer resolution than the 1 km ticks
of PID <code>31</code>.</li>
</ul>
<p>The capture tooling moved from a laptop script into the app itself, with GPS
interleaved into every log as the calibration reference. And because the next
step is a calibration ride, the capture screen now has three glove-sized
buttons — <strong>20 / 40 / 60 km/h</strong> — that stamp labelled steady-speed windows
straight into the log while riding. Tap when you're holding the speed, tap
again when you leave it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-this-lands">Where this lands<a href="https://fastgis.eu/guide/blog/talking-to-the-bike#where-this-lands" class="hash-link" aria-label="Direct link to Where this lands" title="Direct link to Where this lands" translate="no">​</a></h2>
<p>Live engine data is on the phone today (<strong>More → OBD-II (Bluetooth)</strong>), the
screen keeps itself awake, and the whole feature works with CarPlay active —
which on this bike means <em>always</em>, since the head unit comes up with the
ignition. The plan from here: fuse the fine-but-drifting wheel-speed integral
with the coarse-but-honest kilometre counter, fold the proven signals into
ride recordings, and give the rally-roadbook mode the distance source it has
been waiting for.</p>
<p>The bike had a lot to say. We just had to stop asking politely and start
listening.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Rider app" term="Rider app"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The roadbook rides itself]]></title>
        <id>https://fastgis.eu/guide/blog/the-roadbook-rides-itself</id>
        <link href="https://fastgis.eu/guide/blog/the-roadbook-rides-itself"/>
        <updated>2026-09-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Since July the rider app has been able to turn any track into a rally-style]]></summary>
        <content type="html"><![CDATA[<p>Since July the rider app has been able to turn any track into a rally-style
tulip roadbook — the <strong>Rally</strong> side of the Road ⇄ Rally toggle reads the
corners straight off the GPX shape, no road network required. It was a
document: something to scroll with a thumb at a fuel stop. Today it becomes a
navigation mode.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ride-not-read">Ride, not read<a href="https://fastgis.eu/guide/blog/the-roadbook-rides-itself#ride-not-read" class="hash-link" aria-label="Direct link to Ride, not read" title="Direct link to Ride, not read" translate="no">​</a></h2>
<p>Open any track's roadbook, flip it to <strong>Rally</strong>, and there is now a <strong>Ride</strong>
button. It builds a turn-by-turn session from the track's geometry alone:
every bend is classified by its clustered turn angle — <em>kink, mild, sharp,
elbow, hairpin</em> — with S-bends linked into chicanes, and each one becomes a
maneuver the normal navigation engine drives. Nothing is looked up. No
map-matching, no tiles, no server, no signal. If the points are on the phone,
the roadbook rides.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-hud-shows">What the HUD shows<a href="https://fastgis.eu/guide/blog/the-roadbook-rides-itself#what-the-hud-shows" class="hash-link" aria-label="Direct link to What the HUD shows" title="Direct link to What the HUD shows" translate="no">​</a></h2>
<ul>
<li class=""><strong>A tulip glyph</strong> instead of a road arrow — the same approach-line-and-exit
shape you would draw in a paper roadbook, coloured by direction.</li>
<li class=""><strong>Distance to the next note</strong>, counting down odometer-style along the
track as you cover ground, not as the crow flies. On a stack of hairpins the
"nearest note" is often three switchbacks above you; along-track distance
never makes that mistake.</li>
<li class=""><strong>CAP</strong>, the compass heading to hold out of the bend — <code>CAP 135° SE</code> — the
rally navigator's oldest trick for confirming you left a junction the right
way.</li>
<li class=""><strong>Danger chips</strong>: <code>!</code>, <code>!!</code>, <code>!!!</code> from radius, blindness and downhill
grade.</li>
</ul>
<p>Spoken cues follow suit — "hairpin left, 160", "S-bends" — in the navigation
voice language you already chose.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-it-deliberately-does-not-do">What it deliberately does not do<a href="https://fastgis.eu/guide/blog/the-roadbook-rides-itself#what-it-deliberately-does-not-do" class="hash-link" aria-label="Direct link to What it deliberately does not do" title="Direct link to What it deliberately does not do" translate="no">​</a></h2>
<p>A roadbook ride never replans. Going off the line does not summon a router to
"fix" the route (there is none to summon, and on a trail the fix is usually
worse than the problem). The HUD drops to a calm <em>following your track</em> hint,
the voice says so once, and the cues resume the moment you are back on the
line. That quiet path existed in the engine for months as an anticipated
case; this is the first mode to actually ride it.</p>
<p>An advisory corner-speed and gear suggestion (<code>v = √(μ·g·r)</code>, with a grip
coefficient you set) is in there too — <strong>off by default</strong>, and it stays off
unless you turn it on in Settings → Navigation. Estimates about grip are not
something an app should volunteer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-it-stands">Where it stands<a href="https://fastgis.eu/guide/blog/the-roadbook-rides-itself#where-it-stands" class="hash-link" aria-label="Direct link to Where it stands" title="Direct link to Where it stands" translate="no">​</a></h2>
<p>Verified in the simulator with GPS playback along a real Norwegian pass —
eleven hairpins, every one announced and advanced in order. The field test on
the bike is still owed, and that is where the note spacing and the CAP
lead time will get their real calibration.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Rider app" term="Rider app"/>
        <category label="Routing" term="Routing"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Which way were you facing?]]></title>
        <id>https://fastgis.eu/guide/blog/which-way-were-you-facing</id>
        <link href="https://fastgis.eu/guide/blog/which-way-were-you-facing"/>
        <updated>2026-09-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[This was the week we stopped trusting the navigation and started measuring it.]]></summary>
        <content type="html"><![CDATA[<p>This was the week we stopped trusting the navigation and started measuring it.
Real rides, real device logs, and a string of bugs that had been hiding in
plain sight — one of them for three months.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-reroute-never-knew-which-way-the-rider-was-facing">The reroute never knew which way the rider was facing<a href="https://fastgis.eu/guide/blog/which-way-were-you-facing#the-reroute-never-knew-which-way-the-rider-was-facing" class="hash-link" aria-label="Direct link to The reroute never knew which way the rider was facing" title="Direct link to The reroute never knew which way the rider was facing" translate="no">​</a></h2>
<p>The app has computed an anti-U-turn origin heading since early in the
turn-by-turn work. It turns out that heading <strong>never once reached the routing
engine</strong>. The server's validation model declared exactly <code>lon</code> and <code>lat</code>, and
Pydantic silently dropped everything else the phone sent — heading, and the
via <code>type</code> that marks a rejoin hint as a pass-through rather than a stop.</p>
<p>The symptoms were exactly what riders reported. With no heading, a reroute for
someone travelling the wrong way comes back as "continue" — the instruction
they are already following, repeated forever, with a perfectly good roundabout
fifty metres ahead that the engine was never told was <em>ahead</em>. And with every
via defaulting to a stop, one evening's log shows 47 multi-leg reroutes
producing <strong>69 phantom "you will reach the destination" announcements</strong>,
mid-ride.</p>
<p>The docstring above that model warned that Pydantic ignores unknown fields.
It had already cost us once. The lesson is now a house rule with teeth: <strong>test
at the HTTP boundary, not the service</strong> — six of the eight new route-level
tests fail against the old model.</p>
<p>Fixing the heading exposed the next layer. The rejoin logic guaranteed its
target was forward <em>along the route</em> — which is the same as forward for the
rider right up until the rider is travelling opposite to the route, which is
precisely when they are lost. So the origin heading pointed at the roundabout
while the rejoin hint pulled the line straight back through it, and the rider
rode out and back. There is now a guard: a rejoin more than 120° off the
rider's heading is dropped, and the reroute plans to the destination instead.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-week-of-riding-the-logs">A week of riding the logs<a href="https://fastgis.eu/guide/blog/which-way-were-you-facing#a-week-of-riding-the-logs" class="hash-link" aria-label="Direct link to A week of riding the logs" title="Direct link to A week of riding the logs" translate="no">​</a></h2>
<p>The rest of the reliability sweep came straight off device logs from field
rides. A guard-before-<code>await</code> race was leaking native location watchers — one
ride ran with <strong>two GPS pipelines fanning fixes</strong> into an already-saturated
thread, and one day's log showed 24 manager instances. "It stopped syncing
with the GPS" was the app failing to consume fixes, not the phone failing to
make them. A reroute storm — 52 reroutes in 20 minutes — turned out to be the
throttle being disarmed by its own reroute. A rider who left the route stopped
being told they'd arrived; a lost rider stranded by a non-convergence guard
gets a time floor and an auto-retry — and then measurement showed that guard
had shipped dead, its condition unable to fire until it had already fired.</p>
<p>When the marker can't be snapped to a road, it now draws a metre-true
<strong>accuracy halo</strong> instead of a falsely precise dot. And then the best outcome
of the week: the plan's next phase — snapping to the road we <em>think</em> you're
on — set its own stop-condition in advance, two rides measured a 1 m median
residual up to 91 km/h, and the phase is <strong>not being built</strong>. Deciding not to
build something, on evidence, is a result.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pick-the-places-then-ride-a-circle-through-them">Pick the places, then ride a circle through them<a href="https://fastgis.eu/guide/blog/which-way-were-you-facing#pick-the-places-then-ride-a-circle-through-them" class="hash-link" aria-label="Direct link to Pick the places, then ride a circle through them" title="Direct link to Pick the places, then ride a circle through them" translate="no">​</a></h2>
<p>The scenic loop generator got turned inside out. Instead of "give me 120 km of
loop and hope it passes something", you now pick the places — wiki-ranked
villages and POIs by category and budget, curated on the map — and the
optimizer computes the circle through them. Shipped on the phone <em>and</em> in the
desktop planner the same day, and iterated twice on owner feedback before the
weekend.</p>
<p>The plainer loop generator learned honesty too: when the road network can't
deliver the distance you asked for, it says so — on the server, on the phone,
and offline — instead of quietly handing you a shorter ride.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tools-that-lie-are-worse-than-no-tools">Tools that lie are worse than no tools<a href="https://fastgis.eu/guide/blog/which-way-were-you-facing#tools-that-lie-are-worse-than-no-tools" class="hash-link" aria-label="Direct link to Tools that lie are worse than no tools" title="Direct link to Tools that lie are worse than no tools" translate="no">​</a></h2>
<p><code>release-to-phones.sh</code> had a bad week and came out much stronger. It could
hang forever at a prompt when no phone was attached — one run was found still
spinning fourteen hours after the build finished. It couldn't see a phone on a
USB cable because it grepped for a status word only wireless prints. It once
installed the build to the <strong>wrong phone</strong> and wrote a receipt saying the
opposite. And it happily called an empty <code>.app</code> skeleton a successful build,
because xcodebuild creates the folder before compiling anything.</p>
<p>Every one of those is fixed the same way: the script now checks ground truth —
the debug-services probe, the device's product type, the Mach-O inside the
bundle — instead of string-matching its way to optimism. A green exit is not a
result. Same spirit on the code side: the mobile app finally has ESLint with
<code>rules-of-hooks</code> as an error, wired into CI, days after a hooks-order crash
took down every CarPlay routing flow in the field — a class of bug that could
not have been linted for, because there was no linter.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="also">Also<a href="https://fastgis.eu/guide/blog/which-way-were-you-facing#also" class="hash-link" aria-label="Direct link to Also" title="Direct link to Also" translate="no">​</a></h2>
<p>The CarPlay stability audit root-caused all three field symptoms — including
that routing crash — and the usability round put <strong>route alternatives on the
car screen</strong> with a companion picker. A desktop session no longer outlives its
upstream token, and the B2B web app is walled off from rider accounts. Walk
isochrones went from a 23-minute per-request projection to a ~3-second
windowed crop — numerically identical output. The contour basemap was re-baked
on the server: 8.85 GB → 5.3 GB, 102 warnings → 0. An OBD-II BLE dongle now
has a transport and a diagnostic screen, parked until the motorcycle answers.
The screen stays awake while navigating. And the half-ring that haunted
signed-out phones after a group call is dead.</p>
<p>One more thing: the page you are reading lands on main this week. The docs
site now ships with the repo — generated API reference with a drift gate so it
cannot quietly fall behind the code, operations runbooks, and this blog.</p>
<hr>
<p><strong>Dig deeper:</strong> the <a class="" href="https://fastgis.eu/guide/docs/product/rider-app/overview">rider app guide</a>, and
<a class="" href="https://fastgis.eu/guide/docs/product/riding-together">riding together</a> for the CarPlay side.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Rider app" term="Rider app"/>
        <category label="Routing" term="Routing"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[How far to the next stop?]]></title>
        <id>https://fastgis.eu/guide/blog/how-far-to-the-next-stop</id>
        <link href="https://fastgis.eu/guide/blog/how-far-to-the-next-stop"/>
        <updated>2026-09-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Plan a route with a lunch stop and a mountain pass in the middle, start]]></summary>
        <content type="html"><![CDATA[<p>Plan a route with a lunch stop and a mountain pass in the middle, start
navigating, and until today the app would tell you exactly one thing about
time: when you'd reach the <strong>end</strong>. The intermediate stops — the whole reason
the route has that shape — were invisible from the saddle.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="partials-everywhere-the-trip-is-shown">Partials everywhere the trip is shown<a href="https://fastgis.eu/guide/blog/how-far-to-the-next-stop#partials-everywhere-the-trip-is-shown" class="hash-link" aria-label="Direct link to Partials everywhere the trip is shown" title="Direct link to Partials everywhere the trip is shown" translate="no">​</a></h2>
<p>Multi-stop routes now carry per-stop arithmetic through the whole app:</p>
<ul>
<li class=""><strong>In the HUD</strong>, three new cycling metric slots: <strong>NEXT STOP</strong> (distance to
the next intermediate stop), <strong>STOP ETA</strong> (the arrival clock at it), and a
<strong>STOP</strong> counter ("2/4"). The final-destination ETA is untouched.</li>
<li class=""><strong>Before you ride</strong>, the route planner's stops editor lists each stop's own
leg distance, riding time, and projected arrival clock — so you can see at
a glance that lunch lands at 14:30, not 12:00.</li>
<li class=""><strong>On CarPlay and Android Auto</strong>, a new <strong>Stops</strong> button during multi-stop
navigation opens a dedicated list page: every stop with its remaining
distance and arrival time. The car's own estimate still means the final
destination — CarPlay models one trip, one estimate, and pretending
otherwise would corrupt the ETA the car shows in its cluster.</li>
</ul>
<p>The engine room is deliberately boring: the navigation store already kept the
route's legs, and the ETA estimator already weighted remaining time by
Valhalla's per-maneuver durations (so a route mixing motorway and switchbacks
doesn't drift). Per-stop partials are prefix-sum subtractions over that same
distribution — the last stop's numbers are <em>identical</em> to the trip totals by
construction, and a test pins that identity at arbitrary points along the
route.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-the-honest-answer-is-a-dash">When the honest answer is a dash<a href="https://fastgis.eu/guide/blog/how-far-to-the-next-stop#when-the-honest-answer-is-a-dash" class="hash-link" aria-label="Direct link to When the honest answer is a dash" title="Direct link to When the honest answer is a dash" translate="no">​</a></h2>
<p>The interesting design work was deciding when <strong>not</strong> to show a number.</p>
<p>An out-and-back track — Sommeiller, Jafferau — is one leg whose geometry is a
palindrome. The turnaround is not a stop, and "Stop 1 of 1" would be
noise pretending to be information. A TET section or imported GPX rides as a
chain of stitched legs whose boundaries are chunking artifacts, not places
anyone plans to stop. And after an off-route reroute rebuilds the trip as
"approach + trail", reaching the trail again is a milestone, not a stop.</p>
<p>In all of those cases the stop metrics show a plain <strong>—</strong>. The rule the
implementation follows, and the tests enforce: degrade to blank rather than
lie.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Rider app" term="Rider app"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The mountains, in your pocket]]></title>
        <id>https://fastgis.eu/guide/blog/the-mountains-in-your-pocket</id>
        <link href="https://fastgis.eu/guide/blog/the-mountains-in-your-pocket"/>
        <updated>2026-09-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Two weeks ago the map stopped being flat: the routing]]></summary>
        <content type="html"><![CDATA[<p>Two weeks ago <a class="" href="https://fastgis.eu/guide/blog/the-map-was-flat">the map stopped being flat</a>: the routing
graph learned elevation, the road score grew a climb axis, and the server could
finally tell a mountain road from a merely bendy one. All of it, though, lived
behind an API. This week it reaches the phone.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="bends-or-climb--your-call-in-four-places">Bends or climb — your call, in four places<a href="https://fastgis.eu/guide/blog/the-mountains-in-your-pocket#bends-or-climb--your-call-in-four-places" class="hash-link" aria-label="Direct link to Bends or climb — your call, in four places" title="Direct link to Bends or climb — your call, in four places" translate="no">​</a></h2>
<p>The <strong>curvy-roads finder</strong> grew a <em>Rank by</em> switch: <strong>Bends</strong> is the classic
curvature ranking, <strong>Climb</strong> orders the same search by a 0–100 climb rating
instead. The two lists genuinely differ — the bendiest road near you and the
biggest mountain road near you are rarely the same road — and each result now
shows its climb rating alongside elevation range and gain.</p>
<p>The <strong>loop generator</strong> got the same choice. <em>Include curvy roads</em> has always
woven the best road nearby into your loop; now a <strong>Bends / Climb</strong> picker
decides what "best" means, so a mountain loop is one tap instead of a hope.
It is mirrored on CarPlay and Android Auto.</p>
<p>The <strong>route planner</strong> gained a <em>Scenic bias</em> slider — off by default, because
it asks the server for extra work per request. Slide it up and the route is
biased toward the best-rated roads in the corridor, again with the bends-or-
climb choice. The engine's cost model is untouched: the server scores the
roads and simply tells Valhalla some edges are cheaper.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-second-number-beside--unpaved">A second number beside "% unpaved"<a href="https://fastgis.eu/guide/blog/the-mountains-in-your-pocket#a-second-number-beside--unpaved" class="hash-link" aria-label="Direct link to A second number beside &quot;% unpaved&quot;" title="Direct link to A second number beside &quot;% unpaved&quot;" translate="no">​</a></h2>
<p>A planned route's summary already showed how much of it is dirt. It now also
shows how <em>demanding</em> it is: a <strong>difficulty</strong> rating — surface roughness
amplified by climb, on exactly the scale the <code>max_difficulty</code> routing ceiling
uses — with the hard kilometres spelled out beside it. It is deliberately a
second number, never folded into the unpaved share: "40% unpaved" and "40%
unpaved, 12 of them steep loose climbs" are different rides, and the ride you
were promised should match the ride you get. Steep tarmac stays easy — steep
is not the same as difficult.</p>
<p>The offline fold computes the same number on-device from the elevation the
routing tiles now carry, so the badge survives losing the network.</p>
<p>All of this ships in ten languages and rides the next phone build.</p>
<hr>
<p><strong>Dig deeper:</strong> the <a class="" href="https://fastgis.eu/guide/docs/product/rider-app/overview">rider app guide</a> covers
each control in place.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Rider app" term="Rider app"/>
        <category label="Routing" term="Routing"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Your captures, and where they live]]></title>
        <id>https://fastgis.eu/guide/blog/your-captures-three-homes</id>
        <link href="https://fastgis.eu/guide/blog/your-captures-three-homes"/>
        <updated>2026-09-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Field captures — the photos, videos, audio notes and waypoints you record on a]]></summary>
        <content type="html"><![CDATA[<p>Field captures — the photos, videos, audio notes and waypoints you record on a
ride — have always had two homes: the phone that took them and the server they
sync to. Since June, the Library has been smart enough to <em>stream</em> a capture
from the server when its local copy is gone, so deleting data to free space no
longer dead-ends a session. But you still couldn't say what you actually
meant: "keep this, just not on my phone", or "I don't want to see this in my
library any more — but don't destroy it".</p>
<p>Today the session view grows three storage actions that say exactly that.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="remove-from-device">Remove from device<a href="https://fastgis.eu/guide/blog/your-captures-three-homes#remove-from-device" class="hash-link" aria-label="Direct link to Remove from device" title="Direct link to Remove from device" translate="no">​</a></h2>
<p>A synced session's local copies can now be dropped in one tap. The session
stays in your list, the track stays on the map, and every capture keeps
playing — streamed from the server with the same viewers, just marked
"Streaming from server". This is the space-freeing move: media is by far the
heaviest thing the app stores. The action only appears once the session is
fully synced, so it can never throw away the only copy of anything.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="download-for-offline">Download for offline<a href="https://fastgis.eu/guide/blog/your-captures-three-homes#download-for-offline" class="hash-link" aria-label="Direct link to Download for offline" title="Direct link to Download for offline" translate="no">​</a></h2>
<p>The inverse. A session that lives only on the server — because you removed it,
or because it was recorded on another device — can be re-materialized locally:
every capture row and every media file comes back to the phone, and the
session works with no connection again. Sessions the server knew but the phone
had completely forgotten are rebuilt from the registry, wired back up so
future streaming and syncing keep working.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="delete-from-library">Delete from library<a href="https://fastgis.eu/guide/blog/your-captures-three-homes#delete-from-library" class="hash-link" aria-label="Direct link to Delete from library" title="Direct link to Delete from library" translate="no">​</a></h2>
<p>This one needed a new server verb, and the verb had to be <em>soft</em>. The session
registry only knew two things: list a session, or cascade-delete it together
with its GRASS layers. Neither is "remove this from my listing". So the
registry gained a hidden flag — a <code>PATCH</code> that hides a session from the
listing while leaving the layers, the attribute tables, and the media
untouched on the server. Deleting from the library hides the registry entry,
soft-deletes the Library cards (the catalogue has had a trash with restore for
months), and wipes the local copy. A permanent delete remains what it always
was: a separate, explicit act.</p>
<p>The subtle part is discovery. When the registry returns an empty list, the app
falls back to scanning the environment's raw <code>track_*</code> layers — a deliberate
escape hatch for sessions published before the registry existed. But a hidden
session's layers are still there, so the scan would cheerfully resurrect
everything you just deleted. The listing now reports <em>how many</em> sessions it
hid, and the fallback scan only runs when the registry is genuinely empty —
not when it's merely quiet on purpose.</p>
<p>One more lesson from <a class="" href="https://fastgis.eu/guide/blog/which-way-were-you-facing">the reroute
post</a> applied here before the bug could
exist: a background re-publish overwrites a session's whole registry entry
with whatever the phone sends — and the phone never sends <code>hidden</code>. The
register route now preserves the flag across upserts, and a route-level test
pins it: sync more samples all you like, a deleted session stays deleted.</p>
<p>All of it lands with route-level server tests against the real HTTP boundary,
and the mobile pieces ride the next phone build.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Rider app" term="Rider app"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The map was flat]]></title>
        <id>https://fastgis.eu/guide/blog/the-map-was-flat</id>
        <link href="https://fastgis.eu/guide/blog/the-map-was-flat"/>
        <updated>2026-08-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[We route motorcycles through the Alps. This week we discovered that, as far as]]></summary>
        <content type="html"><![CDATA[<p>We route motorcycles through the Alps. This week we discovered that, as far as
the software was concerned, the Alps were not there.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="elevation-everywhere-at-once">Elevation, everywhere at once<a href="https://fastgis.eu/guide/blog/the-map-was-flat#elevation-everywhere-at-once" class="hash-link" aria-label="Direct link to Elevation, everywhere at once" title="Direct link to Elevation, everywhere at once" translate="no">​</a></h2>
<p>An audit of the routing graph found that <strong>most of Europe's tiles carried no
elevation at all</strong> — the majority of the graph's one-degree cells had no DEM
behind them, and a missing DEM yields silently zero grade, which looks exactly
like flat ground. Then the same check on the road-score database: every one of
its million European roads had NULL in all seven elevation columns. The "epic
road" score had never seen a mountain. Same class of bug, found the same way:
by looking instead of assuming.</p>
<p>Both are fixed from the same source — the 1-arcsec DEM we already host,
resampled into the exact grid Valhalla reads. The routing tiles now carry
grade, and the riding knobs that were quietly inert came alive: prefer or
avoid hills, avoid tunnels, cap route difficulty, with a difficulty number
reported beside "% unpaved" rather than folded into it. Steep tarmac stays
easy; a rider who asked for no rough stuff did not ask to avoid mountain
passes.</p>
<p>The road score grew a <strong>climb axis</strong> — a second number beside curvature,
because "twisty but flat" and "high but straight" are different roads and one
blended score cannot say which you got. Ranked by climb, the Alps now answer
Gavia, Stelvio, Timmelsjoch. The Netherlands answers dike roads at 8.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="dont-teach-the-solver-what-good-means">Don't teach the solver what "good" means<a href="https://fastgis.eu/guide/blog/the-map-was-flat#dont-teach-the-solver-what-good-means" class="hash-link" aria-label="Direct link to Don't teach the solver what &quot;good&quot; means" title="Direct link to Don't teach the solver what &quot;good&quot; means" translate="no">​</a></h2>
<p>The tempting design was to mix "fun" into Valhalla's cost model. Measurement
killed it: discount steep edges and the solver buys more steep <em>kilometres</em>,
not more altitude — one test route went 23 km further and 300 m <strong>lower</strong> the
harder we asked it for hills. An optimisation solver optimises what you price,
not what you meant.</p>
<p>So scoring stays in the curvature database, and a new <strong>scenic bias</strong> hands
the router per-edge discounts for the best-ranked roads in the corridor — by
curvature or by climb, off by default, and every failure path returns an
ordinary route. The loop generator learned the same trick: it can now weave in
a road chosen for its climb, not just its bends, with a hard elevation floor
so a flat region says so instead of offering its least-flat lane.</p>
<p>Dirt-first routing shipped the same lesson in miniature. The first cut
inflated the cost of pavement, which made ferries relatively cheap and sent a
"dirt" route across a fjord. Discounting dirt instead fixed the ferry, fixed
the inert middle preset, and the badge now says "no unpaved roads on this
route" rather than a bare 0% that reads as broken.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-desktop-caught-up--and-then-got-audited">The desktop caught up — and then got audited<a href="https://fastgis.eu/guide/blog/the-map-was-flat#the-desktop-caught-up--and-then-got-audited" class="hash-link" aria-label="Direct link to The desktop caught up — and then got audited" title="Direct link to The desktop caught up — and then got audited" translate="no">​</a></h2>
<p>The desktop planner had been dark for a week while the server kept shipping,
so it got a catch-up sprint: the dirt-first axis, grade knobs, climb loops,
the POI seeker reaching <strong>all 39 categories instead of 2</strong>, corridor search
along the route, our own basemap, live view, reach contours, and a wildcamp
planner that says honestly when the analysis backend is not on this server.
The first fix of the sprint was humbling: the slope-coloured terrain strip had
<em>never rendered on prod</em> — a graceful fallback had hidden a missing gateway
rule for weeks.</p>
<p>Then we did something new: an <strong>independent audit of our own sprint</strong>. It
found a real boundary defect — the gateway allowlist could be walked out of
with encoded <code>..</code> segments, turning every prefix rule into a door. Browsers
normalise those before sending, which is exactly why browser testing could
never have caught it. The matcher now refuses dot segments outright, the
client got its first-ever unit tests, and all seven follow-ups were acted on
the same day.</p>
<p>The first real browser pass found the rest: every vector layer on the desktop
was blank (relative tile URLs, which a worker cannot resolve — no request, no
error, nothing in any log), the proxy told browsers a gzipped tile was plain,
raster basemaps silently dropped every text label, and the caching was exactly
backwards — the mutable document was cacheable and the immutable assets were
not. An audit that reads code and a pass that drives a browser find different
bugs. You need both.</p>
<p>One more honest note: <strong>tier gating is now off by default</strong>. The tier line-up
is not decided, the current accounts are all development fakes, and the pilot
will run fully featured so that anything we might later gate still gets
tested. The draft bundles are kept as drafts, clearly marked as not a product
decision.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-phone-stopped-freezing-forensically">The phone stopped freezing, forensically<a href="https://fastgis.eu/guide/blog/the-map-was-flat#the-phone-stopped-freezing-forensically" class="hash-link" aria-label="Direct link to The phone stopped freezing, forensically" title="Direct link to The phone stopped freezing, forensically" translate="no">​</a></h2>
<p>The tab-bar freeze finally gave up its causes, on device, with a watchdog:
duplicate navigator trees stacked by deep links, a frozen scene left on top
still eating taps, sensor readings written to the store 48 times a second, and
one environment load firing ~100 parallel GeoJSON parses that blocked JS for
24 seconds. Each got a targeted fix rather than a workaround.</p>
<p>Also this week, at speed: the rally roadbook grew a 137-glyph symbol library
in four dialects and FIA-layout print — A5 pages and a continuous moto-roll
with cut marks; search learned the famous passes, so three keystrokes surface
Stelvio instead of ten villages named Colle; the TET got a Europe-wide
tap-a-trail overview map; and Settings was regrouped around how riders
actually think, in ten languages.</p>
<hr>
<p><strong>Dig deeper:</strong> the <a class="" href="https://fastgis.eu/guide/docs/product/desktop-client">desktop client</a> and the
<a class="" href="https://fastgis.eu/guide/docs/product/rider-app/overview">rider app guide</a>.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Routing" term="Routing"/>
        <category label="Desktop" term="Desktop"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The week we drew our own map]]></title>
        <id>https://fastgis.eu/guide/blog/our-own-map</id>
        <link href="https://fastgis.eu/guide/blog/our-own-map"/>
        <updated>2026-08-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Losing the OSM volunteer tile servers stung. It also settled an argument we had]]></summary>
        <content type="html"><![CDATA[<p>Losing the OSM volunteer tile servers stung. It also settled an argument we had
been having with ourselves: a routing platform that cannot render its own map is
renting its most visible surface from someone else, on someone else's schedule.</p>
<p>This week we stopped renting.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="our-own-map">Our own map<a href="https://fastgis.eu/guide/blog/our-own-map#our-own-map" class="hash-link" aria-label="Direct link to Our own map" title="Direct link to Our own map" translate="no">​</a></h2>
<p>Phase 0 was Norway, behind a flag: <strong>planetiler over the Europe PBF we already
host</strong>, our own style, our own self-hosted glyphs — because self-hosting the
fonts is the difference between owning the basemap and owning only the tiles.
Surface is a first-class visual channel in it: unpaved roads render ochre with a
dashed casing, so "can I take the GS down there" is answerable at a glance. An
absent <code>surface</code> tag stays neutral — overclaiming gravel is worse than saying
nothing.</p>
<p>Phase 1 was <strong>all of Europe: 28.96 GB, 22.7 million tiles, 53 minutes on 24
cores</strong>. Verified by content, not exit code — the Champs-Élysées, Unter den
Linden and the Colosseo each have to decode out of their own tile, because a
continental build can lose a whole region while every other check stays green.</p>
<p>Then the rest arrived in days rather than months: hillshade and contours from
our own GLO-30 DEM (Galdhøpiggen decodes to within ~10 m of its true summit —
wrong georeferencing would miss by hundreds), POI icons with a palette that
survives mid-zoom, a layer manager, tap-a-POI-to-identify, 3D buildings, a
device-side tile server so the basemap works <strong>offline</strong>, and an unpaved-density
heatmap pilot — because we measured that overview-zoom tiles simply do not
contain the dirt, so no styling could ever answer "where is the riding".</p>
<p>Honesty corner: terrain tiles briefly stalled the whole API at low zoom, a
style-spec subtlety silently dropped an entire contour layer, and the new map
stays behind its flag until the cartography earns the default slot. It is ours
now; it is not yet better. That gap is the roadmap.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="terrain-became-a-first-class-layer">Terrain became a first-class layer<a href="https://fastgis.eu/guide/blog/our-own-map#terrain-became-a-first-class-layer" class="hash-link" aria-label="Direct link to Terrain became a first-class layer" title="Direct link to Terrain became a first-class layer" translate="no">​</a></h2>
<p>The same DEM work pulled GRASS into everything. Per-country slope derivatives
served as COG mosaics, an elevation/slope/aspect profile along any track,
walking-cost services, wild-camp spot scoring refined on 1 m national DTM
patches, and ice-risk that now casts <strong>actual shadows with r.sun at the hour you
will ride past</strong> — a shaded hairpin at 8 a.m. and the same hairpin at noon are
different roads in October.</p>
<p>Sweden's 1 m elevation went in without mirroring the 1.2 TB national product:
we fetch only the tiles a route corridor touches. And the week supplied its own
lesson in humility: in a lat-lon location, <code>r.walk</code> was quietly counting
<strong>degrees as if they were metres</strong>. Everything walk-related now runs in a metric
projection, with a sanity guard so that class of bug cannot ship silently again.</p>
<p>On the phone this surfaces as a climb strip, grade badges and an ice-risk panel;
on the desktop as slope shading with a legend.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wrong-twice-then-right">Wrong twice, then right<a href="https://fastgis.eu/guide/blog/our-own-map#wrong-twice-then-right" class="hash-link" aria-label="Direct link to Wrong twice, then right" title="Direct link to Wrong twice, then right" translate="no">​</a></h2>
<p>Offline routing on region packs was crashing on device with <code>std::out_of_range</code>
while the identical pack routed fine on the server. We root-caused it three
times in three days, and the first two answers were wrong.</p>
<p>First: "the packs are an incomplete graph subset." Second: "the August EU tile
builds are corrupt — suspect the faulty CPU core." Both fit the evidence; both
were disproven by the next experiment. The true cause was in <strong>our own mobile
wrapper</strong>: it installed a tile-download getter even for local packs, so the
first out-of-pack tile reference — normal and harmless when there is no getter —
walked into a <code>substr</code> on an empty URL and threw. The server never reproduced it
because server tools never install a getter.</p>
<p>No tileset was ever corrupt. No rebuild was needed. The fixed engine is pinned,
reproduced and verified off-device in seconds instead of on a physical iPhone,
and confirmed on production Norway and Spain packs. The lesson we keep
re-learning: a plausible root cause that fits the evidence is a hypothesis, not
an answer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="voice-under-field-conditions">Voice under field conditions<a href="https://fastgis.eu/guide/blog/our-own-map#voice-under-field-conditions" class="hash-link" aria-label="Direct link to Voice under field conditions" title="Direct link to Voice under field conditions" translate="no">​</a></h2>
<p>Full-duplex calling met two real phones, and the phones won the first round. The
fixes that came back: answering must go <em>through</em> CallKit, never around it —
iOS refuses the app an audio session while a call sits unanswered, which is a
connected-but-silent call; and a media library was deactivating the shared audio
session a tenth of a second after any of its players stopped, tearing the floor
out from under the live call. Hang-up races, ring cancellation and TURN over
port 443 all landed the same day.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="locks-and-alarm-bells">Locks and alarm bells<a href="https://fastgis.eu/guide/blog/our-own-map#locks-and-alarm-bells" class="hash-link" aria-label="Direct link to Locks and alarm bells" title="Direct link to Locks and alarm bells" translate="no">​</a></h2>
<p>A security audit got paid down: fail-fast on weak secrets, login lockout,
session epochs, SSRF-hardened URL imports, non-root containers, authenticated
Redis. And the confession that started the visibility push — <strong>this repo had no
CI at all</strong>, and the desktop client once sat broken for a week while Swarm
served a stale container that looked perfectly healthy. Now there is CI, a daily
deploy-drift check that messages Telegram <em>only</em> when something is wrong
(a monitor that says "all good" every morning trains you to ignore it), live
probes that exercise routing and geocoding end-to-end, every install reports
exactly which build it is running, and the SOS bot has a watchdog — a dead
emergency channel must make a noise.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="also">Also<a href="https://fastgis.eu/guide/blog/our-own-map#also" class="hash-link" aria-label="Direct link to Also" title="Direct link to Also" translate="no">​</a></h2>
<p>Four more countries joined the EU set (Luxembourg, Ireland with NI, Malta,
Cyprus). SOS alerts now speak the contact's language. A portable cold-archive
drive got a scriptable toolkit — after we measured the real link speed and the
economics of restoring flipped. And enabling Redis AOF on an existing dataset
can boot you into an empty database; we hit it, recovered, and wrote it down.</p>
<hr>
<p><strong>Dig deeper:</strong> <a class="" href="https://fastgis.eu/guide/docs/product/rider-app/offline-data">offline data on the rider app</a>,
and the <a class="" href="https://fastgis.eu/guide/docs/product/data-products">data products</a> behind the new basemap.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Data" term="Data"/>
        <category label="Rider app" term="Rider app"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Roll camera]]></title>
        <id>https://fastgis.eu/guide/blog/cameras-and-cars</id>
        <link href="https://fastgis.eu/guide/blog/cameras-and-cars"/>
        <updated>2026-08-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Riders already carry an action camera. Until now it was a separate device with]]></summary>
        <content type="html"><![CDATA[<p>Riders already carry an action camera. Until now it was a separate device with
its own app, its own battery anxiety and its own timeline that never quite lined
up with the ride.</p>
<p>Not any more.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="action-camera-control">Action-camera control<a href="https://fastgis.eu/guide/blog/cameras-and-cars#action-camera-control" class="hash-link" aria-label="Direct link to Action-camera control" title="Direct link to Action-camera control" translate="no">​</a></h2>
<p>Epona now controls <strong>GoPro over BLE</strong> and the <strong>Insta360 X4 through its SDK</strong>.
Start and stop recording from the app, from the CarPlay screen, or from a helmet
button — without taking a hand off the bars, which is the only interaction model
that matters at speed.</p>
<p>Two cameras, two entirely different integrations. The GoPro speaks a documented
BLE protocol; the X4 needs Insta360's own SDK compiled in. Presenting them as one
feature took more work than either one alone.</p>
<p>Every shot is written to a <strong>shot log</strong> and dropped into the ride as a GPX
waypoint. When you sit down to edit — days later, looking at four hours of
identically-named files — you already know which clip was the pass and which was
the roundabout you filmed by accident.</p>
<p>That is the part we are most pleased with. The camera integration is convenient;
the <em>log</em> is what turns a pile of footage back into a ride.</p>
<p>The two cameras are genuinely different, and the app now respects that instead of
pretending otherwise: only the GoPro gets the Wi-Fi transport switch, and the X4
shows a read-only camera card rather than offering settings that BLE cannot
deliver. Better to say "not available here" than to fail silently after a tap.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="android-auto-and-one-command-releases">Android Auto, and one-command releases<a href="https://fastgis.eu/guide/blog/cameras-and-cars#android-auto-and-one-command-releases" class="hash-link" aria-label="Direct link to Android Auto, and one-command releases" title="Direct link to Android Auto, and one-command releases" translate="no">​</a></h2>
<p>Two scripts that will save more hours than they look like: <code>release-to-phones.sh</code>
builds one release with CarPlay enabled and installs it to <strong>both</strong> dev phones,
and <code>release-to-android.sh</code> produces one release APK with Android Auto on and
pushes it to any adb target.</p>
<p>Small tools, enormous effect. The gap between "the build works" and "the build is
on a bike" was twenty minutes of remembered incantations, and a twenty-minute gap
is why field testing happens on Saturdays instead of on Tuesdays. It is now one
command.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="overpass-keeps-itself-current">Overpass keeps itself current<a href="https://fastgis.eu/guide/blog/cameras-and-cars#overpass-keeps-itself-current" class="hash-link" aria-label="Direct link to Overpass keeps itself current" title="Direct link to Overpass keeps itself current" translate="no">​</a></h2>
<p>Daily Geofabrik diff updates on the self-hosted Overpass instance, applied
automatically. The POI data behind fuel, food and services now ages in days
rather than quarters — so a fuel station that opened last month is a fuel station
you can route to.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="and-the-admin-database-gate">And the admin-database gate<a href="https://fastgis.eu/guide/blog/cameras-and-cars#and-the-admin-database-gate" class="hash-link" aria-label="Direct link to And the admin-database gate" title="Direct link to And the admin-database gate" translate="no">​</a></h2>
<p>A pipeline fix worth naming, because it was a genuinely nasty one. Valhalla
treats a <strong>missing administrative database as a warning</strong>, not an error. A tile
build pointed at a path that did not exist finished successfully and shipped
tiles with no country data at all — which silently switched off legal-speed
defaults and mph display hints across the app.</p>
<p>The pipeline now builds <code>admin.sqlite</code> explicitly, forces the host path, and
<strong>hard-fails</strong> any build whose log mentions a missing admin database. A green exit
is not a result.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-we-are">Where we are<a href="https://fastgis.eu/guide/blog/cameras-and-cars#where-we-are" class="hash-link" aria-label="Direct link to Where we are" title="Direct link to Where we are" translate="no">​</a></h2>
<p>Camera, car screen, helmet, phone, desktop — the ride is becoming one continuous
thing rather than five devices you reconcile afterwards.</p>
<p>There is a lot more of this to come.</p>
<hr>
<p><strong>Dig deeper:</strong> the <a class="" href="https://fastgis.eu/guide/docs/product/rider-app/overview">rider app guide</a>, and
<a class="" href="https://fastgis.eu/guide/docs/product/riding-together">riding together</a> for the CarPlay and safety
side.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Rider app" term="Rider app"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The unglamorous week that everything depends on]]></title>
        <id>https://fastgis.eu/guide/blog/fresh-data</id>
        <link href="https://fastgis.eu/guide/blog/fresh-data"/>
        <updated>2026-08-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[No new screens this week. Instead: the data underneath every screen got fresh,]]></summary>
        <content type="html"><![CDATA[<p>No new screens this week. Instead: the data underneath every screen got fresh,
and stays fresh, and now tells us when it isn't.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="two-silent-failures-found">Two silent failures, found<a href="https://fastgis.eu/guide/blog/fresh-data#two-silent-failures-found" class="hash-link" aria-label="Direct link to Two silent failures, found" title="Direct link to Two silent failures, found" translate="no">​</a></h2>
<p>The best bug reports come from things that never threw an error.</p>
<p><strong>32 of 64 regions had no POI import.</strong> Ireland had zero fuel stations. Nothing
errored, nothing alerted, no log line said so. The map looked exactly as a map
should look — the fuel stations were simply not on it, and you would only find
out by needing one.</p>
<p><strong>A dark sky near Bergen returned nothing.</strong> A rejected request was rendering as
an empty result, which is indistinguishable from a truthful "no stargazing spots
here" — and a user has no way to tell the difference between an app that found
nothing and an app that failed to look.</p>
<p>Both are now fixed, and both are the reason for everything below.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-refresh-train">The refresh train<a href="https://fastgis.eu/guide/blog/fresh-data#the-refresh-train" class="hash-link" aria-label="Direct link to The refresh train" title="Direct link to The refresh train" translate="no">​</a></h2>
<p>A quarterly EU data-refresh pipeline, written down as a runbook rather than lived
in someone's head: fresh Geofabrik extracts for 32 countries, legal-maxspeed
injection for the nine countries with statutory speed tables, an osmium merge
into a <strong>single</strong> PBF — multi-PBF Valhalla builds segfault — TET overlay
re-application, the tile build, and a <strong>route benchmark harness</strong> that compares
the new tiles against golden routes before anything goes anywhere near live.</p>
<p>The overlay step has a subtlety worth naming: OSM churn deletes nodes that the
trail conflate still references, so the pipeline resurrects the missing ones from
the <em>previous</em> run's augmented extract. A sudden jump in that count means the
overlay has aged out and needs re-conflating, which is a much better signal than
discovering it in a broken route.</p>
<p>It is pinned off a faulty CPU core on the build host, because a tile build
scheduled onto that core <strong>corrupts the routing graph rather than crashing</strong> —
which we know because it did, and because a corrupt graph fails days later in a
place with no signal.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="speed-limits-offline">Speed limits, offline<a href="https://fastgis.eu/guide/blog/fresh-data#speed-limits-offline" class="hash-link" aria-label="Direct link to Speed limits, offline" title="Direct link to Speed limits, offline" translate="no">​</a></h2>
<p>Legal speed limits now come from an <strong>on-device profile</strong> computed with Valhalla's
<code>traceAttributes</code>, so the HUD sign works with no signal at all.</p>
<p>Most European roads carry no explicit <code>maxspeed</code> tag in OSM — the limit is the
national default for that road class, which every rider knows and no database
states. So the profile fills those gaps from statutory tables, falls back to live
data when it has a connection, and lets a detected camera override everything.
Three sources, one number on the screen.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="update-badges-that-tell-the-truth">Update badges that tell the truth<a href="https://fastgis.eu/guide/blog/fresh-data#update-badges-that-tell-the-truth" class="hash-link" aria-label="Direct link to Update badges that tell the truth" title="Direct link to Update badges that tell the truth" translate="no">​</a></h2>
<p>A pack freshness manifest, and update-available badges driven by it: the phone
compares each installed region's tileset version against the server's and badges
only what is genuinely behind.</p>
<p>Plus the fix that made them honest. A stale pooled SQLite handle had the badge
looping forever, insisting on an update that had already been installed — the
single fastest way to teach someone to ignore a badge permanently.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="beaches-generalised">Beaches, generalised<a href="https://fastgis.eu/guide/blog/fresh-data#beaches-generalised" class="hash-link" aria-label="Direct link to Beaches, generalised" title="Direct link to Beaches, generalised" translate="no">​</a></h2>
<p>The beach seeker stopped being Norway-specific. Sweden shipped — sea and lake
both — with an SGU <em>jordarter</em> geology prior doing the heavy lifting, and
per-region crowdsourced review pages so riders can correct it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="also">Also<a href="https://fastgis.eu/guide/blog/fresh-data#also" class="hash-link" aria-label="Direct link to Also" title="Direct link to Also" translate="no">​</a></h2>
<p>Italian-locale address search was quietly falling back to a different geocoder
after a 400 error. The vehicle register writes "901 Norden" while Husqvarna writes
"Norden 901". The odometer is now anchored rather than inferred from GPS, because
inferring it was always going to be wrong eventually.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-we-like-weeks-like-this">Why we like weeks like this<a href="https://fastgis.eu/guide/blog/fresh-data#why-we-like-weeks-like-this" class="hash-link" aria-label="Direct link to Why we like weeks like this" title="Direct link to Why we like weeks like this" translate="no">​</a></h2>
<p>Every feature we ship stands on this. A gorgeous route on stale tiles is a
beautiful lie, and we would rather ship a plain truth.</p>
<hr>
<p><strong>Dig deeper:</strong> the <a class="" href="https://fastgis.eu/guide/docs/developers/">developer docs</a> for the manifest format.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Data" term="Data"/>
        <category label="Platform" term="Platform"/>
        <category label="Routing" term="Routing"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The app has a name: Epona]]></title>
        <id>https://fastgis.eu/guide/blog/epona</id>
        <link href="https://fastgis.eu/guide/blog/epona"/>
        <updated>2026-08-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Epona is the Gaulish goddess of horses and riders — the one deity the Roman]]></summary>
        <content type="html"><![CDATA[<p>Epona is the Gaulish goddess of horses and riders — the one deity the Roman
cavalry adopted wholesale and carried across the empire. Protector of anyone on
the move.</p>
<p>It seemed like the right name for the thing in your tank bag.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-changed-and-what-didnt">What changed, and what didn't<a href="https://fastgis.eu/guide/blog/epona#what-changed-and-what-didnt" class="hash-link" aria-label="Direct link to What changed, and what didn't" title="Direct link to What changed, and what didn't" translate="no">​</a></h2>
<p>The rider app is now <strong>Epona</strong>. <strong>EpicRide</strong> is the desktop planner.
<strong>FastGIS</strong> is the platform and the API underneath both — the thing that serves
346 endpoints to all of them.</p>
<p>Deliberately, the infrastructure did <strong>not</strong> follow. Service names, package
identifiers and hostnames stay exactly as they were, because renaming the
plumbing to match a product decision is how you spend a fortnight, break a
deployment and gain nothing a user can see.</p>
<p>The display name now lives in one file per client, reached everywhere else
through a constant or an i18n variable — and there is a test that fails if
someone hard-codes it again. The next rename is two edits instead of an
archaeological dig.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="explore-the-trans-euro-trail-in-3d">Explore the Trans Euro Trail in 3D<a href="https://fastgis.eu/guide/blog/epona#explore-the-trans-euro-trail-in-3d" class="hash-link" aria-label="Direct link to Explore the Trans Euro Trail in 3D" title="Direct link to Explore the Trans Euro Trail in 3D" translate="no">​</a></h2>
<p>The TET explorer shipped: the whole catalogue on a globe you can fly. Three
rounds of polish went into making it feel considered rather than technical — the
camera aims itself at the trail instead of at empty ocean, the overview stays
legible at continental zoom, only one section card opens at a time, and on a
phone it gets out of the way entirely and hands the screen back to the globe.</p>
<p>A 3D globe is easy to build and hard to make legible. Most of the work is
deciding what to hide.</p>
<p>And <code>adv.fastgis.eu</code> was rebuilt as a single <strong>14,300 km ride across Europe</strong> —
one continuous story rather than a feature grid. If the product is about the
ride, the landing page should be a ride.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="weather-that-stays-true">Weather that stays true<a href="https://fastgis.eu/guide/blog/epona#weather-that-stays-true" class="hash-link" aria-label="Direct link to Weather that stays true" title="Direct link to Weather that stays true" translate="no">​</a></h2>
<p>Weather along the route now refreshes <strong>during</strong> the ride, re-sampling ahead of
you as you go. A forecast fetched three hours ago at the start of a long day is a
historical document, not a prediction — and the rain it did not mention is
still rain.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tiers-named">Tiers, named<a href="https://fastgis.eu/guide/blog/epona#tiers-named" class="hash-link" aria-label="Direct link to Tiers, named" title="Direct link to Tiers, named" translate="no">​</a></h2>
<p>Centaur, Pegasus and Alicorn — with the entitlement system rebuilt to be
rail-agnostic and <strong>subtractive from the top</strong>. The app ships complete, the
highest tier holds everything by construction, and each lower tier is defined by
what it keeps rather than by features bolted on.</p>
<p>That inversion matters more than it sounds. Additive tiering means every new
feature needs a decision in every plan, forever; subtractive means a new feature
is included by default and you decide once where to cut. Moving a feature between
tiers is one value in one registry, and admins can grant or revoke per plan at
runtime with no deploy.</p>
<p>Which mythological creature you are is, we admit, the most fun product decision
we have made all year.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="onward">Onward<a href="https://fastgis.eu/guide/blog/epona#onward" class="hash-link" aria-label="Direct link to Onward" title="Direct link to Onward" translate="no">​</a></h2>
<p>A name is a small thing that makes everything after it easier to talk about. Now
back to the riding.</p>
<hr>
<p><strong>Dig deeper:</strong> <a class="" href="https://fastgis.eu/guide/docs/product/accounts">accounts and tiers</a>, and the
<a class="" href="https://fastgis.eu/guide/docs/product/rider-app/overview">rider app</a>.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Announcement" term="Announcement"/>
        <category label="Rider app" term="Rider app"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[EpicRide: the big screen finally earns its keep]]></title>
        <id>https://fastgis.eu/guide/blog/epicride-desktop</id>
        <link href="https://fastgis.eu/guide/blog/epicride-desktop"/>
        <updated>2026-07-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Some things belong on a phone. Planning a two-week trip across four countries is]]></summary>
        <content type="html"><![CDATA[<p>Some things belong on a phone. Planning a two-week trip across four countries is
not one of them. This week was almost entirely the desktop client, and it went
from useful to genuinely good.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-planner-that-keeps-up">A planner that keeps up<a href="https://fastgis.eu/guide/blog/epicride-desktop#a-planner-that-keeps-up" class="hash-link" aria-label="Direct link to A planner that keeps up" title="Direct link to A planner that keeps up" translate="no">​</a></h2>
<p>The layers panel was rebuilt <strong>layers-first</strong>, with track cards folding into
expandable rows — the previous shape drowned once you had a dozen tracks loaded.
Route legs can be dragged to insert vias, so shaping a route is a gesture rather
than a form.</p>
<p>Elevation profiles run the full length of the route and <strong>hover-pin onto the map</strong>
as you scrub them: put the cursor on the climb and the map shows you where the
climb is. There is a measure tool, compass and rotation, per-layer styling that
survives being included in a route, and a basemap catalogue with Standard,
Satellite, Hybrid and vector styles.</p>
<p><strong>Riding modes</strong> landed too: configure them once on their own page, then assign a
mode per leg from a catalogue. The same trip can be motorway to the mountains and
gravel once you arrive, without planning it as two separate routes and
reconciling them by hand.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-real-vector-editor-in-a-browser">A real vector editor, in a browser<a href="https://fastgis.eu/guide/blog/epicride-desktop#a-real-vector-editor-in-a-browser" class="hash-link" aria-label="Direct link to A real vector editor, in a browser" title="Direct link to A real vector editor, in a browser" translate="no">​</a></h2>
<p>QGIS-style editing, and full vertex editing on <strong>group tracks</strong> — loops, TET
trails, imported GPX — which is where most browser editors give up. Live drag
preview shows the reroute as you move the point rather than after you drop it.</p>
<p>There is <strong>undo/redo</strong> at the top of the panel, and sketch persistence, so
closing the tab is an inconvenience rather than a catastrophe. Edit mode is an
explicit toggle: you cannot nudge a carefully built route by mis-clicking on it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="every-layer-in-one-place">Every layer, in one place<a href="https://fastgis.eu/guide/blog/epicride-desktop#every-layer-in-one-place" class="hash-link" aria-label="Direct link to Every layer, in one place" title="Direct link to Every layer, in one place" translate="no">​</a></h2>
<p>Curvy roads. TET with multi-country selection. MTB with trail merging along OSM
ways — because a single trail is usually a dozen disconnected OSM fragments — plus
reach-the-trailhead routing and per-trail stats. Beach seeker. Friluftsliv as
full cards with info, map-select, range and free-text filtering.</p>
<p>Select on the map and the matching result card reveals itself; select the card
and the map follows. Two-way selection sounds obvious and is the thing that makes
a results list usable at 300 items.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-library-round-tripped">The library, round-tripped<a href="https://fastgis.eu/guide/blog/epicride-desktop#the-library-round-tripped" class="hash-link" aria-label="Direct link to The library, round-tripped" title="Direct link to The library, round-tripped" translate="no">​</a></h2>
<p>Plans save and load in full. Library items show map previews and full-extent card
previews. There are landing pages per item, ratings, owner title editing, audience
sharing, and Friends and Groups. What you plan on the desktop is what your phone
already knows about.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="and-the-awkward-limits">And the awkward limits<a href="https://fastgis.eu/guide/blog/epicride-desktop#and-the-awkward-limits" class="hash-link" aria-label="Direct link to And the awkward limits" title="Direct link to And the awkward limits" translate="no">​</a></h2>
<p>Two Valhalla ceilings got fixed rather than worked around. A <strong>50-location cap</strong>
surfaced as a cryptic <code>error 150</code> with no hint that a limit existed; the client
now chunks the request and stitches the result. And the distance limits were
raised to 4000 km, so planning Norway to Portugal does not require splitting the
trip in half and pretending that was your plan.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-this-leaves-us">Where this leaves us<a href="https://fastgis.eu/guide/blog/epicride-desktop#where-this-leaves-us" class="hash-link" aria-label="Direct link to Where this leaves us" title="Direct link to Where this leaves us" translate="no">​</a></h2>
<p>There is a real division of labour now. Plan on the desktop with everything
visible at once; ride with the phone, offline, with the plan already there. Each
does what its screen is good at, and neither pretends to be the other.</p>
<hr>
<p><strong>Dig deeper:</strong> <a class="" href="https://fastgis.eu/guide/docs/product/desktop-client">EpicRide</a>.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Desktop" term="Desktop"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[1,266,642 trails]]></title>
        <id>https://fastgis.eu/guide/blog/a-million-trails</id>
        <link href="https://fastgis.eu/guide/blog/a-million-trails"/>
        <updated>2026-07-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[That is not a rounded number. That is how many mountain-bike trails, routes and]]></summary>
        <content type="html"><![CDATA[<p>That is not a rounded number. That is how many mountain-bike trails, routes and
trail POIs across Europe are now in the catalogue, extracted from OpenStreetMap,
scored, tiled and searchable from the map.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="mtb-the-full-arc-in-one-week">MTB, the full arc in one week<a href="https://fastgis.eu/guide/blog/a-million-trails#mtb-the-full-arc-in-one-week" class="hash-link" aria-label="Direct link to MTB, the full arc in one week" title="Direct link to MTB, the full arc in one week" translate="no">​</a></h2>
<p>It went from pipeline to product in six phases: the EU extraction, the <code>/mtb</code>
API, access-by-car and lift scoring with faceted search, a mobile trail overlay
and browse screen, offline trail tiles, and an on-map search library that behaves
<strong>exactly</strong> like the curvy-roads search — same gestures, same filters, same
scroll-to-card on a map tap. A rider who has learned one search should not have to
learn another.</p>
<p>The access scoring is the part we would defend hardest. A trail you cannot reach
is not a trail you can ride, so the catalogue knows whether you can get there by
car and whether there is a lift, and lets you filter on it.</p>
<p>Then <strong>performances</strong>: segment timing that records as you ride, with GPS
auto-start and auto-stop so there is no button to remember at the top of a
descent. Afterwards you get a detail screen, a chart, and an
elevation-and-surface bar — how much of that climb was gravel is a more
interesting question than how fast you did it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="friluftsliv-with-its-licences-attached">Friluftsliv, with its licences attached<a href="https://fastgis.eu/guide/blog/a-million-trails#friluftsliv-with-its-licences-attached" class="hash-link" aria-label="Direct link to Friluftsliv, with its licences attached" title="Direct link to Friluftsliv, with its licences attached" translate="no">​</a></h2>
<p>43,048 Norwegian outdoor-recreation points from four sources — and, unusually, the
API tells you the licence of <strong>every single one</strong>. Two sources are NLOD 2.0, one
is share-alike ODbL, and one is open with no named licence at all, which is not
the same thing as public domain however much you would like it to be.</p>
<p><code>GET /friluft/sources</code> returns that per source, with the attribution string you
owe and whether share-alike propagates to whatever you build. Most APIs put this
in a footer nobody reads. We put it in the response.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="loops-that-weave">Loops that weave<a href="https://fastgis.eu/guide/blog/a-million-trails#loops-that-weave" class="hash-link" aria-label="Direct link to Loops that weave" title="Direct link to Loops that weave" translate="no">​</a></h2>
<p>The loop generator can now <strong>weave curvy roads and beaches into the loop it
generates</strong>. Ask for 200 km and it threads the good roads through the middle,
rather than handing you a circle that happens to have nice bits somewhere near it.</p>
<p>We also taught it to detect and repair geometry-level dangles. Norway is full of
roads that run out to the end of a fjord and stop; a loop generator that does not
know this will cheerfully route you out and back down the same 40 km and call it
a circuit. It now spots that shape in the geometry and repairs it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-travelling-motorcyclist">The travelling motorcyclist<a href="https://fastgis.eu/guide/blog/a-million-trails#the-travelling-motorcyclist" class="hash-link" aria-label="Direct link to The travelling motorcyclist" title="Direct link to The travelling motorcyclist" translate="no">​</a></h2>
<p>Optimised routing shipped — the travelling-salesman problem, on a motorcycle.
Give it a set of stops and it orders them properly, both on the server and <strong>on
the device</strong>, so re-optimising after you add a stop works with no signal.</p>
<p>Multi-day trip planning stopped being an exercise in dragging list items around
and squinting at the total. It needed Valhalla's matrix distance ceiling raised
to cope, which is the sort of limit you only discover by hitting it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="smarter-search-three-rungs-deeper">Smarter search, three rungs deeper<a href="https://fastgis.eu/guide/blog/a-million-trails#smarter-search-three-rungs-deeper" class="hash-link" aria-label="Direct link to Smarter search, three rungs deeper" title="Direct link to Smarter search, three rungs deeper" translate="no">​</a></h2>
<p>Category and spatial context in Phase 1, a deterministic CPU spell corrector in
Phase 2, and a warmed local LLM in Phase 3 so the rare hard query does not pay an
eight-second cold start.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="also-profiles-and-locking-the-doors">Also: profiles, and locking the doors<a href="https://fastgis.eu/guide/blog/a-million-trails#also-profiles-and-locking-the-doors" class="hash-link" aria-label="Direct link to Also: profiles, and locking the doors" title="Direct link to Also: profiles, and locking the doors" translate="no">​</a></h2>
<p>User profiles with avatars, a garage and riding styles. And two security passes —
eleven launch-blocker findings in one, a fourteen-point desktop audit in the
other — plus per-tenant Postgres isolation, off by default until it has earned
its confidence.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ten-languages">Ten languages<a href="https://fastgis.eu/guide/blog/a-million-trails#ten-languages" class="hash-link" aria-label="Direct link to Ten languages" title="Direct link to Ten languages" translate="no">​</a></h2>
<p>Three became ten, with a first-run safety acknowledgment. Europe is not an
English-speaking continent and we would rather not pretend otherwise.</p>
<hr>
<p><strong>Dig deeper:</strong> <a class="" href="https://fastgis.eu/guide/docs/product/data-products">data products</a> covers MTB,
friluftsliv, beaches and curvy roads, licences included.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Data" term="Data"/>
        <category label="Rider app" term="Rider app"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[A draft, not a verdict]]></title>
        <id>https://fastgis.eu/guide/blog/storymap-editor</id>
        <link href="https://fastgis.eu/guide/blog/storymap-editor"/>
        <updated>2026-07-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Generated writing has one honest failure mode: it is confidently almost right.]]></summary>
        <content type="html"><![CDATA[<p>Generated writing has one honest failure mode: it is confidently almost right.
The fix is not a better model. It is giving you the pen.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-storymap-editor">The StoryMap editor<a href="https://fastgis.eu/guide/blog/storymap-editor#the-storymap-editor" class="hash-link" aria-label="Direct link to The StoryMap editor" title="Direct link to The StoryMap editor" translate="no">​</a></h2>
<p>Your generated story is now a <strong>draft you can edit</strong>. Changes are stored as an
<em>overlay</em> on top of the generation rather than replacing it, so the original is
never destroyed and you can always see what the machine said versus what you
said. Republishing is selective: fix one caption, rebuild that story, keep
everything else exactly as it was.</p>
<p>That overlay model is the whole design. Regenerating from scratch to fix a typo
is how people learn not to fix typos.</p>
<p>Around it, a set of blocks worth having:</p>
<ul>
<li class=""><strong>Wikipedia place context</strong>, proxied through the server so the app never talks
to Wikipedia directly, and looked up by proximity — a pass gets named as the
pass rather than described as "a mountain road".</li>
<li class=""><strong>Pickers for place info and map insets</strong>, so you can drop a small locator map
into the narrative at the point where a reader loses track of where they are.</li>
<li class=""><strong>A cover-photo picker.</strong> The title image decides whether anyone reads the
rest, and letting an algorithm choose it was optimistic.</li>
<li class=""><strong>Grouped photo slideshows</strong> instead of a wall of twenty images, which is what
four days of riding produces if nobody intervenes.</li>
</ul>
<p>Every enrich block is strictly validated before it renders, with localized
thumbnails and proper renderers per block type. A block that fails validation is
not rendered at all — no half-drawn card, no placeholder. Showing nothing is a
better failure than showing something wrong on a page you are about to send to
your friends.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="from-the-phone-in-one-tap">From the phone, in one tap<a href="https://fastgis.eu/guide/blog/storymap-editor#from-the-phone-in-one-tap" class="hash-link" aria-label="Direct link to From the phone, in one tap" title="Direct link to From the phone, in one tap" translate="no">​</a></h2>
<p>There is now an <strong>Edit story</strong> button on the phone with a pre-authenticated
handoff to the editor — you tap it, the browser opens, you are already signed in.
No copying a URL, no logging in again on a laptop while the moment passes.</p>
<p>Creating and sharing a StoryMap from mobile captures works end to end, with a
<strong>sync gate</strong> so you cannot publish a story whose photos are still sitting on the
phone, and explicit warnings when a bake would produce something incomplete. The
failure mode we were designing against is the one where you proudly send a link
and half the images are missing.</p>
<p>We also made the story page work on a phone screen, and stopped the Edit button
falling off the edge of its own card.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="beaches">Beaches<a href="https://fastgis.eu/guide/blog/storymap-editor#beaches" class="hash-link" aria-label="Direct link to Beaches" title="Direct link to Beaches" translate="no">​</a></h2>
<p>Elsewhere entirely: the <strong>beach seeker</strong> pipeline for Norway shipped — sandy-beach
discovery from open geodata, stages zero through five, sea and lake both.</p>
<p>It began as a joke about where to stop for lunch. It is now a dataset, and it is
a good demonstration of what the platform is for: the question "where can I swim
near this road" is a spatial analysis problem, and there has been a GIS sitting
underneath this app the whole time.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-like-about-this-week">What we like about this week<a href="https://fastgis.eu/guide/blog/storymap-editor#what-we-like-about-this-week" class="hash-link" aria-label="Direct link to What we like about this week" title="Direct link to What we like about this week" translate="no">​</a></h2>
<p>The editor is the moment the AI stopped being the author and became the first
draft. That is the right relationship, and everything downstream gets easier once
you accept it.</p>
<hr>
<p><strong>Dig deeper:</strong> <a class="" href="https://fastgis.eu/guide/docs/product/stories">stories</a>, and
<a class="" href="https://fastgis.eu/guide/docs/product/data-products">data products</a> for the beach data.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="AI" term="AI"/>
        <category label="Desktop" term="Desktop"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[What a field test actually costs]]></title>
        <id>https://fastgis.eu/guide/blog/the-field-test-tax</id>
        <link href="https://fastgis.eu/guide/blog/the-field-test-tax"/>
        <updated>2026-07-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[You can test an app for weeks at a desk and learn less than you learn in one]]></summary>
        <content type="html"><![CDATA[<p>You can test an app for weeks at a desk and learn less than you learn in one
afternoon with it mounted on a handlebar. This week was the bill for a real ride.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="things-that-only-break-outdoors">Things that only break outdoors<a href="https://fastgis.eu/guide/blog/the-field-test-tax#things-that-only-break-outdoors" class="hash-link" aria-label="Direct link to Things that only break outdoors" title="Direct link to Things that only break outdoors" translate="no">​</a></h2>
<ul>
<li class=""><strong>"Add stop" silently dropped the waypoint.</strong> You tap it, nothing happens, you
assume you mis-tapped and tap again. Fixed — and stops are now reorderable,
because the second thing you want after adding one is to move it.</li>
<li class=""><strong>The "My rides" toggle was missing in portrait.</strong> Present in landscape,
invisible in portrait: the orientation nobody develops in and everybody rides
in.</li>
<li class=""><strong>Navigation failures were silent.</strong> If a ride would not start, the app said
nothing whatsoever — no error, no spinner, no clue. Both the phone and CarPlay
now surface <em>why</em>, which turned a category of unreproducible field reports into
a screenshot.</li>
<li class=""><strong>CarPlay joypad zoom moved the vehicle.</strong> Zooming with the car's controls
drifted the bike icon off its actual geographic position, so the map was right
and the bike on it was lying.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="rerouting-on-a-trail-again">Rerouting on a trail, again<a href="https://fastgis.eu/guide/blog/the-field-test-tax#rerouting-on-a-trail-again" class="hash-link" aria-label="Direct link to Rerouting on a trail, again" title="Direct link to Rerouting on a trail, again" translate="no">​</a></h2>
<p>Come off a TET trail and the old behaviour rebuilt the whole thing from scratch.
Now the reroute <strong>trims the original trail</strong> — instantly, with turn-by-turn
preserved, no round trip to a server you may not be able to reach.</p>
<p>You rejoin where you left. Being handed a brand-new plan for a ride you are
already halfway through is technically a reroute and practically a betrayal.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="offline-routing-redesigned">Offline routing, redesigned<a href="https://fastgis.eu/guide/blog/the-field-test-tax#offline-routing-redesigned" class="hash-link" aria-label="Direct link to Offline routing, redesigned" title="Direct link to Offline routing, redesigned" translate="no">​</a></h2>
<p>Per-region <code>tile_dir</code> installs replaced the previous scheme, which had a habit of
working perfectly right up until it didn't — and failing in a way that was hard
to diagnose from a layby. Each region now installs into its own directory, so a
corrupt or half-downloaded pack takes out one country rather than your whole
offline setup.</p>
<p>Field fixes went in alongside it, plus some navigation and voice work recovered
from a branch that had quietly drifted out of sight.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-bluetooth-helmet-and-every-ride-at-once">A Bluetooth helmet, and every ride at once<a href="https://fastgis.eu/guide/blog/the-field-test-tax#a-bluetooth-helmet-and-every-ride-at-once" class="hash-link" aria-label="Direct link to A Bluetooth helmet, and every ride at once" title="Direct link to A Bluetooth helmet, and every ride at once" translate="no">​</a></h2>
<p>Two smaller ones with outsized effect. The app now takes microphone input from a
<strong>Bluetooth helmet</strong>, which is the difference between voice notes being a feature
and being something you actually use at 90 km/h.</p>
<p>And the map can overlay <strong>all</strong> your rides at once. Watching a season accumulate
as one tangle of lines across a country is unreasonably satisfying, and it also
shows you — instantly, without any analysis — the parts of your own region you
have somehow never ridden.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="and-a-services-page-on-the-car-screen">And a services page on the car screen<a href="https://fastgis.eu/guide/blog/the-field-test-tax#and-a-services-page-on-the-car-screen" class="hash-link" aria-label="Direct link to And a services page on the car screen" title="Direct link to And a services page on the car screen" translate="no">​</a></h2>
<p>Fuel and campgrounds ahead <strong>along your route</strong> — not "near you", which is useless
at 90 km/h — with one tap to add as a stop.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-lesson-we-keep-relearning">The lesson we keep relearning<a href="https://fastgis.eu/guide/blog/the-field-test-tax#the-lesson-we-keep-relearning" class="hash-link" aria-label="Direct link to The lesson we keep relearning" title="Direct link to The lesson we keep relearning" translate="no">​</a></h2>
<p>Every one of these was invisible until someone rode with it. So we will keep
riding with it.</p>
<hr>
<p><strong>Dig deeper:</strong> the <a class="" href="https://fastgis.eu/guide/docs/product/rider-app/overview">rider app guide</a>.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Rider app" term="Rider app"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The ride writes itself up]]></title>
        <id>https://fastgis.eu/guide/blog/stories-that-write-themselves</id>
        <link href="https://fastgis.eu/guide/blog/stories-that-write-themselves"/>
        <updated>2026-06-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[You get home from four days in the mountains with two hundred photos, a GPX]]></summary>
        <content type="html"><![CDATA[<p>You get home from four days in the mountains with two hundred photos, a GPX
track and no intention of ever writing a trip report. We think that is a shame,
so we built the thing that writes it for you.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ai-trip-stories">AI trip stories<a href="https://fastgis.eu/guide/blog/stories-that-write-themselves#ai-trip-stories" class="hash-link" aria-label="Direct link to AI trip stories" title="Direct link to AI trip stories" translate="no">​</a></h2>
<p>The enrichment gateway shipped, and with it the flagship: point it at a recorded
ride and it produces a <strong>StoryMap</strong> — a scrolling narrative where the 3D globe
flies along with the text, your photos land where you took them, and the prose is
grounded in where you actually were rather than invented.</p>
<p>Generation is a job, not a request. You start it, it runs in the background, you
get a notification. Writing four days of riding takes longer than an HTTP timeout
allows, and pretending otherwise produces a spinner nobody trusts.</p>
<p>It reads your media too. Audio and video samples are transcribed through the
media-AI gateway, so the voice note you recorded at a viewpoint — helmet on,
engine running, describing the road you just rode — becomes part of the story
instead of a file you never open again.</p>
<p>Then it narrates. Text-to-speech per scene, in multiple languages, with the
narration switching when the reader switches language — so a story from a
Norwegian ride can be read aloud in Italian to someone who will never see the
fjord otherwise. The translation and the speech are generated together, so the
voice matches the text on screen rather than reading an English original with an
accent.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="getting-it-honest">Getting it honest<a href="https://fastgis.eu/guide/blog/stories-that-write-themselves#getting-it-honest" class="hash-link" aria-label="Direct link to Getting it honest" title="Direct link to Getting it honest" translate="no">​</a></h2>
<p>The interesting work was not generation, it was <strong>restraint</strong>. Three fixes tell
the story:</p>
<ul>
<li class=""><strong>Each narrative section is used by at most one scene.</strong> Without that, the
story quietly says the same thing three times in slightly different words —
the most recognisable tell of machine-written prose.</li>
<li class=""><strong>Photo references drifted.</strong> The model would cite an image that was not there,
so headings, prose and narration are now reconciled against the media that
actually exists before anything renders.</li>
<li class=""><strong>The camera and the rider sit on the terrain surface</strong>, rather than floating
above it or sinking below. It sounds cosmetic until you watch your own bike
ride straight through a mountain.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-library-filled-in">The library, filled in<a href="https://fastgis.eu/guide/blog/stories-that-write-themselves#the-library-filled-in" class="hash-link" aria-label="Direct link to The library, filled in" title="Direct link to The library, filled in" translate="no">​</a></h2>
<p>Per-capture storage actions, direct sample visualisation, inline previews you can
view and share over the map, and media that <strong>keeps streaming from the server
after you delete the local copy</strong> to free space. Your phone fills up on a long
trip; that should cost you storage, not memories.</p>
<p>Orphaned captures — samples whose session record went missing — now recover
instead of dead-ending on a screen that told you nothing.</p>
<p>Also: photos captured in-app keep their GPS. Expo's image re-encode was silently
stripping the EXIF location — every photo any of us had taken in the app was
ungeotagged, and nothing had ever complained. Coordinates are now injected at
capture time, and everything shot before the fix was backfilled server-side from
the position the app had already recorded alongside it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters">Why this matters<a href="https://fastgis.eu/guide/blog/stories-that-write-themselves#why-this-matters" class="hash-link" aria-label="Direct link to Why this matters" title="Direct link to Why this matters" translate="no">​</a></h2>
<p>A ride you cannot show anyone is a ride that slowly stops existing. We would like
the good ones to last.</p>
<hr>
<p><strong>Dig deeper:</strong> <a class="" href="https://fastgis.eu/guide/docs/product/stories">stories</a> covers generation, editing and
publishing.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="AI" term="AI"/>
        <category label="Rider app" term="Rider app"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[On the car screen, and off the tarmac]]></title>
        <id>https://fastgis.eu/guide/blog/carplay-and-the-trail</id>
        <link href="https://fastgis.eu/guide/blog/carplay-and-the-trail"/>
        <updated>2026-06-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Two threads this week, and they meet in the same place: the moment you stop]]></summary>
        <content type="html"><![CDATA[<p>Two threads this week, and they meet in the same place: the moment you stop
looking at a phone and start riding.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="carplay">CarPlay<a href="https://fastgis.eu/guide/blog/carplay-and-the-trail#carplay" class="hash-link" aria-label="Direct link to CarPlay" title="Direct link to CarPlay" translate="no">​</a></h2>
<p>The navigation HUD now renders on the car screen. Getting there took a scaffold,
a gated config plugin, an absolute-fill layout and a self-healing install — the
sort of list that looks like nothing and takes a week.</p>
<p>CarPlay is unusual to build for because you cannot iterate on a simulator and
trust it; the head unit has opinions the simulator does not. It is on a real
device now, it survives a reconnect, and the install steps are written down so
the next person does not rediscover them.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-trail-problem">The trail problem<a href="https://fastgis.eu/guide/blog/carplay-and-the-trail#the-trail-problem" class="hash-link" aria-label="Direct link to The trail problem" title="Direct link to The trail problem" translate="no">​</a></h2>
<p>Here is a scenario we got wrong, and then got right.</p>
<p>You are riding a road bike profile toward a trail. The router knows the trail
exists. What should happen when you reach it?</p>
<p>The naive answer is "recompute", and the naive answer strands you: the recompute
throws away the trail and sends you around by road, because by every metric a
router optimises for, the road is better.</p>
<p>So we made the vehicle switch on a trail into an <strong>approach rebuild</strong> — car
profile to the trail head, bike profile on the trail itself — and made <em>keeping
the trail</em> the default, with road-to-end as an explicit choice you make rather
than one made for you. The router is very good at answering the question it was
asked. Most of this work is asking it a better one.</p>
<p>Detecting that you are on a trail at all turned out to need two independent
signals: a <strong>structural</strong> one — a routing leg with no manoeuvres in it, because
trails do not have junctions to announce — and an on-trail projection guard that
checks where you actually are against the trail geometry. Either alone produces
false positives on a long empty road. Together they hold up.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="freezes-found-and-killed">Freezes, found and killed<a href="https://fastgis.eu/guide/blog/carplay-and-the-trail#freezes-found-and-killed" class="hash-link" aria-label="Direct link to Freezes, found and killed" title="Direct link to Freezes, found and killed" translate="no">​</a></h2>
<p>Three of them, all in the same fortnight:</p>
<ul>
<li class="">A <strong>12,000-point cap</strong> on the painted route line. A long TET trip was handing
the map more geometry than it could paint, and the whole app stopped.</li>
<li class="">A <strong>stationary guard</strong> on rerouting. Parked with GPS jitter, the app recomputed
forever. Now it notices you are not moving.</li>
<li class="">A degenerate curvy-loop line that failed geometry validation at startup.</li>
</ul>
<p>None of these are features. All of them are the difference between an app you
trust on a long day and one you don't.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="smarter-search">Smarter search<a href="https://fastgis.eu/guide/blog/carplay-and-the-trail#smarter-search" class="hash-link" aria-label="Direct link to Smarter search" title="Direct link to Smarter search" translate="no">​</a></h2>
<p>Self-hosted Photon for European geocoding, a <strong>zero-result relaxation ladder</strong>,
and an LLM rung behind an explicit <code>?deep</code> flag. When the search finds nothing,
the phone loosens the query a rung at a time rather than shrugging.</p>
<p>The LLM rung needed the most care, and the fix was mostly saying no to it: the
prompt is hardened against inventing place names, because a geocoder that
confidently returns a village that does not exist is worse than one that returns
nothing at all. Tuning the prompt to a "rules" variant took the small local model
from 71% to 88% on our benchmark, which is the difference between a feature and a
liability.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next">Next<a href="https://fastgis.eu/guide/blog/carplay-and-the-trail#next" class="hash-link" aria-label="Direct link to Next" title="Direct link to Next" translate="no">​</a></h2>
<p>More car screen, more trail. The two ends of the same ride.</p>
<hr>
<p><strong>Dig deeper:</strong> the <a class="" href="https://fastgis.eu/guide/docs/product/rider-app/overview">rider app guide</a> covers
navigation and the trail behaviour.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Rider app" term="Rider app"/>
        <category label="Routing" term="Routing"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Your data, in the tools you already use]]></title>
        <id>https://fastgis.eu/guide/blog/standards-and-sharing</id>
        <link href="https://fastgis.eu/guide/blog/standards-and-sharing"/>
        <updated>2026-06-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A platform that can only be read by its own app is a trap, however nice the app]]></summary>
        <content type="html"><![CDATA[<p>A platform that can only be read by its own app is a trap, however nice the app
is. This week FastGIS learned to speak the standards, and to let riders see each
other move.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ogc-properly">OGC, properly<a href="https://fastgis.eu/guide/blog/standards-and-sharing#ogc-properly" class="hash-link" aria-label="Direct link to OGC, properly" title="Direct link to OGC, properly" translate="no">​</a></h2>
<p>Not a checkbox — the real thing, over your own GRASS data:</p>
<ul>
<li class=""><strong>OGC API — Features</strong> for your catalogue and saved routes</li>
<li class=""><strong>OGC API — Processes</strong>, exposing allowed GRASS modules as async jobs, with the
<code>callback</code> conformance class so a job can push to a subscriber</li>
<li class=""><strong>OGC API — Records and Maps</strong> over per-user datasets</li>
<li class=""><strong>pygeoapi and STAC sidecars</strong> over PostGIS, behind a forward-auth gate</li>
</ul>
<p>Which means: open QGIS, add a connection, and your analysis is there. No export
step, no format conversion, no "can you send me a shapefile".</p>
<p>Processes is the one we think is underrated. Your allowed GRASS modules are
exposed as OGC jobs — you <code>POST</code> an execution, poll the job, collect the results —
and with the <code>callback</code> class the job pushes to you instead. A slope analysis
over a big raster stops being something you sit and watch.</p>
<p>We also fixed the small things that decide whether a standards client actually
works — advertising the <code>service-desc</code> link so QGIS accepts the landing page,
emitting <code>https</code> hrefs from behind the proxy, and parsing GRASS's binary
floating-point range so raster metadata is right.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="riders-on-the-map-live">Riders on the map, live<a href="https://fastgis.eu/guide/blog/standards-and-sharing#riders-on-the-map-live" class="hash-link" aria-label="Direct link to Riders on the map, live" title="Direct link to Riders on the map, live" translate="no">​</a></h2>
<p>Realtime location sharing shipped: a WebSocket fan-out over Redis pub/sub,
friends, and named groups. Share to the people you choose, for as long as you
choose — it is a session you start, not a setting you leave on and forget.</p>
<p>Friendship is two-sided; you request, they accept. Nobody appears in your
audience because someone typed your name. The desktop viewer grew a "Live Riders"
panel, and the phone shows peer markers moving in something very close to real
time, because Redis pub/sub means a position update fans out to twelve people
without twelve database round trips.</p>
<p>Per-client API tokens landed at the same time, so a phone and a desktop browser
can hold <strong>separate concurrent credentials</strong> for the same account. Revoking your
laptop should not log out your motorcycle, and losing your phone should not mean
re-authenticating everything you own from the side of a road.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="surveys-logbooks-and-android">Surveys, logbooks and Android<a href="https://fastgis.eu/guide/blog/standards-and-sharing#surveys-logbooks-and-android" class="hash-link" aria-label="Direct link to Surveys, logbooks and Android" title="Direct link to Surveys, logbooks and Android" translate="no">​</a></h2>
<p>A spec-driven survey renderer on the phone, a designer on the desktop, and a
schema contract between them — so a form you design in a browser renders natively
on a phone in a field with no signal, without anyone shipping an app update.</p>
<p>Turn-by-turn trips now record themselves into a <strong>ride logbook</strong> automatically.
No "start recording" to forget; if you navigated it, it is in the book.</p>
<p>And Android got its first two phases in quick succession: an online-only build,
then on-device Valhalla routing — the same offline engine iOS runs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-pattern">The pattern<a href="https://fastgis.eu/guide/blog/standards-and-sharing#the-pattern" class="hash-link" aria-label="Direct link to The pattern" title="Direct link to The pattern" translate="no">​</a></h2>
<p>Everything here is the same instinct. Your rides, your data, your tools — reachable
from whatever you already use, and shareable exactly as far as you decide.</p>
<hr>
<p><strong>Dig deeper:</strong> <a class="" href="https://fastgis.eu/guide/docs/product/riding-together">riding together</a> covers sharing
end to end, and the <a class="" href="https://fastgis.eu/guide/docs/developers/">developer docs</a> have the OGC recipes.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Platform" term="Platform"/>
        <category label="Realtime" term="Realtime"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Signal is optional]]></title>
        <id>https://fastgis.eu/guide/blog/offline-first</id>
        <link href="https://fastgis.eu/guide/blog/offline-first"/>
        <updated>2026-05-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Here is the uncomfortable truth about a navigation app for adventure riding: the]]></summary>
        <content type="html"><![CDATA[<p>Here is the uncomfortable truth about a navigation app for adventure riding: the
better the road, the worse the coverage. The places worth going are exactly the
places your phone gives up.</p>
<p>So this fortnight we made the phone stop needing us.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="routing-on-the-device">Routing, on the device<a href="https://fastgis.eu/guide/blog/offline-first#routing-on-the-device" class="hash-link" aria-label="Direct link to Routing, on the device" title="Direct link to Routing, on the device" translate="no">​</a></h2>
<p>Valhalla now runs <strong>on the phone</strong>. Not a cached route, not a downloaded
corridor — the actual routing engine, with the actual tiles, computing an actual
route with no server in sight. Lose signal mid-ride and navigation simply
continues, including a full recompute if you miss a turn.</p>
<p>Getting a C++ routing engine onto a phone is less glamorous than it sounds. The
tiles install per region rather than as one monolith, so adding Austria does not
mean re-downloading France, and the engine is bridged natively rather than run
through a JavaScript shim that would melt the battery.</p>
<p>Spoken navigation came with it, and the rider picks the language <strong>and voice</strong>
independently of the app's interface language. Your phone can be in English while
the directions are in Italian, which sounds like a small thing until you are in
Italy trying to match a spoken street name to a sign that spells it differently
in your head.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-trans-euro-trail-bundled">The Trans Euro Trail, bundled<a href="https://fastgis.eu/guide/blog/offline-first#the-trans-euro-trail-bundled" class="hash-link" aria-label="Direct link to The Trans Euro Trail, bundled" title="Direct link to The Trans Euro Trail, bundled" translate="no">​</a></h2>
<p>The TET augmentation pipeline landed, and it is not a map overlay. The trail
data is folded into the <strong>routing graph itself</strong>, so the router knows a trail is
rideable rather than drawing a line over roads it would rather use. That is
deployed to the EU server and available offline through baked bundles: import a
country, ride it without a connection.</p>
<p>The difference shows the first time you ask for a route that ends on a trail. An
overlay gives you a pretty line and directions to the nearest tarmac. An
augmented graph takes you down the trail.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="speaking-eleven-languages">Speaking eleven languages<a href="https://fastgis.eu/guide/blog/offline-first#speaking-eleven-languages" class="hash-link" aria-label="Direct link to Speaking eleven languages" title="Direct link to Speaking eleven languages" translate="no">​</a></h2>
<p>Full Italian and Spanish translations shipped with an in-app language picker, on
top of the i18n groundwork. A rider in Palermo and a rider in Bergen should get
the same app, not the same app in someone else's language.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="also-this-month">Also this month<a href="https://fastgis.eu/guide/blog/offline-first#also-this-month" class="hash-link" aria-label="Direct link to Also this month" title="Direct link to Also this month" translate="no">​</a></h2>
<p>Multi-user accounts arrived, with a password policy enforced on every path that
touches a credential. Siri phrases and App Intents mean you can start a ride
without touching the screen. The capture workflow was rebuilt around an
explicit Idle → Recording → Paused shell, and then audited four times over —
thirteen bug fixes in one pass, orientation handling in another.</p>
<p>That last part is not glamorous. It is also why the app will still be recording
when you get back.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next">Next<a href="https://fastgis.eu/guide/blog/offline-first#next" class="hash-link" aria-label="Direct link to Next" title="Direct link to Next" translate="no">​</a></h2>
<p>Offline is not a feature you finish. It is a promise you keep extending: maps,
then routing, then search, then everything else. We are two steps in.</p>
<hr>
<p><strong>Dig deeper:</strong> <a class="" href="https://fastgis.eu/guide/docs/product/rider-app/offline-data">offline data</a> covers what
downloads and when, and the <a class="" href="https://fastgis.eu/guide/docs/developers/">developer docs</a> have the
manifest format if you want to build against it.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Rider app" term="Rider app"/>
        <category label="Routing" term="Routing"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The map grows a bike]]></title>
        <id>https://fastgis.eu/guide/blog/the-map-grows-a-bike</id>
        <link href="https://fastgis.eu/guide/blog/the-map-grows-a-bike"/>
        <updated>2026-05-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A week ago FastGIS was a browser tool for running GRASS modules. This week it]]></summary>
        <content type="html"><![CDATA[<p>A week ago FastGIS was a browser tool for running GRASS modules. This week it
learned to plan a motorcycle route — and it turns out those two things want the
same foundations.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="routing-arrives">Routing arrives<a href="https://fastgis.eu/guide/blog/the-map-grows-a-bike#routing-arrives" class="hash-link" aria-label="Direct link to Routing arrives" title="Direct link to Routing arrives" translate="no">​</a></h2>
<p>Valhalla is now wired in properly: real turn-by-turn directions, isochrones,
time/distance matrices, and — the one we were most excited about — a <strong>loop
generator</strong>. Give it a start point and a distance, and it samples waypoints by
angle, tunes how far it reaches, and hands back a ride that comes home.</p>
<p>Most routers can get you from A to B. Getting you from A back to A is a
genuinely harder problem, and a much more useful one for anyone riding for the
pleasure of it rather than to arrive. A naive implementation picks three random
points and gives you a triangle with a motorway down one side. Sampling by
<em>angle</em> from the start, then tuning how far out each arm reaches, is what turns
that into something you would actually ride. It also persists its state, so
tweaking a distance does not throw away the shape you liked.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-phone-not-just-a-browser">A phone, not just a browser<a href="https://fastgis.eu/guide/blog/the-map-grows-a-bike#a-phone-not-just-a-browser" class="hash-link" aria-label="Direct link to A phone, not just a browser" title="Direct link to A phone, not just a browser" translate="no">​</a></h2>
<p>The mobile foundation landed alongside it: GPS integration, POI clustering, and
the first round of usability work that only shows up when you actually hold the
thing at a junction. Clustering sounds like a detail until you zoom out over a
country and the map tries to draw forty thousand individual pins.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="and-the-desktop-tools-kept-moving">And the desktop tools kept moving<a href="https://fastgis.eu/guide/blog/the-map-grows-a-bike#and-the-desktop-tools-kept-moving" class="hash-link" aria-label="Direct link to And the desktop tools kept moving" title="Direct link to And the desktop tools kept moving" translate="no">​</a></h2>
<p>The vector editor grew up considerably — digitise, select, move, vertex-edit,
write attributes. The DB Manager arrived, so you can inspect the SQLite or
PostgreSQL table behind a layer without leaving the browser.</p>
<p>There's a measure tool that does proper <strong>geodetic</strong> length and area rather than
pretending the world is flat — over a few hundred kilometres at Nordic latitudes
the difference is not academic. And the GRASS → mago-3d-terrainer → CesiumJS
pipeline turns a raster DEM into quantized-mesh terrain you can actually fly
over, which is a much better way to understand a mountain pass than a contour
map.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-this-is-going">Where this is going<a href="https://fastgis.eu/guide/blog/the-map-grows-a-bike#where-this-is-going" class="hash-link" aria-label="Direct link to Where this is going" title="Direct link to Where this is going" translate="no">​</a></h2>
<p>The interesting thing about building a rider app on top of a spatial analysis
platform is that the analysis does not go away — it becomes the thing that makes
the routes good. Every algorithm already in GRASS is, potentially, a way of
deciding which road is worth riding.</p>
<p>We are just getting started on that.</p>
<hr>
<p><strong>Dig deeper:</strong> the <a class="" href="https://fastgis.eu/guide/docs/product/web-app/routing-tool">routing tool</a> and the
<a class="" href="https://fastgis.eu/guide/docs/product/web-app/terrain-builder">terrain builder</a> both have guides now.</p>]]></content>
        <author>
            <name>Massimo Di Stefano</name>
            <uri>https://github.com/epifanio</uri>
        </author>
        <category label="Changelog" term="Changelog"/>
        <category label="Rider app" term="Rider app"/>
        <category label="Routing" term="Routing"/>
    </entry>
</feed>