postrunner 0.10.1 → 0.11.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/Activity.rb +2 -1
- data/lib/postrunner/ActivitySummary.rb +92 -0
- data/lib/postrunner/ChartView.rb +5 -5
- data/lib/postrunner/DeviceList.rb +1 -1
- data/lib/postrunner/EPO_Downloader.rb +5 -3
- data/lib/postrunner/FFS_Activity.rb +1 -0
- data/lib/postrunner/FFS_Device.rb +15 -2
- data/lib/postrunner/FitFileStore.rb +17 -11
- data/lib/postrunner/Main.rb +12 -4
- data/lib/postrunner/UserProfileView.rb +29 -16
- data/lib/postrunner/version.rb +1 -1
- data/postrunner.gemspec +2 -2
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8de6ac38768c84e90eb47ddb49429d7a74160b1
|
4
|
+
data.tar.gz: 7816eba71ed5b6f90eef7d7ae15080c19d548624
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3302269a229d363cfc75bbea304879f06c37e8c2e6c1706ebece3ce6b662b02a026315df59e7494de230c5c3cab3fb81b925bd0ef89f4b81b1280805b422b138
|
7
|
+
data.tar.gz: e8179a0afb54a171ba200480cf498523688acffa9f3568b8a2ae1e272a5bdc4c863449c6c43b201dafe6223afeb64f64ca3520799e93317f4aec8114cdcb64f2
|
data/lib/postrunner/Activity.rb
CHANGED
@@ -95,6 +95,7 @@ module PostRunner
|
|
95
95
|
'challenge' => 'Challenge',
|
96
96
|
'indoor_skiing' => 'Indoor Skiing',
|
97
97
|
'cardio_training' => 'Cardio Training',
|
98
|
+
'virtual_activity' => 'Virtual Activity',
|
98
99
|
'all' => 'All'
|
99
100
|
}
|
100
101
|
|
@@ -379,7 +380,7 @@ module PostRunner
|
|
379
380
|
end
|
380
381
|
|
381
382
|
def activity_sub_type
|
382
|
-
ActivitySubTypes[@sub_sport] || 'Undefined'
|
383
|
+
ActivitySubTypes[@sub_sport] || 'Undefined "#{@sub_sport}"'
|
383
384
|
end
|
384
385
|
|
385
386
|
def distance(timestamp, unit_system)
|
@@ -87,6 +87,7 @@ module PostRunner
|
|
87
87
|
{ :metric => 'km', :statute => 'mi'}) ])
|
88
88
|
end
|
89
89
|
t.row([ 'Time:', secsToHMS(session.total_timer_time) ])
|
90
|
+
t.row([ 'Elapsed Time:', secsToHMS(session.total_elapsed_time) ])
|
90
91
|
t.row([ 'Avg. Speed:',
|
91
92
|
local_value(session, 'avg_speed', '%.1f %s',
|
92
93
|
{ :metric => 'km/h', :statute => 'mph' }) ])
|
@@ -145,6 +146,14 @@ module PostRunner
|
|
145
146
|
t.row([ 'Aerobic Training Effect:', session.total_training_effect ])
|
146
147
|
end
|
147
148
|
|
149
|
+
if (p_epoc = peak_epoc) > 0.0
|
150
|
+
t.row([ 'Peak EPOC:', "%.0f ml/kg" % p_epoc ])
|
151
|
+
end
|
152
|
+
|
153
|
+
if (trimp = trimp_exp) > 0.0
|
154
|
+
t.row([ 'TRIMP:', trimp.round ])
|
155
|
+
end
|
156
|
+
|
148
157
|
rec_info = @fit_activity.recovery_info
|
149
158
|
t.row([ 'Ignored Recovery Time:',
|
150
159
|
rec_info ? secsToDHMS(rec_info * 60) : '-' ])
|
@@ -361,6 +370,89 @@ module PostRunner
|
|
361
370
|
end
|
362
371
|
end
|
363
372
|
|
373
|
+
def trimp_exp
|
374
|
+
# According to Bannister/Morton
|
375
|
+
# TRIMPexp = sum(D x HRr x 0.64e^y)
|
376
|
+
# Where
|
377
|
+
#
|
378
|
+
# D is the duration in minutes at a particular Heart Rate
|
379
|
+
# HRr is the Heart Rate as a fraction of Heart Rate Reserve
|
380
|
+
# y is the HRr multiplied by 1.92 for men and 1.67 for women.
|
381
|
+
return 0.0 unless (user_data = @fit_activity.user_data.first)
|
382
|
+
|
383
|
+
user_profile = @fit_activity.user_profiles.first
|
384
|
+
hr_zones = @fit_activity.heart_rate_zones.first
|
385
|
+
session = @fit_activity.sessions[0]
|
386
|
+
|
387
|
+
unless (user_profile && (rest_hr = user_profile.resting_heart_rate)) ||
|
388
|
+
(hr_zones && (rest_hr = hr_zones.resting_heart_rate))
|
389
|
+
# We must have a valid resting heart rate to compute TRIMP.
|
390
|
+
return 0.0
|
391
|
+
end
|
392
|
+
unless (user_data && (max_hr = user_data.max_hr)) ||
|
393
|
+
(hr_zones && (max_hr = hr_zones.max_heart_rate))
|
394
|
+
# We must have a valid maximum heart rate to compute TRIMP.
|
395
|
+
return 0.0
|
396
|
+
end
|
397
|
+
unless (session && session.avg_heart_rate &&
|
398
|
+
avg_hr = session.avg_heart_rate)
|
399
|
+
return 0.0
|
400
|
+
end
|
401
|
+
|
402
|
+
sex_factor = user_data.gender == 'male' ? 1.92 : 1.67
|
403
|
+
|
404
|
+
# Instead of using the average heart rate for the whole activity we
|
405
|
+
# apply the equation for each heart rate sample and accumulate them.
|
406
|
+
sum = 0.0
|
407
|
+
prev_timestamp = nil
|
408
|
+
@activity.fit_activity.records.each do |r|
|
409
|
+
# We need a valid timestmap and a valid previous timestamp. If they
|
410
|
+
# are more than 10 seconds appart we discard the values as there was
|
411
|
+
# likely a pause in the activity.
|
412
|
+
if prev_timestamp && r.timestamp && r.heart_rate &&
|
413
|
+
r.timestamp - prev_timestamp <= 10
|
414
|
+
# Compute the heart rate as fraction of the heart rate reserve
|
415
|
+
hr_r = (r.heart_rate - rest_hr).to_f / (max_hr - rest_hr)
|
416
|
+
|
417
|
+
duration_min = (r.timestamp - prev_timestamp) / 60.0
|
418
|
+
#sum += duration_min * hr_r * 0.64 * Math.exp(sex_factor * hr_r)
|
419
|
+
sum += duration_min * hr_r * 0.64 * Math.exp(sex_factor * hr_r)
|
420
|
+
end
|
421
|
+
|
422
|
+
prev_timestamp = r.timestamp
|
423
|
+
end
|
424
|
+
|
425
|
+
sum
|
426
|
+
|
427
|
+
# Alternatively here is an avarage HR based implementation
|
428
|
+
# hr_r = (session.avg_heart_rate - rest_hr).to_f / (max_hr - rest_hr)
|
429
|
+
# duration_min = session.total_elapsed_time / 60.0
|
430
|
+
# duration_min * hr_r * 0.64 * Math.exp(sex_factor * hr_r)
|
431
|
+
end
|
432
|
+
|
433
|
+
def peak_epoc
|
434
|
+
# Peak EPOC value according to figure 2 in the following white paper by
|
435
|
+
# FristBeat:
|
436
|
+
# https://www.firstbeat.com/wp-content/uploads/2015/10/white_paper_training_effect.pdf
|
437
|
+
unless @fit_activity.physiological_metrics &&
|
438
|
+
(pm = @fit_activity.physiological_metrics.last) &&
|
439
|
+
(te = pm.aerobic_training_effect)
|
440
|
+
return 0.0
|
441
|
+
end
|
442
|
+
unless (user_data = @fit_activity.user_data.first) &&
|
443
|
+
(ac = user_data.activity_class)
|
444
|
+
return 0.0
|
445
|
+
end
|
446
|
+
|
447
|
+
# The following formula was taken from
|
448
|
+
# http://www.movescount.com/apps/app10020404-EPOC_from_TE
|
449
|
+
# It apparently approximates the graph in figure 2 in the FirstBeat
|
450
|
+
# paper.
|
451
|
+
epoc = -11.0 + te * (20.0 + te * (-47.0/4.0 + te * (3.0 - te / 4.0)))
|
452
|
+
(-102.0 + te * (759.0 / 4.0 + te * (-2867.0 / 24.0 +
|
453
|
+
te * (139.0 / 4.0 - 73.0 / 24.0 * te))) - epoc) / 10.0 * ac + epoc
|
454
|
+
end
|
455
|
+
|
364
456
|
end
|
365
457
|
|
366
458
|
end
|
data/lib/postrunner/ChartView.rb
CHANGED
@@ -290,10 +290,10 @@ EOT
|
|
290
290
|
last_value = nil
|
291
291
|
last_timestamp = nil
|
292
292
|
@activity.fit_activity.records.each do |r|
|
293
|
-
if last_timestamp && (r.timestamp - last_timestamp) >
|
293
|
+
if last_timestamp && (r.timestamp - last_timestamp) > 10.0
|
294
294
|
# We have a gap in the values that is longer than 5 seconds. We'll
|
295
295
|
# finish the line and start a new one later.
|
296
|
-
data_set << [ (
|
296
|
+
data_set << [ (last_timestamp - start_time + 1).to_i * 1000, nil ]
|
297
297
|
end
|
298
298
|
if (value = r.get_as(chart[:id], chart[:unit] || ''))
|
299
299
|
if chart[:id] == 'pace'
|
@@ -310,7 +310,7 @@ EOT
|
|
310
310
|
min_value = value if (min_value.nil? || min_value > value)
|
311
311
|
end
|
312
312
|
end
|
313
|
-
|
313
|
+
if value
|
314
314
|
data_set << [ (r.timestamp - start_time).to_i * 1000, value ]
|
315
315
|
end
|
316
316
|
last_value = value
|
@@ -337,7 +337,7 @@ EOT
|
|
337
337
|
#{chart[:colors] ? "color: \"#{chart[:colors]}\"," : ''}
|
338
338
|
lines: { show: true#{chart[:id] == 'pace' ? '' :
|
339
339
|
', fill: true'} } } ],
|
340
|
-
{ xaxis: { mode: "time" },
|
340
|
+
{ xaxis: { mode: "time", min: 0.0 },
|
341
341
|
grid: { markings: lap_marks, hoverable: true }
|
342
342
|
EOT
|
343
343
|
if chart[:id] == 'pace'
|
@@ -417,7 +417,7 @@ EOT
|
|
417
417
|
" fillColor: \"#{chart[:colors][index][0]}\", " +
|
418
418
|
" fill: true, radius: 2 } }"
|
419
419
|
end.join(', ')
|
420
|
-
s << "], { xaxis: { mode: \"time\" }, " +
|
420
|
+
s << "], { xaxis: { mode: \"time\", min: 0.0 }, " +
|
421
421
|
(chart[:id] == 'gct_balance' ? gct_balance_yaxis(data_sets) : '') +
|
422
422
|
" grid: { markings: lap_marks, hoverable: true } });\n"
|
423
423
|
s << lap_mark_labels(chart_id, start_time)
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = DeviceList.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
5
|
#
|
6
|
-
# Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2014, 2015, 2018 by Chris Schlaeger <cs@taskjuggler.org>
|
7
7
|
#
|
8
8
|
# This program is free software; you can redistribute it and/or modify
|
9
9
|
# it under the terms of version 2 of the GNU General Public License as
|
@@ -12,6 +12,7 @@
|
|
12
12
|
|
13
13
|
require 'uri'
|
14
14
|
require 'net/http'
|
15
|
+
require 'net/https'
|
15
16
|
|
16
17
|
module PostRunner
|
17
18
|
|
@@ -20,7 +21,7 @@ module PostRunner
|
|
20
21
|
# devices pick up this file under GARMIN/GARMIN/REMOTESW/EPO.BIN.
|
21
22
|
class EPO_Downloader
|
22
23
|
|
23
|
-
@@URI = URI('
|
24
|
+
@@URI = URI('https://omt.garmin.com/Rce/ProtobufApi/EphemerisService/GetEphemerisData')
|
24
25
|
# This is the payload of the POST request. It was taken from
|
25
26
|
# http://www.kluenter.de/garmin-ephemeris-files-and-linux/. It may contain
|
26
27
|
# a product ID or serial number.
|
@@ -33,7 +34,8 @@ module PostRunner
|
|
33
34
|
|
34
35
|
# Create an EPO_Downloader object.
|
35
36
|
def initialize
|
36
|
-
@
|
37
|
+
@https = Net::HTTP.new(@@URI.host, @@URI.port)
|
38
|
+
@https.use_ssl = true
|
37
39
|
@request = Net::HTTP::Post.new(@@URI.path, initheader = @@HEADER)
|
38
40
|
@request.body = @@POST_DATA
|
39
41
|
end
|
@@ -57,7 +59,7 @@ module PostRunner
|
|
57
59
|
|
58
60
|
def get_epo_from_server
|
59
61
|
begin
|
60
|
-
res = @
|
62
|
+
res = @https.request(@request)
|
61
63
|
rescue => e
|
62
64
|
Log.error "Extended Prediction Orbit (EPO) data download error: " +
|
63
65
|
e.message
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FFS_Device.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
5
|
#
|
6
|
-
# Copyright (c) 2015, 2016 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2015, 2016, 2018 by Chris Schlaeger <cs@taskjuggler.org>
|
7
7
|
#
|
8
8
|
# This program is free software; you can redistribute it and/or modify
|
9
9
|
# it under the terms of version 2 of the GNU General Public License as
|
@@ -23,7 +23,7 @@ module PostRunner
|
|
23
23
|
# dashes. All objects are transparently stored in the PEROBS::Store.
|
24
24
|
class FFS_Device < PEROBS::Object
|
25
25
|
|
26
|
-
attr_persist :activities, :monitorings, :short_uid, :long_uid
|
26
|
+
attr_persist :activities, :monitorings, :metrics, :short_uid, :long_uid
|
27
27
|
|
28
28
|
# Create a new FFS_Device object.
|
29
29
|
# @param p [PEROBS::Handle] p
|
@@ -41,6 +41,7 @@ module PostRunner
|
|
41
41
|
def restore
|
42
42
|
attr_init(:activities) { @store.new(PEROBS::Array) }
|
43
43
|
attr_init(:monitorings) { @store.new(PEROBS::Array) }
|
44
|
+
attr_init(:metrics) { @store.new(PEROBS::Array) }
|
44
45
|
end
|
45
46
|
|
46
47
|
# Add a new FIT file for this device.
|
@@ -61,6 +62,11 @@ module PostRunner
|
|
61
62
|
entities = @monitorings
|
62
63
|
type = 'monitoring'
|
63
64
|
new_entity_class = FFS_Monitoring
|
65
|
+
elsif fit_entity.is_a?(Fit4Ruby::Metrics)
|
66
|
+
entity = metrics_by_file_name(File.basename(fit_file_name))
|
67
|
+
entities = @metrics
|
68
|
+
type = 'metrics'
|
69
|
+
new_entity_class = FFS_Metrics
|
64
70
|
else
|
65
71
|
Log.fatal "Unsupported FIT entity #{fit_entity.class}"
|
66
72
|
end
|
@@ -130,6 +136,13 @@ module PostRunner
|
|
130
136
|
@monitorings.find { |a| a.fit_file_name == file_name }
|
131
137
|
end
|
132
138
|
|
139
|
+
# Return the metrics with the given file name.
|
140
|
+
# @param file_name [String] Base name of the fit file.
|
141
|
+
# @return [FFS_Activity] Corresponding FFS_Metrics or nil.
|
142
|
+
def metrics_by_file_name(file_name)
|
143
|
+
@metrics.find { |a| a.fit_file_name == file_name }
|
144
|
+
end
|
145
|
+
|
133
146
|
# Return all monitorings that overlap with the time interval given by
|
134
147
|
# from_time and to_time.
|
135
148
|
# @param from_time [Time] start time of the interval
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FitFileStore.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
5
|
#
|
6
|
-
# Copyright (c) 2014, 2015, 2016 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2014, 2015, 2016, 2018 by Chris Schlaeger <cs@taskjuggler.org>
|
7
7
|
#
|
8
8
|
# This program is free software; you can redistribute it and/or modify
|
9
9
|
# it under the terms of version 2 of the GNU General Public License as
|
@@ -80,20 +80,15 @@ module PostRunner
|
|
80
80
|
# @return [FFS_Activity or FFS_Monitoring] Corresponding entry in the
|
81
81
|
# FitFileStore or nil if file could not be added.
|
82
82
|
def add_fit_file(fit_file_name, fit_entity = nil, overwrite = false)
|
83
|
-
|
84
|
-
if @store['fit_file_md5sums'].include?(md5sum)
|
85
|
-
# The FIT file is already stored in the DB.
|
86
|
-
return nil unless overwrite
|
87
|
-
end
|
88
|
-
|
89
|
-
# If we the file hasn't been read yet, read it in as a
|
83
|
+
# If the file hasn't been read yet, read it in as a
|
90
84
|
# Fit4Ruby::Activity or Fit4Ruby::Monitoring entity.
|
91
85
|
unless fit_entity
|
92
86
|
return nil unless (fit_entity = read_fit_file(fit_file_name))
|
93
87
|
end
|
94
88
|
|
95
89
|
unless [ Fit4Ruby::Activity,
|
96
|
-
Fit4Ruby::Monitoring_B
|
90
|
+
Fit4Ruby::Monitoring_B,
|
91
|
+
Fit4Ruby::Metrics ].include?(fit_entity.class)
|
97
92
|
Log.fatal "Unsupported FIT file type #{fit_entity.class}"
|
98
93
|
end
|
99
94
|
|
@@ -451,7 +446,8 @@ module PostRunner
|
|
451
446
|
|
452
447
|
def extract_fit_file_id(fit_entity)
|
453
448
|
unless (fid = fit_entity.file_id)
|
454
|
-
Log.
|
449
|
+
Log.error 'FIT file has no file_id section'
|
450
|
+
return nil
|
455
451
|
end
|
456
452
|
|
457
453
|
if fid.manufacturer == 'garmin' &&
|
@@ -470,10 +466,20 @@ module PostRunner
|
|
470
466
|
}
|
471
467
|
end
|
472
468
|
end
|
473
|
-
Log.
|
469
|
+
Log.error "Fit entity has no device info for 0"
|
470
|
+
return nil
|
474
471
|
else
|
475
472
|
# And for all properly developed devices we can just look at the
|
476
473
|
# file_id section.
|
474
|
+
if fid.manufacturer.nil? ||
|
475
|
+
fid.manufacturer[0..'Undocumented value'.length - 1] ==
|
476
|
+
'Undocumented value'
|
477
|
+
Log.error "Cannot store FIT files for unknown manufacturer " +
|
478
|
+
fid.manufacturer
|
479
|
+
return nil
|
480
|
+
end
|
481
|
+
fid.serial_number ||= 0
|
482
|
+
|
477
483
|
return {
|
478
484
|
:manufacturer => fid.manufacturer,
|
479
485
|
:product => fid.garmin_product || fid.product,
|
data/lib/postrunner/Main.rb
CHANGED
@@ -57,7 +57,9 @@ module PostRunner
|
|
57
57
|
create_directory(@db_dir, 'PostRunner data')
|
58
58
|
ensure_flat_file_db
|
59
59
|
@db = PEROBS::Store.new(File.join(@db_dir, 'database'),
|
60
|
-
{ :engine => PEROBS::FlatFileDB
|
60
|
+
{ :engine => PEROBS::FlatFileDB,
|
61
|
+
:progressmeter =>
|
62
|
+
PEROBS::ConsoleProgressMeter.new })
|
61
63
|
# Create a hash to store configuration data in the store unless it
|
62
64
|
# exists already.
|
63
65
|
cfg = (@db['config'] ||= @db.new(PEROBS::Hash))
|
@@ -252,7 +254,7 @@ weekly [ <YYYY-MM-DD> ]
|
|
252
254
|
week. If no date is given, yesterday's week will be used.
|
253
255
|
|
254
256
|
|
255
|
-
<fit file> An absolute or relative name of a .FIT file.
|
257
|
+
<fit file> An absolute or relative name of a .FIT or .fit file.
|
256
258
|
|
257
259
|
<ref> The index or a range of indexes to activities in the database.
|
258
260
|
:1 is the newest imported activity
|
@@ -437,12 +439,12 @@ EOT
|
|
437
439
|
|
438
440
|
def process_files(files_or_dirs, command)
|
439
441
|
if files_or_dirs.empty?
|
440
|
-
Log.abort("You must provide at least one .FIT file name.")
|
442
|
+
Log.abort("You must provide at least one .FIT or .fit file name.")
|
441
443
|
end
|
442
444
|
|
443
445
|
files_or_dirs.each do |fod|
|
444
446
|
if File.directory?(fod)
|
445
|
-
Dir.glob(File.join(fod, '*.FIT')).each do |file|
|
447
|
+
Dir.glob(File.join(fod, '*.FIT'), File::FNM_CASEFOLD).each do |file|
|
446
448
|
process_file(file, command)
|
447
449
|
end
|
448
450
|
else
|
@@ -472,6 +474,12 @@ EOT
|
|
472
474
|
# @return [TrueClass, FalseClass] true if file was successfully imported,
|
473
475
|
# false otherwise
|
474
476
|
def import_fit_file(fit_file_name)
|
477
|
+
md5sum = FitFileStore.calc_md5_sum(fit_file_name)
|
478
|
+
if @ffs.store['fit_file_md5sums'].include?(md5sum)
|
479
|
+
# The FIT file is already stored in the DB.
|
480
|
+
return nil unless @force
|
481
|
+
end
|
482
|
+
|
475
483
|
begin
|
476
484
|
fit_entity = Fit4Ruby.read(fit_file_name)
|
477
485
|
rescue Fit4Ruby::Error
|
@@ -24,14 +24,14 @@ module PostRunner
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def to_html(doc)
|
27
|
-
return nil if @fit_activity.
|
27
|
+
return nil if @fit_activity.user_data.empty?
|
28
28
|
|
29
29
|
ViewFrame.new('user_profile', 'User Profile', 600, profile,
|
30
30
|
true).to_html(doc)
|
31
31
|
end
|
32
32
|
|
33
33
|
def to_s
|
34
|
-
return '' if @fit_activity.
|
34
|
+
return '' if @fit_activity.user_data.empty?
|
35
35
|
profile.to_s
|
36
36
|
end
|
37
37
|
|
@@ -39,31 +39,44 @@ module PostRunner
|
|
39
39
|
|
40
40
|
def profile
|
41
41
|
t = FlexiTable.new
|
42
|
-
|
43
|
-
|
42
|
+
|
43
|
+
user_data = @fit_activity.user_data.first
|
44
|
+
user_profile = @fit_activity.user_profiles.first
|
45
|
+
hr_zones = @fit_activity.heart_rate_zones.first
|
46
|
+
|
47
|
+
if user_data.height
|
44
48
|
unit = { :metric => 'm', :statute => 'ft' }[@unit_system]
|
45
|
-
height =
|
49
|
+
height = user_data.get_as('height', unit)
|
46
50
|
t.cell('Height:', { :width => '40%' })
|
47
51
|
t.cell("#{'%.2f' % height} #{unit}", { :width => '60%' })
|
48
52
|
t.new_row
|
49
53
|
end
|
50
|
-
if
|
54
|
+
if user_data.weight
|
51
55
|
unit = { :metric => 'kg', :statute => 'lbs' }[@unit_system]
|
52
|
-
weight =
|
56
|
+
weight = user_data.get_as('weight', unit)
|
53
57
|
t.row([ 'Weight:', "#{'%.1f' % weight} #{unit}" ])
|
54
58
|
end
|
55
|
-
t.row([ 'Gender:',
|
56
|
-
t.row([ 'Age:', "#{
|
57
|
-
|
58
|
-
|
59
|
+
t.row([ 'Gender:', user_data.gender ]) if user_data.gender
|
60
|
+
t.row([ 'Age:', "#{user_data.age} years" ]) if user_data.age
|
61
|
+
if (user_profile && (rest_hr = user_profile.resting_heart_rate)) ||
|
62
|
+
(hr_zones && (rest_hr = hr_zones.resting_heart_rate))
|
63
|
+
t.row([ 'Resting Heart Rate:', "#{rest_hr} bpm" ])
|
64
|
+
end
|
65
|
+
if (max_hr = user_data.max_hr) ||
|
66
|
+
(max_hr = hr_zones.max_heart_rate)
|
67
|
+
t.row([ 'Max. Heart Rate:', "#{max_hr} bpm" ])
|
68
|
+
end
|
69
|
+
if (lthr = user_data.running_lactate_threshold_heart_rate)
|
59
70
|
t.row([ 'Running LTHR:', "#{lthr} bpm" ])
|
60
71
|
end
|
61
|
-
if
|
62
|
-
t.row([ 'Activity Class:',
|
72
|
+
if (activity_class = user_data.activity_class)
|
73
|
+
t.row([ 'Activity Class:', activity_class ])
|
74
|
+
end
|
75
|
+
if (metmax = user_data.metmax)
|
76
|
+
t.row([ 'METmax:', "#{metmax} MET" ])
|
63
77
|
end
|
64
|
-
if
|
65
|
-
t.row([ '
|
66
|
-
t.row([ 'VO2max:', "#{'%.1f' % (profile.metmax * 3.5)} ml/kg/min" ])
|
78
|
+
if (vo2max = @fit_activity.vo2max)
|
79
|
+
t.row([ 'VO2max:', "#{'%.1f' % vo2max} ml/kg/min" ])
|
67
80
|
end
|
68
81
|
t
|
69
82
|
end
|
data/lib/postrunner/version.rb
CHANGED
data/postrunner.gemspec
CHANGED
@@ -28,8 +28,8 @@ operating systems as well.}
|
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
spec.required_ruby_version = '>=2.0'
|
30
30
|
|
31
|
-
spec.add_dependency 'fit4ruby', '~> 2.0
|
32
|
-
spec.add_dependency 'perobs', '~> 4.
|
31
|
+
spec.add_dependency 'fit4ruby', '~> 3.2.0'
|
32
|
+
spec.add_dependency 'perobs', '~> 4.1.0'
|
33
33
|
spec.add_dependency 'nokogiri', '~> 1.6'
|
34
34
|
|
35
35
|
spec.add_development_dependency 'bundler', '~> 1.6'
|
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.11.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: 2019-06-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fit4ruby
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 2.0
|
19
|
+
version: 3.2.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 2.0
|
26
|
+
version: 3.2.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: perobs
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 4.
|
33
|
+
version: 4.1.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 4.
|
40
|
+
version: 4.1.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: nokogiri
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|