@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
280 lines (279 loc) • 18.4 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 importer_fit_1 = require("./importer.fit");
const data_aerobic_training_effect_1 = require("../../../../data/data-aerobic-training-effect");
const data_anaerobic_training_effect_1 = require("../../../../data/data-anaerobic-training-effect");
const data_recovery_time_1 = require("../../../../data/data.recovery-time");
const data_avg_respiration_rate_1 = require("../../../../data/data.avg-respiration-rate");
const data_heart_rate_zone_five_duration_1 = require("../../../../data/data.heart-rate-zone-five-duration");
const data_heart_rate_zone_four_duration_1 = require("../../../../data/data.heart-rate-zone-four-duration");
const data_heart_rate_zone_one_duration_1 = require("../../../../data/data.heart-rate-zone-one-duration");
const data_heart_rate_zone_three_duration_1 = require("../../../../data/data.heart-rate-zone-three-duration");
const data_heart_rate_zone_two_duration_1 = require("../../../../data/data.heart-rate-zone-two-duration");
const data_jump_count_1 = require("../../../../data/data.jump-count");
const data_max_respiration_rate_1 = require("../../../../data/data.max-respiration-rate");
const data_min_respiration_rate_1 = require("../../../../data/data.min-respiration-rate");
const data_weight_1 = require("../../../../data/data.weight");
const data_training_load_peak_1 = require("../../../../data/data.training-load-peak");
const data_resting_calories_1 = require("../../../../data/data.resting-calories");
const data_est_sweat_loss_1 = require("../../../../data/data.est-sweat-loss");
const data_primary_benefit_1 = require("../../../../data/data.primary-benefit");
const data_sport_profile_name_1 = require("../../../../data/data.sport-profile-name");
const data_total_grit_1 = require("../../../../data/data.total-grit");
const data_avg_flow_1 = require("../../../../data/data.avg-flow");
const data_jump_event_1 = require("../../../../data/data.jump-event");
const data_avg_vam_1 = require("../../../../data/data.avg-vam");
const data_temperature_max_1 = require("../../../../data/data.temperature-max");
const data_temperature_min_1 = require("../../../../data/data.temperature-min");
const data_start_position_1 = require("../../../../data/data.start-position");
const data_end_position_1 = require("../../../../data/data.end-position");
const data_energy_1 = require("../../../../data/data.energy");
const helpers_1 = require("../../../../events/utilities/helpers");
describe('EventImporterFIT MTB Jumps', () => {
const samplesDir = path.resolve(__dirname, '../../../../../samples/fit');
const fitFile = 'jumps-mtb.fit';
it('should parse jumps-mtb.fit and extract grit, flow and jumps', () => __awaiter(void 0, void 0, void 0, function* () {
const filePath = path.join(samplesDir, fitFile);
if (!fs.existsSync(filePath)) {
console.warn(`Sample file ${fitFile} not found. Skipping test.`);
return;
}
const fileBuffer = fs.readFileSync(filePath);
const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength);
const event = yield importer_fit_1.EventImporterFIT.getFromArrayBuffer(arrayBuffer, undefined, fitFile);
expect(event).toBeDefined();
const activities = event.getActivities();
console.log(`Parsed ${activities.length} activities.`);
activities.forEach((a, i) => {
console.log(`Activity ${i}: ${a.startDate.toISOString()} - ${a.endDate.toISOString()}`);
});
const activity = activities[0];
expect(activity).toBeDefined();
// Check Stats
const totalGrit = activity.getStat(data_total_grit_1.DataTotalGrit.type);
expect(totalGrit).toBeDefined();
expect(totalGrit.getValue()).toBeCloseTo(38.4, 1);
const avgFlow = activity.getStat(data_avg_flow_1.DataAvgFlow.type);
expect(avgFlow).toBeDefined();
expect(avgFlow.getValue()).toBeCloseTo(6.13, 2);
// Verify Respiration Rate
const avgResp = activity.getStat(data_avg_respiration_rate_1.DataAvgRespirationRate.type);
expect(avgResp).toBeDefined();
expect(avgResp.getValue()).toBeCloseTo(27.56, 1);
const maxResp = activity.getStat(data_max_respiration_rate_1.DataMaxRespirationRate.type);
expect(maxResp).toBeDefined();
expect(maxResp.getValue()).toBeCloseTo(41.43, 2);
const minResp = activity.getStat(data_min_respiration_rate_1.DataMinRespirationRate.type);
expect(minResp).toBeDefined();
expect(minResp.getValue()).toBeCloseTo(15.77, 2);
// Verify Avg VAM
const avgVam = activity.getStat(data_avg_vam_1.DataAvgVAM.type);
expect(avgVam).toBeDefined();
expect(avgVam.getValue()).toBeCloseTo(100, 1);
// Verify Jump Count
const jumpCount = activity.getStat(data_jump_count_1.DataJumpCount.type);
expect(jumpCount).toBeDefined();
expect(jumpCount.getValue()).toBe(11);
// Verify Training Load Peak
const trainingLoadPeak = activity.getStat(data_training_load_peak_1.DataTrainingLoadPeak.type);
expect(trainingLoadPeak).toBeDefined();
expect(trainingLoadPeak.getValue()).toBe(6079174);
// Verify Resting Calories
const restingCalories = activity.getStat(data_resting_calories_1.DataRestingCalories.type);
expect(restingCalories).toBeDefined();
expect(restingCalories.getValue()).toBe(159);
// Verify Est Sweat Loss
const sweatLoss = activity.getStat(data_est_sweat_loss_1.DataEstSweatLoss.type);
expect(sweatLoss).toBeDefined();
expect(sweatLoss.getValue()).toBe(790);
// Verify Primary Benefit
const primaryBenefit = activity.getStat(data_primary_benefit_1.DataPrimaryBenefit.type);
expect(primaryBenefit).toBeDefined();
expect(primaryBenefit.getValue()).toBe(2);
// Verify Sport Profile Name
const sportProfileName = activity.getStat(data_sport_profile_name_1.DataSportProfileName.type);
expect(sportProfileName).toBeDefined();
expect(sportProfileName.getValue()).toBe('MOUNTAIN');
// Verify Physiological Metrics
const aerobic = activity.getStat(data_aerobic_training_effect_1.DataAerobicTrainingEffect.type);
expect(aerobic).toBeDefined();
expect(aerobic.getValue()).toBe(3);
const anaerobic = activity.getStat(data_anaerobic_training_effect_1.DataAnaerobicTrainingEffect.type);
expect(anaerobic).toBeDefined();
expect(anaerobic.getValue()).toBe(2);
expect(activity.getStat(data_temperature_max_1.DataTemperatureMax.type).getValue()).toBe(19);
expect(activity.getStat(data_temperature_min_1.DataTemperatureMin.type).getValue()).toBe(7);
// Positions
const startPos = activity.getStat(data_start_position_1.DataStartPosition.type).getValue();
expect(startPos.latitudeDegrees).toBeCloseTo(39.664968, 5);
expect(startPos.longitudeDegrees).toBeCloseTo(20.849827, 5);
const endPos = activity.getStat(data_end_position_1.DataEndPosition.type).getValue();
expect(endPos.latitudeDegrees).toBeCloseTo(39.664946, 5);
expect(endPos.longitudeDegrees).toBeCloseTo(20.849807, 5);
const recoveryTime = activity.getStat(data_recovery_time_1.DataRecoveryTime.type);
expect(recoveryTime).toBeDefined();
expect(recoveryTime.getValue()).toBe(1164 * 60);
// User Profile
const weight = activity.getStat(data_weight_1.DataWeight.type);
expect(weight).toBeDefined();
// Resting Calories
expect(activity.getStat(data_resting_calories_1.DataRestingCalories.type).getValue()).toBe(159);
expect(activity.getStat(data_aerobic_training_effect_1.DataAerobicTrainingEffect.type).getValue()).toBe(3);
expect(activity.getStat(data_anaerobic_training_effect_1.DataAnaerobicTrainingEffect.type).getValue()).toBe(2);
expect(activity.getStat(data_energy_1.DataEnergy.type).getValue()).toBe(853);
// HR Zone Durations from time_in_zone (session-level message 216)
const zone1 = activity.getStat(data_heart_rate_zone_one_duration_1.DataHeartRateZoneOneDuration.type);
expect(zone1).toBeDefined();
expect(zone1.getValue()).toBeCloseTo(1831.986, 0); // ~1832 seconds in zone 1 (index 1)
const zone2 = activity.getStat(data_heart_rate_zone_two_duration_1.DataHeartRateZoneTwoDuration.type);
expect(zone2).toBeDefined();
expect(zone2.getValue()).toBeCloseTo(2412.306, 0); // ~2412 seconds in zone 2 (index 2)
const zone3 = activity.getStat(data_heart_rate_zone_three_duration_1.DataHeartRateZoneThreeDuration.type);
expect(zone3).toBeDefined();
expect(zone3.getValue()).toBeCloseTo(2160.994, 0); // ~2161 seconds in zone 3 (index 3)
const zone4 = activity.getStat(data_heart_rate_zone_four_duration_1.DataHeartRateZoneFourDuration.type);
expect(zone4).toBeDefined();
expect(zone4.getValue()).toBeCloseTo(450.999, 0); // ~451 seconds in zone 4 (index 4)
const zone5 = activity.getStat(data_heart_rate_zone_five_duration_1.DataHeartRateZoneFiveDuration.type);
expect(zone5).toBeDefined();
expect(zone5.getValue()).toBeCloseTo(47, 0); // ~47 seconds in zone 5 (index 5)
// Check IntensityZones with boundaries
const hrIntensityZones = activity.intensityZones.find(iz => iz.type === 'Heart Rate');
expect(hrIntensityZones).toBeDefined();
if (hrIntensityZones) {
expect(hrIntensityZones.zone1Duration).toBeCloseTo(1831.986, 0);
expect(hrIntensityZones.zone2Duration).toBeCloseTo(2412.306, 0);
expect(hrIntensityZones.zone3Duration).toBeCloseTo(2160.994, 0);
expect(hrIntensityZones.zone4Duration).toBeCloseTo(450.999, 0);
expect(hrIntensityZones.zone5Duration).toBeCloseTo(47, 0);
// Garmin time_in_zone includes a below-zone bucket at index 0, so boundaries are offset with durations.
expect(hrIntensityZones.zone1LowerLimit).toBe(93);
expect(hrIntensityZones.zone2LowerLimit).toBe(111);
expect(hrIntensityZones.zone3LowerLimit).toBe(130);
expect(hrIntensityZones.zone4LowerLimit).toBe(148);
expect(hrIntensityZones.zone5LowerLimit).toBe(167);
expect(hrIntensityZones.zone6LowerLimit).toBe(185);
expect(hrIntensityZones.zone7LowerLimit).toBeUndefined();
}
// Check Jumps
const jumpEvents = activity.getAllEvents().filter((e) => e.getType() === data_jump_event_1.DataJumpEvent.type);
expect(jumpEvents.length).toBeGreaterThan(0);
expect(jumpEvents.length).toBe(11);
const jump = jumpEvents[0];
expect(jump.jumpData).toBeDefined();
expect((0, helpers_1.isNumber)(jump.jumpData.distance.getValue())).toBeTruthy();
expect((0, helpers_1.isNumber)(jump.jumpData.score.getValue())).toBeTruthy();
// Verify new jump fields with expected values
expect(jump.jumpData.distance.getValue()).toBeCloseTo(2.069, 2);
expect(jump.jumpData.hang_time.getValue()).toBeCloseTo(0.36, 2);
expect(jump.jumpData.score.getValue()).toBeCloseTo(62.44, 1);
expect(jump.jumpData.position_lat.getValue()).toBeCloseTo(39.6679, 3);
expect(jump.jumpData.position_long.getValue()).toBeCloseTo(20.8382, 3);
expect(jump.jumpData.speed.getValue()).toBeCloseTo(5.748, 2);
console.log(`Found ${jumpEvents.length} jumps.`);
console.log('First jump:', jump.jumpData);
// Verify Jump Statistics (Min, Max, Avg)
const { DataJumpDistanceAvg, DataJumpDistanceMax, DataJumpDistanceMin, DataJumpHangTimeAvg, DataJumpHangTimeMax, DataJumpHangTimeMin, DataJumpHeightAvg, DataJumpHeightMax, DataJumpHeightMin, DataJumpRotationsAvg, DataJumpRotationsMax, DataJumpRotationsMin, DataJumpScoreAvg, DataJumpScoreMax, DataJumpScoreMin, DataJumpSpeedAvg, DataJumpSpeedMax, DataJumpSpeedMin } = yield Promise.resolve().then(() => __importStar(require('../../../../data/data.jump-stats')));
// Hangtime
expect(activity.getStat(DataJumpHangTimeMin.type).getValue()).toBeCloseTo(0.36, 2);
expect(activity.getStat(DataJumpHangTimeMax.type).getValue()).toBeCloseTo(0.696, 3);
expect(activity.getStat(DataJumpHangTimeAvg.type).getValue()).toBeCloseTo(0.45, 2);
// Distance
expect(activity.getStat(DataJumpDistanceMin.type).getValue()).toBeCloseTo(1.4, 2);
expect(activity.getStat(DataJumpDistanceMax.type).getValue()).toBeCloseTo(4.68, 2);
expect(activity.getStat(DataJumpDistanceAvg.type).getValue()).toBeCloseTo(3.02, 2);
// Speed
expect(activity.getStat(DataJumpSpeedMin.type).getValue()).toBeCloseTo(3.88, 2);
expect(activity.getStat(DataJumpSpeedMax.type).getValue()).toBeCloseTo(8.995, 3);
expect(activity.getStat(DataJumpSpeedAvg.type).getValue()).toBeCloseTo(6.55, 2);
// Score
expect(activity.getStat(DataJumpScoreMin.type).getValue()).toBeCloseTo(53.9, 1);
expect(activity.getStat(DataJumpScoreMax.type).getValue()).toBeCloseTo(122.6, 1);
expect(activity.getStat(DataJumpScoreAvg.type).getValue()).toBeCloseTo(81.8, 1);
// Rotations (Should be undefined for this file)
expect(activity.getStat(DataJumpRotationsMin.type)).toBeUndefined();
expect(activity.getStat(DataJumpRotationsMax.type)).toBeUndefined();
expect(activity.getStat(DataJumpRotationsAvg.type)).toBeUndefined();
// Height (Should be undefined for this file)
expect(activity.getStat(DataJumpHeightMin.type)).toBeUndefined();
expect(activity.getStat(DataJumpHeightMax.type)).toBeUndefined();
expect(activity.getStat(DataJumpHeightAvg.type)).toBeUndefined();
// We can check if we can find a sample with Grit.
// In sports-lib, samples are often accessed via activity.getStream(type) or similar, BUT
// importer creates `DataPoint`s? Or `DataSample`s?
// Let's assume we just check if parsing succeeded without error for now for samples,
// as verifying exact sample values requires knowing the file content deep structure.
// However, we added mapping for DataGrit/Flow, so they SHOULD be in the data set.
// Verify Devices
expect(activity.creator.devices).toBeDefined();
expect(activity.creator.devices.length).toBeGreaterThan(0);
// Check for specific device with timestamp (from example file analysis)
// Device 3 (unknown/generic) had valid fields
const deviceWithTimestamp = activity.creator.devices.find(d => d.timestamp);
// Based on previous analysis with inspect_fit.js, devices had timestamps
// e.g. "timestamp": "2026-01-14T15:17:27.000Z"
if (deviceWithTimestamp) {
expect(deviceWithTimestamp.timestamp).toBeInstanceOf(Date);
// Verify it's a valid date
expect(deviceWithTimestamp.timestamp.getTime()).not.toBeNaN();
// We can check strictly if we want, but existence is good enough for now
// Verify timestamp is correct
// Note: fit-file-parser seems to extract the timestamp corresponding to Activity Start Time (13:16:37)
// whereas fit-parser extracted End Time (15:17:27). We match what this parser gives.
const expectedDate = new Date('2026-01-14T13:16:37.000Z');
expect(deviceWithTimestamp.timestamp).toEqual(expectedDate);
}
else {
// If no device has timestamp in this file (which contradicts my manual check earlier if I was right), this will fail
// But let's check if ANY device has it.
// Earlier inspect_fit.js output showed ALL devices had timestamp "2026-01-14T15:17:27.000Z"
// So we expect at least one to have it.
// If this expects fails, it means my previous analysis or the importer logic is wrong.
fail('No device found with timestamp, but expected devices to have timestamps.');
}
}));
});