postrunner 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +74 -11
- data/Rakefile +10 -0
- data/lib/postrunner/ActivitiesDB.rb +120 -32
- data/lib/postrunner/Activity.rb +34 -11
- data/lib/postrunner/ActivityListView.rb +195 -0
- data/lib/postrunner/ActivitySummary.rb +141 -0
- data/lib/postrunner/ActivityView.rb +41 -71
- data/lib/postrunner/ChartView.rb +72 -33
- data/lib/postrunner/DeviceList.rb +104 -0
- data/lib/postrunner/FlexiTable.rb +42 -7
- data/lib/postrunner/HTMLBuilder.rb +12 -0
- data/lib/postrunner/Main.rb +68 -9
- data/lib/postrunner/PersonalRecords.rb +12 -0
- data/lib/postrunner/RuntimeConfig.rb +80 -0
- data/lib/postrunner/TrackView.rb +25 -8
- data/lib/postrunner/UserProfileView.rb +70 -0
- data/lib/postrunner/ViewWidgets.rb +107 -0
- data/lib/postrunner/version.rb +13 -1
- data/lib/postrunner.rb +12 -0
- data/misc/icons/back.png +0 -0
- data/misc/icons/back.svg +68 -0
- data/misc/icons/first.png +0 -0
- data/misc/icons/first.svg +64 -0
- data/misc/icons/forward.png +0 -0
- data/misc/icons/forward.svg +64 -0
- data/misc/icons/home.png +0 -0
- data/misc/icons/home.svg +64 -0
- data/misc/icons/last.png +0 -0
- data/misc/icons/last.svg +64 -0
- data/misc/icons/lgpl-3.0.txt +165 -0
- data/postrunner.gemspec +1 -1
- data/spec/ActivitySummary_spec.rb +28 -0
- data/spec/FlexiTable_spec.rb +19 -1
- data/spec/PostRunner_spec.rb +25 -21
- data/spec/spec_helper.rb +62 -0
- metadata +26 -8
- data/LICENSE.txt +0 -22
- data/lib/postrunner/ActivityReport.rb +0 -102
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 10eb940a7f897be53510322b131e9307a844b3d3
|
4
|
+
data.tar.gz: 85b3a20f60cd7b20bede30072eae6d51af9bf93c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c59ca7d3109f211b7719bf1f5b526702c664d38b71766777784bfafef0edd72782efa580b79d9bfadfee5c9aacf79d1be62c9e7aaf9901275d2d04cec5cbe6d
|
7
|
+
data.tar.gz: 3b1d77b712513e49a922bc94a1a44f8e88c9681d36c62df7c797bda14207b60721efb3cee5bf7393452fd8c1993a8f2dd597f1bdecef074113587fd7ce20c7ab
|
data/README.md
CHANGED
@@ -6,44 +6,107 @@ PostRunner is an application to manage FIT files such as those produced by Garmi
|
|
6
6
|
|
7
7
|
PostRunner is a Ruby application. You need to have a Ruby 2.0 or later runtime environment installed.
|
8
8
|
|
9
|
-
|
9
|
+
```
|
10
|
+
$ gem install postrunner
|
11
|
+
```
|
10
12
|
|
11
13
|
## Usage
|
12
14
|
|
15
|
+
### Importing FIT files
|
16
|
+
|
13
17
|
To get started you need to connect your device to your computer and mount it as a drive. Only devices that expose their data as FAT file system are supported. Older devices use proprietary drivers and are not supported by postrunner. Once the device is mounted find out the full path to the directory that contains your FIT files. You can then import all files on the device.
|
14
18
|
|
15
|
-
|
19
|
+
```
|
20
|
+
$ postrunner import /var/run/media/user/GARMIN/GARMIN/ACTIVITY/
|
21
|
+
```
|
16
22
|
|
17
23
|
The above command assumes that your device is mounted as /var/run/media/user. Please replace this with the path to your device. Files that have been imported previously will not be imported again.
|
18
24
|
|
25
|
+
### Viewing FIT file data on the console
|
26
|
+
|
19
27
|
Now you can list all the FIT files in your data base.
|
20
28
|
|
21
|
-
|
29
|
+
```
|
30
|
+
$ postrunner list
|
31
|
+
```
|
22
32
|
|
23
|
-
The first column is the index you can use to reference FIT files. To
|
33
|
+
The first column is the index you can use to reference FIT files. To
|
34
|
+
get a summary of the most recent activity use the following command.
|
35
|
+
References to already imported activities start with a colon followed
|
36
|
+
by the index number.
|
37
|
+
|
38
|
+
```
|
39
|
+
$ postrunner summary :1
|
40
|
+
```
|
24
41
|
|
25
|
-
$ postrunner summary :1
|
26
|
-
|
27
42
|
To get a summary of the oldest activity you can use
|
28
43
|
|
29
|
-
|
30
|
-
|
44
|
+
```
|
45
|
+
$ postrunner summary :-1
|
46
|
+
```
|
47
|
+
|
48
|
+
To select multiple activities you can use a range.
|
49
|
+
|
50
|
+
```
|
51
|
+
$ postrunner summary :1-3
|
52
|
+
```
|
53
|
+
|
31
54
|
You can also get a full dump of the content of a FIT file.
|
32
55
|
|
33
|
-
|
56
|
+
```
|
57
|
+
$ postrunner dump 1234568.FIT
|
58
|
+
```
|
34
59
|
|
35
60
|
If the file is already in the data base you can also use the reference notation.
|
36
61
|
|
37
|
-
|
62
|
+
```
|
63
|
+
$ postrunner dump :1
|
64
|
+
```
|
38
65
|
|
39
66
|
This will provide you with a lot more information contained in the FIT files that is not available through Garmin Connect or most other tools.
|
40
67
|
|
68
|
+
### Viewing FIT file data in your web browser
|
69
|
+
|
70
|
+
You can also view the full details of your activity in your browser.
|
71
|
+
This view includes a map (internet connection for map data required)
|
72
|
+
and charts for speed, pace, heart rate, cadence and the like.
|
73
|
+
|
74
|
+
```
|
75
|
+
$ postrunner show
|
76
|
+
```
|
77
|
+
|
78
|
+
This will open an overview of the most recent activities in your web
|
79
|
+
browser. It will use Firefox by default. You can overwrite this by
|
80
|
+
setting the BROWSER environment variable.
|
81
|
+
|
82
|
+
To view a specific run directly, you can use similar specifications
|
83
|
+
like those explained above.
|
84
|
+
|
85
|
+
```
|
86
|
+
$ postrunner show :1
|
87
|
+
```
|
88
|
+
|
41
89
|
## Contributing
|
42
90
|
|
43
|
-
PostRunner is currently work in progress. It does some things I want
|
91
|
+
PostRunner is currently work in progress. It does some things I want
|
92
|
+
with files from my Garmin FR620. It's certainly possible to do more
|
93
|
+
things and support more devices. Patches are welcome!
|
44
94
|
|
45
95
|
1. Fork it ( https://github.com/scrapper/postrunner/fork )
|
46
96
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
47
97
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
48
98
|
4. Push to the branch (`git push origin my-new-feature`)
|
49
99
|
5. Create a new Pull Request
|
100
|
+
|
101
|
+
## License
|
102
|
+
|
103
|
+
PostRunner is licensed under the GNU GPL version 2.
|
104
|
+
|
105
|
+
The distribution includes third party components that are licensed
|
106
|
+
under different OSI compatible terms.
|
107
|
+
|
108
|
+
* flot: MIT License
|
109
|
+
* jquery: MIT License
|
110
|
+
* openlayers: 2 clause BSD license
|
111
|
+
* Oxygen Icons: GNU LGPLv3 (https://techbase.kde.org/Projects/Oxygen/Licensing)
|
112
|
+
|
data/Rakefile
CHANGED
@@ -1,6 +1,16 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rspec/core/rake_task"
|
3
3
|
|
4
|
+
# Add the include path for the fit4ruby library. We assume it is located in
|
5
|
+
# the same directory as the postrunner directory.
|
6
|
+
fit4ruby = File.realpath(File.join(File.dirname(__FILE__), '..',
|
7
|
+
'fit4ruby', 'lib'))
|
8
|
+
if ENV['RUBYLIB']
|
9
|
+
ENV['RUBYLIB'] += ":#{fit4ruby}"
|
10
|
+
else
|
11
|
+
ENV['RUBYLIB'] = fit4ruby
|
12
|
+
end
|
13
|
+
|
4
14
|
RSpec::Core::RakeTask.new
|
5
15
|
|
6
16
|
task :default => :spec
|
@@ -1,22 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = ActivitiesDB.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2014 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
|
+
|
1
13
|
require 'fileutils'
|
2
14
|
require 'yaml'
|
3
15
|
|
4
16
|
require 'fit4ruby'
|
5
17
|
require 'postrunner/Activity'
|
6
18
|
require 'postrunner/PersonalRecords'
|
7
|
-
require 'postrunner/
|
19
|
+
require 'postrunner/ActivityListView'
|
8
20
|
|
9
21
|
module PostRunner
|
10
22
|
|
11
23
|
class ActivitiesDB
|
12
24
|
|
13
|
-
|
14
|
-
|
15
|
-
attr_reader :db_dir, :fit_dir
|
25
|
+
attr_reader :db_dir, :cfg, :fit_dir, :html_dir, :activities
|
16
26
|
|
17
|
-
def initialize(db_dir)
|
27
|
+
def initialize(db_dir, cfg)
|
18
28
|
@db_dir = db_dir
|
29
|
+
@cfg = cfg
|
19
30
|
@fit_dir = File.join(@db_dir, 'fit')
|
31
|
+
@html_dir = File.join(@db_dir, 'html')
|
20
32
|
@archive_file = File.join(@db_dir, 'archive.yml')
|
21
33
|
|
22
34
|
create_directories
|
@@ -34,16 +46,20 @@ module PostRunner
|
|
34
46
|
Log.fatal "The archive file '#{@archive_file}' is corrupted"
|
35
47
|
end
|
36
48
|
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
49
|
+
# Not all instance variables of Activity are stored in the file. The
|
50
|
+
# normal constructor is not run during YAML::load_file. We have to
|
51
|
+
# initialize those instance variables in a secondary step.
|
40
52
|
@activities.each do |a|
|
41
|
-
a.
|
53
|
+
a.late_init(self)
|
42
54
|
end
|
43
55
|
|
44
56
|
@records = PersonalRecords.new(self)
|
45
57
|
end
|
46
58
|
|
59
|
+
# Add a new FIT file to the database.
|
60
|
+
# @param fit_file [String] Name of the FIT file.
|
61
|
+
# @return [TrueClass or FalseClass] True if the file could be added. False
|
62
|
+
# otherwise.
|
47
63
|
def add(fit_file)
|
48
64
|
base_fit_file = File.basename(fit_file)
|
49
65
|
if @activities.find { |a| a.fit_file == base_fit_file }
|
@@ -77,6 +93,16 @@ module PostRunner
|
|
77
93
|
|
78
94
|
activity.register_records(@records)
|
79
95
|
|
96
|
+
# The HTML activity views contain links to their predecessors and
|
97
|
+
# successors. After inserting a new activity, we need to re-generate
|
98
|
+
# these views as well.
|
99
|
+
if (pred = predecessor(activity))
|
100
|
+
pred.generate_html_view
|
101
|
+
end
|
102
|
+
if (succ = successor(activity))
|
103
|
+
succ.generate_html_view
|
104
|
+
end
|
105
|
+
|
80
106
|
sync
|
81
107
|
Log.info "#{fit_file} successfully added to archive"
|
82
108
|
|
@@ -84,7 +110,17 @@ module PostRunner
|
|
84
110
|
end
|
85
111
|
|
86
112
|
def delete(activity)
|
113
|
+
pred = predecessor(activities)
|
114
|
+
succ = successor(activities)
|
115
|
+
|
87
116
|
@activities.delete(activity)
|
117
|
+
|
118
|
+
# The HTML activity views contain links to their predecessors and
|
119
|
+
# successors. After deleting an activity, we need to re-generate these
|
120
|
+
# views as well.
|
121
|
+
pred.generate_html_view if pred
|
122
|
+
succ.generate_html_view if succ
|
123
|
+
|
88
124
|
sync
|
89
125
|
end
|
90
126
|
|
@@ -139,6 +175,22 @@ module PostRunner
|
|
139
175
|
[]
|
140
176
|
end
|
141
177
|
|
178
|
+
# Return the next Activity after the provided activity. Note that this has
|
179
|
+
# a lower index. If none is found, return nil.
|
180
|
+
def successor(activity)
|
181
|
+
idx = @activities.index(activity)
|
182
|
+
return nil if idx.nil? || idx == 0
|
183
|
+
@activities[idx - 1]
|
184
|
+
end
|
185
|
+
|
186
|
+
# Return the previous Activity before the provided activity. Note that
|
187
|
+
# this has a higher index. If none is found, return nil.
|
188
|
+
def predecessor(activity)
|
189
|
+
idx = @activities.index(activity)
|
190
|
+
return nil if idx.nil? || idx >= @activities.length - 2
|
191
|
+
@activities[idx + 1]
|
192
|
+
end
|
193
|
+
|
142
194
|
def map_to_files(query)
|
143
195
|
case query
|
144
196
|
when /\A-?\d+$\z/
|
@@ -171,36 +223,43 @@ module PostRunner
|
|
171
223
|
[]
|
172
224
|
end
|
173
225
|
|
226
|
+
# Show the activity list in a web browser.
|
227
|
+
def show_list_in_browser
|
228
|
+
ActivityListView.new(self).update_html_index
|
229
|
+
show_in_browser(File.join(@html_dir, 'index.html'))
|
230
|
+
end
|
231
|
+
|
174
232
|
def list
|
175
|
-
|
176
|
-
t = FlexiTable.new
|
177
|
-
t.head
|
178
|
-
t.row(%w( Ref. Activity Start Distance Duration Pace ),
|
179
|
-
{ :halign => :left })
|
180
|
-
t.set_column_attributes([
|
181
|
-
{ :halign => :right },
|
182
|
-
{}, {},
|
183
|
-
{ :halign => :right },
|
184
|
-
{ :halign => :right },
|
185
|
-
{ :halign => :right }
|
186
|
-
])
|
187
|
-
t.body
|
188
|
-
@activities.each do |a|
|
189
|
-
t.row([
|
190
|
-
i += 1,
|
191
|
-
a.name[0..19],
|
192
|
-
a.timestamp.strftime("%a, %Y %b %d %H:%M"),
|
193
|
-
"%.2f" % (a.total_distance / 1000),
|
194
|
-
secsToHMS(a.total_timer_time),
|
195
|
-
speedToPace(a.avg_speed) ])
|
196
|
-
end
|
197
|
-
puts t.to_s
|
233
|
+
puts ActivityListView.new(self).to_s
|
198
234
|
end
|
199
235
|
|
200
236
|
def show_records
|
201
237
|
puts @records.to_s
|
202
238
|
end
|
203
239
|
|
240
|
+
# Launch a web browser and show an HTML file.
|
241
|
+
# @param html_file [String] file name of the HTML file to show
|
242
|
+
def show_in_browser(html_file)
|
243
|
+
cmd = "#{ENV['BROWSER'] || 'firefox'} \"#{html_file}\" &"
|
244
|
+
|
245
|
+
unless system(cmd)
|
246
|
+
Log.fatal "Failed to execute the following shell command: #{$cmd}\n" +
|
247
|
+
"#{$!}"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# This method can be called to re-generate all HTML reports and all HTML
|
252
|
+
# index files.
|
253
|
+
def generate_all_html_reports
|
254
|
+
Log.info "Re-generating all HTML report files..."
|
255
|
+
# Generate HTML views for all activities in the DB.
|
256
|
+
@activities.each { |a| a.generate_html_view }
|
257
|
+
Log.info "All HTML report files have been re-generated."
|
258
|
+
# (Re-)generate index files.
|
259
|
+
ActivityListView.new(self).update_html_index
|
260
|
+
Log.info "HTML index files have been updated."
|
261
|
+
end
|
262
|
+
|
204
263
|
private
|
205
264
|
|
206
265
|
def sync
|
@@ -211,11 +270,18 @@ module PostRunner
|
|
211
270
|
end
|
212
271
|
|
213
272
|
@records.sync
|
273
|
+
ActivityListView.new(self).update_html_index
|
214
274
|
end
|
215
275
|
|
216
276
|
def create_directories
|
217
277
|
create_directory(@db_dir, 'data')
|
218
278
|
create_directory(@fit_dir, 'fit')
|
279
|
+
create_directory(@html_dir, 'html')
|
280
|
+
|
281
|
+
create_symlink('icons')
|
282
|
+
create_symlink('jquery')
|
283
|
+
create_symlink('flot')
|
284
|
+
create_symlink('openlayers')
|
219
285
|
end
|
220
286
|
|
221
287
|
def create_directory(dir, name)
|
@@ -229,6 +295,28 @@ module PostRunner
|
|
229
295
|
end
|
230
296
|
end
|
231
297
|
|
298
|
+
def create_symlink(dir)
|
299
|
+
# This file should be in lib/postrunner. The 'misc' directory should be
|
300
|
+
# found in '../../misc'.
|
301
|
+
misc_dir = File.realpath(File.join(File.dirname(__FILE__),
|
302
|
+
'..', '..', 'misc'))
|
303
|
+
unless Dir.exists?(misc_dir)
|
304
|
+
Log.fatal "Cannot find 'misc' directory under '#{misc_dir}': #{$!}"
|
305
|
+
end
|
306
|
+
src_dir = File.join(misc_dir, dir)
|
307
|
+
unless Dir.exists?(src_dir)
|
308
|
+
Log.fatal "Cannot find '#{src_dir}': #{$!}"
|
309
|
+
end
|
310
|
+
dst_dir = File.join(@html_dir, dir)
|
311
|
+
unless File.exists?(dst_dir)
|
312
|
+
begin
|
313
|
+
FileUtils.ln_s(src_dir, dst_dir)
|
314
|
+
rescue IOError
|
315
|
+
Log.fatal "Cannot create symbolic link to '#{dst_dir}': #{$!}"
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
232
320
|
end
|
233
321
|
|
234
322
|
end
|
data/lib/postrunner/Activity.rb
CHANGED
@@ -1,14 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = Activity.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2014 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
|
+
|
1
13
|
require 'fit4ruby'
|
2
14
|
|
3
|
-
require 'postrunner/
|
15
|
+
require 'postrunner/ActivitySummary'
|
4
16
|
require 'postrunner/ActivityView'
|
5
17
|
|
6
18
|
module PostRunner
|
7
19
|
|
8
20
|
class Activity
|
9
21
|
|
10
|
-
attr_reader :fit_file, :name, :fit_activity
|
11
|
-
attr_accessor :db
|
22
|
+
attr_reader :db, :fit_file, :name, :fit_activity, :html_dir, :html_file
|
12
23
|
|
13
24
|
# This is a list of variables that provide data from the fit file. To
|
14
25
|
# speed up access to it, we cache the data in the activity database.
|
@@ -16,16 +27,24 @@ module PostRunner
|
|
16
27
|
avg_speed )
|
17
28
|
|
18
29
|
def initialize(db, fit_file, fit_activity, name = nil)
|
19
|
-
@db = db
|
20
30
|
@fit_file = fit_file
|
21
31
|
@fit_activity = fit_activity
|
22
32
|
@name = name || fit_file
|
33
|
+
late_init(db)
|
23
34
|
|
24
35
|
@@CachedVariables.each do |v|
|
25
36
|
v_str = "@#{v}"
|
26
37
|
instance_variable_set(v_str, fit_activity.send(v))
|
27
38
|
self.class.send(:attr_reader, v.to_sym)
|
28
39
|
end
|
40
|
+
# Generate HTML file for this activity.
|
41
|
+
generate_html_view
|
42
|
+
end
|
43
|
+
|
44
|
+
def late_init(db)
|
45
|
+
@db = db
|
46
|
+
@html_dir = File.join(@db.db_dir, 'html')
|
47
|
+
@html_file = File.join(@html_dir, "#{@fit_file[0..-5]}.html")
|
29
48
|
end
|
30
49
|
|
31
50
|
def check
|
@@ -61,21 +80,19 @@ module PostRunner
|
|
61
80
|
end
|
62
81
|
|
63
82
|
def show
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
#view.generate_html
|
68
|
-
#chart = ChartView.new(self, '../../html')
|
69
|
-
#chart.generate_html
|
83
|
+
generate_html_view #unless File.exists?(@html_file)
|
84
|
+
|
85
|
+
@db.show_in_browser(@html_file)
|
70
86
|
end
|
71
87
|
|
72
88
|
def summary
|
73
89
|
@fit_activity = load_fit_file unless @fit_activity
|
74
|
-
puts
|
90
|
+
puts ActivitySummary.new(@fit_activity, name, @db.cfg[:unit_system]).to_s
|
75
91
|
end
|
76
92
|
|
77
93
|
def rename(name)
|
78
94
|
@name = name
|
95
|
+
generate_html_view
|
79
96
|
end
|
80
97
|
|
81
98
|
def register_records(db)
|
@@ -90,6 +107,12 @@ module PostRunner
|
|
90
107
|
end
|
91
108
|
end
|
92
109
|
|
110
|
+
def generate_html_view
|
111
|
+
@fit_activity = load_fit_file unless @fit_activity
|
112
|
+
ActivityView.new(self, @db.cfg[:unit_system], @db.predecessor(self),
|
113
|
+
@db.successor(self))
|
114
|
+
end
|
115
|
+
|
93
116
|
private
|
94
117
|
|
95
118
|
def load_fit_file(filter = nil)
|
@@ -0,0 +1,195 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = ActivitListView.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2014 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 'fit4ruby'
|
14
|
+
|
15
|
+
require 'postrunner/FlexiTable'
|
16
|
+
require 'postrunner/HTMLBuilder'
|
17
|
+
require 'postrunner/ViewWidgets'
|
18
|
+
|
19
|
+
module PostRunner
|
20
|
+
|
21
|
+
class ActivityListView
|
22
|
+
|
23
|
+
class ActivityLink
|
24
|
+
|
25
|
+
def initialize(activity)
|
26
|
+
@activity = activity
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_html(doc)
|
30
|
+
doc.a(@activity.name, { :class => 'activity_link',
|
31
|
+
:href => @activity.fit_file[0..-5] + '.html' })
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
@activity.name[0..19]
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
include Fit4Ruby::Converters
|
41
|
+
include ViewWidgets
|
42
|
+
|
43
|
+
def initialize(db)
|
44
|
+
@db = db
|
45
|
+
@unit_system = @db.cfg[:unit_system]
|
46
|
+
@page_size = 20
|
47
|
+
@page_no = -1
|
48
|
+
@last_page = (@db.activities.length - 1) / @page_size
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_html_index
|
52
|
+
0.upto(@last_page) do |page_no|
|
53
|
+
@page_no = page_no
|
54
|
+
generate_html_index_page
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_html(doc)
|
59
|
+
generate_table.to_html(doc)
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_s
|
63
|
+
generate_table.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def generate_html_index_page
|
69
|
+
doc = HTMLBuilder.new
|
70
|
+
|
71
|
+
doc.html {
|
72
|
+
head(doc)
|
73
|
+
body(doc)
|
74
|
+
}
|
75
|
+
|
76
|
+
write_file(doc)
|
77
|
+
end
|
78
|
+
|
79
|
+
def head(doc)
|
80
|
+
doc.head {
|
81
|
+
doc.meta({ 'http-equiv' => 'Content-Type',
|
82
|
+
'content' => 'text/html; charset=utf-8' })
|
83
|
+
doc.title("PostRunner Activities")
|
84
|
+
style(doc)
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
def style(doc)
|
89
|
+
view_widgets_style(doc)
|
90
|
+
doc.style(<<EOT
|
91
|
+
body {
|
92
|
+
font-family: verdana,arial,sans-serif;
|
93
|
+
margin: 0px;
|
94
|
+
}
|
95
|
+
.main {
|
96
|
+
text-align: center;
|
97
|
+
}
|
98
|
+
.widget_frame {
|
99
|
+
width: 900px;
|
100
|
+
}
|
101
|
+
.activity_link {
|
102
|
+
padding: 0px 3px 0px 3px;
|
103
|
+
}
|
104
|
+
.ft_cell {
|
105
|
+
height: 30px
|
106
|
+
}
|
107
|
+
EOT
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
def body(doc)
|
112
|
+
doc.body {
|
113
|
+
first_page = @page_no == 0 ? nil: 'index.html'
|
114
|
+
prev_page = @page_no == 0 ? nil :
|
115
|
+
@page_no == 1 ? 'index.html' :
|
116
|
+
"index#{@page_no - 1}.html"
|
117
|
+
prev_page = @page_no == 0 ? nil :
|
118
|
+
@page_no == 1 ? 'index.html' :
|
119
|
+
"index#{@page_no - 1}.html"
|
120
|
+
next_page = @page_no < @last_page ? "index#{@page_no + 1}.html" : nil
|
121
|
+
last_page = @page_no == @last_page ? nil : "index#{@last_page}.html"
|
122
|
+
titlebar(doc, first_page, prev_page, nil, next_page, last_page)
|
123
|
+
|
124
|
+
doc.div({ :class => 'main' }) {
|
125
|
+
frame(doc, 'Activities') {
|
126
|
+
generate_table.to_html(doc)
|
127
|
+
}
|
128
|
+
}
|
129
|
+
footer(doc)
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
def generate_table
|
134
|
+
i = @page_no < 0 ? 0 : @page_no * @page_size
|
135
|
+
t = FlexiTable.new
|
136
|
+
t.head
|
137
|
+
t.row(%w( Ref. Activity Start Distance Duration Pace ),
|
138
|
+
{ :halign => :left })
|
139
|
+
t.set_column_attributes([
|
140
|
+
{ :halign => :right },
|
141
|
+
{}, {},
|
142
|
+
{ :halign => :right },
|
143
|
+
{ :halign => :right },
|
144
|
+
{ :halign => :right }
|
145
|
+
])
|
146
|
+
t.body
|
147
|
+
activities = @page_no == -1 ? @db.activities :
|
148
|
+
@db.activities[(@page_no * @page_size)..
|
149
|
+
((@page_no + 1) * @page_size - 1)]
|
150
|
+
activities.each do |a|
|
151
|
+
t.row([
|
152
|
+
i += 1,
|
153
|
+
ActivityLink.new(a),
|
154
|
+
a.timestamp.strftime("%a, %Y %b %d %H:%M"),
|
155
|
+
local_value(a.total_distance, 'm', '%.2f',
|
156
|
+
{ :metric => 'km', :statute => 'mi' }),
|
157
|
+
secsToHMS(a.total_timer_time),
|
158
|
+
pace(a.avg_speed) ])
|
159
|
+
end
|
160
|
+
|
161
|
+
t
|
162
|
+
end
|
163
|
+
|
164
|
+
def write_file(doc)
|
165
|
+
output_file = File.join(@db.html_dir,
|
166
|
+
"index#{@page_no == 0 ? '' : @page_no}.html")
|
167
|
+
begin
|
168
|
+
File.write(output_file, doc.to_html)
|
169
|
+
rescue IOError
|
170
|
+
Log.fatal "Cannot write activity index file '#{output_file}: #{$!}"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def local_value(value, from_unit, format, units)
|
175
|
+
to_unit = units[@unit_system]
|
176
|
+
return '-' unless value
|
177
|
+
value *= conversion_factor(from_unit, to_unit)
|
178
|
+
"#{format % [value, to_unit]}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def pace(speed)
|
182
|
+
case @unit_system
|
183
|
+
when :metric
|
184
|
+
"#{speedToPace(speed)}"
|
185
|
+
when :statute
|
186
|
+
"#{speedToPace(speed, 1609.34)}"
|
187
|
+
else
|
188
|
+
Log.fatal "Unknown unit system #{@unit_system}"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|