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

343 lines (342 loc) 18.5 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 }); exports.RouteImporterFIT = void 0; const activity_types_1 = require("../../../../activities/activity.types"); const creator_1 = require("../../../../creators/creator"); const data_ascent_1 = require("../../../../data/data.ascent"); const data_altitude_1 = require("../../../../data/data.altitude"); const data_descent_1 = require("../../../../data/data.descent"); const data_distance_1 = require("../../../../data/data.distance"); 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 parsing_event_lib_error_1 = require("../../../../errors/parsing-event-lib.error"); const file_type_enum_1 = require("../../../../events/adapters/file-type.enum"); const fit_summary_stat_aggregations_1 = require("../../../../fit/fit-summary-stat.aggregations"); const fit_creator_mapper_1 = require("../../../../fit/fit-creator.mapper"); const geolib_adapter_1 = require("../../../../geodesy/adapters/geolib.adapter"); const stats_utilities_1 = require("../../../../stats/stats.utilities"); const route_1 = require("../../../route"); const route_file_1 = require("../../../route-file"); const route_file_utilities_1 = require("../../../route-file.utilities"); const route_parsing_options_1 = require("../../../route-parsing-options"); const route_stream_1 = require("../../../route-stream"); const route_utilities_1 = require("../../../route.utilities"); class RouteImporterFIT { static getFromArrayBuffer(arrayBuffer_1) { return __awaiter(this, arguments, void 0, function* (arrayBuffer, options = route_parsing_options_1.RouteParsingOptions.DEFAULT, name = 'New Route File') { const fitDataObject = yield this.parseFIT(arrayBuffer); if (!this.isFITCourse(fitDataObject)) { throw new parsing_event_lib_error_1.ParsingEventLibError('FIT file is not a route/course file'); } const records = this.getRouteRecords(fitDataObject); if (!records.length) { throw new parsing_event_lib_error_1.ParsingEventLibError('No routes found in FIT'); } const creator = this.getCreator(fitDataObject); const routeName = this.getCourseName(fitDataObject) || name; const route = new route_1.Route(creator, options, routeName, this.getActivityType(fitDataObject), this.getPointsFromRecords(records), this.getRouteMetadata(fitDataObject)); this.addRouteStreams(route, records); this.addRouteSummaryStats(route, fitDataObject, records); route_utilities_1.RouteUtilities.generateMissingStreamsAndStatsForRoute(route); const routeFile = new route_file_1.RouteFile(routeName, file_type_enum_1.FileType.FIT, creator, [route], this.getWaypointsFromCoursePoints(fitDataObject, records), this.getCreatedAt(fitDataObject, records)); route_file_utilities_1.RouteFileUtilities.reGenerateStatsForRouteFile(routeFile); return routeFile; }); } static parseFIT(arrayBuffer) { return __awaiter(this, void 0, void 0, function* () { // @ts-ignore const { default: FitFileParser } = yield Promise.resolve().then(() => __importStar(require('fit-file-parser'))); return new Promise((resolve, reject) => { const fitFileParser = new FitFileParser({ force: true, speedUnit: 'm/s', lengthUnit: 'm', temperatureUnit: 'celsius', elapsedRecordField: false, mode: 'both' }); fitFileParser.parse(arrayBuffer, (error, fitDataObject) => { if (error || !fitDataObject) { reject(new parsing_event_lib_error_1.ParsingEventLibError('Unable to parse FIT route file')); return; } resolve(fitDataObject); }); }); }); } static isFITCourse(fitDataObject) { var _a; const fileIdType = (_a = this.toArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.file_ids)[0]) === null || _a === void 0 ? void 0 : _a.type; if (fileIdType) { return fileIdType === 'course'; } return (!!(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.course) || (this.toArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.course_points).length > 0 && !this.toArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.sessions).length)); } static getRouteRecords(fitDataObject) { const records = this.toArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.records); const lapRecords = this.toArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.laps).reduce((accu, lap) => { return accu.concat(this.toArray(lap === null || lap === void 0 ? void 0 : lap.records)); }, []); return (records.length ? records : lapRecords).filter(record => { return this.isFiniteNumber(record.position_lat) && this.isFiniteNumber(record.position_long); }); } static getPointsFromRecords(records) { return records.map(record => ({ latitudeDegrees: record.position_lat, longitudeDegrees: record.position_long, altitude: this.getRecordAltitude(record) })); } static addRouteStreams(route, records) { route.addStream(new route_stream_1.RouteStream(data_latitude_degrees_1.DataLatitudeDegrees.type, records.map(record => record.position_lat))); route.addStream(new route_stream_1.RouteStream(data_longitude_degrees_1.DataLongitudeDegrees.type, records.map(record => record.position_long))); if (records.some(record => this.isFiniteNumber(this.getRecordAltitude(record)))) { route.addStream(new route_stream_1.RouteStream(data_altitude_1.DataAltitude.type, records.map(record => this.getRecordAltitude(record)))); } if (records.some(record => this.isFiniteNumber(record.distance))) { route.addStream(new route_stream_1.RouteStream(data_distance_1.DataDistance.type, records.map(record => (this.isFiniteNumber(record.distance) ? record.distance : null)))); } } static addRouteSummaryStats(route, fitDataObject, records) { const fitRecord = this.getObjectRecord(fitDataObject); const summaryObjects = [ fitRecord === null || fitRecord === void 0 ? void 0 : fitRecord.course, this.getAggregateSummaryObject(this.toArray(fitRecord === null || fitRecord === void 0 ? void 0 : fitRecord.sessions)), this.getAggregateSummaryObject(this.toArray(fitRecord === null || fitRecord === void 0 ? void 0 : fitRecord.laps)), fitRecord, this.getFinalRecordDistanceSummary(records) ]; this.addSummaryStat(route, summaryObjects, ['total_distance', 'TotalDistance'], data_distance_1.DataDistance); this.addSummaryStat(route, summaryObjects, ['total_ascent', 'TotalAscent'], data_ascent_1.DataAscent); this.addSummaryStat(route, summaryObjects, ['total_descent', 'TotalDescent'], data_descent_1.DataDescent); this.addSummaryStat(route, summaryObjects, ['avg_grade', 'AvgGrade'], data_grade_avg_1.DataGradeAvg); this.addSummaryStat(route, summaryObjects, ['max_pos_grade', 'MaxPositiveGrade'], data_grade_max_1.DataGradeMax); this.addSummaryStat(route, summaryObjects, ['max_neg_grade', 'MaxNegativeGrade'], data_grade_min_1.DataGradeMin); } static addSummaryStat(route, summaryObjects, keys, DataClass) { if (route.getStat(DataClass.type)) { return; } const value = this.getSummaryValue(summaryObjects, keys); if (value === null) { return; } route.addStat(new DataClass(value)); } static getSummaryValue(summaryObjects, keys) { for (const summaryObject of summaryObjects) { const summaryValue = stats_utilities_1.StatsUtilities.getFiniteNumericRecordValue(summaryObject, keys); if (summaryValue !== null) { return summaryValue; } } return null; } static getAggregateSummaryObject(summaryObjects) { const summary = stats_utilities_1.StatsUtilities.aggregateNumericRecords(summaryObjects, fit_summary_stat_aggregations_1.FIT_DISTANCE_ELEVATION_GRADE_SUMMARY_AGGREGATIONS); return Object.keys(summary).length ? summary : null; } static getFinalRecordDistanceSummary(records) { for (let index = records.length - 1; index >= 0; index--) { if (this.isFiniteNumber(records[index].distance)) { return { total_distance: records[index].distance }; } } return null; } static getObjectRecord(value) { return value && typeof value === 'object' ? value : null; } static getWaypointsFromCoursePoints(fitDataObject, records) { return this.toArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.course_points) .map(coursePoint => this.getWaypointFromCoursePoint(coursePoint, records)) .filter((waypoint) => waypoint !== null); } static getWaypointFromCoursePoint(coursePoint, records) { var _a; const routePointIndex = this.getNearestRoutePointIndex(coursePoint, records); const routePoint = routePointIndex === null ? null : records[routePointIndex]; const latitude = this.isFiniteNumber(coursePoint.position_lat) ? coursePoint.position_lat : routePoint === null || routePoint === void 0 ? void 0 : routePoint.position_lat; const longitude = this.isFiniteNumber(coursePoint.position_long) ? coursePoint.position_long : routePoint === null || routePoint === void 0 ? void 0 : routePoint.position_long; if (!this.isFiniteNumber(latitude) || !this.isFiniteNumber(longitude)) { return null; } const routeDistance = routePoint && this.isFiniteNumber(routePoint.distance) ? routePoint.distance : null; const sourceDistance = this.isFiniteNumber(coursePoint.distance) ? coursePoint.distance : null; const distance = sourceDistance !== null && (sourceDistance !== 0 || routePointIndex === 0) ? sourceDistance : routeDistance; return { latitudeDegrees: latitude, longitudeDegrees: longitude, altitude: (_a = this.getRecordAltitude(coursePoint)) !== null && _a !== void 0 ? _a : (routePoint ? this.getRecordAltitude(routePoint) : null), name: coursePoint.name || null, type: coursePoint.type === undefined || coursePoint.type === null ? null : String(coursePoint.type), distance, routeIndex: 0, routePointIndex }; } static getNearestRoutePointIndex(coursePoint, records) { if (!records.length) { return null; } if (this.isFiniteNumber(coursePoint.position_lat) && this.isFiniteNumber(coursePoint.position_long)) { return this.getNearestRoutePointIndexByPosition(coursePoint, records); } if (this.isFiniteNumber(coursePoint.distance)) { return this.getNearestRoutePointIndexByDistance(coursePoint.distance, records); } return null; } static getNearestRoutePointIndexByPosition(coursePoint, records) { let bestIndex = 0; let bestDistance = Infinity; records.forEach((record, index) => { if (!this.isFiniteNumber(record.position_lat) || !this.isFiniteNumber(record.position_long)) { return; } const distance = this.geoLibAdapter.getDistance([ { latitudeDegrees: coursePoint.position_lat, longitudeDegrees: coursePoint.position_long }, { latitudeDegrees: record.position_lat, longitudeDegrees: record.position_long } ]); if (distance < bestDistance) { bestDistance = distance; bestIndex = index; } }); return bestIndex; } static getNearestRoutePointIndexByDistance(distance, records) { let bestIndex = null; let bestDistance = Infinity; records.forEach((record, index) => { if (!this.isFiniteNumber(record.distance)) { return; } const delta = Math.abs(record.distance - distance); if (delta < bestDistance) { bestDistance = delta; bestIndex = index; } }); return bestIndex; } static getCreator(fitDataObject) { if (this.toArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.file_ids).length) { try { return fit_creator_mapper_1.FITCreatorMapper.getCreatorFromFitDataObject(fitDataObject); } catch (_e) { // Fall through to the generic route creator. } } return new creator_1.Creator('Unknown Device'); } static getCourseName(fitDataObject) { var _a; const courseName = (_a = fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.course) === null || _a === void 0 ? void 0 : _a.name; return typeof courseName === 'string' && courseName.length ? courseName : null; } static getActivityType(fitDataObject) { var _a, _b; return (activity_types_1.ActivityTypesHelper.resolveActivityType((_a = fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.course) === null || _a === void 0 ? void 0 : _a.sport) || activity_types_1.ActivityTypesHelper.resolveActivityType((_b = this.toArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.sports)[0]) === null || _b === void 0 ? void 0 : _b.sport) || activity_types_1.ActivityTypes.route); } static getRouteMetadata(fitDataObject) { var _a; const capabilities = (_a = fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.course) === null || _a === void 0 ? void 0 : _a.capabilities; return capabilities ? { extensions: { fit: { courseCapabilities: capabilities } } } : {}; } static getCreatedAt(fitDataObject, records) { var _a, _b; return (this.getDate((_a = this.toArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.file_ids)[0]) === null || _a === void 0 ? void 0 : _a.time_created) || this.getDate((_b = records[0]) === null || _b === void 0 ? void 0 : _b.timestamp) || null); } static getDate(value) { if (value instanceof Date) { return Number.isNaN(value.getTime()) ? null : value; } if (typeof value !== 'string' && typeof value !== 'number') { return null; } const date = new Date(value); return Number.isNaN(date.getTime()) ? null : date; } static getRecordAltitude(record) { if (this.isFiniteNumber(record.enhanced_altitude)) { return record.enhanced_altitude; } return this.isFiniteNumber(record.altitude) ? record.altitude : null; } static isFiniteNumber(value) { return typeof value === 'number' && Number.isFinite(value); } static toArray(value) { if (value === undefined || value === null) { return []; } return Array.isArray(value) ? value : [value]; } } exports.RouteImporterFIT = RouteImporterFIT; RouteImporterFIT.geoLibAdapter = new geolib_adapter_1.GeoLibAdapter();