postrunner 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|