@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
266 lines (265 loc) • 19.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const event_utilities_1 = require("./event.utilities");
const event_1 = require("../event");
const activity_1 = require("../../activities/activity");
const data_power_1 = require("../../data/data.power");
const data_power_curve_1 = require("../../data/data.power-curve");
// @ts-ignore
const activity_types_1 = require("../../activities/activity.types");
const activity_utilities_1 = require("./activity.utilities");
const importer_fit_1 = require("../adapters/importers/fit/importer.fit");
const data_air_power_avg_1 = require("../../data/data.air-power-avg");
const data_air_power_min_1 = require("../../data/data.air-power-min");
const data_air_power_max_1 = require("../../data/data.air-power-max");
const data_vertical_speed_min_1 = require("../../data/data.vertical-speed-min");
const data_vertical_speed_max_1 = require("../../data/data.vertical-speed-max");
const data_ground_contact_time_min_1 = require("../../data/data.ground-contact-time-min");
const data_ground_contact_time_max_1 = require("../../data/data.ground-contact-time-max");
const data_vertical_oscillation_min_1 = require("../../data/data.vertical-oscillation-min");
const data_vertical_oscillation_max_1 = require("../../data/data.vertical-oscillation-max");
const data_grade_adjusted_pace_min_1 = require("../../data/data.grade-adjusted-pace-min");
const data_grade_adjusted_pace_max_1 = require("../../data/data.grade-adjusted-pace-max");
const data_pace_min_1 = require("../../data/data.pace-min");
const data_pace_max_1 = require("../../data/data.pace-max");
const data_temperature_min_1 = require("../../data/data.temperature-min");
const data_altitude_avg_1 = require("../../data/data.altitude-avg");
const data_grade_adjusted_pace_1 = require("../../data/data.grade-adjusted-pace");
const data_pace_1 = require("../../data/data.pace");
const data_duration_1 = require("../../data/data.duration");
const data_pause_1 = require("../../data/data.pause");
const data_distance_1 = require("../../data/data.distance");
const data_effort_pace_avg_1 = require("../../data/data.effort-pace-avg");
const data_effort_pace_min_1 = require("../../data/data.effort-pace-min");
const data_effort_pace_max_1 = require("../../data/data.effort-pace-max");
describe('EventUtilities Power Curve Aggregation', () => {
const createMockActivity = (powerValues, weight) => {
const startDate = new Date();
const endDate = new Date(startDate.getTime() + powerValues.length * 1000);
// @ts-ignore
const act = new activity_1.Activity(startDate, endDate, activity_types_1.ActivityTypes.Cycling, { toJSON: () => ({}) });
const powerStream = act.createStream(data_power_1.DataPower.type);
powerStream.setData(powerValues);
act.addStream(powerStream);
if (weight) {
// Mock getStat for weight
act.getStat = jest.fn().mockImplementation(type => {
if (type === 'Weight')
return { getValue: () => weight };
if (type === 'PowerCurve')
return curve; // Return the pre-calculated curve AFTER it's calculated
return null;
});
}
// Setup initial getStat for calculation if needed, but actually verify if calculateMeanMaxPower uses getStat.
// Yes, it uses act.getStat(DataWeight.type). So we must mock it BEFORE calculation.
act.getStat = jest.fn().mockImplementation(type => {
if (type === 'Weight' && weight)
return { getValue: () => weight };
return null;
});
// Pre-calculate curve for activity
// Note: this uses the mocked getStat above to find weight!
const curve = activity_utilities_1.ActivityUtilities.calculateMeanMaxPower(act, [10]);
act.addStat(curve);
// Re-mock getStat to include the curve we just calculated
act.getStat = jest.fn().mockImplementation(type => {
if (type === 'Weight' && weight)
return { getValue: () => weight };
if (type === 'PowerCurve')
return curve;
return null;
});
// Mock getDuration
act.getDuration = jest.fn().mockReturnValue({ getValue: () => powerValues.length });
// Mock getPause
act.getPause = jest.fn().mockReturnValue({ getValue: () => 0 });
// Mock getDistance
act.getDistance = jest.fn().mockReturnValue({ getValue: () => 1000 });
return act;
};
it('should aggregate power curves from multiple activities by taking maximums', () => {
// Activity 1: Constant 200W
const act1 = createMockActivity(new Array(100).fill(200));
// Activity 2: Constant 300W
const act2 = createMockActivity(new Array(100).fill(300));
// @ts-ignore
const event = new event_1.Event('Test Event', new Date(), new Date(), 'fit', 0, 'desc', true);
event.addActivities([act1, act2]);
// Run aggregation logic
event_utilities_1.EventUtilities.reGenerateStatsForEvent(event);
const eventCurveStat = event.getStat(data_power_curve_1.DataPowerCurve.type);
expect(eventCurveStat).toBeDefined();
const points = eventCurveStat.getValue();
// defined duration in createMockActivity is [10]
const point10s = points.find(p => p.duration.getValue() === 10);
expect(point10s).toBeDefined();
// Should take the max from Act 2 (300W)
expect(point10s.power.getValue()).toBe(300);
});
it('should aggregate W/kg correctly', () => {
// Act 1: 300W @ 100kg = 3.0 W/kg
const act1 = createMockActivity(new Array(100).fill(300), 100);
// Act 2: 300W @ 75kg = 4.0 W/kg
const act2 = createMockActivity(new Array(100).fill(300), 75);
// @ts-ignore
const event = new event_1.Event('Test Event 2', new Date(), new Date(), 'fit', 0, 'desc', true);
event.addActivities([act1, act2]);
event_utilities_1.EventUtilities.reGenerateStatsForEvent(event);
const points = event.getStat(data_power_curve_1.DataPowerCurve.type).getValue();
const point10s = points.find(p => p.duration.getValue() === 10);
expect(point10s.wattsPerKg).toBeDefined();
expect(point10s.wattsPerKg.getValue()).toBe(4.0); // Should take the higher W/kg
});
describe('mergeEvents summary stats integration', () => {
const toArrayBuffer = (filePath) => {
const fileContent = fs.readFileSync(filePath);
return fileContent.buffer.slice(fileContent.byteOffset, fileContent.byteOffset + fileContent.byteLength);
};
const loadEventFromFixture = (relativePath) => __awaiter(void 0, void 0, void 0, function* () {
const fitPath = path.join(__dirname, '..', '..', 'specs', 'fixtures', relativePath);
const event = yield importer_fit_1.EventImporterFIT.getFromArrayBuffer(toArrayBuffer(fitPath));
// Precompute activity stats so merge logic has them
event.getActivities().forEach(activity => activity_utilities_1.ActivityUtilities.generateMissingStreamsAndStatsForActivity(activity));
return event;
});
const ensureMinStatFromStream = (activity, statType, streamType, StatCtor) => {
const stat = activity.getStat(statType);
if (stat && stat.getValue() !== Infinity)
return;
if (!activity.hasStreamData(streamType))
return;
const stream = activity.getStreamData(streamType);
if (!stream || stream.length === 0)
return;
const finite = stream.filter((v) => Number.isFinite(v));
if (!finite.length)
return;
activity.addStat(new StatCtor(Math.min(...finite)));
};
it('propagates new min/max/avg stats into merged event', () => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
// 6860622783.fit carries Air Power, Ground Contact Time, Vertical Oscillation
const eventA = yield loadEventFromFixture(path.join('runs', 'fit', '6860622783.fit'));
// 6909950168.fit carries Vertical Speed, Pace, Grade Adjusted Pace, Temperature
const eventB = yield loadEventFromFixture(path.join('runs', 'fit', '6909950168.fit'));
// Ensure min pace and grade-adjusted pace stats exist when not set by importer
eventA.getActivities().forEach(activity => {
ensureMinStatFromStream(activity, data_pace_min_1.DataPaceMin.type, data_pace_1.DataPace.type, data_pace_min_1.DataPaceMin);
ensureMinStatFromStream(activity, data_grade_adjusted_pace_min_1.DataGradeAdjustedPaceMin.type, data_grade_adjusted_pace_1.DataGradeAdjustedPace.type, data_grade_adjusted_pace_min_1.DataGradeAdjustedPaceMin);
});
eventB.getActivities().forEach(activity => {
ensureMinStatFromStream(activity, data_pace_min_1.DataPaceMin.type, data_pace_1.DataPace.type, data_pace_min_1.DataPaceMin);
ensureMinStatFromStream(activity, data_grade_adjusted_pace_min_1.DataGradeAdjustedPaceMin.type, data_grade_adjusted_pace_1.DataGradeAdjustedPace.type, data_grade_adjusted_pace_min_1.DataGradeAdjustedPaceMin);
});
const merged = event_utilities_1.EventUtilities.mergeEvents([eventA, eventB]);
const getStat = (type) => merged.getStat(type);
const aActivity = eventA.getFirstActivity();
const bActivity = eventB.getFirstActivity();
// Air Power (only present in eventA)
expect(getStat(data_air_power_avg_1.DataAirPowerAvg.type).getValue()).toBe(aActivity.getStat(data_air_power_avg_1.DataAirPowerAvg.type).getValue());
expect(getStat(data_air_power_min_1.DataAirPowerMin.type).getValue()).toBe(aActivity.getStat(data_air_power_min_1.DataAirPowerMin.type).getValue());
expect(getStat(data_air_power_max_1.DataAirPowerMax.type).getValue()).toBe(aActivity.getStat(data_air_power_max_1.DataAirPowerMax.type).getValue());
// Vertical Speed (only present in eventB)
expect(getStat(data_vertical_speed_min_1.DataVerticalSpeedMin.type).getValue()).toBe(bActivity.getStat(data_vertical_speed_min_1.DataVerticalSpeedMin.type).getValue());
expect(getStat(data_vertical_speed_max_1.DataVerticalSpeedMax.type).getValue()).toBe(bActivity.getStat(data_vertical_speed_max_1.DataVerticalSpeedMax.type).getValue());
// Ground Contact Time (present in eventA)
expect(getStat(data_ground_contact_time_min_1.DataGroundContactTimeMin.type).getValue()).toBe(aActivity.getStat(data_ground_contact_time_min_1.DataGroundContactTimeMin.type).getValue());
expect(getStat(data_ground_contact_time_max_1.DataGroundContactTimeMax.type).getValue()).toBe(aActivity.getStat(data_ground_contact_time_max_1.DataGroundContactTimeMax.type).getValue());
// Vertical Oscillation (present in eventA)
expect(getStat(data_vertical_oscillation_min_1.DataVerticalOscillationMin.type).getValue()).toBe(aActivity.getStat(data_vertical_oscillation_min_1.DataVerticalOscillationMin.type).getValue());
expect(getStat(data_vertical_oscillation_max_1.DataVerticalOscillationMax.type).getValue()).toBe(aActivity.getStat(data_vertical_oscillation_max_1.DataVerticalOscillationMax.type).getValue());
// Grade Adjusted Pace (present in eventB)
const gapMinExpected = Math.min(Number((_b = (_a = aActivity.getStat(data_grade_adjusted_pace_min_1.DataGradeAdjustedPaceMin.type)) === null || _a === void 0 ? void 0 : _a.getValue()) !== null && _b !== void 0 ? _b : Infinity), Number((_d = (_c = bActivity.getStat(data_grade_adjusted_pace_min_1.DataGradeAdjustedPaceMin.type)) === null || _c === void 0 ? void 0 : _c.getValue()) !== null && _d !== void 0 ? _d : Infinity));
const gapMaxExpected = Math.max(Number((_f = (_e = aActivity.getStat(data_grade_adjusted_pace_max_1.DataGradeAdjustedPaceMax.type)) === null || _e === void 0 ? void 0 : _e.getValue()) !== null && _f !== void 0 ? _f : -Infinity), Number((_h = (_g = bActivity.getStat(data_grade_adjusted_pace_max_1.DataGradeAdjustedPaceMax.type)) === null || _g === void 0 ? void 0 : _g.getValue()) !== null && _h !== void 0 ? _h : -Infinity));
expect(getStat(data_grade_adjusted_pace_min_1.DataGradeAdjustedPaceMin.type).getValue()).toBe(gapMinExpected);
expect(getStat(data_grade_adjusted_pace_max_1.DataGradeAdjustedPaceMax.type).getValue()).toBe(gapMaxExpected);
// Pace (present in eventB)
const paceMinExpected = Math.min(Number((_k = (_j = aActivity.getStat(data_pace_min_1.DataPaceMin.type)) === null || _j === void 0 ? void 0 : _j.getValue()) !== null && _k !== void 0 ? _k : Infinity), Number((_m = (_l = bActivity.getStat(data_pace_min_1.DataPaceMin.type)) === null || _l === void 0 ? void 0 : _l.getValue()) !== null && _m !== void 0 ? _m : Infinity));
const paceMaxExpected = Math.max(Number((_p = (_o = aActivity.getStat(data_pace_max_1.DataPaceMax.type)) === null || _o === void 0 ? void 0 : _o.getValue()) !== null && _p !== void 0 ? _p : -Infinity), Number((_r = (_q = bActivity.getStat(data_pace_max_1.DataPaceMax.type)) === null || _q === void 0 ? void 0 : _q.getValue()) !== null && _r !== void 0 ? _r : -Infinity));
expect(getStat(data_pace_min_1.DataPaceMin.type).getValue()).toBe(paceMinExpected);
expect(getStat(data_pace_max_1.DataPaceMax.type).getValue()).toBe(paceMaxExpected);
// Temperature min across activities
const expectedTempMin = Math.min(Number((_t = (_s = aActivity.getStat(data_temperature_min_1.DataTemperatureMin.type)) === null || _s === void 0 ? void 0 : _s.getValue()) !== null && _t !== void 0 ? _t : Infinity), Number((_v = (_u = bActivity.getStat(data_temperature_min_1.DataTemperatureMin.type)) === null || _u === void 0 ? void 0 : _u.getValue()) !== null && _v !== void 0 ? _v : Infinity));
expect(getStat(data_temperature_min_1.DataTemperatureMin.type).getValue()).toBe(expectedTempMin);
// Altitude average across activities
const altitudeVals = [
Number((_w = aActivity.getStat(data_altitude_avg_1.DataAltitudeAvg.type)) === null || _w === void 0 ? void 0 : _w.getValue()),
Number((_x = bActivity.getStat(data_altitude_avg_1.DataAltitudeAvg.type)) === null || _x === void 0 ? void 0 : _x.getValue())
].filter(v => Number.isFinite(v));
const expectedAltAvg = altitudeVals.reduce((sum, v) => sum + v, 0) / altitudeVals.length;
expect(getStat(data_altitude_avg_1.DataAltitudeAvg.type).getValue()).toBe(expectedAltAvg);
}));
it('aggregates effort pace min/max/avg into merged events', () => {
var _a, _b, _c;
const creator = { toJSON: () => ({}) };
const createActivity = (startDate, avgEffortPace, minEffortPace, maxEffortPace) => {
// @ts-ignore
const activity = new activity_1.Activity(startDate, new Date(startDate.getTime() + 3600 * 1000), activity_types_1.ActivityTypes.Running, creator);
activity.addStat(new data_duration_1.DataDuration(3600));
activity.addStat(new data_pause_1.DataPause(0));
activity.addStat(new data_distance_1.DataDistance(10000));
activity.addStat(new data_effort_pace_avg_1.DataEffortPaceAvg(avgEffortPace));
activity.addStat(new data_effort_pace_min_1.DataEffortPaceMin(minEffortPace));
activity.addStat(new data_effort_pace_max_1.DataEffortPaceMax(maxEffortPace));
return activity;
};
const activityA = createActivity(new Date('2026-01-01T10:00:00.000Z'), 300, 270, 340);
const activityB = createActivity(new Date('2026-01-01T12:00:00.000Z'), 330, 290, 360);
// @ts-ignore
const eventA = new event_1.Event('Event A', activityA.startDate, activityA.endDate, 'fit', 0, 'a', true);
// @ts-ignore
const eventB = new event_1.Event('Event B', activityB.startDate, activityB.endDate, 'fit', 0, 'b', true);
eventA.addActivities([activityA]);
eventB.addActivities([activityB]);
const merged = event_utilities_1.EventUtilities.mergeEvents([eventA, eventB]);
expect((_a = merged.getStat(data_effort_pace_avg_1.DataEffortPaceAvg.type)) === null || _a === void 0 ? void 0 : _a.getValue()).toBe(315);
expect((_b = merged.getStat(data_effort_pace_min_1.DataEffortPaceMin.type)) === null || _b === void 0 ? void 0 : _b.getValue()).toBe(270);
expect((_c = merged.getStat(data_effort_pace_max_1.DataEffortPaceMax.type)) === null || _c === void 0 ? void 0 : _c.getValue()).toBe(360);
});
});
});