class PostRunner::Activity

Constants

ActivitySubTypes
ActivityTypes

Attributes

db[R]
fit_activity[R]
fit_file[R]
name[R]

Public Class Methods

new(db, fit_file, fit_activity, name = nil) click to toggle source
# File lib/postrunner/Activity.rb, line 169
def initialize(db, fit_file, fit_activity, name = nil)
  @fit_file = fit_file
  @fit_activity = fit_activity
  @name = name || fit_file
  @unset_variables = []
  late_init(db)

  @@CachedActivityValues.each do |v|
    v_str = "@#{v}"
    instance_variable_set(v_str, fit_activity.send(v))
    self.class.send(:attr_reader, v.to_sym)
  end
end

Public Instance Methods

activity_sub_type() click to toggle source
# File lib/postrunner/Activity.rb, line 449
def activity_sub_type
  ActivitySubTypes[@sub_sport] || "Undefined #{@sub_sport}"
end
activity_type() click to toggle source
# File lib/postrunner/Activity.rb, line 445
def activity_type
  ActivityTypes[@sport] || 'Undefined'
end
check() click to toggle source
# File lib/postrunner/Activity.rb, line 200
def check
  generate_html_view
  register_records
  Log.info "FIT file #{@fit_file} is OK"
end
distance(timestamp, unit_system) click to toggle source
# File lib/postrunner/Activity.rb, line 453
def distance(timestamp, unit_system)
  @fit_activity = load_fit_file unless @fit_activity

  @fit_activity.records.each do |record|
    if record.timestamp >= timestamp
      unit = { :metric => 'km', :statute => 'mi'}[unit_system]
      value = record.get_as('distance', unit)
      return '-' unless value
      return "#{'%.2f %s' % [value, unit]}"
    end
  end

  '-'
end
dump(filter) click to toggle source
# File lib/postrunner/Activity.rb, line 206
def dump(filter)
  @fit_activity = load_fit_file(filter)
end
encode_with(coder) click to toggle source

This method is called during Activity::to_yaml() calls. It’s being used to prevent some instance variables from being saved in the YAML file. Only attributes that are listed in @@CachedAttributes are being saved.

# File lib/postrunner/Activity.rb, line 239
def encode_with(coder)
  instance_variables.each do |a|
    name_with_at = a.to_s
    name_without_at = name_with_at[1..-1]
    next unless @@CachedAttributes.include?(name_without_at)

    coder[name_without_at] = instance_variable_get(name_with_at)
  end
end
events() click to toggle source
# File lib/postrunner/Activity.rb, line 268
def events
  @fit_activity = load_fit_file unless @fit_activity
  puts EventList.new(self, @db.cfg[:unit_system]).to_s
end
generate_html_view() click to toggle source
# File lib/postrunner/Activity.rb, line 440
def generate_html_view
  @fit_activity = load_fit_file unless @fit_activity
  ActivityView.new(self, @db.cfg[:unit_system])
end
has_records?() click to toggle source

Return true if this activity generated any personal records.

# File lib/postrunner/Activity.rb, line 436
def has_records?
  !@db.records.activity_records(self).empty?
end
init_with(coder) click to toggle source

This method is called during YAML::load() to initialize the class objects. The initialize() is NOT called during YAML::load(). Any additional initialization work is done in late_init().

# File lib/postrunner/Activity.rb, line 213
def init_with(coder)
  @unset_variables = []
  @@CachedAttributes.each do |name_without_at|
    # Create attr_readers for cached variables.
    self.class.send(:attr_reader, name_without_at.to_sym)

    if coder.map.include?(name_without_at)
      # The YAML file has a value for the instance variable. So just set
      # it.
      instance_variable_set('@' + name_without_at, coder[name_without_at])
    else
      if @@CachedActivityValues.include?(name_without_at)
        @unset_variables << name_without_at
      elsif name_without_at == 'norecord'
        @norecord = false
      else
        Log.fatal "Don't know how to initialize the instance variable " +
                  "#{name_without_at}."
      end
    end
  end
end
late_init(db) click to toggle source

YAML::load() does not call initialize(). We don’t have all attributes stored in the YAML file, so we need to make sure these are properly set after a YAML::load().

# File lib/postrunner/Activity.rb, line 186
def late_init(db)
  @db = db
  @html_file = File.join(@db.cfg[:html_dir], "#{@fit_file[0..-5]}.html")

  @unset_variables.each do |name_without_at|
    # The YAML file does not yet have the instance variable cached.
    # Load the Activity data and extract the value to set the instance
    # variable.
    @fit_activity = load_fit_file unless @fit_activity
    instance_variable_set('@' + name_without_at,
                          @fit_activity.send(name_without_at))
  end
end
query(key) click to toggle source
# File lib/postrunner/Activity.rb, line 249
def query(key)
  unless @@Schemata.include?(key)
    raise ArgumentError, "Unknown key '#{key}' requested in query"
  end

  schema = @@Schemata[key]

  if schema.func
    value = send(schema.func)
  else
    unless instance_variable_defined?(key)
      raise ArgumentError, "Don't know how to query '#{key}'"
    end
    value = instance_variable_get(key)
  end

  QueryResult.new(value, schema)
end
register_records() click to toggle source
# File lib/postrunner/Activity.rb, line 330
def register_records
  # If we have the @norecord flag set, we ignore this Activity for the
  # record collection.
  return if @norecord

  distance_record = 0.0
  distance_record_sport = nil
  # Array with popular distances (in meters) in ascending order.
  record_distances = nil
  # Speed records for popular distances (seconds hashed by distance in
  # meters)
  speed_records = {}

  segment_start_time = @fit_activity.sessions[0].start_time
  segment_start_distance = 0.0

  sport = nil
  last_timestamp = nil
  last_distance = nil

  @fit_activity.records.each do |record|
    if record.timestamp.nil?
      # All records must have a valid timestamp
      Log.warn "Found a record without a valid timestamp"
      return
    end
    if record.distance.nil?
      # All records must have a valid distance mark or the activity does
      # not qualify for a personal record.
      Log.warn "Found a record at #{record.timestamp} without a " +
               "valid distance"
      return
    end

    unless sport
      # If the Activity has sport set to 'multisport' or 'all' we pick up
      # the sport from the FIT records. Otherwise, we just use whatever
      # sport the Activity provides.
      if @sport == 'multisport' || @sport == 'all'
        sport = record.activity_type
      else
        sport = @sport
      end
      return unless PersonalRecords::SpeedRecordDistances.include?(sport)

      record_distances = PersonalRecords::SpeedRecordDistances[sport].
        keys.sort
    end

    segment_start_distance = record.distance unless segment_start_distance
    segment_start_time = record.timestamp unless segment_start_time

    # Total distance covered in this segment so far
    segment_distance = record.distance - segment_start_distance
    # Check if we have reached the next popular distance.
    if record_distances.first &&
       segment_distance >= record_distances.first
      segment_duration = record.timestamp - segment_start_time
      # The distance may be somewhat larger than a popular distance. We
      # normalize the time to the norm distance.
      norm_duration = segment_duration / segment_distance *
        record_distances.first
      # Save the time for this distance.
      speed_records[record_distances.first] = {
        :time => norm_duration, :sport => sport
      }
      # Switch to the next popular distance.
      record_distances.shift
    end

    # We've reached the end of a segment if the sport type changes, we
    # detect a pause of more than 30 seconds or when we've reached the
    # last record.
    if (record.activity_type && sport && record.activity_type != sport) ||
       (last_timestamp && (record.timestamp - last_timestamp) > 30) ||
       record.equal?(@fit_activity.records.last)

      # Check for a total distance record
      if segment_distance > distance_record
        distance_record = segment_distance
        distance_record_sport = sport
      end

      # Prepare for the next segment in this Activity
      segment_start_distance = nil
      segment_start_time = nil
      sport = nil
    end

    last_timestamp = record.timestamp
    last_distance = record.distance
  end

  # Store the found records
  start_time = @fit_activity.sessions[0].timestamp
  if distance_record_sport
    @db.records.register_result(self, distance_record_sport,
                                distance_record, nil, start_time)
  end
  speed_records.each do |dist, info|
    @db.records.register_result(self, info[:sport], dist, info[:time],
                                start_time)
  end
end
rename(name) click to toggle source
# File lib/postrunner/Activity.rb, line 292
def rename(name)
  @name = name
  generate_html_view
end
set(attribute, value) click to toggle source
# File lib/postrunner/Activity.rb, line 297
def set(attribute, value)
  case attribute
  when 'name'
    @name = value
  when 'type'
    @fit_activity = load_fit_file unless @fit_activity
    unless ActivityTypes.values.include?(value)
      Log.fatal "Unknown activity type '#{value}'. Must be one of " +
                ActivityTypes.values.join(', ')
    end
    @sport = ActivityTypes.invert[value]
    # Since the activity changes the records from this Activity need to be
    # removed and added again.
    @db.records.delete_activity(self)
    register_records
  when 'subtype'
    unless ActivitySubTypes.values.include?(value)
      Log.fatal "Unknown activity subtype '#{value}'. Must be one of " +
                ActivitySubTypes.values.join(', ')
    end
    @sub_sport = ActivitySubTypes.invert[value]
  when 'norecord'
    unless %w( true false).include?(value)
      Log.fatal "norecord must either be 'true' or 'false'"
    end
    @norecord = value == 'true'
  else
    Log.fatal "Unknown activity attribute '#{attribute}'. Must be one of " +
              'name, type or subtype'
  end
  generate_html_view
end
show() click to toggle source
# File lib/postrunner/Activity.rb, line 273
def show
  generate_html_view #unless File.exist?(@html_file)

  @db.show_in_browser(@html_file)
end
sources() click to toggle source
# File lib/postrunner/Activity.rb, line 279
def sources
  @fit_activity = load_fit_file unless @fit_activity
  puts DataSources.new(self, @db.cfg[:unit_system]).to_s
end
summary() click to toggle source
# File lib/postrunner/Activity.rb, line 284
def summary
  @fit_activity = load_fit_file unless @fit_activity
  puts ActivitySummary.new(self, @db.cfg[:unit_system],
                           { :name => @name,
                             :type => activity_type,
                             :sub_type => activity_sub_type }).to_s
end

Private Instance Methods

load_fit_file(filter = nil) click to toggle source
# File lib/postrunner/Activity.rb, line 470
def load_fit_file(filter = nil)
  fit_file = File.join(@db.fit_dir, @fit_file)
  begin
    fit_activity = Fit4Ruby.read(fit_file, filter)
  rescue Fit4Ruby::Error
    Log.fatal "#{@fit_file} corrupted: #{$!}"
  end

  unless fit_activity
    Log.fatal "#{fit_file} does not contain any activity records"
  end

  fit_activity
end