Class: Fit4Ruby::Activity

Inherits:
FitDataRecord show all
Defined in:
lib/fit4ruby/Activity.rb

Overview

Activity files are arguably the most common type of FIT file. The Activity class represents the top-level structure of an activity FIT file. It holds references to all other data structures. Each of the objects it references are direct equivalents of the message record structures used in the FIT file.

Constant Summary collapse

FILE_SECTIONS =

These symbols are a complete list of all the sub-sections that an activity FIT file may contain. This list is used to generate accessors, instance variables and other sections of code. Some are just simple instance variables, but the majority can appear multiple times and hence are stored in an Array.

[
  :file_id,
  :file_creator,
  :events,
  :device_infos,
  :data_sources,
  :epo_data,
  :user_profiles,
  :user_data,
  :sensor_settings,
  :developer_data_ids,
  :field_descriptions,
  :records,
  :hrv,
  :laps,
  :lengths,
  :heart_rate_zones,
  :physiological_metrics,
  :sessions,
  :personal_records
]

Constants inherited from FitDataRecord

FitDataRecord::RecordOrder

Constants included from BDFieldNameTranslator

BDFieldNameTranslator::BD_DICT

Instance Attribute Summary

Attributes inherited from FitDataRecord

#message, #timestamp

Instance Method Summary collapse

Methods inherited from FitDataRecord

#<=>, #get, #get_as, #get_unit_by_name, #set, #set_field_values

Methods included from BDFieldNameTranslator

#to_bd_field_name

Methods included from Converters

#conversion_factor, #fit_time_to_time, #secsToDHMS, #secsToHM, #secsToHMS, #speedToPace, #time_to_fit_time

Constructor Details

#initialize(field_values = {}) ⇒ Activity

Create a new Activity object.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/fit4ruby/Activity.rb', line 75

def initialize(field_values = {})
  super('activity')

  # The variables hold references to other parts of the FIT file. These
  # can either be direct references to a certain FIT file section or an
  # Array in case the section can appear multiple times in the FIT file.
  @file_id = new_file_id()
  @file_creator = new_file_creator()
  @epo_data = nil
  # Initialize the remaining variables as empty Array.
  FILE_SECTIONS.each do |fs|
    ivar_name = '@' + fs.to_s
    unless instance_variable_defined?(ivar_name)
      instance_variable_set(ivar_name, [])
    end
  end

  # The following variables hold derived or auxilliary information that
  # are not directly part of the FIT file.
  @meta_field_units['total_gps_distance'] = 'm'
  @cur_session_laps = []

  @cur_lap_records = []
  @cur_lap_lengths = []

  @cur_length_records = []

  @lap_counter = 1
  @length_counter = 1

  set_field_values(field_values)
end

Instance Method Details

#==(a) ⇒ TrueClass/FalseClass

Check if the current Activity is equal to the passed Activity. otherwise false.

Parameters:

  • a (Activity)

    Activity to compare this Activity with.

Returns:

  • (TrueClass/FalseClass)

    true if both Activities are equal,



485
486
487
488
489
490
491
492
493
494
495
496
497
# File 'lib/fit4ruby/Activity.rb', line 485

def ==(a)
  return false unless super(a)

  FILE_SECTIONS.each do |fs|
    ivar_name = '@' + fs.to_s
    ivar = instance_variable_get(ivar_name)
    a_ivar = a.instance_variable_get(ivar_name)

    return false unless ivar == a_ivar
  end

  true
end

#aggregateObject

Call this method to update the aggregated data fields stored in Lap, Length, and Session objects.



242
243
244
245
246
# File 'lib/fit4ruby/Activity.rb', line 242

def aggregate
  @laps.each { |l| l.aggregate }
  @lengths.each { |l| l.aggregate }
  @sessions.each { |s| s.aggregate }
end

#avg_speedObject

Convenience method that averages the speed over all sessions.



249
250
251
252
253
254
255
256
257
# File 'lib/fit4ruby/Activity.rb', line 249

def avg_speed
  speed = 0.0
  @sessions.each do |s|
    if (spd = s.avg_speed || s.enhanced_avg_speed)
      speed += spd
    end
  end
  speed / @sessions.length
end

#checkObject

Perform some basic logical checks on the object and all references sub objects. Any errors will be reported via the Log object.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/fit4ruby/Activity.rb', line 110

def check
  unless @timestamp && @timestamp >= Time.parse('1990-01-01T00:00:00+00:00')
    Log.fatal "Activity has no valid timestamp"
  end
  unless @total_timer_time
    Log.fatal "Activity has no valid total_timer_time"
  end
  unless @device_infos.length > 0
    Log.fatal "Activity must have at least one device_info section"
  end
  @device_infos.each.with_index { |d, index| d.check(index) }
  @sensor_settings.each.with_index { |s, index| s.check(index) }

  # Records must have consecutively growing timestamps and distances.
  ts = Time.parse('1989-12-31')
  distance = nil
  invalid_records = []
  @records.each_with_index do |r, idx|
    Log.fatal "Record has no timestamp" unless r.timestamp
    if r.timestamp < ts
      Log.fatal "Record has earlier timestamp than previous record"
    end
    if r.distance
      if distance && r.distance < distance
        # Normally this should be a fatal error as the FIT file is clearly
        # broken. Unfortunately, the Skiing/Boarding app in the Fenix3
        # produces such broken FIT files. So we just warn about this
        # problem and discard the earlier records.
        Log.error "Record #{r.timestamp} has smaller distance " +
                  "(#{r.distance}) than an earlier record (#{distance})."
        # Index of the list record to be discarded.
        (idx - 1).downto(0) do |i|
          if (ri = @records[i]).distance > r.distance
            # This is just an approximation. It looks like the app adds
            # records to the FIT file for runs that it meant to discard.
            # Maybe the two successive time start events are a better
            # criteria. But this workaround works for now.
            invalid_records << ri
          else
            # All broken records have been found.
            break
          end
        end
      end
      distance = r.distance
    end
    ts = r.timestamp
  end
  unless invalid_records.empty?
    # Delete all the broken records from the @records Array.
    Log.warn "Discarding #{invalid_records.length} earlier records"
    @records.delete_if { |r| invalid_records.include?(r) }
  end

  # Laps must have a consecutively growing message index.
  @laps.each.with_index do |lap, index|
    lap.check(index, self)
    # If we have heart rate zone records, there should be one for each
    # lap
    @heart_rate_zones[index].check(index) if @heart_rate_zones[index]
  end

  # Lengths must have a consecutively growing message index.
  @lengths.each.with_index do |length, index|
    length.check(index)
    # If we have heart rate zone records, there should be one for each
    # length
    @heart_rate_zones[index].check(index) if @heart_rate_zones[index]
  end

  @sessions.each { |s| s.check(self) }
end

#ending_hrObject

Return the heart rate when the activity recording was last stopped.



260
261
262
# File 'lib/fit4ruby/Activity.rb', line 260

def ending_hr
  @records.empty? ? nil : @records[-1].heart_rate
end

#exportObject



567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
# File 'lib/fit4ruby/Activity.rb', line 567

def export
  # Collect all records in a consistent order.
  records = []
  FILE_SECTIONS.each do |fs|
    ivar_name = '@' + fs.to_s
    ivar = instance_variable_get(ivar_name)

    next unless ivar

    if ivar.respond_to?(:sort) and ivar.respond_to?(:empty?)
      records += ivar.sort unless ivar.empty?
    else
      records << ivar if ivar
    end
  end

  records.map do |record|
    record.export
  end
end

#new_data_sources(field_values = {}) ⇒ SourceData

Add a new SourceData to the Activity.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:

  • (SourceData)


388
389
390
# File 'lib/fit4ruby/Activity.rb', line 388

def new_data_sources(field_values = {})
  new_fit_data_record('data_sources', field_values)
end

#new_developer_data_id(field_values = {}) ⇒ DeveloperDataId

Add a new DeveloperDataId to the Activity.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



363
364
365
# File 'lib/fit4ruby/Activity.rb', line 363

def new_developer_data_id(field_values = {})
  new_fit_data_record('developer_data_id', field_values)
end

#new_device_info(field_values = {}) ⇒ DeviceInfo

Add a new DeviceInfo to the Activity.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



380
381
382
# File 'lib/fit4ruby/Activity.rb', line 380

def new_device_info(field_values = {})
  new_fit_data_record('device_info', field_values)
end

#new_event(field_values = {}) ⇒ Event

Add a new Event to the Activity.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



420
421
422
# File 'lib/fit4ruby/Activity.rb', line 420

def new_event(field_values = {})
  new_fit_data_record('event', field_values)
end

#new_field_description(field_values = {}) ⇒ FieldDescription

Add a new FieldDescription to the Activity.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



355
356
357
# File 'lib/fit4ruby/Activity.rb', line 355

def new_field_description(field_values = {})
  new_fit_data_record('field_description', field_values)
end

#new_file_creator(field_values = {}) ⇒ FileCreator

Add a new FileCreator to the Activity. It will replace any previously added FileCreator object.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



372
373
374
# File 'lib/fit4ruby/Activity.rb', line 372

def new_file_creator(field_values = {})
  new_fit_data_record('file_creator', field_values)
end

#new_file_id(field_values = {}) ⇒ FileId

Add a new FileId to the Activity. It will replace any previously added FileId object.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



347
348
349
# File 'lib/fit4ruby/Activity.rb', line 347

def new_file_id(field_values = {})
  new_fit_data_record('file_id', field_values)
end

#new_fit_data_record(record_type, field_values = {}) ⇒ Object

Create a new FitDataRecord.

Parameters:

  • record_type (String)

    Type that identifies the FitDataRecord derived class to create.

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:

  • FitDataRecord



505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# File 'lib/fit4ruby/Activity.rb', line 505

def new_fit_data_record(record_type, field_values = {})
  case record_type
  when 'file_id'
    @file_id = (record = FileId.new(field_values))
  when 'field_description'
    @field_descriptions << (record = FieldDescription.new(field_values))
  when 'developer_data_id'
    @developer_data_ids << (record = DeveloperDataId.new(field_values))
  when 'epo_data'
    @epo_data = (record = EPO_Data.new(field_values))
  when 'file_creator'
    @file_creator = (record = FileCreator.new(field_values))
  when 'device_info'
    @device_infos << (record = DeviceInfo.new(field_values))
  when 'sensor_settings'
    @sensor_settings << (record = SensorSettings.new(field_values))
  when 'data_sources'
    @data_sources << (record = DataSources.new(field_values))
  when 'user_data'
    @user_data << (record = UserData.new(field_values))
  when 'user_profile'
    @user_profiles << (record = UserProfile.new(field_values))
  when 'physiological_metrics'
    @physiological_metrics <<
      (record = PhysiologicalMetrics.new(field_values))
  when 'event'
    @events << (record = Event.new(field_values))
  when 'session'
    unless @cur_lap_records.empty?
      # Copy selected fields from section to lap.
      lap_field_values = {}
      [ :timestamp, :sport ].each do |f|
        lap_field_values[f] = field_values[f] if field_values.include?(f)
      end
      # Ensure that all previous records have been assigned to a lap.
      record = create_new_lap(lap_field_values)
    end
    @sessions << (record = Session.new(@cur_session_laps, @lap_counter,
                                       field_values))
    @cur_session_laps = []
  when 'lap'
    record = create_new_lap(field_values)
  when 'length'
    record = create_new_length(field_values)
  when 'record'
    record = Record.new(self, field_values)
    @cur_lap_records << record
    @cur_length_records << record
    @records << record
  when 'hrv'
    @hrv << (record = HRV.new(field_values))
  when 'heart_rate_zones'
    @heart_rate_zones << (record = HeartRateZones.new(field_values))
  when 'personal_records'
    @personal_records << (record = PersonalRecords.new(field_values))
  else
    record = nil
  end

  record
end

#new_heart_rate_zones(field_values = {}) ⇒ HeartRateZones

Add a new HeartRateZones record to the Activity.

Parameters:

  • field_values (Heash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



461
462
463
# File 'lib/fit4ruby/Activity.rb', line 461

def new_heart_rate_zones(field_values = {})
  new_fit_data_record('heart_rate_zones', field_values)
end

#new_lap(field_values = {}) ⇒ Lap

Add a new Lap to the Activity. All previoulsy added Record objects are associated with this Lap unless they have been associated with another Lap before.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



443
444
445
# File 'lib/fit4ruby/Activity.rb', line 443

def new_lap(field_values = {})
  new_fit_data_record('lap', field_values)
end

#new_length(field_values = {}) ⇒ Length

Add a new Length to the Activity. All previoulsy added Record objects are associated with this Length unless they have been associated with another Length before.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



453
454
455
# File 'lib/fit4ruby/Activity.rb', line 453

def new_length(field_values = {})
  new_fit_data_record('length', field_values)
end

#new_personal_record(field_values = {}) ⇒ PersonalRecord

Add a new PersonalRecord to the Activity.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:

  • (PersonalRecord)


469
470
471
# File 'lib/fit4ruby/Activity.rb', line 469

def new_personal_record(field_values = {})
  new_fit_data_record('personal_record', field_values)
end

#new_physiological_metrics(field_values = {}) ⇒ PhysiologicalMetrics

Add a new PhysiologicalMetrics to the Activity.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



412
413
414
# File 'lib/fit4ruby/Activity.rb', line 412

def new_physiological_metrics(field_values = {})
  new_fit_data_record('physiological_metrics', field_values)
end

#new_record(field_values = {}) ⇒ Record

Add a new Record to the Activity.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



477
478
479
# File 'lib/fit4ruby/Activity.rb', line 477

def new_record(field_values = {})
  new_fit_data_record('record', field_values)
end

#new_session(field_values = {}) ⇒ Session

Add a new Session to the Activity. All previously added Lap objects are associated with this Session unless they have been associated with another Session before. If there are any Record objects that have not yet been associated with a Lap, a new lap will be created and the Record objects will be associated with this Lap. The Lap will be associated with the newly created Session.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



433
434
435
# File 'lib/fit4ruby/Activity.rb', line 433

def new_session(field_values = {})
  new_fit_data_record('session', field_values)
end

#new_user_data(field_values = {}) ⇒ UserData

Add a new UserData to the Activity.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



396
397
398
# File 'lib/fit4ruby/Activity.rb', line 396

def new_user_data(field_values = {})
  new_fit_data_record('user_data', field_values)
end

#new_user_profile(field_values = {}) ⇒ UserProfile

Add a new UserProfile to the Activity.

Parameters:

  • field_values (Hash) (defaults to: {})

    A Hash that provides initial values for certain fields of the FitDataRecord.

Returns:



404
405
406
# File 'lib/fit4ruby/Activity.rb', line 404

def (field_values = {})
  new_fit_data_record('user_profile', field_values)
end

#recovery_hrObject

Return the measured recovery heart rate.



265
266
267
268
269
270
271
# File 'lib/fit4ruby/Activity.rb', line 265

def recovery_hr
  @events.each do |e|
    return e.recovery_hr if e.event == 'recovery_hr'
  end

  nil
end

#recovery_infoObject

Returns the remaining recovery time at the start of the activity.

Returns:

  • remaining recovery time in seconds.



275
276
277
278
279
280
281
# File 'lib/fit4ruby/Activity.rb', line 275

def recovery_info
  @events.each do |e|
    return e.recovery_info if e.event == 'recovery_info'
  end

  nil
end

#recovery_timeObject

Returns the predicted recovery time needed after this activity.

Returns:

  • recovery time in seconds.



285
286
287
288
289
290
291
# File 'lib/fit4ruby/Activity.rb', line 285

def recovery_time
  @events.each do |e|
    return e.recovery_time if e.event == 'recovery_time'
  end

  nil
end

#sportObject

Returns the sport type of this activity.



310
311
312
# File 'lib/fit4ruby/Activity.rb', line 310

def sport
  @sessions[0].sport
end

#sub_sportObject

Returns the sport subtype of this activity.



315
316
317
# File 'lib/fit4ruby/Activity.rb', line 315

def sub_sport
  @sessions[0].sub_sport
end

#total_distanceObject

Convenience method that aggregates all the distances from the included sessions.



185
186
187
188
189
# File 'lib/fit4ruby/Activity.rb', line 185

def total_distance
  d = 0.0
  @sessions.each { |s| d += s.total_distance }
  d
end

#total_gps_distanceObject

Total distance convered by this activity purely computed by the GPS coordinates. This may differ from the distance computed by the device as it can be based on a purely calibrated footpod.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/fit4ruby/Activity.rb', line 194

def total_gps_distance
  timer_stops = []
  # Generate a list of all timestamps where the timer was stopped.
  @events.each do |e|
    if e.event == 'timer' && e.event_type == 'stop_all'
      timer_stops << e.timestamp
    end
  end

  # The first record of a FIT file can already have a distance associated
  # with it. The GPS location of the first record is not where the start
  # button was pressed. This introduces a slight inaccurcy when computing
  # the total distance purely on the GPS coordinates found in the records.
  d = 0.0
  last_lat = last_long = nil
  last_timestamp = nil

  # Iterate over all the records and accumlate the distances between the
  # neiboring coordinates.
  @records.each do |r|
    if (lat = r.position_lat) && (long = r.position_long)
      if last_lat && last_long
        distance = Fit4Ruby::GeoMath.distance(last_lat, last_long,
                                              lat, long)
        d += distance
      end
      if last_timestamp
        speed = distance / (r.timestamp - last_timestamp)
      end
      if timer_stops[0] == r.timestamp
        # If a stop event was found for this record timestamp we clear the
        # last_* values so that the distance covered while being stopped
        # is not added to the total.
        last_lat = last_long = nil
        last_timestamp = nil
        timer_stops.shift
      else
        last_lat = lat
        last_long = long
        last_timestamp = r.timestamp
      end
    end
  end
  d
end

#vo2maxObject

Returns the computed VO2max value. This value is computed by the device based on multiple previous activities.



295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/fit4ruby/Activity.rb', line 295

def vo2max
  # First check the event log for a vo2max reporting event.
  @events.each do |e|
    return e.vo2max if e.event == 'vo2max'
  end
  # Then check the user_data entries for a metmax entry. METmax * 3.5
  # is same value as VO2max.
  @user_data.each do |u|
    return u.metmax * 3.5 if u.metmax
  end

  nil
end

#write(io, id_mapper) ⇒ Object

Write the Activity data to a file.

Parameters:

  • io (IO)

    File reference

  • id_mapper (FitMessageIdMapper)

    Maps global FIT record types to local ones.



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/fit4ruby/Activity.rb', line 323

def write(io, id_mapper)
  @file_id.write(io, id_mapper)
  @file_creator.write(io, id_mapper)
  @epo_data.write(io, id_mapper) if @epo_data

  ary_ivars = []
  FILE_SECTIONS.each do |fs|
    ivar_name = '@' + fs.to_s
    if (ivar = instance_variable_get(ivar_name)) && ivar.respond_to?(:sort)
      ary_ivars += ivar
    end
  end

  ary_ivars.sort.each do |s|
    s.write(io, id_mapper)
  end
  super
end