postrunner 0.0.5 → 0.0.6
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 +21 -6
- data/lib/postrunner/ActivitiesDB.rb +6 -0
- data/lib/postrunner/Activity.rb +42 -18
- data/lib/postrunner/ActivityListView.rb +4 -2
- data/lib/postrunner/ActivitySummary.rb +24 -12
- data/lib/postrunner/ChartView.rb +86 -17
- data/lib/postrunner/Main.rb +11 -2
- data/lib/postrunner/RuntimeConfig.rb +1 -0
- data/lib/postrunner/TrackView.rb +11 -11
- data/lib/postrunner/version.rb +1 -1
- data/postrunner.gemspec +8 -8
- data/spec/PostRunner_spec.rb +17 -1
- data/spec/spec_helper.rb +1 -1
- metadata +21 -21
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 5c193dbf151045522cfe8f47b729d244eb87f957
         | 
| 4 | 
            +
              data.tar.gz: fb92f5b286258cac2c17e96dae821d858face419
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 2a01b57478175e684b1253e30174e46ee5b3119dd8a2941a8cf6141255e7955c6628fff6a20167c3421feaee0ca7c857370ae8c05a4d7ae114fb6730df909953
         | 
| 7 | 
            +
              data.tar.gz: b3f8c2ad6fa0effd9958a4102b6280e373c5bf11a3c90982bfdaaffaeb14d90f03fff37460963d51e6600cd9432f36b11338a86e3b5eb19536252fcd37e4c048
         | 
    
        data/README.md
    CHANGED
    
    | @@ -1,10 +1,15 @@ | |
| 1 1 | 
             
            # PostRunner
         | 
| 2 2 |  | 
| 3 | 
            -
            PostRunner is an application to manage FIT files such as those | 
| 3 | 
            +
            PostRunner is an application to manage FIT files such as those
         | 
| 4 | 
            +
            produced by Garmin products like the Forerunner 620 (FR620). It allows you to
         | 
| 5 | 
            +
            import the files from the device and inspect them. 
         | 
| 4 6 |  | 
| 5 7 | 
             
            ## Installation
         | 
| 6 8 |  | 
| 7 | 
            -
            PostRunner is a Ruby application. You need | 
| 9 | 
            +
            PostRunner is a [http://www.ruby-lang.org](Ruby) application. You need
         | 
| 10 | 
            +
            to have a Ruby 2.0 or later runtime environment installed.  This
         | 
| 11 | 
            +
            application was developed and tested on Linux but may work on other
         | 
| 12 | 
            +
            operating systems as well.
         | 
| 8 13 |  | 
| 9 14 | 
             
            ```
         | 
| 10 15 | 
             
            $ gem install postrunner
         | 
| @@ -14,13 +19,20 @@ $ gem install postrunner | |
| 14 19 |  | 
| 15 20 | 
             
            ### Importing FIT files
         | 
| 16 21 |  | 
| 17 | 
            -
            To get started you need to connect your device to your computer and | 
| 22 | 
            +
            To get started you need to connect your device to your computer and
         | 
| 23 | 
            +
            mount it as a disk drive. Only devices that expose their data as FAT file
         | 
| 24 | 
            +
            system are supported. Older devices use proprietary drivers and are
         | 
| 25 | 
            +
            not supported by postrunner. Once the device is mounted find out the
         | 
| 26 | 
            +
            full path to the directory that contains your FIT files. You can then
         | 
| 27 | 
            +
            import all files on the device.
         | 
| 18 28 |  | 
| 19 29 | 
             
            ```
         | 
| 20 30 | 
             
            $ postrunner import /var/run/media/user/GARMIN/GARMIN/ACTIVITY/
         | 
| 21 31 | 
             
            ```
         | 
| 22 32 |  | 
| 23 | 
            -
            The above command assumes that your device is mounted as | 
| 33 | 
            +
            The above command assumes that your device is mounted as
         | 
| 34 | 
            +
            /var/run/media/user. Please replace this with the path to your device.
         | 
| 35 | 
            +
            Files that have been imported previously will not be imported again. 
         | 
| 24 36 |  | 
| 25 37 | 
             
            ### Viewing FIT file data on the console
         | 
| 26 38 |  | 
| @@ -57,13 +69,16 @@ You can also get a full dump of the content of a FIT file. | |
| 57 69 | 
             
            $ postrunner dump 1234568.FIT
         | 
| 58 70 | 
             
            ```
         | 
| 59 71 |  | 
| 60 | 
            -
            If the file is already in the data base you can also use the reference | 
| 72 | 
            +
            If the file is already in the data base you can also use the reference
         | 
| 73 | 
            +
            notation.
         | 
| 61 74 |  | 
| 62 75 | 
             
            ```
         | 
| 63 76 | 
             
            $ postrunner dump :1
         | 
| 64 77 | 
             
            ```
         | 
| 65 78 |  | 
| 66 | 
            -
            This will provide you with a lot more information contained in the FIT | 
| 79 | 
            +
            This will provide you with a lot more information contained in the FIT
         | 
| 80 | 
            +
            files that is not available through Garmin Connect or most other
         | 
| 81 | 
            +
            tools.
         | 
| 67 82 |  | 
| 68 83 | 
             
            ### Viewing FIT file data in your web browser
         | 
| 69 84 |  | 
| @@ -49,11 +49,17 @@ module PostRunner | |
| 49 49 | 
             
                  # Not all instance variables of Activity are stored in the file. The
         | 
| 50 50 | 
             
                  # normal constructor is not run during YAML::load_file. We have to
         | 
| 51 51 | 
             
                  # initialize those instance variables in a secondary step.
         | 
| 52 | 
            +
                  sync_needed = false
         | 
| 52 53 | 
             
                  @activities.each do |a|
         | 
| 53 54 | 
             
                    a.late_init(self)
         | 
| 55 | 
            +
                    # If the Activity has the data from the FIT file loaded, a value was
         | 
| 56 | 
            +
                    # missing in the YAML file. Set the sync flag so we can update the
         | 
| 57 | 
            +
                    # YAML file once we have checked all Activities.
         | 
| 58 | 
            +
                    sync_needed |= !a.fit_activity.nil?
         | 
| 54 59 | 
             
                  end
         | 
| 55 60 |  | 
| 56 61 | 
             
                  @records = PersonalRecords.new(self)
         | 
| 62 | 
            +
                  sync if sync_needed
         | 
| 57 63 | 
             
                end
         | 
| 58 64 |  | 
| 59 65 | 
             
                # Add a new FIT file to the database.
         | 
    
        data/lib/postrunner/Activity.rb
    CHANGED
    
    | @@ -23,8 +23,10 @@ module PostRunner | |
| 23 23 |  | 
| 24 24 | 
             
                # This is a list of variables that provide data from the fit file. To
         | 
| 25 25 | 
             
                # speed up access to it, we cache the data in the activity database.
         | 
| 26 | 
            -
                @@ | 
| 27 | 
            -
                                        avg_speed )
         | 
| 26 | 
            +
                @@CachedActivityValues = %w( sport timestamp total_distance
         | 
| 27 | 
            +
                                        total_timer_time avg_speed )
         | 
| 28 | 
            +
                # We also store some additional information in the archive index.
         | 
| 29 | 
            +
                @@CachedAttributes = @@CachedActivityValues + %w( fit_file name )
         | 
| 28 30 |  | 
| 29 31 | 
             
                def initialize(db, fit_file, fit_activity, name = nil)
         | 
| 30 32 | 
             
                  @fit_file = fit_file
         | 
| @@ -32,7 +34,7 @@ module PostRunner | |
| 32 34 | 
             
                  @name = name || fit_file
         | 
| 33 35 | 
             
                  late_init(db)
         | 
| 34 36 |  | 
| 35 | 
            -
                  @@ | 
| 37 | 
            +
                  @@CachedActivityValues.each do |v|
         | 
| 36 38 | 
             
                    v_str = "@#{v}"
         | 
| 37 39 | 
             
                    instance_variable_set(v_str, fit_activity.send(v))
         | 
| 38 40 | 
             
                    self.class.send(:attr_reader, v.to_sym)
         | 
| @@ -41,6 +43,9 @@ module PostRunner | |
| 41 43 | 
             
                  generate_html_view
         | 
| 42 44 | 
             
                end
         | 
| 43 45 |  | 
| 46 | 
            +
                # YAML::load() does not call initialize(). We don't have all attributes
         | 
| 47 | 
            +
                # stored in the YAML file, so we need to make sure these are properly set
         | 
| 48 | 
            +
                # after a YAML::load().
         | 
| 44 49 | 
             
                def late_init(db)
         | 
| 45 50 | 
             
                  @db = db
         | 
| 46 51 | 
             
                  @html_dir = File.join(@db.db_dir, 'html')
         | 
| @@ -56,26 +61,45 @@ module PostRunner | |
| 56 61 | 
             
                  @fit_activity = load_fit_file(filter)
         | 
| 57 62 | 
             
                end
         | 
| 58 63 |  | 
| 59 | 
            -
                 | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
                   | 
| 64 | 
            -
             | 
| 65 | 
            -
                     | 
| 64 | 
            +
                # This method is called during YAML::load() to initialize the class
         | 
| 65 | 
            +
                # objects. The initialize() is NOT called during YAML::load(). Any
         | 
| 66 | 
            +
                # additional initialization work is done in late_init().
         | 
| 67 | 
            +
                def init_with(coder)
         | 
| 68 | 
            +
                  @@CachedAttributes.each do |name_without_at|
         | 
| 69 | 
            +
                    name_with_at = '@' + name_without_at
         | 
| 70 | 
            +
                    # Create attr_readers for cached variables.
         | 
| 71 | 
            +
                    self.class.send(:attr_reader, name_without_at.to_sym)
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    if coder.map.include?(name_without_at)
         | 
| 74 | 
            +
                      # The YAML file has a value for the instance variable. So just set
         | 
| 75 | 
            +
                      # it.
         | 
| 76 | 
            +
                      instance_variable_set(name_with_at, coder[name_without_at])
         | 
| 77 | 
            +
                    else
         | 
| 78 | 
            +
                      if @@CachedActivityValues.include?(name_without_at)
         | 
| 79 | 
            +
                        # The YAML file does not yet have the instance variable cached.
         | 
| 80 | 
            +
                        # Load the Activity data and extract the value to set the instance
         | 
| 81 | 
            +
                        # variable.
         | 
| 82 | 
            +
                        @fit_activity = load_fit_file unless @fit_activity
         | 
| 83 | 
            +
                        instance_variable_set(name_with_at,
         | 
| 84 | 
            +
                                              @fit_activity.send(name_without_at))
         | 
| 85 | 
            +
                      else
         | 
| 86 | 
            +
                        Log.fatal "Don't know how to initialize the instance variable " +
         | 
| 87 | 
            +
                                  "#{name_with_at}."
         | 
| 88 | 
            +
                      end
         | 
| 89 | 
            +
                    end
         | 
| 66 90 | 
             
                  end
         | 
| 67 | 
            -
                  # Use the FIT file name as activity name if none has been set yet.
         | 
| 68 | 
            -
                  @name = @fit_file unless @name
         | 
| 69 91 | 
             
                end
         | 
| 70 92 |  | 
| 93 | 
            +
                # This method is called during Activity::to_yaml() calls. It's being used
         | 
| 94 | 
            +
                # to prevent some instance variables from being saved in the YAML file.
         | 
| 95 | 
            +
                # Only attributes that are listed in @@CachedAttributes are being saved.
         | 
| 71 96 | 
             
                def encode_with(coder)
         | 
| 72 | 
            -
                   | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 75 | 
            -
                     | 
| 76 | 
            -
                    next if attr_ignore.include?(v)
         | 
| 97 | 
            +
                  instance_variables.each do |a|
         | 
| 98 | 
            +
                    name_with_at = a.to_s
         | 
| 99 | 
            +
                    name_without_at = name_with_at[1..-1]
         | 
| 100 | 
            +
                    next unless @@CachedAttributes.include?(name_without_at)
         | 
| 77 101 |  | 
| 78 | 
            -
                    coder[ | 
| 102 | 
            +
                    coder[name_without_at] = instance_variable_get(name_with_at)
         | 
| 79 103 | 
             
                  end
         | 
| 80 104 | 
             
                end
         | 
| 81 105 |  | 
| @@ -134,7 +134,7 @@ EOT | |
| 134 134 | 
             
                  i = @page_no < 0 ? 0 : @page_no * @page_size
         | 
| 135 135 | 
             
                  t = FlexiTable.new
         | 
| 136 136 | 
             
                  t.head
         | 
| 137 | 
            -
                  t.row(%w( Ref. Activity Start Distance Duration Pace ),
         | 
| 137 | 
            +
                  t.row(%w( Ref. Activity Start Distance Duration Speed/Pace ),
         | 
| 138 138 | 
             
                        { :halign => :left })
         | 
| 139 139 | 
             
                  t.set_column_attributes([
         | 
| 140 140 | 
             
                    { :halign => :right },
         | 
| @@ -155,7 +155,9 @@ EOT | |
| 155 155 | 
             
                      local_value(a.total_distance, 'm', '%.2f',
         | 
| 156 156 | 
             
                                  { :metric => 'km', :statute => 'mi' }),
         | 
| 157 157 | 
             
                      secsToHMS(a.total_timer_time),
         | 
| 158 | 
            -
                      pace(a.avg_speed)  | 
| 158 | 
            +
                      a.sport == 'running' ? pace(a.avg_speed) :
         | 
| 159 | 
            +
                        local_value(a.avg_speed, 'm/s', '%.1f',
         | 
| 160 | 
            +
                                    { :metric => 'km/h', :statute => 'mph' }) ])
         | 
| 159 161 | 
             
                  end
         | 
| 160 162 |  | 
| 161 163 | 
             
                  t
         | 
| @@ -29,16 +29,12 @@ module PostRunner | |
| 29 29 | 
             
                end
         | 
| 30 30 |  | 
| 31 31 | 
             
                def to_s
         | 
| 32 | 
            -
                   | 
| 33 | 
            -
             | 
| 34 | 
            -
                  summary(session).to_s + "\n" + laps.to_s
         | 
| 32 | 
            +
                  summary.to_s + "\n" + laps.to_s
         | 
| 35 33 | 
             
                end
         | 
| 36 34 |  | 
| 37 35 | 
             
                def to_html(doc)
         | 
| 38 | 
            -
                  session = @fit_activity.sessions[0]
         | 
| 39 | 
            -
             | 
| 40 36 | 
             
                  frame(doc, "Activity: #{@name}") {
         | 
| 41 | 
            -
                    summary | 
| 37 | 
            +
                    summary.to_html(doc)
         | 
| 42 38 | 
             
                  }
         | 
| 43 39 | 
             
                  frame(doc, 'Laps') {
         | 
| 44 40 | 
             
                    laps.to_html(doc)
         | 
| @@ -47,7 +43,9 @@ module PostRunner | |
| 47 43 |  | 
| 48 44 | 
             
                private
         | 
| 49 45 |  | 
| 50 | 
            -
                def summary | 
| 46 | 
            +
                def summary
         | 
| 47 | 
            +
                  session = @fit_activity.sessions[0]
         | 
| 48 | 
            +
             | 
| 51 49 | 
             
                  t = FlexiTable.new
         | 
| 52 50 | 
             
                  t.enable_frame(false)
         | 
| 53 51 | 
             
                  t.body
         | 
| @@ -56,7 +54,13 @@ module PostRunner | |
| 56 54 | 
             
                          local_value(session, 'total_distance', '%.2f %s',
         | 
| 57 55 | 
             
                                      { :metric => 'km', :statute => 'mi'}) ])
         | 
| 58 56 | 
             
                  t.row([ 'Time:', secsToHMS(session.total_timer_time) ])
         | 
| 59 | 
            -
                   | 
| 57 | 
            +
                  if session.sport == 'running'
         | 
| 58 | 
            +
                    t.row([ 'Avg. Pace:', pace(session, 'avg_speed') ])
         | 
| 59 | 
            +
                  else
         | 
| 60 | 
            +
                    t.row([ 'Avg. Speed:',
         | 
| 61 | 
            +
                            local_value(session, 'avg_speed', '%.1f %s',
         | 
| 62 | 
            +
                                        { :metric => 'km/h', :statute => 'mph' }) ])
         | 
| 63 | 
            +
                  end
         | 
| 60 64 | 
             
                  t.row([ 'Total Ascent:',
         | 
| 61 65 | 
             
                          local_value(session, 'total_ascent', '%.0f %s',
         | 
| 62 66 | 
             
                                      { :metric => 'm', :statute => 'ft' }) ])
         | 
| @@ -91,18 +95,26 @@ module PostRunner | |
| 91 95 | 
             
                end
         | 
| 92 96 |  | 
| 93 97 | 
             
                def laps
         | 
| 98 | 
            +
                  session = @fit_activity.sessions[0]
         | 
| 99 | 
            +
             | 
| 94 100 | 
             
                  t = FlexiTable.new
         | 
| 95 101 | 
             
                  t.head
         | 
| 96 | 
            -
                  t.row([ 'Lap', 'Duration', 'Distance', | 
| 97 | 
            -
                          'Avg.  | 
| 102 | 
            +
                  t.row([ 'Lap', 'Duration', 'Distance',
         | 
| 103 | 
            +
                          session.sport == 'running' ? 'Avg. Pace' : 'Avg. Speed',
         | 
| 104 | 
            +
                          'Stride', 'Cadence', 'Avg. HR', 'Max. HR' ])
         | 
| 98 105 | 
             
                  t.set_column_attributes(Array.new(8, { :halign => :right }))
         | 
| 99 106 | 
             
                  t.body
         | 
| 100 | 
            -
                   | 
| 107 | 
            +
                  session.laps.each.with_index do |lap, index|
         | 
| 101 108 | 
             
                    t.cell(index + 1)
         | 
| 102 109 | 
             
                    t.cell(secsToHMS(lap.total_timer_time))
         | 
| 103 110 | 
             
                    t.cell(local_value(lap, 'total_distance', '%.2f',
         | 
| 104 111 | 
             
                                       { :metric => 'km', :statute => 'mi' }))
         | 
| 105 | 
            -
                     | 
| 112 | 
            +
                    if session.sport == 'running'
         | 
| 113 | 
            +
                      t.cell(pace(lap, 'avg_speed', false))
         | 
| 114 | 
            +
                    else
         | 
| 115 | 
            +
                      t.cell(local_value(lap, 'avg_speed', '%.1f',
         | 
| 116 | 
            +
                                         { :metric => 'km/h', :statute => 'mph' }))
         | 
| 117 | 
            +
                    end
         | 
| 106 118 | 
             
                    t.cell(local_value(lap, 'avg_stride_length', '%.2f',
         | 
| 107 119 | 
             
                                       { :metric => 'm', :statute => 'ft' }))
         | 
| 108 120 | 
             
                    t.cell(lap.avg_running_cadence && lap.avg_fractional_cadence ?
         | 
    
        data/lib/postrunner/ChartView.rb
    CHANGED
    
    | @@ -1,5 +1,4 @@ | |
| 1 | 
            -
            #!/usr/bin/env ruby -w
         | 
| 2 | 
            -
            # encoding: UTF-8
         | 
| 1 | 
            +
            #!/usr/bin/env ruby -w # encoding: UTF-8
         | 
| 3 2 | 
             
            #
         | 
| 4 3 | 
             
            # = ChartView.rb -- PostRunner - Manage the data from your Garmin sport devices.
         | 
| 5 4 | 
             
            #
         | 
| @@ -20,6 +19,7 @@ module PostRunner | |
| 20 19 |  | 
| 21 20 | 
             
                def initialize(activity, unit_system)
         | 
| 22 21 | 
             
                  @activity = activity
         | 
| 22 | 
            +
                  @sport = activity.fit_activity.sessions[0].sport
         | 
| 23 23 | 
             
                  @unit_system = unit_system
         | 
| 24 24 | 
             
                  @empty_charts = {}
         | 
| 25 25 | 
             
                end
         | 
| @@ -35,7 +35,11 @@ module PostRunner | |
| 35 35 | 
             
                end
         | 
| 36 36 |  | 
| 37 37 | 
             
                def div(doc)
         | 
| 38 | 
            -
                   | 
| 38 | 
            +
                  if @sport == 'running'
         | 
| 39 | 
            +
                    chart_div(doc, 'pace', "Pace (#{select_unit('min/km')})")
         | 
| 40 | 
            +
                  else
         | 
| 41 | 
            +
                    chart_div(doc, 'speed', "Speed (#{select_unit('km/h')})")
         | 
| 42 | 
            +
                  end
         | 
| 39 43 | 
             
                  chart_div(doc, 'altitude', "Elevation (#{select_unit('m')})")
         | 
| 40 44 | 
             
                  chart_div(doc, 'heart_rate', 'Heart Rate (bpm)')
         | 
| 41 45 | 
             
                  chart_div(doc, 'run_cadence', 'Run Cadence (spm)')
         | 
| @@ -51,7 +55,7 @@ module PostRunner | |
| 51 55 | 
             
                  when :metric
         | 
| 52 56 | 
             
                    metric_unit
         | 
| 53 57 | 
             
                  when :statute
         | 
| 54 | 
            -
                    { 'min/km' => 'min/mi', 'm' => 'ft', 'cm' => 'in',
         | 
| 58 | 
            +
                    { 'min/km' => 'min/mi', 'm' => 'ft', 'cm' => 'in', 'km/h' => 'mph',
         | 
| 55 59 | 
             
                      'bpm' => 'bpm', 'spm' => 'spm', 'ms' => 'ms' }[metric_unit]
         | 
| 56 60 | 
             
                  else
         | 
| 57 61 | 
             
                    Log.fatal "Unknown unit system #{@unit_system}"
         | 
| @@ -91,22 +95,27 @@ EOT | |
| 91 95 | 
             
                def java_script
         | 
| 92 96 | 
             
                  s = "$(function() {\n"
         | 
| 93 97 |  | 
| 94 | 
            -
                  s <<  | 
| 95 | 
            -
                   | 
| 96 | 
            -
             | 
| 97 | 
            -
                   | 
| 98 | 
            +
                  s << tooltip_div
         | 
| 99 | 
            +
                  if @sport == 'running'
         | 
| 100 | 
            +
                    s << line_graph('pace', 'Pace', 'min/km', '#0A7BEE' )
         | 
| 101 | 
            +
                  else
         | 
| 102 | 
            +
                    s << line_graph('speed', 'Speed', 'km/h', '#0A7BEE' )
         | 
| 103 | 
            +
                  end
         | 
| 104 | 
            +
                  s << line_graph('altitude', 'Elevation', 'm', '#5AAA44')
         | 
| 105 | 
            +
                  s << line_graph('heart_rate', 'Heart Rate', 'bpm', '#900000')
         | 
| 106 | 
            +
                  s << point_graph('run_cadence', 'Run Cadence', 'spm',
         | 
| 98 107 | 
             
                                   [ [ '#EE3F2D', 151 ],
         | 
| 99 108 | 
             
                                     [ '#F79666', 163 ],
         | 
| 100 109 | 
             
                                     [ '#A0D488', 174 ],
         | 
| 101 110 | 
             
                                     [ '#96D7DE', 185 ],
         | 
| 102 111 | 
             
                                     [ '#A88BBB', nil ] ])
         | 
| 103 | 
            -
                  s << point_graph('vertical_oscillation', 'cm',
         | 
| 112 | 
            +
                  s << point_graph('vertical_oscillation', 'Vertical Oscillation', 'cm',
         | 
| 104 113 | 
             
                                   [ [ '#A88BBB', 67 ],
         | 
| 105 114 | 
             
                                     [ '#96D7DE', 84 ],
         | 
| 106 115 | 
             
                                     [ '#A0D488', 101 ],
         | 
| 107 116 | 
             
                                     [ '#F79666', 118 ],
         | 
| 108 117 | 
             
                                     [ '#EE3F2D', nil ] ])
         | 
| 109 | 
            -
                  s << point_graph('stance_time', 'ms',
         | 
| 118 | 
            +
                  s << point_graph('stance_time', 'Ground Contact Time', 'ms',
         | 
| 110 119 | 
             
                                   [ [ '#A88BBB', 208 ],
         | 
| 111 120 | 
             
                                     [ '#96D7DE', 241 ],
         | 
| 112 121 | 
             
                                     [ '#A0D488', 273 ],
         | 
| @@ -118,19 +127,52 @@ EOT | |
| 118 127 | 
             
                  s
         | 
| 119 128 | 
             
                end
         | 
| 120 129 |  | 
| 121 | 
            -
                def  | 
| 130 | 
            +
                def tooltip_div
         | 
| 131 | 
            +
                  <<"EOT"
         | 
| 132 | 
            +
                    function timeToHMS(usecs) {
         | 
| 133 | 
            +
                       var secs = parseInt(usecs / 1000.0);
         | 
| 134 | 
            +
                       var s = secs % 60;
         | 
| 135 | 
            +
                       var mins = parseInt(secs / 60);
         | 
| 136 | 
            +
                       var m = mins % 60;
         | 
| 137 | 
            +
                       var h = parseInt(mins / 60);
         | 
| 138 | 
            +
                       s = (s < 10) ? "0" + s : s;
         | 
| 139 | 
            +
                       if (h == 0) {
         | 
| 140 | 
            +
                         return ("" + m + ":" + s);
         | 
| 141 | 
            +
                       } else {
         | 
| 142 | 
            +
                         m = (m < 10) ? "0" + m : m;
         | 
| 143 | 
            +
                         return ("" + h + ":" + m + ":" + s);
         | 
| 144 | 
            +
                       }
         | 
| 145 | 
            +
                    };
         | 
| 146 | 
            +
                    $("<div id='tooltip'></div>").css({
         | 
| 147 | 
            +
                            position: "absolute",
         | 
| 148 | 
            +
                            display: "none",
         | 
| 149 | 
            +
                            border: "1px solid #888",
         | 
| 150 | 
            +
                            padding: "2px",
         | 
| 151 | 
            +
                            "background-color": "#EEE",
         | 
| 152 | 
            +
                            opacity: 0.90,
         | 
| 153 | 
            +
                            "font-size": "8pt"
         | 
| 154 | 
            +
                    }).appendTo("body");
         | 
| 155 | 
            +
            EOT
         | 
| 156 | 
            +
                end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                def line_graph(field, y_label, unit, color = nil)
         | 
| 122 159 | 
             
                  s = "var #{field}_data = [\n"
         | 
| 123 160 |  | 
| 124 161 | 
             
                  data_set = []
         | 
| 125 162 | 
             
                  start_time = @activity.fit_activity.sessions[0].start_time.to_i
         | 
| 163 | 
            +
                  min_value = nil
         | 
| 126 164 | 
             
                  @activity.fit_activity.records.each do |r|
         | 
| 127 165 | 
             
                    value = r.get_as(field, select_unit(unit))
         | 
| 128 166 | 
             
                    if field == 'pace'
         | 
| 129 | 
            -
                       | 
| 167 | 
            +
                      # Slow speeds lead to very large pace values that make the graph
         | 
| 168 | 
            +
                      # hard to read. We cap the pace at 20.0 min/km to keep it readable.
         | 
| 169 | 
            +
                      if value > (@unit_system == :metric ? 20.0 : 36.0 )
         | 
| 130 170 | 
             
                        value = nil
         | 
| 131 171 | 
             
                      else
         | 
| 132 172 | 
             
                        value = (value * 3600.0 * 1000).to_i
         | 
| 133 173 | 
             
                      end
         | 
| 174 | 
            +
                    else
         | 
| 175 | 
            +
                      min_value = value if value && (min_value.nil? || min_value > value)
         | 
| 134 176 | 
             
                    end
         | 
| 135 177 | 
             
                    data_set << [ ((r.timestamp.to_i - start_time) * 1000).to_i, value ]
         | 
| 136 178 | 
             
                  end
         | 
| @@ -144,25 +186,31 @@ EOT | |
| 144 186 | 
             
                    "[ #{set[0]}, #{set[1] ? set[1] : 'null'} ]"
         | 
| 145 187 | 
             
                  end.join(', ')
         | 
| 146 188 |  | 
| 189 | 
            +
                  chart_id = "#{field}_chart"
         | 
| 147 190 | 
             
                  s << <<"EOT"
         | 
| 148 191 | 
             
            	  	];
         | 
| 149 192 |  | 
| 150 | 
            -
            	  	$.plot("##{ | 
| 193 | 
            +
            	  	$.plot(\"##{chart_id}\",
         | 
| 151 194 | 
             
                         [ { data: #{field}_data,
         | 
| 152 195 | 
             
                             #{color ? "color: \"#{color}\"," : ''}
         | 
| 153 196 | 
             
                             lines: { show: true#{field == 'pace' ? '' :
         | 
| 154 197 | 
             
                                                  ', fill: true'} } } ],
         | 
| 155 | 
            -
                         { xaxis: { mode: "time" }
         | 
| 198 | 
            +
                         { xaxis: { mode: "time" },
         | 
| 199 | 
            +
                           grid: { hoverable: true }
         | 
| 156 200 | 
             
            EOT
         | 
| 157 201 | 
             
                  if field == 'pace'
         | 
| 158 202 | 
             
                    s << ", yaxis: { mode: \"time\",\n" +
         | 
| 159 203 | 
             
                         "           transform: function (v) { return -v; },\n" +
         | 
| 160 204 | 
             
                         "           inverseTransform: function (v) { return -v; } }"
         | 
| 205 | 
            +
                  else
         | 
| 206 | 
            +
                    # Set the minimum slightly below the lowest found value.
         | 
| 207 | 
            +
                    s << ", yaxis: { min: #{0.9 * min_value} }"
         | 
| 161 208 | 
             
                  end
         | 
| 162 209 | 
             
                  s << "});\n"
         | 
| 210 | 
            +
                  s << hover_function(chart_id, y_label, select_unit(unit)) + "\n"
         | 
| 163 211 | 
             
                end
         | 
| 164 212 |  | 
| 165 | 
            -
                def point_graph(field, unit, colors)
         | 
| 213 | 
            +
                def point_graph(field, y_label, unit, colors)
         | 
| 166 214 | 
             
                  # We need to split the field values into separate data sets for each
         | 
| 167 215 | 
             
                  # color. The max value for each color determines which set a data point
         | 
| 168 216 | 
             
                  # ends up in.
         | 
| @@ -207,14 +255,17 @@ EOT | |
| 207 255 | 
             
                    s << " ];\n"
         | 
| 208 256 | 
             
                  end
         | 
| 209 257 |  | 
| 210 | 
            -
                   | 
| 258 | 
            +
                  chart_id = "#{field}_chart"
         | 
| 259 | 
            +
                  s << "$.plot(\"##{chart_id}\", [\n"
         | 
| 211 260 | 
             
                  s << data_sets.map do |index, ds|
         | 
| 212 261 | 
             
                         "{ data: #{field}_data_#{index},\n" +
         | 
| 213 262 | 
             
                         "  color: \"#{colors[index][0]}\",\n" +
         | 
| 214 263 | 
             
                         "  points: { show: true, fillColor: \"#{colors[index][0]}\", " +
         | 
| 215 264 | 
             
                         "            fill: true, radius: 2 } }"
         | 
| 216 265 | 
             
                       end.join(', ')
         | 
| 217 | 
            -
                  s << "], { xaxis: { mode: \"time\" } | 
| 266 | 
            +
                  s << "], { xaxis: { mode: \"time\" },
         | 
| 267 | 
            +
                             grid: { hoverable: true } });\n"
         | 
| 268 | 
            +
                  s << hover_function(chart_id, y_label, select_unit(unit))
         | 
| 218 269 |  | 
| 219 270 | 
             
                  s
         | 
| 220 271 | 
             
                end
         | 
| @@ -228,6 +279,24 @@ EOT | |
| 228 279 | 
             
                  }
         | 
| 229 280 | 
             
                end
         | 
| 230 281 |  | 
| 282 | 
            +
                def hover_function(chart_id, y_label, y_unit)
         | 
| 283 | 
            +
                  <<"EOT"
         | 
| 284 | 
            +
                    $("##{chart_id}").bind("plothover", function (event, pos, item) {
         | 
| 285 | 
            +
                     if (item) {
         | 
| 286 | 
            +
                       var x = timeToHMS(item.datapoint[0]);
         | 
| 287 | 
            +
                       var y = #{y_label == 'Pace' ? 'timeToHMS(item.datapoint[1] / 60)' :
         | 
| 288 | 
            +
                                                     'item.datapoint[1].toFixed(0)'};
         | 
| 289 | 
            +
                       $("#tooltip").html("<b>#{y_label}:</b> " + y + " #{y_unit}<br/>" +
         | 
| 290 | 
            +
                                          "<b>Time:</b> " + x + " h:m:s")
         | 
| 291 | 
            +
                         .css({top: item.pageY-20, left: item.pageX+15})
         | 
| 292 | 
            +
                         .fadeIn(200);
         | 
| 293 | 
            +
                     } else {
         | 
| 294 | 
            +
                       $("#tooltip").hide();
         | 
| 295 | 
            +
                     }
         | 
| 296 | 
            +
                   });
         | 
| 297 | 
            +
            EOT
         | 
| 298 | 
            +
                end
         | 
| 299 | 
            +
             | 
| 231 300 | 
             
              end
         | 
| 232 301 |  | 
| 233 302 | 
             
            end
         | 
    
        data/lib/postrunner/Main.rb
    CHANGED
    
    | @@ -83,9 +83,9 @@ EOT | |
| 83 83 | 
             
                    end
         | 
| 84 84 |  | 
| 85 85 | 
             
                    opts.separator ""
         | 
| 86 | 
            -
                    opts.separator "Options for the 'import'  | 
| 86 | 
            +
                    opts.separator "Options for the 'import' command:"
         | 
| 87 87 | 
             
                    opts.on('--name name', String,
         | 
| 88 | 
            -
                            'Name  | 
| 88 | 
            +
                            'Name the activity to the specified name') do |n|
         | 
| 89 89 | 
             
                      @name = n
         | 
| 90 90 | 
             
                    end
         | 
| 91 91 |  | 
| @@ -163,6 +163,12 @@ EOT | |
| 163 163 |  | 
| 164 164 | 
             
                def execute_command(args)
         | 
| 165 165 | 
             
                  @activities = ActivitiesDB.new(@db_dir, @cfg)
         | 
| 166 | 
            +
                  if @cfg.get_option(:version) != VERSION
         | 
| 167 | 
            +
                    Log.warn "Version upgrade detected. Database conversion started..."
         | 
| 168 | 
            +
                    @activities.generate_all_html_reports
         | 
| 169 | 
            +
                    @cfg.set_option(:version, VERSION)
         | 
| 170 | 
            +
                    Log.info "Version upgrade completed."
         | 
| 171 | 
            +
                  end
         | 
| 166 172 |  | 
| 167 173 | 
             
                  case (cmd = args.shift)
         | 
| 168 174 | 
             
                  when 'check'
         | 
| @@ -194,6 +200,9 @@ EOT | |
| 194 200 | 
             
                  when 'records'
         | 
| 195 201 | 
             
                    @activities.show_records
         | 
| 196 202 | 
             
                  when 'rename'
         | 
| 203 | 
            +
                    unless (@name = args.shift)
         | 
| 204 | 
            +
                      Log.fatal "You must provide a new name for the activity"
         | 
| 205 | 
            +
                    end
         | 
| 197 206 | 
             
                    process_activities(args, :rename)
         | 
| 198 207 | 
             
                  when 'show'
         | 
| 199 208 | 
             
                    if args.empty?
         | 
    
        data/lib/postrunner/TrackView.rb
    CHANGED
    
    | @@ -117,19 +117,19 @@ EOT | |
| 117 117 | 
             
                def track_points(script)
         | 
| 118 118 | 
             
                  first = true
         | 
| 119 119 | 
             
                  @activity.fit_activity.sessions.each do |session|
         | 
| 120 | 
            -
                    session. | 
| 121 | 
            -
                       | 
| 122 | 
            -
             | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 125 | 
            -
             | 
| 126 | 
            -
                         | 
| 127 | 
            -
             | 
| 128 | 
            -
                         | 
| 129 | 
            -
             | 
| 120 | 
            +
                    session.records.each do |record|
         | 
| 121 | 
            +
                      long = record.position_long
         | 
| 122 | 
            +
                      lat = record.position_lat
         | 
| 123 | 
            +
                      next unless long && lat
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                      if first
         | 
| 126 | 
            +
                        first = false
         | 
| 127 | 
            +
                      else
         | 
| 128 | 
            +
                        script << ","
         | 
| 129 | 
            +
                      end
         | 
| 130 | 
            +
                      script << <<"EOT"
         | 
| 130 131 | 
             
            new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(#{long}, #{lat}).transform(geographic, mercator))
         | 
| 131 132 | 
             
            EOT
         | 
| 132 | 
            -
                      end
         | 
| 133 133 | 
             
                    end
         | 
| 134 134 | 
             
                  end
         | 
| 135 135 | 
             
                end
         | 
    
        data/lib/postrunner/version.rb
    CHANGED
    
    
    
        data/postrunner.gemspec
    CHANGED
    
    | @@ -7,9 +7,9 @@ Gem::Specification.new do |spec| | |
| 7 7 | 
             
              spec.name          = "postrunner"
         | 
| 8 8 | 
             
              spec.version       = PostRunner::VERSION
         | 
| 9 9 | 
             
              spec.authors       = ["Chris Schlaeger"]
         | 
| 10 | 
            -
              spec.email         = ["chris@ | 
| 10 | 
            +
              spec.email         = ["chris@taskjuggler.org"]
         | 
| 11 11 | 
             
              spec.summary       = %q{Application to manage and analyze Garmin FIT files.}
         | 
| 12 | 
            -
              spec.description   = %q{This application will allow you to manage and analyze .FIT files as generated by Garmin GPS devices. The application was developed for the Garmin Forerunner 620. Other devices may  | 
| 12 | 
            +
              spec.description   = %q{This application will allow you to manage and analyze .FIT files such as those generated by Garmin GPS devices. The application was developed for the Garmin Forerunner 620. Other devices may work as well. They need to export the data as USB Mass Storage devices.}
         | 
| 13 13 | 
             
              spec.homepage      = 'https://github.com/scrapper/postrunner'
         | 
| 14 14 | 
             
              spec.license       = "GNU GPL version 2"
         | 
| 15 15 |  | 
| @@ -19,11 +19,11 @@ Gem::Specification.new do |spec| | |
| 19 19 | 
             
              spec.require_paths = ["lib"]
         | 
| 20 20 | 
             
              spec.required_ruby_version = '>=2.0'
         | 
| 21 21 |  | 
| 22 | 
            -
              spec.add_dependency  | 
| 23 | 
            -
              spec.add_dependency  | 
| 22 | 
            +
              spec.add_dependency 'fit4ruby', '~> 0.0.4'
         | 
| 23 | 
            +
              spec.add_dependency 'nokogiri', '~> 1.6'
         | 
| 24 24 |  | 
| 25 | 
            -
              spec.add_development_dependency  | 
| 26 | 
            -
              spec.add_development_dependency  | 
| 27 | 
            -
              spec.add_development_dependency  | 
| 28 | 
            -
              spec.add_development_dependency  | 
| 25 | 
            +
              spec.add_development_dependency 'bundler', '~> 1.6'
         | 
| 26 | 
            +
              spec.add_development_dependency 'rake', '~> 0.9.6'
         | 
| 27 | 
            +
              spec.add_development_dependency 'rspec', '~> 2.14.1'
         | 
| 28 | 
            +
              spec.add_development_dependency 'yard', '~> 0.8.7'
         | 
| 29 29 | 
             
            end
         | 
    
        data/spec/PostRunner_spec.rb
    CHANGED
    
    | @@ -100,7 +100,7 @@ describe PostRunner::Main do | |
| 100 100 | 
             
              end
         | 
| 101 101 |  | 
| 102 102 | 
             
              it 'should rename FILE2.FIT activity' do
         | 
| 103 | 
            -
                postrunner(%w( rename :1  | 
| 103 | 
            +
                postrunner(%w( rename foobar :1 ))
         | 
| 104 104 | 
             
                list = postrunner(%w( list ))
         | 
| 105 105 | 
             
                list.index('FILE2.FIT').should be_nil
         | 
| 106 106 | 
             
                list.index('foobar').should be_a(Fixnum)
         | 
| @@ -122,5 +122,21 @@ describe PostRunner::Main do | |
| 122 122 | 
             
                postrunner(%w( units metric ))
         | 
| 123 123 | 
             
              end
         | 
| 124 124 |  | 
| 125 | 
            +
              it 'should properly upgrade to a new version' do
         | 
| 126 | 
            +
                # Change version in config file to 0.0.0.
         | 
| 127 | 
            +
                rc = PostRunner::RuntimeConfig.new(@db_dir)
         | 
| 128 | 
            +
                rc.set_option(:version, '0.0.0')
         | 
| 129 | 
            +
                # Check that the config file really was changed.
         | 
| 130 | 
            +
                rc = PostRunner::RuntimeConfig.new(@db_dir)
         | 
| 131 | 
            +
                rc.get_option(:version).should == '0.0.0'
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                # Run some command.
         | 
| 134 | 
            +
                postrunner(%w( list ))
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                # Check that version matches the current version again.
         | 
| 137 | 
            +
                rc = PostRunner::RuntimeConfig.new(@db_dir)
         | 
| 138 | 
            +
                rc.get_option(:version).should == PostRunner::VERSION
         | 
| 139 | 
            +
              end
         | 
| 140 | 
            +
             | 
| 125 141 | 
             
            end
         | 
| 126 142 |  | 
    
        data/spec/spec_helper.rb
    CHANGED
    
    | @@ -38,7 +38,7 @@ def create_fit_activity(date, duration_minutes) | |
| 38 38 | 
             
                end
         | 
| 39 39 | 
             
                ts += 60
         | 
| 40 40 | 
             
              end
         | 
| 41 | 
            -
              a.new_session({ :timestamp => ts })
         | 
| 41 | 
            +
              a.new_session({ :timestamp => ts, :sport => 'running' })
         | 
| 42 42 | 
             
              a.new_event({ :timestamp => ts, :event => 'recovery_time',
         | 
| 43 43 | 
             
                            :event_type => 'marker',
         | 
| 44 44 | 
             
                            :data => 2160 })
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: postrunner
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0. | 
| 4 | 
            +
              version: 0.0.6
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Chris Schlaeger
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2014- | 
| 11 | 
            +
            date: 2014-09-03 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: fit4ruby
         | 
| @@ -16,14 +16,14 @@ dependencies: | |
| 16 16 | 
             
                requirements:
         | 
| 17 17 | 
             
                - - ~>
         | 
| 18 18 | 
             
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            -
                    version: 0.0. | 
| 19 | 
            +
                    version: 0.0.4
         | 
| 20 20 | 
             
              type: :runtime
         | 
| 21 21 | 
             
              prerelease: false
         | 
| 22 22 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 23 | 
             
                requirements:
         | 
| 24 24 | 
             
                - - ~>
         | 
| 25 25 | 
             
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            -
                    version: 0.0. | 
| 26 | 
            +
                    version: 0.0.4
         | 
| 27 27 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 28 28 | 
             
              name: nokogiri
         | 
| 29 29 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -56,50 +56,50 @@ dependencies: | |
| 56 56 | 
             
              name: rake
         | 
| 57 57 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 58 | 
             
                requirements:
         | 
| 59 | 
            -
                - -  | 
| 59 | 
            +
                - - ~>
         | 
| 60 60 | 
             
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            -
                    version:  | 
| 61 | 
            +
                    version: 0.9.6
         | 
| 62 62 | 
             
              type: :development
         | 
| 63 63 | 
             
              prerelease: false
         | 
| 64 64 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 65 | 
             
                requirements:
         | 
| 66 | 
            -
                - -  | 
| 66 | 
            +
                - - ~>
         | 
| 67 67 | 
             
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            -
                    version:  | 
| 68 | 
            +
                    version: 0.9.6
         | 
| 69 69 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 70 70 | 
             
              name: rspec
         | 
| 71 71 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 72 72 | 
             
                requirements:
         | 
| 73 | 
            -
                - -  | 
| 73 | 
            +
                - - ~>
         | 
| 74 74 | 
             
                  - !ruby/object:Gem::Version
         | 
| 75 | 
            -
                    version:  | 
| 75 | 
            +
                    version: 2.14.1
         | 
| 76 76 | 
             
              type: :development
         | 
| 77 77 | 
             
              prerelease: false
         | 
| 78 78 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 79 79 | 
             
                requirements:
         | 
| 80 | 
            -
                - -  | 
| 80 | 
            +
                - - ~>
         | 
| 81 81 | 
             
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            -
                    version:  | 
| 82 | 
            +
                    version: 2.14.1
         | 
| 83 83 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 84 84 | 
             
              name: yard
         | 
| 85 85 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 86 86 | 
             
                requirements:
         | 
| 87 | 
            -
                - -  | 
| 87 | 
            +
                - - ~>
         | 
| 88 88 | 
             
                  - !ruby/object:Gem::Version
         | 
| 89 | 
            -
                    version:  | 
| 89 | 
            +
                    version: 0.8.7
         | 
| 90 90 | 
             
              type: :development
         | 
| 91 91 | 
             
              prerelease: false
         | 
| 92 92 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 93 93 | 
             
                requirements:
         | 
| 94 | 
            -
                - -  | 
| 94 | 
            +
                - - ~>
         | 
| 95 95 | 
             
                  - !ruby/object:Gem::Version
         | 
| 96 | 
            -
                    version:  | 
| 97 | 
            -
            description: This application will allow you to manage and analyze .FIT files  | 
| 98 | 
            -
              by Garmin GPS devices. The application was developed for the | 
| 99 | 
            -
              Other devices may  | 
| 100 | 
            -
              Storage devices | 
| 96 | 
            +
                    version: 0.8.7
         | 
| 97 | 
            +
            description: This application will allow you to manage and analyze .FIT files such
         | 
| 98 | 
            +
              as those generated by Garmin GPS devices. The application was developed for the
         | 
| 99 | 
            +
              Garmin Forerunner 620. Other devices may work as well. They need to export the data
         | 
| 100 | 
            +
              as USB Mass Storage devices.
         | 
| 101 101 | 
             
            email:
         | 
| 102 | 
            -
            - chris@ | 
| 102 | 
            +
            - chris@taskjuggler.org
         | 
| 103 103 | 
             
            executables:
         | 
| 104 104 | 
             
            - postrunner
         | 
| 105 105 | 
             
            extensions: []
         |