postrunner 0.0.4 → 0.0.5
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/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
|
+
|