@sports-alliance/sports-lib
Version:
A Library to for importing / exporting and processing GPX, TCX, FIT and JSON files from services such as Strava, Movescount, Garmin, Polar etc
186 lines (185 loc) • 10.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GradeCalculator = exports.LOOK_AHEAD_IN_METERS = exports.CLAMP = void 0;
exports.CLAMP = 50;
exports.LOOK_AHEAD_IN_METERS = 10;
class GradeCalculator {
static computeGradeStream(timeStream, distanceStream, altitudeStream, aheadMeters = exports.LOOK_AHEAD_IN_METERS, clamp = exports.CLAMP) {
const [squashedAlignedTime, squashedAlignedDist, squashedAlignedAlt] = GradeCalculator.squashAlignStreams(timeStream, distanceStream, altitudeStream);
const squashedAlignedGrade = [];
let indexNow = 0;
do {
// Remove first index of distances & altitudes stream at every loop
const aheadDistances = squashedAlignedDist.slice(indexNow);
const aheadAltitudes = squashedAlignedAlt.slice(indexNow);
// Take our current distance travelled & altitude
const distanceNow = aheadDistances[0];
const altitudeNow = aheadAltitudes[0];
// Find ahead index matching minimal distance travelled
let aheadIndex = aheadDistances.findIndex(dist => distanceNow + aheadMeters <= dist);
// Validate we find an index with distance ahead for sure, else use last index of ahead distances
aheadIndex = aheadIndex >= 0 ? aheadIndex : aheadDistances.length - 1;
// Compute deltas & grade
const aheadDeltaDistance = aheadDistances[aheadIndex] - distanceNow;
const aheadDeltaAltitude = aheadAltitudes[aheadIndex] - altitudeNow;
const aheadGrade = aheadDeltaDistance > 0 ? Math.min(Math.max((aheadDeltaAltitude / aheadDeltaDistance) * 100, -clamp), clamp) : 0;
squashedAlignedGrade.push(Math.round(aheadGrade * 10) / 10);
indexNow++;
} while (indexNow < squashedAlignedTime.length);
// Rebuild grade stream with empty values using computed squashed/aligned time & grade streams
const gradeStream = Array(altitudeStream.length).fill(null);
for (const [index, seconds] of squashedAlignedTime.entries()) {
gradeStream[seconds] = squashedAlignedGrade[index];
}
return gradeStream;
}
static computeGradeStreamByDistance(distanceStream, altitudeStream, aheadMeters = exports.LOOK_AHEAD_IN_METERS, clamp = exports.CLAMP) {
const squashedAlignedPoints = GradeCalculator.squashAlignDistanceAltitudeStreams(distanceStream, altitudeStream);
const gradeStream = Array(Math.max(distanceStream.length, altitudeStream.length)).fill(null);
if (!squashedAlignedPoints.length) {
return gradeStream;
}
const squashedAlignedGrade = [];
let indexNow = 0;
do {
const aheadPoints = squashedAlignedPoints.slice(indexNow);
const pointNow = aheadPoints[0];
let aheadIndex = aheadPoints.findIndex(point => pointNow.distance + aheadMeters <= point.distance);
aheadIndex = aheadIndex >= 0 ? aheadIndex : aheadPoints.length - 1;
const aheadPoint = aheadPoints[aheadIndex];
const aheadDeltaDistance = aheadPoint.distance - pointNow.distance;
const aheadDeltaAltitude = aheadPoint.altitude - pointNow.altitude;
const aheadGrade = aheadDeltaDistance > 0 ? Math.min(Math.max((aheadDeltaAltitude / aheadDeltaDistance) * 100, -clamp), clamp) : 0;
squashedAlignedGrade.push(Math.round(aheadGrade * 10) / 10);
indexNow++;
} while (indexNow < squashedAlignedPoints.length);
for (const [index, point] of squashedAlignedPoints.entries()) {
gradeStream[point.index] = squashedAlignedGrade[index];
}
return gradeStream;
}
/**
* Repair distance and altitude streams based on time streams
* @param timeData
* @param distanceData
* @param altitudeData
*/
static squashAlignStreams(timeData, distanceData, altitudeData) {
const squashedAlignedTime = [];
const squashedAlignedDist = [];
const squashedAlignedAlt = [];
let lastKnownDistance = distanceData.find(dist => Number.isFinite(dist));
let lastKnownAlt = altitudeData.find(alt => Number.isFinite(alt));
timeData.forEach((seconds, index) => {
// If we have time data
if (Number.isFinite(seconds)) {
if (Number.isFinite(distanceData[index]) && Number.isFinite(altitudeData[index])) {
// If we have finite distance and altitude, store values
squashedAlignedTime.push(seconds);
squashedAlignedDist.push(distanceData[index]);
squashedAlignedAlt.push(altitudeData[index]);
// Then track last known dist and alt
lastKnownDistance = distanceData[index];
lastKnownAlt = altitudeData[index];
}
else if (!Number.isFinite(distanceData[index]) && Number.isFinite(altitudeData[index])) {
// If only altitude is finite, then store altitude, and use last known distance instead of empty distance
squashedAlignedTime.push(seconds);
squashedAlignedDist.push(lastKnownDistance);
squashedAlignedAlt.push(altitudeData[index]);
// Then track last known alt
lastKnownAlt = altitudeData[index];
}
else if (Number.isFinite(distanceData[index]) && !Number.isFinite(altitudeData[index])) {
// If only distance is finite, then store distance, and use last known altitude instead of empty altitude
squashedAlignedTime.push(seconds);
squashedAlignedDist.push(distanceData[index]);
squashedAlignedAlt.push(lastKnownAlt);
// Then track last known distance
lastKnownDistance = distanceData[index];
}
else {
// Do nothing. Don't store any time or distance data
}
}
else {
// Do nothing. Don't store any time or distance data
}
});
return [squashedAlignedTime, squashedAlignedDist, squashedAlignedAlt];
}
static squashAlignDistanceAltitudeStreams(distanceData, altitudeData) {
const squashedAlignedPoints = [];
let lastKnownDistance = null;
let lastKnownAlt = null;
Array.from({ length: Math.max(distanceData.length, altitudeData.length) }).forEach((_value, index) => {
const distance = distanceData[index];
const altitude = altitudeData[index];
if (Number.isFinite(distance) && Number.isFinite(altitude)) {
squashedAlignedPoints.push({
index,
distance: distance,
altitude: altitude
});
lastKnownDistance = distance;
lastKnownAlt = altitude;
}
else if (!Number.isFinite(distance) && Number.isFinite(altitude) && Number.isFinite(lastKnownDistance)) {
squashedAlignedPoints.push({
index,
distance: lastKnownDistance,
altitude: altitude
});
lastKnownAlt = altitude;
}
else if (Number.isFinite(distance) && !Number.isFinite(altitude) && Number.isFinite(lastKnownAlt)) {
squashedAlignedPoints.push({
index,
distance: distance,
altitude: lastKnownAlt
});
lastKnownDistance = distance;
}
});
return squashedAlignedPoints;
}
/**
* Contains a 5th order equation which models the Strava GAP behavior described on picture "./strava_gap_modelization.png"
* ------------------------------------------------------------------------------------
* Get Real Strava Premium Grade Adjusted Pace on every strava activities with below gist
* https://gist.github.com/thomaschampagne/2781dce212d12cd048728e70ae791a30
* ------------------------------------------------------------------------------------
*
* This Strava GAP behavior is described by the below data
* [{ grade: -34, speedFactor: 1.7 }, { grade: -32, speedFactor: 1.6 }, { grade: -30, speedFactor: 1.5 },
* { grade: -28, speedFactor: 1.4 }, { grade: -26, speedFactor: 1.3 }, { grade: -24, speedFactor: 1.235 },
* { grade: -22, speedFactor: 1.15 }, { grade: -20, speedFactor: 1.09 }, { grade: -18, speedFactor: 1.02 },
* { grade: -16, speedFactor: 0.95 }, { grade: -14, speedFactor: 0.91 }, { grade: -12, speedFactor: 0.89 },
* { grade: -10, speedFactor: 0.88 }, { grade: -8, speedFactor: 0.88 }, { grade: -6, speedFactor: 0.89 },
* { grade: -4, speedFactor: 0.91 }, { grade: -2, speedFactor: 0.95 }, { grade: 0, speedFactor: 1 },
* { grade: 2, speedFactor: 1.05 }, { grade: 4, speedFactor: 1.14 }, { grade: 6, speedFactor: 1.24 },
* { grade: 8, speedFactor: 1.34 }, { grade: 10, speedFactor: 1.47 }, { grade: 12, speedFactor: 1.5 },
* { grade: 14, speedFactor: 1.76 }, { grade: 16, speedFactor: 1.94 }, { grade: 18, speedFactor: 2.11 },
* { grade: 20, speedFactor: 2.3 }, { grade: 22, speedFactor: 2.4 }, { grade: 24, speedFactor: 2.48 },
* { grade: 26, speedFactor: 2.81 }, { grade: 28, speedFactor: 3 }, { grade: 30, speedFactor: 3.16 },
* { grade: 32, speedFactor: 3.31 }, { grade: 34, speedFactor: 3.49 } ]
*
* The 5th order equation has been curve fitted using plot.ly
*/
static estimateAdjustedSpeed(speedMeterSeconds, grade) {
const kA = 1;
const kB = 0.029290920646623777;
const kC = 0.0018083953212790634;
const kD = 4.0662425671715924e-7;
const kE = -3.686186584867523e-7;
const kF = -2.6628107325930747e-9;
const speedAdjust = kA +
kB * grade +
kC * Math.pow(grade, 2) +
kD * Math.pow(grade, 3) +
kE * Math.pow(grade, 4) +
kF * Math.pow(grade, 5);
return speedMeterSeconds * speedAdjust;
}
}
exports.GradeCalculator = GradeCalculator;