Class: PostRunner::Activity

Inherits:
Object
  • Object
show all
Defined in:
lib/postrunner/Activity.rb

Constant Summary collapse

ActivityTypes =
{
  'generic' => 'Generic',
  'running' => 'Running',
  'cycling' => 'Cycling',
  'transition' => 'Transition',
  'fitness_equipment' => 'Fitness Equipment',
  'swimming' => 'Swimming',
  'basketball' => 'Basketball',
  'soccer' => 'Soccer',
  'tennis' => 'Tennis',
  'american_football' => 'American Football',
  'walking' => 'Walking',
  'cross_country_skiing' => 'Cross Country Skiing',
  'alpine_skiing' => 'Alpine Skiing',
  'snowboarding' => 'Snowboarding',
  'rowing' => 'Rowing',
  'mountaineering' => 'Mountaneering',
  'hiking' => 'Hiking',
  'multisport' => 'Multisport',
  'paddling' => 'Paddling',
  'all' => 'All'
}
ActivitySubTypes =
{
  'generic' => 'Generic',
  'treadmill' => 'Treadmill',
  'street' => 'Street',
  'trail' => 'Trail',
  'track' => 'Track',
  'spin' => 'Spin',
  'indoor_cycling' => 'Indoor Cycling',
  'road' => 'Road',
  'mountain' => 'Mountain',
  'downhill' => 'Downhill',
  'recumbent' => 'Recumbent',
  'cyclocross' => 'Cyclocross',
  'hand_cycling' => 'Hand Cycling',
  'track_cycling' => 'Track Cycling',
  'indoor_rowing' => 'Indoor Rowing',
  'elliptical' => 'Elliptical',
  'stair_climbing' => 'Stair Climbing',
  'lap_swimming' => 'Lap Swimming',
  'open_water' => 'Open Water',
  'flexibility_training' => 'Flexibility Training',
  'strength_training' => 'Strength Training',
  'warm_up' => 'Warm up',
  'match' => 'Match',
  'exercise' => 'Excersize',
  'challenge' => 'Challenge',
  'indoor_skiing' => 'Indoor Skiing',
  'cardio_training' => 'Cardio Training',
  'virtual_activity' => 'Virtual Activity',
  'all' => 'All'
}
@@CachedActivityValues =

This is a list of variables that provide data from the fit file. To speed up access to it, we cache the data in the activity database.

%w( sport sub_sport timestamp total_distance
total_timer_time avg_speed )
@@CachedAttributes =

We also store some additional information in the archive index.

@@CachedActivityValues + %w( fit_file name norecord )
@@Schemata =
{
  'long_date' => Schema.new('long_date', 'Date',
                            { :func => 'timestamp',
                              :column_alignment => :left,
                              :format => 'date_with_weekday' }),
  'sub_type' => Schema.new('sub_type', 'Subtype',
                           { :func => 'activity_sub_type',
                             :column_alignment => :left }),
  'type' => Schema.new('type', 'Type',
                       { :func => 'activity_type',
                         :column_alignment => :left })
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(db, fit_file, fit_activity, name = nil) ⇒ Activity

Returns a new instance of Activity.



102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/postrunner/Activity.rb', line 102

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

Instance Attribute Details

#dbObject (readonly)

Returns the value of attribute db.



26
27
28
# File 'lib/postrunner/Activity.rb', line 26

def db
  @db
end

#fit_activityObject (readonly)

Returns the value of attribute fit_activity.



26
27
28
# File 'lib/postrunner/Activity.rb', line 26

def fit_activity
  @fit_activity
end

#fit_fileObject (readonly)

Returns the value of attribute fit_file.



26
27
28
# File 'lib/postrunner/Activity.rb', line 26

def fit_file
  @fit_file
end

#nameObject (readonly)

Returns the value of attribute name.



26
27
28
# File 'lib/postrunner/Activity.rb', line 26

def name
  @name
end

Instance Method Details

#activity_sub_typeObject



382
383
384
# File 'lib/postrunner/Activity.rb', line 382

def activity_sub_type
  ActivitySubTypes[@sub_sport] || 'Undefined "#{@sub_sport}"'
end

#activity_typeObject



378
379
380
# File 'lib/postrunner/Activity.rb', line 378

def activity_type
  ActivityTypes[@sport] || 'Undefined'
end

#checkObject



133
134
135
136
137
# File 'lib/postrunner/Activity.rb', line 133

def check
  generate_html_view
  register_records
  Log.info "FIT file #{@fit_file} is OK"
end

#distance(timestamp, unit_system) ⇒ Object



386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/postrunner/Activity.rb', line 386

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) ⇒ Object



139
140
141
# File 'lib/postrunner/Activity.rb', line 139

def dump(filter)
  @fit_activity = load_fit_file(filter)
end

#encode_with(coder) ⇒ Object

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.



172
173
174
175
176
177
178
179
180
# File 'lib/postrunner/Activity.rb', line 172

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

#eventsObject



201
202
203
204
# File 'lib/postrunner/Activity.rb', line 201

def events
  @fit_activity = load_fit_file unless @fit_activity
  puts EventList.new(self, @db.cfg[:unit_system]).to_s
end

#generate_html_viewObject



373
374
375
376
# File 'lib/postrunner/Activity.rb', line 373

def generate_html_view
  @fit_activity = load_fit_file unless @fit_activity
  ActivityView.new(self, @db.cfg[:unit_system])
end

#has_records?Boolean

Return true if this activity generated any personal records.

Returns:

  • (Boolean)


369
370
371
# File 'lib/postrunner/Activity.rb', line 369

def has_records?
  !@db.records.activity_records(self).empty?
end

#init_with(coder) ⇒ Object

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().



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/postrunner/Activity.rb', line 146

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) ⇒ Object

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().



119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/postrunner/Activity.rb', line 119

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) ⇒ Object



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/postrunner/Activity.rb', line 182

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_recordsObject



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/postrunner/Activity.rb', line 263

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) ⇒ Object



225
226
227
228
# File 'lib/postrunner/Activity.rb', line 225

def rename(name)
  @name = name
  generate_html_view
end

#set(attribute, value) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/postrunner/Activity.rb', line 230

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

#showObject



206
207
208
209
210
# File 'lib/postrunner/Activity.rb', line 206

def show
  generate_html_view #unless File.exists?(@html_file)

  @db.show_in_browser(@html_file)
end

#sourcesObject



212
213
214
215
# File 'lib/postrunner/Activity.rb', line 212

def sources
  @fit_activity = load_fit_file unless @fit_activity
  puts DataSources.new(self, @db.cfg[:unit_system]).to_s
end

#summaryObject



217
218
219
220
221
222
223
# File 'lib/postrunner/Activity.rb', line 217

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