UNPKG

@sports-alliance/sports-lib

Version:

A Library to for importing / exporting and processing GPX, TCX, FIT and JSON files from services such as Strava, Movescount, Garmin, Polar etc

2,117 lines 130 kB
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.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;