aboutsummaryrefslogtreecommitdiff
path: root/web/frontend/official-climb-profile-logic.test.ts
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-10-18 20:37:54 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-10-18 20:37:54 +0200
commit99e32b642e65d51aaf34a2047b82d3f4baaa3f03 (patch)
tree64ea4d4d80bfcc092bb09eb98e8de47ad9262622 /web/frontend/official-climb-profile-logic.test.ts
parente82599233fa579f665c62636389ab2dc27b10d2a (diff)
Draw climb profile sections where the slope changes
Fixed distance buckets averaged short steep ramps into their bucket: a 300 m @ 12 % pitch inside a 10 km climb reported ~7-8 % for 500 m. Sections now come from the base-grid elevation polyline split at its widest bends, up to a 24 section budget, so ramps keep their own step and label. Labels are dropped only when they would collide, and the distance axis uses a uniform 0.1-10 km step instead of one tick per (now irregular) section.
Diffstat (limited to 'web/frontend/official-climb-profile-logic.test.ts')
-rw-r--r--web/frontend/official-climb-profile-logic.test.ts105
1 files changed, 84 insertions, 21 deletions
diff --git a/web/frontend/official-climb-profile-logic.test.ts b/web/frontend/official-climb-profile-logic.test.ts
index 32521da..ca8dea2 100644
--- a/web/frontend/official-climb-profile-logic.test.ts
+++ b/web/frontend/official-climb-profile-logic.test.ts
@@ -1,13 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { displayStepForLength, profileBandForSlope, officialProfileSections } from "./official-climb-profile-logic.ts";
-
-test("selects a display step that keeps long profiles readable", () => {
- assert.equal(displayStepForLength(2_400), 100);
- assert.equal(displayStepForLength(4_500), 200);
- assert.equal(displayStepForLength(7_000), 500);
- assert.equal(displayStepForLength(20_000), 1000);
-});
+import { officialProfileSections, profileBandForSlope } from "./official-climb-profile-logic.ts";
+import type { ProfilePoint } from "./types.js";
test("assigns each slope to its color band", () => {
assert.equal(profileBandForSlope(-0.1), "downhill");
@@ -20,20 +14,89 @@ test("assigns each slope to its color band", () => {
assert.equal(profileBandForSlope(12), "9-plus");
});
-test("interpolates profile sections at the selected step", () => {
- const points = [
- { distanceKm: 0, elevationM: 100 },
- { distanceKm: 0.12, elevationM: 106 },
- { distanceKm: 0.2, elevationM: 114 },
- { distanceKm: 0.25, elevationM: 120 },
- ];
- const sections = officialProfileSections(points, 0, 3, 100);
+// buildProfile lays out one recorded point every 50 m along the given [length in km, slope in %] stretches.
+const buildProfile = (stretches: [number, number][]): ProfilePoint[] => {
+ const points: ProfilePoint[] = [{ distanceKm: 0, elevationM: 0 }];
+ let distanceKm = 0;
+ let elevationM = 0;
+ for (const [lengthKm, slopePercent] of stretches) {
+ const pointCount = Math.round(lengthKm * 20);
+ for (let index = 1; index <= pointCount; index++) {
+ const distanceStepKm = (lengthKm * index) / pointCount;
+ points.push({ distanceKm: distanceKm + distanceStepKm, elevationM: elevationM + distanceStepKm * 1000 * (slopePercent / 100) });
+ }
+ distanceKm += lengthKm;
+ elevationM += lengthKm * 1000 * (slopePercent / 100);
+ }
+ return points;
+};
+
+test("keeps a short steep ramp as its own section", () => {
+ const points = buildProfile([
+ [0.3, 4],
+ [0.3, 12],
+ [0.3, 4],
+ ]);
+ const sections = officialProfileSections(points, 0, points.length - 1);
assert.equal(sections.length, 3);
+ assert.ok(Math.abs(sections[1].slopePercent - 12) < 0.01);
+ assert.equal(sections[1].band, "9-plus");
+ assert.equal(Math.round((sections[1].endDistanceKm - sections[1].startDistanceKm) * 1000), 300);
+ assert.ok(Math.abs(sections[0].slopePercent - 4) < 0.01);
+ assert.ok(Math.abs(sections[2].slopePercent - 4) < 0.01);
+});
+
+test("collapses a constant slope into a single section", () => {
+ const points = buildProfile([[5, 6]]);
+ const sections = officialProfileSections(points, 0, points.length - 1);
+
+ assert.equal(sections.length, 1);
+ assert.ok(Math.abs(sections[0].slopePercent - 6) < 0.01);
assert.equal(sections[0].startDistanceKm, 0);
- assert.equal(sections[0].endDistanceKm, 0.1);
- assert.equal(sections[0].startElevation, 100);
- assert.equal(sections[0].endElevation, 105);
- assert.equal(sections[2].endDistanceKm, 0.25);
- assert.equal(sections[2].endElevation, 120);
+ assert.equal(sections[0].endDistanceKm, 5);
+});
+
+test("reports a descending stretch as downhill", () => {
+ const points = buildProfile([[2, -5]]);
+ const sections = officialProfileSections(points, 0, points.length - 1);
+
+ assert.equal(sections.length, 1);
+ assert.equal(sections[0].band, "downhill");
+});
+
+test("keeps a noisy profile within the readable section budget", () => {
+ const points: ProfilePoint[] = [];
+ for (let index = 0; index <= 200; index++) {
+ const distanceKm = index * 0.05;
+ points.push({ distanceKm, elevationM: distanceKm * 1000 * 0.05 + Math.sin(index) * 4 });
+ }
+ const sections = officialProfileSections(points, 0, points.length - 1);
+
+ assert.ok(sections.length > 10 && sections.length <= 24, `expected 10-24 sections, got ${sections.length}`);
+ assert.equal(sections[0].startDistanceKm, 0);
+ assert.equal(sections[sections.length - 1].endDistanceKm, 10);
+});
+
+test("reads a slice of a longer profile", () => {
+ const points = buildProfile([
+ [1, 20],
+ [1, 8],
+ [1, 20],
+ ]);
+ const startIndex = points.findIndex((point) => point.distanceKm >= 1);
+ const endIndex = points.findIndex((point) => point.distanceKm >= 2);
+ const sections = officialProfileSections(points, startIndex, endIndex);
+
+ assert.equal(sections.length, 1);
+ assert.equal(Math.round(sections[0].slopePercent), 8);
+ assert.equal(sections[0].startDistanceKm, points[startIndex].distanceKm);
+ assert.equal(sections[0].endDistanceKm, points[endIndex].distanceKm);
+});
+
+test("returns no section for an empty range", () => {
+ const points = buildProfile([[1, 6]]);
+
+ assert.deepEqual(officialProfileSections(points, 3, 3), []);
+ assert.deepEqual(officialProfileSections(points, 0, points.length), []);
});