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.
- checksums.yaml +7 -0
- data/lib/tcxread.rb +229 -0
- 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: []
|