fit4ruby 0.0.2 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 49de464c7356d1b55144fac48b0f6150371e29a9
4
- data.tar.gz: 4b5e2057123c3e4a62c6f2335182cfd6cf8aa8ea
3
+ metadata.gz: 3b1343b7cebb0656f6a7f59aa5b1eb067e684504
4
+ data.tar.gz: c547627b2a01e6ed635cec1da6618f49e8bc5385
5
5
  SHA512:
6
- metadata.gz: b8688224199cac6c1b024711d680bb98b33cb761e46d36af2d83e61aa04701a9f182fc70f5dd442f4a8311c6be43f7a6bf1ccf063034fd31e751191dfff8c2a2
7
- data.tar.gz: 4c0bdd82555fb57d705d8992c21aa48a01b689d9596b61199894a89604d24dd4e159e55e86fcdd8b1327b4070270b688a78e0628e2caaad929f0308af4f88c4a
6
+ metadata.gz: cd337bb5e3d842f4720679ae934cd7c490f92c549569af97a14755e78f8275541faeec5bd44e3b2a858e3a4c647d7c747a54c2d4b2624afbd0e81fe6753cd1c4
7
+ data.tar.gz: bc9a7c8f43d5f239a768df8f18ebbe1989c835ac5c460f68ca71ef7f35b2390b2756ffd8f954b3de805880ea3c58a17a4d7612c0399d3ac4d6bcdf3c38410ada
data/README.md CHANGED
@@ -8,6 +8,93 @@ This libary is still work in progress and probably not yet ready to be
8
8
  used in your application. However, you are welcome to try it and send
9
9
  me comments and patches.
10
10
 
11
+ Usage
12
+ -----
13
+
14
+ You can create an Activity.
15
+
16
+ ```
17
+ require 'fit4ruby'
18
+
19
+ a = Fit4Ruby::Activity.new
20
+ a.total_timer_time = 30 * 60
21
+ a.new_user_profile({ :age => 33, :height => 1.78, :weight => 73.0,
22
+ :gender => 'male', :activity_class => 4.0,
23
+ :max_hr => 178 })
24
+
25
+ a.new_event({ :event => 'timer', :event_type => 'start_time' })
26
+ a.new_device_info({ :device_index => 0 })
27
+ a.new_device_info({ :device_index => 1, :battery_status => 'ok' })
28
+ ts = Time.now
29
+ 0.upto(a.total_timer_time / 60) do |mins|
30
+ ts += 60
31
+ a.new_record({
32
+ :timestamp => ts,
33
+ :position_lat => 51.5512 - mins * 0.0008,
34
+ :position_long => 11.647 + mins * 0.002,
35
+ :distance => 200.0 * mins,
36
+ :altitude => 100 + mins * 0.5,
37
+ :speed => 3.1,
38
+ :vertical_oscillation => 9 + mins * 0.02,
39
+ :stance_time => 235.0 * mins * 0.01,
40
+ :stance_time_percent => 32.0,
41
+ :heart_rate => 140 + mins,
42
+ :cadence => 75,
43
+ :activity_type => 'running',
44
+ :fractional_cadence => (mins % 2) / 2.0
45
+ })
46
+
47
+ if mins > 0 && mins % 5 == 0
48
+ a.new_lap({ :timestamp => ts })
49
+ end
50
+ end
51
+ a.new_session({ :timestamp => ts })
52
+ a.new_event({ :timestamp => ts, :event => 'recovery_time',
53
+ :event_type => 'marker',
54
+ :data => 2160 })
55
+ a.new_event({ :timestamp => ts, :event => 'vo2max',
56
+ :event_type => 'marker', :data => 52 })
57
+ a.new_event({ :timestamp => ts, :event => 'timer',
58
+ :event_type => 'stop_all' })
59
+ a.new_device_info({ :timestamp => ts, :device_index => 0 })
60
+ ts += 1
61
+ a.new_device_info({ :timestamp => ts, :device_index => 1,
62
+ :battery_status => 'low' })
63
+ ts += 120
64
+ a.new_event({ :timestamp => ts, :event => 'recovery_hr',
65
+ :event_type => 'marker', :data => 132 })
66
+ ```
67
+
68
+ Now you can have the accumulated data for laps and sessions computed.
69
+
70
+ ```
71
+ a.aggregate
72
+ ```
73
+
74
+ Save it to a file.
75
+
76
+ ```
77
+ Fit4Ruby.write('TEST.FIT', a)
78
+ ```
79
+
80
+ Or read an Activity from a file.
81
+
82
+ ```
83
+ a = Fit4Ruby.read('TEST.FIT')
84
+ ```
85
+
86
+ Then you can access the data in the file.
87
+
88
+ ```
89
+ a.records.each do |r|
90
+ puts "Latitude: #{r['position_lat']}"
91
+ puts "Longitude: #{r['position_long']}"
92
+ end
93
+ ```
94
+
95
+ Please see lib/fit4ruby/GlobalFitMessages.rb for the data fields that
96
+ are supported for the various FIT record types.
97
+
11
98
  License
12
99
  -------
13
100
 
data/Rakefile CHANGED
@@ -4,8 +4,11 @@ $:.unshift File.join(File.dirname(__FILE__))
4
4
  lib = File.expand_path('../lib', __FILE__)
5
5
  $:.unshift lib unless $:.include?(lib)
6
6
 
7
+ require "bundler/gem_tasks"
7
8
  require "rspec/core/rake_task"
8
9
  require 'rake/clean'
10
+ require 'yard'
11
+ YARD::Rake::YardocTask.new
9
12
 
10
13
  Dir.glob( 'tasks/*.rake').each do |fn|
11
14
  begin
@@ -23,12 +23,18 @@ require 'fit4ruby/PersonalRecords'
23
23
 
24
24
  module Fit4Ruby
25
25
 
26
+ # This is the most important class of this library. It holds references to
27
+ # all other data structures. Each of the objects it references are direct
28
+ # equivalents of the message record structures used in the FIT file.
26
29
  class Activity < FitDataRecord
27
30
 
28
31
  attr_accessor :file_id, :file_creator, :device_infos, :user_profiles,
29
32
  :sessions, :laps, :records, :events, :personal_records
30
33
 
31
- def initialize
34
+ # Create a new Activity object.
35
+ # @param field_values [Hash] A Hash that provides initial values for
36
+ # certain fields of the FitDataRecord.
37
+ def initialize(field_values = {})
32
38
  super('activity')
33
39
  @num_sessions = 0
34
40
 
@@ -46,8 +52,12 @@ module Fit4Ruby
46
52
  @cur_lap_records = []
47
53
 
48
54
  @lap_counter = 1
55
+
56
+ set_field_values(field_values)
49
57
  end
50
58
 
59
+ # Perform some basic logical checks on the object and all references sub
60
+ # objects. Any errors will be reported via the Log object.
51
61
  def check
52
62
  unless @timestamp && @timestamp >= Time.parse('1990-01-01')
53
63
  Log.error "Activity has no valid timestamp"
@@ -63,22 +73,30 @@ module Fit4Ruby
63
73
  @sessions.each { |s| s.check(self) }
64
74
  end
65
75
 
76
+ # Convenience method that aggregates all the distances from the included
77
+ # sessions.
66
78
  def total_distance
67
79
  d = 0.0
68
80
  @sessions.each { |s| d += s.total_distance }
69
81
  d
70
82
  end
71
83
 
84
+ # Call this method to update the aggregated data fields stored in Lap and
85
+ # Session objects.
72
86
  def aggregate
87
+ @laps.each { |l| l.aggregate }
73
88
  @sessions.each { |s| s.aggregate }
74
89
  end
75
90
 
91
+ # Convenience method that averages the speed over all sessions.
76
92
  def avg_speed
77
93
  speed = 0.0
78
94
  @sessions.each { |s| speed += s.avg_speed }
79
95
  speed / @sessions.length
80
96
  end
81
97
 
98
+ # Returns the predicted recovery time needed after this activity.
99
+ # @return recovery time in seconds.
82
100
  def recovery_time
83
101
  @events.each do |e|
84
102
  return e.data if e.event == 'recovery_time'
@@ -87,6 +105,8 @@ module Fit4Ruby
87
105
  nil
88
106
  end
89
107
 
108
+ # Returns the computed VO2max value. This value is computed by the device
109
+ # based on multiple previous activities.
90
110
  def vo2max
91
111
  @events.each do |e|
92
112
  return e.data if e.event == 'vo2max'
@@ -95,6 +115,10 @@ module Fit4Ruby
95
115
  nil
96
116
  end
97
117
 
118
+ # Write the Activity data to a file.
119
+ # @param io [IO] File reference
120
+ # @param id_mapper [FitMessageIdMapper] Maps global FIT record types to
121
+ # local ones.
98
122
  def write(io, id_mapper)
99
123
  @file_id.write(io, id_mapper)
100
124
  @file_creator.write(io, id_mapper)
@@ -106,42 +130,91 @@ module Fit4Ruby
106
130
  super
107
131
  end
108
132
 
133
+ # Add a new FileId to the Activity. It will replace any previously added
134
+ # FileId object.
135
+ # @param field_values [Hash] A Hash that provides initial values for
136
+ # certain fields of the FitDataRecord.
137
+ # @return [FileId]
109
138
  def new_file_id(field_values = {})
110
139
  new_fit_data_record('file_id', field_values)
111
140
  end
112
141
 
142
+ # Add a new FileCreator to the Activity. It will replace any previously
143
+ # added FileCreator object.
144
+ # @param field_values [Hash] A Hash that provides initial values for
145
+ # certain fields of the FitDataRecord.
146
+ # @return [FileCreator]
113
147
  def new_file_creator(field_values = {})
114
148
  new_fit_data_record('file_creator', field_values)
115
149
  end
116
150
 
151
+ # Add a new DeviceInfo to the Activity.
152
+ # @param field_values [Hash] A Hash that provides initial values for
153
+ # certain fields of the FitDataRecord.
154
+ # @return [DeviceInfo]
117
155
  def new_device_info(field_values = {})
118
156
  new_fit_data_record('device_info', field_values)
119
157
  end
120
158
 
159
+ # Add a new UserProfile to the Activity.
160
+ # @param field_values [Hash] A Hash that provides initial values for
161
+ # certain fields of the FitDataRecord.
162
+ # @return [UserProfile]
121
163
  def new_user_profile(field_values = {})
122
164
  new_fit_data_record('user_profile', field_values)
123
165
  end
124
166
 
167
+ # Add a new Event to the Activity.
168
+ # @param field_values [Hash] A Hash that provides initial values for
169
+ # certain fields of the FitDataRecord.
170
+ # @return [Event]
125
171
  def new_event(field_values = {})
126
172
  new_fit_data_record('event', field_values)
127
173
  end
128
174
 
175
+ # Add a new Session to the Activity. All previously added Lap objects are
176
+ # associated with this Session unless they have been associated with
177
+ # another Session before. If there are any Record objects that have not
178
+ # yet been associated with a Lap, a new lap will be created and the
179
+ # Record objects will be associated with this Lap. The Lap will be
180
+ # associated with the newly created Session.
181
+ # @param field_values [Hash] A Hash that provides initial values for
182
+ # certain fields of the FitDataRecord.
183
+ # @return [Session]
129
184
  def new_session(field_values = {})
130
185
  new_fit_data_record('session', field_values)
131
186
  end
132
187
 
188
+ # Add a new Lap to the Activity. All previoulsy added Record objects are
189
+ # associated with this Lap unless they have been associated with another
190
+ # Lap before.
191
+ # @param field_values [Hash] A Hash that provides initial values for
192
+ # certain fields of the FitDataRecord.
193
+ # @return [Lap]
133
194
  def new_lap(field_values = {})
134
195
  new_fit_data_record('lap', field_values)
135
196
  end
136
197
 
198
+ # Add a new PersonalRecord to the Activity.
199
+ # @param field_values [Hash] A Hash that provides initial values for
200
+ # certain fields of the FitDataRecord.
201
+ # @return [PersonalRecord]
137
202
  def new_personal_record(field_values = {})
138
203
  new_fit_data_record('personal_record', field_values)
139
204
  end
140
205
 
206
+ # Add a new Record to the Activity.
207
+ # @param field_values [Hash] A Hash that provides initial values for
208
+ # certain fields of the FitDataRecord.
209
+ # @return [Record]
141
210
  def new_record(field_values = {})
142
211
  new_fit_data_record('record', field_values)
143
212
  end
144
213
 
214
+ # Check if the current Activity is equal to the passed Activity.
215
+ # @param a [Activity] Activity to compare this Activity with.
216
+ # @return [TrueClass/FalseClass] true if both Activities are equal,
217
+ # otherwise false.
145
218
  def ==(a)
146
219
  super(a) && @file_id == a.file_id &&
147
220
  @file_creator == a.file_creator &&
@@ -150,6 +223,12 @@ module Fit4Ruby
150
223
  @sessions == a.sessions && personal_records == a.personal_records
151
224
  end
152
225
 
226
+ # Create a new FitDataRecord.
227
+ # @param record_type [String] Type that identifies the FitDataRecord
228
+ # derived class to create.
229
+ # @param field_values [Hash] A Hash that provides initial values for
230
+ # certain fields of the FitDataRecord.
231
+ # @return FitDataRecord
153
232
  def new_fit_data_record(record_type, field_values = {})
154
233
  case record_type
155
234
  when 'file_id'
@@ -165,20 +244,14 @@ module Fit4Ruby
165
244
  when 'session'
166
245
  unless @cur_lap_records.empty?
167
246
  # Ensure that all previous records have been assigned to a lap.
168
- @lap_counter += 1
169
- @cur_session_laps << (lap = Lap.new(@cur_lap_records, field_values))
170
- @laps << lap
171
- @cur_lap_records = []
247
+ record = create_new_lap(field_values)
172
248
  end
173
249
  @num_sessions += 1
174
250
  @sessions << (record = Session.new(@cur_session_laps, @lap_counter,
175
251
  field_values))
176
252
  @cur_session_laps = []
177
253
  when 'lap'
178
- @lap_counter += 1
179
- @cur_session_laps << (record = Lap.new(@cur_lap_records, field_values))
180
- @laps << record
181
- @cur_lap_records = []
254
+ record = create_new_lap(field_values)
182
255
  when 'record'
183
256
  @cur_lap_records << (record = Record.new(field_values))
184
257
  @records << record
@@ -191,6 +264,18 @@ module Fit4Ruby
191
264
  record
192
265
  end
193
266
 
267
+ private
268
+
269
+ def create_new_lap(field_values)
270
+ lap = Lap.new(@cur_lap_records, @laps.last, field_values)
271
+ @lap_counter += 1
272
+ @cur_session_laps << lap
273
+ @laps << lap
274
+ @cur_lap_records = []
275
+
276
+ lap
277
+ end
278
+
194
279
  end
195
280
 
196
281
  end
@@ -14,9 +14,32 @@ module Fit4Ruby
14
14
 
15
15
  module Converters
16
16
 
17
- def speedToPace(speed)
18
- if speed > 0.01
19
- pace = 1000.0 / (speed * 60.0)
17
+ def conversion_factor(from_unit, to_unit)
18
+ factors = {
19
+ 'm' => { 'km' => 0.001, 'in' => 39.3701, 'ft' => 3.28084,
20
+ 'mi' => 0.000621371 },
21
+ 'mm' => { 'cm' => 0.1, 'in' => 0.0393701 },
22
+ 'm/s' => { 'km/h' => 0.277778 },
23
+ 'min/km' => { 'min/mi' => 1.60934 },
24
+ 'kg' => { 'lbs' => 0.453592 }
25
+ }
26
+ return 1.0 if from_unit == to_unit
27
+ unless factors.include?(from_unit)
28
+ Log.fatal "No conversion factors defined for unit " +
29
+ "'#{from_unit}' to '#{to_unit}'"
30
+ end
31
+
32
+ factor = factors[from_unit][to_unit]
33
+ if factor.nil?
34
+ Log.fatal "No conversion factor from '#{from_unit}' to '#{to_unit}' " +
35
+ "defined."
36
+ end
37
+ factor
38
+ end
39
+
40
+ def speedToPace(speed, distance = 1000.0)
41
+ if speed && speed > 0.01
42
+ pace = distance / (speed * 60.0)
20
43
  int, dec = pace.divmod 1
21
44
  "#{int}:#{'%02d' % (dec * 60)}"
22
45
  else
@@ -17,14 +17,17 @@ module Fit4Ruby
17
17
 
18
18
  class FitDataRecord
19
19
 
20
+ include Converters
21
+
20
22
  def initialize(record_id)
21
23
  @message = GlobalFitMessages.find_by_name(record_id)
22
24
 
23
25
  # Create instance variables that correspond to every field of the
24
- # corresponding FIT # data record.
26
+ # corresponding FIT data record.
25
27
  @message.fields.each do |field_number, field|
26
28
  create_instance_variable(field.name)
27
29
  end
30
+ @meta_field_units = {}
28
31
  @timestamp = Time.now
29
32
  end
30
33
 
@@ -43,6 +46,29 @@ module Fit4Ruby
43
46
  instance_variable_set('@' + name, value)
44
47
  end
45
48
 
49
+ def get(name)
50
+ ivar_name = '@' + name
51
+ return nil unless instance_variable_defined?(ivar_name)
52
+
53
+ instance_variable_get('@' + name)
54
+ end
55
+
56
+ def get_as(name, to_unit)
57
+ value = respond_to?(name) ? send(name) : get(name)
58
+ return nil if value.nil?
59
+
60
+ if @meta_field_units.include?(name)
61
+ unit = @meta_field_units[name]
62
+ else
63
+ field = @message.find_by_name(name)
64
+ unless (unit = field.opts[:unit])
65
+ Log.fatal "Field #{name} has no unit"
66
+ end
67
+ end
68
+
69
+ value * conversion_factor(unit, to_unit)
70
+ end
71
+
46
72
  def ==(fdr)
47
73
  @message.fields.each do |field_number, field|
48
74
  ivar_name = '@' + field.name
@@ -114,6 +114,9 @@ module Fit4Ruby
114
114
  when 'date_time'
115
115
  value = time_to_fit_time(value)
116
116
  end
117
+ if value.is_a?(Float) && @opts[:scale].nil?
118
+ Log.error "Field #{@name} must not be a Float value"
119
+ end
117
120
 
118
121
  value
119
122
  end
@@ -141,6 +144,10 @@ module Fit4Ruby
141
144
  @fields[number] = Field.new(type, name, opts)
142
145
  end
143
146
 
147
+ def find_by_name(field_name)
148
+ @fields.values.find { |f| f.name == field_name }
149
+ end
150
+
144
151
  def write(io, local_message_type)
145
152
  header = FitRecordHeader.new
146
153
  header.normal = 0
@@ -195,7 +202,7 @@ module Fit4Ruby
195
202
  end
196
203
 
197
204
  def find_by_name(name)
198
- @messages.each_value{ |m| return m if m.name == name }
205
+ @messages.values.find { |m| m.name == name }
199
206
  end
200
207
 
201
208
  def field(number, type, name, opts = {})
@@ -43,7 +43,7 @@ module Fit4Ruby
43
43
  field 17, 'uint8', 'max_heart_rate', :unit => 'bpm'
44
44
  field 18, 'uint8', 'avg_running_cadence', :unit => 'strides/min'
45
45
  field 19, 'uint8', 'max_running_cadence', :unit => 'strides/min'
46
- field 22, 'uint16', 'total_ascend', :unit => 'm'
46
+ field 22, 'uint16', 'total_ascent', :unit => 'm'
47
47
  field 23, 'uint16', 'total_descent', :unit => 'm'
48
48
  field 24, 'uint8', 'total_training_effect', :scale => 10
49
49
  field 25, 'uint16', 'first_lap_index'
@@ -162,6 +162,17 @@ module Fit4Ruby
162
162
  field 25, 'enum', 'undocumented_field_25'
163
163
  field 253, 'uint32', 'timestamp', :type => 'date_time'
164
164
 
165
+ message 33, 'totals'
166
+ field 0, 'uint32', 'timer_time', :unit => 's'
167
+ field 1, 'uint32', 'distance', :unit => 'm'
168
+ field 2, 'uint32', 'calories', :unit => 'kcal'
169
+ field 3, 'enum', 'sport', :dict => 'sport'
170
+ field 4, 'uint32', 'elapsed_time', :unit => 's'
171
+ field 5, 'uint16', 'sessions'
172
+ field 6, 'uint32', 'active_time', :unit => 's'
173
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
174
+ field 254, 'uint16', 'message_index'
175
+
165
176
  message 34, 'activity'
166
177
  field 0, 'uint32', 'total_timer_time', :type => 'duration', :scale => 1000
167
178
  field 1, 'uint16', 'num_sessions'
@@ -11,16 +11,35 @@
11
11
  #
12
12
 
13
13
  require 'fit4ruby/FitDataRecord'
14
+ require 'fit4ruby/RecordAggregator'
14
15
 
15
16
  module Fit4Ruby
16
17
 
17
18
  class Lap < FitDataRecord
18
19
 
20
+ include RecordAggregator
21
+
19
22
  attr_reader :records
20
23
 
21
- def initialize(records, field_values)
24
+ def initialize(records, previous_lap, field_values)
22
25
  super('lap')
26
+ @meta_field_units['avg_stride_length'] = 'm'
23
27
  @records = records
28
+ @previous_lap = previous_lap
29
+
30
+ if previous_lap && previous_lap.records && previous_lap.records.last
31
+ # Set the start time of the new lap to the timestamp of the last record
32
+ # of the previous lap.
33
+ @start_time = previous_lap.records.last.timestamp
34
+ elsif records.first
35
+ # Or to the timestamp of the first record.
36
+ @start_time = records.first.timestamp
37
+ end
38
+
39
+ if records.last
40
+ @total_elapsed_time = records.last.timestamp - @start_time
41
+ end
42
+
24
43
  set_field_values(field_values)
25
44
  end
26
45
 
@@ -42,49 +61,11 @@ module Fit4Ruby
42
61
  end
43
62
  end
44
63
 
45
- def aggregate
46
- return if @records.empty?
47
-
48
- r = @records[0]
49
- @start_time = r.timestamp
50
- @start_position_lat = r.position_lat
51
- @start_position_long = r.position_long
52
-
53
- r = @records[-1]
54
- @end_position_lat = r.position_lat
55
- @end_position_long = r.position_long
56
- @total_elapsed_time = r.timestamp - @start_time
57
- @total_timer_time = @total_elapsed_time
58
- @avg_speed = ((@total_distance.to_f / @total_elapsed_time) * 1000).to_i
59
-
60
- @max_speed = 0
61
- first_distance, last_distance = nil, nil
62
- @records.each do |r|
63
- first_distance = r.distance if first_distance.nil? && r.distance
64
- last_distance = r.distance if r.distance
65
- @max_speed = r.speed if r.speed && @max_speed < r.speed
66
-
67
- if r.position_lat
68
- if (@swc_lat.nil? || r.position_lat < @swc_lat)
69
- @swc_lat = r.position_lat
70
- end
71
- if (@nec_lat.nil? || r.position_lat > @nec_lat)
72
- @nec_lat = r.position_lat
73
- end
74
- end
75
- if r.position_long
76
- if (@swc_long.nil? || r.position_long < @swc_long)
77
- @swc_long = r.position_long
78
- end
79
- if (@nec_long.nil? || r.position_long > @nec_long)
80
- @nec_long = r.position_long
81
- end
82
- end
83
-
84
- end
64
+ # Compute the average stride length for this Session.
65
+ def avg_stride_length
66
+ return nil unless @total_strides
85
67
 
86
- @total_distance = last_distance && first_distance ?
87
- last_distance - first_distance : 0
68
+ @total_distance / (@total_strides * 2.0)
88
69
  end
89
70
 
90
71
  end
@@ -14,13 +14,32 @@ require 'fit4ruby/FitDataRecord'
14
14
 
15
15
  module Fit4Ruby
16
16
 
17
+ # The Record corresponds to the record FIT message. A Record is a basic set
18
+ # of primary measurements that are associated with a certain timestamp.
17
19
  class Record < FitDataRecord
18
20
 
21
+ # Create a new Record object.
22
+ # @param field_values [Hash] Hash that provides initial values for certain
23
+ # fields.
19
24
  def initialize(field_values = {})
20
25
  super('record')
26
+ @meta_field_units['pace'] = 'min/km'
27
+ @meta_field_units['run_cadence'] = 'spm'
21
28
  set_field_values(field_values)
22
29
  end
23
30
 
31
+ def run_cadence
32
+ if @cadence && @fractional_cadence
33
+ (@cadence + @fractional_cadence) * 2
34
+ elsif @cadence
35
+ @cadence * 2
36
+ else
37
+ nil
38
+ end
39
+ end
40
+
41
+ # Convert the 'speed' field into a running pace. The pace is measured in
42
+ # minutes per Kilometer.
24
43
  def pace
25
44
  1000.0 / (@speed * 60.0)
26
45
  end
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = RecordAggregator.rb -- Fit4Ruby - FIT file processing library for Ruby
5
+ #
6
+ # Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
7
+ #
8
+ # This program is free software; you can redistribute it and/or modify
9
+ # it under the terms of version 2 of the GNU General Public License as
10
+ # published by the Free Software Foundation.
11
+ #
12
+
13
+ module Fit4Ruby
14
+
15
+ module RecordAggregator
16
+
17
+ def aggregate
18
+ return if @records.empty?
19
+
20
+ # TODO: Add support for pause events.
21
+ @total_timer_time = @total_elapsed_time
22
+
23
+ aggregate_geo_region
24
+ aggregate_ascent_descent
25
+ aggregate_speed_distance
26
+ aggregate_heart_rate
27
+ aggregate_strides
28
+ aggregate_vertical_oscillation
29
+ aggregate_stance_time
30
+ end
31
+
32
+ def aggregate_geo_region
33
+ r = @records.first
34
+ @start_position_lat = r.position_lat
35
+ @start_position_long = r.position_long
36
+
37
+ r = @records.last
38
+ @end_position_lat = r.position_lat
39
+ @end_position_long = r.position_long
40
+
41
+ @records.each do |r|
42
+ if r.position_lat
43
+ if (@swc_lat.nil? || r.position_lat < @swc_lat)
44
+ @swc_lat = r.position_lat
45
+ end
46
+ if (@nec_lat.nil? || r.position_lat > @nec_lat)
47
+ @nec_lat = r.position_lat
48
+ end
49
+ end
50
+ if r.position_long
51
+ if (@swc_long.nil? || r.position_long < @swc_long)
52
+ @swc_long = r.position_long
53
+ end
54
+ if (@nec_long.nil? || r.position_long > @nec_long)
55
+ @nec_long = r.position_long
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def aggregate_ascent_descent
62
+ @total_ascent = @total_descent = 0
63
+ altitude = nil
64
+
65
+ @records.each do |r|
66
+ if altitude
67
+ if r.altitude < altitude
68
+ @total_descent += (altitude - r.altitude)
69
+ else
70
+ @total_ascent += (r.altitude - altitude)
71
+ end
72
+ end
73
+ altitude = r.altitude
74
+ end
75
+ end
76
+
77
+ def aggregate_speed_distance
78
+ @max_speed = 0
79
+ first_distance, last_distance = nil, nil
80
+
81
+ @records.each do |r|
82
+ @max_speed = r.speed if r.speed && @max_speed < r.speed
83
+
84
+ first_distance = r.distance if first_distance.nil? && r.distance
85
+ last_distance = r.distance if r.distance
86
+ end
87
+
88
+ @total_distance = last_distance && first_distance ?
89
+ last_distance - first_distance : 0
90
+ @avg_speed = @total_distance.to_f / @total_elapsed_time
91
+ end
92
+
93
+ def aggregate_heart_rate
94
+ total_heart_beats = 0
95
+ @max_heart_rate = 0
96
+ last_timestamp = @start_time
97
+
98
+ @records.each do |r|
99
+ if r.heart_rate
100
+ delta_t = last_timestamp ? r.timestamp - last_timestamp : nil
101
+ total_heart_beats += (r.heart_rate / 60.0) * delta_t if delta_t
102
+ if r.heart_rate > @max_heart_rate
103
+ @max_heart_rate = r.heart_rate
104
+ end
105
+ end
106
+ last_timestamp = r.timestamp
107
+ end
108
+ @avg_heart_rate = (total_heart_beats.to_f / @total_elapsed_time * 60).to_i
109
+ end
110
+
111
+ def aggregate_strides
112
+ @total_strides = 0
113
+ @max_running_cadence = 0
114
+ last_timestamp = @start_time
115
+
116
+ @records.each do |r|
117
+ delta_t = last_timestamp ? r.timestamp - last_timestamp : nil
118
+
119
+ if delta_t && (run_cadence = r.run_cadence)
120
+ @total_strides += (run_cadence / 60.0) * delta_t
121
+ end
122
+ if run_cadence && run_cadence > @max_running_cadence
123
+ @max_running_cadence = run_cadence.to_i
124
+ end
125
+
126
+ last_timestamp = r.timestamp
127
+ end
128
+ @avg_running_cadence, @avg_fractional_cadence =
129
+ (@total_strides.to_f / @total_timer_time * (60 / 2)).divmod(1)
130
+ @total_strides = @total_strides.to_i
131
+ end
132
+
133
+ def aggregate_vertical_oscillation
134
+ total_vertical_oscillation = 0.0
135
+ vertical_oscillation_count = 0
136
+
137
+ @records.each do |r|
138
+ if r.vertical_oscillation
139
+ total_vertical_oscillation += r.vertical_oscillation
140
+ vertical_oscillation_count += 1
141
+ end
142
+ end
143
+
144
+ @avg_vertical_oscillation = total_vertical_oscillation /
145
+ vertical_oscillation_count
146
+ end
147
+
148
+ def aggregate_stance_time
149
+ total_stance_time = 0.0
150
+ total_stance_time_percent = 0.0
151
+ stance_time_count = 0
152
+ stance_time_percent_count = 0
153
+
154
+ @records.each do |r|
155
+ if r.stance_time
156
+ total_stance_time += r.stance_time
157
+ stance_time_count += 1
158
+ end
159
+ if r.stance_time_percent
160
+ total_stance_time_percent += r.stance_time_percent
161
+ stance_time_percent_count += 1
162
+ end
163
+ end
164
+
165
+ @avg_stance_time = total_stance_time / stance_time_count
166
+ @avg_stance_time_percent = total_stance_time_percent /
167
+ stance_time_percent_count
168
+ end
169
+
170
+ end
171
+
172
+ end
173
+
@@ -12,23 +12,45 @@
12
12
 
13
13
  require 'fit4ruby/Converters'
14
14
  require 'fit4ruby/FitDataRecord'
15
+ require 'fit4ruby/RecordAggregator'
15
16
 
16
17
  module Fit4Ruby
17
18
 
19
+ # The Session objects correspond to the session FIT messages. They hold
20
+ # accumlated data for a set of Lap objects.
18
21
  class Session < FitDataRecord
19
22
 
20
- include Converters
23
+ include RecordAggregator
21
24
 
22
25
  attr_reader :laps
23
26
 
27
+ # Create a new Session object.
28
+ # @param laps [Array of Laps] Laps to associate with the Session.
29
+ # @param first_lap_index [Fixnum] Index of the first Lap in this Session.
30
+ # @param field_values [Hash] Hash that provides initial values for certain
31
+ # fields.
24
32
  def initialize(laps, first_lap_index, field_values)
25
33
  super('session')
34
+ @meta_field_units['avg_stride_length'] = 'm'
26
35
  @laps = laps
36
+ @records = []
37
+ @laps.each { |lap| @records += lap.records }
27
38
  @first_lap_index = first_lap_index
28
39
  @num_laps = @laps.length
40
+
41
+ if @records.first
42
+ # Or to the timestamp of the first record.
43
+ @start_time = @records.first.timestamp
44
+ if @records.last
45
+ @total_elapsed_time = @records.last.timestamp - @start_time
46
+ end
47
+ end
48
+
29
49
  set_field_values(field_values)
30
50
  end
31
51
 
52
+ # Perform some basic consistency and logical checks on the object. Errors
53
+ # are reported via the Log object.
32
54
  def check(activity)
33
55
  unless @first_lap_index
34
56
  Log.error 'first_lap_index is not set'
@@ -47,33 +69,16 @@ module Fit4Ruby
47
69
  @laps.each { |l| l.check }
48
70
  end
49
71
 
50
- def aggregate
51
- @total_distance = 0
52
- @total_elapsed_time = 0
53
- @laps.each do |lap|
54
- lap.aggregate
55
- @total_distance += lap.total_distance if lap.total_distance
56
- @total_elapsed_time += lap.total_elapsed_time if lap.total_elapsed_time
57
- end
58
- if (l = @laps[0])
59
- @start_time = l.start_time
60
- @start_position_lat = l.start_position_lat
61
- @start_position_long = l.start_position_long
62
- end
63
- if (l = @laps[-1])
64
- @end_position_lat = l.end_position_lat
65
- @end_position_long = l.end_position_long
66
- end
67
-
68
- if @total_distance && @total_elapsed_time
69
- @avg_speed = @total_distance / @total_elapsed_time
70
- end
72
+ # Return true if the session contains geographical location data.
73
+ def has_geo_data?
74
+ @swc_long && @swc_lat && @nec_long && nec_lat
71
75
  end
72
76
 
77
+ # Compute the average stride length for this Session.
73
78
  def avg_stride_length
74
79
  return nil unless @total_strides
75
80
 
76
- @total_distance / @total_strides
81
+ @total_distance / (@total_strides * 2.0)
77
82
  end
78
83
 
79
84
  end
@@ -14,8 +14,12 @@ require 'fit4ruby/FitDataRecord'
14
14
 
15
15
  module Fit4Ruby
16
16
 
17
+ # This class corresponds to the user_profile FIT message.
17
18
  class UserProfile < FitDataRecord
18
19
 
20
+ # Create a new UserProfile object.
21
+ # @param field_values [Hash] Hash that provides initial values for certain
22
+ # fields.
19
23
  def initialize(field_values = {})
20
24
  super('user_profile')
21
25
  set_field_values(field_values)
@@ -1,3 +1,4 @@
1
1
  module Fit4Ruby
2
- VERSION = '0.0.2'
2
+ # The version number of the library.
3
+ VERSION = '0.0.3'
3
4
  end
@@ -32,7 +32,7 @@ describe Fit4Ruby do
32
32
  :position_lat => 51.5512 - mins * 0.0008,
33
33
  :position_long => 11.647 + mins * 0.002,
34
34
  :distance => 200.0 * mins,
35
- :altitude => 100 + mins * 0.5,
35
+ :altitude => (100 + mins * 0.5).to_i,
36
36
  :speed => 3.1,
37
37
  :vertical_oscillation => 9 + mins * 0.02,
38
38
  :stance_time => 235.0 * mins * 0.01,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fit4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Schlaeger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-10 00:00:00.000000000 Z
11
+ date: 2014-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bindata
@@ -106,6 +106,7 @@ files:
106
106
  - lib/fit4ruby/Log.rb
107
107
  - lib/fit4ruby/PersonalRecords.rb
108
108
  - lib/fit4ruby/Record.rb
109
+ - lib/fit4ruby/RecordAggregator.rb
109
110
  - lib/fit4ruby/Session.rb
110
111
  - lib/fit4ruby/UserProfile.rb
111
112
  - lib/fit4ruby/version.rb
@@ -140,3 +141,4 @@ specification_version: 4
140
141
  summary: Library to read GARMIN FIT files.
141
142
  test_files:
142
143
  - spec/FitFile_spec.rb
144
+ has_rdoc: