Merge pull request #63 from AmanTahiliani/feat/issue-20-combined-position-graph-track-map-scrubb

Combined position-graph + track-map scrubber (#20)
This commit is contained in:
Aman Tahiliani
2026-07-11 18:20:04 -04:00
committed by GitHub
12 changed files with 1209 additions and 15 deletions

259
internal/web/replay.go Normal file
View File

@@ -0,0 +1,259 @@
package web
import (
"context"
"errors"
"math"
"net/http"
"sort"
"strconv"
"sync"
"time"
"github.com/AmanTahiliani/box-box/internal/models"
)
const (
defaultReplayIntervalMS = 5000
maxReplayFrames = 3000
replayFetchConcurrency = 4
)
type replayDataClient interface {
GetDriversForSession(sessionKey int) ([]models.Driver, error)
GetLocation(sessionKey, driverNumber int) ([]models.Location, error)
}
type replayFramesResponse struct {
SessionKey int `json:"session_key"`
Interval int `json:"interval_ms"`
StartTime string `json:"start_time"`
Frames []replayFrame `json:"frames"`
}
type replayFrame struct {
T int64 `json:"t"`
Cars map[string]replayCar `json:"cars"`
}
type replayCar struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
func (s *Server) handleReplayFrames(w http.ResponseWriter, r *http.Request) {
sessionKey, err := strconv.Atoi(r.URL.Query().Get("session_key"))
if err != nil || sessionKey == 0 {
http.Error(w, "session_key required", http.StatusBadRequest)
return
}
intervalMS := defaultReplayIntervalMS
rawInterval := r.URL.Query().Get("interval_ms")
if rawInterval != "" {
parsed, err := strconv.Atoi(rawInterval)
if err != nil {
http.Error(w, "invalid interval_ms", http.StatusBadRequest)
return
}
if parsed > intervalMS {
intervalMS = parsed
}
}
resp, err := assembleReplayFrames(r.Context(), s.client, sessionKey, intervalMS)
if err != nil {
writeError(w, err, http.StatusInternalServerError, s.client.LastResponseWasStale())
return
}
writeJSON(w, resp)
}
func assembleReplayFrames(ctx context.Context, client replayDataClient, sessionKey, intervalMS int) (replayFramesResponse, error) {
if intervalMS < defaultReplayIntervalMS {
intervalMS = defaultReplayIntervalMS
}
resp := replayFramesResponse{
SessionKey: sessionKey,
Interval: intervalMS,
Frames: []replayFrame{},
}
drivers, err := client.GetDriversForSession(sessionKey)
if err != nil {
return resp, err
}
driverNumbers := uniqueDriverNumbers(drivers)
if len(driverNumbers) == 0 {
return resp, nil
}
series, err := fetchReplayLocationSeries(ctx, client, sessionKey, driverNumbers)
if err != nil && len(series) == 0 {
return resp, err
}
start, ok := earliestReplayLocationTime(series)
if !ok {
return resp, nil
}
resp.StartTime = start.Format(time.RFC3339Nano)
resp.Frames = snapReplayFrames(series, start, intervalMS)
return resp, nil
}
func uniqueDriverNumbers(drivers []models.Driver) []int {
seen := make(map[int]bool, len(drivers))
numbers := make([]int, 0, len(drivers))
for _, driver := range drivers {
if driver.DriverNumber <= 0 || seen[driver.DriverNumber] {
continue
}
seen[driver.DriverNumber] = true
numbers = append(numbers, driver.DriverNumber)
}
sort.Ints(numbers)
return numbers
}
func fetchReplayLocationSeries(ctx context.Context, client replayDataClient, sessionKey int, driverNumbers []int) (map[int][]models.Location, error) {
type result struct {
driver int
locs []models.Location
err error
}
sem := make(chan struct{}, replayFetchConcurrency)
results := make(chan result, len(driverNumbers))
var wg sync.WaitGroup
for _, driverNumber := range driverNumbers {
driverNumber := driverNumber
wg.Add(1)
go func() {
defer wg.Done()
select {
case sem <- struct{}{}:
defer func() { <-sem }()
case <-ctx.Done():
results <- result{driver: driverNumber, err: ctx.Err()}
return
}
locs, err := client.GetLocation(sessionKey, driverNumber)
results <- result{driver: driverNumber, locs: locs, err: err}
}()
}
wg.Wait()
close(results)
series := make(map[int][]models.Location, len(driverNumbers))
var joined error
for result := range results {
if result.err != nil {
joined = errors.Join(joined, result.err)
continue
}
if len(result.locs) > 0 {
series[result.driver] = result.locs
}
}
return series, joined
}
func earliestReplayLocationTime(series map[int][]models.Location) (time.Time, bool) {
var start time.Time
for _, locs := range series {
for _, loc := range locs {
t, err := time.Parse(time.RFC3339Nano, loc.Date)
if err != nil {
continue
}
if start.IsZero() || t.Before(start) {
start = t
}
}
}
if start.IsZero() {
return time.Time{}, false
}
return start, true
}
func snapReplayFrames(series map[int][]models.Location, start time.Time, intervalMS int) []replayFrame {
type accumulator struct {
t int64
cars map[string]replayCar
nearest map[string]int64
}
interval := int64(intervalMS)
framesByIndex := make(map[int]*accumulator)
for driverNumber, locs := range series {
driverKey := strconv.Itoa(driverNumber)
for _, loc := range locs {
if !isFiniteFloat(loc.X) || !isFiniteFloat(loc.Y) {
continue
}
t, err := time.Parse(time.RFC3339Nano, loc.Date)
if err != nil {
continue
}
offset := t.Sub(start).Milliseconds()
if offset < 0 {
continue
}
index := int((offset + interval/2) / interval)
if index < 0 || index >= maxReplayFrames {
continue
}
frameT := int64(index) * interval
distance := absInt64(offset - frameT)
acc, ok := framesByIndex[index]
if !ok {
acc = &accumulator{
t: frameT,
cars: make(map[string]replayCar),
nearest: make(map[string]int64),
}
framesByIndex[index] = acc
}
if prev, ok := acc.nearest[driverKey]; ok && prev <= distance {
continue
}
acc.nearest[driverKey] = distance
acc.cars[driverKey] = replayCar{X: loc.X, Y: loc.Y}
}
}
indexes := make([]int, 0, len(framesByIndex))
for index, acc := range framesByIndex {
if len(acc.cars) > 0 {
indexes = append(indexes, index)
}
}
sort.Ints(indexes)
frames := make([]replayFrame, 0, len(indexes))
for _, index := range indexes {
acc := framesByIndex[index]
frames = append(frames, replayFrame{T: acc.t, Cars: acc.cars})
}
return frames
}
func absInt64(v int64) int64 {
if v < 0 {
return -v
}
return v
}
func isFiniteFloat(v float64) bool {
return !math.IsNaN(v) && !math.IsInf(v, 0)
}

181
internal/web/replay_test.go Normal file
View File

@@ -0,0 +1,181 @@
package web
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/AmanTahiliani/box-box/internal/models"
)
type fakeReplayClient struct {
drivers []models.Driver
locs map[int][]models.Location
err error
mu sync.Mutex
inFlight int
maxInFlight int
delay time.Duration
}
func (f *fakeReplayClient) GetDriversForSession(sessionKey int) ([]models.Driver, error) {
if f.err != nil {
return nil, f.err
}
return f.drivers, nil
}
func (f *fakeReplayClient) GetLocation(sessionKey, driverNumber int) ([]models.Location, error) {
f.mu.Lock()
f.inFlight++
if f.inFlight > f.maxInFlight {
f.maxInFlight = f.inFlight
}
f.mu.Unlock()
if f.delay > 0 {
time.Sleep(f.delay)
}
f.mu.Lock()
f.inFlight--
f.mu.Unlock()
return f.locs[driverNumber], nil
}
func TestAssembleReplayFramesSnapsNearestSamplesAndOmitsEmptyDrivers(t *testing.T) {
start := time.Date(2025, 5, 25, 13, 0, 0, 0, time.UTC)
client := &fakeReplayClient{
drivers: []models.Driver{
{DriverNumber: 1},
{DriverNumber: 4},
{DriverNumber: 16},
},
locs: map[int][]models.Location{
1: {
{Date: start.Add(1 * time.Second).Format(time.RFC3339Nano), X: 10, Y: 20},
{Date: start.Add(4 * time.Second).Format(time.RFC3339Nano), X: 40, Y: 80},
{Date: start.Add(6 * time.Second).Format(time.RFC3339Nano), X: 60, Y: 120},
},
4: {
{Date: start.Add(5 * time.Second).Format(time.RFC3339Nano), X: 100, Y: 200},
},
16: {},
},
}
resp, err := assembleReplayFrames(context.Background(), client, 99, 5000)
if err != nil {
t.Fatalf("assembleReplayFrames() error = %v", err)
}
if resp.SessionKey != 99 || resp.Interval != 5000 {
t.Fatalf("response metadata = %+v", resp)
}
if resp.StartTime != start.Add(1*time.Second).Format(time.RFC3339Nano) {
t.Fatalf("start_time = %q", resp.StartTime)
}
if len(resp.Frames) != 2 {
t.Fatalf("frames len = %d, want 2: %+v", len(resp.Frames), resp.Frames)
}
if _, ok := resp.Frames[0].Cars["16"]; ok {
t.Fatalf("empty driver included in frame: %+v", resp.Frames[0].Cars)
}
if got := resp.Frames[0].Cars["1"]; got.X != 10 || got.Y != 20 {
t.Fatalf("frame 0 car 1 = %+v, want first nearest sample", got)
}
if got := resp.Frames[1].Cars["1"]; got.X != 60 || got.Y != 120 {
t.Fatalf("frame 1 car 1 = %+v, want later nearest sample", got)
}
if got := resp.Frames[1].Cars["4"]; got.X != 100 || got.Y != 200 {
t.Fatalf("frame 1 car 4 = %+v", got)
}
}
func TestAssembleReplayFramesCapsFrameCount(t *testing.T) {
start := time.Date(2025, 5, 25, 13, 0, 0, 0, time.UTC)
locs := make([]models.Location, maxReplayFrames+250)
for i := range locs {
locs[i] = models.Location{
Date: start.Add(time.Duration(i*defaultReplayIntervalMS) * time.Millisecond).Format(time.RFC3339Nano),
X: float64(i),
Y: float64(i * 2),
}
}
client := &fakeReplayClient{
drivers: []models.Driver{{DriverNumber: 1}},
locs: map[int][]models.Location{1: locs},
}
resp, err := assembleReplayFrames(context.Background(), client, 99, defaultReplayIntervalMS)
if err != nil {
t.Fatalf("assembleReplayFrames() error = %v", err)
}
if len(resp.Frames) > maxReplayFrames {
t.Fatalf("frames len = %d, want <= %d", len(resp.Frames), maxReplayFrames)
}
if len(resp.Frames) != maxReplayFrames {
t.Fatalf("frames len = %d, want hard cap %d", len(resp.Frames), maxReplayFrames)
}
}
func TestAssembleReplayFramesBoundsLocationFanOut(t *testing.T) {
drivers := make([]models.Driver, 10)
locs := make(map[int][]models.Location, len(drivers))
now := time.Date(2025, 5, 25, 13, 0, 0, 0, time.UTC)
for i := range drivers {
number := i + 1
drivers[i] = models.Driver{DriverNumber: number}
locs[number] = []models.Location{{Date: now.Format(time.RFC3339Nano), X: float64(number), Y: float64(number)}}
}
client := &fakeReplayClient{
drivers: drivers,
locs: locs,
delay: 5 * time.Millisecond,
}
if _, err := assembleReplayFrames(context.Background(), client, 99, defaultReplayIntervalMS); err != nil {
t.Fatalf("assembleReplayFrames() error = %v", err)
}
if client.maxInFlight > replayFetchConcurrency {
t.Fatalf("max in-flight location calls = %d, want <= %d", client.maxInFlight, replayFetchConcurrency)
}
}
func TestHandleReplayFramesValidatesParamsAndFloorsInterval(t *testing.T) {
srv := testServer(t, nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/replay/frames", nil)
rec := httptest.NewRecorder()
srv.handleReplayFrames(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("missing session_key status = %d, want 400", rec.Code)
}
req = httptest.NewRequest(http.MethodGet, "/api/v1/replay/frames?session_key=1&interval_ms=nope", nil)
rec = httptest.NewRecorder()
srv.handleReplayFrames(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("invalid interval status = %d, want 400", rec.Code)
}
client := &fakeReplayClient{drivers: []models.Driver{{DriverNumber: 1}}, locs: map[int][]models.Location{
1: {{Date: time.Date(2025, 5, 25, 13, 0, 0, 0, time.UTC).Format(time.RFC3339Nano), X: 1, Y: 2}},
}}
resp, err := assembleReplayFrames(context.Background(), client, 99, 1000)
if err != nil {
t.Fatalf("assembleReplayFrames() error = %v", err)
}
body, err := json.Marshal(resp)
if err != nil || len(body) == 0 {
t.Fatalf("marshal response = %d bytes, %v", len(body), err)
}
if resp.Interval != defaultReplayIntervalMS {
t.Fatalf("interval = %d, want floor %d", resp.Interval, defaultReplayIntervalMS)
}
}

View File

@@ -78,6 +78,7 @@ func (s *Server) routes() (http.Handler, error) {
mux.HandleFunc("/api/v1/grid", s.handleGrid)
mux.HandleFunc("/api/v1/laps/comparison", s.handleLapsComparison)
mux.HandleFunc("/api/v1/laps", s.handleLaps)
mux.HandleFunc("/api/v1/replay/frames", s.handleReplayFrames)
mux.HandleFunc("/api/v1/weather", s.handleWeather)
mux.HandleFunc("/api/v1/race-control", s.handleRaceControl)
mux.HandleFunc("/api/v1/telemetry", s.handleTelemetry)