git-fit 0.17.1 → 0.17.3

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
  SHA256:
3
- metadata.gz: 1091af70dde8e2b640208f49633b3572ddf64e41c5063ec4541349a873829988
4
- data.tar.gz: aa450db53cfe1eacc076d32f35c78da9bb0bfe86ab014d73554127d9467db876
3
+ metadata.gz: 8e88e21cd60e366ac83ce9f0dac135a5532d6c8953dc9557873a39b55de07174
4
+ data.tar.gz: 38d23b0f68e6e15ecbe9413d3ac0992fdab84faaa7b2977c6b4284a1f1a223ca
5
5
  SHA512:
6
- metadata.gz: 122b1ecaac183df2215020f86fdd9f534aed6f356e9bee3e0e53c281f88a6d0c519007aa9809f89c99cfbaeb2e563c712b597065373280160f655188c633749a
7
- data.tar.gz: 5c4e2a6f3832390138c1d6ea4d96ede15e8119fb85d67829da6801b81ff0c810d1e42c072bfcd647586b67c5482ec7209168015d0c97bc870326a5b6302916e2
6
+ metadata.gz: 77e28b8840ad990c41c82585de950af40761f52cfddba38f9b7d6d64386fdc5a9e87191e33d4be9d5a945da950a13e0e3f5182a92a1eaeb0f38ecc481dcb6917
7
+ data.tar.gz: f6fcee3ebc58afdcbaeb849cb2eca576de1039ed13783424abda28da6f6fccc80cc9fd5690efdf492e533039b22adcfb16b8a98b28cbc5e5494165dd05b88328
data/lib/git-fit.rb CHANGED
@@ -93,6 +93,8 @@ require_relative 'git_fit/cli/export'
93
93
  require_relative 'git_fit/cli/import_cli'
94
94
  require_relative 'git_fit/cli/geo_cli'
95
95
  require_relative 'git_fit/strava_web/file_check'
96
+ require_relative 'git_fit/cli/elevation'
97
+ require_relative 'git_fit/elevation/backfill'
96
98
  require_relative 'git_fit/cli/strava'
97
99
  require_relative 'git_fit/auth/garmin_token'
98
100
  require_relative 'git_fit/auth/strava'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module GitFit
6
+ class ElevationCLI < Thor
7
+ no_commands do
8
+ def git_fit_config
9
+ @git_fit_config ||= GitFit::Config.new(options[:config])
10
+ end
11
+ end
12
+
13
+ desc 'backfill [SOURCE]', 'Backfill elevation data (gain/loss/min/max) from raw FIT files or L2 data'
14
+ option :force, type: :boolean, default: false, desc: 'Overwrite existing values (default: only fill NULLs)'
15
+ def backfill(source = nil)
16
+ config = git_fit_config
17
+ conn = GitFit::DB::Connection.new(config.db_path)
18
+ conn.migrate!
19
+ db = conn.db
20
+
21
+ result = GitFit::Elevation::Backfill.new(db, force: options[:force]).call(source)
22
+
23
+ msg = "Elevation backfill: #{result[:updated]} updated, " \
24
+ "#{result[:skipped]} skipped, #{result[:failed]} failed"
25
+ say_status :done, msg, :green
26
+ rescue StandardError => e
27
+ say_status :error, "Elevation backfill failed: #{e.message}", :red
28
+ end
29
+ end
30
+ end
data/lib/git_fit/cli.rb CHANGED
@@ -117,6 +117,9 @@ module GitFit
117
117
  say_status :error, "Dedup failed: #{e.message}", :red
118
118
  end
119
119
 
120
+ desc 'elevation SUBCOMMAND', 'Elevation data operations'
121
+ subcommand 'elevation', ElevationCLI
122
+
120
123
  desc 'strava SUBCOMMAND', 'Strava web operations'
121
124
  subcommand 'strava', StravaCLI
122
125
 
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitFit
4
+ module Elevation
5
+ class Backfill
6
+ SOURCES = %w[igpsport xoss strava garmin garmin_cn].freeze
7
+ BATCH_SIZE = 100
8
+
9
+ def initialize(db, force: false)
10
+ @db = db
11
+ @force = force
12
+ @updated = 0
13
+ @skipped = 0
14
+ @failed = 0
15
+ end
16
+
17
+ def call(source = nil)
18
+ sources = source ? [source] : SOURCES
19
+ sources.each do |src|
20
+ backfill_source(src)
21
+ end
22
+ { updated: @updated, skipped: @skipped, failed: @failed }
23
+ end
24
+
25
+ private
26
+
27
+ def backfill_source(source)
28
+ offset = 0
29
+ loop do
30
+ rows = fetch_rows(source, offset)
31
+ break if rows.empty?
32
+
33
+ @db.transaction do
34
+ rows.each { |row| process_row(row, source) }
35
+ end
36
+
37
+ offset += BATCH_SIZE
38
+ end
39
+ end
40
+
41
+ def fetch_rows(source, offset)
42
+ scope = @db[:activities].where(source: source)
43
+ unless @force
44
+ scope = scope.where(Sequel.|(
45
+ elevation_gain: nil,
46
+ elevation_loss: nil,
47
+ elevation_min: nil,
48
+ elevation_max: nil,
49
+ ))
50
+ end
51
+ scope.order(:id).limit(BATCH_SIZE, offset).all
52
+ end
53
+
54
+ def process_row(row, source)
55
+ run_id = row[:run_id]
56
+ natural_id = run_id.sub("#{source}_", '')
57
+
58
+ elev = case source
59
+ when 'igpsport' then compute_from_fit(source, natural_id)
60
+ when 'xoss' then compute_from_fit(source, natural_id)
61
+ when 'garmin' then compute_from_fit(source, natural_id)
62
+ when 'garmin_cn' then compute_from_fit(source, natural_id)
63
+ when 'strava' then compute_from_l2(source, natural_id)
64
+ else return
65
+ end
66
+
67
+ return skip_row(run_id) unless elev
68
+
69
+ update_row(row, elev)
70
+ end
71
+
72
+ def compute_from_fit(source, natural_id)
73
+ fit_path = fit_path_for(source, natural_id)
74
+ return nil unless fit_path && File.exist?(fit_path)
75
+
76
+ records = GitFit::FIT::Decoder.decode(fit_path)
77
+ return nil if records.empty?
78
+
79
+ alts = records.filter_map { |r| r['enhancedAltitude'] || r['altitude'] }
80
+ return nil if alts.size < 2
81
+
82
+ session = begin
83
+ GitFit::FIT::Decoder.session(fit_path)
84
+ rescue StandardError
85
+ nil
86
+ end
87
+
88
+ gain = session&.dig('totalAscent').is_a?(Numeric) ? session['totalAscent'] : elevation_gain_from_alts(alts)
89
+ loss = session&.dig('totalDescent').is_a?(Numeric) ? session['totalDescent'] : elevation_loss_from_alts(alts)
90
+
91
+ {
92
+ elevation_gain: gain&.round(1),
93
+ elevation_loss: loss&.round(1),
94
+ elevation_min: alts.min&.round(1),
95
+ elevation_max: alts.max&.round(1),
96
+ }
97
+ rescue StandardError => e
98
+ warn "Elevation backfill [#{source}][#{natural_id}]: #{e.message}"
99
+ nil
100
+ end
101
+
102
+ def compute_from_l2(source, natural_id)
103
+ l2_path = File.join('data', 'std', source, "#{natural_id}.json")
104
+ return nil unless File.exist?(l2_path)
105
+
106
+ l2 = JSON.parse(File.read(l2_path))
107
+ alts = l2.filter_map { |pt| pt['altitude'] || pt[:altitude] }
108
+ return nil if alts.size < 2
109
+
110
+ {
111
+ elevation_gain: elevation_gain_from_alts(alts)&.round(1),
112
+ elevation_loss: elevation_loss_from_alts(alts)&.round(1),
113
+ elevation_min: alts.min&.round(1),
114
+ elevation_max: alts.max&.round(1),
115
+ }
116
+ rescue StandardError => e
117
+ warn "Elevation backfill [#{source}][#{natural_id}]: #{e.message}"
118
+ nil
119
+ end
120
+
121
+ def fit_path_for(source, natural_id)
122
+ case source
123
+ when 'garmin', 'garmin_cn'
124
+ File.join('data', 'raw', source, natural_id, 'activity.fit')
125
+ else
126
+ File.join('data', 'raw', source, "#{natural_id}.fit")
127
+ end
128
+ end
129
+
130
+ def elevation_gain_from_alts(alts)
131
+ return 0.0 if alts.size < 2
132
+ (1...alts.size).sum do |i|
133
+ d = alts[i] - alts[i - 1]
134
+ d > 0 ? d : 0.0
135
+ end
136
+ end
137
+
138
+ def elevation_loss_from_alts(alts)
139
+ return 0.0 if alts.size < 2
140
+ (1...alts.size).sum do |i|
141
+ d = alts[i - 1] - alts[i]
142
+ d > 0 ? d : 0.0
143
+ end
144
+ end
145
+
146
+ def update_row(row, elev)
147
+ @db[:activities].where(id: row[:id]).update(
148
+ elevation_gain: elev[:elevation_gain],
149
+ elevation_loss: elev[:elevation_loss],
150
+ elevation_min: elev[:elevation_min],
151
+ elevation_max: elev[:elevation_max],
152
+ updated_at: Time.now.utc,
153
+ )
154
+ @updated += 1
155
+ end
156
+
157
+ def skip_row(_run_id)
158
+ @skipped += 1
159
+ end
160
+ end
161
+ end
162
+ end
@@ -39,6 +39,15 @@ module GitFit
39
39
  nil
40
40
  end
41
41
 
42
+ def decode_step_data(step_data)
43
+ return nil unless step_data
44
+ raw = Base64.decode64(step_data)
45
+ decompressed = zlib_inflate(raw)
46
+ JSON.parse(decompressed)
47
+ rescue StandardError
48
+ nil
49
+ end
50
+
42
51
  def aes_decrypt(data)
43
52
  cipher = OpenSSL::Cipher.new('AES-128-CBC')
44
53
  cipher.decrypt
@@ -14,6 +14,8 @@ module GitFit
14
14
  KEEP_TYPES = %w[running hiking cycling].freeze
15
15
  KEEP_RAW_EXT = 'json'
16
16
 
17
+ STEP_ENCODED_FIELDS = %w[stepPoints stepFrequencies fullStepPoints].freeze
18
+
17
19
  TYPE_MAP = {
18
20
  'outdoorWalking' => 'walk',
19
21
  'outdoorRunning' => 'run',
@@ -208,9 +210,16 @@ module GitFit
208
210
  raw = run_data.dup
209
211
  if raw['heartRate'].is_a?(Hash)
210
212
  raw['heartRate'] = raw['heartRate'].dup
211
- raw['heartRate'].delete('heartRates')
213
+ raw['heartRate']['heartRates'] = nil if raw['heartRate'].key?('heartRates')
214
+ end
215
+ raw['geoPoints'] = nil if raw.key?('geoPoints')
216
+
217
+ decoded_steps = {}
218
+ STEP_ENCODED_FIELDS.each do |field|
219
+ next unless raw.key?(field)
220
+ decoded_steps[field] = Source::Keep.decode_step_data(raw[field])
221
+ raw[field] = nil
212
222
  end
213
- raw.delete('geoPoints')
214
223
 
215
224
  write_source_archive({
216
225
  archiveVersion: 0,
@@ -219,7 +228,7 @@ module GitFit
219
228
  raw: raw,
220
229
  geoPoints: payload[:geo_points],
221
230
  heartRates: payload[:hr_data],
222
- }, keep_id)
231
+ }.merge(decoded_steps), keep_id)
223
232
 
224
233
  wgs_points = Source::Keep.compute_wgs_points(payload, run_data)
225
234
  write_standardized_json(wgs_points, payload, run_data, keep_id)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitFit
4
- VERSION = '0.17.1'
4
+ VERSION = '0.17.3'
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.17.1
4
+ version: 0.17.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lax
@@ -257,6 +257,7 @@ files:
257
257
  - lib/git_fit/auth/strava.rb
258
258
  - lib/git_fit/cli.rb
259
259
  - lib/git_fit/cli/checkpointable.rb
260
+ - lib/git_fit/cli/elevation.rb
260
261
  - lib/git_fit/cli/export.rb
261
262
  - lib/git_fit/cli/geo_cli.rb
262
263
  - lib/git_fit/cli/gh_cli.rb
@@ -277,6 +278,7 @@ files:
277
278
  - lib/git_fit/dedup/matcher.rb
278
279
  - lib/git_fit/dedup/service.rb
279
280
  - lib/git_fit/dedup/trajectory.rb
281
+ - lib/git_fit/elevation/backfill.rb
280
282
  - lib/git_fit/export.rb
281
283
  - lib/git_fit/export/calculator.rb
282
284
  - lib/git_fit/export/csv.rb