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 +4 -4
- data/lib/git-fit.rb +2 -0
- data/lib/git_fit/cli/elevation.rb +30 -0
- data/lib/git_fit/cli.rb +3 -0
- data/lib/git_fit/elevation/backfill.rb +162 -0
- data/lib/git_fit/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e88e21cd60e366ac83ce9f0dac135a5532d6c8953dc9557873a39b55de07174
|
|
4
|
+
data.tar.gz: 38d23b0f68e6e15ecbe9413d3ac0992fdab84faaa7b2977c6b4284a1f1a223ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/git_fit/version.rb
CHANGED
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.
|
|
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
|