import { Injectable } from "@angular/core" ; import { YearProgressModel } from "../models/year-progress.model" ; import * as _ from "lodash" ; import * as moment from "moment" ; import { Moment } from "moment" ; import { YearProgressActivityModel } from "../models/year-progress-activity.model" ; import { ProgressModel } from "../models/progress.model" ; import { ActivityCountByTypeModel } from "../models/activity-count-by-type.model" ; import { ProgressAtDayModel } from "../models/progress-at-date.model" ; import { ProgressType } from "../enums/progress-type.enum" ; import { Subject } from "rxjs" ; import { SyncedActivityModel } from "@elevate/shared/models" ; import { Constant } from "@elevate/shared/constants" ; import { YearToDateProgressPresetModel } from "../models/year-to-date-progress-preset.model" ; import { YearProgressPresetDao } from "../dao/year-progress-preset.dao" ; import { AppError } from "../../../shared/models/app-error.model" ; import { YearProgressTypeModel } from "../models/year-progress-type.model" ; import { TargetProgressModel } from "../models/target-progress.model" ; import { ProgressMode } from "../enums/progress-mode.enum" ; import { ProgressConfig } from "../interfaces/progress-config" ; import { YearToDateProgressConfigModel } from "../models/year-to-date-progress-config.model" ; import { RollingProgressConfigModel } from "../models/rolling-progress-config.model" ; import { RollingProgressPresetModel } from "../models/rolling-progress-preset.model" ; @ Injectable ( ) export class YearProgressService { public static readonly ERROR_NO_SYNCED_ACTIVITY_MODELS : string = "Empty SyncedActivityModels" ; public static readonly ERROR_NO_TYPES_FILTER : string = "Empty types filter" ; public static readonly ERROR_NO_YEAR_PROGRESS_MODELS : string = "Empty YearProgressModels from given activity types" ; public momentWatched : Moment ; public momentWatchedChanges : Subject < Moment > ; constructor ( public yearProgressPresetDao ) { this . momentWatched = this . getTodayMoment ( ) . clone ( ) . startOf ( "day" ) ; this . momentWatchedChanges = new Subject < Moment > ( ) ; } public static provideProgressTypes ( isMetric ) { return [ new YearProgressTypeModel ( ProgressType . DISTANCE , "Distance" , ( isMetric ) ? "kilometers" : "miles" , ( isMetric ) ? "km" : "mi" ) , new YearProgressTypeModel ( ProgressType . TIME , "Time" , "hours" , "h" ) , new YearProgressTypeModel ( ProgressType . ELEVATION , "Elevation" , ( isMetric ) ? "meters" : "feet" , ( isMetric ) ? "m" : "ft" ) , new YearProgressTypeModel ( ProgressType . COUNT , "Count" ) ] ; } param config param isMetric param syncedActivityModels public progressions ( config , isMetric , syncedActivityModels ) { if ( _ . isEmpty ( syncedActivityModels ) ) { throw new Error ( YearProgressService . ERROR_NO_SYNCED_ACTIVITY_MODELS ) ; } if ( _ . isEmpty ( config . activityTypes ) ) { throw new Error ( YearProgressService . ERROR_NO_TYPES_FILTER ) ; } let yearProgressActivities = this . filterSyncedActivityModelAlongTypes ( syncedActivityModels , config . activityTypes ) ; if ( _ . isEmpty ( yearProgressActivities ) ) { throw new Error ( YearProgressService . ERROR_NO_YEAR_PROGRESS_MODELS ) ; } yearProgressActivities = _ . sortBy ( yearProgressActivities , ( activity ) => { return activity . start_time ; } ) ; const todayMoment = this . getTodayMoment ( ) ; const fromMoment = moment ( _ . first ( syncedActivityModels ) . start_time ) . startOf ( "year" ) ; const toMoment = this . getTodayMoment ( ) . clone ( ) . endOf ( "year" ) . endOf ( "day" ) ; return ( config . mode === ProgressMode . YEAR_TO_DATE ) ? this . computeYearToDateSumProgressions ( config as YearToDateProgressConfigModel , isMetric , fromMoment , toMoment , todayMoment , yearProgressActivities ) : this . computeRollingSumProgressions ( config as RollingProgressConfigModel , isMetric , fromMoment , toMoment , todayMoment , yearProgressActivities ) ; } param config param isMetric param fromMoment param toMoment param todayMoment param yearProgressActivities public computeYearToDateSumProgressions ( config , isMetric , fromMoment , toMoment , todayMoment , yearProgressActivities ) { const yearProgressions = [ ] ; let lastProgress = null ; const currentDayMoment = moment ( fromMoment ) ; let currentYearProgress = null ; while ( currentDayMoment . isSameOrBefore ( toMoment ) ) { const currentYear = currentDayMoment . year ( ) ; let progress = null ; const isNewYearProgress = ( ! _ . find ( yearProgressions , { year : currentYear } ) ) ; if ( isNewYearProgress ) { lastProgress = null ; currentYearProgress = { mode : config . mode , year : currentYear , progressions : [ ] , } ; progress = new ProgressModel ( currentDayMoment . year ( ) , currentDayMoment . dayOfYear ( ) , 0 , 0 , 0 , 0 ) ; yearProgressions . push ( currentYearProgress ) ; } else { progress = new ProgressModel ( currentDayMoment . year ( ) , currentDayMoment . dayOfYear ( ) , lastProgress . distance , lastProgress . time , lastProgress . elevation , lastProgress . count ) ; } const activitiesFound = _ . filter < YearProgressActivityModel > ( yearProgressActivities , { year : currentDayMoment . year ( ) , dayOfYear : currentDayMoment . dayOfYear ( ) } ) ; if ( activitiesFound . length > 0 ) { for ( let i = 0 ; i < activitiesFound . length ; i ++ ) { if ( ( ! config . includeCommuteRide && activitiesFound [ i ] . commute ) || ( ! config . includeIndoorRide && activitiesFound [ i ] . trainer ) ) { continue ; } progress . distance += activitiesFound [ i ] . distance_raw ; progress . time += activitiesFound [ i ] . moving_time_raw ; progress . elevation += activitiesFound [ i ] . elevation_gain_raw ; progress . count ++ ; } } lastProgress = _ . clone ( progress ) ; progress . isFuture = ( ! currentDayMoment . isSameOrBefore ( todayMoment ) ) ; currentYearProgress . progressions . push ( this . prepareAlongSystemUnits ( progress , isMetric ) ) ; currentDayMoment . add ( 1 , "days" ) ; } return yearProgressions ; } param config param isMetric param fromMoment param toMoment param todayMoment param yearProgressActivities public computeRollingSumProgressions ( config , isMetric , fromMoment , toMoment , todayMoment , yearProgressActivities ) { const yearProgressions = [ ] ; const currentDayMoment = moment ( fromMoment ) ; let currentYearProgress = null ; let isRollingBufferSizeReached = false ; let rollingBufferSize = 0 ; const rollingBuffers = { distance : [ ] , time : [ ] , elevation : [ ] , count : [ ] } ; while ( currentDayMoment . isSameOrBefore ( toMoment ) ) { if ( ! isRollingBufferSizeReached ) { ( rollingBufferSize === config . rollingDays ) ? isRollingBufferSizeReached = true : rollingBufferSize ++ ; } const currentYear = currentDayMoment . year ( ) ; const activitiesFound = _ . filter < YearProgressActivityModel > ( yearProgressActivities , { year : currentDayMoment . year ( ) , dayOfYear : currentDayMoment . dayOfYear ( ) } ) ; const onDayTotals = { distance : 0 , time : 0 , elevation : 0 , count : 0 } ; const hasCurrentDayActivities = ( activitiesFound . length > 0 ) ; if ( hasCurrentDayActivities ) { for ( let i = 0 ; i < activitiesFound . length ; i ++ ) { if ( ( ! config . includeCommuteRide && activitiesFound [ i ] . commute ) || ( ! config . includeIndoorRide && activitiesFound [ i ] . trainer ) ) { continue ; } onDayTotals . distance += activitiesFound [ i ] . distance_raw ; onDayTotals . time += activitiesFound [ i ] . moving_time_raw ; onDayTotals . elevation += activitiesFound [ i ] . elevation_gain_raw ; onDayTotals . count ++ ; } } rollingBuffers . distance . push ( onDayTotals . distance ) ; rollingBuffers . time . push ( onDayTotals . time ) ; rollingBuffers . elevation . push ( onDayTotals . elevation ) ; rollingBuffers . count . push ( onDayTotals . count ) ; const rollingSum = { distance : ( _ . sum ( rollingBuffers . distance ) - ( ( isRollingBufferSizeReached ) ? rollingBuffers . distance . shift ( ) : 0 ) ) , time : ( _ . sum ( rollingBuffers . time ) - ( ( ( isRollingBufferSizeReached ) ? rollingBuffers . time . shift ( ) : 0 ) ) ) , elevation : ( _ . sum ( rollingBuffers . elevation ) - ( ( isRollingBufferSizeReached ) ? rollingBuffers . elevation . shift ( ) : 0 ) ) , count : ( _ . sum ( rollingBuffers . count ) - ( ( isRollingBufferSizeReached ) ? rollingBuffers . count . shift ( ) : 0 ) ) , } ; const progression = new ProgressModel ( currentDayMoment . year ( ) , currentDayMoment . dayOfYear ( ) , rollingSum . distance , rollingSum . time , rollingSum . elevation , rollingSum . count ) ; const isNewYearProgress = ( ! _ . find ( yearProgressions , { year : currentYear } ) ) ; if ( isNewYearProgress ) { currentYearProgress = { mode : config . mode , year : currentYear , progressions : [ ] , } ; yearProgressions . push ( currentYearProgress ) ; } progression . isFuture = ( ! currentDayMoment . isSameOrBefore ( todayMoment ) ) ; currentYearProgress . progressions . push ( this . prepareAlongSystemUnits ( progression , isMetric ) ) ; currentDayMoment . add ( 1 , "days" ) ; } return yearProgressions ; } param progression param isMetric public prepareAlongSystemUnits ( progression , isMetric ) { let distance = progression . distance / 1000 ; if ( ! isMetric ) { distance *= Constant . KM_TO_MILE_FACTOR ; } progression . distance = Math . round ( distance ) ; let elevation = progression . elevation ; if ( ! isMetric ) { elevation *= Constant . METER_TO_FEET_FACTOR ; } progression . elevation = Math . round ( elevation ) ; progression . time = progression . time / 3600 ; return progression ; } param year param targetValue public yearToDateTargetProgression ( year , targetValue ) { const targetProgressModels = [ ] ; const daysInYear = ( moment ( ) . year ( year ) . isLeapYear ( ) ) ? 366 : 365 ; const progressStep = targetValue / daysInYear ; let targetProgress = progressStep ; for ( let day = 1 ; day <= daysInYear ; day ++ ) { targetProgressModels . push ( { dayOfYear : day , value : targetProgress } ) ; targetProgress += progressStep ; } return targetProgressModels ; } param year param targetValue public rollingTargetProgression ( year , targetValue ) { const targetProgressModels = [ ] ; const daysInYear = ( moment ( ) . year ( year ) . isLeapYear ( ) ) ? 366 : 365 ; for ( let day = 1 ; day <= daysInYear ; day ++ ) { targetProgressModels . push ( { dayOfYear : day , value : targetValue } ) ; } return targetProgressModels ; } param { SyncedActivityModel [ ] } syncedActivityModels returns { ActivityCountByTypeModel [ ] } public activitiesByTypes ( syncedActivityModels ) { const activitiesCountByTypes = [ ] ; _ . forIn ( _ . countBy ( _ . map ( syncedActivityModels , "type" ) ) , ( count , type ) => { activitiesCountByTypes . push ( { type : type , count : count } ) ; } ) ; return _ . orderBy ( activitiesCountByTypes , ( activityCountByTypeModel ) => { return activityCountByTypeModel . count * - 1 ; } ) ; } param { SyncedActivityModel [ ] } syncedActivityModels returns { number [ ] } public availableYears ( syncedActivityModels ) { syncedActivityModels = _ . sortBy ( syncedActivityModels , "start_time" ) ; const availableYears = [ ] ; const startYear = moment ( _ . first ( syncedActivityModels ) . start_time ) . year ( ) ; const endYear = this . getTodayMoment ( ) . year ( ) ; let year = startYear ; while ( year <= endYear ) { availableYears . push ( year ) ; year ++ ; } return availableYears . reverse ( ) ; } param { SyncedActivityModel [ ] } activities param { string [ ] } typesFilter returns { YearProgressActivityModel [ ] } public filterSyncedActivityModelAlongTypes ( activities , typesFilter ) { activities = _ . filter ( activities , ( activity ) => { if ( _ . indexOf ( typesFilter , activity . type ) !== - 1 ) { const momentStartTime = moment ( activity . start_time ) ; activity . year = momentStartTime . year ( ) ; activity . dayOfYear = momentStartTime . dayOfYear ( ) ; return true ; } return false ; } ) ; return activities as YearProgressActivityModel [ ] ; } param { YearProgressModel [ ] } yearProgressions param { Moment } dayMoment param { ProgressType } progressType param { number [ ] } selectedYears param { Map < number , string > } yearsColorsMap returns { ProgressAtDayModel [ ] } public findProgressionsAtDay ( yearProgressions , dayMoment , progressType , selectedYears , yearsColorsMap ) { const progressionsAtDay = [ ] ; _ . forEach ( selectedYears , ( selectedYear ) => { const dayMomentAtYear = dayMoment . year ( selectedYear ) . startOf ( "day" ) ; const yearProgressModel = _ . find ( yearProgressions , { year : selectedYear } ) ; if ( ! yearProgressModel ) { return ; } const progressModel = _ . find ( yearProgressModel . progressions , { dayOfYear : dayMomentAtYear . dayOfYear ( ) } ) ; if ( progressModel ) { const progressAtDay = { date : dayMomentAtYear . toDate ( ) , year : dayMomentAtYear . year ( ) , progressType : progressType , value : progressModel . valueOf ( progressType ) , color : ( yearsColorsMap ) ? yearsColorsMap . get ( dayMomentAtYear . year ( ) ) : null } ; progressionsAtDay . push ( progressAtDay ) ; } } ) ; return progressionsAtDay ; } param { Moment } momentWatched public onMomentWatchedChange ( momentWatched ) { this . momentWatched = momentWatched ; this . momentWatchedChanges . next ( momentWatched ) ; } returns { Moment } public resetMomentWatched ( ) { const todayMoment = this . getTodayMoment ( ) . clone ( ) . startOf ( "day" ) ; this . onMomentWatchedChange ( todayMoment . clone ( ) ) ; return todayMoment ; } returns { string } param hoursIn public readableTimeProgress ( hoursIn ) { if ( ! hoursIn ) { return "0h" ; } const duration = moment . duration ( Math . abs ( hoursIn ) , "hours" ) ; const hours = Math . floor ( duration . asHours ( ) ) ; const minutes = duration . minutes ( ) ; const showHours = hours ? `${ hours }h` + ( ( minutes ) ? ", " : "" ) : "" ; const showMinutes = minutes ? `${ minutes }m` : "" ; return `${ showHours }${ showMinutes }` ; } /**
     * Fetch all preset
     */ public fetchPresets ( ) { return ( < Promise < YearToDateProgressPresetModel [ ] > > this . yearProgressPresetDao . fetch ( ) ) ; } param presetModel public addPreset ( presetModel ) { return ( < Promise < YearToDateProgressPresetModel [ ] > > this . yearProgressPresetDao . fetch ( ) . then ( ( models ) => { const query = { mode : presetModel . mode , progressType : presetModel . progressType , activityTypes : presetModel . activityTypes , includeCommuteRide : presetModel . includeCommuteRide , includeIndoorRide : presetModel . includeIndoorRide , targetValue : presetModel . targetValue } ; if ( presetModel . mode === ProgressMode . ROLLING ) { const rollingPresetModel = ( presetModel as RollingProgressPresetModel ) ; query [ "rollingPeriod" ] = rollingPresetModel . rollingPeriod ; query [ "periodMultiplier" ] = rollingPresetModel . periodMultiplier ; } const existingModel = _ . find ( models , query ) ; if ( existingModel ) { return Promise . reject ( new AppError ( AppError . YEAR_PROGRESS_PRESETS_ALREADY_EXISTS , "You already saved this preset." ) ) ; } models . push ( presetModel ) ; return this . yearProgressPresetDao . save ( models ) ; } ) ) ; } param id public deletePreset ( id ) { return this . yearProgressPresetDao . fetch ( ) . then ( ( models ) => { const presetIndex = _ . findIndex ( models , { id : id } ) ; if ( presetIndex === - 1 ) { return Promise . reject ( new AppError ( AppError . YEAR_PROGRESS_PRESETS_DO_NOT_EXISTS , "Year progress cannot be deleted" ) ) ; } models . splice ( presetIndex , 1 ) ; return this . yearProgressPresetDao . save ( models ) . then ( ( ) => { return Promise . resolve ( ) ; } ) ; } ) ; } public getTodayMoment ( ) { return moment ( ) ; } }