ADAM LIN

A. LIN / SESSION: ENGINEERING LOG / DATA: DEMO

LOG000%

· LOG ENTRY

Building the lap data pipeline

How this site's track stage gets its data: OSM outline extraction, spline resampling, curvature, and a disclosed point-mass model generating the demo channels.

The track map on the home page — the one you drive around by scrolling — runs on a generated dataset. I want to walk through how that dataset gets made, partly because it's a fun little pipeline, and partly because if you put telemetry-looking data on a page, you should be able to say exactly where it came from.

Starting with the real track

The circuit is the Sonoma Raceway Karting Center. The outline comes from OpenStreetMap (ways 230287123 / 127 / 129 / 131 / 133 / 139, ODbL — that's why the footer credits OSM contributors wherever the map shows).

Here's the first wrinkle: OSM doesn't store the kart track as one neat loop. The facility can be run in different configurations, so it's mapped as a network — ten junction nodes connected by fifteen arcs. "The track" is really a path you have to compose out of arcs. I picked the composition that matches the National Circuit layout and confirmed it against the real thing: 1,203.8 m once splined, run clockwise, T1 at the northernmost corner, start/finish on the straight leading into it. All eleven corner labels are verified, which is why they get to render as callouts on the map.

From outline to geometry

scripts/build-lap-data.mjs turns that outline into the dataset. It's deterministic — same input, byte-identical output — and dependency-free. The steps:

  1. Compose the chosen arcs into one ordered centerline.
  2. Project the GPS coordinates into local meters. Plain equirectangular projection is fine at track scale, with y flipped because SVG's y-axis points down.
  3. Fit a centripetal Catmull-Rom spline through the loop. The centripetal variant matters: it smooths the raw polyline into a track-shaped curve without the kinks and overshoot a naive fit can produce.
  4. Resample along arc length every 5 m — 242 samples for this lap. Even spacing makes everything downstream simple: lookups become arithmetic.
  5. Estimate curvature at each sample by fitting a circle through it and its two neighbors (a three-point circumcircle), lightly smoothed.

Curvature is the star of the show. It's the one number that says how tight the track is at any point, and that's what decides how fast a car can be there. The corners fall out of it too — they're the local curvature maxima. I assigned the T1–T11 numbering by hand and verified it against the confirmed layout.

Making up honest speed data

Now the honest part: the speed and lateral-g traces on the site are generated, not measured. These aren't logged laps. But rather than draw a plausible-looking squiggle, I generate the channels from the track geometry using a model whose assumptions are fully disclosed — a point-mass quasi-steady-state pass:

  • In a corner, speed is capped by grip: v = min(vmax, sqrt(aLat / |k|)). Tighter corner, bigger curvature k, lower cap.
  • A forward pass limits how quickly speed can build out of corners (the acceleration limit).
  • A backward pass limits how quickly it can shed into them (the braking limit). Running that pass backward is the trick that puts braking zones before corners, where they belong.
  • Iterate until nothing changes.

The parameters sit at the top of the script: 105 km/h top speed, 1.9 g lateral, 1.6 g braking, 0.55 g acceleration — roughly kart-shaped numbers. And every place the data renders, it's labeled demo. That's enforced by the test suite, not left to good intentions.

The swap contract

One nice property fell out of building it this way: scripts/import-aim-csv.mjs can take an AiM RaceStudio CSV export — distance, GPS position, GPS speed, lateral g — and run it through the same 5 m resample, regenerating the same file with source: "aim-racestudio" and a real session label. It's proven against a synthetic fixture in the test flow: the site builds and renders a swapped dataset with zero component changes. So if I ever want the map running on real logged laps instead of a model, it's a one-file change.

One last architectural note: everything downstream of the dataset is hand-authored. The content stations, and the timing sectors derived from them, live in lap-course.ts — so the sectors follow where the content sits on the page, not the geometry. The map is scenery that refuses to lie about its data, and that's the whole trick.

← Return to session sheet