Catmull-Rom route smoothing
The gold polyline on the map is interpolated through the GPS points using a centripetal Catmull-Rom spline. The result is a curve that passes exactly through every point you recorded, but with smooth tangents at every joint — no acute corners on straight roads, no overshoots on tight switchbacks.
Why not draw straight segments?
A LineString built from raw GPS has visible kinks at every joint. The kinks come from sub-degree heading changes that are real but microscopic; rendered as straight segments they look like the walker had spasms. A Bézier-style curve sweeps through the same points with continuous first derivatives — the line "bends" in the same direction the walker did.
Why centripetal Catmull-Rom?
Three Catmull-Rom variants exist: uniform (α = 0), centripetal (α = 0.5), chordal (α = 1).
- Uniform is the textbook version. Fast, but creates cusps and self-intersections when point spacing is uneven — a common case for GPS traces near sharp turns where the walker stopped to look at the map.
- Chordal never self-intersects but creates wide bulges in long-segment regions.
- Centripetal is the practical sweet spot: never self- intersects, never overshoots, parameterised by α = 0.5.
Centripetal also has a desirable physical interpretation: each
control segment is parameterised by Δt = sqrt(distance), so
points spaced 100 m apart get the same curve weight as points
spaced 10 m apart in tight terrain. The rendered curve looks
"natural" regardless of GPS density.
The math
For four consecutive points P₀, P₁, P₂, P₃ and α = 0.5, the parameter spacing is:
t_i = t_{i-1} + |P_i - P_{i-1}|^α
The interpolated curve segment between P₁ and P₂ is:
A_1 = ((t_1 - t)/(t_1 - t_0)) * P_0 + ((t - t_0)/(t_1 - t_0)) * P_1
A_2 = ((t_2 - t)/(t_2 - t_1)) * P_1 + ((t - t_1)/(t_2 - t_1)) * P_2
A_3 = ((t_3 - t)/(t_3 - t_2)) * P_2 + ((t - t_2)/(t_3 - t_2)) * P_3
B_1 = ((t_2 - t)/(t_2 - t_0)) * A_1 + ((t - t_0)/(t_2 - t_0)) * A_2
B_2 = ((t_3 - t)/(t_3 - t_1)) * A_2 + ((t - t_1)/(t_3 - t_1)) * A_3
C = ((t_2 - t)/(t_2 - t_1)) * B_1 + ((t - t_1)/(t_2 - t_1)) * B_2
C is the curve point at parameter t ∈ [t_1, t_2]. Sample 8–16
intermediate t values per segment, render as a polyline.
Performance
For a typical 5,000-point trip:
- Plain LineString: 5,000 segments → renders trivially.
- Catmull-Rom with 12 samples per segment: 60,000 polyline segments. Still trivial (modern browsers GPU-render polylines at hundreds of kHz).
- Memory: 60,000 × 16 bytes (lat/lng pair) = ~1 MB per trip cached in the page.
We compute on the client to keep server CPU free; the math is ~3 ms for 5,000 points.
Edge cases
- First / last point — one-sided. We duplicate the endpoint as a phantom point P₀ at the start and P₃ at the end. The curve still starts and ends exactly at the real endpoint; only the tangent is slightly less constrained.
- Identical adjacent points —
Δt = 0, division by zero. We jitter one of them by 1 µm, imperceptible. - Mode boundaries — a walking → cycling transition shouldn't curve smoothly across (the modes have different visual styles). We compute Catmull-Rom per mode segment, not across the whole trip.
Related
- GPS drift elimination — runs first.
- Mapbox map matching — alternative for driving / cycling segments where snap-to-road wins.
- Transport modes — segment rendering.
Need help? Contact support · Where Is Tereza?