UNPKG

@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

188 lines (187 loc) 10.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GradeCalculator = exports.LOOK_AHEAD_IN_METERS = exports.LOOK_AHEAD_IN_SECONDS = exports.CLAMP = void 0; var helpers_1 = require("../helpers"); var low_pass_filter_1 = require("./low-pass-filter"); var KalmanFilter = require('kalmanjs'); exports.CLAMP = 40; exports.LOOK_AHEAD_IN_SECONDS = 2; exports.LOOK_AHEAD_IN_METERS = 10; var GradeCalculator = /** @class */ (function () { function GradeCalculator() { } GradeCalculator.computeGrade = function (previousDistance, currentDistance, previousAltitude, currentAltitude) { previousDistance = previousDistance !== null ? previousDistance : 0; currentDistance = currentDistance !== null ? currentDistance : 0; previousAltitude = previousAltitude !== null ? previousAltitude : 0; currentAltitude = currentAltitude !== null ? currentAltitude : 0; var distanceDelta = currentDistance - previousDistance; var altitudeDelta = currentAltitude - previousAltitude; if (distanceDelta === 0) { return 0; } var percentage = (altitudeDelta / distanceDelta) * 100; percentage = Math.min(Math.max(percentage, -exports.CLAMP), exports.CLAMP); // Clamp between -CLAMP% & CLAMP% return Math.round(percentage * 10) / 10; }; // Find the next i for distance = prev distance + 5m // @todo perhaps this threshold should be per avg speed / activity type // Or just perhaps the speed? // Perhaps round GradeCalculator.computeGradeStream = function (distanceStream, altitudeStream, filterAltitude, filterGrade, basedOnAltitude, lookAhead, lookAheadInTime) { if (filterAltitude === void 0) { filterAltitude = true; } if (filterGrade === void 0) { filterGrade = true; } if (basedOnAltitude === void 0) { basedOnAltitude = true; } if (lookAhead === void 0) { lookAhead = true; } if (lookAheadInTime === void 0) { lookAheadInTime = true; } // First filter the altitude to remove any noise and predict if (filterAltitude) { var kf_1 = new KalmanFilter(); altitudeStream = altitudeStream.map(function (v) { return (v === null ? null : kf_1.filter(v)); }); } var gradeStream = basedOnAltitude ? this.computeGradeStreamBasedOnAltitude(distanceStream, altitudeStream, lookAhead, lookAheadInTime) : this.computeGradeStreamBasedOnDistance(distanceStream, altitudeStream, lookAhead, lookAheadInTime); if (filterGrade) { var kf_2 = new KalmanFilter(); gradeStream = gradeStream.map(function (v) { return (v === null ? null : kf_2.filter(v)); }); gradeStream = new low_pass_filter_1.LowPassFilter(0.2).smoothArray(gradeStream); } return gradeStream.map(function (v) { return (v === null ? null : Math.round(v * 10) / 10); }); }; /** * Contains a 5th order equation which models the Strava GAP behavior described on picture "./fixture/strava_gap_modelization.png" * * 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 */ GradeCalculator.estimateAdjustedSpeed = function (speedMeterSeconds, grade) { var kA = 0.9944001227713231; var kB = 0.029290920646623777; var kC = 0.0018083953212790634; var kD = 4.0662425671715924e-7; var kE = -3.686186584867523e-7; var kF = -2.6628107325930747e-9; var 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; }; GradeCalculator.computeGradeStreamBasedOnDistance = function (distanceStream, altitudeStream, lookAhead, lookAheadInTime) { var numericAltitudeStream = this.getAltitudeStreamRepaired(altitudeStream); // Reset previous altitude to first element of the numeric array var previousAltitude = numericAltitudeStream[0]; var previousDistance = 0; // Start var gradeStream = Array(altitudeStream.length).fill(null); for (var i = 0; i < distanceStream.length; i++) { var nextIndex = 0; if (lookAheadInTime && lookAhead) { nextIndex = exports.LOOK_AHEAD_IN_SECONDS; } else if (lookAhead) { nextIndex = distanceStream .slice(i) .findIndex(function (d) { return (d === null ? false : d >= previousDistance + exports.LOOK_AHEAD_IN_METERS); }); } nextIndex = nextIndex === -1 ? 0 : nextIndex; // Set the distance previousDistance = distanceStream[i - 1] || previousDistance; var currentDistance = distanceStream[i + nextIndex] || previousDistance; // If no distance current distance will be prev // Find the previous altitude if possible or use an older value previousAltitude = helpers_1.isNumber(numericAltitudeStream[i - 1]) ? numericAltitudeStream[i - 1] : previousAltitude; // If the current (real) distance is null return null and buffer the previous altitude till distance is not null if (distanceStream[i] === null) { numericAltitudeStream[i] = previousAltitude; gradeStream[i] = null; continue; } // perhaps the altitude based can benefit if (currentDistance - previousDistance === 0) { numericAltitudeStream[i] = previousAltitude; gradeStream[i] = 0; continue; } // Set the current altitude var currentAltitude = numericAltitudeStream[i + nextIndex] || previousDistance; // Calc gradeStream[i] = GradeCalculator.computeGrade(previousDistance, currentDistance, previousAltitude, currentAltitude); } return gradeStream; }; GradeCalculator.computeGradeStreamBasedOnAltitude = function (distanceStream, altitudeStream, lookAhead, lookAheadInTime) { var numericAltitudeStream = this.getAltitudeStreamRepaired(altitudeStream); var numericDistanceStream = this.getDistanceStreamRepaired(distanceStream); // Reset previous altitude to first element of the numeric array var previousAltitude = numericAltitudeStream[0]; var previousDistance = 0; // Start var gradeStream = Array(altitudeStream.length).fill(null); for (var i = 0; i < altitudeStream.length; i++) { var nextIndex = 0; if (lookAheadInTime && lookAhead) { nextIndex = exports.LOOK_AHEAD_IN_SECONDS; } else if (lookAhead) { nextIndex = distanceStream .slice(i) .findIndex(function (d) { return (d === null ? false : d >= previousDistance + exports.LOOK_AHEAD_IN_METERS); }); } nextIndex = nextIndex === -1 ? 0 : nextIndex; // We need to check against 0's with is number previousAltitude = helpers_1.isNumber(numericAltitudeStream[i - 1]) ? numericAltitudeStream[i - 1] : previousAltitude; var currentAltitude = numericAltitudeStream[i + nextIndex] || previousAltitude; previousDistance = helpers_1.isNumber(numericDistanceStream[i - 1]) ? numericDistanceStream[i - 1] : previousDistance; // If based on altitude return null where altitude is null if (altitudeStream[i] === null) { numericDistanceStream[i] = previousDistance; gradeStream[i] = null; continue; } previousDistance = numericDistanceStream[i - 1] || previousDistance; var currentDistance = numericDistanceStream[i + nextIndex] || previousDistance; // Calc gradeStream[i] = GradeCalculator.computeGrade(previousDistance, currentDistance, previousAltitude, currentAltitude); } return gradeStream; }; GradeCalculator.getAltitudeStreamRepaired = function (altitudeStream) { // Back and forth fill a new altitude stream. var altitudeSearch = altitudeStream.find(function (v) { return v !== null; }); // @todo If there is no altitude in the altitude array we just should return an grade stream of 0's return altitudeStream.reduce(function (accu, value) { altitudeSearch = value !== null ? value : altitudeSearch; accu.push(altitudeSearch); return accu; }, []); }; GradeCalculator.getDistanceStreamRepaired = function (distanceStream) { // Back and forth fill a new Distance stream. var previousDistance = 0; return distanceStream.reduce(function (accu, value) { previousDistance = value !== null ? value : previousDistance; accu.push(previousDistance); return accu; }, []); }; return GradeCalculator; }()); exports.GradeCalculator = GradeCalculator;