@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
128 lines (127 loc) • 5.29 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MAXIMUM_ALLOWED_POWER_CURVE_BRACKET_DURATION_RATIO = exports.DEFAULT_POWER_CURVE_MAXIMUM_BRACKET_DURATION_RATIO = void 0;
exports.samplePowerCurveAtDuration = samplePowerCurveAtDuration;
exports.comparePowerCurveWindows = comparePowerCurveWindows;
exports.DEFAULT_POWER_CURVE_MAXIMUM_BRACKET_DURATION_RATIO = 1.25;
exports.MAXIMUM_ALLOWED_POWER_CURVE_BRACKET_DURATION_RATIO = 2;
/**
* Samples a mean-max curve in reciprocal-duration (1/t) space.
*
* Exact durations win, duplicate durations retain their strongest positive value, and interpolation
* requires two neighboring points whose duration ratio is at most 1.25 by default (2 is the hard max).
* Durations outside the curve are never extrapolated.
*/
function samplePowerCurveAtDuration(points, durationSeconds, options = {}) {
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
return null;
}
const key = options.key || 'power';
const normalized = normalizeCurve(points, key);
return sampleNormalizedCurveAtDuration(normalized, durationSeconds, resolveMaximumBracketDurationRatio(options));
}
/**
* Compares aligned curve samples. Retention is recent/reference × 100; delta is retention minus 100.
* Each source curve is normalized and sorted once for the complete duration set.
*/
function comparePowerCurveWindows(recentPoints, referencePoints, durationSeconds, options = {}) {
const key = options.key || 'power';
const recent = normalizeCurve(recentPoints, key);
const reference = normalizeCurve(referencePoints, key);
const maximumBracketDurationRatio = resolveMaximumBracketDurationRatio(options);
return durationSeconds.map(duration => {
const recentValue = sampleNormalizedCurveAtDuration(recent, duration, maximumBracketDurationRatio);
const referenceValue = sampleNormalizedCurveAtDuration(reference, duration, maximumBracketDurationRatio);
const retentionPercent = recentValue !== null && referenceValue !== null && referenceValue > 0
? (recentValue / referenceValue) * 100
: null;
return {
durationSeconds: duration,
recentValue,
referenceValue,
retentionPercent,
deltaPercent: retentionPercent === null ? null : retentionPercent - 100
};
});
}
function sampleNormalizedCurveAtDuration(points, durationSeconds, maximumBracketDurationRatio) {
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
return null;
}
let low = 0;
let high = points.length;
while (low < high) {
const middle = low + Math.floor((high - low) / 2);
if (points[middle].duration < durationSeconds) {
low = middle + 1;
}
else {
high = middle;
}
}
if (low < points.length && points[low].duration === durationSeconds) {
return points[low].value;
}
if (low <= 0 || low >= points.length) {
return null;
}
const left = points[low - 1];
const right = points[low];
if (right.duration / left.duration > maximumBracketDurationRatio) {
return null;
}
const leftX = 1 / left.duration;
const rightX = 1 / right.duration;
const targetX = 1 / durationSeconds;
const ratio = (targetX - leftX) / (rightX - leftX);
return left.value + (right.value - left.value) * ratio;
}
function resolveMaximumBracketDurationRatio(options) {
const requestedMaximumRatio = options.maximumBracketDurationRatio;
return typeof requestedMaximumRatio === 'number' &&
Number.isFinite(requestedMaximumRatio) &&
requestedMaximumRatio >= 1 &&
requestedMaximumRatio <= exports.MAXIMUM_ALLOWED_POWER_CURVE_BRACKET_DURATION_RATIO
? requestedMaximumRatio
: exports.DEFAULT_POWER_CURVE_MAXIMUM_BRACKET_DURATION_RATIO;
}
function normalizeCurve(points, key) {
const byDuration = new Map();
(points || []).forEach(point => {
const duration = toFiniteValue(point === null || point === void 0 ? void 0 : point.duration);
const value = toFiniteValue(point === null || point === void 0 ? void 0 : point[key]);
if (duration === null || duration <= 0 || value === null || value <= 0) {
return;
}
byDuration.set(duration, Math.max(value, byDuration.get(duration) || 0));
});
return [...byDuration.entries()]
.map(([duration, value]) => ({ duration, value }))
.sort((left, right) => left.duration - right.duration);
}
function toFiniteValue(value, seen = new Set()) {
if (typeof value === 'number') {
return Number.isFinite(value) ? value : null;
}
if (typeof value === 'string') {
if (!value.trim()) {
return null;
}
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : null;
}
if (!value || typeof value !== 'object' || seen.has(value)) {
return null;
}
const getter = value.getValue;
if (typeof getter !== 'function') {
return null;
}
seen.add(value);
try {
return toFiniteValue(getter.call(value), seen);
}
catch (_error) {
return null;
}
}