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
@@ -0,0 +1,141 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = ActivitySummary.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/ViewWidgets'
|
17
|
+
|
18
|
+
module PostRunner
|
19
|
+
|
20
|
+
class ActivitySummary
|
21
|
+
|
22
|
+
include Fit4Ruby::Converters
|
23
|
+
include ViewWidgets
|
24
|
+
|
25
|
+
def initialize(fit_activity, name, unit_system)
|
26
|
+
@fit_activity = fit_activity
|
27
|
+
@name = name
|
28
|
+
@unit_system = unit_system
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
session = @fit_activity.sessions[0]
|
33
|
+
|
34
|
+
summary(session).to_s + "\n" + laps.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_html(doc)
|
38
|
+
session = @fit_activity.sessions[0]
|
39
|
+
|
40
|
+
frame(doc, "Activity: #{@name}") {
|
41
|
+
summary(session).to_html(doc)
|
42
|
+
}
|
43
|
+
frame(doc, 'Laps') {
|
44
|
+
laps.to_html(doc)
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def summary(session)
|
51
|
+
t = FlexiTable.new
|
52
|
+
t.enable_frame(false)
|
53
|
+
t.body
|
54
|
+
t.row([ 'Date:', session.timestamp])
|
55
|
+
t.row([ 'Distance:',
|
56
|
+
local_value(session, 'total_distance', '%.2f %s',
|
57
|
+
{ :metric => 'km', :statute => 'mi'}) ])
|
58
|
+
t.row([ 'Time:', secsToHMS(session.total_timer_time) ])
|
59
|
+
t.row([ 'Avg. Pace:', pace(session, 'avg_speed') ])
|
60
|
+
t.row([ 'Total Ascent:',
|
61
|
+
local_value(session, 'total_ascent', '%.0f %s',
|
62
|
+
{ :metric => 'm', :statute => 'ft' }) ])
|
63
|
+
t.row([ 'Total Descent:',
|
64
|
+
local_value(session, 'total_descent', '%.0f %s',
|
65
|
+
{ :metric => 'm', :statute => 'ft' }) ])
|
66
|
+
t.row([ 'Calories:', "#{session.total_calories} kCal" ])
|
67
|
+
t.row([ 'Avg. HR:', session.avg_heart_rate ?
|
68
|
+
"#{session.avg_heart_rate} bpm" : '-' ])
|
69
|
+
t.row([ 'Max. HR:', session.max_heart_rate ?
|
70
|
+
"#{session.max_heart_rate} bpm" : '-' ])
|
71
|
+
t.row([ 'Training Effect:', session.total_training_effect ?
|
72
|
+
session.total_training_effect : '-' ])
|
73
|
+
t.row([ 'Avg. Run Cadence:',
|
74
|
+
session.avg_running_cadence ?
|
75
|
+
"#{session.avg_running_cadence.round} spm" : '-' ])
|
76
|
+
t.row([ 'Avg. Vertical Oscillation:',
|
77
|
+
local_value(session, 'avg_vertical_oscillation', '%.1f %s',
|
78
|
+
{ :metric => 'cm', :statute => 'in' }) ])
|
79
|
+
t.row([ 'Avg. Ground Contact Time:',
|
80
|
+
session.avg_stance_time ?
|
81
|
+
"#{session.avg_stance_time.round} ms" : '-' ])
|
82
|
+
t.row([ 'Avg. Stride Length:',
|
83
|
+
local_value(session, 'avg_stride_length', '%.2f %s',
|
84
|
+
{ :metric => 'm', :statute => 'ft' }) ])
|
85
|
+
rec_time = @fit_activity.recovery_time
|
86
|
+
t.row([ 'Recovery Time:', rec_time ? secsToHMS(rec_time * 60) : '-' ])
|
87
|
+
vo2max = @fit_activity.vo2max
|
88
|
+
t.row([ 'VO2max:', vo2max ? vo2max : '-' ])
|
89
|
+
|
90
|
+
t
|
91
|
+
end
|
92
|
+
|
93
|
+
def laps
|
94
|
+
t = FlexiTable.new
|
95
|
+
t.head
|
96
|
+
t.row([ 'Lap', 'Duration', 'Distance', 'Avg. Pace', 'Stride', 'Cadence',
|
97
|
+
'Avg. HR', 'Max. HR' ])
|
98
|
+
t.set_column_attributes(Array.new(8, { :halign => :right }))
|
99
|
+
t.body
|
100
|
+
@fit_activity.sessions[0].laps.each.with_index do |lap, index|
|
101
|
+
t.cell(index + 1)
|
102
|
+
t.cell(secsToHMS(lap.total_timer_time))
|
103
|
+
t.cell(local_value(lap, 'total_distance', '%.2f',
|
104
|
+
{ :metric => 'km', :statute => 'mi' }))
|
105
|
+
t.cell(pace(lap, 'avg_speed', false))
|
106
|
+
t.cell(local_value(lap, 'avg_stride_length', '%.2f',
|
107
|
+
{ :metric => 'm', :statute => 'ft' }))
|
108
|
+
t.cell(lap.avg_running_cadence && lap.avg_fractional_cadence ?
|
109
|
+
'%.1f' % (2 * lap.avg_running_cadence +
|
110
|
+
(2 * lap.avg_fractional_cadence) / 100.0) : '')
|
111
|
+
t.cell(lap.avg_heart_rate.to_s)
|
112
|
+
t.cell(lap.max_heart_rate.to_s)
|
113
|
+
t.new_row
|
114
|
+
end
|
115
|
+
|
116
|
+
t
|
117
|
+
end
|
118
|
+
|
119
|
+
def local_value(fdr, field, format, units)
|
120
|
+
unit = units[@unit_system]
|
121
|
+
value = fdr.get_as(field, unit)
|
122
|
+
return '-' unless value
|
123
|
+
"#{format % [value, unit]}"
|
124
|
+
end
|
125
|
+
|
126
|
+
def pace(fdr, field, show_unit = true)
|
127
|
+
speed = fdr.get(field)
|
128
|
+
case @unit_system
|
129
|
+
when :metric
|
130
|
+
"#{speedToPace(speed)}#{show_unit ? ' min/km' : ''}"
|
131
|
+
when :statute
|
132
|
+
"#{speedToPace(speed, 1609.34)}#{show_unit ? ' min/mi' : ''}"
|
133
|
+
else
|
134
|
+
Log.fatal "Unknown unit system #{@unit_system}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
@@ -1,7 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = ActivityView.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
15
|
require 'postrunner/HTMLBuilder'
|
4
|
-
require 'postrunner/
|
16
|
+
require 'postrunner/ActivitySummary'
|
17
|
+
require 'postrunner/DeviceList'
|
18
|
+
require 'postrunner/UserProfileView'
|
5
19
|
require 'postrunner/ViewWidgets'
|
6
20
|
require 'postrunner/TrackView'
|
7
21
|
require 'postrunner/ChartView'
|
@@ -12,61 +26,28 @@ module PostRunner
|
|
12
26
|
|
13
27
|
include ViewWidgets
|
14
28
|
|
15
|
-
def initialize(activity,
|
29
|
+
def initialize(activity, unit_system, predecessor, successor)
|
16
30
|
@activity = activity
|
17
|
-
@
|
31
|
+
@unit_system = unit_system
|
32
|
+
@predecessor = predecessor
|
33
|
+
@successor = successor
|
34
|
+
@output_dir = activity.html_dir
|
18
35
|
@output_file = nil
|
19
36
|
|
20
|
-
ensure_output_dir
|
21
|
-
|
22
37
|
@doc = HTMLBuilder.new
|
23
38
|
generate_html(@doc)
|
24
39
|
write_file
|
25
|
-
show_in_browser
|
26
40
|
end
|
27
41
|
|
28
42
|
private
|
29
43
|
|
30
|
-
def ensure_output_dir
|
31
|
-
unless Dir.exists?(@output_dir)
|
32
|
-
begin
|
33
|
-
Dir.mkdir(@output_dir)
|
34
|
-
rescue SystemCallError
|
35
|
-
Log.fatal "Cannot create output directory '#{@output_dir}': #{$!}"
|
36
|
-
end
|
37
|
-
end
|
38
|
-
create_symlink('jquery')
|
39
|
-
create_symlink('flot')
|
40
|
-
create_symlink('openlayers')
|
41
|
-
end
|
42
|
-
|
43
|
-
def create_symlink(dir)
|
44
|
-
# This file should be in lib/postrunner. The 'misc' directory should be
|
45
|
-
# found in '../../misc'.
|
46
|
-
misc_dir = File.realpath(File.join(File.dirname(__FILE__),
|
47
|
-
'..', '..', 'misc'))
|
48
|
-
unless Dir.exists?(misc_dir)
|
49
|
-
Log.fatal "Cannot find 'misc' directory under '#{misc_dir}': #{$!}"
|
50
|
-
end
|
51
|
-
src_dir = File.join(misc_dir, dir)
|
52
|
-
unless Dir.exists?(src_dir)
|
53
|
-
Log.fatal "Cannot find '#{src_dir}': #{$!}"
|
54
|
-
end
|
55
|
-
dst_dir = File.join(@output_dir, dir)
|
56
|
-
unless File.exists?(dst_dir)
|
57
|
-
begin
|
58
|
-
FileUtils.ln_s(src_dir, dst_dir)
|
59
|
-
rescue IOError
|
60
|
-
Log.fatal "Cannot create symbolic link to '#{dst_dir}': #{$!}"
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
|
66
44
|
def generate_html(doc)
|
67
|
-
@report =
|
45
|
+
@report = ActivitySummary.new(@activity.fit_activity, @activity.name,
|
46
|
+
@unit_system)
|
47
|
+
@device_list = DeviceList.new(@activity.fit_activity)
|
48
|
+
@user_profile = UserProfileView.new(@activity.fit_activity, @unit_system)
|
68
49
|
@track_view = TrackView.new(@activity)
|
69
|
-
@chart_view = ChartView.new(@activity)
|
50
|
+
@chart_view = ChartView.new(@activity, @unit_system)
|
70
51
|
|
71
52
|
doc.html {
|
72
53
|
head(doc)
|
@@ -83,15 +64,19 @@ module PostRunner
|
|
83
64
|
'initial-scale=1.0, maximum-scale=1.0, ' +
|
84
65
|
'user-scalable=0' })
|
85
66
|
doc.title("PostRunner Activity: #{@activity.name}")
|
86
|
-
style(doc)
|
87
67
|
view_widgets_style(doc)
|
88
68
|
@chart_view.head(doc)
|
89
69
|
@track_view.head(doc)
|
70
|
+
style(doc)
|
90
71
|
}
|
91
72
|
end
|
92
73
|
|
93
74
|
def style(doc)
|
94
75
|
doc.style(<<EOT
|
76
|
+
body {
|
77
|
+
font-family: verdana,arial,sans-serif;
|
78
|
+
margin: 0px;
|
79
|
+
}
|
95
80
|
.main {
|
96
81
|
width: 1210px;
|
97
82
|
margin: 0 auto;
|
@@ -104,39 +89,28 @@ module PostRunner
|
|
104
89
|
float: right;
|
105
90
|
width: 600px;
|
106
91
|
}
|
107
|
-
.widget_container {
|
108
|
-
box-sizing: border-box;
|
109
|
-
width: 600px;
|
110
|
-
padding: 10px 15px 15px 15px;
|
111
|
-
margin: 15px auto 15px auto;
|
112
|
-
border: 1px solid #ddd;
|
113
|
-
background: #fff;
|
114
|
-
background: linear-gradient(#f6f6f6 0, #fff 50px);
|
115
|
-
background: -o-linear-gradient(#f6f6f6 0, #fff 50px);
|
116
|
-
background: -ms-linear-gradient(#f6f6f6 0, #fff 50px);
|
117
|
-
background: -moz-linear-gradient(#f6f6f6 0, #fff 50px);
|
118
|
-
background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px);
|
119
|
-
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
|
120
|
-
-o-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
121
|
-
-ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
122
|
-
-moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
123
|
-
-webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
124
|
-
}
|
125
92
|
EOT
|
126
93
|
)
|
127
94
|
end
|
128
95
|
|
129
96
|
def body(doc)
|
130
|
-
doc.body({
|
131
|
-
|
132
|
-
|
97
|
+
doc.body({ :onload => 'init()' }) {
|
98
|
+
prev_page = @predecessor ? @predecessor.fit_file[0..-5] + '.html' : nil
|
99
|
+
next_page = @successor ? @successor.fit_file[0..-5] + '.html' : nil
|
100
|
+
titlebar(doc, nil, prev_page, 'index.html', next_page)
|
101
|
+
# The main area with the 2 column layout.
|
102
|
+
doc.div({ :class => 'main' }) {
|
103
|
+
doc.div({ :class => 'left_col' }) {
|
133
104
|
@report.to_html(doc)
|
134
105
|
@track_view.div(doc)
|
106
|
+
@device_list.to_html(doc)
|
107
|
+
@user_profile.to_html(doc)
|
135
108
|
}
|
136
|
-
doc.div({
|
109
|
+
doc.div({ :class => 'right_col' }) {
|
137
110
|
@chart_view.div(doc)
|
138
111
|
}
|
139
112
|
}
|
113
|
+
footer(doc)
|
140
114
|
}
|
141
115
|
end
|
142
116
|
|
@@ -149,10 +123,6 @@ EOT
|
|
149
123
|
end
|
150
124
|
end
|
151
125
|
|
152
|
-
def show_in_browser
|
153
|
-
system("firefox \"#{@output_file}\" &")
|
154
|
-
end
|
155
|
-
|
156
126
|
end
|
157
127
|
|
158
128
|
end
|
data/lib/postrunner/ChartView.rb
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = ChartView.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 'postrunner/ViewWidgets'
|
2
14
|
|
3
15
|
module PostRunner
|
@@ -6,8 +18,10 @@ module PostRunner
|
|
6
18
|
|
7
19
|
include ViewWidgets
|
8
20
|
|
9
|
-
def initialize(activity)
|
21
|
+
def initialize(activity, unit_system)
|
10
22
|
@activity = activity
|
23
|
+
@unit_system = unit_system
|
24
|
+
@empty_charts = {}
|
11
25
|
end
|
12
26
|
|
13
27
|
def head(doc)
|
@@ -21,16 +35,29 @@ module PostRunner
|
|
21
35
|
end
|
22
36
|
|
23
37
|
def div(doc)
|
24
|
-
chart_div(doc, 'pace',
|
25
|
-
chart_div(doc, 'altitude',
|
38
|
+
chart_div(doc, 'pace', "Pace (#{select_unit('min/km')})")
|
39
|
+
chart_div(doc, 'altitude', "Elevation (#{select_unit('m')})")
|
26
40
|
chart_div(doc, 'heart_rate', 'Heart Rate (bpm)')
|
27
|
-
chart_div(doc, '
|
28
|
-
chart_div(doc, 'vertical_oscillation',
|
41
|
+
chart_div(doc, 'run_cadence', 'Run Cadence (spm)')
|
42
|
+
chart_div(doc, 'vertical_oscillation',
|
43
|
+
"Vertical Oscillation (#{select_unit('cm')})")
|
29
44
|
chart_div(doc, 'stance_time', 'Ground Contact Time (ms)')
|
30
45
|
end
|
31
46
|
|
32
47
|
private
|
33
48
|
|
49
|
+
def select_unit(metric_unit)
|
50
|
+
case @unit_system
|
51
|
+
when :metric
|
52
|
+
metric_unit
|
53
|
+
when :statute
|
54
|
+
{ 'min/km' => 'min/mi', 'm' => 'ft', 'cm' => 'in',
|
55
|
+
'bpm' => 'bpm', 'spm' => 'spm', 'ms' => 'ms' }[metric_unit]
|
56
|
+
else
|
57
|
+
Log.fatal "Unknown unit system #{@unit_system}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
34
61
|
def style
|
35
62
|
<<EOT
|
36
63
|
.chart-container {
|
@@ -53,7 +80,7 @@ module PostRunner
|
|
53
80
|
-webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
54
81
|
}
|
55
82
|
.chart-placeholder {
|
56
|
-
width:
|
83
|
+
width: 580px;
|
57
84
|
height: 200px;
|
58
85
|
font-size: 14px;
|
59
86
|
line-height: 1.2em;
|
@@ -64,22 +91,22 @@ EOT
|
|
64
91
|
def java_script
|
65
92
|
s = "$(function() {\n"
|
66
93
|
|
67
|
-
s << line_graph('pace', '#0A7BEE' )
|
68
|
-
s << line_graph('altitude', '#5AAA44')
|
69
|
-
s << line_graph('heart_rate', '#900000')
|
70
|
-
s << point_graph('
|
94
|
+
s << line_graph('pace', 'min/km', '#0A7BEE' )
|
95
|
+
s << line_graph('altitude', 'm', '#5AAA44')
|
96
|
+
s << line_graph('heart_rate', 'bpm', '#900000')
|
97
|
+
s << point_graph('run_cadence', 'spm',
|
71
98
|
[ [ '#EE3F2D', 151 ],
|
72
99
|
[ '#F79666', 163 ],
|
73
100
|
[ '#A0D488', 174 ],
|
74
101
|
[ '#96D7DE', 185 ],
|
75
|
-
[ '#A88BBB', nil ] ]
|
76
|
-
s << point_graph('vertical_oscillation',
|
77
|
-
[ [ '#A88BBB',
|
78
|
-
[ '#96D7DE',
|
79
|
-
[ '#A0D488',
|
80
|
-
[ '#F79666',
|
81
|
-
[ '#EE3F2D', nil ] ]
|
82
|
-
s << point_graph('stance_time',
|
102
|
+
[ '#A88BBB', nil ] ])
|
103
|
+
s << point_graph('vertical_oscillation', 'cm',
|
104
|
+
[ [ '#A88BBB', 67 ],
|
105
|
+
[ '#96D7DE', 84 ],
|
106
|
+
[ '#A0D488', 101 ],
|
107
|
+
[ '#F79666', 118 ],
|
108
|
+
[ '#EE3F2D', nil ] ])
|
109
|
+
s << point_graph('stance_time', 'ms',
|
83
110
|
[ [ '#A88BBB', 208 ],
|
84
111
|
[ '#96D7DE', 241 ],
|
85
112
|
[ '#A0D488', 273 ],
|
@@ -91,18 +118,13 @@ EOT
|
|
91
118
|
s
|
92
119
|
end
|
93
120
|
|
94
|
-
def line_graph(field, color = nil)
|
121
|
+
def line_graph(field, unit, color = nil)
|
95
122
|
s = "var #{field}_data = [\n"
|
96
123
|
|
97
|
-
|
124
|
+
data_set = []
|
98
125
|
start_time = @activity.fit_activity.sessions[0].start_time.to_i
|
99
126
|
@activity.fit_activity.records.each do |r|
|
100
|
-
|
101
|
-
first = false
|
102
|
-
else
|
103
|
-
s << ', '
|
104
|
-
end
|
105
|
-
value = r.send(field)
|
127
|
+
value = r.get_as(field, select_unit(unit))
|
106
128
|
if field == 'pace'
|
107
129
|
if value > 20.0
|
108
130
|
value = nil
|
@@ -110,17 +132,26 @@ EOT
|
|
110
132
|
value = (value * 3600.0 * 1000).to_i
|
111
133
|
end
|
112
134
|
end
|
113
|
-
|
114
|
-
"#{value ? value : 'null'} ]"
|
135
|
+
data_set << [ ((r.timestamp.to_i - start_time) * 1000).to_i, value ]
|
115
136
|
end
|
116
137
|
|
138
|
+
# We don't want to plot charts with all nil values.
|
139
|
+
unless data_set.find { |v| v[1] != nil }
|
140
|
+
@empty_charts[field] = true
|
141
|
+
return ''
|
142
|
+
end
|
143
|
+
s << data_set.map do |set|
|
144
|
+
"[ #{set[0]}, #{set[1] ? set[1] : 'null'} ]"
|
145
|
+
end.join(', ')
|
146
|
+
|
117
147
|
s << <<"EOT"
|
118
148
|
];
|
119
149
|
|
120
150
|
$.plot("##{field}_chart",
|
121
151
|
[ { data: #{field}_data,
|
122
152
|
#{color ? "color: \"#{color}\"," : ''}
|
123
|
-
lines: { show: true#{field == 'pace' ? '' :
|
153
|
+
lines: { show: true#{field == 'pace' ? '' :
|
154
|
+
', fill: true'} } } ],
|
124
155
|
{ xaxis: { mode: "time" }
|
125
156
|
EOT
|
126
157
|
if field == 'pace'
|
@@ -131,7 +162,7 @@ EOT
|
|
131
162
|
s << "});\n"
|
132
163
|
end
|
133
164
|
|
134
|
-
def point_graph(field,
|
165
|
+
def point_graph(field, unit, colors)
|
135
166
|
# We need to split the field values into separate data sets for each
|
136
167
|
# color. The max value for each color determines which set a data point
|
137
168
|
# ends up in.
|
@@ -144,8 +175,7 @@ EOT
|
|
144
175
|
start_time = @activity.fit_activity.sessions[0].start_time.to_i
|
145
176
|
@activity.fit_activity.records.each do |r|
|
146
177
|
# Undefined values will be discarded.
|
147
|
-
next unless (value = r.
|
148
|
-
value *= multiplier
|
178
|
+
next unless (value = r.send(field))
|
149
179
|
|
150
180
|
# Find the right set by looking at the maximum allowed values for each
|
151
181
|
# color.
|
@@ -156,13 +186,19 @@ EOT
|
|
156
186
|
# allowed range for this set, so add the value as x/y pair to the
|
157
187
|
# set.
|
158
188
|
x_val = (r.timestamp.to_i - start_time) * 1000
|
159
|
-
data_sets[i] << [ x_val,
|
189
|
+
data_sets[i] << [ x_val, r.get_as(field, select_unit(unit)) ]
|
160
190
|
# Abort the color loop since we've found the right set already.
|
161
191
|
break
|
162
192
|
end
|
163
193
|
end
|
164
194
|
end
|
165
195
|
|
196
|
+
# We don't want to plot charts with all nil values.
|
197
|
+
if data_sets.values.flatten.empty?
|
198
|
+
@empty_charts[field] = true
|
199
|
+
return ''
|
200
|
+
end
|
201
|
+
|
166
202
|
# Now generate the JS variable definitions for each set.
|
167
203
|
s = ''
|
168
204
|
data_sets.each do |index, ds|
|
@@ -184,6 +220,9 @@ EOT
|
|
184
220
|
end
|
185
221
|
|
186
222
|
def chart_div(doc, field, title)
|
223
|
+
# Don't plot frame for graph without data.
|
224
|
+
return if @empty_charts[field]
|
225
|
+
|
187
226
|
frame(doc, title) {
|
188
227
|
doc.div({ 'id' => "#{field}_chart", 'class' => 'chart-placeholder'})
|
189
228
|
}
|
@@ -0,0 +1,104 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = DeviceList.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/ViewWidgets'
|
16
|
+
|
17
|
+
module PostRunner
|
18
|
+
|
19
|
+
class DeviceList
|
20
|
+
|
21
|
+
include ViewWidgets
|
22
|
+
|
23
|
+
def initialize(fit_activity)
|
24
|
+
@fit_activity = fit_activity
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_html(doc)
|
28
|
+
frame(doc, 'Devices') {
|
29
|
+
devices.each { |d| d.to_html(doc) }
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
devices.map { |d| d.to_s }.join("\n")
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def devices
|
40
|
+
tables = []
|
41
|
+
seen_indexes = []
|
42
|
+
@fit_activity.device_infos.reverse_each do |device|
|
43
|
+
next if seen_indexes.include?(device.device_index) ||
|
44
|
+
device.manufacturer.nil? ||
|
45
|
+
device.manufacturer == 'Undocumented value 0' ||
|
46
|
+
device.device_type == 'Undocumented value 0'
|
47
|
+
|
48
|
+
tables << (t = FlexiTable.new)
|
49
|
+
t.set_html_attrs(:style, 'margin-bottom: 15px') if tables.length != 1
|
50
|
+
t.body
|
51
|
+
|
52
|
+
t.cell('Manufacturer:', { :width => '40%' })
|
53
|
+
t.cell(device.manufacturer, { :width => '60%' })
|
54
|
+
t.new_row
|
55
|
+
|
56
|
+
if (product = device.product)
|
57
|
+
t.cell('Product:')
|
58
|
+
rename = { 'fr620' => 'FR620', 'sdm4' => 'SDM4',
|
59
|
+
'hrm_run_single_byte_product_id' => 'HRM Run',
|
60
|
+
'hrm_run' => 'HRM Run' }
|
61
|
+
product = rename[product] if rename.include?(product)
|
62
|
+
t.cell(product)
|
63
|
+
t.new_row
|
64
|
+
end
|
65
|
+
if (type = device.device_type)
|
66
|
+
rename = { 'heart_rate' => 'Heart Rate Sensor',
|
67
|
+
'stride_speed_distance' => 'Footpod',
|
68
|
+
'running_dynamics' => 'Running Dynamics' }
|
69
|
+
type = rename[type] if rename.include?(type)
|
70
|
+
t.cell('Device Type:')
|
71
|
+
t.cell(type)
|
72
|
+
t.new_row
|
73
|
+
end
|
74
|
+
if device.serial_number
|
75
|
+
t.cell('Serial Number:')
|
76
|
+
t.cell(device.serial_number)
|
77
|
+
t.new_row
|
78
|
+
end
|
79
|
+
if device.software_version
|
80
|
+
t.cell('Software Version:')
|
81
|
+
t.cell(device.software_version)
|
82
|
+
t.new_row
|
83
|
+
end
|
84
|
+
if (rx_ok = device.rx_packets_ok) && (rx_err = device.rx_packets_err)
|
85
|
+
t.cell('Packet Errors:')
|
86
|
+
t.cell('%d%%' % ((rx_err.to_f / (rx_ok + rx_err)) * 100).to_i)
|
87
|
+
t.new_row
|
88
|
+
end
|
89
|
+
if device.battery_status
|
90
|
+
t.cell('Battery Status:')
|
91
|
+
t.cell(device.battery_status)
|
92
|
+
t.new_row
|
93
|
+
end
|
94
|
+
|
95
|
+
seen_indexes << device.device_index
|
96
|
+
end
|
97
|
+
|
98
|
+
tables.reverse
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|