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',
  'training' => 'Training',
  'flying' => 'Flying',
  'e_biking' => 'E-Biking',
  'motorcycling' => 'Motor Cycling',
  'boating' => 'Boating',
  'driving' => 'Driving',
  'golf' => 'Golf',
  'hang_gliding' => 'Hang Gliding',
  'horseback_riding' => 'Horseback Riding',
  'hunting' => 'Hunting',
  'fishing' => 'Fishing',
  'inline_skating' => 'Inline Skating',
  'rock_climbing' => 'Rock Climbing',
  'sailing' => 'Sailing',
  'ice_skating' => 'Ice Skating',
  'sky_diving' => 'Sky Diving',
  'snowshoeing' => 'Snowshoeing',
  'snowmobiling' => 'Snowmobiling',
  'stand_up_paddleboarding' => 'Stand Up Paddleboarding',
  'surfing' => 'Surfing',
  'wakeboarding' => 'Wakeboarding',
  'water_skiing' => 'Water Skiing',
  'kayaking' => 'Kayaking',
  'rafting' => 'Rafting',
  'windsurfing' => 'Windsurfing',
  'kitesurfing' => 'Kitesurfing',
  'tactical' => 'Tactical',
  'jumpmaster' => 'Jumpmaster',
  'boxing' => 'Boxing',
  'floor_climbing' => 'Floor Climbing',
  'diving' => 'Diving',
  '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',
  'e_bike_fitness' => 'E Bike Fittness',
  'bmx' => 'BMX',
  'casual_walking' => 'Casual Walking',
  'speed_walking' => 'Speed Walking',
  'bike_to_run_transition' => 'Bike to Run Transition',
  'run_to_bike_transition' => 'Run to Bike Transition',
  'swim_to_bike_transition' => 'Swim to Bike Transition',
  'atv' => 'ATV',
  'motocross' => 'Motocross',
  'backcountry' => 'Backcountry',
  'resort' => 'Resort',
  'rc_drone' => 'RC Drone',
  'wingsuit' => 'Wingsuite',
  'whitewater' => 'Whitemaster',
  'skate_skiing' => 'Skate Skiing',
  'yoga' => 'Yoga',
  'pilates' => 'Pilates',
  'indoor_running' => 'Indoor Running',
  'gravel_cycling' => 'Gravel Cycling',
  'e_bike_mountain' => 'E-Bike Mountain',
  'commuting' => 'Commuting',
  'mixed_surface' => 'Mixed Surface',
  'navigate' => 'Navigate',
  'track_me' => 'Track Me',
  'map' => 'Map',
  'single_gas_diving' => 'Single Gas Diving',
  'multi_gas_diving' => 'Multi Gas Diving',
  'gauge_diving' => 'Gauge Diving',
  'apnea_diving' => 'Apnea Diving',
  'apnea_hunting' => 'Apnea Hunting',
  'virtual_activity' => 'Virtual Activity',
  'obstacle' => 'Obstacle',
  'breathing' => 'Breating',
  'sail_race' => 'Sail Race',
  'ultra' => 'Ultra',
  'indoor_climbing' => 'Indoor Climbing',
  'bouldering' => 'Bouldering',
  '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.



169
170
171
172
173
174
175
176
177
178
179
180
181
# 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

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



449
450
451
# File 'lib/postrunner/Activity.rb', line 449

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

#activity_typeObject



445
446
447
# File 'lib/postrunner/Activity.rb', line 445

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

#checkObject



200
201
202
203
204
# 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) ⇒ Object



453
454
455
456
457
458
459
460
461
462
463
464
465
466
# 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) ⇒ Object



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

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.



239
240
241
242
243
244
245
246
247
# 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

#eventsObject



268
269
270
271
# 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_viewObject



440
441
442
443
# 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?Boolean

Return true if this activity generated any personal records.

Returns:

  • (Boolean)


436
437
438
# File 'lib/postrunner/Activity.rb', line 436

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



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# 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) ⇒ 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().



186
187
188
189
190
191
192
193
194
195
196
197
198
# 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) ⇒ Object



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# 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_recordsObject



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
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# 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) ⇒ Object



292
293
294
295
# File 'lib/postrunner/Activity.rb', line 292

def rename(name)
  @name = name
  generate_html_view
end

#set(attribute, value) ⇒ Object



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
# 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

#showObject



273
274
275
276
277
# File 'lib/postrunner/Activity.rb', line 273

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

  @db.show_in_browser(@html_file)
end

#sourcesObject



279
280
281
282
# 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

#summaryObject



284
285
286
287
288
289
290
# 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