postrunner 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/postrunner/ChartView.rb +12 -3
- data/lib/postrunner/DailyMonitoringAnalyzer.rb +3 -0
- data/lib/postrunner/DailyMonitoringView.rb +85 -0
- data/lib/postrunner/DailySleepAnalyzer.rb +19 -9
- data/lib/postrunner/FitFileStore.rb +41 -1
- data/lib/postrunner/Main.rb +54 -5
- data/lib/postrunner/MonitoringStatistics.rb +204 -5
- data/lib/postrunner/TrackView.rb +7 -3
- data/lib/postrunner/ViewFrame.rb +2 -1
- data/lib/postrunner/version.rb +1 -1
- data/misc/postrunner/postrunner.js +34 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8175543b8737461af3bcc0e1430d0c7185d5502b
|
4
|
+
data.tar.gz: 2b5ede7abe4719de50d2a2f1d6879286744ede18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 132301b28c06fa5514be5fe4f99f271eb0a1c8922db6d3dbd815cab8110d0896cf5791f6ff472e85e2f061bf4c0f28a6170f74136fb6a3229a212e5ea630965f
|
7
|
+
data.tar.gz: 537282d6b43f6874cbc1cc171bc426e1bb609df9a03b49104bbcd461aff62cf5b781f5ae62449b5930b029816d0f3b4d86e94d7b6acd71d3e965ea932e492854
|
data/lib/postrunner/ChartView.rb
CHANGED
@@ -30,7 +30,7 @@ module PostRunner
|
|
30
30
|
:unit => select_unit('min/km'),
|
31
31
|
:graph => :line_graph,
|
32
32
|
:colors => '#0A7BEE',
|
33
|
-
:show => @sport == 'running' || @sport
|
33
|
+
:show => @sport == 'running' || @sport == 'multisport',
|
34
34
|
},
|
35
35
|
{
|
36
36
|
:id => 'speed',
|
@@ -63,7 +63,9 @@ module PostRunner
|
|
63
63
|
:unit => 'ms',
|
64
64
|
:graph => :line_graph,
|
65
65
|
:colors => '#900000',
|
66
|
-
:show => @hrv_analyzer.has_hrv_data
|
66
|
+
:show => @hrv_analyzer.has_hrv_data?,
|
67
|
+
:min_y => -30,
|
68
|
+
:max_y => 30
|
67
69
|
},
|
68
70
|
{
|
69
71
|
:id => 'hrv_score',
|
@@ -327,7 +329,14 @@ EOT
|
|
327
329
|
" inverseTransform: function (v) { return -v; } }"
|
328
330
|
else
|
329
331
|
# Set the minimum slightly below the lowest found value.
|
330
|
-
|
332
|
+
if min_value > 0.0 && !chart[:min_y]
|
333
|
+
s << ", yaxis: { min: #{0.9 * min_value} }"
|
334
|
+
end
|
335
|
+
end
|
336
|
+
if chart[:min_y]
|
337
|
+
s << ", yaxis: { #{chart[:min_y] ? "min: #{chart[:min_y]}" : '' } " +
|
338
|
+
"#{chart[:min_y] && chart[:max_y] ? ', ' : ''}" +
|
339
|
+
"#{chart[:max_y] ? "max: #{chart[:max_y]}" : '' } }"
|
331
340
|
end
|
332
341
|
s << "});\n"
|
333
342
|
s << lap_mark_labels(chart_id, start_time)
|
@@ -0,0 +1,85 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = DailyMonitoringView.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2016 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/View'
|
16
|
+
require 'postrunner/MonitoringStatistics'
|
17
|
+
|
18
|
+
module PostRunner
|
19
|
+
|
20
|
+
class DailyMonitoringView < View
|
21
|
+
|
22
|
+
attr_reader :file_name
|
23
|
+
|
24
|
+
def initialize(db, date, monitoring_files)
|
25
|
+
@db = db
|
26
|
+
@ffs = db['file_store']
|
27
|
+
views = @ffs.views
|
28
|
+
views.current_page = nil
|
29
|
+
@date = date
|
30
|
+
@monitoring_files = monitoring_files
|
31
|
+
|
32
|
+
@file_name = File.join(@db['config']['html_dir'], "#{date}.html")
|
33
|
+
|
34
|
+
pages = PagingButtons.new([ date ])
|
35
|
+
#pages.current_page = "#{date}.html"
|
36
|
+
|
37
|
+
super("PostRunner Daily Monitoring: #{date}", views, pages)
|
38
|
+
generate_html(@doc)
|
39
|
+
write(@file_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def generate_html(doc)
|
45
|
+
doc.unique(:dailymonitoringview_style) {
|
46
|
+
doc.head {
|
47
|
+
[ 'jquery/jquery-2.1.1.min.js', 'flot/jquery.flot.js',
|
48
|
+
'flot/jquery.flot.time.js' ].each do |js|
|
49
|
+
doc.script({ 'language' => 'javascript',
|
50
|
+
'type' => 'text/javascript', 'src' => js })
|
51
|
+
end
|
52
|
+
doc.style(style)
|
53
|
+
}
|
54
|
+
}
|
55
|
+
#doc.meta({ 'name' => 'viewport',
|
56
|
+
# 'content' => 'width=device-width, ' +
|
57
|
+
# 'initial-scale=1.0, maximum-scale=1.0, ' +
|
58
|
+
# 'user-scalable=0' })
|
59
|
+
|
60
|
+
body {
|
61
|
+
doc.body {
|
62
|
+
doc.div({ :class => 'main' }) {
|
63
|
+
MonitoringStatistics.new(@monitoring_files).daily_html(@date, doc)
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def style
|
70
|
+
<<EOT
|
71
|
+
body {
|
72
|
+
font-family: verdana,arial,sans-serif;
|
73
|
+
margin: 0px;
|
74
|
+
}
|
75
|
+
.main {
|
76
|
+
width: 550px;
|
77
|
+
margin: 0 auto;
|
78
|
+
}
|
79
|
+
EOT
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
@@ -277,7 +277,13 @@ module PostRunner
|
|
277
277
|
|
278
278
|
# During the nightly sleep the heart rate is alternating between a high
|
279
279
|
# and a low frequency. The actual frequencies vary so that we need to look
|
280
|
-
# for the transitions to classify each sample as high or low.
|
280
|
+
# for the transitions to classify each sample as high or low. Research has
|
281
|
+
# shown that sleep cycles are roughly 90 minutes long. The early cycles
|
282
|
+
# have a lot more deep sleep (low HR) and less REM (high HR) while with
|
283
|
+
# every cycle the deep sleep phase shortens and the REM phase gets longer.
|
284
|
+
# We assume that a normalized half-phase is at least 25 minutes long and
|
285
|
+
# the weight shifts by 4 minutes towards the high HR (REM) phase with
|
286
|
+
# every phase.
|
281
287
|
def categorize_sleep_heart_rate
|
282
288
|
@sleep_heart_rate_classification = Array.new(TIME_WINDOW_MINUTES, nil)
|
283
289
|
|
@@ -301,10 +307,12 @@ module PostRunner
|
|
301
307
|
if current_category == :high_hr
|
302
308
|
if last_heart_rate > @heart_rate[i]
|
303
309
|
# High/low transition found
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
310
|
+
if i - last_transition_index >= 25 - 2 * transitions
|
311
|
+
current_category = :low_hr
|
312
|
+
transitions += 1
|
313
|
+
last_transition_delta = last_heart_rate - @heart_rate[i]
|
314
|
+
last_transition_index = i
|
315
|
+
end
|
308
316
|
elsif last_heart_rate < @heart_rate[i] &&
|
309
317
|
last_transition_delta < @heart_rate[i] - last_heart_rate
|
310
318
|
# The previously found high segment was wrongly categorized as
|
@@ -320,10 +328,12 @@ module PostRunner
|
|
320
328
|
else
|
321
329
|
if last_heart_rate < @heart_rate[i]
|
322
330
|
# Low/High transition found.
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
331
|
+
if i - last_transition_index >= 25 + 2 * transitions
|
332
|
+
current_category = :high_hr
|
333
|
+
transitions += 1
|
334
|
+
last_transition_delta = @heart_rate[i] - last_heart_rate
|
335
|
+
last_transition_index = i
|
336
|
+
end
|
327
337
|
elsif last_heart_rate > @heart_rate[i] &&
|
328
338
|
last_transition_delta < last_heart_rate - @heart_rate[i]
|
329
339
|
# The previously found low segment was wrongly categorized as
|
@@ -17,6 +17,7 @@ require 'postrunner/Log'
|
|
17
17
|
require 'postrunner/DirUtils'
|
18
18
|
require 'postrunner/FFS_Device'
|
19
19
|
require 'postrunner/ActivityListView'
|
20
|
+
require 'postrunner/DailyMonitoringView'
|
20
21
|
require 'postrunner/ViewButtons'
|
21
22
|
require 'postrunner/MonitoringStatistics'
|
22
23
|
|
@@ -336,7 +337,26 @@ module PostRunner
|
|
336
337
|
end
|
337
338
|
end
|
338
339
|
|
340
|
+
def show_monitoring(day)
|
341
|
+
# 'day' specifies the current day. But we don't know what timezone the
|
342
|
+
# watch was set to for a given date. The files are always named after
|
343
|
+
# the moment of finishing the recording expressed as GMT time.
|
344
|
+
# Each file contains information about the time zone for the specific
|
345
|
+
# file. Recording is always flipped to a new file at midnight GMT but
|
346
|
+
# there are usually multiple files per GMT day.
|
347
|
+
day_as_time = Time.parse(day).gmtime
|
348
|
+
# To get weekly intensity minutes we need 7 days of data prior to the
|
349
|
+
# current date and 1 day after to include the following night. We add
|
350
|
+
# at least 12 extra hours to accomodate time zone changes.
|
351
|
+
monitoring_files = monitorings(day_as_time - 8 * 24 * 60 * 60,
|
352
|
+
day_as_time + 36 * 60 * 60)
|
353
|
+
|
354
|
+
show_in_browser(DailyMonitoringView.new(@store, day, monitoring_files).
|
355
|
+
file_name)
|
356
|
+
end
|
357
|
+
|
339
358
|
def daily_report(day)
|
359
|
+
monitoring_files = gather_monitoring_files(day)
|
340
360
|
# 'day' specifies the current day. But we don't know what timezone the
|
341
361
|
# watch was set to for a given date. The files are always named after
|
342
362
|
# the moment of finishing the recording expressed as GMT time.
|
@@ -353,8 +373,28 @@ module PostRunner
|
|
353
373
|
puts MonitoringStatistics.new(monitoring_files).daily(day)
|
354
374
|
end
|
355
375
|
|
376
|
+
def weekly_report(day)
|
377
|
+
# 'day' specifies the current week. It must be in the form of
|
378
|
+
# YYYY-MM-DD and references a day in the specific week. But we don't
|
379
|
+
# know what timezone the watch was set to for a given date. The files
|
380
|
+
# are always named after the moment of finishing the recording expressed
|
381
|
+
# as GMT time. Each file contains information about the time zone for
|
382
|
+
# the specific file. Recording is always flipped to a new file at
|
383
|
+
# midnight GMT but there are usually multiple files per
|
384
|
+
# GMT day.
|
385
|
+
day_as_time = Time.parse(day).gmtime
|
386
|
+
start_day = day_as_time -
|
387
|
+
(24 * 60 * 60 * (day_as_time.wday - @store['config']['week_start_day']))
|
388
|
+
# To get weekly intensity minutes we need 7 days of data prior to the
|
389
|
+
# current month start and 1 after to include the following night. We add
|
390
|
+
# at least 12 extra hours to accomondate time zone changes.
|
391
|
+
monitoring_files = monitorings(start_day - 8 * 24 * 60 * 60,
|
392
|
+
start_day + 8 * 24 * 60 * 60)
|
393
|
+
|
394
|
+
puts MonitoringStatistics.new(monitoring_files).weekly(start_day)
|
395
|
+
end
|
396
|
+
|
356
397
|
def monthly_report(day)
|
357
|
-
monitorings = []
|
358
398
|
# 'day' specifies the current month. It must be in the form of
|
359
399
|
# YYYY-MM-01. But we don't know what timezone the watch was set to for a
|
360
400
|
# given date. The files are always named after the moment of finishing
|
data/lib/postrunner/Main.rb
CHANGED
@@ -13,6 +13,7 @@
|
|
13
13
|
require 'optparse'
|
14
14
|
require 'fit4ruby'
|
15
15
|
require 'perobs'
|
16
|
+
require 'fileutils'
|
16
17
|
|
17
18
|
require 'postrunner/version'
|
18
19
|
require 'postrunner/Log'
|
@@ -54,12 +55,16 @@ module PostRunner
|
|
54
55
|
|
55
56
|
begin
|
56
57
|
create_directory(@db_dir, 'PostRunner data')
|
57
|
-
|
58
|
+
ensure_flat_file_db
|
59
|
+
@db = PEROBS::Store.new(File.join(@db_dir, 'database'),
|
60
|
+
{ :engine => PEROBS::FlatFileDB })
|
58
61
|
# Create a hash to store configuration data in the store unless it
|
59
62
|
# exists already.
|
60
63
|
cfg = (@db['config'] ||= @db.new(PEROBS::Hash))
|
61
64
|
cfg['unit_system'] ||= :metric
|
62
65
|
cfg['version'] ||= VERSION
|
66
|
+
# First day of the week. 0 means Sunday, 1 Monday and so on.
|
67
|
+
cfg['week_start_day'] ||= 1
|
63
68
|
# We always override the data_dir as the user might have moved the data
|
64
69
|
# directory. The only reason we store it in the DB is to have it
|
65
70
|
# available throught the application.
|
@@ -69,7 +74,10 @@ module PostRunner
|
|
69
74
|
cfg['html_dir'] = File.join(@db_dir, 'html')
|
70
75
|
|
71
76
|
setup_directories
|
72
|
-
|
77
|
+
retval = execute_command(args)
|
78
|
+
@db.exit
|
79
|
+
|
80
|
+
return retval
|
73
81
|
|
74
82
|
rescue Exception => e
|
75
83
|
if e.is_a?(SystemExit) || e.is_a?(Interrupt)
|
@@ -215,9 +223,10 @@ set <attribute> <value> <ref>
|
|
215
223
|
type: The type of the activity
|
216
224
|
subtype: The subtype of the activity
|
217
225
|
|
218
|
-
show [ <ref> ]
|
219
|
-
Show the referenced FIT activity
|
220
|
-
is provided show the list of
|
226
|
+
show [ <ref> | <YYYY-MM-DD> ]
|
227
|
+
Show the referenced FIT activity or monitoring data for the given
|
228
|
+
date in a web browser. If no argument is provided show the list of
|
229
|
+
activities in the database.
|
221
230
|
|
222
231
|
sources [ <ref> ]
|
223
232
|
Show the data sources for the various measurements and how they
|
@@ -235,6 +244,11 @@ htmldir <directory>
|
|
235
244
|
update-gps Download the current set of GPS Extended Prediction Orbit (EPO)
|
236
245
|
data and store them on the device.
|
237
246
|
|
247
|
+
weekly [ <YYYY-MM-DD> ]
|
248
|
+
|
249
|
+
Print a table with various statistics for each day of the specified
|
250
|
+
week. If no date is given, yesterday's week will be used.
|
251
|
+
|
238
252
|
|
239
253
|
<fit file> An absolute or relative name of a .FIT file.
|
240
254
|
|
@@ -307,6 +321,10 @@ EOT
|
|
307
321
|
# Get the date of requested day in 'YY-MM-DD' format. If no argument
|
308
322
|
# is given, use the current date.
|
309
323
|
@ffs.daily_report(day_in_localtime(args, '%Y-%m-%d'))
|
324
|
+
when 'weekly'
|
325
|
+
# Get the date of requested day in 'YY-MM-DD' format. If no argument
|
326
|
+
# is given, use the current date.
|
327
|
+
@ffs.weekly_report(day_in_localtime(args, '%Y-%m-%d'))
|
310
328
|
when 'monthly'
|
311
329
|
# Get the date of requested day in 'YY-MM-DD' format. If no argument
|
312
330
|
# is given, use the current date.
|
@@ -351,6 +369,10 @@ EOT
|
|
351
369
|
when 'show'
|
352
370
|
if args.empty?
|
353
371
|
@ffs.show_list_in_browser
|
372
|
+
elsif args[0] =~ /\A2\d{3}-\d{2}-\d{2}\z/
|
373
|
+
# Likely a valid YYYY-MM-DD date. Show the monitoring data for the
|
374
|
+
# given day in a browser.
|
375
|
+
@ffs.show_monitoring(args[0])
|
354
376
|
else
|
355
377
|
process_activities(args, :show)
|
356
378
|
end
|
@@ -603,6 +625,33 @@ EOT
|
|
603
625
|
end
|
604
626
|
end
|
605
627
|
|
628
|
+
def ensure_flat_file_db
|
629
|
+
# Earlier versions of the PEROBS library used the BTreeDB as standard
|
630
|
+
# database format. With the introduction of FlatFileDB a much more
|
631
|
+
# compact and faster alternative is available now. Check existing
|
632
|
+
# installations and convert the database if needed.
|
633
|
+
database_dir = File.join(@db_dir, 'database')
|
634
|
+
return unless Dir.exist?(File.join(database_dir, '000'))
|
635
|
+
|
636
|
+
Log.info "Converting BTreeDB based database to FlatFileDB format"
|
637
|
+
db = PEROBS::Store.new(database_dir)
|
638
|
+
if Dir.exist?(database_dir + '-new')
|
639
|
+
# This may be a leftover from a failed conversion.
|
640
|
+
FileUtils.rm_rf(database_dir + '-new')
|
641
|
+
end
|
642
|
+
db.copy(database_dir + '-new', { :engine => PEROBS::FlatFileDB })
|
643
|
+
db.exit
|
644
|
+
|
645
|
+
FileUtils.mv(database_dir, database_dir + '-old')
|
646
|
+
FileUtils.mv(database_dir + '-new', database_dir)
|
647
|
+
|
648
|
+
db = PEROBS::Store.new(database_dir, { :engine => PEROBS::FlatFileDB })
|
649
|
+
db.check
|
650
|
+
db.exit
|
651
|
+
# TODO: Delete -old directory in some later version when we have some
|
652
|
+
# confidence that the conversion always works.
|
653
|
+
Log.info "DB conversion completed successfully"
|
654
|
+
end
|
606
655
|
end
|
607
656
|
|
608
657
|
end
|
@@ -55,6 +55,39 @@ module PostRunner
|
|
55
55
|
str
|
56
56
|
end
|
57
57
|
|
58
|
+
# Generate a report for a certain day.
|
59
|
+
# @param day [String] Date of the day as YYYY-MM-DD string.
|
60
|
+
def daily_html(day, doc)
|
61
|
+
sleep_analyzer = DailySleepAnalyzer.new(@monitoring_files, day,
|
62
|
+
+12 * 60 * 60)
|
63
|
+
monitoring_analyzer = DailyMonitoringAnalyzer.new(@monitoring_files, day)
|
64
|
+
|
65
|
+
doc.div {
|
66
|
+
doc.h2("Daily Monitoring Report for #{day}")
|
67
|
+
daily_goals_table(monitoring_analyzer).to_html(doc)
|
68
|
+
doc.br
|
69
|
+
daily_stats_table(monitoring_analyzer, sleep_analyzer).to_html(doc)
|
70
|
+
|
71
|
+
if sleep_analyzer.sleep_cycles.empty?
|
72
|
+
doc.h3('No sleep data available for this day')
|
73
|
+
else
|
74
|
+
doc.h2("Sleep Statistics for " +
|
75
|
+
"#{sleep_analyzer.window_start_time.strftime('%Y-%m-%d')} - " +
|
76
|
+
"#{sleep_analyzer.window_end_time.strftime('%Y-%m-%d')}")
|
77
|
+
daily_sleep_cycle_table(sleep_analyzer).to_html(doc)
|
78
|
+
end
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
# Generate a report for a certain week.
|
83
|
+
# @param day [String] Date of a day in that week as YYYY-MM-DD string.
|
84
|
+
def weekly(start_day)
|
85
|
+
"Monitoring Statistics for the week of " +
|
86
|
+
"#{start_day.strftime('%Y-%m-%d')}\n\n" +
|
87
|
+
weekly_goal_table(start_day).to_s + "\n" +
|
88
|
+
weekly_sleep_table(start_day).to_s
|
89
|
+
end
|
90
|
+
|
58
91
|
# Generate a report for a certain month.
|
59
92
|
# @param day [String] Date of a day in that months as YYYY-MM-DD string.
|
60
93
|
def monthly(day)
|
@@ -193,6 +226,100 @@ module PostRunner
|
|
193
226
|
t
|
194
227
|
end
|
195
228
|
|
229
|
+
def weekly_goal_table(start_day)
|
230
|
+
t = FlexiTable.new
|
231
|
+
left = { :halign => :left }
|
232
|
+
right = { :halign => :right }
|
233
|
+
t.set_column_attributes([ left ] + [ right ] * 10)
|
234
|
+
t.head
|
235
|
+
t.row([ 'Day', 'Steps', '% of', 'Goal', 'Intens.', '% of',
|
236
|
+
'Floors', '% of', 'Floors', 'Dist.', 'Cals.' ])
|
237
|
+
t.row([ '', '', 'Goal', 'Steps', 'Minutes', '150', 'clmbd.', '10',
|
238
|
+
'descd.', 'm', 'kCal' ])
|
239
|
+
t.body
|
240
|
+
totals = Hash.new(0)
|
241
|
+
counted_days = 0
|
242
|
+
intensity_minutes_sum = 0
|
243
|
+
0.upto(7) do |dow|
|
244
|
+
break if (time = start_day + 24 * 60 * 60 * dow) > Time.now
|
245
|
+
|
246
|
+
day_str = time.strftime('%a')
|
247
|
+
t.cell(day_str)
|
248
|
+
|
249
|
+
analyzer = DailyMonitoringAnalyzer.new(@monitoring_files,
|
250
|
+
time.strftime('%Y-%m-%d'))
|
251
|
+
|
252
|
+
steps_distance_calories = analyzer.steps_distance_calories
|
253
|
+
steps = steps_distance_calories[:steps]
|
254
|
+
totals[:steps] += steps
|
255
|
+
steps_goal = analyzer.steps_goal
|
256
|
+
totals[:steps_goal] += steps_goal
|
257
|
+
t.cell(steps)
|
258
|
+
t.cell(percent(steps, steps_goal))
|
259
|
+
t.cell(steps_goal)
|
260
|
+
|
261
|
+
intensity_minutes =
|
262
|
+
analyzer.intensity_minutes[:moderate_minutes] +
|
263
|
+
2 * analyzer.intensity_minutes[:vigorous_minutes]
|
264
|
+
intensity_minutes_sum += intensity_minutes
|
265
|
+
totals[:intensity_minutes] += intensity_minutes
|
266
|
+
t.cell(intensity_minutes.to_i)
|
267
|
+
t.cell(percent(intensity_minutes_sum, 150))
|
268
|
+
|
269
|
+
floors = analyzer.total_floors
|
270
|
+
floors_climbed = floors[:floors_climbed]
|
271
|
+
totals[:floors_climbed] += floors_climbed
|
272
|
+
t.cell(floors_climbed)
|
273
|
+
t.cell(percent(floors_climbed, 10))
|
274
|
+
|
275
|
+
floors_descended = floors[:floors_descended]
|
276
|
+
totals[:floors_descended] += floors_descended
|
277
|
+
t.cell(floors_descended)
|
278
|
+
|
279
|
+
|
280
|
+
distance = steps_distance_calories[:distance]
|
281
|
+
totals[:distance] += distance
|
282
|
+
t.cell(distance.to_i)
|
283
|
+
|
284
|
+
calories = steps_distance_calories[:calories]
|
285
|
+
totals[:calories] += calories
|
286
|
+
t.cell(calories.to_i)
|
287
|
+
|
288
|
+
t.new_row
|
289
|
+
counted_days += 1
|
290
|
+
end
|
291
|
+
|
292
|
+
t.foot
|
293
|
+
t.cell('Totals')
|
294
|
+
t.cell(totals[:steps])
|
295
|
+
t.cell('')
|
296
|
+
t.cell(totals[:steps_goal])
|
297
|
+
t.cell(totals[:intensity_minutes].to_i)
|
298
|
+
t.cell('')
|
299
|
+
t.cell(totals[:floors_climbed])
|
300
|
+
t.cell('')
|
301
|
+
t.cell(totals[:floors_descended])
|
302
|
+
t.cell(totals[:distance].to_i)
|
303
|
+
t.cell(totals[:calories].to_i)
|
304
|
+
t.new_row
|
305
|
+
|
306
|
+
if counted_days > 0
|
307
|
+
t.cell('Averages')
|
308
|
+
t.cell((totals[:steps] / counted_days).to_i)
|
309
|
+
t.cell(percent(totals[:steps], totals[:steps_goal]))
|
310
|
+
t.cell((totals[:steps_goal] / counted_days).to_i)
|
311
|
+
t.cell((totals[:intensity_minutes] / counted_days).to_i)
|
312
|
+
t.cell(percent(totals[:intensity_minutes], (counted_days / 7.0) * 150))
|
313
|
+
t.cell((totals[:floors_climbed] / counted_days).to_i)
|
314
|
+
t.cell(percent(totals[:floors_climbed] / counted_days, 10))
|
315
|
+
t.cell((totals[:floors_descended] / counted_days).to_i)
|
316
|
+
t.cell('%.0f' % (totals[:distance] / counted_days))
|
317
|
+
t.cell((totals[:calories] / counted_days).to_i)
|
318
|
+
end
|
319
|
+
|
320
|
+
t
|
321
|
+
end
|
322
|
+
|
196
323
|
def monthly_goal_table(year, month, last_day_of_month)
|
197
324
|
t = FlexiTable.new
|
198
325
|
left = { :halign => :left }
|
@@ -202,7 +329,7 @@ module PostRunner
|
|
202
329
|
t.row([ 'Day', 'Steps', '% of', 'Goal', 'Wk. Int.', '% of',
|
203
330
|
'Floors', '% of', 'Floors', 'Dist.', 'Cals.' ])
|
204
331
|
t.row([ '', '', 'Goal', 'Steps', 'Minutes', '150', 'clmbd.', '10',
|
205
|
-
'descd.', '
|
332
|
+
'descd.', 'm', 'kCal' ])
|
206
333
|
t.body
|
207
334
|
totals = Hash.new(0)
|
208
335
|
counted_days = 0
|
@@ -213,7 +340,8 @@ module PostRunner
|
|
213
340
|
day_str = time.strftime('%d %a')
|
214
341
|
t.cell(day_str)
|
215
342
|
|
216
|
-
analyzer = DailyMonitoringAnalyzer.new(@monitoring_files,
|
343
|
+
analyzer = DailyMonitoringAnalyzer.new(@monitoring_files,
|
344
|
+
"#{year}-#{month}-#{dom}")
|
217
345
|
|
218
346
|
steps_distance_calories = analyzer.steps_distance_calories
|
219
347
|
steps = steps_distance_calories[:steps]
|
@@ -282,7 +410,7 @@ module PostRunner
|
|
282
410
|
t.cell((totals[:intensity_minutes] / counted_days).to_i)
|
283
411
|
t.cell(percent(totals[:intensity_minutes], (counted_days / 7.0) * 150))
|
284
412
|
t.cell((totals[:floors_climbed] / counted_days).to_i)
|
285
|
-
t.cell(percent(totals[:
|
413
|
+
t.cell(percent(totals[:floors_climbed] / counted_days, 10))
|
286
414
|
t.cell((totals[:floors_descended] / counted_days).to_i)
|
287
415
|
t.cell('%.0f' % (totals[:distance] / counted_days))
|
288
416
|
t.cell((totals[:calories] / counted_days).to_i)
|
@@ -291,6 +419,76 @@ module PostRunner
|
|
291
419
|
t
|
292
420
|
end
|
293
421
|
|
422
|
+
def weekly_sleep_table(start_day)
|
423
|
+
t = FlexiTable.new
|
424
|
+
left = { :halign => :left }
|
425
|
+
right = { :halign => :right }
|
426
|
+
t.set_column_attributes([ left ] + [ right ] * 6)
|
427
|
+
t.head
|
428
|
+
t.row([ 'Date', 'Total Sleep', 'Cycles', 'REM Sleep', 'Light Sleep',
|
429
|
+
'Deep Sleep', 'RHR' ])
|
430
|
+
t.body
|
431
|
+
totals = Hash.new(0)
|
432
|
+
counted_days = 0
|
433
|
+
rhr_days = 0
|
434
|
+
|
435
|
+
0.upto(7) do |dow|
|
436
|
+
break if (time = start_day + 24 * 60 * 60 * dow) > Time.now
|
437
|
+
|
438
|
+
day_str = time.strftime('%a')
|
439
|
+
t.cell(day_str)
|
440
|
+
|
441
|
+
analyzer = DailySleepAnalyzer.new(@monitoring_files,
|
442
|
+
time.strftime('%Y-%m-%d'),
|
443
|
+
-12 * 60 * 60)
|
444
|
+
|
445
|
+
if (analyzer.sleep_cycles.empty?)
|
446
|
+
5.times { t.cell('-') }
|
447
|
+
else
|
448
|
+
totals[:total_sleep] += analyzer.total_sleep
|
449
|
+
totals[:cycles] += analyzer.sleep_cycles.length
|
450
|
+
totals[:rem_sleep] += analyzer.rem_sleep
|
451
|
+
totals[:light_sleep] += analyzer.light_sleep
|
452
|
+
totals[:deep_sleep] += analyzer.deep_sleep
|
453
|
+
counted_days += 1
|
454
|
+
|
455
|
+
t.cell(secsToHM(analyzer.total_sleep))
|
456
|
+
t.cell(analyzer.sleep_cycles.length)
|
457
|
+
t.cell(secsToHM(analyzer.rem_sleep))
|
458
|
+
t.cell(secsToHM(analyzer.light_sleep))
|
459
|
+
t.cell(secsToHM(analyzer.deep_sleep))
|
460
|
+
end
|
461
|
+
|
462
|
+
if (rhr = analyzer.resting_heart_rate) && rhr > 0
|
463
|
+
t.cell(rhr)
|
464
|
+
totals[:rhr] += rhr
|
465
|
+
rhr_days += 1
|
466
|
+
else
|
467
|
+
t.cell('-')
|
468
|
+
end
|
469
|
+
t.new_row
|
470
|
+
end
|
471
|
+
t.foot
|
472
|
+
t.cell('Averages')
|
473
|
+
if counted_days > 0
|
474
|
+
t.cell(secsToHM(totals[:total_sleep] / counted_days))
|
475
|
+
t.cell('%.1f' % (totals[:cycles].to_f / counted_days))
|
476
|
+
t.cell(secsToHM(totals[:rem_sleep] / counted_days))
|
477
|
+
t.cell(secsToHM(totals[:light_sleep] / counted_days))
|
478
|
+
t.cell(secsToHM(totals[:deep_sleep] / counted_days))
|
479
|
+
else
|
480
|
+
5.times { t.cell('-') }
|
481
|
+
end
|
482
|
+
if rhr_days > 0
|
483
|
+
t.cell('%.0f' % (totals[:rhr] / rhr_days))
|
484
|
+
else
|
485
|
+
t.cell('-')
|
486
|
+
end
|
487
|
+
t.new_row
|
488
|
+
|
489
|
+
t
|
490
|
+
end
|
491
|
+
|
294
492
|
def monthly_sleep_table(year, month, last_day_of_month)
|
295
493
|
t = FlexiTable.new
|
296
494
|
left = { :halign => :left }
|
@@ -310,7 +508,8 @@ module PostRunner
|
|
310
508
|
day_str = time.strftime('%d %a')
|
311
509
|
t.cell(day_str)
|
312
510
|
|
313
|
-
analyzer = DailySleepAnalyzer.new(@monitoring_files,
|
511
|
+
analyzer = DailySleepAnalyzer.new(@monitoring_files,
|
512
|
+
"#{year}-#{month}-#{dom}",
|
314
513
|
-12 * 60 * 60)
|
315
514
|
|
316
515
|
if (analyzer.sleep_cycles.empty?)
|
@@ -343,7 +542,7 @@ module PostRunner
|
|
343
542
|
t.cell('Averages')
|
344
543
|
if counted_days > 0
|
345
544
|
t.cell(secsToHM(totals[:total_sleep] / counted_days))
|
346
|
-
t.cell('%.1f' % (totals[:cycles] / counted_days))
|
545
|
+
t.cell('%.1f' % (totals[:cycles].to_f / counted_days))
|
347
546
|
t.cell(secsToHM(totals[:rem_sleep] / counted_days))
|
348
547
|
t.cell(secsToHM(totals[:light_sleep] / counted_days))
|
349
548
|
t.cell(secsToHM(totals[:deep_sleep] / counted_days))
|
data/lib/postrunner/TrackView.rb
CHANGED
@@ -33,22 +33,26 @@ module PostRunner
|
|
33
33
|
def to_html(doc)
|
34
34
|
return unless @has_geo_data
|
35
35
|
|
36
|
+
doc.body_init_script('pr_trackview_init_xy();')
|
37
|
+
|
36
38
|
doc.head {
|
37
39
|
doc.unique(:trackview_style) {
|
38
40
|
doc.style(style)
|
39
41
|
doc.link({ 'rel' => 'stylesheet',
|
40
42
|
'href' => 'openlayers/ol.css',
|
41
43
|
'type' => 'text/css' })
|
42
|
-
doc.script({ '
|
43
|
-
|
44
|
+
doc.script({ 'language' => 'javascript', 'type' => 'text/javascript',
|
45
|
+
'src' => 'openlayers/ol.js' })
|
46
|
+
doc.script({ 'language' => 'javascript', 'type' => 'text/javascript',
|
47
|
+
'src' => 'postrunner/trackview.js' })
|
44
48
|
}
|
45
49
|
doc.script(java_script)
|
46
|
-
doc.body_init_script('pr_trackview_init_xy()')
|
47
50
|
}
|
48
51
|
|
49
52
|
ViewFrame.new('map', 'Map', 600, nil, true) {
|
50
53
|
doc.div({ 'id' => 'map', 'class' => 'trackmap' })
|
51
54
|
}.to_html(doc)
|
55
|
+
|
52
56
|
end
|
53
57
|
|
54
58
|
private
|
data/lib/postrunner/ViewFrame.rb
CHANGED
@@ -40,7 +40,8 @@ module PostRunner
|
|
40
40
|
# Add the necessary style sheet snippets to the document head.
|
41
41
|
doc.head {
|
42
42
|
doc.style(style)
|
43
|
-
doc.script({ '
|
43
|
+
doc.script({ 'language' => 'javascript', 'type' => 'text/javascript',
|
44
|
+
'src' => 'postrunner/postrunner.js' })
|
44
45
|
}
|
45
46
|
}
|
46
47
|
doc.head {
|
data/lib/postrunner/version.rb
CHANGED
@@ -0,0 +1,34 @@
|
|
1
|
+
function createCookie(name,value,days) {
|
2
|
+
if (days) {
|
3
|
+
var date = new Date();
|
4
|
+
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
5
|
+
var expires = "; expires=" + date.toGMTString();
|
6
|
+
}
|
7
|
+
else {
|
8
|
+
var expires = "";
|
9
|
+
}
|
10
|
+
document.cookie = name+"="+value+expires+"; path=/";
|
11
|
+
}
|
12
|
+
|
13
|
+
function readCookie(name) {
|
14
|
+
var nameEQ = name + "=";
|
15
|
+
var ca = document.cookie.split(';');
|
16
|
+
for (var i = 0; i < ca.length; i++) {
|
17
|
+
var c = ca[i];
|
18
|
+
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
|
19
|
+
if (c.indexOf(nameEQ) == 0) {
|
20
|
+
return c.substring(nameEQ.length,c.length);
|
21
|
+
}
|
22
|
+
}
|
23
|
+
return null;
|
24
|
+
}
|
25
|
+
|
26
|
+
var pr_view_frame_toggle = function(checkbox, target_id) {
|
27
|
+
var cookie_name = "postrunner_checkbox_" + target_id;
|
28
|
+
createCookie(cookie_name, checkbox.checked);
|
29
|
+
if (checkbox.checked) {
|
30
|
+
$(target_id).show();
|
31
|
+
} else {
|
32
|
+
$(target_id).hide();
|
33
|
+
};
|
34
|
+
};
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: postrunner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Schlaeger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-02-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fit4ruby
|
@@ -141,6 +141,7 @@ files:
|
|
141
141
|
- lib/postrunner/BackedUpFile.rb
|
142
142
|
- lib/postrunner/ChartView.rb
|
143
143
|
- lib/postrunner/DailyMonitoringAnalyzer.rb
|
144
|
+
- lib/postrunner/DailyMonitoringView.rb
|
144
145
|
- lib/postrunner/DailySleepAnalyzer.rb
|
145
146
|
- lib/postrunner/DataSources.rb
|
146
147
|
- lib/postrunner/DeviceList.rb
|
@@ -305,6 +306,7 @@ files:
|
|
305
306
|
- misc/jquery/jquery-2.1.1.min.js
|
306
307
|
- misc/openlayers/ol.css
|
307
308
|
- misc/openlayers/ol.js
|
309
|
+
- misc/postrunner/postrunner.js
|
308
310
|
- misc/postrunner/trackview.js
|
309
311
|
- postrunner.gemspec
|
310
312
|
- spec/ActivitySummary_spec.rb
|