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

128 lines (127 loc) 5.29 kB
"use strict"; 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; } }