feat: add track seeding migrations, catalog definitions, and test infrastructure for race leaderboards

This commit is contained in:
2026-07-15 18:59:58 +04:00
parent a56841237d
commit 53288ea670
7 changed files with 1668 additions and 1243 deletions
+11 -1
View File
@@ -17,13 +17,23 @@ import "math"
// (1/24 scale), power figures are real (full-scale 1.6L V6 hybrid // (1/24 scale), power figures are real (full-scale 1.6L V6 hybrid
// turbo, ~1000 hp combined MGU-K + ICE). // turbo, ~1000 hp combined MGU-K + ICE).
func DefaultTracks() []Track { func DefaultTracks() []Track {
return []Track{ tracks := []Track{
Monaco(), Monaco(),
BarcelonaCatalunya(), BarcelonaCatalunya(),
AustriaRedBullRing(), AustriaRedBullRing(),
GreatBritainSilverstone(), GreatBritainSilverstone(),
BelgiumSpa(), BelgiumSpa(),
} }
for i := range tracks {
for j := range tracks[i].Centerline {
tracks[i].Centerline[j].Y = -tracks[i].Centerline[j].Y
tracks[i].Centerline[j].HeadingRad = -tracks[i].Centerline[j].HeadingRad
}
tracks[i].Bounds = ComputeBounds(tracks[i].Centerline)
tracks[i].LengthM = tracks[i].Bounds.LengthM
tracks[i].WidthM = tracks[i].Bounds.WidthM
}
return tracks
} }
func DefaultCars() []Car { func DefaultCars() []Car {
+6 -6
View File
@@ -294,17 +294,17 @@ func TestLeaderboard(t *testing.T) {
t.Fatalf("expected 3 calendar entries, got %d", len(res)) t.Fatalf("expected 3 calendar entries, got %d", len(res))
} }
// 1. Monaco (finished) // 1. Austria (planned)
if res[0].TrackID != "monaco" || res[0].Status != "finished" { if res[0].TrackID != "austria-redbull-ring" || res[0].Status != "planned" {
t.Errorf("expected monaco (finished), got %s (%s)", res[0].TrackID, res[0].Status) t.Errorf("expected austria-redbull-ring (planned), got %s (%s)", res[0].TrackID, res[0].Status)
} }
// 2. Barcelona (active) // 2. Barcelona (active)
if res[1].TrackID != "barcelona-catalunya" || res[1].Status != "active" { if res[1].TrackID != "barcelona-catalunya" || res[1].Status != "active" {
t.Errorf("expected barcelona-catalunya (active), got %s (%s)", res[1].TrackID, res[1].Status) t.Errorf("expected barcelona-catalunya (active), got %s (%s)", res[1].TrackID, res[1].Status)
} }
// 3. Austria (planned) // 3. Monaco (finished)
if res[2].TrackID != "austria-redbull-ring" || res[2].Status != "planned" { if res[2].TrackID != "monaco" || res[2].Status != "finished" {
t.Errorf("expected austria-redbull-ring (planned), got %s (%s)", res[2].TrackID, res[2].Status) t.Errorf("expected monaco (finished), got %s (%s)", res[2].TrackID, res[2].Status)
} }
}) })
} }
+41 -42
View File
@@ -30,29 +30,34 @@ var DefaultNow = time.Now
var fallbackTracks = []string{"default"} var fallbackTracks = []string{"default"}
var defaultDriverSeeds = []struct { var defaultDriverSeeds = []struct {
nick string id string
name string nick string
name string
avatarURL string
clanID string
createdMs int64
updatedMs int64
}{ }{
{"ACE", "Alice"}, {"driver-seed-001", "BOR", "Gabriel Bortoleto", "https://minio.is1di.ru/x0gp/avatar/gabriel-bortoleto-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584003},
{"BOB", "Bob"}, {"driver-seed-002", "HUL", "Nico Hulkenberg", "https://minio.is1di.ru/x0gp/avatar/nico-hulkenberg-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584007},
{"CAR", "Carol"}, {"driver-seed-003", "ALB", "Alex Albon", "https://minio.is1di.ru/x0gp/avatar/alex-albon-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584008},
{"DAV", "Dave"}, {"driver-seed-004", "SAI", "Carlos Sainz", "https://minio.is1di.ru/x0gp/avatar/carlos-sainz-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584011},
{"EVE", "Eve"}, {"driver-seed-005", "BEA", "Oliver Bearman", "https://minio.is1di.ru/x0gp/avatar/oliver-bearman-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584012},
{"FRA", "Frank"}, {"driver-seed-006", "OCO", "Esteban Ocon", "https://minio.is1di.ru/x0gp/avatar/esteban-ocon-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584014},
{"GRA", "Grace"}, {"driver-seed-007", "LIN", "Arvid Lindblad", "https://minio.is1di.ru/x0gp/avatar/arvid-lindblad-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584018},
{"HEI", "Heidi"}, {"driver-seed-008", "LAW", "Liam Lawson", "https://minio.is1di.ru/x0gp/avatar/liam-lawson-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584018},
{"IVY", "Ivy"}, {"driver-seed-009", "COL", "Franco Colapinto", "https://minio.is1di.ru/x0gp/avatar/franco-colapinto-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584020},
{"JAC", "Jack"}, {"driver-seed-010", "GAS", "Pierre Gasly", "https://minio.is1di.ru/x0gp/avatar/pierre-gasly-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584023},
{"KEN", "Ken"}, {"driver-seed-011", "STR", "Lance Stroll", "https://minio.is1di.ru/x0gp/avatar/lance-stroll-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584023},
{"LEO", "Leo"}, {"driver-seed-012", "ALO", "Fernando Alonso", "https://minio.is1di.ru/x0gp/avatar/fernando-alonso-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584026},
{"MAX", "Max"}, {"driver-seed-013", "ANT", "Kimi Antonelli", "https://minio.is1di.ru/x0gp/avatar/kimi-antonelli-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584028},
{"NED", "Ned"}, {"driver-seed-014", "RUS", "George Russel", "https://minio.is1di.ru/x0gp/avatar/george-russell-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584028},
{"OLI", "Oliver"}, {"driver-seed-015", "HAD", "Isack Hadjar", "https://minio.is1di.ru/x0gp/avatar/isack-hadjar-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584031},
{"PEP", "Peggy"}, {"driver-seed-016", "VER", "Max Verstappen", "https://minio.is1di.ru/x0gp/avatar/max-verstappen-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584033},
{"QIN", "Quincy"}, {"driver-seed-017", "HAM", "Lewis Hamilton", "https://minio.is1di.ru/x0gp/avatar/lewis-hamilton-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584034},
{"ROY", "Roy"}, {"driver-seed-018", "NOR", "Lando Norris", "https://minio.is1di.ru/x0gp/avatar/lando-norris-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584036},
{"SAM", "Sam"}, {"driver-seed-019", "LEC", "Charles Leclerc", "https://minio.is1di.ru/x0gp/avatar/charles-leclerc-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584038},
{"TED", "Ted"}, {"driver-seed-020", "PIA", "Oscar Piastri", "https://minio.is1di.ru/x0gp/avatar/oscar-piastri-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584039},
} }
// Default clans. // Default clans.
@@ -96,8 +101,8 @@ type Runner struct {
// Filled by loadInputs. // Filled by loadInputs.
trackIDs []string trackIDs []string
driverIDs []string // driver ids in the order they exist after seed driverIDs []string // driver ids in the order they exist after seed
driverInfo []seedDriver // id, nick, name, clanID, clanTag driverInfo []seedDriver // id, nick, name, clanID, clanTag
clanByID map[string]string // clan id -> tag (for fast profile updates) clanByID map[string]string // clan id -> tag (for fast profile updates)
} }
@@ -221,11 +226,11 @@ func (r *Runner) Run(ctx context.Context, opt Options) (Summary, error) {
// 3. Race plans. Mix of one-shot and recurring. // 3. Race plans. Mix of one-shot and recurring.
planShapes := []planShape{ planShapes := []planShape{
{intervalS: 0, offsetS: 5 * 60}, // one-shot in 5 min {intervalS: 0, offsetS: 5 * 60}, // one-shot in 5 min
{intervalS: 60, offsetS: 30 * 60}, // every minute, start in 30 min {intervalS: 60, offsetS: 30 * 60}, // every minute, start in 30 min
{intervalS: 3600, count: 24, offsetS: 60 * 60}, // hourly x24, start in 1h {intervalS: 3600, count: 24, offsetS: 60 * 60}, // hourly x24, start in 1h
{intervalS: 0, offsetS: 6 * 3600}, // one-shot in 6h {intervalS: 0, offsetS: 6 * 3600}, // one-shot in 6h
{intervalS: 86400, offsetS: 24 * 3600}, // daily, start tomorrow {intervalS: 86400, offsetS: 24 * 3600}, // daily, start tomorrow
} }
for i := 0; i < opt.Counts.Plans && i < len(planShapes); i++ { for i := 0; i < opt.Counts.Plans && i < len(planShapes); i++ {
ps := planShapes[i] ps := planShapes[i]
@@ -246,7 +251,7 @@ func (r *Runner) Run(ctx context.Context, opt Options) (Summary, error) {
func (r *Runner) seedTrackCalendar(ctx context.Context) error { func (r *Runner) seedTrackCalendar(ctx context.Context) error {
now := r.now now := r.now
if len(r.trackIDs) == 0 { if len(r.trackIDs) == 0 {
return nil return nil
} }
@@ -353,21 +358,15 @@ func (r *Runner) seedDriversAndClans(ctx context.Context) error {
return fmt.Errorf("count drivers: %w", err) return fmt.Errorf("count drivers: %w", err)
} }
if drvCount == 0 { if drvCount == 0 {
now := time.Now().UnixMilli() for _, ds := range defaultDriverSeeds {
// Round-robin: each driver assigned to one of the seeded clans.
clanIDs := make([]string, 0, len(defaultClanSeeds))
for i := range defaultClanSeeds {
clanIDs = append(clanIDs, fmt.Sprintf("clan-seed-%03d", i+1))
}
for i, ds := range defaultDriverSeeds {
d := drivers.Driver{ d := drivers.Driver{
ID: fmt.Sprintf("driver-seed-%03d", i+1), ID: ds.id,
Nickname: ds.nick, Nickname: ds.nick,
Name: ds.name, Name: ds.name,
AvatarURL: fmt.Sprintf("https://cdn.example.com/drivers/%s.png", ds.nick), AvatarURL: ds.avatarURL,
ClanID: clanIDs[i%len(clanIDs)], ClanID: ds.clanID,
CreatedMs: now, CreatedMs: ds.createdMs,
UpdatedMs: now, UpdatedMs: ds.updatedMs,
} }
if err := r.driversPg.Create(ctx, d); err != nil { if err := r.driversPg.Create(ctx, d); err != nil {
return fmt.Errorf("insert driver %s: %w", ds.nick, err) return fmt.Errorf("insert driver %s: %w", ds.nick, err)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -20,56 +20,56 @@ UPDATE tracks SET
WHERE id = 'monaco'; WHERE id = 'monaco';
-- New centerline (51 waypoints, scale_xy=0.016, v=0.632 m/s) -- New centerline (51 waypoints, scale_xy=0.016, v=0.632 m/s)
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 0, -3.2064, -1.0454, 1.4710, 0.632, -0.1988); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 0, -3.2064, 1.0454, -1.4710, 0.632, -0.1988);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 1, -3.1387, -0.5857, 1.3926, 0.632, -0.1787); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 1, -3.1387, 0.5857, -1.3926, 0.632, -0.1787);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 2, -3.0418, -0.1312, 1.3043, 0.632, -0.2544); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 2, -3.0418, 0.1312, -1.3043, 0.632, -0.2544);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 3, -2.8946, 0.3084, 1.1557, 0.632, -0.4431); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 3, -2.8946, -0.3084, -1.1557, 0.632, -0.4431);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 4, -2.6693, 0.7141, 0.8629, 0.632, -1.0875); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 4, -2.6693, -0.7141, -0.8629, 0.632, -1.0875);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 5, -2.3517, 0.9426, 0.1919, 0.632, -1.2844); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 5, -2.3517, -0.9426, -0.1919, 0.632, -1.2844);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 6, -1.8935, 0.8648, -0.1182, 0.632, -0.2237); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 6, -1.8935, -0.8648, 0.1182, 0.632, -0.2237);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 7, -1.4302, 0.8332, -0.0660, 0.632, 0.0475); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 7, -1.4302, -0.8332, 0.0660, 0.632, 0.0475);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 8, -0.9667, 0.8035, -0.0743, 0.632, 0.0756); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 8, -0.9667, -0.8035, 0.0743, 0.632, 0.0756);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 9, -0.5052, 0.7644, 0.0045, 0.632, 0.0806); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 9, -0.5052, -0.7644, -0.0045, 0.632, 0.0806);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 10, -0.0425, 0.8077, 0.0000, 0.632, -0.0425); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 10, -0.0425, -0.8077, -0.0000, 0.632, -0.0425);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 11, 0.4197, 0.7644, -0.0351, 0.632, 0.3650); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 11, 0.4197, -0.7644, 0.0351, 0.632, 0.3650);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 12, 0.8809, 0.7753, 0.3862, 0.632, 1.3887); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 12, 0.8809, -0.7753, -0.3862, 0.632, 1.3887);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 13, 1.1985, 1.0811, 1.2440, 0.632, 1.4550); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 13, 1.1985, -1.0811, -1.2440, 0.632, 1.4550);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 14, 1.1372, 1.5315, 1.5878, 0.632, -0.2144); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 14, 1.1372, -1.5315, -1.5878, 0.632, -0.2144);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 15, 1.1839, 1.9468, 1.0284, 0.632, -1.2681); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 15, 1.1839, -1.9468, -1.0284, 0.632, -1.2681);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 16, 1.5551, 2.2249, 0.6162, 0.632, -0.4763); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 16, 1.5551, -2.2249, -0.6162, 0.632, -0.4763);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 17, 1.9414, 2.4832, 0.5226, 0.632, -0.1756); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 17, 1.9414, -2.4832, -0.5226, 0.632, -0.1756);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 18, 2.3526, 2.6842, 0.4546, 0.632, -0.1494); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 18, 2.3526, -2.6842, -0.4546, 0.632, -0.1494);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 19, 2.3526, 2.6842, -1.2476, 0.632, -0.0225); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 19, 2.3526, -2.6842, 1.2476, 0.632, -0.0225);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 20, 2.4622, 2.3570, -1.2545, 0.632, 0.1775); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 20, 2.4622, -2.3570, 1.2545, 0.632, 0.1775);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 21, 2.5976, 1.9356, -0.5751, 0.632, 34.1925); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 21, 2.5976, -1.9356, 0.5751, 0.632, 34.1925);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 22, 2.6154, 2.2577, 1.0415, 0.632, -0.8400); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 22, 2.6154, -2.2577, -1.0415, 0.632, -0.8400);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 23, 2.9239, 2.4933, -0.0053, 0.632, -3.1975); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 23, 2.9239, -2.4933, 0.0053, 0.632, -3.1975);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 24, 3.2311, 2.2544, -1.4159, 0.632, -2.7456); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 24, 3.2311, -2.2544, 1.4159, 0.632, -2.7456);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 25, 3.0259, 1.8399, -2.1127, 0.632, -0.6488); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 25, 3.0259, -1.8399, 2.1127, 0.632, -0.6488);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 26, 2.7546, 1.4629, -2.2528, 0.632, -0.2769); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 26, 2.7546, -1.4629, 2.2528, 0.632, -0.2769);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 27, 2.4413, 1.1199, -2.3708, 0.632, -0.3044); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 27, 2.4413, -1.1199, 2.3708, 0.632, -0.3044);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 28, 2.0895, 0.8170, -2.5300, 0.632, -0.3825); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 28, 2.0895, -0.8170, 2.5300, 0.632, -0.3825);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 29, 1.6852, 0.5897, -2.7255, 0.632, -0.4019); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 29, 1.6852, -0.5897, 2.7255, 0.632, -0.4019);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 30, 1.2447, 0.4437, -2.9031, 0.632, -0.3538); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 30, 1.2447, -0.4437, 2.9031, 0.632, -0.3538);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 31, 0.7862, 0.3711, -3.0547, 0.632, -0.3044); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 31, 0.7862, -0.3711, 3.0547, 0.632, -0.3044);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 32, 0.3218, 0.3632, 3.0973, 0.632, -0.0975); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 32, 0.3218, -0.3632, -3.0973, 0.632, -0.0975);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 33, -0.1396, 0.4121, 3.1387, 0.632, -0.0394); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 33, -0.1396, -0.4121, -3.1387, 0.632, -0.0394);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 34, -0.5818, 0.3659, 3.0594, 0.632, -0.2375); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 34, -0.5818, -0.3659, -3.0594, 0.632, -0.2375);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 35, -1.0290, 0.4854, 2.9319, 0.632, -0.0950); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 35, -1.0290, -0.4854, -2.9319, 0.632, -0.0950);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 36, -1.4880, 0.5588, 2.9664, 0.632, -0.2500); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 36, -1.4880, -0.5588, -2.9664, 0.632, -0.2500);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 37, -1.9441, 0.6474, -3.1052, 0.632, 0.8825); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 37, -1.9441, -0.6474, 3.1052, 0.632, 0.8825);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 38, -2.3631, 0.5269, -2.4971, 0.632, 1.3600); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 38, -2.3631, -0.5269, 2.4971, 0.632, 1.3600);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 39, -2.6181, 0.1409, -1.9547, 0.632, 0.9481); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 39, -2.6181, -0.1409, 1.9547, 0.632, 0.9481);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 40, -2.7014, -0.3108, -1.5706, 0.632, 0.3788); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 40, -2.7014, 0.3108, 1.5706, 0.632, 0.3788);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 41, -2.6179, -0.7633, -1.6025, 0.632, -0.3425); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 41, -2.6179, 0.7633, 1.6025, 0.632, -0.3425);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 42, -2.7299, -1.2092, -1.8788, 0.632, -0.0725); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 42, -2.7299, 1.2092, 1.8788, 0.632, -0.0725);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 43, -2.8930, -1.6280, -1.6659, 0.632, 0.7706); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 43, -2.8930, 1.6280, 1.6659, 0.632, 0.7706);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 44, -2.8134, -2.0840, -1.2045, 0.632, 0.2162); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 44, -2.8134, 2.0840, 1.2045, 0.632, 0.2162);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 45, -2.5685, -2.4739, -1.5682, 0.632, -2.4475); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 45, -2.5685, 2.4739, 1.5682, 0.632, -2.4475);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 46, -2.8118, -2.6842, 3.0638, 0.632, -4.0544); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 46, -2.8118, 2.6842, -3.0638, 0.632, -4.0544);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 47, -3.1281, -2.4303, 2.0706, 0.632, -1.2500); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 47, -3.1281, 2.4303, -2.0706, 0.632, -1.2500);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 48, -3.2004, -1.9728, 1.6822, 0.632, -0.4706); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 48, -3.2004, 1.9728, -1.6822, 0.632, -0.4706);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 49, -3.2311, -1.5093, 1.5774, 0.632, -0.2275); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 49, -3.2311, 1.5093, -1.5774, 0.632, -0.2275);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 50, -3.2064, -1.0454, 1.4710, 0.632, -0.1988); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 50, -3.2064, 1.0454, -1.4710, 0.632, -0.1988);
COMMIT; COMMIT;
+395
View File
@@ -0,0 +1,395 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>x0gp — Race Track Viewer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Plus+Jakarta+Sans:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0b0f19;
--card-bg: rgba(17, 24, 39, 0.7);
--card-border: rgba(255, 255, 255, 0.08);
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--accent: #3b82f6;
--accent-glow: rgba(59, 130, 246, 0.15);
--gradient-accent: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
min-height: 100vh;
padding: 2.5rem;
background-image:
radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.05) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(37, 99, 235, 0.05) 0%, transparent 40%);
background-attachment: fixed;
}
header {
max-width: 1200px;
margin: 0 auto 3rem auto;
display: flex;
justify-content: space-between;
align-items: center;
}
h1 {
font-family: 'Outfit', sans-serif;
font-size: 2.5rem;
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #ffffff 0%, #a5b4fc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: var(--text-secondary);
font-size: 0.95rem;
margin-top: 0.25rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
backdrop-filter: blur(12px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(800px circle at var(--x, 0px) var(--y, 0px), rgba(255, 255, 255, 0.06), transparent 40%);
pointer-events: none;
opacity: 0;
transition: opacity 0.5s;
}
.card:hover::before {
opacity: 1;
}
.card:hover {
transform: translateY(-4px);
border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(59, 130, 246, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.track-name {
font-family: 'Outfit', sans-serif;
font-size: 1.35rem;
font-weight: 600;
color: #ffffff;
}
.track-id {
font-family: monospace;
font-size: 0.8rem;
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.05);
padding: 0.2rem 0.5rem;
border-radius: 6px;
}
.canvas-container {
width: 100%;
height: 280px;
background-color: #0b0f19;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.03);
position: relative;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
font-size: 0.85rem;
}
.stat-item {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.03);
padding: 0.5rem 0.75rem;
border-radius: 8px;
display: flex;
justify-content: space-between;
}
.stat-label {
color: var(--text-secondary);
}
.stat-value {
color: #ffffff;
font-weight: 600;
}
.tags-container {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.tag {
font-size: 0.75rem;
padding: 0.2rem 0.6rem;
border-radius: 20px;
background: rgba(59, 130, 246, 0.1);
color: #60a5fa;
border: 1px solid rgba(59, 130, 246, 0.2);
}
.tag-active {
background: rgba(16, 185, 129, 0.1);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.loader {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
width: 100%;
grid-column: 1 / -1;
flex-direction: column;
gap: 1rem;
color: var(--text-secondary);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(59, 130, 246, 0.1);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<header>
<div>
<h1>x0gp — Race Track Catalog</h1>
<div class="subtitle">Visualizing physical tracks and their generated centerline geometry</div>
</div>
</header>
<main class="grid" id="tracks-grid">
<div class="loader">
<div class="spinner"></div>
<div>Loading tracks from server API...</div>
</div>
</main>
<script>
// Track SVG centerline rendering algorithm
function generateSvgPath(centerline, bounds, padding = 30, size = 260) {
if (!centerline || centerline.length < 2) return null;
const minX = bounds.min_x;
const maxX = bounds.max_x;
const minY = bounds.min_y;
const maxY = bounds.max_y;
const rangeX = maxX - minX || 1;
const rangeY = maxY - minY || 1;
const aspectRatio = rangeX / rangeY;
let width, height;
if (aspectRatio > 1) {
width = size;
height = size / aspectRatio;
} else {
width = size * aspectRatio;
height = size;
}
const canvasWidth = width + padding * 2;
const canvasHeight = height + padding * 2;
const pixelPoints = centerline.map((point) => ({
x: ((point.x - minX) / rangeX) * width + padding,
y: ((point.y - minY) / rangeY) * height + padding,
heading: point.heading_rad,
curvature: point.curvature
}));
let dAttribute = '';
for (let i = 0; i < pixelPoints.length; i++) {
const current = pixelPoints[i];
if (i === 0) {
dAttribute += `M ${current.x.toFixed(2)} ${current.y.toFixed(2)}`;
} else {
const prev = pixelPoints[i - 1];
const dx = current.x - prev.x;
const dy = current.y - prev.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const tension = 0.30 + (prev.curvature * 0.05);
const cpLength = distance * tension;
const cp1x = prev.x + Math.cos(prev.heading) * cpLength;
const cp1y = prev.y + Math.sin(prev.heading) * cpLength;
const cp2x = current.x - Math.cos(current.heading) * cpLength;
const cp2y = current.y - Math.sin(current.heading) * cpLength;
dAttribute += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${current.x.toFixed(2)} ${current.y.toFixed(2)}`;
}
}
if (pixelPoints.length > 2) {
const first = pixelPoints[0];
const last = pixelPoints[pixelPoints.length - 1];
const dx = first.x - last.x;
const dy = first.y - last.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const tension = 0.3 + last.curvature * 0.05;
const cpLength = distance * tension;
const cp1x = last.x + Math.cos(last.heading) * cpLength;
const cp1y = last.y + Math.sin(last.heading) * cpLength;
const cp2x = first.x - Math.cos(first.heading) * cpLength;
const cp2y = first.y - Math.sin(first.heading) * cpLength;
dAttribute += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${first.x.toFixed(2)} ${first.y.toFixed(2)} Z`;
}
return {
d: dAttribute,
canvasWidth,
canvasHeight
};
}
async function init() {
const grid = document.getElementById('tracks-grid');
try {
const res = await fetch('/api/tracks');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
grid.innerHTML = '';
if (!data.tracks || data.tracks.length === 0) {
grid.innerHTML = '<div class="loader">No tracks found on the server.</div>';
return;
}
data.tracks.forEach(track => {
const card = document.createElement('div');
card.className = 'card';
// Track hover mouse light effect
card.addEventListener('mousemove', e => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
card.style.setProperty('--x', `${x}px`);
card.style.setProperty('--y', `${y}px`);
});
// Generate SVG
const svgData = generateSvgPath(track.centerline, track.bounds);
let svgHtml = '<div style="color: var(--text-secondary); font-size: 0.85rem;">No centerline coordinates</div>';
if (svgData) {
svgHtml = `
<svg width="100%" height="100%" viewBox="0 0 ${svgData.canvasWidth} ${svgData.canvasHeight}" preserveAspectRatio="xMidYMid meet" style="display: block; max-width: 100%; max-height: 100%; height: auto; transform-origin: center;">
<path d="${svgData.d}" fill="none" stroke="#ffffff" stroke-width="20" stroke-linecap="round" stroke-linejoin="round" />
<path d="${svgData.d}" fill="none" stroke="#111827" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" />
</svg>
`;
}
const isActiveTag = track.is_active ? '<span class="tag tag-active">Active Track</span>' : '';
const bestLapStr = track.best_lap_ms > 0 ? `${(track.best_lap_ms / 1000).toFixed(3)}s` : 'None';
card.innerHTML = `
<div class="card-header">
<div>
<div class="track-name">${track.name}</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-top: 0.2rem;">${track.description || 'No description'}</div>
</div>
<div class="track-id">${track.id}</div>
</div>
<div class="canvas-container">
${svgHtml}
</div>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Length</span>
<span class="stat-value">${track.length_m.toFixed(1)} m</span>
</div>
<div class="stat-item">
<span class="stat-label">Width</span>
<span class="stat-value">${track.width_m.toFixed(1)} m</span>
</div>
<div class="stat-item">
<span class="stat-label">Lane Width</span>
<span class="stat-value">${track.lane_width_m.toFixed(1)} m</span>
</div>
<div class="stat-item">
<span class="stat-label">Best Lap</span>
<span class="stat-value">${bestLapStr}</span>
</div>
</div>
<div class="tags-container">
<span class="tag" style="background: rgba(255,255,255,0.05); color: #fff; border-color: rgba(255,255,255,0.1); text-transform: capitalize;">${track.surface}</span>
${isActiveTag}
</div>
`;
grid.appendChild(card);
});
} catch (err) {
grid.innerHTML = `<div class="loader" style="color: #ef4444;">Failed to load tracks: ${err.message}</div>`;
}
}
init();
</script>
</body>
</html>