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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 44f97b4e5dfa4c8a6c9c8a4f9cab4132b2b19b1a
4
- data.tar.gz: 54a3a63da33ff6520ba83b98c0b8b34d5a87fcf8
3
+ metadata.gz: 8175543b8737461af3bcc0e1430d0c7185d5502b
4
+ data.tar.gz: 2b5ede7abe4719de50d2a2f1d6879286744ede18
5
5
  SHA512:
6
- metadata.gz: 37d9b100dd4a8f2038f03e184d1ca0906f68993a40f7aa1fe27ff85d30571371274376ebccb5960f3b4981533e1b5c0df13cf01aad863129b4b020a87bc9909c
7
- data.tar.gz: 2196e339f331e5789984badbd969af6d08bedb9eaa126441be6dbac319339bee038c39c7c77c023d939519f8d0e04d593985eb5dbfbbcc8e3024c9f278bf5831
6
+ metadata.gz: 132301b28c06fa5514be5fe4f99f271eb0a1c8922db6d3dbd815cab8110d0896cf5791f6ff472e85e2f061bf4c0f28a6170f74136fb6a3229a212e5ea630965f
7
+ data.tar.gz: 537282d6b43f6874cbc1cc171bc426e1bb609df9a03b49104bbcd461aff62cf5b781f5ae62449b5930b029816d0f3b4d86e94d7b6acd71d3e965ea932e492854
@@ -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 = 'multisport',
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
- s << ", yaxis: { min: #{0.9 * min_value} }" if min_value > 0.0
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)
@@ -231,6 +231,9 @@ module PostRunner
231
231
  end
232
232
  end
233
233
 
234
+ unless @window_start_time
235
+ raise RuntimeError, "No window start time set for day #{day}"
236
+ end
234
237
  end
235
238
 
236
239
  end
@@ -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
- current_category = :low_hr
305
- transitions += 1
306
- last_transition_delta = last_heart_rate - @heart_rate[i]
307
- last_transition_index = i
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
- current_category = :high_hr
324
- transitions += 1
325
- last_transition_delta = @heart_rate[i] - last_heart_rate
326
- last_transition_index = i
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
@@ -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
- @db = PEROBS::Store.new(File.join(@db_dir, 'database'))
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
- return execute_command(args)
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 in a web browser. If no reference
220
- is provided show the list of activities in the database.
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.', 'km', 'kCal' ])
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, day_str)
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[:floors] / counted_days, 10))
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, day_str,
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))
@@ -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({ 'src' => 'openlayers/ol.js' })
43
- doc.script({ 'src' => 'postrunner/trackview.js' })
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
@@ -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({ 'src' => 'postrunner/postrunner.js' })
43
+ doc.script({ 'language' => 'javascript', 'type' => 'text/javascript',
44
+ 'src' => 'postrunner/postrunner.js' })
44
45
  }
45
46
  }
46
47
  doc.head {
@@ -11,5 +11,5 @@
11
11
  #
12
12
 
13
13
  module PostRunner
14
- VERSION = '0.6.0'
14
+ VERSION = '0.7.0'
15
15
  end
@@ -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.6.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: 2016-07-13 00:00:00.000000000 Z
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