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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30f139ea0a7c21e9a99189ac7541c8b80c35377f40742823843dbcd5b2ce604a
4
- data.tar.gz: b554587d97956af6e3a532ab67b6eca755377005f82eec38c500414d7bd1c4c4
3
+ metadata.gz: f867a5ad738983c2d5e3a10d837b3f4c0f6de6bc303d1124fdec5fccf4a8796b
4
+ data.tar.gz: 50cefd5a1b88b2282396942ab3809c1a87f7ac6a6ac977c00cf77cf12a39ec04
5
5
  SHA512:
6
- metadata.gz: 5b23770d805a5c42fdbe3b4322e3557bd4938033e2e7e3094e934354d16fbc8c6bb7d7ed028ae5ffe9684b785d7e5fdca560e17480fd020713c1b96302795bb2
7
- data.tar.gz: 32dde756e5dc46d3f859a992779187ccf2e478e9de05b681f79184ba2c35838822f29586f9d4e9c79d26e1d8d767c1771d4ed80172fa6fd21ec9ff7851345720
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: :median,
20
- elevation_loss: :median,
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
- next unless pt['latitude'] && pt['longitude'] && pt['altitude']
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 { |pt| [pt['latitude'].to_f, pt['longitude'].to_f] }
93
- alts_raw = pts.map { |pt| pt['altitude'].to_f }
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
- rows.each { |row| accumulate_monthly_row(monthly, row) }
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
- moving_time_h: (total_time / 3600.0).round(1),
181
- longest_km: longest.round(1), streak: streak,
182
- elevation_gain_m: total_elev.round(0), elevation_loss_m: total_loss.round(0),
183
- steps: total_steps,
184
- max_power: year_max_powers.any? ? year_max_powers.max.round(0) : nil,
185
- avg_max_power: year_max_powers.any? ? (year_max_powers.sum / year_max_powers.size).round(0) : nil }
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitFit
4
- VERSION = '0.21.1'
4
+ VERSION = '0.21.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.21.1
4
+ version: 0.21.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lax