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 +4 -4
- data/lib/postrunner/ActivitiesDB.rb +86 -31
- data/lib/postrunner/Activity.rb +222 -30
- data/lib/postrunner/ActivityLink.rb +59 -0
- data/lib/postrunner/ActivityListView.rb +37 -90
- data/lib/postrunner/ActivitySummary.rb +12 -12
- data/lib/postrunner/ActivityView.rb +49 -72
- data/lib/postrunner/BackedUpFile.rb +56 -0
- data/lib/postrunner/ChartView.rb +14 -16
- data/lib/postrunner/DeviceList.rb +3 -7
- data/lib/postrunner/FlexiTable.rb +28 -1
- data/lib/postrunner/HTMLBuilder.rb +64 -16
- data/lib/postrunner/Main.rb +63 -19
- data/lib/postrunner/NavButtonRow.rb +103 -0
- data/lib/postrunner/PagingButtons.rb +77 -0
- data/lib/postrunner/PersonalRecords.rb +338 -79
- data/lib/postrunner/RecordListPageView.rb +69 -0
- data/lib/postrunner/RuntimeConfig.rb +5 -3
- data/lib/postrunner/TrackView.rb +14 -16
- data/lib/postrunner/UserProfileView.rb +3 -7
- data/lib/postrunner/View.rb +97 -0
- data/lib/postrunner/ViewBottom.rb +54 -0
- data/lib/postrunner/ViewButtons.rb +68 -0
- data/lib/postrunner/ViewFrame.rb +93 -0
- data/lib/postrunner/ViewTop.rb +80 -0
- data/lib/postrunner/version.rb +1 -1
- data/misc/icons/activities.png +0 -0
- data/misc/icons/activities.svg +1582 -0
- data/misc/icons/record-small.png +0 -0
- data/misc/icons/record.png +0 -0
- data/misc/icons/record.svg +15712 -0
- data/spec/ActivitySummary_spec.rb +3 -1
- data/spec/PostRunner_spec.rb +45 -0
- data/spec/View_spec.rb +61 -0
- data/spec/spec_helper.rb +21 -7
- metadata +19 -3
- data/lib/postrunner/ViewWidgets.rb +0 -153
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd23d401a204deb3a2bdb839097e78c7eafc9b17
|
4
|
+
data.tar.gz: a97ce94a450b07cb1744d6b65ee42f463144ae2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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, :
|
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
|
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
|
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(
|
120
|
-
succ = successor(
|
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
|
-
@
|
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.
|
193
|
-
#
|
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?
|
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).
|
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).
|
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
|
-
|
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).
|
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
|
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
|
376
|
+
Log.fatal "Cannot create auxilliary directory '#{dst_dir}': #{$!}"
|
322
377
|
end
|
323
378
|
end
|
324
379
|
end
|
data/lib/postrunner/Activity.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
@
|
52
|
-
|
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
|
-
|
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(
|
137
|
+
instance_variable_set('@' + name_without_at, coder[name_without_at])
|
77
138
|
else
|
78
139
|
if @@CachedActivityValues.include?(name_without_at)
|
79
|
-
|
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
|
-
"#{
|
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,
|
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
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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]
|
137
|
-
|
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
|
-
|
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
|