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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -2
- data/README.md +32 -5
- data/lib/tcx/extensions/activity_extension/v2/activity_lap.rb +21 -0
- data/lib/tcx/extensions/activity_extension/v2/activity_trackpoint.rb +22 -0
- data/lib/tcx/extensions/activity_extension/v2/base.rb +16 -0
- data/lib/tcx/extensions/activity_extension/v2/cadence_sensor_type.rb +16 -0
- data/lib/tcx/extensions/activity_extension/v2.rb +6 -0
- data/lib/tcx/extensions.rb +3 -0
- data/lib/tcx/file.rb +30 -0
- data/lib/tcx/types/abstract_source.rb +7 -0
- data/lib/tcx/{models → types}/activity.rb +6 -0
- data/lib/tcx/types/activity_list.rb +25 -0
- data/lib/tcx/types/activity_reference.rb +7 -0
- data/lib/tcx/types/application.rb +13 -0
- data/lib/tcx/types/base.rb +135 -0
- data/lib/tcx/types/boolean_type.rb +10 -0
- data/lib/tcx/types/build.rb +10 -0
- data/lib/tcx/types/build_type.rb +12 -0
- data/lib/tcx/types/cadence.rb +9 -0
- data/lib/tcx/types/cadence_target.rb +8 -0
- data/lib/tcx/types/calories_burned.rb +7 -0
- data/lib/tcx/{models → types}/course_folder.rb +5 -0
- data/lib/tcx/types/course_list.rb +21 -0
- data/lib/tcx/{models → types}/course_point.rb +1 -1
- data/lib/tcx/types/custom_speed_zone.rb +9 -0
- data/lib/tcx/types/database.rb +48 -0
- data/lib/tcx/{models/abstract_source.rb → types/device.rb} +5 -2
- data/lib/tcx/types/distance.rb +7 -0
- data/lib/tcx/types/duration.rb +10 -0
- data/lib/tcx/types/extensions_list.rb +9 -0
- data/lib/tcx/types/first_sport.rb +7 -0
- data/lib/tcx/types/folders.rb +9 -0
- data/lib/tcx/types/heart_rate.rb +7 -0
- data/lib/tcx/types/heart_rate_above.rb +7 -0
- data/lib/tcx/types/heart_rate_as_percent_of_max.rb +9 -0
- data/lib/tcx/types/heart_rate_below.rb +7 -0
- data/lib/tcx/types/heart_rate_bpm.rb +9 -0
- data/lib/tcx/types/heart_rate_in_beats_per_minute.rb +9 -0
- data/lib/tcx/types/history.rb +11 -0
- data/lib/tcx/types/history_folder.rb +19 -0
- data/lib/tcx/{models → types}/lap.rb +6 -2
- data/lib/tcx/types/multi_sport_folder.rb +16 -0
- data/lib/tcx/types/multisport_session.rb +10 -0
- data/lib/tcx/types/next_sport.rb +8 -0
- data/lib/tcx/types/none.rb +9 -0
- data/lib/tcx/types/plan.rb +14 -0
- data/lib/tcx/types/predefined_speed_zone.rb +7 -0
- data/lib/tcx/types/quick_workout.rb +8 -0
- data/lib/tcx/types/repeat.rb +8 -0
- data/lib/tcx/types/speed.rb +7 -0
- data/lib/tcx/types/speed_type.rb +10 -0
- data/lib/tcx/types/time.rb +11 -0
- data/lib/tcx/{models → types}/trackpoint.rb +2 -2
- data/lib/tcx/types/training.rb +14 -0
- data/lib/tcx/types/training_type.rb +10 -0
- data/lib/tcx/{models/duration.rb → types/user_initiated.rb} +1 -1
- data/lib/tcx/types/week.rb +12 -0
- data/lib/tcx/{models → types}/workout.rb +6 -0
- data/lib/tcx/types/workout_folder.rb +14 -0
- data/lib/tcx/types/workout_list.rb +21 -0
- data/lib/tcx/types/workouts.rb +10 -0
- data/lib/tcx/types/zone.rb +9 -0
- data/lib/tcx/types.rb +73 -0
- data/lib/tcx/version.rb +1 -1
- data/lib/tcx.rb +9 -4
- metadata +81 -41
- data/lib/tcx/models/activity_list.rb +0 -9
- data/lib/tcx/models/base.rb +0 -30
- data/lib/tcx/models/cadence.rb +0 -9
- data/lib/tcx/models/course_list.rb +0 -9
- data/lib/tcx/models/database.rb +0 -11
- data/lib/tcx/models/extensions.rb +0 -6
- data/lib/tcx/models/folders.rb +0 -7
- data/lib/tcx/models/heart_rate_as_percent_of_max.rb +0 -9
- data/lib/tcx/models/heart_rate_bpm.rb +0 -9
- data/lib/tcx/models/heart_rate_in_beats_per_minute.rb +0 -9
- data/lib/tcx/models/value.rb +0 -9
- data/lib/tcx/models/workout_list.rb +0 -9
- data/lib/tcx/models.rb +0 -40
- /data/lib/tcx/{models → types}/course.rb +0 -0
- /data/lib/tcx/{models → types}/course_lap.rb +0 -0
- /data/lib/tcx/{models → types}/course_point_type.rb +0 -0
- /data/lib/tcx/{models → types}/courses.rb +0 -0
- /data/lib/tcx/{models → types}/custom_heart_rate_zone.rb +0 -0
- /data/lib/tcx/{models → types}/gender.rb +0 -0
- /data/lib/tcx/{models → types}/heart_rate_value.rb +0 -0
- /data/lib/tcx/{models → types}/intensity.rb +0 -0
- /data/lib/tcx/{models → types}/name_key_reference.rb +0 -0
- /data/lib/tcx/{models → types}/position.rb +0 -0
- /data/lib/tcx/{models → types}/predefined_heart_rate_zone.rb +0 -0
- /data/lib/tcx/{models → types}/sensor_state.rb +0 -0
- /data/lib/tcx/{models → types}/sport.rb +0 -0
- /data/lib/tcx/{models → types}/step.rb +0 -0
- /data/lib/tcx/{models → types}/target.rb +0 -0
- /data/lib/tcx/{models → types}/track.rb +0 -0
- /data/lib/tcx/{models → types}/trigger_method.rb +0 -0
- /data/lib/tcx/{models → types}/version.rb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a22d167ca20a21779421b65e48f939affd43b7983bda56f22da9e5013d6af6c
|
|
4
|
+
data.tar.gz: 7d526b593d253b3e6c4dcb64aea2ca7df4fa255d457871608d3677a16768b216
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f2f772cb32f27738b1683c0087e13ca7a82987350846d875ae202d245a21bca3f0fafc5d0ed0cc8793c558adc585f29af080495feadf70dbee95c1baf8f45788
|
|
7
|
+
data.tar.gz: 625852f7eb80fe4c1123ba2d4187934aa28da3c1dd65de21ffc5a504b1c74c9c506c49fabd71df50e6a668273758907885429362ac91c9c87d66ac1e96bb6994
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
### 0.
|
|
1
|
+
### 0.3.0 (2025-10-24)
|
|
2
2
|
|
|
3
|
-
*
|
|
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
|
[](https://badge.fury.io/rb/tcx)
|
|
4
4
|
[](https://github.com/dblock/tcx/actions/workflows/test.yml)
|
|
5
5
|
|
|
6
|
-
A Garmin Training Center XML (TCX) reader
|
|
6
|
+
A Garmin Training Center XML (.TCX) reader and writer.
|
|
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('
|
|
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.
|
|
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
|
-
|
|
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
|
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
|
|
@@ -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,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 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
|
|
@@ -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
|
|
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 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
|