tcx 0.2.0 → 0.3.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +4 -2
  4. data/lib/tcx/types/activity.rb +2 -0
  5. data/lib/tcx/types/activity_list.rb +7 -3
  6. data/lib/tcx/types/activity_reference.rb +7 -0
  7. data/lib/tcx/types/base.rb +84 -60
  8. data/lib/tcx/types/boolean_type.rb +10 -0
  9. data/lib/tcx/types/build.rb +1 -1
  10. data/lib/tcx/types/cadence_target.rb +8 -0
  11. data/lib/tcx/types/calories_burned.rb +7 -0
  12. data/lib/tcx/types/course_folder.rb +5 -0
  13. data/lib/tcx/types/course_list.rb +3 -3
  14. data/lib/tcx/types/course_point.rb +1 -1
  15. data/lib/tcx/types/custom_speed_zone.rb +9 -0
  16. data/lib/tcx/types/database.rb +18 -3
  17. data/lib/tcx/types/distance.rb +7 -0
  18. data/lib/tcx/types/first_sport.rb +7 -0
  19. data/lib/tcx/types/folders.rb +3 -1
  20. data/lib/tcx/types/heart_rate.rb +7 -0
  21. data/lib/tcx/types/heart_rate_above.rb +7 -0
  22. data/lib/tcx/types/heart_rate_below.rb +7 -0
  23. data/lib/tcx/types/history.rb +11 -0
  24. data/lib/tcx/types/history_folder.rb +19 -0
  25. data/lib/tcx/types/lap.rb +1 -1
  26. data/lib/tcx/types/multi_sport_folder.rb +16 -0
  27. data/lib/tcx/types/multisport_session.rb +10 -0
  28. data/lib/tcx/types/next_sport.rb +8 -0
  29. data/lib/tcx/types/none.rb +9 -0
  30. data/lib/tcx/types/plan.rb +14 -0
  31. data/lib/tcx/types/predefined_speed_zone.rb +7 -0
  32. data/lib/tcx/types/quick_workout.rb +8 -0
  33. data/lib/tcx/types/repeat.rb +8 -0
  34. data/lib/tcx/types/speed.rb +7 -0
  35. data/lib/tcx/types/speed_type.rb +10 -0
  36. data/lib/tcx/types/time.rb +11 -0
  37. data/lib/tcx/types/trackpoint.rb +1 -1
  38. data/lib/tcx/types/training.rb +14 -0
  39. data/lib/tcx/types/training_type.rb +10 -0
  40. data/lib/tcx/types/user_initiated.rb +6 -0
  41. data/lib/tcx/types/week.rb +12 -0
  42. data/lib/tcx/types/workout.rb +6 -0
  43. data/lib/tcx/types/workout_folder.rb +14 -0
  44. data/lib/tcx/types/workout_list.rb +3 -3
  45. data/lib/tcx/types/workouts.rb +10 -0
  46. data/lib/tcx/types/zone.rb +9 -0
  47. data/lib/tcx/types.rb +32 -2
  48. data/lib/tcx/version.rb +1 -1
  49. metadata +32 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16a519fa1d9c70b3f319428170d9e52f36a1eaa4baacdd4004d336e35c57d2c6
4
- data.tar.gz: 57571958ee9e4182e5c271ebec43639c78b46253e837caf0eb1bbd55b553398c
3
+ metadata.gz: 2a22d167ca20a21779421b65e48f939affd43b7983bda56f22da9e5013d6af6c
4
+ data.tar.gz: 7d526b593d253b3e6c4dcb64aea2ca7df4fa255d457871608d3677a16768b216
5
5
  SHA512:
6
- metadata.gz: 5e1339b4ef3c6817baf72d973d1b00c92b1ad109927a7012892758d27e3140b863786a431ee25b8176130801549edbad992939c3db4dfa853763802d60d36630
7
- data.tar.gz: 631de4c6b5e4dabe0c3db3b87255583d4dd30e05cb58425e4e10aa407b2b43136c82c2f3e840cd428d5c16e86b10f89cf25655d13e371b3e8bcdee2ab9357500
6
+ metadata.gz: f2f772cb32f27738b1683c0087e13ca7a82987350846d875ae202d245a21bca3f0fafc5d0ed0cc8793c558adc585f29af080495feadf70dbee95c1baf8f45788
7
+ data.tar.gz: 625852f7eb80fe4c1123ba2d4187934aa28da3c1dd65de21ffc5a504b1c74c9c506c49fabd71df50e6a668273758907885429362ac91c9c87d66ac1e96bb6994
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ### 0.3.0 (2025-10-24)
2
+
3
+ * Added reading and writing multisport activity folders - [@dblock](https://github.com/dblock).
4
+ * Added reading and writing workouts - [@dblock](https://github.com/dblock).
5
+ * Write required namespaces dynamically - [@dblock](https://github.com/dblock).
6
+ * Added reading and writing courses - [@dblock](https://github.com/dblock).
7
+ * Added writing multiple extension attributes - [@dblock](https://github.com/dblock).
8
+
1
9
  ### 0.2.0 (2025-10-24)
2
10
 
3
11
  * Added support for writing TCX files - [@dblock](https://github.com/dblock).
data/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/tcx.svg)](https://badge.fury.io/rb/tcx)
4
4
  [![Test](https://github.com/dblock/tcx/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/dblock/tcx/actions/workflows/test.yml)
5
5
 
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.
6
+ A Garmin Training Center XML (.TCX) reader and writer.
7
+
8
+ Unlike other libraries such as [tcx_rb](https://github.com/keithdoggett/tcx_rb) or [tcxread](https://github.com/firefly-cpp/tcxread), provides a more idiomatic API, implements both read and write, and supports the entire [TCX schema](https://www8.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd), including extensions.
7
9
 
8
10
  ## Installation
9
11
 
@@ -51,7 +53,7 @@ tcx.dump('activities2.tcx') # writes to activities2.tcx
51
53
 
52
54
  ### Examples
53
55
 
54
- See [examples/multiple_running_activities.rb](examples/multiple_running_activities.rb) for a complete working example.
56
+ See [examples](examples) for a complete set of working samples.
55
57
 
56
58
  ## Upgrading
57
59
 
@@ -6,7 +6,9 @@ module Tcx
6
6
  property 'id', from: 'Id'
7
7
  property 'laps', from: 'Lap', transform_with: ->(v) { to_array(v).map { |el| Lap.parse(el) } }
8
8
  property 'notes', from: 'Notes'
9
+ property 'training', from: 'Training', transform_with: ->(v) { to_array(v).map { |el| Training.parse(el) } }
9
10
  property 'creator', from: 'Creator', transform_with: ->(v) { AbstractSource.parse(v) }
11
+ property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
10
12
 
11
13
  def self.attributes
12
14
  ['sport']
@@ -3,17 +3,21 @@
3
3
  module Tcx
4
4
  class ActivityList < Base
5
5
  property 'activities', from: 'Activities', transform_with: ->(v) { v.map { |el| Activity.parse(el) } }
6
+ property 'multisport_sessions', from: 'MultisportSession', transform_with: ->(v) { v.map { |el| MultisportSession.parse(el) } }
6
7
 
7
8
  def_delegators :activities, :each, :count
8
9
 
9
10
  def self.parse(list)
10
- ActivityList.new('Activities' => list.xpath('xmlns:Activity'))
11
+ ActivityList.new(
12
+ 'Activities' => list.xpath('xmlns:Activity'),
13
+ 'MultisportSession' => list.xpath('xmlns:MultiSportSession')
14
+ )
11
15
  end
12
16
 
13
- def build_xml(builder)
17
+ def build_xml(builder, namespace = nil)
14
18
  activities.each do |activity|
15
19
  builder.Activity(activity.attributes) do |activity_builder|
16
- activity.build_xml(activity_builder)
20
+ activity.build_xml(activity_builder, namespace)
17
21
  end
18
22
  end
19
23
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class ActivityReference < Base
5
+ property 'id', from: 'Id', transform_with: ->(v) { ::Time.parse(v) }
6
+ end
7
+ end
@@ -5,104 +5,128 @@ module Tcx
5
5
  include Hashie::Extensions::IgnoreUndeclared
6
6
  extend Forwardable
7
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)
8
+ DEFAULT_NAMESPACE = 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2'
9
+
10
+ # TODO: dynamically register additional namespaces to support custom extensions
11
+ DEFAULT_NAMESPACE_DEFINITIONS = {
12
+ 'xsi:schemaLocation' => 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd',
13
+ 'xmlns' => 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2',
14
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
15
+ 'xmlns:ns2' => 'http://www.garmin.com/xmlschemas/UserProfile/v2',
16
+ 'xmlns:ns3' => 'http://www.garmin.com/xmlschemas/ActivityExtension/v2',
17
+ 'xmlns:ns4' => 'http://www.garmin.com/xmlschemas/ProfileExtension/v1',
18
+ 'xmlns:ns5' => 'http://www.garmin.com/xmlschemas/ActivityGoals/v1'
19
+ }.freeze
20
+
21
+ INVERTED_DEFAULT_NAMESPACE_DEFINITIONS = DEFAULT_NAMESPACE_DEFINITIONS.invert.freeze
22
+
23
+ class << self
24
+ def parse(xml)
25
+ klass = to_class(xml['xsi:type'])
26
+ return klass.parse(xml) if klass
27
+
28
+ attributes = xml.attributes.to_h
29
+
30
+ xml.children.each do |child|
31
+ next unless child.is_a?(Nokogiri::XML::Element)
32
+
33
+ attributes[child.name] = if attributes.key?(child.name)
34
+ to_array(attributes[child.name]) + [child]
35
+ elsif child.children.one? && child.children.first.is_a?(Nokogiri::XML::Text)
36
+ child.text
37
+ else
38
+ child
39
+ end
40
+ end
18
41
 
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
42
+ new attributes
26
43
  end
27
44
 
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
45
+ # Transform an XmlElement into [XmlElement], cannot be used with Array or causes a coercion error
46
+ def to_array(value)
47
+ value.is_a?(Array) ? value : [value]
48
+ end
35
49
 
36
- def self.attributes
37
- []
38
- end
50
+ def to_class(xsi_type)
51
+ return unless xsi_type
39
52
 
40
- def self.namespace
41
- 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2'
42
- end
53
+ # TODO: not all extension types are Type_t
54
+ klass = Tcx.const_get(xsi_type.gsub(/_t$/, ''))
55
+ self == klass ? nil : klass
56
+ rescue NameError # extension not implemented
57
+ nil
58
+ end
43
59
 
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
60
+ def attributes
61
+ []
62
+ end
55
63
 
56
- def self.xmlns
57
- @xmlns ||= namespace_definitions.invert
64
+ def namespace
65
+ DEFAULT_NAMESPACE
66
+ end
58
67
  end
59
68
 
60
69
  def attributes
61
- attribute_values = {}
62
- self.class.attributes.each do |attribute|
70
+ self.class.attributes.map do |attribute|
63
71
  value = self[attribute]
64
72
  next unless value
65
73
 
66
74
  property_name = self.class.inverse_translations.fetch(attribute, attribute)
67
- attribute_values[property_name] = build_value(value)
68
- end
69
- attribute_values
75
+ [property_name, build_value(value)]
76
+ end.compact.to_h
70
77
  end
71
78
 
72
- def build_xml(builder)
79
+ def build_xml(builder, namespace = nil)
73
80
  (self.class.properties - self.class.attributes).each do |property|
74
81
  value = self[property]
75
82
  next unless value
76
83
 
77
84
  property_name = self.class.inverse_translations.fetch(property, property)
78
85
 
79
- if value.is_a?(Base)
86
+ case value
87
+ when Base
80
88
  build_el(builder, property_name, value)
81
- elsif value.is_a?(Array)
82
- value.each do |el|
83
- build_el(builder, property_name, el)
89
+ when Array
90
+ value.each do |element|
91
+ build_el(builder, property_name, element)
84
92
  end
85
93
  else
94
+ builder = builder[namespace] if namespace
86
95
  builder.send(property_name, build_value(value))
87
96
  end
88
97
  end
89
98
  end
90
99
 
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)
100
+ def namespace_definitions
101
+ DEFAULT_NAMESPACE_DEFINITIONS
102
+ end
103
+
104
+ def inverted_namespace_definitions
105
+ @inverted_namespace_definitions ||= if namespace_definitions == DEFAULT_NAMESPACE_DEFINITIONS
106
+ INVERTED_DEFAULT_NAMESPACE_DEFINITIONS
107
+ else
108
+ namespace_definitions.invert
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def build_el(builder, property_name, element)
115
+ attributes = element.attributes if element.is_a?(Base)
116
+ namespace = inverted_namespace_definitions[element.class.namespace]&.split(':')&.[](1) if element.is_a?(Base)
94
117
  builder = builder[namespace] if namespace
95
118
 
96
119
  builder.send(property_name, attributes) do |property_builder|
97
- property_builder = property_builder[namespace] if namespace
98
- el.build_xml(property_builder)
120
+ element.build_xml(property_builder, namespace)
99
121
  end
100
122
  end
101
123
 
102
124
  def build_value(value)
103
125
  case value
104
- when Time
105
- value.iso8601.gsub('Z', '.000Z')
126
+ when Nokogiri::XML::Element
127
+ # empty node
128
+ when ::Time
129
+ value.iso8601
106
130
  else
107
131
  value
108
132
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class BooleanType
5
+ include Ruby::Enum
6
+
7
+ define :true, 'true' # rubocop:disable Lint/BooleanSymbol
8
+ define :false, 'false' # rubocop:disable Lint/BooleanSymbol
9
+ end
10
+ end
@@ -4,7 +4,7 @@ module Tcx
4
4
  class Build < Base
5
5
  property 'version', from: 'Version', transform_with: ->(v) { Version.parse(v) }
6
6
  property 'type', from: 'Type', transform_with: ->(v) { BuildType.parse(v) }
7
- property 'time', from: 'Time', transform_with: ->(v) { Time.parse(v) }
7
+ property 'time', from: 'Time', transform_with: ->(v) { ::Time.parse(v) }
8
8
  property 'builder', from: 'Builder'
9
9
  end
10
10
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class CadenceTarget < Target
5
+ property 'low', from: 'Low', transform_with: lambda(&:to_f)
6
+ property 'high', from: 'High', transform_with: lambda(&:to_f)
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class CaloriesBurned < Duration
5
+ property 'calories', from: 'Calories', transform_with: lambda(&:to_i)
6
+ end
7
+ end
@@ -5,5 +5,10 @@ module Tcx
5
5
  property 'folders', from: 'Folder', transform_with: ->(v) { to_array(v).map { |el| CourseFolder.parse(el) } }
6
6
  property 'course_references', from: 'CourseNameRef', transform_with: ->(v) { to_array(v).map { |el| NameKeyReference.parse(el) } }
7
7
  property 'notes', from: 'Notes'
8
+ property 'name', from: 'Name'
9
+
10
+ def self.attributes
11
+ ['name']
12
+ end
8
13
  end
9
14
  end
@@ -10,10 +10,10 @@ module Tcx
10
10
  CourseList.new('Courses' => list.xpath('xmlns:Course'))
11
11
  end
12
12
 
13
- def build_xml(builder)
13
+ def build_xml(builder, namespace = nil)
14
14
  courses.each do |course|
15
- builder.Course do |course_builder|
16
- course.build_xml(course_builder)
15
+ builder.Course(course.attributes) do |course_builder|
16
+ course.build_xml(course_builder, namespace)
17
17
  end
18
18
  end
19
19
  end
@@ -3,7 +3,7 @@
3
3
  module Tcx
4
4
  class CoursePoint < Base
5
5
  property 'name', from: 'Name'
6
- property 'time', from: 'Time', transform_with: ->(v) { Time.parse(v) }
6
+ property 'time', from: 'Time', transform_with: ->(v) { ::Time.parse(v) }
7
7
  property 'position', from: 'Position', transform_with: ->(v) { Position.parse(v) }
8
8
  property 'altitude_meters', from: 'AltitudeMeters', transform_with: lambda(&:to_f)
9
9
  property 'point_type', from: 'PointType', transform_with: ->(v) { CoursePointType.parse(v) }
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class CustomSpeedZone < Zone
5
+ property 'view_as', from: 'ViewAs', transform_with: ->(v) { SpeedType.parse(v) }
6
+ property 'low_in_meters_per_second', from: 'LowInMetersPerSecond', transform_with: lambda(&:to_f)
7
+ property 'high_in_meters_per_second', from: 'HighInMetersPerSecond', transform_with: lambda(&:to_f)
8
+ end
9
+ end
@@ -8,8 +8,23 @@ module Tcx
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
10
 
11
- def self.load(data)
12
- parse(Nokogiri::XML(data).root)
11
+ attr_accessor :namespace_definitions
12
+
13
+ class << self
14
+ def load(data)
15
+ xml = Nokogiri::XML(data)
16
+ parse(xml.root)
17
+ end
18
+
19
+ def parse(xml)
20
+ instance = super
21
+ instance.namespace_definitions = {
22
+ 'xsi:schemaLocation' => xml['xsi:schemaLocation']
23
+ }.merge(xml.namespace_definitions.to_h do |ns|
24
+ [['xmlns', ns.prefix].compact.join(':'), ns.href]
25
+ end)
26
+ instance
27
+ end
13
28
  end
14
29
 
15
30
  def dump(target_path)
@@ -24,7 +39,7 @@ module Tcx
24
39
 
25
40
  def to_xml_builder
26
41
  Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
27
- xml.TrainingCenterDatabase(Base.namespace_definitions) do |xml|
42
+ xml.TrainingCenterDatabase(namespace_definitions) do |xml|
28
43
  build_xml(xml)
29
44
  end
30
45
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Distance < Duration
5
+ property 'meters', from: 'Meters', transform_with: lambda(&:to_i)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class FirstSport < Base
5
+ property 'activity', from: 'Activity', transform_with: ->(v) { Activity.parse(v) }
6
+ end
7
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Tcx
4
4
  class Folders < Base
5
- property 'folders', from: 'Folder', transform_with: ->(v) { to_array(v).map { |el| CourseFolder.parse(el) } }
5
+ property 'history', from: 'History', transform_with: ->(v) { to_array(v).map { |el| History.parse(el) } }
6
+ property 'courses', from: 'Courses', transform_with: ->(v) { to_array(v).map { |el| Courses.parse(el) } }
7
+ property 'workouts', from: 'Workouts', transform_with: ->(v) { to_array(v).map { |el| Workouts.parse(el) } }
6
8
  end
7
9
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class HeartRate < Target
5
+ property 'heart_rate_zone', from: 'HeartRateZone', transform_with: ->(v) { Zone.parse(v) }
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class HeartRateAbove < Duration
5
+ property 'heart_rate', from: 'HeartRate', transform_with: ->(v) { HeartRateValue.parse(v) }
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class HeartRateBelow < Duration
5
+ property 'heart_rate', from: 'HeartRate', transform_with: ->(v) { HeartRateValue.parse(v) }
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class History < Base
5
+ property 'running', from: 'Running', transform_with: ->(v) { HistoryFolder.parse(v) }
6
+ property 'biking', from: 'Biking', transform_with: ->(v) { HistoryFolder.parse(v) }
7
+ property 'other', from: 'Other', transform_with: ->(v) { HistoryFolder.parse(v) }
8
+ property 'multi_sport', from: 'MultiSport', transform_with: ->(v) { MultiSportFolder.parse(v) }
9
+ property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ # HistoryFolder_t
5
+ # Organizes history by activity folders
6
+ class HistoryFolder < Base
7
+ property 'folders', from: 'Folder', transform_with: ->(v) { to_array(v).map { |el| HistoryFolder.parse(el) } }
8
+ property 'activity_references', from: 'ActivityRef', transform_with: ->(v) { to_array(v).map { |el| ActivityReference.parse(el) } }
9
+ property 'weeks', from: 'Week', transform_with: ->(v) { to_array(v).map { |el| Week.parse(el) } }
10
+ property 'notes', from: 'Notes'
11
+ property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
12
+
13
+ def self.attributes
14
+ %w[name]
15
+ end
16
+
17
+ property 'name', from: 'Name'
18
+ end
19
+ end
data/lib/tcx/types/lap.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Tcx
4
4
  class Lap < Base
5
- property 'start_time', from: 'StartTime', transform_with: ->(v) { Time.parse(v) }
5
+ property 'start_time', from: 'StartTime', transform_with: ->(v) { ::Time.parse(v) }
6
6
  property 'total_time_seconds', from: 'TotalTimeSeconds', transform_with: lambda(&:to_f)
7
7
  property 'distance_meters', from: 'DistanceMeters', transform_with: lambda(&:to_f)
8
8
  property 'maximum_speed', from: 'MaximumSpeed', transform_with: lambda(&:to_f)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class MultiSportFolder < Base
5
+ property 'folders', from: 'Folder', transform_with: ->(v) { to_array(v).map { |el| MultiSportFolder.parse(el) } }
6
+ property 'multisport_activity_references', from: 'MultisportActivityRef', transform_with: ->(v) { to_array(v).map { |el| ActivityReference.parse(el) } }
7
+ property 'weeks', from: 'Week', transform_with: ->(v) { to_array(v).map { |el| Week.parse(el) } }
8
+ property 'notes', from: 'Notes'
9
+ property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
10
+ property 'name', from: 'Name'
11
+
12
+ def self.attributes
13
+ %w[name]
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class MultisportSession < Base
5
+ property 'id', from: 'Id', transform_with: ->(v) { ::Time.parse(v) }
6
+ property 'first_sport', from: 'FirstSport', transform_with: ->(v) { FirstSport.parse(v) }
7
+ property 'next_sports', from: 'NextSport', transform_with: ->(v) { to_array(v).map { |el| NextSport.parse(el) } }
8
+ property 'notes', from: 'Notes'
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class NextSport < Base
5
+ property 'transition', from: 'Transition', transform_with: ->(v) { Lap.parse(v) }
6
+ property 'activity', from: 'Activity', transform_with: ->(v) { Activity.parse(v) }
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class None < Base
5
+ def attributes
6
+ super.merge('xsi:type' => 'None_t')
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Plan < Base
5
+ property 'name', from: 'Name'
6
+ property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
7
+ property 'type', from: 'Type', transform_with: ->(v) { TrainingType.parse(v) }
8
+ property 'interval_workout', from: 'IntervalWorkout', transform_with: ->(v) { BooleanType.parse(v) }
9
+
10
+ def self.attributes
11
+ %w[interval_workout type]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class PredefinedSpeedZone < Zone
5
+ property 'number', from: 'Number', transform_with: lambda(&:to_i)
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class QuickWorkout < Base
5
+ property 'total_time_seconds', from: 'TotalTimeSeconds', transform_with: lambda(&:to_f)
6
+ property 'distance_meters', from: 'DistanceMeters', transform_with: lambda(&:to_f)
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Repeat < Step
5
+ property 'repetitions', from: 'Repetitions', transform_with: lambda(&:to_i)
6
+ property 'child', from: 'Child', transform_with: ->(v) { to_array(v).map { |el| Step.parse(el) } }
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Speed < Target
5
+ property 'speed_zone', from: 'SpeedZone', transform_with: ->(v) { Zone.parse(v) }
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class SpeedType
5
+ include Ruby::Enum
6
+
7
+ define :pace, 'Pace'
8
+ define :speed, 'Speed'
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Time < Base
5
+ property 'seconds', from: 'Seconds', transform_with: lambda(&:to_i)
6
+
7
+ def attributes
8
+ super.merge('xsi:type' => 'Time_t')
9
+ end
10
+ end
11
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Tcx
4
4
  class Trackpoint < Base
5
- property 'time', from: 'Time', transform_with: ->(v) { Time.parse(v) }
5
+ property 'time', from: 'Time', transform_with: ->(v) { ::Time.parse(v) }
6
6
  property 'position', from: 'Position', transform_with: ->(v) { Position.parse(v) }
7
7
  property 'altitude_meters', from: 'AltitudeMeters', transform_with: lambda(&:to_f)
8
8
  property 'distance_meters', from: 'DistanceMeters', transform_with: lambda(&:to_f)
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Training < Base
5
+ property 'quick_workout_results', from: 'QuickWorkoutResults', transform_with: ->(v) { QuickWorkout.parse(v) }
6
+ property 'plan', from: 'Plan', transform_with: ->(v) { Plan.parse(v) }
7
+
8
+ property 'virtual_partner', from: 'VirtualPartner', transform_with: ->(v) { BooleanType.parse(v) }
9
+
10
+ def self.attributes
11
+ ['virtual_partner']
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class TrainingType
5
+ include Ruby::Enum
6
+
7
+ define :workout, 'Workout'
8
+ define :course, 'Course'
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class UserInitiated < Duration
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Week < Base
5
+ property 'notes', from: 'Notes'
6
+ property 'start_day', from: 'StartDay', transform_with: ->(v) { Date.parse(v) }
7
+
8
+ def self.attributes
9
+ %w[start_day]
10
+ end
11
+ end
12
+ end
@@ -6,5 +6,11 @@ module Tcx
6
6
  property 'sport', from: 'Sport', transform_with: ->(v) { Sport.parse(v) }
7
7
  property 'steps', from: 'Step', transform_with: ->(v) { to_array(v).map { |el| Step.parse(el) } }
8
8
  property 'creator', from: 'Creator', transform_with: ->(v) { AbstractSource.parse(v) }
9
+ property 'notes', from: 'Notes'
10
+ property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
11
+
12
+ def self.attributes
13
+ ['sport']
14
+ end
9
15
  end
10
16
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class WorkoutFolder < Base
5
+ property 'name', from: 'Name'
6
+ property 'folders', from: 'Folder', transform_with: ->(v) { to_array(v).map { |el| WorkoutFolder.parse(el) } }
7
+ property 'workout_name_ref', from: 'WorkoutNameRef', transform_with: ->(v) { to_array(v).map { |el| NameKeyReference.parse(el) } }
8
+ property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
9
+
10
+ def self.attributes
11
+ ['name']
12
+ end
13
+ end
14
+ end
@@ -10,10 +10,10 @@ module Tcx
10
10
  WorkoutList.new('Workouts' => list.xpath('xmlns:Workout'))
11
11
  end
12
12
 
13
- def build_xml(builder)
13
+ def build_xml(builder, namespace = nil)
14
14
  workouts.each do |workout|
15
- builder.Workout do |workout_builder|
16
- workout.build_xml(workout_builder)
15
+ builder.Workout(workout.attributes) do |workout_builder|
16
+ workout.build_xml(workout_builder, namespace)
17
17
  end
18
18
  end
19
19
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Workouts < Base
5
+ property 'running', from: 'Running', transform_with: ->(v) { WorkoutFolder.parse(v) }
6
+ property 'biking', from: 'Biking', transform_with: ->(v) { WorkoutFolder.parse(v) }
7
+ property 'other', from: 'Other', transform_with: ->(v) { WorkoutFolder.parse(v) }
8
+ property 'extensions', from: 'Extensions', transform_with: ->(v) { ExtensionsList.parse(v) }
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ # Zone_t (abstract base class)
5
+ # Base class for speed and heart rate zones
6
+ class Zone < Base
7
+ # Abstract base class - no properties
8
+ end
9
+ end
data/lib/tcx/types.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'types/base'
4
+ require_relative 'types/boolean_type'
4
5
  require_relative 'types/abstract_source'
5
6
  require_relative 'types/activity'
6
7
  require_relative 'types/activity_list'
8
+ require_relative 'types/activity_reference'
7
9
  require_relative 'types/application'
8
10
  require_relative 'types/build'
9
11
  require_relative 'types/build_type'
@@ -15,11 +17,20 @@ require_relative 'types/course_list'
15
17
  require_relative 'types/course_point'
16
18
  require_relative 'types/course_point_type'
17
19
  require_relative 'types/courses'
18
- require_relative 'types/custom_heart_rate_zone'
19
20
  require_relative 'types/database'
20
21
  require_relative 'types/device'
21
22
  require_relative 'types/duration'
23
+ require_relative 'types/distance'
24
+ require_relative 'types/calories_burned'
25
+ require_relative 'types/heart_rate_above'
26
+ require_relative 'types/heart_rate_below'
27
+ require_relative 'types/user_initiated'
22
28
  require_relative 'types/extensions_list'
29
+ require_relative 'types/first_sport'
30
+ require_relative 'types/week'
31
+ require_relative 'types/history_folder'
32
+ require_relative 'types/multi_sport_folder'
33
+ require_relative 'types/history'
23
34
  require_relative 'types/folders'
24
35
  require_relative 'types/gender'
25
36
  require_relative 'types/heart_rate_as_percent_of_max'
@@ -29,15 +40,34 @@ require_relative 'types/heart_rate_value'
29
40
  require_relative 'types/intensity'
30
41
  require_relative 'types/lap'
31
42
  require_relative 'types/name_key_reference'
43
+ require_relative 'types/next_sport'
44
+ require_relative 'types/multisport_session'
45
+ require_relative 'types/none'
46
+ require_relative 'types/plan'
32
47
  require_relative 'types/position'
33
- require_relative 'types/predefined_heart_rate_zone'
48
+ require_relative 'types/quick_workout'
34
49
  require_relative 'types/sensor_state'
50
+ require_relative 'types/speed_type'
35
51
  require_relative 'types/sport'
36
52
  require_relative 'types/step'
53
+ require_relative 'types/repeat'
37
54
  require_relative 'types/target'
55
+ require_relative 'types/zone'
56
+ require_relative 'types/custom_heart_rate_zone'
57
+ require_relative 'types/custom_speed_zone'
58
+ require_relative 'types/predefined_heart_rate_zone'
59
+ require_relative 'types/predefined_speed_zone'
60
+ require_relative 'types/cadence_target'
61
+ require_relative 'types/heart_rate'
62
+ require_relative 'types/speed'
63
+ require_relative 'types/time'
38
64
  require_relative 'types/track'
39
65
  require_relative 'types/trackpoint'
66
+ require_relative 'types/training'
67
+ require_relative 'types/training_type'
40
68
  require_relative 'types/trigger_method'
41
69
  require_relative 'types/version'
42
70
  require_relative 'types/workout'
71
+ require_relative 'types/workout_folder'
72
+ require_relative 'types/workouts'
43
73
  require_relative 'types/workout_list'
data/lib/tcx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tcx
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  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.2.0
4
+ version: 0.3.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-24 00:00:00.000000000 Z
11
+ date: 2025-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashie
@@ -73,11 +73,15 @@ files:
73
73
  - lib/tcx/types/abstract_source.rb
74
74
  - lib/tcx/types/activity.rb
75
75
  - lib/tcx/types/activity_list.rb
76
+ - lib/tcx/types/activity_reference.rb
76
77
  - lib/tcx/types/application.rb
77
78
  - lib/tcx/types/base.rb
79
+ - lib/tcx/types/boolean_type.rb
78
80
  - lib/tcx/types/build.rb
79
81
  - lib/tcx/types/build_type.rb
80
82
  - lib/tcx/types/cadence.rb
83
+ - lib/tcx/types/cadence_target.rb
84
+ - lib/tcx/types/calories_burned.rb
81
85
  - lib/tcx/types/course.rb
82
86
  - lib/tcx/types/course_folder.rb
83
87
  - lib/tcx/types/course_lap.rb
@@ -86,31 +90,57 @@ files:
86
90
  - lib/tcx/types/course_point_type.rb
87
91
  - lib/tcx/types/courses.rb
88
92
  - lib/tcx/types/custom_heart_rate_zone.rb
93
+ - lib/tcx/types/custom_speed_zone.rb
89
94
  - lib/tcx/types/database.rb
90
95
  - lib/tcx/types/device.rb
96
+ - lib/tcx/types/distance.rb
91
97
  - lib/tcx/types/duration.rb
92
98
  - lib/tcx/types/extensions_list.rb
99
+ - lib/tcx/types/first_sport.rb
93
100
  - lib/tcx/types/folders.rb
94
101
  - lib/tcx/types/gender.rb
102
+ - lib/tcx/types/heart_rate.rb
103
+ - lib/tcx/types/heart_rate_above.rb
95
104
  - lib/tcx/types/heart_rate_as_percent_of_max.rb
105
+ - lib/tcx/types/heart_rate_below.rb
96
106
  - lib/tcx/types/heart_rate_bpm.rb
97
107
  - lib/tcx/types/heart_rate_in_beats_per_minute.rb
98
108
  - lib/tcx/types/heart_rate_value.rb
109
+ - lib/tcx/types/history.rb
110
+ - lib/tcx/types/history_folder.rb
99
111
  - lib/tcx/types/intensity.rb
100
112
  - lib/tcx/types/lap.rb
113
+ - lib/tcx/types/multi_sport_folder.rb
114
+ - lib/tcx/types/multisport_session.rb
101
115
  - lib/tcx/types/name_key_reference.rb
116
+ - lib/tcx/types/next_sport.rb
117
+ - lib/tcx/types/none.rb
118
+ - lib/tcx/types/plan.rb
102
119
  - lib/tcx/types/position.rb
103
120
  - lib/tcx/types/predefined_heart_rate_zone.rb
121
+ - lib/tcx/types/predefined_speed_zone.rb
122
+ - lib/tcx/types/quick_workout.rb
123
+ - lib/tcx/types/repeat.rb
104
124
  - lib/tcx/types/sensor_state.rb
125
+ - lib/tcx/types/speed.rb
126
+ - lib/tcx/types/speed_type.rb
105
127
  - lib/tcx/types/sport.rb
106
128
  - lib/tcx/types/step.rb
107
129
  - lib/tcx/types/target.rb
130
+ - lib/tcx/types/time.rb
108
131
  - lib/tcx/types/track.rb
109
132
  - lib/tcx/types/trackpoint.rb
133
+ - lib/tcx/types/training.rb
134
+ - lib/tcx/types/training_type.rb
110
135
  - lib/tcx/types/trigger_method.rb
136
+ - lib/tcx/types/user_initiated.rb
111
137
  - lib/tcx/types/version.rb
138
+ - lib/tcx/types/week.rb
112
139
  - lib/tcx/types/workout.rb
140
+ - lib/tcx/types/workout_folder.rb
113
141
  - lib/tcx/types/workout_list.rb
142
+ - lib/tcx/types/workouts.rb
143
+ - lib/tcx/types/zone.rb
114
144
  - lib/tcx/version.rb
115
145
  homepage: http://github.com/dblock/tcx
116
146
  licenses: