diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-16 14:55:26 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-16 14:55:26 +0200 |
| commit | df7c9b5130ba193fc918a947a461f2a70dc2dd3c (patch) | |
| tree | 0366e0b60338e268d758511621b9adf721be8322 /web/frontend | |
| parent | 53ab332e6d2afe9818914e0026310e4cadcc4202 (diff) | |
fix(web): use Cotacol for climb difficulty
Diffstat (limited to 'web/frontend')
| -rw-r--r-- | web/frontend/ride-detail-canvas.ts | 4 | ||||
| -rw-r--r-- | web/frontend/ride-detail-logic.test.ts | 12 | ||||
| -rw-r--r-- | web/frontend/ride-detail-logic.ts | 27 | ||||
| -rw-r--r-- | web/frontend/types.d.ts | 2 |
4 files changed, 25 insertions, 20 deletions
diff --git a/web/frontend/ride-detail-canvas.ts b/web/frontend/ride-detail-canvas.ts index 1453013..c61afee 100644 --- a/web/frontend/ride-detail-canvas.ts +++ b/web/frontend/ride-detail-canvas.ts @@ -1,4 +1,4 @@ -import { clamp, climbMetrics, formatDistance, formatElevation, nearestPointIndex } from "./ride-detail-logic.js"; +import { clamp, climbMetrics, formatClimbLabel, formatDistance, formatElevation, nearestPointIndex } from "./ride-detail-logic.js"; import type { BoundarySource, ClimbBounds, RideDetailColors, RideProfile, RideProfilePoint } from "./types.js"; interface Plot { @@ -137,7 +137,7 @@ export class RideProfileCanvas { const endX = clamp(this.xForDistance(metrics.end.distanceKm), plot.left, plot.right); this.context.fillStyle = climbIndex === this.focusedClimbIndex ? this.colors.climbFocusFill : this.colors.accentFill; this.context.fillRect(startX, plot.top, Math.max(endX - startX, 1), plot.bottom - plot.top); - const label = climb.name || `${metrics.category} ${Math.round(metrics.score)}`; + const label = formatClimbLabel(climb.name, metrics.category, metrics.cotacol); this.context.fillStyle = this.colors.climbLabel; this.context.textAlign = "center"; this.context.textBaseline = "top"; diff --git a/web/frontend/ride-detail-logic.test.ts b/web/frontend/ride-detail-logic.test.ts index 024f018..b848f7e 100644 --- a/web/frontend/ride-detail-logic.test.ts +++ b/web/frontend/ride-detail-logic.test.ts @@ -1,10 +1,11 @@ import test from "node:test"; import assert from "node:assert/strict"; import { - categoryForScore, + categoryForCotacol, climbMetrics, cotacolForClimb, elevationAtDistance, + formatClimbLabel, formatDistance, formatElevation, nearestPointIndex, @@ -28,7 +29,7 @@ test("interpolates elevation between route points", () => { assert.equal(elevationAtDistance(points, 2, 250), 120); }); -test("calculates climb metrics with the Cotacol score", () => { +test("calculates climb metrics with Cotacol", () => { const metrics = climbMetrics(points, { startIndex: 0, endIndex: 3 }); assert.ok(metrics); @@ -36,7 +37,12 @@ test("calculates climb metrics with the Cotacol score", () => { assert.equal(metrics.elevationGain, 20); assert.equal(metrics.slope, 8); assert.equal(metrics.cotacol, cotacolForClimb(points, 0, 3)); - assert.equal(metrics.category, categoryForScore(metrics.score)); + assert.equal(metrics.category, categoryForCotacol(metrics.cotacol)); +}); + +test("formats profile climb labels with Cotacol", () => { + assert.equal(formatClimbLabel("", "Cat 4", 45.9), "Cat 4 45.9"); + assert.equal(formatClimbLabel("Col de Test", "Cat 4", 45.9), "Col de Test"); }); test("formats profile labels consistently", () => { diff --git a/web/frontend/ride-detail-logic.ts b/web/frontend/ride-detail-logic.ts index 825bb2c..a2ba80a 100644 --- a/web/frontend/ride-detail-logic.ts +++ b/web/frontend/ride-detail-logic.ts @@ -19,15 +19,17 @@ export const nearestPointIndex = (points: ProfilePoint[], distance: number): num return distance - previous.distanceKm < points[low].distanceKm - distance ? low - 1 : low; }; -export const categoryForScore = (score: number): string => { - if (score < 35) return "NO"; - if (score < 80) return "Cat 4"; - if (score < 180) return "Cat 3"; - if (score < 250) return "Cat 2"; - if (score < 600) return "Cat 1"; +export const categoryForCotacol = (cotacol: number): string => { + if (cotacol < 35) return "NO"; + if (cotacol < 80) return "Cat 4"; + if (cotacol < 180) return "Cat 3"; + if (cotacol < 250) return "Cat 2"; + if (cotacol < 600) return "Cat 1"; return "HC"; }; +export const formatClimbLabel = (name: string, category: string, cotacol: number): string => name || `${category} ${cotacol.toFixed(1)}`; + export const elevationAtDistance = (points: ProfilePoint[], index: number, distanceM: number): number => { if (index + 1 >= points.length) return points[index].elevationM; const startDistanceM = points[index].distanceKm * 1000; @@ -40,7 +42,7 @@ export const cotacolForClimb = (points: ProfilePoint[], startIndex: number, endI const startDistanceM = points[startIndex].distanceKm * 1000; const lastDistanceM = points[endIndex].distanceKm * 1000; if (lastDistanceM <= startDistanceM) return 0; - let score = 0; + let cotacol = 0; let pointIndex = startIndex; for (let segmentStartM = startDistanceM; segmentStartM < lastDistanceM; segmentStartM += 100) { const segmentEndM = Math.min(segmentStartM + 100, lastDistanceM); @@ -49,9 +51,9 @@ export const cotacolForClimb = (points: ProfilePoint[], startIndex: number, endI while (pointIndex < endIndex && points[pointIndex + 1].distanceKm * 1000 < segmentEndM) pointIndex++; const endElevation = elevationAtDistance(points, pointIndex, segmentEndM); const slope = (endElevation - startElevation) / (segmentEndM - segmentStartM); - if (slope > 0) score += ((segmentEndM - segmentStartM) / 1000) * (slope * 100) ** 2; + if (slope > 0) cotacol += ((segmentEndM - segmentStartM) / 1000) * (slope * 100) ** 2; } - return score; + return cotacol; }; export const climbMetrics = (points: ProfilePoint[], bounds: ClimbBounds | undefined): ClimbMetrics | null => { @@ -70,15 +72,14 @@ export const climbMetrics = (points: ProfilePoint[], bounds: ClimbBounds | undef const distanceKm = end.distanceKm - start.distanceKm; const elevationGain = end.elevationM - start.elevationM; const slope = distanceKm > 0 ? elevationGain / (distanceKm * 10) : 0; - const score = distanceKm > 0 ? ((Math.abs(elevationGain) * elevationGain) / (distanceKm * 1000)) * 10 : 0; + const cotacol = cotacolForClimb(points, bounds.startIndex, bounds.endIndex); return { start, end, distanceKm, elevationGain, slope, - score, - cotacol: cotacolForClimb(points, bounds.startIndex, bounds.endIndex), - category: categoryForScore(score), + cotacol, + category: categoryForCotacol(cotacol), }; }; diff --git a/web/frontend/types.d.ts b/web/frontend/types.d.ts index 66f1ad2..3642971 100644 --- a/web/frontend/types.d.ts +++ b/web/frontend/types.d.ts @@ -18,7 +18,6 @@ export interface RideProfileClimb { topKm: number; topElevationM: number; name: string; - score: number; category: string; distanceKm: number; slopePercent: number; @@ -69,7 +68,6 @@ export interface ClimbMetrics { distanceKm: number; elevationGain: number; slope: number; - score: number; cotacol: number; category: string; } |