git-fit 0.17.2 → 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: 472d41138e7c8607cbc63dbff14db13079b6b824c342ec03661696c77f08f8f9
4
- data.tar.gz: 291394e38307334eaedd49106b5c6c057331d972ccf4fa2619ce68904ca89a86
3
+ metadata.gz: 8e88e21cd60e366ac83ce9f0dac135a5532d6c8953dc9557873a39b55de07174
4
+ data.tar.gz: 38d23b0f68e6e15ecbe9413d3ac0992fdab84faaa7b2977c6b4284a1f1a223ca
5
5
  SHA512:
6
- metadata.gz: 2f7e4971cc16db4f139ae122a65fe19842bfd1e999362bfc57294e4fa2b41a3adbd228824e9126b13a13fc2499643dec07ad195e1d6050618137a54f12b34a48
7
- data.tar.gz: 713ff0444661494cb22469f5f3535de7b20f89229ec82b5db07398654b6cd2622267db18a0d1841517f796a351bbc1d29800d79b53469009454532dba855b61e
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitFit
4
- VERSION = '0.17.2'
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.2
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