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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -2
  3. data/README.md +30 -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 +4 -0
  13. data/lib/tcx/types/activity_list.rb +21 -0
  14. data/lib/tcx/types/application.rb +13 -0
  15. data/lib/tcx/types/base.rb +111 -0
  16. data/lib/tcx/types/build.rb +10 -0
  17. data/lib/tcx/types/build_type.rb +12 -0
  18. data/lib/tcx/types/cadence.rb +9 -0
  19. data/lib/tcx/types/course_list.rb +21 -0
  20. data/lib/tcx/{models → types}/database.rb +22 -0
  21. data/lib/tcx/{models/abstract_source.rb → types/device.rb} +5 -2
  22. data/lib/tcx/types/duration.rb +10 -0
  23. data/lib/tcx/types/extensions_list.rb +9 -0
  24. data/lib/tcx/types/heart_rate_as_percent_of_max.rb +9 -0
  25. data/lib/tcx/types/heart_rate_bpm.rb +9 -0
  26. data/lib/tcx/types/heart_rate_in_beats_per_minute.rb +9 -0
  27. data/lib/tcx/{models → types}/lap.rb +5 -1
  28. data/lib/tcx/{models → types}/trackpoint.rb +1 -1
  29. data/lib/tcx/types/workout_list.rb +21 -0
  30. data/lib/tcx/types.rb +43 -0
  31. data/lib/tcx/version.rb +1 -1
  32. data/lib/tcx.rb +9 -4
  33. metadata +51 -41
  34. data/lib/tcx/models/activity_list.rb +0 -9
  35. data/lib/tcx/models/base.rb +0 -30
  36. data/lib/tcx/models/cadence.rb +0 -9
  37. data/lib/tcx/models/course_list.rb +0 -9
  38. data/lib/tcx/models/duration.rb +0 -6
  39. data/lib/tcx/models/extensions.rb +0 -6
  40. data/lib/tcx/models/heart_rate_as_percent_of_max.rb +0 -9
  41. data/lib/tcx/models/heart_rate_bpm.rb +0 -9
  42. data/lib/tcx/models/heart_rate_in_beats_per_minute.rb +0 -9
  43. data/lib/tcx/models/value.rb +0 -9
  44. data/lib/tcx/models/workout_list.rb +0 -9
  45. data/lib/tcx/models.rb +0 -40
  46. /data/lib/tcx/{models → types}/course.rb +0 -0
  47. /data/lib/tcx/{models → types}/course_folder.rb +0 -0
  48. /data/lib/tcx/{models → types}/course_lap.rb +0 -0
  49. /data/lib/tcx/{models → types}/course_point.rb +0 -0
  50. /data/lib/tcx/{models → types}/course_point_type.rb +0 -0
  51. /data/lib/tcx/{models → types}/courses.rb +0 -0
  52. /data/lib/tcx/{models → types}/custom_heart_rate_zone.rb +0 -0
  53. /data/lib/tcx/{models → types}/folders.rb +0 -0
  54. /data/lib/tcx/{models → types}/gender.rb +0 -0
  55. /data/lib/tcx/{models → types}/heart_rate_value.rb +0 -0
  56. /data/lib/tcx/{models → types}/intensity.rb +0 -0
  57. /data/lib/tcx/{models → types}/name_key_reference.rb +0 -0
  58. /data/lib/tcx/{models → types}/position.rb +0 -0
  59. /data/lib/tcx/{models → types}/predefined_heart_rate_zone.rb +0 -0
  60. /data/lib/tcx/{models → types}/sensor_state.rb +0 -0
  61. /data/lib/tcx/{models → types}/sport.rb +0 -0
  62. /data/lib/tcx/{models → types}/step.rb +0 -0
  63. /data/lib/tcx/{models → types}/target.rb +0 -0
  64. /data/lib/tcx/{models → types}/track.rb +0 -0
  65. /data/lib/tcx/{models → types}/trigger_method.rb +0 -0
  66. /data/lib/tcx/{models → types}/version.rb +0 -0
  67. /data/lib/tcx/{models → types}/workout.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: 16a519fa1d9c70b3f319428170d9e52f36a1eaa4baacdd4004d336e35c57d2c6
4
+ data.tar.gz: 57571958ee9e4182e5c271ebec43639c78b46253e837caf0eb1bbd55b553398c
5
5
  SHA512:
6
- metadata.gz: 5ce16d273b98f168852ae1544b913ee8ac92fc7fda6db73a34c181a1787d1056c8b4883aecb74ce3ebb78ccfeaa3ab518a8ec9fe0a8696368267046bb4bc8113
7
- data.tar.gz: e1666d5a681fb5e95ccdb59f97a2fb34c64a4b561a68d7e2c47bf2273c718a99bca0bb18480f380dd31ff1396dec742091c75c4036c1161db6a5b76aed5713bd
6
+ metadata.gz: 5e1339b4ef3c6817baf72d973d1b00c92b1ad109927a7012892758d27e3140b863786a431ee25b8176130801549edbad992939c3db4dfa853763802d60d36630
7
+ data.tar.gz: 631de4c6b5e4dabe0c3db3b87255583d4dd30e05cb58425e4e10aa407b2b43136c82c2f3e840cd428d5c16e86b10f89cf25655d13e371b3e8bcdee2ab9357500
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
- ### 0.1.0 (Next)
1
+ ### 0.2.0 (2025-10-24)
2
2
 
3
- * Initial public release - [@dblock](https://github.com/dblock).
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
  [![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. 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('examples/multiple_running_activities.tcx') # => Tcx::Database
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.activities # => [Tcx::Activity]
26
- ...
35
+ tcx.dump('activities2.tcx') # writes to activities2.tcx
27
36
  ```
28
37
 
29
- See [examples/multiple_running_activities.rb](examples/multiple_running_activities.rb) for a detailed example.
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
@@ -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
@@ -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,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,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 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,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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class HeartRateAsPercentOfMax < 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class HeartRateBpm < 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tcx
4
+ class HeartRateInBeatsPerMinute < Base
5
+ property 'value', from: 'Value', transform_with: lambda(&:to_i)
6
+
7
+ def_delegators :value, :to_i, :==
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) { Extensions.parse(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) { Extensions.parse(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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tcx
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
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/models'
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
- xml = Nokogiri::XML(File.open(path))
14
- xml.root.add_namespace_definition('ns3', 'http://www.garmin.com/xmlschemas/ActivityExtension/v2')
15
- Tcx::Database.parse(xml.root)
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.1.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-23 00:00:00.000000000 Z
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/models.rb
66
- - lib/tcx/models/abstract_source.rb
67
- - lib/tcx/models/activity.rb
68
- - lib/tcx/models/activity_list.rb
69
- - lib/tcx/models/base.rb
70
- - lib/tcx/models/cadence.rb
71
- - lib/tcx/models/course.rb
72
- - lib/tcx/models/course_folder.rb
73
- - lib/tcx/models/course_lap.rb
74
- - lib/tcx/models/course_list.rb
75
- - lib/tcx/models/course_point.rb
76
- - lib/tcx/models/course_point_type.rb
77
- - lib/tcx/models/courses.rb
78
- - lib/tcx/models/custom_heart_rate_zone.rb
79
- - lib/tcx/models/database.rb
80
- - lib/tcx/models/duration.rb
81
- - lib/tcx/models/extensions.rb
82
- - lib/tcx/models/folders.rb
83
- - lib/tcx/models/gender.rb
84
- - lib/tcx/models/heart_rate_as_percent_of_max.rb
85
- - lib/tcx/models/heart_rate_bpm.rb
86
- - lib/tcx/models/heart_rate_in_beats_per_minute.rb
87
- - lib/tcx/models/heart_rate_value.rb
88
- - lib/tcx/models/intensity.rb
89
- - lib/tcx/models/lap.rb
90
- - lib/tcx/models/name_key_reference.rb
91
- - lib/tcx/models/position.rb
92
- - lib/tcx/models/predefined_heart_rate_zone.rb
93
- - lib/tcx/models/sensor_state.rb
94
- - lib/tcx/models/sport.rb
95
- - lib/tcx/models/step.rb
96
- - lib/tcx/models/target.rb
97
- - lib/tcx/models/track.rb
98
- - lib/tcx/models/trackpoint.rb
99
- - lib/tcx/models/trigger_method.rb
100
- - lib/tcx/models/value.rb
101
- - lib/tcx/models/version.rb
102
- - lib/tcx/models/workout.rb
103
- - lib/tcx/models/workout_list.rb
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:
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tcx
4
- class ActivityList
5
- def self.parse(list)
6
- list.xpath('xmlns:Activity').map { |el| Activity.parse(el) }
7
- end
8
- end
9
- end
@@ -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
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tcx
4
- class Cadence < Value
5
- def self.parse(node)
6
- Value.parse(node).to_i
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tcx
4
- class CourseList
5
- def self.parse(list)
6
- list.xpath('xmlns:Course').map { |el| Course.parse(el) }
7
- end
8
- end
9
- end
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tcx
4
- class Duration < Base
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tcx
4
- class Extensions < Base
5
- end
6
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tcx
4
- class HeartRateAsPercentOfMax < Value
5
- def self.parse(node)
6
- Value.parse(node).to_i
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tcx
4
- class HeartRateBpm < Value
5
- def self.parse(node)
6
- Value.parse(node).to_i
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tcx
4
- class HeartRateInBeatsPerMinute < Value
5
- def self.parse(node)
6
- Value.parse(node).to_i
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tcx
4
- class Value
5
- def self.parse(node)
6
- node.xpath('xmlns:Value').text
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Tcx
4
- class WorkoutList
5
- def self.parse(list)
6
- list.xpath('xmlns:Workout').map { |el| Workout.parse(el) }
7
- end
8
- end
9
- end
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