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 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