postrunner 0.7.5 → 0.8.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: 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: