git-fit 0.21.1 → 0.21.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/dedup/field_elector.rb +13 -2
- data/lib/git_fit/elevation/correct.rb +9 -4
- data/lib/git_fit/export/calculator.rb +147 -10
- data/lib/git_fit/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f867a5ad738983c2d5e3a10d837b3f4c0f6de6bc303d1124fdec5fccf4a8796b
|
|
4
|
+
data.tar.gz: 50cefd5a1b88b2282396942ab3809c1a87f7ac6a6ac977c00cf77cf12a39ec04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4390df21b2c3a0cb8167c0d5b0deb5e9fc495391fe0993104f942199249e6520d7ea8923d96f80c23683331c8ddfdff1e3e5cb47e69add1c067185290fe4befd
|
|
7
|
+
data.tar.gz: 3bc09da8cf7493ba99b274fa2aecbbdee9843e5a2b83e8fb3d8e1ca8dfe3db4016978e240666ae13751f8fea09537b19f65fcbff7632ed4244f7b1a9aba5d56f
|
|
@@ -16,8 +16,8 @@ module GitFit
|
|
|
16
16
|
average_cadence: :non_null,
|
|
17
17
|
max_cadence: :non_null,
|
|
18
18
|
summary_polyline: :best_polyline,
|
|
19
|
-
elevation_gain: :
|
|
20
|
-
elevation_loss: :
|
|
19
|
+
elevation_gain: :priority,
|
|
20
|
+
elevation_loss: :priority,
|
|
21
21
|
elevation_min: :min,
|
|
22
22
|
elevation_max: :max,
|
|
23
23
|
steps: :median,
|
|
@@ -56,6 +56,8 @@ module GitFit
|
|
|
56
56
|
candidates.max_by { |v| decode_polyline_size(v) }
|
|
57
57
|
when :computed
|
|
58
58
|
nil
|
|
59
|
+
when :priority
|
|
60
|
+
priority_pick(values, sources)
|
|
59
61
|
when :best_name
|
|
60
62
|
best_name(values, sources)
|
|
61
63
|
when :earliest
|
|
@@ -76,6 +78,15 @@ module GitFit
|
|
|
76
78
|
polyline.size / 3
|
|
77
79
|
end
|
|
78
80
|
|
|
81
|
+
def priority_pick(values, sources)
|
|
82
|
+
pairs = values.zip(sources)
|
|
83
|
+
# keep only non-nil values with source priority
|
|
84
|
+
filtered = pairs.reject { |v, _| v.nil? }
|
|
85
|
+
return nil if filtered.empty?
|
|
86
|
+
|
|
87
|
+
filtered.min_by { |_, src| SOURCE_NAME_PRIORITY[src&.to_sym] || 99 }&.first
|
|
88
|
+
end
|
|
89
|
+
|
|
79
90
|
def best_name(values, sources)
|
|
80
91
|
pairs = values.zip(sources)
|
|
81
92
|
pairs.min_by { |_, src| SOURCE_NAME_PRIORITY[src&.to_sym] || 99 }&.first
|
|
@@ -81,16 +81,21 @@ module GitFit
|
|
|
81
81
|
l2 = JSON.parse(File.read(l2_path))
|
|
82
82
|
return nil unless l2.is_a?(Array)
|
|
83
83
|
|
|
84
|
+
# L2 std has two coordinate key styles: keep/2bulu/apple_health/xingzhe
|
|
85
|
+
# write latitude/longitude, FIT-derived sources write positionLat/
|
|
86
|
+
# positionLong (same convention as Dedup::Trajectory and export/gpx).
|
|
84
87
|
pts = l2.filter_map do |pt|
|
|
85
88
|
next unless pt.is_a?(Hash)
|
|
86
|
-
|
|
89
|
+
lat = pt['latitude'] || pt['positionLat']
|
|
90
|
+
lng = pt['longitude'] || pt['positionLong']
|
|
91
|
+
next unless lat && lng && pt['altitude']
|
|
87
92
|
|
|
88
|
-
pt
|
|
93
|
+
[lat.to_f, lng.to_f, pt['altitude'].to_f]
|
|
89
94
|
end
|
|
90
95
|
return nil if pts.size < 2
|
|
91
96
|
|
|
92
|
-
locations = pts.map { |
|
|
93
|
-
alts_raw = pts.map { |
|
|
97
|
+
locations = pts.map { |lat, lng, _alt| [lat, lng] }
|
|
98
|
+
alts_raw = pts.map { |_lat, _lng, alt| alt }
|
|
94
99
|
|
|
95
100
|
dem_alts = dem_corrected_altitudes(locations, alts_raw)
|
|
96
101
|
h = hysteresis_threshold(source, locations, alts_raw)
|
|
@@ -71,6 +71,8 @@ module GitFit
|
|
|
71
71
|
unique = rows.reject { |a| grouped_run_ids.include?(a[:run_id]) }
|
|
72
72
|
unique_count = unique.size + (@db.table_exists?(:dedup_groups) ? @db[:dedup_groups].count : 0)
|
|
73
73
|
|
|
74
|
+
uniq = compute_unique_totals(rows)
|
|
75
|
+
|
|
74
76
|
{
|
|
75
77
|
total_activities: rows.size,
|
|
76
78
|
unique_activities: unique_count,
|
|
@@ -79,6 +81,11 @@ module GitFit
|
|
|
79
81
|
total_elevation_gain_m: total_elev,
|
|
80
82
|
total_elevation_loss_m: total_loss,
|
|
81
83
|
total_steps: total_steps,
|
|
84
|
+
unique_total_distance_km: uniq[:distance_km],
|
|
85
|
+
unique_total_moving_time_h: uniq[:moving_time_h],
|
|
86
|
+
unique_total_elevation_gain_m: uniq[:elev_gain],
|
|
87
|
+
unique_total_elevation_loss_m: uniq[:elev_loss],
|
|
88
|
+
unique_total_steps: uniq[:steps],
|
|
82
89
|
avg_cadence: cadences.any? ? (cadences.sum / cadences.size).round(1) : nil,
|
|
83
90
|
avg_power: powers.any? ? (powers.sum / powers.size).round(1) : nil,
|
|
84
91
|
max_power: max_powers.any? ? max_powers.max.round(1) : nil,
|
|
@@ -90,6 +97,76 @@ module GitFit
|
|
|
90
97
|
}
|
|
91
98
|
end
|
|
92
99
|
|
|
100
|
+
def compute_unique_totals(rows)
|
|
101
|
+
return { distance_km: 0.0, moving_time_h: 0.0, elev_gain: 0.0, elev_loss: 0.0, steps: 0 } if rows.empty?
|
|
102
|
+
return dedup_aware_totals(rows) if dedup_tables_present?
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
distance_km: rows.sum { |r| r[:distance] || 0 } / 1000.0,
|
|
106
|
+
moving_time_h: rows.sum { |r| r[:moving_time] || 0 } / 3600.0,
|
|
107
|
+
elev_gain: rows.sum { |r| r[:elevation_gain_terrain] || r[:elevation_gain] || 0 },
|
|
108
|
+
elev_loss: rows.sum { |r| r[:elevation_loss_terrain] || r[:elevation_loss] || 0 },
|
|
109
|
+
steps: rows.sum { |r| r[:steps] || 0 },
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def dedup_tables_present?
|
|
114
|
+
@db.table_exists?(:dedup_groups) && @db.table_exists?(:dedup_group_members)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def dedup_aware_totals(rows)
|
|
118
|
+
run_id_to_row = rows.each_with_object({}) { |r, h| h[r[:run_id]] = r }
|
|
119
|
+
grouped_ids = @db[:dedup_group_members].select_map(:run_id)
|
|
120
|
+
unique_rows = rows.reject { |r| grouped_ids.include?(r[:run_id]) }
|
|
121
|
+
|
|
122
|
+
uniq_dist = unique_rows.sum { |r| r[:distance] || 0 }
|
|
123
|
+
uniq_time = unique_rows.sum { |r| r[:moving_time] || 0 }
|
|
124
|
+
uniq_gain = unique_rows.sum { |r| r[:elevation_gain_terrain] || r[:elevation_gain] || 0 }
|
|
125
|
+
uniq_loss = unique_rows.sum { |r| r[:elevation_loss_terrain] || r[:elevation_loss] || 0 }
|
|
126
|
+
uniq_steps = unique_rows.sum { |r| r[:steps] || 0 }
|
|
127
|
+
|
|
128
|
+
@db[:dedup_groups].select_map(:group_id).each do |gid|
|
|
129
|
+
member_ids = @db[:dedup_group_members].where(group_id: gid).select_map(:run_id)
|
|
130
|
+
members = member_ids.filter_map { |rid| run_id_to_row[rid] }
|
|
131
|
+
next if members.empty?
|
|
132
|
+
|
|
133
|
+
uniq_dist += median_value(members.map { |r| r[:distance] })
|
|
134
|
+
uniq_time += median_value(members.map { |r| r[:moving_time] })
|
|
135
|
+
uniq_gain += priority_elevation(members, :elevation_gain)
|
|
136
|
+
uniq_loss += priority_elevation(members, :elevation_loss)
|
|
137
|
+
uniq_steps += median_value(members.map { |r| r[:steps] })
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
distance_km: (uniq_dist / 1000.0).round(1),
|
|
142
|
+
moving_time_h: (uniq_time / 3600.0).round(1),
|
|
143
|
+
elev_gain: uniq_gain.round(0),
|
|
144
|
+
elev_loss: uniq_loss.round(0),
|
|
145
|
+
steps: uniq_steps,
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def median_value(values)
|
|
150
|
+
vals = values.compact
|
|
151
|
+
return 0 if vals.empty?
|
|
152
|
+
|
|
153
|
+
sorted = vals.sort
|
|
154
|
+
sorted[sorted.size / 2]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def priority_elevation(members, field)
|
|
158
|
+
terrain_field = :"#{field}_terrain"
|
|
159
|
+
pairs = members.map do |r|
|
|
160
|
+
v = r[terrain_field] || r[field]
|
|
161
|
+
[v, r[:source]&.to_sym]
|
|
162
|
+
end
|
|
163
|
+
filtered = pairs.reject { |v, _| v.nil? }
|
|
164
|
+
return 0 if filtered.empty?
|
|
165
|
+
|
|
166
|
+
pri = GitFit::Dedup::FieldElector::SOURCE_NAME_PRIORITY
|
|
167
|
+
filtered.min_by { |_, src| pri[src] || 99 }[0]
|
|
168
|
+
end
|
|
169
|
+
|
|
93
170
|
def compute_monthly(rows)
|
|
94
171
|
monthly = Hash.new do |h, k|
|
|
95
172
|
h[k] = { count: 0, distance_km: 0.0, moving_time_h: 0.0,
|
|
@@ -99,10 +176,19 @@ module GitFit
|
|
|
99
176
|
total_power: 0.0, power_count: 0,
|
|
100
177
|
max_power_peak: 0.0, total_max_power: 0.0, max_power_count: 0 }
|
|
101
178
|
end
|
|
179
|
+
@_monthly_rows = Hash.new { |h, k| h[k] = [] }
|
|
180
|
+
|
|
181
|
+
rows.each do |row|
|
|
182
|
+
accumulate_monthly_row(monthly, row)
|
|
183
|
+
next unless row[:start_date_local] || row[:start_date]
|
|
102
184
|
|
|
103
|
-
|
|
185
|
+
ym = (row[:start_date_local] || row[:start_date])[0..6]
|
|
186
|
+
@_monthly_rows[ym] << row
|
|
187
|
+
end
|
|
104
188
|
|
|
105
|
-
format_monthly(monthly.sort)
|
|
189
|
+
result = format_monthly(monthly.sort)
|
|
190
|
+
@_monthly_rows = nil
|
|
191
|
+
result
|
|
106
192
|
end
|
|
107
193
|
|
|
108
194
|
def accumulate_monthly_row(monthly, row)
|
|
@@ -134,11 +220,17 @@ module GitFit
|
|
|
134
220
|
end
|
|
135
221
|
|
|
136
222
|
def format_monthly(sorted)
|
|
223
|
+
# For dedup-aware per-month unique totals, compute via filtered rows
|
|
224
|
+
# Cache rows grouped by ym to reuse dedup_aware logic
|
|
225
|
+
# Need original rows; reconstruct from monthly accumulation is lossy,
|
|
226
|
+
# so compute via second pass if caller provides rows? Instead compute
|
|
227
|
+
# unique via dedup_aware if tables present, else mirror totals.
|
|
228
|
+
# We store original rows in thread-local via compute_monthly caller.
|
|
137
229
|
sorted.map do |ym, v|
|
|
138
230
|
pace = if v[:moving_time_h] > 0 && v[:distance_km] > 0
|
|
139
231
|
format_pace(v[:moving_time_h] * 3600 / v[:distance_km])
|
|
140
232
|
end
|
|
141
|
-
{
|
|
233
|
+
base = {
|
|
142
234
|
year: ym[0..3].to_i,
|
|
143
235
|
month: ym[5..6].to_i,
|
|
144
236
|
count: v[:count],
|
|
@@ -154,9 +246,37 @@ module GitFit
|
|
|
154
246
|
elevation_loss_m: v[:total_loss].round(0),
|
|
155
247
|
steps: v[:total_steps],
|
|
156
248
|
}
|
|
249
|
+
if defined?(@_monthly_rows)
|
|
250
|
+
month_rows = @_monthly_rows[ym] || []
|
|
251
|
+
uniq = if dedup_tables_present?
|
|
252
|
+
dedup_aware_totals(month_rows)
|
|
253
|
+
else
|
|
254
|
+
{
|
|
255
|
+
distance_km: v[:distance_km], moving_time_h: v[:moving_time_h],
|
|
256
|
+
elev_gain: v[:total_elev], elev_loss: v[:total_loss], steps: v[:total_steps]
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
base[:unique_distance_km] = uniq[:distance_km]
|
|
260
|
+
base[:unique_moving_time_h] = uniq[:moving_time_h]
|
|
261
|
+
base[:unique_elevation_gain_m] = uniq[:elev_gain].round(0)
|
|
262
|
+
base[:unique_elevation_loss_m] = uniq[:elev_loss].round(0)
|
|
263
|
+
base[:unique_steps] = uniq[:steps]
|
|
264
|
+
base[:unique_count] = dedup_tables_present? ? unique_count_for_rows(month_rows) : v[:count]
|
|
265
|
+
end
|
|
266
|
+
base
|
|
157
267
|
end
|
|
158
268
|
end
|
|
159
269
|
|
|
270
|
+
def unique_count_for_rows(rows)
|
|
271
|
+
return rows.size unless dedup_tables_present?
|
|
272
|
+
|
|
273
|
+
grouped = @db[:dedup_group_members].select_map(:run_id)
|
|
274
|
+
rows.count { |r| !grouped.include?(r[:run_id]) } + rows.map { |r|
|
|
275
|
+
gid = @db[:dedup_group_members].where(run_id: r[:run_id]).get(:group_id)
|
|
276
|
+
gid
|
|
277
|
+
}.compact.uniq.size
|
|
278
|
+
end
|
|
279
|
+
|
|
160
280
|
def compute_yearly(rows)
|
|
161
281
|
by_year = Hash.new { |h, k| h[k] = [] }
|
|
162
282
|
rows.each do |r|
|
|
@@ -176,13 +296,30 @@ module GitFit
|
|
|
176
296
|
|
|
177
297
|
year_max_powers = acts.map { |r| r[:max_power] }.compact
|
|
178
298
|
|
|
179
|
-
{ year: year, count: acts.size, distance_km: total_dist.round(1),
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
299
|
+
base = { year: year, count: acts.size, distance_km: total_dist.round(1),
|
|
300
|
+
moving_time_h: (total_time / 3600.0).round(1),
|
|
301
|
+
longest_km: longest.round(1), streak: streak,
|
|
302
|
+
elevation_gain_m: total_elev.round(0), elevation_loss_m: total_loss.round(0),
|
|
303
|
+
steps: total_steps,
|
|
304
|
+
max_power: year_max_powers.any? ? year_max_powers.max.round(0) : nil,
|
|
305
|
+
avg_max_power: year_max_powers.any? ? (year_max_powers.sum / year_max_powers.size).round(0) : nil }
|
|
306
|
+
if dedup_tables_present?
|
|
307
|
+
uniq = dedup_aware_totals(acts)
|
|
308
|
+
base[:unique_distance_km] = uniq[:distance_km]
|
|
309
|
+
base[:unique_moving_time_h] = uniq[:moving_time_h]
|
|
310
|
+
base[:unique_elevation_gain_m] = uniq[:elev_gain].round(0)
|
|
311
|
+
base[:unique_elevation_loss_m] = uniq[:elev_loss].round(0)
|
|
312
|
+
base[:unique_steps] = uniq[:steps]
|
|
313
|
+
base[:unique_count] = unique_count_for_rows(acts)
|
|
314
|
+
else
|
|
315
|
+
base[:unique_distance_km] = total_dist.round(1)
|
|
316
|
+
base[:unique_moving_time_h] = (total_time / 3600.0).round(1)
|
|
317
|
+
base[:unique_elevation_gain_m] = total_elev.round(0)
|
|
318
|
+
base[:unique_elevation_loss_m] = total_loss.round(0)
|
|
319
|
+
base[:unique_steps] = total_steps
|
|
320
|
+
base[:unique_count] = acts.size
|
|
321
|
+
end
|
|
322
|
+
base
|
|
186
323
|
end
|
|
187
324
|
end
|
|
188
325
|
|
data/lib/git_fit/version.rb
CHANGED