summaryrefslogtreecommitdiff
path: root/web/frontend/official-climb-profile-logic.ts
blob: 1b2bc83a67f23be2e7206f9aeb583d3a623ad7cf (plain) (blame)
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
import type { ProfileBand, ProfilePoint } from "./types.js";

export interface OfficialProfileSection {
  startDistanceKm: number;
  endDistanceKm: number;
  startElevation: number;
  endElevation: number;
  slopePercent: number;
  band: ProfileBand;
}

const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
const profileStepSizesM: number[] = [100, 200, 500, 1000];
const displayStepForLength = (lengthM: number): number =>
  profileStepSizesM.find((stepM) => Math.ceil(lengthM / stepM) <= 30) || profileStepSizesM[profileStepSizesM.length - 1];
const profileBandForSlope = (slopePercent: number): ProfileBand => {
  if (slopePercent < 0) return "downhill";
  if (slopePercent < 3) return "0-3";
  if (slopePercent < 6) return "3-6";
  if (slopePercent < 9) return "6-9";
  if (slopePercent < 12) return "9-12";
  return "12-plus";
};
const officialProfileSections = (points: ProfilePoint[], startIndex: number, endIndex: number, stepM: number): OfficialProfileSection[] => {
  const startDistanceM = points[startIndex].distanceKm * 1000;
  const endDistanceM = points[endIndex].distanceKm * 1000;
  const sections: OfficialProfileSection[] = [];
  let pointIndex = startIndex;
  const elevationAtDistance = (distanceM: number) => {
    while (pointIndex < endIndex - 1 && points[pointIndex + 1].distanceKm * 1000 < distanceM) pointIndex++;
    const nextIndex = Math.min(pointIndex + 1, endIndex);
    const first = points[pointIndex];
    const next = points[nextIndex];
    const distanceSpan = next.distanceKm * 1000 - first.distanceKm * 1000;
    if (distanceSpan <= 0) return first.elevationM;
    const fraction = (distanceM - first.distanceKm * 1000) / distanceSpan;
    return first.elevationM + clamp(fraction, 0, 1) * (next.elevationM - first.elevationM);
  };
  for (let sectionStartM = startDistanceM; sectionStartM < endDistanceM; sectionStartM += stepM) {
    const sectionEndM = Math.min(sectionStartM + stepM, endDistanceM);
    const startElevation = elevationAtDistance(sectionStartM);
    const endElevation = elevationAtDistance(sectionEndM);
    const slopePercent = ((endElevation - startElevation) / (sectionEndM - sectionStartM)) * 100;
    sections.push({
      startDistanceKm: sectionStartM / 1000,
      endDistanceKm: sectionEndM / 1000,
      startElevation,
      endElevation,
      slopePercent,
      band: profileBandForSlope(slopePercent),
    });
  }
  return sections;
};

export { displayStepForLength, profileBandForSlope, officialProfileSections };