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
@@ -0,0 +1,103 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = NavButtonRow.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/HTMLBuilder'
|
14
|
+
|
15
|
+
module PostRunner
|
16
|
+
|
17
|
+
# Auxilliary class that stores the name of an icon file and a URL as a
|
18
|
+
# String. It is used to describe a NavButtonRow button.
|
19
|
+
class NavButtonDef < Struct.new(:icon, :url)
|
20
|
+
end
|
21
|
+
|
22
|
+
# A NavButtonRow is a row of buttons used to navigate between HTML pages.
|
23
|
+
class NavButtonRow
|
24
|
+
|
25
|
+
# A class to store the icon and URL of a button in the NavButtonRow
|
26
|
+
# objects.
|
27
|
+
class Button
|
28
|
+
|
29
|
+
# Create a Button object.
|
30
|
+
# @param icon [String] File name of the icon file
|
31
|
+
# @param url [String] URL of the page to change to
|
32
|
+
def initialize(icon, url = nil)
|
33
|
+
@icon = icon
|
34
|
+
@url = url
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add the object as HTML Elements to the document.
|
38
|
+
# @param doc [HTMLBuilder] XML Document
|
39
|
+
def to_html(doc)
|
40
|
+
if @url
|
41
|
+
doc.a({ :href => @url }) {
|
42
|
+
doc.img({ :src => "icons/#{@icon}", :class => 'active_button' })
|
43
|
+
}
|
44
|
+
else
|
45
|
+
doc.img({ :src => "icons/#{@icon}", :class => 'inactive_button' })
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create a new NavButtonRow object.
|
52
|
+
# @param float [String, Nil] specifies if the HTML representation should
|
53
|
+
# be a floating object that floats left or right.
|
54
|
+
def initialize(float = nil)
|
55
|
+
unless float.nil? || %w( left right ).include?(float)
|
56
|
+
raise ArgumentError "float argument must be nil, 'left' or 'right'"
|
57
|
+
end
|
58
|
+
|
59
|
+
@float = float
|
60
|
+
@buttons = []
|
61
|
+
end
|
62
|
+
|
63
|
+
# Add a new button to the NavButtonRow object.
|
64
|
+
# @param icon [String] File name of the icon file
|
65
|
+
# @param url [String] URL of the page to change to
|
66
|
+
def addButton(icon, url = nil)
|
67
|
+
@buttons << Button.new(icon, url)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Add the object as HTML Elements to the document.
|
71
|
+
# @param doc [HTMLBuilder] XML Document
|
72
|
+
def to_html(doc)
|
73
|
+
doc.unique(:nav_button_row_style) {
|
74
|
+
doc.head { doc.style(style) }
|
75
|
+
}
|
76
|
+
doc.div({ :class => 'nav_button_row',
|
77
|
+
:style => "width: #{@buttons.length * (32 + 10)}px; " +
|
78
|
+
"#{@float ? "float: #{@float};" :
|
79
|
+
'margin-left: auto; margin-right: auto'}"}) {
|
80
|
+
@buttons.each { |btn| btn.to_html(doc) }
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def style
|
87
|
+
<<"EOT"
|
88
|
+
.nav_button_row {
|
89
|
+
padding: 3px 30px;
|
90
|
+
}
|
91
|
+
.active_button {
|
92
|
+
padding: 5px;
|
93
|
+
}
|
94
|
+
.inactive_button {
|
95
|
+
padding: 5px;
|
96
|
+
opacity: 0.4;
|
97
|
+
}
|
98
|
+
EOT
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = PagingButtons.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/NavButtonRow'
|
14
|
+
|
15
|
+
module PostRunner
|
16
|
+
|
17
|
+
# A class to generate a set of forward/backward buttons for an HTML page. It
|
18
|
+
# can also include jump to first/last buttons.
|
19
|
+
class PagingButtons
|
20
|
+
|
21
|
+
# Create a new PagingButtons object.
|
22
|
+
# @param page_urls [Array of String] Sorted list of all possible pages
|
23
|
+
# @param end_buttons [Boolean] If true jump to first/last buttons are
|
24
|
+
# included
|
25
|
+
def initialize(page_urls, end_buttons = true)
|
26
|
+
if page_urls.empty?
|
27
|
+
raise ArgumentError.new("'page_urls' must not be empty")
|
28
|
+
end
|
29
|
+
@pages = page_urls
|
30
|
+
@current_page_index = 0
|
31
|
+
@end_buttons = end_buttons
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return the URL of the current page
|
35
|
+
def current_page
|
36
|
+
@pages[@current_page_index]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Set the URL for the current page. It must be included in the URL set
|
40
|
+
# passed at creation time. The forward/backward links will be derived from
|
41
|
+
# the setting of the current page.
|
42
|
+
# @param page_url [String] URL of the page
|
43
|
+
def current_page=(page_url)
|
44
|
+
unless (@current_page_index = @pages.index(page_url))
|
45
|
+
raise ArgumentError.new("URL #{page_url} is not a known page URL")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Iterate over all buttons. A NavButtonDef object is passed to the block
|
50
|
+
# that contains the icon and URL for the button. If no URL is set, the
|
51
|
+
# button is inactive.
|
52
|
+
def each
|
53
|
+
%w( first back forward last ).each do |button_name|
|
54
|
+
button = NavButtonDef.new
|
55
|
+
button.icon = button_name + '.png'
|
56
|
+
button.url =
|
57
|
+
case button_name
|
58
|
+
when 'first'
|
59
|
+
@current_page_index == 0 || !@end_buttons ? nil : @pages.first
|
60
|
+
when 'back'
|
61
|
+
@current_page_index == 0 ? nil :
|
62
|
+
@pages[@current_page_index - 1]
|
63
|
+
when 'forward'
|
64
|
+
@current_page_index == @pages.length - 1 ? nil :
|
65
|
+
@pages[@current_page_index + 1]
|
66
|
+
when 'last'
|
67
|
+
@current_page_index == @pages.length - 1 ||
|
68
|
+
!@end_buttons ? nil : @pages.last
|
69
|
+
end
|
70
|
+
|
71
|
+
yield(button)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = PersonalRecords.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,115 +14,349 @@ require 'fileutils'
|
|
14
14
|
require 'yaml'
|
15
15
|
|
16
16
|
require 'fit4ruby'
|
17
|
+
require 'postrunner/BackedUpFile'
|
18
|
+
require 'postrunner/RecordListPageView'
|
19
|
+
require 'postrunner/ActivityLink'
|
17
20
|
|
18
21
|
module PostRunner
|
19
22
|
|
20
23
|
class PersonalRecords
|
21
24
|
|
25
|
+
include Fit4Ruby::Converters
|
26
|
+
|
27
|
+
SpeedRecordDistances = {
|
28
|
+
'cycling' => {
|
29
|
+
5000.0 => '5 km',
|
30
|
+
8000.0 => '8 km',
|
31
|
+
9000.0 => '9 km',
|
32
|
+
10000.0 => '10 km',
|
33
|
+
20000.0 => '20 km',
|
34
|
+
40000.0 => '40 km',
|
35
|
+
80000.0 => '80 km',
|
36
|
+
90000.0 => '90 km',
|
37
|
+
12000.0 => '120 km',
|
38
|
+
18000.0 => '180 km',
|
39
|
+
},
|
40
|
+
'running' => {
|
41
|
+
1000.0 => '1 km',
|
42
|
+
1609.0 => '1 mi',
|
43
|
+
2000.0 => '2 km',
|
44
|
+
3000.0 => '3 km',
|
45
|
+
5000.0 => '5 km',
|
46
|
+
10000.0 => '10 km',
|
47
|
+
20000.0 => '20 km',
|
48
|
+
30000.0 => '30 km',
|
49
|
+
21097.5 => 'Half Marathon',
|
50
|
+
42195.0 => 'Marathon'
|
51
|
+
},
|
52
|
+
'swimming' => {
|
53
|
+
100.0 => '100 m',
|
54
|
+
300.0 => '300 m',
|
55
|
+
400.0 => '400 m',
|
56
|
+
750.0 => '750 m',
|
57
|
+
1500.0 => '1.5 km',
|
58
|
+
1930.0 => '1.2 mi',
|
59
|
+
3000.0 => '3 km',
|
60
|
+
4000.0 => '4 km',
|
61
|
+
3860.0 => '2.4 mi'
|
62
|
+
},
|
63
|
+
'walking' => {
|
64
|
+
1000.0 => '1 km',
|
65
|
+
1609.0 => '1 mi',
|
66
|
+
5000.0 => '5 km',
|
67
|
+
10000.0 => '10 km',
|
68
|
+
21097.5 => 'Half Marathon',
|
69
|
+
42195.0 => 'Marathon'
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
22
73
|
class Record
|
23
74
|
|
24
|
-
|
75
|
+
include Fit4Ruby::Converters
|
76
|
+
|
77
|
+
attr_accessor :activity, :sport, :distance, :duration, :start_time
|
25
78
|
|
26
|
-
def initialize(distance, duration, start_time
|
79
|
+
def initialize(activity, sport, distance, duration, start_time)
|
80
|
+
@activity = activity
|
81
|
+
@sport = sport
|
27
82
|
@distance = distance
|
28
83
|
@duration = duration
|
29
84
|
@start_time = start_time
|
30
|
-
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_table_row(t)
|
88
|
+
t.row((@duration.nil? ?
|
89
|
+
[ 'Longest Run', '%.1f m' % @distance, '-' ] :
|
90
|
+
[ PersonalRecords::SpeedRecordDistances[@sport][@distance],
|
91
|
+
secsToHMS(@duration),
|
92
|
+
speedToPace(@distance / @duration) ]) +
|
93
|
+
[ @activity.db.ref_by_fit_file(@activity.fit_file),
|
94
|
+
ActivityLink.new(@activity, false),
|
95
|
+
@start_time.strftime("%Y-%m-%d") ])
|
31
96
|
end
|
32
97
|
|
33
98
|
end
|
34
99
|
|
35
|
-
|
100
|
+
class RecordSet
|
36
101
|
|
37
|
-
|
38
|
-
@activities = activities
|
39
|
-
@db_dir = activities.db_dir
|
40
|
-
@records_file = File.join(@db_dir, 'records.yml')
|
41
|
-
@records = []
|
102
|
+
include Fit4Ruby::Converters
|
42
103
|
|
43
|
-
|
44
|
-
end
|
104
|
+
attr_reader :year
|
45
105
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
106
|
+
def initialize(sport, year)
|
107
|
+
@sport = sport
|
108
|
+
@year = year
|
109
|
+
@distance = nil
|
110
|
+
@speed_records = {}
|
111
|
+
PersonalRecords::SpeedRecordDistances[@sport].each_key do |dist|
|
112
|
+
@speed_records[dist] = nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def register_result(result)
|
117
|
+
if result.duration
|
118
|
+
# We have a potential speed record for a known distance.
|
119
|
+
unless PersonalRecords::SpeedRecordDistances[@sport].
|
120
|
+
include?(result.distance)
|
121
|
+
Log.fatal "Unknown record distance #{result.distance}"
|
122
|
+
end
|
123
|
+
|
124
|
+
old_record = @speed_records[result.distance]
|
125
|
+
if old_record.nil? || old_record.duration > result.duration
|
126
|
+
@speed_records[result.distance] = result
|
127
|
+
Log.info "New #{@year ? @year.to_s : 'all-time'} " +
|
128
|
+
"#{result.sport} speed record for " +
|
129
|
+
"#{PersonalRecords::SpeedRecordDistances[@sport][
|
130
|
+
result.distance]}: " +
|
131
|
+
"#{secsToHMS(result.duration)}"
|
132
|
+
return true
|
64
133
|
end
|
65
134
|
else
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
record.fit_file = fit_file
|
72
|
-
Log.info "New distance record #{distance} m"
|
135
|
+
# We have a potential distance record.
|
136
|
+
if @distance.nil? || result.distance > @distance.distance
|
137
|
+
@distance = result
|
138
|
+
Log.info "New #{@year ? @year.to_s : 'all-time'} " +
|
139
|
+
"#{result.sport} distance record: #{result.distance} m"
|
73
140
|
return true
|
74
|
-
else
|
75
|
-
# No new distance record.
|
76
|
-
return false
|
77
141
|
end
|
78
142
|
end
|
143
|
+
|
144
|
+
false
|
145
|
+
end
|
146
|
+
|
147
|
+
def delete_activity(activity)
|
148
|
+
if @distance && @distance.activity == activity
|
149
|
+
@distance = nil
|
150
|
+
end
|
151
|
+
PersonalRecords::SpeedRecordDistances[@sport].each_key do |dist|
|
152
|
+
if @speed_records[dist] && @speed_records[dist].activity == activity
|
153
|
+
@speed_records[dist] = nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Return true if no Record is stored in this RecordSet object.
|
159
|
+
def empty?
|
160
|
+
return false if @distance
|
161
|
+
@speed_records.each_value { |r| return false if r }
|
162
|
+
|
163
|
+
true
|
164
|
+
end
|
165
|
+
|
166
|
+
# Iterator for all Record objects that are stored in this data structure.
|
167
|
+
def each(&block)
|
168
|
+
yield(@distance) if @distance
|
169
|
+
@speed_records.each_value do |record|
|
170
|
+
yield(record) if record
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def to_s
|
175
|
+
return '' if empty?
|
176
|
+
|
177
|
+
generate_table.to_s + "\n"
|
178
|
+
end
|
179
|
+
|
180
|
+
def to_html(doc)
|
181
|
+
generate_table.to_html(doc)
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def generate_table
|
187
|
+
t = FlexiTable.new
|
188
|
+
t.head
|
189
|
+
t.row([ 'Record', 'Time/Dist.', 'Avg. Pace', 'Ref.', 'Activity',
|
190
|
+
'Date' ],
|
191
|
+
{ :halign => :center })
|
192
|
+
t.set_column_attributes([
|
193
|
+
{},
|
194
|
+
{ :halign => :right },
|
195
|
+
{ :halign => :right },
|
196
|
+
{ :halign => :right },
|
197
|
+
{ :halign => :left },
|
198
|
+
{ :halign => :left }
|
199
|
+
])
|
200
|
+
t.body
|
201
|
+
|
202
|
+
records = @speed_records.values.delete_if { |r| r.nil? }
|
203
|
+
records << @distance if @distance
|
204
|
+
|
205
|
+
records.sort { |r1, r2| r1.distance <=> r2.distance }.each do |r|
|
206
|
+
r.to_table_row(t)
|
207
|
+
end
|
208
|
+
|
209
|
+
t
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
class SportRecords
|
216
|
+
|
217
|
+
attr_reader :sport, :all_time, :yearly
|
218
|
+
|
219
|
+
def initialize(sport)
|
220
|
+
@sport = sport
|
221
|
+
@all_time = RecordSet.new(@sport, nil)
|
222
|
+
@yearly = {}
|
223
|
+
end
|
224
|
+
|
225
|
+
def register_result(result)
|
226
|
+
year = result.start_time.year
|
227
|
+
unless @yearly[year]
|
228
|
+
@yearly[year] = RecordSet.new(@sport, year)
|
229
|
+
end
|
230
|
+
|
231
|
+
new_at = @all_time.register_result(result)
|
232
|
+
new_yr = @yearly[year].register_result(result)
|
233
|
+
|
234
|
+
new_at || new_yr
|
235
|
+
end
|
236
|
+
|
237
|
+
def delete_activity(activity)
|
238
|
+
([ @all_time ] + @yearly.values).each do |r|
|
239
|
+
r.delete_activity(activity)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Return true if no record is stored in this SportRecords object.
|
244
|
+
def empty?
|
245
|
+
return false unless @all_time.empty?
|
246
|
+
@yearly.each_value { |r| return false unless r.empty? }
|
247
|
+
|
248
|
+
true
|
249
|
+
end
|
250
|
+
|
251
|
+
# Iterator for all Record objects that are stored in this data structure.
|
252
|
+
def each(&block)
|
253
|
+
records = @yearly.values
|
254
|
+
records << @all_time if @all_time
|
255
|
+
records.each { |r| r.each(&block) }
|
256
|
+
end
|
257
|
+
|
258
|
+
def to_s
|
259
|
+
return '' if empty?
|
260
|
+
|
261
|
+
str = "All-time records:\n\n#{@all_time.to_s}" unless @all_time.empty?
|
262
|
+
@yearly.values.sort{ |r1, r2| r2.year <=> r1.year }.each do |record|
|
263
|
+
unless record.empty?
|
264
|
+
str += "Records of #{record.year}:\n\n#{record.to_s}"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
str
|
269
|
+
end
|
270
|
+
|
271
|
+
def to_html(doc)
|
272
|
+
return nil if empty?
|
273
|
+
|
274
|
+
doc.div {
|
275
|
+
doc.h3('All-time records')
|
276
|
+
@all_time.to_html(doc)
|
277
|
+
@yearly.values.sort{ |r1, r2| r2.year <=> r1.year }.each do |record|
|
278
|
+
puts record.year
|
279
|
+
unless record.empty?
|
280
|
+
doc.h3("Records of #{record.year}")
|
281
|
+
record.to_html(doc)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
}
|
79
285
|
end
|
80
286
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
287
|
+
end
|
288
|
+
|
289
|
+
def initialize(activities)
|
290
|
+
@activities = activities
|
291
|
+
@db_dir = activities.db_dir
|
292
|
+
@records_file = File.join(@db_dir, 'records.yml')
|
293
|
+
delete_all_records
|
294
|
+
|
295
|
+
load_records
|
296
|
+
end
|
297
|
+
|
298
|
+
def register_result(activity, sport, distance, duration, start_time)
|
299
|
+
unless @sport_records.include?(sport)
|
300
|
+
Log.info "Ignoring records for activity type '#{sport}' in " +
|
301
|
+
"#{activity.fit_file}"
|
302
|
+
return false
|
87
303
|
end
|
88
304
|
|
89
|
-
|
305
|
+
result = Record.new(activity, sport, distance, duration, start_time)
|
306
|
+
@sport_records[sport].register_result(result)
|
307
|
+
end
|
308
|
+
|
309
|
+
def delete_all_records
|
310
|
+
@sport_records = {}
|
311
|
+
SpeedRecordDistances.keys.each do |sport|
|
312
|
+
@sport_records[sport] = SportRecords.new(sport)
|
313
|
+
end
|
90
314
|
end
|
91
315
|
|
92
|
-
def delete_activity(
|
93
|
-
@
|
316
|
+
def delete_activity(activity)
|
317
|
+
@sport_records.each_value { |r| r.delete_activity(activity) }
|
94
318
|
end
|
95
319
|
|
96
320
|
def sync
|
97
321
|
save_records
|
322
|
+
|
323
|
+
non_empty_records = @sport_records.select { |s, r| !r.empty? }
|
324
|
+
max = non_empty_records.length
|
325
|
+
i = 0
|
326
|
+
non_empty_records.each do |sport, record|
|
327
|
+
output_file = File.join(@activities.cfg[:html_dir],
|
328
|
+
"records-#{i}.html")
|
329
|
+
RecordListPageView.new(@activities, record, max, i).
|
330
|
+
write(output_file)
|
331
|
+
end
|
98
332
|
end
|
99
333
|
|
100
334
|
def to_s
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
335
|
+
str = ''
|
336
|
+
@sport_records.each do |sport, record|
|
337
|
+
next if record.empty?
|
338
|
+
str += "Records for activity type #{sport}:\n\n#{record.to_s}"
|
339
|
+
end
|
340
|
+
|
341
|
+
str
|
342
|
+
end
|
343
|
+
|
344
|
+
# Iterator for all Record objects that are stored in this data structure.
|
345
|
+
def each(&block)
|
346
|
+
@sport_records.each_value { |r| r.each(&block) }
|
347
|
+
end
|
348
|
+
|
349
|
+
# Return an Array of all the records associated with the given Activity.
|
350
|
+
def activity_records(activity)
|
351
|
+
records = []
|
352
|
+
each do |record|
|
353
|
+
# puts record.activity
|
354
|
+
if record.activity.equal?(activity) && !records.include?(record)
|
355
|
+
records << record
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
records
|
126
360
|
end
|
127
361
|
|
128
362
|
private
|
@@ -130,25 +364,50 @@ module PostRunner
|
|
130
364
|
def load_records
|
131
365
|
begin
|
132
366
|
if File.exists?(@records_file)
|
133
|
-
@
|
367
|
+
@sport_records = YAML.load_file(@records_file)
|
134
368
|
else
|
135
369
|
Log.info "No records file found at '#{@records_file}'"
|
136
370
|
end
|
137
|
-
rescue
|
371
|
+
rescue IOError
|
138
372
|
Log.fatal "Cannot load records file '#{@records_file}': #{$!}"
|
139
373
|
end
|
140
374
|
|
141
|
-
unless @
|
375
|
+
unless @sport_records.is_a?(Hash)
|
142
376
|
Log.fatal "The personal records file '#{@records_file}' is corrupted"
|
143
377
|
end
|
378
|
+
fit_file_names_to_activity_refs
|
144
379
|
end
|
145
380
|
|
146
381
|
def save_records
|
382
|
+
activity_refs_to_fit_file_names
|
147
383
|
begin
|
148
|
-
|
149
|
-
|
384
|
+
BackedUpFile.open(@records_file, 'w') do |f|
|
385
|
+
f.write(@sport_records.to_yaml)
|
386
|
+
end
|
387
|
+
rescue IOError
|
150
388
|
Log.fatal "Cannot write records file '#{@records_file}': #{$!}"
|
151
389
|
end
|
390
|
+
fit_file_names_to_activity_refs
|
391
|
+
end
|
392
|
+
|
393
|
+
# Convert FIT file names in all Record objects into Activity references.
|
394
|
+
def fit_file_names_to_activity_refs
|
395
|
+
each do |record|
|
396
|
+
# Record objects can be referenced multiple times.
|
397
|
+
if record.activity.is_a?(String)
|
398
|
+
record.activity = @activities.activity_by_fit_file(record.activity)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# Convert Activity references in all Record objects into FIT file names.
|
404
|
+
def activity_refs_to_fit_file_names
|
405
|
+
each do |record|
|
406
|
+
# Record objects can be referenced multiple times.
|
407
|
+
unless record.activity.is_a?(String)
|
408
|
+
record.activity = record.activity.fit_file
|
409
|
+
end
|
410
|
+
end
|
152
411
|
end
|
153
412
|
|
154
413
|
end
|