fit4ruby 3.3.0 → 3.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
- SHA1:
3
- metadata.gz: c76d1a2e0091c4aa84e7e26a381630def4b5e5b0
4
- data.tar.gz: 21814b487808a93810dfedf2fe4908d7f9ecf246
2
+ SHA256:
3
+ metadata.gz: 29f7e7354dfe1a0b9cfa30849b6131693deba24858f7cf6ec0745b923bcbd86a
4
+ data.tar.gz: 24aa3e37347db57f6e26078118bc20da45692097c395a21ad480702ebb66f0c6
5
5
  SHA512:
6
- metadata.gz: fbb6bf23b5adadb57b114444de5d067b13273bb73537e76b6a8b6436e61a5fea69af8f619625369ac5c6c9fd13018a31514e3e1662d8437f33ba71b8a52b949e
7
- data.tar.gz: 5404ca45effe3b389041a036711c55006c95f304f4afa91a888c2dfda4f38ad2fc55faf450e23791cfd571c6b0c9373865161b881e87e53e268af36e93f130a3
6
+ metadata.gz: 2638207885da4f16185df8ef71d7999855b2f9199be60d1156727473a855a01039ce0fd36faaa7093696f4576247f78c208c2628df200da2d5761832ebc46142
7
+ data.tar.gz: a7fd64132dba6d6594e9ade153e1596d66ef7f47c7846f79ad7971240ac8b6dfb0b457474e80dcd369faadc88dc79fee17233a3c3cc4cfe2d40f40f01df28e47
data/Gemfile.lock CHANGED
@@ -1,15 +1,34 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fit4ruby (0.0.1)
5
- bindata (>= 2.0.0)
4
+ fit4ruby (3.3.0)
5
+ bindata (= 2.3.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- bindata (2.1.0)
10
+ bindata (2.3.0)
11
+ coderay (1.1.2)
12
+ diff-lcs (1.3)
13
+ method_source (0.9.2)
14
+ pry (0.12.2)
15
+ coderay (~> 1.1.0)
16
+ method_source (~> 0.9.0)
11
17
  rake (0.9.6)
12
- yard (0.8.7.4)
18
+ rspec (3.8.0)
19
+ rspec-core (~> 3.8.0)
20
+ rspec-expectations (~> 3.8.0)
21
+ rspec-mocks (~> 3.8.0)
22
+ rspec-core (3.8.2)
23
+ rspec-support (~> 3.8.0)
24
+ rspec-expectations (3.8.4)
25
+ diff-lcs (>= 1.2.0, < 2.0)
26
+ rspec-support (~> 3.8.0)
27
+ rspec-mocks (3.8.1)
28
+ diff-lcs (>= 1.2.0, < 2.0)
29
+ rspec-support (~> 3.8.0)
30
+ rspec-support (3.8.2)
31
+ yard (0.9.20)
13
32
 
14
33
  PLATFORMS
15
34
  ruby
@@ -17,5 +36,10 @@ PLATFORMS
17
36
  DEPENDENCIES
18
37
  bundler (>= 1.6.4)
19
38
  fit4ruby!
20
- rake
21
- yard
39
+ pry (>= 0.12)
40
+ rake (~> 12.0.0)
41
+ rspec (>= 3.8)
42
+ yard (~> 0.9.20)
43
+
44
+ BUNDLED WITH
45
+ 2.0.2
data/fit4ruby.gemspec CHANGED
@@ -24,8 +24,10 @@ EOT
24
24
  spec.require_paths = ["lib"]
25
25
  spec.required_ruby_version = '>=2.0'
26
26
 
27
- spec.add_dependency('bindata', '=2.3.0')
27
+ spec.add_dependency('bindata', '~>2.4.8')
28
28
  spec.add_development_dependency('yard', '~>0.9.20')
29
- spec.add_development_dependency('rake', '~>0.9.6')
29
+ spec.add_development_dependency('rake', '~>12.0.0')
30
30
  spec.add_development_dependency('bundler', '>=1.6.4')
31
+ spec.add_development_dependency('rspec', '>=3.8')
32
+ spec.add_development_dependency('pry', '>=0.12')
31
33
  end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = Activity.rb -- Fit4Ruby - FIT file processing library for Ruby
5
5
  #
6
- # Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
6
+ # Copyright (c) 2014, 2015, 2020 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
@@ -24,6 +24,7 @@ require 'fit4ruby/UserProfile'
24
24
  require 'fit4ruby/PhysiologicalMetrics'
25
25
  require 'fit4ruby/Session'
26
26
  require 'fit4ruby/Lap'
27
+ require 'fit4ruby/Length'
27
28
  require 'fit4ruby/Record'
28
29
  require 'fit4ruby/HRV'
29
30
  require 'fit4ruby/HeartRateZones'
@@ -32,48 +33,74 @@ require 'fit4ruby/PersonalRecords'
32
33
 
33
34
  module Fit4Ruby
34
35
 
35
- # This is the most important class of this library. It holds references to
36
- # all other data structures. Each of the objects it references are direct
37
- # equivalents of the message record structures used in the FIT file.
36
+ # Activity files are arguably the most common type of FIT file. The Activity
37
+ # class represents the top-level structure of an activity FIT file.
38
+ # It holds references to all other data structures. Each of the objects it
39
+ # references are direct equivalents of the message record structures used in
40
+ # the FIT file.
38
41
  class Activity < FitDataRecord
39
42
 
40
- attr_accessor :file_id, :field_descriptions, :developer_data_ids, :epo_data,
41
- :file_creator, :device_infos, :sensor_settings, :data_sources,
42
- :user_data, :user_profiles, :physiological_metrics,
43
- :sessions, :laps, :records, :hrv,
44
- :heart_rate_zones, :events, :personal_records
43
+ # These symbols are a complete list of all the sub-sections that an
44
+ # activity FIT file may contain. This list is used to generate accessors,
45
+ # instance variables and other sections of code. Some are just simple
46
+ # instance variables, but the majority can appear multiple times and hence
47
+ # are stored in an Array.
48
+ FILE_SECTIONS = [
49
+ :file_id,
50
+ :file_creator,
51
+ :events,
52
+ :device_infos,
53
+ :data_sources,
54
+ :epo_data,
55
+ :user_profiles,
56
+ :user_data,
57
+ :sensor_settings,
58
+ :developer_data_ids,
59
+ :field_descriptions,
60
+ :records,
61
+ :hrv,
62
+ :laps,
63
+ :lengths,
64
+ :heart_rate_zones,
65
+ :physiological_metrics,
66
+ :sessions,
67
+ :personal_records
68
+ ]
69
+
70
+ attr_accessor *FILE_SECTIONS
45
71
 
46
72
  # Create a new Activity object.
47
73
  # @param field_values [Hash] A Hash that provides initial values for
48
74
  # certain fields of the FitDataRecord.
49
75
  def initialize(field_values = {})
50
76
  super('activity')
51
- @meta_field_units['total_gps_distance'] = 'm'
52
- @num_sessions = 0
53
77
 
54
- @file_id = FileId.new
55
- @field_descriptions = []
56
- @developer_data_ids = []
78
+ # The variables hold references to other parts of the FIT file. These
79
+ # can either be direct references to a certain FIT file section or an
80
+ # Array in case the section can appear multiple times in the FIT file.
81
+ @file_id = new_file_id()
82
+ @file_creator = new_file_creator()
57
83
  @epo_data = nil
58
- @file_creator = FileCreator.new
59
- @device_infos = []
60
- @sensor_settings = []
61
- @data_sources = []
62
- @user_data = []
63
- @user_profiles = []
64
- @physiological_metrics = []
65
- @events = []
66
- @sessions = []
67
- @laps = []
68
- @records = []
69
- @hrv = []
70
- @heart_rate_zones = []
71
- @personal_records = []
84
+ # Initialize the remaining variables as empty Array.
85
+ FILE_SECTIONS.each do |fs|
86
+ ivar_name = '@' + fs.to_s
87
+ unless instance_variable_defined?(ivar_name)
88
+ instance_variable_set(ivar_name, [])
89
+ end
90
+ end
72
91
 
92
+ # The following variables hold derived or auxilliary information that
93
+ # are not directly part of the FIT file.
94
+ @meta_field_units['total_gps_distance'] = 'm'
73
95
  @cur_session_laps = []
96
+
74
97
  @cur_lap_records = []
98
+ @cur_lap_lengths = []
99
+
100
+ @cur_length_records = []
75
101
 
76
102
  @lap_counter = 1
103
+ @length_counter = 1
77
104
 
78
105
  set_field_values(field_values)
79
106
  end
@@ -92,11 +119,6 @@ module Fit4Ruby
92
119
  end
93
120
  @device_infos.each.with_index { |d, index| d.check(index) }
94
121
  @sensor_settings.each.with_index { |s, index| s.check(index) }
95
- unless @num_sessions == @sessions.count
96
- Log.fatal "Activity record requires #{@num_sessions}, but "
97
- "#{@sessions.length} session records were found in the "
98
- "FIT file."
99
- end
100
122
 
101
123
  # Records must have consecutively growing timestamps and distances.
102
124
  ts = Time.parse('1989-12-31')
@@ -141,11 +163,20 @@ module Fit4Ruby
141
163
 
142
164
  # Laps must have a consecutively growing message index.
143
165
  @laps.each.with_index do |lap, index|
144
- lap.check(index)
166
+ lap.check(index, self)
145
167
  # If we have heart rate zone records, there should be one for each
146
168
  # lap
147
169
  @heart_rate_zones[index].check(index) if @heart_rate_zones[index]
148
170
  end
171
+
172
+ # Lengths must have a consecutively growing message index.
173
+ @lengths.each.with_index do |length, index|
174
+ length.check(index)
175
+ # If we have heart rate zone records, there should be one for each
176
+ # length
177
+ @heart_rate_zones[index].check(index) if @heart_rate_zones[index]
178
+ end
179
+
149
180
  @sessions.each { |s| s.check(self) }
150
181
  end
151
182
 
@@ -206,17 +237,22 @@ module Fit4Ruby
206
237
  d
207
238
  end
208
239
 
209
- # Call this method to update the aggregated data fields stored in Lap and
210
- # Session objects.
240
+ # Call this method to update the aggregated data fields stored in Lap,
241
+ # Length, and Session objects.
211
242
  def aggregate
212
243
  @laps.each { |l| l.aggregate }
244
+ @lengths.each { |l| l.aggregate }
213
245
  @sessions.each { |s| s.aggregate }
214
246
  end
215
247
 
216
248
  # Convenience method that averages the speed over all sessions.
217
249
  def avg_speed
218
250
  speed = 0.0
219
- @sessions.each { |s| speed += s.avg_speed if s.avg_speed }
251
+ @sessions.each do |s|
252
+ if (spd = s.avg_speed || s.enhanced_avg_speed)
253
+ speed += spd
254
+ end
255
+ end
220
256
  speed / @sessions.length
221
257
  end
222
258
 
@@ -287,13 +323,17 @@ module Fit4Ruby
287
323
  def write(io, id_mapper)
288
324
  @file_id.write(io, id_mapper)
289
325
  @file_creator.write(io, id_mapper)
326
+ @epo_data.write(io, id_mapper) if @epo_data
290
327
 
291
- (@field_descriptions + @developer_data_ids +
292
- @device_infos + @sensor_settings +
293
- @data_sources + @user_profiles +
294
- @physiological_metrics + @events +
295
- @sessions + @laps + @records + @heart_rate_zones +
296
- @personal_records).sort.each do |s|
328
+ ary_ivars = []
329
+ FILE_SECTIONS.each do |fs|
330
+ ivar_name = '@' + fs.to_s
331
+ if (ivar = instance_variable_get(ivar_name)) && ivar.respond_to?(:sort)
332
+ ary_ivars += ivar
333
+ end
334
+ end
335
+
336
+ ary_ivars.sort.each do |s|
297
337
  s.write(io, id_mapper)
298
338
  end
299
339
  super
@@ -404,6 +444,16 @@ module Fit4Ruby
404
444
  new_fit_data_record('lap', field_values)
405
445
  end
406
446
 
447
+ # Add a new Length to the Activity. All previoulsy added Record objects are
448
+ # associated with this Length unless they have been associated with another
449
+ # Length before.
450
+ # @param field_values [Hash] A Hash that provides initial values for
451
+ # certain fields of the FitDataRecord.
452
+ # @return [Length]
453
+ def new_length(field_values = {})
454
+ new_fit_data_record('length', field_values)
455
+ end
456
+
407
457
  # Add a new HeartRateZones record to the Activity.
408
458
  # @param field_values [Heash] A Hash that provides initial values for
409
459
  # certain fields of the FitDataRecord.
@@ -433,18 +483,17 @@ module Fit4Ruby
433
483
  # @return [TrueClass/FalseClass] true if both Activities are equal,
434
484
  # otherwise false.
435
485
  def ==(a)
436
- super(a) && @file_id == a.file_id &&
437
- @file_creator == a.file_creator &&
438
- @field_descriptions == a.field_descriptions &&
439
- @developer_data_ids == a.developer_data_ids &&
440
- @device_infos == a.device_infos &&
441
- @sensor_settings == a.sensor_settings &&
442
- @data_sources == a.data_sources &&
443
- @physiological_metrics == a.physiological_metrics &&
444
- @user_profiles == a.user_profiles &&
445
- @heart_rate_zones == a.heart_rate_zones &&
446
- @events == a.events &&
447
- @sessions == a.sessions && personal_records == a.personal_records
486
+ return false unless super(a)
487
+
488
+ FILE_SECTIONS.each do |fs|
489
+ ivar_name = '@' + fs.to_s
490
+ ivar = instance_variable_get(ivar_name)
491
+ a_ivar = a.instance_variable_get(ivar_name)
492
+
493
+ return false unless ivar == a_ivar
494
+ end
495
+
496
+ true
448
497
  end
449
498
 
450
499
  # Create a new FitDataRecord.
@@ -490,14 +539,17 @@ module Fit4Ruby
490
539
  # Ensure that all previous records have been assigned to a lap.
491
540
  record = create_new_lap(lap_field_values)
492
541
  end
493
- @num_sessions += 1
494
542
  @sessions << (record = Session.new(@cur_session_laps, @lap_counter,
495
543
  field_values))
496
544
  @cur_session_laps = []
497
545
  when 'lap'
498
546
  record = create_new_lap(field_values)
547
+ when 'length'
548
+ record = create_new_length(field_values)
499
549
  when 'record'
500
- @cur_lap_records << (record = Record.new(field_values))
550
+ record = Record.new(self, field_values)
551
+ @cur_lap_records << record
552
+ @cur_length_records << record
501
553
  @records << record
502
554
  when 'hrv'
503
555
  @hrv << (record = HRV.new(field_values))
@@ -512,19 +564,54 @@ module Fit4Ruby
512
564
  record
513
565
  end
514
566
 
567
+ def export
568
+ # Collect all records in a consistent order.
569
+ records = []
570
+ FILE_SECTIONS.each do |fs|
571
+ ivar_name = '@' + fs.to_s
572
+ ivar = instance_variable_get(ivar_name)
573
+
574
+ next unless ivar
575
+
576
+ if ivar.respond_to?(:sort) and ivar.respond_to?(:empty?)
577
+ records += ivar.sort unless ivar.empty?
578
+ else
579
+ records << ivar if ivar
580
+ end
581
+ end
582
+
583
+ records.map do |record|
584
+ record.export
585
+ end
586
+ end
587
+
515
588
  private
516
589
 
517
590
  def create_new_lap(field_values)
518
- lap = Lap.new(@cur_lap_records, @laps.last, field_values)
591
+ lap = Lap.new(self, @cur_lap_records, @laps.last,
592
+ field_values,
593
+ @length_counter, @cur_lap_lengths)
519
594
  lap.message_index = @lap_counter - 1
520
595
  @lap_counter += 1
521
596
  @cur_session_laps << lap
522
597
  @laps << lap
523
598
  @cur_lap_records = []
599
+ @cur_lap_lengths = []
524
600
 
525
601
  lap
526
602
  end
527
603
 
604
+ def create_new_length(field_values)
605
+ length = Length.new(@cur_length_records, @lengths.last, field_values)
606
+ length.message_index = @length_counter - 1
607
+ @length_counter += 1
608
+ @cur_lap_lengths << length
609
+ @lengths << length
610
+ @cur_length_records = []
611
+
612
+ length
613
+ end
614
+
528
615
  end
529
616
 
530
617
  end
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = FitDataRecord.rb -- Fit4Ruby - FIT file processing library for Ruby
5
+ #
6
+ # Copyright (c) 2020 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 Fit4Ruby
14
+
15
+ # Some FIT message field names conflict with BinData reserved names. We use
16
+ # this translation method to map the conflicting names to BinData compatible
17
+ # names.
18
+ module BDFieldNameTranslator
19
+
20
+ BD_DICT = {
21
+ 'array' => '_array',
22
+ 'type' => '_type'
23
+ }
24
+
25
+ def to_bd_field_name(name)
26
+ if (bd_name = BD_DICT[name])
27
+ return bd_name
28
+ end
29
+
30
+ name
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
@@ -32,6 +32,43 @@ module Fit4Ruby
32
32
  @timestamp <=> fdr.timestamp
33
33
  end
34
34
 
35
+ def numeric_manufacturer
36
+ if @manufacturer && @manufacturer.is_a?(String)
37
+ if @manufacturer[0..17] == 'Undocumented value'
38
+ return @manufacturer[18..-1].to_i
39
+ else
40
+ return GlobalFitDictionaries['manufacturer'].
41
+ value_by_name(@manufacturer)
42
+ end
43
+ end
44
+
45
+ Log.fatal "Unexpected @manufacturer (#{@manufacturer}) value"
46
+ end
47
+
48
+ def numeric_product
49
+ # The numeric product ID must be an integer or nil. In case the
50
+ # dictionary did not contain an entry for the numeric ID in the fit file
51
+ # the @garmin_product or @product variables contain a String starting
52
+ # with 'Undocumented value ' followed by the ID.
53
+ if @garmin_product && @garmin_product.is_a?(String)
54
+ if @garmin_product[0..17] == 'Undocumented value'
55
+ return @garmin_product[18..-1].to_i
56
+ else
57
+ return GlobalFitDictionaries['garmin_product'].
58
+ value_by_name(@garmin_product)
59
+ end
60
+ elsif @product && @product.is_a?(String)
61
+ if @product[0..17] == 'Undocumented value'
62
+ return @product[18..-1].to_i
63
+ else
64
+ return GlobalFitDictionaries['product'].value_by_name(@product)
65
+ end
66
+ end
67
+
68
+ Log.fatal "Unexpected @product (#{@product}) or " +
69
+ "@garmin_product (#{@garmin_product}) values"
70
+ end
71
+
35
72
  def check(index)
36
73
  unless @device_index
37
74
  Log.fatal 'device info record must have a device_index'