tcxread 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/tcxread.rb +155 -221
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 467aa1170c1d1e4187ad0bf62cdf274e3323427e1a17b373761d9e15344f3246
|
|
4
|
+
data.tar.gz: 3bb6690e5169d24ea3a18cbcea16af923a3d203a1f230364b5db2e68363ced11
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8fa40c28c9ead5bbdbdb3d3c7a79958f7145f6b53398771af1776be3e6d44c25d2e0e9ec8165026d4c54789dccad0550fd69e882e1471dec322feb79a3a3c059
|
|
7
|
+
data.tar.gz: ff9edd51b84495ce42b3db1ea53a807f23df2c97b264524f5f274a9b2b9a815efcedd6d0e7c2bc540268a4655a85c3cdd183ce1aa1f732edc22476bcbf53e157
|
data/lib/tcxread.rb
CHANGED
|
@@ -1,11 +1,49 @@
|
|
|
1
1
|
require "nokogiri"
|
|
2
2
|
|
|
3
|
+
# The `TCXRead` class processes and analyzes data from a TCX (Training Center XML) file.
|
|
4
|
+
# It extracts key metrics such as distance, time, calories, ascent/descent, altitude, heart rate,
|
|
5
|
+
# power (watts), cadence, and speed from the activities recorded in the TCX file.
|
|
6
|
+
#
|
|
7
|
+
# Reference (see also):
|
|
8
|
+
# I. Jr. Fister, L. Lukač, A. Rajšp, I. Fister, L. Pečnik, and D. Fister,
|
|
9
|
+
# "A minimalistic toolbox for extracting features from sport activity files,"
|
|
10
|
+
# 2021 IEEE 25th International Conference on Intelligent Engineering Systems (INES), 2021,
|
|
11
|
+
# pp. 121-126, doi: 10.1109/INES52918.2021.9512927.
|
|
3
12
|
class TCXRead
|
|
13
|
+
# @!attribute [r] total_distance_meters
|
|
14
|
+
# @return [Float] The total distance covered in meters.
|
|
15
|
+
# @!attribute [r] total_time_seconds
|
|
16
|
+
# @return [Float] The total time of activities in seconds.
|
|
17
|
+
# @!attribute [r] total_calories
|
|
18
|
+
# @return [Integer] The total calories burned.
|
|
19
|
+
# @!attribute [r] total_ascent
|
|
20
|
+
# @return [Float] The total ascent in meters.
|
|
21
|
+
# @!attribute [r] total_descent
|
|
22
|
+
# @return [Float] The total descent in meters.
|
|
23
|
+
# @!attribute [r] max_altitude
|
|
24
|
+
# @return [Float] The maximum altitude reached in meters.
|
|
25
|
+
# @!attribute [r] average_heart_rate
|
|
26
|
+
# @return [Float] The average heart rate in beats per minute.
|
|
27
|
+
# @!attribute [r] max_watts
|
|
28
|
+
# @return [String, Float] The maximum power output in watts, or 'NA' if unavailable.
|
|
29
|
+
# @!attribute [r] average_watts
|
|
30
|
+
# @return [String, Float] The average power output in watts, or 'NA' if unavailable.
|
|
31
|
+
# @!attribute [r] average_cadence_all
|
|
32
|
+
# @return [Float] The average cadence in RPM.
|
|
33
|
+
# @!attribute [r] average_cadence_biking
|
|
34
|
+
# @return [Float] The average cadence for the whole activity in RPM.
|
|
35
|
+
# @!attribute [r] average_speed_all
|
|
36
|
+
# @return [Float] The average speed for the whole activity in meters per second.
|
|
37
|
+
# @!attribute [r] average_speed_moving
|
|
38
|
+
# @return [Float] The average speed while moving in meters per second.
|
|
39
|
+
|
|
4
40
|
attr_reader :total_distance_meters, :total_time_seconds, :total_calories,
|
|
5
41
|
:total_ascent, :total_descent, :max_altitude, :average_heart_rate,
|
|
6
42
|
:max_watts, :average_watts, :average_cadence_all, :average_cadence_biking,
|
|
7
43
|
:average_speed_all, :average_speed_moving
|
|
8
44
|
|
|
45
|
+
# Initializes the TCXRead object and parses the TCX file.
|
|
46
|
+
# @param file_path [String] The file path of the TCX file to process.
|
|
9
47
|
def initialize(file_path)
|
|
10
48
|
@file_path = file_path
|
|
11
49
|
@doc = Nokogiri::XML(File.open(file_path))
|
|
@@ -28,277 +66,173 @@ class TCXRead
|
|
|
28
66
|
parse
|
|
29
67
|
end
|
|
30
68
|
|
|
69
|
+
# Parses the TCX file and computes metrics for all activities.
|
|
31
70
|
def parse
|
|
32
71
|
activities = parse_activities
|
|
33
|
-
if activities.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@average_cadence_biking = cadence_results[:average_cadence_biking]
|
|
43
|
-
speed_results = calculate_average_speed_from_activities(activities)
|
|
44
|
-
@average_speed_all = speed_results[:average_speed_all]
|
|
45
|
-
@average_speed_moving = speed_results[:average_speed_moving]
|
|
46
|
-
end
|
|
72
|
+
return if activities.empty?
|
|
73
|
+
|
|
74
|
+
@total_time_seconds = activities.sum { |activity| activity[:total_time_seconds] }
|
|
75
|
+
@total_distance_meters = activities.sum { |activity| activity[:total_distance_meters] }
|
|
76
|
+
@total_calories = activities.sum { |activity| activity[:total_calories] }
|
|
77
|
+
|
|
78
|
+
@total_ascent, @total_descent, @max_altitude = calculate_ascent_descent_and_max_altitude_from_activities(activities)
|
|
79
|
+
@average_heart_rate = calculate_average(:heart_rate, activities)
|
|
80
|
+
@max_watts, @average_watts = calculate_watts_from_activities(activities)
|
|
47
81
|
|
|
48
|
-
|
|
82
|
+
cadence_results = calculate_average_cadence_from_activities(activities)
|
|
83
|
+
@average_cadence_all = cadence_results[:average_cadence_all]
|
|
84
|
+
@average_cadence_biking = cadence_results[:average_cadence_biking]
|
|
85
|
+
|
|
86
|
+
speed_results = calculate_average_speed_from_activities(activities)
|
|
87
|
+
@average_speed_all = speed_results[:average_speed_all]
|
|
88
|
+
@average_speed_moving = speed_results[:average_speed_moving]
|
|
49
89
|
end
|
|
50
90
|
|
|
51
91
|
private
|
|
52
92
|
|
|
93
|
+
# Parses activities from the TCX file.
|
|
94
|
+
# @return [Array<Hash>] An array of parsed activity data.
|
|
53
95
|
def parse_activities
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
laps = parse_laps(activity)
|
|
57
|
-
total_time_seconds = laps.sum { |lap| lap[:total_time_seconds] }
|
|
58
|
-
total_distance_meters = laps.sum { |lap| lap[:distance_meters] }
|
|
59
|
-
total_calories = laps.sum { |lap| lap[:calories] }
|
|
60
|
-
total_ascent, total_descent, max_altitude = calculate_ascent_descent_and_max_altitude(laps)
|
|
61
|
-
average_heart_rate = calculate_average_heart_rate(laps)
|
|
96
|
+
@doc.xpath('//xmlns:Activities/xmlns:Activity').map do |activity|
|
|
97
|
+
laps = activity.xpath('xmlns:Lap').map { |lap| parse_lap(lap) }
|
|
62
98
|
|
|
63
|
-
|
|
99
|
+
{
|
|
64
100
|
sport: activity.attr('Sport'),
|
|
65
101
|
id: activity.xpath('xmlns:Id').text,
|
|
66
102
|
laps: laps,
|
|
67
|
-
total_time_seconds: total_time_seconds,
|
|
68
|
-
total_distance_meters:
|
|
69
|
-
total_calories:
|
|
70
|
-
total_ascent: total_ascent,
|
|
71
|
-
total_descent: total_descent,
|
|
72
|
-
max_altitude: max_altitude,
|
|
73
|
-
average_heart_rate:
|
|
103
|
+
total_time_seconds: laps.sum { |lap| lap[:total_time_seconds] },
|
|
104
|
+
total_distance_meters: laps.sum { |lap| lap[:distance_meters] },
|
|
105
|
+
total_calories: laps.sum { |lap| lap[:calories] },
|
|
106
|
+
total_ascent: laps.sum { |lap| lap[:total_ascent] },
|
|
107
|
+
total_descent: laps.sum { |lap| lap[:total_descent] },
|
|
108
|
+
max_altitude: laps.map { |lap| lap[:max_altitude] }.max,
|
|
109
|
+
average_heart_rate: calculate_average(:heart_rate, laps)
|
|
74
110
|
}
|
|
75
111
|
end
|
|
76
|
-
activities
|
|
77
112
|
end
|
|
78
113
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
total_time_seconds: lap.xpath('xmlns:TotalTimeSeconds').text.to_f,
|
|
85
|
-
distance_meters: lap.xpath('xmlns:DistanceMeters').text.to_f,
|
|
86
|
-
maximum_speed: lap.xpath('xmlns:MaximumSpeed').text.to_f,
|
|
87
|
-
calories: lap.xpath('xmlns:Calories').text.to_i,
|
|
88
|
-
average_heart_rate: lap.xpath('xmlns:AverageHeartRateBpm/xmlns:Value').text.to_i,
|
|
89
|
-
maximum_heart_rate: lap.xpath('xmlns:MaximumHeartRateBpm/xmlns:Value').text.to_i,
|
|
90
|
-
intensity: lap.xpath('xmlns:Intensity').text,
|
|
91
|
-
cadence: lap.xpath('xmlns:Cadence').text.to_i,
|
|
92
|
-
trigger_method: lap.xpath('xmlns:TriggerMethod').text,
|
|
93
|
-
tracks: parse_tracks(lap)
|
|
94
|
-
}
|
|
95
|
-
end
|
|
96
|
-
laps
|
|
97
|
-
end
|
|
114
|
+
# Parses a single lap from the TCX file.
|
|
115
|
+
# @param lap [Nokogiri::XML::Node] The lap node to parse.
|
|
116
|
+
# @return [Hash] The parsed lap data.
|
|
117
|
+
def parse_lap(lap)
|
|
118
|
+
trackpoints = lap.xpath('xmlns:Track/xmlns:Trackpoint').map { |tp| parse_trackpoint(tp) }
|
|
98
119
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
heart_rate: trackpoint.xpath('xmlns:HeartRateBpm/xmlns:Value').text.to_i,
|
|
110
|
-
cadence: trackpoint.xpath('xmlns:Cadence').text.to_i,
|
|
111
|
-
sensor_state: trackpoint.xpath('xmlns:SensorState').text,
|
|
112
|
-
watts: trackpoint.xpath('xmlns:Extensions/ns3:TPX/ns3:Watts').text.to_f
|
|
113
|
-
}
|
|
114
|
-
end
|
|
115
|
-
tracks << trackpoints
|
|
116
|
-
end
|
|
117
|
-
tracks
|
|
120
|
+
{
|
|
121
|
+
start_time: lap.attr('StartTime'),
|
|
122
|
+
total_time_seconds: lap.xpath('xmlns:TotalTimeSeconds').text.to_f,
|
|
123
|
+
distance_meters: lap.xpath('xmlns:DistanceMeters').text.to_f,
|
|
124
|
+
calories: lap.xpath('xmlns:Calories').text.to_i,
|
|
125
|
+
total_ascent: calculate_ascent(trackpoints),
|
|
126
|
+
total_descent: calculate_descent(trackpoints),
|
|
127
|
+
max_altitude: trackpoints.map { |tp| tp[:altitude_meters] }.max || 0,
|
|
128
|
+
trackpoints: trackpoints || []
|
|
129
|
+
}
|
|
118
130
|
end
|
|
119
131
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
# Parses a single trackpoint from the TCX file.
|
|
133
|
+
# @param trackpoint [Nokogiri::XML::Node] The trackpoint node to parse.
|
|
134
|
+
# @return [Hash] The parsed trackpoint data.
|
|
135
|
+
def parse_trackpoint(trackpoint)
|
|
124
136
|
{
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
time: trackpoint.xpath('xmlns:Time').text,
|
|
138
|
+
altitude_meters: trackpoint.xpath('xmlns:AltitudeMeters').text.to_f,
|
|
139
|
+
distance_meters: trackpoint.xpath('xmlns:DistanceMeters').text.to_f,
|
|
140
|
+
heart_rate: trackpoint.xpath('xmlns:HeartRateBpm/xmlns:Value').text.to_i,
|
|
141
|
+
cadence: trackpoint.xpath('xmlns:Cadence').text.to_i,
|
|
142
|
+
watts: trackpoint.xpath('xmlns:Extensions/ns3:TPX/ns3:Watts').text.to_f,
|
|
143
|
+
speed: trackpoint.xpath('xmlns:Extensions/ns3:TPX/ns3:Speed').text.to_f
|
|
127
144
|
}
|
|
128
145
|
end
|
|
129
146
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
147
|
+
# Calculates total ascent from an array of trackpoints.
|
|
148
|
+
# @param trackpoints [Array<Hash>] An array of trackpoint data.
|
|
149
|
+
# @return [Float] The total ascent in meters.
|
|
150
|
+
def calculate_ascent(trackpoints)
|
|
134
151
|
previous_altitude = nil
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if previous_altitude
|
|
142
|
-
altitude_change = altitude - previous_altitude
|
|
143
|
-
if altitude_change > 0
|
|
144
|
-
total_ascent += altitude_change
|
|
145
|
-
elsif altitude_change < 0
|
|
146
|
-
total_descent += altitude_change.abs
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
previous_altitude = altitude
|
|
151
|
-
end
|
|
152
|
+
trackpoints.sum do |tp|
|
|
153
|
+
altitude = tp[:altitude_meters]
|
|
154
|
+
ascent = previous_altitude && altitude > previous_altitude ? altitude - previous_altitude : 0
|
|
155
|
+
previous_altitude = altitude
|
|
156
|
+
ascent
|
|
152
157
|
end
|
|
153
|
-
|
|
154
|
-
[total_ascent, total_descent, max_altitude]
|
|
155
158
|
end
|
|
156
159
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
# Calculates total descent from an array of trackpoints.
|
|
161
|
+
# @param trackpoints [Array<Hash>] An array of trackpoint data.
|
|
162
|
+
# @return [Float] The total descent in meters.
|
|
163
|
+
def calculate_descent(trackpoints)
|
|
164
|
+
previous_altitude = nil
|
|
165
|
+
trackpoints.sum do |tp|
|
|
166
|
+
altitude = tp[:altitude_meters]
|
|
167
|
+
descent = previous_altitude && altitude < previous_altitude ? previous_altitude - altitude : 0
|
|
168
|
+
previous_altitude = altitude
|
|
169
|
+
descent
|
|
166
170
|
end
|
|
167
|
-
|
|
168
|
-
[total_ascent, total_descent, max_altitude]
|
|
169
171
|
end
|
|
170
172
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
total_heart_rate += heart_rate
|
|
180
|
-
heart_rate_count += 1
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
heart_rate_count > 0 ? total_heart_rate.to_f / heart_rate_count : 0.0
|
|
173
|
+
# Calculates ascent, descent, and maximum altitude from activities.
|
|
174
|
+
# @param activities [Array<Hash>] An array of activity data.
|
|
175
|
+
# @return [Array<Float>] Total ascent, total descent, and maximum altitude.
|
|
176
|
+
def calculate_ascent_descent_and_max_altitude_from_activities(activities)
|
|
177
|
+
total_ascent = activities.sum { |activity| activity[:total_ascent] }
|
|
178
|
+
total_descent = activities.sum { |activity| activity[:total_descent] }
|
|
179
|
+
max_altitude = activities.map { |activity| activity[:max_altitude] }.max
|
|
180
|
+
[total_ascent, total_descent, max_altitude]
|
|
186
181
|
end
|
|
187
182
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
heart_rate_count > 0 ? total_heart_rate.to_f / heart_rate_count : 0.0
|
|
183
|
+
# Calculates the average value of a metric across laps or activities.
|
|
184
|
+
# @param metric [Symbol] The metric to average (e.g., :heart_rate).
|
|
185
|
+
# @param laps_or_activities [Array<Hash>] An array of lap or activity data.
|
|
186
|
+
# @return [Float] The average value of the metric.
|
|
187
|
+
def calculate_average(metric, laps_or_activities)
|
|
188
|
+
values = laps_or_activities.flat_map do |lap|
|
|
189
|
+
next [] unless lap[:trackpoints]
|
|
190
|
+
lap[:trackpoints].map { |tp| tp[metric] }
|
|
191
|
+
end.compact
|
|
192
|
+
|
|
193
|
+
values.any? ? values.sum.to_f / values.size : 0.0
|
|
205
194
|
end
|
|
206
195
|
|
|
196
|
+
# Calculates power metrics from activities.
|
|
197
|
+
# @param activities [Array<Hash>] An array of activity data.
|
|
198
|
+
# @return [Array<String, Float>] Maximum and average power output.
|
|
207
199
|
def calculate_watts_from_activities(activities)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
activities.each do |activity|
|
|
213
|
-
activity[:laps].each do |lap|
|
|
214
|
-
lap[:tracks].flatten.each do |trackpoint|
|
|
215
|
-
watts = trackpoint[:watts]
|
|
216
|
-
if watts > 0
|
|
217
|
-
total_watts += watts
|
|
218
|
-
watts_count += 1
|
|
219
|
-
max_watts = watts if watts > max_watts
|
|
220
|
-
end
|
|
221
|
-
end
|
|
200
|
+
watts = activities.flat_map do |activity|
|
|
201
|
+
activity[:laps].flat_map do |lap|
|
|
202
|
+
lap[:trackpoints]&.map { |tp| tp[:watts] } || []
|
|
222
203
|
end
|
|
223
|
-
end
|
|
204
|
+
end.compact
|
|
224
205
|
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
206
|
+
if watts.any?
|
|
207
|
+
max_watts = watts.max
|
|
208
|
+
avg_watts = watts.sum.to_f / watts.size
|
|
228
209
|
else
|
|
229
|
-
average_watts = 'NA'
|
|
230
210
|
max_watts = 'NA'
|
|
211
|
+
avg_watts = 'NA'
|
|
231
212
|
end
|
|
232
213
|
|
|
233
|
-
[max_watts,
|
|
214
|
+
[max_watts, avg_watts]
|
|
234
215
|
end
|
|
235
216
|
|
|
217
|
+
# Calculates average cadence metrics from activities.
|
|
218
|
+
# @param activities [Array<Hash>] An array of activity data.
|
|
219
|
+
# @return [Hash] Average cadence metrics for all activities and biking only.
|
|
236
220
|
def calculate_average_cadence_from_activities(activities)
|
|
237
|
-
|
|
238
|
-
total_cadence_biking = 0
|
|
239
|
-
cadence_count_all = 0
|
|
240
|
-
cadence_count_biking = 0
|
|
241
|
-
|
|
242
|
-
activities.each do |activity|
|
|
243
|
-
activity[:laps].each do |lap|
|
|
244
|
-
lap[:tracks].flatten.each do |trackpoint|
|
|
245
|
-
cadence = trackpoint[:cadence]
|
|
246
|
-
total_cadence_all += cadence
|
|
247
|
-
cadence_count_all += 1
|
|
248
|
-
|
|
249
|
-
if cadence > 0
|
|
250
|
-
total_cadence_biking += cadence
|
|
251
|
-
cadence_count_biking += 1
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
average_cadence_all = cadence_count_all > 0 ? total_cadence_all.to_f / cadence_count_all : 0.0
|
|
258
|
-
average_cadence_biking = cadence_count_biking > 0 ? total_cadence_biking.to_f / cadence_count_biking : 0.0
|
|
259
|
-
|
|
221
|
+
cadences = activities.flat_map { |activity| activity[:laps].flat_map { |lap| lap[:trackpoints].map { |tp| tp[:cadence] } if lap[:trackpoints] } }.compact
|
|
260
222
|
{
|
|
261
|
-
average_cadence_all:
|
|
262
|
-
average_cadence_biking:
|
|
223
|
+
average_cadence_all: cadences.any? ? cadences.sum.to_f / cadences.size : 0.0,
|
|
224
|
+
average_cadence_biking: cadences.reject(&:zero?).any? ? cadences.reject(&:zero?).sum.to_f / cadences.reject(&:zero?).size : 0.0
|
|
263
225
|
}
|
|
264
226
|
end
|
|
265
227
|
|
|
266
|
-
# Calculates
|
|
267
|
-
#
|
|
268
|
-
# @
|
|
269
|
-
# @return [Hash] a hash containing average speed including zeros and average speed while moving.
|
|
228
|
+
# Calculates average speed metrics from activities.
|
|
229
|
+
# @param activities [Array<Hash>] An array of activity data.
|
|
230
|
+
# @return [Hash] Average speed metrics for the whole activity and moving activities only.
|
|
270
231
|
def calculate_average_speed_from_activities(activities)
|
|
271
|
-
|
|
272
|
-
total_speed_moving = 0
|
|
273
|
-
speed_count_all = 0
|
|
274
|
-
speed_count_moving = 0
|
|
275
|
-
|
|
276
|
-
activities.each do |activity|
|
|
277
|
-
activity[:laps].each do |lap|
|
|
278
|
-
lap[:tracks].flatten.each do |trackpoint|
|
|
279
|
-
distance = trackpoint[:distance_meters]
|
|
280
|
-
time_seconds = lap[:total_time_seconds]
|
|
281
|
-
speed = distance / time_seconds if time_seconds > 0
|
|
282
|
-
|
|
283
|
-
if speed
|
|
284
|
-
total_speed_all += speed
|
|
285
|
-
speed_count_all += 1
|
|
286
|
-
|
|
287
|
-
if speed > 0
|
|
288
|
-
total_speed_moving += speed
|
|
289
|
-
speed_count_moving += 1
|
|
290
|
-
end
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
average_speed_all = speed_count_all > 0 ? total_speed_all.to_f / speed_count_all : 0.0
|
|
297
|
-
average_speed_moving = speed_count_moving > 0 ? total_speed_moving.to_f / speed_count_moving : 0.0
|
|
298
|
-
|
|
232
|
+
speeds = activities.flat_map { |activity| activity[:laps].flat_map { |lap| lap[:trackpoints].map { |tp| tp[:speed] } if lap[:trackpoints] } }.compact
|
|
299
233
|
{
|
|
300
|
-
average_speed_all:
|
|
301
|
-
average_speed_moving:
|
|
234
|
+
average_speed_all: speeds.any? ? speeds.sum.to_f / speeds.size : 0.0,
|
|
235
|
+
average_speed_moving: speeds.reject(&:zero?).any? ? speeds.reject(&:zero?).sum.to_f / speeds.reject(&:zero?).size : 0.0
|
|
302
236
|
}
|
|
303
237
|
end
|
|
304
238
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tcxread
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- firefly-cpp
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2024-
|
|
11
|
+
date: 2024-12-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|
|
@@ -56,7 +56,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
56
56
|
- !ruby/object:Gem::Version
|
|
57
57
|
version: '0'
|
|
58
58
|
requirements: []
|
|
59
|
-
rubygems_version: 3.5.
|
|
59
|
+
rubygems_version: 3.5.22
|
|
60
60
|
signing_key:
|
|
61
61
|
specification_version: 4
|
|
62
62
|
summary: tcx reader/parser in Ruby
|