gpx 0.8.3 → 1.1.1
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 +37 -0
- data/.gitignore +4 -0
- data/.rubocop +1 -0
- data/.rubocop.yml +162 -0
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/.travis.yml +4 -6
- data/CHANGELOG.md +32 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +38 -17
- data/Rakefile +22 -12
- data/UPGRADING.md +7 -0
- data/bin/gpx_distance +5 -6
- data/bin/gpx_smooth +25 -26
- data/gpx.gemspec +14 -11
- 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 +45 -13
- data/lib/gpx/trackpoint.rb +0 -60
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
|
@@ -1,30 +1,10 @@
|
|
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
|
# This class will parse the lat/lon and time data from a Magellan track log,
|
25
5
|
# which is a NMEA formatted CSV list of points.
|
26
6
|
class MagellanTrackLog
|
27
|
-
#PMGNTRK
|
7
|
+
# PMGNTRK
|
28
8
|
# This message is to be used to transmit Track information (basically a list of previous position fixes)
|
29
9
|
# which is often displayed on the plotter or map screen of the unit. The first field in this message
|
30
10
|
# is the Latitude, followed by N or S. The next field is the Longitude followed by E or W. The next
|
@@ -35,98 +15,86 @@ module GPX
|
|
35
15
|
# of the fix. Note that this field is (and its preceding comma) is only produced by the unit when the
|
36
16
|
# command PMGNCMD,TRACK,2 is given. It is not present when a simple command of PMGNCMD,TRACK is issued.
|
37
17
|
|
38
|
-
#NOTE: The Latitude and Longitude Fields are shown as having two decimal
|
18
|
+
# NOTE: The Latitude and Longitude Fields are shown as having two decimal
|
39
19
|
# places. As many additional decimal places may be added as long as the total
|
40
20
|
# length of the message does not exceed 82 bytes.
|
41
21
|
|
42
22
|
# $PMGNTRK,llll.ll,a,yyyyy.yy,a,xxxxx,a,hhmmss.ss,A,c----c,ddmmyy*hh<CR><LF>
|
43
23
|
require 'csv'
|
44
24
|
|
45
|
-
LAT
|
46
|
-
LAT_HEMI
|
47
|
-
LON
|
48
|
-
LON_HEMI
|
49
|
-
ELE
|
25
|
+
LAT = 1
|
26
|
+
LAT_HEMI = 2
|
27
|
+
LON = 3
|
28
|
+
LON_HEMI = 4
|
29
|
+
ELE = 5
|
50
30
|
ELE_UNITS = 6
|
51
|
-
TIME
|
31
|
+
TIME = 7
|
52
32
|
INVALID_FLAG = 8
|
53
|
-
DATE
|
33
|
+
DATE = 10
|
54
34
|
|
55
35
|
FEET_TO_METERS = 0.3048
|
56
36
|
|
57
37
|
class << self
|
58
|
-
|
59
38
|
# Takes the name of a magellan file, converts the contents to GPX, and
|
60
39
|
# writes the result to gpx_filename.
|
61
40
|
def convert_to_gpx(magellan_filename, gpx_filename)
|
62
|
-
|
63
41
|
segment = Segment.new
|
64
42
|
|
65
|
-
CSV.open(magellan_filename,
|
66
|
-
next if(row.size < 10
|
43
|
+
CSV.open(magellan_filename, 'r').each do |row|
|
44
|
+
next if (row.size < 10) || (row[INVALID_FLAG] == 'V')
|
67
45
|
|
68
|
-
lat_deg
|
69
|
-
lat_min
|
46
|
+
lat_deg = row[LAT][0..1]
|
47
|
+
lat_min = row[LAT][2...-1]
|
70
48
|
lat_hemi = row[LAT_HEMI]
|
71
49
|
|
72
50
|
lat = lat_deg.to_f + (lat_min.to_f / 60.0)
|
73
|
-
lat =
|
51
|
+
lat = -lat if lat_hemi == 'S'
|
74
52
|
|
75
|
-
lon_deg
|
76
|
-
lon_min
|
53
|
+
lon_deg = row[LON][0..2]
|
54
|
+
lon_min = row[LON][3..]
|
77
55
|
lon_hemi = row[LON_HEMI]
|
78
56
|
|
79
57
|
lon = lon_deg.to_f + (lon_min.to_f / 60.0)
|
80
|
-
lon =
|
81
|
-
|
58
|
+
lon = -lon if lon_hemi == 'W'
|
82
59
|
|
83
60
|
ele = row[ELE]
|
84
61
|
ele_units = row[ELE_UNITS]
|
85
62
|
ele = ele.to_f
|
86
|
-
if
|
87
|
-
ele *= FEET_TO_METERS
|
88
|
-
end
|
63
|
+
ele *= FEET_TO_METERS if ele_units == 'F'
|
89
64
|
|
90
|
-
hrs
|
65
|
+
hrs = row[TIME][0..1].to_i
|
91
66
|
mins = row[TIME][2..3].to_i
|
92
67
|
secs = row[TIME][4..5].to_i
|
93
|
-
day
|
94
|
-
mon
|
95
|
-
yr
|
68
|
+
day = row[DATE][0..1].to_i
|
69
|
+
mon = row[DATE][2..3].to_i
|
70
|
+
yr = 2000 + row[DATE][4..5].to_i
|
96
71
|
|
97
72
|
time = Time.gm(yr, mon, day, hrs, mins, secs)
|
98
73
|
|
99
|
-
#must create point
|
100
|
-
pt = TrackPoint.new(:
|
74
|
+
# must create point
|
75
|
+
pt = TrackPoint.new(lat: lat, lon: lon, time: time, elevation: ele)
|
101
76
|
segment.append_point(pt)
|
102
|
-
|
103
77
|
end
|
104
78
|
|
105
79
|
trk = Track.new
|
106
80
|
trk.append_segment(segment)
|
107
|
-
gpx_file = GPXFile.new(:
|
81
|
+
gpx_file = GPXFile.new(tracks: [trk])
|
108
82
|
gpx_file.write(gpx_filename)
|
109
|
-
|
110
83
|
end
|
111
84
|
|
112
85
|
# Tests to see if the given file is a magellan NMEA track log.
|
113
|
-
def
|
86
|
+
def magellan_file?(filename)
|
114
87
|
i = 0
|
115
|
-
File.open(filename,
|
88
|
+
File.open(filename, 'r') do |f|
|
116
89
|
f.each do |line|
|
117
|
-
i +=
|
118
|
-
if line =~ /^\$PMGNTRK/
|
119
|
-
|
120
|
-
|
121
|
-
return false
|
122
|
-
elsif(i > 10)
|
123
|
-
return false
|
124
|
-
end
|
90
|
+
i += 1
|
91
|
+
return true if line =~ /^\$PMGNTRK/
|
92
|
+
return false if line =~ /<\?xml/
|
93
|
+
return false if i > 10
|
125
94
|
end
|
126
95
|
end
|
127
|
-
|
96
|
+
false
|
128
97
|
end
|
129
98
|
end
|
130
|
-
|
131
99
|
end
|
132
100
|
end
|
data/lib/gpx/point.rb
CHANGED
@@ -1,46 +1,35 @@
|
|
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
|
# The base class for all points. Trackpoint and Waypoint both descend from this base class.
|
25
5
|
class Point < Base
|
26
|
-
D_TO_R = Math::PI/180.0
|
27
|
-
attr_accessor :
|
6
|
+
D_TO_R = Math::PI / 180.0
|
7
|
+
attr_accessor :time, :elevation, :gpx_file, :speed, :extensions
|
8
|
+
attr_reader :lat, :lon
|
28
9
|
|
29
10
|
# When you need to manipulate individual points, you can create a Point
|
30
11
|
# object with a latitude, a longitude, an elevation, and a time. In
|
31
12
|
# addition, you can pass an XML element to this initializer, and the
|
32
13
|
# relevant info will be parsed out.
|
33
|
-
def initialize(opts = {:
|
14
|
+
def initialize(opts = { lat: 0.0, lon: 0.0, elevation: 0.0, time: Time.now })
|
15
|
+
super()
|
34
16
|
@gpx_file = opts[:gpx_file]
|
35
|
-
if
|
17
|
+
if opts[:element]
|
36
18
|
elem = opts[:element]
|
37
|
-
@lat
|
38
|
-
@
|
39
|
-
|
40
|
-
@
|
41
|
-
|
42
|
-
@
|
43
|
-
|
19
|
+
@lat = elem['lat'].to_f
|
20
|
+
@lon = elem['lon'].to_f
|
21
|
+
@latr = (D_TO_R * @lat)
|
22
|
+
@lonr = (D_TO_R * @lon)
|
23
|
+
# '-'? yyyy '-' mm '-' dd 'T' hh ':' mm ':' ss ('.' s+)? (zzzzzz)?
|
24
|
+
@time = (
|
25
|
+
begin
|
26
|
+
Time.xmlschema(elem.at('time').inner_text)
|
27
|
+
rescue StandardError
|
28
|
+
nil
|
29
|
+
end)
|
30
|
+
@elevation = elem.at('ele').inner_text.to_f unless elem.at('ele').nil?
|
31
|
+
@speed = elem.at('speed').inner_text.to_f unless elem.at('speed').nil?
|
32
|
+
@extensions = elem.at('extensions') unless elem.at('extensions').nil?
|
44
33
|
else
|
45
34
|
@lat = opts[:lat]
|
46
35
|
@lon = opts[:lon]
|
@@ -49,10 +38,8 @@ module GPX
|
|
49
38
|
@speed = opts[:speed]
|
50
39
|
@extensions = opts[:extensions]
|
51
40
|
end
|
52
|
-
|
53
41
|
end
|
54
42
|
|
55
|
-
|
56
43
|
# Returns the latitude and longitude (in that order), separated by the
|
57
44
|
# given delimeter. This is useful for passing a point into another API
|
58
45
|
# (i.e. the Google Maps javascript API).
|