fit4ruby 3.3.0 → 3.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
- 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'