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.

Need help? Contact support · Where Is Tereza?