gpx 0.8.3 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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).
|