tcxread 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []