import * as _ from "lodash" ; import * as moment from "moment" ; import { Moment } from "moment" ; import { Injectable } from "@angular/core" ; import { ActivityService } from "../../../shared/services/activity/activity.service" ; import { DayStressModel } from "../models/day-stress.model" ; import { DayFitnessTrendModel } from "../models/day-fitness-trend.model" ; import { FitnessPreparedActivityModel } from "../models/fitness-prepared-activity.model" ; import { HeartRateImpulseMode } from "../enums/heart-rate-impulse-mode.enum" ; import { AppError } from "../../../shared/models/app-error.model" ; import { SyncedActivityModel } from "@elevate/shared/models" ; import { FitnessTrendConfigModel } from "../models/fitness-trend-config.model" ; @ Injectable ( ) export class FitnessService { public static readonly FUTURE_DAYS_PREVIEW : number = 14 ; public static readonly DEFAULT_LTHR_KARVONEN_HRR_FACTOR : number = 0.85 ; constructor ( public activityService ) { } param { FitnessTrendConfigModel } fitnessTrendConfigModel param { boolean } powerMeterEnable param { boolean } swimEnable param { string [ ] } skipActivityTypes returns { Promise < FitnessPreparedActivityModel [ ] > } public prepare ( fitnessTrendConfigModel , powerMeterEnable , swimEnable , skipActivityTypes ? ) { return new Promise ( ( resolve , reject ) => { return this . activityService . fetch ( ) . then ( ( activities ) => { if ( _ . isEmpty ( activities ) || activities . length === 0 ) { reject ( new AppError ( AppError . FT_NO_ACTIVITIES , "No activities available to generate the fitness trend" ) ) ; } activities = this . filterActivities ( activities , fitnessTrendConfigModel . ignoreBeforeDate , fitnessTrendConfigModel . ignoreActivityNamePatterns ) ; if ( _ . isEmpty ( activities ) || activities . length === 0 ) { reject ( new AppError ( AppError . FT_ALL_ACTIVITIES_FILTERED , "No activities available. They all have been filtered. Unable to generate the fitness trend." ) ) ; } const fitnessPreparedActivities = [ ] ; _ . forEach ( activities , ( activity ) => { if ( ! _ . isEmpty ( skipActivityTypes ) && _ . indexOf ( skipActivityTypes , activity . type ) !== - 1 ) { return ; } if ( ! activity . athleteSnapshot ) { reject ( new AppError ( AppError . FT_NO_ACTIVITY_ATHLETE_MODEL , "Some of your synced activities are missing athlete settings. To fix that check " + "your athlete settings and \"clear and re-sync your activities\"" ) ) ; return ; } const hasHeartRateData = activity . extendedStats && ! _ . isEmpty ( activity . extendedStats . heartRateData ) && ( ( _ . isNumber ( activity . extendedStats . heartRateData . TRIMP ) && fitnessTrendConfigModel . heartRateImpulseMode === HeartRateImpulseMode . TRIMP ) || ( _ . isNumber ( activity . extendedStats . heartRateData . HRSS ) && fitnessTrendConfigModel . heartRateImpulseMode === HeartRateImpulseMode . HRSS ) ) ; const hasPowerData = ( activity . type === "Ride" || activity . type === "VirtualRide" || activity . type === "EBikeRide" ) && powerMeterEnable && fitnessTrendConfigModel . heartRateImpulseMode !== HeartRateImpulseMode . TRIMP && _ . isNumber ( activity . athleteSnapshot . athleteSettings . cyclingFtp ) && activity . extendedStats && activity . extendedStats . powerData && ( activity . extendedStats . powerData . hasPowerMeter || fitnessTrendConfigModel . allowEstimatedPowerStressScore ) && _ . isNumber ( activity . extendedStats . powerData . powerStressScore ) ; const hasRunningData = ( activity . type === "Run" || activity . type === "VirtualRun" ) && fitnessTrendConfigModel . heartRateImpulseMode !== HeartRateImpulseMode . TRIMP && _ . isNumber ( activity . athleteSnapshot . athleteSettings . runningFtp ) && activity . extendedStats && activity . extendedStats . paceData && _ . isNumber ( activity . extendedStats . paceData . runningStressScore ) && fitnessTrendConfigModel . allowEstimatedRunningStressScore ; const hasSwimmingData = ( swimEnable && _ . isNumber ( activity . athleteSnapshot . athleteSettings . swimFtp ) && activity . athleteSnapshot . athleteSettings . swimFtp > 0 && activity . type === "Swim" && fitnessTrendConfigModel . heartRateImpulseMode !== HeartRateImpulseMode . TRIMP && _ . isNumber ( activity . distance_raw ) && _ . isNumber ( activity . moving_time_raw ) && activity . moving_time_raw > 0 ) ; const momentStartTime = moment ( activity . start_time ) ; const fitnessReadyActivity = { id : activity . id , date : momentStartTime . toDate ( ) , timestamp : momentStartTime . toDate ( ) . getTime ( ) , dayOfYear : momentStartTime . dayOfYear ( ) , year : momentStartTime . year ( ) , type : activity . type , hasPowerMeter : ( activity . extendedStats && activity . extendedStats . powerData && activity . extendedStats . powerData . hasPowerMeter ) , name : activity . name , athleteSnapshot : activity . athleteSnapshot } ; if ( hasHeartRateData ) { if ( fitnessTrendConfigModel . heartRateImpulseMode === HeartRateImpulseMode . TRIMP ) { fitnessReadyActivity . trainingImpulseScore = activity . extendedStats . heartRateData . TRIMP ; } else if ( fitnessTrendConfigModel . heartRateImpulseMode === HeartRateImpulseMode . HRSS ) { fitnessReadyActivity . heartRateStressScore = activity . extendedStats . heartRateData . HRSS ; } } if ( hasPowerData ) { fitnessReadyActivity . powerStressScore = activity . extendedStats . powerData . powerStressScore ; } if ( hasRunningData ) { fitnessReadyActivity . runningStressScore = activity . extendedStats . paceData . runningStressScore ; } if ( hasSwimmingData ) { fitnessReadyActivity . swimStressScore = this . computeSwimStressScore ( activity . distance_raw , activity . moving_time_raw , activity . elapsed_time_raw , activity . athleteSnapshot . athleteSettings . swimFtp ) ; } fitnessPreparedActivities . push ( fitnessReadyActivity ) ; } ) ; resolve ( fitnessPreparedActivities ) ; } ) ; } ) ; } param { FitnessTrendConfigModel } fitnessTrendConfigModel param { boolean } powerMeterEnable param { boolean } swimEnable param { string [ ] } skipActivityTypes returns { Promise < DayStressModel [ ] > } public generateDailyStress ( fitnessTrendConfigModel , powerMeterEnable , swimEnable , skipActivityTypes ? ) { return new Promise ( ( resolve , reject ) => { this . prepare ( fitnessTrendConfigModel , powerMeterEnable , swimEnable , skipActivityTypes ) . then ( ( fitnessPreparedActivities ) => { const startDay = moment ( _ . first ( fitnessPreparedActivities ) . date ) . subtract ( 1 , "days" ) . startOf ( "day" ) ; const today = this . getTodayMoment ( ) . startOf ( "day" ) ; const dailyActivity = [ ] ; const currentDay = moment ( startDay ) . clone ( ) ; while ( currentDay . isSameOrBefore ( today , "day" ) ) { const dayStress = this . dayStressOnDate ( currentDay , fitnessPreparedActivities ) ; dailyActivity . push ( dayStress ) ; if ( currentDay . isSame ( today , "day" ) ) { break ; } currentDay . add ( 1 , "days" ) ; } this . appendPreviewDaysToDailyActivity ( currentDay , dailyActivity ) ; resolve ( dailyActivity ) ; } , error => reject ( error ) ) ; } ) ; } param { number } distance param { number } movingTime param { number } elaspedTime param { number } swimFtp returns { number } public computeSwimStressScore ( distance , movingTime , elaspedTime , swimFtp ) { const normalizedSwimSpeed = distance / ( movingTime / 60 ) ; const swimIntensity = normalizedSwimSpeed / swimFtp ; return Math . pow ( swimIntensity , 3 ) * ( elaspedTime / 3600 ) * 100 ; } param { FitnessTrendConfigModel } fitnessTrendConfigModel param { boolean } isPowerMeterEnabled param { boolean } isSwimEnabled param { string [ ] } skipActivityTypes returns { Promise < DayFitnessTrendModel [ ] > } public computeTrend ( fitnessTrendConfigModel , isPowerMeterEnabled , isSwimEnabled , skipActivityTypes ? ) { return new Promise ( ( resolve , reject ) => { this . generateDailyStress ( fitnessTrendConfigModel , isPowerMeterEnabled , isSwimEnabled , skipActivityTypes ) . then ( ( dailyActivity ) => { let ctl , atl , tsb ; const fitnessTrend = [ ] ; let previousDayFitnessTrend = null ; _ . forEach ( dailyActivity , ( dayStress , index ) => { const isPreStartDay = ( index === 0 ) ; if ( isPreStartDay ) { if ( fitnessTrendConfigModel . initializedFitnessTrendModel ) { ctl = ( ! _ . isNull ( fitnessTrendConfigModel . initializedFitnessTrendModel . ctl ) ) ? fitnessTrendConfigModel . initializedFitnessTrendModel . ctl : 0 ; atl = ( ! _ . isNull ( fitnessTrendConfigModel . initializedFitnessTrendModel . atl ) ) ? fitnessTrendConfigModel . initializedFitnessTrendModel . atl : 0 ; tsb = ctl - atl ; } else { ctl = atl = tsb = 0 ; } } else { tsb = ctl - atl ; ctl = ctl + ( dayStress . finalStressScore - ctl ) * ( 1 - Math . exp ( - 1 / 42 ) ) ; atl = atl + ( dayStress . finalStressScore - atl ) * ( 1 - Math . exp ( - 1 / 7 ) ) ; } let dayFitnessTrend ; if ( previousDayFitnessTrend ) { dayFitnessTrend = new DayFitnessTrendModel ( dayStress , ctl , atl , tsb , previousDayFitnessTrend . ctl , previousDayFitnessTrend . atl , previousDayFitnessTrend . tsb ) ; } else { dayFitnessTrend = new DayFitnessTrendModel ( dayStress , ctl , atl , tsb ) ; } if ( _ . isNumber ( dayStress . heartRateStressScore ) && dayStress . heartRateStressScore > 0 ) { dayFitnessTrend . heartRateStressScore = dayStress . heartRateStressScore ; } if ( _ . isNumber ( dayStress . trainingImpulseScore ) && dayStress . trainingImpulseScore > 0 ) { dayFitnessTrend . trainingImpulseScore = dayStress . trainingImpulseScore ; } if ( _ . isNumber ( dayStress . powerStressScore ) && dayStress . powerStressScore > 0 ) { dayFitnessTrend . powerStressScore = dayStress . powerStressScore ; } if ( _ . isNumber ( dayStress . runningStressScore ) && dayStress . runningStressScore > 0 ) { dayFitnessTrend . runningStressScore = dayStress . runningStressScore ; } if ( _ . isNumber ( dayStress . swimStressScore ) && dayStress . swimStressScore > 0 ) { dayFitnessTrend . swimStressScore = dayStress . swimStressScore ; } if ( _ . isNumber ( dayStress . finalStressScore ) && dayStress . finalStressScore > 0 ) { dayFitnessTrend . finalStressScore = dayStress . finalStressScore ; } previousDayFitnessTrend = dayFitnessTrend ; fitnessTrend . push ( dayFitnessTrend ) ; } ) ; resolve ( fitnessTrend ) ; } , error => reject ( error ) ) ; } ) ; } param { moment . Moment } startFrom param { DayStressModel [ ] } dailyActivity public appendPreviewDaysToDailyActivity ( startFrom , dailyActivity ) { for ( let i = 0 ; i < FitnessService . FUTURE_DAYS_PREVIEW ; i ++ ) { const futureDate = startFrom . add ( 1 , "days" ) . startOf ( "day" ) . toDate ( ) ; const dayActivity = new DayStressModel ( futureDate , true ) ; dailyActivity . push ( dayActivity ) ; } } param { moment . Moment } currentDay param { FitnessPreparedActivityModel [ ] } fitnessPreparedActivities returns { DayStressModel } public dayStressOnDate ( currentDay , fitnessPreparedActivities ) { const foundActivitiesThatDay = _ . filter ( fitnessPreparedActivities , { year : currentDay . year ( ) , dayOfYear : currentDay . dayOfYear ( ) , } ) ; const dayActivity = new DayStressModel ( currentDay . toDate ( ) , false ) ; if ( foundActivitiesThatDay . length > 0 ) { _ . forEach ( foundActivitiesThatDay , ( activity ) => { dayActivity . ids . push ( activity . id ) ; dayActivity . activitiesName . push ( activity . name ) ; dayActivity . types . push ( activity . type ) ; dayActivity . athleteSnapshot = activity . athleteSnapshot ; const hasPowerStressScore = _ . isNumber ( activity . powerStressScore ) ; const hasHeartRateStressScore = _ . isNumber ( activity . heartRateStressScore ) ; const hasRunningStressScore = _ . isNumber ( activity . runningStressScore ) ; const hasSwimStressScore = _ . isNumber ( activity . swimStressScore ) ; if ( hasPowerStressScore ) { if ( ! dayActivity . powerStressScore ) { dayActivity . powerStressScore = 0 ; } dayActivity . powerStressScore += activity . powerStressScore ; } if ( hasHeartRateStressScore ) { if ( ! dayActivity . heartRateStressScore ) { dayActivity . heartRateStressScore = 0 ; } dayActivity . heartRateStressScore += activity . heartRateStressScore ; } if ( hasRunningStressScore ) { if ( ! dayActivity . runningStressScore ) { dayActivity . runningStressScore = 0 ; } dayActivity . runningStressScore += activity . runningStressScore ; } if ( _ . isNumber ( activity . trainingImpulseScore ) ) { if ( ! dayActivity . trainingImpulseScore ) { dayActivity . trainingImpulseScore = 0 ; } dayActivity . trainingImpulseScore += activity . trainingImpulseScore ; } if ( hasSwimStressScore ) { if ( ! dayActivity . swimStressScore ) { dayActivity . swimStressScore = 0 ; } dayActivity . swimStressScore += activity . swimStressScore ; } if ( activity . powerStressScore && activity . hasPowerMeter ) { dayActivity . finalStressScore += activity . powerStressScore ; } else if ( activity . heartRateStressScore ) { dayActivity . finalStressScore += activity . heartRateStressScore ; } else if ( activity . trainingImpulseScore ) { dayActivity . finalStressScore += activity . trainingImpulseScore ; } else if ( activity . powerStressScore && ! activity . hasPowerMeter ) { dayActivity . finalStressScore += activity . powerStressScore ; } else if ( activity . runningStressScore ) { dayActivity . finalStressScore += activity . runningStressScore ; } else if ( activity . swimStressScore ) { dayActivity . finalStressScore += activity . swimStressScore ; } } ) ; } return dayActivity ; } param { SyncedActivityModel [ ] } activities param { string } ignoreBeforeDate param { string [ ] } ignoreActivityNamePatterns returns { SyncedActivityModel [ ] } public filterActivities ( activities , ignoreBeforeDate , ignoreActivityNamePatterns ) { const hasIgnoreBeforeDate = ! _ . isEmpty ( ignoreBeforeDate ) ; const hasIgnoreActivityNamePatterns = ignoreActivityNamePatterns && ignoreActivityNamePatterns . length > 0 ; if ( hasIgnoreBeforeDate || hasIgnoreActivityNamePatterns ) { activities = _ . filter ( activities , ( activity ) => { if ( hasIgnoreBeforeDate ) { const isActivityAfterIgnoreDate = moment ( activity . start_time ) . startOf ( "day" ) . isSameOrAfter ( ignoreBeforeDate ) ; if ( ! isActivityAfterIgnoreDate ) { return false ; } } if ( hasIgnoreActivityNamePatterns ) { let ignoreActivityFromPattern = false ; _ . forEach ( ignoreActivityNamePatterns , ( ignorePattern ) => { const hasPattern = activity . name . indexOf ( ignorePattern ) !== - 1 ; if ( hasPattern ) { ignoreActivityFromPattern = true ; return ; } } ) ; if ( ignoreActivityFromPattern ) { return false ; } } return true ; } ) ; } return activities ; } public getTodayMoment ( ) { return moment ( ) ; } }