Never been to DZone Snippets before?

Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

About this user

Scott Chacon

« Newer Snippets
Older Snippets »
Showing 1-1 of 1 total  RSS 

Garmin Forerunner TCX file processing

This is a small ruby file I wrote to process the TCX file that I can download from MotionBased website that is processed from the data off my Garmin Forerunner 305. It processes the XML file and generates a little badge/infographic that I can put on a website.

   1  
   2  // insert code here..
   3  require "date"
   4  require "rexml/document"
   5  require 'rubygems'
   6  require 'RMagick'
   7  
   8  class ForeRunner
   9    
  10    M2MI = 1609.344 # meters to miles
  11    MINIMUM_LAP_TIME = 300 # minimum seconds to count as a full lap
  12    
  13    attr_accessor :laps, :total_time, :distance, :calories
  14    
  15    def initialize(file)
  16      @source_doc = REXML::Document.new file
  17      
  18      @laps = 0
  19      @lap_times = Array.new
  20      @lap_bpm = Array.new
  21      @full_laps = 0
  22  
  23      @total_time = 0    
  24      @distance = 0
  25      @calories = 0
  26      @map_data = []
  27      
  28      self.process_file  
  29    end
  30    
  31    def generate_infographic(output_filename)
  32      canvas = Magick::Image.new(250, 80)
  33      map_size = 50
  34      map_color = 'green'
  35  
  36      max_lap_height = 25
  37      
  38      gc = Magick::Draw.new
  39  
  40      # Draw ellipse
  41      gc.stroke('grey50')
  42      gc.stroke_width(2)
  43      gc.fill_opacity(0)
  44  
  45      # draw the relative lap times
  46      lap = 0
  47      max_time = @lap_times.max
  48      @lap_times.each do |s|
  49        lap += 1
  50        x = 10 + (lap * 5)
  51        y = 60 - (s / (max_time / max_lap_height))
  52        gc.line(x, 60, x, y)
  53      end
  54  
  55      #draw the heartbeat avg
  56      gc.stroke('#c9a')
  57      
  58      lap = 0
  59      lx = nil
  60      ly = nil
  61      max_bpm = @lap_bpm.max
  62      min_bpm = @lap_bpm.min
  63      
  64      max_bpm_height = 18
  65      
  66      @lap_bpm.each do |s|
  67        lap += 1
  68        x = 10 + (lap * 5)
  69        y = 21 - ( (s - min_bpm) / ((max_bpm - min_bpm) / max_bpm_height))
  70        if !lx
  71          lx = x
  72          ly = y
  73        end
  74  
  75        gc.line(lx, ly, x, y)
  76  
  77        lx = x
  78        ly = y
  79      end
  80  
  81      # draw the map
  82      lat_diff = (@map_data['max_lat'] - @map_data['min_lat']).abs
  83      lon_diff = (@map_data['max_lon'] - @map_data['min_lon']).abs
  84  
  85      lat_off = lat_diff / map_size
  86      lon_off = lon_diff / map_size
  87      
  88      gc.fill(map_color)
  89      @map_data['map_data'].each do |i|
  90        lt = (map_size - ((i[0] - @map_data['min_lat']) / lat_off)).round
  91        lg = ((i[1] - @map_data['min_lon']) / lon_off).round
  92        gc.point((240 - map_size) + lg, (65 - map_size) + lt)
  93      end
  94  
  95      # Annotate
  96      gc.stroke('transparent')
  97      gc.fill('black')
  98      gc.text(120, 15, @distance.to_s[0, 5] + ' mi')
  99      gc.text(120, 30, (@total_time / 60 / 60).to_s[0, 4] + ' hr')
 100      gc.text(120, 45, @calories.to_s + ' cal')
 101  
 102      gc.fill('#555')
 103      total_hr = 0 
 104      @lap_bpm.each { |hr| total_hr += hr }
 105      gc.text(15, 32, 'avg bpm: ' + (total_hr / @laps).round.to_s)
 106      gc.text(15, 75, 'avg pace: ' + ((@total_time / @full_laps) / 60).to_s[0, 4] + ' min/lap')
 107  
 108      gc.draw(canvas)
 109      canvas.write(output_filename)
 110    end
 111    
 112    protected 
 113    
 114    def process_file
 115      @source_doc.elements.each('TrainingCenterDatabase/Activities/Activity/Lap/*') do |element| 
 116        if element.name == "TotalTimeSeconds"
 117          @lap_times << element.text.to_f
 118          if element.text.to_f > 300  # for removing warmup and warmdown laps
 119            @total_time += element.text.to_f
 120            @full_laps += 1
 121          end
 122          @laps += 1
 123        end
 124  
 125        if element.name == "DistanceMeters" 
 126          @distance += (element.text.to_f / M2MI)
 127        end
 128  
 129        if element.name == "Calories" 
 130          @calories += element.text.to_f
 131        end
 132  
 133        if element.name == "AverageHeartRateBpm" 
 134          element.elements.each('Value') { |v| @lap_bpm << v.text.to_f }
 135        end
 136      end
 137      @map_data = self.generate_map_points
 138    end
 139    
 140    def generate_map_points  
 141      map = []
 142  
 143      max_lat = -300
 144      max_lon = -300
 145      min_lat = 300
 146      min_lon = 300
 147  
 148      @source_doc.elements.each('TrainingCenterDatabase/Activities/Activity/Lap/Track/Trackpoint/*') do |element|
 149        if element.elements['LatitudeDegrees']
 150          lat = element.elements['LatitudeDegrees'].text.to_f
 151          lon = element.elements['LongitudeDegrees'].text.to_f
 152  
 153          map << [lat, lon]
 154          max_lat = lat if (lat > max_lat) 
 155          max_lon = lon if (lon > max_lon) 
 156          min_lat = lat if (lat < min_lat) 
 157          min_lon = lon if (lon < min_lon) 
 158        end
 159      end
 160      {'map_data' => map, 'max_lat' => max_lat, 'min_lat' => min_lat, 'max_lon' => max_lon, 'min_lon' => min_lon}
 161    end
 162    
 163  
 164  end
 165  
 166  
 167  if ARGV[0].nil? 
 168    puts "This program takes one argument, a Garmin Forerunner TCX File"
 169    puts "Like this: run.rb filename.tcx"
 170    exit
 171  end
 172  
 173  output_filename = ARGV[0].gsub(/\.tcx$/, "-" + Time.now.strftime("%Y%m%d") + ".png")
 174  puts "Generating Graph #{output_filename}"
 175  
 176  fr = ForeRunner.new(File.new(ARGV[0]))
 177  fr.generate_infographic(output_filename)
« Newer Snippets
Older Snippets »
Showing 1-1 of 1 total  RSS