gpx 0.1

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.
data/README ADDED
@@ -0,0 +1,43 @@
1
+ = GPX Gem
2
+ Copyright (C) 2006 Doug Fales
3
+ Doug Fales mailto:doug.fales@gmail.com
4
+
5
+ == What It Does
6
+ This library reads GPX files and provides an API for reading and manipulating
7
+ the data as objects. For more info on the GPX format, see
8
+ http://www.topografix.com/gpx.asp.
9
+
10
+ In addition to parsing GPX files, this library is capable of converting
11
+ Magellan NMEA files to GPX, and writing new GPX files. It can crop and delete
12
+ rectangular areas within a file, and it also calculates some meta-data about
13
+ the tracks and points in a file (such as distance, duration, average speed,
14
+ etc).
15
+
16
+ == Examples
17
+ Reading a GPX file, and cropping its contents to a given area:
18
+ gpx = GPX::GPXFile.new(:gpx_file => filename) # Read GPX file
19
+ bounds = GPX::Bounds.new(params) # Create a rectangular area to crop
20
+ gpx.crop(bounds) # Crop it
21
+ gpx.write(filename) # Save it
22
+
23
+ Converting a Magellan track log to GPX:
24
+ if GPX::MagellanTrackLog::is_magellan_file?(filename)
25
+ GPX::MagellanTrackLog::convert_to_gpx(filename, "#{filename}.gpx")
26
+ end
27
+
28
+
29
+ == Notes
30
+ This library was written to bridge the gap between my Garmin Geko
31
+ and my website, WalkingBoss.org. For that reason, it has always been more of a
32
+ work-in-progress than an attempt at full GPX compliance. The track side of the
33
+ library has seen much more use than the route/waypoint side, so if you're doing
34
+ something with routes or waypoints, you may need to tweak some things.
35
+
36
+ Since this code uses REXML to read an entire GPX file into memory, it is not
37
+ the fastest possible solution for working with GPX data, especially if you are
38
+ working with tracks from several days or weeks.
39
+
40
+ Finally, it should be noted that none of the distance/speed calculation or
41
+ crop/delete code has been tested under International Date Line-crossing
42
+ conditions. That particular part of the code will likely be unreliable if
43
+ you're zig-zagging across 180 degrees longitude routinely.
data/Rakefile ADDED
@@ -0,0 +1,81 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+ require 'rake/gempackagetask'
6
+ require File.dirname(__FILE__) + '/lib/gpx'
7
+
8
+ PKG_VERSION = GPX::VERSION
9
+ PKG_NAME = "gpx"
10
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
11
+ RUBY_FORGE_PROJECT = "gpx"
12
+ RUBY_FORGE_USER = ENV['RUBY_FORGE_USER'] || "dougfales"
13
+ RELEASE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
14
+
15
+ PKG_FILES = FileList[
16
+ "lib/**/*", "bin/*", "tests/**/*", "[A-Z]*", "Rakefile", "doc/**/*"
17
+ ]
18
+
19
+ desc "Default Task"
20
+ task :default => [ :test ]
21
+
22
+ # Run the unit tests
23
+ desc "Run all unit tests"
24
+ Rake::TestTask.new("test") { |t|
25
+ t.libs << "lib"
26
+ t.pattern = 'tests/*_test.rb'
27
+ t.verbose = true
28
+ }
29
+
30
+ # Make a console, useful when working on tests
31
+ desc "Generate a test console"
32
+ task :console do
33
+ verbose( false ) { sh "irb -I lib/ -r 'gpx'" }
34
+ end
35
+
36
+ # Genereate the RDoc documentation
37
+ desc "Create documentation"
38
+ Rake::RDocTask.new("doc") { |rdoc|
39
+ rdoc.title = "Ruby GPX API"
40
+ rdoc.rdoc_dir = 'html'
41
+ rdoc.rdoc_files.include('README')
42
+ rdoc.rdoc_files.include('lib/**/*.rb')
43
+ }
44
+
45
+ # Genereate the package
46
+ spec = Gem::Specification.new do |s|
47
+
48
+ s.name = 'gpx'
49
+ s.version = PKG_VERSION
50
+ s.summary = <<-EOF
51
+ A basic API for reading and writing GPX files.
52
+ EOF
53
+ s.description = <<-EOF
54
+ A basic API for reading and writing GPX files.
55
+ EOF
56
+
57
+ s.files = PKG_FILES
58
+
59
+ s.require_path = 'lib'
60
+ s.autorequire = 'gpx'
61
+
62
+ s.has_rdoc = true
63
+
64
+ s.author = "Doug Fales"
65
+ s.email = "doug.fales@gmail.com"
66
+ s.homepage = "http://gpx.rubyforge.com/"
67
+ end
68
+
69
+ Rake::GemPackageTask.new(spec) do |pkg|
70
+ pkg.need_zip = true
71
+ pkg.need_tar = true
72
+ end
73
+
74
+ desc "Report code statistics (KLOCs, etc) from the application"
75
+ task :stats do
76
+ require 'code_statistics'
77
+ CodeStatistics.new(
78
+ ["Library", "lib"],
79
+ ["Units", "tests"]
80
+ ).to_s
81
+ end
data/lib/gpx.rb ADDED
@@ -0,0 +1,37 @@
1
+ #--
2
+ # Copyright (c) 2006 Doug Fales
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+ $:.unshift(File.dirname(__FILE__))
24
+ require 'rexml/document'
25
+ require 'date'
26
+ require 'time'
27
+ require 'csv'
28
+ require 'gpx/gpx'
29
+ require 'gpx/gpx_file'
30
+ require 'gpx/bounds'
31
+ require 'gpx/track'
32
+ require 'gpx/route'
33
+ require 'gpx/segment'
34
+ require 'gpx/point'
35
+ require 'gpx/trackpoint'
36
+ require 'gpx/waypoint'
37
+ require 'gpx/magellan_track_log'
data/lib/gpx/bounds.rb ADDED
@@ -0,0 +1,83 @@
1
+ #--
2
+ # Copyright (c) 2006 Doug Fales
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+ module GPX
24
+ class Bounds < Base
25
+ attr_accessor :min_lat, :max_lat, :max_lon, :min_lon, :center_lat, :center_lon
26
+
27
+ # Creates a new bounds object with the passed-in min and max longitudes
28
+ # and latitudes.
29
+ def initialize(opts = { :min_lat => 90.0, :max_lat => -90.0, :min_lon => 180.0, :max_lon => -180.0})
30
+ @min_lat, @max_lat = opts[:min_lat].to_f, opts[:max_lat].to_f
31
+ @min_lon, @max_lon = opts[:min_lon].to_f, opts[:max_lon].to_f
32
+ end
33
+
34
+ # Returns the middle latitude.
35
+ def center_lat
36
+ distance = (max_lat - min_lat)/2.0
37
+ (min_lat + distance)
38
+ end
39
+
40
+ # Returns the middle longitude.
41
+ def center_lon
42
+ distance = (max_lon - min_lon)/2.0
43
+ (min_lon + distance)
44
+ end
45
+
46
+ def to_xml
47
+ bnd = REXML::Element.new('bounds')
48
+ bnd.attributes['min_lat'] = min_lat
49
+ bnd.attributes['min_lon'] = min_lon
50
+ bnd.attributes['max_lat'] = max_lat
51
+ bnd.attributes['max_lon'] = max_lon
52
+ bnd
53
+ end
54
+
55
+ # Returns true if the pt is within these bounds.
56
+ def contains?(pt)
57
+ (pt.lat >= min_lat and pt.lat <= max_lat and pt.lon >= min_lon and pt.lon <= max_lon)
58
+ end
59
+
60
+ # Adds an item to itself, expanding its min/max lat/lon as needed to
61
+ # contain the given item. The item can be either another instance of
62
+ # Bounds or a Point.
63
+ def add(item)
64
+ if(item.respond_to?(:lat) and item.respond_to?(:lon))
65
+ @min_lat = item.lat if item.lat < @min_lat
66
+ @min_lon = item.lon if item.lon < @min_lon
67
+ @max_lat = item.lat if item.lat > @max_lat
68
+ @max_lon = item.lon if item.lon > @max_lon
69
+ else
70
+ @min_lat = item.min_lat if item.min_lat < @min_lat
71
+ @min_lon = item.min_lon if item.min_lon < @min_lon
72
+ @max_lat = item.max_lat if item.max_lat > @max_lat
73
+ @max_lon = item.max_lon if item.max_lon > @max_lon
74
+ end
75
+ end
76
+
77
+ # Returns the min_lat, min_lon, max_lat, and max_lon in a labeled string.
78
+ def to_s
79
+ "min_lat: #{min_lat} min_lon: #{min_lon} max_lat: #{max_lat} max_lon: #{max_lon}"
80
+ end
81
+
82
+ end
83
+ end
data/lib/gpx/gpx.rb ADDED
@@ -0,0 +1,53 @@
1
+ #--
2
+ # Copyright (c) 2006 Doug Fales
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+ module GPX
24
+ VERSION = "0.1"
25
+
26
+ # A common base class which provides a useful initializer method to many
27
+ # class in the GPX library.
28
+ class Base
29
+ include REXML
30
+
31
+ # This initializer can take a REXML::Element and scrape out any text
32
+ # elements with the names given in the "text_elements" array. Each
33
+ # element found underneath "parent" with a name in "text_elements" causes
34
+ # an attribute to be initialized on the instance. This means you don't
35
+ # have to pick out individual text elements in each initializer of each
36
+ # class (Route, TrackPoint, Track, etc). Just pass an array of possible
37
+ # attributes to this method.
38
+ def instantiate_with_text_elements(parent, text_elements)
39
+ text_elements.each do |el|
40
+ unless parent.elements[el].nil?
41
+ val = parent.elements[el].text
42
+ code = <<-code
43
+ attr_accessor #{ el }
44
+ #{el}=#{val}
45
+ code
46
+ class_eval code
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,222 @@
1
+ #--
2
+ # Copyright (c) 2006 Doug Fales
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+ module GPX
24
+ class GPXFile < Base
25
+ attr_reader :tracks, :routes, :waypoints, :bounds, :lowest_point, :highest_point, :distance, :duration, :average_speed
26
+
27
+
28
+ # This initializer can be used to create a new GPXFile from an existing
29
+ # file or to create a new GPXFile instance with no data (so that you can
30
+ # add tracks and points and write it out to a new file later).
31
+ # To read an existing GPX file, do this:
32
+ # gpx_file = GPXFile.new(:gpx_file => 'mygpxfile.gpx')
33
+ # puts "Speed: #{gpx_file.average_speed}"
34
+ # puts "Duration: #{gpx_file.duration}"
35
+ # puts "Bounds: #{gpx_file.bounds}"
36
+ #
37
+ # To create a new blank GPXFile instance:
38
+ # gpx_file = GPXFile.new
39
+ # Note that you can pass in any instance variables to this form of the initializer, including Tracks or Segments:
40
+ # some_track = get_track_from_csv('some_other_format.csv')
41
+ # gpx_file = GPXFile.new(:tracks => [some_track])
42
+ #
43
+ def initialize(opts = {})
44
+ @duration = 0
45
+ if(opts[:gpx_file])
46
+ gpx_file = opts[:gpx_file]
47
+ case gpx_file
48
+ when String
49
+ gpx_file = File.open(gpx_file)
50
+ end
51
+ reset_meta_data
52
+ @xml = Document.new(gpx_file, :ignore_whitespace_nodes => :all)
53
+
54
+ bounds_element = (XPath.match(@xml, "/gpx/metadata/bounds").first rescue nil)
55
+ if bounds_element
56
+ @bounds.min_lat = bounds_element.attributes["min_lat"].to_f
57
+ @bounds.min_lon = bounds_element.attributes["min_lon"].to_f
58
+ @bounds.max_lat = bounds_element.attributes["max_lat"].to_f
59
+ @bounds.max_lon = bounds_element.attributes["max_lon"].to_f
60
+ else
61
+ get_bounds = true
62
+ end
63
+
64
+ @tracks = XPath.match(@xml, "/gpx/trk").collect do |trk|
65
+ trk = Track.new(:element => trk, :gpx_file => self)
66
+ update_meta_data(trk, get_bounds)
67
+ trk
68
+ end
69
+ @waypoints = XPath.match(@xml, "/gpx/wpt").collect { |wpt| Waypoint.new(:element => wpt, :gpx_file => self) }
70
+ @routes = XPath.match(@xml, "/gpx/rte").collect { |rte| Route.new(:element => rte, :gpx_file => self) }
71
+
72
+ @tracks.delete_if { |t| t.empty? }
73
+
74
+ calculate_duration
75
+ else
76
+ reset_meta_data
77
+ opts.each { |attr_name, value| instance_variable_set("@#{attr_name.to_s}", value) }
78
+ unless(@tracks.nil? or @tracks.size.zero?)
79
+ @tracks.each { |trk| update_meta_data(trk) }
80
+ calculate_duration
81
+ end
82
+ end
83
+ end
84
+
85
+ # Returns the distance, in kilometers, meters, or miles, of all of the
86
+ # tracks and segments contained in this GPXFile.
87
+ def distance(opts = { :units => 'kilometers' })
88
+ case opts[:units]
89
+ when /kilometers/i
90
+ return @distance
91
+ when /meters/i
92
+ return (@distance * 1000)
93
+ when /miles/i
94
+ return (@distance * 0.62)
95
+ end
96
+ end
97
+
98
+ # Returns the average speed, in km/hr, meters/hr, or miles/hr, of this
99
+ # GPXFile. The calculation is based on the total distance divided by the
100
+ # total duration of the entire file.
101
+ def average_speed(opts = { :units => 'kilometers' })
102
+ case opts[:units]
103
+ when /kilometers/i
104
+ return @distance / (@duration/3600.0)
105
+ when /meters/i
106
+ return (@distance * 1000) / (@duration/3600.0)
107
+ when /miles/i
108
+ return (@distance * 0.62) / (@duration/3600.0)
109
+ end
110
+ end
111
+
112
+ # Crops any points falling within a rectangular area. Identical to the
113
+ # delete_area method in every respect except that the points outside of
114
+ # the given area are deleted. Note that this method automatically causes
115
+ # the meta data to be updated after deletion.
116
+ def crop(area)
117
+ reset_meta_data
118
+ keep_tracks = []
119
+ tracks.each do |trk|
120
+ trk.crop(area)
121
+ unless trk.empty?
122
+ update_meta_data(trk)
123
+ keep_tracks << trk
124
+ end
125
+ end
126
+ @tracks = keep_tracks
127
+ routes.each { |rte| rte.crop(area) }
128
+ waypoints.each { |wpt| wpt.crop(area) }
129
+ end
130
+
131
+ # Deletes any points falling within a rectangular area. The "area"
132
+ # parameter is usually an instance of the Bounds class. Note that this
133
+ # method cascades into similarly named methods of subordinate classes
134
+ # (i.e. Track, Segment), which means, if you want the deletion to apply
135
+ # to all the data, you only call this one (and not the one in Track or
136
+ # Segment classes). Note that this method automatically causes the meta
137
+ # data to be updated after deletion.
138
+ def delete_area(area)
139
+ reset_meta_data
140
+ keep_tracks = []
141
+ tracks.each do |trk|
142
+ trk.delete_area(area)
143
+ unless trk.empty?
144
+ update_meta_data(trk)
145
+ keep_tracks << trk
146
+ end
147
+ end
148
+ @tracks = keep_tracks
149
+ routes.each { |rte| rte.delete_area(area) }
150
+ waypoints.each { |wpt| wpt.delete_area(area) }
151
+ end
152
+
153
+ # Resets the meta data for this GPX file. Meta data includes the bounds,
154
+ # the high and low points, and the distance.
155
+ def reset_meta_data
156
+ @bounds = Bounds.new
157
+ @highest_point = nil
158
+ @lowest_point = nil
159
+ @distance = 0.0
160
+ end
161
+
162
+ # Updates the meta data for this GPX file. Meta data includes the
163
+ # bounds, the high and low points, and the distance. This is useful when
164
+ # you modify the GPX data (i.e. by adding or deleting points) and you
165
+ # want the meta data to accurately reflect the new data.
166
+ def update_meta_data(trk, get_bounds = true)
167
+ @lowest_point = trk.lowest_point if(@lowest_point.nil? or trk.lowest_point.elevation < @lowest_point.elevation)
168
+ @highest_point = trk.highest_point if(@highest_point.nil? or trk.highest_point.elevation > @highest_point.elevation)
169
+ @bounds.add(trk.bounds) if get_bounds
170
+ @distance += trk.distance
171
+ end
172
+
173
+ # Serialize the current GPXFile to a gpx file named <filename>.
174
+ # If the file does not exist, it is created. If it does exist, it is overwritten.
175
+ def write(filename)
176
+
177
+ doc = Document.new
178
+ gpx_elem = Element.new('gpx')
179
+ doc.add(gpx_elem)
180
+ gpx_elem.attributes['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
181
+ gpx_elem.attributes['xmlns'] = "http://www.topografix.com/GPX/1/1"
182
+ gpx_elem.attributes['version'] = "1.1"
183
+ gpx_elem.attributes['creator'] = "GPX RubyGem 0.1 Copyright 2006 Doug Fales -- http://walkingboss.com"
184
+ gpx_elem.attributes['xsi:schemaLocation'] = "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
185
+
186
+ meta_data_elem = Element.new('metadata')
187
+ name_elem = Element.new('name')
188
+ name_elem.text = File.basename(filename)
189
+ meta_data_elem.elements << name_elem
190
+
191
+ time_elem = Element.new('time')
192
+ time_elem.text = Time.now.xmlschema
193
+ meta_data_elem.elements << time_elem
194
+
195
+ meta_data_elem.elements << bounds.to_xml
196
+
197
+ gpx_elem.elements << meta_data_elem
198
+
199
+ tracks.each { |t| gpx_elem.add_element t.to_xml } unless tracks.nil?
200
+ waypoints.each { |w| gpx_elem.add_element w.to_xml } unless waypoints.nil?
201
+ routes.each { |r| gpx_elem.add_element r.to_xml } unless routes.nil?
202
+
203
+ File.open(filename, 'w') { |f| doc.write(f) }
204
+ end
205
+
206
+ private
207
+
208
+ # Calculates and sets the duration attribute by subtracting the time on
209
+ # the very first point from the time on the very last point.
210
+ def calculate_duration
211
+ @duration = 0
212
+ if(@tracks.nil? or @tracks.size.zero? or @tracks[0].segments.nil? or @tracks[0].segments.size.zero?)
213
+ return @duration
214
+ end
215
+ @duration = (@tracks[-1].segments[-1].points[-1].time - @tracks.first.segments.first.points.first.time)
216
+ rescue
217
+ @duration = 0
218
+ end
219
+
220
+
221
+ end
222
+ end