@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
JavaScript
;
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;