tcxread 0.1.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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/tcxread.rb +229 -0
  3. metadata +61 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dc0fbb7ddc6ca33b8c42dd160de524513ed81570dcf9f82c2b28410af1e036c9
4
+ data.tar.gz: 9ee1726006b6f3a267bd3cc72f6accae4c7d275058a0998febdb039d365d8b98
5
+ SHA512:
6
+ metadata.gz: a5fcce73beceff1cfd7dadfaf6c5c9afa4c8e599ac2be1cf09181d505140b2d48c464f981705b20cf98b808023cd5bf04d442bdebf671d3b4c529c18270ab3d5
7
+ data.tar.gz: 9537cdffd7f6b8973ca497841e8b818b330f318fcbebee83a2120a87a75f6a6b797438f7f074579cb02b811a980968d6307907b552c42597d3da5ae52266009b
data/lib/tcxread.rb ADDED
@@ -0,0 +1,229 @@
1
+ require "nokogiri"
2
+
3
+ # TCXRead is a class that parses TCX (Training Center XML) files to extract
4
+ # workout data such as activities, laps, tracks, trackpoints, and integral metrics.
5
+ class TCXRead
6
+ attr_reader :total_distance_meters, :total_time_seconds, :total_calories,
7
+ :total_ascent, :total_descent, :max_altitude, :average_heart_rate
8
+
9
+ # Initializes the TCXRead with the path to the TCX file.
10
+ #
11
+ # @param file_path [String] the path to the TCX file.
12
+ def initialize(file_path)
13
+ @file_path = file_path
14
+ @doc = Nokogiri::XML(File.open(file_path))
15
+
16
+ # Init the properties
17
+ @total_distance_meters = 0
18
+ @total_time_seconds = 0
19
+ @total_calories = 0
20
+ @total_ascent = 0
21
+ @total_descent = 0
22
+ @max_altitude = 0
23
+ @average_heart_rate = 0
24
+
25
+ parse
26
+ end
27
+
28
+ # Parses the TCX file and extracts data.
29
+ #
30
+ # @return [Hash] a hash containing the parsed activities.
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
+ end
40
+
41
+ { activities: activities }
42
+ end
43
+
44
+ private
45
+
46
+ # Parses the activities from the TCX file.
47
+ #
48
+ # @return [Array<Hash>] an array of hashes, each representing an activity.
49
+ def parse_activities
50
+ activities = []
51
+ @doc.xpath('//xmlns:Activities/xmlns:Activity').each do |activity|
52
+ laps = parse_laps(activity)
53
+ total_time_seconds = laps.sum { |lap| lap[:total_time_seconds] }
54
+ total_distance_meters = laps.sum { |lap| lap[:distance_meters] }
55
+ total_calories = laps.sum { |lap| lap[:calories] }
56
+ total_ascent, total_descent, max_altitude = calculate_ascent_descent_and_max_altitude(laps)
57
+ average_heart_rate = calculate_average_heart_rate(laps)
58
+
59
+ activities << {
60
+ sport: activity.attr('Sport'),
61
+ id: activity.xpath('xmlns:Id').text,
62
+ laps: laps,
63
+ total_time_seconds: total_time_seconds,
64
+ total_distance_meters: total_distance_meters,
65
+ total_calories: total_calories,
66
+ total_ascent: total_ascent,
67
+ total_descent: total_descent,
68
+ max_altitude: max_altitude,
69
+ average_heart_rate: average_heart_rate
70
+ }
71
+ end
72
+ activities
73
+ end
74
+
75
+ # Parses the laps for a given activity.
76
+ #
77
+ # @param activity [Nokogiri::XML::Element] the activity element from the TCX file.
78
+ # @return [Array<Hash>] an array of hashes, each representing a lap.
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
98
+
99
+ # Parses the tracks for a given lap.
100
+ #
101
+ # @param lap [Nokogiri::XML::Element] the lap element from the TCX file.
102
+ # @return [Array<Array<Hash>>] an array of arrays, each representing a track containing trackpoints.
103
+ def parse_tracks(lap)
104
+ tracks = []
105
+ lap.xpath('xmlns:Track').each do |track|
106
+ trackpoints = []
107
+ track.xpath('xmlns:Trackpoint').each do |trackpoint|
108
+ trackpoints << {
109
+ time: trackpoint.xpath('xmlns:Time').text,
110
+ position: parse_position(trackpoint),
111
+ altitude_meters: trackpoint.xpath('xmlns:AltitudeMeters').text.to_f,
112
+ distance_meters: trackpoint.xpath('xmlns:DistanceMeters').text.to_f,
113
+ heart_rate: trackpoint.xpath('xmlns:HeartRateBpm/xmlns:Value').text.to_i,
114
+ cadence: trackpoint.xpath('xmlns:Cadence').text.to_i,
115
+ sensor_state: trackpoint.xpath('xmlns:SensorState').text
116
+ }
117
+ end
118
+ tracks << trackpoints
119
+ end
120
+ tracks
121
+ end
122
+
123
+ # Parses the position for a given trackpoint.
124
+ #
125
+ # @param trackpoint [Nokogiri::XML::Element] the trackpoint element from the TCX file.
126
+ # @return [Hash, nil] a hash representing the position (latitude and longitude) or nil if no position is available.
127
+ def parse_position(trackpoint)
128
+ position = trackpoint.at_xpath('xmlns:Position')
129
+ return nil unless position
130
+
131
+ {
132
+ latitude: position.xpath('xmlns:LatitudeDegrees').text.to_f,
133
+ longitude: position.xpath('xmlns:LongitudeDegrees').text.to_f
134
+ }
135
+ end
136
+
137
+ # Calculates the total ascent, total descent, and maximum altitude from the laps.
138
+ #
139
+ # @param laps [Array<Hash>] an array of lap hashes.
140
+ # @return [Array<Float>] an array containing total ascent, total descent, and maximum altitude.
141
+ def calculate_ascent_descent_and_max_altitude(laps)
142
+ total_ascent = 0.0
143
+ total_descent = 0.0
144
+ max_altitude = -Float::INFINITY
145
+ previous_altitude = nil
146
+
147
+ laps.each do |lap|
148
+ lap[:tracks].flatten.each do |trackpoint|
149
+ altitude = trackpoint[:altitude_meters]
150
+ max_altitude = altitude if altitude > max_altitude
151
+
152
+ if previous_altitude
153
+ altitude_change = altitude - previous_altitude
154
+ if altitude_change > 0
155
+ total_ascent += altitude_change
156
+ elsif altitude_change < 0
157
+ total_descent += altitude_change.abs
158
+ end
159
+ end
160
+
161
+ previous_altitude = altitude
162
+ end
163
+ end
164
+
165
+ [total_ascent, total_descent, max_altitude]
166
+ end
167
+
168
+ # Calculates the total ascent, total descent, and maximum altitude from the activities.
169
+ #
170
+ # @param activities [Array<Hash>] an array of activity hashes.
171
+ # @return [Array<Float>] an array containing total ascent, total descent, and maximum altitude.
172
+ def calculate_ascent_descent_and_max_altitude_from_activities(activities)
173
+ total_ascent = 0.0
174
+ total_descent = 0.0
175
+ max_altitude = -Float::INFINITY
176
+
177
+ activities.each do |activity|
178
+ total_ascent += activity[:total_ascent]
179
+ total_descent += activity[:total_descent]
180
+ max_altitude = activity[:max_altitude] if activity[:max_altitude] > max_altitude
181
+ end
182
+
183
+ [total_ascent, total_descent, max_altitude]
184
+ end
185
+
186
+ # Calculates the average heart rate from the laps.
187
+ #
188
+ # @param laps [Array<Hash>] an array of lap hashes.
189
+ # @return [Float] the average heart rate.
190
+ def calculate_average_heart_rate(laps)
191
+ total_heart_rate = 0
192
+ heart_rate_count = 0
193
+
194
+ 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
+
204
+ heart_rate_count > 0 ? total_heart_rate.to_f / heart_rate_count : 0.0
205
+ end
206
+
207
+ # Calculates the average heart rate from the activities.
208
+ #
209
+ # @param activities [Array<Hash>] an array of activity hashes.
210
+ # @return [Float] the average heart rate.
211
+ def calculate_average_heart_rate_from_activities(activities)
212
+ total_heart_rate = 0
213
+ heart_rate_count = 0
214
+
215
+ activities.each do |activity|
216
+ activity[:laps].each do |lap|
217
+ lap[:tracks].flatten.each do |trackpoint|
218
+ heart_rate = trackpoint[:heart_rate]
219
+ if heart_rate > 0
220
+ total_heart_rate += heart_rate
221
+ heart_rate_count += 1
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ heart_rate_count > 0 ? total_heart_rate.to_f / heart_rate_count : 0.0
228
+ end
229
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tcxread
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - firefly-cpp
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-07-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ description:
28
+ email:
29
+ - iztok@iztok-jr-fister.eu
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/tcxread.rb
35
+ homepage: https://github.com/firefly-cpp/tcxread
36
+ licenses:
37
+ - MIT
38
+ metadata:
39
+ homepage_uri: https://github.com/firefly-cpp/tcxread
40
+ source_code_uri: https://github.com/firefly-cpp/tcxread
41
+ changelog_uri: https://github.com/firefly-cpp/tcxread
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.6.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.5.11
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: tcx reader/parser in Ruby
61
+ test_files: []