Module: GitFit::Elevation::TerrainCoeff

Defined in:
lib/git_fit/elevation/terrain_coeff.rb,
lib/git_fit/elevation/terrain_lookup.rb

Defined Under Namespace

Modules: Lookup

Constant Summary collapse

MODEL_CACHE_DIR =
ENV.fetch('GIT_FIT_DEM_MODEL_DIR', 'data/cache/dem_fit')
BUNDLED_MODEL_DIR =
File.expand_path('model', __dir__)
BUNDLED_MODEL =
File.join(BUNDLED_MODEL_DIR, 'dem_model.msgpack')
MODEL_FILE =
'dem_model.msgpack'
GRID =
0.0003
DEM_SLICE_INTERVAL =

OpenTopography free tier allows 1 request/second — pacing below that guarantees a 429 on every cold-cache slice (observed in workouts CI).

1.05

Class Method Summary collapse

Class Method Details

.dem_spacing(gain_per_km:, range_per_km:, seg_density:) ⇒ Object



59
60
61
62
63
64
# File 'lib/git_fit/elevation/terrain_coeff.rb', line 59

def dem_spacing(gain_per_km:, range_per_km:, seg_density:)
  Lookup.nearest_neighbor_spacing(lookup, gain_per_km, range_per_km, seg_density)
rescue StandardError => e
  warn "TerrainCoeff: dem_spacing fallback to 181m (#{e.message})"
  181
end

.elevations_for(locations) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/git_fit/elevation/terrain_coeff.rb', line 66

def elevations_for(locations)
  uniq_vals = uniquify_locations(locations)
  elevs = {}
  last_api = nil
  uniq_vals.each_slice(100).with_index do |slice, bi|
    cached = DemCache.load_batch(slice)
    elevs.merge!(cached)
    missing = DemCache.missing_keys(slice, cached)
    next if missing.empty?

    # Pace API→API independently of interleaved cache-hit slices:
    # cache processing is free, only consecutive fetches must respect
    # the provider's 1 req/s limit.
    if last_api
      elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - last_api
      sleep(DEM_SLICE_INTERVAL - elapsed) if elapsed < DEM_SLICE_INTERVAL
    end
    missing_locations = missing.map { |lat, lng| [lat, lng] }
    batch = {}
    DemFetch.fetch_batch(missing_locations, bi, batch, method(:grid_key))
    elevs.merge!(batch)
    DemCache.save_batch(batch)
    last_api = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  end
  locations.map { |lat, lng| elevs[grid_key(lat, lng)] }
end

.find_modelObject



31
32
33
34
35
36
# File 'lib/git_fit/elevation/terrain_coeff.rb', line 31

def find_model
  cache_path = File.join(MODEL_CACHE_DIR, MODEL_FILE)
  return cache_path if File.exist?(cache_path)
  return BUNDLED_MODEL if File.exist?(BUNDLED_MODEL)
  raise "No #{MODEL_FILE} found in cache (#{MODEL_CACHE_DIR}) or bundled (#{BUNDLED_MODEL})"
end

.grid_key(lat, lng) ⇒ Object



99
100
101
# File 'lib/git_fit/elevation/terrain_coeff.rb', line 99

def grid_key(lat, lng)
  "#{(lat / GRID).round}/#{(lng / GRID).round}"
end

.hysteresis_threshold(m_val) ⇒ Object



55
56
57
# File 'lib/git_fit/elevation/terrain_coeff.rb', line 55

def hysteresis_threshold(m_val)
  Lookup.hysteresis_threshold(model['pav_h'], m_val)
end

.lookupObject



103
104
105
# File 'lib/git_fit/elevation/terrain_coeff.rb', line 103

def lookup
  model['lookup']
end

.modelObject



27
28
29
# File 'lib/git_fit/elevation/terrain_coeff.rb', line 27

def model
  @model ||= MessagePack.unpack(File.read(model_path))
end

.model_pathObject



23
24
25
# File 'lib/git_fit/elevation/terrain_coeff.rb', line 23

def model_path
  @model_path ||= find_model
end

.reset!Object



38
39
40
41
# File 'lib/git_fit/elevation/terrain_coeff.rb', line 38

def reset!
  @model_path = nil
  @model = nil
end

.terrain_m(gain_per_km:, range_per_km:, seg_density:) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/git_fit/elevation/terrain_coeff.rb', line 43

def terrain_m(gain_per_km:, range_per_km:, seg_density:)
  g = gain_per_km.to_f
  r = range_per_km.to_f
  s = seg_density.to_f
  norms = model['norms']
  g_n = g / norms[0]
  r_n = r / norms[1]
  s_n = s / norms[2]
  w = model['ridge_h']
  g_n * w[0] + r_n * w[1] + s_n * w[2]
end

.uniquify_locations(locations) ⇒ Object



93
94
95
96
97
# File 'lib/git_fit/elevation/terrain_coeff.rb', line 93

def uniquify_locations(locations)
  uniq = {}
  locations.each { |lat, lng| uniq[grid_key(lat, lng)] ||= [lat, lng] }
  uniq.values
end