postrunner 0.0.2 → 0.0.3
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/COPYING +339 -0
- data/Rakefile +6 -0
- data/lib/postrunner/ActivitiesDB.rb +113 -37
- data/lib/postrunner/Activity.rb +55 -12
- data/lib/postrunner/ActivityReport.rb +102 -0
- data/lib/postrunner/ActivityView.rb +133 -0
- data/lib/postrunner/ChartView.rb +195 -0
- data/lib/postrunner/FlexiTable.rb +289 -0
- data/lib/postrunner/HTMLBuilder.rb +67 -0
- data/lib/postrunner/Main.rb +74 -34
- data/lib/postrunner/PersonalRecords.rb +145 -0
- data/lib/postrunner/TrackView.rb +129 -0
- data/lib/postrunner/ViewWidgets.rb +46 -0
- data/lib/postrunner/version.rb +1 -1
- data/lib/postrunner.rb +0 -1
- data/postrunner.gemspec +5 -0
- data/spec/FlexiTable_spec.rb +14 -0
- data/spec/PostRunner_spec.rb +122 -0
- metadata +72 -4
- data/lib/postrunner/RuntimeConfig.rb +0 -20
@@ -0,0 +1,289 @@
|
|
1
|
+
require 'postrunner/HTMLBuilder'
|
2
|
+
|
3
|
+
module PostRunner
|
4
|
+
|
5
|
+
class FlexiTable
|
6
|
+
|
7
|
+
class Attributes
|
8
|
+
|
9
|
+
attr_accessor :min_terminal_width, :halign
|
10
|
+
|
11
|
+
def initialize(attrs = {})
|
12
|
+
@min_terminal_width = nil
|
13
|
+
@halign = nil
|
14
|
+
|
15
|
+
attrs.each do |name, value|
|
16
|
+
ivar_name = '@' + name.to_s
|
17
|
+
unless instance_variable_defined?(ivar_name)
|
18
|
+
Log.fatal "Unsupported attribute #{name}"
|
19
|
+
end
|
20
|
+
instance_variable_set(ivar_name, value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](name)
|
25
|
+
ivar_name = '@' + name.to_s
|
26
|
+
return nil unless instance_variable_defined?(ivar_name)
|
27
|
+
|
28
|
+
instance_variable_get(ivar_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
class Cell
|
34
|
+
|
35
|
+
def initialize(table, row, content, attributes)
|
36
|
+
@table = table
|
37
|
+
@row = row
|
38
|
+
@content = content
|
39
|
+
@attributes = attributes
|
40
|
+
|
41
|
+
@column_index = nil
|
42
|
+
@row_index = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def min_terminal_width
|
46
|
+
@content.to_s.length
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_indicies(col_idx, row_idx)
|
50
|
+
@column_index = col_idx
|
51
|
+
@row_index = row_idx
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
s = @content.to_s
|
56
|
+
width = get_attribute(:min_terminal_width)
|
57
|
+
case get_attribute(:halign)
|
58
|
+
when :left, nil
|
59
|
+
s + ' ' * (width - s.length)
|
60
|
+
when :right
|
61
|
+
' ' * (width - s.length) + s
|
62
|
+
when :center
|
63
|
+
w = width - s.length
|
64
|
+
left_padding = w / 2
|
65
|
+
right_padding = w / 2 + w % 2
|
66
|
+
' ' * left_padding + s + ' ' * right_padding
|
67
|
+
else
|
68
|
+
raise "Unknown alignment"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_html(doc)
|
73
|
+
doc.td(@content.respond_to?('to_html') ?
|
74
|
+
@content.to_html(doc) : @content.to_s)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def get_attribute(name)
|
80
|
+
@attributes[name] ||
|
81
|
+
@row.attributes[name] ||
|
82
|
+
@table.column_attributes[@column_index][name]
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
class Row < Array
|
88
|
+
|
89
|
+
attr_reader :attributes
|
90
|
+
|
91
|
+
def initialize(table)
|
92
|
+
@table = table
|
93
|
+
@attributes = Attributes.new
|
94
|
+
super()
|
95
|
+
end
|
96
|
+
|
97
|
+
def cell(content, attributes)
|
98
|
+
c = Cell.new(@table, self, content, attributes)
|
99
|
+
self << c
|
100
|
+
c
|
101
|
+
end
|
102
|
+
|
103
|
+
def set_indicies(col_idx, row_idx)
|
104
|
+
self[col_idx].set_indicies(col_idx, row_idx)
|
105
|
+
end
|
106
|
+
|
107
|
+
def set_row_attributes(attributes)
|
108
|
+
@attributes = Attributes.new(attributes)
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_s
|
112
|
+
s = ''
|
113
|
+
frame = @table.frame
|
114
|
+
|
115
|
+
s << '|' if frame
|
116
|
+
s << join(frame ? '|' : ' ')
|
117
|
+
s << '|' if frame
|
118
|
+
|
119
|
+
s
|
120
|
+
end
|
121
|
+
|
122
|
+
def to_html(doc)
|
123
|
+
doc.tr {
|
124
|
+
each { |c| c.to_html(doc) }
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
attr_reader :frame, :column_attributes
|
131
|
+
|
132
|
+
def initialize(&block)
|
133
|
+
@head_rows = []
|
134
|
+
@body_rows = []
|
135
|
+
@foot_rows = []
|
136
|
+
@column_count = 0
|
137
|
+
|
138
|
+
@current_section = :body
|
139
|
+
@current_row = nil
|
140
|
+
|
141
|
+
@frame = true
|
142
|
+
|
143
|
+
@column_attributes = []
|
144
|
+
|
145
|
+
instance_eval(&block) if block_given?
|
146
|
+
end
|
147
|
+
|
148
|
+
def head
|
149
|
+
@current_section = :head
|
150
|
+
end
|
151
|
+
|
152
|
+
def body
|
153
|
+
@current_section = :body
|
154
|
+
end
|
155
|
+
|
156
|
+
def foot
|
157
|
+
@current_section = :foot
|
158
|
+
end
|
159
|
+
|
160
|
+
def new_row
|
161
|
+
@current_row = nil
|
162
|
+
end
|
163
|
+
|
164
|
+
def cell(content, attributes = {})
|
165
|
+
if @current_row.nil?
|
166
|
+
case @current_section
|
167
|
+
when :head
|
168
|
+
@head_rows
|
169
|
+
when :body
|
170
|
+
@body_rows
|
171
|
+
when :foot
|
172
|
+
@foot_rows
|
173
|
+
else
|
174
|
+
raise "Unknown section #{@current_section}"
|
175
|
+
end << (@current_row = Row.new(self))
|
176
|
+
end
|
177
|
+
@current_row.cell(content, attributes)
|
178
|
+
end
|
179
|
+
|
180
|
+
def row(cells, attributes = {})
|
181
|
+
cells.each { |c| cell(c) }
|
182
|
+
set_row_attributes(attributes)
|
183
|
+
new_row
|
184
|
+
end
|
185
|
+
|
186
|
+
def set_column_attributes(col_attributes)
|
187
|
+
col_attributes.each.with_index do |ca, idx|
|
188
|
+
@column_attributes[idx] = Attributes.new(ca)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def set_row_attributes(row_attributes)
|
193
|
+
unless @current_row
|
194
|
+
raise "No current row. Use after first cell definition but before " +
|
195
|
+
"new_row call."
|
196
|
+
end
|
197
|
+
@current_row.set_row_attributes(row_attributes)
|
198
|
+
end
|
199
|
+
|
200
|
+
def enable_frame(enabled)
|
201
|
+
@frame = enabled
|
202
|
+
end
|
203
|
+
|
204
|
+
def to_s
|
205
|
+
index_table
|
206
|
+
calc_terminal_columns
|
207
|
+
|
208
|
+
s = frame_line_to_s
|
209
|
+
s << rows_to_s(@head_rows)
|
210
|
+
s << frame_line_to_s unless @head_rows.empty?
|
211
|
+
s << rows_to_s(@body_rows)
|
212
|
+
s << frame_line_to_s unless @body_rows.empty?
|
213
|
+
s << rows_to_s(@foot_rows)
|
214
|
+
s << frame_line_to_s unless @foot_rows.empty?
|
215
|
+
|
216
|
+
s
|
217
|
+
end
|
218
|
+
|
219
|
+
def to_html(doc)
|
220
|
+
doc.table {
|
221
|
+
@head_rows.each { |r| r.to_html(doc) }
|
222
|
+
@body_rows.each { |r| r.to_html(doc) }
|
223
|
+
@foot_rows.each { |r| r.to_html(doc) }
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
|
229
|
+
def index_table
|
230
|
+
@column_count = (@head_rows[0] || @body_rows[0]).length
|
231
|
+
|
232
|
+
@column_count.times do |i|
|
233
|
+
index_table_rows(i, @head_rows)
|
234
|
+
index_table_rows(i, @body_rows)
|
235
|
+
index_table_rows(i, @foot_rows)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def index_table_rows(col_idx, rows)
|
240
|
+
rows.each.with_index do |r, row_idx|
|
241
|
+
r.set_indicies(col_idx, row_idx)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def calc_terminal_columns
|
246
|
+
@column_count.times do |i|
|
247
|
+
col_mtw = nil
|
248
|
+
|
249
|
+
col_mtw = calc_section_teminal_columns(i, col_mtw, @head_rows)
|
250
|
+
col_mtw = calc_section_teminal_columns(i, col_mtw, @body_rows)
|
251
|
+
col_mtw = calc_section_teminal_columns(i, col_mtw, @foot_rows)
|
252
|
+
|
253
|
+
@column_attributes[i] = Attributes.new unless @column_attributes[i]
|
254
|
+
@column_attributes[i].min_terminal_width = col_mtw
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def calc_section_teminal_columns(col_idx, col_mtw, rows)
|
259
|
+
rows.each do |r|
|
260
|
+
if r[col_idx].nil?
|
261
|
+
raise ArgumentError, "Not all rows have same number of cells"
|
262
|
+
end
|
263
|
+
|
264
|
+
mtw = r[col_idx].min_terminal_width
|
265
|
+
if col_mtw.nil? || col_mtw < mtw
|
266
|
+
col_mtw = mtw
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
col_mtw
|
271
|
+
end
|
272
|
+
|
273
|
+
def rows_to_s(x_rows)
|
274
|
+
x_rows.empty? ? '' : (x_rows.map { |r| r.to_s}.join("\n") + "\n")
|
275
|
+
end
|
276
|
+
|
277
|
+
def frame_line_to_s
|
278
|
+
return '' unless @frame
|
279
|
+
s = '+'
|
280
|
+
@column_attributes.each do |c|
|
281
|
+
s += '-' * c.min_terminal_width + '+'
|
282
|
+
end
|
283
|
+
s + "\n"
|
284
|
+
end
|
285
|
+
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module PostRunner
|
4
|
+
|
5
|
+
# Nokogiri is great, but I don't like the HTMLBuilder interface. This class
|
6
|
+
# is a wrapper around Nokogiri that provides a more Ruby-like interface.
|
7
|
+
class HTMLBuilder
|
8
|
+
|
9
|
+
# Create a new HTMLBuilder object.
|
10
|
+
def initialize
|
11
|
+
# This is the Nokogiri Document that will store all the data.
|
12
|
+
@doc = Nokogiri::HTML::Document.new
|
13
|
+
# We only need to keep a stack of the currently edited nodes so we know
|
14
|
+
# where we are in the node tree.
|
15
|
+
@node_stack = []
|
16
|
+
end
|
17
|
+
|
18
|
+
# Any call to an undefined method will create a HTML node of the same
|
19
|
+
# name.
|
20
|
+
def method_missing(method_name, *args)
|
21
|
+
node = Nokogiri::XML::Node.new(method_name.to_s, @doc)
|
22
|
+
if (parent = @node_stack.last)
|
23
|
+
parent.add_child(node)
|
24
|
+
else
|
25
|
+
@doc.add_child(node)
|
26
|
+
end
|
27
|
+
@node_stack.push(node)
|
28
|
+
|
29
|
+
args.each do |arg|
|
30
|
+
if arg.is_a?(String)
|
31
|
+
node.add_child(Nokogiri::XML::Text.new(arg, @doc))
|
32
|
+
elsif arg.is_a?(Hash)
|
33
|
+
# Hash arguments are attribute sets for the node. We just pass them
|
34
|
+
# directly to the node.
|
35
|
+
arg.each { |k, v| node[k] = v }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
yield if block_given?
|
40
|
+
@node_stack.pop
|
41
|
+
end
|
42
|
+
|
43
|
+
# Only needed to comply with style guides. This all calls to unknown
|
44
|
+
# method will be handled properly. So, we always return true.
|
45
|
+
def respond_to?(method)
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
# Dump the HTML document as HTML formatted String.
|
50
|
+
def to_html
|
51
|
+
@doc.to_html
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def add_child(parent, node)
|
57
|
+
if parent
|
58
|
+
parent.add_child(node)
|
59
|
+
else
|
60
|
+
@doc.add_child(node)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
data/lib/postrunner/Main.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
require 'logger'
|
3
3
|
require 'fit4ruby'
|
4
|
-
|
4
|
+
|
5
|
+
require 'postrunner/version'
|
5
6
|
require 'postrunner/ActivitiesDB'
|
6
7
|
|
7
8
|
module PostRunner
|
@@ -11,15 +12,19 @@ module PostRunner
|
|
11
12
|
Log.formatter = proc { |severity, datetime, progname, msg|
|
12
13
|
"#{severity == Logger::INFO ? '' : "#{severity}:"} #{msg}\n"
|
13
14
|
}
|
15
|
+
Log.level = Logger::INFO
|
14
16
|
|
15
17
|
class Main
|
16
18
|
|
17
19
|
def initialize(args)
|
18
20
|
@filter = nil
|
19
21
|
@name = nil
|
20
|
-
@activities =
|
22
|
+
@activities = nil
|
23
|
+
@db_dir = File.join(ENV['HOME'], '.postrunner')
|
24
|
+
|
25
|
+
return if (args = parse_options(args)).nil?
|
21
26
|
|
22
|
-
execute_command(
|
27
|
+
execute_command(args)
|
23
28
|
end
|
24
29
|
|
25
30
|
private
|
@@ -72,25 +77,30 @@ EOT
|
|
72
77
|
|
73
78
|
opts.separator ""
|
74
79
|
opts.separator "General options:"
|
80
|
+
opts.on('--dbdir dir', String,
|
81
|
+
'Directory for the activity database and related files') do |d|
|
82
|
+
@db_dir = d
|
83
|
+
end
|
75
84
|
opts.on('-v', '--verbose',
|
76
85
|
'Show internal messages helpful for debugging problems') do
|
77
86
|
Log.level = Logger::DEBUG
|
78
87
|
end
|
79
88
|
opts.on('-h', '--help', 'Show this message') do
|
80
89
|
$stderr.puts opts
|
81
|
-
|
90
|
+
return nil
|
82
91
|
end
|
83
92
|
opts.on('--version', 'Show version number') do
|
84
93
|
$stderr.puts VERSION
|
85
|
-
|
94
|
+
return nil
|
86
95
|
end
|
87
96
|
|
88
97
|
opts.separator <<"EOT"
|
89
98
|
|
90
99
|
Commands:
|
91
100
|
|
92
|
-
check <fit file> ...
|
93
|
-
Check the provided FIT file(s) for structural errors.
|
101
|
+
check [ <fit file> | <ref> ... ]
|
102
|
+
Check the provided FIT file(s) for structural errors. If no file or
|
103
|
+
reference is provided, the complete archive is checked.
|
94
104
|
|
95
105
|
dump <fit file> | <ref>
|
96
106
|
Dump the content of the FIT file.
|
@@ -104,11 +114,17 @@ delete <ref>
|
|
104
114
|
list
|
105
115
|
List all FIT files stored in the data base.
|
106
116
|
|
117
|
+
records
|
118
|
+
List all personal records.
|
119
|
+
|
107
120
|
rename <ref>
|
108
121
|
Replace the FIT file name with a more meaningful name that describes
|
109
122
|
the activity.
|
110
123
|
|
111
|
-
|
124
|
+
show <ref>
|
125
|
+
Show the FIT activity in a web browser.
|
126
|
+
|
127
|
+
summary <ref>
|
112
128
|
Display the summary information for the FIT file.
|
113
129
|
EOT
|
114
130
|
|
@@ -118,9 +134,15 @@ EOT
|
|
118
134
|
end
|
119
135
|
|
120
136
|
def execute_command(args)
|
137
|
+
@activities = ActivitiesDB.new(@db_dir)
|
138
|
+
|
121
139
|
case (cmd = args.shift)
|
122
140
|
when 'check'
|
123
|
-
|
141
|
+
if args.empty?
|
142
|
+
@activities.check
|
143
|
+
else
|
144
|
+
process_files_or_activities(args, :check)
|
145
|
+
end
|
124
146
|
when 'delete'
|
125
147
|
process_activities(args, :delete)
|
126
148
|
when 'dump'
|
@@ -130,10 +152,14 @@ EOT
|
|
130
152
|
process_files(args, :import)
|
131
153
|
when 'list'
|
132
154
|
@activities.list
|
155
|
+
when 'records'
|
156
|
+
@activities.show_records
|
133
157
|
when 'rename'
|
134
158
|
process_activities(args, :rename)
|
159
|
+
when 'show'
|
160
|
+
process_activities(args, :show)
|
135
161
|
when 'summary'
|
136
|
-
|
162
|
+
process_activities(args, :summary)
|
137
163
|
when nil
|
138
164
|
Log.fatal("No command provided. " +
|
139
165
|
"See 'postrunner -h' for more information.")
|
@@ -146,30 +172,24 @@ EOT
|
|
146
172
|
def process_files_or_activities(files_or_activities, command)
|
147
173
|
files_or_activities.each do |foa|
|
148
174
|
if foa[0] == ':'
|
149
|
-
|
150
|
-
if files.empty?
|
151
|
-
Log.warn "No matching activities found for '#{foa}'"
|
152
|
-
return
|
153
|
-
end
|
154
|
-
|
155
|
-
process_files(files, command)
|
175
|
+
process_activities([ foa ], command)
|
156
176
|
else
|
157
177
|
process_files([ foa ], command)
|
158
178
|
end
|
159
179
|
end
|
160
180
|
end
|
161
181
|
|
162
|
-
def process_activities(
|
163
|
-
|
164
|
-
if
|
165
|
-
|
166
|
-
if
|
167
|
-
Log.warn "No matching activities found for '#{
|
182
|
+
def process_activities(activity_refs, command)
|
183
|
+
activity_refs.each do |a_ref|
|
184
|
+
if a_ref[0] == ':'
|
185
|
+
activities = @activities.find(a_ref[1..-1])
|
186
|
+
if activities.empty?
|
187
|
+
Log.warn "No matching activities found for '#{a_ref}'"
|
168
188
|
return
|
169
189
|
end
|
170
|
-
|
190
|
+
activities.each { |a| process_activity(a, command) }
|
171
191
|
else
|
172
|
-
Log.fatal "Activity references must start with ':': #{
|
192
|
+
Log.fatal "Activity references must start with ':': #{a_ref}"
|
173
193
|
end
|
174
194
|
end
|
175
195
|
|
@@ -193,19 +213,39 @@ EOT
|
|
193
213
|
|
194
214
|
def process_file(file, command)
|
195
215
|
case command
|
196
|
-
when :
|
197
|
-
|
216
|
+
when :check, :dump
|
217
|
+
read_fit_file(file)
|
198
218
|
when :import
|
199
219
|
@activities.add(file)
|
220
|
+
else
|
221
|
+
Log.fatal("Unknown file command #{command}")
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def process_activity(activity, command)
|
226
|
+
case command
|
227
|
+
when :check
|
228
|
+
activity.check
|
229
|
+
when :delete
|
230
|
+
@activities.delete(activity)
|
231
|
+
when :dump
|
232
|
+
activity.dump(@filter)
|
200
233
|
when :rename
|
201
|
-
@activities.rename(
|
234
|
+
@activities.rename(activity, @name)
|
235
|
+
when :show
|
236
|
+
activity.show
|
237
|
+
when :summary
|
238
|
+
activity.summary
|
202
239
|
else
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
240
|
+
Log.fatal("Unknown activity command #{command}")
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def read_fit_file(fit_file)
|
245
|
+
begin
|
246
|
+
return Fit4Ruby::read(fit_file, @filter)
|
247
|
+
rescue StandardError
|
248
|
+
Log.error("Cannot read FIT file '#{fit_file}': #{$!}")
|
209
249
|
end
|
210
250
|
end
|
211
251
|
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'fit4ruby'
|
5
|
+
|
6
|
+
module PostRunner
|
7
|
+
|
8
|
+
class PersonalRecords
|
9
|
+
|
10
|
+
class Record
|
11
|
+
|
12
|
+
attr_accessor :distance, :duration, :start_time, :fit_file
|
13
|
+
|
14
|
+
def initialize(distance, duration, start_time, fit_file)
|
15
|
+
@distance = distance
|
16
|
+
@duration = duration
|
17
|
+
@start_time = start_time
|
18
|
+
@fit_file = fit_file
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
include Fit4Ruby::Converters
|
24
|
+
|
25
|
+
def initialize(activities)
|
26
|
+
@activities = activities
|
27
|
+
@db_dir = activities.db_dir
|
28
|
+
@records_file = File.join(@db_dir, 'records.yml')
|
29
|
+
@records = []
|
30
|
+
|
31
|
+
load_records
|
32
|
+
end
|
33
|
+
|
34
|
+
def register_result(distance, duration, start_time, fit_file)
|
35
|
+
@records.each do |record|
|
36
|
+
if record.duration > 0
|
37
|
+
if duration > 0
|
38
|
+
# This is a speed record for a popular distance.
|
39
|
+
if distance == record.distance
|
40
|
+
if duration < record.duration
|
41
|
+
record.duration = duration
|
42
|
+
record.start_time = start_time
|
43
|
+
record.fit_file = fit_file
|
44
|
+
Log.info "New record for #{distance} m in " +
|
45
|
+
"#{secsToHMS(duration)}"
|
46
|
+
return true
|
47
|
+
else
|
48
|
+
# No new record for this distance.
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
else
|
54
|
+
if distance > record.distance
|
55
|
+
# This is a new distance record.
|
56
|
+
record.distance = distance
|
57
|
+
record.duration = 0
|
58
|
+
record.start_time = start_time
|
59
|
+
record.fit_file = fit_file
|
60
|
+
Log.info "New distance record #{distance} m"
|
61
|
+
return true
|
62
|
+
else
|
63
|
+
# No new distance record.
|
64
|
+
return false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# We have not found a record.
|
70
|
+
@records << Record.new(distance, duration, start_time, fit_file)
|
71
|
+
if duration == 0
|
72
|
+
Log.info "New distance record #{distance} m"
|
73
|
+
else
|
74
|
+
Log.info "New record for #{distance}m in #{secsToHMS(duration)}"
|
75
|
+
end
|
76
|
+
|
77
|
+
true
|
78
|
+
end
|
79
|
+
|
80
|
+
def delete_activity(fit_file)
|
81
|
+
@records.delete_if { |r| r.fit_file == fit_file }
|
82
|
+
end
|
83
|
+
|
84
|
+
def sync
|
85
|
+
save_records
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_s
|
89
|
+
record_names = { 1000.0 => '1 km', 1609.0 => '1 mi', 5000.0 => '5 km',
|
90
|
+
21097.5 => '1/2 Marathon', 42195.0 => 'Marathon' }
|
91
|
+
t = FlexiTable.new
|
92
|
+
t.head
|
93
|
+
t.row([ 'Record', 'Time/Dist.', 'Avg. Pace', 'Ref.', 'Activity', 'Date' ],
|
94
|
+
{ :halign => :center })
|
95
|
+
t.set_column_attributes([
|
96
|
+
{},
|
97
|
+
{ :halign => :right },
|
98
|
+
{ :halign => :right },
|
99
|
+
{ :halign => :right },
|
100
|
+
{ :halign => :right },
|
101
|
+
{ :halign => :left }
|
102
|
+
])
|
103
|
+
t.body
|
104
|
+
@records.sort { |r1, r2| r1.distance <=> r2.distance }.each do |r|
|
105
|
+
activity = @activities.activity_by_fit_file(r.fit_file)
|
106
|
+
t.row((r.duration == 0 ?
|
107
|
+
[ 'Longest Run', '%.1f m' % r.distance, '-' ] :
|
108
|
+
[ record_names[r.distance], secsToHMS(r.duration),
|
109
|
+
speedToPace(r.distance / r.duration) ]) +
|
110
|
+
[ @activities.ref_by_fit_file(r.fit_file),
|
111
|
+
activity.name, r.start_time.strftime("%Y-%m-%d") ])
|
112
|
+
end
|
113
|
+
t.to_s
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def load_records
|
119
|
+
begin
|
120
|
+
if File.exists?(@records_file)
|
121
|
+
@records = YAML.load_file(@records_file)
|
122
|
+
else
|
123
|
+
Log.info "No records file found at '#{@records_file}'"
|
124
|
+
end
|
125
|
+
rescue StandardError
|
126
|
+
Log.fatal "Cannot load records file '#{@records_file}': #{$!}"
|
127
|
+
end
|
128
|
+
|
129
|
+
unless @records.is_a?(Array)
|
130
|
+
Log.fatal "The personal records file '#{@records_file}' is corrupted"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def save_records
|
135
|
+
begin
|
136
|
+
File.open(@records_file, 'w') { |f| f.write(@records.to_yaml) }
|
137
|
+
rescue StandardError
|
138
|
+
Log.fatal "Cannot write records file '#{@records_file}': #{$!}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|