GpsTrail 0.3

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.
@@ -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
+