postrunner 0.0.11 → 0.1.0
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/README.md +5 -2
- data/Rakefile +18 -2
- data/lib/postrunner/ActivitiesDB.rb +1 -22
- data/lib/postrunner/Activity.rb +21 -0
- data/lib/postrunner/ActivityLink.rb +1 -1
- data/lib/postrunner/ActivityListView.rb +10 -9
- data/lib/postrunner/ActivitySummary.rb +42 -15
- data/lib/postrunner/ActivityView.rb +10 -8
- data/lib/postrunner/ChartView.rb +238 -80
- data/lib/postrunner/DataSources.rb +1 -14
- data/lib/postrunner/DeviceList.rb +18 -10
- data/lib/postrunner/DirUtils.rb +33 -0
- data/lib/postrunner/EventList.rb +149 -0
- data/lib/postrunner/FFS_Activity.rb +297 -0
- data/lib/postrunner/FFS_Device.rb +129 -0
- data/lib/postrunner/FitFileStore.rb +372 -0
- data/lib/postrunner/HRV_Analyzer.rb +178 -0
- data/lib/postrunner/LinearPredictor.rb +46 -0
- data/lib/postrunner/Main.rb +135 -33
- data/lib/postrunner/Percentiles.rb +45 -0
- data/lib/postrunner/PersonalRecords.rb +203 -114
- data/lib/postrunner/RecordListPageView.rb +6 -6
- data/lib/postrunner/UserProfileView.rb +4 -0
- data/lib/postrunner/version.rb +1 -1
- data/misc/postrunner/trackview.js +99 -0
- data/postrunner.gemspec +5 -5
- data/spec/ActivitySummary_spec.rb +15 -4
- data/spec/FitFileStore_spec.rb +133 -0
- data/spec/FlexiTable_spec.rb +1 -1
- data/spec/PersonalRecords_spec.rb +206 -0
- data/spec/PostRunner_spec.rb +64 -60
- data/spec/View_spec.rb +1 -1
- data/spec/spec_helper.rb +76 -2
- metadata +42 -28
@@ -0,0 +1,178 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = HRV_Analyzer.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2015 by Chris Schlaeger <cs@taskjuggler.org>
|
7
|
+
#
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
10
|
+
# published by the Free Software Foundation.
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'postrunner/LinearPredictor'
|
14
|
+
|
15
|
+
module PostRunner
|
16
|
+
|
17
|
+
# This class analyzes the heart rate variablity based on the R-R intervals
|
18
|
+
# in the given FIT file. It can compute RMSSD and a HRV score if the data
|
19
|
+
# quality is good enough.
|
20
|
+
class HRV_Analyzer
|
21
|
+
|
22
|
+
attr_reader :rr_intervals, :timestamps
|
23
|
+
|
24
|
+
# Create a new HRV_Analyzer object.
|
25
|
+
# @param fit_file [Fit4Ruby::Activity] FIT file to analyze.
|
26
|
+
def initialize(fit_file)
|
27
|
+
@fit_file = fit_file
|
28
|
+
collect_rr_intervals
|
29
|
+
end
|
30
|
+
|
31
|
+
# The method can be used to check if we have valid HRV data. The FIT file
|
32
|
+
# must have HRV data and the measurement duration must be at least 30
|
33
|
+
# seconds.
|
34
|
+
def has_hrv_data?
|
35
|
+
!@fit_file.hrv.empty? && total_duration > 30.0
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return the total duration of all measured intervals in seconds.
|
39
|
+
def total_duration
|
40
|
+
@timestamps[-1]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Compute the root mean square of successive differences.
|
44
|
+
# @param start_time [Float] Determines at what time mark (in seconds) the
|
45
|
+
# computation should start.
|
46
|
+
# @param duration [Float] The duration of the total inteval in seconds to
|
47
|
+
# be considered for the computation. This value should be larger
|
48
|
+
# then 30 seconds to produce meaningful values.
|
49
|
+
def rmssd(start_time = 0.0, duration = nil)
|
50
|
+
# Find the start index based on the requested interval start time.
|
51
|
+
start_idx = 0
|
52
|
+
@timestamps.each do |ts|
|
53
|
+
break if ts >= start_time
|
54
|
+
start_idx += 1
|
55
|
+
end
|
56
|
+
# Find the end index based on the requested interval duration.
|
57
|
+
if duration
|
58
|
+
end_time = start_time + duration
|
59
|
+
end_idx = start_idx
|
60
|
+
while end_idx < (@timestamps.length - 1) &&
|
61
|
+
@timestamps[end_idx] < end_time
|
62
|
+
end_idx += 1
|
63
|
+
end
|
64
|
+
else
|
65
|
+
end_idx = -1
|
66
|
+
end
|
67
|
+
|
68
|
+
last_i = nil
|
69
|
+
sum = 0.0
|
70
|
+
cnt = 0
|
71
|
+
@rr_intervals[start_idx..end_idx].each do |i|
|
72
|
+
if i && last_i
|
73
|
+
sum += (last_i - i) ** 2.0
|
74
|
+
cnt += 1
|
75
|
+
end
|
76
|
+
last_i = i
|
77
|
+
end
|
78
|
+
|
79
|
+
Math.sqrt(sum / cnt)
|
80
|
+
end
|
81
|
+
|
82
|
+
# The RMSSD value is not very easy to memorize. Alternatively, we can
|
83
|
+
# multiply the natural logarithm of RMSSD by -20. This usually results in
|
84
|
+
# values between 1.0 (for untrained) and 100.0 (for higly trained)
|
85
|
+
# athletes. Values larger than 100.0 are rare but possible.
|
86
|
+
# @param start_time [Float] Determines at what time mark (in seconds) the
|
87
|
+
# computation should start.
|
88
|
+
# @param duration [Float] The duration of the total inteval in seconds to
|
89
|
+
# be considered for the computation. This value should be larger
|
90
|
+
# then 30 seconds to produce meaningful values.
|
91
|
+
def lnrmssdx20(start_time = 0.0, duration = nil)
|
92
|
+
-20.0 * Math.log(rmssd(start_time, duration))
|
93
|
+
end
|
94
|
+
|
95
|
+
# This method is similar to lnrmssdx20 but it tries to search the data for
|
96
|
+
# the best time period to compute the lnrmssdx20 value from.
|
97
|
+
def lnrmssdx20_1sigma
|
98
|
+
# Create a new Array that consists of rr_intervals and timestamps
|
99
|
+
# tuples.
|
100
|
+
set = []
|
101
|
+
0.upto(@rr_intervals.length - 1) do |i|
|
102
|
+
set << [ @rr_intervals[i] ? @rr_intervals[i] : 0.0, @timestamps[i] ]
|
103
|
+
end
|
104
|
+
|
105
|
+
percentiles = Percentiles.new(set)
|
106
|
+
# Compile a list of all tuples with rr_intervals that are outside of the
|
107
|
+
# PT84 (aka +1sigma range. Sort the list by time.
|
108
|
+
not_1sigma = percentiles.not_tp_x(84.13).sort { |e1, e2| e1[1] <=> e2[1] }
|
109
|
+
|
110
|
+
# Then find the largest time gap in that list. So all the values in that
|
111
|
+
# gap are within TP84.
|
112
|
+
gap_start = gap_end = 0
|
113
|
+
last = nil
|
114
|
+
not_1sigma.each do |e|
|
115
|
+
if last
|
116
|
+
if (e[1] - last) > (gap_end - gap_start)
|
117
|
+
gap_start = last
|
118
|
+
gap_end = e[1]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
last = e[1]
|
122
|
+
end
|
123
|
+
# That gap should be at least 30 seconds long. Otherwise we'll just use
|
124
|
+
# all the values.
|
125
|
+
return lnrmssdx20 if gap_end - gap_start < 30
|
126
|
+
|
127
|
+
lnrmssdx20(gap_start, gap_end - gap_start)
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def collect_rr_intervals
|
133
|
+
# The rr_intervals Array stores the beat-to-beat time intervals (R-R).
|
134
|
+
# If one or move beats have been skipped during measurement, a nil value
|
135
|
+
# is inserted.
|
136
|
+
@rr_intervals = []
|
137
|
+
# The timestamps Array stores the relative (to start of sequence) time
|
138
|
+
# for each interval in the rr_intervals Array.
|
139
|
+
@timestamps = []
|
140
|
+
|
141
|
+
# Each Fit4Ruby::HRV object has an Array called 'time' that contains up
|
142
|
+
# to 5 R-R interval durations. If less than 5 are present, they are
|
143
|
+
# filled with nil.
|
144
|
+
raw_rr_intervals = []
|
145
|
+
@fit_file.hrv.each do |hrv|
|
146
|
+
raw_rr_intervals += hrv.time.compact
|
147
|
+
end
|
148
|
+
return if raw_rr_intervals.empty?
|
149
|
+
|
150
|
+
window = 20
|
151
|
+
intro_mean = raw_rr_intervals[0..4 * window].reduce(:+) / (4 * window)
|
152
|
+
predictor = LinearPredictor.new(window, intro_mean)
|
153
|
+
|
154
|
+
# The timer accumulates the interval durations.
|
155
|
+
timer = 0.0
|
156
|
+
raw_rr_intervals.each do |dt|
|
157
|
+
timer += dt
|
158
|
+
@timestamps << timer
|
159
|
+
|
160
|
+
# Sometimes the hrv data is missing one or more beats. The next
|
161
|
+
# detected beat is than listed with the time interval since the last
|
162
|
+
# detected beat. We try to detect these skipped beats by looking for
|
163
|
+
# time intervals that are 1.5 or more times larger than the predicted
|
164
|
+
# value for this interval.
|
165
|
+
if (next_dt = predictor.predict) && dt > 1.5 * next_dt
|
166
|
+
@rr_intervals << nil
|
167
|
+
else
|
168
|
+
@rr_intervals << dt
|
169
|
+
# Feed the value into the predictor.
|
170
|
+
predictor.insert(dt)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = LinearPredictor.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2015 by Chris Schlaeger <cs@taskjuggler.org>
|
7
|
+
#
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
10
|
+
# published by the Free Software Foundation.
|
11
|
+
#
|
12
|
+
|
13
|
+
module PostRunner
|
14
|
+
|
15
|
+
# For now we use a trivial adaptive linear predictor that just uses the
|
16
|
+
# average of past values to predict the next value.
|
17
|
+
class LinearPredictor
|
18
|
+
|
19
|
+
# Create a new LinearPredictor object.
|
20
|
+
# @param n [Fixnum] The number of coefficients the predictor should use.
|
21
|
+
def initialize(n, default = nil)
|
22
|
+
@values = Array.new(n, default)
|
23
|
+
@size = n
|
24
|
+
@next = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# Tell the predictor about the actual next value.
|
28
|
+
# @param value [Float] next value
|
29
|
+
def insert(value)
|
30
|
+
@values << value
|
31
|
+
|
32
|
+
if @values.length > @size
|
33
|
+
@values.shift
|
34
|
+
@next = @values.reduce(:+) / @size
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Float] The predicted value of the next sample.
|
39
|
+
def predict
|
40
|
+
@next
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
data/lib/postrunner/Main.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = Main.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
5
|
#
|
6
|
-
# Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2014, 2015, 2016 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
|
@@ -16,7 +16,10 @@ require 'perobs'
|
|
16
16
|
|
17
17
|
require 'postrunner/version'
|
18
18
|
require 'postrunner/Log'
|
19
|
+
require 'postrunner/DirUtils'
|
19
20
|
require 'postrunner/RuntimeConfig'
|
21
|
+
require 'postrunner/FitFileStore'
|
22
|
+
require 'postrunner/PersonalRecords'
|
20
23
|
require 'postrunner/ActivitiesDB'
|
21
24
|
require 'postrunner/MonitoringDB'
|
22
25
|
require 'postrunner/EPO_Downloader'
|
@@ -25,20 +28,40 @@ module PostRunner
|
|
25
28
|
|
26
29
|
class Main
|
27
30
|
|
31
|
+
include DirUtils
|
32
|
+
|
28
33
|
def initialize(args)
|
29
34
|
@filter = nil
|
30
35
|
@name = nil
|
36
|
+
@force = false
|
31
37
|
@attribute = nil
|
32
38
|
@value = nil
|
33
|
-
@activities = nil
|
34
|
-
@monitoring = nil
|
35
39
|
@db_dir = File.join(ENV['HOME'], '.postrunner')
|
36
40
|
|
37
41
|
return if (args = parse_options(args)).nil?
|
38
42
|
|
39
|
-
@
|
43
|
+
create_directory(@db_dir, 'PostRunner data')
|
40
44
|
@db = PEROBS::Store.new(File.join(@db_dir, 'database'))
|
45
|
+
# Create a hash to store configuration data in the store unless it
|
46
|
+
# exists already.
|
47
|
+
cfg = (@db['config'] ||= @db.new(PEROBS::Hash))
|
48
|
+
cfg['unit_system'] ||= :metric
|
49
|
+
cfg['version'] ||= VERSION
|
50
|
+
# We always override the data_dir as the user might have moved the data
|
51
|
+
# directory. The only reason we store it in the DB is to have it
|
52
|
+
# available throught the application.
|
53
|
+
cfg['data_dir'] = @db_dir
|
54
|
+
# Always update html_dir setting so that the DB directory can be moved
|
55
|
+
# around by the user.
|
56
|
+
cfg['html_dir'] = File.join(@db_dir, 'html')
|
57
|
+
|
58
|
+
setup_directories
|
59
|
+
if (errors = @db.check) != 0
|
60
|
+
Log.fatal "Postrunner database is corrupted: #{errors} errors found"
|
61
|
+
end
|
41
62
|
execute_command(args)
|
63
|
+
|
64
|
+
@db.sync
|
42
65
|
end
|
43
66
|
|
44
67
|
private
|
@@ -81,6 +104,11 @@ EOT
|
|
81
104
|
@filter = Fit4Ruby::FitFilter.new unless @filter
|
82
105
|
@filter.ignore_undef = true
|
83
106
|
end
|
107
|
+
opts.on('--force',
|
108
|
+
'Import files even if they have been deleted from the ' +
|
109
|
+
'database before.') do
|
110
|
+
@force = true
|
111
|
+
end
|
84
112
|
|
85
113
|
opts.separator ""
|
86
114
|
opts.separator "Options for the 'import' command:"
|
@@ -104,7 +132,7 @@ EOT
|
|
104
132
|
return nil
|
105
133
|
end
|
106
134
|
opts.on('--version', 'Show version number') do
|
107
|
-
|
135
|
+
puts VERSION
|
108
136
|
return nil
|
109
137
|
end
|
110
138
|
|
@@ -119,6 +147,9 @@ check [ <fit file> | <ref> ... ]
|
|
119
147
|
dump <fit file> | <ref>
|
120
148
|
Dump the content of the FIT file.
|
121
149
|
|
150
|
+
events [ <ref> ]
|
151
|
+
List all the events of the specified activies.
|
152
|
+
|
122
153
|
import [ <fit file> | <directory> ]
|
123
154
|
Import the provided FIT file(s) into the postrunner database. If no
|
124
155
|
file or directory is provided, the directory that was used for the
|
@@ -187,16 +218,50 @@ EOT
|
|
187
218
|
end
|
188
219
|
end
|
189
220
|
|
221
|
+
def setup_directories
|
222
|
+
create_directory(@db['config']['html_dir'], 'HTML output')
|
223
|
+
|
224
|
+
%w( icons jquery flot openlayers postrunner ).each do |dir|
|
225
|
+
# This file should be in lib/postrunner. The 'misc' directory should be
|
226
|
+
# found in '../../misc'.
|
227
|
+
misc_dir = File.realpath(File.join(File.dirname(__FILE__),
|
228
|
+
'..', '..', 'misc'))
|
229
|
+
unless Dir.exists?(misc_dir)
|
230
|
+
Log.fatal "Cannot find 'misc' directory under '#{misc_dir}': #{$!}"
|
231
|
+
end
|
232
|
+
src_dir = File.join(misc_dir, dir)
|
233
|
+
unless Dir.exists?(src_dir)
|
234
|
+
Log.fatal "Cannot find '#{src_dir}': #{$!}"
|
235
|
+
end
|
236
|
+
dst_dir = @db['config']['html_dir']
|
237
|
+
|
238
|
+
begin
|
239
|
+
FileUtils.cp_r(src_dir, dst_dir)
|
240
|
+
rescue IOError
|
241
|
+
Log.fatal "Cannot copy auxilliary data directory '#{dst_dir}': #{$!}"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
190
246
|
def execute_command(args)
|
191
|
-
|
192
|
-
@
|
247
|
+
# Create or load the FitFileStore data.
|
248
|
+
unless (@ffs = @db['file_store'])
|
249
|
+
@ffs = @db['file_store'] = @db.new(FitFileStore)
|
250
|
+
end
|
251
|
+
# Create or load the PersonalRecords data.
|
252
|
+
unless (@records = @db['records'])
|
253
|
+
@records = @db['records'] = @db.new(PersonalRecords)
|
254
|
+
end
|
193
255
|
handle_version_update
|
256
|
+
import_legacy_archive
|
194
257
|
|
195
258
|
case (cmd = args.shift)
|
196
259
|
when 'check'
|
197
260
|
if args.empty?
|
198
|
-
@
|
199
|
-
|
261
|
+
@ffs.check
|
262
|
+
Log.info "Datebase cleanup started. Please wait ..."
|
263
|
+
@db.gc
|
264
|
+
Log.info "Database cleanup finished"
|
200
265
|
else
|
201
266
|
process_files_or_activities(args, :check)
|
202
267
|
end
|
@@ -205,23 +270,25 @@ EOT
|
|
205
270
|
when 'dump'
|
206
271
|
@filter = Fit4Ruby::FitFilter.new unless @filter
|
207
272
|
process_files_or_activities(args, :dump)
|
273
|
+
when 'events'
|
274
|
+
process_files_or_activities(args, :events)
|
208
275
|
when 'import'
|
209
276
|
if args.empty?
|
210
277
|
# If we have no file or directory for the import command, we get the
|
211
278
|
# most recently used directory from the runtime config.
|
212
|
-
process_files([ @
|
279
|
+
process_files([ @db['config']['import_dir'] ], :import)
|
213
280
|
else
|
214
281
|
process_files(args, :import)
|
215
282
|
if args.length == 1 && Dir.exists?(args[0])
|
216
283
|
# If only one directory was specified as argument we store the
|
217
284
|
# directory for future use.
|
218
|
-
@
|
285
|
+
@db['config']['import_dir'] = args[0]
|
219
286
|
end
|
220
287
|
end
|
221
288
|
when 'list'
|
222
|
-
@
|
289
|
+
@ffs.list_activities
|
223
290
|
when 'records'
|
224
|
-
@
|
291
|
+
puts @records.to_s
|
225
292
|
when 'rename'
|
226
293
|
unless (@name = args.shift)
|
227
294
|
Log.fatal 'You must provide a new name for the activity'
|
@@ -237,7 +304,7 @@ EOT
|
|
237
304
|
process_activities(args, :set)
|
238
305
|
when 'show'
|
239
306
|
if args.empty?
|
240
|
-
@
|
307
|
+
@ffs.show_list_in_browser
|
241
308
|
else
|
242
309
|
process_activities(args, :show)
|
243
310
|
end
|
@@ -277,7 +344,7 @@ EOT
|
|
277
344
|
|
278
345
|
activity_refs.each do |a_ref|
|
279
346
|
if a_ref[0] == ':'
|
280
|
-
activities = @
|
347
|
+
activities = @ffs.find(a_ref[1..-1])
|
281
348
|
if activities.empty?
|
282
349
|
Log.warn "No matching activities found for '#{a_ref}'"
|
283
350
|
return
|
@@ -287,6 +354,8 @@ EOT
|
|
287
354
|
Log.fatal "Activity references must start with ':': #{a_ref}"
|
288
355
|
end
|
289
356
|
end
|
357
|
+
|
358
|
+
nil
|
290
359
|
end
|
291
360
|
|
292
361
|
def process_files(files_or_dirs, command)
|
@@ -334,9 +403,9 @@ EOT
|
|
334
403
|
end
|
335
404
|
|
336
405
|
if fit_entity.is_a?(Fit4Ruby::Activity)
|
337
|
-
return @
|
338
|
-
elsif fit_entity.is_a?(Fit4Ruby::Monitoring_B)
|
339
|
-
|
406
|
+
return @ffs.add_fit_file(fit_file_name, fit_entity, @force)
|
407
|
+
#elsif fit_entity.is_a?(Fit4Ruby::Monitoring_B)
|
408
|
+
# return @monitoring.add(fit_file_name, fit_entity)
|
340
409
|
else
|
341
410
|
Log.error "#{fit_file_name} is not a recognized FIT file"
|
342
411
|
return false
|
@@ -348,13 +417,15 @@ EOT
|
|
348
417
|
when :check
|
349
418
|
activity.check
|
350
419
|
when :delete
|
351
|
-
@
|
420
|
+
@ffs.delete_activity(activity)
|
352
421
|
when :dump
|
353
422
|
activity.dump(@filter)
|
423
|
+
when :events
|
424
|
+
activity.events
|
354
425
|
when :rename
|
355
|
-
@
|
426
|
+
@ffs.rename_activity(activity, @name)
|
356
427
|
when :set
|
357
|
-
@
|
428
|
+
@ffs.set_activity_attribute(activity, @attribute, @value)
|
358
429
|
when :show
|
359
430
|
activity.show
|
360
431
|
when :sources
|
@@ -375,9 +446,9 @@ EOT
|
|
375
446
|
Log.fatal("You must specify 'metric' or 'statute' as unit system.")
|
376
447
|
end
|
377
448
|
|
378
|
-
if @
|
379
|
-
@
|
380
|
-
@
|
449
|
+
if @db['config']['unit_system'].to_s != args[0]
|
450
|
+
@db['config']['unit_system'] = args[0].to_sym
|
451
|
+
@ffs.change_unit_system
|
381
452
|
end
|
382
453
|
end
|
383
454
|
|
@@ -386,16 +457,16 @@ EOT
|
|
386
457
|
Log.fatal('You must specify a directory')
|
387
458
|
end
|
388
459
|
|
389
|
-
if @
|
390
|
-
@
|
391
|
-
@
|
392
|
-
@
|
460
|
+
if @db['config']['html_dir'] != args[0]
|
461
|
+
@db['config']['html_dir'] = args[0]
|
462
|
+
@ffs.create_directories
|
463
|
+
@ffs.generate_all_html_reports
|
393
464
|
end
|
394
465
|
end
|
395
466
|
|
396
467
|
def update_gps_data
|
397
468
|
epo_dir = File.join(@db_dir, 'epo')
|
398
|
-
|
469
|
+
create_directory(epo_dir, 'GPS Data Cache')
|
399
470
|
epo_file = File.join(epo_dir, 'EPO.BIN')
|
400
471
|
|
401
472
|
if !File.exists?(epo_file) ||
|
@@ -403,7 +474,7 @@ EOT
|
|
403
474
|
# The EPO file only changes every 6 hours. No need to download it more
|
404
475
|
# frequently if it already exists.
|
405
476
|
if EPO_Downloader.new.download(epo_file)
|
406
|
-
unless (remotesw_dir = @
|
477
|
+
unless (remotesw_dir = @db['config']['import_dir'])
|
407
478
|
Log.error "No device directory set. Please import an activity " +
|
408
479
|
"from your device first."
|
409
480
|
return
|
@@ -426,14 +497,45 @@ EOT
|
|
426
497
|
end
|
427
498
|
|
428
499
|
def handle_version_update
|
429
|
-
if @
|
500
|
+
if @db['config']['version'] != VERSION
|
430
501
|
Log.warn "PostRunner version upgrade detected."
|
431
|
-
@
|
432
|
-
@
|
502
|
+
@ffs.handle_version_update
|
503
|
+
@db['config']['version'] = VERSION
|
433
504
|
Log.info "Version upgrade completed."
|
434
505
|
end
|
435
506
|
end
|
436
507
|
|
508
|
+
# Earlier versions of PostRunner used a YAML file to store the activity
|
509
|
+
# data. This method transfers the data from the old storage to the new
|
510
|
+
# FitFileStore based database.
|
511
|
+
def import_legacy_archive
|
512
|
+
old_fit_dir = File.join(@db_dir, 'old_fit_dir')
|
513
|
+
create_directory(old_fit_dir, 'Old Fit')
|
514
|
+
|
515
|
+
cfg = RuntimeConfig.new(@db_dir)
|
516
|
+
activities = ActivitiesDB.new(@db_dir, cfg).activities
|
517
|
+
# Ensure that activities are sorted from earliest to last to properly
|
518
|
+
# recognize the personal records during import.
|
519
|
+
activities.sort! { |a1, a2| a1.timestamp <=> a2.timestamp }
|
520
|
+
activities.each do |activity|
|
521
|
+
file_name = File.join(@db_dir, 'fit', activity.fit_file)
|
522
|
+
next unless File.exists?(file_name)
|
523
|
+
|
524
|
+
Log.info "Converting #{activity.fit_file} to new DB format"
|
525
|
+
@db.transaction do
|
526
|
+
unless (new_activity = @ffs.add_fit_file(file_name))
|
527
|
+
Log.warn "Cannot convert #{file_name} to new database format"
|
528
|
+
next
|
529
|
+
end
|
530
|
+
new_activity.sport = activity.sport
|
531
|
+
new_activity.sub_sport = activity.sub_sport
|
532
|
+
new_activity.name = activity.name
|
533
|
+
new_activity.norecord = activity.norecord
|
534
|
+
FileUtils.move(file_name, File.join(old_fit_dir, activity.fit_file))
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
437
539
|
end
|
438
540
|
|
439
541
|
end
|