@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
2,117 lines • 130 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.EventImporterFIT = void 0;
const event_1 = require("../../../event");
const activity_1 = require("../../../../activities/activity");
const swim_length_1 = require("../../../../swim-lengths/swim-length");
const lap_1 = require("../../../../laps/lap");
const activity_types_1 = require("../../../../activities/activity.types");
const data_duration_1 = require("../../../../data/data.duration");
const data_elapsed_time_1 = require("../../../../data/data.elapsed-time");
const data_energy_1 = require("../../../../data/data.energy");
const data_distance_1 = require("../../../../data/data.distance");
const garmin_profile_data_1 = require("../../../../fit/garmin-profile.data");
const data_pause_1 = require("../../../../data/data.pause");
const data_cadence_1 = require("../../../../data/data.cadence");
const data_cadence_avg_1 = require("../../../../data/data.cadence-avg");
const data_power_avg_1 = require("../../../../data/data.power-avg");
const data_speed_avg_1 = require("../../../../data/data.speed-avg");
const data_cadence_max_1 = require("../../../../data/data.cadence-max");
const data_power_max_1 = require("../../../../data/data.power-max");
const data_ascent_1 = require("../../../../data/data.ascent");
const data_descent_1 = require("../../../../data/data.descent");
const data_heart_rate_avg_1 = require("../../../../data/data.heart-rate-avg");
const data_heart_rate_max_1 = require("../../../../data/data.heart-rate-max");
const data_speed_max_1 = require("../../../../data/data.speed-max");
const lap_types_1 = require("../../../../laps/lap.types");
const data_heart_rate_min_1 = require("../../../../data/data.heart-rate-min");
const data_power_min_1 = require("../../../../data/data.power-min");
const data_aerobic_training_effect_1 = require("../../../../data/data-aerobic-training-effect");
const importer_fit_mapper_1 = require("./importer.fit.mapper");
const helpers_1 = require("../../../utilities/helpers");
const event_utilities_1 = require("../../../utilities/event.utilities");
const ibi_stream_1 = require("../../../../streams/ibi-stream");
const device_1 = require("../../../../activities/devices/device");
const importer_fit_ant_plus_device_names_1 = require("../../../../fit/device-names/importer.fit.ant-plus.device.names");
const data_recovery_time_1 = require("../../../../data/data.recovery-time");
const data_peak_epoc_1 = require("../../../../data/data.peak-epoc");
const data_feeling_1 = require("../../../../data/data.feeling");
const data_temperature_max_1 = require("../../../../data/data.temperature-max");
const data_temperature_min_1 = require("../../../../data/data.temperature-min");
const data_temperature_avg_1 = require("../../../../data/data.temperature-avg");
const data_speed_min_1 = require("../../../../data/data.speed-min");
const data_cadence_min_1 = require("../../../../data/data.cadence-min");
const data_swolf_25m_1 = require("../../../../data/data.swolf-25m");
const data_description_1 = require("../../../../data/data.description");
const data_vo2_max_1 = require("../../../../data/data.vo2-max");
const intensity_zones_1 = require("../../../../intensity-zones/intensity-zones");
const data_heart_rate_1 = require("../../../../data/data.heart-rate");
const data_power_1 = require("../../../../data/data.power");
const data_speed_1 = require("../../../../data/data.speed");
const data_heart_rate_zone_one_duration_1 = require("../../../../data/data.heart-rate-zone-one-duration");
const data_heart_rate_zone_two_duration_1 = require("../../../../data/data.heart-rate-zone-two-duration");
const data_heart_rate_zone_three_duration_1 = require("../../../../data/data.heart-rate-zone-three-duration");
const data_heart_rate_zone_four_duration_1 = require("../../../../data/data.heart-rate-zone-four-duration");
const data_heart_rate_zone_five_duration_1 = require("../../../../data/data.heart-rate-zone-five-duration");
const data_heart_rate_zone_six_duration_1 = require("../../../../data/data.heart-rate-zone-six-duration");
const data_heart_rate_zone_seven_duration_1 = require("../../../../data/data.heart-rate-zone-seven-duration");
const data_power_zone_one_duration_1 = require("../../../../data/data.power-zone-one-duration");
const data_power_zone_two_duration_1 = require("../../../../data/data.power-zone-two-duration");
const data_power_zone_three_duration_1 = require("../../../../data/data.power-zone-three-duration");
const data_power_zone_four_duration_1 = require("../../../../data/data.power-zone-four-duration");
const data_power_zone_five_duration_1 = require("../../../../data/data.power-zone-five-duration");
const data_power_zone_six_duration_1 = require("../../../../data/data.power-zone-six-duration");
const data_power_zone_seven_duration_1 = require("../../../../data/data.power-zone-seven-duration");
const data_speed_zone_one_duration_1 = require("../../../../data/data.speed-zone-one-duration");
const data_speed_zone_two_duration_1 = require("../../../../data/data.speed-zone-two-duration");
const data_speed_zone_three_duration_1 = require("../../../../data/data.speed-zone-three-duration");
const data_speed_zone_four_duration_1 = require("../../../../data/data.speed-zone-four-duration");
const data_speed_zone_five_duration_1 = require("../../../../data/data.speed-zone-five-duration");
const data_speed_zone_six_duration_1 = require("../../../../data/data.speed-zone-six-duration");
const data_speed_zone_seven_duration_1 = require("../../../../data/data.speed-zone-seven-duration");
const empty_event_sports_libs_error_1 = require("../../../../errors/empty-event-sports-libs.error");
const data_start_event_1 = require("../../../../data/data.start-event");
const data_stop_event_1 = require("../../../../data/data.stop-event");
const data_stop_all_event_1 = require("../../../../data/data.stop-all-event");
const data_moving_time_1 = require("../../../../data/data.moving-time");
const activity_utilities_1 = require("../../../utilities/activity.utilities");
const data_timer_time_1 = require("../../../../data/data.timer-time");
const data_total_cycles_1 = require("../../../../data/data-total-cycles");
const data_pool_length_1 = require("../../../../data/data.pool-length");
const data_active_lengths_1 = require("../../../../data/data-active-lengths");
const data_active_lap_1 = require("../../../../data/data-active-lap");
const data_swolf_50m_1 = require("../../../../data/data.swolf-50m");
const file_type_enum_1 = require("../../file-type.enum");
const data_power_torque_effectiveness_left_1 = require("../../../../data/data.power-torque-effectiveness-left");
const data_power_torque_effectiveness_right_1 = require("../../../../data/data.power-torque-effectiveness-right");
const data_power_pedal_smoothness_left_1 = require("../../../../data/data.power-pedal-smoothness-left");
const data_power_pedal_smoothness_right_1 = require("../../../../data/data.power-pedal-smoothness-right");
const data_ftp_1 = require("../../../../data/data.ftp");
const data_power_normalized_1 = require("../../../../data/data.power-normalized");
const data_power_intensity_factor_1 = require("../../../../data/data.power-intensity-factor");
const data_training_stress_score_1 = require("../../../../data/data.training-stress-score");
const data_power_work_1 = require("../../../../data/data.power-work");
const data_cycling_standing_time_1 = require("../../../../data/data.cycling-standing-time");
const data_cycling_seated_time_1 = require("../../../../data/data.cycling-seated-time");
const data_cycling_position_1 = require("../../../../data/data.cycling-position");
const data_rider_position_change_event_1 = require("../../../../data/data.rider-position-change-event");
const data_ground_contact_time_avg_1 = require("../../../../data/data.ground-contact-time-avg");
const data_depth_max_1 = require("../../../../data/data.depth-max");
const data_effort_pace_avg_1 = require("../../../../data/data.effort-pace-avg");
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 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_avg_stride_length_1 = require("../../../../data/data.avg-stride-length");
const data_anaerobic_training_effect_1 = require("../../../../data/data-anaerobic-training-effect");
const data_avg_respiration_rate_1 = require("../../../../data/data.avg-respiration-rate");
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_jump_count_1 = require("../../../../data/data.jump-count");
const data_total_grit_1 = require("../../../../data/data.total-grit");
const data_total_flow_1 = require("../../../../data/data.total-flow");
const data_avg_flow_1 = require("../../../../data/data.avg-flow");
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_resting_calories_1 = require("../../../../data/data.resting-calories");
const data_training_load_peak_1 = require("../../../../data/data.training-load-peak");
const data_avg_vam_1 = require("../../../../data/data.avg-vam");
const data_end_position_1 = require("../../../../data/data.end-position");
const data_start_position_1 = require("../../../../data/data.start-position");
const activity_parsing_options_1 = require("../../../../activities/activity-parsing-options");
const parsing_event_lib_error_1 = require("../../../../errors/parsing-event-lib.error");
const data_power_down_1 = require("../../../../data/data.power-down");
const data_power_up_1 = require("../../../../data/data.power-up");
const fit_summary_stat_aggregations_1 = require("../../../../fit/fit-summary-stat.aggregations");
const fit_creator_mapper_1 = require("../../../../fit/fit-creator.mapper");
const data_weight_1 = require("../../../../data/data.weight");
const data_height_1 = require("../../../../data/data.height");
const data_age_1 = require("../../../../data/data.age");
const data_gender_1 = require("../../../../data/data.gender");
const data_avg_grit_1 = require("../../../../data/data.avg-grit");
const data_jump_event_1 = require("../../../../data/data.jump-event");
const data_battery_consumption_1 = require("../../../../data/data.battery-consumption");
const data_battery_life_estimation_1 = require("../../../../data/data.battery-life-estimation");
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_stamina_1 = require("../../../../data/data.stamina");
const data_jump_stats_1 = require("../../../../data/data.jump-stats");
const stream_selection_1 = require("../../../../streams/stream.selection");
const stats_utilities_1 = require("../../../../stats/stats.utilities");
// Threshold to detect that session.timestamp are not trustable (when exceeding 15% of session.total_elapsed_time)
const INVALID_DATES_ELAPSED_TIME_RATIO_THRESHOLD = 1.15;
const TIMER_ELAPSED_ROUNDING_TOLERANCE_SECONDS = 1;
class EventImporterFIT {
static getFromArrayBuffer(arrayBuffer_1) {
return __awaiter(this, arguments, void 0, function* (arrayBuffer, options = activity_parsing_options_1.ActivityParsingOptions.DEFAULT, name = 'New Event') {
const streamSelection = (0, stream_selection_1.getStreamSelectionFromOptions)(options);
// @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) => {
var _a, _b;
if (error) {
// For now, assume any error from parser on this file means it's broken/empty in a way we treat as EmptyEventLibError
// to satisfy existing tests. Or ideally we wrap in a generic EventLibError.
// But test expects EmptyEventLibError.
reject(new empty_event_sports_libs_error_1.EmptyEventLibError());
return;
}
if (!fitDataObject) {
reject(new empty_event_sports_libs_error_1.EmptyEventLibError());
return;
}
this.normalizeFitDataObjectForActivities(fitDataObject);
if (!fitDataObject.sessions.length) {
reject(new empty_event_sports_libs_error_1.EmptyEventLibError());
return;
}
// Check if we have length data at the top level (new parser behavior or missing mapping)
if (fitDataObject.lengths && fitDataObject.lengths.length > 0) {
(_a = fitDataObject.sessions) === null || _a === void 0 ? void 0 : _a.forEach((session) => {
var _a;
const sessionStartTime = new Date(session.start_time).getTime();
const sessionEndTime = sessionStartTime + (session.total_elapsed_time || 0) * 1000;
session.lengths = fitDataObject.lengths.filter((length) => {
const lengthTime = new Date(length.timestamp || length.start_time).getTime();
return lengthTime >= sessionStartTime && lengthTime < sessionEndTime;
});
// Also distribute to laps
(_a = session.laps) === null || _a === void 0 ? void 0 : _a.forEach((lap) => {
const lapStartTime = new Date(lap.start_time).getTime();
const lapEndTime = lapStartTime + (lap.total_elapsed_time || 0) * 1000;
lap.lengths = fitDataObject.lengths.filter((length) => {
const lengthTime = new Date(length.timestamp || length.start_time).getTime();
return lengthTime >= lapStartTime && lengthTime < lapEndTime;
});
});
});
}
// Check for jumps data at the top level
if (fitDataObject.jumps && fitDataObject.jumps.length > 0) {
(_b = fitDataObject.sessions) === null || _b === void 0 ? void 0 : _b.forEach((session) => {
const sessionStartTime = new Date(session.start_time).getTime();
const sessionEndTime = sessionStartTime + (session.total_elapsed_time || 0) * 1000;
session.jumps = fitDataObject.jumps.filter((jump) => {
const jumpTime = new Date(jump.timestamp).getTime();
return jumpTime >= sessionStartTime && jumpTime < sessionEndTime;
});
});
}
const allowedSampleMappings = importer_fit_mapper_1.FITSampleMapper.filter(sampleMapping => (0, stream_selection_1.isStreamTypeAllowedForImport)(sampleMapping.dataType, streamSelection));
// Iterate over the sessions and create their activities
const activities = fitDataObject.sessions.map((sessionObject) => {
// Get the activity from the sessionObject
const activity = this.getActivityFromSessionObject(sessionObject, fitDataObject, options);
// Go over the laps
sessionObject.laps.forEach((sessionLapObject, index) => {
activity.addLap(this.getLapFromSessionLapObject(sessionLapObject, activity, index, options));
});
const manufacturer = (fitDataObject.file_ids && fitDataObject.file_ids[0] && fitDataObject.file_ids[0].manufacturer) || '';
const zoneIndexOffset = manufacturer === 'garmin' ? 1 : 0;
// Go over the hr zone info
if (sessionObject.time_in_hr_zone && sessionObject.time_in_hr_zone.length) {
// Add the stats
if ((0, helpers_1.isNumber)(sessionObject.time_in_hr_zone[0 + zoneIndexOffset])) {
activity.addStat(new data_heart_rate_zone_one_duration_1.DataHeartRateZoneOneDuration(sessionObject.time_in_hr_zone[0 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_hr_zone[1 + zoneIndexOffset])) {
activity.addStat(new data_heart_rate_zone_two_duration_1.DataHeartRateZoneTwoDuration(sessionObject.time_in_hr_zone[1 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_hr_zone[2 + zoneIndexOffset])) {
activity.addStat(new data_heart_rate_zone_three_duration_1.DataHeartRateZoneThreeDuration(sessionObject.time_in_hr_zone[2 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_hr_zone[3 + zoneIndexOffset])) {
activity.addStat(new data_heart_rate_zone_four_duration_1.DataHeartRateZoneFourDuration(sessionObject.time_in_hr_zone[3 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_hr_zone[4 + zoneIndexOffset])) {
activity.addStat(new data_heart_rate_zone_five_duration_1.DataHeartRateZoneFiveDuration(sessionObject.time_in_hr_zone[4 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_hr_zone[5 + zoneIndexOffset])) {
activity.addStat(new data_heart_rate_zone_six_duration_1.DataHeartRateZoneSixDuration(sessionObject.time_in_hr_zone[5 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_hr_zone[6 + zoneIndexOffset])) {
activity.addStat(new data_heart_rate_zone_seven_duration_1.DataHeartRateZoneSevenDuration(sessionObject.time_in_hr_zone[6 + zoneIndexOffset]));
}
const hrIntensityZones = this.createIntensityZonesFromFitZones(data_heart_rate_1.DataHeartRate.type, sessionObject.time_in_hr_zone, zoneIndexOffset, sessionObject.hr_zone_high_boundary, 255);
activity.intensityZones.push(hrIntensityZones);
}
// Go over the power zone info
if (sessionObject.time_in_power_zone && sessionObject.time_in_power_zone.length) {
if ((0, helpers_1.isNumber)(sessionObject.time_in_power_zone[0 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_one_duration_1.DataPowerZoneOneDuration(sessionObject.time_in_power_zone[0 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_power_zone[1 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_two_duration_1.DataPowerZoneTwoDuration(sessionObject.time_in_power_zone[1 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_power_zone[2 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_three_duration_1.DataPowerZoneThreeDuration(sessionObject.time_in_power_zone[2 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_power_zone[3 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_four_duration_1.DataPowerZoneFourDuration(sessionObject.time_in_power_zone[3 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_power_zone[4 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_five_duration_1.DataPowerZoneFiveDuration(sessionObject.time_in_power_zone[4 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_power_zone[5 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_six_duration_1.DataPowerZoneSixDuration(sessionObject.time_in_power_zone[5 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_power_zone[6 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_seven_duration_1.DataPowerZoneSevenDuration(sessionObject.time_in_power_zone[6 + zoneIndexOffset]));
}
const powerIntensityZones = this.createIntensityZonesFromFitZones(data_power_1.DataPower.type, sessionObject.time_in_power_zone, zoneIndexOffset, sessionObject.power_zone_high_boundary, 65535);
activity.intensityZones.push(powerIntensityZones);
}
// Go over the speed zone info
if (sessionObject.time_in_speed_zone && sessionObject.time_in_speed_zone.length) {
if ((0, helpers_1.isNumber)(sessionObject.time_in_speed_zone[0 + zoneIndexOffset])) {
activity.addStat(new data_speed_zone_one_duration_1.DataSpeedZoneOneDuration(sessionObject.time_in_speed_zone[0 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_speed_zone[1 + zoneIndexOffset])) {
activity.addStat(new data_speed_zone_two_duration_1.DataSpeedZoneTwoDuration(sessionObject.time_in_speed_zone[1 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_speed_zone[2 + zoneIndexOffset])) {
activity.addStat(new data_speed_zone_three_duration_1.DataSpeedZoneThreeDuration(sessionObject.time_in_speed_zone[2 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_speed_zone[3 + zoneIndexOffset])) {
activity.addStat(new data_speed_zone_four_duration_1.DataSpeedZoneFourDuration(sessionObject.time_in_speed_zone[3 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_speed_zone[4 + zoneIndexOffset])) {
activity.addStat(new data_speed_zone_five_duration_1.DataSpeedZoneFiveDuration(sessionObject.time_in_speed_zone[4 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_speed_zone[5 + zoneIndexOffset])) {
activity.addStat(new data_speed_zone_six_duration_1.DataSpeedZoneSixDuration(sessionObject.time_in_speed_zone[5 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(sessionObject.time_in_speed_zone[6 + zoneIndexOffset])) {
activity.addStat(new data_speed_zone_seven_duration_1.DataSpeedZoneSevenDuration(sessionObject.time_in_speed_zone[6 + zoneIndexOffset]));
}
const speedIntensityZones = new intensity_zones_1.IntensityZones(data_speed_1.DataSpeed.type);
speedIntensityZones.zone1Duration = sessionObject.time_in_speed_zone[0 + zoneIndexOffset] || 0;
speedIntensityZones.zone2Duration = sessionObject.time_in_speed_zone[1 + zoneIndexOffset] || 0;
speedIntensityZones.zone3Duration = sessionObject.time_in_speed_zone[2 + zoneIndexOffset] || 0;
speedIntensityZones.zone4Duration = sessionObject.time_in_speed_zone[3 + zoneIndexOffset] || 0;
speedIntensityZones.zone5Duration = sessionObject.time_in_speed_zone[4 + zoneIndexOffset] || 0;
speedIntensityZones.zone6Duration = sessionObject.time_in_speed_zone[5 + zoneIndexOffset] || 0;
speedIntensityZones.zone7Duration = sessionObject.time_in_speed_zone[6 + zoneIndexOffset] || 0;
activity.intensityZones.push(speedIntensityZones);
}
// Add the events
fitDataObject.events
.filter((activityEvent) => {
return activityEvent.timestamp >= activity.startDate && activityEvent.timestamp <= activity.endDate;
})
.forEach((activityEvent) => {
if (activityEvent.event === 'timer') {
switch (activityEvent.event_type) {
case 'start':
activity.addEvent(new data_start_event_1.DataStartEvent(activity.getDateIndex(activityEvent.timestamp)));
break;
case 'stop':
activity.addEvent(new data_stop_event_1.DataStopEvent(activity.getDateIndex(activityEvent.timestamp)));
break;
case 'stop_all':
activity.addEvent(new data_stop_all_event_1.DataStopAllEvent(activity.getDateIndex(activityEvent.timestamp)));
break;
default:
break;
}
}
else if (activityEvent.event === 'power_down') {
activity.addEvent(new data_power_down_1.DataPowerDown(activity.getDateIndex(activityEvent.timestamp)));
}
else if (activityEvent.event === 'power_up') {
activity.addEvent(new data_power_up_1.DataPowerUp(activity.getDateIndex(activityEvent.timestamp)));
}
else if (activityEvent.event === 'rider_position_change') {
const positionChange = activityEvent.data;
if (positionChange === data_cycling_position_1.RiderPosition.SEATED ||
positionChange === data_cycling_position_1.RiderPosition.STANDING ||
positionChange === data_cycling_position_1.RiderPosition.TRANSITION_TO_SEATED ||
positionChange === data_cycling_position_1.RiderPosition.TRANSITION_TO_STANDING) {
activity.addEvent(new data_rider_position_change_event_1.DataRiderPositionChangeEvent(activity.getDateIndex(activityEvent.timestamp), positionChange));
}
}
});
// Add jumps
if (fitDataObject.jumps && fitDataObject.jumps.length) {
fitDataObject.jumps
.filter((jump) => {
const timestamp = new Date(jump.timestamp);
// Relaxed check for jumps as they might be recorded slightly outside session stats or have timezone offsets
const margin = 24 * 60 * 60 * 1000; // 24 hours
return (timestamp.getTime() >= activity.startDate.getTime() - margin &&
timestamp.getTime() <= activity.endDate.getTime() + margin);
})
.forEach((jump) => {
activity.addEvent(new data_jump_event_1.DataJumpEvent(activity.getDateIndex(jump.timestamp), {
distance: jump.distance,
height: jump.height,
score: jump.score,
hang_time: jump.hang_time,
position_lat: jump.position_lat,
position_long: jump.position_long,
speed: jump.speed,
rotations: jump.rotations
}));
});
}
// Get the samples..
// Test if activity is lengths based
// Indeed when based on lengths, an activity do not provides samples under records object (e.g. Pool swimming activities)
// Note: this is how Strava generate streams for this kind of activities
const isLengthsBased = this.isLengthsBased(sessionObject);
const samples = isLengthsBased
? this.generateSamplesFromLengths(sessionObject, options)
: fitDataObject.records.filter((record) => {
return record.timestamp >= activity.startDate && record.timestamp <= activity.endDate;
});
// Setup sample info which could be use when getting sample values
const hasPowerMeter = samples.findIndex((sample) => Number.isFinite((0, helpers_1.isNumber)(sample.power) ? sample.power : (0, helpers_1.isNumber)(sample.Power) ? sample.Power : sample.RP_Power)) !== -1;
const samplesInfo = { hasPowerMeter: hasPowerMeter };
allowedSampleMappings.forEach(sampleMapping => {
// @todo not sure if we need to check for number only ...
const subjectSamples = (samples.filter((sample) => (0, helpers_1.isNumber)(sampleMapping.getSampleValue(sample, samplesInfo))));
if (subjectSamples.length) {
// When we create a stream here it has the length of the activity elapsed time (end-start) filled with nulls.
// We keep nulls in order to preserve the array length.
activity.addStream(activity.createStream(sampleMapping.dataType));
subjectSamples.forEach(subjectSample => {
activity.addDataToStream(sampleMapping.dataType, new Date(subjectSample.timestamp), sampleMapping.getSampleValue(subjectSample, samplesInfo));
});
}
});
return activity;
});
// If there are no activities to parse ....
if (!activities.length) {
reject(new empty_event_sports_libs_error_1.EmptyEventLibError());
return;
}
// Get the HRV to IBI if exist
if (fitDataObject.hrv && fitDataObject.hrv.length) {
activities.forEach((activity) => {
const ibiData = this.getIBIDataForActivity(fitDataObject.hrv, activities[0].startDate, activity);
// set the IBI
activity.addStream(new ibi_stream_1.IBIStream(ibiData));
});
}
// Parse the device infos
if (fitDataObject.device_infos && fitDataObject.device_infos.length) {
const fitDeviceInfos = fitDataObject.device_infos;
activities.forEach(activity => {
// Filter device infos to find those relevant to this activity's timeframe.
// This list is used by `changes` mode compaction and battery stats.
const activityDeviceInfos = fitDeviceInfos
.filter((di) => {
const timestamp = new Date(di.timestamp).getTime();
// Allow a small margin (e.g. 1 minute) before/after activity
return (timestamp >= activity.startDate.getTime() - 60000 && timestamp <= activity.endDate.getTime() + 60000);
})
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
/**
* FIT `device_info` often repeats the same device identity every second with only timestamp changing.
*
* - `raw`: keep all rows for backwards compatibility.
* - `changes`: keep first+last sample for each contiguous run where all fields except timestamp match.
*/
if (options.deviceInfoMode === 'changes') {
activity.creator.devices = this.compactDeviceInfosByRuns(this.mapDeviceInfosToDevices(activityDeviceInfos));
}
else {
activity.creator.devices = this.mapDeviceInfosToDevices(fitDeviceInfos);
}
// Compute battery consumption & estimation
// We focus on the device that has recorded the activity (usually index 0 or source_type 'local')
// Group by device index to track individual devices
const deviceGroups = new Map();
activityDeviceInfos.forEach((di) => {
var _a;
// Default to index 0 if undefined
const index = di.device_index !== undefined ? di.device_index : 0;
if (!deviceGroups.has(index)) {
deviceGroups.set(index, []);
}
(_a = deviceGroups.get(index)) === null || _a === void 0 ? void 0 : _a.push(di);
});
// Iterate over devices to find the one with battery data drain
// We prioritize device index 0 (main unit) but will look at others if 0 has no data
// or if it's the only one with significant data.
// For now, let's look for the main recording device (index 0).
const mainDeviceInfos = deviceGroups.get(0) || deviceGroups.get(1); // Sometimes 1? usually 0.
if (mainDeviceInfos && mainDeviceInfos.length >= 2) {
const startInfo = mainDeviceInfos[0];
const endInfo = mainDeviceInfos[mainDeviceInfos.length - 1];
const getLevel = (d) => (0, helpers_1.isNumber)(d.battery_level) ? d.battery_level : (0, helpers_1.isNumber)(d.battery_soc) ? d.battery_soc : null;
const startLevel = getLevel(startInfo);
const endLevel = getLevel(endInfo);
if (startLevel !== null && endLevel !== null && startLevel > endLevel) {
const consumption = startLevel - endLevel;
activity.addStat(new data_battery_consumption_1.DataBatteryConsumption(consumption));
const duration = (new Date(endInfo.timestamp).getTime() - new Date(startInfo.timestamp).getTime()) / 1000;
if (duration > 0 && consumption > 0) {
const estimatedTotalLife = (duration / consumption) * 100;
activity.addStat(new data_battery_life_estimation_1.DataBatteryLifeEstimation(Math.round(estimatedTotalLife)));
}
}
}
});
}
// Create an event
// @todo check if the start and end date can derive from the file
const event = new event_1.Event(name, activities[0].startDate, activities[activities.length - 1].endDate, file_type_enum_1.FileType.FIT);
activities.forEach(activity => event.addActivity(activity));
// debugger;
event_utilities_1.EventUtilities.generateStatsForAll(event);
event.getActivities().forEach(activity => {
(0, stream_selection_1.pruneActivityStreamsBySelection)(activity, streamSelection);
});
// debugger;
resolve(event);
});
});
});
}
/**
* Tell if an activity is lengths based (e.g. Pool swimming activities)
* @param sessionObject
* @private
*/
static isLengthsBased(sessionObject) {
var _a;
return ((_a = sessionObject.laps) === null || _a === void 0 ? void 0 : _a.filter((lap) => { var _a; return (_a = lap.lengths) === null || _a === void 0 ? void 0 : _a.length; }).length) > 1;
}
static getIBIDataForActivity(hrvRecords, eventStartDate, activity) {
let elapsedTime = 0;
return hrvRecords
.reduce((ibiArray, hrvRecord) => ibiArray.concat(hrvRecord.time), [])
.map((ibi) => ibi * 1000)
.filter((ibi) => {
elapsedTime += ibi;
const ibiDataDate = new Date(eventStartDate.getTime() + elapsedTime);
// FIT uses 0xFFFF as the invalid uint16 sentinel. Some parsers surface it as 65.535s.
if (ibi === this.INVALID_FIT_HRV_INTERVAL_MS) {
return false;
}
return ibiDataDate >= activity.startDate && ibiDataDate <= activity.endDate;
});
}
/**
* Generate streams samples based on lengths on an activity
* When based on lengths, an activity do not provides sample under records object
* @param sessionObject
* @param options
* @private
*/
static generateSamplesFromLengths(sessionObject, options) {
if (!this.isLengthsBased(sessionObject)) {
throw new parsing_event_lib_error_1.ParsingEventLibError('Trying to get samples from activities lengths, but no lengths is available');
}
let samples = [];
// Loop on every laps to catch every lengths where data is (speed, cadence, hr, ...)
sessionObject.laps.forEach((lap) => {
var _a;
// Loop on every laps
if ((_a = lap.lengths) === null || _a === void 0 ? void 0 : _a.length) {
// Get length in meters from lap total distance and total number of lengths
// We will use it to generate the distance stream below
const lengthMeters = lap.total_distance / lap.lengths.length;
// For each length of every laps build the streams data we will need for a later use
lap.lengths.forEach((length) => {
// Resolve start/end date of current length
const lengthStartDate = length.start_time;
const lengthDuration = length.total_timer_time || length.total_elapsed_time || 0;
const lengthEndDate = new Date(lengthStartDate.getTime() + lengthDuration * 1000);
// We check if length is valid comparing to max activity duration
if (lengthDuration > options.maxActivityDurationDays * 24 * 60 * 60) {
return;
}
if (lengthEndDate.getTime() <= lengthStartDate.getTime()) {
return;
}
// Generate a stream from length start date to end date filled by null values
const streamLength = activity_utilities_1.ActivityUtilities.getDataLength(lengthStartDate, lengthEndDate);
let lengthStream = Array(streamLength).fill(null);
// Define distance step to be used for distance stream
const lengthStepSize = lengthMeters / (lengthStream.length - 1);
// Generate the length stream based on data we have on current length
lengthStream = lengthStream.map((value, index) => {
var _a;
return {
timestamp: new Date(lengthStartDate.getTime() + index * 1000),
distance: (((_a = samples[samples.length - 1]) === null || _a === void 0 ? void 0 : _a.distance) || 0) + lengthStepSize * index,
speed: length.avg_speed || lap.avg_speed,
cadence: length.avg_cadence || length.avg_swimming_cadence || lap.avg_cadence,
heart_rate: length.avg_heart_rate || lap.avg_heart_rate
};
});
// Append to existing samples
samples = samples.concat(lengthStream);
});
}
});
return samples;
}
static mapDeviceInfosToDevices(deviceInfos) {
return deviceInfos.map((deviceInfo) => {
const device = new device_1.Device(deviceInfo.device_type);
device.index = deviceInfo.device_index;
device.name =
deviceInfo.product_name ||
importer_fit_ant_plus_device_names_1.ImporterFitAntPlusDeviceNames[deviceInfo.ant_device_number] ||
deviceInfo.ant_device_number;
device.batteryStatus = deviceInfo.battery_status;
device.batteryLevel = (0, helpers_1.isNumber)(deviceInfo.battery_level) ? deviceInfo.battery_level : deviceInfo.battery_soc;
device.batteryVoltage = deviceInfo.battery_voltage;
device.manufacturer = deviceInfo.manufacturer;
device.serialNumber = deviceInfo.serial_number;
device.product = deviceInfo.product;
device.swInfo = deviceInfo.software_version;
device.hwInfo = deviceInfo.hardware_version;
device.antDeviceNumber = deviceInfo.ant_device_number;
device.antTransmissionType = deviceInfo.ant_transmission_type;
device.antNetwork = deviceInfo.ant_network;
device.sourceType = deviceInfo.source_type;
if (deviceInfo.ant_id) {
device.antId = deviceInfo.ant_id;
}
device.cumOperatingTime = deviceInfo.cum_operating_time;
if (deviceInfo.timestamp) {
device.timestamp = new Date(deviceInfo.timestamp);
}
return device;
});
}
static deviceSignatureWithoutTimestamp(device) {
return JSON.stringify([
device.type,
device.name,
device.index,
device.batteryStatus,
device.batteryLevel,
device.batteryVoltage,
device.manufacturer,
device.serialNumber,
device.product,
device.swInfo,
device.hwInfo,
device.antDeviceNumber,
device.antTransmissionType,
device.antNetwork,
device.sourceType,
device.antId,
device.cumOperatingTime
]);
}
/**
* Keep first+last of each contiguous run with identical signature (all fields except timestamp).
* This preserves transitions while collapsing timestamp-only spam from FIT `device_info`.
*/
static compactDeviceInfosByRuns(devices) {
if (devices.length <= 1) {
return devices;
}
const compacted = [];
let runStart = devices[0];
let runEnd = devices[0];
let runSignature = this.deviceSignatureWithoutTimestamp(devices[0]);
for (let i = 1; i < devices.length; i++) {
const current = devices[i];
const currentSignature = this.deviceSignatureWithoutTimestamp(current);
if (currentSignature === runSignature) {
runEnd = current;
continue;
}
compacted.push(runStart);
if (runEnd !== runStart) {
compacted.push(runEnd);
}
runStart = current;
runEnd = current;
runSignature = currentSignature;
}
compacted.push(runStart);
if (runEnd !== runStart) {
compacted.push(runEnd);
}
return compacted;
}
static getNumericValue(value) {
if (!(0, helpers_1.isNumberOrString)(value)) {
return null;
}
const numericValue = Number(value);
if (!Number.isFinite(numericValue)) {
return null;
}
return numericValue;
}
static getDateFromValue(value) {
if (this.isValidDate(value)) {
return value;
}
if (typeof value === 'string' || typeof value === 'number') {
const dateValue = new Date(value);
if (this.isValidDate(dateValue)) {
return dateValue;
}
}
return null;
}
static getSessionlessLapStartDate(lapObject) {
const startTime = this.getDateFromValue(lapObject === null || lapObject === void 0 ? void 0 : lapObject.start_time);
if (startTime) {
return startTime;
}
const timestamp = this.getDateFromValue(lapObject === null || lapObject === void 0 ? void 0 : lapObject.timestamp);
if (timestamp && (0, helpers_1.isNumber)(lapObject === null || lapObject === void 0 ? void 0 : lapObject.total_elapsed_time) && lapObject.total_elapsed_time > 0) {
return new Date(timestamp.getTime() - lapObject.total_elapsed_time * 1000);
}
return timestamp;
}
static getSessionlessLapEndDate(lapObject, lapStartDate) {
const timestamp = this.getDateFromValue(lapObject === null || lapObject === void 0 ? void 0 : lapObject.timestamp);
if (timestamp) {
return timestamp;
}
if (lapStartDate && (0, helpers_1.isNumber)(lapObject === null || lapObject === void 0 ? void 0 : lapObject.total_elapsed_time) && lapObject.total_elapsed_time > 0) {
return new Date(lapStartDate.getTime() + lapObject.total_elapsed_time * 1000);
}
return null;
}
static buildSessionFromTopLevelMessages(fitDataObject) {
const laps = Array.isArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.laps)
? fitDataObject.laps.filter((lap) => lap && typeof lap === 'object')
: [];
const records = Array.isArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.records)
? fitDataObject.records.filter((record) => this.getDateFromValue(record === null || record === void 0 ? void 0 : record.timestamp))
: [];
const events = Array.isArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.events)
? fitDataObject.events.filter((event) => this.getDateFromValue(event === null || event === void 0 ? void 0 : event.timestamp))
: [];
if (!laps.length && !records.length && !events.length) {
return null;
}
const boundaryDates = [];
laps.forEach((lapObject) => {
const lapStartDate = this.getSessionlessLapStartDate(lapObject);
const lapEndDate = this.getSessionlessLapEndDate(lapObject, lapStartDate);
if (lapStartDate) {
boundaryDates.push(lapStartDate);
}
if (lapEndDate) {
boundaryDates.push(lapEndDate);
}
});
records.forEach((record) => {
const recordTimestamp = this.getDateFromValue(record === null || record === void 0 ? void 0 : record.timestamp);
if (recordTimestamp) {
boundaryDates.push(recordTimestamp);
}
});
events.forEach((event) => {
const eventTimestamp = this.getDateFromValue(event === null || event === void 0 ? void 0 : event.timestamp);
if (eventTimestamp) {
boundaryDates.push(eventTimestamp);
}
});
if (!boundaryDates.length) {
return null;
}
boundaryDates.sort((a, b) => a.getTime() - b.getTime());
const startDate = boundaryDates[0];
let endDate = boundaryDates[boundaryDates.length - 1];
const lapElapsedTime = laps.reduce((total, lapObject) => {
const lapElapsed = this.getNumericValue(lapObject === null || lapObject === void 0 ? void 0 : lapObject.total_elapsed_time);
return lapElapsed && lapElapsed > 0 ? total + lapElapsed : total;
}, 0);
const lapTimerTime = laps.reduce((total, lapObject) => {
const lapTimer = this.getNumericValue(lapObject === null || lapObject === void 0 ? void 0 : lapObject.total_timer_time);
return lapTimer && lapTimer > 0 ? total + lapTimer : total;
}, 0);
const elapsedTimeFromDates = (endDate.getTime() - startDate.getTime()) / 1000;
const totalElapsedTime = lapElapsedTime > 0 ? lapElapsedTime : elapsedTimeFromDates > 0 ? elapsedTimeFromDates : 0;
const totalTimerTime = lapTimerTime > 0 ? lapTimerTime : totalElapsedTime;
if (endDate.getTime() <= startDate.getTime() && totalElapsedTime > 0) {
endDate = new Date(startDate.getTime() + totalElapsedTime * 1000);
}
if (endDate.getTime() <= startDate.getTime()) {
return null;
}
const firstLapWithSport = laps.find((lapObject) => {
return (lapObject === null || lapObject === void 0 ? void 0 : lapObject.sport) !== undefined || (lapObject === null || lapObject === void 0 ? void 0 : lapObject.sub_sport) !== undefined;
});
const lapSummary = stats_utilities_1.StatsUtilities.aggregateNumericRecords(laps, fit_summary_stat_aggregations_1.FIT_DISTANCE_ELEVATION_GRADE_SUMMARY_AGGREGATIONS);
const sessionObject = Object.assign({ laps, start_time: startDate, timestamp: endDate }, lapSummary);
if (totalElapsedTime > 0) {
sessionObject.total_elapsed_time = totalElapsedTime;
}
if (totalTimerTime > 0) {
sessionObject.total_timer_time = totalTimerTime;
}
if ((firstLapWithSport === null || firstLapWithSport === void 0 ? void 0 : firstLapWithSport.sport) !== undefined) {
sessionObject.sport = firstLapWithSport.sport;
}
if ((firstLapWithSport === null || firstLapWithSport === void 0 ? void 0 : firstLapWithSport.sub_sport) !== undefined) {
sessionObject.sub_sport = firstLapWithSport.sub_sport;
}
const totalDistance = this.getFinalRecordDistance(records);
if (sessionObject.total_distance === undefined && totalDistance !== null) {
sessionObject.total_distance = totalDistance;
}
return sessionObject;
}
static getFinalRecordDistance(records) {
var _a;
for (let index = records.length - 1; index >= 0; index--) {
const distance = this.getNumericValue((_a = records[index]) === null || _a === void 0 ? void 0 : _a.distance);
if (distance !== null) {
return distance;
}
}
return null;
}
static normalizeFitDataObjectForActivities(fitDataObject) {
fitDataObject.records = Array.isArray(fitDataObject.records) ? fitDataObject.records : [];
fitDataObject.events = Array.isArray(fitDataObject.events) ? fitDataObject.events : [];
fitDataObject.laps = Array.isArray(fitDataObject.laps) ? fitDataObject.laps : [];
const sessions = Array.isArray(fitDataObject.sessions)
? fitDataObject.sessions.filter((session) => session && typeof session === 'object')
: [];
if (sessions.length) {
fitDataObject.sessions = sessions.map((session) => (Object.assign(Object.assign({}, session), { laps: Array.isArray(session === null || session === void 0 ? void 0 : session.laps) ? session.laps : [] })));
return;
}
const synthesizedSession = this.buildSessionFromTopLevelMessages(fitDataObject);
fitDataObject.sessions = synthesizedSession ? [synthesizedSession] : [];
}
static isValidDate(value) {
return value instanceof Date && Number.isFinite(value.getTime());
}
static getValidLapTimestamp(sessionLapObject, startDate, lastRecordTimestamp) {
const timestamp = sessionLapObject === null || sessionLapObject === void 0 ? void 0 : sessionLapObject.timestamp;
if (!this.isValidDate(timestamp)) {
return null;
}
if (startDate && timestamp.getTime() < startDate.getTime()) {
return null;
}
if (startDate &&
(0, helpers_1.isNumber)(sessionLapObject === null || sessionLapObject === void 0 ? void 0 : sessionLapObject.total_elapsed_time) &&
sessionLapObject.total_elapsed_time > 0 &&
timestamp.getTime() <= startDate.getTime()) {
return null;
}
if (lastRecordTimestamp && timestamp.getTime() < lastRecordTimestamp.getTime()) {
return null;
}
return timestamp;
}
static normalizeElapsedTimeForResolvedDates(object, startDate, endDate, options) {
if (!startDate || !endDate || endDate <= startDate) {
return object;
}
const maxActivityDurationSeconds = options.maxActivityDurationDays * 24 * 60 * 60;
const resolvedElapsedTime = (endDate.getTime() - startDate.getTime()) / 1000;
if (!(0, helpers_1.isNumber)(object === null || object === void 0 ? void 0 : object.total_elapsed_time) ||
object.total_elapsed_time <= maxActivityDurationSeconds ||
resolvedElapsedTime > maxActivityDurationSeconds) {
return object;
}
return Object.assign(Object.assign(Object.assign({}, object), { start_time: startDate, timestamp: endDate, total_elapsed_time: resolvedElapsedTime }), ((0, helpers_1.isNumber)(object.total_timer_time) && object.total_timer_time > resolvedElapsedTime
? { total_timer_time: resolvedElapsedTime }
: {}));
}
static resolveInvalidLapDates(sessionLapObject, activity, startDate, endDate, firstRecordTimestamp, lastRecordTimestamp) {
var _a;
const rawStartDate = this.getDateFromValue(sessionLapObject === null || sessionLapObject === void 0 ? void 0 : sessionLapObject.start_time);
const rawEndDate = this.getDateFromValue(sessionLapObject === null || sessionLapObject === void 0 ? void 0 : sessionLapObject.timestamp);
const totalElapsedTime = (_a = this.getNumericValue(sessionLapObject === null || sessionLapObject === void 0 ? void 0 : sessionLapObject.total_elapsed_time)) !== null && _a !== void 0 ? _a : this.getNumericValue(sessionLapObject === null || sessionLapObject === void 0 ? void 0 : sessionLapObject.total_timer_time);
if (rawStartDate && rawEndDate && rawEndDate <= rawStartDate) {
const recoveredStartDate = firstRecordTimestamp ||
(activity.startDate < rawEndDate ? activity.startDate : null) ||
(totalElapsedTime && totalElapsedTime > 0
? new Date(rawEndDate.getTime() - totalElapsedTime * 1000)
: null);
if (recoveredStartDate && recoveredStartDate < rawEndDate) {
return {
startDate: recoveredStartDate,
endDate: rawEndDate
};
}
}
if (startDate && totalElapsedTime && totalElapsedTime > 0) {
const recoveredEndDate = new Date(startDate.getTime() + totalElapsedTime * 1000);
if (recoveredEndDate > startDate) {
return {
startDate,
endDate: recoveredEndDate
};
}
}
if (endDate && totalElapsedTime && totalElapsedTime > 0) {
const recoveredStartDate = new Date(endDate.getTime() - totalElapsedTime * 1000);
if (recoveredStartDate < endDate) {
return {
startDate: recoveredStartDate,
endDate
};
}
}
const fallbackStartDate = firstRecordTimestamp ||
(activity.startDate && (!endDate || activity.startDate < endDate) ? activity.startDate : null) ||
startDate ||
rawStartDate;
const fallbackEndDate = lastRecordTimestamp ||
(rawEndDate && fallbackStartDate && rawEndDate > fallbackStartDate ? rawEndDate : null) ||
(activity.endDate && fallbackStartDate && activity.endDate > fallbackStartDate ? activity.endDate : null) ||
endDate ||
rawEndDate;
if (fallbackStartDate && fallbackEndDate && fallbackEndDate > fallbackStartDate) {
return {
startDate: fallbackStartDate,
endDate: fallbackEndDate
};
}
return {
startDate: activity.startDate,
endDate: activity.endDate > activity.startDate
? activity.endDate
: new Date(activity.startDate.getTime() + 1000)
};
}
static getLapFromSessionLapObject(sessionLapObject, activity, lapIndex, options) {
var _a, _b, _c, _d;
this.swapTimesIfRequired(sessionLapObject);
const firstRecordTimestamp = ((_b = (_a = sessionLapObject === null || sessionLapObject === void 0 ? void 0 : sessionLapObject.records) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.timestamp) || null;
const lastRecordTimestamp = ((_c = sessionLapObject === null || sessionLapObject === void 0 ? void 0 : sessionLapObject.records) === null || _c === void 0 ? void 0 : _c.length) > 0
? (_d = sessionLapObject.records[sessionLapObject.records.length - 1]) === null || _d === void 0 ? void 0 : _d.timestamp
: null;
const startDate = (sessionLapObject === null || sessionLapObject === void 0 ? void 0 : sessionLapObject.start_time) ||
firstRecordTimestamp ||
((sessionLapObject === null || sessionLapObject === void 0 ? void 0 : sessionLapObject.total_elapsed_time) &&
new Date(sessionLapObject.timestamp.getTime() - sessionLapObject.total_elapsed_time * 1000)) ||
null;
const validTimestamp = this.getValidLapTimestamp(sessionLapObject, startDate, lastRecordTimestamp);
const endDate = validTimestamp ||
lastRecordTimestamp ||
(sessionLapObject.start_time &&
sessionLapObject.total_elapsed_time &&
new Date(sessionLapObject.start_time.getTime() + sessionLapObject.total_elapsed_time * 1000)) ||
null;
const resolvedDates = !startDate || !endDate || endDate <= startDate
? this.resolveInvalidLapDates(sessionLapObject, activity, startDate, endDate, firstRecordTimestamp, lastRecordTimestamp)
: { startDate, endDate };
const lap = new lap_1.Lap(resolvedDates.startDate, resolvedDates.endDate, // Some dont have a timestamp
lapIndex + 1, lap_types_1.LapTypes[sessionLapObject.lap_trigger] || lap_types_1.LapTypes.unknown);
// Set the calories
if (sessionLapObject.total_calories) {
lap.addStat(new data_energy_1.DataEnergy(sessionLapObject.total_calories));
}
// Add stats to the lap
const normalizedSessionLapObject = this.normalizeElapsedTimeForResolvedDates(sessionLapObject, resolvedDates.startDate, resolvedDates.endDate, options);
this.getStatsFromObject(normalizedSessionLapObject, activity, true).forEach(stat => lap.addStat(stat));
return lap;
}
static getActivityFromSessionObject(sessionObject, fitDataObject, options) {
var _a;
/**
* Provides start/end date based on records available in given session object first, then in parent fit object
*/
const getStartEndDatesFromRecords = (sessionObject, fitDataObject) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
let startDate = null;
let endDate = null;
// Try to get from session first
if (((_a = sessionObject === null || sessionObject === void 0 ? void 0 : sessionObject.laps) === null || _a === void 0 ? void 0 : _a.length) > 0 && // Current session has laps
((_b = sessionObject.laps[0].records) === null || _b === void 0 ? void 0 : _b.length) > 0 && // Current session has records in first lap
((_c = sessionObject.laps[sessionObject.laps.length - 1].records) === null || _c === void 0 ? void 0 : _c.length) > 0 // Current session has records in last lap
) {
const firstLapRecords = sessionObject.laps[0].records;
const firstRecordTimeStamp = (_d = firstLapRecords[0]) === null || _d === void 0 ? void 0 : _d.timestamp;
if (firstRecordTimeStamp) {
startDate = firstRecordTimeStamp;
}
const lastLapRecords = sessionObject.laps[sessionObject.laps.length - 1].records;
const lastRecordTimeStamp = (_e = lastLapRecords[lastLapRecords.length - 1]) === null || _e === void 0 ? void 0 : _e.timestamp;
if (lastRecordTimeStamp) {
endDate = lastRecordTimeStamp;
}
}
// Then from parent fit object first
if ((!startDate || !endDate) && ((_f = fitDataObject.records) === null || _f === void 0 ? void 0 : _f.length)) {
startDate = ((_g = fitDataObject.records[0]) === null || _g === void 0 ? void 0 : _g.timestamp) || null;
endDate = ((_h = fitDataObject.records[fitDataObject.records.length - 1]) === null || _h === void 0 ? void 0 : _h.timestamp) || null;
}
return [startDate, endDate];
};
// For some unknown reasons... the fit provided total_timer_time & total_elapsed_time could be inverted..
// Just invert fields if that's the case
this.swapTimesIfRequired(sessionObject);
// Start finding out total elapsed time from fit dedicated fields
const totalElapsedTime = sessionObject.total_elapsed_time || sessionObject.total_timer_time || 0;
const [recordStartDate, recordEndDate] = getStartEndDatesFromRecords(sessionObject, fitDataObject);
// Pick start/end date values
let startDate = sessionObject.start_time || recordStartDate || null;
let endDate = sessionObject.timestamp ||
(startDate && totalElapsedTime && new Date(startDate.getTime() + totalElapsedTime * 1000)) ||
recordEndDate ||
null;
// Some fit files have wrong dates for session.timestamp && session.start_time and those miss an elapsed time
// Get dates from records in that case
if (!totalElapsedTime // Elapsed time missing
) {
if (recordStartDate) {
startDate = recordStartDate;
}
if (recordEndDate) {
endDate = recordEndDate;
}
}
// Now verify the start/end date compliance,
// If for some reason this happens, get from records too
if (startDate && endDate && endDate <= startDate) {
if (recordStartDate) {
startDate = recordStartDate;
}
if (recordEndDate) {
endDate = recordEndDate;
}
}
if (startDate && endDate && endDate <= startDate && totalElapsedTime > 0) {
endDate = new Date(startDate.getTime() + totalElapsedTime * 1000);
}
const elapsedTimeFromDates = (+endDate - +startDate) / 1000; // Get elapsed calculated from dates
const maxActivityDurationMs = options.maxActivityDurationDays * 24 * 60 * 60 * 1000;
const recordDurationMs = recordStartDate && recordEndDate ? +recordEndDate - +recordStartDate : 0;
const hasReliableRecordDates = recordDurationMs > 0 && recordDurationMs <= maxActivityDurationMs;
const totalElapsedTimeExceedsRecordDuration = totalElapsedTime > 0 &&
recordDurationMs > 0 &&
totalElapsedTime / (recordDurationMs / 1000) > INVALID_DATES_ELAPSED_TIME_RATIO_THRESHOLD;
// Test case where sometime elapsed time (calculated from dates) can be very high comparing to computed totalElapsedTime
// If elapsed time (calculated from dates) is detected as "strange" then use elapsed time from fit fields instead
// @see test case implying 'fixtures/rides/fit/5319808632.fit' fit file
if (totalElapsedTime && elapsedTimeFromDates / totalElapsedTime > INVALID_DATES_ELAPSED_TIME_RATIO_THRESHOLD) {
if (totalElapsedTime) {
endDate = new Date(startDate.getTime() + totalElapsedTime * 1000);
}
}
// Re-test potential updated activity duration against max accepted duration
if (+endDate - +startDate > maxActivityDurationMs) {
if (recordStartDate &&
recordEndDate &&
hasReliableRecordDates &&
(!totalElapsedTime || totalElapsedTimeExceedsRecordDuration)) {
startDate = recordStartDate;
endDate = recordEndDate;
}
else {
endDate = new Date(startDate.getTime() + totalElapsedTime * 1000);
}
}
if (!startDate || !endDate) {
throw new parsing_event_lib_error_1.ParsingEventLibError('Cannot parse start and end dates');
}
else {
// Create an activity
const activity = new activity_1.Activity(startDate, endDate, this.getActivityTypeFromSessionObject(sessionObject), this.getCreatorFromFitDataObject(fitDataObject), options);
const normalizedSessionObject = this.normalizeElapsedTimeForResolvedDates(Object.assign(Object.assign({}, sessionObject), { threshold_power: (0, helpers_1.isNumberOrString)(sessionObject === null || sessionObject === void 0 ? void 0 : sessionObject.threshold_power) ||
!(0, helpers_1.isNumberOrString)((_a = fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.zones_target) === null || _a === void 0 ? void 0 : _a.functional_threshold_power)
? sessionObject === null || sessionObject === void 0 ? void 0 : sessionObject.threshold_power
: fitDataObject.zones_target.functional_threshold_power }), startDate, endDate, options);
// Set the activity stats
this.getStatsFromObject(normalizedSessionObject, activity, false).forEach(stat => activity.addStat(stat));
// Check for User Profile
if (fitDataObject.user_profile) {
const userProfile = fitDataObject.user_profile;
if ((0, helpers_1.isNumberOrString)(userProfile.weight)) {
activity.addStat(new data_weight_1.DataWeight(userProfile.weight));
}
if ((0, helpers_1.isNumberOrString)(userProfile.height)) {
activity.addStat(new data_height_1.DataHeight(userProfile.height));
}
if ((0, helpers_1.isNumberOrString)(userProfile.age)) {
activity.addStat(new data_age_1.DataAge(userProfile.age));
}
const gender = this.getStringValue(userProfile.gender);
if (gender !== null) {
activity.addStat(new data_gender_1.DataGender(gender));
}
}
// Check for Activity Metrics (VO2Max etc)
if (fitDataObject.activity_metrics && fitDataObject.activity_metrics.length > 0) {
// Try to find matching sport or use the first one if only one session
const activityMetric = fitDataObject.activity_metrics.find((am) => am.sport === sessionObject.sport || am.sport === activity.type) || fitDataObject.activity_metrics[0];
if (activityMetric) {
const metricVO2Max = this.getFirstValidVO2MaxValue(activityMetric.vo2_max, activityMetric.first_vo2_max);
const metricRecoveryTime = this.getPositiveNumericValue(activityMetric.recovery_time);
if (metricVO2Max !== null && !activity.getStat(data_vo2_max_1.DataVO2Max.type)) {
activity.addStat(new data_vo2_max_1.DataVO2Max(metricVO2Max));
}
if (metricRecoveryTime !== null && !activity.getStat(data_recovery_time_1.DataRecoveryTime.type)) {
// FIT activity_metrics.recovery_time is in minutes. DataRecoveryTime is stored in seconds.
activity.addStat(new data_recovery_time_1.DataRecoveryTime(metricRecoveryTime * 60));
}
if ((0, helpers_1.isNumberOrString)(activityMetric.anaerobic_training_effect) &&
!activity.getStat(data_anaerobic_training_effect_1.DataAnaerobicTrainingEffect.type)) {
activity.addStat(new data_anaerobic_training_effect_1.DataAnaerobicTrainingEffect(activityMetric.anaerobic_training_effect));
}
if ((0, helpers_1.isNumberOrString)(activityMetric.aerobic_training_effect) &&
!activity.getStat(data_aerobic_training_effect_1.DataAerobicTrainingEffect.type)) {
activity.addStat(new data_aerobic_training_effect_1.DataAerobicTrainingEffect(activityMetric.aerobic_training_effect));
}
}
}
const userMetric = this.getUserMetricForSession(fitDataObject.user_metrics, sessionObject);
const userMetricVO2Max = this.getFirstValidVO2MaxValue(userMetric === null || userMetric === void 0 ? void 0 : userMetric.vo2_max, userMetric === null || userMetric === void 0 ? void 0 : userMetric.first_vo2_max);
if (userMetricVO2Max !== null && !activity.getStat(data_vo2_max_1.DataVO2Max.type)) {
activity.addStat(new data_vo2_max_1.DataVO2Max(userMetricVO2Max));
}
// Check for HR zone durations from time_in_zone messages
// This is an alternative source when sessionObject.time_in_hr_zone is not available
if (fitDataObject.time_in_zone && fitDataObject.time_in_zone.length) {
// Find session-level time_in_zone message (reference_mesg = 18 for session, reference_index = 0 for first session)
const manufacturer = (fitDataObject.file_ids && fitDataObject.file_ids[0] && fitDataObject.file_ids[0].manufacturer) || '';
const zoneIndexOffset = manufacturer === 'garmin' ? 1 : 0;
const sessionTimeInZone = fitDataObject.time_in_zone.find((z) => z.reference_mesg === 18 && (z.reference_index === 0 || z.reference_index === undefined));
if (sessionTimeInZone &&
sessionTimeInZone.time_in_hr_zone &&
Array.isArray(sessionTimeInZone.time_in_hr_zone)) {
const hrZones = sessionTimeInZone.time_in_hr_zone;
const hrZoneBoundaries = sessionTimeInZone.hr_zone_high_boundary;
// Only add zone duration stats if not already set from sessionObject.time_in_hr_zone
if (!activity.getStat(data_heart_rate_zone_one_duration_1.DataHeartRateZoneOneDuration.type)) {
if (hrZones[0 + zoneIndexOffset] !== undefined && hrZones[0 + zoneIndexOffset] !== null) {
activity.addStat(new data_heart_rate_zone_one_duration_1.DataHeartRateZoneOneDuration(hrZones[0 + zoneIndexOffset]));
}
if (hrZones[1 + zoneIndexOffset] !== undefined && hrZones[1 + zoneIndexOffset] !== null) {
activity.addStat(new data_heart_rate_zone_two_duration_1.DataHeartRateZoneTwoDuration(hrZones[1 + zoneIndexOffset]));
}
if (hrZones[2 + zoneIndexOffset] !== undefined && hrZones[2 + zoneIndexOffset] !== null) {
activity.addStat(new data_heart_rate_zone_three_duration_1.DataHeartRateZoneThreeDuration(hrZones[2 + zoneIndexOffset]));
}
if (hrZones[3 + zoneIndexOffset] !== undefined && hrZones[3 + zoneIndexOffset] !== null) {
activity.addStat(new data_heart_rate_zone_four_duration_1.DataHeartRateZoneFourDuration(hrZones[3 + zoneIndexOffset]));
}
if (hrZones[4 + zoneIndexOffset] !== undefined && hrZones[4 + zoneIndexOffset] !== null) {
activity.addStat(new data_heart_rate_zone_five_duration_1.DataHeartRateZoneFiveDuration(hrZones[4 + zoneIndexOffset]));
}
if (hrZones[5 + zoneIndexOffset] !== undefined && hrZones[5 + zoneIndexOffset] !== null) {
activity.addStat(new data_heart_rate_zone_six_duration_1.DataHeartRateZoneSixDuration(hrZones[5 + zoneIndexOffset]));
}
if (hrZones[6 + zoneIndexOffset] !== undefined && hrZones[6 + zoneIndexOffset] !== null) {
activity.addStat(new data_heart_rate_zone_seven_duration_1.DataHeartRateZoneSevenDuration(hrZones[6 + zoneIndexOffset]));
}
}
// Check if IntensityZones for HR is already set, if not create one with boundaries
const existingHrZones = activity.intensityZones.find(iz => iz.type === data_heart_rate_1.DataHeartRate.type);
if (!existingHrZones && Array.isArray(hrZoneBoundaries) && hrZoneBoundaries.length > 0) {
const hrIntensityZones = this.createIntensityZonesFromFitZones(data_heart_rate_1.DataHeartRate.type, hrZones, zoneIndexOffset, hrZoneBoundaries, 255);
activity.intensityZones.push(hrIntensityZones);
}
else if (existingHrZones && Array.isArray(hrZoneBoundaries) && hrZoneBoundaries.length > 0) {
this.applyFitZoneHighBoundaries(existingHrZones, hrZoneBoundaries, zoneIndexOffset, 255);
}
}
if (sessionTimeInZone &&
sessionTimeInZone.time_in_power_zone &&
Array.isArray(sessionTimeInZone.time_in_power_zone)) {
const powerZones = sessionTimeInZone.time_in_power_zone;
const powerZoneBoundaries = sessionTimeInZone.power_zone_high_boundary;
// Only add zone duration stats if not already set
if (!activity.getStat(data_power_zone_one_duration_1.DataPowerZoneOneDuration.type)) {
if ((0, helpers_1.isNumber)(powerZones[0 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_one_duration_1.DataPowerZoneOneDuration(powerZones[0 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(powerZones[1 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_two_duration_1.DataPowerZoneTwoDuration(powerZones[1 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(powerZones[2 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_three_duration_1.DataPowerZoneThreeDuration(powerZones[2 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(powerZones[3 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_four_duration_1.DataPowerZoneFourDuration(powerZones[3 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(powerZones[4 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_five_duration_1.DataPowerZoneFiveDuration(powerZones[4 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(powerZones[5 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_six_duration_1.DataPowerZoneSixDuration(powerZones[5 + zoneIndexOffset]));
}
if ((0, helpers_1.isNumber)(powerZones[6 + zoneIndexOffset])) {
activity.addStat(new data_power_zone_seven_duration_1.DataPowerZoneSevenDuration(powerZones[6 + zoneIndexOffset]));
}
}
// Check if IntensityZones for Power is already set, if not create one with boundaries
const existingPowerZones = activity.intensityZones.find(iz => iz.type === data_power_1.DataPower.type);
if (!existingPowerZones && Array.isArray(powerZones) && powerZones.length > 0) {
const powerIntensityZones = this.createIntensityZonesFromFitZones(data_power_1.DataPower.type, powerZones, zoneIndexOffset, powerZoneBoundaries, 65535);
activity.intensityZones.push(powerIntensityZones);
}
else if (existingPowerZones && Array.isArray(powerZoneBoundaries) && powerZoneBoundaries.length > 0) {
this.applyFitZoneHighBoundaries(existingPowerZones, powerZoneBoundaries, zoneIndexOffset, 65535);
}
}
}
this.addSwimLengthsFromSessionObject(activity, sessionObject, fitDataObject);
return activity;
}
}
static createIntensityZonesFromFitZones(type, durations, zoneIndexOffset, highBoundaries, invalidHighBoundaryValue) {
const intensityZones = new intensity_zones_1.IntensityZones(type);
for (let zoneNumber = 1; zoneNumber <= 7; zoneNumber++) {
const duration = durations[zoneNumber - 1 + zoneIndexOffset];
this.setIntensityZoneDuration(intensityZones, zoneNumber, (0, helpers_1.isNumber)(duration) ? duration : 0);
}
this.applyFitZoneHighBoundaries(intensityZones, highBoundaries, zoneIndexOffset, invalidHighBoundaryValue, true);
return intensityZones;
}
static applyFitZoneHighBoundaries(intensityZones, highBoundaries, zoneIndexOffset, invalidHighBoundaryValue, overwrite = false) {
if (!Array.isArray(highBoundaries) || highBoundaries.length === 0) {
return;
}
for (let zoneNumber = 1; zoneNumber <= 7; zoneNumber++) {
const sourceBoundaryIndex = zoneNumber + zoneIndexOffset - 2;
if (sourceBoundaryIndex < 0) {
continue;
}
const lowerLimit = highBoundaries[sourceBoundaryIndex];
if (!(0, helpers_1.isNumber)(lowerLimit) || lowerLimit === invalidHighBoundaryValue) {
continue;
}
if (!overwrite && this.hasIntensityZoneLowerLimit(intensityZones, zoneNumber)) {
continue;
}
this.setIntensityZoneLowerLimit(intensityZones, zoneNumber, lowerLimit);
}
}
static setIntensityZoneDuration(intensityZones, zoneNumber, duration) {
switch (zoneNumber) {
case 1:
intensityZones.zone1Duration = duration;
break;
case 2:
intensityZones.zone2Duration = duration;
break;
case 3:
intensityZones.zone3Duration = duration;
break;
case 4:
intensityZones.zone4Duration = duration;
break;
case 5:
intensityZones.zone5Duration = duration;
break;
case 6:
intensityZones.zone6Duration = duration;
break;
case 7:
intensityZones.zone7Duration = duration;
break;
default:
break;
}
}
static setIntensityZoneLowerLimit(intensityZones, zoneNumber, lowerLimit) {
switch (zoneNumber) {
case 1:
intensityZones.zone1LowerLimit = lowerLimit;
break;
case 2:
intensityZones.zone2LowerLimit = lowerLimit;
break;
case 3:
intensityZones.zone3LowerLimit = lowerLimit;
break;
case 4:
intensityZones.zone4LowerLimit = lowerLimit;
break;
case 5:
intensityZones.zone5LowerLimit = lowerLimit;
break;
case 6:
intensityZones.zone6LowerLimit = lowerLimit;
break;
case 7:
intensityZones.zone7LowerLimit = lowerLimit;
break;
default:
break;
}
}
static hasIntensityZoneLowerLimit(intensityZones, zoneNumber) {
switch (zoneNumber) {
case 1:
return (0, helpers_1.isNumber)(intensityZones.zone1LowerLimit);
case 2:
return (0, helpers_1.isNumber)(intensityZones.zone2LowerLimit);
case 3:
return (0, helpers_1.isNumber)(intensityZones.zone3LowerLimit);
case 4:
return (0, helpers_1.isNumber)(intensityZones.zone4LowerLimit);
case 5:
return (0, helpers_1.isNumber)(intensityZones.zone5LowerLimit);
case 6:
return (0, helpers_1.isNumber)(intensityZones.zone6LowerLimit);
case 7:
return (0, helpers_1.isNumber)(intensityZones.zone7LowerLimit);
default:
return false;
}
}
/**
* For some unknown reasons... the fit provided total_timer_time & total_elapsed_time could be inverted..
* Just swap them if that's the case
*/
static swapTimesIfRequired(object) {
if ((0, helpers_1.isNumber)(object.total_timer_time) &&
(0, helpers_1.isNumber)(object.total_elapsed_time) &&
object.total_elapsed_time < object.total_timer_time) {
const timeDelta = object.total_timer_time - object.total_elapsed_time;
// Small inversions are usually a rounding artifact from vendor exporters.
// Keep semantics consistent (timer <= elapsed) by clamping without swapping.
if (timeDelta <= TIMER_ELAPSED_ROUNDING_TOLERANCE_SECONDS) {
object.total_timer_time = object.total_elapsed_time;
return;
}
const realTimerTime = object.total_elapsed_time;
const realElapsedTime = object.total_timer_time;
object.total_timer_time = realTimerTime;
object.total_elapsed_time = realElapsedTime;
}
}
static getActivityTypeByKey(value) {
return activity_types_1.ActivityTypesHelper.resolveActivityType(value);
}
static resolveGarminProfileName(value, map) {
if (!(0, helpers_1.isNumberOrString)(value)) {
return null;
}
if (typeof value === 'number') {
return map[value] || null;
}
const trimmed = value.trim();
if (!trimmed) {
return null;
}
if (/^\d+$/.test(trimmed)) {
return map[parseInt(trimmed, 10)] || null;
}
return trimmed;
}
static getPositiveNumericValue(value) {
if (!(0, helpers_1.isNumberOrString)(value)) {
return null;
}
const numericValue = Number(value);
if (!Number.isFinite(numericValue) || numericValue <= 0) {
return null;
}
return numericValue;
}
static getFirstNumericValue(...values) {
for (const value of values) {
const numericValue = this.getNumericValue(value);
if (numericValue !== null) {
return numericValue;
}
}
return null;
}
static getFitEnumValue(value) {
var _a;
return ((_a = this.getStringValue(value)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || null;
}
static getPoolLengthUnit(value) {
switch (this.getFitEnumValue(value)) {
case '0':
case 'metric':
case 'meter':
case 'meters':
case 'm':
return 'metric';
case '1':
case 'statute':
case 'yard':
case 'yards':
case 'yd':
return 'statute';
default:
return null;
}
}
static getSwimLengthType(value) {
switch (this.getFitEnumValue(value)) {
case '0':
case 'idle':
return 'idle';
case '1':
case 'active':
return 'active';
default:
return this.getStringValue(value);
}
}
static getSwimStroke(value) {
switch (this.getFitEnumValue(value)) {
case '0':
case 'freestyle':
return 'freestyle';
case '1':
case 'backstroke':
return 'backstroke';
case '2':
case 'breaststroke':
return 'breaststroke';
case '3':
case 'butterfly':
return 'butterfly';
case '4':
case 'drill':
return 'drill';
case '5':
case 'mixed':
return 'mixed';
case '6':
case 'im':
return 'im';
default:
return this.getStringValue(value);
}
}
static getPoolLengthMeters(object) {
const poolLength = this.getNumericValue(object === null || object === void 0 ? void 0 : object.pool_length);
if (poolLength === null) {
return null;
}
if (this.getPoolLengthUnit(object === null || object === void 0 ? void 0 : object.pool_length_unit) === 'statute') {
return poolLength * 0.9144;
}
return poolLength;
}
static getSwimLengthEndDate(length, startDate) {
const timerTime = this.getNumericValue(length === null || length === void 0 ? void 0 : length.total_timer_time);
const elapsedTime = this.getNumericValue(length === null || length === void 0 ? void 0 : length.total_elapsed_time);
const duration = timerTime !== null && timerTime !== void 0 ? timerTime : elapsedTime;
if (duration !== null) {
return new Date(startDate.getTime() + duration * 1000);
}
return this.getDateFromValue(length === null || length === void 0 ? void 0 : length.timestamp) || startDate;
}
static getLapWindow(lap) {
var _a;
const startDate = this.getDateFromValue(lap === null || lap === void 0 ? void 0 : lap.start_time);
const elapsedTime = (_a = this.getNumericValue(lap === null || lap === void 0 ? void 0 : lap.total_elapsed_time)) !== null && _a !== void 0 ? _a : this.getNumericValue(lap === null || lap === void 0 ? void 0 : lap.total_timer_time);
const endDate = this.getDateFromValue(lap === null || lap === void 0 ? void 0 : lap.timestamp) ||
(startDate && elapsedTime !== null ? new Date(startDate.getTime() + elapsedTime * 1000) : null);
return { startDate, endDate };
}
static getLengthLapIndex(length, laps) {
const lengthStartDate = this.getDateFromValue(length === null || length === void 0 ? void 0 : length.start_time) || this.getDateFromValue(length === null || length === void 0 ? void 0 : length.timestamp);
if (!lengthStartDate) {
return null;
}
const lengthTime = lengthStartDate.getTime();
const lapIndex = laps.findIndex(lap => {
const { startDate, endDate } = this.getLapWindow(lap);
return !!startDate && !!endDate && lengthTime >= startDate.getTime() && lengthTime < endDate.getTime();
});
return lapIndex === -1 ? null : lapIndex + 1;
}
static getSwimLengthDistance(length, lap, lapIndex, poolLength, activeLengthCountByLapIndex) {
const type = this.getSwimLengthType(length === null || length === void 0 ? void 0 : length.length_type);
if (type !== 'active') {
return null;
}
const explicitDistance = this.getNumericValue(length === null || length === void 0 ? void 0 : length.total_distance);
if (explicitDistance !== null) {
return explicitDistance;
}
if (poolLength !== null) {
return poolLength;
}
if (!lap || lapIndex === null) {
return null;
}
const activeLengthCount = activeLengthCountByLapIndex.get(lapIndex);
const lapDistance = this.getNumericValue(lap === null || lap === void 0 ? void 0 : lap.total_distance);
if (!activeLengthCount || lapDistance === null) {
return null;
}
return lapDistance / activeLengthCount;
}
static addSwimLengthsFromSessionObject(activity, sessionObject, fitDataObject) {
var _a;
const sessionStartDate = this.getDateFromValue(sessionObject === null || sessionObject === void 0 ? void 0 : sessionObject.start_time);
const sessionElapsedTime = (_a = this.getNumericValue(sessionObject === null || sessionObject === void 0 ? void 0 : sessionObject.total_elapsed_time)) !== null && _a !== void 0 ? _a : this.getNumericValue(sessionObject === null || sessionObject === void 0 ? void 0 : sessionObject.total_timer_time);
const sessionEndDate = this.getDateFromValue(sessionObject === null || sessionObject === void 0 ? void 0 : sessionObject.timestamp) ||
(sessionStartDate && sessionElapsedTime !== null
? new Date(sessionStartDate.getTime() + sessionElapsedTime * 1000)
: null);
const rawLengths = Array.isArray(sessionObject === null || sessionObject === void 0 ? void 0 : sessionObject.lengths)
? sessionObject.lengths
: Array.isArray(fitDataObject === null || fitDataObject === void 0 ? void 0 : fitDataObject.lengths)
? fitDataObject.lengths
: [];
if (!rawLengths.length || !sessionStartDate || !sessionEndDate) {
return;
}
const lengths = rawLengths
.filter((length) => {
const lengthDate = this.getDateFromValue(length === null || length === void 0 ? void 0 : length.start_time) || this.getDateFromValue(length === null || length === void 0 ? void 0 : length.timestamp);
return (!!lengthDate &&
lengthDate.getTime() >= sessionStartDate.getTime() &&
lengthDate.getTime() < sessionEndDate.getTime());
})
.sort((a, b) => {
const aDate = this.getDateFromValue(a === null || a === void 0 ? void 0 : a.start_time) || this.getDateFromValue(a === null || a === void 0 ? void 0 : a.timestamp);
const bDate = this.getDateFromValue(b === null || b === void 0 ? void 0 : b.start_time) || this.getDateFromValue(b === null || b === void 0 ? void 0 : b.timestamp);
return ((aDate === null || aDate === void 0 ? void 0 : aDate.getTime()) || 0) - ((bDate === null || bDate === void 0 ? void 0 : bDate.getTime()) || 0);
});
if (!lengths.length) {
return;
}
const laps = Array.isArray(sessionObject === null || sessionObject === void 0 ? void 0 : sessionObject.laps) ? sessionObject.laps : [];
const poolLength = this.getPoolLengthMeters(sessionObject);
const activeLengthCountByLapIndex = new Map();
const lapByIndex = new Map();
laps.forEach((lap, index) => {
const lapIndex = index + 1;
lapByIndex.set(lapIndex, lap);
const activeLengthCount = lengths.filter((length) => {
return (this.getLengthLapIndex(length, laps) === lapIndex && this.getSwimLengthType(length === null || length === void 0 ? void 0 : length.length_type) === 'active');
}).length;
activeLengthCountByLapIndex.set(lapIndex, activeLengthCount);
});
lengths.forEach((length, lengthIndex) => {
const startDate = this.getDateFromValue(length === null || length === void 0 ? void 0 : length.start_time) || this.getDateFromValue(length === null || length === void 0 ? void 0 : length.timestamp);
if (!startDate) {
return;
}
const lapIndex = this.getLengthLapIndex(length, laps);
const lap = lapIndex === null ? undefined : lapByIndex.get(lapIndex);
const timerTime = this.getNumericValue(length === null || length === void 0 ? void 0 : length.total_timer_time);
const elapsedTime = this.getNumericValue(length === null || length === void 0 ? void 0 : length.total_elapsed_time);
activity.addSwimLength(new swim_length_1.SwimLength({
index: lengthIndex + 1,
lapIndex,
startDate,
endDate: this.getSwimLengthEndDate(length, startDate),
type: this.getSwimLengthType(length === null || length === void 0 ? void 0 : length.length_type) || 'unknown',
stroke: this.getSwimStroke(length === null || length === void 0 ? void 0 : length.swim_stroke),
strokes: this.getNumericValue(length === null || length === void 0 ? void 0 : length.total_strokes),
elapsedTime: elapsedTime === null ? null : new data_duration_1.DataDuration(elapsedTime),
timerTime: timerTime === null ? null : new data_duration_1.DataDuration(timerTime),
distance: (() => {
const distance = this.getSwimLengthDistance(length, lap, lapIndex, poolLength, activeLengthCountByLapIndex);
return distance === null ? null : new data_distance_1.DataDistance(distance);
})(),
poolLength: poolLength === null ? null : new data_distance_1.DataDistance(poolLength),
avgSpeed: (() => {
const avgSpeed = this.getNumericValue(length === null || length === void 0 ? void 0 : length.avg_speed);
return avgSpeed === null ? null : new data_speed_1.DataSpeed(avgSpeed);
})(),
avgCadence: (() => {
const avgCadence = this.getFirstNumericValue(length === null || length === void 0 ? void 0 : length.avg_cadence, length === null || length === void 0 ? void 0 : length.avg_swimming_cadence);
return avgCadence === null ? null : new data_cadence_1.DataCadence(avgCadence);
})(),
avgHeartRate: (() => {
const avgHeartRate = this.getNumericValue(length === null || length === void 0 ? void 0 : length.avg_heart_rate);
return avgHeartRate === null ? null : new data_heart_rate_1.DataHeartRate(avgHeartRate);
})(),
maxHeartRate: (() => {
const maxHeartRate = this.getNumericValue(length === null || length === void 0 ? void 0 : length.max_heart_rate);
return maxHeartRate === null ? null : new data_heart_rate_1.DataHeartRate(maxHeartRate);
})(),
swolf: this.getNumericValue(length === null || length === void 0 ? void 0 : length.avg_swolf),
calories: (() => {
const calories = this.getNumericValue(length === null || length === void 0 ? void 0 : length.total_calories);
return calories === null ? null : new data_energy_1.DataEnergy(calories);
})()
}));
});
}
static getValidVO2MaxValue(value) {
const numericValue = this.getPositiveNumericValue(value);
if (numericValue === null || numericValue <= this.MIN_VALID_VO2_MAX || numericValue > this.MAX_VALID_VO2_MAX) {
return null;
}
return numericValue;
}
static getFirstValidVO2MaxValue(...values) {
for (const value of values) {
const numericValue = this.getValidVO2MaxValue(value);
if (numericValue !== null) {
return numericValue;
}
}
return null;
}
static getUserMetricForSession(userMetrics, sessionObject) {
if (!Array.isArray(userMetrics) || !userMetrics.length) {
return null;
}
const sessionStart = this.getDateFromValue(sessionObject === null || sessionObject === void 0 ? void 0 : sessionObject.start_time);
if (!sessionStart) {
return userMetrics[0];
}
return (userMetrics.find((metric) => {
const metricStart = this.getDateFromValue(metric === null || metric === void 0 ? void 0 : metric.start_of_activity) || this.getDateFromValue(metric === null || metric === void 0 ? void 0 : metric.timestamp);
return metricStart ? Math.abs(metricStart.getTime() - sessionStart.getTime()) <= 1000 : false;
}) || userMetrics[0]);
}
static getStringValue(value) {
var _a, _b, _c;
if (typeof value === 'string') {
const trimmedValue = value.trim();
return trimmedValue ? trimmedValue : null;
}
if (typeof value === 'number' && Number.isFinite(value)) {
return String(value);
}
if (value && typeof value === 'object') {
const enumValueCandidate = (_c = (_b = (_a = value.mappedValue) !== null && _a !== void 0 ? _a : value.value) !== null && _b !== void 0 ? _b : value.rawValue) !== null && _c !== void 0 ? _c : value.name;
if (enumValueCandidate !== undefined && enumValueCandidate !== value) {
return this.getStringValue(enumValueCandidate);
}
}
return null;
}
static getActivityTypeFromSessionObject(session) {
// FIT sport fields can be either profile IDs (number / numeric string) or already-resolved names.
// Example for the reported file: sport="rock_climbing", sub_sport=68 ("indoor_climbing").
const resolvedSport = this.resolveGarminProfileName(session.sport, garmin_profile_data_1.GarminSports);
const resolvedSubSportName = this.resolveGarminProfileName(session.sub_sport, garmin_profile_data_1.GarminSubSports);
const resolvedSubSport = resolvedSubSportName && resolvedSubSportName !== 'generic' ? resolvedSubSportName : null;
// 1. Try composite key: sport_subSport (e.g. "rock_climbing_indoor_climbing")
let activityType = null;
if (resolvedSport && resolvedSubSport) {
activityType = this.getActivityTypeByKey(`${resolvedSport}_${resolvedSubSport}`);
}
// 2. Try sub_sport name alone (e.g. "indoor_climbing" or "indoorClimbing")
if (!activityType || activityType === activity_types_1.ActivityTypes.unknown) {
if (resolvedSubSport) {
activityType = this.getActivityTypeByKey(resolvedSubSport);
}
}
// 3. sport_profile_name (user-defined profile, most specific after composite key)
// e.g. "ENDURO MTB" overrides generic "cycling" sport type.
if ((!activityType || activityType === activity_types_1.ActivityTypes.unknown) && (0, helpers_1.isNumberOrString)(session.sport_profile_name)) {
activityType =
this.getActivityTypeByKey(session.sport_profile_name) ||
this.getActivityTypeByKey(`${resolvedSport !== null && resolvedSport !== void 0 ? resolvedSport : session.sport}_${session.sport_profile_name}`);
}
// 4. Try sport name alone as last resort (e.g. "rock_climbing" or "rockClimbing")
if (!activityType || activityType === activity_types_1.ActivityTypes.unknown) {
if (resolvedSport) {
activityType = this.getActivityTypeByKey(resolvedSport);
}
}
if (activityType && activityType !== activity_types_1.ActivityTypes.unknown) {
return activityType;
}
const fallbackType = this.getActivityTypeByKey(session.sport_profile_name) ||
this.getActivityTypeByKey(resolvedSubSportName) ||
this.getActivityTypeByKey(resolvedSport) ||
this.getActivityTypeByKey(session.sport);
return fallbackType || activity_types_1.ActivityTypes.unknown;
}
// @todo move this to a mapper
static getStatsFromObject(object, activity, isLap) {
var _a, _b, _c;
const stats = [];
// For some unknown reasons... the fit provided total_timer_time & total_elapsed_time could be inverted..
// Just invert fields if that's the case
this.swapTimesIfRequired(object);
// TOTAL ELAPSED TIME on Object (activity, lap...)
let elapsedTime = 0;
if ((0, helpers_1.isNumber)(object.total_elapsed_time)) {
elapsedTime = object.total_elapsed_time;
}
else if ((object.timestamp - object.start_time) / 1000) {
elapsedTime = (object.timestamp - object.start_time) / 1000;
}
// 0 should be not included aha it's not legit to have a 0 for total timer time
// And that typically is a device error we should look at the samples
// Since start and end date are inclusive for sample size eg at time [0] there can be a value
if (!elapsedTime) {
elapsedTime = activity_utilities_1.ActivityUtilities.getDataLength(activity.startDate, activity.endDate) - 1;
}
const roundedElapsedTime = Math.round(elapsedTime * 100) / 100;
stats.push(new data_elapsed_time_1.DataElapsedTime(roundedElapsedTime));
// TOTAL TIMER TIME on Object (activity, lap...)
let timerTime = 0;
if ((0, helpers_1.isNumber)(object.total_timer_time)) {
timerTime = object.total_timer_time;
}
// If timer time is unknown then assign elapsedTime value
if (!timerTime) {
timerTime = elapsedTime;
}
const roundedTimerTime = Math.round(timerTime * 100) / 100;
stats.push(new data_duration_1.DataDuration(roundedTimerTime));
stats.push(new data_timer_time_1.DataTimerTime(roundedTimerTime));
// Moving TIME on Object (activity, lap...)
let movingTime = (0, helpers_1.isNumber)(object.total_moving_time) ? object.total_moving_time : 0;
if (!movingTime) {
if (object.lengths && object.lengths.length > 0) {
object.lengths.forEach((lengthVal) => {
if (lengthVal.length_type === 'active') {
movingTime += lengthVal.total_timer_time;
}
});
}
else if (object.records && object.records.length > 0) {
const speedThreshold = activity_types_1.ActivityTypesMoving.getSpeedThreshold(activity.type);
object.records.forEach((record, index) => {
var _a;
if ((record.speed || record.enhanced_speed) > speedThreshold) {
const previousRecordTime = ((_a = object.records[index - 1]) === null || _a === void 0 ? void 0 : _a.timestamp) || object.start_time || object.records[0].timestamp;
movingTime += (record.timestamp.getTime() - previousRecordTime.getTime()) / 1000;
}
});
}
}
if (isLap) {
// In case we moved (distance > 0) & moving time is invalid, then set it to timer time value
if (object.total_distance > 0 && (!movingTime || movingTime > timerTime)) {
movingTime = timerTime;
}
}
// Append moving stat only if moving time has been detected
// We need that to compute total global moving time later
if (movingTime > 0) {
stats.push(new data_moving_time_1.DataMovingTime(Math.round(movingTime * 100) / 100));
}
// Pause TIME on Object (activity, lap...)
const pause = elapsedTime > timerTime ? Math.round((elapsedTime - timerTime) * 100) / 100 : 0;
stats.push(new data_pause_1.DataPause(pause));
const getStatValue = (obj, keys) => {
for (const key of keys) {
if ((0, helpers_1.isNumberOrString)(obj[key])) {
return obj[key];
}
}
return null;
};
const avgSpeed = getStatValue(object, ['enhanced_avg_speed', 'EnhancedAvgSpeed', 'avg_speed', 'AvgSpeed']);
const totalDistance = getStatValue(object, ['total_distance', 'TotalDistance']);
// Assign is active lap status
stats.push(new data_active_lap_1.DataActiveLap(!!(totalDistance || avgSpeed)));
if (totalDistance !== null) {
stats.push(new data_distance_1.DataDistance(totalDistance));
}
else {
stats.push(new data_distance_1.DataDistance(0));
}
// Heart Rate
if ((0, helpers_1.isNumberOrString)(object.avg_heart_rate)) {
stats.push(new data_heart_rate_avg_1.DataHeartRateAvg(object.avg_heart_rate));
}
if ((0, helpers_1.isNumberOrString)(object.min_heart_rate)) {
stats.push(new data_heart_rate_min_1.DataHeartRateMin(object.min_heart_rate));
}
if ((0, helpers_1.isNumberOrString)(object.max_heart_rate)) {
stats.push(new data_heart_rate_max_1.DataHeartRateMax(object.max_heart_rate));
}
// Cadence
const cyclingVO2Max = this.getValidVO2MaxValue(object.vo2_max_cycling);
if (cyclingVO2Max !== null) {
stats.push(new data_vo2_max_1.DataVO2Max(cyclingVO2Max));
}
if ((0, helpers_1.isNumberOrString)(object.avg_cadence)) {
stats.push(new data_cadence_avg_1.DataCadenceAvg(object.avg_cadence));
}
if ((0, helpers_1.isNumberOrString)(object.min_cadence)) {
stats.push(new data_cadence_min_1.DataCadenceMin(object.min_cadence));
}
if ((0, helpers_1.isNumberOrString)(object.max_cadence)) {
stats.push(new data_cadence_max_1.DataCadenceMax(object.max_cadence));
}
// Power
if ((0, helpers_1.isNumberOrString)(object.avg_power)) {
stats.push(new data_power_avg_1.DataPowerAvg(object.avg_power));
}
// Grit & Flow
if ((0, helpers_1.isNumberOrString)(object.total_flow)) {
stats.push(new data_total_flow_1.DataTotalFlow(object.total_flow));
}
if ((0, helpers_1.isNumberOrString)(object.avg_grit)) {
stats.push(new data_avg_grit_1.DataAvgGrit(object.avg_grit));
}
if ((0, helpers_1.isNumberOrString)(object.avg_flow)) {
stats.push(new data_avg_flow_1.DataAvgFlow(object.avg_flow));
}
if ((0, helpers_1.isNumberOrString)(object.min_power)) {
stats.push(new data_power_min_1.DataPowerMin(object.min_power));
}
if ((0, helpers_1.isNumberOrString)(object.max_power)) {
stats.push(new data_power_max_1.DataPowerMax(object.max_power));
}
if (Number.isFinite(object.normalized_power)) {
stats.push(new data_power_normalized_1.DataPowerNormalized(object.normalized_power));
}
if (Number.isFinite(object.intensity_factor)) {
stats.push(new data_power_intensity_factor_1.DataPowerIntensityFactor(object.intensity_factor));
}
if (Number.isFinite(object.training_stress_score)) {
stats.push(new data_training_stress_score_1.DataTrainingStressScore(object.training_stress_score));
}
if (Number.isFinite(object.total_work)) {
stats.push(new data_power_work_1.DataPowerWork(Math.round(object.total_work / 1000)));
}
if (!isLap && (0, helpers_1.isNumberOrString)(object.threshold_power)) {
const importedFTP = Number(object.threshold_power);
if (Number.isFinite(importedFTP) && importedFTP > 0) {
stats.push(new data_ftp_1.DataFTP(importedFTP));
}
}
if (Number.isFinite(object.avg_left_torque_effectiveness)) {
stats.push(new data_power_torque_effectiveness_left_1.DataPowerTorqueEffectivenessLeft(object.avg_left_torque_effectiveness));
}
if (Number.isFinite(object.avg_right_torque_effectiveness)) {
stats.push(new data_power_torque_effectiveness_right_1.DataPowerTorqueEffectivenessRight(object.avg_right_torque_effectiveness));
}
if (Number.isFinite(object.avg_left_pedal_smoothness)) {
stats.push(new data_power_pedal_smoothness_left_1.DataPowerPedalSmoothnessLeft(object.avg_left_pedal_smoothness));
}
if (Number.isFinite(object.avg_right_pedal_smoothness)) {
stats.push(new data_power_pedal_smoothness_right_1.DataPowerPedalSmoothnessRight(object.avg_right_pedal_smoothness));
}
// Speed
if (avgSpeed !== null) {
stats.push(new data_speed_avg_1.DataSpeedAvg(avgSpeed));
}
const minSpeed = getStatValue(object, ['enhanced_min_speed', 'EnhancedMinSpeed', 'min_speed', 'MinSpeed']);
if (minSpeed !== null) {
stats.push(new data_speed_min_1.DataSpeedMin(minSpeed));
}
const maxSpeed = getStatValue(object, ['enhanced_max_speed', 'EnhancedMaxSpeed', 'max_speed', 'MaxSpeed']);
if (maxSpeed !== null) {
stats.push(new data_speed_max_1.DataSpeedMax(maxSpeed));
}
const avgEffortSpeed = getStatValue(object, ['Effort Pace', 'effort_pace']);
if (avgEffortSpeed !== null && avgEffortSpeed > 0) {
const avgEffortPace = (0, helpers_1.convertSpeedToPace)(avgEffortSpeed);
if (Number.isFinite(avgEffortPace)) {
stats.push(new data_effort_pace_avg_1.DataEffortPaceAvg(avgEffortPace));
}
}
// Temperature
if ((0, helpers_1.isNumberOrString)(object.avg_temperature)) {
stats.push(new data_temperature_avg_1.DataTemperatureAvg(object.avg_temperature));
}
if ((0, helpers_1.isNumberOrString)(object.min_temperature)) {
stats.push(new data_temperature_min_1.DataTemperatureMin(object.min_temperature));
}
if ((0, helpers_1.isNumberOrString)(object.max_temperature)) {
stats.push(new data_temperature_max_1.DataTemperatureMax(object.max_temperature));
}
// Ascent
const ascent = getStatValue(object, ['total_ascent', 'TotalAscent']);
if (ascent !== null) {
stats.push(new data_ascent_1.DataAscent(ascent));
}
// Descent
const descent = getStatValue(object, ['total_descent', 'TotalDescent']);
if (descent !== null) {
stats.push(new data_descent_1.DataDescent(descent));
}
if ((0, helpers_1.isNumberOrString)(object.max_depth)) {
stats.push(new data_depth_max_1.DataDepthMax(object.max_depth));
}
// Calories
if ((0, helpers_1.isNumberOrString)(object.total_calories)) {
stats.push(new data_energy_1.DataEnergy(object.total_calories));
}
// Total training effect = Aerobic training effect
if ((0, helpers_1.isNumberOrString)(object.total_training_effect)) {
stats.push(new data_aerobic_training_effect_1.DataAerobicTrainingEffect(object.total_training_effect));
}
// Total training anaerobic effect
if ((0, helpers_1.isNumberOrString)(object.total_anaerobic_training_effect)) {
stats.push(new data_anaerobic_training_effect_1.DataAnaerobicTrainingEffect(object.total_anaerobic_training_effect));
}
else if ((0, helpers_1.isNumberOrString)(object.total_anaerobic_effect)) {
stats.push(new data_anaerobic_training_effect_1.DataAnaerobicTrainingEffect(object.total_anaerobic_effect));
}
// Vo2Max
const estimatedVO2Max = this.getValidVO2MaxValue(object.estimated_vo2_max);
if (estimatedVO2Max !== null) {
stats.push(new data_vo2_max_1.DataVO2Max(estimatedVO2Max));
}
// Peak Epoc
if ((0, helpers_1.isNumberOrString)(object.peak_epoc)) {
stats.push(new data_peak_epoc_1.DataPeakEPOC(object.peak_epoc));
}
// Recovery time
const recoveryTime = this.getPositiveNumericValue(object.recovery_time);
if (recoveryTime !== null) {
stats.push(new data_recovery_time_1.DataRecoveryTime(recoveryTime));
}
// Feeling
if ((0, helpers_1.isNumberOrString)(object.feeling)) {
stats.push(new data_feeling_1.DataFeeling(object.feeling));
}
// Pool length
if ((0, helpers_1.isNumberOrString)(object.pool_length)) {
let poolLength = object.pool_length;
if (object.pool_length_unit) {
poolLength = object.pool_length_unit.match(/metric/i) ? object.pool_length : object.pool_length * 0.9144; // Convert to meters from yards when not metric
}
stats.push(new data_pool_length_1.DataPoolLength(poolLength));
}
// Average SWOLF in 25m and 50m pool
if ((activity.type === activity_types_1.ActivityTypes.Swimming || activity.type === activity_types_1.ActivityTypes.OpenWaterSwimming) &&
((0, helpers_1.isNumberOrString)(object.avg_speed) || (0, helpers_1.isNumberOrString)(object.enhanced_avg_speed)) &&
(0, helpers_1.isNumberOrString)(object.avg_cadence)) {
const avgPace100m = (0, helpers_1.convertSpeedToSwimPace)(object.avg_speed || object.enhanced_avg_speed);
if (Number.isFinite(avgPace100m) && Number.isFinite(object.avg_cadence)) {
const avgCadence = object.avg_cadence;
const swolf25m = activity_utilities_1.ActivityUtilities.computeSwimSwolf(avgPace100m, avgCadence, 25);
stats.push(new data_swolf_25m_1.DataSWOLF25m(swolf25m));
const swolf50m = activity_utilities_1.ActivityUtilities.computeSwimSwolf(avgPace100m, avgCadence, 50);
stats.push(new data_swolf_50m_1.DataSWOLF50m(swolf50m));
}
}
if ((0, helpers_1.isNumberOrString)(object.avg_stroke_distance)) {
stats.push(new data_avg_stroke_distance_1.DataAvgStrokeDistance(object.avg_stroke_distance));
}
if ((0, helpers_1.isNumberOrString)(object.avg_stroke_count)) {
stats.push(new data_avg_stroke_count_1.DataAvgStrokeCount(object.avg_stroke_count));
}
// Active lengths
if ((0, helpers_1.isNumberOrString)(object.num_active_lengths)) {
stats.push(new data_active_lengths_1.DataActiveLengths(object.num_active_lengths));
}
// Total cycle
if ((0, helpers_1.isNumberOrString)(object.total_cycles)) {
stats.push(new data_total_cycles_1.DataTotalCycles(object.total_cycles));
}
// Description
if ((0, helpers_1.isNumberOrString)(object.description)) {
stats.push(new data_description_1.DataDescription(object.description));
}
// Cycling dynamics
if (Number.isFinite(object.time_standing)) {
const standingTime = Math.round(object.time_standing);
stats.push(new data_cycling_standing_time_1.DataCyclingStandingTime(standingTime));
const seatedTime = Math.round(timerTime - standingTime);
stats.push(new data_cycling_seated_time_1.DataCyclingSeatedTime(seatedTime));
}
// Running dynamics
if ((0, helpers_1.isNumberOrString)(object.avg_stance_time)) {
stats.push(new data_ground_contact_time_avg_1.DataGroundContactTimeAvg(object.avg_stance_time));
}
if ((0, helpers_1.isNumberOrString)(object.avg_vertical_oscillation)) {
stats.push(new data_vertical_oscillation_avg_1.DataVerticalOscillationAvg(object.avg_vertical_oscillation));
}
if ((0, helpers_1.isNumberOrString)(object.avg_vertical_ratio)) {
stats.push(new data_vertical_ratio_avg_1.DataVerticalRatioAvg(object.avg_vertical_ratio));
}
// Grade summary from session stats (if available)
if ((0, helpers_1.isNumberOrString)(object.avg_grade)) {
stats.push(new data_grade_avg_1.DataGradeAvg(object.avg_grade));
}
if ((0, helpers_1.isNumberOrString)(object.max_pos_grade)) {
stats.push(new data_grade_max_1.DataGradeMax(object.max_pos_grade));
}
if ((0, helpers_1.isNumberOrString)(object.max_neg_grade)) {
stats.push(new data_grade_min_1.DataGradeMin(object.max_neg_grade));
}
if ((0, helpers_1.isNumberOrString)(object.beginning_potential_stamina)) {
stats.push(new data_stamina_1.DataBeginningPotentialStamina(object.beginning_potential_stamina));
}
if ((0, helpers_1.isNumberOrString)(object.ending_potential_stamina)) {
stats.push(new data_stamina_1.DataEndingPotentialStamina(object.ending_potential_stamina));
}
if ((0, helpers_1.isNumberOrString)(object.min_stamina)) {
stats.push(new data_stamina_1.DataStaminaMin(object.min_stamina));
}
if (Number.isFinite(object.avg_step_length)) {
const avgStrideLengthMeters = object.avg_step_length / 1000;
stats.push(new data_avg_stride_length_1.DataAvgStrideLength(Math.round(avgStrideLengthMeters * 100) / 100));
}
// Respiration Rate
if ((0, helpers_1.isNumberOrString)(object.avg_respiration_rate) || (0, helpers_1.isNumberOrString)(object.enhanced_avg_respiration_rate)) {
stats.push(new data_avg_respiration_rate_1.DataAvgRespirationRate((_a = object.enhanced_avg_respiration_rate) !== null && _a !== void 0 ? _a : object.avg_respiration_rate));
}
if ((0, helpers_1.isNumberOrString)(object.max_respiration_rate) || (0, helpers_1.isNumberOrString)(object.enhanced_max_respiration_rate)) {
stats.push(new data_max_respiration_rate_1.DataMaxRespirationRate((_b = object.enhanced_max_respiration_rate) !== null && _b !== void 0 ? _b : object.max_respiration_rate));
}
if ((0, helpers_1.isNumberOrString)(object.min_respiration_rate) || (0, helpers_1.isNumberOrString)(object.enhanced_min_respiration_rate)) {
stats.push(new data_min_respiration_rate_1.DataMinRespirationRate((_c = object.enhanced_min_respiration_rate) !== null && _c !== void 0 ? _c : object.min_respiration_rate));
}
// Total Grit
if ((0, helpers_1.isNumberOrString)(object.total_grit)) {
stats.push(new data_total_grit_1.DataTotalGrit(object.total_grit));
}
// Avg Flow
if ((0, helpers_1.isNumberOrString)(object.avg_flow)) {
stats.push(new data_avg_flow_1.DataAvgFlow(object.avg_flow));
}
// Est Sweat Loss
if ((0, helpers_1.isNumberOrString)(object.est_sweat_loss)) {
stats.push(new data_est_sweat_loss_1.DataEstSweatLoss(object.est_sweat_loss));
}
// Primary Benefit
if ((0, helpers_1.isNumberOrString)(object.primary_benefit)) {
stats.push(new data_primary_benefit_1.DataPrimaryBenefit(object.primary_benefit));
}
// Sport Profile Name
if (object.sport_profile_name) {
stats.push(new data_sport_profile_name_1.DataSportProfileName(object.sport_profile_name));
}
// Jump Count
if ((0, helpers_1.isNumberOrString)(object.jump_count)) {
stats.push(new data_jump_count_1.DataJumpCount(object.jump_count));
}
// Training Load Peak
if ((0, helpers_1.isNumberOrString)(object.training_load_peak)) {
stats.push(new data_training_load_peak_1.DataTrainingLoadPeak(object.training_load_peak));
}
// Positions
if ((0, helpers_1.isNumber)(object.start_position_lat) && (0, helpers_1.isNumber)(object.start_position_long)) {
stats.push(new data_start_position_1.DataStartPosition({
latitudeDegrees: object.start_position_lat,
longitudeDegrees: object.start_position_long
}));
}
if ((0, helpers_1.isNumber)(object.end_position_lat) && (0, helpers_1.isNumber)(object.end_position_long)) {
stats.push(new data_end_position_1.DataEndPosition({
latitudeDegrees: object.end_position_lat,
longitudeDegrees: object.end_position_long
}));
}
// Resting Calories
if ((0, helpers_1.isNumberOrString)(object.resting_calories)) {
stats.push(new data_resting_calories_1.DataRestingCalories(object.resting_calories));
}
// Avg VAM
if ((0, helpers_1.isNumberOrString)(object.avg_vam)) {
stats.push(new data_avg_vam_1.DataAvgVAM(object.avg_vam));
}
// Jump Statistics
if (object.jumps && object.jumps.length > 0) {
const jumps = object.jumps;
const createJumpAggregate = () => ({
sum: 0,
min: Number.MAX_VALUE,
max: -Number.MAX_VALUE,
count: 0
});
const accumulateJumpValue = (aggregate, value) => {
if (typeof value === 'number' && Number.isFinite(value)) {
aggregate.sum += value;
aggregate.min = Math.min(aggregate.min, value);
aggregate.max = Math.max(aggregate.max, value);
aggregate.count += 1;
}
};
const hangTimeAggregate = createJumpAggregate();
const distanceAggregate = createJumpAggregate();
const speedAggregate = createJumpAggregate();
const rotationsAggregate = createJumpAggregate();
const scoreAggregate = createJumpAggregate();
const heightAggregate = createJumpAggregate();
jumps.forEach((j) => {
accumulateJumpValue(hangTimeAggregate, j.hang_time);
accumulateJumpValue(distanceAggregate, j.distance);
accumulateJumpValue(speedAggregate, j.speed);
accumulateJumpValue(rotationsAggregate, j.rotations);
accumulateJumpValue(scoreAggregate, j.score);
accumulateJumpValue(heightAggregate, j.height);
});
if (hangTimeAggregate.count > 0) {
stats.push(new data_jump_stats_1.DataJumpHangTimeMin(hangTimeAggregate.min));
stats.push(new data_jump_stats_1.DataJumpHangTimeMax(hangTimeAggregate.max));
stats.push(new data_jump_stats_1.DataJumpHangTimeAvg(hangTimeAggregate.sum / hangTimeAggregate.count));
}
if (distanceAggregate.count > 0) {
stats.push(new data_jump_stats_1.DataJumpDistanceMin(distanceAggregate.min));
stats.push(new data_jump_stats_1.DataJumpDistanceMax(distanceAggregate.max));
stats.push(new data_jump_stats_1.DataJumpDistanceAvg(distanceAggregate.sum / distanceAggregate.count));
}
if (speedAggregate.count > 0) {
stats.push(new data_jump_stats_1.DataJumpSpeedMin(speedAggregate.min));
stats.push(new data_jump_stats_1.DataJumpSpeedMax(speedAggregate.max));
stats.push(new data_jump_stats_1.DataJumpSpeedAvg(speedAggregate.sum / speedAggregate.count));
}
if (rotationsAggregate.count > 0) {
stats.push(new data_jump_stats_1.DataJumpRotationsMin(rotationsAggregate.min));
stats.push(new data_jump_stats_1.DataJumpRotationsMax(rotationsAggregate.max));
stats.push(new data_jump_stats_1.DataJumpRotationsAvg(rotationsAggregate.sum / rotationsAggregate.count));
}
if (scoreAggregate.count > 0) {
stats.push(new data_jump_stats_1.DataJumpScoreMin(scoreAggregate.min));
stats.push(new data_jump_stats_1.DataJumpScoreMax(scoreAggregate.max));
stats.push(new data_jump_stats_1.DataJumpScoreAvg(scoreAggregate.sum / scoreAggregate.count));
}
if (heightAggregate.count > 0) {
stats.push(new data_jump_stats_1.DataJumpHeightMin(heightAggregate.min));
stats.push(new data_jump_stats_1.DataJumpHeightMax(heightAggregate.max));
stats.push(new data_jump_stats_1.DataJumpHeightAvg(heightAggregate.sum / heightAggregate.count));
}
}
return stats;
}
static getCreatorFromFitDataObject(fitDataObject) {
return fit_creator_mapper_1.FITCreatorMapper.getCreatorFromFitDataObject(fitDataObject);
}
}
exports.EventImporterFIT = EventImporterFIT;
EventImporterFIT.INVALID_FIT_HRV_INTERVAL_MS = 65535;
EventImporterFIT.MIN_VALID_VO2_MAX = 10;
EventImporterFIT.MAX_VALID_VO2_MAX = 100;