summaryrefslogtreecommitdiff
path: root/web/static/ride-detail-logic.js
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-16 13:25:21 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-16 13:25:21 +0200
commit2d18d177f48285eb123e7da8680fc00e7a6fe19c (patch)
tree2b339bcec21c244242f6e26d4444feebdf3b73d0 /web/static/ride-detail-logic.js
parentbe78280c669a04c112afaf1801f22ed2eec7c55a (diff)
refactor(web): split ride detail JavaScript
Diffstat (limited to 'web/static/ride-detail-logic.js')
-rw-r--r--web/static/ride-detail-logic.js82
1 files changed, 82 insertions, 0 deletions
diff --git a/web/static/ride-detail-logic.js b/web/static/ride-detail-logic.js
new file mode 100644
index 0000000..4ab408b
--- /dev/null
+++ b/web/static/ride-detail-logic.js
@@ -0,0 +1,82 @@
+export const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
+
+export const formatDistance = (distance) => `${distance.toFixed(distance < 10 ? 1 : 0)} km`;
+
+export const formatElevation = (elevation) => `${Math.round(elevation)} m`;
+
+export const nearestPointIndex = (points, distance) => {
+ 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 categoryForScore = (score) => {
+ 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";
+ return "HC";
+};
+
+export const elevationAtDistance = (points, index, distanceM) => {
+ 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, startIndex, endIndex) => {
+ const startDistanceM = points[startIndex].distanceKm * 1000;
+ const lastDistanceM = points[endIndex].distanceKm * 1000;
+ if (lastDistanceM <= startDistanceM) return 0;
+ let score = 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) score += ((segmentEndM - segmentStartM) / 1000) * (slope * 100) ** 2;
+ }
+ return score;
+};
+
+export const climbMetrics = (points, bounds) => {
+ 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 score = distanceKm > 0 ? ((Math.abs(elevationGain) * elevationGain) / (distanceKm * 1000)) * 10 : 0;
+ return {
+ start,
+ end,
+ distanceKm,
+ elevationGain,
+ slope,
+ score,
+ cotacol: cotacolForClimb(points, bounds.startIndex, bounds.endIndex),
+ category: categoryForScore(score),
+ };
+};