postrunner 0.0.11 → 0.1.0
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/README.md +5 -2
 - data/Rakefile +18 -2
 - data/lib/postrunner/ActivitiesDB.rb +1 -22
 - data/lib/postrunner/Activity.rb +21 -0
 - data/lib/postrunner/ActivityLink.rb +1 -1
 - data/lib/postrunner/ActivityListView.rb +10 -9
 - data/lib/postrunner/ActivitySummary.rb +42 -15
 - data/lib/postrunner/ActivityView.rb +10 -8
 - data/lib/postrunner/ChartView.rb +238 -80
 - data/lib/postrunner/DataSources.rb +1 -14
 - data/lib/postrunner/DeviceList.rb +18 -10
 - data/lib/postrunner/DirUtils.rb +33 -0
 - data/lib/postrunner/EventList.rb +149 -0
 - data/lib/postrunner/FFS_Activity.rb +297 -0
 - data/lib/postrunner/FFS_Device.rb +129 -0
 - data/lib/postrunner/FitFileStore.rb +372 -0
 - data/lib/postrunner/HRV_Analyzer.rb +178 -0
 - data/lib/postrunner/LinearPredictor.rb +46 -0
 - data/lib/postrunner/Main.rb +135 -33
 - data/lib/postrunner/Percentiles.rb +45 -0
 - data/lib/postrunner/PersonalRecords.rb +203 -114
 - data/lib/postrunner/RecordListPageView.rb +6 -6
 - data/lib/postrunner/UserProfileView.rb +4 -0
 - data/lib/postrunner/version.rb +1 -1
 - data/misc/postrunner/trackview.js +99 -0
 - data/postrunner.gemspec +5 -5
 - data/spec/ActivitySummary_spec.rb +15 -4
 - data/spec/FitFileStore_spec.rb +133 -0
 - data/spec/FlexiTable_spec.rb +1 -1
 - data/spec/PersonalRecords_spec.rb +206 -0
 - data/spec/PostRunner_spec.rb +64 -60
 - data/spec/View_spec.rb +1 -1
 - data/spec/spec_helper.rb +76 -2
 - metadata +42 -28
 
| 
         @@ -0,0 +1,178 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #!/usr/bin/env ruby -w
         
     | 
| 
      
 2 
     | 
    
         
            +
            # encoding: UTF-8
         
     | 
| 
      
 3 
     | 
    
         
            +
            #
         
     | 
| 
      
 4 
     | 
    
         
            +
            # = HRV_Analyzer.rb -- PostRunner - Manage the data from your Garmin sport devices.
         
     | 
| 
      
 5 
     | 
    
         
            +
            #
         
     | 
| 
      
 6 
     | 
    
         
            +
            # Copyright (c) 2015 by Chris Schlaeger <cs@taskjuggler.org>
         
     | 
| 
      
 7 
     | 
    
         
            +
            #
         
     | 
| 
      
 8 
     | 
    
         
            +
            # This program is free software; you can redistribute it and/or modify
         
     | 
| 
      
 9 
     | 
    
         
            +
            # it under the terms of version 2 of the GNU General Public License as
         
     | 
| 
      
 10 
     | 
    
         
            +
            # published by the Free Software Foundation.
         
     | 
| 
      
 11 
     | 
    
         
            +
            #
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            require 'postrunner/LinearPredictor'
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            module PostRunner
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
              # This class analyzes the heart rate variablity based on the R-R intervals
         
     | 
| 
      
 18 
     | 
    
         
            +
              # in the given FIT file. It can compute RMSSD and a HRV score if the data
         
     | 
| 
      
 19 
     | 
    
         
            +
              # quality is good enough.
         
     | 
| 
      
 20 
     | 
    
         
            +
              class HRV_Analyzer
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                attr_reader :rr_intervals, :timestamps
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                # Create a new HRV_Analyzer object.
         
     | 
| 
      
 25 
     | 
    
         
            +
                # @param fit_file [Fit4Ruby::Activity] FIT file to analyze.
         
     | 
| 
      
 26 
     | 
    
         
            +
                def initialize(fit_file)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  @fit_file = fit_file
         
     | 
| 
      
 28 
     | 
    
         
            +
                  collect_rr_intervals
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                # The method can be used to check if we have valid HRV data. The FIT file
         
     | 
| 
      
 32 
     | 
    
         
            +
                # must have HRV data and the measurement duration must be at least 30
         
     | 
| 
      
 33 
     | 
    
         
            +
                # seconds.
         
     | 
| 
      
 34 
     | 
    
         
            +
                def has_hrv_data?
         
     | 
| 
      
 35 
     | 
    
         
            +
                  !@fit_file.hrv.empty? && total_duration > 30.0
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                # Return the total duration of all measured intervals in seconds.
         
     | 
| 
      
 39 
     | 
    
         
            +
                def total_duration
         
     | 
| 
      
 40 
     | 
    
         
            +
                  @timestamps[-1]
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                # Compute the root mean square of successive differences.
         
     | 
| 
      
 44 
     | 
    
         
            +
                # @param start_time [Float] Determines at what time mark (in seconds) the
         
     | 
| 
      
 45 
     | 
    
         
            +
                #        computation should start.
         
     | 
| 
      
 46 
     | 
    
         
            +
                # @param duration [Float] The duration of the total inteval in seconds to
         
     | 
| 
      
 47 
     | 
    
         
            +
                #        be considered for the computation. This value should be larger
         
     | 
| 
      
 48 
     | 
    
         
            +
                #        then 30 seconds to produce meaningful values.
         
     | 
| 
      
 49 
     | 
    
         
            +
                def rmssd(start_time = 0.0, duration = nil)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  # Find the start index based on the requested interval start time.
         
     | 
| 
      
 51 
     | 
    
         
            +
                  start_idx = 0
         
     | 
| 
      
 52 
     | 
    
         
            +
                  @timestamps.each do |ts|
         
     | 
| 
      
 53 
     | 
    
         
            +
                    break if ts >= start_time
         
     | 
| 
      
 54 
     | 
    
         
            +
                    start_idx += 1
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
                  # Find the end index based on the requested interval duration.
         
     | 
| 
      
 57 
     | 
    
         
            +
                  if duration
         
     | 
| 
      
 58 
     | 
    
         
            +
                    end_time = start_time + duration
         
     | 
| 
      
 59 
     | 
    
         
            +
                    end_idx = start_idx
         
     | 
| 
      
 60 
     | 
    
         
            +
                    while end_idx < (@timestamps.length - 1) &&
         
     | 
| 
      
 61 
     | 
    
         
            +
                          @timestamps[end_idx] < end_time
         
     | 
| 
      
 62 
     | 
    
         
            +
                      end_idx += 1
         
     | 
| 
      
 63 
     | 
    
         
            +
                    end
         
     | 
| 
      
 64 
     | 
    
         
            +
                  else
         
     | 
| 
      
 65 
     | 
    
         
            +
                    end_idx = -1
         
     | 
| 
      
 66 
     | 
    
         
            +
                  end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                  last_i = nil
         
     | 
| 
      
 69 
     | 
    
         
            +
                  sum = 0.0
         
     | 
| 
      
 70 
     | 
    
         
            +
                  cnt = 0
         
     | 
| 
      
 71 
     | 
    
         
            +
                  @rr_intervals[start_idx..end_idx].each do |i|
         
     | 
| 
      
 72 
     | 
    
         
            +
                    if i && last_i
         
     | 
| 
      
 73 
     | 
    
         
            +
                      sum += (last_i - i) ** 2.0
         
     | 
| 
      
 74 
     | 
    
         
            +
                      cnt += 1
         
     | 
| 
      
 75 
     | 
    
         
            +
                    end
         
     | 
| 
      
 76 
     | 
    
         
            +
                    last_i = i
         
     | 
| 
      
 77 
     | 
    
         
            +
                  end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                  Math.sqrt(sum / cnt)
         
     | 
| 
      
 80 
     | 
    
         
            +
                end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                # The RMSSD value is not very easy to memorize. Alternatively, we can
         
     | 
| 
      
 83 
     | 
    
         
            +
                # multiply the natural logarithm of RMSSD by -20. This usually results in
         
     | 
| 
      
 84 
     | 
    
         
            +
                # values between 1.0 (for untrained) and 100.0 (for higly trained)
         
     | 
| 
      
 85 
     | 
    
         
            +
                # athletes. Values larger than 100.0 are rare but possible.
         
     | 
| 
      
 86 
     | 
    
         
            +
                # @param start_time [Float] Determines at what time mark (in seconds) the
         
     | 
| 
      
 87 
     | 
    
         
            +
                #        computation should start.
         
     | 
| 
      
 88 
     | 
    
         
            +
                # @param duration [Float] The duration of the total inteval in seconds to
         
     | 
| 
      
 89 
     | 
    
         
            +
                #        be considered for the computation. This value should be larger
         
     | 
| 
      
 90 
     | 
    
         
            +
                #        then 30 seconds to produce meaningful values.
         
     | 
| 
      
 91 
     | 
    
         
            +
                def lnrmssdx20(start_time = 0.0, duration = nil)
         
     | 
| 
      
 92 
     | 
    
         
            +
                  -20.0 * Math.log(rmssd(start_time, duration))
         
     | 
| 
      
 93 
     | 
    
         
            +
                end
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                # This method is similar to lnrmssdx20 but it tries to search the data for
         
     | 
| 
      
 96 
     | 
    
         
            +
                # the best time period to compute the lnrmssdx20 value from.
         
     | 
| 
      
 97 
     | 
    
         
            +
                def lnrmssdx20_1sigma
         
     | 
| 
      
 98 
     | 
    
         
            +
                  # Create a new Array that consists of rr_intervals and timestamps
         
     | 
| 
      
 99 
     | 
    
         
            +
                  # tuples.
         
     | 
| 
      
 100 
     | 
    
         
            +
                  set = []
         
     | 
| 
      
 101 
     | 
    
         
            +
                  0.upto(@rr_intervals.length - 1) do |i|
         
     | 
| 
      
 102 
     | 
    
         
            +
                    set << [ @rr_intervals[i] ? @rr_intervals[i] : 0.0, @timestamps[i] ]
         
     | 
| 
      
 103 
     | 
    
         
            +
                  end
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                  percentiles = Percentiles.new(set)
         
     | 
| 
      
 106 
     | 
    
         
            +
                  # Compile a list of all tuples with rr_intervals that are outside of the
         
     | 
| 
      
 107 
     | 
    
         
            +
                  # PT84 (aka +1sigma range. Sort the list by time.
         
     | 
| 
      
 108 
     | 
    
         
            +
                  not_1sigma = percentiles.not_tp_x(84.13).sort { |e1, e2| e1[1] <=> e2[1] }
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                  # Then find the largest time gap in that list. So all the values in that
         
     | 
| 
      
 111 
     | 
    
         
            +
                  # gap are within TP84.
         
     | 
| 
      
 112 
     | 
    
         
            +
                  gap_start = gap_end = 0
         
     | 
| 
      
 113 
     | 
    
         
            +
                  last = nil
         
     | 
| 
      
 114 
     | 
    
         
            +
                  not_1sigma.each do |e|
         
     | 
| 
      
 115 
     | 
    
         
            +
                    if last
         
     | 
| 
      
 116 
     | 
    
         
            +
                      if (e[1] - last) > (gap_end - gap_start)
         
     | 
| 
      
 117 
     | 
    
         
            +
                        gap_start = last
         
     | 
| 
      
 118 
     | 
    
         
            +
                        gap_end = e[1]
         
     | 
| 
      
 119 
     | 
    
         
            +
                      end
         
     | 
| 
      
 120 
     | 
    
         
            +
                    end
         
     | 
| 
      
 121 
     | 
    
         
            +
                    last = e[1]
         
     | 
| 
      
 122 
     | 
    
         
            +
                  end
         
     | 
| 
      
 123 
     | 
    
         
            +
                  # That gap should be at least 30 seconds long. Otherwise we'll just use
         
     | 
| 
      
 124 
     | 
    
         
            +
                  # all the values.
         
     | 
| 
      
 125 
     | 
    
         
            +
                  return lnrmssdx20 if gap_end - gap_start < 30
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                  lnrmssdx20(gap_start, gap_end - gap_start)
         
     | 
| 
      
 128 
     | 
    
         
            +
                end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                private
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
                def collect_rr_intervals
         
     | 
| 
      
 133 
     | 
    
         
            +
                  # The rr_intervals Array stores the beat-to-beat time intervals (R-R).
         
     | 
| 
      
 134 
     | 
    
         
            +
                  # If one or move beats have been skipped during measurement, a nil value
         
     | 
| 
      
 135 
     | 
    
         
            +
                  # is inserted.
         
     | 
| 
      
 136 
     | 
    
         
            +
                  @rr_intervals = []
         
     | 
| 
      
 137 
     | 
    
         
            +
                  # The timestamps Array stores the relative (to start of sequence) time
         
     | 
| 
      
 138 
     | 
    
         
            +
                  # for each interval in the rr_intervals Array.
         
     | 
| 
      
 139 
     | 
    
         
            +
                  @timestamps = []
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
                  # Each Fit4Ruby::HRV object has an Array called 'time' that contains up
         
     | 
| 
      
 142 
     | 
    
         
            +
                  # to 5 R-R interval durations. If less than 5 are present, they are
         
     | 
| 
      
 143 
     | 
    
         
            +
                  # filled with nil.
         
     | 
| 
      
 144 
     | 
    
         
            +
                  raw_rr_intervals = []
         
     | 
| 
      
 145 
     | 
    
         
            +
                  @fit_file.hrv.each do |hrv|
         
     | 
| 
      
 146 
     | 
    
         
            +
                    raw_rr_intervals += hrv.time.compact
         
     | 
| 
      
 147 
     | 
    
         
            +
                  end
         
     | 
| 
      
 148 
     | 
    
         
            +
                  return if raw_rr_intervals.empty?
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                  window = 20
         
     | 
| 
      
 151 
     | 
    
         
            +
                  intro_mean = raw_rr_intervals[0..4 * window].reduce(:+) / (4 * window)
         
     | 
| 
      
 152 
     | 
    
         
            +
                  predictor = LinearPredictor.new(window, intro_mean)
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
                  # The timer accumulates the interval durations.
         
     | 
| 
      
 155 
     | 
    
         
            +
                  timer = 0.0
         
     | 
| 
      
 156 
     | 
    
         
            +
                  raw_rr_intervals.each do |dt|
         
     | 
| 
      
 157 
     | 
    
         
            +
                    timer += dt
         
     | 
| 
      
 158 
     | 
    
         
            +
                    @timestamps << timer
         
     | 
| 
      
 159 
     | 
    
         
            +
             
     | 
| 
      
 160 
     | 
    
         
            +
                    # Sometimes the hrv data is missing one or more beats. The next
         
     | 
| 
      
 161 
     | 
    
         
            +
                    # detected beat is than listed with the time interval since the last
         
     | 
| 
      
 162 
     | 
    
         
            +
                    # detected beat. We try to detect these skipped beats by looking for
         
     | 
| 
      
 163 
     | 
    
         
            +
                    # time intervals that are 1.5 or more times larger than the predicted
         
     | 
| 
      
 164 
     | 
    
         
            +
                    # value for this interval.
         
     | 
| 
      
 165 
     | 
    
         
            +
                    if (next_dt = predictor.predict) && dt > 1.5 * next_dt
         
     | 
| 
      
 166 
     | 
    
         
            +
                      @rr_intervals << nil
         
     | 
| 
      
 167 
     | 
    
         
            +
                    else
         
     | 
| 
      
 168 
     | 
    
         
            +
                      @rr_intervals << dt
         
     | 
| 
      
 169 
     | 
    
         
            +
                      # Feed the value into the predictor.
         
     | 
| 
      
 170 
     | 
    
         
            +
                      predictor.insert(dt)
         
     | 
| 
      
 171 
     | 
    
         
            +
                    end
         
     | 
| 
      
 172 
     | 
    
         
            +
                  end
         
     | 
| 
      
 173 
     | 
    
         
            +
                end
         
     | 
| 
      
 174 
     | 
    
         
            +
             
     | 
| 
      
 175 
     | 
    
         
            +
              end
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
            end
         
     | 
| 
      
 178 
     | 
    
         
            +
             
     | 
| 
         @@ -0,0 +1,46 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #!/usr/bin/env ruby -w
         
     | 
| 
      
 2 
     | 
    
         
            +
            # encoding: UTF-8
         
     | 
| 
      
 3 
     | 
    
         
            +
            #
         
     | 
| 
      
 4 
     | 
    
         
            +
            # = LinearPredictor.rb -- PostRunner - Manage the data from your Garmin sport devices.
         
     | 
| 
      
 5 
     | 
    
         
            +
            #
         
     | 
| 
      
 6 
     | 
    
         
            +
            # Copyright (c) 2015 by Chris Schlaeger <cs@taskjuggler.org>
         
     | 
| 
      
 7 
     | 
    
         
            +
            #
         
     | 
| 
      
 8 
     | 
    
         
            +
            # This program is free software; you can redistribute it and/or modify
         
     | 
| 
      
 9 
     | 
    
         
            +
            # it under the terms of version 2 of the GNU General Public License as
         
     | 
| 
      
 10 
     | 
    
         
            +
            # published by the Free Software Foundation.
         
     | 
| 
      
 11 
     | 
    
         
            +
            #
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            module PostRunner
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              # For now we use a trivial adaptive linear predictor that just uses the
         
     | 
| 
      
 16 
     | 
    
         
            +
              # average of past values to predict the next value.
         
     | 
| 
      
 17 
     | 
    
         
            +
              class LinearPredictor
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                # Create a new LinearPredictor object.
         
     | 
| 
      
 20 
     | 
    
         
            +
                # @param n [Fixnum] The number of coefficients the predictor should use.
         
     | 
| 
      
 21 
     | 
    
         
            +
                def initialize(n, default = nil)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  @values = Array.new(n, default)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  @size = n
         
     | 
| 
      
 24 
     | 
    
         
            +
                  @next = nil
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                # Tell the predictor about the actual next value.
         
     | 
| 
      
 28 
     | 
    
         
            +
                # @param value [Float] next value
         
     | 
| 
      
 29 
     | 
    
         
            +
                def insert(value)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @values << value
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  if @values.length > @size
         
     | 
| 
      
 33 
     | 
    
         
            +
                    @values.shift
         
     | 
| 
      
 34 
     | 
    
         
            +
                    @next = @values.reduce(:+) / @size
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                # @return [Float] The predicted value of the next sample.
         
     | 
| 
      
 39 
     | 
    
         
            +
                def predict
         
     | 
| 
      
 40 
     | 
    
         
            +
                  @next
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
            end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
    
        data/lib/postrunner/Main.rb
    CHANGED
    
    | 
         @@ -3,7 +3,7 @@ 
     | 
|
| 
       3 
3 
     | 
    
         
             
            #
         
     | 
| 
       4 
4 
     | 
    
         
             
            # = Main.rb -- PostRunner - Manage the data from your Garmin sport devices.
         
     | 
| 
       5 
5 
     | 
    
         
             
            #
         
     | 
| 
       6 
     | 
    
         
            -
            # Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
         
     | 
| 
      
 6 
     | 
    
         
            +
            # Copyright (c) 2014, 2015, 2016 by Chris Schlaeger <cs@taskjuggler.org>
         
     | 
| 
       7 
7 
     | 
    
         
             
            #
         
     | 
| 
       8 
8 
     | 
    
         
             
            # This program is free software; you can redistribute it and/or modify
         
     | 
| 
       9 
9 
     | 
    
         
             
            # it under the terms of version 2 of the GNU General Public License as
         
     | 
| 
         @@ -16,7 +16,10 @@ require 'perobs' 
     | 
|
| 
       16 
16 
     | 
    
         | 
| 
       17 
17 
     | 
    
         
             
            require 'postrunner/version'
         
     | 
| 
       18 
18 
     | 
    
         
             
            require 'postrunner/Log'
         
     | 
| 
      
 19 
     | 
    
         
            +
            require 'postrunner/DirUtils'
         
     | 
| 
       19 
20 
     | 
    
         
             
            require 'postrunner/RuntimeConfig'
         
     | 
| 
      
 21 
     | 
    
         
            +
            require 'postrunner/FitFileStore'
         
     | 
| 
      
 22 
     | 
    
         
            +
            require 'postrunner/PersonalRecords'
         
     | 
| 
       20 
23 
     | 
    
         
             
            require 'postrunner/ActivitiesDB'
         
     | 
| 
       21 
24 
     | 
    
         
             
            require 'postrunner/MonitoringDB'
         
     | 
| 
       22 
25 
     | 
    
         
             
            require 'postrunner/EPO_Downloader'
         
     | 
| 
         @@ -25,20 +28,40 @@ module PostRunner 
     | 
|
| 
       25 
28 
     | 
    
         | 
| 
       26 
29 
     | 
    
         
             
              class Main
         
     | 
| 
       27 
30 
     | 
    
         | 
| 
      
 31 
     | 
    
         
            +
                include DirUtils
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
       28 
33 
     | 
    
         
             
                def initialize(args)
         
     | 
| 
       29 
34 
     | 
    
         
             
                  @filter = nil
         
     | 
| 
       30 
35 
     | 
    
         
             
                  @name = nil
         
     | 
| 
      
 36 
     | 
    
         
            +
                  @force = false
         
     | 
| 
       31 
37 
     | 
    
         
             
                  @attribute = nil
         
     | 
| 
       32 
38 
     | 
    
         
             
                  @value = nil
         
     | 
| 
       33 
     | 
    
         
            -
                  @activities = nil
         
     | 
| 
       34 
     | 
    
         
            -
                  @monitoring = nil
         
     | 
| 
       35 
39 
     | 
    
         
             
                  @db_dir = File.join(ENV['HOME'], '.postrunner')
         
     | 
| 
       36 
40 
     | 
    
         | 
| 
       37 
41 
     | 
    
         
             
                  return if (args = parse_options(args)).nil?
         
     | 
| 
       38 
42 
     | 
    
         | 
| 
       39 
     | 
    
         
            -
                  @ 
     | 
| 
      
 43 
     | 
    
         
            +
                  create_directory(@db_dir, 'PostRunner data')
         
     | 
| 
       40 
44 
     | 
    
         
             
                  @db = PEROBS::Store.new(File.join(@db_dir, 'database'))
         
     | 
| 
      
 45 
     | 
    
         
            +
                  # Create a hash to store configuration data in the store unless it
         
     | 
| 
      
 46 
     | 
    
         
            +
                  # exists already.
         
     | 
| 
      
 47 
     | 
    
         
            +
                  cfg = (@db['config'] ||= @db.new(PEROBS::Hash))
         
     | 
| 
      
 48 
     | 
    
         
            +
                  cfg['unit_system'] ||= :metric
         
     | 
| 
      
 49 
     | 
    
         
            +
                  cfg['version'] ||= VERSION
         
     | 
| 
      
 50 
     | 
    
         
            +
                  # We always override the data_dir as the user might have moved the data
         
     | 
| 
      
 51 
     | 
    
         
            +
                  # directory. The only reason we store it in the DB is to have it
         
     | 
| 
      
 52 
     | 
    
         
            +
                  # available throught the application.
         
     | 
| 
      
 53 
     | 
    
         
            +
                  cfg['data_dir'] = @db_dir
         
     | 
| 
      
 54 
     | 
    
         
            +
                  # Always update html_dir setting so that the DB directory can be moved
         
     | 
| 
      
 55 
     | 
    
         
            +
                  # around by the user.
         
     | 
| 
      
 56 
     | 
    
         
            +
                  cfg['html_dir'] = File.join(@db_dir, 'html')
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                  setup_directories
         
     | 
| 
      
 59 
     | 
    
         
            +
                  if (errors = @db.check) != 0
         
     | 
| 
      
 60 
     | 
    
         
            +
                    Log.fatal "Postrunner database is corrupted: #{errors} errors found"
         
     | 
| 
      
 61 
     | 
    
         
            +
                  end
         
     | 
| 
       41 
62 
     | 
    
         
             
                  execute_command(args)
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                  @db.sync
         
     | 
| 
       42 
65 
     | 
    
         
             
                end
         
     | 
| 
       43 
66 
     | 
    
         | 
| 
       44 
67 
     | 
    
         
             
                private
         
     | 
| 
         @@ -81,6 +104,11 @@ EOT 
     | 
|
| 
       81 
104 
     | 
    
         
             
                      @filter = Fit4Ruby::FitFilter.new unless @filter
         
     | 
| 
       82 
105 
     | 
    
         
             
                      @filter.ignore_undef = true
         
     | 
| 
       83 
106 
     | 
    
         
             
                    end
         
     | 
| 
      
 107 
     | 
    
         
            +
                    opts.on('--force',
         
     | 
| 
      
 108 
     | 
    
         
            +
                            'Import files even if they have been deleted from the ' +
         
     | 
| 
      
 109 
     | 
    
         
            +
                            'database before.') do
         
     | 
| 
      
 110 
     | 
    
         
            +
                      @force = true
         
     | 
| 
      
 111 
     | 
    
         
            +
                    end
         
     | 
| 
       84 
112 
     | 
    
         | 
| 
       85 
113 
     | 
    
         
             
                    opts.separator ""
         
     | 
| 
       86 
114 
     | 
    
         
             
                    opts.separator "Options for the 'import' command:"
         
     | 
| 
         @@ -104,7 +132,7 @@ EOT 
     | 
|
| 
       104 
132 
     | 
    
         
             
                      return nil
         
     | 
| 
       105 
133 
     | 
    
         
             
                    end
         
     | 
| 
       106 
134 
     | 
    
         
             
                    opts.on('--version', 'Show version number') do
         
     | 
| 
       107 
     | 
    
         
            -
                       
     | 
| 
      
 135 
     | 
    
         
            +
                      puts VERSION
         
     | 
| 
       108 
136 
     | 
    
         
             
                      return nil
         
     | 
| 
       109 
137 
     | 
    
         
             
                    end
         
     | 
| 
       110 
138 
     | 
    
         | 
| 
         @@ -119,6 +147,9 @@ check [ <fit file> | <ref> ... ] 
     | 
|
| 
       119 
147 
     | 
    
         
             
            dump <fit file> | <ref>
         
     | 
| 
       120 
148 
     | 
    
         
             
                       Dump the content of the FIT file.
         
     | 
| 
       121 
149 
     | 
    
         | 
| 
      
 150 
     | 
    
         
            +
            events [ <ref> ]
         
     | 
| 
      
 151 
     | 
    
         
            +
                       List all the events of the specified activies.
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
       122 
153 
     | 
    
         
             
            import [ <fit file> | <directory> ]
         
     | 
| 
       123 
154 
     | 
    
         
             
                       Import the provided FIT file(s) into the postrunner database. If no
         
     | 
| 
       124 
155 
     | 
    
         
             
                       file or directory is provided, the directory that was used for the
         
     | 
| 
         @@ -187,16 +218,50 @@ EOT 
     | 
|
| 
       187 
218 
     | 
    
         
             
                  end
         
     | 
| 
       188 
219 
     | 
    
         
             
                end
         
     | 
| 
       189 
220 
     | 
    
         | 
| 
      
 221 
     | 
    
         
            +
                def setup_directories
         
     | 
| 
      
 222 
     | 
    
         
            +
                  create_directory(@db['config']['html_dir'], 'HTML output')
         
     | 
| 
      
 223 
     | 
    
         
            +
             
     | 
| 
      
 224 
     | 
    
         
            +
                  %w( icons jquery flot openlayers postrunner ).each do |dir|
         
     | 
| 
      
 225 
     | 
    
         
            +
                    # This file should be in lib/postrunner. The 'misc' directory should be
         
     | 
| 
      
 226 
     | 
    
         
            +
                    # found in '../../misc'.
         
     | 
| 
      
 227 
     | 
    
         
            +
                    misc_dir = File.realpath(File.join(File.dirname(__FILE__),
         
     | 
| 
      
 228 
     | 
    
         
            +
                                                       '..', '..', 'misc'))
         
     | 
| 
      
 229 
     | 
    
         
            +
                    unless Dir.exists?(misc_dir)
         
     | 
| 
      
 230 
     | 
    
         
            +
                      Log.fatal "Cannot find 'misc' directory under '#{misc_dir}': #{$!}"
         
     | 
| 
      
 231 
     | 
    
         
            +
                    end
         
     | 
| 
      
 232 
     | 
    
         
            +
                    src_dir = File.join(misc_dir, dir)
         
     | 
| 
      
 233 
     | 
    
         
            +
                    unless Dir.exists?(src_dir)
         
     | 
| 
      
 234 
     | 
    
         
            +
                      Log.fatal "Cannot find '#{src_dir}': #{$!}"
         
     | 
| 
      
 235 
     | 
    
         
            +
                    end
         
     | 
| 
      
 236 
     | 
    
         
            +
                    dst_dir = @db['config']['html_dir']
         
     | 
| 
      
 237 
     | 
    
         
            +
             
     | 
| 
      
 238 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 239 
     | 
    
         
            +
                      FileUtils.cp_r(src_dir, dst_dir)
         
     | 
| 
      
 240 
     | 
    
         
            +
                    rescue IOError
         
     | 
| 
      
 241 
     | 
    
         
            +
                      Log.fatal "Cannot copy auxilliary data directory '#{dst_dir}': #{$!}"
         
     | 
| 
      
 242 
     | 
    
         
            +
                    end
         
     | 
| 
      
 243 
     | 
    
         
            +
                  end
         
     | 
| 
      
 244 
     | 
    
         
            +
                end
         
     | 
| 
      
 245 
     | 
    
         
            +
             
     | 
| 
       190 
246 
     | 
    
         
             
                def execute_command(args)
         
     | 
| 
       191 
     | 
    
         
            -
                   
     | 
| 
       192 
     | 
    
         
            -
                  @ 
     | 
| 
      
 247 
     | 
    
         
            +
                  # Create or load the FitFileStore data.
         
     | 
| 
      
 248 
     | 
    
         
            +
                  unless (@ffs = @db['file_store'])
         
     | 
| 
      
 249 
     | 
    
         
            +
                    @ffs = @db['file_store'] = @db.new(FitFileStore)
         
     | 
| 
      
 250 
     | 
    
         
            +
                  end
         
     | 
| 
      
 251 
     | 
    
         
            +
                  # Create or load the PersonalRecords data.
         
     | 
| 
      
 252 
     | 
    
         
            +
                  unless (@records = @db['records'])
         
     | 
| 
      
 253 
     | 
    
         
            +
                    @records = @db['records'] = @db.new(PersonalRecords)
         
     | 
| 
      
 254 
     | 
    
         
            +
                  end
         
     | 
| 
       193 
255 
     | 
    
         
             
                  handle_version_update
         
     | 
| 
      
 256 
     | 
    
         
            +
                  import_legacy_archive
         
     | 
| 
       194 
257 
     | 
    
         | 
| 
       195 
258 
     | 
    
         
             
                  case (cmd = args.shift)
         
     | 
| 
       196 
259 
     | 
    
         
             
                  when 'check'
         
     | 
| 
       197 
260 
     | 
    
         
             
                    if args.empty?
         
     | 
| 
       198 
     | 
    
         
            -
                      @ 
     | 
| 
       199 
     | 
    
         
            -
                       
     | 
| 
      
 261 
     | 
    
         
            +
                      @ffs.check
         
     | 
| 
      
 262 
     | 
    
         
            +
                      Log.info "Datebase cleanup started. Please wait ..."
         
     | 
| 
      
 263 
     | 
    
         
            +
                      @db.gc
         
     | 
| 
      
 264 
     | 
    
         
            +
                      Log.info "Database cleanup finished"
         
     | 
| 
       200 
265 
     | 
    
         
             
                    else
         
     | 
| 
       201 
266 
     | 
    
         
             
                      process_files_or_activities(args, :check)
         
     | 
| 
       202 
267 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -205,23 +270,25 @@ EOT 
     | 
|
| 
       205 
270 
     | 
    
         
             
                  when 'dump'
         
     | 
| 
       206 
271 
     | 
    
         
             
                    @filter = Fit4Ruby::FitFilter.new unless @filter
         
     | 
| 
       207 
272 
     | 
    
         
             
                    process_files_or_activities(args, :dump)
         
     | 
| 
      
 273 
     | 
    
         
            +
                  when 'events'
         
     | 
| 
      
 274 
     | 
    
         
            +
                    process_files_or_activities(args, :events)
         
     | 
| 
       208 
275 
     | 
    
         
             
                  when 'import'
         
     | 
| 
       209 
276 
     | 
    
         
             
                    if args.empty?
         
     | 
| 
       210 
277 
     | 
    
         
             
                      # If we have no file or directory for the import command, we get the
         
     | 
| 
       211 
278 
     | 
    
         
             
                      # most recently used directory from the runtime config.
         
     | 
| 
       212 
     | 
    
         
            -
                      process_files([ @ 
     | 
| 
      
 279 
     | 
    
         
            +
                      process_files([ @db['config']['import_dir']  ], :import)
         
     | 
| 
       213 
280 
     | 
    
         
             
                    else
         
     | 
| 
       214 
281 
     | 
    
         
             
                      process_files(args, :import)
         
     | 
| 
       215 
282 
     | 
    
         
             
                      if args.length == 1 && Dir.exists?(args[0])
         
     | 
| 
       216 
283 
     | 
    
         
             
                        # If only one directory was specified as argument we store the
         
     | 
| 
       217 
284 
     | 
    
         
             
                        # directory for future use.
         
     | 
| 
       218 
     | 
    
         
            -
                        @ 
     | 
| 
      
 285 
     | 
    
         
            +
                        @db['config']['import_dir'] = args[0]
         
     | 
| 
       219 
286 
     | 
    
         
             
                      end
         
     | 
| 
       220 
287 
     | 
    
         
             
                    end
         
     | 
| 
       221 
288 
     | 
    
         
             
                  when 'list'
         
     | 
| 
       222 
     | 
    
         
            -
                    @ 
     | 
| 
      
 289 
     | 
    
         
            +
                    @ffs.list_activities
         
     | 
| 
       223 
290 
     | 
    
         
             
                  when 'records'
         
     | 
| 
       224 
     | 
    
         
            -
                    @ 
     | 
| 
      
 291 
     | 
    
         
            +
                    puts @records.to_s
         
     | 
| 
       225 
292 
     | 
    
         
             
                  when 'rename'
         
     | 
| 
       226 
293 
     | 
    
         
             
                    unless (@name = args.shift)
         
     | 
| 
       227 
294 
     | 
    
         
             
                      Log.fatal 'You must provide a new name for the activity'
         
     | 
| 
         @@ -237,7 +304,7 @@ EOT 
     | 
|
| 
       237 
304 
     | 
    
         
             
                    process_activities(args, :set)
         
     | 
| 
       238 
305 
     | 
    
         
             
                  when 'show'
         
     | 
| 
       239 
306 
     | 
    
         
             
                    if args.empty?
         
     | 
| 
       240 
     | 
    
         
            -
                      @ 
     | 
| 
      
 307 
     | 
    
         
            +
                      @ffs.show_list_in_browser
         
     | 
| 
       241 
308 
     | 
    
         
             
                    else
         
     | 
| 
       242 
309 
     | 
    
         
             
                      process_activities(args, :show)
         
     | 
| 
       243 
310 
     | 
    
         
             
                    end
         
     | 
| 
         @@ -277,7 +344,7 @@ EOT 
     | 
|
| 
       277 
344 
     | 
    
         | 
| 
       278 
345 
     | 
    
         
             
                  activity_refs.each do |a_ref|
         
     | 
| 
       279 
346 
     | 
    
         
             
                    if a_ref[0] == ':'
         
     | 
| 
       280 
     | 
    
         
            -
                      activities = @ 
     | 
| 
      
 347 
     | 
    
         
            +
                      activities = @ffs.find(a_ref[1..-1])
         
     | 
| 
       281 
348 
     | 
    
         
             
                      if activities.empty?
         
     | 
| 
       282 
349 
     | 
    
         
             
                        Log.warn "No matching activities found for '#{a_ref}'"
         
     | 
| 
       283 
350 
     | 
    
         
             
                        return
         
     | 
| 
         @@ -287,6 +354,8 @@ EOT 
     | 
|
| 
       287 
354 
     | 
    
         
             
                      Log.fatal "Activity references must start with ':': #{a_ref}"
         
     | 
| 
       288 
355 
     | 
    
         
             
                    end
         
     | 
| 
       289 
356 
     | 
    
         
             
                  end
         
     | 
| 
      
 357 
     | 
    
         
            +
             
     | 
| 
      
 358 
     | 
    
         
            +
                  nil
         
     | 
| 
       290 
359 
     | 
    
         
             
                end
         
     | 
| 
       291 
360 
     | 
    
         | 
| 
       292 
361 
     | 
    
         
             
                def process_files(files_or_dirs, command)
         
     | 
| 
         @@ -334,9 +403,9 @@ EOT 
     | 
|
| 
       334 
403 
     | 
    
         
             
                  end
         
     | 
| 
       335 
404 
     | 
    
         | 
| 
       336 
405 
     | 
    
         
             
                  if fit_entity.is_a?(Fit4Ruby::Activity)
         
     | 
| 
       337 
     | 
    
         
            -
                    return @ 
     | 
| 
       338 
     | 
    
         
            -
                  elsif fit_entity.is_a?(Fit4Ruby::Monitoring_B)
         
     | 
| 
       339 
     | 
    
         
            -
             
     | 
| 
      
 406 
     | 
    
         
            +
                    return @ffs.add_fit_file(fit_file_name, fit_entity, @force)
         
     | 
| 
      
 407 
     | 
    
         
            +
                  #elsif fit_entity.is_a?(Fit4Ruby::Monitoring_B)
         
     | 
| 
      
 408 
     | 
    
         
            +
                  #  return @monitoring.add(fit_file_name, fit_entity)
         
     | 
| 
       340 
409 
     | 
    
         
             
                  else
         
     | 
| 
       341 
410 
     | 
    
         
             
                    Log.error "#{fit_file_name} is not a recognized FIT file"
         
     | 
| 
       342 
411 
     | 
    
         
             
                    return false
         
     | 
| 
         @@ -348,13 +417,15 @@ EOT 
     | 
|
| 
       348 
417 
     | 
    
         
             
                  when :check
         
     | 
| 
       349 
418 
     | 
    
         
             
                    activity.check
         
     | 
| 
       350 
419 
     | 
    
         
             
                  when :delete
         
     | 
| 
       351 
     | 
    
         
            -
                    @ 
     | 
| 
      
 420 
     | 
    
         
            +
                    @ffs.delete_activity(activity)
         
     | 
| 
       352 
421 
     | 
    
         
             
                  when :dump
         
     | 
| 
       353 
422 
     | 
    
         
             
                    activity.dump(@filter)
         
     | 
| 
      
 423 
     | 
    
         
            +
                  when :events
         
     | 
| 
      
 424 
     | 
    
         
            +
                    activity.events
         
     | 
| 
       354 
425 
     | 
    
         
             
                  when :rename
         
     | 
| 
       355 
     | 
    
         
            -
                    @ 
     | 
| 
      
 426 
     | 
    
         
            +
                    @ffs.rename_activity(activity, @name)
         
     | 
| 
       356 
427 
     | 
    
         
             
                  when :set
         
     | 
| 
       357 
     | 
    
         
            -
                    @ 
     | 
| 
      
 428 
     | 
    
         
            +
                    @ffs.set_activity_attribute(activity, @attribute, @value)
         
     | 
| 
       358 
429 
     | 
    
         
             
                  when :show
         
     | 
| 
       359 
430 
     | 
    
         
             
                    activity.show
         
     | 
| 
       360 
431 
     | 
    
         
             
                  when :sources
         
     | 
| 
         @@ -375,9 +446,9 @@ EOT 
     | 
|
| 
       375 
446 
     | 
    
         
             
                    Log.fatal("You must specify 'metric' or 'statute' as unit system.")
         
     | 
| 
       376 
447 
     | 
    
         
             
                  end
         
     | 
| 
       377 
448 
     | 
    
         | 
| 
       378 
     | 
    
         
            -
                  if @ 
     | 
| 
       379 
     | 
    
         
            -
                    @ 
     | 
| 
       380 
     | 
    
         
            -
                    @ 
     | 
| 
      
 449 
     | 
    
         
            +
                  if @db['config']['unit_system'].to_s != args[0]
         
     | 
| 
      
 450 
     | 
    
         
            +
                    @db['config']['unit_system'] = args[0].to_sym
         
     | 
| 
      
 451 
     | 
    
         
            +
                    @ffs.change_unit_system
         
     | 
| 
       381 
452 
     | 
    
         
             
                  end
         
     | 
| 
       382 
453 
     | 
    
         
             
                end
         
     | 
| 
       383 
454 
     | 
    
         | 
| 
         @@ -386,16 +457,16 @@ EOT 
     | 
|
| 
       386 
457 
     | 
    
         
             
                    Log.fatal('You must specify a directory')
         
     | 
| 
       387 
458 
     | 
    
         
             
                  end
         
     | 
| 
       388 
459 
     | 
    
         | 
| 
       389 
     | 
    
         
            -
                  if @ 
     | 
| 
       390 
     | 
    
         
            -
                    @ 
     | 
| 
       391 
     | 
    
         
            -
                    @ 
     | 
| 
       392 
     | 
    
         
            -
                    @ 
     | 
| 
      
 460 
     | 
    
         
            +
                  if @db['config']['html_dir'] != args[0]
         
     | 
| 
      
 461 
     | 
    
         
            +
                    @db['config']['html_dir'] =  args[0]
         
     | 
| 
      
 462 
     | 
    
         
            +
                    @ffs.create_directories
         
     | 
| 
      
 463 
     | 
    
         
            +
                    @ffs.generate_all_html_reports
         
     | 
| 
       393 
464 
     | 
    
         
             
                  end
         
     | 
| 
       394 
465 
     | 
    
         
             
                end
         
     | 
| 
       395 
466 
     | 
    
         | 
| 
       396 
467 
     | 
    
         
             
                def update_gps_data
         
     | 
| 
       397 
468 
     | 
    
         
             
                  epo_dir = File.join(@db_dir, 'epo')
         
     | 
| 
       398 
     | 
    
         
            -
                   
     | 
| 
      
 469 
     | 
    
         
            +
                  create_directory(epo_dir, 'GPS Data Cache')
         
     | 
| 
       399 
470 
     | 
    
         
             
                  epo_file = File.join(epo_dir, 'EPO.BIN')
         
     | 
| 
       400 
471 
     | 
    
         | 
| 
       401 
472 
     | 
    
         
             
                  if !File.exists?(epo_file) ||
         
     | 
| 
         @@ -403,7 +474,7 @@ EOT 
     | 
|
| 
       403 
474 
     | 
    
         
             
                    # The EPO file only changes every 6 hours. No need to download it more
         
     | 
| 
       404 
475 
     | 
    
         
             
                    # frequently if it already exists.
         
     | 
| 
       405 
476 
     | 
    
         
             
                    if EPO_Downloader.new.download(epo_file)
         
     | 
| 
       406 
     | 
    
         
            -
                      unless (remotesw_dir = @ 
     | 
| 
      
 477 
     | 
    
         
            +
                      unless (remotesw_dir = @db['config']['import_dir'])
         
     | 
| 
       407 
478 
     | 
    
         
             
                        Log.error "No device directory set. Please import an activity " +
         
     | 
| 
       408 
479 
     | 
    
         
             
                                  "from your device first."
         
     | 
| 
       409 
480 
     | 
    
         
             
                        return
         
     | 
| 
         @@ -426,14 +497,45 @@ EOT 
     | 
|
| 
       426 
497 
     | 
    
         
             
                end
         
     | 
| 
       427 
498 
     | 
    
         | 
| 
       428 
499 
     | 
    
         
             
                def handle_version_update
         
     | 
| 
       429 
     | 
    
         
            -
                  if @ 
     | 
| 
      
 500 
     | 
    
         
            +
                  if @db['config']['version'] != VERSION
         
     | 
| 
       430 
501 
     | 
    
         
             
                    Log.warn "PostRunner version upgrade detected."
         
     | 
| 
       431 
     | 
    
         
            -
                    @ 
     | 
| 
       432 
     | 
    
         
            -
                    @ 
     | 
| 
      
 502 
     | 
    
         
            +
                    @ffs.handle_version_update
         
     | 
| 
      
 503 
     | 
    
         
            +
                    @db['config']['version'] = VERSION
         
     | 
| 
       433 
504 
     | 
    
         
             
                    Log.info "Version upgrade completed."
         
     | 
| 
       434 
505 
     | 
    
         
             
                  end
         
     | 
| 
       435 
506 
     | 
    
         
             
                end
         
     | 
| 
       436 
507 
     | 
    
         | 
| 
      
 508 
     | 
    
         
            +
                # Earlier versions of PostRunner used a YAML file to store the activity
         
     | 
| 
      
 509 
     | 
    
         
            +
                # data. This method transfers the data from the old storage to the new
         
     | 
| 
      
 510 
     | 
    
         
            +
                # FitFileStore based database.
         
     | 
| 
      
 511 
     | 
    
         
            +
                def import_legacy_archive
         
     | 
| 
      
 512 
     | 
    
         
            +
                  old_fit_dir = File.join(@db_dir, 'old_fit_dir')
         
     | 
| 
      
 513 
     | 
    
         
            +
                  create_directory(old_fit_dir, 'Old Fit')
         
     | 
| 
      
 514 
     | 
    
         
            +
             
     | 
| 
      
 515 
     | 
    
         
            +
                  cfg = RuntimeConfig.new(@db_dir)
         
     | 
| 
      
 516 
     | 
    
         
            +
                  activities = ActivitiesDB.new(@db_dir, cfg).activities
         
     | 
| 
      
 517 
     | 
    
         
            +
                  # Ensure that activities are sorted from earliest to last to properly
         
     | 
| 
      
 518 
     | 
    
         
            +
                  # recognize the personal records during import.
         
     | 
| 
      
 519 
     | 
    
         
            +
                  activities.sort! { |a1, a2| a1.timestamp <=> a2.timestamp }
         
     | 
| 
      
 520 
     | 
    
         
            +
                  activities.each do |activity|
         
     | 
| 
      
 521 
     | 
    
         
            +
                    file_name = File.join(@db_dir, 'fit', activity.fit_file)
         
     | 
| 
      
 522 
     | 
    
         
            +
                    next unless File.exists?(file_name)
         
     | 
| 
      
 523 
     | 
    
         
            +
             
     | 
| 
      
 524 
     | 
    
         
            +
                    Log.info "Converting #{activity.fit_file} to new DB format"
         
     | 
| 
      
 525 
     | 
    
         
            +
                    @db.transaction do
         
     | 
| 
      
 526 
     | 
    
         
            +
                      unless (new_activity = @ffs.add_fit_file(file_name))
         
     | 
| 
      
 527 
     | 
    
         
            +
                        Log.warn "Cannot convert #{file_name} to new database format"
         
     | 
| 
      
 528 
     | 
    
         
            +
                        next
         
     | 
| 
      
 529 
     | 
    
         
            +
                      end
         
     | 
| 
      
 530 
     | 
    
         
            +
                      new_activity.sport = activity.sport
         
     | 
| 
      
 531 
     | 
    
         
            +
                      new_activity.sub_sport = activity.sub_sport
         
     | 
| 
      
 532 
     | 
    
         
            +
                      new_activity.name = activity.name
         
     | 
| 
      
 533 
     | 
    
         
            +
                      new_activity.norecord = activity.norecord
         
     | 
| 
      
 534 
     | 
    
         
            +
                      FileUtils.move(file_name, File.join(old_fit_dir, activity.fit_file))
         
     | 
| 
      
 535 
     | 
    
         
            +
                    end
         
     | 
| 
      
 536 
     | 
    
         
            +
                  end
         
     | 
| 
      
 537 
     | 
    
         
            +
                end
         
     | 
| 
      
 538 
     | 
    
         
            +
             
     | 
| 
       437 
539 
     | 
    
         
             
              end
         
     | 
| 
       438 
540 
     | 
    
         | 
| 
       439 
541 
     | 
    
         
             
            end
         
     |