class Fit4Ruby::Activity

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.

Constants

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.

Public Class Methods

new(field_values = {}) click to toggle source

Create a new Activity object. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.
Calls superclass method
# File lib/fit4ruby/Activity.rb, line 79
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

Public Instance Methods

==(a) click to toggle source

Check if the current Activity is equal to the passed Activity. @param a [Activity] Activity to compare this Activity with. @return [TrueClass/FalseClass] true if both Activities are equal, otherwise false.

Calls superclass method
# File lib/fit4ruby/Activity.rb, line 515
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
aggregate() click to toggle source

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

# File lib/fit4ruby/Activity.rb, line 246
def aggregate
  @laps.each { |l| l.aggregate }
  @lengths.each { |l| l.aggregate }
  @sessions.each { |s| s.aggregate }
end
avg_speed() click to toggle source

Convenience method that averages the speed over all sessions.

# File lib/fit4ruby/Activity.rb, line 253
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
check() click to toggle source

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

# File lib/fit4ruby/Activity.rb, line 114
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_hr() click to toggle source

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

# File lib/fit4ruby/Activity.rb, line 264
def ending_hr
  @records.empty? ? nil : @records[-1].heart_rate
end
export() click to toggle source
# File lib/fit4ruby/Activity.rb, line 601
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 = {}) click to toggle source

Add a new SourceData to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [SourceData]

# File lib/fit4ruby/Activity.rb, line 402
def new_data_sources(field_values = {})
  new_fit_data_record('data_sources', field_values)
end
new_developer_data_id(field_values = {}) click to toggle source

Add a new DeveloperDataId to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [DeveloperDataId]

# File lib/fit4ruby/Activity.rb, line 377
def new_developer_data_id(field_values = {})
  new_fit_data_record('developer_data_id', field_values)
end
new_device_info(field_values = {}) click to toggle source

Add a new DeviceInfo to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [DeviceInfo]

# File lib/fit4ruby/Activity.rb, line 394
def new_device_info(field_values = {})
  new_fit_data_record('device_info', field_values)
end
new_event(field_values = {}) click to toggle source

Add a new Event to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [Event]

# File lib/fit4ruby/Activity.rb, line 450
def new_event(field_values = {})
  new_fit_data_record('event', field_values)
end
new_field_description(field_values = {}) click to toggle source

Add a new FieldDescription to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [FieldDescription]

# File lib/fit4ruby/Activity.rb, line 369
def new_field_description(field_values = {})
  new_fit_data_record('field_description', field_values)
end
new_file_creator(field_values = {}) click to toggle source

Add a new FileCreator to the Activity. It will replace any previously added FileCreator object. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [FileCreator]

# File lib/fit4ruby/Activity.rb, line 386
def new_file_creator(field_values = {})
  new_fit_data_record('file_creator', field_values)
end
new_file_id(field_values = {}) click to toggle source

Add a new FileId to the Activity. It will replace any previously added FileId object. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [FileId]

# File lib/fit4ruby/Activity.rb, line 361
def new_file_id(field_values = {})
  new_fit_data_record('file_id', field_values)
end
new_fit_data_record(record_type, field_values = {}) click to toggle source

Create a new FitDataRecord. @param record_type [String] Type that identifies the FitDataRecord

derived class to create.

@param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return FitDataRecord

# File lib/fit4ruby/Activity.rb, line 535
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 'workout'
    @workouts << (record = Workout.new(field_values))
  when 'workout_step'
    @workout_steps << (record = WorkoutStep.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 = {}) click to toggle source

Add a new HeartRateZones record to the Activity. @param field_values [Heash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [HeartRateZones]

# File lib/fit4ruby/Activity.rb, line 491
def new_heart_rate_zones(field_values = {})
  new_fit_data_record('heart_rate_zones', field_values)
end
new_lap(field_values = {}) click to toggle source

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. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [Lap]

# File lib/fit4ruby/Activity.rb, line 473
def new_lap(field_values = {})
  new_fit_data_record('lap', field_values)
end
new_length(field_values = {}) click to toggle source

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. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [Length]

# File lib/fit4ruby/Activity.rb, line 483
def new_length(field_values = {})
  new_fit_data_record('length', field_values)
end
new_personal_record(field_values = {}) click to toggle source

Add a new PersonalRecord to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [PersonalRecord]

# File lib/fit4ruby/Activity.rb, line 499
def new_personal_record(field_values = {})
  new_fit_data_record('personal_record', field_values)
end
new_physiological_metrics(field_values = {}) click to toggle source

Add a new PhysiologicalMetrics to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [PhysiologicalMetrics]

# File lib/fit4ruby/Activity.rb, line 442
def new_physiological_metrics(field_values = {})
  new_fit_data_record('physiological_metrics', field_values)
end
new_record(field_values = {}) click to toggle source

Add a new Record to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [Record]

# File lib/fit4ruby/Activity.rb, line 507
def new_record(field_values = {})
  new_fit_data_record('record', field_values)
end
new_session(field_values = {}) click to toggle source

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. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [Session]

# File lib/fit4ruby/Activity.rb, line 463
def new_session(field_values = {})
  new_fit_data_record('session', field_values)
end
new_user_data(field_values = {}) click to toggle source

Add a new UserData to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [UserData]

# File lib/fit4ruby/Activity.rb, line 410
def new_user_data(field_values = {})
  new_fit_data_record('user_data', field_values)
end
new_user_profile(field_values = {}) click to toggle source

Add a new UserProfile to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [UserProfile]

# File lib/fit4ruby/Activity.rb, line 418
def new_user_profile(field_values = {})
  new_fit_data_record('user_profile', field_values)
end
new_workout(field_values = {}) click to toggle source

Add a new Workout to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [UserProfile]

# File lib/fit4ruby/Activity.rb, line 426
def new_workout(field_values = {})
  new_fit_data_record('workout', field_values)
end
new_workout_set(field_values = {}) click to toggle source

Add a new WorkoutSet to the Activity. @param field_values [Hash] A Hash that provides initial values for

certain fields of the FitDataRecord.

@return [UserProfile]

# File lib/fit4ruby/Activity.rb, line 434
def new_workout_set(field_values = {})
  new_fit_data_record('workout_set', field_values)
end
recovery_hr() click to toggle source

Return the measured recovery heart rate.

# File lib/fit4ruby/Activity.rb, line 269
def recovery_hr
  @events.each do |e|
    return e.recovery_hr if e.event == 'recovery_hr'
  end

  nil
end
recovery_info() click to toggle source

Returns the remaining recovery time at the start of the activity. @return remaining recovery time in seconds.

# File lib/fit4ruby/Activity.rb, line 279
def recovery_info
  @events.each do |e|
    return e.recovery_info if e.event == 'recovery_info'
  end

  nil
end
recovery_time() click to toggle source

Returns the predicted recovery time needed after this activity. @return recovery time in seconds.

# File lib/fit4ruby/Activity.rb, line 289
def recovery_time
  @events.each do |e|
    return e.recovery_time if e.event == 'recovery_time'
  end

  nil
end
sport() click to toggle source

Returns the sport type of this activity.

# File lib/fit4ruby/Activity.rb, line 314
def sport
  @sessions[0].sport
end
sport=(sport) click to toggle source

Sets the sport type of this activity.

# File lib/fit4ruby/Activity.rb, line 319
def sport=(sport)
  @sessions[0].sport = sport
end
sub_sport() click to toggle source

Returns the sport subtype of this activity.

# File lib/fit4ruby/Activity.rb, line 324
def sub_sport
  @sessions[0].sub_sport
end
sub_sport=(sub_sport) click to toggle source

Sets the sport subtype of this activity.

# File lib/fit4ruby/Activity.rb, line 329
def sub_sport=(sub_sport)
  @sessions[0].sub_sport = sub_sport
end
total_distance() click to toggle source

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

# File lib/fit4ruby/Activity.rb, line 189
def total_distance
  d = 0.0
  @sessions.each { |s| d += (s.total_distance || 0) }
  d
end
total_gps_distance() click to toggle source

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.

# File lib/fit4ruby/Activity.rb, line 198
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
vo2max() click to toggle source

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

# File lib/fit4ruby/Activity.rb, line 299
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) click to toggle source

Write the Activity data to a file. @param io [IO] File reference @param id_mapper [FitMessageIdMapper] Maps global FIT record types to

local ones.
Calls superclass method
# File lib/fit4ruby/Activity.rb, line 337
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

Private Instance Methods

create_new_lap(field_values) click to toggle source
# File lib/fit4ruby/Activity.rb, line 624
def create_new_lap(field_values)
  lap = Lap.new(self, @cur_lap_records, @laps.last,
                field_values,
                @length_counter, @cur_lap_lengths)
  lap.message_index = @lap_counter - 1
  @lap_counter += 1
  @cur_session_laps << lap
  @laps << lap
  @cur_lap_records = []
  @cur_lap_lengths = []

  lap
end
create_new_length(field_values) click to toggle source
# File lib/fit4ruby/Activity.rb, line 638
def create_new_length(field_values)
  length = Length.new(@cur_length_records, @lengths.last, field_values)
  length.message_index = @length_counter - 1
  @length_counter += 1
  @cur_lap_lengths << length
  @lengths << length
  @cur_length_records = []

  length
end