1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
import test from "node:test";
import assert from "node:assert/strict";
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");
assert.equal(profileBandForSlope(0), "downhill");
assert.equal(profileBandForSlope(0.1), "0-3");
assert.equal(profileBandForSlope(2.9), "0-3");
assert.equal(profileBandForSlope(3), "3-6");
assert.equal(profileBandForSlope(6), "6-9");
assert.equal(profileBandForSlope(9), "9-plus");
assert.equal(profileBandForSlope(12), "9-plus");
});
// 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, 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), []);
});
|