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

254 lines (253 loc) 15.4 kB
"use strict"; 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_1 = require("fs"); const activity_types_1 = require("../../../../activities/activity.types"); const data_ascent_1 = require("../../../../data/data.ascent"); const data_descent_1 = require("../../../../data/data.descent"); const data_distance_1 = require("../../../../data/data.distance"); const data_gnss_distance_miles_1 = require("../../../../data/data.gnss-distance-miles"); const data_grade_1 = require("../../../../data/data.grade"); const data_grade_avg_1 = require("../../../../data/data.grade-avg"); const data_grade_max_1 = require("../../../../data/data.grade-max"); const data_grade_min_1 = require("../../../../data/data.grade-min"); const data_latitude_degrees_1 = require("../../../../data/data.latitude-degrees"); const data_longitude_degrees_1 = require("../../../../data/data.longitude-degrees"); const data_speed_1 = require("../../../../data/data.speed"); const parsing_event_lib_error_1 = require("../../../../errors/parsing-event-lib.error"); const file_type_enum_1 = require("../../../../events/adapters/file-type.enum"); const index_1 = require("../../../../index"); const route_parsing_options_1 = require("../../../route-parsing-options"); const importer_route_fit_1 = require("./importer.route.fit"); describe('RouteImporterFIT', () => { const importerWithPrivateParse = importer_route_fit_1.RouteImporterFIT; const readFixture = (path) => { const buffer = (0, fs_1.readFileSync)(path); return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); }; it('should import a FIT course as a first-class route file', () => __awaiter(void 0, void 0, void 0, function* () { var _a; const routeFile = yield importer_route_fit_1.RouteImporterFIT.getFromArrayBuffer(readFixture(`${process.cwd()}/samples/fit-routes/coursepointer-cptr002.fit`)); const route = routeFile.getFirstRoute(); const waypoints = routeFile.getWaypoints(); expect(routeFile.name).toBe('cptr002'); expect(routeFile.srcFileType).toBe(file_type_enum_1.FileType.FIT); expect((_a = routeFile.createdAt) === null || _a === void 0 ? void 0 : _a.toISOString()).toBe('2025-05-06T15:56:24.000Z'); expect(routeFile.getRoutes()).toHaveLength(1); expect(route.activityType).toBe(activity_types_1.ActivityTypes.Cycling); expect(route.getPointCount()).toBe(666); expect(route.hasStreamData(data_latitude_degrees_1.DataLatitudeDegrees.type)).toBe(true); expect(route.hasStreamData(data_longitude_degrees_1.DataLongitudeDegrees.type)).toBe(true); expect(route.hasStreamData(data_distance_1.DataDistance.type)).toBe(true); expect(route.hasStreamData(data_grade_1.DataGrade.type)).toBe(true); expect(route.getStat(data_distance_1.DataDistance.type).getValue()).toBeCloseTo(23187.9, 1); expect(route.getStreamData(data_distance_1.DataDistance.type)[0]).toBe(0); expect(route.getStreamData(data_distance_1.DataDistance.type)[route.getPointCount() - 1]).toBeCloseTo(23187.75, 1); expect(routeFile.getStat(data_distance_1.DataDistance.type).getValue()).toEqual(route.getStat(data_distance_1.DataDistance.type).getValue()); expect(routeFile.getStat(data_ascent_1.DataAscent.type).getValue()).toEqual(route.getStat(data_ascent_1.DataAscent.type).getValue()); expect(routeFile.getStat(data_grade_min_1.DataGradeMin.type).getValue()).toEqual(route.getStat(data_grade_min_1.DataGradeMin.type).getValue()); expect(routeFile.getStat(data_grade_max_1.DataGradeMax.type).getValue()).toEqual(route.getStat(data_grade_max_1.DataGradeMax.type).getValue()); expect(routeFile.getStat(data_grade_avg_1.DataGradeAvg.type).getValue()).toEqual(route.getStat(data_grade_avg_1.DataGradeAvg.type).getValue()); expect(waypoints).toHaveLength(2); expect(waypoints[0]).toMatchObject({ name: 'First here cour', type: 'generic', distance: 8813, routeIndex: 0 }); expect(waypoints[0].routePointIndex).toBeGreaterThan(0); expect(waypoints[1]).toMatchObject({ name: 'Point2', routeIndex: 0 }); expect(waypoints[1].distance).toBeCloseTo(18349.29, 1); expect(waypoints[1].routePointIndex).toBeGreaterThan(waypoints[0].routePointIndex); })); it('should expose FIT course import through the public SportsLib route API', () => __awaiter(void 0, void 0, void 0, function* () { const routeFile = yield index_1.SportsLib.importRoutesFromFit(readFixture(`${process.cwd()}/samples/fit-routes/coursepointer-cptr002.fit`)); expect(routeFile.name).toBe('cptr002'); expect(routeFile.getFirstRoute().getPointCount()).toBe(666); })); it('should infer course point distance from geometry when Garmin Connect exports zero distances', () => __awaiter(void 0, void 0, void 0, function* () { const routeFile = yield importer_route_fit_1.RouteImporterFIT.getFromArrayBuffer(readFixture(`${process.cwd()}/samples/fit-routes/coursepointer-cptr003-connect.fit`)); const waypoints = routeFile.getWaypoints(); expect(waypoints).toHaveLength(4); expect(waypoints.map(waypoint => waypoint.name)).toEqual(['Russian Ridge', 'Saratoga', 'OLH Start', 'OLH End']); expect(waypoints.every(waypoint => typeof waypoint.type === 'string')).toBe(true); waypoints.forEach(waypoint => { expect(waypoint.distance).toBeGreaterThan(0); expect(waypoint.routePointIndex).toEqual(expect.any(Number)); }); expect(waypoints[0].distance).toBeCloseTo(23382.02, 1); expect(waypoints[1].distance).toBeCloseTo(44795.42, 1); expect(waypoints[2].distance).toBeCloseTo(8879.36, 1); expect(waypoints[3].distance).toBeCloseTo(14168.11, 1); })); it('should support route stream includeTypes for FIT courses', () => __awaiter(void 0, void 0, void 0, function* () { const routeFile = yield importer_route_fit_1.RouteImporterFIT.getFromArrayBuffer(readFixture(`${process.cwd()}/samples/fit-routes/coursepointer-cptr002.fit`), new route_parsing_options_1.RouteParsingOptions({ streams: { includeTypes: [data_distance_1.DataDistance.type, data_gnss_distance_miles_1.DataGNSSDistanceMiles.type, data_grade_1.DataGrade.type] } })); const streamTypes = routeFile .getFirstRoute() .getAllStreams() .map(stream => stream.type); expect(new Set(streamTypes)).toEqual(new Set([data_distance_1.DataDistance.type, data_gnss_distance_miles_1.DataGNSSDistanceMiles.type, data_grade_1.DataGrade.type])); })); it('should reject unsupported activity-only includeTypes for FIT routes', () => __awaiter(void 0, void 0, void 0, function* () { yield expect(importer_route_fit_1.RouteImporterFIT.getFromArrayBuffer(readFixture(`${process.cwd()}/samples/fit-routes/coursepointer-cptr002.fit`), new route_parsing_options_1.RouteParsingOptions({ streams: { includeTypes: [data_speed_1.DataSpeed.type] } }))).rejects.toThrow('Unsupported route stream includeTypes'); })); it('should preserve parser-decoded course point types without FIT enum mapping in sports-lib', () => { const parseSpy = jest.spyOn(importerWithPrivateParse, 'parseFIT').mockResolvedValue({ file_ids: [{ type: 'course', manufacturer: 'garmin', product: 0 }], course: { name: 'Decoded Course', sport: 'cycling' }, records: [ { position_lat: 1, position_long: 2, distance: 0, altitude: 10 }, { position_lat: 1.1, position_long: 2.1, distance: 100, altitude: 12 } ], course_points: [ { position_lat: 1, position_long: 2, type: 'rest_area', name: 'Decoded' }, { position_lat: 1.1, position_long: 2.1, type: 29, name: 'Numeric' } ] }); return importer_route_fit_1.RouteImporterFIT.getFromArrayBuffer(new ArrayBuffer(0)) .then(routeFile => { expect(routeFile.getWaypoints().map(waypoint => waypoint.type)).toEqual(['rest_area', '29']); }) .finally(() => { parseSpy.mockRestore(); }); }); it('should preserve FIT course summary stats when they are present', () => { const parseSpy = jest.spyOn(importerWithPrivateParse, 'parseFIT').mockResolvedValue({ file_ids: [{ type: 'course', manufacturer: 'garmin', product: 0 }], course: { name: 'Summary Course', sport: 'cycling', total_distance: 222, total_ascent: 77, total_descent: 33, avg_grade: 5, max_pos_grade: 11, max_neg_grade: -9 }, records: [ { position_lat: 1, position_long: 2, distance: 0, altitude: 10 }, { position_lat: 1.1, position_long: 2.1, distance: 100, altitude: 20 } ], course_points: [] }); return importer_route_fit_1.RouteImporterFIT.getFromArrayBuffer(new ArrayBuffer(0)) .then(routeFile => { const route = routeFile.getFirstRoute(); expect(route.getStat(data_distance_1.DataDistance.type).getValue()).toBe(222); expect(route.getStat(data_ascent_1.DataAscent.type).getValue()).toBe(77); expect(route.getStat(data_descent_1.DataDescent.type).getValue()).toBe(33); expect(route.getStat(data_grade_avg_1.DataGradeAvg.type).getValue()).toBe(5); expect(route.getStat(data_grade_max_1.DataGradeMax.type).getValue()).toBe(11); expect(route.getStat(data_grade_min_1.DataGradeMin.type).getValue()).toBe(-9); expect(routeFile.getStat(data_distance_1.DataDistance.type).getValue()).toBe(222); }) .finally(() => { parseSpy.mockRestore(); }); }); it('should aggregate FIT lap summary stats when course summaries are absent', () => { const parseSpy = jest.spyOn(importerWithPrivateParse, 'parseFIT').mockResolvedValue({ file_ids: [{ type: 'course', manufacturer: 'garmin', product: 0 }], course: { name: 'Lap Summary Course', sport: 'cycling' }, laps: [ { total_distance: 100, total_ascent: 10, total_descent: 5, avg_grade: 2, max_pos_grade: 8, max_neg_grade: -3, records: [ { position_lat: 1, position_long: 2, distance: 0, altitude: 10 }, { position_lat: 1.01, position_long: 2.01, distance: 100, altitude: 20 } ] }, { total_distance: 300, total_ascent: 20, total_descent: 15, avg_grade: 6, max_pos_grade: 12, max_neg_grade: -9, records: [ { position_lat: 1.02, position_long: 2.02, distance: 100, altitude: 20 }, { position_lat: 1.03, position_long: 2.03, distance: 400, altitude: 15 } ] } ], course_points: [] }); return importer_route_fit_1.RouteImporterFIT.getFromArrayBuffer(new ArrayBuffer(0)) .then(routeFile => { const route = routeFile.getFirstRoute(); expect(route.getStat(data_distance_1.DataDistance.type).getValue()).toBe(400); expect(route.getStat(data_ascent_1.DataAscent.type).getValue()).toBe(30); expect(route.getStat(data_descent_1.DataDescent.type).getValue()).toBe(20); expect(route.getStat(data_grade_avg_1.DataGradeAvg.type).getValue()).toBe(5); expect(route.getStat(data_grade_max_1.DataGradeMax.type).getValue()).toBe(12); expect(route.getStat(data_grade_min_1.DataGradeMin.type).getValue()).toBe(-9); expect(routeFile.getStat(data_distance_1.DataDistance.type).getValue()).toBe(400); }) .finally(() => { parseSpy.mockRestore(); }); }); it('should use final record distance when FIT lap summary distance coverage is incomplete', () => { const parseSpy = jest.spyOn(importerWithPrivateParse, 'parseFIT').mockResolvedValue({ file_ids: [{ type: 'course', manufacturer: 'garmin', product: 0 }], course: { name: 'Partial Lap Summary Course', sport: 'cycling' }, laps: [ { total_distance: 100, total_ascent: 10, records: [ { position_lat: 1, position_long: 2, distance: 0, altitude: 10 }, { position_lat: 1.01, position_long: 2.01, distance: 100, altitude: 20 } ] }, { records: [ { position_lat: 1.02, position_long: 2.02, distance: 100, altitude: 20 }, { position_lat: 1.03, position_long: 2.03, distance: 400, altitude: 40 }, { position_lat: 1.04, position_long: 2.04, altitude: 40 } ] } ], course_points: [] }); return importer_route_fit_1.RouteImporterFIT.getFromArrayBuffer(new ArrayBuffer(0), new route_parsing_options_1.RouteParsingOptions({ streams: { smooth: { altitudeSmooth: false, grade: false, gradeSmooth: false } } })) .then(routeFile => { const route = routeFile.getFirstRoute(); expect(route.getStat(data_distance_1.DataDistance.type).getValue()).toBe(400); expect(routeFile.getStat(data_distance_1.DataDistance.type).getValue()).toBe(400); expect(route.getStat(data_ascent_1.DataAscent.type).getValue()).toBe(30); expect(routeFile.getStat(data_ascent_1.DataAscent.type).getValue()).toBe(30); }) .finally(() => { parseSpy.mockRestore(); }); }); it('should reject normal activity FIT files', () => __awaiter(void 0, void 0, void 0, function* () { yield expect(importer_route_fit_1.RouteImporterFIT.getFromArrayBuffer(readFixture(`${process.cwd()}/src/specs/fixtures/runs/fit/2067489619.fit`))).rejects.toBeInstanceOf(parsing_event_lib_error_1.ParsingEventLibError); })); });