aboutsummaryrefslogtreecommitdiff
path: root/ride/moving_time.go
blob: 62deb24d7f201d755dba088be4d285630961910f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

package ride

import "time"

const MovingTimeAlgorithmVersion = "v2"

// movingTimeMinSpeedMps is the minimum implied speed between two consecutive
// track points for the interval to count as moving (0.4 km/h).
const movingTimeMinSpeedMps = 0.4 / 3.6

// movingTimeMinPowerW is the minimum power output for an interval to count as
// moving when GPS speed is below the minimum speed.
const movingTimeMinPowerW = 20

// movingTimeMaxGap is the maximum time gap between consecutive track points
// that can count as moving. Longer gaps are pauses (stops, lost signal).
const movingTimeMaxGap = 120 * time.Second

// Bench (104 rides, `-backtest`, distance >= 10 km, moving/elapsed ratio >= 0.8):
// absMean 0.4%, stderr 0.1, p50 0.2, p75 0.4, p90 0.8, p95 1.4, p99 2.9.
func MovingTime(r Ride) time.Duration {
	return movingTime(r.timestamps, r.Distances, r.powers, r.cadences, movingTimeMinSpeedMps, movingTimeMaxGap)
}

func movingTime(timestamps []time.Time, distances, powers, cadences []float64, minSpeedMps float64, maxGap time.Duration) time.Duration {
	var moving time.Duration
	for i := 1; i < len(timestamps); i++ {
		dt := timestamps[i].Sub(timestamps[i-1])
		if dt <= 0 || dt > maxGap {
			continue
		}
		dd := distances[i] - distances[i-1]
		gpsMoving := dd/dt.Seconds() > minSpeedMps
		powerMoving := false
		if len(powers) > 0 {
			powerMoving = powers[i] >= movingTimeMinPowerW
		}
		cadenceMoving := false
		if len(cadences) > 0 {
			cadenceMoving = cadences[i] > 0
		}
		if gpsMoving || powerMoving || cadenceMoving {
			moving += dt
		}
	}
	return moving
}