tcxread 0.1.5 → 0.2.1

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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -16
  3. data/lib/tcxread.rb +171 -225
  4. metadata +3 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0f94a5a07acd01e1c6b0419324bbef97912e0d7e67b3f2b1dc18d50336dd7e2
4
- data.tar.gz: 776953d7610ffa6c6903449332eb1bf36ff2a7fb54c5fa10dde8e591ecefd3df
3
+ metadata.gz: 3d133eea1f6023a5ede73c5598671052bb312ea47d621d9dfe49bb6faa4cb4a6
4
+ data.tar.gz: 7650d1a88ec174b8e963ddc3a21319fca4157d016ccdf9d08b81c11873de02ea
5
5
  SHA512:
6
- metadata.gz: dc1240984ccb04288e3790d69529eea52269f16888e1b6d8906017b42252ce98eeef44240b729ed1358444e2f0d3f7ce377f3a49123ea5c85c21493a2438cc77
7
- data.tar.gz: 99863f90228c50f2590709105fab6d55395c26462bec7b2ba2b11c429f2e85453867583fdcb964d83599f669bbc1dc991fa6ca8a0c5eff6aba8a3681007d1b48
6
+ metadata.gz: ade42be5169a40e65f05eedb6546727ffb3bf9ba00a4d6fe412ed0a6546dc675b643ac818aec909f238661771acbe68973fb557f9ef26143fe4a4c2506a51ecb
7
+ data.tar.gz: 7bccd7b16ddd291a1963b03b458fbcb2fad41b162810a0187f309c35cb37af61d8f669d5333d9dab9f094ec16dabfb15548cf271e30fa515045a07428bd8669b
data/README.md CHANGED
@@ -38,24 +38,27 @@ $ gem install tcxread
38
38
  ```ruby
39
39
  require 'tcxread'
40
40
 
41
- data = TCXRead.new('23.tcx')
42
-
43
- puts "Distance meters: #{data.total_distance_meters}, " \
44
- "Time seconds: #{data.total_time_seconds}, " \
45
- "Calories: #{data.total_calories}, " \
46
- "Total ascent: #{data.total_ascent}, " \
47
- "Total descent: #{data.total_descent}, " \
48
- "Max altitude: #{data.max_altitude}, " \
49
- "Average heart rate: #{data.average_heart_rate}, " \
50
- "Average watts: #{data.average_watts}, " \
51
- "Max watts: #{data.max_watts}, " \
52
- "Average speed: #{data.average_speed_all}, " \
53
- "Average speed (moving): #{data.average_speed_moving}, " \
54
- "Average cadence (moving): #{data.average_cadence_biking}, " \
55
- "Average cadence: #{data.average_cadence_all}"
56
-
41
+ data = TCXRead.load_file('23.tcx')
42
+
43
+ puts [
44
+ "Distance meters: #{data.total_distance_meters}",
45
+ "Time seconds: #{data.total_time_seconds}",
46
+ "Calories: #{data.total_calories}",
47
+ "Total ascent: #{data.total_ascent}",
48
+ "Total descent: #{data.total_descent}",
49
+ "Max altitude: #{data.max_altitude}",
50
+ "Average heart rate: #{data.average_heart_rate}",
51
+ "Average watts: #{data.average_watts}",
52
+ "Max watts: #{data.max_watts}",
53
+ "Average speed: #{data.average_speed_all}",
54
+ "Average speed (moving): #{data.average_speed_moving}",
55
+ "Average cadence (moving): #{data.average_cadence_biking}",
56
+ "Average cadence: #{data.average_cadence_all}"
57
+ ].join("\n")
57
58
  ```
58
59
 
60
+ Use `TCXRead.parse(data)` to parse raw TCX data.
61
+
59
62
  ## 💾 Datasets
60
63
 
61
64
  Datasets available and used in the examples on the following links: [DATASET1](http://iztok-jr-fister.eu/static/publications/Sport5.zip), [DATASET2](http://iztok-jr-fister.eu/static/css/datasets/Sport.zip), [DATASET3](https://github.com/firefly-cpp/tcx-test-files).
data/lib/tcxread.rb CHANGED
@@ -1,14 +1,51 @@
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
 
9
- def initialize(file_path)
10
- @file_path = file_path
11
- @doc = Nokogiri::XML(File.open(file_path))
45
+ # Initializes the TCXRead object and parses the TCX file.
46
+ # @param file_path_or_xml [String] The file path of the TCX file to process or a Nokogiri::XML document.
47
+ def initialize(file_path_or_xml)
48
+ @doc = file_path_or_xml.is_a?(Nokogiri::XML::Document) ? file_path_or_xml : Nokogiri::XML(File.open(file_path_or_xml))
12
49
  @doc.root.add_namespace_definition('ns3', 'http://www.garmin.com/xmlschemas/ActivityExtension/v2')
13
50
 
14
51
  @total_distance_meters = 0
@@ -28,276 +65,185 @@ class TCXRead
28
65
  parse
29
66
  end
30
67
 
31
- def parse
32
- activities = parse_activities
33
- if activities.any?
34
- @total_time_seconds = activities.sum { |activity| activity[:total_time_seconds] }
35
- @total_distance_meters = activities.sum { |activity| activity[:total_distance_meters] }
36
- @total_calories = activities.sum { |activity| activity[:total_calories] }
37
- @total_ascent, @total_descent, @max_altitude = calculate_ascent_descent_and_max_altitude_from_activities(activities)
38
- @average_heart_rate = calculate_average_heart_rate_from_activities(activities)
39
- @max_watts, @average_watts = calculate_watts_from_activities(activities)
40
- cadence_results = calculate_average_cadence_from_activities(activities)
41
- @average_cadence_all = cadence_results[:average_cadence_all]
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
68
+ # Returns a TCXRead object from a File or file path.
69
+ # @param file_or_path [String, File] The file path or a Fule of the TCX file to process.
70
+ def self.load_file(file_or_path)
71
+ TCXRead.new(Nokogiri::XML(file_or_path.is_a?(File) ? file_or_path : File.open(file_or_path)))
72
+ end
47
73
 
48
- { activities: activities }
74
+ # Returns a TCXRead object from raw TCX data.
75
+ # @param data [String] TCX data to load.
76
+ def self.parse(data)
77
+ TCXRead.new(Nokogiri::XML(data))
49
78
  end
50
79
 
51
80
  private
52
81
 
82
+ # Parses the TCX file and computes metrics for all activities.
83
+ def parse
84
+ activities = parse_activities
85
+ return if activities.empty?
86
+
87
+ @total_time_seconds = activities.sum { |activity| activity[:total_time_seconds] }
88
+ @total_distance_meters = activities.sum { |activity| activity[:total_distance_meters] }
89
+ @total_calories = activities.sum { |activity| activity[:total_calories] }
90
+
91
+ @total_ascent, @total_descent, @max_altitude = calculate_ascent_descent_and_max_altitude_from_activities(activities)
92
+ @average_heart_rate = calculate_average(:heart_rate, activities)
93
+ @max_watts, @average_watts = calculate_watts_from_activities(activities)
94
+
95
+ cadence_results = calculate_average_cadence_from_activities(activities)
96
+ @average_cadence_all = cadence_results[:average_cadence_all]
97
+ @average_cadence_biking = cadence_results[:average_cadence_biking]
98
+
99
+ speed_results = calculate_average_speed_from_activities(activities)
100
+ @average_speed_all = speed_results[:average_speed_all]
101
+ @average_speed_moving = speed_results[:average_speed_moving]
102
+ end
103
+
104
+ # Parses activities from the TCX file.
105
+ # @return [Array<Hash>] An array of parsed activity data.
53
106
  def parse_activities
54
- activities = []
55
- @doc.xpath('//xmlns:Activities/xmlns:Activity').each do |activity|
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)
107
+ @doc.xpath('//xmlns:Activities/xmlns:Activity').map do |activity|
108
+ laps = activity.xpath('xmlns:Lap').map { |lap| parse_lap(lap) }
62
109
 
63
- activities << {
110
+ {
64
111
  sport: activity.attr('Sport'),
65
112
  id: activity.xpath('xmlns:Id').text,
66
113
  laps: laps,
67
- total_time_seconds: total_time_seconds,
68
- total_distance_meters: total_distance_meters,
69
- total_calories: total_calories,
70
- total_ascent: total_ascent,
71
- total_descent: total_descent,
72
- max_altitude: max_altitude,
73
- average_heart_rate: average_heart_rate
114
+ total_time_seconds: laps.sum { |lap| lap[:total_time_seconds] },
115
+ total_distance_meters: laps.sum { |lap| lap[:distance_meters] },
116
+ total_calories: laps.sum { |lap| lap[:calories] },
117
+ total_ascent: laps.sum { |lap| lap[:total_ascent] },
118
+ total_descent: laps.sum { |lap| lap[:total_descent] },
119
+ max_altitude: laps.map { |lap| lap[:max_altitude] }.max,
120
+ average_heart_rate: calculate_average(:heart_rate, laps)
74
121
  }
75
122
  end
76
- activities
77
123
  end
78
124
 
79
- def parse_laps(activity)
80
- laps = []
81
- activity.xpath('xmlns:Lap').each do |lap|
82
- laps << {
83
- start_time: lap.attr('StartTime'),
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
125
+ # Parses a single lap from the TCX file.
126
+ # @param lap [Nokogiri::XML::Node] The lap node to parse.
127
+ # @return [Hash] The parsed lap data.
128
+ def parse_lap(lap)
129
+ trackpoints = lap.xpath('xmlns:Track/xmlns:Trackpoint').map { |tp| parse_trackpoint(tp) }
98
130
 
99
- def parse_tracks(lap)
100
- tracks = []
101
- lap.xpath('xmlns:Track').each do |track|
102
- trackpoints = []
103
- track.xpath('xmlns:Trackpoint').each do |trackpoint|
104
- trackpoints << {
105
- time: trackpoint.xpath('xmlns:Time').text,
106
- position: parse_position(trackpoint),
107
- altitude_meters: trackpoint.xpath('xmlns:AltitudeMeters').text.to_f,
108
- distance_meters: trackpoint.xpath('xmlns:DistanceMeters').text.to_f,
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
- speed: trackpoint.xpath('xmlns:Extensions/ns3:TPX/ns3:Speed').text.to_f
114
- }
115
- end
116
- tracks << trackpoints
117
- end
118
- tracks
131
+ {
132
+ start_time: lap.attr('StartTime'),
133
+ total_time_seconds: lap.xpath('xmlns:TotalTimeSeconds').text.to_f,
134
+ distance_meters: lap.xpath('xmlns:DistanceMeters').text.to_f,
135
+ calories: lap.xpath('xmlns:Calories').text.to_i,
136
+ total_ascent: calculate_ascent(trackpoints),
137
+ total_descent: calculate_descent(trackpoints),
138
+ max_altitude: trackpoints.map { |tp| tp[:altitude_meters] }.max || 0,
139
+ trackpoints: trackpoints || []
140
+ }
119
141
  end
120
142
 
121
- def parse_position(trackpoint)
122
- position = trackpoint.at_xpath('xmlns:Position')
123
- return nil unless position
124
-
143
+ # Parses a single trackpoint from the TCX file.
144
+ # @param trackpoint [Nokogiri::XML::Node] The trackpoint node to parse.
145
+ # @return [Hash] The parsed trackpoint data.
146
+ def parse_trackpoint(trackpoint)
125
147
  {
126
- latitude: position.xpath('xmlns:LatitudeDegrees').text.to_f,
127
- longitude: position.xpath('xmlns:LongitudeDegrees').text.to_f
148
+ time: trackpoint.xpath('xmlns:Time').text,
149
+ altitude_meters: trackpoint.xpath('xmlns:AltitudeMeters').text.to_f,
150
+ distance_meters: trackpoint.xpath('xmlns:DistanceMeters').text.to_f,
151
+ heart_rate: trackpoint.xpath('xmlns:HeartRateBpm/xmlns:Value').text.to_i,
152
+ cadence: trackpoint.xpath('xmlns:Cadence').text.to_i,
153
+ watts: trackpoint.xpath('xmlns:Extensions/ns3:TPX/ns3:Watts').text.to_f,
154
+ speed: trackpoint.xpath('xmlns:Extensions/ns3:TPX/ns3:Speed').text.to_f
128
155
  }
129
156
  end
130
157
 
131
- def calculate_ascent_descent_and_max_altitude(laps)
132
- total_ascent = 0.0
133
- total_descent = 0.0
134
- max_altitude = -Float::INFINITY
158
+ # Calculates total ascent from an array of trackpoints.
159
+ # @param trackpoints [Array<Hash>] An array of trackpoint data.
160
+ # @return [Float] The total ascent in meters.
161
+ def calculate_ascent(trackpoints)
135
162
  previous_altitude = nil
136
-
137
- laps.each do |lap|
138
- lap[:tracks].flatten.each do |trackpoint|
139
- altitude = trackpoint[:altitude_meters]
140
- max_altitude = altitude if altitude > max_altitude
141
-
142
- if previous_altitude
143
- altitude_change = altitude - previous_altitude
144
- if altitude_change > 0
145
- total_ascent += altitude_change
146
- elsif altitude_change < 0
147
- total_descent += altitude_change.abs
148
- end
149
- end
150
-
151
- previous_altitude = altitude
152
- end
163
+ trackpoints.sum do |tp|
164
+ altitude = tp[:altitude_meters]
165
+ ascent = previous_altitude && altitude > previous_altitude ? altitude - previous_altitude : 0
166
+ previous_altitude = altitude
167
+ ascent
153
168
  end
154
-
155
- [total_ascent, total_descent, max_altitude]
156
169
  end
157
170
 
158
- def calculate_ascent_descent_and_max_altitude_from_activities(activities)
159
- total_ascent = 0.0
160
- total_descent = 0.0
161
- max_altitude = -Float::INFINITY
162
-
163
- activities.each do |activity|
164
- total_ascent += activity[:total_ascent]
165
- total_descent += activity[:total_descent]
166
- max_altitude = activity[:max_altitude] if activity[:max_altitude] > max_altitude
171
+ # Calculates total descent from an array of trackpoints.
172
+ # @param trackpoints [Array<Hash>] An array of trackpoint data.
173
+ # @return [Float] The total descent in meters.
174
+ def calculate_descent(trackpoints)
175
+ previous_altitude = nil
176
+ trackpoints.sum do |tp|
177
+ altitude = tp[:altitude_meters]
178
+ descent = previous_altitude && altitude < previous_altitude ? previous_altitude - altitude : 0
179
+ previous_altitude = altitude
180
+ descent
167
181
  end
168
-
169
- [total_ascent, total_descent, max_altitude]
170
182
  end
171
183
 
172
- def calculate_average_heart_rate(laps)
173
- total_heart_rate = 0
174
- heart_rate_count = 0
175
-
176
- laps.each do |lap|
177
- lap[:tracks].flatten.each do |trackpoint|
178
- heart_rate = trackpoint[:heart_rate]
179
- if heart_rate > 0
180
- total_heart_rate += heart_rate
181
- heart_rate_count += 1
182
- end
183
- end
184
- end
185
-
186
- heart_rate_count > 0 ? total_heart_rate.to_f / heart_rate_count : 0.0
184
+ # Calculates ascent, descent, and maximum altitude from activities.
185
+ # @param activities [Array<Hash>] An array of activity data.
186
+ # @return [Array<Float>] Total ascent, total descent, and maximum altitude.
187
+ def calculate_ascent_descent_and_max_altitude_from_activities(activities)
188
+ total_ascent = activities.sum { |activity| activity[:total_ascent] }
189
+ total_descent = activities.sum { |activity| activity[:total_descent] }
190
+ max_altitude = activities.map { |activity| activity[:max_altitude] }.max
191
+ [total_ascent, total_descent, max_altitude]
187
192
  end
188
193
 
189
- def calculate_average_heart_rate_from_activities(activities)
190
- total_heart_rate = 0
191
- heart_rate_count = 0
192
-
193
- activities.each do |activity|
194
- activity[:laps].each do |lap|
195
- lap[:tracks].flatten.each do |trackpoint|
196
- heart_rate = trackpoint[:heart_rate]
197
- if heart_rate > 0
198
- total_heart_rate += heart_rate
199
- heart_rate_count += 1
200
- end
201
- end
202
- end
203
- end
204
-
205
- heart_rate_count > 0 ? total_heart_rate.to_f / heart_rate_count : 0.0
194
+ # Calculates the average value of a metric across laps or activities.
195
+ # @param metric [Symbol] The metric to average (e.g., :heart_rate).
196
+ # @param laps_or_activities [Array<Hash>] An array of lap or activity data.
197
+ # @return [Float] The average value of the metric.
198
+ def calculate_average(metric, laps_or_activities)
199
+ values = laps_or_activities.flat_map do |lap|
200
+ next [] unless lap[:trackpoints]
201
+ lap[:trackpoints].map { |tp| tp[metric] }
202
+ end.compact
203
+
204
+ values.any? ? values.sum.to_f / values.size : 0.0
206
205
  end
207
206
 
207
+ # Calculates power metrics from activities.
208
+ # @param activities [Array<Hash>] An array of activity data.
209
+ # @return [Array<String, Float>] Maximum and average power output.
208
210
  def calculate_watts_from_activities(activities)
209
- max_watts = 0
210
- total_watts = 0
211
- watts_count = 0
212
-
213
- activities.each do |activity|
214
- activity[:laps].each do |lap|
215
- lap[:tracks].flatten.each do |trackpoint|
216
- watts = trackpoint[:watts]
217
- if watts > 0
218
- total_watts += watts
219
- watts_count += 1
220
- max_watts = watts if watts > max_watts
221
- end
222
- end
211
+ watts = activities.flat_map do |activity|
212
+ activity[:laps].flat_map do |lap|
213
+ lap[:trackpoints]&.map { |tp| tp[:watts] } || []
223
214
  end
224
- end
215
+ end.compact
225
216
 
226
- if watts_count > 0
227
- average_watts = total_watts.to_f / watts_count
228
- max_watts = max_watts
217
+ if watts.any?
218
+ max_watts = watts.max
219
+ avg_watts = watts.sum.to_f / watts.size
229
220
  else
230
- average_watts = 'NA'
231
221
  max_watts = 'NA'
222
+ avg_watts = 'NA'
232
223
  end
233
224
 
234
- [max_watts, average_watts]
225
+ [max_watts, avg_watts]
235
226
  end
236
227
 
228
+ # Calculates average cadence metrics from activities.
229
+ # @param activities [Array<Hash>] An array of activity data.
230
+ # @return [Hash] Average cadence metrics for all activities and biking only.
237
231
  def calculate_average_cadence_from_activities(activities)
238
- total_cadence_all = 0
239
- total_cadence_biking = 0
240
- cadence_count_all = 0
241
- cadence_count_biking = 0
242
-
243
- activities.each do |activity|
244
- activity[:laps].each do |lap|
245
- lap[:tracks].flatten.each do |trackpoint|
246
- cadence = trackpoint[:cadence]
247
- total_cadence_all += cadence
248
- cadence_count_all += 1
249
-
250
- if cadence > 0
251
- total_cadence_biking += cadence
252
- cadence_count_biking += 1
253
- end
254
- end
255
- end
256
- end
257
-
258
- average_cadence_all = cadence_count_all > 0 ? total_cadence_all.to_f / cadence_count_all : 0.0
259
- average_cadence_biking = cadence_count_biking > 0 ? total_cadence_biking.to_f / cadence_count_biking : 0.0
260
-
232
+ cadences = activities.flat_map { |activity| activity[:laps].flat_map { |lap| lap[:trackpoints].map { |tp| tp[:cadence] } if lap[:trackpoints] } }.compact
261
233
  {
262
- average_cadence_all: average_cadence_all,
263
- average_cadence_biking: average_cadence_biking
234
+ average_cadence_all: cadences.any? ? cadences.sum.to_f / cadences.size : 0.0,
235
+ average_cadence_biking: cadences.reject(&:zero?).any? ? cadences.reject(&:zero?).sum.to_f / cadences.reject(&:zero?).size : 0.0
264
236
  }
265
237
  end
266
238
 
267
- # Calculates the average speed from the activities.
268
- #
269
- # @param activities [Array<Hash>] an array of activity hashes.
270
- # @return [Hash] a hash containing average speed including zeros and average speed while moving.
239
+ # Calculates average speed metrics from activities.
240
+ # @param activities [Array<Hash>] An array of activity data.
241
+ # @return [Hash] Average speed metrics for the whole activity and moving activities only.
271
242
  def calculate_average_speed_from_activities(activities)
272
- total_speed_all = 0
273
- total_speed_moving = 0
274
- speed_count_all = 0
275
- speed_count_moving = 0
276
-
277
- activities.each do |activity|
278
- activity[:laps].each do |lap|
279
- lap[:tracks].flatten.each do |trackpoint|
280
- speed = trackpoint[:speed]
281
-
282
- if speed
283
- total_speed_all += speed
284
- speed_count_all += 1
285
-
286
- if speed > 0
287
- total_speed_moving += speed
288
- speed_count_moving += 1
289
- end
290
- end
291
- end
292
- end
293
- end
294
-
295
- average_speed_all = speed_count_all > 0 ? total_speed_all.to_f / speed_count_all : 0.0
296
- average_speed_moving = speed_count_moving > 0 ? total_speed_moving.to_f / speed_count_moving : 0.0
297
-
243
+ speeds = activities.flat_map { |activity| activity[:laps].flat_map { |lap| lap[:trackpoints].map { |tp| tp[:speed] } if lap[:trackpoints] } }.compact
298
244
  {
299
- average_speed_all: average_speed_all,
300
- average_speed_moving: average_speed_moving
245
+ average_speed_all: speeds.any? ? speeds.sum.to_f / speeds.size : 0.0,
246
+ average_speed_moving: speeds.reject(&:zero?).any? ? speeds.reject(&:zero?).sum.to_f / speeds.reject(&:zero?).size : 0.0
301
247
  }
302
248
  end
303
249
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tcxread
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - firefly-cpp
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-08-20 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: nokogiri
@@ -24,7 +23,6 @@ dependencies:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
25
  version: '1.11'
27
- description:
28
26
  email:
29
27
  - iztok@iztok-jr-fister.eu
30
28
  executables: []
@@ -41,7 +39,6 @@ metadata:
41
39
  homepage_uri: https://github.com/firefly-cpp/tcxread
42
40
  source_code_uri: https://github.com/firefly-cpp/tcxread
43
41
  changelog_uri: https://github.com/firefly-cpp/tcxread
44
- post_install_message:
45
42
  rdoc_options: []
46
43
  require_paths:
47
44
  - lib
@@ -56,8 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
56
53
  - !ruby/object:Gem::Version
57
54
  version: '0'
58
55
  requirements: []
59
- rubygems_version: 3.5.11
60
- signing_key:
56
+ rubygems_version: 3.6.9
61
57
  specification_version: 4
62
58
  summary: tcx reader/parser in Ruby
63
59
  test_files: []