GpsTrail 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ class BaseConverter
2
+ @@converters = Array.new
3
+
4
+ def BaseConverter.inherited(sub)
5
+ @@converters.push(sub.to_s)
6
+ end
7
+
8
+ def BaseConverter.converters
9
+ @@converters
10
+ end
11
+
12
+ end
@@ -0,0 +1,54 @@
1
+ require 'rexml/document'
2
+ require 'journey'
3
+ require 'point'
4
+ require 'sample'
5
+ require 'fileutil'
6
+ require 'template'
7
+
8
+ include REXML
9
+
10
+ class Forerunner301 < BaseConverter
11
+
12
+ @@device_name = "Forerunner301"
13
+
14
+ def convert(xml_doc)
15
+ convert_to_domain_model(xml_doc)
16
+ end
17
+
18
+ def convert_to_domain_model(doc)
19
+ journeys = Array.new
20
+ doc.elements.each("//Run/") do |journey_xml|
21
+ journeys.push(process_journey(journey_xml))
22
+ end
23
+ journeys.sort
24
+ end
25
+
26
+ def process_journey(journey_xml)
27
+ journey = Journey.new('Run')
28
+
29
+ journey_xml.each_element("Lap/Track/Trackpoint") do |trackpoint|
30
+ process_trackpoint(journey, trackpoint)
31
+ end
32
+
33
+ journey
34
+ end
35
+
36
+ def process_trackpoint(journey, trackpoint)
37
+ position = trackpoint.get_elements("Position")[0]
38
+
39
+ if(position)
40
+ time = trackpoint.get_elements("Time")[0].get_text
41
+ lat = position.get_elements("LatitudeDegrees")[0].get_text
42
+ long = position.get_elements("LongitudeDegrees")[0].get_text
43
+ alt = position.get_elements("AltitudeMeters")[0].get_text
44
+ point = Point.new(lat.to_s.to_f, long.to_s.to_f, alt.to_s.to_f)
45
+ sample = Sample.new(time, point)
46
+ journey.add_sample(sample)
47
+ end
48
+ end
49
+
50
+ def to_s
51
+ @@device_name
52
+ end
53
+
54
+ end
@@ -0,0 +1,10 @@
1
+ class FileUtil
2
+
3
+ # Illegal characters in Windows filenames (XP SP2) are:
4
+ # \ / : * ? " < > |
5
+
6
+ def FileUtil.convert_to_win_filename!(filename)
7
+ filename.gsub(/[\\\/:*?"<>|]/, '-')
8
+ end
9
+
10
+ end
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <kml xmlns="http://earth.google.com/kml/2.0">
3
+ <Document>
4
+ <name>Forerunner 301</name>
5
+ <visibility>0</visibility>
6
+ <open>1</open>
7
+ <Folder>
8
+ <name>Forerunner 301</name>
9
+ <open>1</open>
10
+ <Folder>
11
+ <name>${journey_type}</name>
12
+ <open>1</open>
13
+ ${placemarks}
14
+ </Folder>
15
+ </Folder>
16
+ </Document>
17
+ </kml>
@@ -0,0 +1,113 @@
1
+ #
2
+ # == Synonpsis
3
+ #
4
+ # GpsTrail lets you view your GPS trips in Google Earth.
5
+ #
6
+ # == Usage
7
+ #
8
+ # ruby gpstrail.rb [ -ld] | [ -h | -- help] [ -d gpsdevice] [ -f file ]
9
+ #
10
+ # -ld
11
+ # List the supported GPS devices
12
+ # -d
13
+ # Specify a listed device
14
+ # -f
15
+ # Specify the file containing the output from the specified device
16
+ #
17
+ # == Example
18
+ #
19
+ # ruby gpstrail.rb -d Forerunner301 -f "C:\Sport\history.hst"
20
+
21
+ require 'find'
22
+ require 'rexml/document'
23
+ require 'journey'
24
+ require 'point'
25
+ require 'sample'
26
+ require 'fileutil'
27
+ require 'template'
28
+ require 'baseconverter'
29
+ require 'rdoc/usage'
30
+ require 'optparse'
31
+ require 'ostruct'
32
+
33
+ include REXML
34
+
35
+ class GpsTrail
36
+
37
+ def process(file_to_process, converter)
38
+ start_time = Time.now
39
+ puts "Processing #{file_to_process}\nPlease wait..."
40
+ STDOUT.flush
41
+ xml_doc = Document.new(File.open(file_to_process))
42
+ journeys = converter.convert(xml_doc)
43
+
44
+ placemarks_kml = ""
45
+ journeys.each do |journey|
46
+ placemarks_kml += create_placemark_kml(journey)
47
+ end
48
+
49
+ map = {"placemarks" => placemarks_kml, "journey_type" => "Run"}
50
+ kml = Template.load_and_substitute('folder_template.kml', map)
51
+
52
+ kml_file_name = File.join(ENV['HOMEPATH'], "gpstrail.kml")
53
+ kml_file = File.new(kml_file_name, "w")
54
+ kml_file.puts(kml)
55
+ kml_file.close
56
+
57
+ total_time = Time.now - start_time
58
+ puts "Output written to #{kml_file_name}"
59
+ puts "Processed #{journeys.size} journeys in #{total_time}s"
60
+ end
61
+
62
+ def create_placemark_kml(journey)
63
+ look_at_lat, look_at_long = journey.find_center
64
+ coords = create_coords_list(journey)
65
+
66
+ map = { "look_at_long" => look_at_long.to_s, "look_at_lat" => look_at_lat.to_s,
67
+ "range" => journey.find_distance_of_longest_vector.to_s,
68
+ "tilt" => 45.to_s, "heading" => 0.to_s, "coordinates" => coords,
69
+ "name" => journey.start_time.to_s}
70
+
71
+ Template.load_and_substitute('line_template.kml', map)
72
+ end
73
+
74
+ def create_coords_list(journey)
75
+ coords = ""
76
+ journey.samples.each do |sample|
77
+ point = sample.point
78
+ coords += point.long.to_s + "," + point.lat.to_s + ",0\n"
79
+ end
80
+ coords
81
+ end
82
+
83
+ def load_converters
84
+ Find.find("./converters") do |filename|
85
+ load filename if filename =~ /rb$/
86
+ end
87
+
88
+ BaseConverter.converters
89
+ end
90
+
91
+ end
92
+
93
+ $: << 'converters'
94
+ options = OpenStruct.new
95
+
96
+ opts = OptionParser.new
97
+ opts.on("-h", "--help") {RDoc::usage}
98
+ opts.on("-ld") do
99
+ puts "Device List:\n"
100
+ GpsTrail.new.load_converters.each {|converter| puts "\t#{converter}" }
101
+ options.devices_listed = true
102
+ end
103
+ opts.on("-d" "=dev_name") {|dn| options.device_name = dn}
104
+ opts.on("-f", "=file_name") {|fn| options.file_name = fn}
105
+ opts.parse(ARGV) rescue RDoc::usage
106
+
107
+ if(options == OpenStruct.new)
108
+ RDoc::usage
109
+ elsif(options.device_name && options.file_name)
110
+ require options.device_name
111
+ dev = Object.const_get(options.device_name).new
112
+ GpsTrail.new.process(options.file_name, dev)
113
+ end
@@ -0,0 +1,70 @@
1
+ class Journey
2
+ include Enumerable
3
+
4
+ attr_reader :samples, :type
5
+
6
+ def initialize(type)
7
+ @samples = Array.new
8
+ @type = type
9
+ @max_long_index, @min_long_index = 0, 0
10
+ @max_lat_index, @min_lat_index = 0, 0
11
+ end
12
+
13
+ def add_sample(sample)
14
+ @samples.push(sample)
15
+ new_long = sample.long
16
+ new_lat = sample.lat
17
+
18
+ @max_long_index = @samples.length-1 if new_long > @samples[@max_long_index].long
19
+ @min_long_index = @samples.length-1 if new_long < @samples[@min_long_index].long
20
+ @max_lat_index = @samples.length-1 if new_lat > @samples[@max_lat_index].lat
21
+ @min_lat_index = @samples.length-1 if new_lat < @samples[@min_lat_index].lat
22
+ end
23
+
24
+ def [](index)
25
+ @samples[index]
26
+ end
27
+
28
+ def find_center
29
+ lat = (@samples[@max_lat_index].lat + @samples[@min_lat_index].lat) / 2
30
+ long = (@samples[@max_long_index].long + @samples[@min_long_index].long) / 2
31
+ [lat, long]
32
+ end
33
+
34
+ def find_distance_of_longest_vector
35
+ max_long_point = @samples[@max_long_index].point
36
+ min_long_point = @samples[@min_long_index].point
37
+ long_distance = max_long_point.distance_from(min_long_point)
38
+
39
+ max_lat_point = @samples[@max_lat_index].point
40
+ min_lat_point = @samples[@min_lat_index].point
41
+ lat_distance = max_lat_point.distance_from(min_lat_point)
42
+ [lat_distance, long_distance].max
43
+ end
44
+
45
+ def size
46
+ @samples.size
47
+ end
48
+
49
+ def each
50
+ samples.each {|sample| yield sample}
51
+ end
52
+
53
+ def <=>(other)
54
+ self.start_time <=> other.start_time
55
+ end
56
+
57
+ def start_time
58
+ samples[0].time
59
+ end
60
+
61
+ def to_s
62
+ time = @samples[0].time
63
+ journey = "#{journey_type} at #{time}:\n"
64
+ @samples.each do |sample|
65
+ journey += sample.to_s + "\n"
66
+ end
67
+ journey
68
+ end
69
+
70
+ end
@@ -0,0 +1,28 @@
1
+ <Placemark>
2
+ <name>${name}</name>
3
+ <LookAt>
4
+ <longitude>${look_at_long}</longitude>
5
+ <latitude>${look_at_lat}</latitude>
6
+ <range>${range}</range>
7
+ <tilt>${tilt}</tilt>
8
+ <heading>${heading}</heading>
9
+ </LookAt>
10
+ <visibility>0</visibility>
11
+ <open>0</open>
12
+ <Style>
13
+ <LineStyle>
14
+ <color>ff00ffff</color>
15
+ </LineStyle>
16
+ <PolyStyle>
17
+ <color>7f00ff00</color>
18
+ </PolyStyle>
19
+ </Style>
20
+ <LineString>
21
+ <extrude>0</extrude>
22
+ <tessellate>1</tessellate>
23
+ <altitudeMode>absolute</altitudeMode>
24
+ <coordinates>
25
+ ${coordinates}
26
+ </coordinates>
27
+ </LineString>
28
+ </Placemark>
@@ -0,0 +1,51 @@
1
+ # A Point specifies a latitude, longitude and elevation
2
+ # Note that the elevation is not considered in distance calculations
3
+ class Point
4
+
5
+ AVG_CIRCUMFERANCE_OF_EARTH = 6372795
6
+ attr_reader :lat, :long, :elevation
7
+
8
+ def initialize(lat, long, elevation=0.0)
9
+ if(lat.nil? or long.nil?)
10
+ raise ArgumentError
11
+ end
12
+
13
+ @lat = lat
14
+ @long = long
15
+ @elevation = elevation
16
+ end
17
+
18
+ def to_s
19
+ "Lat: #@lat Long: #@long Elevation: #@elevation"
20
+ end
21
+
22
+ # Return the distance between one point and another in metres.
23
+ # Note that distance_from does not take elevation into account.
24
+ # The formula used is based on great circle distance formula from spherical
25
+ # goemetry, but as the Earth is not a sphere a small error may result (up to 0.5%).
26
+ # The formula used is that listed on http://en.wikipedia.org/wiki/Great-circle_distance
27
+ def distance_from(other)
28
+ lat1, lat2 = Point.degrees_to_radians(@lat), Point.degrees_to_radians(other.lat)
29
+ long1, long2 = Point.degrees_to_radians(@long), Point.degrees_to_radians(other.long)
30
+ long_diff = (long1 - long2).abs
31
+
32
+ dvd1 = (Math.cos(lat2) * Math.sin(long_diff))**2
33
+ dvd2 = ((Math.cos(lat1)*Math.sin(lat2)) - (Math.sin(lat1)*Math.cos(lat2)*Math.cos(long_diff)))**2
34
+ dividend = Math.sqrt(dvd1 + dvd2)
35
+
36
+ dvr1 = Math.sin(lat1)*Math.sin(lat2)
37
+ dvr2 = Math.cos(lat1)*Math.cos(lat2)*Math.cos(long_diff)
38
+ divisor = dvr1 + dvr2
39
+
40
+ arctan_arg = dividend / divisor
41
+
42
+ angular_diff = Math.atan(arctan_arg)
43
+
44
+ angular_diff * AVG_CIRCUMFERANCE_OF_EARTH
45
+ end
46
+
47
+ def Point.degrees_to_radians(degrees)
48
+ degrees * ((2*Math::PI)/360)
49
+ end
50
+
51
+ end
@@ -0,0 +1,26 @@
1
+ class Sample
2
+
3
+ attr_reader :time, :point
4
+
5
+ def initialize(time, point)
6
+ if(time.nil? or point.nil?)
7
+ raise ArgumentError
8
+ end
9
+
10
+ @time = time
11
+ @point = point
12
+ end
13
+
14
+ def to_s
15
+ @time.to_s + " " + @point.to_s
16
+ end
17
+
18
+ def lat
19
+ point.lat
20
+ end
21
+
22
+ def long
23
+ point.long
24
+ end
25
+
26
+ end
@@ -0,0 +1,18 @@
1
+ class Template
2
+
3
+ def Template.substitute(template, key_value_map)
4
+ result = template
5
+ key_value_map.each do |key, value|
6
+ result = result.gsub("${#{key}}", value)
7
+ end
8
+ result
9
+ end
10
+
11
+ def Template.load_and_substitute(template_name, key_value_map)
12
+ f = File.new(template_name)
13
+ template = f.readlines.to_s
14
+ f.close
15
+ Template.substitute(template, key_value_map)
16
+ end
17
+
18
+ end
data/readme ADDED
@@ -0,0 +1,43 @@
1
+ # == Overview
2
+ #
3
+ # GpsTrail lets you view your GPS trips in Google Earth.
4
+ #
5
+ # == How To Use
6
+ #
7
+ # 1. Open Garmin Training Center
8
+ # 2. Click File -> Export History... -> Save
9
+ # 3. Run converter.rb and specify the location of the file you just saved
10
+ # 4. In Windows Explorer, navigate to the output directory
11
+ # (e.g. C:\Documents and Settings\Paul) and double click on forerunner301.kml
12
+ #
13
+ # Note: You'll need a Ruby interpreter installed on your Windows system in order
14
+ # to run the converter. If you don't have one, download the installer from
15
+ # http://rubyinstaller.rubyforge.org/wiki/wiki.pl
16
+ #
17
+ # == Known Issues (in the order I intend to fix them)
18
+ #
19
+ # - Currently only exports Runs (not Bike rides, Multisport or Other)
20
+ # - The KML file could become prohibitively large
21
+ # - Try KMZ rather than KML
22
+ # - Offer an option to create a single Google Earth file per trip
23
+ # - The message specifying the output file could be improved
24
+ # - Sometimes trips aren't perfectly centered in the Google Earth frame
25
+ # - Some paths could be tidied (e.g. tc_template.rb)
26
+ #
27
+ # == Contact
28
+ #
29
+ # Comments and suggestions welcome at paul.p.carey@gmail.com
30
+ #
31
+ # == Copyright 2006 Paul Carey
32
+ #
33
+ # Licensed under the Apache License, Version 2.0 (the "License");
34
+ # you may not use this file except in compliance with the License.
35
+ # You may obtain a copy of the License at
36
+ #
37
+ # http://www.apache.org/licenses/LICENSE-2.0
38
+ #
39
+ # Unless required by applicable law or agreed to in writing, software
40
+ # distributed under the License is distributed on an "AS IS" BASIS,
41
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
42
+ # See the License for the specific language governing permissions and
43
+ # limitations under the License.
@@ -0,0 +1,13 @@
1
+ require 'test/unit'
2
+ require 'fileutil'
3
+
4
+ class FileUtilTest < Test::Unit::TestCase
5
+
6
+ def test_convert_to_win_filename
7
+ chars = %W(\\ / : * ? \" < > |)
8
+ chars.each do |char|
9
+ assert_equal("1-1", FileUtil.convert_to_win_filename!("1#{char}1"))
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,37 @@
1
+ require 'test/unit'
2
+ require 'journey'
3
+ require 'point'
4
+ require 'sample'
5
+
6
+ class JourneyTest < Test::Unit::TestCase
7
+
8
+ def setup
9
+ now = Time.now
10
+ later = now + 60
11
+ p1 = Point.new(54.0, 2.0, 70)
12
+ p2 = Point.new(55.0, -1.0, 80)
13
+ @s1 = Sample.new(now, p1)
14
+ @s2 = Sample.new(later, p2)
15
+
16
+ @j1 = Journey.new('run')
17
+ @j1.add_sample(@s1)
18
+ @j1.add_sample(@s2)
19
+ end
20
+
21
+ def test_journey_type
22
+ assert_equal('run', @j1.type)
23
+ end
24
+
25
+ def test_run
26
+ assert_equal(2, @j1.size)
27
+ assert_equal(@s1, @j1[0])
28
+ assert_equal(@s2, @j1[1])
29
+ end
30
+
31
+ def test_find_center
32
+ lat, long = @j1.find_center
33
+ assert_equal(54.5, lat)
34
+ assert_equal(0.5, long)
35
+ end
36
+
37
+ end
@@ -0,0 +1,40 @@
1
+ require 'test/unit'
2
+ require 'point'
3
+
4
+ class PointTest < Test::Unit::TestCase
5
+
6
+ def test_valid_point
7
+ p1 = Point.new(2.3, 4.5, 50)
8
+ assert_equal(2.3, p1.lat)
9
+ assert_equal(4.5, p1.long)
10
+ assert_equal(50, p1.elevation)
11
+ end
12
+
13
+ def test_variable_length_constructor
14
+ p1 = Point.new(5, 10)
15
+ assert_equal(0, p1.elevation)
16
+ end
17
+
18
+ def test_nil_atts_passed_to_constructor
19
+ assert_raise(ArgumentError) {Point.new(nil, 1, 1)}
20
+ assert_raise(ArgumentError) {Point.new(1, nil, 1)}
21
+ end
22
+
23
+ def test_degrees_to_radians
24
+ assert_in_delta(0.0174532925, Point.degrees_to_radians(1), 0.00001)
25
+ end
26
+
27
+ def test_distance1
28
+ bna = Point.new(36.12, -86.67)
29
+ lax = Point.new(33.94, -118.4)
30
+ distance = bna.distance_from(lax)
31
+ assert_in_delta(2887000, distance, 10000)
32
+ end
33
+
34
+ def test_distance2
35
+ dublin = Point.new(53.21, -6.15)
36
+ lusaka = Point.new(-15.28, 28.16)
37
+ assert_in_delta(8300000, dublin.distance_from(lusaka), 10000)
38
+ end
39
+
40
+ end
@@ -0,0 +1,28 @@
1
+ require 'test/unit'
2
+ require 'point'
3
+ require 'sample'
4
+
5
+ class SampleTest < Test::Unit::TestCase
6
+
7
+ def setup
8
+ @now = Time.now
9
+ @p1 = Point.new(54, 6.8, 70)
10
+ end
11
+
12
+ def test_valid_sample
13
+ s1 = Sample.new(@now, @p1)
14
+ assert_equal(@now, s1.time)
15
+ assert_equal(@p1, s1.point)
16
+ end
17
+
18
+ def test_nil_time
19
+ assert_raise(ArgumentError) { Sample.new(nil, @p1) }
20
+ end
21
+
22
+ def test_nil_point
23
+ assert_raise(ArgumentError) do
24
+ Sample.new(@now, nil)
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1 @@
1
+ How ${pronoun} doin'?
@@ -0,0 +1,32 @@
1
+ require 'test/unit'
2
+ require 'template'
3
+
4
+ class TemplateTest < Test::Unit::TestCase
5
+
6
+ def test_no_substitution
7
+ s = "hello name"
8
+ result = Template.substitute(s, Hash.new)
9
+ assert_equal("hello name", result)
10
+ end
11
+
12
+ def test_single_substitution
13
+ s = "hello ${name}"
14
+ m = { 'name' => 'paul'}
15
+ result = Template.substitute(s, m)
16
+ assert_equal("hello paul", result)
17
+ end
18
+
19
+ def test_multiple_substitution
20
+ s = "Hello ${name}. This is <${de_ity}> speaking. It's good to meet you ${name}."
21
+ m = { 'name' => 'Paul', 'de_ity' => 'God'}
22
+ result = Template.substitute(s, m)
23
+ assert_equal("Hello Paul. This is <God> speaking. It's good to meet you Paul.", result)
24
+ end
25
+
26
+ def test_substitution_from_file
27
+ m = { 'pronoun' => 'you'}
28
+ result = Template.load_and_substitute('../test/tc_template.kml', m)
29
+ assert_equal("How you doin'?", result)
30
+ end
31
+
32
+ end
@@ -0,0 +1,7 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), "..", "test")
2
+ require 'test/unit'
3
+ require 'tc_point'
4
+ require 'tc_journey'
5
+ require 'tc_sample'
6
+ require 'tc_template'
7
+ require 'tc_forerunner'
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: GpsTrail
5
+ version: !ruby/object:Gem::Version
6
+ version: "0.3"
7
+ date: 2006-02-28 00:00:00 +00:00
8
+ summary: GpsTrail lets you view your Forerunner 301 trips in Google Earth
9
+ require_paths:
10
+ - lib
11
+ email: paul.p.carey@gmail.com
12
+ homepage:
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: converter
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Paul Carey
30
+ files:
31
+ - lib/baseconverter.rb
32
+ - lib/converters
33
+ - lib/fileutil.rb
34
+ - lib/folder_template.kml
35
+ - lib/gpstrail.rb
36
+ - lib/journey.rb
37
+ - lib/line_template.kml
38
+ - lib/point.rb
39
+ - lib/sample.rb
40
+ - lib/template.rb
41
+ - lib/converters/forerunner301.rb
42
+ - test/tc_fileutil.rb
43
+ - test/tc_journey.rb
44
+ - test/tc_point.rb
45
+ - test/tc_sample.rb
46
+ - test/tc_template.kml
47
+ - test/tc_template.rb
48
+ - test/ts_gpstrail.rb
49
+ - readme
50
+ test_files:
51
+ - test/ts_gpstrail.rb
52
+ rdoc_options: []
53
+
54
+ extra_rdoc_files:
55
+ - readme
56
+ executables: []
57
+
58
+ extensions: []
59
+
60
+ requirements: []
61
+
62
+ dependencies: []
63
+