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

266 lines (265 loc) 19.3 kB
"use strict"; 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); }); }); });