gpx 0.9.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +162 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -5
- data/CHANGELOG.md +15 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +20 -5
- data/Rakefile +22 -12
- data/bin/gpx_distance +5 -6
- data/bin/gpx_smooth +25 -26
- data/gpx.gemspec +13 -12
- data/lib/gpx/bounds.rb +13 -31
- data/lib/gpx/geo_json.rb +199 -0
- data/lib/gpx/gpx.rb +4 -26
- data/lib/gpx/gpx_file.rb +140 -134
- data/lib/gpx/magellan_track_log.rb +34 -66
- data/lib/gpx/point.rb +22 -35
- data/lib/gpx/route.rb +10 -31
- data/lib/gpx/segment.rb +63 -90
- data/lib/gpx/track.rb +38 -42
- data/lib/gpx/track_point.rb +32 -0
- data/lib/gpx/version.rb +3 -1
- data/lib/gpx/waypoint.rb +10 -34
- data/lib/gpx.rb +13 -34
- data/tests/geojson_files/combined_data.json +68 -0
- data/tests/geojson_files/line_string_data.json +83 -0
- data/tests/geojson_files/multi_line_string_data.json +74 -0
- data/tests/geojson_files/multi_point_data.json +14 -0
- data/tests/geojson_files/point_data.json +22 -0
- data/tests/geojson_test.rb +92 -0
- data/tests/gpx10_test.rb +7 -6
- data/tests/gpx_file_test.rb +31 -19
- data/tests/gpx_files/one_segment_mixed_times.gpx +884 -0
- data/tests/gpx_files/routes_without_names.gpx +29 -0
- data/tests/gpx_files/with_empty_tracks.gpx +72 -0
- data/tests/magellan_test.rb +12 -11
- data/tests/output_test.rb +93 -94
- data/tests/route_test.rb +75 -30
- data/tests/segment_test.rb +104 -93
- data/tests/track_file_test.rb +50 -70
- data/tests/track_point_test.rb +22 -11
- data/tests/track_test.rb +73 -61
- data/tests/waypoint_test.rb +46 -48
- metadata +47 -18
- data/lib/gpx/trackpoint.rb +0 -60
data/lib/gpx/geo_json.rb
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module GPX
|
6
|
+
# Class to parse GeoJSON LineStrings, MultiLineStrings, Points,
|
7
|
+
# and MultiPoint geometric objects to GPX format. For the full
|
8
|
+
# specification of GeoJSON, see:
|
9
|
+
# http://geojson.org/geojson-spec.html
|
10
|
+
# Note that GeoJSON coordinates are specified in lon/lat format,
|
11
|
+
# instead of the more traditional lat/lon format.
|
12
|
+
#
|
13
|
+
class GeoJSON
|
14
|
+
class << self
|
15
|
+
FEATURE = 'Feature'
|
16
|
+
LINESTRING = 'LineString'
|
17
|
+
MULTILINESTRING = 'MultiLineString'
|
18
|
+
POINT = 'Point'
|
19
|
+
MULTIPOINT = 'MultiPoint'
|
20
|
+
|
21
|
+
# Conversion can be initiated by either specifying a file,
|
22
|
+
# file name, or by passing in GeoJSON data as a string.
|
23
|
+
# Examples:
|
24
|
+
# GPX::GeoJSON.convert_to_gpx(geojson_file: 'mygeojsonfile.json')
|
25
|
+
# or
|
26
|
+
# file = File.new('mygeojsonfile.json', 'r')
|
27
|
+
# GPX::GeoJSON.convert_to_gpx(geojson_file: file)
|
28
|
+
# or
|
29
|
+
# data = JSON.generate(my_geojson_hash)
|
30
|
+
# GPX::GeoJSON.convert_to_gpx(geojson_data: data)
|
31
|
+
#
|
32
|
+
# Returns a GPX::GPX_File object populated with the converted data.
|
33
|
+
#
|
34
|
+
def convert_to_gpx(opts = {})
|
35
|
+
geojson = geojson_data_from(opts)
|
36
|
+
gpx_file = GPX::GPXFile.new
|
37
|
+
add_tracks_to(gpx_file, geojson)
|
38
|
+
add_waypoints_to(gpx_file, geojson)
|
39
|
+
gpx_file
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def geojson_data_from(opts)
|
45
|
+
if opts[:geojson_file]
|
46
|
+
parse_geojson_data_from_file(opts[:geojson_file])
|
47
|
+
elsif opts[:geojson_data]
|
48
|
+
parse_geojson_data(opts[:geojson_data])
|
49
|
+
else
|
50
|
+
raise ArgumentError,
|
51
|
+
'Must pass value for \':geojson_file\' ' \
|
52
|
+
'or \':geojson_data\' to convert_to_gpx'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_geojson_data_from_file(filename)
|
57
|
+
parse_geojson_data(IO.read(filename))
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_geojson_data(data)
|
61
|
+
JSON.parse(data)
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_tracks_to(gpx_file, geojson)
|
65
|
+
tracks = [line_strings_to_track(geojson)] +
|
66
|
+
multi_line_strings_to_tracks(geojson)
|
67
|
+
tracks.compact!
|
68
|
+
gpx_file.tracks += tracks
|
69
|
+
gpx_file.tracks.each { |t| gpx_file.update_meta_data(t) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_waypoints_to(gpx_file, geojson)
|
73
|
+
gpx_file.waypoints +=
|
74
|
+
points_to_waypoints(geojson, gpx_file) +
|
75
|
+
multi_points_to_waypoints(geojson, gpx_file)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Converts GeoJSON 'LineString' features.
|
79
|
+
# Current strategy is to convert each LineString into a
|
80
|
+
# Track Segment, returning a Track for all LineStrings.
|
81
|
+
#
|
82
|
+
def line_strings_to_track(geojson)
|
83
|
+
line_strings = line_strings_in(geojson)
|
84
|
+
return nil unless line_strings.any?
|
85
|
+
|
86
|
+
track = GPX::Track.new
|
87
|
+
line_strings.each do |ls|
|
88
|
+
coords = ls['geometry']['coordinates']
|
89
|
+
track.append_segment(coords_to_segment(coords))
|
90
|
+
end
|
91
|
+
track
|
92
|
+
end
|
93
|
+
|
94
|
+
# Converts GeoJSON 'MultiLineString' features.
|
95
|
+
# Current strategy is to convert each MultiLineString
|
96
|
+
# into a Track, with each set of LineString coordinates
|
97
|
+
# within a MultiLineString a Track Segment.
|
98
|
+
#
|
99
|
+
def multi_line_strings_to_tracks(geojson)
|
100
|
+
tracks = []
|
101
|
+
multi_line_strings_in(geojson).each do |mls|
|
102
|
+
track = GPX::Track.new
|
103
|
+
mls['geometry']['coordinates'].each do |coords|
|
104
|
+
seg = coords_to_segment(coords)
|
105
|
+
seg.track = track
|
106
|
+
track.append_segment(seg)
|
107
|
+
end
|
108
|
+
tracks << track
|
109
|
+
end
|
110
|
+
tracks
|
111
|
+
end
|
112
|
+
|
113
|
+
# Converts GeoJSON 'Point' features.
|
114
|
+
# Current strategy is to convert each Point
|
115
|
+
# feature into a GPX waypoint.
|
116
|
+
#
|
117
|
+
def points_to_waypoints(geojson, gpx_file)
|
118
|
+
points_in(geojson).reduce([]) do |acc, pt|
|
119
|
+
coords = pt['geometry']['coordinates']
|
120
|
+
acc << point_to_waypoint(coords, gpx_file)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Converts GeoJSON 'MultiPoint' features.
|
125
|
+
# Current strategy is to convert each coordinate
|
126
|
+
# point in a MultiPoint to a GPX waypoint.
|
127
|
+
#
|
128
|
+
# NOTE: It is debatable that a MultiPoint feature
|
129
|
+
# might translate best into a GPX route, which is
|
130
|
+
# described as
|
131
|
+
# "an ordered list of waypoints representing a
|
132
|
+
# series of turn points leading to a destination."
|
133
|
+
# See http://www.topografix.com/gpx/1/1/#type_rteType
|
134
|
+
#
|
135
|
+
def multi_points_to_waypoints(geojson, gpx_file)
|
136
|
+
multi_points_in(geojson).reduce([]) do |acc, mpt|
|
137
|
+
mpt['geometry']['coordinates'].each do |coords|
|
138
|
+
acc << point_to_waypoint(coords, gpx_file)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Given an array of [lng, lat, ele] coordinates,
|
144
|
+
# return a GPX track segment.
|
145
|
+
#
|
146
|
+
def coords_to_segment(coords)
|
147
|
+
seg = GPX::Segment.new
|
148
|
+
coords.each do |pt|
|
149
|
+
seg.append_point(point_to_track_point(pt, seg))
|
150
|
+
end
|
151
|
+
seg
|
152
|
+
end
|
153
|
+
|
154
|
+
# Given a GeoJSON coordinate point, return
|
155
|
+
# a GPX::Waypoint
|
156
|
+
def point_to_waypoint(point, gpx_file)
|
157
|
+
GPX::Waypoint.new(gpx_file: gpx_file,
|
158
|
+
lon: point[0],
|
159
|
+
lat: point[1],
|
160
|
+
elevation: point[2])
|
161
|
+
end
|
162
|
+
|
163
|
+
# Given a GeoJSON coorindate point, and
|
164
|
+
# GPX segment, return a GPX::TrackPoint.
|
165
|
+
#
|
166
|
+
def point_to_track_point(point, seg)
|
167
|
+
GPX::TrackPoint.new(segment: seg,
|
168
|
+
lon: point[0],
|
169
|
+
lat: point[1],
|
170
|
+
elevation: point[2])
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns all features in the passed geojson
|
174
|
+
# that match the type.
|
175
|
+
#
|
176
|
+
def features_for(geojson, type)
|
177
|
+
geojson['features'].find_all do |f|
|
178
|
+
f['type'] == FEATURE && f['geometry']['type'] == type
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def points_in(geojson)
|
183
|
+
features_for(geojson, POINT)
|
184
|
+
end
|
185
|
+
|
186
|
+
def multi_points_in(geojson)
|
187
|
+
features_for(geojson, MULTIPOINT)
|
188
|
+
end
|
189
|
+
|
190
|
+
def line_strings_in(geojson)
|
191
|
+
features_for(geojson, LINESTRING)
|
192
|
+
end
|
193
|
+
|
194
|
+
def multi_line_strings_in(geojson)
|
195
|
+
features_for(geojson, MULTILINESTRING)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/lib/gpx/gpx.rb
CHANGED
@@ -1,25 +1,5 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
#++
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
23
3
|
module GPX
|
24
4
|
# A common base class which provides a useful initializer method to many
|
25
5
|
# class in the GPX library.
|
@@ -33,14 +13,12 @@ module GPX
|
|
33
13
|
# attributes to this method.
|
34
14
|
def instantiate_with_text_elements(parent, text_elements)
|
35
15
|
text_elements.each do |el|
|
36
|
-
child_xpath =
|
16
|
+
child_xpath = el.to_s
|
37
17
|
unless parent.at(child_xpath).nil?
|
38
18
|
val = parent.at(child_xpath).inner_text
|
39
|
-
|
19
|
+
send("#{el}=", val)
|
40
20
|
end
|
41
21
|
end
|
42
|
-
|
43
22
|
end
|
44
|
-
|
45
23
|
end
|
46
24
|
end
|
data/lib/gpx/gpx_file.rb
CHANGED
@@ -1,30 +1,11 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
#++
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
23
3
|
module GPX
|
24
4
|
class GPXFile < Base
|
25
|
-
attr_accessor :tracks,
|
5
|
+
attr_accessor :tracks,
|
6
|
+
:routes, :waypoints, :bounds, :lowest_point, :highest_point, :duration, :ns, :time, :name, :version, :creator, :description, :moving_duration
|
26
7
|
|
27
|
-
DEFAULT_CREATOR = "GPX RubyGem #{GPX::VERSION} -- http://dougfales.github.io/gpx/"
|
8
|
+
DEFAULT_CREATOR = "GPX RubyGem #{GPX::VERSION} -- http://dougfales.github.io/gpx/"
|
28
9
|
|
29
10
|
# This initializer can be used to create a new GPXFile from an existing
|
30
11
|
# file or to create a new GPXFile instance with no data (so that you can
|
@@ -44,10 +25,14 @@ module GPX
|
|
44
25
|
# gpx_file = GPXFile.new(:tracks => [some_track])
|
45
26
|
#
|
46
27
|
def initialize(opts = {})
|
28
|
+
super()
|
47
29
|
@duration = 0
|
48
30
|
@attributes = {}
|
49
31
|
@namespace_defs = []
|
50
|
-
|
32
|
+
@tracks = []
|
33
|
+
@time = nil
|
34
|
+
|
35
|
+
if opts[:gpx_file] || opts[:gpx_data]
|
51
36
|
if opts[:gpx_file]
|
52
37
|
gpx_file = opts[:gpx_file]
|
53
38
|
gpx_file = File.open(gpx_file) unless gpx_file.is_a?(File)
|
@@ -59,41 +44,54 @@ module GPX
|
|
59
44
|
gpx_element = @xml.at('gpx')
|
60
45
|
@attributes = gpx_element.attributes
|
61
46
|
@namespace_defs = gpx_element.namespace_definitions
|
62
|
-
#$stderr.puts gpx_element.attributes.sort.inspect
|
63
|
-
#$stderr.puts @xmlns.inspect
|
64
|
-
#$stderr.puts @xsi.inspect
|
65
47
|
@version = gpx_element['version']
|
66
48
|
reset_meta_data
|
67
|
-
bounds_element = (
|
49
|
+
bounds_element = (
|
50
|
+
begin
|
51
|
+
@xml.at('metadata/bounds')
|
52
|
+
rescue StandardError
|
53
|
+
nil
|
54
|
+
end)
|
68
55
|
if bounds_element
|
69
|
-
@bounds.min_lat = get_bounds_attr_value(bounds_element, %w
|
70
|
-
@bounds.min_lon = get_bounds_attr_value(bounds_element, %w
|
71
|
-
@bounds.max_lat = get_bounds_attr_value(bounds_element, %w
|
72
|
-
@bounds.max_lon = get_bounds_attr_value(bounds_element, %w
|
56
|
+
@bounds.min_lat = get_bounds_attr_value(bounds_element, %w[min_lat minlat minLat])
|
57
|
+
@bounds.min_lon = get_bounds_attr_value(bounds_element, %w[min_lon minlon minLon])
|
58
|
+
@bounds.max_lat = get_bounds_attr_value(bounds_element, %w[max_lat maxlat maxLat])
|
59
|
+
@bounds.max_lon = get_bounds_attr_value(bounds_element, %w[max_lon maxlon maxLon])
|
73
60
|
else
|
74
61
|
get_bounds = true
|
75
62
|
end
|
76
63
|
|
77
|
-
@time =
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
64
|
+
@time = begin
|
65
|
+
Time.parse(@xml.at('metadata/time').inner_text)
|
66
|
+
rescue StandardError
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
@name = begin
|
70
|
+
@xml.at('metadata/name').inner_text
|
71
|
+
rescue StandardError
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
@description = begin
|
75
|
+
@xml.at('metadata/desc').inner_text
|
76
|
+
rescue StandardError
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
@xml.search('trk').each do |trk|
|
80
|
+
trk = Track.new(element: trk, gpx_file: self)
|
83
81
|
update_meta_data(trk, get_bounds)
|
84
82
|
@tracks << trk
|
85
83
|
end
|
86
84
|
@waypoints = []
|
87
|
-
@xml.search(
|
85
|
+
@xml.search('wpt').each { |wpt| @waypoints << Waypoint.new(element: wpt, gpx_file: self) }
|
88
86
|
@routes = []
|
89
|
-
@xml.search(
|
90
|
-
@tracks.delete_if
|
87
|
+
@xml.search('rte').each { |rte| @routes << Route.new(element: rte, gpx_file: self) }
|
88
|
+
@tracks.delete_if(&:empty?)
|
91
89
|
|
92
90
|
calculate_duration
|
93
91
|
else
|
94
92
|
reset_meta_data
|
95
|
-
opts.each { |attr_name, value| instance_variable_set("@#{attr_name
|
96
|
-
unless
|
93
|
+
opts.each { |attr_name, value| instance_variable_set("@#{attr_name}", value) }
|
94
|
+
unless @tracks.nil? || @tracks.empty?
|
97
95
|
@tracks.each { |trk| update_meta_data(trk) }
|
98
96
|
calculate_duration
|
99
97
|
end
|
@@ -109,19 +107,24 @@ module GPX
|
|
109
107
|
result = el[name]
|
110
108
|
break unless result.nil?
|
111
109
|
end
|
112
|
-
|
110
|
+
(
|
111
|
+
begin
|
112
|
+
result.to_f
|
113
|
+
rescue StandardError
|
114
|
+
nil
|
115
|
+
end)
|
113
116
|
end
|
114
117
|
|
115
118
|
# Returns the distance, in kilometers, meters, or miles, of all of the
|
116
119
|
# tracks and segments contained in this GPXFile.
|
117
|
-
def distance(opts = { :
|
120
|
+
def distance(opts = { units: 'kilometers' })
|
118
121
|
case opts[:units]
|
119
122
|
when /kilometers/i
|
120
|
-
|
123
|
+
@distance
|
121
124
|
when /meters/i
|
122
|
-
|
125
|
+
(@distance * 1000)
|
123
126
|
when /miles/i
|
124
|
-
|
127
|
+
(@distance * 0.62)
|
125
128
|
end
|
126
129
|
end
|
127
130
|
|
@@ -129,14 +132,14 @@ module GPX
|
|
129
132
|
# GPXFile. The calculation is based on the total distance divided by the
|
130
133
|
# sum of duration of all segments of all tracks
|
131
134
|
# (not taking into accounting pause time).
|
132
|
-
def average_speed(opts = { :
|
135
|
+
def average_speed(opts = { units: 'kilometers' })
|
133
136
|
case opts[:units]
|
134
137
|
when /kilometers/i
|
135
|
-
|
138
|
+
distance / (moving_duration / 3600.0)
|
136
139
|
when /meters/i
|
137
|
-
|
140
|
+
(distance * 1000) / (moving_duration / 3600.0)
|
138
141
|
when /miles/i
|
139
|
-
|
142
|
+
(distance * 0.62) / (moving_duration / 3600.0)
|
140
143
|
end
|
141
144
|
end
|
142
145
|
|
@@ -176,7 +179,7 @@ module GPX
|
|
176
179
|
keep_tracks << trk
|
177
180
|
end
|
178
181
|
end
|
179
|
-
@tracks =
|
182
|
+
@tracks = keep_tracks
|
180
183
|
routes.each { |rte| rte.delete_area(area) }
|
181
184
|
waypoints.each { |wpt| wpt.delete_area(area) }
|
182
185
|
end
|
@@ -191,13 +194,15 @@ module GPX
|
|
191
194
|
@moving_duration = 0.0
|
192
195
|
end
|
193
196
|
|
197
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
198
|
+
|
194
199
|
# Updates the meta data for this GPX file. Meta data includes the
|
195
200
|
# bounds, the high and low points, and the distance. This is useful when
|
196
201
|
# you modify the GPX data (i.e. by adding or deleting points) and you
|
197
202
|
# want the meta data to accurately reflect the new data.
|
198
203
|
def update_meta_data(trk, get_bounds = true)
|
199
|
-
@lowest_point
|
200
|
-
@highest_point
|
204
|
+
@lowest_point = trk.lowest_point if @lowest_point.nil? || (!trk.lowest_point.nil? && (trk.lowest_point.elevation < @lowest_point.elevation))
|
205
|
+
@highest_point = trk.highest_point if @highest_point.nil? || (!trk.highest_point.nil? && (trk.highest_point.elevation > @highest_point.elevation))
|
201
206
|
@bounds.add(trk.bounds) if get_bounds
|
202
207
|
@distance += trk.distance
|
203
208
|
@moving_duration += trk.moving_duration
|
@@ -206,17 +211,18 @@ module GPX
|
|
206
211
|
# Serialize the current GPXFile to a gpx file named <filename>.
|
207
212
|
# If the file does not exist, it is created. If it does exist, it is overwritten.
|
208
213
|
def write(filename, update_time = true)
|
209
|
-
@time = Time.now if
|
214
|
+
@time = Time.now if @time.nil? || update_time
|
210
215
|
@name ||= File.basename(filename)
|
211
216
|
doc = generate_xml_doc
|
212
217
|
File.open(filename, 'w+') { |f| f.write(doc.to_xml) }
|
213
218
|
end
|
214
219
|
|
215
220
|
def to_s(update_time = true)
|
216
|
-
@time = Time.now if
|
221
|
+
@time = Time.now if @time.nil? || update_time
|
217
222
|
doc = generate_xml_doc
|
218
223
|
doc.to_xml
|
219
224
|
end
|
225
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
220
226
|
|
221
227
|
def inspect
|
222
228
|
"<#{self.class.name}:...>"
|
@@ -231,117 +237,117 @@ module GPX
|
|
231
237
|
end
|
232
238
|
|
233
239
|
private
|
240
|
+
|
234
241
|
def attributes_and_nsdefs_as_gpx_attributes
|
235
|
-
|
242
|
+
# $stderr.puts @namespace_defs.inspect
|
236
243
|
gpx_header = {}
|
237
|
-
@attributes.each do |k,v|
|
238
|
-
k = v.namespace.prefix
|
244
|
+
@attributes.each do |k, v|
|
245
|
+
k = "#{v.namespace.prefix}:#{k}" if v.namespace
|
239
246
|
gpx_header[k] = v.value
|
240
|
-
end
|
247
|
+
end
|
241
248
|
|
242
249
|
@namespace_defs.each do |nsd|
|
243
250
|
tag = 'xmlns'
|
244
|
-
if nsd.prefix
|
245
|
-
tag += ':' + nsd.prefix
|
246
|
-
end
|
251
|
+
tag += ":#{nsd.prefix}" if nsd.prefix
|
247
252
|
gpx_header[tag] = nsd.href
|
248
253
|
end
|
249
|
-
|
254
|
+
gpx_header
|
250
255
|
end
|
251
|
-
|
256
|
+
|
252
257
|
def generate_xml_doc
|
253
258
|
@version ||= '1.1'
|
254
|
-
version_dir = version.
|
259
|
+
version_dir = version.tr('.', '/')
|
255
260
|
|
256
261
|
gpx_header = attributes_and_nsdefs_as_gpx_attributes
|
257
|
-
|
258
|
-
gpx_header['version'] = @version.to_s
|
259
|
-
gpx_header['creator'] = DEFAULT_CREATOR
|
260
|
-
gpx_header['xsi:schemaLocation'] = "http://www.topografix.com/GPX/#{version_dir} http://www.topografix.com/GPX/#{version_dir}/gpx.xsd"
|
261
|
-
gpx_header['xmlns:xsi'] =
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
262
|
+
|
263
|
+
gpx_header['version'] = @version.to_s unless gpx_header['version']
|
264
|
+
gpx_header['creator'] = DEFAULT_CREATOR unless gpx_header['creator']
|
265
|
+
gpx_header['xsi:schemaLocation'] = "http://www.topografix.com/GPX/#{version_dir} http://www.topografix.com/GPX/#{version_dir}/gpx.xsd" unless gpx_header['xsi:schemaLocation']
|
266
|
+
gpx_header['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance' if !gpx_header['xsi'] && !gpx_header['xmlns:xsi']
|
267
|
+
|
268
|
+
# $stderr.puts gpx_header.keys.inspect
|
269
|
+
|
270
|
+
# rubocop:disable Metrics/BlockLength
|
271
|
+
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
272
|
+
xml.gpx(gpx_header) do
|
273
|
+
# version 1.0 of the schema doesn't support the metadata element, so push them straight to the root 'gpx' element
|
274
|
+
if @version == '1.0'
|
275
|
+
xml.name @name
|
276
|
+
xml.time @time.xmlschema
|
277
|
+
xml.bound(
|
278
|
+
minlat: bounds.min_lat,
|
279
|
+
minlon: bounds.min_lon,
|
280
|
+
maxlat: bounds.max_lat,
|
281
|
+
maxlon: bounds.max_lon
|
282
|
+
)
|
283
|
+
else
|
284
|
+
xml.metadata do
|
270
285
|
xml.name @name
|
271
286
|
xml.time @time.xmlschema
|
272
287
|
xml.bound(
|
273
288
|
minlat: bounds.min_lat,
|
274
289
|
minlon: bounds.min_lon,
|
275
290
|
maxlat: bounds.max_lat,
|
276
|
-
maxlon: bounds.max_lon
|
291
|
+
maxlon: bounds.max_lon
|
277
292
|
)
|
278
|
-
else
|
279
|
-
xml.metadata {
|
280
|
-
xml.name @name
|
281
|
-
xml.time @time.xmlschema
|
282
|
-
xml.bound(
|
283
|
-
minlat: bounds.min_lat,
|
284
|
-
minlon: bounds.min_lon,
|
285
|
-
maxlat: bounds.max_lat,
|
286
|
-
maxlon: bounds.max_lon,
|
287
|
-
)
|
288
|
-
}
|
289
293
|
end
|
294
|
+
end
|
295
|
+
|
296
|
+
tracks&.each do |t|
|
297
|
+
xml.trk do
|
298
|
+
xml.name t.name
|
290
299
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
xml.trkpt(lat: p.lat, lon: p.lon) {
|
299
|
-
xml.time p.time.xmlschema unless p.time.nil?
|
300
|
-
xml.ele p.elevation unless p.elevation.nil?
|
301
|
-
xml << p.extensions.to_xml unless p.extensions.nil?
|
302
|
-
}
|
300
|
+
t.segments.each do |seg|
|
301
|
+
xml.trkseg do
|
302
|
+
seg.points.each do |p|
|
303
|
+
xml.trkpt(lat: p.lat, lon: p.lon) do
|
304
|
+
xml.time p.time.xmlschema unless p.time.nil?
|
305
|
+
xml.ele p.elevation unless p.elevation.nil?
|
306
|
+
xml << p.extensions.to_xml unless p.extensions.nil?
|
303
307
|
end
|
304
|
-
|
308
|
+
end
|
305
309
|
end
|
306
|
-
|
307
|
-
end
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
end
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
waypoints&.each do |w|
|
315
|
+
xml.wpt(lat: w.lat, lon: w.lon) do
|
316
|
+
xml.time w.time.xmlschema unless w.time.nil?
|
317
|
+
Waypoint::SUB_ELEMENTS.each do |sub_elem|
|
318
|
+
xml.send(sub_elem, w.send(sub_elem)) if w.respond_to?(sub_elem) && !w.send(sub_elem).nil?
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
routes&.each do |r|
|
324
|
+
xml.rte do
|
325
|
+
xml.name r.name
|
326
|
+
|
327
|
+
r.points.each do |p|
|
328
|
+
xml.rtept(lat: p.lat, lon: p.lon) do
|
329
|
+
xml.time p.time.xmlschema unless p.time.nil?
|
330
|
+
xml.ele p.elevation unless p.elevation.nil?
|
327
331
|
end
|
328
|
-
|
329
|
-
end
|
330
|
-
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
331
336
|
end
|
332
|
-
|
333
|
-
return doc
|
337
|
+
# rubocop:enable Metrics/BlockLength
|
334
338
|
end
|
335
339
|
|
336
340
|
# Calculates and sets the duration attribute by subtracting the time on
|
337
341
|
# the very first point from the time on the very last point.
|
338
342
|
def calculate_duration
|
339
343
|
@duration = 0
|
340
|
-
if
|
344
|
+
if @tracks.nil? || @tracks.empty? || @tracks[0].segments.nil? || @tracks[0].segments.empty?
|
341
345
|
return @duration
|
346
|
+
|
342
347
|
end
|
348
|
+
|
343
349
|
@duration = (@tracks[-1].segments[-1].points[-1].time - @tracks.first.segments.first.points.first.time)
|
344
|
-
rescue
|
350
|
+
rescue StandardError
|
345
351
|
@duration = 0
|
346
352
|
end
|
347
353
|
end
|