tcx 0.1.0 → 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/CHANGELOG.md +6 -2
- data/README.md +30 -5
- data/lib/tcx/extensions/activity_extension/v2/activity_lap.rb +21 -0
- data/lib/tcx/extensions/activity_extension/v2/activity_trackpoint.rb +22 -0
- data/lib/tcx/extensions/activity_extension/v2/base.rb +16 -0
- data/lib/tcx/extensions/activity_extension/v2/cadence_sensor_type.rb +16 -0
- data/lib/tcx/extensions/activity_extension/v2.rb +6 -0
- data/lib/tcx/extensions.rb +3 -0
- data/lib/tcx/file.rb +30 -0
- data/lib/tcx/types/abstract_source.rb +7 -0
- data/lib/tcx/{models → types}/activity.rb +4 -0
- data/lib/tcx/types/activity_list.rb +21 -0
- data/lib/tcx/types/application.rb +13 -0
- data/lib/tcx/types/base.rb +111 -0
- data/lib/tcx/types/build.rb +10 -0
- data/lib/tcx/types/build_type.rb +12 -0
- data/lib/tcx/types/cadence.rb +9 -0
- data/lib/tcx/types/course_list.rb +21 -0
- data/lib/tcx/{models → types}/database.rb +22 -0
- data/lib/tcx/{models/abstract_source.rb → types/device.rb} +5 -2
- data/lib/tcx/types/duration.rb +10 -0
- data/lib/tcx/types/extensions_list.rb +9 -0
- data/lib/tcx/types/heart_rate_as_percent_of_max.rb +9 -0
- data/lib/tcx/types/heart_rate_bpm.rb +9 -0
- data/lib/tcx/types/heart_rate_in_beats_per_minute.rb +9 -0
- data/lib/tcx/{models → types}/lap.rb +5 -1
- data/lib/tcx/{models → types}/trackpoint.rb +1 -1
- data/lib/tcx/types/workout_list.rb +21 -0
- data/lib/tcx/types.rb +43 -0
- data/lib/tcx/version.rb +1 -1
- data/lib/tcx.rb +9 -4
- metadata +51 -41
- data/lib/tcx/models/activity_list.rb +0 -9
- data/lib/tcx/models/base.rb +0 -30
- data/lib/tcx/models/cadence.rb +0 -9
- data/lib/tcx/models/course_list.rb +0 -9
- data/lib/tcx/models/duration.rb +0 -6
- data/lib/tcx/models/extensions.rb +0 -6
- data/lib/tcx/models/heart_rate_as_percent_of_max.rb +0 -9
- data/lib/tcx/models/heart_rate_bpm.rb +0 -9
- data/lib/tcx/models/heart_rate_in_beats_per_minute.rb +0 -9
- data/lib/tcx/models/value.rb +0 -9
- data/lib/tcx/models/workout_list.rb +0 -9
- data/lib/tcx/models.rb +0 -40
- /data/lib/tcx/{models → types}/course.rb +0 -0
- /data/lib/tcx/{models → types}/course_folder.rb +0 -0
- /data/lib/tcx/{models → types}/course_lap.rb +0 -0
- /data/lib/tcx/{models → types}/course_point.rb +0 -0
- /data/lib/tcx/{models → types}/course_point_type.rb +0 -0
- /data/lib/tcx/{models → types}/courses.rb +0 -0
- /data/lib/tcx/{models → types}/custom_heart_rate_zone.rb +0 -0
- /data/lib/tcx/{models → types}/folders.rb +0 -0
- /data/lib/tcx/{models → types}/gender.rb +0 -0
- /data/lib/tcx/{models → types}/heart_rate_value.rb +0 -0
- /data/lib/tcx/{models → types}/intensity.rb +0 -0
- /data/lib/tcx/{models → types}/name_key_reference.rb +0 -0
- /data/lib/tcx/{models → types}/position.rb +0 -0
- /data/lib/tcx/{models → types}/predefined_heart_rate_zone.rb +0 -0
- /data/lib/tcx/{models → types}/sensor_state.rb +0 -0
- /data/lib/tcx/{models → types}/sport.rb +0 -0
- /data/lib/tcx/{models → types}/step.rb +0 -0
- /data/lib/tcx/{models → types}/target.rb +0 -0
- /data/lib/tcx/{models → types}/track.rb +0 -0
- /data/lib/tcx/{models → types}/trigger_method.rb +0 -0
- /data/lib/tcx/{models → types}/version.rb +0 -0
- /data/lib/tcx/{models → types}/workout.rb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16a519fa1d9c70b3f319428170d9e52f36a1eaa4baacdd4004d336e35c57d2c6
|
|
4
|
+
data.tar.gz: 57571958ee9e4182e5c271ebec43639c78b46253e837caf0eb1bbd55b553398c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e1339b4ef3c6817baf72d973d1b00c92b1ad109927a7012892758d27e3140b863786a431ee25b8176130801549edbad992939c3db4dfa853763802d60d36630
|
|
7
|
+
data.tar.gz: 631de4c6b5e4dabe0c3db3b87255583d4dd30e05cb58425e4e10aa407b2b43136c82c2f3e840cd428d5c16e86b10f89cf25655d13e371b3e8bcdee2ab9357500
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
### 0.
|
|
1
|
+
### 0.2.0 (2025-10-24)
|
|
2
2
|
|
|
3
|
-
*
|
|
3
|
+
* Added support for writing TCX files - [@dblock](https://github.com/dblock).
|
|
4
|
+
|
|
5
|
+
### 0.1.0 (2025-10-23)
|
|
6
|
+
|
|
7
|
+
* Initial public release, generic TCX reader - [@dblock](https://github.com/dblock).
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://badge.fury.io/rb/tcx)
|
|
4
4
|
[](https://github.com/dblock/tcx/actions/workflows/test.yml)
|
|
5
5
|
|
|
6
|
-
A Garmin Training Center XML (TCX) reader
|
|
6
|
+
A Garmin Training Center XML (TCX) reader and writer. Unlike other libraries such as [tcx_rb](https://github.com/keithdoggett/tcx_rb) or [tcxread](https://github.com/firefly-cpp/tcxread), provides a consistent API by implementing the complete read/write [TCX schema](https://www8.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd) with extensions in a structured and organized way.
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
@@ -17,16 +17,41 @@ Run `bundle install`.
|
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
19
19
|
|
|
20
|
+
### Working with Files
|
|
21
|
+
|
|
22
|
+
Read and write TCX files using `Tcx#load_file` and `dump`.
|
|
23
|
+
|
|
20
24
|
```ruby
|
|
21
25
|
require 'tcx'
|
|
22
26
|
|
|
23
|
-
tcx = Tcx.load_file('
|
|
27
|
+
tcx = Tcx.load_file('activities.tcx') # => Tcx::Database
|
|
28
|
+
|
|
29
|
+
tcx.activities # => [Tcx::Activity], array of Tcx::Activity
|
|
30
|
+
tcx.workouts # => [Tcx::Workout]
|
|
31
|
+
tcx.courses # => [Tcx::Course]
|
|
32
|
+
|
|
33
|
+
tcx.dump # overwrites activities.tcx
|
|
24
34
|
|
|
25
|
-
tcx.
|
|
26
|
-
...
|
|
35
|
+
tcx.dump('activities2.tcx') # writes to activities2.tcx
|
|
27
36
|
```
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
### Working with XML Data
|
|
39
|
+
|
|
40
|
+
Directly manipulate TCX data without creating files.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
data = File.read('activities.tcx') # String
|
|
44
|
+
|
|
45
|
+
tcx = Tcx.load(data) # => Tcx::Database
|
|
46
|
+
|
|
47
|
+
tcx.to_xml # => XML string
|
|
48
|
+
|
|
49
|
+
tcx.dump('activities2.tcx') # writes to activities2.tcx
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Examples
|
|
53
|
+
|
|
54
|
+
See [examples/multiple_running_activities.rb](examples/multiple_running_activities.rb) for a complete working example.
|
|
30
55
|
|
|
31
56
|
## Upgrading
|
|
32
57
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
module Extensions
|
|
5
|
+
module ActivityExtension
|
|
6
|
+
module V2
|
|
7
|
+
# LX
|
|
8
|
+
class ActivityLap < Base
|
|
9
|
+
property 'avg_speed', from: 'AvgSpeed', transform_with: lambda(&:to_f)
|
|
10
|
+
property 'max_bike_cadence', from: 'MaxBikeCadence', transform_with: lambda(&:to_i)
|
|
11
|
+
property 'avg_run_cadence', from: 'AvgRunCadence', transform_with: lambda(&:to_i)
|
|
12
|
+
property 'max_run_cadence', from: 'MaxRunCadence', transform_with: lambda(&:to_i)
|
|
13
|
+
property 'steps', from: 'Steps', transform_with: lambda(&:to_i)
|
|
14
|
+
property 'avg_watts', from: 'AvgWatts', transform_with: lambda(&:to_i)
|
|
15
|
+
property 'max_watts', from: 'MaxWatts', transform_with: lambda(&:to_i)
|
|
16
|
+
property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
module Extensions
|
|
5
|
+
module ActivityExtension
|
|
6
|
+
module V2
|
|
7
|
+
# TPX
|
|
8
|
+
class ActivityTrackpoint < Base
|
|
9
|
+
property 'speed', from: 'Speed', transform_with: lambda(&:to_f)
|
|
10
|
+
property 'run_cadence', from: 'RunCadence', transform_with: lambda(&:to_i)
|
|
11
|
+
property 'watts', from: 'Watts', transform_with: lambda(&:to_i)
|
|
12
|
+
property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
|
|
13
|
+
property 'cadence_sensor', from: 'CadenceSensor', transform_with: ->(v) { CadenceSensorType.parse(v) }
|
|
14
|
+
|
|
15
|
+
def self.attributes
|
|
16
|
+
['cadence_sensor']
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
module Extensions
|
|
5
|
+
module ActivityExtension
|
|
6
|
+
module V2
|
|
7
|
+
# LX
|
|
8
|
+
class Base < Tcx::Base
|
|
9
|
+
def self.namespace
|
|
10
|
+
'http://www.garmin.com/xmlschemas/ActivityExtension/v2'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/tcx/file.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
class File
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
attr_accessor :file_path
|
|
8
|
+
|
|
9
|
+
def_delegators :database, :folders, :activities, :workouts, :courses, :author, :to_xml
|
|
10
|
+
|
|
11
|
+
def initialize(file_path = nil)
|
|
12
|
+
@file_path = file_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def database
|
|
16
|
+
@database ||= if file_path
|
|
17
|
+
::File.open(file_path) do |file|
|
|
18
|
+
xml = Nokogiri::XML(file)
|
|
19
|
+
Tcx::Database.parse(xml.root)
|
|
20
|
+
end
|
|
21
|
+
else
|
|
22
|
+
Tcx::Database.new
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def dump(target_path = nil)
|
|
27
|
+
database.dump(target_path)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -7,5 +7,9 @@ module Tcx
|
|
|
7
7
|
property 'laps', from: 'Lap', transform_with: ->(v) { to_array(v).map { |el| Lap.parse(el) } }
|
|
8
8
|
property 'notes', from: 'Notes'
|
|
9
9
|
property 'creator', from: 'Creator', transform_with: ->(v) { AbstractSource.parse(v) }
|
|
10
|
+
|
|
11
|
+
def self.attributes
|
|
12
|
+
['sport']
|
|
13
|
+
end
|
|
10
14
|
end
|
|
11
15
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
class ActivityList < Base
|
|
5
|
+
property 'activities', from: 'Activities', transform_with: ->(v) { v.map { |el| Activity.parse(el) } }
|
|
6
|
+
|
|
7
|
+
def_delegators :activities, :each, :count
|
|
8
|
+
|
|
9
|
+
def self.parse(list)
|
|
10
|
+
ActivityList.new('Activities' => list.xpath('xmlns:Activity'))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build_xml(builder)
|
|
14
|
+
activities.each do |activity|
|
|
15
|
+
builder.Activity(activity.attributes) do |activity_builder|
|
|
16
|
+
activity.build_xml(activity_builder)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
class Application < AbstractSource
|
|
5
|
+
property 'build', from: 'Build', transform_with: ->(v) { Build.parse(v) }
|
|
6
|
+
property 'lang_id', from: 'LangID'
|
|
7
|
+
property 'part_number', from: 'PartNumber'
|
|
8
|
+
|
|
9
|
+
def attributes
|
|
10
|
+
super.merge('xsi:type' => 'Application_t')
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
class Base < Hashie::Trash
|
|
5
|
+
include Hashie::Extensions::IgnoreUndeclared
|
|
6
|
+
extend Forwardable
|
|
7
|
+
|
|
8
|
+
def self.parse(xml)
|
|
9
|
+
if (tt = xml['xsi:type'])
|
|
10
|
+
klass = Tcx.const_get(tt.gsub(/_t$/, ''))
|
|
11
|
+
return klass.parse(xml) if self != klass
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attributes = xml.attributes.to_h
|
|
15
|
+
|
|
16
|
+
xml.children.each do |child|
|
|
17
|
+
next unless child.is_a?(Nokogiri::XML::Element)
|
|
18
|
+
|
|
19
|
+
attributes[child.name] = if attributes.key?(child.name)
|
|
20
|
+
to_array(attributes[child.name]) + [child]
|
|
21
|
+
elsif child.children.one? && child.children.first.is_a?(Nokogiri::XML::Text)
|
|
22
|
+
child.text
|
|
23
|
+
else
|
|
24
|
+
child
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
new attributes
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Transform an XmlElement into [XmlElement], cannot be used with Array or causes a coercion error
|
|
32
|
+
def self.to_array(value)
|
|
33
|
+
value.is_a?(Array) ? value : [value]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.attributes
|
|
37
|
+
[]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.namespace
|
|
41
|
+
'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.namespace_definitions
|
|
45
|
+
{
|
|
46
|
+
'xsi:schemaLocation' => 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd',
|
|
47
|
+
'xmlns' => 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2',
|
|
48
|
+
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
|
|
49
|
+
'xmlns:ns2' => 'http://www.garmin.com/xmlschemas/UserProfile/v2',
|
|
50
|
+
'xmlns:ns3' => 'http://www.garmin.com/xmlschemas/ActivityExtension/v2',
|
|
51
|
+
'xmlns:ns4' => 'http://www.garmin.com/xmlschemas/ProfileExtension/v1',
|
|
52
|
+
'xmlns:ns5' => 'http://www.garmin.com/xmlschemas/ActivityGoals/v1'
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.xmlns
|
|
57
|
+
@xmlns ||= namespace_definitions.invert
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def attributes
|
|
61
|
+
attribute_values = {}
|
|
62
|
+
self.class.attributes.each do |attribute|
|
|
63
|
+
value = self[attribute]
|
|
64
|
+
next unless value
|
|
65
|
+
|
|
66
|
+
property_name = self.class.inverse_translations.fetch(attribute, attribute)
|
|
67
|
+
attribute_values[property_name] = build_value(value)
|
|
68
|
+
end
|
|
69
|
+
attribute_values
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_xml(builder)
|
|
73
|
+
(self.class.properties - self.class.attributes).each do |property|
|
|
74
|
+
value = self[property]
|
|
75
|
+
next unless value
|
|
76
|
+
|
|
77
|
+
property_name = self.class.inverse_translations.fetch(property, property)
|
|
78
|
+
|
|
79
|
+
if value.is_a?(Base)
|
|
80
|
+
build_el(builder, property_name, value)
|
|
81
|
+
elsif value.is_a?(Array)
|
|
82
|
+
value.each do |el|
|
|
83
|
+
build_el(builder, property_name, el)
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
builder.send(property_name, build_value(value))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_el(builder, property_name, el)
|
|
92
|
+
attributes = el.attributes if el.is_a?(Base)
|
|
93
|
+
namespace = Base.xmlns[el.class.namespace]&.split(':')&.[](1) if el.is_a?(Base)
|
|
94
|
+
builder = builder[namespace] if namespace
|
|
95
|
+
|
|
96
|
+
builder.send(property_name, attributes) do |property_builder|
|
|
97
|
+
property_builder = property_builder[namespace] if namespace
|
|
98
|
+
el.build_xml(property_builder)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build_value(value)
|
|
103
|
+
case value
|
|
104
|
+
when Time
|
|
105
|
+
value.iso8601.gsub('Z', '.000Z')
|
|
106
|
+
else
|
|
107
|
+
value
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
class Build < Base
|
|
5
|
+
property 'version', from: 'Version', transform_with: ->(v) { Version.parse(v) }
|
|
6
|
+
property 'type', from: 'Type', transform_with: ->(v) { BuildType.parse(v) }
|
|
7
|
+
property 'time', from: 'Time', transform_with: ->(v) { Time.parse(v) }
|
|
8
|
+
property 'builder', from: 'Builder'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
class CourseList < Base
|
|
5
|
+
property 'courses', from: 'Courses', transform_with: ->(v) { v.map { |el| Course.parse(el) } }
|
|
6
|
+
|
|
7
|
+
def_delegators :courses, :each, :to_a
|
|
8
|
+
|
|
9
|
+
def self.parse(list)
|
|
10
|
+
CourseList.new('Courses' => list.xpath('xmlns:Course'))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build_xml(builder)
|
|
14
|
+
courses.each do |course|
|
|
15
|
+
builder.Course do |course_builder|
|
|
16
|
+
course.build_xml(course_builder)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -7,5 +7,27 @@ module Tcx
|
|
|
7
7
|
property 'workouts', from: 'Workouts', transform_with: ->(arr) { WorkoutList.parse(arr) }
|
|
8
8
|
property 'courses', from: 'Courses', transform_with: ->(arr) { CourseList.parse(arr) }
|
|
9
9
|
property 'author', from: 'Author', transform_with: ->(el) { AbstractSource.parse(el) }
|
|
10
|
+
|
|
11
|
+
def self.load(data)
|
|
12
|
+
parse(Nokogiri::XML(data).root)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def dump(target_path)
|
|
16
|
+
::File.write(target_path, to_xml)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_xml
|
|
20
|
+
to_xml_builder.to_xml
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def to_xml_builder
|
|
26
|
+
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
|
27
|
+
xml.TrainingCenterDatabase(Base.namespace_definitions) do |xml|
|
|
28
|
+
build_xml(xml)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
10
32
|
end
|
|
11
33
|
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Tcx
|
|
4
|
-
class
|
|
5
|
-
property 'name', from: 'Name'
|
|
4
|
+
class Device < AbstractSource
|
|
6
5
|
property 'unit_id', from: 'UnitId'
|
|
7
6
|
property 'product_id', from: 'ProductID'
|
|
8
7
|
property 'version', from: 'Version', transform_with: ->(v) { Version.parse(v) }
|
|
8
|
+
|
|
9
|
+
def attributes
|
|
10
|
+
super.merge('xsi:type' => 'Device_t')
|
|
11
|
+
end
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
class Duration < Base
|
|
5
|
+
property 'seconds', from: 'Seconds', transform_with: lambda(&:to_i)
|
|
6
|
+
property 'meters', from: 'Meters', transform_with: lambda(&:to_i)
|
|
7
|
+
property 'heart_rate', from: 'HeartRate', transform_with: ->(v) { HeartRateValue.parse(v) }
|
|
8
|
+
property 'calories', from: 'Calories', transform_with: lambda(&:to_i)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
class ExtensionsList < Base
|
|
5
|
+
# TODO: forward all extension properties up
|
|
6
|
+
property 'TPX', transform_with: ->(v) { Tcx::Extensions::ActivityExtension::V2::ActivityTrackpoint.parse(v) }
|
|
7
|
+
property 'LX', transform_with: ->(v) { Tcx::Extensions::ActivityExtension::V2::ActivityLap.parse(v) }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -14,6 +14,10 @@ module Tcx
|
|
|
14
14
|
property 'trigger_method', from: 'TriggerMethod', transform_with: ->(v) { TriggerMethod.parse(v) }
|
|
15
15
|
property 'tracks', from: 'Track', transform_with: ->(v) { to_array(v).map { |el| Track.parse(el) } }
|
|
16
16
|
property 'notes', from: 'Notes'
|
|
17
|
-
property 'extensions', from: 'Extensions', transform_with: ->(v) {
|
|
17
|
+
property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
|
|
18
|
+
|
|
19
|
+
def self.attributes
|
|
20
|
+
['start_time']
|
|
21
|
+
end
|
|
18
22
|
end
|
|
19
23
|
end
|
|
@@ -9,7 +9,7 @@ module Tcx
|
|
|
9
9
|
property 'heart_rate_bpm', from: 'HeartRateBpm', transform_with: ->(v) { HeartRateInBeatsPerMinute.parse(v) }
|
|
10
10
|
property 'cadence', from: 'Cadence', transform_with: ->(v) { Cadence.parse(v) }
|
|
11
11
|
property 'sensor_state', from: 'SensorState', transform_with: ->(v) { SensorState.parse(v) }
|
|
12
|
-
property 'extensions', from: 'Extensions', transform_with: ->(v) {
|
|
12
|
+
property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
|
|
13
13
|
# TODO: Extensions/TPX/Speed
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tcx
|
|
4
|
+
class WorkoutList < Base
|
|
5
|
+
property 'workouts', from: 'Workouts', transform_with: ->(v) { v.map { |el| Workout.parse(el) } }
|
|
6
|
+
|
|
7
|
+
def_delegators :workouts, :each, :count
|
|
8
|
+
|
|
9
|
+
def self.parse(list)
|
|
10
|
+
WorkoutList.new('Workouts' => list.xpath('xmlns:Workout'))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build_xml(builder)
|
|
14
|
+
workouts.each do |workout|
|
|
15
|
+
builder.Workout do |workout_builder|
|
|
16
|
+
workout.build_xml(workout_builder)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/tcx/types.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'types/base'
|
|
4
|
+
require_relative 'types/abstract_source'
|
|
5
|
+
require_relative 'types/activity'
|
|
6
|
+
require_relative 'types/activity_list'
|
|
7
|
+
require_relative 'types/application'
|
|
8
|
+
require_relative 'types/build'
|
|
9
|
+
require_relative 'types/build_type'
|
|
10
|
+
require_relative 'types/cadence'
|
|
11
|
+
require_relative 'types/course'
|
|
12
|
+
require_relative 'types/course_folder'
|
|
13
|
+
require_relative 'types/course_lap'
|
|
14
|
+
require_relative 'types/course_list'
|
|
15
|
+
require_relative 'types/course_point'
|
|
16
|
+
require_relative 'types/course_point_type'
|
|
17
|
+
require_relative 'types/courses'
|
|
18
|
+
require_relative 'types/custom_heart_rate_zone'
|
|
19
|
+
require_relative 'types/database'
|
|
20
|
+
require_relative 'types/device'
|
|
21
|
+
require_relative 'types/duration'
|
|
22
|
+
require_relative 'types/extensions_list'
|
|
23
|
+
require_relative 'types/folders'
|
|
24
|
+
require_relative 'types/gender'
|
|
25
|
+
require_relative 'types/heart_rate_as_percent_of_max'
|
|
26
|
+
require_relative 'types/heart_rate_bpm'
|
|
27
|
+
require_relative 'types/heart_rate_in_beats_per_minute'
|
|
28
|
+
require_relative 'types/heart_rate_value'
|
|
29
|
+
require_relative 'types/intensity'
|
|
30
|
+
require_relative 'types/lap'
|
|
31
|
+
require_relative 'types/name_key_reference'
|
|
32
|
+
require_relative 'types/position'
|
|
33
|
+
require_relative 'types/predefined_heart_rate_zone'
|
|
34
|
+
require_relative 'types/sensor_state'
|
|
35
|
+
require_relative 'types/sport'
|
|
36
|
+
require_relative 'types/step'
|
|
37
|
+
require_relative 'types/target'
|
|
38
|
+
require_relative 'types/track'
|
|
39
|
+
require_relative 'types/trackpoint'
|
|
40
|
+
require_relative 'types/trigger_method'
|
|
41
|
+
require_relative 'types/version'
|
|
42
|
+
require_relative 'types/workout'
|
|
43
|
+
require_relative 'types/workout_list'
|
data/lib/tcx/version.rb
CHANGED
data/lib/tcx.rb
CHANGED
|
@@ -4,14 +4,19 @@ require 'hashie'
|
|
|
4
4
|
require 'time'
|
|
5
5
|
require 'ruby-enum'
|
|
6
6
|
require 'nokogiri'
|
|
7
|
+
require 'forwardable'
|
|
7
8
|
|
|
8
9
|
require_relative 'tcx/version'
|
|
9
|
-
require_relative 'tcx/
|
|
10
|
+
require_relative 'tcx/types'
|
|
11
|
+
require_relative 'tcx/extensions'
|
|
12
|
+
require_relative 'tcx/file'
|
|
10
13
|
|
|
11
14
|
module Tcx
|
|
12
15
|
def self.load_file(path)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
Tcx::File.new(path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.load(data)
|
|
20
|
+
Tcx::Database.load(data)
|
|
16
21
|
end
|
|
17
22
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tcx
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Doubrovkine
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-10-
|
|
11
|
+
date: 2025-10-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: hashie
|
|
@@ -62,45 +62,55 @@ files:
|
|
|
62
62
|
- LICENSE.md
|
|
63
63
|
- README.md
|
|
64
64
|
- lib/tcx.rb
|
|
65
|
-
- lib/tcx/
|
|
66
|
-
- lib/tcx/
|
|
67
|
-
- lib/tcx/
|
|
68
|
-
- lib/tcx/
|
|
69
|
-
- lib/tcx/
|
|
70
|
-
- lib/tcx/
|
|
71
|
-
- lib/tcx/
|
|
72
|
-
- lib/tcx/
|
|
73
|
-
- lib/tcx/
|
|
74
|
-
- lib/tcx/
|
|
75
|
-
- lib/tcx/
|
|
76
|
-
- lib/tcx/
|
|
77
|
-
- lib/tcx/
|
|
78
|
-
- lib/tcx/
|
|
79
|
-
- lib/tcx/
|
|
80
|
-
- lib/tcx/
|
|
81
|
-
- lib/tcx/
|
|
82
|
-
- lib/tcx/
|
|
83
|
-
- lib/tcx/
|
|
84
|
-
- lib/tcx/
|
|
85
|
-
- lib/tcx/
|
|
86
|
-
- lib/tcx/
|
|
87
|
-
- lib/tcx/
|
|
88
|
-
- lib/tcx/
|
|
89
|
-
- lib/tcx/
|
|
90
|
-
- lib/tcx/
|
|
91
|
-
- lib/tcx/
|
|
92
|
-
- lib/tcx/
|
|
93
|
-
- lib/tcx/
|
|
94
|
-
- lib/tcx/
|
|
95
|
-
- lib/tcx/
|
|
96
|
-
- lib/tcx/
|
|
97
|
-
- lib/tcx/
|
|
98
|
-
- lib/tcx/
|
|
99
|
-
- lib/tcx/
|
|
100
|
-
- lib/tcx/
|
|
101
|
-
- lib/tcx/
|
|
102
|
-
- lib/tcx/
|
|
103
|
-
- lib/tcx/
|
|
65
|
+
- lib/tcx/extensions.rb
|
|
66
|
+
- lib/tcx/extensions/activity_extension/v2.rb
|
|
67
|
+
- lib/tcx/extensions/activity_extension/v2/activity_lap.rb
|
|
68
|
+
- lib/tcx/extensions/activity_extension/v2/activity_trackpoint.rb
|
|
69
|
+
- lib/tcx/extensions/activity_extension/v2/base.rb
|
|
70
|
+
- lib/tcx/extensions/activity_extension/v2/cadence_sensor_type.rb
|
|
71
|
+
- lib/tcx/file.rb
|
|
72
|
+
- lib/tcx/types.rb
|
|
73
|
+
- lib/tcx/types/abstract_source.rb
|
|
74
|
+
- lib/tcx/types/activity.rb
|
|
75
|
+
- lib/tcx/types/activity_list.rb
|
|
76
|
+
- lib/tcx/types/application.rb
|
|
77
|
+
- lib/tcx/types/base.rb
|
|
78
|
+
- lib/tcx/types/build.rb
|
|
79
|
+
- lib/tcx/types/build_type.rb
|
|
80
|
+
- lib/tcx/types/cadence.rb
|
|
81
|
+
- lib/tcx/types/course.rb
|
|
82
|
+
- lib/tcx/types/course_folder.rb
|
|
83
|
+
- lib/tcx/types/course_lap.rb
|
|
84
|
+
- lib/tcx/types/course_list.rb
|
|
85
|
+
- lib/tcx/types/course_point.rb
|
|
86
|
+
- lib/tcx/types/course_point_type.rb
|
|
87
|
+
- lib/tcx/types/courses.rb
|
|
88
|
+
- lib/tcx/types/custom_heart_rate_zone.rb
|
|
89
|
+
- lib/tcx/types/database.rb
|
|
90
|
+
- lib/tcx/types/device.rb
|
|
91
|
+
- lib/tcx/types/duration.rb
|
|
92
|
+
- lib/tcx/types/extensions_list.rb
|
|
93
|
+
- lib/tcx/types/folders.rb
|
|
94
|
+
- lib/tcx/types/gender.rb
|
|
95
|
+
- lib/tcx/types/heart_rate_as_percent_of_max.rb
|
|
96
|
+
- lib/tcx/types/heart_rate_bpm.rb
|
|
97
|
+
- lib/tcx/types/heart_rate_in_beats_per_minute.rb
|
|
98
|
+
- lib/tcx/types/heart_rate_value.rb
|
|
99
|
+
- lib/tcx/types/intensity.rb
|
|
100
|
+
- lib/tcx/types/lap.rb
|
|
101
|
+
- lib/tcx/types/name_key_reference.rb
|
|
102
|
+
- lib/tcx/types/position.rb
|
|
103
|
+
- lib/tcx/types/predefined_heart_rate_zone.rb
|
|
104
|
+
- lib/tcx/types/sensor_state.rb
|
|
105
|
+
- lib/tcx/types/sport.rb
|
|
106
|
+
- lib/tcx/types/step.rb
|
|
107
|
+
- lib/tcx/types/target.rb
|
|
108
|
+
- lib/tcx/types/track.rb
|
|
109
|
+
- lib/tcx/types/trackpoint.rb
|
|
110
|
+
- lib/tcx/types/trigger_method.rb
|
|
111
|
+
- lib/tcx/types/version.rb
|
|
112
|
+
- lib/tcx/types/workout.rb
|
|
113
|
+
- lib/tcx/types/workout_list.rb
|
|
104
114
|
- lib/tcx/version.rb
|
|
105
115
|
homepage: http://github.com/dblock/tcx
|
|
106
116
|
licenses:
|
data/lib/tcx/models/base.rb
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tcx
|
|
4
|
-
class Base < Hashie::Trash
|
|
5
|
-
include Hashie::Extensions::IgnoreUndeclared
|
|
6
|
-
|
|
7
|
-
def self.parse(xml)
|
|
8
|
-
attributes = xml.attributes.to_h
|
|
9
|
-
|
|
10
|
-
xml.children.each do |child|
|
|
11
|
-
next unless child.is_a?(Nokogiri::XML::Element)
|
|
12
|
-
|
|
13
|
-
attributes[child.name] = if attributes.key?(child.name)
|
|
14
|
-
to_array(attributes[child.name]) + [child]
|
|
15
|
-
elsif child.children.one? && child.children.first.is_a?(Nokogiri::XML::Text)
|
|
16
|
-
child.text
|
|
17
|
-
else
|
|
18
|
-
child
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
new attributes
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Transform an XmlElement into [XmlElement], cannot be used with Array or causes a coercion error
|
|
26
|
-
def self.to_array(value)
|
|
27
|
-
value.is_a?(Array) ? value : [value]
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
data/lib/tcx/models/cadence.rb
DELETED
data/lib/tcx/models/duration.rb
DELETED
data/lib/tcx/models/value.rb
DELETED
data/lib/tcx/models.rb
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'models/base'
|
|
4
|
-
require_relative 'models/value'
|
|
5
|
-
require_relative 'models/abstract_source'
|
|
6
|
-
require_relative 'models/activity'
|
|
7
|
-
require_relative 'models/activity_list'
|
|
8
|
-
require_relative 'models/cadence'
|
|
9
|
-
require_relative 'models/course'
|
|
10
|
-
require_relative 'models/course_folder'
|
|
11
|
-
require_relative 'models/course_lap'
|
|
12
|
-
require_relative 'models/course_list'
|
|
13
|
-
require_relative 'models/course_point'
|
|
14
|
-
require_relative 'models/course_point_type'
|
|
15
|
-
require_relative 'models/courses'
|
|
16
|
-
require_relative 'models/custom_heart_rate_zone'
|
|
17
|
-
require_relative 'models/database'
|
|
18
|
-
require_relative 'models/duration'
|
|
19
|
-
require_relative 'models/extensions'
|
|
20
|
-
require_relative 'models/folders'
|
|
21
|
-
require_relative 'models/gender'
|
|
22
|
-
require_relative 'models/heart_rate_as_percent_of_max'
|
|
23
|
-
require_relative 'models/heart_rate_bpm'
|
|
24
|
-
require_relative 'models/heart_rate_in_beats_per_minute'
|
|
25
|
-
require_relative 'models/heart_rate_value'
|
|
26
|
-
require_relative 'models/intensity'
|
|
27
|
-
require_relative 'models/lap'
|
|
28
|
-
require_relative 'models/name_key_reference'
|
|
29
|
-
require_relative 'models/position'
|
|
30
|
-
require_relative 'models/predefined_heart_rate_zone'
|
|
31
|
-
require_relative 'models/sensor_state'
|
|
32
|
-
require_relative 'models/sport'
|
|
33
|
-
require_relative 'models/step'
|
|
34
|
-
require_relative 'models/target'
|
|
35
|
-
require_relative 'models/track'
|
|
36
|
-
require_relative 'models/trackpoint'
|
|
37
|
-
require_relative 'models/trigger_method'
|
|
38
|
-
require_relative 'models/version'
|
|
39
|
-
require_relative 'models/workout'
|
|
40
|
-
require_relative 'models/workout_list'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|