gpx 0.9.0 → 1.1.0
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.
- 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
|