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