gpx 0.1

Sign up to get free protection for your applications and to get access to all the features.
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