tcx 0.1.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -2
  3. data/README.md +32 -5
  4. data/lib/tcx/extensions/activity_extension/v2/activity_lap.rb +21 -0
  5. data/lib/tcx/extensions/activity_extension/v2/activity_trackpoint.rb +22 -0
  6. data/lib/tcx/extensions/activity_extension/v2/base.rb +16 -0
  7. data/lib/tcx/extensions/activity_extension/v2/cadence_sensor_type.rb +16 -0
  8. data/lib/tcx/extensions/activity_extension/v2.rb +6 -0
  9. data/lib/tcx/extensions.rb +3 -0
  10. data/lib/tcx/file.rb +30 -0
  11. data/lib/tcx/types/abstract_source.rb +7 -0
  12. data/lib/tcx/{models → types}/activity.rb +6 -0
  13. data/lib/tcx/types/activity_list.rb +25 -0
  14. data/lib/tcx/types/activity_reference.rb +7 -0
  15. data/lib/tcx/types/application.rb +13 -0
  16. data/lib/tcx/types/base.rb +135 -0
  17. data/lib/tcx/types/boolean_type.rb +10 -0
  18. data/lib/tcx/types/build.rb +10 -0
  19. data/lib/tcx/types/build_type.rb +12 -0
  20. data/lib/tcx/types/cadence.rb +9 -0
  21. data/lib/tcx/types/cadence_target.rb +8 -0
  22. data/lib/tcx/types/calories_burned.rb +7 -0
  23. data/lib/tcx/{models → types}/course_folder.rb +5 -0
  24. data/lib/tcx/types/course_list.rb +21 -0
  25. data/lib/tcx/{models → types}/course_point.rb +1 -1
  26. data/lib/tcx/types/custom_speed_zone.rb +9 -0
  27. data/lib/tcx/types/database.rb +48 -0
  28. data/lib/tcx/{models/abstract_source.rb → types/device.rb} +5 -2
  29. data/lib/tcx/types/distance.rb +7 -0
  30. data/lib/tcx/types/duration.rb +10 -0
  31. data/lib/tcx/types/extensions_list.rb +9 -0
  32. data/lib/tcx/types/first_sport.rb +7 -0
  33. data/lib/tcx/types/folders.rb +9 -0
  34. data/lib/tcx/types/heart_rate.rb +7 -0
  35. data/lib/tcx/types/heart_rate_above.rb +7 -0
  36. data/lib/tcx/types/heart_rate_as_percent_of_max.rb +9 -0
  37. data/lib/tcx/types/heart_rate_below.rb +7 -0
  38. data/lib/tcx/types/heart_rate_bpm.rb +9 -0
  39. data/lib/tcx/types/heart_rate_in_beats_per_minute.rb +9 -0
  40. data/lib/tcx/types/history.rb +11 -0
  41. data/lib/tcx/types/history_folder.rb +19 -0
  42. data/lib/tcx/{models → types}/lap.rb +6 -2
  43. data/lib/tcx/types/multi_sport_folder.rb +16 -0
  44. data/lib/tcx/types/multisport_session.rb +10 -0
  45. data/lib/tcx/types/next_sport.rb +8 -0
  46. data/lib/tcx/types/none.rb +9 -0
  47. data/lib/tcx/types/plan.rb +14 -0
  48. data/lib/tcx/types/predefined_speed_zone.rb +7 -0
  49. data/lib/tcx/types/quick_workout.rb +8 -0
  50. data/lib/tcx/types/repeat.rb +8 -0
  51. data/lib/tcx/types/speed.rb +7 -0
  52. data/lib/tcx/types/speed_type.rb +10 -0
  53. data/lib/tcx/types/time.rb +11 -0
  54. data/lib/tcx/{models → types}/trackpoint.rb +2 -2
  55. data/lib/tcx/types/training.rb +14 -0
  56. data/lib/tcx/types/training_type.rb +10 -0
  57. data/lib/tcx/{models/duration.rb → types/user_initiated.rb} +1 -1
  58. data/lib/tcx/types/week.rb +12 -0
  59. data/lib/tcx/{models → types}/workout.rb +6 -0
  60. data/lib/tcx/types/workout_folder.rb +14 -0
  61. data/lib/tcx/types/workout_list.rb +21 -0
  62. data/lib/tcx/types/workouts.rb +10 -0
  63. data/lib/tcx/types/zone.rb +9 -0
  64. data/lib/tcx/types.rb +73 -0
  65. data/lib/tcx/version.rb +1 -1
  66. data/lib/tcx.rb +9 -4
  67. metadata +81 -41
  68. data/lib/tcx/models/activity_list.rb +0 -9
  69. data/lib/tcx/models/base.rb +0 -30
  70. data/lib/tcx/models/cadence.rb +0 -9
  71. data/lib/tcx/models/course_list.rb +0 -9
  72. data/lib/tcx/models/database.rb +0 -11
  73. data/lib/tcx/models/extensions.rb +0 -6
  74. data/lib/tcx/models/folders.rb +0 -7
  75. data/lib/tcx/models/heart_rate_as_percent_of_max.rb +0 -9
  76. data/lib/tcx/models/heart_rate_bpm.rb +0 -9
  77. data/lib/tcx/models/heart_rate_in_beats_per_minute.rb +0 -9
  78. data/lib/tcx/models/value.rb +0 -9
  79. data/lib/tcx/models/workout_list.rb +0 -9
  80. data/lib/tcx/models.rb +0 -40
  81. /data/lib/tcx/{models → types}/course.rb +0 -0
  82. /data/lib/tcx/{models → types}/course_lap.rb +0 -0
  83. /data/lib/tcx/{models → types}/course_point_type.rb +0 -0
  84. /data/lib/tcx/{models → types}/courses.rb +0 -0
  85. /data/lib/tcx/{models → types}/custom_heart_rate_zone.rb +0 -0
  86. /data/lib/tcx/{models → types}/gender.rb +0 -0
  87. /data/lib/tcx/{models → types}/heart_rate_value.rb +0 -0
  88. /data/lib/tcx/{models → types}/intensity.rb +0 -0
  89. /data/lib/tcx/{models → types}/name_key_reference.rb +0 -0
  90. /data/lib/tcx/{models → types}/position.rb +0 -0
  91. /data/lib/tcx/{models → types}/predefined_heart_rate_zone.rb +0 -0
  92. /data/lib/tcx/{models → types}/sensor_state.rb +0 -0
  93. /data/lib/tcx/{models → types}/sport.rb +0 -0
  94. /data/lib/tcx/{models → types}/step.rb +0 -0
  95. /data/lib/tcx/{models → types}/target.rb +0 -0
  96. /data/lib/tcx/{models → types}/track.rb +0 -0
  97. /data/lib/tcx/{models → types}/trigger_method.rb +0 -0
  98. /data/lib/tcx/{models → types}/version.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d01c5e7b68db327c2799dedc1bae6129d7e15611fc6150f0e579370e6e88f03
4
- data.tar.gz: 91be5cd3892f53f44624ee133e0a19fb9bab5d81199529dfacc33a2310d02c79
3
+ metadata.gz: 2a22d167ca20a21779421b65e48f939affd43b7983bda56f22da9e5013d6af6c
4
+ data.tar.gz: 7d526b593d253b3e6c4dcb64aea2ca7df4fa255d457871608d3677a16768b216
5
5
  SHA512:
6
- metadata.gz: 5ce16d273b98f168852ae1544b913ee8ac92fc7fda6db73a34c181a1787d1056c8b4883aecb74ce3ebb78ccfeaa3ab518a8ec9fe0a8696368267046bb4bc8113
7
- data.tar.gz: e1666d5a681fb5e95ccdb59f97a2fb34c64a4b561a68d7e2c47bf2273c718a99bca0bb18480f380dd31ff1396dec742091c75c4036c1161db6a5b76aed5713bd
6
+ metadata.gz: f2f772cb32f27738b1683c0087e13ca7a82987350846d875ae202d245a21bca3f0fafc5d0ed0cc8793c558adc585f29af080495feadf70dbee95c1baf8f45788
7
+ data.tar.gz: 625852f7eb80fe4c1123ba2d4187934aa28da3c1dd65de21ffc5a504b1c74c9c506c49fabd71df50e6a668273758907885429362ac91c9c87d66ac1e96bb6994
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
- ### 0.1.0 (Next)
1
+ ### 0.3.0 (2025-10-24)
2
2
 
3
- * Initial public release - [@dblock](https://github.com/dblock).
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
+
9
+ ### 0.2.0 (2025-10-24)
10
+
11
+ * Added support for writing TCX files - [@dblock](https://github.com/dblock).
12
+
13
+ ### 0.1.0 (2025-10-23)
14
+
15
+ * Initial public release, generic TCX reader - [@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 future writer). Unlike prior art such as [tcx_rb](https://github.com/keithdoggett/tcx_rb) or [tcxread](https://github.com/firefly-cpp/tcxread), aims to provide a more coherent API by implementing the complete read/write [TCX schema](https://www8.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd) in a more structured 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
 
@@ -17,16 +19,41 @@ Run `bundle install`.
17
19
 
18
20
  ## Usage
19
21
 
22
+ ### Working with Files
23
+
24
+ Read and write TCX files using `Tcx#load_file` and `dump`.
25
+
20
26
  ```ruby
21
27
  require 'tcx'
22
28
 
23
- tcx = Tcx.load_file('examples/multiple_running_activities.tcx') # => Tcx::Database
29
+ tcx = Tcx.load_file('activities.tcx') # => Tcx::Database
30
+
31
+ tcx.activities # => [Tcx::Activity], array of Tcx::Activity
32
+ tcx.workouts # => [Tcx::Workout]
33
+ tcx.courses # => [Tcx::Course]
34
+
35
+ tcx.dump # overwrites activities.tcx
36
+
37
+ tcx.dump('activities2.tcx') # writes to activities2.tcx
38
+ ```
39
+
40
+ ### Working with XML Data
41
+
42
+ Directly manipulate TCX data without creating files.
43
+
44
+ ```ruby
45
+ data = File.read('activities.tcx') # String
24
46
 
25
- tcx.activities # => [Tcx::Activity]
26
- ...
47
+ tcx = Tcx.load(data) # => Tcx::Database
48
+
49
+ tcx.to_xml # => XML string
50
+
51
+ tcx.dump('activities2.tcx') # writes to activities2.tcx
27
52
  ```
28
53
 
29
- See [examples/multiple_running_activities.rb](examples/multiple_running_activities.rb) for a detailed example.
54
+ ### Examples
55
+
56
+ See [examples](examples) for a complete set of working samples.
30
57
 
31
58
  ## Upgrading
32
59
 
@@ -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
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ module Extensions
5
+ module ActivityExtension
6
+ module V2
7
+ class CadenceSensorType
8
+ include Ruby::Enum
9
+
10
+ define :footpod, 'Footpod'
11
+ define :bike, 'Bike'
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'v2/base'
4
+ require_relative 'v2/cadence_sensor_type'
5
+ require_relative 'v2/activity_lap'
6
+ require_relative 'v2/activity_trackpoint'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'extensions/activity_extension/v2'
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class AbstractSource < Base
5
+ property 'name', from: 'Name'
6
+ end
7
+ end
@@ -6,6 +6,12 @@ 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) }
12
+
13
+ def self.attributes
14
+ ['sport']
15
+ end
10
16
  end
11
17
  end
@@ -0,0 +1,25 @@
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
+ property 'multisport_sessions', from: 'MultisportSession', transform_with: ->(v) { v.map { |el| MultisportSession.parse(el) } }
7
+
8
+ def_delegators :activities, :each, :count
9
+
10
+ def self.parse(list)
11
+ ActivityList.new(
12
+ 'Activities' => list.xpath('xmlns:Activity'),
13
+ 'MultisportSession' => list.xpath('xmlns:MultiSportSession')
14
+ )
15
+ end
16
+
17
+ def build_xml(builder, namespace = nil)
18
+ activities.each do |activity|
19
+ builder.Activity(activity.attributes) do |activity_builder|
20
+ activity.build_xml(activity_builder, namespace)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ 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
@@ -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,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Base < Hashie::Trash
5
+ include Hashie::Extensions::IgnoreUndeclared
6
+ extend Forwardable
7
+
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
41
+
42
+ new attributes
43
+ end
44
+
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
49
+
50
+ def to_class(xsi_type)
51
+ return unless xsi_type
52
+
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
59
+
60
+ def attributes
61
+ []
62
+ end
63
+
64
+ def namespace
65
+ DEFAULT_NAMESPACE
66
+ end
67
+ end
68
+
69
+ def attributes
70
+ self.class.attributes.map do |attribute|
71
+ value = self[attribute]
72
+ next unless value
73
+
74
+ property_name = self.class.inverse_translations.fetch(attribute, attribute)
75
+ [property_name, build_value(value)]
76
+ end.compact.to_h
77
+ end
78
+
79
+ def build_xml(builder, namespace = nil)
80
+ (self.class.properties - self.class.attributes).each do |property|
81
+ value = self[property]
82
+ next unless value
83
+
84
+ property_name = self.class.inverse_translations.fetch(property, property)
85
+
86
+ case value
87
+ when Base
88
+ build_el(builder, property_name, value)
89
+ when Array
90
+ value.each do |element|
91
+ build_el(builder, property_name, element)
92
+ end
93
+ else
94
+ builder = builder[namespace] if namespace
95
+ builder.send(property_name, build_value(value))
96
+ end
97
+ end
98
+ end
99
+
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)
117
+ builder = builder[namespace] if namespace
118
+
119
+ builder.send(property_name, attributes) do |property_builder|
120
+ element.build_xml(property_builder, namespace)
121
+ end
122
+ end
123
+
124
+ def build_value(value)
125
+ case value
126
+ when Nokogiri::XML::Element
127
+ # empty node
128
+ when ::Time
129
+ value.iso8601
130
+ else
131
+ value
132
+ end
133
+ end
134
+ end
135
+ 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
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class BuildType
5
+ include Ruby::Enum
6
+
7
+ define :internal, 'Internal'
8
+ define :alpha, 'Alpha'
9
+ define :beta, 'Beta'
10
+ define :release, 'Release'
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Cadence < Base
5
+ property 'value', from: 'Value', transform_with: lambda(&:to_i)
6
+
7
+ def_delegators :value, :to_i, :==
8
+ end
9
+ 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
@@ -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, namespace = nil)
14
+ courses.each do |course|
15
+ builder.Course(course.attributes) do |course_builder|
16
+ course.build_xml(course_builder, namespace)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ 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
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Database < Base
5
+ property 'folders', from: 'Folders', transform_with: ->(arr) { Folders.parse(arr) }
6
+ property 'activities', from: 'Activities', transform_with: ->(arr) { ActivityList.parse(arr) }
7
+ property 'workouts', from: 'Workouts', transform_with: ->(arr) { WorkoutList.parse(arr) }
8
+ property 'courses', from: 'Courses', transform_with: ->(arr) { CourseList.parse(arr) }
9
+ property 'author', from: 'Author', transform_with: ->(el) { AbstractSource.parse(el) }
10
+
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
28
+ end
29
+
30
+ def dump(target_path)
31
+ ::File.write(target_path, to_xml)
32
+ end
33
+
34
+ def to_xml
35
+ to_xml_builder.to_xml
36
+ end
37
+
38
+ private
39
+
40
+ def to_xml_builder
41
+ Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
42
+ xml.TrainingCenterDatabase(namespace_definitions) do |xml|
43
+ build_xml(xml)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tcx
4
- class AbstractSource < Base
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,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,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
@@ -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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class Folders < Base
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) } }
8
+ end
9
+ end