@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
263 lines (262 loc) • 16 kB
JavaScript
"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 importer_fit_1 = require("./importer.fit");
const data_moving_time_1 = require("../../../../data/data.moving-time");
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");
const data_effort_pace_1 = require("../../../../data/data.effort-pace");
const data_depth_max_1 = require("../../../../data/data.depth-max");
const data_avg_stroke_distance_1 = require("../../../../data/data.avg-stroke-distance");
const data_avg_stroke_count_1 = require("../../../../data/data.avg-stroke-count");
const activity_1 = require("../../../../activities/activity");
const activity_types_1 = require("../../../../activities/activity.types");
const creator_1 = require("../../../../creators/creator");
const data_vertical_oscillation_avg_1 = require("../../../../data/data.vertical-oscillation-avg");
const data_vertical_ratio_avg_1 = require("../../../../data/data.vertical-ratio-avg");
const data_ground_contact_time_avg_1 = require("../../../../data/data.ground-contact-time-avg");
const data_grade_avg_1 = require("../../../../data/data.grade-avg");
const data_grade_min_1 = require("../../../../data/data.grade-min");
const data_grade_max_1 = require("../../../../data/data.grade-max");
const data_recovery_time_1 = require("../../../../data/data.recovery-time");
const data_stamina_1 = require("../../../../data/data.stamina");
const data_vo2_max_1 = require("../../../../data/data.vo2-max");
const helpers_1 = require("../../../utilities/helpers");
const activity_parsing_options_1 = require("../../../../activities/activity-parsing-options");
describe('EventImporterFIT session stats mapping', () => {
const toArrayBuffer = (filePath) => {
const fileContent = fs.readFileSync(filePath);
return fileContent.buffer.slice(fileContent.byteOffset, fileContent.byteOffset + fileContent.byteLength);
};
it('should use session total_moving_time when available', () => __awaiter(void 0, void 0, void 0, function* () {
const fitFilePath = path.join(__dirname, '../../../../specs/fixtures/rides/fit/971150603.fit');
const event = yield importer_fit_1.EventImporterFIT.getFromArrayBuffer(toArrayBuffer(fitFilePath));
const activity = event.getFirstActivity();
const movingTime = activity.getStat(data_moving_time_1.DataMovingTime.type);
expect(movingTime).toBeDefined();
expect(movingTime.getValue()).toBe(3802);
}));
it('should map session Effort Pace summary stat', () => __awaiter(void 0, void 0, void 0, function* () {
const fitFilePath = path.join(__dirname, '../../../../../samples/coros/step-effort.fit');
if (!fs.existsSync(fitFilePath)) {
console.warn(`Sample file not found at ${fitFilePath}. Skipping test.`);
return;
}
const event = yield importer_fit_1.EventImporterFIT.getFromArrayBuffer(toArrayBuffer(fitFilePath));
const activity = event.getFirstActivity();
const effortPace = activity.getStat(data_effort_pace_avg_1.DataEffortPaceAvg.type);
expect(effortPace).toBeDefined();
expect(effortPace.getValue()).toBeCloseTo((0, helpers_1.convertSpeedToPace)(3.412), 3);
}));
it('should parse Effort Pace stream and stats for 2026-01-31_10-51_2.fit', () => __awaiter(void 0, void 0, void 0, function* () {
const fitFilePath = path.join(__dirname, '../../../../../samples/fit/2026-01-31_10-51_2.fit');
if (!fs.existsSync(fitFilePath)) {
console.warn(`Sample file not found at ${fitFilePath}. Skipping test.`);
return;
}
const event = yield importer_fit_1.EventImporterFIT.getFromArrayBuffer(toArrayBuffer(fitFilePath));
const activity = event.getFirstActivity();
const effortPaceStream = activity
.getStreamData(data_effort_pace_1.DataEffortPace.type)
.filter((value) => Number.isFinite(value));
const effortPaceAvg = activity.getStat(data_effort_pace_avg_1.DataEffortPaceAvg.type);
const effortPaceMin = activity.getStat(data_effort_pace_min_1.DataEffortPaceMin.type);
const effortPaceMax = activity.getStat(data_effort_pace_max_1.DataEffortPaceMax.type);
expect(effortPaceStream.length).toBeGreaterThan(3000);
expect(effortPaceAvg).toBeDefined();
expect(effortPaceMin).toBeDefined();
expect(effortPaceMax).toBeDefined();
const minValue = Number(effortPaceMin.getValue());
const avgValue = Number(effortPaceAvg.getValue());
const maxValue = Number(effortPaceMax.getValue());
expect(Number.isFinite(minValue)).toBe(true);
expect(Number.isFinite(avgValue)).toBe(true);
expect(Number.isFinite(maxValue)).toBe(true);
expect(minValue).toBeLessThanOrEqual(avgValue);
expect(avgValue).toBeLessThanOrEqual(maxValue);
}));
it('should map session max_depth to Maximum Depth stat', () => __awaiter(void 0, void 0, void 0, function* () {
const fitFilePath = path.join(__dirname, '../../../../../samples/fit/2025-08-27_10-52.fit');
if (!fs.existsSync(fitFilePath)) {
console.warn(`Sample file not found at ${fitFilePath}. Skipping test.`);
return;
}
const event = yield importer_fit_1.EventImporterFIT.getFromArrayBuffer(toArrayBuffer(fitFilePath));
const activity = event.getFirstActivity();
const maxDepth = activity.getStat(data_depth_max_1.DataDepthMax.type);
expect(maxDepth).toBeDefined();
expect(maxDepth.getValue()).toBeCloseTo(3.86, 2);
}));
it('should ignore zero VO2 max and recovery time activity metrics', () => __awaiter(void 0, void 0, void 0, function* () {
const fitFilePath = path.join(__dirname, '../../../../../samples/fit/2025-10-17_12-26.fit');
if (!fs.existsSync(fitFilePath)) {
console.warn(`Sample file not found at ${fitFilePath}. Skipping test.`);
return;
}
const event = yield importer_fit_1.EventImporterFIT.getFromArrayBuffer(toArrayBuffer(fitFilePath));
const activity = event.getFirstActivity();
const vo2Max = activity.getStat(data_vo2_max_1.DataVO2Max.type);
const recoveryTime = activity.getStat(data_recovery_time_1.DataRecoveryTime.type);
expect(vo2Max).toBeUndefined();
expect(recoveryTime).toBeUndefined();
}));
it('should prefer Garmin activity metrics VO2 max over user metrics VO2 max', () => __awaiter(void 0, void 0, void 0, function* () {
const fitFilePath = path.join(__dirname, '../../../../../samples/fit/2026-05-04_16-22.fit');
if (!fs.existsSync(fitFilePath)) {
console.warn(`Sample file not found at ${fitFilePath}. Skipping test.`);
return;
}
const event = yield importer_fit_1.EventImporterFIT.getFromArrayBuffer(toArrayBuffer(fitFilePath));
const activity = event.getFirstActivity();
const vo2Max = activity.getStat(data_vo2_max_1.DataVO2Max.type);
expect(vo2Max).toBeDefined();
expect(vo2Max.getValue()).toBeCloseTo(56.58073425292969, 6);
}));
it('should fall back to Garmin user metrics VO2 max when activity metrics VO2 max is invalid', () => {
const startTime = new Date('2026-05-04T13:22:31.000Z');
const endTime = new Date('2026-05-04T13:32:31.000Z');
const activity = importer_fit_1.EventImporterFIT.getActivityFromSessionObject({
start_time: startTime,
timestamp: endTime,
total_elapsed_time: 600,
total_timer_time: 600,
sport: 'cycling',
laps: []
}, {
file_ids: [{ manufacturer: 'garmin', product: 4655 }],
records: [],
events: [],
laps: [],
activity_metrics: [{ sport: 'cycling', vo2_max: 114688, first_vo2_max: 0, recovery_time: 0 }],
user_metrics: [{ start_of_activity: startTime, vo2_max: 53.18017578125, first_vo2_max: 53.18 }]
}, activity_parsing_options_1.ActivityParsingOptions.DEFAULT);
const vo2Max = activity.getStat(data_vo2_max_1.DataVO2Max.type);
expect(vo2Max).toBeDefined();
expect(vo2Max.getValue()).toBeCloseTo(53.18017578125, 6);
});
it('should ignore irrational Garmin VO2 max values', () => {
const startTime = new Date('2026-05-04T13:22:31.000Z');
const endTime = new Date('2026-05-04T13:32:31.000Z');
const activity = importer_fit_1.EventImporterFIT.getActivityFromSessionObject({
start_time: startTime,
timestamp: endTime,
total_elapsed_time: 600,
total_timer_time: 600,
sport: 'cycling',
laps: []
}, {
file_ids: [{ manufacturer: 'garmin', product: 4655 }],
records: [],
events: [],
laps: [],
activity_metrics: [{ sport: 'cycling', vo2_max: 114688, first_vo2_max: 0, recovery_time: 0 }],
user_metrics: [{ start_of_activity: startTime, vo2_max: 500, first_vo2_max: -1 }]
}, activity_parsing_options_1.ActivityParsingOptions.DEFAULT);
expect(activity.getStat(data_vo2_max_1.DataVO2Max.type)).toBeUndefined();
});
it('should map swim session stroke summary stats', () => __awaiter(void 0, void 0, void 0, function* () {
const fitFilePath = path.join(__dirname, '../../../../specs/fixtures/swim/fit/7617306288.fit');
const event = yield importer_fit_1.EventImporterFIT.getFromArrayBuffer(toArrayBuffer(fitFilePath));
const activity = event.getFirstActivity();
const avgStrokeDistance = activity.getStat(data_avg_stroke_distance_1.DataAvgStrokeDistance.type);
const avgStrokeCount = activity.getStat(data_avg_stroke_count_1.DataAvgStrokeCount.type);
expect(avgStrokeDistance).toBeDefined();
expect(avgStrokeDistance.getValue()).toBeCloseTo(2.11, 2);
expect(avgStrokeCount).toBeDefined();
expect(avgStrokeCount.getValue()).toBeCloseTo(11.8, 1);
}));
it('should map running dynamics and grade summary from session fields when available', () => {
var _a, _b, _c, _d, _e, _f;
const activity = new activity_1.Activity(new Date(0), new Date(10000), activity_types_1.ActivityTypes.Running, new creator_1.Creator('test'));
const stats = importer_fit_1.EventImporterFIT.getStatsFromObject({
total_elapsed_time: 10,
total_timer_time: 10,
avg_stance_time: 271.3,
avg_vertical_oscillation: 97.2,
avg_vertical_ratio: 9.57,
avg_grade: 0.13,
max_pos_grade: 4.47,
max_neg_grade: -3.84
}, activity, false);
const getStat = (type) => stats.find(stat => stat.getType() === type);
expect((_a = getStat(data_ground_contact_time_avg_1.DataGroundContactTimeAvg.type)) === null || _a === void 0 ? void 0 : _a.getValue()).toBe(271.3);
expect((_b = getStat(data_vertical_oscillation_avg_1.DataVerticalOscillationAvg.type)) === null || _b === void 0 ? void 0 : _b.getValue()).toBe(97.2);
expect((_c = getStat(data_vertical_ratio_avg_1.DataVerticalRatioAvg.type)) === null || _c === void 0 ? void 0 : _c.getValue()).toBe(9.57);
expect((_d = getStat(data_grade_avg_1.DataGradeAvg.type)) === null || _d === void 0 ? void 0 : _d.getValue()).toBe(0.13);
expect((_e = getStat(data_grade_min_1.DataGradeMin.type)) === null || _e === void 0 ? void 0 : _e.getValue()).toBe(-3.84);
expect((_f = getStat(data_grade_max_1.DataGradeMax.type)) === null || _f === void 0 ? void 0 : _f.getValue()).toBe(4.47);
});
it('should map grade min/max/avg from FIT session summary when fields exist', () => __awaiter(void 0, void 0, void 0, function* () {
const fitFilePath = path.join(__dirname, '../../../../specs/fixtures/rides/fit/7739869618.fit');
const event = yield importer_fit_1.EventImporterFIT.getFromArrayBuffer(toArrayBuffer(fitFilePath));
const activity = event.getFirstActivity();
const avgGrade = activity.getStat(data_grade_avg_1.DataGradeAvg.type);
const minGrade = activity.getStat(data_grade_min_1.DataGradeMin.type);
const maxGrade = activity.getStat(data_grade_max_1.DataGradeMax.type);
expect(avgGrade).toBeDefined();
expect(minGrade).toBeDefined();
expect(maxGrade).toBeDefined();
expect(avgGrade.getValue()).toBeCloseTo(0.13, 2);
expect(minGrade.getValue()).toBeCloseTo(-3.84, 2);
expect(maxGrade.getValue()).toBeCloseTo(4.47, 2);
}));
it('should map Garmin stamina session summary fields', () => {
var _a, _b, _c;
const activity = new activity_1.Activity(new Date(0), new Date(10000), activity_types_1.ActivityTypes.Running, new creator_1.Creator('test'));
const stats = importer_fit_1.EventImporterFIT.getStatsFromObject({
total_elapsed_time: 10,
total_timer_time: 10,
beginning_potential_stamina: 95,
ending_potential_stamina: 66,
min_stamina: 34
}, activity, false);
const getStat = (type) => stats.find(stat => stat.getType() === type);
expect((_a = getStat(data_stamina_1.DataBeginningPotentialStamina.type)) === null || _a === void 0 ? void 0 : _a.getValue()).toBe(95);
expect((_b = getStat(data_stamina_1.DataEndingPotentialStamina.type)) === null || _b === void 0 ? void 0 : _b.getValue()).toBe(66);
expect((_c = getStat(data_stamina_1.DataStaminaMin.type)) === null || _c === void 0 ? void 0 : _c.getValue()).toBe(34);
});
});