summaryrefslogtreecommitdiff
path: root/web/frontend/ride-detail-logic.ts
blob: f8eb040a8b252bd9a6f30134843607cca3695d5f (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
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
import type { ClimbBounds, ClimbMetrics, ProfilePoint } from "./types.js";

export const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));

export const formatDistance = (distance: number): string => `${distance.toFixed(distance < 10 ? 1 : 0)} km`;

export const formatElevation = (elevation: number): string => `${Math.round(elevation)} m`;

export const nearestPointIndex = (points: ProfilePoint[], distance: number): number => {
  let low = 0;
  let high = points.length - 1;
  while (low < high) {
    const middle = Math.floor((low + high) / 2);
    if (points[middle].distanceKm < distance) low = middle + 1;
    else high = middle;
  }
  if (low === 0) return low;
  const previous = points[low - 1];
  return distance - previous.distanceKm < points[low].distanceKm - distance ? low - 1 : low;
};

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, officialClimb = false): string => {
  if (name) return officialClimb ? `${name} (${category})` : name;
  return `${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;
  const endDistanceM = points[index + 1].distanceKm * 1000;
  const fraction = (distanceM - startDistanceM) / (endDistanceM - startDistanceM);
  return points[index].elevationM + fraction * (points[index + 1].elevationM - points[index].elevationM);
};

export const cotacolForClimb = (points: ProfilePoint[], startIndex: number, endIndex: number): number => {
  const startDistanceM = points[startIndex].distanceKm * 1000;
  const lastDistanceM = points[endIndex].distanceKm * 1000;
  if (lastDistanceM <= startDistanceM) return 0;
  let cotacol = 0;
  let pointIndex = startIndex;
  for (let segmentStartM = startDistanceM; segmentStartM < lastDistanceM; segmentStartM += 100) {
    const segmentEndM = Math.min(segmentStartM + 100, lastDistanceM);
    while (pointIndex < endIndex && points[pointIndex + 1].distanceKm * 1000 <= segmentStartM) pointIndex++;
    const startElevation = elevationAtDistance(points, pointIndex, segmentStartM);
    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) cotacol += ((segmentEndM - segmentStartM) / 1000) * (slope * 100) ** 2;
  }
  return cotacol;
};

export const climbMetrics = (points: ProfilePoint[], bounds: ClimbBounds | undefined): ClimbMetrics | null => {
  if (
    !bounds ||
    !Number.isInteger(bounds.startIndex) ||
    !Number.isInteger(bounds.endIndex) ||
    bounds.startIndex < 0 ||
    bounds.endIndex >= points.length ||
    bounds.startIndex >= bounds.endIndex
  ) {
    return null;
  }
  const start = points[bounds.startIndex];
  const end = points[bounds.endIndex];
  const distanceKm = end.distanceKm - start.distanceKm;
  const elevationGain = end.elevationM - start.elevationM;
  const slope = distanceKm > 0 ? elevationGain / (distanceKm * 10) : 0;
  const cotacol = cotacolForClimb(points, bounds.startIndex, bounds.endIndex);
  return {
    start,
    end,
    distanceKm,
    elevationGain,
    slope,
    cotacol,
    category: categoryForCotacol(cotacol),
  };
};