postrunner 0.7.5 → 0.8.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: d3e72cd4d14f546d36e537a7e9da9b82be913717
4
- data.tar.gz: 5676aa62ed9ee8ac9123e3b0b3f9a086b93d7cba
3
+ metadata.gz: e282bd17e22dd3984588ac737f95e0bb819ee674
4
+ data.tar.gz: bf6a551a65a477b2a68526c549488098e9374a00
5
5
  SHA512:
6
- metadata.gz: 1e8eb20c4e281dd11e28e56678134ed589b0271857d9175cc2bd73b8d60783338bdb55a9eac5e45b6cbe6ef5525516508db2d7c33b845f41a5925b49ebd3bb77
7
- data.tar.gz: c2c91df7c337f283080feef69651fc234935dbafaf6a8869e138d7689bee8560188455f20340cd0c8cb983d10af3fa6b05178f464928261c7f26bcd62bc46977
6
+ metadata.gz: e35e8f9d8604cdf26d76d5686e70739f783e0fc385fa9b686e8abe7d5dcea84e4a0e93f06a8b1bd697d5add11539cbc0d9c8a20ac5b6ab74493c7c7eaa01853f
7
+ data.tar.gz: 8eb18c7a1af1c8d56889985d4bfa203ca4b2dd39fa7941220ee835613b06a2bdb986bab58fdd95c4b23fc2ee5e7c275e0b4c5d6461ad0fcb68b83815a6892b59
@@ -16,11 +16,16 @@ require 'postrunner/FlexiTable'
16
16
  require 'postrunner/ViewFrame'
17
17
  require 'postrunner/HRV_Analyzer'
18
18
  require 'postrunner/Percentiles'
19
+ require 'postrunner/HRZoneDetector'
19
20
 
20
21
  module PostRunner
21
22
 
22
23
  class ActivitySummary
23
24
 
25
+ class HRZone < Struct.new(:index, :low, :high, :time_in_zone,
26
+ :percent_in_zone)
27
+ end
28
+
24
29
  include Fit4Ruby::Converters
25
30
 
26
31
  def initialize(activity, unit_system, custom_fields)
@@ -33,9 +38,12 @@ module PostRunner
33
38
  end
34
39
 
35
40
  def to_s
36
- summary.to_s + "\n" +
37
- (@activity.note ? note.to_s + "\n" : '') +
38
- laps.to_s
41
+ s = summary.to_s + "\n" +
42
+ (@activity.note ? note.to_s + "\n" : '') +
43
+ laps.to_s
44
+ s += hr_zones.to_s if has_hr_zones?
45
+
46
+ s
39
47
  end
40
48
 
41
49
  def to_html(doc)
@@ -45,6 +53,10 @@ module PostRunner
45
53
  ViewFrame.new('note', 'Note', width, note,
46
54
  true).to_html(doc) if @activity.note
47
55
  ViewFrame.new('laps', 'Laps', width, laps, true).to_html(doc)
56
+ if has_hr_zones?
57
+ ViewFrame.new('hr_zones', 'Heart Rate Zones', width, hr_zones, true).
58
+ to_html(doc)
59
+ end
48
60
  end
49
61
 
50
62
  private
@@ -119,8 +131,19 @@ module PostRunner
119
131
  "#{(2 * session.avg_cadence).round} rpm" : '-' ])
120
132
  end
121
133
 
122
- t.row([ 'Training Effect:', session.total_training_effect ?
123
- session.total_training_effect : '-' ])
134
+ if @fit_activity.physiological_metrics &&
135
+ (physiological_metrics = @fit_activity.physiological_metrics.last)
136
+ if physiological_metrics.anaerobic_training_effect
137
+ t.row([ 'Anaerobic Training Effect:',
138
+ physiological_metrics.anaerobic_training_effect ])
139
+ end
140
+ if physiological_metrics.aerobic_training_effect
141
+ t.row([ 'Aerobic Training Effect:',
142
+ physiological_metrics.aerobic_training_effect ])
143
+ end
144
+ elsif session.total_training_effect
145
+ t.row([ 'Aerobic Training Effect:', session.total_training_effect ])
146
+ end
124
147
 
125
148
  rec_info = @fit_activity.recovery_info
126
149
  t.row([ 'Ignored Recovery Time:',
@@ -178,6 +201,140 @@ module PostRunner
178
201
  t
179
202
  end
180
203
 
204
+ def hr_zones
205
+ session = @fit_activity.sessions[0]
206
+
207
+ t = FlexiTable.new
208
+ t.head
209
+ t.row([ 'Zone', 'Exertion', 'Min. HR [bpm]', 'Max. HR [bpm]',
210
+ 'Time in Zone', '% of Time in Zone' ])
211
+ t.set_column_attributes([
212
+ { :halign => :right },
213
+ { :halign => :left},
214
+ { :halign => :right },
215
+ { :halign => :right },
216
+ { :halign => :right },
217
+ { :halign => :right },
218
+ ])
219
+ t.body
220
+
221
+ # Calculate the total time in all the 5 relevant zones. We'll need this
222
+ # later as the basis for the percentage values.
223
+ total_secs = 0
224
+ zones = gather_hr_zones
225
+
226
+ zones.each do |zone|
227
+ t.cell(zone.index + 1)
228
+ t.cell([ 'Warm Up', 'Easy', 'Aerobic', 'Threshold', 'Maximum' ][zone.index])
229
+ t.cell(zone.low)
230
+ t.cell(zone.high)
231
+ t.cell(secsToHMS(zone.time_in_zone))
232
+ t.cell('%.0f%%' % zone.percent_in_zone)
233
+
234
+ t.new_row
235
+ end
236
+
237
+ t
238
+ end
239
+
240
+ def has_hr_zones?
241
+ # Depending on the age of the device we may have heart rate zone data
242
+ # with zone boundaries, without zone boundaries or no data at all.
243
+ if @fit_activity.heart_rate_zones.empty?
244
+ # The FIT file has no heart_rate_zone records. It might have a
245
+ # time_in_hr_zone record for the session.
246
+ counted_zones = 0
247
+ total_time_in_zone = 0
248
+ each_hr_zone_with_index do |secs_in_zone, i|
249
+ if secs_in_zone
250
+ counted_zones += 1
251
+ total_time_in_zone += secs_in_zone
252
+ end
253
+ end
254
+
255
+ return counted_zones == 5 && total_time_in_zone > 0.0
256
+ else
257
+ # The FIT file has explicit heart_rate_zones records. We need the
258
+ # session record that has type 19.
259
+ @fit_activity.heart_rate_zones.each do |hrz|
260
+ if hrz.type == 18 && hrz.heart_rate_zones &&
261
+ !hrz.heart_rate_zones.empty?
262
+ return true
263
+ end
264
+ end
265
+ end
266
+ end
267
+
268
+ def gather_hr_zones
269
+ zones = []
270
+
271
+ if @fit_activity.heart_rate_zones.empty?
272
+ # The FIT file has no heart_rate_zone records. It might have a
273
+ # time_in_hr_zone record for the session.
274
+ counted_zones = 0
275
+ total_time_in_zone = 0
276
+ each_hr_zone_with_index do |secs_in_zone, i|
277
+ if secs_in_zone
278
+ counted_zones += 1
279
+ total_time_in_zone += secs_in_zone
280
+ end
281
+ end
282
+
283
+ if counted_zones == 5 && total_time_in_zone > 0.0
284
+ session = @fit_activity.sessions[0]
285
+ hr_mins = HRZoneDetector::detect_zones(
286
+ @fit_activity.records, session.time_in_hr_zone[0..5])
287
+ 0.upto(4) do |i|
288
+ low = hr_mins[i + 1]
289
+ high = i == HRZoneDetector::GARMIN_ZONES - 1 ?
290
+ session.max_heart_rate || '-' :
291
+ hr_mins[i + 2].nil? || hr_mins[i + 2] == 0 ? '-' :
292
+ (hr_mins[i + 2] - 1)
293
+ tiz = @fit_activity.sessions[0].time_in_hr_zone[i + 1]
294
+ piz = tiz / total_time_in_zone * 100.0
295
+ zones << HRZone.new(i, low, high, tiz, piz)
296
+ end
297
+ end
298
+ else
299
+ @fit_activity.heart_rate_zones.each do |zone|
300
+ if zone.type == 18
301
+ total_time = 0.0
302
+ if zone.time_in_hr_zone
303
+ zone.time_in_hr_zone.each { |tiz| total_time += tiz }
304
+ end
305
+ break if total_time <= 0.0
306
+ if zone.heart_rate_zones
307
+ zone.heart_rate_zones.each_with_index do |hr, i|
308
+ break if i > 4
309
+ zones << HRZone.new(i, hr, zone.heart_rate_zones[i + 1],
310
+ zone.time_in_hr_zone[i + 1],
311
+ zone.time_in_hr_zone[i + 1] /
312
+ total_time * 100.0)
313
+ end
314
+ end
315
+ break
316
+ end
317
+ end
318
+ end
319
+
320
+ zones
321
+ end
322
+
323
+ def each_hr_zone_with_index
324
+ return unless (zones = @fit_activity.sessions[0].time_in_hr_zone)
325
+
326
+ zones.each_with_index do |secs_in_zone, i|
327
+ # There seems to be a zone 0 in the FIT files that isn't displayed on
328
+ # the watch or Garmin Connect. Just ignore it.
329
+ next if i == 0
330
+ # There are more zones in the FIT file, but they are not displayed on
331
+ # the watch or on the GC.
332
+ break if i >= 6
333
+
334
+ yield(secs_in_zone, i)
335
+ end
336
+ end
337
+
181
338
  def local_value(fdr, field, format, units)
182
339
  unit = units[@unit_system]
183
340
  value = fdr.get_as(field, unit)
@@ -75,6 +75,13 @@ module PostRunner
75
75
  :colors => '#900000',
76
76
  :show => false
77
77
  },
78
+ {
79
+ :id => 'performance_condition',
80
+ :label => 'Performance Condition',
81
+ :graph => :line_graph,
82
+ :colors => '#7CB7E7',
83
+ :show => true
84
+ },
78
85
  {
79
86
  :id => 'run_cadence',
80
87
  :label => 'Run Cadence',
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = HRZoneDetector.rb -- PostRunner - Manage the data from your Garmin sport devices.
5
+ #
6
+ # Copyright (c) 2017 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
+ module PostRunner
14
+
15
+ module HRZoneDetector
16
+
17
+ # Number of heart rate zones supported by Garmin devices.
18
+ GARMIN_ZONES = 5
19
+ # Maximum heart rate that can be stored in FIT files.
20
+ MAX_HR = 255
21
+
22
+ def HRZoneDetector::detect_zones(fit_records, secs_in_zones)
23
+ if fit_records.empty?
24
+ raise RuntimeError, "records must not be empty"
25
+ end
26
+ if secs_in_zones.size != GARMIN_ZONES + 1
27
+ raise RuntimeError, "secs_in_zones must have #{GARMIN_ZONES + 1} " +
28
+ "elements"
29
+ end
30
+
31
+ # We generate a histogram of the time spent at each integer heart rate.
32
+ histogram = Array.new(MAX_HR + 1, 0)
33
+
34
+ last_timestamp = nil
35
+ fit_records.each do |record|
36
+ next unless record.heart_rate
37
+
38
+ if last_timestamp
39
+ # We ignore all intervals that are larger than 10 seconds. This
40
+ # potentially conflicts with smart recording, but I can't see how a
41
+ # larger sampling interval can yield usable results.
42
+ if (delta_t = record.timestamp - last_timestamp) <= 10
43
+ histogram[record.heart_rate] += delta_t
44
+ end
45
+ end
46
+ last_timestamp = record.timestamp
47
+ end
48
+
49
+ # We'll process zones 5 downto 1.
50
+ zone = GARMIN_ZONES
51
+ hr_mins = Array.new(GARMIN_ZONES)
52
+ # Sum of time spent in current zone.
53
+ secs_in_current_zone = 0
54
+ # We process the histogramm from highest to smallest HR value. Whenever
55
+ # we have accumulated the provided amount of time we have found a HR
56
+ # zone boundary. We complete the current zone and continue with the next
57
+ # one.
58
+ MAX_HR.downto(0) do |i|
59
+ secs_in_current_zone += histogram[i]
60
+
61
+ if secs_in_current_zone > secs_in_zones[zone]
62
+ # In case we have collected more time than was specified for the
63
+ # zone we carry the delta over to the next zone.
64
+ secs_in_current_zone -= secs_in_zones[zone]
65
+ # puts "Zone #{zone}: #{secs_in_current_zone} #{secs_in_zones[zone]}"
66
+ break if (zone -= 1) < 0
67
+ end
68
+ hr_mins[zone] = i
69
+ end
70
+
71
+ hr_mins
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
@@ -92,6 +92,8 @@ module PostRunner
92
92
  "#{VERSION}!")
93
93
  end
94
94
  return -1
95
+ ensure
96
+ @db.exit if @db
95
97
  end
96
98
  end
97
99
 
@@ -103,7 +105,7 @@ module PostRunner
103
105
 
104
106
  opts.separator <<"EOT"
105
107
 
106
- Copyright (c) 2014, 2015, 2016 by Chris Schlaeger
108
+ Copyright (c) 2014, 2015, 2016, 2017 by Chris Schlaeger
107
109
 
108
110
  This program is free software; you can redistribute it and/or modify it under
109
111
  the terms of version 2 of the GNU General Public License as published by the
@@ -346,6 +346,9 @@ module PostRunner
346
346
  # meters)
347
347
  speed_records = {}
348
348
 
349
+ # Ignore FIT files that don't have an activity or session
350
+ return unless activity.fit_activity && activity.fit_activity.sessions
351
+
349
352
  segment_start_time = activity.fit_activity.sessions[0].start_time
350
353
  segment_start_distance = 0.0
351
354
 
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = version.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, 2017 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
@@ -11,5 +11,5 @@
11
11
  #
12
12
 
13
13
  module PostRunner
14
- VERSION = '0.7.5'
14
+ VERSION = '0.8.0'
15
15
  end
@@ -10,14 +10,15 @@ GEM_SPEC = Gem::Specification.new do |spec|
10
10
  spec.email = ["cs@taskjuggler.org"]
11
11
  spec.summary = %q{Application to manage and analyze Garmin FIT files.}
12
12
  spec.description = %q{PostRunner is an application to manage FIT files
13
- such as those produced by Garmin products like the Forerunner 620 (FR620) and
14
- Fenix 3 or Fenix 3HR. It allows you to import the files from the device and
15
- analyze the data. In addition to the common features like plotting pace, heart
16
- rates, elevation and other captured values it also provides a heart rate
17
- variability (HRV) analysis. It can also update satellite orbit prediction
18
- (EPO) data on the device to speed-up GPS fix times. It is an offline alternative
19
- to Garmin Connect. The software has been developed and tested on Linux but
20
- should work on other operating systems as well.}
13
+ such as those produced by Garmin products like the Forerunner 620 (FR620),
14
+ Fenix 3, Fenix 3HR, Fenix 5 (S and X). It allows you to import the files from
15
+ the device and analyze the data. In addition to the common features like
16
+ plotting pace, heart rates, elevation and other captured values it also
17
+ provides a heart rate variability (HRV) and sleep analysis. It can also update
18
+ satellite orbit prediction (EPO) data on the device to speed-up GPS fix times.
19
+ It is an offline alternative to Garmin Connect. The software has been
20
+ developed and tested on Linux but should work on other operating systems as
21
+ well.}
21
22
  spec.homepage = 'https://github.com/scrapper/postrunner'
22
23
  spec.license = "GNU GPL version 2"
23
24
 
@@ -27,8 +28,8 @@ should work on other operating systems as well.}
27
28
  spec.require_paths = ["lib"]
28
29
  spec.required_ruby_version = '>=2.0'
29
30
 
30
- spec.add_dependency 'fit4ruby', '~> 1.5.1'
31
- spec.add_dependency 'perobs', '~> 2.4.1'
31
+ spec.add_dependency 'fit4ruby', '~> 1.6.0'
32
+ spec.add_dependency 'perobs', '~> 3.0.1'
32
33
  spec.add_dependency 'nokogiri', '~> 1.6'
33
34
 
34
35
  spec.add_development_dependency 'bundler', '~> 1.6'
@@ -31,7 +31,9 @@ describe PostRunner::Main do
31
31
  $stderr = old_stderr
32
32
  end
33
33
 
34
- { :retval => retval, :stdout => stdout.string, :stderr => stderr.string }
34
+ stdout.rewind
35
+ stderr.rewind
36
+ { :retval => retval, :stdout => stdout.read, :stderr => stderr.read}
35
37
  end
36
38
 
37
39
  before(:all) do
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.7.5
4
+ version: 0.8.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: 2017-02-26 00:00:00.000000000 Z
11
+ date: 2017-08-13 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: 1.5.1
19
+ version: 1.6.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: 1.5.1
26
+ version: 1.6.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: 2.4.1
33
+ version: 3.0.1
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: 2.4.1
40
+ version: 3.0.1
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: nokogiri
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -110,14 +110,15 @@ dependencies:
110
110
  version: 0.8.7
111
111
  description: |-
112
112
  PostRunner is an application to manage FIT files
113
- such as those produced by Garmin products like the Forerunner 620 (FR620) and
114
- Fenix 3 or Fenix 3HR. It allows you to import the files from the device and
115
- analyze the data. In addition to the common features like plotting pace, heart
116
- rates, elevation and other captured values it also provides a heart rate
117
- variability (HRV) analysis. It can also update satellite orbit prediction
118
- (EPO) data on the device to speed-up GPS fix times. It is an offline alternative
119
- to Garmin Connect. The software has been developed and tested on Linux but
120
- should work on other operating systems as well.
113
+ such as those produced by Garmin products like the Forerunner 620 (FR620),
114
+ Fenix 3, Fenix 3HR, Fenix 5 (S and X). It allows you to import the files from
115
+ the device and analyze the data. In addition to the common features like
116
+ plotting pace, heart rates, elevation and other captured values it also
117
+ provides a heart rate variability (HRV) and sleep analysis. It can also update
118
+ satellite orbit prediction (EPO) data on the device to speed-up GPS fix times.
119
+ It is an offline alternative to Garmin Connect. The software has been
120
+ developed and tested on Linux but should work on other operating systems as
121
+ well.
121
122
  email:
122
123
  - cs@taskjuggler.org
123
124
  executables:
@@ -154,6 +155,7 @@ files:
154
155
  - lib/postrunner/FitFileStore.rb
155
156
  - lib/postrunner/FlexiTable.rb
156
157
  - lib/postrunner/HRV_Analyzer.rb
158
+ - lib/postrunner/HRZoneDetector.rb
157
159
  - lib/postrunner/HTMLBuilder.rb
158
160
  - lib/postrunner/LinearPredictor.rb
159
161
  - lib/postrunner/Log.rb
@@ -339,7 +341,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
339
341
  version: '0'
340
342
  requirements: []
341
343
  rubyforge_project:
342
- rubygems_version: 2.2.2
344
+ rubygems_version: 2.2.5
343
345
  signing_key:
344
346
  specification_version: 4
345
347
  summary: Application to manage and analyze Garmin FIT files.
@@ -351,4 +353,3 @@ test_files:
351
353
  - spec/PostRunner_spec.rb
352
354
  - spec/View_spec.rb
353
355
  - spec/spec_helper.rb
354
- has_rdoc: