git-fit 0.13.0 → 0.14.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.
@@ -24,6 +24,12 @@ module GitFit
24
24
  bytes = File.read(path, mode: 'rb').bytes
25
25
  CTX.eval("FitDecoder.fitSport(#{bytes.inspect})")
26
26
  end
27
+
28
+ def self.session(path)
29
+ bytes = File.read(path, mode: 'rb').bytes
30
+ json = CTX.eval("FitDecoder.fitSession(#{bytes.inspect})")
31
+ json ? JSON.parse(json) : nil
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -147,7 +147,7 @@ module GitFit
147
147
 
148
148
  l2_points = nil
149
149
  polyline = nil
150
- elevation_gain = nil
150
+ elevation = {}
151
151
 
152
152
  if route_paths&.any?
153
153
  gpx_l1_path = File.join(raw_dir, "#{run_id}.gpx")
@@ -189,14 +189,14 @@ module GitFit
189
189
 
190
190
  coords = l2_points.map { |pt| [pt[:lat], pt[:lng]] }
191
191
  polyline = GitFit::Geo::Polyline.encode(coords)
192
- elevation_gain = elevation_gain_from_points(l2_points)
192
+ elevation = elevation_profile(l2_points)
193
193
  end
194
194
  end
195
195
 
196
196
  title = build_title(workout_type, start_date)
197
- attrs = build_attrs(source_name, run_id, stats, meta, polyline, elevation_gain,
197
+ attrs = build_attrs(source_name, run_id, stats, meta, polyline,
198
198
  workout_type, start_date, end_date, duration, duration_unit,
199
- hr_avg, hr_max)
199
+ hr_avg: hr_avg, hr_max: hr_max, elevation: elevation)
200
200
  attrs[:title] = title
201
201
 
202
202
  if attrs[:start_date_local].nil? && l2_points&.any?
@@ -433,9 +433,9 @@ module GitFit
433
433
  nil
434
434
  end
435
435
 
436
- def build_attrs(_source_name, run_id, stats, meta, polyline, elevation_gain,
436
+ def build_attrs(_source_name, run_id, stats, meta, polyline,
437
437
  workout_type, start_date, end_date, duration, duration_unit,
438
- hr_avg = nil, hr_max = nil)
438
+ hr_avg: nil, hr_max: nil, elevation: {})
439
439
  distance = nil
440
440
  calories = nil
441
441
 
@@ -470,7 +470,11 @@ module GitFit
470
470
  max_heartrate: hr_max,
471
471
  average_speed: avg_speed,
472
472
  calories: calories,
473
- elevation_gain: elevation_gain&.round(1),
473
+ elevation_gain: elevation[:gain]&.round(1),
474
+ elevation_loss: elevation[:loss]&.round(1),
475
+ elevation_min: elevation[:min]&.round(1),
476
+ elevation_max: elevation[:max]&.round(1),
477
+ steps: steps_from_stats(stats),
474
478
  average_temperature: parse_temperature(meta['HKWeatherTemperature']),
475
479
  source: 'apple_health',
476
480
  external_id: meta['HKExternalUUID'],
@@ -569,6 +573,31 @@ module GitFit
569
573
  total
570
574
  end
571
575
 
576
+ def steps_from_stats(stats)
577
+ sum = stats.dig('HKQuantityTypeIdentifierStepCount', 'sum')
578
+ sum&.to_f&.round&.nonzero?
579
+ end
580
+
581
+ def elevation_profile(points)
582
+ {
583
+ gain: elevation_gain_from_points(points),
584
+ loss: elevation_loss_from_points(points),
585
+ min: points.map { |pt| pt[:ele] }.compact.min,
586
+ max: points.map { |pt| pt[:ele] }.compact.max,
587
+ }
588
+ end
589
+
590
+ def elevation_loss_from_points(points)
591
+ total = 0.0
592
+ (1...points.length).each do |i|
593
+ elev1 = points[i - 1][:ele] || 0.0
594
+ elev2 = points[i][:ele] || 0.0
595
+ diff = elev1 - elev2
596
+ total += diff if diff > 0
597
+ end
598
+ total
599
+ end
600
+
572
601
  def apple_utc_to_local(utc_iso8601, tz_name)
573
602
  return utc_iso8601 unless utc_iso8601 && tz_name
574
603
  t = Time.parse(utc_iso8601)
@@ -110,8 +110,8 @@ module GitFit
110
110
  end
111
111
 
112
112
  def upsert_activity(activity)
113
- # steps/total_up/total_down are hash-only signals, not DB columns
114
- persistable = activity.reject { |k, _| %i[steps total_up total_down].include?(k) }
113
+ # total_up/total_down are hash-only signals, not DB columns (mapped to elevation_gain/loss by the parser)
114
+ persistable = activity.reject { |k, _| %i[total_up total_down].include?(k) }
115
115
  super(persistable)
116
116
  end
117
117
 
@@ -46,6 +46,28 @@ module GitFit
46
46
  end
47
47
  end
48
48
 
49
+ def elevation_loss(points)
50
+ return 0.0 if points.length < 2
51
+ (1...points.length).sum do |i|
52
+ elev1 = points[i - 1][2] || 0.0
53
+ elev2 = points[i][2] || 0.0
54
+ loss = elev1 - elev2
55
+ loss > 0 ? loss : 0.0
56
+ end
57
+ end
58
+
59
+ def elevation_min(points)
60
+ elevations(points).min
61
+ end
62
+
63
+ def elevation_max(points)
64
+ elevations(points).max
65
+ end
66
+
67
+ def elevations(points)
68
+ points.map { |p| p[2] }.compact
69
+ end
70
+
49
71
  def moving_time_from_points(points, threshold_s: 30)
50
72
  return 0 if points.length < 2
51
73
  total = 0
@@ -112,6 +134,10 @@ module GitFit
112
134
  average_heartrate: data[:average_heartrate]&.round(1),
113
135
  average_speed: data[:distance] && data[:moving_time].to_f.positive? ? (data[:distance].to_f / data[:moving_time]).round(2) : data[:average_speed],
114
136
  elevation_gain: data[:elevation_gain]&.round(1),
137
+ elevation_loss: data[:elevation_loss]&.round(1),
138
+ elevation_min: data[:elevation_min]&.round(1),
139
+ elevation_max: data[:elevation_max]&.round(1),
140
+ steps: data[:steps]&.round,
115
141
  source: @source_name || data[:source],
116
142
  }
117
143
  end
@@ -25,6 +25,15 @@ module GitFit
25
25
 
26
26
  sport_type = GitFit::FIT::Decoder.sport(path)
27
27
 
28
+ session = GitFit::FIT::Decoder.session(path)
29
+ if session && (session['totalAscent'].is_a?(Numeric) || session['totalDescent'].is_a?(Numeric))
30
+ elevation_gain = session['totalAscent'] || 0.0
31
+ elevation_loss = session['totalDescent'] || 0.0
32
+ else
33
+ elevation_gain = elevation_gain(points)
34
+ elevation_loss = elevation_loss(points)
35
+ end
36
+
28
37
  [build_activity_attrs({
29
38
  run_id: build_run_id('fit', SecureRandom.hex(8)),
30
39
  name: nil,
@@ -38,7 +47,10 @@ module GitFit
38
47
  location_country: nil,
39
48
  summary_polyline: simplify_polyline(points.map { |p| [p[0], p[1]] }),
40
49
  average_heartrate: average_heartrate(points),
41
- elevation_gain: elevation_gain(points),
50
+ elevation_gain: elevation_gain,
51
+ elevation_loss: elevation_loss,
52
+ elevation_min: elevation_min(points),
53
+ elevation_max: elevation_max(points),
42
54
  source: 'fit',
43
55
  })]
44
56
  end
@@ -51,7 +63,8 @@ module GitFit
51
63
  return nil unless lat && lon
52
64
  ts = rec['timestamp']
53
65
  time = ts.is_a?(Numeric) ? FIT_EPOCH + ts : parse_time(ts)
54
- [lat, lon, rec['altitude'], time, rec['heartRate']]
66
+ altitude = rec['enhancedAltitude'] || rec['altitude']
67
+ [lat, lon, altitude, time, rec['heartRate']]
55
68
  end
56
69
 
57
70
  def average_heartrate(points)
@@ -41,6 +41,9 @@ module GitFit
41
41
  summary_polyline: simplify_polyline(points.map { |p| [p[0], p[1]] }),
42
42
  average_heartrate: average_heartrate(points),
43
43
  elevation_gain: elevation_gain(points),
44
+ elevation_loss: elevation_loss(points),
45
+ elevation_min: elevation_min(points),
46
+ elevation_max: elevation_max(points),
44
47
  source: 'gpx',
45
48
  }
46
49
  build_activity_attrs(data)
@@ -82,7 +82,10 @@ module GitFit
82
82
  location_country: nil,
83
83
  summary_polyline: nil,
84
84
  average_heartrate: nil,
85
- elevation_gain: elevation_gain(points),
85
+ elevation_gain: ti['totalup']&.to_f || elevation_gain(points),
86
+ elevation_loss: ti['totaldown']&.to_f || elevation_loss(points),
87
+ elevation_min: elevation_min(points),
88
+ elevation_max: elevation_max(points),
86
89
  source: '2bulu',
87
90
  })
88
91
  attach_sensor_stats(attrs, ti)
@@ -204,7 +207,10 @@ module GitFit
204
207
  location_country: nil,
205
208
  summary_polyline: nil,
206
209
  average_heartrate: nil,
207
- elevation_gain: elevation_gain(points),
210
+ elevation_gain: ext['ElevationGain']&.to_f || elevation_gain(points),
211
+ elevation_loss: ext['ElevationLoss']&.to_f || elevation_loss(points),
212
+ elevation_min: elevation_min(points),
213
+ elevation_max: elevation_max(points),
208
214
  source: '2bulu',
209
215
  })
210
216
  attach_kml_stats(attrs, ext)
@@ -27,6 +27,7 @@ module GitFit
27
27
  total_moving_time = lap_data.sum { |ld| ld[:moving_time] || 0 }
28
28
  total_elapsed_time = lap_data.sum { |ld| ld[:elapsed_time] || 0 }
29
29
  elevation_gain = lap_data.sum { |ld| ld[:elevation_gain] || 0.0 }
30
+ elevation_loss = lap_data.sum { |ld| ld[:elevation_loss] || 0.0 }
30
31
  avg_hr = lap_data.map { |ld| ld[:avg_hr] }.compact
31
32
  all_points = lap_data.flat_map { |ld| ld[:points] }
32
33
  times = all_points.map { |p| p[3] }.compact
@@ -45,6 +46,9 @@ module GitFit
45
46
  summary_polyline: simplify_polyline(all_points.map { |p| [p[0], p[1]] }),
46
47
  average_heartrate: avg_hr.empty? ? nil : (avg_hr.sum / avg_hr.size).round(1),
47
48
  elevation_gain: elevation_gain,
49
+ elevation_loss: elevation_loss,
50
+ elevation_min: elevation_min(all_points),
51
+ elevation_max: elevation_max(all_points),
48
52
  source: 'tcx',
49
53
  }
50
54
  build_activity_attrs(data)
@@ -58,10 +62,11 @@ module GitFit
58
62
  avg_hr_node = lap.at_xpath('tcx:AverageHeartRateBpm/tcx:Value', NS)
59
63
  avg_hr = avg_hr_node&.text&.to_f
60
64
  elev_gain = points.length >= 2 ? elevation_gain(points) : 0.0
65
+ elev_loss = points.length >= 2 ? elevation_loss(points) : 0.0
61
66
  moving_time = moving_time_from_points(points, threshold_s: 30)
62
67
  { distance: distance, elapsed_time: total_time&.round,
63
68
  moving_time: moving_time > 0 ? moving_time : (total_time&.round || 0),
64
- avg_hr: avg_hr, elevation_gain: elev_gain, points: points }
69
+ avg_hr: avg_hr, elevation_gain: elev_gain, elevation_loss: elev_loss, points: points }
65
70
  end
66
71
 
67
72
  def parse_track(lap)
@@ -199,7 +199,7 @@ module GitFit
199
199
  end
200
200
 
201
201
  def save_secret_to_config
202
- path = @config['config_path'] || 'config/config.yml'
202
+ path = @config['config_path'] || GitFit::Config.default_path
203
203
  return unless File.exist?(path)
204
204
 
205
205
  cfg = YAML.safe_load(File.read(path)) || {}
@@ -477,7 +477,7 @@ module GitFit
477
477
  end
478
478
 
479
479
  def reauth_hint
480
- "Re-auth: set sync.#{source_prefix}.secret in config/config.yml"
480
+ "Re-auth: set sync.#{source_prefix}.secret in #{GitFit::Config.default_path}"
481
481
  end
482
482
 
483
483
  def build_attrs(detail, activity_id, type, polyline = nil)
@@ -509,6 +509,10 @@ module GitFit
509
509
  average_temperature: summary['avgTemperature']&.to_f,
510
510
  average_speed: summary['averageSpeed']&.to_f,
511
511
  elevation_gain: summary['elevationGain']&.to_f,
512
+ elevation_loss: summary['elevationLoss']&.to_f,
513
+ elevation_min: summary['minElevation']&.to_f,
514
+ elevation_max: summary['maxElevation']&.to_f,
515
+ steps: summary['steps']&.to_i,
512
516
  source: source_prefix,
513
517
  }
514
518
  end
@@ -103,8 +103,9 @@ module GitFit
103
103
  write_standardized_json(wgs_points, ride_id)
104
104
  polyline = Geo::Polyline.encode(wgs_points.map { |pt| [pt['positionLat'], pt['positionLong']] })
105
105
  sensor_summary = compute_sensor_summary(wgs_points)
106
+ elevation_stats = elevation_stats_from_records(wgs_points, fit_path)
106
107
 
107
- attrs = build_attrs(act, ride_id, polyline, sensor_summary)
108
+ attrs = build_attrs(act, ride_id, polyline, sensor_summary, elevation_stats)
108
109
  return false unless attrs
109
110
 
110
111
  upsert_activity(attrs)
@@ -153,7 +154,7 @@ module GitFit
153
154
  JSON.parse(resp.body).dig('data', 'rows')
154
155
  end
155
156
 
156
- def build_attrs(act, ride_id, polyline = nil, sensor = {})
157
+ def build_attrs(act, ride_id, polyline = nil, sensor = {}, elevation_stats = {})
157
158
  type = map_sport(act['exerciseType'])
158
159
  start_t = @fit_start_utc || parse_date(act['startTime'])
159
160
  start_t_local = @fit_start_local || start_t
@@ -182,6 +183,9 @@ module GitFit
182
183
  average_temperature: sensor[:average_temperature],
183
184
  average_speed: avg_speed > 0 ? avg_speed : nil,
184
185
  elevation_gain: act['totalAscent'].to_f.nonzero?,
186
+ elevation_loss: elevation_stats[:elevation_loss],
187
+ elevation_min: elevation_stats[:elevation_min],
188
+ elevation_max: elevation_stats[:elevation_max],
185
189
  summary_polyline: polyline,
186
190
  source: 'igpsport',
187
191
  }
@@ -204,6 +208,28 @@ module GitFit
204
208
  }
205
209
  end
206
210
 
211
+ def elevation_stats_from_records(records, fit_path)
212
+ alts = records.map { |r| r['enhancedAltitude'] || r['altitude'] }.compact
213
+ points_loss = 0.0
214
+ (1...alts.size).each do |i|
215
+ d = alts[i - 1] - alts[i]
216
+ points_loss += d if d > 0
217
+ end
218
+
219
+ session_descent = begin
220
+ session = FIT::Decoder.session(fit_path)
221
+ session && session['totalDescent'].is_a?(Numeric) ? session['totalDescent'] : nil
222
+ rescue StandardError
223
+ nil
224
+ end
225
+
226
+ {
227
+ elevation_loss: (session_descent || (alts.size >= 2 ? points_loss : nil))&.round(1),
228
+ elevation_min: alts.min&.round(1),
229
+ elevation_max: alts.max&.round(1),
230
+ }
231
+ end
232
+
207
233
  def map_sport(exercise_type)
208
234
  EXERCISE_MAP[exercise_type] || 'other'
209
235
  end
@@ -306,12 +306,14 @@ module GitFit
306
306
 
307
307
  polyline_str = nil
308
308
  total_elevation = run_data['elevation']
309
+ total_elevation_loss = nil
309
310
  computed_distance = run_data['distance']
310
311
 
311
312
  if wgs_points.any?
312
313
  if wgs_points.any? { |pt| pt[3] }
313
314
  computed_distance ||= total_distance_haversine(wgs_points)
314
315
  total_elevation ||= elevation_gain_from_points(wgs_points)
316
+ total_elevation_loss = elevation_loss_from_points(wgs_points)
315
317
  end
316
318
 
317
319
  polyline_str = Geo::Polyline.encode(wgs_points.map { |pt| [pt[0], pt[1]] })
@@ -336,6 +338,9 @@ module GitFit
336
338
  average_heartrate: avg_hr&.round(1),
337
339
  average_speed: computed_distance && moving_time&.positive? ? (computed_distance.to_f / moving_time).round(2) : nil,
338
340
  elevation_gain: total_elevation&.round(1),
341
+ elevation_loss: total_elevation_loss&.round(1),
342
+ elevation_min: elevation_min_from_points(wgs_points),
343
+ elevation_max: elevation_max_from_points(wgs_points),
339
344
  source: 'keep',
340
345
  }
341
346
  end
@@ -424,6 +429,25 @@ module GitFit
424
429
  total
425
430
  end
426
431
 
432
+ def elevation_loss_from_points(points)
433
+ total = 0.0
434
+ (1...points.length).each do |i|
435
+ elev1 = points[i - 1][2] || 0.0
436
+ elev2 = points[i][2] || 0.0
437
+ diff = elev1 - elev2
438
+ total += diff if diff > 0
439
+ end
440
+ total
441
+ end
442
+
443
+ def elevation_min_from_points(points)
444
+ points.map { |p| p[2] }.compact.min
445
+ end
446
+
447
+ def elevation_max_from_points(points)
448
+ points.map { |p| p[2] }.compact.max
449
+ end
450
+
427
451
  def spider_sleep
428
452
  sleep 0.1
429
453
  end
@@ -17,28 +17,32 @@ module GitFit
17
17
  location_country: 'CN', average_heartrate: 148.0, max_heartrate: 165.0,
18
18
  average_cadence: 172.0, max_cadence: 185.0, average_power: nil, max_power: nil,
19
19
  average_temperature: 22.0, calories: 380,
20
- average_speed: 5200.0 / 1800, elevation_gain: 125.0, external_id: 'lorem_001' },
20
+ average_speed: 5200.0 / 1800, elevation_gain: 125.0, elevation_loss: 110.0,
21
+ elevation_min: 35.0, elevation_max: 160.0, steps: 8200, external_id: 'lorem_001' },
21
22
  { id: 'beta_002', name: 'Ipsum Cycling Session', sport_category: 'ride',
22
23
  distance: 35000.0, moving_time: 5400, elapsed_time: 6000,
23
24
  start_date: '2025-01-14T08:00:00Z', start_date_local: '2025-01-14T16:00:00+08:00',
24
25
  location_country: 'CN', average_heartrate: 138.0, max_heartrate: 155.0,
25
26
  average_cadence: 85.0, max_cadence: 105.0, average_power: 185.0, max_power: 420.0,
26
27
  average_temperature: 18.0, calories: 850,
27
- average_speed: 35000.0 / 5400, elevation_gain: 450.0, external_id: 'lorem_002' },
28
+ average_speed: 35000.0 / 5400, elevation_gain: 450.0, elevation_loss: 430.0,
29
+ elevation_min: 12.0, elevation_max: 460.0, external_id: 'lorem_002' },
28
30
  { id: 'gamma_003', name: 'Dolor Hike', sport_category: 'hike',
29
31
  distance: 12000.0, moving_time: 7200, elapsed_time: 8100,
30
32
  start_date: '2025-01-13T09:00:00Z', start_date_local: '2025-01-13T17:00:00+08:00',
31
33
  location_country: 'CN', average_heartrate: 122.0, max_heartrate: 142.0,
32
34
  average_cadence: 110.0, max_cadence: 135.0, average_power: nil, max_power: nil,
33
35
  average_temperature: 15.0, calories: 620,
34
- average_speed: 12000.0 / 7200, elevation_gain: 820.0, external_id: 'lorem_003' },
36
+ average_speed: 12000.0 / 7200, elevation_gain: 820.0, elevation_loss: 790.0,
37
+ elevation_min: 300.0, elevation_max: 900.0, steps: 15_200, external_id: 'lorem_003' },
35
38
  { id: 'delta_004', name: 'Sit Amet Walk', sport_category: 'walk',
36
39
  distance: 5000.0, moving_time: 3600, elapsed_time: 3900,
37
40
  start_date: '2025-01-12T18:00:00Z', start_date_local: '2025-01-12T18:00:00+08:00',
38
41
  location_country: 'CN', average_heartrate: 108.0, max_heartrate: 125.0,
39
42
  average_cadence: 105.0, max_cadence: 118.0, average_power: nil, max_power: nil,
40
43
  average_temperature: 20.0, calories: 280,
41
- average_speed: 5000.0 / 3600, elevation_gain: 35.0, external_id: 'lorem_004' },
44
+ average_speed: 5000.0 / 3600, elevation_gain: 35.0, elevation_loss: 40.0,
45
+ steps: 6400, external_id: 'lorem_004' },
42
46
  { id: 'epsilon_005', name: 'Consectetur Swim', sport_category: 'swim',
43
47
  distance: 2000.0, moving_time: 2400, elapsed_time: 2700,
44
48
  start_date: '2025-01-11T07:00:00Z', start_date_local: '2025-01-11T15:00:00+08:00',
@@ -100,6 +104,10 @@ module GitFit
100
104
  calories: act[:calories],
101
105
  average_speed: act[:average_speed],
102
106
  elevation_gain: act[:elevation_gain],
107
+ elevation_loss: act[:elevation_loss],
108
+ elevation_min: act[:elevation_min],
109
+ elevation_max: act[:elevation_max],
110
+ steps: act[:steps],
103
111
  source: 'lorem',
104
112
  external_id: act[:external_id],
105
113
  }
@@ -86,7 +86,8 @@ module GitFit
86
86
  act.dig('map', 'summary_polyline')
87
87
  end
88
88
 
89
- attrs = build_attrs(act, type, polyline, external_id: external_id)
89
+ attrs = build_attrs(act, type, polyline, external_id: external_id,
90
+ elevation_loss: l2 ? elevation_loss_from_stream(l2) : nil)
90
91
  upsert_activity(attrs)
91
92
  attrs
92
93
  end
@@ -170,7 +171,7 @@ module GitFit
170
171
  data && i < data.size ? data[i] : nil
171
172
  end
172
173
 
173
- def build_attrs(act, category, polyline = nil, external_id: nil)
174
+ def build_attrs(act, category, polyline = nil, external_id: nil, elevation_loss: nil)
174
175
  start_t = Time.parse(act['start_date']) rescue nil
175
176
  local_t = Time.parse(act['start_date_local']) rescue nil
176
177
 
@@ -196,11 +197,23 @@ module GitFit
196
197
  average_temperature: act['average_temp'],
197
198
  average_speed: act['average_speed'],
198
199
  elevation_gain: act['total_elevation_gain'],
200
+ elevation_loss: elevation_loss&.round(1),
201
+ elevation_min: act['elev_low'],
202
+ elevation_max: act['elev_high'],
199
203
  source: 'strava',
200
204
  external_id: external_id,
201
205
  }
202
206
  end
203
207
 
208
+ def elevation_loss_from_stream(l2)
209
+ alts = l2.filter_map { |pt| pt[:altitude] }
210
+ return nil if alts.size < 2
211
+ (1...alts.size).sum do |i|
212
+ diff = alts[i - 1] - alts[i]
213
+ diff > 0 ? diff : 0.0
214
+ end
215
+ end
216
+
204
217
  def map_sport_type(type, sport_type)
205
218
  SportMapper.canonicalize(sport_type || type)
206
219
  end
@@ -251,6 +251,7 @@ module GitFit
251
251
  start_date_local: start_t.iso8601,
252
252
  average_speed: (act['avg_speed'].to_f / 3.6).round(2).nonzero?,
253
253
  elevation_gain: act['elevation_gain'].to_f.nonzero?,
254
+ elevation_loss: act['elevation_loss'].to_f.nonzero?,
254
255
  source: 'xingzhe',
255
256
  }
256
257
 
@@ -316,6 +316,7 @@ module GitFit
316
316
  pts = wgs_points.map { |pt| [pt['positionLat'], pt['positionLong']] }
317
317
  attrs[:summary_polyline] = Geo::Polyline.encode(pts)
318
318
  attrs.merge!(compute_sensor_summary(wgs_points))
319
+ attrs.merge!(elevation_stats_from_records(wgs_points, raw_path(act_id)))
319
320
  end
320
321
  end
321
322
  end
@@ -371,6 +372,28 @@ module GitFit
371
372
  }
372
373
  end
373
374
 
375
+ def elevation_stats_from_records(records, fit_path)
376
+ alts = records.map { |r| r['enhancedAltitude'] || r['altitude'] }.compact
377
+ points_loss = 0.0
378
+ (1...alts.size).each do |i|
379
+ d = alts[i - 1] - alts[i]
380
+ points_loss += d if d > 0
381
+ end
382
+
383
+ session_descent = begin
384
+ session = FIT::Decoder.session(fit_path)
385
+ session && session['totalDescent'].is_a?(Numeric) ? session['totalDescent'] : nil
386
+ rescue StandardError
387
+ nil
388
+ end
389
+
390
+ {
391
+ elevation_loss: (session_descent || (alts.size >= 2 ? points_loss : nil))&.round(1),
392
+ elevation_min: alts.min&.round(1),
393
+ elevation_max: alts.max&.round(1),
394
+ }
395
+ end
396
+
374
397
  def map_sport(sport)
375
398
  SPORT_MAP[sport] || 'other'
376
399
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitFit
4
- VERSION = '0.13.0'
4
+ VERSION = '0.14.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git-fit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lax
@@ -244,6 +244,7 @@ extra_rdoc_files: []
244
244
  files:
245
245
  - db/migrations/001_full_schema.rb
246
246
  - db/migrations/002_iso8601_time_format.rb
247
+ - db/migrations/003_add_elevation_fields.rb
247
248
  - exe/git-fit
248
249
  - lib/git-fit.rb
249
250
  - lib/git_fit/auth/garmin.rb