fit4ruby 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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: