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

906 lines 91.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ActivityUtilities = void 0; var data_heart_rate_1 = require("../../data/data.heart-rate"); var data_cadence_1 = require("../../data/data.cadence"); var data_speed_1 = require("../../data/data.speed"); var data_vertical_speed_1 = require("../../data/data.vertical-speed"); var data_temperature_1 = require("../../data/data.temperature"); var data_altitude_1 = require("../../data/data.altitude"); var data_power_1 = require("../../data/data.power"); var data_altitude_max_1 = require("../../data/data.altitude-max"); var data_altitude_min_1 = require("../../data/data.altitude-min"); var data_altitude_avg_1 = require("../../data/data.altitude-avg"); var data_heart_rate_max_1 = require("../../data/data.heart-rate-max"); var data_heart_rate_min_1 = require("../../data/data.heart-rate-min"); var data_heart_rate_avg_1 = require("../../data/data.heart-rate-avg"); var data_cadence_max_1 = require("../../data/data.cadence-max"); var data_cadence_min_1 = require("../../data/data.cadence-min"); var data_cadence_avg_1 = require("../../data/data.cadence-avg"); var data_speed_max_1 = require("../../data/data.speed-max"); var data_speed_min_1 = require("../../data/data.speed-min"); var data_speed_avg_1 = require("../../data/data.speed-avg"); var data_vertical_speed_max_1 = require("../../data/data.vertical-speed-max"); var data_vertical_speed_min_1 = require("../../data/data.vertical-speed-min"); var data_vertical_speed_avg_1 = require("../../data/data.vertical-speed-avg"); var data_power_max_1 = require("../../data/data.power-max"); var data_power_min_1 = require("../../data/data.power-min"); var data_power_avg_1 = require("../../data/data.power-avg"); var data_temperature_max_1 = require("../../data/data.temperature-max"); var data_temperature_min_1 = require("../../data/data.temperature-min"); var data_temperature_avg_1 = require("../../data/data.temperature-avg"); var data_distance_1 = require("../../data/data.distance"); var data_duration_1 = require("../../data/data.duration"); var data_pause_1 = require("../../data/data.pause"); var data_ascent_1 = require("../../data/data.ascent"); var data_descent_1 = require("../../data/data.descent"); var geolib_adapter_1 = require("../../geodesy/adapters/geolib.adapter"); var data_pace_max_1 = require("../../data/data.pace-max"); var data_pace_1 = require("../../data/data.pace"); var data_pace_min_1 = require("../../data/data.pace-min"); var data_pace_avg_1 = require("../../data/data.pace-avg"); var data_battery_charge_1 = require("../../data/data.battery-charge"); var data_battery_consumption_1 = require("../../data/data.battery-consumption"); var data_battery_life_estimation_1 = require("../../data/data.battery-life-estimation"); var data_latitude_degrees_1 = require("../../data/data.latitude-degrees"); var stream_1 = require("../../streams/stream"); var helpers_1 = require("./helpers"); var data_longitude_degrees_1 = require("../../data/data.longitude-degrees"); var data_energy_1 = require("../../data/data.energy"); var data_start_altitude_1 = require("../../data/data.start-altitude"); var data_end_altitude_1 = require("../../data/data.end-altitude"); var data_swim_pace_max_1 = require("../../data/data.swim-pace-max"); var data_swim_pace_1 = require("../../data/data.swim-pace"); var data_swim_pace_min_1 = require("../../data/data.swim-pace-min"); var data_swim_pace_avg_1 = require("../../data/data.swim-pace-avg"); var data_feeling_1 = require("../../data/data.feeling"); var data_power_left_1 = require("../../data/data.power-left"); var data_right_balance_1 = require("../../data/data.right-balance"); var data_left_balance_1 = require("../../data/data.left-balance"); var data_power_right_1 = require("../../data/data.power-right"); var data_air_power_min_1 = require("../../data/data.air-power-min"); var data_air_power_1 = require("../../data/data.air-power"); var data_air_power_max_1 = require("../../data/data.air-power-max"); var data_air_power_avg_1 = require("../../data/data.air-power-avg"); var data_rpe_1 = require("../../data/data.rpe"); var data_gnss_distance_1 = require("../../data/data.gnss-distance"); var data_heart_rate_zone_one_duration_1 = require("../../data/data.heart-rate-zone-one-duration"); var data_heart_rate_zone_two_duration_1 = require("../../data/data.heart-rate-zone-two-duration"); var data_heart_rate_zone_three_duration_1 = require("../../data/data.heart-rate-zone-three-duration"); var data_heart_rate_zone_four_duration_1 = require("../../data/data.heart-rate-zone-four-duration"); var data_heart_rate_zone_five_duration_1 = require("../../data/data.heart-rate-zone-five-duration"); var data_power_zone_one_duration_1 = require("../../data/data.power-zone-one-duration"); var data_power_zone_two_duration_1 = require("../../data/data.power-zone-two-duration"); var data_power_zone_three_duration_1 = require("../../data/data.power-zone-three-duration"); var data_power_zone_four_duration_1 = require("../../data/data.power-zone-four-duration"); var data_power_zone_five_duration_1 = require("../../data/data.power-zone-five-duration"); var data_speed_zone_one_duration_1 = require("../../data/data.speed-zone-one-duration"); var data_speed_zone_two_duration_1 = require("../../data/data.speed-zone-two-duration"); var data_speed_zone_three_duration_1 = require("../../data/data.speed-zone-three-duration"); var data_speed_zone_four_duration_1 = require("../../data/data.speed-zone-four-duration"); var data_speed_zone_five_duration_1 = require("../../data/data.speed-zone-five-duration"); var data_store_1 = require("../../data/data.store"); var data_start_position_1 = require("../../data/data.start-position"); var data_end_position_1 = require("../../data/data.end-position"); var data_grade_adjusted_speed_avg_1 = require("../../data/data.grade-adjusted-speed-avg"); var data_grade_adjusted_pace_avg_1 = require("../../data/data.grade-adjusted-pace-avg"); var data_grade_adjusted_speed_1 = require("../../data/data.grade-adjusted-speed"); var data_grade_adjusted_pace_1 = require("../../data/data.grade-adjusted-pace"); var data_grade_adjusted_speed_max_1 = require("../../data/data.grade-adjusted-speed-max"); var data_grade_adjusted_speed_min_1 = require("../../data/data.grade-adjusted-speed-min"); var data_grade_adjusted_pace_max_1 = require("../../data/data.grade-adjusted-pace-max"); var data_grade_adjusted_pace_min_1 = require("../../data/data.grade-adjusted-pace-min"); var data_grade_1 = require("../../data/data.grade"); var grade_calculator_1 = require("./grade-calculator/grade-calculator"); var activity_types_1 = require("../../activities/activity.types"); var data_moving_time_1 = require("../../data/data.moving-time"); var ActivityUtilities = /** @class */ (function () { function ActivityUtilities() { } ActivityUtilities.getDataTypeAvg = function (activity, streamType, startDate, endDate) { var data = (activity .getSquashedStreamData(streamType, startDate, endDate) .filter(function (streamData) { return streamData !== Infinity && streamData !== -Infinity; })); return this.getAverage(data); }; ActivityUtilities.getAverage = function (data) { var sum = data.reduce(function (sumbuff, value) { sumbuff += value; return sumbuff; }, 0); return sum / data.length; }; ActivityUtilities.getDataTypeMax = function (activity, streamType, startDate, endDate) { return this.getActivityDataTypeMinOrMax(activity, streamType, true, startDate, endDate); }; ActivityUtilities.getDataTypeMin = function (activity, streamType, startDate, endDate) { return this.getActivityDataTypeMinOrMax(activity, streamType, false, startDate, endDate); }; ActivityUtilities.getDataTypeMinToMaxDifference = function (activity, streamType, startDate, endDate) { return (this.getDataTypeMax(activity, streamType, startDate, endDate) - this.getDataTypeMin(activity, streamType, startDate, endDate)); }; ActivityUtilities.getDataTypeFirst = function (activity, streamType, startDate, endDate) { var data = activity.getSquashedStreamData(streamType, startDate, endDate); return data[0]; }; ActivityUtilities.getDataTypeLast = function (activity, streamType, startDate, endDate) { var data = activity.getSquashedStreamData(streamType, startDate, endDate); return data[data.length - 1]; }; ActivityUtilities.cropDistance = function (startDistance, endDistance, activity) { // Short to do the search just in case var startDistanceDate; // Does not sound right var endDistanceDate; // debugger; activity.getStreamData(data_distance_1.DataDistance.type).forEach(function (distanceFromData, index) { // Find the index with greater dinstnce and convert it to time if (startDistance && !startDistanceDate && distanceFromData && distanceFromData >= startDistance) { startDistanceDate = new Date(activity.startDate.getTime() + index * 1000); return; } // Same for end if (endDistance && !endDistanceDate && distanceFromData && distanceFromData >= endDistance) { endDistanceDate = new Date(activity.startDate.getTime() + index * 1000); return; } }); if (!startDistanceDate && !endDistanceDate) { return activity; } activity = this.cropTime(activity, startDistanceDate, endDistanceDate); // Remove because it is invalid, you cannot just offset the distance as a stream I think var distanceStream = activity.getAllStreams().find(function (s) { return data_distance_1.DataDistance.type === s.type; }); if (distanceStream) { activity.removeStream(distanceStream); } var gnssDistanceStream = activity.getAllStreams().find(function (s) { return data_gnss_distance_1.DataGNSSDistance.type === s.type; }); if (gnssDistanceStream) { activity.removeStream(gnssDistanceStream); } return activity; }; /** * Crops left,right on time. * Start and end date need to be relative to the activity start / end time * @param activity * @param startDate * @param endDate */ ActivityUtilities.cropTime = function (activity, startDate, endDate) { activity.getAllStreams().forEach(function (stream) { // Get the data for the range specified var trimmedStreamData = activity.getStreamData(stream.type, startDate, endDate); activity.removeStream(stream); activity.addStream(new stream_1.Stream(stream.type, trimmedStreamData)); }); activity.startDate = startDate || activity.startDate; activity.endDate = endDate || activity.endDate; // debugger return activity; }; ActivityUtilities.getStreamDataTypesBasedOnDataType = function (streamToBaseOn, streams) { return streamToBaseOn.getData().reduce(function (accu, streamDataItem, index) { var _a; if (!helpers_1.isNumberOrString(streamDataItem)) { return accu; } var dataItem = (_a = {}, _a[streamToBaseOn.type] = streamDataItem, _a); streams.forEach(function (stream) { dataItem[stream.type] = stream.getData()[index]; }); accu.push(dataItem); return accu; }, []); }; ActivityUtilities.getStreamDataTypesBasedOnTime = function (startDate, endDate, streams) { var streamDataBasedOnTime = {}; var _loop_1 = function (i) { // Perhaps this can be optimized with a search function streams.forEach(function (stream) { if (helpers_1.isNumber(stream.getData()[i])) { streamDataBasedOnTime[startDate.getTime() + i * 1000] = streamDataBasedOnTime[startDate.getTime() + i * 1000] || {}; streamDataBasedOnTime[startDate.getTime() + i * 1000][stream.type] = stream.getData()[i]; } }); }; for (var i = 0; i < this.getDataLength(startDate, endDate); i++) { _loop_1(i); } return streamDataBasedOnTime; }; ActivityUtilities.getDataLength = function (startDate, endDate) { return Math.ceil((+endDate - +startDate) / 1000) + 1; }; ActivityUtilities.generateMissingStreamsAndStatsForActivity = function (activity) { this.generateMissingStreamsForActivity(activity); activity.addStreams(this.createUnitStreamsFromStreams(activity.getAllStreams(), activity.type)); this.generateMissingStatsForActivity(activity); this.generateMissingSpeedDerivedStatsForActivity(activity); this.generateMissingUnitStatsForActivity(activity); // Perhaps this needs to happen on user level so needs to go out of here }; ActivityUtilities.getSummaryStatsForActivities = function (activities) { var stats = []; // If only one if (activities.length === 1) { return activities[0].getStatsAsArray(); } var duration = 0; var ascent = 0; var descent = 0; var energy = 0; var distance = 0; var pauseTime = 0; var averageHeartRate = 0; var averagePower = 0; var averageCadence = 0; var averageSpeed = 0; var averageGradeAdjustedSpeed = 0; var averagePace = 0; var averageGradeAdjustedPace = 0; var averageSwimPace = 0; var averageTemperature = 0; var averageFeeling = 0; var averageRPE = 0; // Sum Duration activities.forEach(function (activity) { duration += activity.getDuration().getValue(); }); stats.push(new data_duration_1.DataDuration(duration)); // Sum pause time activities.forEach(function (activity) { pauseTime += activity.getPause().getValue(); }); stats.push(new data_pause_1.DataPause(pauseTime)); // Sum Distance activities.forEach(function (activity) { distance += activity.getDistance().getValue(); }); stats.push(new data_distance_1.DataDistance(distance)); // Sum ascent activities.forEach(function (activity) { var activityAscent = activity.getStat(data_ascent_1.DataAscent.type); if (activityAscent) { ascent += activityAscent.getValue(); } }); stats.push(new data_ascent_1.DataAscent(ascent)); // Sum descent activities.forEach(function (activity) { var activityDescent = activity.getStat(data_descent_1.DataDescent.type); if (activityDescent) { descent += activityDescent.getValue(); } }); stats.push(new data_descent_1.DataDescent(descent)); // Sum energy activities.forEach(function (activity) { var activityEnergy = activity.getStat(data_energy_1.DataEnergy.type); if (activityEnergy) { energy += activityEnergy.getValue(); } }); stats.push(new data_energy_1.DataEnergy(energy)); // Avg Avg HR activities.forEach(function (activity) { var activityAvgHeartRate = activity.getStat(data_heart_rate_avg_1.DataHeartRateAvg.type); if (activityAvgHeartRate) { // The below will fallback for 0 averageHeartRate = averageHeartRate ? (averageHeartRate + activityAvgHeartRate.getValue()) / 2 : activityAvgHeartRate.getValue(); } }); if (averageHeartRate) { stats.push(new data_heart_rate_avg_1.DataHeartRateAvg(averageHeartRate)); } // Avg Avg HR activities.forEach(function (activity) { var activityAvgHeartRate = activity.getStat(data_heart_rate_avg_1.DataHeartRateAvg.type); if (activityAvgHeartRate) { // The below will fallback for 0 averageHeartRate = averageHeartRate ? (averageHeartRate + activityAvgHeartRate.getValue()) / 2 : activityAvgHeartRate.getValue(); } }); if (averageHeartRate) { stats.push(new data_heart_rate_avg_1.DataHeartRateAvg(averageHeartRate)); } // Avg Avg Power activities.forEach(function (activity) { var activityAvgPower = activity.getStat(data_power_avg_1.DataPowerAvg.type); if (activityAvgPower) { // The below will fallback for 0 averagePower = averagePower ? (averagePower + activityAvgPower.getValue()) / 2 : activityAvgPower.getValue(); } }); if (averagePower) { stats.push(new data_power_avg_1.DataPowerAvg(averagePower)); } // Avg Avg Cadence activities.forEach(function (activity) { var activityAvgCadence = activity.getStat(data_cadence_avg_1.DataCadenceAvg.type); if (activityAvgCadence) { // The below will fallback for 0 averageCadence = averageCadence ? (averageCadence + activityAvgCadence.getValue()) / 2 : activityAvgCadence.getValue(); } }); if (averageCadence) { stats.push(new data_cadence_avg_1.DataCadenceAvg(averageCadence)); } // Avg Avg Speed activities.forEach(function (activity) { var activityAvgSpeed = activity.getStat(data_speed_avg_1.DataSpeedAvg.type); if (activityAvgSpeed) { // The below will fallback for 0 averageSpeed = averageSpeed ? (averageSpeed + activityAvgSpeed.getValue()) / 2 : activityAvgSpeed.getValue(); } }); if (averageSpeed) { stats.push(new data_speed_avg_1.DataSpeedAvg(averageSpeed)); } // Avg Avg Gap Speed activities.forEach(function (activity) { var activityAvgGradeAdjustedSpeed = activity.getStat(data_grade_adjusted_speed_avg_1.DataGradeAdjustedSpeedAvg.type); if (activityAvgGradeAdjustedSpeed) { // The below will fallback for 0 averageGradeAdjustedSpeed = averageGradeAdjustedSpeed ? (averageGradeAdjustedSpeed + activityAvgGradeAdjustedSpeed.getValue()) / 2 : activityAvgGradeAdjustedSpeed.getValue(); } }); if (averageGradeAdjustedSpeed) { stats.push(new data_grade_adjusted_speed_avg_1.DataGradeAdjustedSpeedAvg(averageGradeAdjustedSpeed)); } // Avg Avg Pace activities.forEach(function (activity) { var activityAvgPace = activity.getStat(data_pace_avg_1.DataPaceAvg.type); if (activityAvgPace) { // The below will fallback for 0 averagePace = averagePace ? (averagePace + activityAvgPace.getValue()) / 2 : activityAvgPace.getValue(); } }); if (averagePace) { stats.push(new data_pace_avg_1.DataPaceAvg(averagePace)); } // Avg Avg GAP Pace activities.forEach(function (activity) { var activityAvgGradeAdjustedPace = activity.getStat(data_grade_adjusted_pace_avg_1.DataGradeAdjustedPaceAvg.type); if (activityAvgGradeAdjustedPace) { // The below will fallback for 0 averageGradeAdjustedPace = averageGradeAdjustedPace ? (averageGradeAdjustedPace + activityAvgGradeAdjustedPace.getValue()) / 2 : activityAvgGradeAdjustedPace.getValue(); } }); if (averageGradeAdjustedPace) { stats.push(new data_grade_adjusted_pace_avg_1.DataGradeAdjustedPaceAvg(averageGradeAdjustedPace)); } // Avg Avg SwimPace activities.forEach(function (activity) { var activityAvgSwimPace = activity.getStat(data_swim_pace_avg_1.DataSwimPaceAvg.type); if (activityAvgSwimPace) { // The below will fallback for 0 averageSwimPace = averageSwimPace ? (averageSwimPace + activityAvgSwimPace.getValue()) / 2 : activityAvgSwimPace.getValue(); } }); if (averageSwimPace) { stats.push(new data_swim_pace_avg_1.DataSwimPaceAvg(averageSwimPace)); } // Avg Avg Temperature activities.forEach(function (activity) { var activityAvgTemperature = activity.getStat(data_temperature_avg_1.DataTemperatureAvg.type); if (activityAvgTemperature) { // The below will fallback for 0 averageTemperature = averageTemperature ? (averageTemperature + activityAvgTemperature.getValue()) / 2 : activityAvgTemperature.getValue(); } }); if (averageTemperature) { stats.push(new data_temperature_avg_1.DataTemperatureAvg(averageTemperature)); } // Avg Feeling activities.forEach(function (activity) { var activityAvgFeeling = activity.getStat(data_feeling_1.DataFeeling.type); if (activityAvgFeeling) { // The below will fallback for 0 averageFeeling = averageFeeling ? Math.ceil((averageFeeling + activityAvgFeeling.getValue()) / 2) : activityAvgFeeling.getValue(); } }); if (averageFeeling) { stats.push(new data_feeling_1.DataFeeling(averageFeeling)); } // Avg RPE activities.forEach(function (activity) { var activityAvgRPE = activity.getStat(data_feeling_1.DataFeeling.type); if (activityAvgRPE) { // The below will fallback for 0 averageRPE = averageRPE ? Math.ceil((averageRPE + activityAvgRPE.getValue()) / 2) : activityAvgRPE.getValue(); } }); if (averageRPE) { stats.push(new data_rpe_1.DataRPE(averageRPE)); } stats.push.apply(stats, this.getIntensityZonesStatsAggregated(activities)); // Add start and end position // This expects the to be sorted var activitiesWithStartPosition = activities.filter(function (activity) { return activity.getStat(data_start_position_1.DataStartPosition.type); }); var activitiesWithEndPosition = activities.filter(function (activity) { return activity.getStat(data_end_position_1.DataEndPosition.type); }); if (activitiesWithStartPosition && activitiesWithStartPosition.length) { var startPositionStat = activitiesWithStartPosition[0].getStat(data_start_position_1.DataStartPosition.type); stats.push(new data_start_position_1.DataStartPosition(startPositionStat.getValue())); } if (activitiesWithEndPosition && activitiesWithEndPosition.length) { var endPositionStat = (activitiesWithEndPosition[activitiesWithEndPosition.length - 1].getStat(data_end_position_1.DataEndPosition.type)); stats.push(new data_end_position_1.DataEndPosition(endPositionStat.getValue())); } // debugger; return stats; }; ActivityUtilities.getIntensityZonesStatsAggregated = function (statClassInstances) { return [ data_heart_rate_zone_one_duration_1.DataHeartRateZoneOneDuration.type, data_heart_rate_zone_two_duration_1.DataHeartRateZoneTwoDuration.type, data_heart_rate_zone_three_duration_1.DataHeartRateZoneThreeDuration.type, data_heart_rate_zone_four_duration_1.DataHeartRateZoneFourDuration.type, data_heart_rate_zone_five_duration_1.DataHeartRateZoneFiveDuration.type, data_power_zone_one_duration_1.DataPowerZoneOneDuration.type, data_power_zone_two_duration_1.DataPowerZoneTwoDuration.type, data_power_zone_three_duration_1.DataPowerZoneThreeDuration.type, data_power_zone_four_duration_1.DataPowerZoneFourDuration.type, data_power_zone_five_duration_1.DataPowerZoneFiveDuration.type, data_speed_zone_one_duration_1.DataSpeedZoneOneDuration.type, data_speed_zone_two_duration_1.DataSpeedZoneTwoDuration.type, data_speed_zone_three_duration_1.DataSpeedZoneThreeDuration.type, data_speed_zone_four_duration_1.DataSpeedZoneFourDuration.type, data_speed_zone_five_duration_1.DataSpeedZoneFiveDuration.type ].reduce(function (statsArray, zone) { var zoneDuration = statClassInstances.reduce(function (duration, statClassInstance) { var durationStat = statClassInstance.getStat(zone); if (durationStat) { duration = duration || 0; duration += durationStat.getValue(); } return duration; }, null); if (helpers_1.isNumber(zoneDuration)) { statsArray.push(data_store_1.DynamicDataLoader.getDataInstanceFromDataType(zone, zoneDuration)); } return statsArray; }, []); }; ActivityUtilities.getActivityDataTypeGain = function (activity, streamType, starDate, endDate, minDiff) { return this.getActivityDataTypeGainOrLoss(activity, streamType, true, starDate, endDate, minDiff); }; ActivityUtilities.getActivityDataTypeLoss = function (activity, streamType, starDate, endDate, minDiff) { return this.getActivityDataTypeGainOrLoss(activity, streamType, false, starDate, endDate, minDiff); }; ActivityUtilities.getGainOrLoss = function (data, gain, minDiff) { if (minDiff === void 0) { minDiff = 3; } var gainOrLoss = 0; data.reduce(function (previousValue, nextValue) { // For gain if (gain) { // Increase the gain if eligible first check to be greater plus diff [200, 300, 400, 100, 101, 102] if (previousValue + minDiff <= nextValue) { gainOrLoss += nextValue - previousValue; return nextValue; } // if not eligible check if smaller without the diff and if yes do not register it and send it back as the last to check against if (previousValue < nextValue) { return previousValue; } return nextValue; } // For Loss if (previousValue - minDiff >= nextValue) { gainOrLoss += previousValue - nextValue; return nextValue; } // if not eligible check if smaller without the diff and if yes do not register it and send it back as the last to check against if (previousValue > nextValue) { return previousValue; } return nextValue; }); return gainOrLoss; }; ActivityUtilities.getMax = function (data) { return data.reduce(function (previousValue, currentValue) { return Math.max(previousValue, currentValue); }, -Infinity); }; ActivityUtilities.getMin = function (data) { return data.reduce(function (previousValue, currentValue) { return Math.min(previousValue, currentValue); }, Infinity); }; ActivityUtilities.calculateTotalDistanceForActivity = function (activity, startDate, endDate) { return this.geoLibAdapter.getDistance(activity.getPositionData(startDate, endDate).filter(function (position) { return position !== null; })); }; /** * Returns streams that derive from speed based on the activity type * @param speedStream * @param activityType */ ActivityUtilities.createByActivityTypeSpeedBasedStreams = function (speedStream, activityType) { return activity_types_1.ActivityTypesHelper.speedDerivedDataTypesToUseForActivityType(activityType).reduce(function (array, dataType) { switch (dataType) { case data_pace_1.DataPace.type: return array.concat([ new stream_1.Stream(data_pace_1.DataPace.type, speedStream.getData().map(function (dataValue) { if (!helpers_1.isNumber(dataValue)) { return null; } return helpers_1.convertSpeedToPace(dataValue); })) ]); case data_swim_pace_1.DataSwimPace.type: return array.concat([ new stream_1.Stream(data_swim_pace_1.DataSwimPace.type, speedStream.getData().map(function (dataValue) { if (!helpers_1.isNumber(dataValue)) { return null; } return helpers_1.convertSpeedToSwimPace(dataValue); })) ]); case data_speed_1.DataSpeed.type: return array.concat(speedStream); default: return array; } }, []); }; /** * Returns streams that derive from grade adjusted speed based on the activity type * @param gradeAdjustedSpeedStream * @param activityType */ ActivityUtilities.createByActivityTypeAltiDistanceSpeedBasedStreams = function (gradeAdjustedSpeedStream, activityType) { return activity_types_1.ActivityTypesHelper.altiDistanceSpeedDerivedDataTypesToUseForActivityType(activityType).reduce(function (array, dataType) { switch (dataType) { case data_grade_adjusted_pace_1.DataGradeAdjustedPace.type: return array.concat([ new stream_1.Stream(data_grade_adjusted_pace_1.DataGradeAdjustedPace.type, gradeAdjustedSpeedStream.getData().map(function (dataValue) { if (!helpers_1.isNumber(dataValue)) { return null; } return helpers_1.convertSpeedToPace(dataValue); })) ]); case data_grade_adjusted_speed_1.DataGradeAdjustedSpeed.type: return array.concat(gradeAdjustedSpeedStream); default: return array; } }, []); }; /** * @todo unit test (get the pun?) * This creates streams that are deriving as unit based streams * For example it will create pace from speed, swim pace from speed but also speed in km/h as a unitstream * @param streams * @param activityType * @param unitStreamTypes DynamicDataLoader.allUnitDerivedDataTypes this acts like a whitelist for the unit derived units ONLY! */ ActivityUtilities.createUnitStreamsFromStreams = function (streams, activityType, unitStreamTypes) { // @todo perhaps check input to be unitStreamTypesStrictly var unitStreamTypesToCreate = unitStreamTypes || data_store_1.DynamicDataLoader.allUnitDerivedDataTypes; var baseUnitStreams = []; var speedStream = streams.find(function (stream) { return stream.type === data_speed_1.DataSpeed.type; }); if (speedStream) { baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeSpeedBasedStreams(speedStream, activityType)); } var gradeAdjustedSpeedStream = streams.find(function (stream) { return stream.type === data_grade_adjusted_speed_1.DataGradeAdjustedSpeed.type; }); if (gradeAdjustedSpeedStream) { baseUnitStreams = baseUnitStreams.concat(this.createByActivityTypeAltiDistanceSpeedBasedStreams(gradeAdjustedSpeedStream, activityType)); } var verticalSpeedStream = streams.find(function (stream) { return stream.type === data_vertical_speed_1.DataVerticalSpeed.type; }); if (verticalSpeedStream) { // For vertical speed (yet) we dont need a seperate function so just add the base that is the "derived" one baseUnitStreams = activity_types_1.ActivityTypesHelper.verticalSpeedDerivedDataTypesToUseForActivityType(activityType).length ? baseUnitStreams.concat(verticalSpeedStream) : baseUnitStreams; } // @todo add distance ? var startWith = baseUnitStreams.filter(function (baseUnitStream) { return unitStreamTypesToCreate.indexOf(baseUnitStream.type) !== -1 && streams.indexOf(baseUnitStream) === -1; }); return Object.keys(data_store_1.DynamicDataLoader.dataTypeUnitGroups).reduce(function (array, baseDataType) { var baseStream = baseUnitStreams.find(function (stream) { return stream.type === baseDataType; }); if (!baseStream) { return array; } var unitStreams = Object.keys(data_store_1.DynamicDataLoader.dataTypeUnitGroups[baseDataType]) .filter(function (unitBasedDataType) { return unitStreamTypesToCreate.indexOf(unitBasedDataType) !== -1; }) // @todo perhaps dont filter .map(function (unitBasedDataType) { return new stream_1.Stream(unitBasedDataType, baseStream.getData().map(function (dataValue) { if (!helpers_1.isNumber(dataValue)) { return null; } return data_store_1.DynamicDataLoader.dataTypeUnitGroups[baseDataType][unitBasedDataType](dataValue); })); }); return array.concat(unitStreams); }, startWith); }; /** * Generates missing streams for an activity such as distance etc if they are missing * This will always create a steam even if the distance is 0 * @param activity */ ActivityUtilities.generateMissingStreamsForActivity = function (activity) { var _this = this; // First add any missing data to the streams via interpolating and extrapolating this.addMissingDataToStreams(activity); if (activity.hasStreamData(data_latitude_degrees_1.DataLatitudeDegrees.type) && activity.hasStreamData(data_latitude_degrees_1.DataLatitudeDegrees.type) && (!activity.hasStreamData(data_distance_1.DataDistance.type) || !activity.hasStreamData(data_gnss_distance_1.DataGNSSDistance.type))) { var streamData_1 = activity.createStream(data_distance_1.DataDistance.type).getData(); // Creating does not add it to activity just presets the resolution to 1s var distance_1 = 0; streamData_1[0] = distance_1; // Force first distance sample to be equal to 0 instead of null activity .getPositionData() .reduce(function (prevPosition, position, index, array) { if (!position) { return prevPosition; } if (prevPosition && position) { distance_1 += Number(_this.geoLibAdapter.getDistance([prevPosition, position]).toFixed(1)); } streamData_1[index] = distance_1; return position; }); if (!activity.hasStreamData(data_distance_1.DataDistance.type)) { activity.addStream(new stream_1.Stream(data_distance_1.DataDistance.type, streamData_1)); } if (!activity.hasStreamData(data_gnss_distance_1.DataGNSSDistance.type)) { activity.addStream(new stream_1.Stream(data_gnss_distance_1.DataGNSSDistance.type, streamData_1)); } if (!activity.hasStreamData(data_speed_1.DataSpeed.type)) { var speedStreamData_1 = activity.createStream(data_speed_1.DataSpeed.type).getData(); activity.getStreamDataByDuration(data_distance_1.DataDistance.type).forEach(function (distanceData, index) { if (distanceData.value === 0) { speedStreamData_1[index] = 0; return; } if (distanceData.value !== null && isFinite(distanceData.time) && distanceData.time > 0) { speedStreamData_1[index] = Math.round((distanceData.value / (distanceData.time / 1000)) * 100) / 100; return; } speedStreamData_1[index] = null; }); activity.addStream(new stream_1.Stream(data_speed_1.DataSpeed.type, speedStreamData_1)); } } // Check if we can get a grade stream if (!activity.hasStreamData(data_grade_1.DataGrade.type) && activity.hasStreamData(data_distance_1.DataDistance.type) && activity.hasStreamData(data_altitude_1.DataAltitude.type)) { var distanceData = activity.getStreamData(data_distance_1.DataDistance.type); var altitudeData = activity.getStreamData(data_altitude_1.DataAltitude.type); var gradeStreamData = grade_calculator_1.GradeCalculator.computeGradeStream(distanceData, altitudeData); activity.addStream(new stream_1.Stream(data_grade_1.DataGrade.type, gradeStreamData)); } // Get a grade adjusted speed (the model applies to running only) if ((activity_types_1.ActivityTypesHelper.getActivityGroupForActivityType(activity.type) === activity_types_1.ActivityTypeGroups.Running || activity_types_1.ActivityTypesHelper.getActivityGroupForActivityType(activity.type) === activity_types_1.ActivityTypeGroups.TrailRunning) && !activity.hasStreamData(data_grade_adjusted_speed_1.DataGradeAdjustedSpeed.type) && activity.hasStreamData(data_grade_1.DataGrade.type) && activity.hasStreamData(data_speed_1.DataSpeed.type)) { var speedStreamData = activity.getStreamData(data_speed_1.DataSpeed.type); var gradeStreamData_1 = activity.getStreamData(data_grade_1.DataGrade.type); var gradeAdjustedSpeedData = speedStreamData.map(function (value, index) { return value === null ? null : Math.round(grade_calculator_1.GradeCalculator.estimateAdjustedSpeed(value, gradeStreamData_1[index] || 0) * 100) / 100; }); activity.addStream(new stream_1.Stream(data_grade_adjusted_speed_1.DataGradeAdjustedSpeed.type, gradeAdjustedSpeedData)); } if (activity.hasStreamData(data_power_1.DataPower.type) && activity.hasStreamData(data_right_balance_1.DataRightBalance.type) && !activity.hasStreamData(data_power_right_1.DataPowerRight.type)) { var rightPowerStream = activity.createStream(data_power_right_1.DataPowerRight.type); var powerStreamData_1 = activity.getStreamData(data_power_1.DataPower.type); var rightBalanceStreamData = activity.getStreamData(data_right_balance_1.DataRightBalance.type); rightPowerStream.setData(rightBalanceStreamData.reduce(function (accu, streamData, index) { var powerStreamDataItem = powerStreamData_1[index]; if (streamData === null || !powerStreamData_1 || powerStreamDataItem === null) { return accu; } accu[index] = (streamData / 100) * powerStreamDataItem; return accu; }, [])); activity.addStream(rightPowerStream); } if (activity.hasStreamData(data_power_1.DataPower.type) && activity.hasStreamData(data_left_balance_1.DataLeftBalance.type) && !activity.hasStreamData(data_power_left_1.DataPowerLeft.type)) { var leftPowerStream = activity.createStream(data_power_left_1.DataPowerLeft.type); var powerStreamData_2 = activity.getStreamData(data_power_1.DataPower.type); var leftBalanceStreamData = activity.getStreamData(data_left_balance_1.DataLeftBalance.type); leftPowerStream.setData(leftBalanceStreamData.reduce(function (accu, streamData, index) { var powerStreamDataItem = powerStreamData_2[index]; if (streamData === null || !powerStreamData_2 || powerStreamDataItem === null) { return accu; } accu[index] = (streamData / 100) * powerStreamDataItem; return accu; }, [])); activity.addStream(leftPowerStream); } return activity; }; /** * Back and forth fills an activity's stream data so they can be more "tree" like * It does this for: * * [DataAltitude.type, * DataHeartRate.type, * DataCadence.type, * DataDistance.type] * * Example * * Distance[0, 10, 30, 40, 50,null,60] #null here is legit eg missing record * Altitude[100, 101, null, 103, null, null, 106] * Should be * Altitude[100,101,101,103,103,103,106] * * @param activity */ ActivityUtilities.addMissingDataToStreams = function (activity) { /** * This tries to align data with Strava. * Strava fills HR alti cadence with the last value. * For Power and temperature it doesn't but keeps nulls. * However, if you keep nulls for paused portions then strava doens't give back null * that typically indicates a sensor disconnect I suppose. */ var streamTypesToBackAndForthFill = [ data_altitude_1.DataAltitude.type, data_heart_rate_1.DataHeartRate.type, data_cadence_1.DataCadence.type, data_distance_1.DataDistance.type // DataSpeed.type, @todo should we be backfilling speed? ]; // First generate the time stream var timeStream = activity.generateTimeStream(); /** * We do a second pass here and we add missing data on crossing time indexes * for example: * Time[0,1,2,3,4,5,7] * Distance[0, 10, 30, 40, 50,null,60] #null here is legit eg missing record * Altitude[100, 101, null, 103, null, null, 106] * Should be * Altitude[100,101,101,103,103,103,106] */ activity .getAllStreams() .filter(function (stream) { return streamTypesToBackAndForthFill.indexOf(stream.type) !== -1; }) .forEach(function (stream) { // Find the first sample value var currentValue = stream.getData(true, true)[0]; // The time stream will always have more length than each stream when not back/forthfilled var timeStreamData = timeStream.getData(); stream.setData(timeStreamData.reduce(function (data, time, timeIndex) { // If there is no timeslot put whatever was if (!helpers_1.isNumber(time)) { data.push(stream.getData()[timeIndex]); return data; } // We have a time slot here on ... (for the first run, old is the very first next) // If it's a number set the current , else leave it to old to forth fill if (helpers_1.isNumber(stream.getData()[time])) { currentValue = stream.getData()[time]; } // Fill the current or old... data.push(currentValue); return data; }, [])); }); /** * @todo * Linear fill distance where: * a) There is not distance but it's not paused * b) There is no corespoding lat/long but there is distace (aka distance = not trusted) * About B I am not sure. That is because if there is for example an internal accelerometer * that reports better this can help with pace and other things. Even for GAP */ }; ActivityUtilities.getActivityDataTypeGainOrLoss = function (activity, streamType, gain, startDate, endDate, minDiff) { return this.getGainOrLoss(activity.getSquashedStreamData(streamType, startDate, endDate), gain, minDiff); }; ActivityUtilities.getActivityDataTypeMinOrMax = function (activity, streamType, max, startDate, endDate) { var data = activity .getSquashedStreamData(streamType, startDate, endDate) .filter(function (streamData) { return streamData !== Infinity && streamData !== -Infinity; }); if (max) { return this.getMax(data); } return this.getMin(data); }; /** * Generates the stats for an activity * @todo move to factory with next version * @param activity */ ActivityUtilities.generateMissingStatsForActivity = function (activity) { // If there is no distance or distance for some reason is 0 var activityDistanceStat = activity.getStat(data_distance_1.DataDistance.type); if (!activityDistanceStat || activityDistanceStat.getValue() === 0) { var distance = 0; if (activity.hasStreamData(data_distance_1.DataDistance.type)) { var distanceData = activity.getSquashedStreamData(data_distance_1.DataDistance.type); distance = distanceData[distanceData.length - 1] - distanceData[0] || 0; } else if (activity.hasStreamData(data_longitude_degrees_1.DataLongitudeDegrees.type) && activity.hasStreamData(data_latitude_degrees_1.DataLatitudeDegrees.type)) { distance = this.calculateTotalDistanceForActivity(activity, activity.startDate, activity.endDate); } activity.addStat(new data_distance_1.DataDistance(distance)); } if (!activity.getStat(data_gnss_distance_1.DataGNSSDistance.type) && activity.hasStreamData(data_gnss_distance_1.DataGNSSDistance.type)) { activity.addStat(new data_gnss_distance_1.DataGNSSDistance(activity.getSquashedStreamData(data_gnss_distance_1.DataGNSSDistance.type)[activity.getSquashedStreamData(data_gnss_distance_1.DataGNSSDistance.type).length - 1])); } // Ascent (altitude gain) if (!activity.getStat(data_ascent_1.DataAscent.type) && activity.hasStreamData(data_altitude_1.DataAltitude.type)) { activity.addStat(new data_ascent_1.DataAscent(this.getActivityDataTypeGain(activity, data_altitude_1.DataAltitude.type))); } // Descent (altitude loss) if (!activity.getStat(data_descent_1.DataDescent.type) && activity.hasStreamData(data_altitude_1.DataAltitude.type)) { activity.addStat(new data_descent_1.DataDescent(this.getActivityDataTypeLoss(activity, data_altitude_1.DataAltitude.type))); } // Altitude Max if (!activity.getStat(data_altitude_max_1.DataAltitudeMax.type) && activity.hasStreamData(data_altitude_1.DataAltitude.type)) { activity.addStat(new data_altitude_max_1.DataAltitudeMax(this.getDataTypeMax(activity, data_altitude_1.DataAltitude.type))); } // Altitude Min if (!activity.getStat(data_altitude_min_1.DataAltitudeMin.type) && activity.hasStreamData(data_altitude_1.DataAltitude.type)) { activity.addStat(new data_altitude_min_1.DataAltitudeMin(this.getDataTypeMin(activity, data_altitude_1.DataAltitude.type))); } // Altitude Avg if (!activity.getStat(data_altitude_avg_1.DataAltitudeAvg.type) && activity.hasStreamData(data_altitude_1.DataAltitude.type)) { activity.addStat(new data_altitude_avg_1.DataAltitudeAvg(this.getDataTypeAvg(activity, data_altitude_1.DataAltitude.type))); } // Altitude start if (!activity.getStat(data_start_altitude_1.DataStartAltitude.type) && activity.hasStreamData(data_altitude_1.DataAltitude.type)) { activity.addStat(new data_start_altitude_1.DataStartAltitude(this.getDataTypeFirst(activity, data_altitude_1.DataAltitude.type))); } // Altitude end if (!activity.getStat(data_end_altitude_1.DataEndAltitude.type) && activity.hasStreamData(data_altitude_1.DataAltitude.type)) { activity.addStat(new data_end_altitude_1.DataEndAltitude(this.getDataTypeLast(activity, data_altitude_1.DataAltitude.type))); } // Heart Rate Max if (!activity.getStat(data_heart_rate_max_1.DataHeartRateMax.type) && activity.hasStreamData(data_heart_rate_1.DataHeartRate.type)) { activity.addStat(new data_heart_rate_max_1.DataHeartRateMax(this.getDataTypeMax(activity, data_heart_rate_1.DataHeartRate.type))); } // Heart Rate Min if (!activity.getStat(data_heart_rate_min_1.DataHeartRateMin.type) && activity.hasStreamData(data_heart_rate_1.DataHeartRate.type)) { activity.addStat(new data_heart_rate_min_1.DataHeartRateMin(this.getDataTypeMin(activity, data_heart_rate_1.DataHeartRate.type))); } // Heart Rate Avg if (!activity.getStat(data_heart_rate_avg_1.DataHeartRateAvg.type) && activity.hasStreamData(data_heart_rate_1.DataHeartRate.type)) { activity.addStat(new data_heart_rate_avg_1.DataHeartRateAvg(this.getDataTypeAvg(activity, data_heart_rate_1.DataHeartRate.type))); } // Cadence Max if (!activ