@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
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 });
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();