postrunner 0.6.0 → 0.7.0
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/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
|