postrunner 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c193dbf151045522cfe8f47b729d244eb87f957
4
- data.tar.gz: fb92f5b286258cac2c17e96dae821d858face419
3
+ metadata.gz: dd23d401a204deb3a2bdb839097e78c7eafc9b17
4
+ data.tar.gz: a97ce94a450b07cb1744d6b65ee42f463144ae2a
5
5
  SHA512:
6
- metadata.gz: 2a01b57478175e684b1253e30174e46ee5b3119dd8a2941a8cf6141255e7955c6628fff6a20167c3421feaee0ca7c857370ae8c05a4d7ae114fb6730df909953
7
- data.tar.gz: b3f8c2ad6fa0effd9958a4102b6280e373c5bf11a3c90982bfdaaffaeb14d90f03fff37460963d51e6600cd9432f36b11338a86e3b5eb19536252fcd37e4c048
6
+ metadata.gz: beb4337b53ade2b2ff109f95f59a1d4bc3f21c1d1abc7f97101f046e45db78cf3d3bcc677a88b6ce1d14373a582397540ac8c0192a429d3056bf578e820e55ac
7
+ data.tar.gz: 32b1f9ef0128568d5d0e909c205477e955fcedca800f12d47406e499366eb4e26bdabae8b9811ce5d66a1197a7b9d0a28da919522be787ecd31fd7bb4b8ad6f3
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = ActivitiesDB.rb -- PostRunner - Manage the data from your Garmin sport devices.
5
5
  #
6
- # Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
6
+ # Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
7
7
  #
8
8
  # This program is free software; you can redistribute it and/or modify
9
9
  # it under the terms of version 2 of the GNU General Public License as
@@ -14,22 +14,24 @@ require 'fileutils'
14
14
  require 'yaml'
15
15
 
16
16
  require 'fit4ruby'
17
+ require 'postrunner/BackedUpFile'
17
18
  require 'postrunner/Activity'
18
19
  require 'postrunner/PersonalRecords'
19
20
  require 'postrunner/ActivityListView'
21
+ require 'postrunner/ViewButtons'
20
22
 
21
23
  module PostRunner
22
24
 
23
25
  class ActivitiesDB
24
26
 
25
- attr_reader :db_dir, :cfg, :fit_dir, :html_dir, :activities
27
+ attr_reader :db_dir, :cfg, :fit_dir, :activities, :records, :views
26
28
 
27
29
  def initialize(db_dir, cfg)
28
30
  @db_dir = db_dir
29
31
  @cfg = cfg
30
32
  @fit_dir = File.join(@db_dir, 'fit')
31
- @html_dir = File.join(@db_dir, 'html')
32
33
  @archive_file = File.join(@db_dir, 'archive.yml')
34
+ @auxilliary_dirs = %w( icons jquery flot openlayers )
33
35
 
34
36
  create_directories
35
37
  begin
@@ -38,7 +40,7 @@ module PostRunner
38
40
  else
39
41
  @activities = []
40
42
  end
41
- rescue StandardError
43
+ rescue RuntimeError
42
44
  Log.fatal "Cannot load archive file '#{@archive_file}': #{$!}"
43
45
  end
44
46
 
@@ -58,10 +60,31 @@ module PostRunner
58
60
  sync_needed |= !a.fit_activity.nil?
59
61
  end
60
62
 
63
+ # Define which View objects the HTML output will contain off. This
64
+ # doesn't really belong in ActivitiesDB but for now it's the best place
65
+ # to put it.
66
+ @views = ViewButtons.new([
67
+ NavButtonDef.new('activities.png', 'index.html'),
68
+ NavButtonDef.new('record.png', "records-0.html")
69
+ ])
70
+
61
71
  @records = PersonalRecords.new(self)
62
72
  sync if sync_needed
63
73
  end
64
74
 
75
+ # Ensure that all necessary directories are present to store the output
76
+ # files. This method is idempotent and can be called even when directories
77
+ # exist already.
78
+ def create_directories
79
+ create_directory(@db_dir, 'data')
80
+ create_directory(@fit_dir, 'fit')
81
+ create_directory(@cfg[:html_dir], 'html')
82
+
83
+ @auxilliary_dirs.each do |dir|
84
+ create_auxdir(dir)
85
+ end
86
+ end
87
+
65
88
  # Add a new FIT file to the database.
66
89
  # @param fit_file [String] Name of the FIT file.
67
90
  # @return [TrueClass or FalseClass] True if the file could be added. False
@@ -84,6 +107,10 @@ module PostRunner
84
107
  Log.error $!
85
108
  return false
86
109
  end
110
+ unless fit_activity
111
+ Log.error "#{fit_file} does not contain any activity records"
112
+ return false
113
+ end
87
114
 
88
115
  begin
89
116
  FileUtils.cp(fit_file, @fit_dir)
@@ -97,7 +124,10 @@ module PostRunner
97
124
  a2.timestamp <=> a1.timestamp
98
125
  end
99
126
 
100
- activity.register_records(@records)
127
+ activity.register_records
128
+
129
+ # Generate HTML file for this activity.
130
+ activity.generate_html_view
101
131
 
102
132
  # The HTML activity views contain links to their predecessors and
103
133
  # successors. After inserting a new activity, we need to re-generate
@@ -116,10 +146,11 @@ module PostRunner
116
146
  end
117
147
 
118
148
  def delete(activity)
119
- pred = predecessor(activities)
120
- succ = successor(activities)
149
+ pred = predecessor(activity)
150
+ succ = successor(activity)
121
151
 
122
152
  @activities.delete(activity)
153
+ @records.delete_activity(activity)
123
154
 
124
155
  # The HTML activity views contain links to their predecessors and
125
156
  # successors. After deleting an activity, we need to re-generate these
@@ -135,8 +166,19 @@ module PostRunner
135
166
  sync
136
167
  end
137
168
 
169
+ def set(activity, attribute, value)
170
+ activity.set(attribute, value)
171
+ sync
172
+ end
173
+
138
174
  def check
139
- @activities.each { |a| a.check }
175
+ @records.delete_all_records
176
+ @activities.sort do |a1, a2|
177
+ a1.timestamp <=> a2.timestamp
178
+ end.each { |a| a.check }
179
+ @records.sync
180
+ # Ensure that HTML index is up-to-date.
181
+ ActivityListView.new(self).update_index_pages
140
182
  end
141
183
 
142
184
  def ref_by_fit_file(fit_file)
@@ -189,11 +231,12 @@ module PostRunner
189
231
  @activities[idx - 1]
190
232
  end
191
233
 
192
- # Return the previous Activity before the provided activity. Note that
193
- # this has a higher index. If none is found, return nil.
234
+ # Return the previous Activity before the provided activity.
235
+ # If none is found, return nil.
194
236
  def predecessor(activity)
195
237
  idx = @activities.index(activity)
196
- return nil if idx.nil? || idx >= @activities.length - 2
238
+ return nil if idx.nil?
239
+ # Activities indexes are reversed. The predecessor has a higher index.
197
240
  @activities[idx + 1]
198
241
  end
199
242
 
@@ -231,8 +274,8 @@ module PostRunner
231
274
 
232
275
  # Show the activity list in a web browser.
233
276
  def show_list_in_browser
234
- ActivityListView.new(self).update_html_index
235
- show_in_browser(File.join(@html_dir, 'index.html'))
277
+ ActivityListView.new(self).update_index_pages
278
+ show_in_browser(File.join(@cfg[:html_dir], 'index.html'))
236
279
  end
237
280
 
238
281
  def list
@@ -243,6 +286,10 @@ module PostRunner
243
286
  puts @records.to_s
244
287
  end
245
288
 
289
+ def activity_records(activity)
290
+ @records.activity_records(activity)
291
+ end
292
+
246
293
  # Launch a web browser and show an HTML file.
247
294
  # @param html_file [String] file name of the HTML file to show
248
295
  def show_in_browser(html_file)
@@ -262,32 +309,39 @@ module PostRunner
262
309
  @activities.each { |a| a.generate_html_view }
263
310
  Log.info "All HTML report files have been re-generated."
264
311
  # (Re-)generate index files.
265
- ActivityListView.new(self).update_html_index
312
+ ActivityListView.new(self).update_index_pages
266
313
  Log.info "HTML index files have been updated."
267
314
  end
268
315
 
316
+ # Take all necessary steps to convert user data to match an updated
317
+ # PostRunner version.
318
+ def handle_version_update
319
+ # An updated version may bring new auxilliary directories. We remove the
320
+ # old directories and create new copies.
321
+ Log.warn('Removing old HTML auxilliary directories')
322
+ @auxilliary_dirs.each do |dir|
323
+ auxdir = File.join(@cfg[:html_dir], dir)
324
+ FileUtils.rm_rf(auxdir)
325
+ end
326
+ create_directories
327
+
328
+ Log.warn('Updating HTML files...')
329
+ generate_all_html_reports
330
+ end
331
+
269
332
  private
270
333
 
271
334
  def sync
272
335
  begin
273
- File.open(@archive_file, 'w') { |f| f.write(@activities.to_yaml) }
336
+ BackedUpFile.open(@archive_file, 'w') do |f|
337
+ f.write(@activities.to_yaml)
338
+ end
274
339
  rescue StandardError
275
340
  Log.fatal "Cannot write archive file '#{@archive_file}': #{$!}"
276
341
  end
277
342
 
278
343
  @records.sync
279
- ActivityListView.new(self).update_html_index
280
- end
281
-
282
- def create_directories
283
- create_directory(@db_dir, 'data')
284
- create_directory(@fit_dir, 'fit')
285
- create_directory(@html_dir, 'html')
286
-
287
- create_symlink('icons')
288
- create_symlink('jquery')
289
- create_symlink('flot')
290
- create_symlink('openlayers')
344
+ ActivityListView.new(self).update_index_pages
291
345
  end
292
346
 
293
347
  def create_directory(dir, name)
@@ -301,7 +355,7 @@ module PostRunner
301
355
  end
302
356
  end
303
357
 
304
- def create_symlink(dir)
358
+ def create_auxdir(dir)
305
359
  # This file should be in lib/postrunner. The 'misc' directory should be
306
360
  # found in '../../misc'.
307
361
  misc_dir = File.realpath(File.join(File.dirname(__FILE__),
@@ -313,12 +367,13 @@ module PostRunner
313
367
  unless Dir.exists?(src_dir)
314
368
  Log.fatal "Cannot find '#{src_dir}': #{$!}"
315
369
  end
316
- dst_dir = File.join(@html_dir, dir)
370
+ dst_dir = File.join(@cfg[:html_dir], dir)
317
371
  unless File.exists?(dst_dir)
318
372
  begin
319
- FileUtils.ln_s(src_dir, dst_dir)
373
+ #FileUtils.ln_s(src_dir, dst_dir)
374
+ FileUtils.cp_r(src_dir, dst_dir)
320
375
  rescue IOError
321
- Log.fatal "Cannot create symbolic link to '#{dst_dir}': #{$!}"
376
+ Log.fatal "Cannot create auxilliary directory '#{dst_dir}': #{$!}"
322
377
  end
323
378
  end
324
379
  end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = Activity.rb -- PostRunner - Manage the data from your Garmin sport devices.
5
5
  #
6
- # Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
6
+ # Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
7
7
  #
8
8
  # This program is free software; you can redistribute it and/or modify
9
9
  # it under the terms of version 2 of the GNU General Public License as
@@ -19,19 +19,73 @@ module PostRunner
19
19
 
20
20
  class Activity
21
21
 
22
- attr_reader :db, :fit_file, :name, :fit_activity, :html_dir, :html_file
22
+ attr_reader :db, :fit_file, :name, :fit_activity
23
23
 
24
24
  # This is a list of variables that provide data from the fit file. To
25
25
  # speed up access to it, we cache the data in the activity database.
26
- @@CachedActivityValues = %w( sport timestamp total_distance
27
- total_timer_time avg_speed )
26
+ @@CachedActivityValues = %w( sport sub_sport timestamp total_distance
27
+ total_timer_time avg_speed )
28
28
  # We also store some additional information in the archive index.
29
29
  @@CachedAttributes = @@CachedActivityValues + %w( fit_file name )
30
30
 
31
+ ActivityTypes = {
32
+ 'generic' => 'Generic',
33
+ 'running' => 'Running',
34
+ 'cycling' => 'Cycling',
35
+ 'transition' => 'Transition',
36
+ 'fitness_equipment' => 'Fitness Equipment',
37
+ 'swimming' => 'Swimming',
38
+ 'basketball' => 'Basketball',
39
+ 'soccer' => 'Soccer',
40
+ 'tennis' => 'Tennis',
41
+ 'american_football' => 'American Football',
42
+ 'walking' => 'Walking',
43
+ 'cross_country_skiing' => 'Cross Country Skiing',
44
+ 'alpine_skiing' => 'Alpine Skiing',
45
+ 'snowboarding' => 'Snowboarding',
46
+ 'rowing' => 'Rowing',
47
+ 'mountaineering' => 'Mountaneering',
48
+ 'hiking' => 'Hiking',
49
+ 'multisport' => 'Multisport',
50
+ 'paddling' => 'Paddling',
51
+ 'all' => 'All'
52
+ }
53
+ ActivitySubTypes = {
54
+ 'generic' => 'Generic',
55
+ 'treadmill' => 'Treadmill',
56
+ 'street' => 'Street',
57
+ 'trail' => 'Trail',
58
+ 'track' => 'Track',
59
+ 'spin' => 'Spin',
60
+ 'indoor_cycling' => 'Indoor Cycling',
61
+ 'road' => 'Road',
62
+ 'mountain' => 'Mountain',
63
+ 'downhill' => 'Downhill',
64
+ 'recumbent' => 'Recumbent',
65
+ 'cyclocross' => 'Cyclocross',
66
+ 'hand_cycling' => 'Hand Cycling',
67
+ 'track_cycling' => 'Track Cycling',
68
+ 'indoor_rowing' => 'Indoor Rowing',
69
+ 'elliptical' => 'Elliptical',
70
+ 'stair_climbing' => 'Stair Climbing',
71
+ 'lap_swimming' => 'Lap Swimming',
72
+ 'open_water' => 'Open Water',
73
+ 'flexibility_training' => 'Flexibility Training',
74
+ 'strength_training' => 'Strength Training',
75
+ 'warm_up' => 'Warm up',
76
+ 'match' => 'Match',
77
+ 'exercise' => 'Excersize',
78
+ 'challenge' => 'Challenge',
79
+ 'indoor_skiing' => 'Indoor Skiing',
80
+ 'cardio_training' => 'Cardio Training',
81
+ 'all' => 'All'
82
+ }
83
+
31
84
  def initialize(db, fit_file, fit_activity, name = nil)
32
85
  @fit_file = fit_file
33
86
  @fit_activity = fit_activity
34
87
  @name = name || fit_file
88
+ @unset_variables = []
35
89
  late_init(db)
36
90
 
37
91
  @@CachedActivityValues.each do |v|
@@ -39,8 +93,6 @@ module PostRunner
39
93
  instance_variable_set(v_str, fit_activity.send(v))
40
94
  self.class.send(:attr_reader, v.to_sym)
41
95
  end
42
- # Generate HTML file for this activity.
43
- generate_html_view
44
96
  end
45
97
 
46
98
  # YAML::load() does not call initialize(). We don't have all attributes
@@ -48,12 +100,21 @@ module PostRunner
48
100
  # after a YAML::load().
49
101
  def late_init(db)
50
102
  @db = db
51
- @html_dir = File.join(@db.db_dir, 'html')
52
- @html_file = File.join(@html_dir, "#{@fit_file[0..-5]}.html")
103
+ @html_file = File.join(@db.cfg[:html_dir], "#{@fit_file[0..-5]}.html")
104
+
105
+ @unset_variables.each do |name_without_at|
106
+ # The YAML file does not yet have the instance variable cached.
107
+ # Load the Activity data and extract the value to set the instance
108
+ # variable.
109
+ @fit_activity = load_fit_file unless @fit_activity
110
+ instance_variable_set('@' + name_without_at,
111
+ @fit_activity.send(name_without_at))
112
+ end
53
113
  end
54
114
 
55
115
  def check
56
- @fit_activity = load_fit_file
116
+ generate_html_view
117
+ register_records
57
118
  Log.info "FIT file #{@fit_file} is OK"
58
119
  end
59
120
 
@@ -65,26 +126,21 @@ module PostRunner
65
126
  # objects. The initialize() is NOT called during YAML::load(). Any
66
127
  # additional initialization work is done in late_init().
67
128
  def init_with(coder)
129
+ @unset_variables = []
68
130
  @@CachedAttributes.each do |name_without_at|
69
- name_with_at = '@' + name_without_at
70
131
  # Create attr_readers for cached variables.
71
132
  self.class.send(:attr_reader, name_without_at.to_sym)
72
133
 
73
134
  if coder.map.include?(name_without_at)
74
135
  # The YAML file has a value for the instance variable. So just set
75
136
  # it.
76
- instance_variable_set(name_with_at, coder[name_without_at])
137
+ instance_variable_set('@' + name_without_at, coder[name_without_at])
77
138
  else
78
139
  if @@CachedActivityValues.include?(name_without_at)
79
- # The YAML file does not yet have the instance variable cached.
80
- # Load the Activity data and extract the value to set the instance
81
- # variable.
82
- @fit_activity = load_fit_file unless @fit_activity
83
- instance_variable_set(name_with_at,
84
- @fit_activity.send(name_without_at))
140
+ @unset_variables << name_without_at
85
141
  else
86
142
  Log.fatal "Don't know how to initialize the instance variable " +
87
- "#{name_with_at}."
143
+ "#{name_without_at}."
88
144
  end
89
145
  end
90
146
  end
@@ -111,7 +167,10 @@ module PostRunner
111
167
 
112
168
  def summary
113
169
  @fit_activity = load_fit_file unless @fit_activity
114
- puts ActivitySummary.new(@fit_activity, name, @db.cfg[:unit_system]).to_s
170
+ puts ActivitySummary.new(@fit_activity, @db.cfg[:unit_system],
171
+ { :name => @name,
172
+ :type => activity_type,
173
+ :sub_type => activity_sub_type }).to_s
115
174
  end
116
175
 
117
176
  def rename(name)
@@ -119,22 +178,149 @@ module PostRunner
119
178
  generate_html_view
120
179
  end
121
180
 
122
- def register_records(db)
123
- @fit_activity.personal_records.each do |r|
124
- if r.longest_distance == 1
125
- # In case longest_distance is 1 the distance is stored in the
126
- # duration field in 10-th of meters.
127
- db.register_result(r.duration * 10.0 , 0, r.start_time, @fit_file)
128
- else
129
- db.register_result(r.distance, r.duration, r.start_time, @fit_file)
181
+ def set(attribute, value)
182
+ case attribute
183
+ when 'name'
184
+ @name = value
185
+ when 'type'
186
+ @fit_activity = load_fit_file unless @fit_activity
187
+ unless ActivityTypes.values.include?(value)
188
+ Log.fatal "Unknown activity type '#{value}'. Must be one of " +
189
+ ActivityTypes.values.join(', ')
190
+ end
191
+ @sport = ActivityTypes.invert[value]
192
+ # Since the activity changes the records from this Activity need to be
193
+ # removed and added again.
194
+ @db.records.delete_activity(self)
195
+ register_records
196
+ when 'subtype'
197
+ unless ActivitySubTypes.values.include?(value)
198
+ Log.fatal "Unknown activity subtype '#{value}'. Must be one of " +
199
+ ActivitySubTypes.values.join(', ')
200
+ end
201
+ @sub_sport = ActivitySubTypes.invert[value]
202
+ else
203
+ Log.fatal "Unknown activity attribute '#{attribute}'. Must be one of " +
204
+ 'name, type or subtype'
205
+ end
206
+ generate_html_view
207
+ end
208
+
209
+ def register_records
210
+ distance_record = 0.0
211
+ distance_record_sport = nil
212
+ # Array with popular distances (in meters) in ascending order.
213
+ record_distances = nil
214
+ # Speed records for popular distances (seconds hashed by distance in
215
+ # meters)
216
+ speed_records = {}
217
+
218
+ segment_start_time = @fit_activity.sessions[0].start_time
219
+ segment_start_distance = 0.0
220
+
221
+ sport = nil
222
+ last_timestamp = nil
223
+ last_distance = nil
224
+
225
+ @fit_activity.records.each do |record|
226
+ if record.distance.nil?
227
+ # All records must have a valid distance mark or the activity does
228
+ # not qualify for a personal record.
229
+ Log.warn "Found a record without a valid distance"
230
+ return
231
+ end
232
+ if record.timestamp.nil?
233
+ Log.warn "Found a record without a valid timestamp"
234
+ return
235
+ end
236
+
237
+ unless sport
238
+ # If the Activity has sport set to 'multisport' or 'all' we pick up
239
+ # the sport from the FIT records. Otherwise, we just use whatever
240
+ # sport the Activity provides.
241
+ if @sport == 'multisport' || @sport == 'all'
242
+ sport = record.activity_type
243
+ else
244
+ sport = @sport
245
+ end
246
+ return unless PersonalRecords::SpeedRecordDistances.include?(sport)
247
+
248
+ record_distances = PersonalRecords::SpeedRecordDistances[sport].
249
+ keys.sort
130
250
  end
251
+
252
+ segment_start_distance = record.distance unless segment_start_distance
253
+ segment_start_time = record.timestamp unless segment_start_time
254
+
255
+ # Total distance covered in this segment so far
256
+ segment_distance = record.distance - segment_start_distance
257
+ # Check if we have reached the next popular distance.
258
+ if record_distances.first &&
259
+ segment_distance >= record_distances.first
260
+ segment_duration = record.timestamp - segment_start_time
261
+ # The distance may be somewhat larger than a popular distance. We
262
+ # normalize the time to the norm distance.
263
+ norm_duration = segment_duration / segment_distance *
264
+ record_distances.first
265
+ # Save the time for this distance.
266
+ speed_records[record_distances.first] = {
267
+ :time => norm_duration, :sport => sport
268
+ }
269
+ # Switch to the next popular distance.
270
+ record_distances.shift
271
+ end
272
+
273
+ # We've reached the end of a segment if the sport type changes, we
274
+ # detect a pause of more than 30 seconds or when we've reached the
275
+ # last record.
276
+ if (record.activity_type && sport && record.activity_type != sport) ||
277
+ (last_timestamp && (record.timestamp - last_timestamp) > 30) ||
278
+ record.equal?(@fit_activity.records.last)
279
+
280
+ # Check for a total distance record
281
+ if segment_distance > distance_record
282
+ distance_record = segment_distance
283
+ distance_record_sport = sport
284
+ end
285
+
286
+ # Prepare for the next segment in this Activity
287
+ segment_start_distance = nil
288
+ segment_start_time = nil
289
+ sport = nil
290
+ end
291
+
292
+ last_timestamp = record.timestamp
293
+ last_distance = record.distance
294
+ end
295
+
296
+ # Store the found records
297
+ start_time = @fit_activity.sessions[0].timestamp
298
+ if @distance_record_sport
299
+ @db.records.register_result(self, distance_record_sport,
300
+ distance_record, nil, start_time)
131
301
  end
302
+ speed_records.each do |dist, info|
303
+ @db.records.register_result(self, info[:sport], dist, info[:time],
304
+ start_time)
305
+ end
306
+ end
307
+
308
+ # Return true if this activity generated any personal records.
309
+ def has_records?
310
+ !@db.records.activity_records(self).empty?
132
311
  end
133
312
 
134
313
  def generate_html_view
135
314
  @fit_activity = load_fit_file unless @fit_activity
136
- ActivityView.new(self, @db.cfg[:unit_system], @db.predecessor(self),
137
- @db.successor(self))
315
+ ActivityView.new(self, @db.cfg[:unit_system])
316
+ end
317
+
318
+ def activity_type
319
+ ActivityTypes[@sport] || 'Undefined'
320
+ end
321
+
322
+ def activity_sub_type
323
+ ActivitySubTypes[@sub_sport] || 'Undefined'
138
324
  end
139
325
 
140
326
  private
@@ -142,10 +328,16 @@ module PostRunner
142
328
  def load_fit_file(filter = nil)
143
329
  fit_file = File.join(@db.fit_dir, @fit_file)
144
330
  begin
145
- return Fit4Ruby.read(fit_file, filter)
331
+ fit_activity = Fit4Ruby.read(fit_file, filter)
146
332
  rescue Fit4Ruby::Error
147
333
  Log.fatal $!
148
334
  end
335
+
336
+ unless fit_activity
337
+ Log.fatal "#{fit_file} does not contain any activity records"
338
+ end
339
+
340
+ fit_activity
149
341
  end
150
342
 
151
343
  end