DZone 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

Snippets has posted 5883 posts at DZone. View Full User Profile

Garmin Forerunner TCX File Processing

03.21.2007
| 10281 views |
  • submit to reddit
        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.

// insert code here..
require "date"
require "rexml/document"
require 'rubygems'
require 'RMagick'

class ForeRunner
  
  M2MI = 1609.344 # meters to miles
  MINIMUM_LAP_TIME = 300 # minimum seconds to count as a full lap
  
  attr_accessor :laps, :total_time, :distance, :calories
  
  def initialize(file)
    @source_doc = REXML::Document.new file
    
    @laps = 0
    @lap_times = Array.new
    @lap_bpm = Array.new
    @full_laps = 0

    @total_time = 0    
    @distance = 0
    @calories = 0
    @map_data = []
    
    self.process_file  
  end
  
  def generate_infographic(output_filename)
    canvas = Magick::Image.new(250, 80)
    map_size = 50
    map_color = 'green'

    max_lap_height = 25
    
    gc = Magick::Draw.new

    # Draw ellipse
    gc.stroke('grey50')
    gc.stroke_width(2)
    gc.fill_opacity(0)

    # draw the relative lap times
    lap = 0
    max_time = @lap_times.max
    @lap_times.each do |s|
      lap += 1
      x = 10 + (lap * 5)
      y = 60 - (s / (max_time / max_lap_height))
      gc.line(x, 60, x, y)
    end

    #draw the heartbeat avg
    gc.stroke('#c9a')
    
    lap = 0
    lx = nil
    ly = nil
    max_bpm = @lap_bpm.max
    min_bpm = @lap_bpm.min
    
    max_bpm_height = 18
    
    @lap_bpm.each do |s|
      lap += 1
      x = 10 + (lap * 5)
      y = 21 - ( (s - min_bpm) / ((max_bpm - min_bpm) / max_bpm_height))
      if !lx
        lx = x
        ly = y
      end

      gc.line(lx, ly, x, y)

      lx = x
      ly = y
    end

    # draw the map
    lat_diff = (@map_data['max_lat'] - @map_data['min_lat']).abs
    lon_diff = (@map_data['max_lon'] - @map_data['min_lon']).abs

    lat_off = lat_diff / map_size
    lon_off = lon_diff / map_size
    
    gc.fill(map_color)
    @map_data['map_data'].each do |i|
      lt = (map_size - ((i[0] - @map_data['min_lat']) / lat_off)).round
      lg = ((i[1] - @map_data['min_lon']) / lon_off).round
      gc.point((240 - map_size) + lg, (65 - map_size) + lt)
    end

    # Annotate
    gc.stroke('transparent')
    gc.fill('black')
    gc.text(120, 15, @distance.to_s[0, 5] + ' mi')
    gc.text(120, 30, (@total_time / 60 / 60).to_s[0, 4] + ' hr')
    gc.text(120, 45, @calories.to_s + ' cal')

    gc.fill('#555')
    total_hr = 0 
    @lap_bpm.each { |hr| total_hr += hr }
    gc.text(15, 32, 'avg bpm: ' + (total_hr / @laps).round.to_s)
    gc.text(15, 75, 'avg pace: ' + ((@total_time / @full_laps) / 60).to_s[0, 4] + ' min/lap')

    gc.draw(canvas)
    canvas.write(output_filename)
  end
  
  protected 
  
  def process_file
    @source_doc.elements.each('TrainingCenterDatabase/Activities/Activity/Lap/*') do |element| 
      if element.name == "TotalTimeSeconds"
        @lap_times << element.text.to_f
        if element.text.to_f > 300  # for removing warmup and warmdown laps
          @total_time += element.text.to_f
          @full_laps += 1
        end
        @laps += 1
      end

      if element.name == "DistanceMeters" 
        @distance += (element.text.to_f / M2MI)
      end

      if element.name == "Calories" 
        @calories += element.text.to_f
      end

      if element.name == "AverageHeartRateBpm" 
        element.elements.each('Value') { |v| @lap_bpm << v.text.to_f }
      end
    end
    @map_data = self.generate_map_points
  end
  
  def generate_map_points  
    map = []

    max_lat = -300
    max_lon = -300
    min_lat = 300
    min_lon = 300

    @source_doc.elements.each('TrainingCenterDatabase/Activities/Activity/Lap/Track/Trackpoint/*') do |element|
      if element.elements['LatitudeDegrees']
        lat = element.elements['LatitudeDegrees'].text.to_f
        lon = element.elements['LongitudeDegrees'].text.to_f

        map << [lat, lon]
        max_lat = lat if (lat > max_lat) 
        max_lon = lon if (lon > max_lon) 
        min_lat = lat if (lat < min_lat) 
        min_lon = lon if (lon < min_lon) 
      end
    end
    {'map_data' => map, 'max_lat' => max_lat, 'min_lat' => min_lat, 'max_lon' => max_lon, 'min_lon' => min_lon}
  end
  

end


if ARGV[0].nil? 
  puts "This program takes one argument, a Garmin Forerunner TCX File"
  puts "Like this: run.rb filename.tcx"
  exit
end

output_filename = ARGV[0].gsub(/\.tcx$/, "-" + Time.now.strftime("%Y%m%d") + ".png")
puts "Generating Graph #{output_filename}"

fr = ForeRunner.new(File.new(ARGV[0]))
fr.generate_infographic(output_filename)