import * as _ from "lodash" ; import { Helper } from "../helper" ; import { EffortInfo , LeaderBoardData } from "./activity-segment-time-comparison.modifier" ; import { AbstractModifier } from "./abstract.modifier" ; import { AthleteSnapshotModel } from "@elevate/shared/models" ; export class SegmentRecentEffortsHRATimeModifier extends AbstractModifier { protected displayRecentEffortsHRAdjustedPacePower : boolean ; protected athleteSnapshot : AthleteSnapshotModel ; protected athleteId : number ; protected segmentId : number ; protected hraTimeLoop : number ; constructor ( displayRecentEffortsHRAdjustedPacePower , athleteSnapshot , athleteId , segmentId ) { super ( ) ; this . displayRecentEffortsHRAdjustedPacePower = displayRecentEffortsHRAdjustedPacePower ; this . athleteSnapshot = athleteSnapshot ; this . athleteId = athleteId ; this . segmentId = segmentId ; } public modify ( ) { if ( this . displayRecentEffortsHRAdjustedPacePower ) { this . hraTimeLoop = window . setInterval ( ( ) => { this . hraTime ( ) ; } , 750 ) ; } } protected findCurrentSegmentEfforts ( segmentId , page ? , deferred ? , fetchedLeaderBoardData ? ) { if ( ! page ) { page = 1 ; } if ( ! deferred ) { deferred = $ . Deferred ( ) ; } if ( ! fetchedLeaderBoardData ) { fetchedLeaderBoardData = [ ] ; } const perPage = 50 ; const jqxhr = $ . getJSON ( "/segments/" + segmentId + "/leaderboard?raw=true&page=" + page + "&per_page=" + perPage + "&viewer_context=false&filter=my_results" ) ; jqxhr . done ( ( leaderBoardData ) => { fetchedLeaderBoardData = _ . flatten ( _ . union ( leaderBoardData . top_results , fetchedLeaderBoardData ) ) ; if ( leaderBoardData . top_results . length == 0 ) { deferred . resolve ( fetchedLeaderBoardData ) ; } else { this . findCurrentSegmentEfforts ( segmentId , page + 1 , deferred , fetchedLeaderBoardData ) ; } } ) . fail ( ( error ) => { deferred . reject ( error ) ; } ) ; return deferred . promise ( ) ; } protected hraTime ( ) { function createElementSVG ( kind , ... attribs ) { const ret = document . createElementNS ( "http://www.w3.org/2000/svg" , kind ) ; for ( const attrib of attribs ) { ret . setAttribute ( attrib [ 0 ] , attrib [ 1 ] ) ; } return ret ; } function createChildElementSVG ( parent , kind , ... attribs ) { const ch = createElementSVG ( kind , ... attribs ) ; parent . appendChild ( ch ) ; return ch ; } const recentEffortsChart = $ ( "#athlete-history-chart" ) ; if ( ! recentEffortsChart . hasClass ( "elevateGraph" ) ) { recentEffortsChart . addClass ( "elevateGraph" ) ; const chart = recentEffortsChart . find ( "svg" ) ; const marks = chart . find ( "circle" ) . filter ( ".mark" ) ; interface XY { x : number ; y : number ; } const xyFromMark = function ( m ) : XY { return { x : m . cx . baseVal . value , y : m . cy . baseVal . value } ; } ; let maxY = null , minY = null ; let minX = null , maxX = null ; marks . each ( ( i , m ) => { const xy = xyFromMark ( m ) ; minY = Helper . safeMin ( minY , xy . y ) ; maxY = Helper . safeMax ( maxY , xy . y ) ; minX = Helper . safeMin ( minX , xy . x ) ; maxX = Helper . safeMax ( maxX , xy . x ) ; } ) ; this . findCurrentSegmentEfforts ( this . segmentId ) . then ( ( fetchedLeaderBoardData ) => { fetchedLeaderBoardData = fetchedLeaderBoardData . sort ( ( left , right ) => { const lDate = new Date ( left . start_date_local_raw ) ; const rDate = new Date ( right . start_date_local_raw ) ; return lDate . getTime ( ) - rDate . getTime ( ) ; } ) ; if ( marks . length < fetchedLeaderBoardData . length ) { fetchedLeaderBoardData = fetchedLeaderBoardData . splice ( - marks . length , marks . length ) ; } let showWatts = true ; fetchedLeaderBoardData . forEach ( ( r ) => { if ( r . avg_watts == null ) { showWatts = false ; } } ) ; let minHR = null , maxHR = null ; fetchedLeaderBoardData . forEach ( ( r ) => { minHR = Helper . safeMin ( minHR , r . avg_heart_rate ) ; maxHR = Helper . safeMax ( maxHR , r . avg_heart_rate ) ; } ) ; const restHR = this . athleteSnapshot . athleteSettings . restHr ; const targetHR = maxHR ; class HRValueComputed { public effort : EffortInfo ; public hraValue : number ; constructor ( effort , hraValue ? ) { this . effort = effort ; this . hraValue = hraValue ; } } const hrValuesComputed = fetchedLeaderBoardData . map ( ( r ) => { if ( r . avg_heart_rate != null && r . avg_heart_rate > restHR ) { const mValue = showWatts ? r . avg_watts : r . elapsed_time_raw ; const ratio = ( r . avg_heart_rate - restHR ) / ( targetHR - restHR ) ; return new HRValueComputed ( r , showWatts ? mValue / ratio : mValue * ratio ) ; } else { return new HRValueComputed ( r ) ; } } ) ; const hrValues = hrValuesComputed . filter ( ( h ) => h . hraValue != null ) . length ; if ( hrValues > 1 ) { let fastestValue = null ; let slowestValue = null ; if ( showWatts ) { hrValuesComputed . forEach ( ( r ) => { const rValue = r . hraValue ; if ( rValue != null ) { fastestValue = Helper . safeMax ( fastestValue , rValue ) ; slowestValue = Helper . safeMin ( slowestValue , rValue ) ; } } ) ; } else { fetchedLeaderBoardData . forEach ( ( r ) => { const rValue = r . elapsed_time_raw ; fastestValue = Helper . safeMin ( fastestValue , rValue ) ; slowestValue = Helper . safeMax ( slowestValue , rValue ) ; } ) ; } if ( showWatts ) { const minWattRange = 100 ; const wattRange = fastestValue - slowestValue ; if ( wattRange < minWattRange ) { slowestValue -= ( minWattRange - wattRange ) / 2 ; if ( slowestValue < 0 ) { slowestValue = 0 ; } fastestValue = slowestValue + minWattRange ; } } const topY = 10 ; const bottomY = parseInt ( chart [ 0 ] . getAttribute ( "height" ) ) - 10 ; let slowY = maxY ; let fastY = minY ; if ( showWatts ) { const translateRegEx = /translate\((.*),(.*)\)/ ; const yAxis = chart . find ( ".y.axis" ) ; const ticks = yAxis . find ( ".tick" ) ; const ticksY = ticks . map ( ( index , domElement ) => { const tickText = $ ( domElement ) . attr ( "transform" ) ; const yTick = translateRegEx . exec ( tickText ) [ 2 ] ; return parseFloat ( yTick ) ; } ) . valueOf ( ) as number [ ] ; const yTickTop = ticksY [ 0 ] ; const yTickBot = ticksY [ ticksY . length - 1 ] ; slowY = yTickTop + ( yTickBot - yTickTop ) * 0.25 ; fastY = yTickBot - ( yTickBot - yTickTop ) * 0.2 ; let step = 25 ; if ( fastestValue - slowestValue >= 400 ) { step = 100 ; } else if ( fastestValue - slowestValue >= 200 ) { step = 50 ; } const roundFastestDown = Math . floor ( fastestValue / step ) * step ; const roundSlowestUp = Math . ceil ( slowestValue / step ) * step ; const wattMarks = [ ] ; for ( let mWatts = roundSlowestUp ; mWatts <= roundFastestDown + 1 ; mWatts += step ) { wattMarks . push ( mWatts ) ; } const wattAxisX = maxX ; const gAxis = createElementSVG ( "g" , [ "class" , "y axis" ] , [ "style" , "opacity: 1;" ] , [ "transform" , "translate(" + wattAxisX + ", 0)" ] ) ; wattMarks . forEach ( ( mWatts ) => { const f = ( mWatts - fastestValue ) / ( slowestValue - fastestValue ) ; const mY = f * ( slowY - fastY ) + fastY ; const g = createChildElementSVG ( gAxis , "g" , [ "class" , "tick" ] , [ "style" , "opacity: 1;" ] , [ "transform" , "translate(0," + mY . toFixed ( 0 ) + ")" ] ) ; createChildElementSVG ( g , "line" , [ "x2" , "0" ] , [ "y2" , "0" ] , [ "class" , "grid" ] ) ; const text = createChildElementSVG ( g , "text" , [ "x" , "50" ] , [ "y" , "0" ] , [ "dy" , ".32em" ] , [ "class" , "hra-axis axis-tick-text" ] , [ "style" , "text-anchor: end;" ] ) ; text . textContent = mWatts . toFixed ( 0 ) + " W" ; } ) ; const insertDOM = chart . find ( ".y.axis" ) . eq ( 0 ) ; insertDOM . before ( gAxis ) ; } const mapValueToY = function ( value ) : number { return ( value - fastestValue ) / ( slowestValue - fastestValue ) * ( slowY - fastY ) + fastY ; } ; const clampY = function ( resY ) : number { return Math . min ( Math . max ( topY , resY ) , bottomY ) ; } ; const markData = marks . map ( ( i , m ) => { const xy = xyFromMark ( m ) ; const hraValue = hrValuesComputed [ i ] . hraValue ; if ( hraValue != null ) { const resY = mapValueToY ( hraValue ) ; return [ [ i , m , resY , hraValue , xy . x ] ] ; } } ) . valueOf ( ) as number [ ] [ ] ; const mappedMarks = $ . map ( markData , ( imr ) => { const [ , , resY , hraValue , mx ] = imr ; const clampedY = clampY ( resY ) ; const mark = createElementSVG ( "circle" , [ "class" , "hra-time-mark mark" ] , [ "cx" , mx . toString ( ) ] , [ "cy" , clampedY . toString ( ) ] , [ "r" , "3" ] ) ; if ( resY < topY || resY > bottomY ) { const title = createElementSVG ( "text" , [ "x" , ( mx + 4 ) . toString ( ) ] , [ "y" , ( clampedY + 4 ) . toString ( ) ] , [ "class" , "axis-tick-text" ] ) ; title . textContent = showWatts ? hraValue . toFixed ( 0 ) : Helper . secondsToHHMMSS ( hraValue , true ) ; return [ mark , title ] ; } return mark ; } ) ; const lines = [ ] ; let infobox = null ; for ( let i = 1 ; i < markData . length ; i ++ ) { const imrPrev = markData [ i - 1 ] ; const imrNext = markData [ i ] ; const line = createElementSVG ( "line" , [ "class" , "hra-line" ] , [ "x1" , imrPrev [ 4 ] . toString ( ) ] , [ "y1" , clampY ( imrPrev [ 2 ] ) . toString ( ) ] , [ "x2" , imrNext [ 4 ] . toString ( ) ] , [ "y2" , clampY ( imrNext [ 2 ] ) . toString ( ) ] ) ; lines . push ( line ) ; } if ( lines . length > 0 ) { const lastLine = lines [ lines . length - 1 ] ; const pbLabel = chart . find ( ".personal-best-label" ) ; const pbValue = chart . find ( ".personal-best-value" ) ; const pbLabelBox = ( pbLabel [ 0 ] as any ) . getBBox ( ) ; const pbLabelValue = ( pbValue [ 0 ] as any ) . getBBox ( ) ; const pbTop = Math . min ( pbLabelBox . y , pbLabelValue . y ) ; const pbBot = Math . max ( pbLabelBox . y + pbLabelBox . height , pbLabelValue . y + pbLabelValue . height ) ; const lastHRAY = parseFloat ( lastLine . getAttribute ( "y2" ) ) ; const hoverW = 14 ; const hoverH = 14 ; const hoverX = 20 ; const hoverY = - hoverH / 2 ; let infoY = lastHRAY ; if ( infoY + hoverH / 2 < pbTop ) { } else if ( infoY - hoverH / 2 > pbBot ) { } else { if ( infoY < ( pbTop + pbBot ) / 2 ) { infoY = pbTop - hoverH / 2 ; } else { infoY = pbBot + hoverH / 2 ; } } const boxX = parseFloat ( pbLabel . attr ( "x" ) ) ; const line = createElementSVG ( "line" , [ "class" , "hra-line" ] , [ "x1" , lastLine . getAttribute ( "x2" ) ] , [ "y1" , lastHRAY . toString ( ) ] , [ "x2" , ( boxX - 3 ) . toString ( ) ] , [ "y2" , infoY . toString ( ) ] ) ; lines . push ( line ) ; infobox = createElementSVG ( "g" , [ "transform" , "translate(" + boxX . toString ( ) + ", " + infoY . toString ( ) + ")" ] ) ; { const infoboxValue = createChildElementSVG ( infobox , "text" , [ "id" , "hra-value" ] , [ "x" , "0" ] , [ "y" , ( hoverY + hoverH / 2 ) . toString ( ) ] ) ; infoboxValue . textContent = maxHR . toFixed ( ) ; } { createChildElementSVG ( infobox , "rect" , [ "id" , "hra-box-help" ] , [ "x" , hoverX . toString ( ) ] , [ "y" , hoverY . toString ( ) ] , [ "width" , hoverW . toString ( ) ] , [ "height" , hoverH . toString ( ) ] ) ; const infoboxHelp = createChildElementSVG ( infobox , "text" , [ "id" , "hra-value-help" ] , [ "x" , ( hoverX + hoverW / 2 ) . toString ( ) ] , [ "y" , ( hoverY + hoverH / 2 ) . toString ( ) ] ) ; infoboxHelp . textContent = "?" ; } const infoboxHoverG = createChildElementSVG ( infobox , "g" , [ "id" , "hra-hover" ] ) ; { createChildElementSVG ( infoboxHoverG , "rect" , [ "id" , "hra-hover-box" ] , [ "x" , hoverX . toString ( ) ] , [ "y" , hoverY . toString ( ) ] , [ "width" , hoverW . toString ( ) ] , [ "height" , hoverH . toString ( ) ] ) ; const lineH = 15 ; const textX = 5 ; const textY = 3 ; const performance = showWatts ? "power" : "time" ; const infoText = [ "Estimation of " + performance + " you could" , "achieve at " + maxHR . toFixed ( ) + "bpm," , "the highest average HR of" , "all efforts in this segment." , ] ; const infoboxH = infoText . length * lineH + textY * 2 + 5 ; const infoboxW = 200 ; const infoboxRectG = createChildElementSVG ( infoboxHoverG , "g" , [ "id" , "hra-hover-g" ] , [ "transform" , "translate(" + ( 34 - infoboxW ) . toString ( ) + "," + ( hoverY + hoverH ) . toString ( ) + ")" ] ) ; createChildElementSVG ( infoboxRectG , "rect" , [ "id" , "hra-box" ] , [ "width" , infoboxW . toString ( ) ] , [ "height" , infoboxH . toString ( ) ] ) ; for ( let l = 0 ; l < infoText . length ; l ++ ) { const text = createChildElementSVG ( infoboxRectG , "text" , [ "x" , textX . toString ( ) ] , [ "y" , ( textY + lineH * ( l + 1 ) ) . toString ( ) ] ) ; text . textContent = infoText [ l ] ; } } } const firstMark = chart . find ( "circle" ) . eq ( 0 ) ; firstMark . before ( mappedMarks as any ) ; const bestMark = chart . find ( "circle" ) . filter ( ".personal-best-mark" ) ; bestMark . after ( lines ) ; if ( infobox != null ) { const topG = chart . children ( "g" ) . last ( ) ; const newG = createChildElementSVG ( topG [ 0 ] , "g" , [ "transform" , topG . attr ( "transform" ) ] ) ; newG . appendChild ( infobox ) ; topG . after ( newG ) ; } } } ) ; clearInterval ( this . hraTimeLoop ) ; } } }